Skip to content

chore(deprecate): context injection (gatherContext + MCPL beforeInference) - #218

Open
antra-tess wants to merge 1 commit into
mainfrom
chore/deprecate-context-injection
Open

antra-tess wants to merge 1 commit into
mainfrom
chore/deprecate-context-injection

Conversation

@antra-tess

@antra-tess antra-tess commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Context injection, whether from module gatherContext() or MCPL context/beforeInference contextInjections, produces per-compile overlays. Injected blocks are never stored and are re-anchored to the latest user-participant message on every compile. This has two consequences (#171):

  • Prompt caching breaks across activations. At the next activation the previous block vanishes from its slot and reappears at the new tail, so the request diverges from every earlier one at that point. On OpenAI Responses/Codex, where the cache only hits boundaries an earlier request wrote, the first call of every activation falls back to a head-only cache hit. On Anthropic the loss is bounded but real.
  • Blocks can land between a tool call and its result. A mid-activation recompile, such as a stream retry, can anchor the block there.

The decision is to deprecate the mechanism and recommend against it.

What this does

Behavior is unchanged: injections still apply. Remaining uses become visible:

  • @deprecated annotations on Module.gatherContext, Module.contextTimeoutMs and BeforeInferenceResult.contextInjections, with the alternative. Durable content belongs in the system prompt. Changing state should be delivered as conversation content (an EventResponse that adds a message, a push event, a tool result), so it is stored once at its own position and stays cache-stable.
  • Deprecation warnings: ModuleRegistry.gatherContext and HookOrchestrator.beforeInference log one [deprecated] line per module or MCPL server the first time it actually injects, via the new src/context-injection-deprecation.ts.
    • Sources that return nothing stay silent.
    • Injections at positions outside a server's grant are still dropped exactly as before, and are not warned about.
  • README: the module-interface and context-management sections now say the mechanism is deprecated and point to Ephemeral context injections re-anchor on every activation and defeat prompt caching (head-only hits on OpenAI lanes) #171.
  • Changelog fragment: changelog.d/deprecate-context-injection.changed.md.

Removing the mechanism is left for a follow-up. The spec-level deprecation (and 0.6 removal schedule) is anima-research/mcpl#15 (RFC-010); the context-manager placement API is deprecated in anima-research/context-manager#140.

Tests

  • New: test/context-injection-deprecation.test.ts has 4 tests:

    • a module injection still applies and warns once across turns;
    • a module returning nothing is silent;
    • a granted MCPL injection still applies and warns once;
    • an ungranted position is still dropped and does not warn.

    The first three fail without the two call-site changes; the fourth guards the unchanged path.

  • Full suite: npm run typecheck and npm run build are clean. npm test has no failures attributable to this change. The two mcpl-awareness-barrier tests ("startup globally gates host/command…" / "…push/event…") fail intermittently under full-suite load on unmodified main too: I reproduced them on one of two clean main runs, and they pass 25/25 when the file is run alone. They are a pre-existing timing flake.

Companion PRs

anima-research/connectome-host#189 deprecates the host modules that are built on this hook (retrieval, subagents, instructions). It doesn't depend on this one, so they can merge in either order.

🤖 Generated with Claude Code

…ence)

Injected context blocks are per-compile overlays: never stored, and
re-anchored to the latest user-participant message on every compile. At
the next activation the old block vanishes from its slot and reappears at
the new tail, so the request diverges from every earlier one there — on
OpenAI Responses/Codex the first call of each activation falls back to a
head-only cache hit, and a mid-activation recompile can anchor the block
between a tool call and its result (#171).

Deprecate both sources without changing behavior:
- `Module.gatherContext`, `Module.contextTimeoutMs` and
  `BeforeInferenceResult.contextInjections` carry @deprecated with the
  recommended alternative (system prompt for durable content; conversation
  content — events, push events, tool results — for changing state).
- ModuleRegistry and HookOrchestrator log one `[deprecated]` line per
  module / MCPL server the first time it actually injects (sources that
  return nothing stay silent; ungranted positions are still dropped as
  before and not warned about).
- README notes + changelog fragment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5 Tier: apex

[Medium risk] Marks context injection as deprecated, still functional.

The PR appears safe to merge, with a non-blocking correction to the explanation of system injections.

Findings

  1. P2 `system` warnings overstate cache loss ▶
Fix with agent prompt
### Issue 1
src/context-injection-deprecation.ts:27-29
This notice also runs for `system` injections, but `buildSystemPrompt()` appends that content to the system prompt. Unchanged content can stay cache-stable and cannot split a tool call from its result. Saying it moves beside the latest user message can send users toward the wrong cache fix.

Keep the deprecation, but limit the movement and tool-pairing claims to `beforeUser` and `afterUser` here and in the matching docs.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Deprecates Module.gatherContext, Module.contextTimeoutMs, and BeforeInferenceResult.contextInjections without removing existing injection behavior.

  • Adds one-time source warnings and four focused tests.
  • Updates migration advice in README.md and the changelog.
  • The warning and docs should distinguish fixed system injections from injections placed beside user messages.
  • antra-tess defers mechanism removal and MCPL spec changes to follow-ups. antra-tess also acknowledges the two mcpl-awareness-barrier failures as pre-existing timing flakes reproduced on unchanged main.

Reviews (1) · Last reviewed commit: "chore(deprecate): context injection (gat..."

Comment on lines +27 to +29
'Context injection is deprecated: injected blocks are per-compile overlays that are never ' +
'stored and are re-anchored on every activation, which breaks prompt-cache prefixes ' +
'(head-only hits on OpenAI lanes) and can land between a tool call and its result. ' +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 system warnings overstate cache loss

This notice also runs for system injections, but buildSystemPrompt() appends that content to the system prompt. Unchanged content can stay cache-stable and cannot split a tool call from its result. Saying it moves beside the latest user message can send users toward the wrong cache fix.

Keep the deprecation, but limit the movement and tool-pairing claims to beforeUser and afterUser here and in the matching docs.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/context-injection-deprecation.ts
Line: 27-29

Comment:
**`system` warnings overstate cache loss**

This notice also runs for `system` injections, but `buildSystemPrompt()` appends that content to the system prompt. Unchanged content can stay cache-stable and cannot split a tool call from its result. Saying it moves beside the latest user message can send users toward the wrong cache fix.

Keep the deprecation, but limit the movement and tool-pairing claims to `beforeUser` and `afterUser` here and in the matching docs.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@nissa-seru

Copy link
Copy Markdown
Contributor

@antra-tess We've opened #610 into this branch. It corrects what the texts say each injection position costs. A system injection is appended to the system prompt and never re-anchored, as Greptile noted above, and only a beforeUser block can land between a tool call and its result. It also makes the [deprecated] line say that the injections still apply. Nothing is needed from you for it.

We read #​218 against main at ea4297ab, 1,983 commits past this branch's base. It conflicts at its two call sites, both of which main has rewritten:

  • ModuleRegistry.gatherContext's .then now also notes a system block the system prompt can't carry (noteBlocksNotShown).
  • HookOrchestrator.beforeInference now converts each injection inside a try, which drops one it can't read.

In our trial merge the server warning comes after convertMcplInjection succeeds, so a server isn't named for an injection main drops as unreadable. A probe on that merge, with the text fix on top, showed it: an injection with content: null is dropped with "it couldn't be read", and no [deprecated] line names its server. With the warning before the conversion, as #​218 orders it, the same injection's server is named.

On that trial merge (main at ea4297ab merged with 6552677f, call sites resolved as above), on Linux with Node 24.22.0:

  • tsc and the build are clean.
  • The suite passes 5,716 of 5,760 tests, with 44 skipped and no failures. main's CI at ea4297ab (Ubuntu, Node 24) passes 5,711 of 5,756 with 45 skipped. The difference is the four new tests, plus one test CI skips because bun isn't on its PATH.
  • The four new tests pass. With both warnings removed, the three that pin them fail, and with either one removed, its own tests fail.
  • test/mcpl-awareness-barrier.test.ts, which the description's Tests section mentions, was removed from main in 2cb501a.

In connectome-host's main (f26b03dc), three modules implement gatherContext, so their operators would see the line:

  • instructions (src/modules/instructions-module.ts), at system unless configured otherwise;
  • retrieval (src/modules/retrieval-module.ts), at afterUser, which four of its recipes turn on;
  • the subagents module's Fleet Status HUD (src/modules/subagent-module.ts), at afterUser, which returns nothing until the agent turns it on with the module's hud tool.

connectome-host writes stderr to a log file in headless and TUI modes (src/headless.ts:154, src/tui.ts:441). An operator there would see one line per such module in each run of the host, the first time it injects.

We'd also suggest, outside the pull request, an instance set on ModuleRegistry and HookOrchestrator in place of the process-wide set. It would drop the test-only reset export, and main already keeps such a set for its line about system blocks, printed once per module and namespace.

Kestrel-2409 (Claude Opus 5.5), from an internal review.

— Daniel-2612

nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 25, 2026
Each sentence this round changes was checked against anima-research#218's head
(agent-framework 0.19.0, context-manager 0.11.0, membrane 0.5.87) and
against main (0.26.0, context-manager 0.22.0, membrane 0.7.1), since anima-research#218
ships only by merging into main.

- R3-1: the framework's own system injections are the subconscious's
  standing dispositions and, before 0.23.0, a silent heartbeat's
  ephemeralSystemPrompt. ee43daf, first released in 0.23.0, removed the
  heartbeat's.
- R3-2: the line, header and README name the per-server switch,
  disabledCapabilities: ["contextHooks.beforeInference.inject"], and say a
  server's system injections are denied by default. On both trees the
  switch leaves every inject leaf ungranted and observe, pushEvents and
  tools granted. The hook then collects none of the server's injections
  and prints a drop line for each. The whole-line test's literal follows.
- R3-3: system text that changes at runtime has "no provider-independent
  replacement". The header and both @deprecated docs say what membrane 0.7's
  system_change block can do (add text from its place, through the native
  formatter and the Anthropic adapter only) and what it can't.
- R3-4: a moved block or new system text also means no later reply can keep
  its thinking on a model that binds it, as context-manager 0.22's thinking
  binding describes.
- R3-5: the MCPL criterion holds in either order of warning and
  conversion. Each module and server whose injections are collected is
  named once, and none whose injections the feature-set and grant checks
  all drop. At this head the warning comes before the conversion, so a
  server whose granted injection can't be converted is named while the
  hook throws and collects nothing. "At the first injection the host
  collects from it" would be false there. The two comments no longer
  depend on that order.
- R3-6: the tests' titles and messages say "collected", which is what they
  check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor

@antra-tess On agent-framework 0.27.0, an afterUser injection is stored in the agent's window and stays there, and the framework's own toolsets module injects through gatherContext. So this PR's deprecation, as written, contradicts the release it would ship after. We'd narrow it to the system and beforeUser positions, which 0.27.0 still places at every compile, and leave afterUser out of it. What we need from you is the scope you want here, since what #​610's texts should say depends on it. @Anarchid's review of #​610 found this (R4).

What 0.27.0 does (main at 0a4a2c69):

  • Since #​782, a start appends an afterUser injection to the agent's window, where it stays until a fold. Unchanged content isn't appended again while the agent's current copy holds it. A module can opt out with placedEachCompile: true, and compile() then places its injection at each start, as before. An MCPL server's afterUser injection can't opt out. compile() still places system and beforeUser injections at each start. The tests in test/stored-injections.test.ts pin each of these (:142, :170, :181, :229).
  • Since #​799, the toolsets module, registered when a server is configured with toolHints: true, answers gatherContext with the RFC-014 §8.3 reminder, when one is due, as an afterUser injection (src/mcpl/toolsets-module.ts:235-239; test/mcpl-tool-hints.test.ts:357).
  • main's own doc for placedEachCompile (src/types/module.ts:28-48) recommends opting out, so that compile() places an afterUser injection at each start, for "context meant to stay near the end, such as instructions moved off system". It names that injection's cost: "it moves, and on a model that binds a reply's thinking to everything before it, every reply after its old place loses its thinking".

What this PR says (src/types/module.ts:112-120 at 6552677): injections are "per-compile overlays — never stored, re-anchored to the latest user message on every compile", and "Do not add new implementations". On 0.27.0 the first part is false for an afterUser injection that doesn't opt out, and the toolsets module is a new implementation. git merge-tree of this PR's head into main merges src/types/module.ts without a conflict, and the merged doc then says both that an afterUser injection is stored "where it then stays" (:32) and that injections are "never stored" (:197).

Why we'd narrow to system and beforeUser. compile() places both at every start, so they're still the per-compile overlays this PR describes. main documents both forms of afterUser as supported. The stored form is the one #​782 added and the toolsets reminder uses. The opted-out form is what the placedEachCompile doc recommends, with its move cost written down. Deprecating either would contradict that doc. Exempting only the toolsets module would leave every other stored afterUser injection under a deprecation whose stated costs it no longer has. Keeping the deprecation whole would need the module and the placedEachCompile doc changed in this PR as well.

— Kestrel-2409

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants