TAI BUI
Back to curriculum
CapstonePython (stdlib)

Capstone Lesson 29: End-to-End Coding Agent on the Harness

Track A's payoff. This lesson stitches the gate chain, the sandbox, the observation ledger, and the OTel spans into one working coding agent that fixes a real (small, fixture-scale) bug in a multi-file Python project. The agent is a deterministic policy, not an LLM. That keeps the run reproducible and shows that the harness was the interesting part all along. The contract is identical when a real model plugs in at the policy seam.

TIME: ~90 minutesPREREQUISITES: Phase 19 · 25 (verification gates), Phase 19 · 26 (sandbox), Phase 19 · 27 (eval harness), Phase 19 · 28 (observability), Phase 14 · 38 (verification gates), Phase 14 · 41 (workbench for real repos), Phase 14 · 42 (agent workbench capstone)

Learning Objectives

  • Compose the gate chain, sandbox, observation ledger, and span builder into a single agent loop.
  • Implement a deterministic policy that uses read_file, run_tests, and write_file to fix a fixture bug.
  • Enforce a global step budget plus an observation token budget across an end-to-end run.
  • Emit complete OTel GenAI traces and Prometheus metrics for the full run.
  • Verify the agent solves the fixture in fewer than 12 steps with zero gate trips on legal tools.

The Problem

Most agent demos work in isolation: a sandbox by itself, an eval harness by itself, a span emitter by itself. Each looks fine alone. Compose them and the seams show.

The gate chain says ALLOW but the sandbox refuses for a reason the chain did not anticipate. The eval harness records a pass but the OTel spans say the gate refused a tool the agent claims it used. The Prometheus counter is incremented twice when it should be incremented once. The observation budget is exceeded and the agent keeps going, because the chain tracked the budget and the sandbox didn't know.

This lesson is the integration test for the whole track. The agent does its work in order: read the source file, run the tests, identify the bug from the test failure, write the fix, rerun the tests, and stop. Every operation goes through the gate chain. Every tool runs against the sandbox's project root. Every step is wrapped in a span. The demo then asserts the outcome.

Warm-up0/3
1. A program feeds every tool result back into a language model's context. Why would its designers limit the total amount of tool output read in one run?Warm-up
2. A unit test fails after a teammate's change and you are asked to fix the code. What is the sensible first step?Warm-up
3. A function should return the numbers 1 through n but always stops one short, at n - 1. What is this kind of bug commonly called?Warm-up
Check: The Problem0/3
1. In this harness, what is the gate chain?Define
2. Why does the lesson treat composing the gate chain, sandbox, eval harness and spans as a test of its own?Explain
3. A call arrives as tool run_tests with argv ("rm", "-rf", "/"). Which part of the harness refuses it, and does Sandbox.run ever see it?Apply

The Concept

Mermaid diagram

The agent's policy is a state machine with five working states, plus a terminal HALT. Each state maps to one tool call, and each tool call passes through the gate chain.

SURVEY: the agent reads src/fizz.py. The next state is RUN_TESTS.

RUN_TESTS: the agent runs the test command. If the tests pass, the state machine halts with success. Otherwise the next state is INSPECT.

INSPECT: the agent reads tests/test_fizz.py. The next state is FIX.

FIX: the agent writes the corrected file. The next state is VERIFY.

VERIFY: the agent runs the test command again, then the state moves to HALT whatever the result. This scaffold sets solved whenever HALT is reached, so a failing VERIFY is not reported as a failure. A real harness would check the exit code here.

If the gate chain denies a tool call, the agent records the refusal as a step, reports it in halted_reason, and halts unsolved.

The fixture bug is an off-by-one in fizz.py. The deterministic policy detects the bug with a substring check on the test failure output ("expected" present and "fizz" in the lower-cased text) and emits the corrected file. Both branches of that check emit the same hardcoded fix. Replacing the policy with an LLM does not change the harness contract.

Check: The Concept0/9
1. Which order do the working states run in on a normal pass, before the policy reaches its terminal HALT?Define
2. What is the step budget?Define
3. The fixture bug is an off-by-one: range(1, n) instead of range(1, n + 1). Where does the policy select its fix?Explain
4. Why does a run carry both a step budget and an observation token budget?Explain
5. The policy's corrected fizz.py uses range(1, n + 1). What does fizz(6) return?Apply
6. A real model replaces the policy and wanders for 30 steps without fixing the bug. How does the harness respond?Apply
7. Suppose FIX writes a bad file so the tests still fail in VERIFY. What does this scaffold do?Analyze
8. With the budgets at 8000 tokens and 12 steps, one read_file returns about 50,000 tokens at step 3. Which mechanism stops the run?Analyze
9. You want a run whose VERIFY step still fails to be reported as unsolved. What is the right change?Design

Architecture

Mermaid diagram

The lesson is self-contained. Each prior-lesson primitive (gate, sandbox, ledger, span) is reimplemented at minimal scale in main.py, so the demo runs without importing its siblings. The names match lessons 25-28 exactly, which keeps the mapping back to those lessons unambiguous. Only run_tests goes through Sandbox.run. The read_file and write_file tools do their own root-path checks against sandbox.project_root.

