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,7 +14,10 @@ import {
negotiatePrimeAgentBackend,
type PrimeAgentBackendNegotiationInput,
} from "./PrimeAgentBackendSelection.ts";
import type { PrimeAgentDaemonManagerInput } from "./PrimeAgentDaemonManager.ts";
import {
PrimeAgentDaemonManagerError,
type PrimeAgentDaemonManagerInput,
} from "./PrimeAgentDaemonManager.ts";

const testManager = (id: string) => ({
id,
Expand Down Expand Up @@ -291,6 +294,29 @@ it.layer(NodeServices.layer)("negotiatePrimeAgentBackend", (it) => {
}),
);

it.effect("reports a stock Prime SDK as an sdk-contract fallback", () =>
Effect.gen(function* () {
const selection = yield* negotiatePrimeAgentBackend(baseInput, {
resolveExecutable: () => Effect.succeed("/fake/prime-agent"),
makeManager: () =>
Effect.fail(
new PrimeAgentDaemonManagerError({
reason: "incompatible-hello",
detail:
"The installed Prime Agent SDK does not provide the required caller-owned session contract.",
socket: "/tmp/fake.sock",
}),
),
});
expect(selection).toEqual({
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.",
});
}),
);

it.effect("selects ACP on native Windows without resolving, importing, or starting Prime", () =>
Effect.gen(function* () {
let resolutionAttempts = 0;
Expand Down
30 changes: 24 additions & 6 deletions apps/server/src/provider/prime/PrimeAgentBackendSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@ import type { PrimeAgentMaterializedIdentity } from "./PrimeAgentRuntimeContext.
import * as Effect from "effect/Effect";
import * as Path from "effect/Path";
import * as Result from "effect/Result";
import * as Schema from "effect/Schema";

import type { PrimeAgentDaemonManagerInput } from "./PrimeAgentDaemonManager.ts";
import {
PrimeAgentDaemonManagerError,
type PrimeAgentDaemonManagerInput,
} from "./PrimeAgentDaemonManager.ts";

const isPrimeAgentDaemonManagerError = Schema.is(PrimeAgentDaemonManagerError);

const FALLBACK_MESSAGES = {
launchArgs:
"Prime Agent daemon mode does not support custom launch arguments; using ACP compatibility mode.",
resolution:
"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.",
} as const;

const NATIVE_ONLY_MESSAGES = {
Expand All @@ -31,12 +39,16 @@ export type PrimeAgentBackendSelection<Manager> =
| { readonly runtime: "daemon"; readonly manager: Manager }
| {
readonly runtime: "acp";
readonly fallbackCategory?: "launch-args" | "binary-resolution" | "daemon-setup";
readonly fallbackCategory?:
| "launch-args"
| "binary-resolution"
| "daemon-setup"
| "sdk-contract";
readonly fallbackMessage?: string;
}
| {
readonly runtime: "unavailable";
readonly reason: "launch-args" | "binary-resolution" | "daemon-setup";
readonly reason: "launch-args" | "binary-resolution" | "daemon-setup" | "sdk-contract";
readonly message: string;
};

Expand Down Expand Up @@ -155,16 +167,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");
return input.requireNative === true
? ({
runtime: "unavailable",
reason: "daemon-setup",
reason: isStockSdkContract ? "sdk-contract" : "daemon-setup",
message: NATIVE_ONLY_MESSAGES.manager,
} as const)
: ({
runtime: "acp",
fallbackCategory: "daemon-setup",
fallbackMessage: FALLBACK_MESSAGES.manager,
fallbackCategory: isStockSdkContract ? "sdk-contract" : "daemon-setup",
fallbackMessage: isStockSdkContract
? FALLBACK_MESSAGES.sdkContract
: FALLBACK_MESSAGES.manager,
} as const);
}

Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/provider/prime/PrimeAgentRuntimeContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@ export interface PrimeAgentNativeProofIdentity {
export type PrimeAgentRuntimeBackendIdentity =
| {
readonly kind: "acp";
readonly fallbackCategory?: "launch-args" | "binary-resolution" | "daemon-setup";
readonly fallbackCategory?:
| "launch-args"
| "binary-resolution"
| "daemon-setup"
| "sdk-contract";
}
| {
readonly kind: "daemon";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,9 @@ describe("deriveProviderModelsForDisplay", () => {
);

expect(markup).toContain("sign in separately");
expect(markup).toContain("It does not enable multiple Prime instances");
expect(markup).toContain("Prime Agent runs one enabled account per Pylon environment");
expect(markup).toContain("A stock Prime Agent install runs in ACP compatibility mode");
expect(markup).toContain("Install the Pylon Prime build to enable native mode");
expect(markup).toContain("stop-all maintenance remains external");
expect(markup).toContain(`role="status">${reason}`);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ describe("provider multiple-instance presentation", () => {

expect(presentation).toEqual({ supported: false, reason });
expect(presentation.reason).not.toContain(privateValue);
expect(PRIME_AGENT_ACP_GUIDANCE).toContain("limited to one enabled account");
expect(PRIME_AGENT_ACP_GUIDANCE).toContain("does not enable multiple Prime instances");
expect(PRIME_AGENT_ACP_GUIDANCE).toBe(
"Prime Agent runs one enabled account per Pylon environment. A stock Prime Agent install runs in ACP compatibility mode (Full access only, model changes need a new thread). Install the Pylon Prime build to enable native mode; see the Prime Agent guide.",
);
expect(PRIME_AGENT_ACP_GUIDANCE).not.toContain(privateValue);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
export const PRIME_AGENT_INSTANCE_GUIDANCE =
"Use a distinct, non-nested Agent home for this instance, then sign in separately inside that home. Credentials, settings, model catalogs, sessions, sockets, checkpoints, and MCP state stay with that instance.";
export const PRIME_AGENT_ACP_GUIDANCE =
"Prime Agent is limited to one enabled account in this release. It does not enable multiple Prime instances. ACP compatibility is the explicit degraded fallback for that one account on macOS, Linux, and WSL2, and only after native teardown finishes.";
"Prime Agent runs one enabled account per Pylon environment. A stock Prime Agent install runs in ACP compatibility mode (Full access only, model changes need a new thread). Install the Pylon Prime build to enable native mode; see the Prime Agent guide.";
export const PRIME_AGENT_MAINTENANCE_GUIDANCE =
"Pylon manages per-instance Prime processes only. OS-user-global Prime update, doctor, shutdown, and stop-all maintenance remains external and is never run for an instance.";

Expand Down
Loading