// Props
import { Optimizer } from "smthrs";
type OptimizerProps = {
id?: string; // default "optimizer"; task ids {id}-generate, {id}-evaluate
generator: AgentLike;
evaluator: AgentLike | ((candidate: unknown) => unknown | Promise<unknown>); // function = compute task
generateOutput: OutputTarget;
evaluateOutput: OutputTarget; // must include `score: number`
targetScore?: number; // omit to run all iterations
maxIterations?: number; // default 10
onMaxReached?: "return-last" | "fail"; // default "return-last"
skipIf?: boolean;
children: string | ReactNode; // initial generation prompt
};
bunx smthrs graph):
/** @jsxImportSource smthrs */
import { createSmithers, Optimizer, ClaudeCodeAgent } from "smthrs";
import { z } from "zod";
const { Workflow, smithers, outputs } = createSmithers({
prompt: z.object({ text: z.string() }),
evaluation: z.object({ score: z.number(), feedback: z.string() }),
});
const agent = new ClaudeCodeAgent({ model: "claude-sonnet-5" });
export default smithers(() => (
<Workflow name="prompt-optimizer">
<Optimizer
generator={agent}
evaluator={agent}
generateOutput={outputs.prompt}
evaluateOutput={outputs.evaluation}
targetScore={90}
maxIterations={5}
>
Generate a prompt for summarizing legal documents.
</Optimizer>
</Workflow>
));
Notes
scoredrives convergence againsttargetScore.- A function
evaluatorrenders as a compute task rather than an agent task.
Source
The<Optimizer> 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("./OptimizerProps.ts").OptimizerProps} OptimizerProps */
// @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";
/**
* Generate -> evaluate -> improve loop with score convergence.
*
* Composes Loop, Sequence, and Task to create an iterative
* optimization pattern. Each iteration receives the previous
* score and feedback to guide improvement.
* @param {OptimizerProps} props
*/
export function Optimizer(props) {
if (props.skipIf) return null;
const {
id,
generator,
evaluator,
generateOutput,
evaluateOutput,
targetScore,
maxIterations = 10,
onMaxReached = "return-last",
children,
} = props;
const prefix = id ?? "optimizer";
const generateId = `${prefix}-generate`;
const evaluateId = `${prefix}-evaluate`;
const ctx = useOptionalSmithersContext();
// Read the most recent evaluation for both the convergence check and to feed
// the previous score/feedback back into the next generation prompt.
const latestEvaluation = ctx?.latest?.(evaluateOutput, evaluateId);
const score = typeof latestEvaluation?.score === "number" ? latestEvaluation.score : undefined;
const converged = targetScore != null && score != null && score >= targetScore;
const isAgentEvaluator = typeof evaluator !== "function";
// On iterations after the first, fold the prior score and feedback into the
// generator's prompt so it improves rather than regenerates from scratch.
const priorEval = score != null ? latestEvaluation : undefined;
const generateChildren = priorEval
? React.createElement(
React.Fragment,
null,
children,
"\n\nPrevious attempt score: ",
String(score),
". Improve on it using this feedback:\n",
JSON.stringify(priorEval),
)
: children;
return React.createElement(
Loop,
{
id: prefix,
until: converged,
maxIterations,
onMaxReached,
},
React.createElement(
Sequence,
null,
React.createElement(Task, {
id: generateId,
output: generateOutput,
agent: generator,
children: generateChildren,
}),
isAgentEvaluator
? React.createElement(Task, {
id: evaluateId,
output: evaluateOutput,
agent: evaluator,
// `needs` gates the evaluator until the candidate is terminal; `deps`
// resolves the candidate into its prompt (needs alone injects nothing).
needs: { candidate: generateId },
deps: { candidate: generateOutput },
children: (d) =>
`Evaluate the generated candidate and provide a score.\n\nCandidate:\n${JSON.stringify(d.candidate ?? "(no candidate)")}`,
})
: React.createElement(Task, {
id: evaluateId,
output: evaluateOutput,
needs: { candidate: generateId },
children: evaluator,
}),
),
);
}
// @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 OptimizerProps = {
id?: string;
/** Agent that generates or improves candidates each iteration. */
generator: AgentLike;
/** Agent (or compute function) that scores candidates. */
evaluator: AgentLike | ((candidate: unknown) => unknown | Promise<unknown>);
/** Output schema for generated candidates. */
generateOutput: OutputTarget;
/** Output schema for evaluation results. Must include a `score: number` field. */
evaluateOutput: OutputTarget;
/** Score threshold to stop early. When omitted, runs all iterations. */
targetScore?: number;
/** Maximum optimization rounds. @default 10 */
maxIterations?: number;
/** Behavior when maxIterations is reached. @default "return-last" */
onMaxReached?: "return-last" | "fail";
/** Skip the entire optimization loop. */
skipIf?: boolean;
/** Initial generation prompt (string or ReactNode). */
children: string | React.ReactNode;
};