fix(server): treat pi aborts as cancellations

This commit is contained in:
Mohamed Boudra
2026-04-20 19:02:37 +07:00
parent 242f61937a
commit 4cbfe95473
2 changed files with 104 additions and 4 deletions
@@ -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<void>): 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.",
},
]);
});
});
@@ -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<ModelRegistry, "find" | "getAll">;
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<Api> | 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,