Reading contract. The entire chapter follows one request: “Continue yesterday's certificate investigation and produce a report.” By the end, you should be able to replay where that work starts, how Model and tools take turns, where progress and files are saved, and how Summary, Context Compaction, and Session Recall divide responsibility around one durable event ledger.
Evidence boundary.
Framework-overview links are pinned to public snapshot d3b50fd90d60b4ece8a78e89cf890a4f06d78235;
the context deep dive uses 0c7774187da9330144df2a038ef18ee89ef2ae1c, and experiments use
c9aabe56c0eb1cb80e927d2a34ecc72658173cbe. “Workbench,” “ledger,” and “projection” are explanatory summaries;
concrete behavior follows the linked source and reports.
1. See What A Support Agent Meets In Production
Imagine an enterprise support agent investigating a production error. It reads logs, searches old tickets, runs diagnostic code when needed, and produces a report. Calling a model once is easy. Finishing this work continuously and reliably is the real problem.
A request may come from the Web or another agent. Log lookup and code execution create side effects and files. Deterministic diagnostic steps cannot depend entirely on model improvisation. A restart must not lose the task. On day three, the user may only say, "the same error as yesterday." tRPC-Agent-Go brings these production duties, which all live outside the model itself, into one runtime path.
A useful first definition is: tRPC-Agent-Go is the task-running system around an Agent. Model generates the next step from the material it sees. Agent turns those steps into executable work. The framework must also own entry points, conversations, tools, files, recovery, and cleanup. The full lifecycle from accepting an external request to streaming the result and persisting completion is one run. A run may contain many model and tool calls; it is not one completion request.
2. Runner Is Not A Pipe; It Opens A Runtime Workbench
When the user says “continue yesterday's certificate investigation,” the system cannot immediately forward that sentence to a model. It must identify the user and conversation, reopen yesterday's record, choose the responsible Agent, prepare durable context and file storage, and keep execution cancellable, persistent, and streamable. Runner owns that workbench.
One run managed by Runner
1. Identify the user and open the current case
2. Select an Agent and create this run's work order
3. Prepare durable context and file services
4. Receive progress, persist it, and stream it outward
5. After completion, decide whether to summarize, remember, or review
This is a work allocation map, not another package list. Session, Memory, and Artifact are not distant side services. Runner holds their service implementations and places them into the work order available during execution. Runner also holds Evolution, but Evolution is different: it does not enter the foreground work order; it reviews the run after completion.
2.1 Runner Opens The Case And Supervises The Whole Run
Runner does not begin by reasoning about certificates. It first turns a request into managed work: get or create the Session, select the Agent, persist the current input, and register the run so cancellation, concurrency, and completion have an owner. Think of it as the support desk supervisor. The supervisor does not diagnose the certificate, but must keep the case, worker, reference material, output files, and reporting channel together.
The source reflects this ownership. The
runner fields
directly hold the Agent registry and Session, Memory, Artifact, and Evolution services.
Run
goes from session lookup to Agent.Run. The work order it creates is an
Invocation.
Invocation is not another worker. It gathers who will execute, which message and Session belong to this run, and which services are available.
2.2 Agent Accepts The Work Order And Chooses How To Finish It
Agent is the worker that accepts the work order. During the certificate investigation, it decides whether to read history or logs first, when to run diagnostics, and when enough evidence exists to deliver an answer. Different work needs different execution styles, so Agent does not mean one fixed LLM loop. LLMAgent alternates between a model and tools; GraphAgent follows a compiled state graph; Chain, Parallel, and Cycle compose multiple Agents.
The framework hides those differences behind the same
Agent interface:
Runner provides an Invocation, and Agent returns an Event channel. Runner can supervise the run without knowing whether execution is one model loop,
a graph, or several sub-agents running in parallel.
2.3 Model And Tool: One Proposes The Next Step, The Other Acts
Model is not the whole Agent. It sees the request prepared for this moment and returns text or a tool call such as “query the certificate store.” Tool performs the real query, code execution, or file operation. LLMAgent puts that result back into context and asks Model what comes next. One investigation can therefore repeat “model decision → tool action → model decision” several times.
The source boundary is equally narrow:
model.Model
only defines generation and model information. The provider call happens inside
llmflow.
Session persistence, the tool loop, files, and durable user context do not belong to Model.
2.4 Three State Services: Current Record, Durable Context, And Files
Continue The Current Investigation: SessionService
Session is the case file for this certificate investigation. It keeps what the user said, which tools Agent called,
and how results and state changed. Each progress record is an Event; SessionService creates, reads, and appends those records.
When the user returns tomorrow, Runner reopens “where yesterday stopped” from this service instead of asking a model to remember.
See
Session,
session.Service,
and
Event.
Carry Stable Context Into Another Case: MemoryService
Session answers “what happened in this case.” If a new case should still know that the user's production environment uses Go,
that stable fact belongs to MemoryService. It stores facts and episodes across Sessions, which can be preloaded or searched on demand,
without replacing the current case's raw record. See
memory.Service.
Keep Reports Out Of The Transcript: ArtifactService
Diagnostic scripts, log extracts, and reports may be large and revised repeatedly. Putting every file into Event text makes versioning and stable UI references difficult.
ArtifactService stores files by Session, name, and revision. Agent creates or reads them through tools while Session keeps the action and visible result.
The service contract is
artifact.Service.
One more service runs only after foreground work: Evolution Service. It does not supply evidence for the current investigation or store user facts. After completion, Runner gives it a Session snapshot so it can decide whether a method such as “check certificate expiry first” should become a reusable Skill. A later section follows that background lifecycle in full.
3. Follow One Support Investigation Through A Complete Run
3.1 The Request Enters Runner, Not The Model
An AG-UI client sends “continue yesterday's certificate investigation and produce a report.” The protocol adapter first translates the external request into the user, Session, and message Runner expects. A2A and OpenAI-compatible endpoints can also start a run, but they carry identity and history differently. Protocol code translates; Runner executes.
3.2 Runner Opens The Workbench And Builds The Invocation
Runner uses app, user, and Session identifiers to reopen yesterday's case, selects the support Agent, then builds the Invocation.
That work order contains the message, Session, Agent, run options, SessionService, MemoryService, and ArtifactService.
Runner persists the current input before starting Agent, so a later model or tool failure cannot make the request disappear.
The handoff appears in
session lookup, Invocation setup, and pre-run persistence
and
newRunInvocation.
3.3 Agent Lets Model And Tools Take Turns
Once Runner calls Agent, execution can branch. Assume this run selects LLMAgent. It prepares the relevant Session history, any preloaded Memory, available tools, and current message for Model. Model first requests certificate details; a tool returns the evidence; LLMAgent calls Model again. Model then requests diagnostics; CodeExecutor performs the work, and ArtifactService stores the report.
User: Continue yesterday's certificate investigation and produce a report
Runner: Reopen the case; prepare Agent, durable context, and file services
Agent: Assemble the current material and ask Model for the next step
Model: Query the certificate details
Tool: Return evidence that the certificate expired
Model: Run diagnostics and produce a report
CodeExecutor / ArtifactService: Execute diagnostics and save the report
Model: The evidence is sufficient; produce the final explanation
The example establishes three boundaries: Runner owns the whole run, Agent owns the execution loop, and Model generates one step's text or tool call.
In source, Runner calls
Agent.Run,
and LLMAgent enters the
LLM flow.
3.4 Events Return Progress To Runner; Background Work Starts Later
Text chunks, tool calls, tool results, errors, and state changes become Events. Runner streams them back through the protocol layer
while SessionService appends qualifying complete events. Event is therefore the transport format for progress, not another worker.
Persisted events may trigger a summary check. After Runner emits completion, it can enqueue auto-memory and evolution work.
The source path is
processSingleAgentEvent,
handleEventPersistence,
and
the completion path.
| Event shape | Visible to protocol? | Appended to Session? | Can trigger Summary? |
|---|---|---|---|
| Partial text chunk | Yes, for live display | Normally no; cancellation recovery is a separate synthesized path | No |
| Complete user message | Yes | Yes, as input evidence | No; user input alone does not enqueue |
| Complete tool call | Yes | Yes, preserving call and ID | No; wait for result or final text |
| Complete tool result | Yes | Yes, preserving its tool-call relation | Yes, though the checker may no-op |
| Complete assistant text or valid error | Yes | Yes; error first receives persistable content | Yes, unless SkipSummarization |
| State delta only | Depends on protocol translation | Yes even without a complete response | No without valid content |
| Runner completion | Yes, as run terminator | It is a completion signal, not conversation body | Enqueues auto Memory and Evolution instead |
An Event already has identity when it is constructed and injected with invocation metadata; Runner persists that same complete Event. Partial tokens do not each become Session rows. The persistence predicate is small: state delta exists, or the response is complete and valid. Summary applies another filter only after append succeeds, so “streamed,” “durably recorded,” and “starts maintenance” are three distinct conditions.
We can now replay the run: the protocol layer accepts the request → Runner opens the workbench → Agent coordinates Model and tools → Events report progress → Runner persists and closes the run → summary, memory, and evolution handle later state when their gates fire. When the user returns tomorrow, Runner reopens the same Session. The illustration at the top focuses on how older records and durable context enter that next model view.
4. Only Now Compress The Run Back Into Source Owners
The reader now knows when every name appears in the investigation. The table compresses those already-taught questions into source entry points instead of sorting packages by feature count. The broader project scope is visible in the README feature map.
| Question in the run | Source owner | Reader model | First source |
|---|---|---|---|
| Who opens and closes the whole run? | Runner / Invocation | Runner opens and supervises the workbench; Invocation is the work order passed to Agent. | runner fields, Invocation |
| Who chooses the execution style? | Agent family | LLMAgent, GraphAgent, and composed Agents execute through one interface and emit Events. | Agent, LLMAgent.Run |
| Who only generates the next step? | Model adapters | Send a normalized Request to a provider and return streamed Responses to Agent flow. | model.Model |
| Where is this run's progress preserved? | SessionService + Session / Event | Session is the case file, Event is an ordered progress record, and Service reads and writes them. | Session, Event |
| How does a new case recover stable old context? | MemoryService | Store facts and episodes across Sessions, then preload or retrieve them on demand. | memory.Service |
| How are reports and scripts stored separately? | ArtifactService | Version files by Session and name instead of embedding file bodies in conversation text. | artifact.Service |
| Who performs an action after Model requests it? | Tool / Skill / CodeExecutor | Expose queries, reusable workflows, and code execution as bounded Agent capabilities. | tool.Tool, CodeExecutor |
| How can this run's method become a reusable Skill? | Evolution Service | Review the completed Session and send procedural learning through controlled Skill publication. | evolution.Service |
| How do Web, agent, and compatible clients connect? | Server adapters | Translate each identity, history, and event contract into Runner calls and back. | AG-UI, A2A, OpenAI-compatible |
The table only compresses a flow we already followed. The reusable model is: Runner opens the workbench and holds services; Agent accepts the work order; Model generates one next step; Tool performs the action; Event carries progress into Session. The final three deep dives follow that Event ledger through three pressures: the current session grows too long, future sessions still need stable facts, and later tasks should reuse a proven procedure.
5. How The Runtime Workbench Expands Into A Framework
Once the basic run is clear, other mechanisms appear in response to concrete pressure. A deterministic route calls for GraphAgent. Queries, code execution, and reusable operating procedures call for Tool, CodeExecutor, and Skill. Generated files go to ArtifactService. Repeated task patterns can be reviewed by Evolution after the run. Web, agent, and compatible clients connect through protocol adapters around Runner. The following sections select the paths that materially change application design.
5.1 GraphAgent: A Workflow Graph Returns Through The Agent Interface
Start with the simpler design that would fail in production: put "run A, then B, maybe in parallel, then summarize" into a system prompt. That can work in a demo, but it cannot guarantee routing, branch completion, interrupt recovery, or how LLM nodes, tool nodes, and sub-agent nodes write events back into one session.
tRPC-Agent-Go moves the decision about “where to go next” out of the prompt and into a state graph defined in Go. A node can be an ordinary function, a model call, a tool call, or another Agent. Edges express sequence, branches, and joins. The graph is validated before execution, so unreachable endings and invalid routes do not have to be discovered in production. Sources: node registration and edges, joins, conditions, and compile.
| Product pressure | Runtime action | Invariant protected | Source |
|---|---|---|---|
| The app needs a reproducible multi-step workflow. | StateGraph compiles nodes, edges, conditional routing, and joins into an executable graph. |
The workflow shape is validated by Go code instead of being left to model compliance. | state_graph.go |
| The app still wants to call the graph like an agent. | New passes the graph, channel buffer, concurrency, checkpoint saver, and execution engine to the executor. |
Graph does not become a separate product surface; it still follows the common agent.Agent run semantics. |
graphagent.New, NewExecutor |
| One request must bring session history, summary, filters, and parent-agent metadata into the graph. | GraphAgent.Run builds the initial state; createInitialState reuses the content processor for messages, summary, branches, and history filters. |
Graph nodes see the same model-visible history rules as a normal LLMAgent. | GraphAgent.Run, createInitialState |
| Execution needs concurrency, checkpointing, resume, barrier events, and error handling. | Executor.Execute creates an ExecutionContext per run, filters completion and barrier events, prepares checkpoints and pending writes, then enters the DAG or BSP loop. |
The executor can be reused safely; mutable per-run state does not leak into the next run. | Execute, executeGraph |
So GraphAgent is not merely another way to write an agent. It owns workflow control: the model generates content and tool arguments,
while the runtime owns routing, concurrency, joins, checkpoints, resume, and graph events. That boundary matters later: summary and session history
are not add-ons outside the graph; they are projected into the initial state by the same processor used by the rest of the runtime.
5.2 Skill + CodeExecutor + Artifact: Capabilities Become A Workspace
Skill is easy to misread as a prompt-snippet library. The stronger reading is that a skill is a packaged capability:
instructions, optional docs, optional scripts, and a controlled workspace. The model should not read every supporting file up front, and it should not
receive arbitrary shell access.
Read The Instructions Before Running Anything
Return to the certificate investigation. When the Agent decides that it needs to produce a diagnostic report, it first finds that capability in the
Skill repository and reads its instructions. If those instructions reference a report format or troubleshooting guide, it opens those documents only when needed.
Loaded state is recorded in the current Session, so the next model call knows what has already been read without receiving the entire library again. This path starts at
FSRepository.Refresh
and
skill_load.
Open A Controlled Workspace Only When Work Begins
Knowing how to do something and receiving permission to execute it are different capabilities. A read-only Agent can receive only Skill instructions; an Agent that must run a script can additionally receive workspace and process-control tools. LLMAgent assembles that tool surface from configuration, so every Skill does not receive arbitrary shell access by default. The same capability can therefore be read as guidance or, when explicitly enabled, become an observable and cancellable execution. Sources: tool-surface construction, skill profiles, and Skill tool assembly.
| Runtime step | What the runtime does | Why this is not prompt concatenation |
|---|---|---|
| The model decides it needs a skill. | skill_load records loaded state and optional docs, without running scripts. |
Capability instructions can be opened gradually, and the session can track what was loaded. |
| The model needs to run the skill. | skill_run may require prior loading, applies artifact-save overrides, prepares a workspace, executes the program, and collects outputs and manifests. |
The execution path has state, arguments, files, and artifact references. It is not a paragraph of advice. |
| The script needs a writable directory. | stageSkill creates a writable copy; command policy uses allow / deny lists and CleanEnv. |
The runtime can separate skill source files, working copies, and process environment. |
| The run produces a report, image, or intermediate file. | workspace_save_artifact persists an existing workspace file, returns an artifact:// reference, and records it in state delta. |
Large files do not need to be inlined into model messages; later UI, sessions, and tools can dereference them. |
The three easily confused owners can now be separated. CodeExecutor answers “how does this machine run code?” A workspace is the temporary worksite for this run. ArtifactService decides which results leave that worksite as durable files. In the certificate investigation, a script writes the report in the workspace, the executor runs that script, and ArtifactService finally persists the report with app, user, and session ownership. Follow the implementation through Skill execution, workspace staging, and artifact persistence.
5.3 Evolution Service: Skills Can Be Learned After The Run
Once skills can be loaded and executed, the next question is where new skills come from. The tempting design is to let the foreground agent edit
SKILL.md while it is solving the user task. That mixes the user's request, file writes, durable facts, one-off outputs, and library maintenance.
A model can easily turn a private user fact into a general skill, or preserve a task-specific procedure that should never be reused.
tRPC-Agent-Go schedules that review after the run. Runner first hands the current Session to a background worker so the user receives the result promptly. The worker reads only what changed since the last review and looks for evidence worth preserving: repeated tool coordination, a user correction, or a successful recovery after failure. Weak evidence is skipped; strong evidence is sent to a reviewer to decide whether it represents a reusable method. Evolution therefore learns “how to handle this kind of task,” not “what this user just said.” Start with Runner's post-run enqueue, incremental review, and the review gate.
| Learning stage | What the runtime does | Invariant protected | Source |
|---|---|---|---|
| Queue after completion | Runner queues auto memory and evolution learning after completion; the worker owns its own timeout. | The user response is not blocked by review, and learning is not cancelled just because the request context ended. | completion hook, worker.Enqueue |
| Build review input | The worker reviews only the delta since the last review and gives the reviewer the current skill names, descriptions, and body excerpts. | Learning does not re-read infinite history, and it does not create duplicates because it ignored the existing library. | ReviewInput, buildUserPrompt |
| Reviewer decision | The LLM reviewer may only return JSON containing skip_reason, skills, updates, and deletions. |
Evolution owns only the skill library; durable facts and episodes stay in the memory pipeline. | review prompt, ReviewDecision / SkillSpec |
| Publish or hold | The raw decision is reconciled against the library. With revision governance enabled it becomes a candidate revision, but only the configured Spec, Safety, Effectiveness, or Human gates run. | Each configured gate protects only its own invariant; an absent gate is not evidence that its risk was validated. | reconcileWithLibrary, processRevision, runGates |
A reviewer's suggestion is first compared with the existing library. Reconciliation reduces duplicates and conflicts on a best-effort basis; it is not complete quality proof. Spec, Safety, Effectiveness, and Human gates are independently optional. The runtime executes only those the application configures, and without HumanGate there is no human approval step. If all revision and gate components are absent, compatibility mode still direct-publishes. Only a governed deployment may therefore be summarized as “candidate → configured checks → optional approval → publish.” Publisher write plus repository refresh makes the body visible to future runs. The paths are implemented in library reconciliation, publication gates, and external approval.
Self-evolution is therefore not "the model edits its own prompt." It is a post-run skill-library maintenance pipeline. It reads session deltas and writes managed skills. It can use evaluator outcomes for failure-aware learning, but it does not write facts into memory. That is why it belongs right after the Skill section: without skill loading, execution, isolation, and repository refresh, evolution would have nowhere clean to land.
5.4 AG-UI: The Frontend Protocol Owns Input, Runs, And Event Translation
The protocol layer is easy to underestimate. A text stream only has to emit assistant deltas, but a real Agent UI must answer three larger questions: has this run started or finished, where is the current tool or graph step, and how do files, approval waits, or cancellation requests map back to the same run? AG-UI translates those runtime-only states into protocol events that a frontend can consume reliably.
On input, the adapter turns browser messages into one Runner call. A normal user message becomes fresh input. If the browser is returning results from an external tool, trailing tool results are grouped into the same run before execution resumes. That distinction prevents “continue the previous tool call” from being mistaken for a new conversation. Read input parsing before service assembly.
During execution, the adapter registers the run so that a later cancellation request can find the correct work. It then calls the core Runner and consumes the Event stream from the Agent.
For a graph resuming after an interrupt, it first restores the resume signal before letting execution continue. The path is in
Runner.Run
and
the internal run loop.
On output, the translator acts as a ledger for this run. It remembers which text fragments, tool calls, and files have already been emitted, then turns internal Events into frontend start, delta, tool-progress, artifact, and finish events. A retry or multi-part stream therefore does not render the same call twice, and a report attachment remains associated with the correct run. See translator run state and the event translation path.
That is why the protocol layer is not just an HTTP shell. UI-visible run IDs, tool-call IDs, graph activity, artifact references, external tool results, and final completion are not naturally present in model text. The AG-UI adapter translates runtime events into a protocol the frontend can consume reliably.
5.5 A2A And OpenAI Compatibility: One Runner, Different Continuity Contracts
AG-UI is only one server-plane exit. A2A and the OpenAI-compatible server also reach the same Runner, but they disagree on how continuous work is identified, who carries history, and which events leave the runtime. Comparing all three prevents a protocol adapter from looking like a simple URL change.
| Protocol surface | How continuity enters Runner | How results leave Runner | Boundary to remember |
|---|---|---|---|
| AG-UI | RunAgentInput carries trailing user/tool messages, runtime state, external tools, and run semantics. |
Rich graph, text, reasoning, tool, artifact, cancellation, and finish events. | Continuity is organized around a frontend run and recoverable UI state. |
| A2A | An AgentCard advertises capability; the message processor maps ContextID to a session and message metadata to runtime state. |
The task manager emits submitted/status, artifact update, completed, or structured failure results. | Continuity is organized around cross-agent context and task lifecycle. |
| OpenAI-compatible | /v1/chat/completions treats the last message as current input, passes earlier messages through WithMessages, and continues a session with X-Session-ID. |
Non-streaming aggregates internal events; streaming emits Chat Completions SSE chunks followed by [DONE]. |
This is Chat Completions compatibility, not the Responses API item/event model. |
The A2A entry can receive an existing Runner or build one from an Agent; a Runner supplied directly also
requires an AgentCard. buildProcessor assembles converters in both directions: A2A messages to
agent messages, and internal events to A2A results. During processing, ContextID supplies session
continuity, message metadata becomes runtime state, and Runner events become task status, artifacts, and
completion. Sources:
a2a.New,
buildProcessor,
message and streaming task setup,
and
task status / artifact / completion translation.
The OpenAI-compatible layer deliberately narrows itself to /v1/chat/completions. It can reuse an
external Runner or create and own one for an Agent. The converter takes the final message as current input
and threads earlier messages through agent.WithMessages. Non-streaming requests still collect and
aggregate every internal event; streaming requests convert events into SSE chunks and finish with
[DONE]. In this snapshot, request-level generation parameters such as temperature and max_tokens
are ignored and should be configured when the Agent is created. Sources:
server and route setup,
non-streaming history, session, and aggregation,
and
streaming SSE path.
Sharing Runner unifies the execution core; it does not make wire semantics interchangeable. Frontend resume, cross-agent tasks, and OpenAI SDK compatibility require different identity, history, and event contracts. Moving between them is therefore more than replacing an endpoint.
Map the runtime first, then context governance makes sense. Graph, skill workspaces, Evolution, and AG-UI all show the same boundary: the model is not the runtime. Summary, Compaction, and Recall are not isolated features. They form naturally once Runner persists Events, processors build model requests, tool surfaces expose recovery, and server adapters keep sessions alive.
6. Put The Three Deep Dives On One Time Map
The certificate investigation can now run, persist progress, and produce files. The harder questions appear with time.
Yesterday's conversation is growing but must continue today. A new conversation next week should still know the user's stable environment.
A future incident should reuse the procedure learned here. Those are not one memory switch; they are three time horizons.
| Time horizon | The reader's actual problem | Main mechanisms | Write target |
|---|---|---|---|
| Current session | How can old messages shrink without making exact evidence unreachable? | Summary, Context Compaction, Session Recall | Raw Session Events and Summary boundary |
| Across sessions | How do stable facts and past episodes return when the user starts a new session? | Memory extraction, retrieval, and update | App/user-scoped Facts and Episodes |
| Future tasks | How does a success or failure become a reusable procedure? | Evolution review, revision, and gates | Managed Skill revisions |
6.1 Where Each Background Path Stores Its Cursor
| Path | Delta cursor owner | Whole-job failure | Successful effect |
|---|---|---|---|
| Summary | Session.Summaries[key].Boundary | Text and boundary do not advance | Later model views; a synchronous path may affect this run |
| Auto Memory | Session.State[memory:last_extract_at] | Search/extractor/job failure does not advance; individual write failures may still advance the batch cursor | App/User Memory store for future Sessions |
| Evolution | Session.State[evolution:last_review_at] | Policy/reviewer error does not advance; completed review advances even after gate rejection | Candidate revision; publisher write plus repository refresh reaches future Agents, while ActivePointer records the governed version |
All three use Session to remember how far raw Events were scanned, but they are not one transaction. Summary writes model-view state back to Session; Memory writes a user-scoped store; Evolution writes a revision store. Cursor location does not imply identical consistency for their outputs.
The tRPC-Agent-Go deep read therefore ends in three chapters. This chapter asks how the current session becomes shorter yet recoverable. The next explains cross-session Memory. The final chapter follows Evolution from run outcome to validated skill revision. All three may read the same session, but they write different objects, become visible at different times, and fail differently.
7. Context Is This Turn's Model Projection, Not The Raw Record
When the user returns with “continue yesterday's certificate issue,” Session still contains yesterday's user messages, model replies, and tool results. Sending all of them on every turn makes each request grow. Deleting them would make a later question about an exact certificate serial number impossible to verify. tRPC-Agent-Go separates the durable ledger from the model view.
Durable layer: Session.Events
yesterday's question -> log tool call -> 12 KB result -> early conclusion -> today's question
Summary boundary: Summary{Text, CutoffAt, LastEventID}
“Network ruled out; inspecting the certificate chain” + “covered through event-42”
This turn's model view
Summary text + messages after event-42 + current user input
Session
owns both Events and filter-keyed Summaries. During request construction, the
content processor
reads the Summary boundary and appends only Events after that cutoff. Summary changes what the model sees this turn;
it does not rewrite the durable Session into one short paragraph.
Three overloaded words need distinct meanings here. History is the set of Events that actually happened in the Session. A Summary is a durable handoff note with a coverage boundary. Context is the temporary input that processors assemble for one model call. Context can disappear after that call; History remains the evidence; Summary remains reusable state. The same Event can therefore still exist in History while no longer appearing in Context because it sits before the Summary boundary.
| Object | Owner | May it be lossy? | How a later turn sees it |
|---|---|---|---|
Session.Events | Session Service | Compaction must not rewrite or delete it | Request projection or on-demand Recall |
Session.Summaries[key] | Session Service | Yes, but only with a coverage boundary | Injected as system or user context |
model.Request.Messages | The current LLM flow | May be trimmed, replaced, and rebuilt | Valid only for this model call |
One invariant comes first: compression may be lossy, but raw evidence must not disappear because of it. Summary, Context Compaction, and Session Recall respectively generalize old facts, drop expensive payloads, and return to the ledger for evidence.
8. Summary Advances Incrementally Instead Of Rewriting Everything
After the first part of the investigation, the runtime can compress completed work into a handoff note. The naive next update would summarize the entire transcript again. That grows more expensive with every turn and repeatedly paraphrases old facts. Rememorio's rolling Summary keeps a boundary, so the next update only processes Events that arrived after the previous one.
8.1 What One Rolling Update Does
| Step | Change in the certificate investigation | Source owner |
|---|---|---|
| 1. Test the trigger | If event, token, or context pressure stays below its threshold, do nothing. | SessionSummarizer |
| 2. Compute the delta | Read the old Summary and select only certificate checks after its boundary. | SummarizeSession |
| 3. Update the handoff | Use the old Summary as known background and merge the new events. | buildSummaryInput |
| 4. Advance the boundary | Persist the new Summary and record the last Event it covers. | boundary selection |
Routine Summary work can run asynchronously after Runner persists an Event, keeping it off the foreground response path. That creates a limit: a queued job cannot rescue the request that is already approaching the model window. The runtime needs a second, in-request pressure path. That path is Context Compaction.
8.2 The Boundary Makes The Next Update Delta-Only
Suppose the old Summary covers through event-42, and today appends event-43 through event-51.
SummarizeSession computes the delta after the old boundary, then presents the old Summary as a synthetic system Event
before those nine new records. The summarizer reads “one handoff note plus nine additions,” not all 51 raw Events. Once it returns new text,
the runtime advances the boundary to the latest covered Event and writes text, UpdatedAt, and boundary together.
Before
summary = "Network ruled out"
boundary = event-42
Summarizer input
system: "Network ruled out" // previous Summary
event-43 ... event-51 // new Events only
After
summary = "Network ruled out; intermediate certificate missing"
boundary = event-51
With locking and logging removed, the source path that advances state is compact:
// session/internal/summary/summary.go (excerpt)
input, ok := buildSummaryInput(ctx, m, base, filterKey, force, prev)
if !ok {
return false, nil
}
text, err := m.Summarize(input.ctx, input.session)
if err != nil {
return false, fmt.Errorf("summarize session %s failed: %w", base.ID, err)
}
if text == "" {
return false, nil
}
boundary := selectSummaryBoundary(
input.session, filterKey, prev.boundary,
input.latestBoundary, input.hasDelta,
)
writeSummary(base, filterKey, text, boundary.CutoffTime(), boundary)
return true, nil
Text and coverage must move atomically at the algorithm level. Updating text without the boundary summarizes the same Events again next time.
Advancing the boundary before obtaining text hides evidence that never entered the Summary. The implementation therefore calls
writeSummary only after the summarizer returns non-empty text. See
SummarizeSession
and buildSummaryInput.
The branches expose three distinct boundaries. A false buildSummaryInput result means trigger or delta did not qualify, so no model call
occurs. A summarizer error or empty text leaves prior state untouched. Only non-empty text reaches boundary selection and one
writeSummary call carrying both text and coverage. “Together” describes this function-level transition; it does not claim that every
Session backend provides a cross-service database transaction.
8.3 A Valid Run Often Does Nothing
Rolling Summary does not mean “call a model after every run.” A missing summarizer, an empty post-boundary delta, a trigger below threshold,
or empty model output returns updated=false and leaves the old Summary untouched. Runner also does not enqueue after every Event:
it persists first, skips user input, bare tool calls, and invalid Events, then normally considers Summary after a tool result or final assistant text.
With intra-run synchronous Summary enabled, intermediate tool rounds skip the asynchronous job so two paths do not summarize the same delta.
The summarizer configuration owns the trigger; the framework does not silently fix one policy. With WithContextThreshold, the default
fires at 50% of the dynamically resolved model window, never below 2,000 tokens, and falls back to an 8,192-token window when resolution fails.
Applications may use event-count or custom checkers and another ratio. This 50% line belongs to routine rolling Summary; it is distinct from the
70% in-request Compaction emergency threshold below.
A per-session, per-filter lock serializes concurrent updates. It does not block unrelated sessions; it prevents two workers from both reading
boundary event-42, independently summarizing through event-51, then overwriting one another. Filter keys also let a Graph
branch own a branch Summary while a full-session Summary advances only when its cascade rules permit. One Session need not collapse every branch
into a mixed handoff note.
8.4 Background, Cache-Safe, And Emergency Paths Are Different
| Path | When it is used | Blocks the current response? | First observer |
|---|---|---|---|
| Post-run asynchronous Summary | A normal run finishes and its trigger fires | No | A later request after persistence |
| Intra-run Summary | Explicitly enabled for a long tool loop | Yes, before the next model call | The next LLM iteration in the same run |
| Pressure-triggered Compaction | The request approaches the context window | Yes | The rebuilt current request |
Cache-safe forking addresses a separate cost. The Summary request tries to retain the current model request's stable prefix and append only the compaction instruction, giving providers a chance to reuse prompt cache. If no parent exists or the parent cannot fit the Summary model's window, it falls back to a bounded standalone input. This changes how Summary input is delivered, not what the Summary means.
9. Context Compaction Cleans Tool Payloads Before It Summarizes
The log tool has returned 12 KB. The model already consumed most of it and concluded that an intermediate certificate is missing. Old tool payloads are often the largest and safest material to reduce. tRPC-Agent-Go therefore does not immediately summarize the whole session at the first threshold. During model-view construction, it applies up to three passes from cheaper to more invasive.
| Pass | When it changes a result | Transformation | Invariant protected |
|---|---|---|---|
| Pass 0: policy cleanup | The application explicitly names a tool as cleanable. | Replace its result with a successful-call placeholder. | Application-specific tool semantics. |
| Pass 1: historical cleanup | An old tool result exceeds the default 1,024-token threshold. | Protect the latest completed request; replace older large results with recoverable placeholders. | Recent reasoning continuity and side-effect safety. |
| Pass 2: oversized truncation | Any result exceeds an explicitly configured cap. | Keep head and tail around a truncation marker; 8,192 is suggested but disabled by default. | One extreme current result cannot exhaust the window. |
9.1 All Three Passes Rewrite A Request Copy
The defaults and placeholders live near the top of
context_compact.go;
compactIncrementEvents
fixes the pass order. A placeholder does more than say “deleted.” It records that the call succeeded and was consumed, carries
event_id, tool_call_id, and tool name, and recommends session_load when that tool is actually available.
Otherwise it permits only safe read-only or idempotent reruns. This prevents the model from interpreting compaction as a failed call
and repeating a side effect.
compactIncrementEvents first copies the Event slice. When a message actually changes, its helper also clones
the nested Response, so the durable Event in Session Service does not change.
Pass 0 only applies an application-declared tool-name policy, and a keep allowlist can protect tools that must remain visible. Pass 1 protects
the current request and a configurable recent tail. Pass 2 is the only one allowed to touch an extreme result in the current request, and its
default threshold is zero, meaning disabled. These conservative defaults matter because a new tool result may still be unconsumed evidence.
// internal/flow/processor/context_compact.go (excerpt)
compacted := make([]event.Event, len(events))
copy(compacted, events)
if forceCleanActive {
passEvents, passStats := applyForceCleanToolResultPass(
ctx, compacted, protectedRequestIDs, cfg,
)
compacted = passEvents
stats = mergeContextCompactionStats(stats, passStats)
}
if pass1Active {
passEvents, passStats := applyHistoricalToolResultPass(
ctx, compacted, protectedRequestIDs,
cfg.ToolResultMaxTokens, cfg,
)
compacted = passEvents
stats = mergeContextCompactionStats(stats, passStats)
}
if pass2Active {
passEvents, passStats := applyOversizedToolResultPass(
ctx, compacted, currentKey,
cfg.OversizedToolResultMaxTokens, cfg,
)
compacted = passEvents
stats = mergeContextCompactionStats(stats, passStats)
}
return compacted, stats
The first two lines provide only the first isolation layer: helpers replace elements in a new slice
instead of assigning directly to events[i]. An event.Event still contains a Response
pointer, so that shallow copy alone does not prove nested messages are immutable. On the first actual replacement,
rewriteToolResultEventMessages clones the Response and writes the new message into that clone:
clonedResponse := evt.Response
// ...first determine whether msg needs rewriting
if !choiceChanged {
clonedResponse = evt.Response.Clone()
choiceChanged = true
}
clonedResponse.Choices[j].Message = msg
// ...
evt.Response = clonedResponse
The full invariant therefore needs two copies: the slice copy isolates Event replacement, while
Response.Clone() isolates nested content mutation. The three if blocks also freeze policy order; a disabled pass disappears
entirely rather than asking a helper to guess. Stats are merged beside each transformed copy, so observability reports what was actually replaced,
not only the final token count.
event-37 in Session (unchanged)
tool: certificate_scan
content: "... 12 KB raw payload ..."
event-37 in this request after Pass 1
tool: certificate_scan
content: "Call succeeded and its result was consumed; event_id=event-37;
use session_load for exact slices if needed"
9.2 Only A Still-Oversized Request Forces Synchronous Summary
After projection, LLM flow estimates message tokens. By default, it checks for synchronous compaction at 70% of the model context window. Missing Session services, unsupported Summary setup, or an unavailable safe-rebuild snapshot all make it skip. When it does fire, it passes the complete current request to cache-safe Summary as a parent request, waits for the boundary to advance, and rebuilds from the snapshot captured before content processing.
Old request: old Summary + many post-boundary Events + current input + tools
|
| tokens >= 0.7 * context window
v
Cache-safe Summary: preserve the parent request's stable prefix; append a compaction instruction
|
v
New request: new Summary + a few post-boundary Events + current input + tools
// internal/flow/llmflow/llmflow.go (diagnostics omitted)
before := snapshotSummary(invocation.Session, filterKey)
err := invocation.SessionService.CreateSessionSummary(
summaryCtx, invocation.Session, filterKey, false,
)
after := snapshotSummary(invocation.Session, filterKey)
updated := before.advanced(after)
if !updated {
return req
}
rebuilt := f.rebuildRequestForContextCompaction(
ctx, invocation, rebuildPlan,
)
if rebuilt == nil {
return req
}
if err != nil {
log.WarnfContext(ctx, "summary advanced in memory; persistence failed: %v", err)
}
return rebuilt
The trigger, synchronous Summary, and rebuild are implemented by
maybeCompactContextBeforeLLM,
runContextCompaction, and
rebuildRequestForContextCompaction.
The boundary is precise: Summary is durable rolling state; Context Compaction is the pressure policy that cleans payloads,
invokes Summary when needed, and rebuilds the model view.
The rebuild condition is not “the Summary call returned no error.” It compares snapshots through advanced. A no-op may have no error,
but an unchanged boundary cannot produce a shorter projection. Conversely, in-memory state may advance while persistence reports an error; the current
request can still rebuild for pressure relief, with a warning recording the split. Finally, rebuilt == nil protects replay safety:
when processors cannot be reconstructed coherently, the runtime keeps the old request instead of producing a half-old, half-new Context.
9.3 A New Summary Requires A New Request Projection
Rebuildable request pipeline
raw request snapshot
-> content processor: inject Summary and project post-boundary Events
-> context compaction: replace or truncate tool payloads
-> tail processors that declare replay support
-> Model
Synchronous Summary changes Session state; rebuild replays the last three stages.
A persisted Summary update does not mutate the old model.Request, which still contains the old Summary and post-boundary Events.
LLM flow must return to a snapshot captured before content processing and replay “read boundary, project Events, compact tool results.”
To avoid replaying opaque custom processors, this path requires an all-events timeline, Summary injection in the content processor,
a safe rebuild plan, and tail processors that explicitly support rebuilding.
Failure is biased toward completing the current request. If the token threshold is not crossed, Summary does not advance, a service is missing, or rebuild is unsafe, the original request continues. A Summary error also does not fail the user run. If in-memory Summary state advances but persistence fails, the current call may still use the shorter rebuilt request while the runtime records a warning. Compaction is pressure relief, not a new business transaction.
| Result | Current call | Durable Session | Next request |
|---|---|---|---|
| Summary unchanged or rebuild unsafe | Continue the original request | Unchanged | Starts from the old boundary |
| Summary and persistence succeed | Use rebuilt short request | New text and boundary | Reliably reads the new boundary |
| In-memory state advances, persistence fails | May rebuild from memory | May still have old boundary | Same live Session may see new state; backend reload sees old state |
The last row does not promise automatic repair. The runtime warns, but does not turn pressure relief into a distributed transaction or guarantee that the next request retries successfully. Deployments need SessionService failure metrics, Summary reports, and later persistence work to detect and converge the split.
10. Session Recall Returns To The Raw Ledger For Evidence
“The certificate chain is incomplete” is enough for most turns. If the user asks for yesterday's exact serial number, however, the Summary may not contain it. Restoring the entire transcript would erase the savings; guessing from the Summary would sacrifice correctness. Session Recall uses two steps: search first, then load a small raw window around the hit.
session_search(query="certificate serial", scope="current_hidden")
-> event-37 before the current Summary boundary
session_load(event_id="event-37", before=1, after=1)
-> only the nearby user message, tool call, and tool result
10.1 Search Finds An Anchor Before Loading Raw Text
current_hidden uses the active Summary boundary to search before the cutoff and filters results to Events truly hidden in this Session.
See searchCurrentHidden.
The resulting session_load
reads a window around an Event ID and can resolve the anchor through a tool-call ID when needed.
A search hit acts as an index card: session, Event ID, and a small snippet, not a reinjected transcript. Compound queries receive bounded clause
and keyword fallbacks, then results merge by session and Event identity. This helps proper nouns and short exact phrases, but the hit remains
candidate evidence. A long tool payload still needs session_load; the search snippet is not a substitute for raw content.
10.2 Load Uses An Event Window And Can Page Large Content
session_load normally anchors on event_id and returns a bounded before/after window of user, assistant, and tool messages.
If a placeholder only has tool_call_id, it first resolves the corresponding tool-result Event in the in-memory Session, then the
persisted Session. When both identifiers were supplied and the Event anchor is stale, it can retry through the tool-call ID.
content_offset and content_limit page a still-large content array instead of reinserting all 12 KB.
The response also says that loaded history is context for the question, not active instructions. That distinction is a security boundary: old user messages and tool payloads may contain expired or adversarial commands. Recall restores evidence; it must not silently elevate historical content into current system policy.
No search hits produce an empty result, so the model can continue from Summary and current evidence or state that evidence is insufficient. Search-service errors and missing load anchors return explicit tool errors; Agent may reformulate, retry through a tool-call ID, or stop. Recall never rewrites Session on retrieval failure and never treats “not found” as proof that a fact is false.
10.3 Preload And On-Demand Recall Solve Different Questions
A separate preload path searches other sessions with the current user question before the model's first answer and injects
a small result set. That helps “the certificate issue from last week.” On-demand current_hidden handles details hidden by Summary
inside the current long session. They have different scopes and should not be collapsed into one vague recall feature.
The preload route is getPreloadSessionRecallMessage.
| Question | First path | Reason |
|---|---|---|
| “What was the serial number earlier in this long session?” | current_hidden → session_load | The current Summary boundary hides the answer. |
| “How did last week's other ticket end?” | Cross-session preload search | The current Session does not own those Events. |
| “Which Go version do I normally use?” | Durable Memory | This is a stable cross-session fact, not evidence to rediscover every time. |
The compression loop is complete only now. Summary creates the cheap main view. Compaction trims the request copy under pressure. Recall follows stable references back to the raw ledger. Compression without Recall loses exact facts; Recall without compression still makes the model carry the whole ledger.
11. Benchmarks Must Measure Savings, Loss, And Recovery Together
“The prompt became shorter” is not enough evidence. Rememorio's benchmark separates three questions. MT-Bench-101 measures rolling Summary cost and continuity. QMSum asks whether a compressed long meeting can still answer a targeted question. LongMemEval tests whether exact facts can be recovered across many sessions. Results below come from the merged Summary benchmark report.
11.1 MT-Bench-101: Short Conversations May Not Repay Summary Cost
MT-Bench-101 contains 917 cases across nine multi-turn task families. The experiment uses DeepSeek-V3.2 and summarizes every two turns. Rolling Summary reduces total tokens by 12.89% and prompt tokens by 24.47%, reaches 0.853 consistency, and passes the first turn in 92.3% of cases. Yet 329 cases, or 35.9%, have negative savings. Four-turn-or-longer conversations usually save 28% to 40%; at two turns or fewer, the extra Summary call can cost more than the original history.
The result is a trigger rule, not “always enable Summary.” For a short conversation, the best Summary is no Summary. A rolling boundary pays back only after enough history accumulates.
11.2 QMSum: On-Demand Recall Recovers Most Of The Quality Loss
The experiment loads 244 QMSum test questions and keeps 189 whose supporting evidence is at least 80 messages from the meeting's end, preventing the recent window from seeing the answer directly. GPT-4o-mini is used; Summary triggers at 40 messages and the main view keeps 20.
| Mode | ROUGE-L | F1 | Avg prompt tokens | Avg latency |
|---|---|---|---|---|
| Full long context | 0.1930 | 0.3132 | 18,986 | 4,556 ms |
| Summary only | 0.1516 | 0.2238 | 888 | 2,994 ms |
| Summary + on-demand Recall | 0.1770 | 0.2774 | 3,857 | 8,656 ms |
On-demand Recall retains 76.69% prompt-token savings while recovering 61.5% of the ROUGE-L loss and 59.9% of the F1 loss from Summary alone. Search and a second load also make it slower than both other modes. Recall is a quality fallback, not a free speedup.
11.3 LongMemEval: Raw Recall Determines Whether Exact Facts Survive
LongMemEval's single-session-user subset has 70 cases. Each averages about 50 sessions, 500 turns, and 103K tokens. The dataset intentionally asks about a detail much later, exposing information that a compact Summary omitted.
| Mode | ROUGE-L | LLM Judge | Exact Match | Avg prompt tokens |
|---|---|---|---|---|
| Full long context | 0.1192 | 0.7386 | 0.6571 | 103,565 |
| Summary only | 0.0477 | 0.0907 | 0.0143 | 445 |
| Summary + on-demand Recall | 0.2694 | 0.9000 | 0.7571 | 6,182 |
Summary + Recall saves 94.04% of prompt tokens versus full context while scoring higher on all three quality metrics. The reason is not that a Summary is more precise than raw text. Retrieval finds evidence inside 100K tokens, then the model answers from a small relevant window. A nine-section structured Summary that preserved user messages verbatim also failed to improve the result: ROUGE-L fell from 0.2694 with the compact Summary to 0.2528. A longer, tidier Summary does not automatically beat “short Summary + raw evidence.”
A supplemental LoCoMo experiment also shows that Session Recall and durable Memory are complements. Session Recall reaches 0.549 overall F1, above both full long context and optimized Memory at 0.469. On temporal questions, optimized Memory scores 0.247 while Recall scores 0.174. Raw Events preserve wording and local evidence; structured Memory can organize cross-session temporal facts. The next chapter takes that path.
12. Beside Claude Code And Codex, Recovery Is The Key Difference
Claude Code, Codex, and tRPC-Agent-Go all separate durable execution history from the model-visible context that can be projected, trimmed, or replaced. They differ in what they sacrifice first under pressure and how compressed details become recoverable.
| System | First response to pressure | Post-compaction view | Detail recovery |
|---|---|---|---|
| tRPC-Agent-Go | Replace old tool results, then synchronously advance Session Summary at the threshold. | Summary plus post-boundary Events. | session_search / session_load return to raw Events. |
| Claude Code | Escalate through tool budgets, local cleanup, and auto compact. | Recovery record and later messages after a compact boundary. | Durable transcript, recovery record, and tool rereads. |
| Codex | Project and truncate outputs first; compact the rollout when needed. | A compacted rollout installed as replacement history. | Durable history and rollout recovery continue execution. |
This comparison stays at the ownership boundary instead of repeating two existing source reads. See Claude Code context management for boundaries, microcompaction, and auto compact, and Codex context management for prompt projection, replacement history, and rollout recovery. tRPC-Agent-Go's distinctive combination is a durable Session Summary plus an explicit recall tool surface for compacted tool payloads and old Events.
13. Carry The Design Into Another Agent
The portable lesson is an order of decisions, not a set of Go type names. Preserve recoverable raw Events. Project only what this turn needs. Clean large tool results that have already been consumed. Advance a rolling Summary only near meaningful pressure. Keep search and window-load tools for exact evidence. Only then hand cross-session facts and reusable procedures to Memory and Evolution as separate owners.
Context governance is not a contest to make history shortest; it preserves the right recovery path inside a budget. The next chapter follows the same certificate investigation into durable Memory: what deserves to survive across sessions, why extraction searches before writing, and what LoCoMo reveals about retrieval, updates, and temporal reasoning.
Sources
- tRPC-Agent-Go context-mechanism snapshot
- request-side tool-result compaction
- rolling Summary and boundary advancement
- Session Recall search and raw Event window loading
- Summary and Session Recall benchmark report
- Claude Code context-management source read
- Codex context-management source read
- tRPC-Agent-Go repository snapshot
- project README feature map
- agent interface
- run invocation
- model interface
- event envelope
- graph agent
- state graph builder
- graph executor
- tool interface
- LLMAgent runtime and model handoff
- skill repository
- skill run tool
- workspace exec tool
- workspace artifact save tool
- code executor
- artifact service
- evolution service
- evolution worker
- evolution reviewer
- evolution revision store
- evolution gates
- AG-UI server
- AG-UI runner
- AG-UI translator
- A2A server
- OpenAI-compatible server
- runner runtime
- session model
- session summarizer
- internal summary orchestration
- content request processor
- LLM flow compaction
- memory API
- auto memory worker
- session search recall tool
- session load recall tool