🐛 fix(context-engine): stop rewriting tool results with placeholder values (#19809)
* 🐛 fix(context-engine): stop rewriting tool results with placeholder values
A tool result is a record of what a tool returned, but
PlaceholderVariablesProcessor substituted `{{...}}` into every message on
every call. When a tool legitimately returns text containing a placeholder
token, that message therefore changed between calls — diverging from the
prefix the provider had already cached, so every token after it was
re-processed at the uncached price for the rest of the run.
Seen in production: an agent ran `git diff` over `locales/en-US/chat.json`,
whose i18n source string is literally `"Updated {{time}}"`. The diff landed
in a tool result 66k tokens into the context and its seconds ticked on every
call. 134 of 136 calls broke the prefix, 43M tokens were re-processed, and
the run cost $15.47 against a $0.59 baseline — the same topic's earlier runs
held a 98.9% cache hit rate.
Tool results are now passed through verbatim unless the tool is on a
hydration allowlist: the activation surface `ActivationResultTrimProcessor`
already recognises (`lobe-activator.activateTools`,
`lobe-activator.activateSkill`, `lobe-skills.activateSkill`). Those return a
platform-authored template, and a skill that is activated but not injected
into the system prompt keeps its tool result as the only channel for a
SKILL.md body referencing `{{agent_id}}` / `{{topic_id}}`.
System prompt, injected blocks, user and assistant turns are unchanged — a
user who writes `{{time}}` still gets a live clock.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* 🐛 fix(context-engine): keep tool-result provenance through group role conversion
GroupRoleTransformProcessor folds another agent's tool results into
`role: 'user'` content and drops `plugin`, so the role check alone classified
them as ordinary user prose and kept rewriting their `{{...}}` literals —
leaving both the wrong output and the broken cache prefix in place on the
group path.
Tool results now carry a `foldedToolResult` marker across the conversion, and
the hydration decision keys off it as well as the role. The marker never
reaches the provider: MessageCleanupProcessor runs afterwards and allow-lists
`content` / `role` for user messages.
Reported by Codex on #19809.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com> A
Arvin Xu committed
6e5ffa4ad53632788ac4eabc80f121e83166ae4b
Parent: 2dd28b1
Committed by GitHub <noreply@github.com>
on 9/22/2026, 12:35:09 PM