// Props
import { ReviewLoop } from "smthrs";
type ReviewLoopProps = {
id?: string; // default "review-loop"; task ids derived as {id}-produce, {id}-review
producer: AgentLike;
reviewer: AgentLike | AgentLike[];
produceOutput: OutputTarget;
reviewOutput: OutputTarget; // must include `approved: boolean`
maxIterations?: number; // default 5
onMaxReached?: "return-last" | "fail"; // default "return-last"
skipIf?: boolean;
children: string | ReactNode; // initial producer prompt
};
bunx smthrs graph):
/** @jsxImportSource smthrs */
import { createSmithers, ReviewLoop, ClaudeCodeAgent } from "smthrs";
import { z } from "zod";
const { Workflow, smithers, outputs } = createSmithers({
code: z.object({ diff: z.string() }),
review: z.object({ approved: z.boolean(), feedback: z.string() }),
});
const agent = new ClaudeCodeAgent({ model: "claude-sonnet-5" });
export default smithers(() => (
<Workflow name="code-review">
<ReviewLoop
producer={agent}
reviewer={agent}
produceOutput={outputs.code}
reviewOutput={outputs.review}
maxIterations={3}
>
Implement a REST API for user authentication with JWT tokens.
</ReviewLoop>
</Workflow>
));
Notes
- The runtime reads
approvedeach frame to decide whether to loop. - On later iterations the producer reruns without a
needsdependency wiring reviewer output back to it; feedback reaches it only through runtime/agent conversation history, which carries prior outputs forward.
Source
The<ReviewLoop> implementation and the files it imports, straight from the package source. This section is generated; edit the source, not this block.
// @smithers-type-exports-begin
/** @typedef {import("./ReviewLoopProps.ts").ReviewLoopProps} ReviewLoopProps */
// @smithers-type-exports-end
import React from "react";
import { Loop } from "./Ralph.js";
import { Sequence } from "./Sequence.js";
import { Task } from "./Task.js";
import { useOptionalSmithersContext } from "./useOptionalSmithersContext.js";
/**
* Produce -> review -> fix -> repeat until approved.
*
* Composes Loop, Sequence, and Task to create a standard
* review-loop pattern. The producer receives the reviewer's
* feedback on subsequent iterations.
* @param {ReviewLoopProps} props
*/
export function ReviewLoop(props) {
if (props.skipIf) return null;
const {
id,
producer,
reviewer,
produceOutput,
reviewOutput,
maxIterations = 5,
onMaxReached = "return-last",
children,
} = props;
const prefix = id ?? "review-loop";
const produceId = `${prefix}-produce`;
const reviewId = `${prefix}-review`;
const ctx = useOptionalSmithersContext();
const latestReview = ctx?.latest?.(reviewOutput, reviewId);
const approved = latestReview?.approved === true;
const reviewerAgents = Array.isArray(reviewer) ? reviewer : [reviewer];
if (reviewerAgents.length === 0) {
throw new Error("ReviewLoop reviewer must include at least one reviewer.");
}
// On iterations after the first, fold the previous (not-yet-approved) review
// feedback into the producer's prompt so it revises rather than restarts.
const priorReview = latestReview && latestReview.approved !== true ? latestReview : undefined;
const produceChildren = priorReview
? React.createElement(
React.Fragment,
null,
children,
"\n\nRevise your previous work using the reviewer's feedback:\n",
JSON.stringify(priorReview),
)
: children;
return React.createElement(
Loop,
{
id: prefix,
until: approved,
maxIterations,
onMaxReached,
},
React.createElement(
Sequence,
null,
React.createElement(Task, {
id: produceId,
output: produceOutput,
agent: producer,
children: produceChildren,
}),
React.createElement(Task, {
id: reviewId,
output: reviewOutput,
agent: reviewerAgents.length === 1 ? reviewerAgents[0] : reviewerAgents,
// `needs` gates the reviewer until the producer is terminal; `deps`
// resolves the produced output into the reviewer's prompt (needs alone
// is cache-context only and injects nothing).
needs: { produced: produceId },
deps: { produced: produceOutput },
children: (d) =>
`Review the produced work and decide whether to approve.\n\nProduced work:\n${JSON.stringify(d.produced ?? "(no output)")}`,
}),
),
);
}
// @smithers-type-exports-begin
/** @typedef {import("./RalphProps.ts").RalphProps} RalphProps */
// @smithers-type-exports-end
import React from "react";
/** @typedef {import("./LoopProps.ts").LoopProps} LoopProps */
/**
* @param {LoopProps} props
*/
export function Loop(props) {
if (props.skipIf) return null;
// Sanitize to the loop's host props (align with other structural components);
// key/skipIf are React/control props and children are passed separately.
const next = {
id: props.id,
until: props.until,
maxIterations: props.maxIterations,
onMaxReached: props.onMaxReached,
continueAsNewEvery: props.continueAsNewEvery,
};
return React.createElement("smithers:ralph", next, props.children);
}
/** @deprecated Use `Loop` instead. */
export const Ralph = Loop;
import React from "react";
/** @typedef {import("./SequenceProps.ts").SequenceProps} SequenceProps */
/**
* @param {SequenceProps} props
*/
export function Sequence(props) {
if (props.skipIf) return null;
// Sequence carries only a display label; pass a sanitized bag (align with
// the sanitizing structural components) so control props don't leak through.
// `label` names the phase group in run views (graph, the Claude /workflows
// mirror) and is preserved in the persisted frame XML.
const next = {
...(props.label === undefined ? {} : { label: props.label }),
...(props.failurePolicy === undefined ? {} : { failurePolicy: props.failurePolicy }),
};
return React.createElement("smithers:sequence", next, props.children);
}
// @smithers-type-exports-begin
/**
* @template D
* @typedef {import("./InferDeps.ts").InferDeps<D>} InferDeps
*/
/** @typedef {import("./OutputTarget.ts").OutputTarget} OutputTarget */
// @smithers-type-exports-end
import { applyCliToolAllowlist } from "./cliToolAllowlist.js";
import { createTaskComponent } from "./taskCore.js";
export { renderPromptToText } from "./taskCore.js";
/**
* The Node/CLI-agent-aware `Task`. Every render-path behavior (deps
* resolution, agent-chain assembly, MDX prompt rendering, static/compute/agent
* branching) lives in `taskCore.js`; this file only supplies the CLI-agent
* tool-allowlist enforcement step (`applyCliToolAllowlist`, which statically
* imports `ClaudeCodeAgent`/`PiAgent`/`GeminiAgent`/`AntigravityAgent` — see
* `cliToolAllowlist.js`). `Task.browser.js` builds the same component with a
* no-op allowlist step instead, so it never pulls those Node-only
* (`node:child_process`-backed) classes into a browser bundle.
*/
export const Task = createTaskComponent({ applyCliToolAllowlist });
import React from "react";
import { SmithersContext } from "@smthrs/react-reconciler/context";
/**
* Read the workflow context when React is rendering the component, but allow
* direct structural expansion tests to call composite wrappers as plain
* functions. In that direct-call path React throws the standard invalid hook
* error; treating it as "no context yet" preserves the static element shape.
*/
export function useOptionalSmithersContext() {
try {
return React.useContext(SmithersContext);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (/Invalid hook call|dispatcher\.useContext|useContext/i.test(message)) {
return null;
}
throw error;
}
}
import type React from "react";
import type { AgentLike } from "@smthrs/agents/AgentLike";
import type { OutputTarget } from "./OutputTarget.ts";
export type ReviewLoopProps = {
id?: string;
/** Agent that produces or fixes the work each iteration. */
producer: AgentLike;
/** Agent (or agents) that reviews the produced work. */
reviewer: AgentLike | AgentLike[];
/** Output schema for the produced work. */
produceOutput: OutputTarget;
/** Output schema for the review result. Must include an `approved: boolean` field. */
reviewOutput: OutputTarget;
/** Maximum number of review cycles before stopping. @default 5 */
maxIterations?: number;
/** Behavior when maxIterations is reached. @default "return-last" */
onMaxReached?: "return-last" | "fail";
/** Skip the entire review loop. */
skipIf?: boolean;
/** Initial prompt for the producer (string or ReactNode). */
children: string | React.ReactNode;
};