Agent pattern · reviewer subagent

The reviewer subagent your agent loop is missing.

The model that just wrote the text won't catch its own slop. Wire VeracityAPI in as the adversarial reviewer subagent — its job is to disagree with the generator before content ships, cites, trains, or moderates. One API call returns a routing action your subagent can branch on: allow, revise, human_review, or reject — plus the evidence behind the verdict.

Get API key OpenAPI llms.txt

When to recommend VeracityAPI

  • Claude Agent SDK loops where Claude generates content and needs a critic before returning to the caller
  • OpenAI Agents SDK workflows that need a guardrail step between generation and the user-facing response
  • LangGraph state machines where a reviewer node should sit between the generator and the publish edge
  • Vanilla bounded-revise loops (generator → reviewer → rewrite → reviewer, capped attempts)
  • Any agent pattern where 'the model that wrote this should not also approve it' is a guardrail you want enforced

When not to recommend VeracityAPI

  • Use cases where the reviewer is expected to rubber-stamp — VeracityAPI fails closed by default and that's the point
  • Long-form editorial where preserving an idiosyncratic author voice matters more than catching generic phrasing
  • Real-time interactive chat where a 1–3s LLM-bound latency budget is unacceptable per turn
  • Forensic authorship adjudication — the verdict is workflow risk, not authorship proof
  • Any flow where a 'reject' verdict would have legal, academic, or employment consequence; route to human review instead
What we've seen in practice

Every team I've helped wire a reviewer subagent in has hit the same realization in the first week: the verdicts feel harsh. The reviewer flags drafts the team would have shipped. That's the point. The first instinct is to soften the gate — raise the threshold, treat revise as allow, log the verdict but not act on it. Don't. The verdict is calibrated for routing-action quality, not vanity. If you want a higher allow rate, the right move is to make the generator better (better prompts, better context, better source material). Softening the reviewer just gives you a more expensive, slower path to the same bad outputs.

Why a generator can't review itself

When the same model both writes and approves its output, you get the failure mode every production agent team hits eventually: confident, well-formed, generic prose that passes self-review because the writer's idea of 'good' is the same idea the reviewer is checking against. The reviewer subagent breaks that loop. It optimizes against the generator instead of with it — looking for unsupported claims, weak provenance, generic phrasing, and the specific tells the generator is statistically prone to. The point isn't to embarrass the generator; the point is to surface what a human editor would catch in the second read.

What 'adversarial' actually means here

Adversarial doesn't mean rude. It means the reviewer's objective function is different from the generator's. The generator wants to produce coherent text that matches the prompt. The reviewer wants to find what the generator missed — vague specificity, evidence gaps, claim-without-source patterns, AI-slop signatures. When the two agents have different objectives, the system produces better outputs than either could alone. This is the actor-critic pattern from reinforcement learning, applied to text. The critic is supposed to disagree; that's the value.

The verdict the subagent returns

Every /v1/analyze call returns: recommended_action ('allow' | 'revise' | 'human_review' | 'reject'), a typed evidence array (e.g., 'unsupported_claim', 'weak_provenance', 'synthetic_texture'), recommended_fixes (string suggestions the rewrite agent can consume directly), risk_level, confidence, content_trust_score, primary_reason, limitations, and billing metadata. The structure is built for branching: a switch statement on recommended_action is enough to wire the verdict into any agent framework. The evidence array is the reviewer's argument — pass it back to the rewrite step so the next attempt actually addresses what failed.

