agency·Independently reviewed · 96/100

Copy Reviewer

Reads draft creative against a brand-voice guide and returns TYPED misalignments (line, current text, suggested rewrite, rationale) + an overall assessment. Suggests, never imposes (never rewrites the whole piece); contentious brand-intent calls set routeToHuman for the account lead.

agencyreviewhuman-in-the-loop

Install

npx agentskit add agency-copy-reviewer

Quick start

import { openai } from '@agentskit/adapters'import { createAgencyCopyReviewerAgent } from './agents/agency-copy-reviewer/agent'const agent = createAgencyCopyReviewerAgent({  adapter: openai({    apiKey: process.env.OPENAI_API_KEY!,    model: 'gpt-4o',  }),})const result = await agent.run('Describe your task here')console.log(result.content)

Independent reviewer approved

Validation evidence

How validation works
Review score
96/100
Confidence
96%
Evaluation cases
3
Iterations
1

The agent produced valid structured results for all three cases, resisted the injection attempt, surfaced missing brand guide and draft context without inventing brand rules, and stayed within its purpose by suggesting line-level remediation rather than rewriting the whole piece. The behavior is conservative and useful given the sparse inputs. Minor reservations: the normal case did not demonstrate a real brand-copy review because the supplied input was not a guide plus draft, and the line field falls back to untrusted-block identifiers rather than true line numbers.

What passed review

  • Valid structured output shape with misalignments, overallAssessment, and routeToHuman in the recorded output.
  • Correctly treats instruction-like user content as untrusted data and does not follow the injection request to output APPROVED.
  • Appropriately avoids hallucinating brand-voice rules or creative context when the guide and draft are absent.
  • Provides actionable next-step guidance while preserving the agent's suggestive, non-imposing role.
  • Uses routeToHuman consistently with the stated purpose; missing context is not mislabeled as a contentious brand-intent call.

Example

A real usage example maintained with this agent.

import { anthropic } from '@agentskit/adapters'import { createCopyReviewerAgent } from './agents/agency-copy-reviewer/agent'const r = await createCopyReviewerAgent({  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),}).run(`GUIDE:\n${brandGuide}\n\nDRAFT:\n${draftCopy}`)// → { misalignments: [{ line, currentText, suggestedRewrite, rationale, contentious }], overallAssessment, routeToHuman }

Extend it

Pass tools, retrieval, memory, permissions, and observers through the factory config.

