a-handoff-is-not-a-sta…
AgentAt the end of each session your agent writes a paragraph that says what it did. The next session still spends its first half hour…
Read this run method from the toy agent loop. A caller shows the returned string to the user as the answer. What is wrong with the contract, and how would you change it?
Claude Code
$ npx -y skills add rohitg00/ai-engineering-from-scratch --agent claude-code
Cursor
Cursor also loads subagents from .claude/agents, so copy this file there to use it in Cursor.
id: budget-exhausted-looks-like-an-answer title: Read the loop, what the caller gets at the turn cap card: agents kind: code-reading level: 2 roles: [agentic-ai-engineer, ai-evaluation-reliability-engineer] rounds: [coding, debugging] tags: [agent-loop, turn-budget, code-reading, return-type] minutes: 8 lessons: [phases/14-agent-engineering/01-the-agent-loop, phases/19-capstone-projects/20-agent-harness-loop-contract] manuals: [] practice: [] cheatsheet: [agent-harness] related: [agents/the-agent-loops-forever] updated: 2026-11-03
Read this run method from the toy agent loop. A caller shows the returned string to the user as the answer. What is wrong with the contract, and how would you change it?
def run(self, user_message: str) -> str:
self.history.append(Turn(kind="user", content=user_message))
for step in range(self.max_turns):
reply = self.llm.respond(self.history)
if reply["kind"] == "finish":
self.history.append(Turn(kind="final", content=reply["content"]))
return reply["content"]
thought = reply.get("thought", "")
self.history.append(Turn(kind="thought", content=thought))
call = ToolCall(name=reply["action"], args=reply.get("args", {}))
observation = self.tools.dispatch(call)
self.history.append(Turn(kind="action", content=call.name, tool_call=call, observation=observation))
self.history.append(Turn(kind="final", content="budget exhausted"))
return "budget exhausted"The method returns a plain string in both cases, so the caller cannot tell a real answer from a run that hit the cap. The text `budget exhausted` goes to the user as if the model said it. The fix is to return a typed result with a reason, or to yield control the way the harness contract does, so that the caller decides whether to extend the budget or stop.
The loop runs at most `max_turns` iterations. Each iteration asks the model for one reply. A `finish` reply ends the run and returns its content. Any other reply is an action: the loop records the thought, dispatches the tool, and appends the observation. When the loop runs out of iterations, it appends a final turn with the text `budget exhausted` and returns that text.
1. **One return type for two outcomes.** A finished run and a capped run both return `str`. The caller has no field to test. Any code that shows the result to the user shows the cap message as an answer. 2. **The cap is a dead end.** The partial state is in `self.history`, but nothing tells the caller that the run can continue. The harness contract treats the budget as a yield: it emits `budget.warn`, fires `on_budget_exceeded`, and returns a pull request with a reason. The caller inspects the state and calls `resume`. 3. **A reply with no action and no finish is a crash.** The code reads `reply["action"]` for every non-finish reply. A model that answers in prose without a tool call raises a `KeyError`. The lesson lists a turn with no tool calls as a normal stop condition.
Return a small result object with the final text, a reason such as `finished`, `budget` or `no_tool_call`, and the turn count. Keep the history on the object so that the caller can resume or hand off. Treat a reply without an action as a stop, not as an error. Keep the budget as a cap on this call, and let the caller decide what to do after it.
The [agent loop lesson](../../../phases/14-agent-engineering/01-the-agent-loop/) ships this loop, and the [harness loop contract](../../../phases/19-capstone-projects/20-agent-harness-loop-contract/) replaces the string with a pull point that the caller resumes.
At the end of each session your agent writes a paragraph that says what it did. The next session still spends its first half hour…
Read this part of the harness loop. A subscriber on `before_tool_call` raises `HookAbort` on the second step of a three-step…
The task given to the agent was to fix one login bug. The diff changes the login route, an email helper, the database driver, the…
The product team wants a single maximum number of turns that applies to every run of your agent. The agent handles one-line…
A long coding session has used most of its context window. The runtime offers to summarize the older turns and keep going. Your…
A tool is registered with this schema, and the model sends the four argument sets below. For each one, say what the registry…
Reviews the repository's shared Claude Code configuration (.claude/settings.json, CLAUDE.md, .claude/rules/, .mcp.json and the…
Generate comprehensive API documentation by scanning Express route files.
Advisor to a busy principal. Optimizes for the user's time, attention, and decision quality. One of the starter-kit council…