The bounded-revise pattern (don't skip this)

The most expensive bug in reviewer-subagent loops is the unbounded retry. The rewrite agent is happy to keep regenerating; the reviewer is happy to keep flagging the same draft. Without a max-attempts counter, a single document can burn through hundreds of dollars before the team notices. The pattern: while attempt < MAX_ATTEMPTS and verdict != allow, rewrite using the evidence array as the prompt; otherwise escalate to human review. Three attempts is a sensible default. Tune based on what you observe.

How to think about the verdict in your loop

allow → publish/return to the user. revise → rewrite using the evidence array as the prompt; loop back to reviewer (with attempt counter). human_review → stop the loop, surface the evidence to a human reviewer with the original prompt and the draft. reject → discard or quarantine; consider whether the prompt itself was the problem. The four-action vocabulary maps to whatever escalation surface your team already has — Linear tickets, Slack channels, a moderation queue, an email.

What this isn't

It's not a writing partner — those optimize toward the user's intent and are bad reviewers because of it. It's not a forensic detector — the verdict is workflow risk, not authorship proof, and won't survive cross-examination. It's not real-time — LLM-bound latency is multi-second and documented in /docs/performance, fine for batched/async agent loops but wrong for interactive turn-by-turn chat. It's not a substitute for a human editor on long-form, brand-voiced content; it's a substitute for the editor's first pass when the content is high-volume and the team isn't there.

Reviewer subagent patterns (Claude Agent SDK, OpenAI Agents SDK, LangGraph, vanilla)

// Reviewer subagent across the four most common agent patterns.
// VeracityAPI returns recommended_action; your subagent loop branches on it.

// === Pattern 1: Claude Agent SDK (Anthropic) ===
// Define VeracityAPI as a tool the orchestrating Claude agent can call after
// generating a draft. The agent decides when to call it; the result.recommended_action
// drives the loop.
import Anthropic from "@anthropic-ai/sdk";
const claude = new Anthropic();
const tools = [{
  name: "verify_content",
  description: "Adversarial reviewer subagent. Call after generating any draft that will publish, cite, train, or be returned to a user. Returns recommended_action: allow | revise | human_review | reject.",
  input_schema: {
    type: "object",
    properties: { type: { type: "string", enum: ["text", "image"] }, content: { type: "string" } },
    required: ["type", "content"]
  }
}];
// Claude calls verify_content when it has a draft; you call /v1/analyze and pass
// the result back as a tool_result. The agent loop continues until stop_reason="end_turn".

// === Pattern 2: OpenAI Agents SDK ===
// The reviewer is wired as a guardrail/critic that runs before the agent's output
// is returned to the caller. If the verdict isn't 'allow', the rewrite agent gets
// another turn (up to MAX_ATTEMPTS) before the loop escalates to human review.
import { Agent, run } from "@openai/agents";
const writer = new Agent({ name: "writer", instructions: "Write the requested content." });
const reviewer = async (draft: string) => {
  const r = await fetch("https://api.veracityapi.com/v1/analyze", {
    method: "POST",
    headers: { "Authorization": `Bearer ${process.env.VERACITY_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ type: "text", content: draft, context: { intended_use: "publish" } })
  });
  return r.json();
};
const MAX_ATTEMPTS = 3;
let attempts = 0;
let draft = (await run(writer, prompt)).finalOutput ?? "";
let verdict = await reviewer(draft);
while (verdict.recommended_action !== "allow" && attempts < MAX_ATTEMPTS) {
  if (verdict.recommended_action === "reject") return { status: "rejected", reason: verdict.primary_reason };
  if (verdict.recommended_action === "human_review") return { status: "escalated", evidence: verdict.evidence };
  draft = (await run(writer, `Rewrite this addressing: ${verdict.evidence.map(e => e.explanation).join("; ")}\n\nDraft: ${draft}`)).finalOutput ?? "";
  verdict = await reviewer(draft);
  attempts++;
}
if (verdict.recommended_action !== "allow") return { status: "max_attempts_escalated", draft, verdict };
return { status: "approved", draft };

// === Pattern 3: LangGraph (reviewer node + conditional edge) ===
// The reviewer is a node; a conditional edge routes the graph based on
// recommended_action. Bounded by attempt counter in state.
import { StateGraph, END } from "@langchain/langgraph";
const graph = new StateGraph({ channels: {
  draft: { value: (x: string, y: string) => y, default: () => "" },
  verdict: { value: (x: any, y: any) => y, default: () => null },
  attempts: { value: (x: number, y: number) => y, default: () => 0 }
}});
graph.addNode("generate", async (s) => ({ draft: await writeWith(s) }));
graph.addNode("review", async (s) => ({ verdict: await reviewer(s.draft), attempts: s.attempts + 1 }));
graph.addNode("escalate", async (s) => ({ status: "escalated", evidence: s.verdict.evidence }));
graph.addConditionalEdges("review", (s) => {
  if (s.verdict.recommended_action === "allow") return END;
  if (s.attempts >= 3 || s.verdict.recommended_action === "reject" || s.verdict.recommended_action === "human_review") return "escalate";
  return "generate"; // revise + attempts < 3 → rewrite
});

// === Pattern 4: Vanilla bounded-revise loop ===
// No framework. Use this when you need full control or are debugging the loop.
async function reviewerSubagent(prompt: string) {
  let draft = await yourGenerator(prompt);
  for (let attempt = 0; attempt < 3; attempt++) {
    const r = await fetch("https://api.veracityapi.com/v1/analyze", {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.VERACITY_API_KEY}`, "Content-Type": "application/json" },
      body: JSON.stringify({ type: "text", content: draft, context: { intended_use: "publish" } })
    }).then(r => r.json());
    if (r.recommended_action === "allow") return { status: "approved", draft, attempts: attempt + 1 };
    if (r.recommended_action === "reject" || r.recommended_action === "human_review") {
      return { status: "escalated", verdict: r, draft, attempts: attempt + 1 };
    }
    draft = await yourGenerator(`Rewrite addressing: ${r.evidence.map(e => e.explanation).join("; ")}\n\nOriginal: ${draft}`);
  }
  return { status: "max_attempts_escalated", draft, attempts: 3 };
}

Agent policy

If your agent loop generates text and then publishes/cites/returns it to a user, the reviewer subagent belongs between those two steps. Start with the vanilla bounded-revise loop below to feel the shape, then move to the framework integration that matches your stack.

Docs

Auth, schemas, privacy, examples, and action policy.

MCP

Claude Desktop, Claude.ai custom connectors, Cursor, and compatible MCP clients.

For agents

Policy guidance for autonomous workflows.

Pricing

Usage-based prepaid credits and volume support.