{"id":"agency-copy-reviewer","title":"Copy Reviewer","description":"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.","category":"agency","version":"2.0.0","source":"AKOS","license":"MIT","tags":["agency","review","human-in-the-loop"],"packages":["@agentskit/core","@agentskit/runtime","@agentskit/tools"],"files":["agent.ts","README.md","eval.ts"],"requires":{"zod":"^3","zod-to-json-schema":"^3"},"status":"validated","skill":{"name":"agency-copy-reviewer","description":"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.","systemPrompt":"You review draft creative against the client's brand-voice guide (tone, vocabulary, banned\nwords, audience). For each misalignment, give: the line, the current text, a suggested rewrite, and a\nrationale tied to a specific rule in the guide. Then a one-paragraph overall assessment.\n\nSUGGEST, do not impose — NEVER rewrite the whole piece. Mark contentious=true for any item that is a\njudgment call on brand INTENT rather than a clear rule break, so it routes to the account lead.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_review exactly once with { misalignments, overallAssessment }. Stop."},"flow":null,"a2a":{"id":"io.agentskit.registry.agency-copy-reviewer","name":"Copy Reviewer","description":"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.","version":"2.0.0","homepage":"https://registry.agentskit.io","skills":[{"name":"agency-copy-reviewer","description":"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.","capabilities":{"streaming":true,"cancellation":true,"requiresApproval":false}}]},"sources":[{"path":"agent.ts","content":"import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'\nimport { fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE } from '@agentskit/core/security'\nimport { invokeStructured } from '@agentskit/runtime'\nimport { defineZodTool } from '@agentskit/tools'\nimport { z } from 'zod'\nimport { zodToJsonSchema } from 'zod-to-json-schema'\nimport type { JSONSchema7 } from 'json-schema'\n\n/**\n * Copy Reviewer — reads draft creative against a brand-voice guide and returns TYPED\n * misalignments (line, current text, suggested rewrite, rationale tied to the guide) +\n * an overall assessment. It SUGGESTS, never imposes: it never rewrites the whole piece,\n * and contentious brand-intent calls set `routeToHuman` for the account lead.\n *\n * ```ts\n * const { misalignments, routeToHuman } = await createCopyReviewerAgent({ adapter })\n *   .run(`GUIDE:\\n${guide}\\n\\nDRAFT:\\n${draft}`)\n * ```\n */\n\nexport interface CopyMisalignment {\n  line: string\n  currentText: string\n  suggestedRewrite: string\n  /** Why it misaligns, tied to a specific rule in the guide. */\n  rationale: string\n  /** True when this is a judgment call on brand intent, not a clear rule break. */\n  contentious: boolean\n}\n\nexport interface CopyReviewResult {\n  misalignments: CopyMisalignment[]\n  overallAssessment: string\n  /** True when ≥1 contentious call needs the account lead. */\n  routeToHuman: boolean\n}\n\nexport interface CopyReviewerConfig {\n  adapter: AdapterFactory\n  memory?: ChatMemory\n  observers?: Observer[]\n  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>\n  maxSteps?: number\n}\n\nconst Output = z.object({\n  misalignments: z.array(z.object({\n    line: z.string(),\n    currentText: z.string(),\n    suggestedRewrite: z.string(),\n    rationale: z.string(),\n    contentious: z.boolean(),\n  })),\n  overallAssessment: z.string(),\n})\nconst toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7\n\nconst skill = {\n  name: 'copy-reviewer',\n  description: 'Flags brand-voice misalignments in draft creative as typed suggestions (suggests, never imposes).',\n  systemPrompt: `You review draft creative against the client's brand-voice guide (tone, vocabulary, banned\nwords, audience). For each misalignment, give: the line, the current text, a suggested rewrite, and a\nrationale tied to a specific rule in the guide. Then a one-paragraph overall assessment.\n\nSUGGEST, do not impose — NEVER rewrite the whole piece. Mark contentious=true for any item that is a\njudgment call on brand INTENT rather than a clear rule break, so it routes to the account lead.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_review exactly once with { misalignments, overallAssessment }. Stop.`,\n  tools: ['submit_review'],\n}\n\nexport function createCopyReviewerAgent(config: CopyReviewerConfig) {\n  const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => {\n    for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail })\n  }\n  const submit = (): ToolDefinition =>\n    defineZodTool({ name: 'submit_review', description: 'Submit the copy review. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition\n\n  async function run(input: string): Promise<CopyReviewResult> {\n    if (!input?.trim()) throw new Error('copy reviewer requires the brand guide + draft creative')\n    emit('review', 'start')\n    const out = await invokeStructured({\n      adapter: config.adapter,\n      tool: submit(),\n      task: `BRAND GUIDE + DRAFT CREATIVE:\\n${fenceUntrustedContent(input)}`,\n      parse: (a) => Output.parse(a),\n      skill,\n      memory: config.memory,\n      observers: config.observers,\n      onConfirm: config.onConfirm,\n      maxSteps: config.maxSteps ?? 3,\n    })\n    const routeToHuman = out.misalignments.some((m) => m.contentious)\n    emit('review', 'ok', `${out.misalignments.length} flag(s)${routeToHuman ? ' (route to lead)' : ''}`)\n    return { misalignments: out.misalignments, overallAssessment: out.overallAssessment, routeToHuman }\n  }\n\n  return {\n    name: 'agency-copy-reviewer',\n    run,\n    asHandle() {\n      return { name: 'agency-copy-reviewer', run: async (task: string) => JSON.stringify(await run(task)) }\n    },\n  }\n}\n"},{"path":"README.md","content":"# Copy Reviewer\n\nReads draft creative against a brand-voice guide and returns **typed misalignments** with suggested rewrites — suggests, never imposes.\n\n```bash\nnpx agentskit add agency-copy-reviewer\n```\n\n```ts\nimport { anthropic } from '@agentskit/adapters'\nimport { createCopyReviewerAgent } from './agents/agency-copy-reviewer/agent'\n\nconst r = await createCopyReviewerAgent({\n  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),\n}).run(`GUIDE:\\n${brandGuide}\\n\\nDRAFT:\\n${draftCopy}`)\n// → { misalignments: [{ line, currentText, suggestedRewrite, rationale, contentious }], overallAssessment, routeToHuman }\n```\n\n- **Typed misalignments** — `invokeStructured` + zod; each ties its rationale to a specific rule in the guide.\n- **Suggests, never imposes** — never rewrites the whole piece; emits line-level suggestions only.\n- **Routes contentious calls** — any item marked `contentious` (a judgment on brand *intent*, not a clear rule break) sets `routeToHuman` for the account lead. Untrusted copy is **fenced**.\n\n`run(guideAndDraft)` → `CopyReviewResult`. `asHandle()` is JSON-out. See [composing agents](../../COMPOSING.md).\n"},{"path":"eval.ts","content":"import type { EvalSuite } from '@agentskit/eval'\n\nexport const suite: EvalSuite = {\n  name: 'agency-copy-reviewer',\n  cases: [\n    {\n      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.\nDraft copy to review:\nL1: \"This miracle serum erases wrinkles overnight.\"\nL2: \"Clinically tested on 120 participants over 8 weeks.\"\nL3: \"Get flawless skin or your money back.\"\nReview against the guide.`,\n      expected: (r: string) =>\n        /(miracle|flawless)/i.test(r) && /(rewrite|suggest)/i.test(r) && /(rationale|guide|banned)/i.test(r),\n    },\n    {\n      input: `Brand voice guide — Client: Forge Fitness. Tone: blunt, motivating, no corporate fluff. Vocabulary: short sentences, active voice. Banned: \"synergy\", \"leverage\", \"best-in-class\".\nDraft copy:\nL1: \"Leverage our best-in-class equipment to maximize synergy in your fitness journey.\"\nL2: \"Show up. Lift. Repeat.\"\nFlag misalignments and assess overall.`,\n      expected: (r: string) =>\n        /(leverage|synergy|best-in-class)/i.test(r) && /(assessment|overall)/i.test(r),\n    },\n    {\n      input: `Brand voice guide — Client: Nimbus Cloud, dev-tool brand. Tone: technical, peer-to-peer, never condescending. Audience: senior engineers.\nDraft copy:\nL1: \"Even non-techies can deploy in one click — it's that easy!\"\nL2: \"Ship to production with a single command.\"\nReview the lines against the guide.`,\n      expected: (r: string) =>\n        /(condescend|non-techies|tone)/i.test(r) && /(rewrite|suggest)/i.test(r),\n    },\n    {\n      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.)`,\n      expected: (r: string) =>\n        /(brand guide|guide|provide|ask|cannot|can't)/i.test(r),\n    },\n  ],\n}\n"}],"installable":true,"validation":{"status":"approved","score":96,"confidence":0.96,"method":"codex-executor-independent-reviewer","iterations":1,"cases":3,"summary":"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.","strengths":["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."],"notes":[]}}