How codeagent works: concepts and boundaries
Source:vignettes/architecture-concepts.Rmd
architecture-concepts.RmdLanguage: English | 简体中文
This article is the conceptual map of codeagent. It explains the order of a turn, the separation between authorization and extensibility, and the context lifecycle around every provider request. For file- and symbol-level structure, see Code architecture map.
The diagrams deliberately omit volatile counts, model names, and token thresholds. Detailed defaults remain in the linked topic articles and API reference.
Reading the visual language
| Visual | Meaning |
|---|---|
| Blue | user, host, entry, or portable result |
| Purple | model/provider work or model-generated summaries |
| Amber/red | policy, authorization, rejection, or recovery |
| Green | execution or a safe/validated result |
| Teal/gray | lifecycle, context, internal state, or persistence |
| Solid arrow | main call/data path |
| Dashed/dotted arrow | callback, state, or persistence relationship |
One foreground turn
Audience: User / Integrator
Sources: R/stream.R,
R/turn_pipeline.R, R/input_gate.R,
R/output_gate.R, R/sessions.R
A turn has five stable stages:
- Input boundary. Text and text-bearing attachments pass through the input gate before reaching the model. A Data Shield may redact, ask, or block.
- Turn setup. The harness adds dynamic context and reminders, manages resources, and prepares request-boundary compaction.
- Provider/tool rounds. ellmer drives one or more provider requests. A tool request enters the safety pipeline below, executes only after authorization, and returns a normalized result to the next provider request.
- Final response boundary. Finish reason and deterministic citations are finalized before the output gate. Shield/citation modes buffer the response; raw provider deltas never reach the browser first.
- Teardown and persistence. Usage and lifecycle hooks run, then the session stores lossless provider-facing state plus a presentation view.
The diagram shows codeagent_stream_async(), but the same
boundaries are reused by one-shot, REPL, and Shiny adapters. The
adapters own presentation; the Chat and turn services own model/tool
state.
Important turn invariants
- A tool preview is not approval.
- A failed or rejected tool does not bypass the central gate through another UI entrypoint.
- Shield and citation buffering occurs on the server, not by hiding data that was already sent to the browser.
- Session presentation may be redacted while lossless provider-facing state is retained separately; storage policy is an independent security concern.
Related articles:
Tool-call safety pipeline
Audience: Integrator / Maintainer
Sources: R/tools_gate.R,
R/tool_input_hook.R, R/hooks.R,
R/data_shield.R, R/stream.R
Three responsibilities are adjacent but intentionally separate:
-
Permission is the authority. The central gate
interprets tool metadata, enabled sets, capability policy, per-tool
overrides, modes, and rules. An
askdecision is resolved by the host’sask_fn; no callback means deny. -
Hooks are extension points.
PreToolUsemay deny or rewrite arguments. Rewritten arguments are sent through permission and Shield checks again.PostToolUseruns after the result path. - Data Shield protects model boundaries. Ingress scans arguments before execution; egress filters the result before it returns to the model. Shield bypass never bypasses the independent permission gate.
codeagent_stream_async(on_tool_request=) emits a
pre-gate preview for UI feedback. It must not create an
approval decision or mark execution as started. Actual approval is
driven by install_permission_gate(..., ask_fn=) and
correlated with the same tool-call id.
The result is normalized into three channels:
value portable model/UI text fallback
artifact versioned UI-neutral structured data
display optional shinychat adapter
Non-shinychat hosts consume artifact and fall back to
value; they do not parse display HTML.
Request-boundary context management
Audience: Integrator / Maintainer
Sources: R/compaction.R,
R/resource.R, R/turn_pipeline.R
Current compaction is attached to ellmer’s
on_request_start, so it runs before every provider
request, including internal tool-loop rounds. The callback’s
outgoing turns already contain the pending turn.
The ordering is intentional:
- Replace eligible large historical tool results.
- Perform initial model-aware accounting.
- Apply a cheap, budget-aware micro-snip before asking another model to summarize.
- Rebuild persisted history plus the original pending turn exactly once.
- Recount the rebuilt structure; stale provider usage is not a post-mutation lower bound.
- Only if still over budget, attempt incremental summary and then the optional full-summary fallback.
- Validate tool request/result pairing before sending the provider request.
If the provider still reports prompt-too-long/413, recovery drops complete historical API rounds, validates the result, and retries once. It never removes an arbitrary half of a tool request/result pair.
See Context compaction for thresholds, controls, and failure semantics.
State and ownership boundaries
| State | Owner | Sharing rule |
|---|---|---|
| Provider/model configuration | ellmer Chat | cloned or reconstructed only through verified paths |
| Tool set + permission callback | Chat / host | subagents may only narrow the parent’s actual tools |
| Turn lifecycle | harness | shared by one-shot, stream, REPL, and Shiny adapters |
| Data Shield engine | client/session | foreground codeagent-owned clones may share the same live engine |
| Browser UI state | host adapter | never an authorization source |
| Session JSONL | codeagent session store | contains lossless state and presentation records |
| Process worker state | worker process | receives an immutable security snapshot, not parent mutable state |
These boundaries explain why codeagent does not register
shinychat::chat_server(): codeagent must remain the single
owner of streaming, permissions, hooks, Data Shield, and persistence
even when shinychat owns the visual components.
Where details live
| Question | Article |
|---|---|
| How does the permission gate decide? | Permissions |
| How are three model boundaries protected? | Data Shield |
| How is context compacted? | Compaction |
| How do subagents and teams differ? | Team coordination |
| How can another UI host codeagent? | Backend integration |
| How should a UI consume tool results? | Tool artifacts |
| How are skills discovered and loaded? | Skills |