From 981866baa6c7bb22c6fb759ba0813dc2580a97f6 Mon Sep 17 00:00:00 2001 From: hidewong <129483665+hidewong@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:20:01 +0800 Subject: [PATCH] fix(pi): restore subagent compatibility --- package.json | 2 +- pnpm-lock.yaml | 36 +++++++++++------------ src/local-agent-pi.test.ts | 32 +++++++++++++++++++-- src/local-agent-pi.ts | 59 ++++++++++++++++++++------------------ 4 files changed, 80 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index fa39fb46c..16bfccb65 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@anthropic-ai/claude-agent-sdk": "0.3.200", "@anthropic-ai/sandbox-runtime": "0.0.71", "@clack/prompts": "^1.5.1", - "@earendil-works/pi-coding-agent": "^0.80.3", + "@earendil-works/pi-coding-agent": "0.80.10", "@modelcontextprotocol/ext-apps": "^1.7.2", "@modelcontextprotocol/node": "^2.0.0", "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92ca7f368..142b01b10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: ^1.5.1 version: 1.5.1 '@earendil-works/pi-coding-agent': - specifier: ^0.80.3 - version: 0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + specifier: 0.80.10 + version: 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) '@modelcontextprotocol/ext-apps': specifier: ^1.7.2 version: 1.7.2(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3) @@ -311,22 +311,22 @@ packages: resolution: {integrity: sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==} engines: {node: '>= 20.12.0'} - '@earendil-works/pi-agent-core@0.80.3': - resolution: {integrity: sha512-3qw0/GeRQBU/nlGjDe5Yb7ePKTmoxefx2YxyKMFAviFUMXpFexBG/hS7mBtwFahFvzrrTPPoRT6sFIDjwoDWPQ==} + '@earendil-works/pi-agent-core@0.80.10': + resolution: {integrity: sha512-nwnOR3SuLYGRFfyQm8ri4Nj5VGVAvAM9GuqQd3u7BUQj0d6hmD2F8w7OHAAjThE3CuySIdM+v8E22QJG6/RfCg==} engines: {node: '>=22.19.0'} - '@earendil-works/pi-ai@0.80.3': - resolution: {integrity: sha512-jPZLMeGL5kkMSEAwAklfXTMHqZvfhsJtCCpKGIr5Duk7mc0n4skjB1dugk7y0z3z8ZHIUCmPAWHdyDqgUz5vdA==} + '@earendil-works/pi-ai@0.80.10': + resolution: {integrity: sha512-Moe/H8c87yacDGK9dPbWphZNjVsrb3nTrIHycOQJAkFEnY9PYxOOd74+ny44kATfPU9Dm7aTHefar3pZF+UKUA==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-coding-agent@0.80.3': - resolution: {integrity: sha512-TIggw9gCXpA+Ph7OjdTA7ka2NPwTVuPmy39KDSyUzaKq8VvHfMGR7vtRz4JB7Um/RMRblmzhu4p9tUCk6MTgGA==} + '@earendil-works/pi-coding-agent@0.80.10': + resolution: {integrity: sha512-aL4apbupCHiVLSXASXvRzH4Q2vmtfrDa+0s909CJuVu/GgGylbDzr7oyF1mPmip5E+VxYYxKWmph4hV04wUcQg==} engines: {node: '>=22.19.0'} hasBin: true - '@earendil-works/pi-tui@0.80.3': - resolution: {integrity: sha512-2BJI6qwRQfnM0Q7seL1+SbacU/jRRjBnN7Hu3n9BjAn7/s5FaBNnvdD1qBQYRsFTHfjqMaDsjYqanPyqwXj99w==} + '@earendil-works/pi-tui@0.80.10': + resolution: {integrity: sha512-c2JO29PbhKPEQ6fgHQKAl0WhwuFqzWfzspMmP+8B5tpDuP+0mvarRbKKg8gq4b+pQx/QX+6aVS4ko7deoyjQjg==} engines: {node: '>=22.19.0'} '@emnapi/core@1.10.0': @@ -2492,9 +2492,9 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@earendil-works/pi-agent-core@0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-agent-core@0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-ai': 0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) ignore: 7.0.5 typebox: 1.1.38 yaml: 2.9.0 @@ -2506,7 +2506,7 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-ai@0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1048.0 @@ -2527,11 +2527,11 @@ snapshots: - ws - zod - '@earendil-works/pi-coding-agent@0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-coding-agent@0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: - '@earendil-works/pi-agent-core': 0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-ai': 0.80.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) - '@earendil-works/pi-tui': 0.80.3 + '@earendil-works/pi-agent-core': 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-ai': 0.80.10(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + '@earendil-works/pi-tui': 0.80.10 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 cross-spawn: 7.0.6 @@ -2557,7 +2557,7 @@ snapshots: - ws - zod - '@earendil-works/pi-tui@0.80.3': + '@earendil-works/pi-tui@0.80.10': dependencies: get-east-asian-width: 1.6.0 marked: 18.0.5 diff --git a/src/local-agent-pi.test.ts b/src/local-agent-pi.test.ts index df111666c..abcab627b 100644 --- a/src/local-agent-pi.test.ts +++ b/src/local-agent-pi.test.ts @@ -1,4 +1,7 @@ import assert from "node:assert/strict"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import type { AgentSessionEvent, AgentSessionEventListener } from "@earendil-works/pi-coding-agent"; import { PiLocalAgentDriver, @@ -11,7 +14,10 @@ import type { LocalAgentRuntimeContext } from "./local-agent-runtime.js"; class FakePiSession implements PiSessionLike { readonly sessionId = "pi_session_1"; readonly messages: any[] = []; - readonly modelRegistry = { find: () => ({ id: "model" }) } as unknown as PiSessionLike["modelRegistry"]; + readonly modelRuntime = { + getModel: () => ({ id: "model" }), + getModels: () => [], + } as unknown as PiSessionLike["modelRuntime"]; private readonly listeners = new Set(); disposeCount = 0; model?: unknown; @@ -135,7 +141,9 @@ assert.deepEqual(sessions[1]?.activeTools, ["read", "grep", "find", "ls", "edit" await pool.close(); const missingModelSession = new FakePiSession(); -Object.defineProperty(missingModelSession, "modelRegistry", { value: { find: () => undefined } }); +Object.defineProperty(missingModelSession, "modelRuntime", { + value: { getModel: () => undefined, getModels: () => [] }, +}); const missingModelDriver = new PiLocalAgentDriver(async () => missingModelSession); const missingModelRuntime = await missingModelDriver.createRuntime(context); assert.equal(missingModelRuntime.isOk(), true); @@ -152,3 +160,23 @@ if (missingModel.isErr()) { assert.match(missingModel.error.message, /provider\/missing-model/); } await missingModelRuntime.value.close(); + +const originalPiAgentDir = process.env.PI_CODING_AGENT_DIR; +const piAgentDir = mkdtempSync(join(tmpdir(), "devspace-pi-sdk-smoke-")); +process.env.PI_CODING_AGENT_DIR = piAgentDir; +try { + const realDriver = new PiLocalAgentDriver(); + const realRuntime = await realDriver.createRuntime({ + agentId: "agt_pi_sdk_smoke", + provider: "pi", + workspaceRoot: piAgentDir, + writeMode: "full_access", + }); + assert.equal(realRuntime.isOk(), true, "default Pi factory initializes the installed SDK"); + if (realRuntime.isErr()) throw realRuntime.error; + await realRuntime.value.close(); +} finally { + if (originalPiAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR; + else process.env.PI_CODING_AGENT_DIR = originalPiAgentDir; + rmSync(piAgentDir, { recursive: true, force: true }); +} diff --git a/src/local-agent-pi.ts b/src/local-agent-pi.ts index bfb85cb3a..ddd08bbbe 100644 --- a/src/local-agent-pi.ts +++ b/src/local-agent-pi.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import type { AgentSession, ModelRegistry } from "@earendil-works/pi-coding-agent"; +import type { AgentSession, ModelRuntime, ModelRuntimeAuthOverrides } from "@earendil-works/pi-coding-agent"; import { AgentProviderExecutionError, AgentProviderProtocolError, @@ -31,7 +31,7 @@ export type PiSessionLike = Pick< AgentSession, | "sessionId" | "messages" - | "modelRegistry" + | "modelRuntime" | "prompt" | "subscribe" | "setActiveToolsByName" @@ -144,7 +144,7 @@ export class PiSessionRuntime implements LocalAgentRuntime { await updatePiSandboxSession(this.session, input.workspaceRoot, input.writeMode ?? "allowed"); this.session.setActiveToolsByName([...piToolsForWriteMode(input.writeMode)]); if (input.model) { - const model = resolvePiModel(this.session.modelRegistry, input.model); + const model = resolvePiModel(this.session.modelRuntime, input.model); if (!model) { throw new AgentProviderProtocolError({ code: "PROVIDER_PROTOCOL_ERROR", @@ -202,8 +202,7 @@ async function defaultPiSessionFactory( env: NodeJS.ProcessEnv = {}, ): Promise { const { - AuthStorage, - ModelRegistry, + ModelRuntime, SessionManager, DefaultResourceLoader, createAgentSession, @@ -212,11 +211,13 @@ async function defaultPiSessionFactory( // DevSpace's agentDir is the compatibility directory used for instructions; // Pi keeps its own native auth, model, and session state under getAgentDir(). const agentDir = getAgentDir(); - const authStorage = AuthStorage.create(join(agentDir, "auth.json")); - const modelRegistry = ModelRegistry.create(authStorage, join(agentDir, "models.json")); - applyPiProviderEnvironment(modelRegistry, env); + const modelRuntime = await ModelRuntime.create({ + authPath: join(agentDir, "auth.json"), + modelsPath: join(agentDir, "models.json"), + }); + applyPiProviderEnvironment(modelRuntime, env); const sessionManager = await resolveSessionManager(SessionManager, input.workspaceRoot, input.providerSessionId); - const model = input.model ? resolvePiModel(modelRegistry, input.model) : undefined; + const model = input.model ? resolvePiModel(modelRuntime, input.model) : undefined; if (input.model && !model) { throw new AgentProviderProtocolError({ code: "PROVIDER_PROTOCOL_ERROR", @@ -238,8 +239,7 @@ async function defaultPiSessionFactory( const result = await createAgentSession({ cwd: input.workspaceRoot, agentDir, - authStorage, - modelRegistry, + modelRuntime, sessionManager: sessionManager as never, resourceLoader, ...(model ? { model: model as never } : {}), @@ -265,24 +265,25 @@ async function defaultPiSessionFactory( } function applyPiProviderEnvironment( - modelRegistry: ModelRegistry, + modelRuntime: ModelRuntime, env: NodeJS.ProcessEnv, ): void { - const getApiKeyAndHeaders = modelRegistry.getApiKeyAndHeaders.bind(modelRegistry); const providerEnv = Object.fromEntries( Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined), ); - modelRegistry.getApiKeyAndHeaders = async (model) => { - const auth = await getApiKeyAndHeaders(model); - if (!auth.ok) return auth; - return { - ...auth, - env: { - ...auth.env, - ...providerEnv, - }, - }; - }; + if (Object.keys(providerEnv).length === 0) return; + + const getAuth = modelRuntime.getAuth.bind(modelRuntime); + modelRuntime.getAuth = (( + target: string | Parameters[0], + overrides?: ModelRuntimeAuthOverrides, + ) => getAuth(target as never, { + ...overrides, + env: { + ...overrides?.env, + ...providerEnv, + }, + })) as ModelRuntime["getAuth"]; } export function piToolsForWriteMode(writeMode: LocalAgentRunInput["writeMode"]): readonly string[] { @@ -321,13 +322,15 @@ async function resolveSessionManager( return SessionManager.open(match.path); } -function resolvePiModel(registry: { find(provider: string, modelId: string): unknown; getAll?: () => unknown[] }, reference: string): unknown { +function resolvePiModel(runtime: { + getModel(providerId: string, modelId: string): unknown; + getModels(): readonly unknown[]; +}, reference: string): unknown { const separator = reference.indexOf("/"); if (separator !== -1) { - return registry.find(reference.slice(0, separator), reference.slice(separator + 1)); + return runtime.getModel(reference.slice(0, separator), reference.slice(separator + 1)); } - const all = registry.getAll?.() ?? []; - return all.find((model) => asRecord(model)?.id === reference); + return runtime.getModels().find((model) => asRecord(model)?.id === reference); } export function extractPiFinalResponse(value: unknown): string {