feat(coding-agent): allow message_end replacements

closes #3982
This commit is contained in:
Mario Zechner
2026-04-30 21:19:09 +02:00
parent 24dec9fcd5
commit 40c6eabb8f
7 changed files with 188 additions and 4 deletions

View File

@@ -120,6 +120,47 @@ describe("AgentSessionRuntime characterization", () => {
return { runtime, faux, tempDir };
}
it("persists message_end assistant replacements to the session manager", async () => {
const { runtime } = await createRuntimeForTest((pi: ExtensionAPI) => {
pi.on("message_end", (event) => {
if (event.message.role !== "assistant") return;
return {
message: {
...event.message,
usage: {
...event.message.usage,
cost: {
...event.message.usage.cost,
total: 0.123,
},
},
},
};
});
});
await runtime.session.prompt("hello");
const sessionAssistant = runtime.session.messages.find((message) => message.role === "assistant");
expect(sessionAssistant?.role).toBe("assistant");
if (sessionAssistant?.role !== "assistant") {
throw new Error("missing assistant message");
}
expect(sessionAssistant.usage.cost.total).toBe(0.123);
const persistedAssistant = runtime.session.sessionManager
.getEntries()
.filter((entry) => entry.type === "message")
.map((entry) => entry.message)
.find((message) => message.role === "assistant");
expect(persistedAssistant?.role).toBe("assistant");
if (persistedAssistant?.role !== "assistant") {
throw new Error("missing persisted assistant message");
}
expect(persistedAssistant.usage.cost.total).toBe(0.123);
});
it("emits session_before_switch and session_start for new and resume flows", async () => {
const events: RecordedSessionEvent[] = [];
const { runtime } = await createRuntimeForTest((pi: ExtensionAPI) => {

View File

@@ -0,0 +1,56 @@
import { fauxAssistantMessage } from "@mariozechner/pi-ai";
import { afterEach, describe, expect, it } from "vitest";
import { createHarness, type Harness } from "../harness.js";
describe("regression #3982: message_end cost override", () => {
const harnesses: Harness[] = [];
afterEach(() => {
while (harnesses.length > 0) {
harnesses.pop()?.cleanup();
}
});
it("allows extensions to replace finalized assistant usage cost", async () => {
const harness = await createHarness({
extensionFactories: [
(pi) => {
pi.on("message_end", (event) => {
if (event.message.role !== "assistant") return;
return {
message: {
...event.message,
usage: {
...event.message.usage,
cost: {
...event.message.usage.cost,
total: 0.123,
},
},
},
};
});
},
],
});
harnesses.push(harness);
harness.setResponses([fauxAssistantMessage("hello")]);
await harness.session.prompt("hi");
const assistantMessage = harness.session.messages.find((message) => message.role === "assistant");
expect(assistantMessage?.role).toBe("assistant");
if (assistantMessage?.role !== "assistant") {
throw new Error("missing assistant message");
}
expect(assistantMessage.usage.cost.total).toBe(0.123);
const messageEnd = harness.eventsOfType("message_end").find((event) => event.message.role === "assistant");
expect(messageEnd?.message.role).toBe("assistant");
if (messageEnd?.message.role !== "assistant") {
throw new Error("missing assistant message_end event");
}
expect(messageEnd.message.usage.cost.total).toBe(0.123);
});
});