The system prompt lists tool snippets and per-tool guidelines in the
order of the active tools. When an extension called
`pi.setActiveTools()` with the same tools in a different order (for
example `[...pi.getActiveTools()].reverse()`), the "tools" and "rules"
sections came out different. pi then recorded a "System Prompt Update:
tools, rules" message even though the active tools had not changed. On
providers without mid-conversation system messages, this also rewrites
the prompt head and loses the cache.
`_applyToolLoadout` in `agent-session.ts` now treats the requested names
as a set:
- tools that stay active keep their current order;
- newly activated tools are appended at the end.
Reordering the same set therefore leaves the prompt unchanged. Adding a
tool only appends its bullet. The same rule covers tool lists changed by
`before_agent_start` handlers, since they go through the same function.
Testing:
- Added a test in `test/system-prompt-updates.test.ts`: it reverses the
active tools between two prompts and expects a single system message. It
fails without the change (a second message patches `tools` and `rules`)
and passes with it.
- `npm run check` passes.
- `./test.sh`: all coding-agent suites pass. The only failure is an
unrelated Anthropic model metadata test in `packages/ai`.
Fixes#11495
Requested by @badlogic
Co-authored-by: triagent <triagent@localhost>
Remove SystemMessage.replace. A prompt forced from before_agent_start is a rendering
of the current prompt for the run, not conversation state: persisting it as a
replacing system message wrote the full prompt and tool list on every change,
lost the structured sections while forced, and disabled mid-conversation system
messages for the rest of the session once a replacement existed.
The transcript now always records section patches. AgentSession wraps
transformContext and, when a forced prompt is active, collapses the system
messages into one head carrying the forced text and the current tools. Providers
receive the same request as before; nothing is persisted and the projection ends
with the run.
The evals harness validated the without_docs variant from the transcript, which
relied on the persisted replacement. Its transform extension now records the
prompt it forced and validates that instead.
Add SystemMessage.replace: replaying a system message with the flag discards the
accumulated prompt content, sections, and tools before applying it, and providers
collapse the transcript into one leading system message whenever a later message
replaces it.
A forced prompt from before_agent_start is opaque, so it is persisted as a replace
message holding the text in content with no sections (the agent loop fills in the
full tool set). Leaving force mode persists another replace message with the
structured sections. Previously the forced prompt became a preamble section patch,
so models with native mid-conversation system messages kept the original prompt
as their leading system prompt and received the forced one as a later update.
This change makes system prompt text and tool changes part of the transcript rather than silently rewriting its starting conditions. This lets Pi record when instructions changed or tools became available, restore that state after resuming or navigating branches, and preserve cached prompt prefixes where the upstream supports it.