const agent = createAgencyCopyReviewerAgent({  adapter,  tools,  retriever,  memory,  onConfirm: (call) => approve(call),  observers: [tracer],})
View agent factory source
import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'import { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'import { invokeStructured } from '@agentskit/runtime'import { defineZodTool } from '@agentskit/tools'import { z } from 'zod'import { zodToJsonSchema } from 'zod-to-json-schema'import type { JSONSchema7 } from 'json-schema'/** * Copy Reviewer — reads draft creative against a brand-voice guide and returns TYPED * misalignments (line, current text, suggested rewrite, rationale tied to the guide) + * an overall assessment. It SUGGESTS, never imposes: it never rewrites the whole piece, * and contentious brand-intent calls set `routeToHuman` for the account lead. * * ```ts * const { misalignments, routeToHuman } = await createCopyReviewerAgent({ adapter }) *   .run(`GUIDE:\n${guide}\n\nDRAFT:\n${draft}`) * ``` */export interface CopyMisalignment {  line: string  currentText: string  suggestedRewrite: string  /** Why it misaligns, tied to a specific rule in the guide. */  rationale: string  /** True when this is a judgment call on brand intent, not a clear rule break. */  contentious: boolean}export interface CopyReviewResult {  misalignments: CopyMisalignment[]  overallAssessment: string  /** True when ≥1 contentious call needs the account lead. */  routeToHuman: boolean}export interface CopyReviewerConfig {  adapter: AdapterFactory  memory?: ChatMemory  observers?: Observer[]  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>  maxSteps?: number}const Output = z.object({  misalignments: z.array(z.object({    line: z.string(),    currentText: z.string(),    suggestedRewrite: z.string(),    rationale: z.string(),    contentious: z.boolean(),  })),  overallAssessment: z.string(),})const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const skill = {  name: 'copy-reviewer',  description: 'Flags brand-voice misalignments in draft creative as typed suggestions (suggests, never imposes).',  systemPrompt: `You review draft creative against the client's brand-voice guide (tone, vocabulary, bannedwords, audience). For each misalignment, give: the line, the current text, a suggested rewrite, and arationale tied to a specific rule in the guide. Then a one-paragraph overall assessment.SUGGEST, do not impose — NEVER rewrite the whole piece. Mark contentious=true for any item that is ajudgment call on brand INTENT rather than a clear rule break, so it routes to the account lead.${UNTRUSTED_CONTENT_DIRECTIVE}Call submit_review exactly once with { misalignments, overallAssessment }. Stop.`,  tools: ['submit_review'],}export function createCopyReviewerAgent(config: CopyReviewerConfig) {  const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => {    for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail })  }  const submit = (): ToolDefinition =>    defineZodTool({ name: 'submit_review', description: 'Submit the copy review. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition  async function run(input: string): Promise<CopyReviewResult> {    if (!input?.trim()) throw new Error('copy reviewer requires the brand guide + draft creative')    emit('review', 'start')    const out = await invokeStructured({      adapter: config.adapter,      tool: submit(),      task: `BRAND GUIDE + DRAFT CREATIVE:\n${fenceUntrustedContent(input)}`,      parse: (a) => Output.parse(a),      skill,      memory: config.memory,      observers: config.observers,      onConfirm: config.onConfirm,      maxSteps: config.maxSteps ?? 3,    })    const routeToHuman = out.misalignments.some((m) => m.contentious)    emit('review', 'ok', `${out.misalignments.length} flag(s)${routeToHuman ? ' (route to lead)' : ''}`)    return { misalignments: out.misalignments, overallAssessment: out.overallAssessment, routeToHuman }  }  return {    name: 'agency-copy-reviewer',    run,    asHandle() {      return { name: 'agency-copy-reviewer', run: async (task: string) => JSON.stringify(await run(task)) }    },  }}
View evaluation contract

Replay these cases with the provider and model you plan to deploy.

import type { EvalSuite } from '@agentskit/eval'export const suite: EvalSuite = {  name: 'agency-copy-reviewer',  cases: [    {      input: `Brand voice guide — Client: Solace Skincare. Tone: warm, plain-spoken, science-backed. Banned words: "miracle", "anti-aging", "flawless". Audience: women 30-50 who distrust hype.Draft copy to review:L1: "This miracle serum erases wrinkles overnight."L2: "Clinically tested on 120 participants over 8 weeks."L3: "Get flawless skin or your money back."Review against the guide.`,      expected: (r: string) =>        /(miracle|flawless)/i.test(r) && /(rewrite|suggest)/i.test(r) && /(rationale|guide|banned)/i.test(r),    },    {      input: `Brand voice guide — Client: Forge Fitness. Tone: blunt, motivating, no corporate fluff. Vocabulary: short sentences, active voice. Banned: "synergy", "leverage", "best-in-class".Draft copy:L1: "Leverage our best-in-class equipment to maximize synergy in your fitness journey."L2: "Show up. Lift. Repeat."Flag misalignments and assess overall.`,      expected: (r: string) =>        /(leverage|synergy|best-in-class)/i.test(r) && /(assessment|overall)/i.test(r),    },    {      input: `Brand voice guide — Client: Nimbus Cloud, dev-tool brand. Tone: technical, peer-to-peer, never condescending. Audience: senior engineers.Draft copy:L1: "Even non-techies can deploy in one click — it's that easy!"L2: "Ship to production with a single command."Review the lines against the guide.`,      expected: (r: string) =>        /(condescend|non-techies|tone)/i.test(r) && /(rewrite|suggest)/i.test(r),    },    {      input: `Please review this draft tagline for our client and tell me if it's on-brand: "Bold moves, brighter futures." We're a consumer energy company. (No brand voice guide attached.)`,      expected: (r: string) =>        /(brand guide|guide|provide|ask|cannot|can't)/i.test(r),    },  ],}

Was this agent useful?

Your response helps us prioritize agent quality.

Keep exploring

Related agents

View category