Context Engineering: The Dashboard Hung at the End of the Conversation
Source version of CodeSmith:
v0.5.0(commit3a74c82f). All paths are relative to the repo root; line numbers refer to this version.
Intended audience: readers curious about what the Agent itself gets to "see" — how the status bar, the TODO list, and environment information enter the model's field of view.
The command line has its prompt, git has status, the GUI has a status bar — letting the user sense the system's current state is interface-design common sense. An Agent, though, has two users: the person sitting at the terminal, and the model at the other end of the context. The model needs to know the same kind of things: what today's date is, which directory it is in, which step of the task it is on, and whether that tool just now has already been called three times with the same arguments.
The status bar built for the model is this installment's protagonist. It has to answer three questions: what to write (task planning, event side-channels, environment observation), where to write it (at which position in the message sequence), and in what role (system or user). The third question looks like a mere protocol detail, but it is tied directly into the cache discipline of Article 9.
Why does the status bar work at all? The theoretical warrant lies in the nature of attention: in-context learning behaves more like retrieval than reasoning. The context window is a retrieval engine that ships with only half its parts — and the "retrieval" half is extremely strong: ask about anything, and attention can dredge the relevant raw records out of tens of thousands of tokens, which amounts to building RAG into every forward pass. What is missing is the other half: a distillation layer. Nothing in the context ever gets automatically counted, indexed, or summarized in place into a conclusion — for conclusions about the content itself ("how many phone calls have been made in total," "how many TODOs remain"), the model must recompute from the raw records every single time it needs one; the cost balloons with the context, and the error rate climbs with it. Long contexts add a second torment: attention is a finite resource, and key information buried in the middle has its weights diluted by everything that comes after — and the task goal set down early on is precisely what most easily drowns in the flood of tool results.
One controlled experiment slapped both defects onto a heatmap: a customer-service Agent had already called Xfinity 3 times, and the user pressed with "can you call again to chase it up?" In the cohort without a status bar, attention scattered across the regions of the three call records, with the thinking tokens spent entirely on "counting"; in the cohort with an <agent_status> line appended at the end of the trajectory ("phone_call called 3 times, Xfinity at limit 3/3"), attention concentrated sharply on the status bar, and the ready-made conclusion was consumed directly. For small models in the Qwen3-0.6B class, the former frequently broke the rule and dialed anyway, while the latter complied reliably. The quantified conclusion that carries more economic weight: a status bar computed in advance lets a small model's accuracy approach that of frontier large models, while cutting thinking tokens, latency, and spend per iteration by roughly an order of magnitude — without the status bar, thinking volume keeps growing as the context lengthens; with it, it stays essentially constant. A cheap brain paired with a dashboard is the other half of this project's economics: Frugal Architecture (Articles 04 and 05) saves on the unit price of tokens; the status bar saves on how much the model has to think.
The identity question: the user role, living at the end
The status bar's number-one design decision: in what role does this framework-generated state information enter the context at the API layer?
The intuitive answer is system — "it's system state, after all." CodeSmith's answer is user, and specifically a user message appended to the end of the conversation. The one place in the entire repo that explains this choice verbatim sits on the constructor of the sub-agent completion sentinel (crates/agent-runtime/src/engine/turn/postprocess.rs:90-101):
/// Role is `"user"`, not `"system"`: some OpenAI-compatible backends apply a
/// strict chat template (e.g. vLLM serving Qwen3) that requires any system
/// message to be messages[0]. A system message appended mid-conversation
/// makes the template raise "System message must be at the beginning",
/// which surfaces as a 400 BadRequest and breaks the whole sub-agent hand-off
/// in the parent turn. The `visibility="internal"` tag already tells the
/// model this is a runtime event rather than user input, so the role carries
/// no semantic weight here — only template-compatibility cost.
Translated, it has two layers. The surface layer is compatibility: quite a few OpenAI-compatible backends (vLLM serving Qwen3, for instance) apply a chat template that requires the system message to be the first one; a system message inserted mid-conversation is an immediate 400 error. The deeper layer is a decoupling of semantics from format: the user here is merely a message slot in the API protocol, not "input from the human at the terminal" — whether something "was actually said by a real user" is instead declared by content tags such as <system-reminder> and visibility="internal"; the role itself carries no semantics.
And appending at the end (rather than editing the system message at the head) is exactly the cache discipline of Article 9: system belongs to the prefix zone, where touching a single byte rebills the whole thing at full price; the end belongs to the append zone, where however much you write, you pay only for the new content. The status bar, in essence, borrows the shell of the user role to hang dynamic state somewhere the prefix cache cannot reach.
What remains is the question of content. Status-bar information falls into two families: what gets appended along with events (side-channels, anomaly alerts), and what keeps being updated as the task advances (task planning, environment observation). CodeSmith gave each family its own carrier.
Environment observation:
The first ContentBlock of every new user message is <turn_meta> (engine/turn_meta.rs:1-15): the current date, the model and thinking tier that auto routing picked, a working-set summary, and the conditional skills that were hit. Article 9 already covered its day-granularity date and byte-stable design; here we look at the "observation" ingredient of its content — the working-set summary (working_set.rs:717-738):
lines.push("## Repo Working Set".to_string());
lines.push(format!("Workspace: {}", workspace.display()));
// repo overview: "Key files: Cargo.toml, README.md, …" + top-level directories
lines.push("Active paths (prioritize these):".to_string());
// one line per active path: - crates/xxx/src/foo.rs (file)
lines.push(
"When in doubt, use tools to verify and keep changes focused on the working set."
.to_string(),
);
It takes "which files the recent user messages and tool calls have been touching" — implicit state inside the engine — and renders it explicit as an observation list visible to the model. This is the status bar's third kind of information: the conversion of implicit state into explicit observation — every decision-relevant fact the engine knows should be made visible to the model as well.
But the boundary is drawn just as crisply: git branch and dirty state are not injected, and system time stops at the day. Want to know git status? Call a tool. The logic of this dividing line gets its turn later.
Task planning: the three lives of a TODO
The classic first kind of status-bar information is the TODO list: in a long task the model easily sinks into a local subtask and forgets the original ask — parking the list at the end of the trajectory as a recurring reminder is the popular fix. CodeSmith's implementation is a bit unexpected: there is no mechanism that "periodically stuffs the TODO back onto the tail of the trajectory". This can literally be counted: across the three-thousand-line engine/mod.rs, config.todos is referenced exactly four times in total — line 1368 packs it into the compaction probe, lines 2625/2726 read it at cycle boundaries, line 2920 reinjects it after compaction — and not one of those hangs on a "before every request" path.
Before the forms, one backbone to string the whole section together: the TODO's body does not live in the trajectory; it lives in the engine's memory — a shared lock hanging off EngineConfig (crates/agent-runtime/src/engine_config.rs:127, SharedTodoList = Arc<Mutex<TodoList>>). Every list that has ever appeared in the trajectory is merely a projection of that body, and the three lives are three moments of projection.
Form one: the tool output is the snapshot. What the model has at hand is an eight-piece kit: in one go, the registry mounts the four canonical names checklist_write/add/update/list plus the four compatibility aliases todo_write/add/update/list, and all eight tools share the same lock (crates/tui/src/tools/registry.rs:661-671); the todo_write description declares its own identity: "Compatibility alias for checklist_write." The canonical name's semantics are full replacement, nailed down in one schema sentence (crates/tool-impls/src/tools/todo.rs:313): "The complete list of todo items. This replaces the existing list." — the parameter is a lone todos array, each item a content plus a three-valued status enum. The execute is just as unambiguous (todo.rs:343-384):
// Clear and rebuild the list
list.clear();
for item in todos {
// ......
list.add(content.to_string(), status);
}
let snapshot = list.snapshot();
let result = serde_json::to_string_pretty(&snapshot).unwrap_or_else(|_| "{}".to_string());
Ok(ToolResult::success(format!(
"Todo list updated ({} items, {}% complete)\n{}",
snapshot.items.len(),
snapshot.completion_pct,
result
))
.with_metadata(checklist_metadata(&snapshot, self.tool_name)))
This code does exactly one thing: dumps the old list out of the lock wholesale, rebuilds one from the arguments, and stuffs the fresh snapshot back into the tool result as-is. Notice there is no "skip the write if the list hasn't changed" short-circuit — the judgment of whether to resend, along with its cost, is handed back to the model.
Take the repo's own tests as the example. The output asserted at crates/tui/src/tui/ui/tests.rs:6811-6825 corresponds to a call with a three-step list:
checklist_write({ "todos": [
{ "content": "Plan it out", "status": "in_progress" },
{ "content": "Wire the thing", "status": "pending" },
{ "content": "Run gates", "status": "pending" }
] })
After finishing the first step and pushing the second item to in_progress, call once more, and what the model sees in the tool result is the full state (fields and values as in the test fixture; indentation compressed here — what actually gets sent is pretty JSON):
Todo list updated (3 items, 33% complete)
{ "items": [
{ "id": 1, "content": "Plan it out", "status": "completed" },
{ "id": 2, "content": "Wire the thing", "status": "in_progress" },
{ "id": 3, "content": "Run gates", "status": "pending" }
],
"completion_pct": 33,
"in_progress_id": 2 }
That is the whole of the TODO's day-to-day existence: a tool result appended to the end of the trajectory as one new message — the model sees the list exactly when it calls the tool; if it doesn't call, it doesn't see. No timer, no "remind every N turns."
It ships with one beautiful invariant (crates/agent-runtime/src/tool_state/todo.rs:153-159):
fn set_single_in_progress(&mut self, allow_id: Option<u32>) {
for item in &mut self.items {
if Some(item.id) != allow_id && item.status == TodoStatus::InProgress {
item.status = TodoStatus::Pending;
}
}
}
(Both entrances summon it: add passes None when it lands a new in_progress item, demoting everyone; update_status passes Some(id) when it pushes an item to in_progress, demoting everyone but the target. At most one item is in progress at any given moment — the in_progress_id in the JSON above is singular, not an array; the list is forever a story that can be told linearly.)
Form two: rebuild after compaction. Compaction swaps out the historical messages; tool results vanish along with the snapshots inside them — but the body is still in memory, and rebuilding is just one more projection. The trigger sits at crates/agent-runtime/src/engine/capacity_flow.rs:1157-1168: once summary compaction has replaced the transcript through rebuild_transcript (declaring RebuildReason::AutoCompaction), the very next line calls reinject_compaction_attachments. The TODO candidate's copy (engine/mod.rs:2930-2936) is "Active todos resumed after context compaction:" plus an itemized list, and the list is assembled by this loop (crates/agent-runtime/src/compaction/attachment_reinject.rs:95-107):
for item in &snapshot.items {
lines.push(format!(
"- #{} {:?}: {}",
item.id, item.status, item.content
));
}
(Mind that {:?}: the status goes through Rust's Debug formatting, so what comes out is - #2 InProgress: Wire the thing, not the in_progress of the tool output — one body, two spellings.)
The outer wrapping is that same user shell again (compaction_reinject_message, attachment_reinject.rs:51-59): the content is wrapped in <system-reminder> and appended to the end of the trajectory under the user role. At the beat when the compaction lands, what the model sees is (a <turn_meta> block is also prepended before sending — omitted here; the full list of the four candidate classes gets a detailed telling in the compaction installment):
<system-reminder>
Active todos resumed after context compaction:
- #1 Completed: Plan it out
- #2 InProgress: Wire the thing
- #3 Pending: Run gates
</system-reminder>
Form three: the seed at the cycle boundary. A cycle is the conversation's rolling restart: after each turn the input tokens are estimated, and once the threshold is reached, the old trajectory is archived and a fresh one opened (entry at engine/mod.rs:1638-1646, issue #124; the threshold defaults to 768K, roughly 75% of the 1M window, cycle_manager.rs:60-65; the mechanics are covered in detail in the compaction installment). The new context takes structured state as its seed message, and a doc comment spells out the label's intent (crates/agent-runtime/src/cycle_manager.rs:484-498):
labeled with
[CYCLE STATE]so the assistant can tell it apart from a real user turn.
The list portion is rendered by to_system_block (crates/tui/src/cycle_manager.rs:110-123). Although this function lives in the tui crate, what it produces is not a human-facing interface but the body of the seed message — this very text is what the model reads verbatim in the new context:
for item in &todos.items {
let marker = match item.status {
crate::tools::todo::TodoStatus::Pending => "[ ]",
crate::tools::todo::TodoStatus::InProgress => "[~]",
crate::tools::todo::TodoStatus::Completed => "[✓]",
};
out.push_str(&format!("- {marker} {}\n", item.content));
}
The same three-step list once more, as it looks at the opening of a new cycle (Workspace is an example value):
[CYCLE STATE — auto-preserved across the cycle boundary]
## Cycle State (Auto-Preserved)
- Mode: `AGENT`
- Workspace: `/Users/dev/CodeSmith`
### Work
Checklist (33% complete)
- [✓] Plan it out
- [~] Wire the thing
- [ ] Run gates
The seed is a user message, padded underneath with a synthetic assistant reply — its body a single sentence, "Acknowledged. State carried into the new cycle." (cycle_manager.rs:507-528) — and a comment gives the reason:
A user message expects an assistant ack so the next real user message lands on a clean alternation. We synthesize a one-line ack.
(One contrast is worth savoring: form two's reinjection pads no ack underneath — the consecutive user messages go out as-is — making this fastidiousness the cycle seed's alone.)
Taken together, the three forms amount to a trade-off statement: CodeSmith has said no to the classic recipe of "resend the TODO every turn." Resending spends budget on every turn — the three-line list above is about fifty tokens, and a hundred turns make a five-thousand-token repeat bill — and it must first answer "has it changed?"; CodeSmith skips even that judgment: no unchanged-detection on the tool side, and byte-level deduplication on the injection side happens only once, at the compaction boundary. Tool-output reflow costs nothing extra; the price is that the reminder's strength depends on the model's own diligence in calling the list tools. A cheap brain gets cheap reminders — this project's temperament, consistent to the end.
Posed as a textbook question, "how does state update" is a choice between two options, and each implementation carries a well-defined cache cost. Replace every turn: before each request, remove the previous turn's state message and append the latest at the end — the context always holds only the latest state, and the cost is cache invalidation from the removal point onward; fortunately, since the state lives at the end, the invalidated range usually covers only the single turn added since the last injection, and the bulk of the prefix is unharmed. Persistent append: once injected, a state message stays in the trajectory for good, and each turn only appends the new one — completely cache-friendly (append-only, never edited), at the cost of stale state accumulating in the context, spending tokens while forcing the model to single out "the latest one" by itself and ignore the outdated ones.
Claude Code's <system-reminder> is a card-carrying persistent appender. Locating CodeSmith on this coordinate system: the event side-channel appends persistently along with events; <turn_meta> appends per message and deduplicates through same-day byte stability; the TODO picks neither, relying on tool reflow plus boundary rebuilds — the three lives occupy one quadrant each, and the trade-off statement above thereby gains its theoretical coordinates.
Anomaly alerts: "you've already made this call three times"
The second kind of status-bar information: making environmental anomalies explicit. The flagship is the loop guard (crates/agent-runtime/src/engine/loop_guard.rs):
const IDENTICAL_CALL_BLOCK_THRESHOLD: u32 = 3;
const FAILURE_WARN_THRESHOLD: u32 = 3;
const FAILURE_HALT_THRESHOLD: u32 = 8;
The third call of the same tool with the same arguments is blocked outright and answered with (loop_guard.rs:38-42):
This call (
{tool}) has already been made {count} times this turn with the same arguments — try a different approach or change the arguments.
Three consecutive failures draw a warning, eight a halt (loop_guard.rs:54-63). Two engineering details deserve a look. The "duplicate" verdict uses a canonical-JSON hash of the arguments (keys sorted, then hashed, loop_guard.rs:76-111) — which is why paginated reads of the same file (a different offset) do not count as duplicates, a point locked down by a dedicated regression test, paginated_reads_are_not_false_positives. And these alerts are not standalone messages; they flow back as tool results — appended along with the event itself, so the model receives its correction vector at the very scene of the mistake.
Two more belong to the same family. LSP diagnostics: after every edit, before the next request, the accumulated compiler errors are bundled up and flushed as a single user message (engine/lsp_hooks.rs:79-95), in a compact format (lsp_diagnostics.rs:1-15):
<diagnostics file="crates/tui/src/foo.rs">
ERROR [12:8] missing semicolon
ERROR [13:1] expected `,`, found `}`
</diagnostics>
Failure here is silent — the comment says "a missing/crashing LSP server must never block the agent"; auxiliary information being absent must not hold up the main line. The envelope used when a tool result is compressed for exceeding the limit (engine/context.rs:330-338), meanwhile, writes out a purely engine-side fact, the character counts: (Original: {raw_chars} chars, omitted: {omitted} chars.) — how much got trimmed away is something the model has a right to know.
The side channel's dividing line: what stays out of the context
Side-channel information means auxiliary information that does not travel on the main data channel but helps in understanding events — exact times, durations, retry counts, that sort of thing. CodeSmith's handling of it is the cautionary-tale-shaped positive specimen I most wanted to discuss in this entire piece.
At the construction of the sub-agent completion sentinel sits a comment (crates/tui/src/tools/subagent/mod.rs:4562-4565) weighing whether to put the duration into the sentinel payload:
Keep this payload deliberately lean. ... Wall-clock duration is useful UI telemetry, but it is volatile and not useful for model coordination.
This one sentence draws the status bar's dividing line. Signals such as durations, exact timestamps, and retry countdowns all travel, in this system, on the human-facing side channel: retry state is a process-global RetryState that the footer reads every frame (retry_status.rs:1-14, #499); the token cost of background LLM calls (compaction summaries, cycle hand-off) is booked through a process-level cost pool (cost_status.rs:1-21, #526), and a comment says outright why a side channel rather than a callback — "those leaking callers are internal engine machinery and cannot get a handle on the App or the event channel."
Why keep these from the model's sight? Two reasons.
- Volatile signals teach the model nothing: knowing the sub-agent ran for 47 seconds does not improve the next decision; knowing that it finished, and what the result was, does.
- The old reason from Article 9: minute-granularity timestamps change every turn, and injecting them pours guaranteed-to-expire content into the context — it does not break the cache (they live in the append zone); it pollutes the corpus. Day-granularity dates are enough: knowing "today" is all it takes to judge whether "yesterday's" files are still fresh.
So CodeSmith's side-channel philosophy is triage, not stockpiling: every piece of metadata bound for the context must answer "what different decision will the model make because of this?" Whatever cannot answer gets diverted to the human interface. The CLI and the GUI work hard to keep the user sensing the system's state; when the same principle is executed on the model's interface, one more question must be asked — who pays for the sensing?
Anti-bloat: the injection ledger
The status bar lives in the append zone and never touches the prefix, but every one of its bytes is cold, hard input-token cash — and permanent cash at that: as Article 9 established, in an append-only world, garbage that enters the context is a liability for life. Hence the strict bookkeeping around injection.
-
Byte-level deduplication before reinjection: compare byte-for-byte against the current transcript, and if identical, don't send (
engine/mod.rs:2968-2975) — the day-granularity, byte-stable turn_meta is what makes "identical" comparable; -
A budget dry-run before reinjection: clone a copy of the history, push it in, and estimate the tokens; over budget, skip (
engine/mod.rs:2976-2984); - Memory recall (KoD) quotas: at most 5 items per turn, each capped at 30 lines or 10 KB, with a 10-second collection timeout that gives up silently (
knowledge/budget.rs:8-27); - Keep snippet previews of the last 12
read_filecalls, 1,200 characters each (session.rs:22-23), and when injecting them, add one line of advice: "Treat these as reminders only; re-read files before making claims."
Conclusion: make the implicit explicit, and pay only append-zone prices
To gather this installment into a three-element comparison of the status bar: task planning — the TODO reflows through tool output, is rebuilt after compaction as a <system-reminder> user message, and is relayed at the cycle boundary by a [CYCLE STATE] seed, with no periodic resend; the event side-channel — appended along with events as tool-result envelopes, diagnostic blocks, and block messages, with volatile signals triaged to the human interface; environment observation — <turn_meta> with its day-granularity date and working-set summary, git status left to the tools. And the identity all of these share: messages in the user role, hung at the end of the conversation — the protocol slot is borrowed, the semantics are declared by content tags, and the cache loses not one cent.
The status bar keeps "now" forever in the model's sight. But one class of operations is fated to break "now" — when the history grows too long and the window fills up, the past must be rewritten by hand. That is context engineering's one and only lawful crime.
Top comments (0)