diff --git a/packages/server/src/server/agent/providers/pi-direct-agent.test.ts b/packages/server/src/server/agent/providers/pi-direct-agent.test.ts new file mode 100644 index 000000000..044c0cf5c --- /dev/null +++ b/packages/server/src/server/agent/providers/pi-direct-agent.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test, vi } from "vitest"; + +import type { AgentStreamEvent } from "../agent-sdk-types.js"; +import { PiDirectAgentSession, type PiDirectSessionAdapter } from "./pi-direct-agent.js"; + +function createPiSession(prompt: () => Promise): PiDirectSessionAdapter { + return { + sessionId: "pi-session-1", + thinkingLevel: "medium", + model: undefined, + messages: [], + extensionRunner: undefined, + promptTemplates: [], + resourceLoader: { + getSkills: () => ({ skills: [] }), + }, + agent: { + state: { + systemPrompt: "", + errorMessage: null, + }, + }, + sessionManager: { + getSessionFile: () => "/tmp/pi-session.json", + getCwd: () => "/tmp/paseo-pi-test", + }, + subscribe: vi.fn(), + prompt, + abort: vi.fn(), + dispose: vi.fn(), + getSessionStats: vi.fn(() => ({})), + setThinkingLevel: vi.fn(), + }; +} + +describe("PiDirectAgentSession", () => { + test("treats SDK request abort rejections as turn cancellations", async () => { + const session = new PiDirectAgentSession( + createPiSession(() => Promise.reject(new Error("Request was aborted."))), + { find: vi.fn(), getAll: vi.fn(() => []) }, + { + provider: "pi", + cwd: "/tmp/paseo-pi-test", + }, + ); + const events: AgentStreamEvent[] = []; + session.subscribe((event) => events.push(event)); + + const { turnId } = await session.startTurn("hello"); + await Promise.resolve(); + + expect(events).toEqual([ + { + type: "turn_canceled", + provider: "pi", + turnId, + reason: "Request was aborted.", + }, + ]); + }); +}); diff --git a/packages/server/src/server/agent/providers/pi-direct-agent.ts b/packages/server/src/server/agent/providers/pi-direct-agent.ts index ed8ee13f6..360eedb26 100644 --- a/packages/server/src/server/agent/providers/pi-direct-agent.ts +++ b/packages/server/src/server/agent/providers/pi-direct-agent.ts @@ -100,6 +100,28 @@ interface StartTurnResult { turnId: string; } +export type PiDirectSessionAdapter = Pick< + PiAgentSession, + | "abort" + | "agent" + | "dispose" + | "extensionRunner" + | "getSessionStats" + | "messages" + | "model" + | "prompt" + | "promptTemplates" + | "resourceLoader" + | "sessionId" + | "sessionManager" + | "setModel" + | "setThinkingLevel" + | "subscribe" + | "thinkingLevel" +>; + +type PiDirectModelRegistry = Pick; + interface PiToolResultObject { output?: string; stdout?: string; @@ -666,7 +688,7 @@ function mapSkillCommand(skill: Skill): AgentSlashCommand { }; } -function buildSlashCommands(session: PiAgentSession): AgentSlashCommand[] { +function buildSlashCommands(session: PiDirectSessionAdapter): AgentSlashCommand[] { const commands: AgentSlashCommand[] = []; const extensionCommands = session.extensionRunner?.getRegisteredCommands() ?? []; @@ -749,6 +771,14 @@ function getStreamEventTurnId(event: AgentStreamEvent): string | undefined { } } +function isPiRequestAbortError(error: unknown): boolean { + if (error instanceof Error && error.name === "AbortError") { + return true; + } + + return /\brequest was aborted\b/i.test(stringifyUnknownError(error)); +} + function resolveThinkingOptionId( cachedThinkingOptionId: string | null, sessionThinkingLevel: ThinkingLevel, @@ -773,7 +803,7 @@ function mapThinkingOption(option: (typeof PI_THINKING_OPTIONS)[number]) { } function findModelInRegistry( - registry: ModelRegistry, + registry: PiDirectModelRegistry, parsedReference: PiModelReference, ): Model | undefined { if (parsedReference.provider) { @@ -799,8 +829,8 @@ export class PiDirectAgentSession implements AgentSession { private latestUsage: AgentUsage | undefined; constructor( - private readonly session: PiAgentSession, - private readonly modelRegistry: ModelRegistry, + private readonly session: PiDirectSessionAdapter, + private readonly modelRegistry: PiDirectModelRegistry, private readonly config: AgentSessionConfig, ) { this.lastKnownThinkingOptionId = @@ -1076,6 +1106,15 @@ export class PiDirectAgentSession implements AgentSession { .catch((error) => { const failedTurnId = this.activeTurnId ?? turnId; this.activeTurnId = null; + if (isPiRequestAbortError(error)) { + this.emit({ + type: "turn_canceled", + provider: PI_PROVIDER, + turnId: failedTurnId, + reason: stringifyUnknownError(error), + }); + return; + } this.emit({ type: "turn_failed", provider: PI_PROVIDER,