Build a Contract Review Agent
Use A3S Code structured output to build an autonomous contract review agent that produces deterministic JSON reports.
Build a Contract Review Agent
This tutorial builds a contract review agent that autonomously reads and
analyzes contracts, then outputs a deterministic JSON report using
generate_object.
The key insight: the agent reasons freely (reads files, analyzes clauses), but outputs deterministically (schema-validated JSON). This separates "understanding" from "reporting".
Architecture
Contract file→ Agent reads file (autonomous, uses read/bash tools)→ Agent analyzes clauses (LLM reasoning)→ generate_object (deterministic structured output)→ Schema-validated JSON report
Define the Review Schema
const CONTRACT_REVIEW_SCHEMA = {type: 'object',required: ['summary', 'risk_level', 'parties', 'key_terms', 'issues', 'recommendations'],properties: {summary: { type: 'string', minLength: 20 },risk_level: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },parties: {type: 'array',minItems: 2,items: {type: 'object',required: ['name', 'role'],properties: {name: { type: 'string' },role: { type: 'string' },},},},key_terms: {type: 'object',properties: {contract_amount: { type: 'string' },duration: { type: 'string' },payment_terms: { type: 'string' },termination_clause: { type: 'string' },liability_cap: { type: 'string' },},},issues: {type: 'array',items: {type: 'object',required: ['severity', 'clause', 'description'],properties: {severity: { type: 'string', enum: ['info', 'warning', 'critical'] },clause: { type: 'string' },description: { type: 'string' },suggestion: { type: 'string' },},},},recommendations: {type: 'array',items: { type: 'string' },minItems: 1,},},};
Approach 1: Single-Pass (Agent-Driven)
Let the agent autonomously decide how to review and when to output:
import { Agent } from '@a3s-lab/code';const agent = await Agent.create('config.acl');const session = agent.session('.', {permissionPolicy: { defaultDecision: 'allow' },maxToolRounds: 20,});const result = await session.send(`You are a professional contract review lawyer. Complete these steps:1. Read the contract file: contracts/service-agreement.txt2. Identify all parties, key terms, and potential risks3. Analyze for: unfair clauses, ambiguous language, missing protections,unbalanced liability, compliance issues4. Use the generate_object tool to output your findings as a structured report.Use schema_name "contract_review" with this schema:${JSON.stringify(CONTRACT_REVIEW_SCHEMA, null, 2)}Your prompt to generate_object should contain your complete analysis.`);
Approach 2: Two-Phase (Strict Determinism)
Separate reasoning from output for maximum control:
async function reviewContract(contractPath: string) {const session = agent.session('.', {permissionPolicy: { defaultDecision: 'allow' },});// Phase 1: Autonomous analysis (agent uses any tools it needs)const analysis = await session.send(`You are a contract review lawyer. Read ${contractPath} and providea thorough analysis covering:- All parties and their roles- Key financial terms and obligations- Duration and termination conditions- Risk factors and problematic clauses- Missing standard protections- Compliance concerns`);// Phase 2: Deterministic structured output (bypasses LLM decision-making)const structured = await session.tool('generate_object', {schema: CONTRACT_REVIEW_SCHEMA,schema_name: 'contract_review',prompt: `Based on this contract review analysis, produce a structured report:\n\n${analysis.text}`,mode: 'tool',max_repair_attempts: 3,});if (structured.exitCode !== 0) {throw new Error(`Structured output failed: ${structured.output}`);}return JSON.parse(structured.output).object;}const report = await reviewContract('contracts/service-agreement.txt');console.log(`Risk: ${report.risk_level}`);console.log(`Issues: ${report.issues.length}`);
Approach 3: Streaming with Progress
Show real-time progress as the report is generated:
async function reviewContractStreaming(contractPath: string) {const session = agent.session('.', {permissionPolicy: { defaultDecision: 'allow' },});const stream = await session.stream(`Review the contract at ${contractPath}. Analyze all clauses for risks,then use generate_object to output a structured report with schema_name"contract_review" and this schema: ${JSON.stringify(CONTRACT_REVIEW_SCHEMA)}`);let finalReport = null;for await (const ev of stream) {switch (ev.type) {case 'text_delta':// Agent's reasoning process (optional: show to user)process.stdout.write(ev.text);break;case 'tool_output_delta':if (ev.toolName === 'generate_object') {const { object_partial } = JSON.parse(ev.text);renderPartialReport(object_partial);}break;case 'tool_end':if (ev.toolName === 'generate_object') {finalReport = JSON.parse(ev.toolOutput).object;}break;}}return finalReport;}
Python Version
from a3s_code import Agent, SessionOptions, PermissionPolicyimport jsondef review_contract(contract_path: str) -> dict:agent = Agent.create(open('config.acl').read())opts = SessionOptions()opts.permission_policy = PermissionPolicy(default_decision="allow")session = agent.session('.', opts)# Phase 1: Autonomous analysisanalysis = session.send(f"""You are a contract review lawyer. Read {contract_path} and analyze:- Parties and roles- Key terms (amount, duration, payment, termination)- Risk factors and problematic clauses- Missing protections""")# Phase 2: Deterministic outputresult = session.tool("generate_object", {"schema": CONTRACT_REVIEW_SCHEMA,"schema_name": "contract_review","prompt": f"Based on this analysis, produce a structured report:\n\n{analysis.text}","mode": "tool","max_repair_attempts": 3,})assert result.exit_code == 0, f"Failed: {result.output}"return json.loads(result.output)["object"]
Batch Processing
Review multiple contracts and aggregate results:
import * as fs from 'fs';async function batchReview(contractDir: string) {const files = await session.glob(`${contractDir}/*.txt`);const reports = [];for (const file of files) {const report = await reviewContract(file);reports.push({ file, ...report });}// Aggregate risk summaryconst critical = reports.filter(r => r.risk_level === 'critical');const high = reports.filter(r => r.risk_level === 'high');console.log(`Reviewed ${reports.length} contracts`);console.log(`Critical risk: ${critical.length}`);console.log(`High risk: ${high.length}`);return reports;}
Why This Works
The determinism guarantee comes from three layers:
- Tool-call mode: The LLM is forced to call a synthetic tool whose parameters ARE the schema. It cannot output free-form text.
- Schema validation: The output is validated against the full JSON Schema (type checks, required fields, enums, ranges, patterns).
- Repair loop: If validation fails, errors are fed back to the model for
automatic correction (up to
max_repair_attemptstimes).
The agent's reasoning (Phase 1) is non-deterministic — it may read different parts of the contract, use different tools, take different paths. But the final output (Phase 2) is always a valid JSON object matching your schema.