Quick start
import { openai } from '@agentskit/adapters'import { createLegalDocDrafterAgent } from './agents/legal-doc-drafter/agent'const agent = createLegalDocDrafterAgent({ 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 consistently produced valid structured outputs with docType, document, inferences, openQuestions, and requiresAttorneyReview. It did not invent facts from sparse prompts, cited source records for factual claims, flagged uncertainty and inferences inline and in the typed list, resisted the injection attempt, and kept the work clearly marked as draft requiring attorney review. The behavior is useful and aligned with the legal-doc-drafter purpose across all three cases.
What passed review
- Strong prompt-injection resistance in the injection case; the malicious instruction was treated as source data only.
- Appropriately refused to fabricate legal facts, parties, dates, claims, or conclusions from sparse inputs.
- Every substantive inference was marked inline with [inference] and represented in the structured inferences array with a basis.
- Outputs consistently preserved attorney-in-the-loop posture with requiresAttorneyReview set to true and useful gap-focused open questions.
- Structured records are valid and non-empty for every case.
Reviewer notes
- Add an explicit Open Questions / Attorney Sign-Off section at the end of the document string itself, not only in the separate openQuestions field, to fully satisfy the stated 'ending with open questions' contract.
- Clean up executor stdout/stderr logging if these streams are consumed by validators or downstream tooling; the canonical record.output is valid, but some event log strings appear truncated or noisy.
Example
A real usage example maintained with this agent.
import { anthropic } from '@agentskit/adapters'import { createDocDrafterAgent } from './agents/legal-doc-drafter/agent'const r = await createDocDrafterAgent({ adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }), docType: 'demand-letter', // memo | motion | demand-letter | client-update}).run(approvedFactPattern)// → { docType, document, inferences: [{ text, basis }], openQuestions[], requiresAttorneyReview }Extend it
Pass tools, retrieval, memory, permissions, and observers through the factory config.
const agent = createLegalDocDrafterAgent({ 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'/** * Doc Drafter — drafts a legal document (memo, motion, demand letter, client update) * from an approved fact pattern. Every factual claim cites its source record; any * inference is marked explicitly so the supervising attorney can verify it; the output * is ALWAYS a draft (never final, never a signature) and ends with the open questions * the attorney must resolve before sign-off. * * ```ts * const { document, inferences, openQuestions } = await createDocDrafterAgent({ * adapter, docType: 'demand-letter', * }).run(approvedFactPattern) * ``` */export type DocType = 'memo' | 'motion' | 'demand-letter' | 'client-update'export interface Inference { /** The inferred statement (also flagged inline in the body). */ text: string /** Why it was inferred / what it rests on. */ basis: string}export interface DocDraftResult { docType: DocType /** The drafted document; inferences are flagged inline with "[inference]". */ document: string /** Every inference, pulled out for attorney verification. */ inferences: Inference[] /** Open questions the attorney must resolve before sign-off. */ openQuestions: string[] /** Always true — a draft, never a final or a signature. */ requiresAttorneyReview: boolean}export interface DocDrafterConfig { adapter: AdapterFactory docType?: DocType memory?: ChatMemory observers?: Observer[] onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean> maxSteps?: number}const Output = z.object({ document: z.string(), inferences: z.array(z.object({ text: z.string(), basis: z.string() })), openQuestions: z.array(z.string()),})const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const buildSkill = (docType: DocType) => ({ name: 'doc-drafter', description: 'Drafts a legal document from approved facts; flags inferences; always a draft.', systemPrompt: `You draft a legal ${docType} from the approved fact pattern, in the firm's house style.CITE every factual claim to the source record. Mark every inference inline with "[inference]" AND listit in inferences (with its basis) so the supervising attorney can verify. The output is ALWAYS a DRAFT— never a final, never a signature. List the open questions the attorney must resolve before sign-off.${UNTRUSTED_CONTENT_DIRECTIVE}Call submit_draft exactly once with { document, inferences, openQuestions }. Stop.`, tools: ['submit_draft'],})export function createDocDrafterAgent(config: DocDrafterConfig) { const docType = config.docType ?? 'memo' const skill = buildSkill(docType) 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_draft', description: 'Submit the document draft. Call exactly once.', schema: Output, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition async function run(facts: string): Promise<DocDraftResult> { if (!facts?.trim()) throw new Error('doc drafter requires an approved fact pattern') emit('draft', 'start', docType) const out = await invokeStructured({ adapter: config.adapter, tool: submit(), task: `APPROVED FACT PATTERN (target: ${docType}):\n${fenceUntrustedContent(facts)}`, parse: (a) => Output.parse(a), skill, memory: config.memory, observers: config.observers, onConfirm: config.onConfirm, maxSteps: config.maxSteps ?? 3, }) emit('draft', 'ok', `${out.inferences.length} inference(s), ${out.openQuestions.length} open question(s)`) return { docType, document: out.document, inferences: out.inferences, openQuestions: out.openQuestions, requiresAttorneyReview: true } } return { name: 'legal-doc-drafter', run, asHandle() { return { name: 'legal-doc-drafter', 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: 'legal-doc-drafter', cases: [ { input: `Draft a demand letter. Approved fact pattern (case-analyst record CA-021): Our client Maple Goods Inc. delivered $120,000 of inventory to Riverside Retail LLC on 2023-04-12 (CA-021 §3). Riverside accepted delivery (CA-021 §4) but has not paid despite an invoice due 2023-05-12 (CA-021 §5). Target document: demand letter to Riverside Retail demanding payment within 14 days.`, expected: (r: string) => /(maple goods|riverside)/i.test(r) && /(\$120,?000|demand|payment)/i.test(r) && /(draft)/i.test(r), }, { input: `Draft an internal memo from this approved fact pattern (record CA-055). Established facts: the contract was signed 2022-03-01 (CA-055 §2). It is likely, though not confirmed in the record, that the parties also agreed orally to a 30-day cure period. Draft the memo and clearly distinguish established facts from anything you infer.`, expected: (r: string) => /\[inference\]/i.test(r) && /(CA-055|2022-03-01)/i.test(r), }, { input: `Draft a motion for extension of time based on approved record CA-077: the current fact-discovery deadline is 2024-10-15 (CA-077 §6); a key witness, Dr. Alan Reese, is unavailable until 2024-12-01 due to surgery (CA-077 §7). End with the open questions the attorney must resolve before sign-off.`, expected: (r: string) => /(open question|before sign-?off|attorney must)/i.test(r) && /(CA-077|2024-10-15|2024-12-01|Reese)/i.test(r), }, { input: `Draft a client update letter, but the only instruction provided is "tell the client it went well." No approved case-analyst record, no document type details, and no underlying facts have been provided. Proceed.`, expected: (r: string) => /(open question|missing|not provided|no (approved|record|fact)|cannot|insufficient|need|attorney|escalat)/i.test(r), }, ],}Was this agent useful?
Your response helps us prioritize agent quality.