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 } from "@oh-my-pi/pi-ai"; import { createMockModel } from "@oh-my-pi/pi-ai/providers/mock"; import { getBundledModel } from "@oh-my-pi/pi-catalog/models"; 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"; function lastAgentMessage(session: AgentSession): AssistantMessage { const message = session.agent.state.messages.at(-1); if (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"); } const mock = createMockModel({ responses: [ { throw: "manual retry test failure" }, { content: ["recovered after manual retry"], stopReason: "stop" }, ], }); const agent = new Agent({ getApiKey: model => `${model.provider}-test-key`, initialState: { model, systemPrompt: ["Test"], tools: [], messages: [], }, streamFn: mock.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(mock.calls.length).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"); } const mock = createMockModel({ responses: [{ content: ["already done"], stopReason: "stop" }], }); const agent = new Agent({ getApiKey: model => `${model.provider}-test-key`, initialState: { model, systemPrompt: ["Test"], tools: [], messages: [], }, streamFn: mock.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(mock.calls.length).toBe(1); expect(lastAgentMessage(session).content).toContainEqual({ type: "text", text: "already done" }); }); });