{"id":"legal-case-analyst","title":"Case Analyst","description":"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); SOL/filing-deadline risks surfaced separately. Always a draft for the attorney.","category":"legal","version":"2.0.0","source":"AKOS","license":"MIT","tags":["legal","structured-output","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":"legal-case-analyst","description":"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); SOL/filing-deadline risks surfaced separately. Always a draft for the attorney.","systemPrompt":"You analyse a case file (pleadings, exhibits, correspondence). Produce: parties + counsel,\njurisdiction + venue, procedural posture, claims, defenses, key dates, open discovery requests.\n\nCITE the source document + page for EVERY datum (in citation). NEVER editorialise. When the record is\nsilent on a field, set value to \"${NOT_IN_RECORD}\" and citation to \"${NOT_IN_RECORD}\" rather than\ninferring. Surface every statute-of-limitations or filing-deadline risk in deadlineRisks.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_analysis exactly once with the structured fields + deadlineRisks. Stop."},"flow":null,"a2a":{"id":"io.agentskit.registry.legal-case-analyst","name":"Case Analyst","description":"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); SOL/filing-deadline risks surfaced separately. Always a draft for the attorney.","version":"2.0.0","homepage":"https://registry.agentskit.io","skills":[{"name":"legal-case-analyst","description":"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); SOL/filing-deadline risks surfaced separately. Always a draft for the attorney.","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 * Case Analyst — extracts a TYPED structured analysis from a case file (parties, venue,\n * posture, claims, defenses, key dates, open discovery). Every datum cites its source\n * document + page; gaps are \"not in record\" (never inferred); statute-of-limitations /\n * filing-deadline risks are surfaced separately at the top. Always a draft.\n *\n * ```ts\n * const { analysis, deadlineRisks } = await createCaseAnalystAgent({ adapter }).run(caseFile)\n * ```\n */\n\nconst NOT_IN_RECORD = 'not in record'\n\nexport interface CitedFact {\n  value: string\n  /** Source document + page, or \"not in record\". */\n  citation: string\n}\n\nexport interface CaseAnalysis {\n  parties: CitedFact[]\n  jurisdictionVenue: CitedFact\n  proceduralPosture: CitedFact\n  claims: CitedFact[]\n  defenses: CitedFact[]\n  keyDates: CitedFact[]\n  openDiscovery: CitedFact[]\n}\n\nexport interface CaseAnalysisResult {\n  analysis: CaseAnalysis\n  /** SOL / filing-deadline risks, surfaced at the top for the attorney. */\n  deadlineRisks: string[]\n  /** Always true — a draft for the supervising attorney. */\n  requiresAttorneyReview: boolean\n}\n\nexport interface CaseAnalystConfig {\n  adapter: AdapterFactory\n  memory?: ChatMemory\n  observers?: Observer[]\n  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>\n  maxSteps?: number\n}\n\nconst Cited = z.object({ value: z.string(), citation: z.string() })\nconst Analysis = z.object({\n  parties: z.array(Cited),\n  jurisdictionVenue: Cited,\n  proceduralPosture: Cited,\n  claims: z.array(Cited),\n  defenses: z.array(Cited),\n  keyDates: z.array(Cited),\n  openDiscovery: z.array(Cited),\n  deadlineRisks: z.array(z.string()),\n})\nconst toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7\n\nconst skill = {\n  name: 'case-analyst',\n  description: 'Extracts a typed, cited case analysis from a case file (never infers).',\n  systemPrompt: `You analyse a case file (pleadings, exhibits, correspondence). Produce: parties + counsel,\njurisdiction + venue, procedural posture, claims, defenses, key dates, open discovery requests.\n\nCITE the source document + page for EVERY datum (in citation). NEVER editorialise. When the record is\nsilent on a field, set value to \"${NOT_IN_RECORD}\" and citation to \"${NOT_IN_RECORD}\" rather than\ninferring. Surface every statute-of-limitations or filing-deadline risk in deadlineRisks.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_analysis exactly once with the structured fields + deadlineRisks. Stop.`,\n  tools: ['submit_analysis'],\n}\n\nexport function createCaseAnalystAgent(config: CaseAnalystConfig) {\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_analysis', description: 'Submit the case analysis. Call exactly once.', schema: Analysis, toJsonSchema: toJson, async execute() { return 'recorded' } }) as ToolDefinition\n\n  async function run(caseFile: string): Promise<CaseAnalysisResult> {\n    if (!caseFile?.trim()) throw new Error('case analyst requires a non-empty case file')\n    emit('analyse', 'start')\n    const out = await invokeStructured({\n      adapter: config.adapter,\n      tool: submit(),\n      task: `CASE FILE:\\n${fenceUntrustedContent(caseFile)}`,\n      parse: (a) => Analysis.parse(a),\n      skill,\n      memory: config.memory,\n      observers: config.observers,\n      onConfirm: config.onConfirm,\n      maxSteps: config.maxSteps ?? 3,\n    })\n    const { deadlineRisks, ...analysis } = out\n    emit('analyse', 'ok', `${deadlineRisks.length} deadline risk(s)`)\n    return { analysis, deadlineRisks, requiresAttorneyReview: true }\n  }\n\n  return {\n    name: 'legal-case-analyst',\n    run,\n    asHandle() {\n      return { name: 'legal-case-analyst', run: async (task: string) => JSON.stringify(await run(task)) }\n    },\n  }\n}\n"},{"path":"README.md","content":"# Case Analyst\n\nExtracts a **typed, cited analysis** from a case file — every datum cites its source, gaps are \"not in record\", deadline risks surfaced at the top.\n\n```bash\nnpx agentskit add legal-case-analyst\n```\n\n```ts\nimport { anthropic } from '@agentskit/adapters'\nimport { createCaseAnalystAgent } from './agents/legal-case-analyst/agent'\n\nconst r = await createCaseAnalystAgent({\n  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),\n}).run(caseFile)\n// → { analysis: { parties[], jurisdictionVenue, proceduralPosture, claims[], defenses[], keyDates[], openDiscovery[] }, deadlineRisks[], requiresAttorneyReview }\n```\n\n- **Typed + cited** — `invokeStructured` + zod; every field is a `{ value, citation }` pair (source document + page).\n- **Never infers** — a field the record is silent on is `\"not in record\"` for both value and citation, never guessed.\n- **Deadline-first** — statute-of-limitations / filing-deadline risks are pulled into `deadlineRisks` for the attorney. Always a draft; untrusted file text is **fenced**.\n\n`run(caseFile)` → `CaseAnalysisResult`. `asHandle()` is JSON-out. See [composing agents](../../COMPOSING.md). Feeds [`legal-doc-drafter`](../legal-doc-drafter) and [`legal-case-summariser`](../legal-case-summariser).\n"},{"path":"eval.ts","content":"import type { EvalSuite } from '@agentskit/eval'\n\nexport const suite: EvalSuite = {\n  name: 'legal-case-analyst',\n  cases: [\n    {\n      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.`,\n      expected: (r: string) =>\n        /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),\n    },\n    {\n      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.`,\n      expected: (r: string) =>\n        /(statute of limitations|limitations|deadline|time-barred|untimely)/i.test(r) && /(risk|2 year|two year)/i.test(r),\n    },\n    {\n      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.`,\n      expected: (r: string) =>\n        /(procedural posture|posture|discovery|motion to dismiss)/i.test(r) && /2024/.test(r),\n    },\n    {\n      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.`,\n      expected: (r: string) =>\n        /not in record/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 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.","strengths":["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."],"notes":[]}}