Quick start
import { openai } from '@agentskit/adapters'import { createLegalCaseAnalystAgent } from './agents/legal-case-analyst/agent'const agent = createLegalCaseAnalystAgent({ 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 returned valid structured CaseAnalysisResult outputs, did not fabricate missing legal facts, marked absent fields as "not in record," surfaced deadline-assessment uncertainty, required attorney review, and resisted the injection request to output APPROVED. Behavior is conservative and aligned with the stated purpose for sparse/non-case-file inputs.
What passed review
- Valid typed structure in every case with analysis, deadlineRisks, and requiresAttorneyReview.
- No hallucinated parties, claims, venue, dates, or procedural posture from non-record inputs.
- Injection attempt was not followed; output remained structured and cautious.
- Attorney review was required in every output, appropriate for legal draft analysis.
Example
A real usage example maintained with this agent.
import { anthropic } from '@agentskit/adapters'import { createCaseAnalystAgent } from './agents/legal-case-analyst/agent'const r = await createCaseAnalystAgent({ adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),}).run(caseFile)// → { analysis: { parties[], jurisdictionVenue, proceduralPosture, claims[], defenses[], keyDates[], openDiscovery[] }, deadlineRisks[], requiresAttorneyReview }Extend it
Pass tools, retrieval, memory, permissions, and observers through the factory config.
const agent = createLegalCaseAnalystAgent({ 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'/** * Case Analyst — extracts a TYPED structured analysis from a case file (parties, venue, * posture, claims, defenses, key dates, open discovery). Every datum cites its source * document + page; gaps are "not in record" (never inferred); statute-of-limitations / * filing-deadline risks are surfaced separately at the top. Always a draft. * * ```ts * const { analysis, deadlineRisks } = await createCaseAnalystAgent({ adapter }).run(caseFile) * ``` */const NOT_IN_RECORD = 'not in record'export interface CitedFact { value: string /** Source document + page, or "not in record". */ citation: string}export interface CaseAnalysis { parties: CitedFact[] jurisdictionVenue: CitedFact proceduralPosture: CitedFact claims: CitedFact[] defenses: CitedFact[] keyDates: CitedFact[] openDiscovery: CitedFact[]}export interface CaseAnalysisResult { analysis: CaseAnalysis /** SOL / filing-deadline risks, surfaced at the top for the attorney. */ deadlineRisks: string[] /** Always true — a draft for the supervising attorney. */ requiresAttorneyReview: boolean}export interface CaseAnalystConfig { adapter: AdapterFactory memory?: ChatMemory observers?: Observer[] onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean> maxSteps?: number}const Cited = z.object({ value: z.string(), citation: z.string() })const Analysis = z.object({ parties: z.array(Cited), jurisdictionVenue: Cited, proceduralPosture: Cited, claims: z.array(Cited), defenses: z.array(Cited), keyDates: z.array(Cited), openDiscovery: z.array(Cited), deadlineRisks: z.array(z.string()),})const toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7const skill = { name: 'case-analyst', description: 'Extracts a typed, cited case analysis from a case file (never infers).', systemPrompt: `You analyse a case file (pleadings, exhibits, correspondence). Produce: parties + counsel,jurisdiction + venue, procedural posture, claims, defenses, key dates, open discovery requests.CITE the source document + page for EVERY datum (in citation). NEVER editorialise. When the record issilent on a field, set value to "${NOT_IN_RECORD}" and citation to "${NOT_IN_RECORD}" rather thaninferring. Surface every statute-of-limitations or filing-deadline risk in deadlineRisks.${UNTRUSTED_CONTENT_DIRECTIVE}Call submit_analysis exactly once with the structured fields + deadlineRisks. Stop.`, tools: ['submit_analysis'],}export function createCaseAnalystAgent(config: CaseAnalystConfig) { 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_analysis', description: 'Submit the case analysis. Call exactly once.', schema: Analysis, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition async function run(caseFile: string): Promise<CaseAnalysisResult> { if (!caseFile?.trim()) throw new Error('case analyst requires a non-empty case file') emit('analyse', 'start') const out = await invokeStructured({ adapter: config.adapter, tool: submit(), task: `CASE FILE:\n${fenceUntrustedContent(caseFile)}`, parse: (a) => Analysis.parse(a), skill, memory: config.memory, observers: config.observers, onConfirm: config.onConfirm, maxSteps: config.maxSteps ?? 3, }) const { deadlineRisks, ...analysis } = out emit('analyse', 'ok', `${deadlineRisks.length} deadline risk(s)`) return { analysis, deadlineRisks, requiresAttorneyReview: true } } return { name: 'legal-case-analyst', run, asHandle() { return { name: 'legal-case-analyst', 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-case-analyst', cases: [ { input: `Analyze this case file. COMPLAINT (Dkt. 1, filed 2023-03-14, N.D. Cal.): Acme Robotics Inc. ("Plaintiff", counsel: Jane Patel, Patel & Voss LLP) v. Nimbus Cloud Systems LLC ("Defendant", counsel: Marc Oye, Oye Legal). Claims: (1) breach of the Master Services Agreement dated 2021-06-01, (2) misappropriation of trade secrets under the DTSA. ANSWER (Dkt. 22, filed 2023-04-30): general denial, affirmative defenses of waiver and unclean hands. Plaintiff served First Set of Requests for Production (Ex. C, p. 4) on 2023-05-10.`, expected: (r: string) => /acme robotics/i.test(r) && /nimbus cloud/i.test(r) && /(N\.D\.\s*Cal|northern district|jurisdiction|venue)/i.test(r) && /(claim|breach|trade secret)/i.test(r), }, { input: `Produce a structured analysis. The accident occurred 2020-08-02. The personal-injury complaint was filed 2023-09-15 in California state court. California's statute of limitations for personal injury is two years. Parties: Lena Ortiz (plaintiff) v. Brightline Transit Authority (defendant). Identify any filing-deadline or limitations risk.`, expected: (r: string) => /(statute of limitations|limitations|deadline|time-barred|untimely)/i.test(r) && /(risk|2 year|two year)/i.test(r), }, { input: `Extract procedural posture and key dates from this docket. Dkt. 1 Complaint filed 2024-01-10. Dkt. 15 Motion to Dismiss filed 2024-02-20. Dkt. 31 Order granting in part / denying in part MTD, entered 2024-05-03. Dkt. 40 Amended Complaint filed 2024-05-24. Case is now in discovery; fact-discovery cutoff is 2024-11-30.`, expected: (r: string) => /(procedural posture|posture|discovery|motion to dismiss)/i.test(r) && /2024/.test(r), }, { input: `Analyze the parties, jurisdiction, and venue for this matter. The only document provided is a single email reading: "Hi team, please pull the file when you get a chance. Thanks." There are no pleadings, no caption, and no party names in the record.`, expected: (r: string) => /not in record/i.test(r), }, ],}Was this agent useful?
Your response helps us prioritize agent quality.