What happened?
A change to the selected tool set made while a run is in progress moves the head of the
system prompt, so the run's next request re-bills from that point. The head is where the
- <tool>: <snippet> bullets are rendered, and it re-derives the selection from the live
set every turn, so adding or removing one selected tool rewrites it.
Measured on one session (72 responses, provider deepseek): five requests followed such a
change, each exactly one tool bullet (266 characters); a sixth head move came from the
injected path described in the comment below. Two of the six re-billed the whole prompt —
156,362 and 192,928 input tokens, with only 1,664 and 128 tokens cached. The other four
re-billed 2.7–17.8% of the prompt.
Steps to reproduce
Validated with pi -ne, so no ambient extensions were loaded. One small extension is
enough — it registers a tool with a promptSnippet, selects it for the run, and removes it
from the live set during the first tool call:
// repro.ts — pi --mode rpc -ne -e ./capture.ts -e ./repro.ts
import { Type } from "typebox";
export default function (pi: any) {
const TOOL = "probe_enable";
pi.registerTool({
name: TOOL,
description: "Scratch tool that exists so the system prompt can carry a bullet for it.",
parameters: Type.Object({}, { additionalProperties: false }),
promptSnippet: `${TOOL} is installed. Call it to enable the scratch tools.`,
async execute() { return { content: [{ type: "text", text: "ok" }] }; },
});
let removed = false;
pi.on("tool_call", () => {
if (removed) return undefined;
removed = true;
pi.setActiveTools(pi.getActiveTools().filter((n) => n !== TOOL));
return undefined;
});
pi.on("before_agent_start", (event) => {
const active = pi.getActiveTools();
if (!active.includes(TOOL)) pi.setActiveTools([...active, TOOL]);
const selected = event.systemPromptOptions?.selectedTools;
if (Array.isArray(selected) && !selected.includes(TOOL)) selected.push(TOOL);
return undefined;
});
}
Capture what the provider is sent (wrap globalThis.fetch and write init.body to a
file), then send one prompt that makes the model call any tool:
- before the tool call: system prompt 13,796 chars,
- probe_enable: … present;
- after it: 13,716 chars, and the only difference in the system prompt is that bullet
line (79 chars + newline);
- later requests stay there, so it is billed once — but the whole head moved.
Expected behavior
A tool-set change made during a run should not rewrite the head of a prompt that has
already been sent, or — if it must — the cost should not be the conversation behind it.
Rendering the bullets from the same set the request will carry would close the
prompt/payload half; applying selection changes at a run boundary would close the rest.
Happy to prepare a PR if that is the direction you prefer.
Version
0.87.1 (Linux; provider deepseek, model deepseek-flash, openai-completions).
What happened?
A change to the selected tool set made while a run is in progress moves the head of the
system prompt, so the run's next request re-bills from that point. The head is where the
- <tool>: <snippet>bullets are rendered, and it re-derives the selection from the liveset every turn, so adding or removing one selected tool rewrites it.
Measured on one session (72 responses, provider
deepseek): five requests followed such achange, each exactly one tool bullet (266 characters); a sixth head move came from the
injected path described in the comment below. Two of the six re-billed the whole prompt —
156,362 and 192,928 input tokens, with only 1,664 and 128 tokens cached. The other four
re-billed 2.7–17.8% of the prompt.
Steps to reproduce
Validated with
pi -ne, so no ambient extensions were loaded. One small extension isenough — it registers a tool with a
promptSnippet, selects it for the run, and removes itfrom the live set during the first tool call:
Capture what the provider is sent (wrap
globalThis.fetchand writeinit.bodyto afile), then send one prompt that makes the model call any tool:
- probe_enable: …present;line (79 chars + newline);
Expected behavior
A tool-set change made during a run should not rewrite the head of a prompt that has
already been sent, or — if it must — the cost should not be the conversation behind it.
Rendering the bullets from the same set the request will carry would close the
prompt/payload half; applying selection changes at a run boundary would close the rest.
Happy to prepare a PR if that is the direction you prefer.
Version
0.87.1 (Linux; provider
deepseek, modeldeepseek-flash, openai-completions).