Skip to main content

Ralph Component

The <Ralph> component controls iterative loops in Smithers workflows. Named after Ralph Wiggum’s “I’m in danger” catchphrase, it provides safety rails for loops that could potentially run away.

Basic Usage

import { Ralph } from "smithers";

<Ralph maxIterations={10}>
  <Claude onFinished={() => { /* state change triggers re-render */ }}>
    Keep improving until tests pass.
  </Claude>
</Ralph>

Props

maxIterations
number
default:"10"
Maximum number of loop iterations before stopping.
<Ralph maxIterations={5}>
  {/* Will stop after 5 iterations */}
</Ralph>
onIteration
(iteration: number) => void
Called at the start of each iteration.
<Ralph
  maxIterations={10}
  onIteration={(i) => console.log(`Starting iteration ${i}`)}
>
onComplete
() => void
Called when the loop completes normally (no more pending work).
<Ralph onComplete={() => console.log("Loop finished")}>
onMaxIterations
() => void
Called when maxIterations is reached.
<Ralph
  maxIterations={10}
  onMaxIterations={() => console.log("I'm in danger!")}
>

How It Works

Ralph tracks iterations and pending tasks:
┌──────────────────────────────────────────────────┐
│  Ralph Loop                                       │
│                                                   │
│  Iteration 1:                                    │
│    → Render children                              │
│    → <Claude> mounts and executes                │
│    → onFinished updates signal                   │
│    → Signal change triggers re-render            │
│                                                   │
│  Iteration 2:                                    │
│    → Render children (new state)                 │
│    → Different <Claude> may mount                │
│    → Continue until no pending work or maxIter   │
│                                                   │
└──────────────────────────────────────────────────┘

State-Driven Iterations

The power of Ralph comes from combining it with Solid.js signals:
function TestFixWorkflow() {
  const [testsPass, setTestsPass] = createSignal(false);
  const [attempts, setAttempts] = createSignal(0);

  return (
    <Ralph
      maxIterations={5}
      onIteration={(i) => console.log(`Attempt ${i}`)}
      onMaxIterations={() => console.log("Giving up after 5 attempts")}
    >
      {!testsPass() && (
        <Claude
          allowedTools={["Read", "Edit", "Bash"]}
          onFinished={(result) => {
            setAttempts(a => a + 1);
            if (result.output.includes("All tests pass")) {
              setTestsPass(true);
            }
          }}
        >
          Fix the failing tests.
          Current attempt: {attempts()}
        </Claude>
      )}
    </Ralph>
  );
}

Phase Transitions

Use Ralph for multi-phase workflows:
function PhaseWorkflow() {
  const [phase, setPhase] = createSignal<"research" | "implement" | "test" | "done">("research");

  return (
    <Ralph maxIterations={20}>
      {phase() === "research" && (
        <Phase name="Research">
          <Claude
            allowedTools={["Read", "Glob", "Grep"]}
            onFinished={() => setPhase("implement")}
          >
            Research the problem.
          </Claude>
        </Phase>
      )}

      {phase() === "implement" && (
        <Phase name="Implementation">
          <Claude
            allowedTools={["Edit", "Write"]}
            onFinished={() => setPhase("test")}
          >
            Implement the solution.
          </Claude>
        </Phase>
      )}

      {phase() === "test" && (
        <Phase name="Testing">
          <Claude
            allowedTools={["Bash"]}
            onFinished={(result) => {
              if (result.output.includes("PASS")) {
                setPhase("done");
              } else {
                setPhase("implement");  // Loop back
              }
            }}
          >
            Run tests.
          </Claude>
        </Phase>
      )}

      {/* When phase is "done", nothing renders, loop ends */}
    </Ralph>
  );
}

Task Registration

For custom async operations, use the Ralph context:
import { useRalph } from "smithers";

function CustomAsyncStep() {
  const ralph = useRalph();

  onMount(() => {
    // Register that we have pending work
    const taskId = ralph.registerTask();

    // Do async work
    fetchData().then((data) => {
      processData(data);
      // Mark task complete so Ralph knows we're done
      ralph.completeTask(taskId);
    });
  });

  return <step-node />;
}

Combining with Orchestration

Ralph works inside Orchestration for additional controls:
<SmithersProvider db={db} executionId={executionId}>
  <Orchestration
    globalTimeout={3600000}
    stopConditions={[{ type: "total_tokens", value: 50000 }]}
  >
    <Ralph
      maxIterations={10}
      onMaxIterations={() => {
        db.vcs.addReport({
          type: "warning",
          severity: "warning",
          title: "Max iterations reached",
          content: "Workflow did not complete within iteration limit",
        });
      }}
    >
      <WorkflowContent />
    </Ralph>
  </Orchestration>
</SmithersProvider>

Nested Ralph Components

You can nest Ralph components for sub-loops:
<Ralph maxIterations={5}>
  {phase() === "refine" && (
    <Ralph
      maxIterations={3}
      onMaxIterations={() => setPhase("review")}
    >
      <Claude onFinished={handleRefinement}>
        Refine the implementation.
      </Claude>
    </Ralph>
  )}
</Ralph>

Debugging

Track what’s happening in the loop:
<Ralph
  maxIterations={10}
  onIteration={(i) => {
    console.log(`--- Iteration ${i} ---`);
    console.log(`Phase: ${phase()}`);
    console.log(`Attempts: ${attempts()}`);
  }}
  onComplete={() => console.log("Loop completed normally")}
  onMaxIterations={() => {
    console.error("Max iterations reached!");
    console.log(`Final phase: ${phase()}`);
  }}
>

Best Practices

Prevent infinite loops:
// Good - has limit
<Ralph maxIterations={10}>...</Ralph>

// Dangerous - could run forever
<Ralph>...</Ralph>
Know when you’ve hit the limit:
<Ralph
  maxIterations={10}
  onMaxIterations={() => {
    console.error("Loop did not complete");
    db.state.set("status", "incomplete");
  }}
>
Let reactivity drive the loop:
const [done, setDone] = createSignal(false);

<Ralph maxIterations={10}>
  {!done() && (
    <Claude onFinished={() => setDone(true)}>
      Work until done.
    </Claude>
  )}
</Ralph>
Structure so the loop can end before max iterations:
// The loop ends when phase() === "done" because nothing renders
{phase() !== "done" && (
  <Claude onFinished={() => {
    if (success) setPhase("done");
  }}>
    Try to complete.
  </Claude>
)}