fix(coding-agent): preserve run prompt during tool refresh

closes #6162
This commit is contained in:
Mario Zechner
2026-06-30 14:08:48 +02:00
parent e547bb9f41
commit fd6659dd5d
7 changed files with 217 additions and 77 deletions
@@ -0,0 +1,136 @@
import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import type { ExtensionFactory } from "../../../src/index.ts";
import { createHarness } from "../harness.ts";
describe("extension active tools next-turn refresh", () => {
it("applies pi.setActiveTools before the next provider request in the same run", async () => {
const extensionFactories: ExtensionFactory[] = [
(pi) => {
pi.registerTool({
name: "switch_tools",
label: "Switch Tools",
description: "Switch the active extension tool set",
promptSnippet: "Switch to the next extension tool",
parameters: Type.Object({}),
execute: async () => {
pi.setActiveTools(["after_switch"]);
return {
content: [{ type: "text", text: "switched" }],
details: {},
};
},
});
pi.registerTool({
name: "after_switch",
label: "After Switch",
description: "Tool that should be available after switching",
promptSnippet: "Run after the active tool set changes",
parameters: Type.Object({}),
execute: async () => ({
content: [{ type: "text", text: "after" }],
details: {},
}),
});
},
];
const harness = await createHarness({
extensionFactories,
});
try {
harness.session.setActiveToolsByName(["switch_tools"]);
const providerToolNames: string[][] = [];
harness.setResponses([
(context) => {
providerToolNames.push((context.tools ?? []).map((tool) => tool.name).sort());
return fauxAssistantMessage(fauxToolCall("switch_tools", {}), { stopReason: "toolUse" });
},
(context) => {
providerToolNames.push((context.tools ?? []).map((tool) => tool.name).sort());
return fauxAssistantMessage("done");
},
]);
expect(harness.session.getActiveToolNames()).toEqual(["switch_tools"]);
await harness.session.prompt("start");
expect(harness.session.getActiveToolNames()).toEqual(["after_switch"]);
expect(providerToolNames).toEqual([["switch_tools"], ["after_switch"]]);
} finally {
harness.cleanup();
}
});
it("preserves before_agent_start system prompt overrides when tools change mid-run", async () => {
const extensionFactories: ExtensionFactory[] = [
(pi) => {
pi.on("before_agent_start", async (event) => ({
systemPrompt: `${event.systemPrompt}\n\nkeep this run override`,
}));
pi.registerTool({
name: "switch_tools",
label: "Switch Tools",
description: "Switch the active extension tool set",
promptSnippet: "Switch to the next extension tool",
parameters: Type.Object({}),
execute: async () => {
pi.setActiveTools(["after_switch"]);
return {
content: [{ type: "text", text: "switched" }],
details: {},
};
},
});
pi.registerTool({
name: "after_switch",
label: "After Switch",
description: "Tool that should be available after switching",
promptSnippet: "Run after the active tool set changes",
parameters: Type.Object({}),
execute: async () => ({
content: [{ type: "text", text: "after" }],
details: {},
}),
});
},
];
const harness = await createHarness({
extensionFactories,
});
try {
harness.session.setActiveToolsByName(["switch_tools"]);
const providerSystemPrompts: string[] = [];
const providerToolNames: string[][] = [];
harness.setResponses([
(context) => {
providerSystemPrompts.push(context.systemPrompt ?? "");
providerToolNames.push((context.tools ?? []).map((tool) => tool.name).sort());
return fauxAssistantMessage(fauxToolCall("switch_tools", {}), { stopReason: "toolUse" });
},
(context) => {
providerSystemPrompts.push(context.systemPrompt ?? "");
providerToolNames.push((context.tools ?? []).map((tool) => tool.name).sort());
return fauxAssistantMessage("done");
},
]);
await harness.session.prompt("start");
expect(providerToolNames).toEqual([["switch_tools"], ["after_switch"]]);
expect(providerSystemPrompts).toHaveLength(2);
expect(providerSystemPrompts[0]).toContain("keep this run override");
expect(providerSystemPrompts[1]).toContain("keep this run override");
} finally {
harness.cleanup();
}
});
});
@@ -1,68 +0,0 @@
import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import type { ExtensionFactory } from "../../../src/index.ts";
import { createHarness } from "../harness.ts";
describe("extension active tools next-turn refresh", () => {
it("applies pi.setActiveTools before the next provider request in the same run", async () => {
const extensionFactories: ExtensionFactory[] = [
(pi) => {
pi.registerTool({
name: "switch_tools",
label: "Switch Tools",
description: "Switch the active extension tool set",
promptSnippet: "Switch to the next extension tool",
parameters: Type.Object({}),
execute: async () => {
pi.setActiveTools(["after_switch"]);
return {
content: [{ type: "text", text: "switched" }],
details: {},
};
},
});
pi.registerTool({
name: "after_switch",
label: "After Switch",
description: "Tool that should be available after switching",
promptSnippet: "Run after the active tool set changes",
parameters: Type.Object({}),
execute: async () => ({
content: [{ type: "text", text: "after" }],
details: {},
}),
});
},
];
const harness = await createHarness({
extensionFactories,
});
try {
harness.session.setActiveToolsByName(["switch_tools"]);
const providerToolNames: string[][] = [];
harness.setResponses([
(context) => {
providerToolNames.push((context.tools ?? []).map((tool) => tool.name).sort());
return fauxAssistantMessage(fauxToolCall("switch_tools", {}), { stopReason: "toolUse" });
},
(context) => {
providerToolNames.push((context.tools ?? []).map((tool) => tool.name).sort());
return fauxAssistantMessage("done");
},
]);
expect(harness.session.getActiveToolNames()).toEqual(["switch_tools"]);
await harness.session.prompt("start");
expect(harness.session.getActiveToolNames()).toEqual(["after_switch"]);
expect(providerToolNames).toEqual([["switch_tools"], ["after_switch"]]);
} finally {
harness.cleanup();
}
});
});