Check: Architecture0/9
1. What is the observation ledger?Define
2. What does a span, such as gen_ai.tool.execution, represent in this harness?Define
3. In the agent loop of main.py, what happens to a tool call, in order?Explain
4. Why does main.py re-state the gate, sandbox, ledger, and span primitives instead of importing them from lessons 25-28?Explain
5. Which statement about path protection in main.py is accurate?Explain
6. estimate_tokens uses len(text) // 4, and returns 0 for empty text. The write_file result is "wrote 74 bytes to src/fizz.py", 29 characters. How many tokens does the ledger record?Apply
7. The default chain's BudgetGate has max_tokens=8000 and the ledger already holds exactly 8000 tokens. The policy proposes read_file on src/fizz.py. What happens?Apply
8. Sandbox.run is called with ["cat", "../secret.txt"] where cat is not on the denylist. What is the result?Apply
9. You add a list_dir tool to the agent. Which change keeps the harness contract intact?Design

What you will build

main.py ships:

  1. The minimal harness primitives, copied with the same names as lessons 25-28: GateChain, Sandbox, ObservationLedger, SpanBuilder, MetricsRegistry.
  2. CodingAgentPolicy class: state machine with five working states plus a terminal HALT.
  3. prepare_scratch_repo() function: prepares a scratch dir with the bundled buggy fixture.
  4. AgentRun class: drives the policy, dispatches through the harness, returns an AgentRunReport.
  5. A bundled fixture (fixture_repo/) with src/fizz.py and tests/test_fizz.py.
  6. Demo: runs the policy end-to-end, prints the step-by-step trace, asserts pass, prints metrics.

The bundled fixture is a buggy file and a tests file, the same shape as lesson 27's tasks. The test failure message carries enough information for the deterministic policy to identify the fix. A real LLM would do the same job, slower and with broader recall, and the harness's expectations would stay the same.

Check: What you will build0/2
1. What is the bundled fixture in this lesson?Define
2. The bundled fizz.py is `[i if i % 3 else 'fizz' for i in range(1, n)]`. What does fizz(5) return?Apply

Why the policy is not an LLM

A real LLM needs an API key, a network call, and tolerance for unverifiable stochasticity. The harness is the part this lesson cares about. A deterministic policy lets the demo run on any developer laptop with zero external dependencies, and lets the test suite assert exact step counts.

The policy is a strict subset of what an LLM agent does: it reads the repo, sees the failing test, identifies the line, and emits a fix. An LLM goes through the same loop under the same harness contract, and the bookkeeping is identical.

Check: Why the policy is not an LLM0/2
1. In this harness, what does the policy decide?Define
2. You want an integration test of an agent harness (gates, sandbox, tracing) that behaves identically on every run. What is the main benefit of replacing the model with a deterministic stand-in policy?Explain

What the demo asserts

The demo exits nonzero unless four things hold, and the test suite reasserts them programmatically.

The policy solved the fixture (solved is true).

The run took fewer than 12 steps.

The observation tokens stayed within the budget.

Zero gate denials fired on legal tools (refused_legal_tool_calls == 0). The agent never invented a denied tool name.

Two more things are printed but not asserted: the number of spans emitted (one chat span plus one per tool execution, also written to traces.jsonl) and an excerpt of the Prometheus exposition, which includes tools_called_total and the tool_latency_ms histogram.

Check: What the demo asserts0/4
1. Which of these does the demo print without asserting?Explain
2. Why does the demo assert that zero legal tool calls were refused, rather than only that the fixture was solved?Explain
3. The demo run takes 5 steps (read, run tests, read, write, run tests) and every call is allowed. How many spans are emitted?Apply
4. tools_called_total reports read_file 2, run_tests 2 and write_file 1, which is 5, yet the run emitted 6 spans. A teammate says a counter is wrong. What is the mistake?Analyze

How this composes with the rest of Track A

This lesson is the integration. Lesson 25 wrote the gate chain, lesson 26 the sandbox, lesson 27 the eval harness, and lesson 28 the observability. Lesson 29 shows they work as a system. A real agent harness extends from here in three swaps: a model for the deterministic policy, a real-repo task for the bundled fixture, and OTLP for the JSONL exporter.

Check: How this composes with the rest of Track A0/1
1. After swapping in a model, the demo reports refused_legal_tool_calls of 2 on tools the model was told it could use. Where does the lesson say to look first?Analyze
Author's Note

I'd build the harness first and plug the model in last. The policy here is a deterministic state machine, which is why the test suite can assert exact step counts and the demo can run on a laptop with no API key. My bet is that the integration seams (gate chain, sandbox, ledger, spans) are the part worth getting right, and that the policy is the piece you expect to swap out.

When you do swap in a model, I'd keep the same step budget and observation budget and watch the gate-denial count on legal tools. If that number is not zero, I'd treat it as a harness question before blaming the model.

Running it

bash
cd phases/19-capstone-projects/29-end-to-end-coding-task-demo
python3 code/main.py
python3 -m pytest code/tests/ -v

The demo prints a run summary, a per-step trace, the span count, and a Prometheus excerpt, then exits with code zero. The tests cover the policy state transitions, the gate refusals on synthetic tool calls, the end-to-end run on the bundled fixture, and the step-budget invariants.

Check: Running it0/3
1. The test file has three tests, for n = 5, n = 1 and n = 3. How many of them fail against the unfixed fizz.py?Apply
2. In the policy tests, the state is set to RUN_TESTS and observe("run_tests", 0, "ok") is called. What is the state afterward?Apply
3. The state is RUN_TESTS and observe("read_file", 0, "text") is called. What is the state afterward?Apply

Code