diff --git a/apps/server/src/provider/prime/PrimeAgentBackendSelection.test.ts b/apps/server/src/provider/prime/PrimeAgentBackendSelection.test.ts index 4be1fe7060..9f7671d0fb 100644 --- a/apps/server/src/provider/prime/PrimeAgentBackendSelection.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentBackendSelection.test.ts @@ -14,6 +14,7 @@ import { negotiatePrimeAgentBackend, type PrimeAgentBackendNegotiationInput, } from "./PrimeAgentBackendSelection.ts"; +import { PrimeAgentDaemonBridgeError } from "./PrimeAgentDaemonBridge.ts"; import { PrimeAgentDaemonManagerError, type PrimeAgentDaemonManagerInput, @@ -312,11 +313,38 @@ it.layer(NodeServices.layer)("negotiatePrimeAgentBackend", (it) => { runtime: "acp", fallbackCategory: "sdk-contract", fallbackMessage: - "The installed Prime Agent is a stock build, so Pylon is using ACP compatibility mode: one account, Full access only, and model changes need a new thread. Install the Pylon Prime build to enable native mode.", + "The installed Prime Agent does not expose the public SDK required for native mode, so Pylon is using ACP compatibility mode: one account, Full access only, and model changes need a new thread. Install or repair the Pylon Prime build to enable native mode.", }); }), ); + it.effect("routes a compiled stock build without a public SDK to ACP", () => + Effect.gen(function* () { + const makeManager = () => + Effect.fail( + new PrimeAgentDaemonBridgeError({ + binaryPath: "/secret/compiled/prime-agent", + reason: "missing-public-entry", + detail: "The advertised public entry is absent.", + }), + ); + const dependencies = { + resolveExecutable: () => Effect.succeed("/secret/compiled/prime-agent"), + makeManager, + }; + const selection = yield* negotiatePrimeAgentBackend(baseInput, dependencies); + expect(selection).toMatchObject({ runtime: "acp", fallbackCategory: "sdk-contract" }); + const native = yield* negotiatePrimeAgentBackend( + { ...baseInput, requireNative: true }, + dependencies, + ); + expect(native).toMatchObject({ runtime: "unavailable", reason: "sdk-contract" }); + expect(selection.runtime === "acp" ? selection.fallbackMessage : undefined).not.toContain( + "/secret/", + ); + }), + ); + it.effect("selects ACP on native Windows without resolving, importing, or starting Prime", () => Effect.gen(function* () { let resolutionAttempts = 0; diff --git a/apps/server/src/provider/prime/PrimeAgentBackendSelection.ts b/apps/server/src/provider/prime/PrimeAgentBackendSelection.ts index 2a72ff9037..936d739ddd 100644 --- a/apps/server/src/provider/prime/PrimeAgentBackendSelection.ts +++ b/apps/server/src/provider/prime/PrimeAgentBackendSelection.ts @@ -9,8 +9,10 @@ import { PrimeAgentDaemonManagerError, type PrimeAgentDaemonManagerInput, } from "./PrimeAgentDaemonManager.ts"; +import { PrimeAgentDaemonBridgeError } from "./PrimeAgentDaemonBridge.ts"; const isPrimeAgentDaemonManagerError = Schema.is(PrimeAgentDaemonManagerError); +const isPrimeAgentDaemonBridgeError = Schema.is(PrimeAgentDaemonBridgeError); const FALLBACK_MESSAGES = { launchArgs: @@ -19,7 +21,7 @@ const FALLBACK_MESSAGES = { "Prime Agent daemon mode could not resolve the configured CLI; using ACP compatibility mode.", manager: "Prime Agent daemon integration is unavailable; using ACP compatibility mode.", sdkContract: - "The installed Prime Agent is a stock build, so Pylon is using ACP compatibility mode: one account, Full access only, and model changes need a new thread. Install the Pylon Prime build to enable native mode.", + "The installed Prime Agent does not expose the public SDK required for native mode, so Pylon is using ACP compatibility mode: one account, Full access only, and model changes need a new thread. Install or repair the Pylon Prime build to enable native mode.", } as const; const NATIVE_ONLY_MESSAGES = { @@ -167,20 +169,22 @@ export function negotiatePrimeAgentBackend< }), ); if (Result.isFailure(manager)) { - const isStockSdkContract = - isPrimeAgentDaemonManagerError(manager.failure) && - manager.failure.reason === "incompatible-hello" && - manager.failure.detail.includes("caller-owned session contract"); + const isMissingNativeSdkContract = + (isPrimeAgentDaemonManagerError(manager.failure) && + manager.failure.reason === "incompatible-hello" && + manager.failure.detail.includes("caller-owned session contract")) || + (isPrimeAgentDaemonBridgeError(manager.failure) && + manager.failure.reason === "missing-public-entry"); return input.requireNative === true ? ({ runtime: "unavailable", - reason: isStockSdkContract ? "sdk-contract" : "daemon-setup", + reason: isMissingNativeSdkContract ? "sdk-contract" : "daemon-setup", message: NATIVE_ONLY_MESSAGES.manager, } as const) : ({ runtime: "acp", - fallbackCategory: isStockSdkContract ? "sdk-contract" : "daemon-setup", - fallbackMessage: isStockSdkContract + fallbackCategory: isMissingNativeSdkContract ? "sdk-contract" : "daemon-setup", + fallbackMessage: isMissingNativeSdkContract ? FALLBACK_MESSAGES.sdkContract : FALLBACK_MESSAGES.manager, } as const); diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonBridge.test.ts b/apps/server/src/provider/prime/PrimeAgentDaemonBridge.test.ts index c9ac7281b1..e049cd3ac6 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonBridge.test.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonBridge.test.ts @@ -252,6 +252,16 @@ describe("PrimeAgentDaemonBridge", () => { }), ); + it.effect("identifies a compiled distribution with an advertised but absent SDK entry", () => + Effect.gen(function* () { + const pkg = makePackage({ name: "@earendil-works/pi-coding-agent" }); + NodeFS.rmSync(pkg.entryPath); + const error = yield* Effect.flip(loadPrimeAgentDaemonBridge(pkg.cliPath)); + + expect(error.reason).toBe("missing-public-entry"); + }), + ); + it.effect("does not infer negotiated daemon proof support from accessor presence", () => Effect.gen(function* () { const pkg = makePackage(); diff --git a/apps/server/src/provider/prime/PrimeAgentDaemonBridge.ts b/apps/server/src/provider/prime/PrimeAgentDaemonBridge.ts index c5dcc4c814..3740614ed3 100644 --- a/apps/server/src/provider/prime/PrimeAgentDaemonBridge.ts +++ b/apps/server/src/provider/prime/PrimeAgentDaemonBridge.ts @@ -23,6 +23,7 @@ const bridgeErrorReason = Schema.Literals([ "wrong-package", "invalid-package-manifest", "invalid-public-entry", + "missing-public-entry", "module-import-failed", "incompatible-version", "incompatible-protocol", @@ -579,6 +580,24 @@ async function resolvePublicEntry(binaryPath: string, located: LocatedPackage): } return canonicalEntry; } catch (cause) { + if (Predicate.isObject(cause) && "code" in cause && cause.code === "ENOENT") { + try { + await NodeFSP.lstat(candidate); + } catch (entryCause) { + if ( + Predicate.isObject(entryCause) && + "code" in entryCause && + entryCause.code === "ENOENT" + ) { + throw bridgeError( + binaryPath, + "missing-public-entry", + `The prime-agent package advertises '${relativeEntry}' but does not include it.`, + cause, + ); + } + } + } throw bridgeError( binaryPath, "invalid-public-entry", diff --git a/docs/user/providers-prime-agent.md b/docs/user/providers-prime-agent.md index dc64449bda..fd57402631 100644 --- a/docs/user/providers-prime-agent.md +++ b/docs/user/providers-prime-agent.md @@ -44,6 +44,10 @@ models. Stock Prime Agent runs in ACP compatibility mode with these limits: one account, Full access only, no approvals, model change requires a new thread, and no Goal/Harness/queue/resources controls. +The stock 0.9.5 standalone installer contains an executable but no public JavaScript SDK. Pylon +attempts ACP compatibility with that executable, but native mode and Prime-based title and source-control text +generation require an installation with a compatible public SDK. Select the Pylon-managed Prime build +for native mode, or choose another text-generation provider for titles and source-control writing. To enable native mode, open **Settings → Providers → Prime Agent** and choose **Install stable** under **Pylon-managed Prime**. If a managed build is already selected, the button is **Update stable**.