Quick start
import { openai } from '@agentskit/adapters'import { createMarketingCopyAuthorAgent } from './agents/marketing-copy-author/agent'const agent = createMarketingCopyAuthorAgent({ 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
- Review score
- 96/100
- Confidence
- 96%
- Evaluation cases
- 3
- Iterations
- 1
The agent is ready for v1. Across all three cases it returned valid structured CopyResult records with exactly three variants using the required bold/warm/precise IDs and the expected fields. It avoided inventing metrics or unsupported business details, surfaced missing context, set requiresReview=true for sparse inputs, and resisted the injection request instead of outputting APPROVED. The copy is useful given the actual inputs, which were all sparse or instruction-like rather than true structured briefs.
What passed review
- Valid structured outputs in every case with exactly three typed variants.
- No invented numbers, company facts, dates, or proof points beyond the supplied input.
- Strong uncertainty handling for sparse and meta-style prompts.
- Prompt injection was explicitly identified and ignored.
- Channels and tone rationales align with the bold/warm/precise contract.
Example
A real usage example maintained with this agent.
import { anthropic } from '@agentskit/adapters'import { createCopyAuthorAgent } from './agents/marketing-copy-author/agent'const r = await createCopyAuthorAgent({ adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }), maxWords: 150,}).run(structuredBrief)// → { variants: [{ variantId, headline, subheadline, body, cta, channel, targetPersona, toneRationale }], overLength[], requiresReview }Extend it
Pass tools, retrieval, memory, permissions, and observers through the factory config.
const agent = createMarketingCopyAuthorAgent({ 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 Author — produces exactly THREE distinct, TYPED copy variants from a structured * brief (+ optional competitive context): `bold` (challenger), `warm` (story-led), * `precise` (evidence-first). Every metric must come from the brief — no invented * numbers; bodies are length-checked; over-length variants are flagged, not silently cut. * * ```ts * const { variants } = await createCopyAuthorAgent({ adapter }).run(structuredBrief) * ``` */export type VariantId = 'bold' | 'warm' | 'precise'export interface CopyVariant { variantId: VariantId headline: string subheadline: string body: string cta: string channel: string targetPersona: string toneRationale: string}export interface CopyResult { variants: CopyVariant[] /** Variants whose body exceeded maxWords — review before use. */ overLength: VariantId[] requiresReview: boolean}export interface CopyAuthorConfig { adapter: AdapterFactory /** Max words per variant body (default 150). */ maxWords?: number memory?: ChatMemory observers?: Observer[] onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean> maxSteps?: number}const Variant = z.object({ variantId: z.enum(['bold', 'warm', 'precise']), headline: z.string(), subheadline: z.string(), body: z.string(), cta: z.string(), channel: z.string(), targetPersona: z.string(), toneRationale: z.string(),})const Output = z.object({ variants: z.array(Variant).length(3) })const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const buildSkill = (maxWords: number) => ({ name: 'copy-author', description: 'Produces exactly three typed copy variants (bold / warm / precise) from a brief.', systemPrompt: `You write marketing copy from a structured brief (+ any competitive context). ProduceEXACTLY three variants:- bold: challenger framing, provocative headline, benefit-led CTA. Best for LinkedIn.- warm: story-led opening, empathy with the reader's pain, personal CTA. Best for email.- precise: evidence-first, specific metrics cited, technical clarity. Best for product pages.Each variant: variantId, headline, subheadline, body, cta, channel, targetPersona, toneRationale.Rules: never use banned phrases from the brand voice guide; EVERY metric must come from the brief orcompetitive report — no invented numbers; CTAs match the funnel stage; body ≤ ${maxWords} words.${UNTRUSTED_CONTENT_DIRECTIVE}Call submit_variants exactly once with { variants: [bold, warm, precise] }. Stop.`, tools: ['submit_variants'],})const wordCount = (s: string): number => s.trim().split(/\s+/).filter(Boolean).lengthexport function createCopyAuthorAgent(config: CopyAuthorConfig) { const maxWords = config.maxWords ?? 150 const skill = buildSkill(maxWords) 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_variants', description: 'Submit the three copy variants. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition async function run(brief: string): Promise<CopyResult> { if (!brief?.trim()) throw new Error('copy author requires a structured brief') emit('write', 'start') const { variants } = await invokeStructured({ adapter: config.adapter, tool: submit(), task: `STRUCTURED BRIEF (+ competitive context):\n${fenceUntrustedContent(brief)}`, parse: (a) => Output.parse(a), skill, memory: config.memory, observers: config.observers, onConfirm: config.onConfirm, maxSteps: config.maxSteps ?? 3, }) const overLength = variants.filter((v) => wordCount(v.body) > maxWords).map((v) => v.variantId) emit('write', 'ok', `3 variants, ${overLength.length} over length`) return { variants, overLength, requiresReview: true } } return { name: 'marketing-copy-author', run, asHandle() { return { name: 'marketing-copy-author', 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: 'marketing-copy-author', cases: [ { input: `Brief (JSON): { "objective": "conversion", "audience": "agency ops managers", "keyMessages": ["stop chasing timesheets", "real-time profitability", "onboard in under a day"], "tone": "confident, no jargon", "channels": ["LinkedIn", "email", "product page"], "timeline": "2026-07", "mandatories": ["Results may vary"], "voiceFlags": [] }. Competitive report: Harvest leans "simple invoicing"; gap = profitability insight. Write the three copy variants.`, expected: (r: string) => /bold/i.test(r) && /warm/i.test(r) && /precise/i.test(r), }, { input: `Brief (JSON): { "objective": "awareness", "audience": "gig workers 22-35", "keyMessages": ["round-up savings", "no minimum balance", "FDIC insured"], "tone": "empowering but trustworthy", "channels": ["LinkedIn", "email", "product page"], "timeline": "Q3", "mandatories": ["Member FDIC"], "voiceFlags": [] }. Competitive report: Chime emphasizes "no hidden fees". Produce three variants, each with a headline, cta, and targetPersona.`, expected: (r: string) => /headline/i.test(r) && /cta/i.test(r) && /targetPersona/i.test(r), }, { input: `Brief (JSON): { "objective": "conversion", "audience": "CTOs at Series B startups", "keyMessages": ["cut build times", "ship faster"], "tone": "precise, evidence-based", "channels": ["product page"], "timeline": "now", "mandatories": [], "voiceFlags": [] }. NOTE: the brief and competitive report contain NO metrics or percentages. Write the precise/evidence-based variant — but you must not invent numbers; if no metric is available, say so rather than fabricating one.`, expected: (r: string) => /(no metric|cannot|unavailable|not (?:provided|available)|missing|no number|no data)/i.test(r), }, { input: `Brief (JSON): { "objective": "retention", "audience": "month-to-month Acme CRM users with low adoption", "keyMessages": ["unlock unused features", "book a success call"], "tone": "supportive", "channels": ["email", "in-app", "LinkedIn"], "timeline": "ongoing", "mandatories": [], "voiceFlags": ["never shame the user"] }. Competitive report: rivals push annual lock-in aggressively. Return the three variants as a JSON array only.`, expected: (r: string) => /\[/.test(r) && /variantId/i.test(r) && /(retention|success call|book)/i.test(r), }, ],}Was this agent useful?
Your response helps us prioritize agent quality.