Context injection explained for long-horizon agents
Manus reports that a typical task takes around fifty tool calls. The sandbox runtime I built at Midsphere runs planner-and-executor jobs for hours. Neither fits in a context window if you do the obvious thing, which is to keep a list of messages, append every action and observation to it, and send the whole list on every call. And yet neither system gets noticeably dumber as the task goes on.
That’s not because the window is big. It’s because on every call the runtime decides what the model needs right now and puts it at the end of the prompt for that call only: the goal, the plan position, the budget, the rules that apply, the notes a memory process has left. That’s context injection. The transcript is only one input to the prompt, and after the first few dozen turns it isn’t the one doing the work.
What follows is the mechanism, what fills it, and how it goes wrong, with pseudocode for each piece.
The list and the call are different things
You keep the message list, but the thing you send to the model is the list plus a batch of extra messages that you compute fresh for this call and throw away afterwards.
agent has:
messages -- durable history, append-only
ephemeral -- cleared after every model call
inject_ephemeral(msg):
append msg to ephemeral
-- it never enters messages, so it never shows up in history,
-- snapshots, or anything persisted from them
call_model():
batch = ephemeral
ephemeral = empty
call_messages = messages + batch -- messages itself is never touched
return provider.complete(call_messages, tools)
The line that matters is call_messages = messages + batch. The batch goes on the end, and the history is never modified by anything except appending a completed turn. It would be just as easy to insert the batch after the system prompt, or right before the last user message, and for the first few turns you wouldn’t notice a difference. You’d notice it on the bill.
Providers cache the attention state for a prompt prefix. When the first N messages of this call are byte-for-byte identical to the previous call’s, those tokens are served from cache at a fraction of the price. Manus puts the cache hit rate as the single most important metric for a production agent, and their numbers explain why: an agent’s input-to-output token ratio is around 100:1, and on Claude Sonnet a cached input token costs a tenth of an uncached one. Insert the batch anywhere but the tail and every message after it is different from last time, so the cache hit ends there and a long run pays full price for its whole history on every call. At the tail, a call pays full price for the batch and the new turn, and cache price for everything else.
What the model sees
From the model’s side there is no history and no batch. There is one prompt, a flat sequence of messages, and it reads the whole thing fresh on every call with no memory of the previous one. Call 41 of a run looks like this to the model:
system ...
user Compare the pricing pages of our four competitors and write up the gaps.
assistant ...turn 1
tool ...
...
assistant ...turn 40
tool Ran 'curl .../pricing'. Output saved to /workspace/acme-pricing.md. Read file if needed
assistant Where I am in my plan -- goal I am working toward: compare four competitors' pricing.
I am currently on phase 2 of 4: Collect pricing data.
Progress so far: 1/4 phases completed.
...
assistant Context check: I'm at roughly 80% of my window -- compression is imminent...
assistant - The Acme page loads prices client-side; the curl output is the empty shell.
- Use browse for Acme, not web_fetch.
The last three messages came from the batch. The model has no way to know that. They’re assistant messages, so as far as it’s concerned they’re things it said, and it continues from them the same way it continues from turn 40. On call 42 they’re gone and different ones are there instead, and the model doesn’t notice that either, because it has nothing to compare against. It never remembers writing them and it never misses them.
That property is what everything else depends on. It’s why the first-person voice works: the model isn’t being told something, it’s picking up where it apparently left off. And it’s why stale injections are dangerous: a plan position from five turns ago is indistinguishable from one from this turn.
The model is not a participant that remembers the run. It’s a function of the prompt, called once per turn. If you want it to know something on this call, you put it in this call’s prompt, and the transcript is only one of the places that can come from.
How this differs from steering
Steering is the thing most agent frameworks already have: a way to push a message into a running agent, usually from a person, usually to change what it’s doing. “Stop, use the other account.” “Skip the tests, just get the build green.” It lands in the durable history as a user message and it stays there, because the model should still remember ten turns later that it was told to change course. In my runtime it’s an HTTP endpoint that calls inject_message on the live planner, and it returns 409 if the run isn’t active.
Context injection uses the same mechanism, a message pushed into the next call from outside the model’s own loop, but differs on three points.
Retention. A steering message persists. An injected message is ephemeral by default, because it describes the current state of something, the plan position, the budget, what the memory agent currently thinks matters, and that state will be different next turn. Keeping it would leave stale snapshots in the history that the model can’t tell apart from current ones. They’d also pile up; a plan block on every turn for 300 turns is a lot of tokens describing a plan position that’s no longer true.
Source and frequency. Steering is occasional and comes from a person or a supervising process making a decision. Injection is automatic and happens on every call, driven by hooks that read the run’s state and emit whatever the model needs right now. Nobody decides to inject the plan this turn. It’s injected because it’s a turn.
Voice. A steering message is a user turn, an instruction from outside, and that’s correct, because it is one. An injected message is an assistant turn in the first person, because it isn’t an instruction. It’s state the model needs to hold, and the most reliable way to make a model hold something is to have it appear to have said it.
Both go through the same messages + batch construction. Steering appends to the history. Injection appends to the batch. If you ever want a steering message the model should forget, or an injected message it should remember, that’s the signal you’ve put it in the wrong queue.
Hooks: where the batch gets filled
The queue is the mechanism. What fills it is a hook system attached to the agent’s lifecycle, with a dozen or so hooks each responsible for one kind of injection, composed differently for different roles.
lifecycle points:
START -- once, before the first call of a run
PRE_LLM_CALL -- before every model call
POST_LLM_CALL -- after every model call, with the response
PRE_TOOL_CALL
POST_TOOL_CALL
FINISHED
ERROR
Hooks at a point run in registration order, and since each one appends to the ephemeral queue, registration order is the order the model reads the batch in. The framework aborts the run if a hook raises, so every hook wraps its body in a try/except and logs. A missing note is a better failure than a dead run.
The plan for a planner in my runtime:
planner hook plan:
PRE_LLM_CALL : policy thought -- only if the run has policies;
always registered first
PRE_LLM_CALL : message discipline -- only while there is no plan
PRE_LLM_CALL : plan context
PRE_LLM_CALL : budget monitor
PRE_LLM_CALL : project coordination -- only in shared-sandbox runs
PRE_LLM_CALL : knowledge-manager advice -- only if the KM is enabled
POST_LLM_CALL : record the provider's token usage -- feeds the budget monitor
POST_LLM_CALL : content-burst trigger -- KM only
POST_LLM_CALL : prefetch trigger -- KM only
An executor gets a different plan: a task-bound version of the plan hook, a filesystem-first hook, and a max-steps warning hook. The planner additionally gets a START hook that fires once per user message. What follows is what each of these puts in the batch and why, grouped by purpose.
Keeping the agent oriented
The plan hook does the most work per token. The planner has a plan tool that creates a structured plan with phases, and once one exists, every call gets this at the tail:
<!-- plan -->
Where I am in my plan -- goal I am working toward: compare four competitors' pricing.
I am currently on phase 2 of 4: Collect pricing data.
Progress so far: 1/4 phases completed.
Full plan map (so I can see where this step fits):
- phase 1 -- Scope the sources (done)
- phase 2 -- Collect pricing data (in progress) <- I am here
- phase 3 -- Build the comparison table (pending)
- phase 4 -- Write up the gaps (pending)
Skills relevant to the phase I am on: web_research, spreadsheets.
Communication discipline: when I need to communicate with the user, I must use my message tool. ...
The goal, the current phase, a map of the whole plan with a cursor, and the skills that matter for this phase. The hook reads the plan fresh every call, and the model never has to remember where it was because it’s told every time. The first line is an HTML comment. The model reads straight past it, and anything grepping a transcript can find every plan injection by that marker.
The executor’s version is bound to its task at spawn time, because an executor has no history of its own beyond the task it was handed:
<!-- task -->
The task I have been handed: fetch the Acme pricing page and save the tiers to acme-pricing.md
Why this task matters -- the overall goal I am contributing to is: compare four competitors' pricing.
This task is part of phase 2 of 4: Collect pricing data.
Where the phase sits in the larger plan:
- phase 1 -- Scope the sources (done)
- phase 2 -- Collect pricing data (in progress) <- my task belongs to this phase
- phase 3 -- Build the comparison table (pending)
- phase 4 -- Write up the gaps (pending)
My scope for this run: complete the task as described, nothing more. Other phases are not mine to
work on -- even if I notice something I could improve there, I leave it for the planner to assign.
Staying inside phase 2 keeps the plan coherent.
The executor is shown the whole plan so it understands why its task exists, and then told in the same breath not to touch any of it. That last paragraph exists because executors that could see the plan started doing phase 3 when they finished phase 2 early, and the planner is the only thing that knows what else is in flight.
The planner also gets one injection at START, before its first call on each new user message:
User sent a fresh message, I need to acknowledge user's request with my available message tool
immediately before I decide I should plan or not. If this likely needs 2+ delegations, I should
plan first. When I delegate, I should assign only one atomic objective.
This is a prefill. It shapes the first action of the run and then it’s gone. Without it the planner would sometimes go quiet for a minute while it delegated, and the user would be looking at nothing.
Shaping how the agent behaves
Three hooks inject rules rather than state, and each has a condition on when it fires that matters as much as the text.
The policy hook injects the run’s non-negotiable policies wrapped in a first-person frame: “Before I act, I check myself against the absolute, non-negotiable policies set for me. They come before the user’s request, before my goal, before everything,” then the policies, then “Stepping outside these policies is the worst thing I can do: an immediate, unrecoverable failure that aborts my task and erases my work, no matter how good the rest was.” It’s registered first so it’s the first thing in the batch. A policy that appears after a plan block reads as one consideration among several. A policy that appears first reads as the frame everything else sits inside.
The message-discipline hook tells the agent how to talk to the user: use the message tool for anything user-facing, keep raw assistant text under 200 words and internal thoughts under 100, don’t repeat content already delivered. It fires only while there is no plan. The rules don’t stop mattering once a plan exists; the plan block carries the same lines at its tail. The reminder moves rather than disappears, so the batch has one block about the current state instead of two that overlap.
The project-coordination hook fires only in runs where several agents share one sandbox. It says the workspace is shared mutable state, that AGENTS.md is the coordination channel and should be read once per conversation rather than every turn, that only information other agents couldn’t discover on their own goes in it, and that the agent claims a git worktree before its first edit. In a single-agent run none of that is true and injecting it would be noise the model has to decide to ignore.
Warning the agent before it runs out
The filesystem-first hook fires on every executor call and tells the agent what to write down and, more importantly, what not to re-read. The rules that earned their place:
- Save to disk: search hits, web fetch results, findings from images or PDFs, long command output. Do not save content just read from the workspace; writing it again creates a duplicate under a new name.
- Before re-reading a saved file, scan recent messages first. Call the read tool on your own output only when you genuinely can no longer see it, not as a reflex because this reminder made you doubt your context.
- A file written this conversation is not re-read on the next turn or the one after. It becomes worth re-reading once it has scrolled out of recent history.
- Multimodal is the exception: after viewing an image or PDF, write findings now. Pixels don’t persist even if notes do.
The first version of this hook said “persist important state to disk” and produced executors that wrote the same content to three files and re-read their own output every turn to check it was still there. The clause about not doubting your context because this reminder made you doubt it is there because the reminder itself was causing the re-reads.
The budget warning fires once per run, when the provider-reported input tokens cross 80% of the context window:
Context check: I'm at roughly 82% of my window -- compression is imminent. If I have unwritten
state that would hurt to lose across compression, I persist it now before my next action.
Once, because a warning on every call past 80% is a warning the model learns to ignore. It knows the fill ratio only because a post-call hook recorded the provider’s token count on the previous turn. That hook injects nothing; it exists so this one has a number to act on. Post-call measures, pre-call acts.
The max-steps warning is the executor’s equivalent, fired once when the call count reaches three short of the cap:
I'm close to the maximum turn budget for this executor session -- I need to wrap up now while
I still have room. I stop starting new work, summarize what I accomplished and what remains,
and return my structured output so the planner can re-delegate anything unfinished.
The words that matter are “structured output” and “re-delegate anything unfinished”. In a different runtime I built, the framework’s own wrap-up injection just said to wrap up, and an obedient agent told to wrap up stops cleanly, which looks exactly like finishing. Agents were “completing” steps they had abandoned and the runner accepted it. Any injection that tells the model to stop has to also tell it how to mark the stop as partial, or the caller will read every stop as done.
A second agent writing into the batch
Everything above is deterministic: a hook reads some state and formats it. The last group connects a separate model-driven process, the knowledge manager, to the working agents’ batches, and the whole design is about doing that without the working agents noticing.
The knowledge manager runs alongside the planner and executors. It reads the trajectory and emits short guidance for the working agents. Three hooks wire it in.
The pre-call hook captures a snapshot of the exact message list the agent is about to send, so the knowledge manager later reasons about what the agent actually saw. It pops any nudges queued for this agent’s role, reads the most recent guidance block for the role from a cache, and injects both as one flat bullet list:
knowledge-manager pre-call hook:
role = planner | executor | other, from the agent's name
if role is other: -- the KM itself; never inject into it
return
snapshot the message list the agent is about to send
nudges = pop every queued nudge addressed to this role (or to both)
advice = latest cached guidance for this role, unless older than 90s
block = nudges (high urgency first) then advice, as bare bullets
if block is not empty:
inject_ephemeral(assistant message: block)
The role check matters. The knowledge manager is itself an agent with the same hooks, and without that early return its own calls would trigger more knowledge-manager runs.
The two post-call hooks decide when it runs, and never run it inline. They schedule a background task on the snapshot and return, so the planner’s next call isn’t blocked on a second model. Prefetch schedules a run after every call, rate-limited to one per ten seconds and four new messages. Content burst fires immediately after three content-heavy tool calls in a row, meaning web search, web fetch, browse, browser snapshot, or image analysis, because those are the calls that put raw external text in front of the agent and the guidance about them is worthless if it arrives five turns later. A fifteen-second ticker and a post-executor trigger cover the gaps. Whichever trigger fires, the output lands in the cache, and the next pre-call hook injects it. The knowledge manager runs between the agent’s turns and its output arrives on the next one, so the agent never waits for it.
Each run produces at most two things that reach the batch: nudges and a guidance block per role. Most runs produce neither. The prompt says so: over a long run the knowledge manager should expect to produce output on a minority of invocations, often well under half, and should not invent work to justify the run. Its per-call instruction ends with “if the window above has nothing new worth persisting, no nudge-worthy behaviour, and nothing step-critical to inject, return nothing and stop.”
Nudges are the urgent channel. The knowledge manager is told what they’re for: missed persistence, goal drift, repeated failure, forgotten actions, expensive redos. Each is one to three sentences, targeted at a role, with an urgency. They live in memory, are consumed by the first matching injection, and never touch disk. High urgency sorts to the top of the block. The nudge’s title and reason are logged and never injected.
Guidance blocks are the slower channel: up to six lines of bare bullets per role, scoped to the exact step the agent is about to take, cached with a ninety-second maximum age, and invalidated whenever an executor completes.
Both channels obey the invisibility rule, which the knowledge manager’s prompt spends a whole section on. The planner and executor do not know it exists. What it emits has to read as a note they left themselves: no self-reference, no “I notice”, no “Guidance” or “Steering” headers, no attribution, and if a line would tip off the reader that another agent composed it, rewrite it until it doesn’t. The formatter enforces the shape, bare bullets only, and prepends nudges to the advice bullets so the reader can’t tell which came from where. The prompt’s own image is a sticky note the agent actually needed to leave for itself, followed by the observation that most moments in a working agent’s day do not need a sticky note.
One detail I’d copy anywhere. When the knowledge manager runs on an executor’s trajectory, it’s given the same task block the executor was given, the one starting <!-- task --> above, prepended to the messages. It reasons about the executor’s situation from the executor’s own framing, which is the only way its notes can be scoped to the right step.
Drift
Drift is what happens when the goal is the oldest thing in the transcript. Each turn appends more evidence about the current sub-task, and nothing appends the task itself, so after enough turns the model is continuing the pattern of the recent history, which is about something narrower than what was asked. Nothing fails. The agent finishes the wrong thing well.
The plan hook is the direct fix. On every call the goal and the current phase are the newest thing in the prompt as well as one of the oldest, so they can’t age out of attention. Manus’s todo.md recitation is the same fix by a different route: their agent rewrites a todo file as it works, so the plan keeps landing at the end of the context where attention is. The difference is who pays. Recitation costs the model a tool call and some judgement about when to recite. A hook costs nothing per call and can’t be skipped.
The executor’s scope line is the same fix from the other side: a sub-agent that can see the whole plan and is told which one line of it is its own. The task block is also what makes delegation safe. An executor starts with an empty history, and the planner’s prompt says so directly: the executor starts with zero context, your task description is all it has. Writing the goal in full at every handoff is what a boundary that carries nothing forces you to do.
The knowledge manager has goal drift as a named nudge trigger, which handles the case the plan hook can’t: the agent is on the right phase and doing the wrong thing within it. The working agent can’t see its own drift, because its recent context is the thing it’s drifting toward. A second reader can.
Things that go wrong
Budgets stated in prompts aren’t enforced. The knowledge manager’s guidance blocks are capped at six lines per role, and that number lives in its prompt, its output model’s docstrings and its task template, and nowhere in the formatter, which does no truncation at all. It holds by convention. If a budget matters, count it in code.
Injections change behaviour beyond what they say. The filesystem-first hook produced agents that saved the same content twice and re-read their own output every turn, and it now carries a clause defending against itself. Every reminder you inject is also a suggestion, and the model will follow the suggestion whether or not it was the point.
Hooks that inject on every call need a reason to stop. The budget warning fires once. The max-steps warning fires once. The message-discipline block stops when the plan block takes over. Guidance expires in ninety seconds. Nudges are consumed on first read. Every one of those limits was added after the unlimited version turned into noise the model learned to skip.
And anything you inject that came from outside is a prompt-injection channel that fires on every call. The knowledge manager’s store keeps the original external content in a raw section that is never injected, and injects only its own paraphrase. If your agent reads programs or documents other people wrote, mark them as text to read and not as instructions, and keep them out of the batch.
That’s the whole trick. The transcript is one input to the prompt. The batch is the other, and after the first few dozen turns it’s the one carrying the goal, the position, the budget and the rules, put back in front of the model fresh on every call by a hook that runs whether or not the model would have thought to ask.
