{"slug": "ai-agent-autonomy-levels-a-small-typescript-approval-gate", "title": "AI Agent Autonomy Levels: A Small TypeScript Approval Gate", "summary": "A developer published a TypeScript approval-gate pattern that assigns autonomy levels 0 through 4 to individual tools rather than whole agents, classifying each tool as read, write or irreversible and routing calls to allow, ask or deny accordingly. The reference implementation, run via an AUTONOMY environment variable against the Anthropic SDK, lets reads run freely while irreversible actions like deletion require human approval below level 4. The author argues autonomy is a policy the code enforces, not a trait of the model.", "body_md": "An agent with a delete tool and a loose prompt can read “clean up my old entries” as permission. Nothing malfunctions. The message allows that reading and the tool allows the action. What's missing is a rule about when the tool may run.\n\nThis post continues my series on building agents. The previous one covered [MCP](https://zahidhasantonmoy.vercel.app/blog/what-is-mcp-model-context-protocol).\n\nAn agent's autonomy is how much it may do on its own before a human has to approve, review or step in. It is a policy your code enforces, not a trait of the model.\n\nNo standard defines these levels. This is my own working scale.\n\n| Level | Meaning | \n|---|---|\n| 0 | No tools. A plain chatbot | \n| 1 | Read-only tools | \n| 2 | Writes, each approved by a human | \n| 3 | Routine writes run alone inside limits, irreversible actions still ask | \n| 4 | Full autonomy, rarely justified | \n\nA level belongs to a tool, not to the whole agent. The same agent can run reads at Level 4 and deletes at Level 2.\n\nA **read** tool changes nothing. A **write** tool changes something you can undo. An **irreversible** tool can't be taken back: deleting data, sending a message, moving money. The class comes from the cost of a wrong call, not the tool's name.\n\n``` php\nflowchart TD\n    A[Model requests a tool] --> B{Tool risk class}\n    B -->|read| C[Run it]\n    B -->|write| D{Autonomy level}\n    B -->|irreversible| E{Autonomy level}\n    D -->|1| F[Refuse]\n    D -->|2| G[Ask a human]\n    D -->|3 or 4| C\n    E -->|1| F\n    E -->|2 or 3| G\n    E -->|4| C\n    G -->|approved| C\n    G -->|declined| I[Send an error result to the model]\n    F --> I\n    C --> H[Send the result to the model]\n```\n\nRun it with `AUTONOMY=2 npx tsx agent/autonomy.ts` and change the value to compare levels.\n\n``` python\n// agent/autonomy.ts\nimport Anthropic from '@anthropic-ai/sdk';\nimport { createInterface } from 'node:readline/promises';\n\nconst client = new Anthropic();\nconst rl = createInterface({ input: process.stdin, output: process.stdout });\n\ntype Risk = 'read' | 'write' | 'irreversible';\ntype Decision = 'allow' | 'ask' | 'deny';\n\nconst LEVEL = Number(process.env.AUTONOMY ?? 2);\n\nconst expenses = [{ id: 1, item: 'tea', amount: 40 }];\nlet nextId = 2;\n\nconst risks: Record<string, Risk> = {\n  get_total: 'read',\n  add_expense: 'write',\n  delete_expense: 'irreversible',\n};\n\nfunction decide(level: number, risk: Risk): Decision {\n  if (risk === 'read') return 'allow';\n  if (level <= 1) return 'deny';\n  if (level >= 4) return 'allow';\n  if (risk === 'write' && level === 3) return 'allow';\n  return 'ask';\n}\n\nconst tools: Anthropic.Tool[] = [\n  {\n    name: 'get_total',\n    description: 'Return the exact sum of every expense recorded so far. Use this whenever the user asks for a total.',\n    input_schema: { type: 'object', properties: {} },\n  },\n  {\n    name: 'add_expense',\n    description: 'Record one expense. Use it whenever the user says they spent money.',\n    input_schema: {\n      type: 'object',\n      properties: { item: { type: 'string' }, amount: { type: 'number' } },\n      required: ['item', 'amount'],\n    },\n  },\n  {\n    name: 'delete_expense',\n    description: 'Permanently delete one expense by its id. Use it only when the user names the expense to remove.',\n    input_schema: {\n      type: 'object',\n      properties: { id: { type: 'number' } },\n      required: ['id'],\n    },\n  },\n];\n\nfunction execute(name: string, input: Record<string, unknown>): unknown {\n  if (name === 'get_total') return { total: expenses.reduce((sum, e) => sum + e.amount, 0) };\n  if (name === 'add_expense') {\n    const expense = { id: nextId++, item: String(input.item), amount: Number(input.amount) };\n    expenses.push(expense);\n    return expense;\n  }\n  if (name === 'delete_expense') {\n    const index = expenses.findIndex((e) => e.id === Number(input.id));\n    if (index === -1) return { error: 'no such expense' };\n    expenses.splice(index, 1);\n    return { ok: true };\n  }\n  return { error: 'unknown tool' };\n}\n\nasync function approve(name: string, input: Record<string, unknown>): Promise<boolean> {\n  const answer = await rl.question(`Allow ${name} ${JSON.stringify(input)}? (y/n) `);\n  return answer.trim().toLowerCase() === 'y';\n}\n\nasync function guarded(name: string, input: Record<string, unknown>): Promise<unknown> {\n  const decision = decide(LEVEL, risks[name] ?? 'irreversible');\n  console.log(`[${decision}] ${name}`);\n  if (decision === 'deny') return { error: 'not permitted at this autonomy level' };\n  if (decision === 'ask' && !(await approve(name, input))) return { error: 'the user declined this action' };\n  return execute(name, input);\n}\n\nasync function runAgent(userText: string): Promise<string> {\n  const messages: Anthropic.MessageParam[] = [{ role: 'user', content: userText }];\n\n  for (let step = 0; step < 6; step++) {\n    const response = await client.messages.create({\n      model: 'claude-sonnet-5',\n      max_tokens: 500,\n      tools,\n      messages,\n    });\n\n    messages.push({ role: 'assistant', content: response.content });\n\n    if (response.stop_reason !== 'tool_use') {\n      return response.content.flatMap((b) => (b.type === 'text' ? [b.text] : [])).join('');\n    }\n\n    const results: Anthropic.ToolResultBlockParam[] = [];\n    for (const block of response.content) {\n      if (block.type === 'tool_use') {\n        const output = await guarded(block.name, block.input as Record<string, unknown>);\n        results.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(output) });\n      }\n    }\n    messages.push({ role: 'user', content: results });\n  }\n\n  return 'Stopped: step limit reached';\n}\n\nrunAgent('Add 300 for groceries, then delete expense 1.')\n  .then(console.log)\n  .finally(() => rl.close());\n```\n\nA tool missing from `risks` falls back to `irreversible`, so anything you forget to classify gets the strictest treatment. Denials and declined approvals go back as error results, not exceptions, so the model can explain what happened.\n\nThe system prompt template in my prompt-engineering post says “Never delete or change a past expense unless the user explicitly asks to.” The agent in that post has no delete tool. The line guards an action the agent can't take. The tracker from the components post writes with no approval and only a six-step cap, which was a Level 3 decision I never named. That is why the gate here lives in code next to `execute`, with the prompt line as a second layer.\n\nI write each post in English and Bangla. The full version is on my blog: [How Autonomous Should an AI Agent Be?](https://zahidhasantonmoy.vercel.app/blog/ai-agent-autonomy-levels)\n\nWhich of your agent's tools would you never let run without asking?", "url": "https://wpnews.pro/news/ai-agent-autonomy-levels-a-small-typescript-approval-gate", "canonical_source": "https://dev.to/zahidhasantonmoy/ai-agent-autonomy-levels-a-small-typescript-approval-gate-138c", "published_at": "2026-10-17 15:55:00+00:00", "updated_at": "2026-10-17 16:03:46.781492+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "developer-tools"], "entities": ["Anthropic", "TypeScript", "Model Context Protocol"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/ai-agent-autonomy-levels-a-small-typescript-approval-gate", "markdown": "https://wpnews.pro/news/ai-agent-autonomy-levels-a-small-typescript-approval-gate.md", "text": "https://wpnews.pro/news/ai-agent-autonomy-levels-a-small-typescript-approval-gate.txt", "jsonld": "https://wpnews.pro/news/ai-agent-autonomy-levels-a-small-typescript-approval-gate.jsonld"}}