Building Tool-Using Agents in Python
Use for agent runtime architecture: context collection, prompt shape, tool contracts, parsing, permissioning, context reduction, memory, delegation.
Boundary
- Pair with
python for Python impl/toolchain.
- Pair with
arch when runtime must fit system architecture or SDD.
- Pair with
security for tool risk, approvals, trust boundaries, threat modeling.
- Pair with
quality for eval loops, regression checks, RCA after agent failures.
- Pair with
docs when deliverable is design/ops doc.
- Own harness + control loop. Do not absorb generic Python/arch/security rules.
Assets
Use assets over inline examples when implementing:
assets/project/pyproject.toml -- Python agent project setup
assets/project/main.py -- entrypoint
assets/project/agent.py -- agent construction/result typing
assets/project/tools.py -- tool registry + implementations
assets/project/session.py -- memory/transcript shaping
assets/project/tests/test_agent.py -- runtime tests
Mental Model
Agent = runtime harness around model. Model emits tool call or final answer; harness owns everything else.
Core loop:
- collect runtime context
- build stable prompt
- call model
- parse response as tool/final/retry
- validate + approve tool call
- execute + record result
- reduce context/memory
- stop on final or circuit breaker
Non-negotiables:
max_steps and max_attempts; no infinite loops.
- Malformed output becomes retry notice; no crash.
- Tool results are recorded before next model call.
- Runtime decides; model only suggests.
- Apparent model quality is often context quality.
Components
| Component |
Contract |
| Runtime context |
immutable snapshot + render() prompt text |
| Prompt shape |
stable prefix + volatile suffix; cache stable parts |
| Tools |
closed registry of typed Tool objects |
| Validation |
check tool name, args, paths, domains, mutation risk, recursion/delegation |
| Permissions |
ASK / AUTO / NEVER; risky tools require approval |
| Parser |
one response = one tool call, one final answer, or retry |
| Context reduction |
clip tool output, dedupe old reads, summarize old transcript |
| Sessions |
append-only transcript + small working memory |
| Delegation |
bounded read-only child agents with smaller step budget and depth limit |
Runtime Context
Default shape:
dataclass(frozen=True, slots=True)
pathlib.Path
- domain fields only
render() -> str
Coding context includes:
- repo root:
git rev-parse --show-toplevel
- branch, short status, recent commits
- selected anchor docs:
AGENTS*, README.md, manifests
- snippet limits for docs
Use assets for concrete code.
Prompt Shape
Split prompt:
- stable prefix: rules, tools, operating mode, runtime context
- volatile suffix: working memory, compact transcript, current user message
Build stable prefix once. Cache if provider supports it; otherwise keep text identical for auto-cache hits.
Prompt rules:
- force exact output contract
- tell model to use tools over guessing
- forbid invented tool results
- forbid repeated same tool call/args
- require one
<tool>...</tool> or one <final>...</final>
Tools
Expose closed set of named tools. Avoid arbitrary command execution by default.
Tool contract:
- frozen dataclass
name
description
- human-readable
signature
risk: safe or risky
run(args: dict[str, Any]) -> str
Common tools:
- coding:
list_files, read_file, search, write_file, patch_file, run_shell
- research:
web_search, fetch_url, read_file, save_note
- assistant:
list_tasks, create_task, complete_task, query_memory
- ops:
query_logs, query_metrics, list_alerts, run_runbook, page_oncall
- all domains:
delegate
Flat registry beats plugin hierarchy until real extension pressure exists.
Validation and Permissions
Validate before execution:
- tool exists
- required args present and non-empty
- arg types/shapes acceptable
- paths stay inside allowed roots
- network calls hit allowed domains
- risky mutations have approval
- recursion/delegation depth bounded
Filesystem invariant:
- resolve path
- compare against workspace root with
Path.relative_to
- reject escapes
Approval invariant:
ASK: ask human
AUTO: allow
NEVER: deny
Dispatcher owns validation, approval, execution, error capture. Return errors as strings the model can see and correct next turn.
Parsing
Response contract: exactly one tool call or one final answer.
Supported formats:
- JSON inside
<tool> for simple args
- XML attrs/body for multiline content
<final>...</final> for final answer
Parser returns tagged result:
("tool", payload)
("final", text)
("retry", message)
Retry message is recorded into next turn. This makes weak/malformed model output recoverable.
Context Reduction
Context hygiene keeps agents alive after turn 8.
Rules:
- cap every tool output; mark truncation
- preserve recent events at higher fidelity
- compress older events aggressively
- dedupe repeated old reads
- keep working memory small and current
- separate stable prefix from volatile history
Suggested limits:
- tool output: ~4k chars
- recent item: ~900 chars
- old item: ~180 chars
- full rendered history: ~12k chars
Sessions and Memory
Two layers:
- full transcript: durable append-only JSON for resume/compaction
- working memory: small mutable prompt state
Working memory tracks:
- current task
- recent files/entities
- decisions
- short notes from recent tool results
Use Path.write_text + JSON first. Add database only after persistence pressure is real.
Delegation
Delegation reduces main transcript noise and parallelizes bounded side work.
Constraints:
- child is read-only by default
- child approval policy =
NEVER
max_depth small, usually 1
- child
max_steps smaller than parent
- pass summary of parent history, not whole transcript
- expose narrower tool subset
Do not create SubAgent subclass unless responsibilities truly diverge. Same Agent class with stricter config is enough.
Full Agent Loop
Agent.ask should stay small:
- record user message
- build prompt from stable prefix + memory + rendered history
- call model
- parse result
- on final: record + return
- on retry: record retry notice + continue
- on tool: dispatch, record tool result, update memory
- stop at step limit
Use ModelClient Protocol with one method:
class ModelClient(Protocol):
def complete(self, prompt: str, max_new_tokens: int = 512) -> str: ...
Test loop with FakeModelClient returning canned outputs.
Specialization Recipes
| Agent |
Context |
First Tools |
Memory |
| Coding |
WorkspaceContext, git state, anchor docs |
files, search, patch/write, shell, delegate |
task, last files, tool notes |
| Research |
question, deadline, constraints |
search, fetch, read, save/list notes, delegate |
sources, hypotheses, subquestions |
| Personal assistant |
user, time zone, calendar/tasks handles |
tasks, memory, notes, calendar, web |
goal, entities, decisions |
| Ops/support |
connected systems, on-call, active incident |
logs, metrics, alerts, runbook, page, status |
incident state, actions, evidence |
Risky prod tools use approval.
Project Layout
Generic runtime:
src/agent_runtime/
├── agent.py
├── context.py
├── prompt.py
├── parser.py
├── compaction.py
├── permissions.py
├── session.py
├── models/
└── tools/
tests/
Specializations stay thin:
src/coding_agent/
├── workspace.py
├── tools.py
└── cli.py
Runtime shared. Domain layer chooses tools/context.
Build Order
Build v1:
- runtime/specialized context
- JSON
SessionStore
WorkingMemory
- parser: tool/final/retry/empty
- core domain tools
safe_path if touching files
- approval policy
- recency-weighted history
- one read-only delegated child
- fake model + pytest tests
Defer:
- persistent agent teams
- background tasks
- per-task worktrees
- web/Slack/Discord bridges
- automatic risk classification
- directory skill loading
- full MCP server/OAuth
- streaming tool output
- multi-model routing
- planner/executor split
- long-horizon orchestration
References
1---2name: building-agents3description: Building tool-using LLM agents in Python -- runtime context, prompt shape, tools, validation, parsing, context reduction, memory, delegation. Load when designing or implementing agents, ReAct loops, or multi-agent systems.4---56# Building Tool-Using Agents in Python78Use for agent runtime architecture: context collection, prompt shape, tool contracts, parsing, permissioning, context reduction, memory, delegation.910## Boundary1112- Pair with `python` for Python impl/toolchain.13- Pair with `arch` when runtime must fit system architecture or SDD.14- Pair with `security` for tool risk, approvals, trust boundaries, threat modeling.15- Pair with `quality` for eval loops, regression checks, RCA after agent failures.16- Pair with `docs` when deliverable is design/ops doc.17- Own harness + control loop. Do not absorb generic Python/arch/security rules.1819## Assets2021Use assets over inline examples when implementing:2223- `assets/project/pyproject.toml` -- Python agent project setup24- `assets/project/main.py` -- entrypoint25- `assets/project/agent.py` -- agent construction/result typing26- `assets/project/tools.py` -- tool registry + implementations27- `assets/project/session.py` -- memory/transcript shaping28- `assets/project/tests/test_agent.py` -- runtime tests2930## Mental Model3132Agent = runtime harness around model. Model emits tool call or final answer; harness owns everything else.3334Core loop:35361. collect runtime context372. build stable prompt383. call model394. parse response as tool/final/retry405. validate + approve tool call416. execute + record result427. reduce context/memory438. stop on final or circuit breaker4445Non-negotiables:4647- `max_steps` and `max_attempts`; no infinite loops.48- Malformed output becomes retry notice; no crash.49- Tool results are recorded before next model call.50- Runtime decides; model only suggests.51- Apparent model quality is often context quality.5253## Components5455| Component | Contract |56| ----------------- | -------------------------------------------------------------------------- |57| Runtime context | immutable snapshot + `render()` prompt text |58| Prompt shape | stable prefix + volatile suffix; cache stable parts |59| Tools | closed registry of typed `Tool` objects |60| Validation | check tool name, args, paths, domains, mutation risk, recursion/delegation |61| Permissions | `ASK` / `AUTO` / `NEVER`; risky tools require approval |62| Parser | one response = one tool call, one final answer, or retry |63| Context reduction | clip tool output, dedupe old reads, summarize old transcript |64| Sessions | append-only transcript + small working memory |65| Delegation | bounded read-only child agents with smaller step budget and depth limit |6667## Runtime Context6869Default shape:7071- `dataclass(frozen=True, slots=True)`72- `pathlib.Path`73- domain fields only74- `render() -> str`7576Coding context includes:7778- repo root: `git rev-parse --show-toplevel`79- branch, short status, recent commits80- selected anchor docs: `AGENTS*`, `README.md`, manifests81- snippet limits for docs8283Use assets for concrete code.8485## Prompt Shape8687Split prompt:8889- stable prefix: rules, tools, operating mode, runtime context90- volatile suffix: working memory, compact transcript, current user message9192Build stable prefix once. Cache if provider supports it; otherwise keep text identical for auto-cache hits.9394Prompt rules:9596- force exact output contract97- tell model to use tools over guessing98- forbid invented tool results99- forbid repeated same tool call/args100- require one `<tool>...</tool>` or one `<final>...</final>`101102## Tools103104Expose closed set of named tools. Avoid arbitrary command execution by default.105106Tool contract:107108- frozen dataclass109- `name`110- `description`111- human-readable `signature`112- `risk`: `safe` or `risky`113- `run(args: dict[str, Any]) -> str`114115Common tools:116117- coding: `list_files`, `read_file`, `search`, `write_file`, `patch_file`, `run_shell`118- research: `web_search`, `fetch_url`, `read_file`, `save_note`119- assistant: `list_tasks`, `create_task`, `complete_task`, `query_memory`120- ops: `query_logs`, `query_metrics`, `list_alerts`, `run_runbook`, `page_oncall`121- all domains: `delegate`122123Flat registry beats plugin hierarchy until real extension pressure exists.124125## Validation and Permissions126127Validate before execution:128129- tool exists130- required args present and non-empty131- arg types/shapes acceptable132- paths stay inside allowed roots133- network calls hit allowed domains134- risky mutations have approval135- recursion/delegation depth bounded136137Filesystem invariant:138139- resolve path140- compare against workspace root with `Path.relative_to`141- reject escapes142143Approval invariant:144145- `ASK`: ask human146- `AUTO`: allow147- `NEVER`: deny148149Dispatcher owns validation, approval, execution, error capture. Return errors as strings the model can see and correct next turn.150151## Parsing152153Response contract: exactly one tool call or one final answer.154155Supported formats:156157- JSON inside `<tool>` for simple args158- XML attrs/body for multiline content159- `<final>...</final>` for final answer160161Parser returns tagged result:162163- `("tool", payload)`164- `("final", text)`165- `("retry", message)`166167Retry message is recorded into next turn. This makes weak/malformed model output recoverable.168169## Context Reduction170171Context hygiene keeps agents alive after turn 8.172173Rules:174175- cap every tool output; mark truncation176- preserve recent events at higher fidelity177- compress older events aggressively178- dedupe repeated old reads179- keep working memory small and current180- separate stable prefix from volatile history181182Suggested limits:183184- tool output: ~4k chars185- recent item: ~900 chars186- old item: ~180 chars187- full rendered history: ~12k chars188189## Sessions and Memory190191Two layers:192193- full transcript: durable append-only JSON for resume/compaction194- working memory: small mutable prompt state195196Working memory tracks:197198- current task199- recent files/entities200- decisions201- short notes from recent tool results202203Use `Path.write_text` + JSON first. Add database only after persistence pressure is real.204205## Delegation206207Delegation reduces main transcript noise and parallelizes bounded side work.208209Constraints:210211- child is read-only by default212- child approval policy = `NEVER`213- `max_depth` small, usually `1`214- child `max_steps` smaller than parent215- pass summary of parent history, not whole transcript216- expose narrower tool subset217218Do not create `SubAgent` subclass unless responsibilities truly diverge. Same `Agent` class with stricter config is enough.219220## Full Agent Loop221222`Agent.ask` should stay small:2232241. record user message2252. build prompt from stable prefix + memory + rendered history2263. call model2274. parse result2285. on final: record + return2296. on retry: record retry notice + continue2307. on tool: dispatch, record tool result, update memory2318. stop at step limit232233Use `ModelClient` `Protocol` with one method:234235```python236class ModelClient(Protocol):237 def complete(self, prompt: str, max_new_tokens: int = 512) -> str: ...238```239240Test loop with `FakeModelClient` returning canned outputs.241242## Specialization Recipes243244| Agent | Context | First Tools | Memory |245| ------------------ | ------------------------------------------- | ---------------------------------------------- | --------------------------------- |246| Coding | `WorkspaceContext`, git state, anchor docs | files, search, patch/write, shell, delegate | task, last files, tool notes |247| Research | question, deadline, constraints | search, fetch, read, save/list notes, delegate | sources, hypotheses, subquestions |248| Personal assistant | user, time zone, calendar/tasks handles | tasks, memory, notes, calendar, web | goal, entities, decisions |249| Ops/support | connected systems, on-call, active incident | logs, metrics, alerts, runbook, page, status | incident state, actions, evidence |250251Risky prod tools use approval.252253## Project Layout254255Generic runtime:256257```text258src/agent_runtime/259├── agent.py260├── context.py261├── prompt.py262├── parser.py263├── compaction.py264├── permissions.py265├── session.py266├── models/267└── tools/268tests/269```270271Specializations stay thin:272273```text274src/coding_agent/275├── workspace.py276├── tools.py277└── cli.py278```279280Runtime shared. Domain layer chooses tools/context.281282## Build Order283284Build v1:285286- runtime/specialized context287- JSON `SessionStore`288- `WorkingMemory`289- parser: tool/final/retry/empty290- core domain tools291- `safe_path` if touching files292- approval policy293- recency-weighted history294- one read-only delegated child295- fake model + pytest tests296297Defer:298299- persistent agent teams300- background tasks301- per-task worktrees302- web/Slack/Discord bridges303- automatic risk classification304- directory skill loading305- full MCP server/OAuth306- streaming tool output307- multi-model routing308- planner/executor split309- long-horizon orchestration310311## References312313- Raschka, _Components of Coding Agent_: <https://magazine.sebastianraschka.com/p/components-of-a-coding-agent>314- `rasbt/mini-coding-agent`: <https://github.com/rasbt/mini-coding-agent>315- `badlogic/pi-mono`: <https://github.com/badlogic/pi-mono/tree/main>316- `sanbuphy/learn-coding-agent`: <https://github.com/sanbuphy/learn-coding-agent>317- `Leonxlnx/agentic-ai-prompt-research`: <https://github.com/Leonxlnx/agentic-ai-prompt-research>318- Zen of Python: `python -c "import this"`