Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
negotiatePrimeAgentBackend,
type PrimeAgentBackendNegotiationInput,
} from "./PrimeAgentBackendSelection.ts";
import { PrimeAgentDaemonBridgeError } from "./PrimeAgentDaemonBridge.ts";
import {
PrimeAgentDaemonManagerError,
type PrimeAgentDaemonManagerInput,
Expand Down Expand Up @@ -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;
Expand Down
20 changes: 12 additions & 8 deletions apps/server/src/provider/prime/PrimeAgentBackendSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 = {
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions apps/server/src/provider/prime/PrimeAgentDaemonBridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/provider/prime/PrimeAgentDaemonBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions docs/user/providers-prime-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
Expand Down
Loading