mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): settle tool results before disposal (#9126)
Fixes #9124.
Disposing a runtime during tool execution disconnects persistence before
the interrupted turn settles, leaving a stored assistant tool call
without its result. Await `session.abort()` before shutdown and disposal
so the interrupted tool result and final assistant message are
persisted.
This matches `teardownCurrent()`. `AgentSession.dispose()` remains
synchronous. Like the existing replacement path, the wait has no
timeout; handling tools that ignore cancellation is outside this change.
Related: #9986 concerns results for tool calls skipped during a
mid-session abort. This fix preserves the result of an already-running
tool during runtime shutdown.
Validation on main `f1b2e77f5`:
- The regression fails without the fix and passes with it, checking the
durable message order and `Operation aborted` result immediately after
`dispose()` returns, before awaiting the prompt.
- `npm run check`, offline build, and `./test.sh` pass.
This commit is contained in:
parent
2cfab7c269
commit
7a53c310a3
2 changed files with 83 additions and 0 deletions
|
|
@ -402,6 +402,7 @@ export class AgentSessionRuntime {
|
|||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
await this.session.abort();
|
||||
await emitSessionShutdownEvent(this.session.extensionRunner, {
|
||||
type: "session_shutdown",
|
||||
reason: "quit",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
||||
import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai";
|
||||
import { Type } from "typebox";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
AgentSessionRuntime,
|
||||
type AgentSessionServices,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
} from "../../../src/core/agent-session-runtime.ts";
|
||||
import { createHarness, type Harness } from "../harness.ts";
|
||||
|
||||
describe("regression #9124: runtime disposal during tool execution", () => {
|
||||
const harnesses: Harness[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
while (harnesses.length > 0) {
|
||||
harnesses.pop()?.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
async function createRuntime(tool: AgentTool): Promise<{ harness: Harness; runtime: AgentSessionRuntime }> {
|
||||
const harness = await createHarness({ tools: [tool] });
|
||||
harnesses.push(harness);
|
||||
const services: AgentSessionServices = {
|
||||
cwd: harness.tempDir,
|
||||
agentDir: harness.tempDir,
|
||||
modelRuntime: harness.session.modelRuntime,
|
||||
settingsManager: harness.settingsManager,
|
||||
resourceLoader: harness.session.resourceLoader,
|
||||
diagnostics: [],
|
||||
};
|
||||
const unusedFactory: CreateAgentSessionRuntimeFactory = async () => {
|
||||
throw new Error("runtime replacement is not used in this test");
|
||||
};
|
||||
return {
|
||||
harness,
|
||||
runtime: new AgentSessionRuntime(harness.session, services, unusedFactory),
|
||||
};
|
||||
}
|
||||
|
||||
it("persists the aborted tool turn before disconnecting the session", async () => {
|
||||
let markToolStarted!: () => void;
|
||||
const toolStarted = new Promise<void>((resolve) => {
|
||||
markToolStarted = resolve;
|
||||
});
|
||||
const tool: AgentTool = {
|
||||
name: "wait",
|
||||
label: "Wait",
|
||||
description: "Wait until aborted",
|
||||
parameters: Type.Object({}),
|
||||
execute: (_toolCallId, _params, signal) =>
|
||||
new Promise<AgentToolResult<unknown>>((_resolve, reject) => {
|
||||
markToolStarted();
|
||||
signal?.addEventListener("abort", () => setTimeout(() => reject(new Error("Operation aborted")), 10), {
|
||||
once: true,
|
||||
});
|
||||
}),
|
||||
};
|
||||
const { harness, runtime } = await createRuntime(tool);
|
||||
harness.setResponses([fauxAssistantMessage(fauxToolCall("wait", {}), { stopReason: "toolUse" })]);
|
||||
|
||||
const prompt = runtime.session.prompt("run tool");
|
||||
await toolStarted;
|
||||
try {
|
||||
await runtime.dispose();
|
||||
// #9124: disposal itself must settle persistence; awaiting the prompt must not be necessary.
|
||||
const messages = harness.sessionManager
|
||||
.getEntries()
|
||||
.filter((entry) => entry.type === "message")
|
||||
.map((entry) => entry.message)
|
||||
.filter((message) => message.role !== "system");
|
||||
expect(messages.map((message) => message.role)).toEqual(["user", "assistant", "toolResult", "assistant"]);
|
||||
expect(messages[2]).toMatchObject({
|
||||
role: "toolResult",
|
||||
content: [{ type: "text", text: "Operation aborted" }],
|
||||
isError: true,
|
||||
});
|
||||
} finally {
|
||||
await prompt;
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue