Repository navigation
agent memory: structured truncated tool_result on the wire (not Tool: crumbs) #549
Description
Parent
#548 (agent session architecture)
Problem
formatPromptWithHistory drops every tool_run row (lib/sessionStore.ts).
Those cards are display-only (plan #345). The model already saw tool results
this turn; on the next turn it gets only user/assistant/system/error
prose, flattened into one { prompt } string.
Operators see: the agent re-reads the same files, forgets what it grepped,
never builds session memory, and talks like a new hire each send.
We persist up to 8 MiB of those cards in Blob/localStorage and then
omit them from inference.
Goal (locked direction)
Put truncated tool_result on the wire as structured messages — not
Tool: … lines stuffed into formatPromptWithHistory.
Peer harnesses (OpenCode / Pi / Oh My Pi) send a real message array:
user / assistant + tool_use / tool_result (thinking only if the
provider wants it). That is how the next turn still knows what was read.
Invincible today: single-shot { prompt } + display-only tool_run.
Success: after a turn that read_files / greps / change_dirs, the next
user message can be “use what you already found” and the model does not
rediscover the tree from zero.
How (directional — lock in create-plan)
- Host/server keep a model-facing trace (call id, name, args, truncated
result, ok) distinct from the Wasm paint card (tool_runstays
display-primary). - Default send: bounded result (paths + status + short excerpt). OpenCode
~2k lines / 50 KB; Pi ~2k chars at compact time. Do not re-send
TOOL_RUN_PREVIEW_MAX_CHARS(100k) per item. - Prefer AI SDK / Gateway native tool-result messages over a flattened
transcript. Flattening is a fallback if a provider cannot take the array —
still truncated, still paired (never a call without its result). - Abort/cancel: persist committed tools the same way we persist
change_dir
(honest, not a hallucinated summary of a cancelled mid-tool).
Constraints (lock)
- Wasm ring /
tool_runpaint stays display-primary (feature-divide). - Caps: any new per-result / per-turn tool-payload budget is a NEW cap
in a futurecreate-planCaps table; changing 3.5M / 400 is inference budget: replace 400-row / 3.5M-char fold with model-window token budget #551. - Function/Gateway body ceilings still apply to what we send.
Peer harness notes
| Idea | Take |
|---|---|
Native tool_use / tool_result in the messages array |
This issue |
| Truncate tool output; do not omit | This issue |
| Never split a call from its result at a cut | #552 |
| Files touched listed in the compact summary | #552 |
| Skills catalog vs full bodies | #557 |
Non-goals
- Compaction engine (compaction: model-capability context compaction (not API payload slices) #552)
- Pagination / scrollbar (spike: viewport hydrate — latest-page F5 (#924), slim local, scrollbar (A5 rebase) #553)
- Putting thinking into SessionStore
- Hashline / LSP-on-write / in-process bash (wrong runtime)
Suggested next
create-plan against #548 A1 before coding.
Orrery (2026-08-15)
- Hygiene: few summary lines in the model-facing result; verbose exec/test output stays on the sandbox disk (file path in the result). Same Anthropic “parallel Claudes” lesson as their
exectool. Caps for that excerpt land in the A1create-plantable — do not default toTOOL_RUN_PREVIEW_MAX_CHARS. - Assembly order:
model → tools → system → historyis locked before the request is built (inference: cache-stable two-block system prompt (don’t bust the KV prefix) #558). Structured messages are the history part, not a second system dump.
Related tools (not this board)
Tool-side shaping (so there is something honest to fold):
- #562 first-class
search - #563 windowed
read_file - #564 stale
str_replacewindow - #565
execsummary + disk log
Those issues are sandbox/tools, not agent-session. This issue remains structured tool_result on the wire.
Activity
[-]agent memory: fold tool-run evidence into the model prompt (stop re-reading every turn)[/-][+]agent memory: structured truncated tool_result on the wire (not Tool: crumbs)[/+]btipling commentedon Aug 18, 2026
This is A1 for the “every send is my first turn” report (parent #548 2026-08-18).
The model is not amnesiac. We throw away the turn’s execution graph and feed it a sanitized
User:/Assistant:string (formatPromptWithHistory—lib/sessionStore.ts). Notool_use/tool_result, notoolCallId, so prior work looks like ungrounded third-party prose.Concrete lock for the A1
create-plan(from the same research pass):lib/agent/runAgent.tsalready usesstreamText+stepCountIs. Persistresult.response.messages(oronFinish({ response })) — the fullModelMessage[](tool-calls,role: 'tool'results, reasoning parts, final text) — not the terminal assistant string.Tool:crumbs toformatPromptWithHistory.TOOL_RUN_PREVIEW_MAX_CHARS(100k) per item. LLM payload ≠tool_runpaint JSON (feature-divide).change_dir).reasoning_contentreplay; Anthropic thinking needsproviderMetadata. SessionStore CoT paint stays ephemeral — decide model-facing replay in this plan, do not dump thinking into Redis/Blob.Peer: Pi split tool result (llm vs UI); Codex inner cycle = reasoning + function_call + function_call_output before yielding to the operator.
btipling commentedon Aug 19, 2026
2026-08-19 source-level peer research (Codex / DeepSeek / OpenCode / Oh My Pi)
A1 is still the first-turn fix. These are locks for the create-plan, not a new issue.
Mechanism we already have
Invincible Functions are already OpenCode-shaped: every POST reloads. There is no process-local
ContextManagervector that can drift from disk. The bug is that we reload paint rows and flatten them (formatPromptWithHistory→{ prompt }, droptool_run).Steal OpenCode’s invariant, implemented as a durable model-facing projection:
Carrier: session-owned (envelope pointer + Blob sidecar, or a distinct object — lock in create-plan). Paint stays
tool_run(feature-divide). LLM payload ≠ UI payload (Pi).Truncate / spill, don’t omit
tool-output/file, 7-day TTL; settlement fails if retain failsartifact://; images →blob:sha256:Our cloud analogue: verbose bytes on sandbox disk (already #565 for
exec) or a Blob object; the structuredtool_resultcarries a bounded excerpt + path. Do not default toTOOL_RUN_PREVIEW_MAX_CHARS(100k). OpenCode’s honesty bar is worth taking: if we cannot retain the full output, the tool did not succeed.Interrupted closer
DeepSeek / OMP: crash mid-turn → synthetic
interrupted/aborted, keep committed tool pairs. We already persistchange_diron cancel. A1 must persist committedtool_use+tool_resultthe same way, plus an honest stop reason, so turn N+1 does not treat a torn assistant string as a finished mind.Persist ≠ fresh
A1 snapshots are not live file views. #277 already empties the freshness ledger every POST. After A1 the model will see the old
read_fileand think it has a grant — it does not. That composition is #693 (plan with A1). Do not rewritetool_resultwhen the file changes; do not put fingerprints on the session.Skip
Codex in-memory vector as authority; DeepSeek JSONL as our SoT; OMP snapcompact PNGs; stuffing paint JSON into the Gateway.
feat(agent): structured truncated tool_result on the wire (plan #936,…
fix(persist): keep modelMessagesPointer across host PUT; cap-trim newest
fix(persist): host cloudMetaFor never emits modelMessagesPointer
fix(turns): 503 on bound model-messages seed miss without sidecar
fix(persist): local copy-forward of observed modelMessagesPointer
btipling commentedon Sep 4, 2026
Done by PR #937 (
9736c37), which shipped plan #936.Next-turn seed is the structured truncated
tool_resultBlob behindmeta.modelMessagesPointer, notformatPromptWithHistorycrumbs. Parent #548 stays open (compaction / viewport / remaining A-rows).test(workflows): lock canvas closure against Buffer