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
62 changes: 62 additions & 0 deletions apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3962,6 +3962,68 @@ describe("PrimeAgentDaemonAdapter", () => {
).pipe(Effect.provide(testLayer)),
);

it.effect("restores Prime's own default when a thread switches to Prime Agent Default", () =>
Effect.scoped(
Effect.gen(function* () {
const captures = makeCaptures();
const adapter = yield* makePrimeAgentDaemonAdapter(decodeSettings({}), manager, {
instanceId,
runtimeFactory: fakeRuntimeFactory(captures),
});
const subscription = yield* subscribe(adapter);
yield* adapter.startSession({
threadId,
cwd: process.cwd(),
runtimeMode: "full-access",
modelSelection: { instanceId, model: "openai/first" },
});
yield* awaitObservedType(subscription.observed, "thread.started");

const failure = yield* adapter
.sendTurn({ threadId, input: "hello", modelSelection: { instanceId, model: "default" } })
.pipe(Effect.flip);

// "default" is Pylon's sentinel for letting Prime choose, not a provider/model
// selector, so it must never reach setModel as an id.
expect(captures.models).not.toContain("default");
expect(failure).toMatchObject({
operation: "sendTurn",
issue: expect.stringContaining("cannot return to its own default model"),
});
}),
).pipe(Effect.provide(testLayer)),
);

it.effect("keeps running a thread that stays on Prime Agent Default", () =>
Effect.scoped(
Effect.gen(function* () {
const captures = makeCaptures();
const adapter = yield* makePrimeAgentDaemonAdapter(decodeSettings({}), manager, {
instanceId,
runtimeFactory: fakeRuntimeFactory(captures),
});
const subscription = yield* subscribe(adapter);
yield* adapter.startSession({
threadId,
cwd: process.cwd(),
runtimeMode: "full-access",
modelSelection: { instanceId, model: "default" },
});
yield* awaitObservedType(subscription.observed, "thread.started");

const turnFiber = yield* adapter
.sendTurn({ threadId, input: "hello", modelSelection: { instanceId, model: "default" } })
.pipe(Effect.forkChild);
yield* Queue.take(captures.promptObserved!);

// Prime already owns the model here, so the same selection is a no-op, not a change.
expect(captures.models).toEqual([]);

yield* Fiber.interrupt(turnFiber);
}),
).pipe(Effect.provide(testLayer)),
);

it.effect("persists a server-private identity behind the opaque v3 cursor", () =>
Effect.scoped(
Effect.gen(function* () {
Expand Down
23 changes: 23 additions & 0 deletions apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ import {
} from "./PrimeAgentSessionIdentity.ts";

const PROVIDER = ProviderDriverKind.make("primeAgent");
/** Defers to Prime's own configured or restored model instead of forcing one. */
const PRIME_AGENT_DEFAULT_MODEL = "default";
const SESSION_STATS_TIMEOUT_MS = 1_000;
const MODEL_DISCOVERY_TIMEOUT_MS = 15_000;
export const PRIME_AGENT_FAILED_RUN_SETTLEMENT_GRACE_MS = 3_000;
Expand Down Expand Up @@ -2823,6 +2825,27 @@ export function makePrimeAgentDaemonAdapter(
issue: "Prime Agent cannot start a turn during context compaction.",
});
}
// "default" defers to Prime's own model rather than naming one, and Prime
// exposes no daemon method to restore that choice inside a running session:
// `setModel` demands an explicit provider and id, and `getModelCatalog`
// returns only models and configuredProviders with no default to re-select.
// Reject the switch rather than run the old model behind a default label.
//
// Revisit when Prime publishes either a session method that returns model
// choice to its own default, or an authoritative default id in the catalog.
// Either one turns this into a real selection: probe for the capability and
// apply it here instead of failing. Tracked in the parity ledger.
if (
requestedModel === PRIME_AGENT_DEFAULT_MODEL &&
context.session.model !== PRIME_AGENT_DEFAULT_MODEL
) {
return yield* new ProviderAdapterValidationError({
provider: PROVIDER,
operation: "sendTurn",
issue:
"Prime Agent cannot return to its own default model in a running session. Start a new thread to use Prime Agent Default.",
});
}
yield* applyTurnSelection(context, input.threadId, requestedModel, turnControls);
const turnId = TurnId.make(yield* randomUUIDv4);
const turn: PrimeAgentDaemonActiveTurn = {
Expand Down
9 changes: 9 additions & 0 deletions docs/internals/prime-agent-daemon-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ export local file-backed `AuthStorage` and `SettingsManager` SDK APIs, but they
methods and require separate ownership, locking, callback, and reload design before Pylon can expose
them safely.

Returning model choice to Prime's own default belongs on that list. `setModel` demands an explicit
provider and id, and `AgentConnectionModelCatalog` carries only `models` and `configuredProviders`,
so a session that has already run on a named model cannot be handed back to Prime's default. Pylon's
**Prime Agent Default** selection is therefore honored when a session starts and refused mid-session,
rather than leaving a thread on its previous model behind a default label. Revisit when Prime
publishes either a session method that restores its own default or an authoritative default id in the
catalog: either one makes this a real selection, so probe for the capability and apply it in
`applyTurnSelection`'s caller instead of failing.

When Prime adds a public, reconnect-safe outcome, Pylon should add a capability probe and a typed
provider-neutral vertical rather than sending raw daemon commands across the client boundary.

Expand Down
5 changes: 5 additions & 0 deletions docs/user/providers-prime-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ Agent rejected the selection and that the model may no longer exist in its catal
in the picker to continue. Threads left on **Prime Agent Default** follow the new release's default
instead of failing.

**Prime Agent Default** can only be chosen for a thread that has not already run on a named model.
Prime Agent exposes no way to hand model choice back to itself inside a running session, so switching
an existing thread to it reports that the thread must start fresh rather than quietly continuing on
the model it was already using. Naming a different model in an existing thread still works normally.

While a daemon-backed turn is working, sending another message steers the same turn. The separate
**Queue follow-up** action admits the current draft for the next native run instead. Pylon shows only
privacy-safe steering and follow-up counts; it never sends queued prompt previews to clients. The
Expand Down
Loading