Skip to main content

Durable Ralphing

Ralphing = continuous agent iteration with verification gates. Run → verify → feedback → run again until done. Durable Ralphing = Smithers’ implementation that makes Ralph loops resumable, inspectable, and controllable.
┌─────────────────────────────────────────────────────────────────────────────┐
│  RALPH LOOP (the technique)                                                 │
│  while (!done) { run_agent(); verify(); if (pass) done = true; }            │
│                                                                             │
│  DURABLE RALPHING (Smithers' implementation)                                │
│  + Persists every iteration to SQLite                                       │
│  + Resumes from crash/restart at exact iteration                            │
│  + Explicit verification gates and stop conditions                          │
│  + Audit trail of what happened and why                                     │
└─────────────────────────────────────────────────────────────────────────────┘

The Execution Model

Smithers uses a reactive render loop powered by React state:
┌─────────────────────────────────────────────────────────┐
│                                                         │
│  ┌──────────┐    ┌─────────┐    ┌──────────────────┐   │
│  │  Render  │───▶│ Execute │───▶│ Update Signals   │   │
│  │   JSX    │    │ Claude  │    │ (onFinished)     │   │
│  └──────────┘    └─────────┘    └──────────────────┘   │
│       ▲                                   │             │
│       │                                   │             │
│       └───────────────────────────────────┘             │
│                  (re-render on signal change)           │
│                                                         │
└─────────────────────────────────────────────────────────┘
  1. Render: Your JSX components render using React’s state management
  2. Execute: <Claude> nodes execute via the Claude Code CLI
  3. Update: onFinished callbacks update React state
  4. Re-render: State changes trigger reactive updates
  5. Loop: Process repeats until no pending agents remain

Why Smithers for Ralphing

Traditional Ralph loops work great for simple iteration. Smithers lets you build sophisticated Ralph loops while staying reliable:
Simple Ralph LoopSmithers Ralph Loop
Single agent iteratingMulti-phase workflows with parallel agents
Linear control flowConditional branches, phases, steps
Ad-hoc structureComposable React components
Plans in promptsPlans as reviewable, versionable code
Manual orchestrationDeclarative - Claude can generate it
Smithers doesn’t replace simple Ralph loops. It’s for when your Ralph loop needs structure.

Controlling the Ralph Loop

You can control the Ralph loop using the stopped prop. This is the idiomatic way to stop iteration:
import { SmithersProvider, useSmithers, Claude, If } from "smithers-orchestrator";
import { useQueryValue } from "smithers-orchestrator/reactive-sqlite";

function IterativeWorkflow() {
  const { db } = useSmithers();

  // State in SQLite, not useState (see State Management docs)
  const attempts = useQueryValue<number>(
    db.db,
    "SELECT CAST(value AS INTEGER) as v FROM state WHERE key = 'attempts'"
  ) ?? 0;

  const testsPassing = useQueryValue<boolean>(
    db.db,
    "SELECT CAST(value AS INTEGER) as v FROM state WHERE key = 'tests_passing'"
  ) ?? false;

  return (
    <SmithersProvider
      db={db}
      executionId={db.execution.current()?.id ?? ''}
      maxIterations={5}
      stopped={testsPassing}
      onIteration={(i) => console.log(`Iteration ${i}`)}
      onComplete={() => console.log("Workflow complete")}
    >
      <If condition={!testsPassing}>
        <Claude
          model="sonnet"
          onFinished={(result) => {
            db.db.run(
              "INSERT OR REPLACE INTO state (key, value) VALUES ('attempts', ?)",
              [attempts + 1]
            );
            if (result.output.includes("All tests pass")) {
              db.db.run(
                "INSERT OR REPLACE INTO state (key, value) VALUES ('tests_passing', 1)"
              );
            }
          }}
        >
          Fix the failing tests. Current attempt: {attempts}
        </Claude>
      </If>
    </SmithersProvider>
  );
}

Implicit Stopping

Ralph also stops implicitly when nothing renders for an iteration. If no components mount and register tasks, the loop completes after 500ms. However, using the stopped prop is more explicit and recommended.

Reactive Updates

The power of the Ralph Loop comes from React reactivity:
import { SmithersProvider, useSmithers, Claude } from "smithers-orchestrator";
import { useQueryValue } from "smithers-orchestrator/reactive-sqlite";

function PhaseWorkflow() {
  const { db } = useSmithers();

  // State in SQLite, not useState
  const phase = useQueryValue<string>(
    db.db,
    "SELECT value FROM state WHERE key = 'phase'"
  ) ?? "research";

  const setPhase = (newPhase: string) => {
    db.db.run(
      "INSERT OR REPLACE INTO state (key, value) VALUES ('phase', ?)",
      [newPhase]
    );
  };

  return (
    <SmithersProvider
      db={db}
      executionId={db.execution.current()?.id ?? ''}
      maxIterations={10}
      stopped={phase === "done"}
    >
      {/* This block re-renders when phase changes */}

      <If condition={phase === "research"}>
        <Claude onFinished={() => setPhase("implement")}>
          Research the problem space.
        </Claude>
      </If>

      <If condition={phase === "implement"}>
        <Claude onFinished={() => setPhase("test")}>
          Implement the solution.
        </Claude>
      </If>

      <If condition={phase === "test"}>
        <Claude onFinished={(r) => {
          if (r.output.includes("PASS")) {
            setPhase("done");
          } else {
            setPhase("implement");
          }
        }}>
          Run and verify tests.
        </Claude>
      </If>
    </SmithersProvider>
  );
}
When setPhase("implement") is called:
  1. The database state updates
  2. useQueryValue detects the change and triggers a re-render
  3. Only the affected branch re-renders
  4. The new <Claude> component mounts and executes

Fire-and-Forget Pattern

Components use useMount to execute when they mount:
import { useMount } from "smithers-orchestrator";

function Claude(props) {
  useMount(() => {
    // Fire-and-forget async execution
    (async () => {
      const result = await executeClaudeAgent(props);
      props.onFinished?.(result);
    })();
  });

  return <claude-node {...props} />;
}
This means:
  • Components execute when they mount
  • Results trigger callbacks that update state
  • State updates cause re-renders
  • New components mount and execute

React Workflow Rules

Smithers uses React as a workflow DSL. Understanding these rules prevents common mistakes.

What persists vs what doesn’t

┌─────────────────────────────────────────────────────────────────┐
│ PERSISTS (survives restarts)          DOES NOT PERSIST          │
│ ─────────────────────────             ─────────────────────     │
│ SQLite state (db.state)               useState / useRef         │
│ db.tasks entries                      Local variables           │
│ db.phases / db.steps                  Closures                  │
│ Execution records                     In-memory caches          │
└─────────────────────────────────────────────────────────────────┘

Dos and Don’ts

// Good - durable
const phase = useQueryValue<string>(db.db,
  "SELECT value FROM state WHERE key = 'phase'") ?? "start";
const setPhase = (p: string) => db.state.set('phase', p);
// Bad - lost on restart, causes hydration issues
const [phase, setPhase] = useState("start");
// Good - orchestrator waits for this to complete
const taskId = db.tasks.start('fetch-data');
await fetchData();
db.tasks.complete(taskId);
// Bad - orchestrator may advance before this completes
useMount(() => {
  fetchData(); // No tracking!
});
// Good - state change triggers re-render
<Claude onFinished={() => db.state.set('phase', 'next')}>
  Do work
</Claude>
// Bad - nothing triggers advancement
<Claude onFinished={() => console.log('done')}>
  Do work
</Claude>
// Workflow stalls because state didn't change

What triggers re-renders

  1. SQLite state changes via useQueryValue or useQuery
  2. Task completion detected by orchestration loop
  3. Phase/Step advancement managed by PhaseRegistryProvider
  4. Stop conditions met (token limit, pattern match, timeout)

Common anti-patterns

Anti-PatternProblemFix
useState for phaseLost on restartUse db.state + useQueryValue
Untracked promisesOrchestrator advances earlyUse db.tasks.start/complete
No onFinished callbackWorkflow stallsAlways update state in onFinished
Infinite maxIterationsRunaway loopsSet reasonable limits (10-100)

Orchestration Context

SmithersProvider exposes orchestration context. useRalph is deprecated; use db.tasks for task coordination:
import { useSmithers, useMount } from "smithers-orchestrator";
import { useRef } from "react";

function CustomStep() {
  const { db } = useSmithers();
  const taskIdRef = useRef<string | null>(null);

  useMount(() => {
    taskIdRef.current = db.tasks.start("custom-step");

    doSomeWork().finally(() => {
      if (taskIdRef.current) {
        db.tasks.complete(taskIdRef.current);
      }
    });
  });

  return <step-node />;
}
The orchestration context tracks:
  • Active tasks (db.tasks)
  • Completed tasks
  • Iteration count
  • Stop/timeout state

Best Practices for Durable Ralphing

Without a limit, a bug in your state logic could cause infinite loops:
// Good - explicit limit
<SmithersProvider maxIterations={50}>...</SmithersProvider>

// Risky - could run forever
<SmithersProvider maxIterations={Infinity}>...</SmithersProvider>
State in SQLite survives restarts. State in memory doesn’t:
// Good - durable, survives restarts
const phase = useQueryValue<string>(db.db,
  "SELECT value FROM state WHERE key = 'phase'") ?? "start";
const setPhase = (p: string) =>
  db.db.run("INSERT OR REPLACE INTO state (key, value) VALUES ('phase', ?)", [p]);

// Bad - lost on restart
const [phase, setPhase] = useState("start");
Know when you’ve hit the limit:
<SmithersProvider
  maxIterations={50}
  onMaxIterations={() => {
    console.error("Workflow did not complete in 50 iterations");
    db.state.set("status", "needs_attention");
  }}
>
Explicit verification makes loops trustworthy:
<Claude
  onFinished={(result) => {
    if (result.output.includes("All tests pass")) {
      db.state.set("verified", "true");
    }
    // Iteration continues until verified
  }}
>
  Run tests. If any fail, fix them.
</Claude>

SmithersProvider: The Root Ralph Loop

<SmithersProvider> is a special Ralph Wiggum Loop that serves as the root orchestration wrapper. Unlike <Ralph> which is used for specific workflows within your component tree, <SmithersProvider> wraps your entire orchestration and manages the global Ralph loop. Default Behavior: By default, <SmithersProvider> runs the Ralph loop for up to 100 iterations. This is sufficient for most workflows. Infinite Loops: For long-running orchestrations that need to continue indefinitely (like CI/CD monitors, autonomous agents, or server processes), you can configure it to run forever:
import { SmithersProvider } from "smithers-orchestrator";

<SmithersProvider
  db={db}
  executionId={executionId}
  maxIterations={Infinity}
  onIteration={(i) => console.log(`Iteration ${i}`)}
>
  <Claude>
    Monitor the repository and respond to changes
  </Claude>
</SmithersProvider>
Custom Iteration Limits: You can also set custom limits based on your workflow needs:
// Quick task - only needs a few iterations
<SmithersProvider maxIterations={5}>
  <Claude>Fix the linting errors</Claude>
</SmithersProvider>

// Complex multi-phase workflow
<SmithersProvider maxIterations={50}>
  <MultiPhaseOrchestration />
</SmithersProvider>

// Long-running agent
<SmithersProvider maxIterations={Infinity}>
  <AutonomousAgent />
</SmithersProvider>

Next Steps