Files
oh-my-pi/packages/coding-agent/test/agent-session-manual-retry.test.ts
T
can1357 2f871b6d23 feat(coding-agent): added loadMode and summary to AgentTool discovery
- Added optional `loadMode` and `summary` fields to `AgentTool` and related type declarations.
- Added `loadMode` and `summary` metadata to built-in tool classes for discoverable/essential behavior.
- Replaced `BUILTIN_TOOL_METADATA` with per-tool fields in discovery code paths.
- Updated `search_tool_bm25` and discovery indexing to use each tool's `summary` text.
- Updated discovery tests to validate tool `loadMode` and summary completeness.
2026-05-06 19:14:39 +02:00

174 lines
5.5 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import * as path from "node:path";
import { Agent } from "@oh-my-pi/pi-agent-core";
import { type AssistantMessage, getBundledModel, type Model } from "@oh-my-pi/pi-ai";
import { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { TempDir } from "@oh-my-pi/pi-utils";
class MockAssistantStream extends AssistantMessageEventStream {}
function createAssistantMessage(
model: Model,
options: { text?: string; stopReason: "stop" | "error" | "aborted"; errorMessage?: string },
): AssistantMessage {
return {
role: "assistant",
content: options.text ? [{ type: "text", text: options.text }] : [],
api: model.api,
provider: model.provider,
model: model.id,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: options.stopReason,
errorMessage: options.errorMessage,
timestamp: Date.now(),
};
}
function lastAgentMessage(session: AgentSession): AssistantMessage {
const message = session.agent.state.messages.at(-1);
if (!message || message.role !== "assistant") {
throw new Error("Expected trailing assistant message");
}
return message as AssistantMessage;
}
describe("AgentSession manual retry", () => {
let tempDir: TempDir;
let authStorage: AuthStorage;
let session: AgentSession | undefined;
beforeEach(async () => {
tempDir = TempDir.createSync("@pi-manual-retry-");
authStorage = await AuthStorage.create(path.join(tempDir.path(), "testauth.db"));
authStorage.setRuntimeApiKey("anthropic", "test-key");
});
afterEach(async () => {
if (session) {
await session.dispose();
session = undefined;
}
authStorage.close();
tempDir.removeSync();
});
it("removes the failed assistant turn and continues with a fresh attempt", async () => {
const model = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!model) {
throw new Error("Expected bundled Anthropic test model to exist");
}
let streamCalls = 0;
const agent = new Agent({
getApiKey: provider => `${provider}-test-key`,
initialState: {
model,
systemPrompt: ["Test"],
tools: [],
messages: [],
},
streamFn: requestedModel => {
streamCalls += 1;
const stream = new MockAssistantStream();
queueMicrotask(() => {
if (streamCalls === 1) {
const message = createAssistantMessage(requestedModel, {
stopReason: "error",
errorMessage: "manual retry test failure",
});
stream.push({ type: "start", partial: message });
stream.push({ type: "error", reason: "error", error: message });
return;
}
const message = createAssistantMessage(requestedModel, {
text: "recovered after manual retry",
stopReason: "stop",
});
stream.push({
type: "start",
partial: createAssistantMessage(requestedModel, { text: "", stopReason: "stop" }),
});
stream.push({ type: "done", reason: "stop", message });
});
return stream;
},
});
session = new AgentSession({
agent,
sessionManager: SessionManager.inMemory(),
settings: Settings.isolated({ "compaction.enabled": false, "retry.enabled": false }),
modelRegistry: new ModelRegistry(authStorage),
});
session.subscribe(() => {});
await session.prompt("fail once");
await session.waitForIdle();
expect(lastAgentMessage(session).stopReason).toBe("error");
await expect(session.retry()).resolves.toBe(true);
await session.waitForIdle();
expect(streamCalls).toBe(2);
expect(lastAgentMessage(session).stopReason).toBe("stop");
expect(lastAgentMessage(session).content).toContainEqual({ type: "text", text: "recovered after manual retry" });
});
it("returns false when the trailing assistant turn succeeded", async () => {
const model = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!model) {
throw new Error("Expected bundled Anthropic test model to exist");
}
let streamCalls = 0;
const agent = new Agent({
getApiKey: provider => `${provider}-test-key`,
initialState: {
model,
systemPrompt: ["Test"],
tools: [],
messages: [],
},
streamFn: requestedModel => {
streamCalls += 1;
const stream = new MockAssistantStream();
queueMicrotask(() => {
const message = createAssistantMessage(requestedModel, { text: "already done", stopReason: "stop" });
stream.push({
type: "start",
partial: createAssistantMessage(requestedModel, { text: "", stopReason: "stop" }),
});
stream.push({ type: "done", reason: "stop", message });
});
return stream;
},
});
session = new AgentSession({
agent,
sessionManager: SessionManager.inMemory(),
settings: Settings.isolated({ "compaction.enabled": false }),
modelRegistry: new ModelRegistry(authStorage),
});
session.subscribe(() => {});
await session.prompt("succeed");
await session.waitForIdle();
await expect(session.retry()).resolves.toBe(false);
expect(streamCalls).toBe(1);
expect(lastAgentMessage(session).content).toContainEqual({ type: "text", text: "already done" });
});
});