Skip to content

fix: a system injection's blocks that aren't text say so in the system prompt (#421) - #489

Merged
nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:fix/system-injection-non-text
Oct 18, 2026
Merged

nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:fix/system-injection-non-text

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

A block that isn't text in a system injection, such as an image an MCPL server or a module injected there, now reads in the system prompt as a line saying so, in its place, and the operator is told once per server or module and namespace. Fixes #421.

This is part of a series of small PRs from a sweep of the errors and notices agent-framework gives agents, against one bar: an error should tell the agent how to fix it, or say plainly that nothing can be done.

Problem

A system injection's blocks that aren't text vanished. context-manager hands a system injection's content to the agent as it came (0.14.1 dist/src/context-manager.js:838-839), and buildSystemPrompt joined only its text blocks (src/agent.ts:1540-1554 at main 0756302). So an image or audio block that an MCPL server's context/beforeInference hook or a module's gatherContext injected at system never reached the model, and nothing said so: the agent read the injection's text alone, and the host printed nothing. ContextInjection's type calls its content multimodal at every position (0.14.1 dist/src/types/context.d.ts:83-84), so neither a server's author nor a module's had a sign.

Changes

  • A line in place of each block that isn't text (src/agent.ts). buildSystemPrompt writes each block of the system injections on its own line: a text block's text, and for any other block [<type> block not shown: the system prompt carries text only.], so the agent reads that something was there, and of what kind. An injection holding only such blocks now adds its lines too. The KV prefix hash is taken through the same function (src/agent.ts:1202-1208).
  • The server's line (src/mcpl/hook-orchestrator.ts). The first system injection in a namespace that holds a block that isn't text gets a console line naming the server and the namespace, once per server and namespace while the host runs, beside the orchestrator's other lines about a server's injections.
  • The module's line (src/module-registry.ts). The same for a module's gatherContext, naming the module, beside its gatherContext failures.
Injected at system Before (main) After
by a server: "before", a base64 image, a base64 audio block, a url image, "after" the system prompt gains before and after; no line it gains before, [image block not shown: the system prompt carries text only.], the same for audio and for the url image, then after; the host prints [mcpl] editor: a system injection in namespace notes holds a block that isn't text, … once
by a module: an image alone the system prompt is unchanged; no line it gains [image block not shown: the system prompt carries text only.]; the host prints [notes-module] gatherContext: a system injection in namespace charts holds a block that isn't text, … once

What each part is for

  • The line in buildSystemPrompt is the fix for the agent, at the one site every system injection reaches, whatever its source. The KV prefix hash already goes through it.
  • The server's line tells the server's operator, who otherwise sees nothing. An injection comes again at every turn, so it prints once per server and namespace.
  • The module's line does the same for a module, at the site that already names a module's gatherContext failures.
  • Left out:
    • turning such blocks into text in the MCPL converter, which would cover servers and not modules, and put the rule in a second place;
    • the namespace in the agent's line, since compile hands buildSystemPrompt the blocks alone, and the operator's lines name it;
    • refusing such an injection, since its text still reaches the model, and a refusal would drop that too;
    • sending such blocks to the model elsewhere, such as in the messages: membrane's request takes its system prompt as a string (NormalizedRequest.system, 0.5.87 dist/types/request.d.ts:86), and moving a block out of system would put it where its server or module didn't place it.

Tests

test/system-injection-blocks-not-shown.test.ts (new), through previewActivation and a turn:

  • server: three servers' injections, through a HookOrchestrator: system injections with text, base64 and url images and audio, one of an image alone in a namespace that prints escaped, one of text alone, an afterUser one with an image, and a system one from a server not granted that position. It checks the system prompt's lines, the afterUser image still in the messages, and the host's lines: once per server and namespace across the preview and the turn, and for the server not granted the position, only the grant's own line;
  • module: the same for two modules' gatherContext injections, with the injections that print nothing ahead of the ones that do.

Red-first: with the base's src/ and these tests, both new tests fail. The servers' injections reach the system prompt as base, before, after and plain_text alone, and the modules' as base, module_text and plain_text.

Each piece removed or changed on its own (rebuilt, the new test file run each time), 16 pieces, each failing at least one test:

Piece removed Tests that fail
the line in place of a block that isn't text server, module
the server's line server
the server's line only for a system injection server
the server's line only when a block isn't text server
the server's line printed once server
once per server server
once per namespace, for a server server
the module's line module
the module's line only for a system injection module
the module's line only when a block isn't text module
the module's line printed once module
once per module module
once per namespace, for a module module
the module's line passing over an injection, not ending the loop module
the server's line printing the namespace escaped server
the module's line printing the namespace escaped module

The new test and the 15 test files that run system injections, gatherContext, the hook orchestrator or previewActivation pass at the head, 308 of 308. These ran on Linux, on Node 24.22.0, with a clean typecheck under TypeScript 7.0.2.

Checks: CI runs the typecheck, the build and the suite at this head, on Ubuntu and macOS with Node 20 and Node 24: its run. At this head, CI's macOS Node 20 leg failed one test, force-exit-blocking-stdout's large write before a forced exit, with 9941432 of 10000000 bytes reaching the runner. This PR touches neither that test nor what it tests, and main's CI passed it at 07563022.

Not verified

  • A real MCPL server and a real model: the tests stand in for the servers behind a HookOrchestrator, use a module of their own and a mock membrane, and read the request through previewActivation and a turn.
  • macOS, which CI covers.

Out of scope

  • A block that isn't text in a beforeUser or afterUser injection goes into the request's messages, as before, and its operator isn't told.

  • Changelog fragment added: changelog.d/fix-system-injection-non-text.fixed.md

Found by Anarchid's review of #369; Ada-1017 settled the design. Our internal review read the change at 9e038e85, and Ogden-2688 read it whole at 7c6e3236, its fix round included. Written by Alfred-2570.

— Alfred-2570 (Opus), contributing through nissa-seru

🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's Pi tooling, and posted from the shared nissa-seru account.

…m prompt (anima-research#421)

The system prompt is text, and context-manager hands a system
injection's content to the agent as it came, so buildSystemPrompt kept
only its text blocks: an image or audio block that an MCPL server or a
module injected at system vanished, with no sign to the agent or the
operator, and an injection holding only such blocks left the system
prompt as it was. Each such block now reads as a line in its place,
naming its type: "[image block not shown: the system prompt carries text
only.]". The KV prefix hash is taken through the same function.

The operator is told too, once per source and namespace while the host
runs: the hook orchestrator names the server, beside its other lines
about a server's injections, and the module registry names the module,
beside its gatherContext failures. Each line is printed where the
source's injections are gathered, before the framework scopes them to an
agent, so it says what an agent given the injection reads.

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

greptile-apps Bot commented Oct 18, 2026

Copy link
Copy Markdown

Required label not found on this PR.

@nissa-seru
nissa-seru merged commit 20a599f into anima-research:main Oct 18, 2026
5 of 6 checks passed
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.

MCPL injections at position system silently drop image and audio blocks

1 participant