Repository navigation
Description
Human Overview
Summary
ContextInjector, the Todos reminder and MemoryManager add their text to the latest user message for one model call only. On the next call that message is sent without it. Preserved-thinking Claude models bind each thinking block to the history before it, so the request is refused or its thinking is dropped (#5499). On providers that cache by exact prefix, it also defeats prompt caching (#4879).
The fix is to write injected context once, as a content block in the user message it belongs to, and keep it. The agent sends the block to each model in a form its API accepts. Design: content blocks for injected context. Part of #5160.
TypeScript goes first. Each TypeScript item is followed by its Python port, with the same names.
Part 1: fix #5499
- Decision D1: the SDK injectors write injected text into the message by default
- feat(injection)!: write injected text into history as an InjectedBlock (typescript) #5569
ContextInjectorandMemoryManagerwrite injected text once into the latest user message, as aTextBlockmarkedmetadata.injected. Memory extraction leaves it out, and context manager offload leaves injected blocks in place and out of summaries. It addsTextBlockMetadatawith theinjectedkey; feat(context)!: offload conditions decide granularity (typescript) #5325 and feat(context-manager)!: split base and native summarizers and add opt-in Anthropic compaction #5765 addoffloadedto the same type. In API review. Python needs the same.
#5595 (MessageUpdatedEvent) is parked. No MessageAddedEvent listener needs to hear about the write.
Before #5569 is released
harness-ts and strands-cli accept any SDK from 1.19.0 below 2.0, so they get stored injected text as soon as the SDK ships.
- API review of feat(injection)!: write injected text into history as an InjectedBlock (typescript) #5569: whether to keep an opt-out for per-call injection, and the
TextBlockMetadatanames, together with the later blocks' names (systemContextshould followinjected). The types are@experimental, but saved sessions keep the shape that was released. - harness-ts drops the todos reminder, since every
todo_writeresult has the list and a stored reminder would add a copy on every change (branch ready). Harness versions already published will still store the reminder with the new SDK. - strands-cli leaves injected text out when it builds restored turns, replays ACP history and previews sessions. Otherwise it shows the environment block and recalled memories as the user's words.
Follow-ups after #5569
-
SummarizingConversationManagerleaves injected text out (TS). Its summarizer resends earlier thinking, so check live that this doesn't make Claude drop thinking. - The token estimate counts text that
BeforeModelCallEventhooks write (TS). It's made before them today, so the call that writes the text doesn't count it. - Decide how Bedrock guardrails, guardrail redaction and telemetry treat injected text (TS)
- Code that rebuilds a
TextBlockkeeps its metadata where it should, so dedupe still finds the block (TS) - The context-manager docs say that block-level offload leaves injected blocks in place, so a large one is reclaimed only by whole-message compaction (TS)
Injected text is a TextBlock, so every model already receives it as text and an older SDK loads it. Python providers must send only text from a text block: Bedrock does, the others need checking.
Before systemContext and the tool blocks
Later blocks may ship in the same release as their first writer, so the release before won't know them. These let it load such sessions anyway.
- fix(types): keep unknown content blocks when restoring saved data (typescript) #5564 Keep unknown content blocks when restoring saved data (TS)
- Providers skip unknown content blocks instead of raising
TypeError(Py)
Part 2: the systemContext block (blocked by #5305, #5313)
- Rendering capabilities in model metadata, pinned per conversation
-
SystemContextBlock, sent as text by default - Anthropic sends it as a mid-conversation system message ([FEATURE] Allow role "system" messages mid-conversation #5176), OpenAI as a
developermessage -
ContextInjector, the harness environment line andGuidecan write it
Part 3: tools (with design 0021)
-
toolReferenceblock and dynamic tool loading: tracked in Epic: dynamic tool loading (design 0021) #5668 -
toolRemovaland policy-driven tool changes ([FEATURE] Send tool list changes as mid-conversation system messages #5407)
Follow-ups
Before the injectors write injected blocks
- The call that triggers a write must already send the block.
InvokeModelStage.Inputmiddleware runs on a copy of the messages, so a write toagent.messagesfrom there reaches the model one call late, and the message changes between calls. (feat(injection)!: write injected text into history as an InjectedBlock (typescript) #5569 writes from aBeforeModelCallEventhook.) - Settle every rendering rule first. Once blocks are stored, a rendering change alters earlier messages in live sessions. (The separator before injected text is part of the stored text, as the fold adds it today.)
- Use one separator rule in both SDKs. Python adds
\n\nonly when the message already has a text block (fix(injection): drop the leading blank line on tool-result turns (py) #4828, which fixed Claude on Bedrock reading a tool-result turn as an empty user message). TypeScript still adds it when the message has any block, because the TS port, fix(injection): drop the leading blank line on tool-result turns (ts) #4829, was closed without being merged. - Dedupe checks that the target message still holds a block with the same text, not just its tracking id. Messages and app state aren't always restored together: the Goal plugin restores messages without state, and some apps save only
agent.messages. (feat(injection)!: write injected text into history as an InjectedBlock (typescript) #5569 reads both checks from the conversation.) - Models that reject text next to a tool result: port the [BUG] An error occurred (ValidationException) when calling the Converse operation: messages.4.content: Conversation blocks and tool result blocks cannot be provided in the same turn #1223 handling to TypeScript and land fix(responses): emit tool results before user text #5180. On those models a stored block in a tool-result message fails every later request, not one call.
- Decide how a conversation recovers when a stored block trips a guardrail or causes a refusal. It's sent again on every later call.
- A message-level offload strategy can merge an older user message into the latest one, injected blocks included, and then every injector skips the new turn. Only the trailing injected blocks should count as written for this message.
- Unit tests for the paths the docs claim: forced structured output, resume, restore then invoke, and message metadata kept.
Because injected text is now stored
- The
ContextInjector, memory and harness docs say injected text is never stored, so apps may inject secrets or text that changes every call (see also [BUG] ContextInjector "never persists" guarantee is false on stateful models (OpenAI responses API, stateful: true) #4102). feat(injection)!: write injected text into history as an InjectedBlock (typescript) #5569 and the harness todos change give those pages a TypeScript tab; the Python tab changes with the Python port. - Stored text can't be taken back. A memory removed from the store stays where it was recalled.
Harness
- Split the environment block, so a new date doesn't write AGENTS.md (up to 16,000 characters) again.
- Live test that an AGENTS.md rule is still followed many turns later, once it's no longer in the latest message.
Other
-
<context-status>(contextManager: 'agentic') still edits the latest message on every call, so those agents keep [BUG] Ephemeral context injection (ContextInjector, Todos reminder) edits history that preserved-thinking models check, so the next request is refused #5499.
Related
- Epic: server-side context management for preserved-thinking models #5410 tracks server-side compaction and the other history edits. Its [FEATURE] Allow role "system" messages mid-conversation #5176 and [FEATURE] Send tool list changes as mid-conversation system messages #5407 would ship through Part 2 and Part 3 here.
- [FEATURE] Agree on a cross-SDK policy for unsupported content blocks and implement it uniformly across model providers #5316: one policy for unsupported content blocks across SDKs.
- docs(designs): add 0022 mid-conversation system messages #5235: design 0022, a public
systemrole. Optional, for 2.0.
Metadata
Metadata
Assignees
Labels
Type
Fields
Language
Activity
refactor(types): mark injected text with TextBlock metadata
feat(injection): write injected text into history
docs(site): say TypeScript keeps injected text
feat(context): skip injected text when offloading
refactor(types): write injected text as an InjectedBlock
feat(injection)!: write injected text into history as an InjectedBlock
feat(injection)!: write injected text into history as an InjectedBlock
feat(injection)!: write injected text into history as an InjectedBlock
feat(injection)!: write injected text into history as an InjectedBlock