From a6f6c169c8a2d3754bf8b02aa3bee9c9f3fd142a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 4 Apr 2026 16:14:17 +0700 Subject: [PATCH] Fix Pi thinking mode state mapping --- .../server/agent/providers/acp-agent.test.ts | 80 +++++++- .../server/agent/providers/pi-acp-agent.ts | 187 +++++++++++++++++- 2 files changed, 265 insertions(+), 2 deletions(-) diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index 4fc9365f4..e51f57c47 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -8,7 +8,7 @@ import { deriveModesFromACP, mapACPUsage, } from "./acp-agent.js"; -import { transformPiModels, transformPiSessionResponse } from "./pi-acp-agent.js"; +import { transformPiModels, transformPiSessionResponse, wrapPiSession } from "./pi-acp-agent.js"; import { createTestLogger } from "../../../test-utils/test-logger.js"; function createSession(): ACPAgentSession { @@ -526,6 +526,84 @@ describe("ACPAgentSession", () => { ]); }); + test("Pi session wrapper hides synthetic modes and exposes them as thinking", async () => { + const wrapped = wrapPiSession( + { + provider: "pi", + id: "session-1", + capabilities: { + supportsStreaming: true, + supportsSessionPersistence: true, + supportsDynamicModes: true, + supportsMcpServers: false, + supportsReasoningStream: true, + supportsToolInvocations: true, + }, + features: [ + { + type: "select", + id: "thought_level", + label: "Thinking", + value: "medium", + options: [ + { id: "low", label: "Low" }, + { id: "medium", label: "Medium" }, + ], + }, + ], + run: vi.fn(), + startTurn: vi.fn(), + subscribe: vi.fn(() => () => {}), + streamHistory: async function* () {}, + getRuntimeInfo: vi.fn(async () => ({ + provider: "pi", + sessionId: "session-1", + model: "gpt-4.1-mini", + thinkingOptionId: null, + modeId: "xhigh", + })), + getAvailableModes: vi.fn(async () => [ + { id: "xhigh", label: "xhigh" }, + ]), + getCurrentMode: vi.fn(async () => "xhigh"), + setMode: vi.fn(), + getPendingPermissions: vi.fn(() => []), + respondToPermission: vi.fn(), + describePersistence: vi.fn(() => null), + interrupt: vi.fn(), + close: vi.fn(), + setThinkingOption: vi.fn(), + }, + { + provider: "pi", + cwd: "/tmp/paseo-acp-test", + thinkingOptionId: "medium", + }, + ); + + await expect(wrapped.getAvailableModes()).resolves.toEqual([]); + await expect(wrapped.getCurrentMode()).resolves.toBeNull(); + await expect(wrapped.getRuntimeInfo()).resolves.toEqual({ + provider: "pi", + sessionId: "session-1", + model: "gpt-4.1-mini", + thinkingOptionId: "xhigh", + modeId: null, + }); + expect(wrapped.features).toEqual([ + { + type: "select", + id: "thought_level", + label: "Thinking", + value: "xhigh", + options: [ + { id: "low", label: "Low" }, + { id: "medium", label: "Medium" }, + ], + }, + ]); + }); + test("emits assistant and reasoning chunks as deltas while user chunks stay accumulated", async () => { const session = createSession(); const events: Array<{ type: string; item?: { type: string; text?: string } }> = []; diff --git a/packages/server/src/server/agent/providers/pi-acp-agent.ts b/packages/server/src/server/agent/providers/pi-acp-agent.ts index 655ddcd45..baab3b48a 100644 --- a/packages/server/src/server/agent/providers/pi-acp-agent.ts +++ b/packages/server/src/server/agent/providers/pi-acp-agent.ts @@ -9,7 +9,25 @@ import type { ToolKind, } from "@agentclientprotocol/sdk"; -import type { AgentCapabilityFlags, AgentModelDefinition } from "../agent-sdk-types.js"; +import type { + AgentLaunchContext, + AgentCapabilityFlags, + AgentFeature, + AgentMode, + AgentModelDefinition, + AgentPermissionRequest, + AgentPermissionResponse, + AgentPersistenceHandle, + AgentPromptInput, + AgentRunOptions, + AgentRunResult, + AgentRuntimeInfo, + AgentSession, + AgentSessionConfig, + AgentSlashCommand, + AgentStreamEvent, + AgentFeatureSelect, +} from "../agent-sdk-types.js"; import type { ProviderRuntimeSettings } from "../provider-launch-config.js"; import { isCommandAvailable } from "../../../utils/executable.js"; import { @@ -108,6 +126,152 @@ export function transformPiSessionResponse( }; } +function isThoughtLevelFeature(feature: AgentFeature): feature is AgentFeatureSelect { + return feature.type === "select" && feature.id === "thought_level"; +} + +function normalizePiFeatures( + features: AgentFeature[] | undefined, + thinkingOptionId: string | null | undefined, +): AgentFeature[] | undefined { + if (!features) { + return features; + } + + return features.map((feature) => { + if (!isThoughtLevelFeature(feature)) { + return feature; + } + + return { + ...feature, + value: thinkingOptionId ?? feature.value, + }; + }); +} + +class PiACPAgentSession implements AgentSession { + readonly provider: AgentSession["provider"]; + readonly capabilities: AgentSession["capabilities"]; + + get id(): string | null { + return this.inner.id; + } + + get features(): AgentFeature[] | undefined { + return normalizePiFeatures(this.inner.features, this.thinkingOptionId); + } + + private thinkingOptionId: string | null; + + constructor( + private readonly inner: AgentSession, + config: AgentSessionConfig, + ) { + this.provider = inner.provider; + this.capabilities = inner.capabilities; + this.thinkingOptionId = config.thinkingOptionId ?? null; + } + + async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise { + return this.inner.run(prompt, options); + } + + async startTurn( + prompt: AgentPromptInput, + options?: AgentRunOptions, + ): Promise<{ turnId: string }> { + return this.inner.startTurn(prompt, options); + } + + subscribe(callback: (event: AgentStreamEvent) => void): () => void { + return this.inner.subscribe(callback); + } + + async *streamHistory(): AsyncGenerator { + yield* this.inner.streamHistory(); + } + + async getRuntimeInfo(): Promise { + const runtimeInfo = await this.inner.getRuntimeInfo(); + const thinkingOptionId = + runtimeInfo.modeId ?? runtimeInfo.thinkingOptionId ?? this.thinkingOptionId; + this.thinkingOptionId = thinkingOptionId ?? null; + return { + ...runtimeInfo, + modeId: null, + thinkingOptionId: thinkingOptionId ?? null, + }; + } + + async getAvailableModes(): Promise { + return []; + } + + async getCurrentMode(): Promise { + return null; + } + + async setMode(modeId: string): Promise { + void modeId; + throw new Error("Pi does not expose selectable modes"); + } + + getPendingPermissions(): AgentPermissionRequest[] { + return this.inner.getPendingPermissions(); + } + + async respondToPermission( + requestId: string, + response: AgentPermissionResponse, + ): Promise { + await this.inner.respondToPermission(requestId, response); + } + + describePersistence(): AgentPersistenceHandle | null { + return this.inner.describePersistence(); + } + + async interrupt(): Promise { + await this.inner.interrupt(); + } + + async close(): Promise { + await this.inner.close(); + } + + async listCommands(): Promise { + return this.inner.listCommands ? this.inner.listCommands() : []; + } + + async setModel(modelId: string | null): Promise { + if (this.inner.setModel) { + await this.inner.setModel(modelId); + } + } + + async setThinkingOption(thinkingOptionId: string | null): Promise { + this.thinkingOptionId = thinkingOptionId ?? null; + if (this.inner.setThinkingOption) { + await this.inner.setThinkingOption(thinkingOptionId); + } + } + + async setFeature(featureId: string, value: unknown): Promise { + if (!this.inner.setFeature) { + throw new Error("Agent session does not support setting features"); + } + await this.inner.setFeature(featureId, value); + } +} + +export function wrapPiSession( + session: AgentSession, + config: Pick, +): AgentSession { + return new PiACPAgentSession(session, config); +} + export class PiACPAgentClient extends ACPAgentClient { constructor(options: PiACPAgentClientOptions) { super({ @@ -130,6 +294,27 @@ export class PiACPAgentClient extends ACPAgentClient { }); } + override async createSession( + config: AgentSessionConfig, + launchContext?: AgentLaunchContext, + ): Promise { + const session = await super.createSession(config, launchContext); + return wrapPiSession(session, config); + } + + override async resumeSession( + handle: AgentPersistenceHandle, + overrides?: Partial, + launchContext?: AgentLaunchContext, + ): Promise { + const session = await super.resumeSession(handle, overrides, launchContext); + return wrapPiSession(session, { + provider: "pi", + cwd: overrides?.cwd ?? process.cwd(), + thinkingOptionId: overrides?.thinkingOptionId, + }); + } + override async isAvailable(): Promise { if (!existsSync(resolvedPiAcpPath)) { return false;