Production-ready agents, Part 1.5: The harness, the loop and the context
Three disciplines sit underneath every part of this series: harness engineering (the code around the model), loop engineering (how an agent plans, acts, observes and stops) and context engineering (what goes into the model's window on every call). What each one is, how ADK implements it, and where the other seven disciplines live in the series.
People who build agents for a living now talk about a list of disciplines rather than one skill called “prompting”: harness engineering, loop engineering, context engineering, tool design, memory, orchestration, guardrails, evals, human-in-the-loop design and observability. When I lined that list up against this series, seven of the ten already had a part of their own. Three did not, because they sit underneath every part rather than in one place.
This short part fills that gap. It explains the three foundations, the harness, the loop and the context, from first principles, shows how Google’s Agent Development Kit (ADK) implements each one, and maps all ten disciplines to the parts that cover them. It sits between Part 1, which defined a model, a tool and an agent, and Part 2, where we build the first one.
The running example is the same as in the rest of the series: Acme Consumer Brands, whose router agent answers marketing and supply questions by calling a marketing agent (BigQuery) and a supply agent (Databricks).
If you prefer to watch first, here is the heart of this part as a short animated explainer.
Video: the harness, the loop and the context underneath every agent, in three minutes. The narration is a synthetic voice; the script and animation are mine.
Where are we in the architecture?
This part looks inside the agent box: the harness, the loop and the context that surround every call to Gemini. Part 1.25 walks through the whole picture, layer by layer.
where Part 1.5 sits in the Acme architecture: highlighted boxes are the subject of this part, solid boxes come from earlier parts, faded boxes arrive later
What are the ten disciplines, and where is each one in this series?
Here is the whole list in one place. The three tinted rows are this post; every other row points to the part that covers it in depth.
ten agent-engineering disciplines and where each lives in this series: harness, loop and context engineering in this post, tool design in Part 2, memory architecture in Part 8, orchestration in Parts 5 and 6, guardrails and permissions in Parts 3, 7 and 9, evals in Part 10, human-in-the-loop design in Part 9, and observability and tracing in Part 11
- Harness engineering: designing the software around the model: what runs the turn, executes tools, applies checks and stores state. This post.
- Loop engineering: designing how an agent works through a multi-step task and recovers from errors: plan, act, observe, decide, and when to stop. This post.
- Context engineering: deciding what goes into the model’s context window on every call. This post, building on skills in Part 4 and compaction in Part 8.
- Tool design: how an agent picks a tool and fills its arguments, and why narrow tools with clear descriptions beat a tool that runs any SQL. Part 2.
- Memory architecture: short-term session and state, and long-term memory across conversations. Part 8.
- Orchestration patterns: coordinating several agents: the router’s graph in Part 5 and agents calling agents over A2A in Part 6.
- Guardrails and permissions: callbacks that block unsafe queries (Part 3), acting as the user (Part 7) and plugins and Model Armor (Part 9).
- Evals for agents: trajectories, tool-calling accuracy and final answers, tested before and after release. Part 10.
- Human-in-the-loop (HITL) design: a pause point where a person approves a high-stakes action; ours is the one tool that writes data. Part 9.
- Observability and tracing: logging every step, prompt, tool call and state change so you can debug and cost a question. Part 11.
Four more topics come up in every production review, and each has its own short part: which agent framework to use and where ADK fits (Part 1.75), running model-written code safely (Part 3.5), multi-agent topologies beyond a single router (Part 6.5), and deterministic fallbacks with cost and latency budgets enforced while a turn runs (Part 11.5).
Three last topics close the series. Choosing models, including which model each step uses and how to live with versions and retirement dates, is Part 13. Where knowledge lives, from retrieval and long context to when fine-tuning is worth it, is Part 14. And what breaks in production, with how to diagnose and fix each failure, is the field guide in Part 15 and Part 16.
What is harness engineering?
Simple definition first: the harness is all the software around the model that turns it into a working agent. The model on its own only turns text into text. The harness decides what text goes in, runs the tools the model asks for, applies the checks, stores what happened and hands the answer back. Harness engineering is designing that software so the agent behaves reliably.
An everyday example first. Picture a brilliant consultant who works from a locked room. They cannot leave it, open a filing cabinet or make a phone call. All they can do is read the papers slid under the door and slide a note back. Outside sits an office manager who does everything else:
- chooses which papers to slide in for this question, and leaves the rest of the archive outside;
- reads each note (“please pull last March’s invoices”), checks it is allowed, fetches the invoices and slides them in;
- refuses a note that breaks the rules (“shred the ledger”) without ever fetching anything;
- files a copy of every note and every answer, and hands the final reply to the client.
The consultant is the model, the papers are the context, each note is a tool call, and the office manager is the harness: context builder, tool executor, checks and state services in one. The consultant can be as clever as you like; if the manager slides in the wrong papers or fetches anything they are asked for, the office still makes mistakes.
A more formal definition comes from Anthropic’s engineering team, who describe their Agent SDK as “a powerful, general-purpose agent harness” with “context management capabilities such as compaction” in Effective harnesses for long-running agents. Broken into parts, a harness provides:
- A driver for each turn, which receives the user’s message and keeps the agent running until it has finished.
- A tool executor, which runs the call the model asked for and returns the result.
- A context builder, which assembles the instruction, history and other inputs for every model call.
- Checks and hooks, which run before and after model and tool calls.
- Services for state, which store sessions, memory and files between calls.
One detail matters more than any other, and it is the most common misconception about harnesses. It is tempting to draw the harness as a box in front of the agent, between the user and the model, like a receptionist. In real systems the harness wraps the model: the model never calls a database or an API itself. It writes a request for a tool call, and the harness decides whether to run it, runs it, and feeds the result back.
the harness wraps the model and runs the tools: the user goal enters the harness, which in ADK is the Runner, the App with its plugins, the context builder and the session and memory services; the model only reads context and writes text or a tool call, the tool executor runs the call against BigQuery, Databricks, other agents or APIs and hands the result back, and the harness returns the answer
In ADK, the harness is the Runner, the App and their services. ADK’s
event loop page describes the division of work exactly. The
Runner “receives a user query and asks the main Agent to start processing”. The agent “runs until it
has something to report (like a response, a request to use a tool, or a state change)” and then
yields an event (a record of one thing that happened in the turn). The Runner processes the event,
using its services to commit any changes, and “the Agent’s logic resumes from where it paused only
after the Runner has processed the event”. This cycle repeats until the agent has nothing more to
yield for that question.
How does this apply to Acme? The office manager has a name in ADK. Acme’s harness, which Part 1 introduced, is built from those pieces:
Runner: drives each turn and saves every event to the session.App: wraps the root agent with app-wide settings: the plugins, compaction and resumability.- Plugins: the kill switch, request policy, rate limit, Model Armor and audit, in that order (Part 9).
- Session and memory services: where turns and long-term facts are kept (Part 8).
Each event carries the invocation_id (the ID of one user-question-to-final-answer cycle) and the
name of the agent that produced it; ADK’s context page lists both for
“logging and debugging”. That is why observability in Part 11 can trace a single question through
every step: the harness stamps every step it runs.
A worked harness turn. Maria asks the marketing agent “What was Verde’s CTR in Western Europe in the first two weeks of March?”, and this time the model picks the right measure first. Here is what the harness does, step by step:
1. Runner receives the message and starts a new invocation.
2. Plugins run before anything else: kill switch off, policy passes, rate limit passes.
3. Context the harness builds the model's input: the instruction, the tool docstring and
the question.
4. Model writes a tool call, not text:
query_marketing_measures(measures=["ctr_pct"], brand=["Verde"],
region=["Western Europe"], week_from="2026-03-02", week_to="2026-03-09")
5. Executor the harness runs the tool against BigQuery and commits the call as an event.
6. Result {"status": "ok", "rows": [{"brand": "Verde", "region": "Western Europe",
"ctr_pct": 1.6}]} is committed and fed back to the model.
7. Model writes plain text: "Verde's CTR in Western Europe for weeks 1 and 2 was 1.6%."
The Runner commits it as the final event and returns it.
Every one of those events carries the same invocation_id and the author acb_demo_marketing. The
model did two things, steps 4 and 7, exactly like the consultant writing a note and then a reply; the
harness did everything else, exactly like the office manager. Now flip the kill switch on and
ask again: the plugin ends the run at step 2 with a message, so there are no model calls and the tool
never runs (Part 9 builds this). That is what “the harness decides whether to run it” means in
practice: the decision is code, before the model is even asked. (The figure is the series’ toy data.)
My view: most “the agent is unreliable” problems I see are harness problems, not model problems. The model asked for something reasonable; the harness gave it too much context, ran a tool without a limit, or had no rule for what to do when the tool failed.
What is loop engineering?
The short answer: every agent runs a loop, and loop engineering is making sure it stops for the right reason. There are two kinds, and the key distinction is who ends them: the model ends the inner loop by answering; your code ends the outer loop with a rule and a ceiling.
An agent is, at its core, a model calling tools in a loop. Anthropic’s Building effective agents puts it plainly: agents “are typically just LLMs using tools based on environmental feedback in a loop”. Loop engineering is designing that loop on purpose: how the agent plans, acts, observes the result, decides what to do next, recovers from errors and, above all, stops.
The pattern has a well-known name. The 2022 paper ReAct: Synergizing Reasoning and Acting in Language Models showed that interleaving reasoning (thinking about what to do) with acting (calling a tool) and reading the result works better than either alone. Most agent frameworks, ADK included, run some version of that loop.
An everyday example first: a cook seasoning a soup. They taste it (observe), decide it needs salt (plan), add a pinch (act) and taste again. When it tastes right they stop and serve it, and nobody told them when; the cook decided. Now add a head chef at the pass with a written rule: “the soup must not be over-salted and must be hot”. If the bowl fails, it goes back with a note, and after three returns the head chef stops sending it back and decides what to do instead. Two loops, and two different deciders:
- The cook’s taste, adjust, taste again is the inner loop. The model runs it and decides when to stop, by answering.
- The head chef’s send it back, at most three times, against a written rule, is the outer loop. Your workflow runs it, and code decides when it stops.
A common misconception is that the more an agent loops, the better its answer gets. Without a rule for “good enough” and a ceiling, a loop just spends more tokens on the same mistake.
There are really two loops, and they need different designs:
two loops: the inner loop runs inside one LLM agent during one turn, where the model plans, asks for a tool, the harness runs it and returns the result, and the model decides to answer or loop again, stopping at a final answer and helped by narrow tools, errors returned as data and a cap on tool calls; the outer loop is a workflow you design on purpose, goal, draft, check, improve or exit, which in ADK is a LoopAgent that always needs max_iterations and an exit signal; Acme's router has no outer loop on its main path
The inner loop runs inside one LLM agent during one turn. The model reads its context and plans, asks for a tool, the harness runs it and returns the result, and the model decides whether to answer or call another tool. You do not write this loop in ADK; the framework runs it. What you design is how well it behaves:
- Narrow tools, so each step is small and checkable (Part 2).
- Errors returned as data. ADK’s
function tools page says the preferred return type
is a dictionary and suggests a
"status"key, so instead of crashing, a tool returns something like{"status": "error", "message": "brand not found"}and the model can correct itself on the next step. - A limit on steps. A confused model can call the same tool again and again. ADK’s
RunConfighasmax_llm_calls, which “caps the total number of LLM calls per run” and defaults to 500. Set it far lower for a narrow agent, and keep the rate limit and cost limits from Parts 3 and 9 as a backstop.
A worked inner loop. How does this apply to Acme? The cook’s tasting becomes a tool result, and the pinch of salt becomes a corrected tool call. Maria asks the marketing agent: “What was Verde’s CTR in Western Europe in the first two weeks of March?” Here is everything that happens inside that one turn:
1. Plan The model reads the question and its one tool, query_marketing_measures.
2. Act It asks for a tool call:
query_marketing_measures(measures=["ctr"], brand=["Verde"],
region=["Western Europe"], week_from="2026-03-02", week_to="2026-03-09")
3. Observe The harness runs the tool, which refuses with data, not a crash:
{"status": "error", "message": "unknown measure 'ctr'; use one of
media_spend_eur, ctr_pct, cpm_eur"}
4. Decide Not done yet. The model reads the message and loops back.
2. Act query_marketing_measures(measures=["ctr_pct"], brand=["Verde"],
region=["Western Europe"], week_from="2026-03-02", week_to="2026-03-09")
3. Observe {"status": "ok", "rows": [{"brand": "Verde", "region": "Western Europe",
"ctr_pct": 1.6}]}
4. Decide Enough to answer. The model writes plain text instead of a tool call:
"Verde's CTR in Western Europe for weeks 1 and 2 was 1.6%."
Two tool calls, one mistake fixed, one answer, and Maria saw none of the loop. Notice what made it work: the error said which values were allowed (narrow tools), it came back as data the model could read (errors as data), and if the model had kept guessing, the step limit would have stopped it. Notice also who decided to stop: the model, simply by answering instead of asking for another tool. (The figure is from the series’ toy data.)
The outer loop is a workflow you build on purpose, for tasks such as “draft, check, improve until
good enough”. In ADK this is a LoopAgent, and its
documentation is blunt: “the LoopAgent itself
does not inherently decide when to stop looping. You must implement a termination mechanism to
prevent infinite loops.” The two standard mechanisms are a maximum number of iterations and an
exit signal, where a sub-agent calls a tool that sets tool_context.actions.escalate = True:
from google.adk.agents import LoopAgent
def exit_loop(tool_context):
"""Call this when the draft passes the check."""
tool_context.actions.escalate = True # leave the loop now
return {"status": "done"}
refine = LoopAgent(
name="refine",
sub_agents=[drafter, checker], # checker may call exit_loop
max_iterations=3, # hard stop, whatever the model thinks
)
A worked outer loop. Acme’s main path does not need one, but a weekly brand summary does. This is the
head chef at the pass, with the written rule turned into a checker. At Acme, this is how it would
work: a weekly brand summary is emailed to the leadership team. It has two rules a single draft
often misses: every figure must come from the specialists’ findings, and it must end with the line
“Figures are provisional until the week closes.” So the job wraps two agents in a LoopAgent:
- drafter writes the summary from the findings. Its instruction includes the checker’s last feedback, which ADK fills in from session state, so each attempt can fix what the last one got wrong.
- checker reads the draft against the two rules. It either writes feedback (saved to state with
output_key="feedback") or callsexit_loop.
Iteration 1
drafter "Verde spend rose 12% week on week to EUR 120,400. Acme Pure held steady..."
checker FAIL: "12%" is not in the findings; the provisional line is missing.
-> feedback saved, no exit, so the loop goes round again
Iteration 2
drafter "Verde spend was EUR 120,400 this week. Acme Pure held steady...
Figures are provisional until the week closes."
checker PASS -> calls exit_loop, escalate = True, the loop ends
Result the iteration 2 draft is sent. Iteration 3 never runs.
If iteration 3 had also failed, max_iterations=3 would have stopped the loop anyway, and code,
not the model, would decide what happens next: hold the email and send the last draft to a person
instead of to leadership. Here who decided to stop: the workflow, through the exit signal or the
ceiling. Inside each drafter step, the drafter may still run its own inner loop of tool calls; the
outer loop wraps around whole steps.
Where possible, make the checker code rather than a model. “Does the draft end with this exact line?” and “is every number in the findings?” are a few lines of Python, cheaper and fully repeatable. Use a model as the checker only for judgements code cannot make, such as tone.
Where Acme puts its loops. The router’s main path has no outer loop at all. Its route is a fixed graph decided in code (Part 5), so every question takes a known, testable path. The model loops only inside each specialist, with narrow, read-only tools. The “observe” step that matters most is not a model at all: after the writer drafts an answer, code checks that every number came from a specialist and replaces the answer if one did not. That follows the series’ design rule: models read and write language, code makes the decisions.
Which loop do I need? Every tool-using agent already runs an inner loop; you get it for free and your job is to make it behave. Add an outer loop only when one pass is not reliable enough and you can say exactly what “good enough” means, so a checker can test it. A quick test on four jobs:
- “What was Harvest Row’s spend last week?” One agent, a tool call or two: inner loop only.
- “Is Acme Pure’s late delivery hurting its campaign?” Two specialists and a writer, in a fixed order: a graph workflow (Part 5), still no outer loop.
- “Write the weekly leadership summary, and it must pass these two rules.” A draft, check and
revise cycle: an outer loop with an exit signal and
max_iterations. - “Keep improving the summary until it is perfect.” No testable bar, so no loop. Define the bar first.
What is context engineering?
Every model call is stateless: the model sees what is in its context window (everything sent to it on that call) and nothing else. It remembers nothing from the last call unless your code sends it again. Context engineering is deciding what goes into that window on every call. Prerequisite 3 showed why: a model keeps nothing between calls, so whatever it needs has to be sent each time. The short answer to the heading, then: context engineering is choosing the smallest set of inputs that lets this call do its job, and choosing it again on every call.
An everyday example first: a locum doctor covering a night shift. They have never met the patient and remember nothing about them, so everything they know comes from the handover sheet. A good sheet fits on one page: the allergy to penicillin, today’s medicines, the problem tonight and the last blood result. A bad one is the whole 300-page file, with the allergy on page 212. The locum is just as capable either way, but with the file they are slower, and more likely to miss the one line that matters.
The locum is the model, the handover sheet is the context window, and writing a good sheet, fresh for every shift, is context engineering. The 300-page file is what many agents send by default: everything we have. The misconception to drop is that a bigger window means you can safely send everything; the two reasons below are why you cannot.
Anthropic’s Effective context engineering for AI agents defines it as “the art and science of curating what will go into the limited context window” and gives the principle I use: “finding the smallest possible set of high-signal tokens that maximize the likelihood of some desired outcome”. Two reasons make this more than tidiness:
- Cost and latency. Every token is paid for and processed on every call, so a bloated context makes every answer slower and dearer.
- Context rot. The same article reports that “as the number of tokens in the context window increases, the model’s ability to accurately recall information from that context decreases”. A bigger window does not fix that; it only delays it.
A little arithmetic shows the first point (illustrative numbers). Suppose a turn makes three model calls and the session history is 2,000 tokens. The history is resent on every call, so that is 6,000 input tokens per question before any real work. Compact the history to 200 tokens and the same turn carries 600.
It also differs from prompt engineering, which is writing one good prompt. Context engineering is done again on every call, because the inputs change: new tool results, a longer history, a different skill.
context engineering: one model call's context window is built fresh each time from the system instruction, tool descriptions, a skill if loaded, state values injected with key templating, memory recall, compacted session history, this turn's tool results and the user question; secrets, unneeded raw rows, stale tool results and instructions for other jobs stay out, because every token costs money and time and recall gets worse as the window fills
Each ingredient has a home in ADK and in this series:
- System instruction: the agent’s job and rules, written as rules rather than prose (Part 2).
- Tool descriptions: each tool’s docstring becomes its contract, and the model reads all of them on every call, so a few clear tools cost less context than many vague ones.
- Skills, loaded just in time: a skill’s full instructions enter the window only when the task needs them, which Part 4 calls progressive disclosure (Part 4).
- State values: ADK can inject a value from session state into an instruction with
{key}templating, so the instruction carries this user’s facts without repeating the whole history. Apart from a tool’s result, this is the only way the model sees state at all (Part 8 shows the full picture). - Memory recall: a few relevant facts from past sessions, not the past sessions themselves (Part 8).
- Session history, compacted: after a set number of turns ADK summarises older events, which the compaction page says “optimizes latency and reduces costs”. Acme compacts every five turns.
- Tool results: the rows a tool returned this turn. Keep them small at the source, with row limits in the tool, rather than trimming them afterwards.
How does this apply to Acme? Three of those ingredients are easier to see with a real input, each one a line on the handover sheet:
{key}templating. Part 1’sget_spendwriteslast_brandinto session state. Suppose the instruction contains the line “The user last asked about {last_brand}. Resolve ‘it’ and ‘that brand’ to it.” Withlast_brand = "Acme Pure"in state, the model receives “The user last asked about Acme Pure…”, so “What about its CTR?” becomes a call withbrand=["Acme Pure"]without resending the earlier turns. (Write{last_brand?}if the key may be missing, as the router’s writer does in Part 5.)- Compaction. Acme’s setting is
compaction_interval=5withoverlap_size=1(Part 8). Suppose Maria’s first five turns asked for spend on each brand and then OTIF. After turn 5, a model replaces those turns in the context with one summary event, something like “Maria asked about spend: Acme Pure EUR 283,590, Verde EUR 120,400, Harvest Row EUR 98,000; then OTIF for Acme Pure, 87.4%.” Turn 6 carries that one line instead of five turns of questions, tool calls, results and answers. The raw events stay stored in the session; they just stop being sent. - Small tool results. Give the model a general SQL tool and let it run
SELECT *oncampaign_performancefor the three brands, and every weekly row comes back, each with campaign, region, week, spend, impressions and clicks; the whole lot then rides along on every later call in the turn. The narrowquery_marketing_measureswithmeasures=["media_spend_eur"]andgroup_by=["brand"]returns three rows, and the answer “Acme Pure: EUR 283,590” needs only one.
Just as important is what stays out: secrets and tokens (which belong in headers and metadata, as Part 6 shows), raw rows the answer does not need, stale tool results once they are summarised, and instructions meant for a different agent.
Context engineering in Acme’s router is mostly subtraction. Each step gets only what its job needs:
Acme's router gives each step only what its job needs: extract is a model call that sees the question, the conversation so far and a short instruction, but no tools or rows, route is code with no context window, each specialist sees its own instruction, narrow tools, one skill and tool results, and the writer sees the question, the specialists' answers and format rules, followed by a code number check
Here is what each step actually receives for one question, “What did we spend on Verde?” (sizes are rough and illustrative; the instructions are shortened from Part 5):
1. extract (model) in: "Rewrite the user's latest question as ONE self-contained line ...
Do not answer it." + the conversation so far + the question
out: "Media spend for Verde"
no tool docstrings, no rows: roughly 60 tokens in on a first question,
more as the chat grows (until compaction trims it)
2. route (code) in: the rewritten question, Maria's team and last_plan from state
out: marketing; no model call, zero tokens
3. specialist the marketing agent's own instruction, tools and skill, plus its tool
result {"status": "ok", "brand": "Verde", "spend_eur": 120400}
out: marketing_findings = "Verde media spend: EUR 120,400"
4. writer (model) in: "Write the final answer ... using ONLY these findings."
+ the question + "Marketing findings: Verde media spend: EUR 120,400"
+ "Supply findings: " (empty) + the format rules
out: "Verde's media spend was EUR 120,400."
roughly 150 tokens in
5. number check code: 120,400 is in the findings, so the answer is sent; zero tokens
Extract is the one router step that does see the conversation, because its whole job is to turn “What did those stockouts cost us?” into a line that stands alone, and it cannot know what those means without the earlier turns. That is why Part 5 tells it to resolve those and it “from the conversation”. What it never gets is tools or data. Neither router model call ever sees the specialist’s tool docstrings, the raw rows or the supply agent’s rules. “Everything we have” would put all of that, plus the whole session, into both calls. The choice of what each call sees was made by us, in code, not by the model. (Spend is the series’ toy data.)
My view: the cheapest context engineering decision is to not call a model. Acme’s route and number check are code, so they need no context window at all, and the two model calls that remain each get a small, purpose-built one.
How do the three fit together?
The three disciplines are layers of the same system. The harness is the machine: it runs the turn, executes tools, applies checks and stores state. The loop is the behaviour the harness produces: plan, act, observe, decide, stop. The context is what the harness feeds the model on each pass round the loop. A good agent needs all three: a careful context is wasted if the loop never stops, and a well-bounded loop still fails if every pass sends the model the wrong inputs.
Put the three examples from this part into one restaurant kitchen and the layers are easy to tell apart. The kitchen itself, with its staff, its rules and its order book, is the harness. The cook’s taste, adjust, taste again, plus the head chef’s send-backs, is the loop. What is on the cook’s counter at each moment, the order ticket and the ingredients for this dish and nothing else, is the context. A great cook still fails in a kitchen with no rules, at a counter buried in someone else’s ingredients.
Take the worked inner loop from earlier, Maria’s question about Verde’s CTR in March, and read it three ways:
- Harness: the Runner received the question, the plugins let it through, and the tool executor
ran
query_marketing_measurestwice and committed every event under oneinvocation_id. - Loop: two passes. Pass one used the bad measure name
ctrand got an error back as data; pass two usedctr_pctand got 1.6%; then the model stopped by answering. - Context: each pass sent the instruction, the tool docstring and the question, plus only the tool results so far in this turn: none on pass one, the error on pass two, the error and the row on the final pass.
Everything else in the series builds on these. Tool design shapes the “act” step. Memory and compaction feed the context. Guardrails and human approval are hooks in the harness. Evals test the loop’s trajectory, and observability records what the harness did.
The layers also tell you where to look when something goes wrong. When an agent misbehaves, ask which layer the symptom belongs to before you reach for a bigger model:
Symptom Layer First thing to check
Runs a tool it should not, or ignores a rule harness a callback or plugin check (Parts 3 and 9)
Repeats the same call, or never stops loop the error message, the tool's allowed values
and the step limit (max_llm_calls)
Wrong or vague answer although the data exists context what that call actually saw: missing skill,
stale state, buried in history
Every answer is slow and expensive context history size, compaction, tool result size
Only then model a different model, measured with evals (Parts 10, 13)
Try it: measure a context before you send it
You do not need a model to practise context engineering. This script assembles a context the way a harness does and estimates its size, using the rough rule of about four characters per token for English text. Run it with plain Python 3 and change the inputs to see what each one costs:
parts = {
"instruction": "You answer marketing questions for Acme. Use only the tools. " * 3,
"tools": "get_spend(brand): total media spend in EUR for Acme Pure, Verde or Harvest Row. " * 2,
"history": "user: what did we spend on Acme Pure?\nagent: EUR 283,590 in total. " * 12,
"tool_result": '{"status": "ok", "brand": "Verde", "spend_eur": 120400} ',
"question": "And Verde?",
}
total = 0
for name, text in parts.items():
tokens = len(text) // 4 # rough estimate: about 4 characters per token
total += tokens
print(f"{name:<12} ~{tokens:>5} tokens")
print(f"{'total':<12} ~{total:>5} tokens")
When I run it, it prints this (the figures are the toy get_spend data from Part 1):
instruction ~ 45 tokens
tools ~ 40 tokens
history ~ 201 tokens
tool_result ~ 14 tokens
question ~ 2 tokens
total ~ 302 tokens
The history is two thirds of the context, and the new question is two tokens. Then change * 12 in
the history to * 2, which is roughly what compaction does to a long session, and watch the total
fall: history drops to ~33 tokens and the total to ~134, less than half, with nothing the model needs
for “And Verde?” taken away. For exact counts, the Gemini API has a token-counting method; the estimate is
enough to see where the bulk of a context comes from.
What can go wrong
- The harness keeps the default step limit. A model calls the same failing tool again and again,
and with
max_llm_callsat its default of 500 the turn burns tokens for a long time. Set a limit that fits the job, and keep rate and cost limits as a backstop. For example, suppose a tool keeps returning the same “unknown measure” error and the model keeps retrying. With the default it can go round hundreds of times; withRunConfig(max_llm_calls=10)ADK Python raisesLlmCallsLimitExceededErrorat the cap and the run ends with an error, not an answer, so your code should catch it and tell Maria plainly that the figure could not be fetched (Part 11.5 does this politely with a plugin). - A
LoopAgentwithoutmax_iterations. The checker never quite approves, and the loop runs until something external stops it. Always set both a maximum and an exit signal. - Tools that raise instead of returning errors. The run crashes instead of letting the model try again with better arguments. Return a dictionary with a status and a message.
- “Send everything, the window is big enough.” Costs grow on every call and recall gets worse. Send the smallest set of inputs that does the job.
- Secrets in the context. A token pasted into an instruction or a tool result can be repeated,
logged or leaked. Keep credentials in headers and metadata, never in text the model reads. Wrong:
an instruction that says “Databricks token: dapi-…”, which the model can repeat if Maria asks
“what credentials are you using?”, and which the event log then stores. Right: the token travels
in call metadata and sits in
temp:databricks_token(Parts 1 and 6), so the model never sees it and has nothing to repeat.
What code did we write in this part?
Every part ends with the same map of Acme’s build: which files exist so far, and the code this part added, explained line by line. The map grows with each part.
This part adds no code to Acme’s build. It explains the harness, the agent loop and context engineering using traces of the existing marketing agent; the LoopAgent snippet is an illustrative extension (a weekly summary with a drafter and a checker) that is not in the build, and the token counting script is a practice exercise.
Where does it live?
marketing/ the marketing specialist, on BigQuery
app/agent.py P1 marketing Agent with its tools, wrapped in an App
app/tools.py P1 query_marketing_measures, the marketing tool on BigQuery
shared/ code all three services share
routing.py P1 classify: picks the specialist in code, no model
semantic.yaml P1 semantic registry: each measure, its owner and access
+ added in this part ~ changed in this part Pn = the parts that touched the file
What did we learn?
- Seven of the ten agent-engineering disciplines already have their own part in this series; harness, loop and context engineering sit underneath all of them.
- The harness is the software that wraps the model: in ADK, the Runner, the App, its plugins and its services, which run each turn, execute tools, apply checks and store state.
- An agent works in a loop of plan, act, observe and decide; the inner loop runs inside one turn,
while an outer loop such as a
LoopAgentmust always have a maximum and an exit signal. - Context engineering means building the smallest useful context for every model call, because the model is stateless, every token costs money and recall gets worse as the window fills.
- Acme keeps all three small: a fixed route in code, loops only inside specialists with narrow tools, and two model calls that each see only what their job needs.
What comes next?
Before we build with ADK, Production-ready agents, Part 1.75: Agent frameworks, and where ADK fits looks at what an agent framework is and compares ADK with LangGraph, CrewAI, the OpenAI Agents SDK, Microsoft Agent Framework and the Claude Agent SDK, using the harness, loop and context vocabulary from this part. Then Part 2 builds the marketing agent.
Sources
- ADK: Event loop
- ADK: Agent context
- ADK: Loop workflow (LoopAgent)
- ADK: Function tools
- ADK: Runtime configuration (RunConfig)
- Gemini API: Understand and count tokens
- ADK: Session state and
{key}templating - ADK: Context compression
- ADK: Skills
- Anthropic: Building effective agents
- Anthropic: Effective context engineering for AI agents
- Anthropic: Effective harnesses for long-running agents
- Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models (2022)
Opinions are my own. Every fact in this post comes from the public documentation linked above.