Skip to main content

Multi-Agent Review

Two reviewers run concurrently via <Parallel>, then an aggregator produces a final verdict.

Workflow Definition

/** @jsxImportSource smithers-orchestrator */
// multi-agent-review.tsx
import { createSmithers, Task, Sequence, Parallel } from "smithers-orchestrator";
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";

const { Workflow, smithers, outputs } = createSmithers({
  review: z.object({
    approved: z.boolean(),
    feedback: z.string(),
  }),
  verdict: z.object({
    approved: z.boolean(),
    summary: z.string(),
  }),
});

const securityReviewer = new Agent({
  model: anthropic("claude-sonnet-5"),
  instructions:
    "You are a security-focused code reviewer. Look for vulnerabilities, injection risks, and auth issues. Return your verdict and detailed feedback.",
});

const qualityReviewer = new Agent({
  model: anthropic("claude-sonnet-5"),
  instructions:
    "You are a code quality reviewer. Evaluate readability, test coverage, error handling, and adherence to best practices. Return your verdict and detailed feedback.",
});

const aggregator = new Agent({
  model: anthropic("claude-sonnet-5"),
  instructions:
    "You receive two code reviews. Synthesize them into a single verdict. Approve only if both reviewers approve.",
});

export default smithers((ctx) => {
  const diff = [
    "- const token = req.query.token;",
    "+ const token = sanitize(req.headers.authorization);",
  ].join("\n");

  return (
    <Workflow name="multi-agent-review">
      <Sequence>
        <Parallel maxConcurrency={2}>
          <Task id="security-review" output={outputs.review} agent={securityReviewer}>
            {`Review this PR diff for security issues:\n\n${diff}`}
          </Task>

          <Task id="quality-review" output={outputs.review} agent={qualityReviewer}>
            {`Review this PR diff for code quality:\n\n${diff}`}
          </Task>
        </Parallel>

        <Task
          id="aggregate"
          output={outputs.verdict}
          agent={aggregator}
          needs={{ secReview: "security-review", qualReview: "quality-review" }}
          deps={{ secReview: outputs.review, qualReview: outputs.review }}
        >
          {(deps) => (
            <>
              Combine these two reviews into a final verdict:{"\n\n"}
              Security review: {deps.secReview.approved ? "APPROVED" : "REJECTED"} -{" "}
              {deps.secReview.feedback}
              {"\n\n"}
              Quality review: {deps.qualReview.approved ? "APPROVED" : "REJECTED"} -{" "}
              {deps.qualReview.feedback}
            </>
          )}
        </Task>
      </Sequence>
    </Workflow>
  );
});

Running

bunx smithers-orchestrator up multi-agent-review.tsx --input '{}'
[multi-agent-review] Starting run jkl012
[security-review] Running...
[quality-review] Running...
[security-review] Done -> { approved: true, feedback: "Good: moved token from query to header, added sanitization." }
[quality-review] Done -> { approved: false, feedback: "Missing error handling if authorization header is absent." }
[aggregate] Done -> { approved: false, summary: "Security looks good, but quality reviewer flagged missing null check on header." }
[multi-agent-review] Completed

How Parallel Works

  • All children of <Parallel> start at the same time.
  • maxConcurrency limits simultaneous tasks. If omitted, all run at once.
  • The Sequence waits for all parallel tasks to finish before continuing.
  • Tasks sharing the same output table are disambiguated by nodeId.
Retrieve each result with ctx.outputMaybe(schemaKey, { nodeId }):
const secReview = ctx.outputMaybe("review", { nodeId: "security-review" });
const qualReview = ctx.outputMaybe("review", { nodeId: "quality-review" });
Both return undefined until their respective tasks complete. When a downstream task just needs to consume those results as its prompt, as aggregate does above, prefer <Task deps={{ secReview: outputs.review, qualReview: outputs.review }} needs={{ secReview: "security-review", qualReview: "quality-review" }}> with a (deps) => ... children callback instead: it defers the task until both rows exist and hands you resolved, non-optional values.