Single tool-using agent
Build
Function calling as the core agent primitive
Sprint 0 built the loop skeleton. Sprint 1 fills in the most important part: tool use. The Anthropic tool-use docs define the primitive clearly: you describe a function to the model (name, description, JSON schema for its inputs), the model returns a structured tool_use block when it decides to call it, your application executes the function, and you return the result as a tool_result message. The model never runs code itself — it produces a structured call and waits. Your application is always in the execution seat.
That separation is not incidental. It is the entire design. The model contributes judgment: which tool to call and with what arguments. The tool contributes correctness: it runs deterministically and returns a reliable result. Schema validation is the contract between the two halves — the model must produce an argument object that matches the tool's declared schema, and your application should enforce that before execution.
What Toolformer showed about learned tool use
Before function calling was an API feature, Toolformer (Schick et al., 2023) demonstrated that language models could learn when to invoke external APIs — calculator, search, calendar, Q&A — by self-supervising on whether a call improved next-token prediction. The key result: a much smaller Toolformer model outperformed a far larger baseline on tasks that required arithmetic or factual retrieval, because those tasks were redirected to tools that execute correctly by construction. The model stopped trying to memorize facts or compute sums, and started deciding when to delegate. That delegation pattern — model decides, tool executes — is the same primitive your agent uses now, just with an explicit schema instead of learned API syntax.
Error recovery: what an agent does when a tool fails
Tool calls fail. The tool may raise an exception, return malformed output, or time out. A naive loop crashes; a production agent recovers. The recovery pattern from Building Effective Agents applies here: treat failures as observations. When a tool call throws, catch the exception, format it as an error observation, and feed it back into the agent's context on the next step — exactly the same way a successful result is fed back. The model can then decide to retry with corrected arguments, call a fallback tool, or escalate. What the model must not do is silently swallow the error and hallucinate an answer.
Concretely, this means your retry loop has three components: a max-retry budget (a deterministic ceiling — the loop cannot run forever), a catch block that captures the exception and formats it as a string observation, and a re-entry point that passes that observation back to decide() on the next iteration. The model sees the error as context and can adjust its next tool call accordingly.
Never ask an LLM to do something a deterministic function can do reliably. Arithmetic, string reversal, date math, regex matching, schema validation — these belong in tools, not prompts. The model's job is to decide which tool to call and with what argument. The tool's job is to execute correctly. Keep that boundary explicit in every agent you build.
Deterministic vs. judgment calls
Not every decision in an agent loop is a judgment call. Some decisions are deterministic and should never be delegated to the model. Whether to retry after a timeout (yes, up to N times) is deterministic. Which counter to increment (the one named in the schema) is deterministic. Whether the output of a schema validator is valid (it either matches or it does not) is deterministic. These are not places to ask the model for its opinion — they are places to write code.
Judgment calls are what remain after you have pulled out everything deterministic: choosing between two valid tools when the goal is ambiguous, deciding whether a partial result is good enough to return, rephrasing a failed query. The model earns its place in the loop precisely because these decisions require understanding the goal, not just executing a rule. The discipline of this sprint is knowing which type of decision you are making at each line of code.
The Anthropic tool-use docs specify that each tool definition includes an input_schema — a JSON Schema object describing the expected arguments. When your application receives a tool_use block from the model, validating its input against that schema before executing the tool is the correct posture. Validation catches malformed calls early, produces clear error messages that can be fed back as observations, and prevents a model hallucination from propagating into a side-effectful function call. In the reference agent below, schema validation is done with jsonschema.validate() before every tool dispatch — a deliberate design choice, not an afterthought.
What to build this sprint
The reference implementation at projects/sprint-1/agent.py is a complete, runnable example. It has two tools, schema validation on every call, and a retry-on-error path that exercises a deliberately-failing call. Read every line before writing your own.
-
Run the reference agent. From the repo root:
Three demos, one per tool and one showing recovery from a schema-validation failure.bash
$ python agentic-engineering/projects/sprint-1/agent.py --- demo: calculator tool --- step 1 | tool=calc arg={"expression": "12 * (3 + 7)"} obs: RESULT: 120 answer: 120 --- demo: reverse tool --- step 1 | tool=reverse arg={"text": "agentic"} obs: RESULT: citnega answer: citnega --- demo: error recovery (bad schema arg) --- step 1 | tool=calc arg={"expression": 999} obs: ERROR[schema]: 999 is not of type 'string' step 2 | tool=calc arg={"expression": "999"} obs: RESULT: 999 answer: 999 All demos passed.
-
Trace the retry path. Find the
catchblock inagent.pythat formats a schema error as an observation. Add aprint()statement inside it that logs the raw validation error object. Run the demo again. Notice that the error string is the same text the model would receive as context on the next step. -
Add a third tool. Write a
word_count(text: str) -> inttool with a schema. Add it to the tool registry. Extenddecide()so that goals containing "count" route to it. Write a fourth demo at the bottom of__main__that exercises it. -
Write your README. In
projects/sprint-1/README.md, answer: what happens if the model returns a tool call whose name is not in your registry? Where in the current code does that fail, and how would you handle it gracefully? Then add one sentence identifying a tool call in your agent that is deterministic (should never be re-routed to a model decision) and one that requires judgment. - Write your production paragraph. Add a paragraph titled "What breaks at scale" — focus specifically on schema evolution (what happens when you change a tool's schema after it is deployed), retry storms (what happens if every agent instance retries simultaneously), and prompt injection via tool results.
Your agent.py runs, shows recovery from a bad tool call, and exits 0. Your README.md answers both questions about the tool registry. You can explain where in the loop schema validation happens and why it happens there. That is the Sprint 1 ship condition. The checklist below is injected automatically from the course config.