Make MCP provider controls match the app (#1198)

* Align MCP provider controls with app provider state

* Deepen provider snapshot routing

* Make provider snapshots the daemon authority

* Move provider shutdown behind a generic AgentClient seam

OpenCodeServerManager is now owned entirely by the OpenCode provider.
ProviderSnapshotManager.shutdown() and provider-registry.shutdownProviders()
materialize enabled clients and call an optional shutdown() per client; the
OpenCode client forwards to its runtime. Other providers ignore it.

Also wires providerSnapshotManager into the remaining Session-constructing
tests that were missing it (server-tests CI failure).

* Use platform-native cwd in change-event test
This commit is contained in:
Mohamed Boudra
2026-05-28 18:25:09 +08:00
committed by GitHub
parent 8234ecb7ba
commit 3176f844e7
27 changed files with 1731 additions and 2454 deletions
@@ -48,6 +48,7 @@ export interface AgentMode {
description?: string; description?: string;
icon?: string; icon?: string;
colorTier?: string; colorTier?: string;
isUnattended?: boolean;
} }
export type ProviderStatus = "ready" | "loading" | "error" | "unavailable"; export type ProviderStatus = "ready" | "loading" | "error" | "unavailable";
@@ -93,6 +94,32 @@ export interface ProviderSnapshotEntry {
defaultModeId?: string | null; defaultModeId?: string | null;
} }
export interface AgentCreateConfigParent {
provider: AgentProvider;
modeId: string | null;
isUnattended: boolean;
}
export interface ResolveAgentCreateConfigInput {
provider: AgentProvider;
requestedMode: string | undefined;
featureValues: Record<string, unknown> | undefined;
parent: AgentCreateConfigParent | null;
availableModes: AgentMode[] | undefined;
}
export interface ResolveAgentCreateConfigResult {
modeId: string | undefined;
featureValues: Record<string, unknown> | undefined;
}
export interface AgentCreateConfigUnattendedInput {
modeId: string | null;
config: AgentSessionConfig;
features?: AgentFeature[];
availableModes: AgentMode[];
}
export interface AgentFeatureToggle { export interface AgentFeatureToggle {
type: "toggle"; type: "toggle";
id: string; id: string;
@@ -590,6 +617,8 @@ export interface AgentClient {
): Promise<AgentSession>; ): Promise<AgentSession>;
listModels(options: ListModelsOptions): Promise<AgentModelDefinition[]>; listModels(options: ListModelsOptions): Promise<AgentModelDefinition[]>;
listModes?(options: ListModesOptions): Promise<AgentMode[]>; listModes?(options: ListModesOptions): Promise<AgentMode[]>;
resolveCreateConfig?(input: ResolveAgentCreateConfigInput): ResolveAgentCreateConfigResult;
isCreateConfigUnattended?(input: AgentCreateConfigUnattendedInput): boolean;
listCommands?(config: AgentSessionConfig): Promise<AgentSlashCommand[]>; listCommands?(config: AgentSessionConfig): Promise<AgentSlashCommand[]>;
listFeatures?(config: AgentSessionConfig): Promise<AgentFeature[]>; listFeatures?(config: AgentSessionConfig): Promise<AgentFeature[]>;
listPersistedAgents?(options?: ListPersistedAgentsOptions): Promise<PersistedAgentDescriptor[]>; listPersistedAgents?(options?: ListPersistedAgentsOptions): Promise<PersistedAgentDescriptor[]>;
@@ -604,4 +633,10 @@ export interface AgentClient {
* Called when Paseo archives an agent so the provider's own UI reflects the same state. * Called when Paseo archives an agent so the provider's own UI reflects the same state.
*/ */
archiveNativeSession?(handle: AgentPersistenceHandle): Promise<void>; archiveNativeSession?(handle: AgentPersistenceHandle): Promise<void>;
/**
* Release any provider-owned resources held by this client (background
* processes, sockets, cached subprocesses, etc.). Called when the daemon
* shuts down. Must be idempotent.
*/
shutdown?(): Promise<void>;
} }
@@ -1,4 +1,10 @@
import type { AgentProvider } from "./agent-sdk-types.js"; import type {
AgentCreateConfigUnattendedInput,
AgentMode,
AgentProvider,
ResolveAgentCreateConfigInput,
ResolveAgentCreateConfigResult,
} from "./agent-sdk-types.js";
interface CreateAgentModeParent { interface CreateAgentModeParent {
provider: AgentProvider; provider: AgentProvider;
@@ -55,3 +61,32 @@ export function resolveAndValidateCreateAgentMode(
`cannot inherit mode '${parent.modeId ?? "<none>"}' from caller (provider '${parent.provider}') for new agent (provider '${targetProvider}'). Pass an explicit mode. Available modes for '${targetProvider}': ${listModes(availableModes)}`, `cannot inherit mode '${parent.modeId ?? "<none>"}' from caller (provider '${parent.provider}') for new agent (provider '${targetProvider}'). Pass an explicit mode. Available modes for '${targetProvider}': ${listModes(availableModes)}`,
); );
} }
export function resolveDefaultAgentCreateConfig(
input: ResolveAgentCreateConfigInput,
): ResolveAgentCreateConfigResult {
const availableModeIds = input.availableModes?.map((mode) => mode.id);
return {
modeId: resolveAndValidateCreateAgentMode({
requestedMode: input.requestedMode,
targetProvider: input.provider,
parent: input.parent,
availableModes: availableModeIds,
targetUnattendedMode: input.availableModes?.find(isUnattendedMode)?.id,
}),
featureValues: input.featureValues,
};
}
export function isDefaultAgentCreateConfigUnattended(
input: AgentCreateConfigUnattendedInput,
): boolean {
if (input.modeId === null) {
return false;
}
return input.availableModes.some((mode) => mode.id === input.modeId && isUnattendedMode(mode));
}
function isUnattendedMode(mode: AgentMode): boolean {
return mode.isUnattended === true;
}
@@ -16,17 +16,14 @@ import type { AgentAttachment, FirstAgentContext, GitSetupOptions } from "../../
import type { AgentManager, ManagedAgent } from "../agent-manager.js"; import type { AgentManager, ManagedAgent } from "../agent-manager.js";
import { scheduleAgentMetadataGeneration } from "../agent-metadata-generator.js"; import { scheduleAgentMetadataGeneration } from "../agent-metadata-generator.js";
import type { import type {
AgentProvider,
AgentPromptContentBlock, AgentPromptContentBlock,
AgentPromptInput, AgentPromptInput,
AgentRunOptions, AgentRunOptions,
AgentSessionConfig, AgentSessionConfig,
} from "../agent-sdk-types.js"; } from "../agent-sdk-types.js";
import type { AgentStorage } from "../agent-storage.js"; import type { AgentStorage } from "../agent-storage.js";
import { getAgentProviderDefinition } from "@getpaseo/protocol/provider-manifest"; import type { ProviderSnapshotManager } from "../provider-snapshot-manager.js";
import type { ProviderDefinition } from "../provider-registry.js";
import { setupFinishNotification, startCreatedAgentInitialPrompt } from "../agent-prompt.js"; import { setupFinishNotification, startCreatedAgentInitialPrompt } from "../agent-prompt.js";
import { resolveAndValidateCreateAgentMode } from "../create-agent-mode.js";
import { resolveClientMessageId } from "../../client-message-id.js"; import { resolveClientMessageId } from "../../client-message-id.js";
import { resolveRequiredProviderModel } from "../mcp-shared.js"; import { resolveRequiredProviderModel } from "../mcp-shared.js";
import { import {
@@ -34,51 +31,6 @@ import {
emitLiveTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown,
} from "../timeline-append.js"; } from "../timeline-append.js";
const OPENCODE_PROVIDER_ID = "opencode";
const OPENCODE_BUILD_MODE_ID = "build";
const OPENCODE_LEGACY_FULL_ACCESS_MODE_ID = "full-access";
const OPENCODE_AUTO_ACCEPT_FEATURE_ID = "auto_accept";
function isOpenCodeLegacyFullAccessMode(
provider: AgentProvider,
modeId: string | undefined,
): boolean {
return provider === OPENCODE_PROVIDER_ID && modeId === OPENCODE_LEGACY_FULL_ACCESS_MODE_ID;
}
function withOpenCodeAutoAcceptFeature(
features: Record<string, unknown> | undefined,
enabled: boolean,
): Record<string, unknown> {
return {
...features,
[OPENCODE_AUTO_ACCEPT_FEATURE_ID]: enabled,
};
}
function hasOpenCodeAutoAcceptFeature(agent: ManagedAgent): boolean {
if (agent.provider !== OPENCODE_PROVIDER_ID) {
return false;
}
return (
agent.features?.some(
(feature) =>
feature.id === OPENCODE_AUTO_ACCEPT_FEATURE_ID &&
(feature.value === true || feature.value === "true"),
) === true || agent.config.featureValues?.[OPENCODE_AUTO_ACCEPT_FEATURE_ID] === true
);
}
function isAgentInUnattendedState(
dependencies: CreateAgentCommandDependencies,
agent: ManagedAgent,
): boolean {
return (
isParentInUnattendedMode(dependencies, agent.provider, agent.currentModeId) ||
hasOpenCodeAutoAcceptFeature(agent)
);
}
export interface CreateAgentWorkspace { export interface CreateAgentWorkspace {
workspaceId: string; workspaceId: string;
} }
@@ -98,7 +50,7 @@ interface CreateAgentCommandDependencies {
"getSnapshot" | "listWorktrees" | "resolveRepoRoot" "getSnapshot" | "listWorktrees" | "resolveRepoRoot"
>; >;
terminalManager?: TerminalManager | null; terminalManager?: TerminalManager | null;
providerRegistry?: Record<AgentProvider, ProviderDefinition> | null; providerSnapshotManager: ProviderSnapshotManager;
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn; createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
} }
@@ -293,22 +245,14 @@ async function resolveMcpCreateAgent(
initialPrompt: input.initialPrompt, initialPrompt: input.initialPrompt,
}); });
const parentForResolve = parentAgent const { modeId: resolvedMode, featureValues: resolvedFeatures } =
? { await dependencies.providerSnapshotManager.resolveCreateConfig({
provider: parentAgent.provider, cwd: resolvedCwd,
modeId: parentAgent.currentModeId,
isUnattended: isAgentInUnattendedState(dependencies, parentAgent),
}
: null;
const { mode: resolvedMode, features: resolvedFeatures } = resolveCreateModeAndFeatures(
dependencies,
{
provider, provider,
requestedMode: input.mode, requestedMode: input.mode,
parent: parentForResolve, featureValues: input.features,
features: input.features, parent: parentAgent,
}, });
);
const labels = mergeLabels( const labels = mergeLabels(
input.callerAgentId, input.callerAgentId,
@@ -337,44 +281,6 @@ async function resolveMcpCreateAgent(
}; };
} }
function resolveCreateModeAndFeatures(
dependencies: CreateAgentCommandDependencies,
input: {
provider: AgentProvider;
requestedMode: string | undefined;
parent: { provider: AgentProvider; modeId: string | null; isUnattended: boolean } | null;
features: Record<string, unknown> | undefined;
},
): { mode: string | undefined; features: Record<string, unknown> | undefined } {
const legacyOpenCodeFullAccess = isOpenCodeLegacyFullAccessMode(
input.provider,
input.requestedMode,
);
const inheritsOpenCodeUnattended =
input.provider === OPENCODE_PROVIDER_ID &&
input.requestedMode === undefined &&
input.parent?.isUnattended === true;
const inheritsOpenCodeAutoAccept =
inheritsOpenCodeUnattended && input.features?.[OPENCODE_AUTO_ACCEPT_FEATURE_ID] === undefined;
const requestedMode = legacyOpenCodeFullAccess ? OPENCODE_BUILD_MODE_ID : input.requestedMode;
const features =
legacyOpenCodeFullAccess || inheritsOpenCodeAutoAccept
? withOpenCodeAutoAcceptFeature(input.features, true)
: input.features;
const mode =
inheritsOpenCodeUnattended && requestedMode === undefined
? OPENCODE_BUILD_MODE_ID
: resolveAndValidateCreateAgentMode({
requestedMode,
targetProvider: input.provider,
parent: input.parent,
availableModes: getAvailableModeIds(dependencies, input.provider),
targetUnattendedMode: getUnattendedModeId(dependencies, input.provider),
});
return { mode, features };
}
async function sendInitialPrompt( async function sendInitialPrompt(
dependencies: CreateAgentCommandDependencies, dependencies: CreateAgentCommandDependencies,
resolved: ResolvedCreateAgent, resolved: ResolvedCreateAgent,
@@ -565,47 +471,3 @@ function mergeLabels(
}; };
return Object.keys(mergedLabels).length > 0 ? mergedLabels : undefined; return Object.keys(mergedLabels).length > 0 ? mergedLabels : undefined;
} }
function getProviderModes(
dependencies: CreateAgentCommandDependencies,
provider: AgentProvider,
): ProviderDefinition["modes"] | undefined {
const fromRegistry = dependencies.providerRegistry?.[provider];
if (fromRegistry) {
return fromRegistry.modes;
}
try {
return getAgentProviderDefinition(provider).modes;
} catch {
return undefined;
}
}
function getAvailableModeIds(
dependencies: CreateAgentCommandDependencies,
provider: AgentProvider,
): string[] | undefined {
return getProviderModes(dependencies, provider)?.map((mode) => mode.id);
}
function getUnattendedModeId(
dependencies: CreateAgentCommandDependencies,
provider: AgentProvider,
): string | undefined {
return getProviderModes(dependencies, provider)?.find((mode) => mode.isUnattended)?.id;
}
function isParentInUnattendedMode(
dependencies: CreateAgentCommandDependencies,
provider: AgentProvider,
modeId: string | null,
): boolean {
if (modeId === null) {
return false;
}
const modes = getProviderModes(dependencies, provider);
if (!modes) {
return false;
}
return modes.some((mode) => mode.id === modeId && mode.isUnattended === true);
}
@@ -208,7 +208,7 @@ test("listImportableProviderSessions filters, sorts, limits, and projects import
}), }),
agentManager, agentManager,
agentStorage, agentStorage,
providerRegistry: { codex: { label: "Codex" } }, providerSnapshotManager: { getProviderLabel: () => "Codex" },
}); });
expect(listImportablePersistedAgents).toHaveBeenCalledWith({ expect(listImportablePersistedAgents).toHaveBeenCalledWith({
@@ -274,7 +274,7 @@ test("listImportableProviderSessions filters out metadata generation sessions",
agentStorage: { agentStorage: {
list: async () => [], list: async () => [],
} satisfies Pick<AgentStorage, "list">, } satisfies Pick<AgentStorage, "list">,
providerRegistry: { codex: { label: "Codex" } }, providerSnapshotManager: { getProviderLabel: () => "Codex" },
}); });
expect(result.entries).toHaveLength(1); expect(result.entries).toHaveLength(1);
@@ -309,7 +309,7 @@ test("listImportableProviderSessions keeps realpath-equivalent cwd matches", asy
agentStorage: { agentStorage: {
list: async () => [], list: async () => [],
} satisfies Pick<AgentStorage, "list">, } satisfies Pick<AgentStorage, "list">,
providerRegistry: { pi: { label: "Pi" } }, providerSnapshotManager: { getProviderLabel: () => "Pi" },
}); });
expect(result.entries.map((entry) => entry.providerHandleId)).toEqual(["pi-handle"]); expect(result.entries.map((entry) => entry.providerHandleId)).toEqual(["pi-handle"]);
@@ -326,7 +326,7 @@ test("listImportableProviderSessions rejects invalid since values", async () =>
agentStorage: { agentStorage: {
list: async () => [], list: async () => [],
} satisfies Pick<AgentStorage, "list">, } satisfies Pick<AgentStorage, "list">,
providerRegistry: {}, providerSnapshotManager: { getProviderLabel: () => "" },
}), }),
).rejects.toMatchObject( ).rejects.toMatchObject(
new ImportSessionsRequestError("invalid_since", "Invalid recent provider sessions since"), new ImportSessionsRequestError("invalid_since", "Invalid recent provider sessions since"),
@@ -1,6 +1,6 @@
import type { z } from "zod"; import type { z } from "zod";
import type { Logger } from "pino"; import type { Logger } from "pino";
import type { ProviderDefinition } from "./provider-registry.js"; import type { ProviderSnapshotManager } from "./provider-snapshot-manager.js";
import type { AgentManager, ManagedAgent } from "./agent-manager.js"; import type { AgentManager, ManagedAgent } from "./agent-manager.js";
import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js"; import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
import type { import type {
@@ -49,7 +49,7 @@ export interface ListImportableProviderSessionsInput {
request: FetchRecentProviderSessionsRequestMessage; request: FetchRecentProviderSessionsRequestMessage;
agentManager: Pick<AgentManager, "listAgents" | "listImportablePersistedAgents">; agentManager: Pick<AgentManager, "listAgents" | "listImportablePersistedAgents">;
agentStorage: Pick<AgentStorage, "list">; agentStorage: Pick<AgentStorage, "list">;
providerRegistry: Record<string, Pick<ProviderDefinition, "label"> | undefined>; providerSnapshotManager: Pick<ProviderSnapshotManager, "getProviderLabel">;
} }
export interface ListImportableProviderSessionsResult { export interface ListImportableProviderSessionsResult {
@@ -99,7 +99,7 @@ export function normalizeImportAgentRequest(
export async function listImportableProviderSessions( export async function listImportableProviderSessions(
input: ListImportableProviderSessionsInput, input: ListImportableProviderSessionsInput,
): Promise<ListImportableProviderSessionsResult> { ): Promise<ListImportableProviderSessionsResult> {
const { request, agentManager, agentStorage, providerRegistry } = input; const { request, agentManager, agentStorage, providerSnapshotManager } = input;
const limit = request.limit ?? 20; const limit = request.limit ?? 20;
const sinceTimestamp = parseRecentProviderSessionsSince(request.since); const sinceTimestamp = parseRecentProviderSessionsSince(request.since);
const providerFilter = request.providers ? new Set(request.providers) : undefined; const providerFilter = request.providers ? new Set(request.providers) : undefined;
@@ -140,7 +140,7 @@ export async function listImportableProviderSessions(
.slice(0, limit) .slice(0, limit)
.map((descriptor) => .map((descriptor) =>
toRecentProviderSessionDescriptorPayload(descriptor, { toRecentProviderSessionDescriptorPayload(descriptor, {
providerLabel: providerRegistry[descriptor.provider]?.label ?? descriptor.provider, providerLabel: providerSnapshotManager.getProviderLabel(descriptor.provider),
}), }),
); );
File diff suppressed because it is too large Load Diff
+44 -116
View File
@@ -9,7 +9,7 @@ import type {
ServerRequest, ServerRequest,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import type { AgentProvider } from "./agent-sdk-types.js"; import type { AgentMode, AgentProvider } from "./agent-sdk-types.js";
import type { AgentManager, WaitForAgentResult } from "./agent-manager.js"; import type { AgentManager, WaitForAgentResult } from "./agent-manager.js";
import { import {
AgentFeatureSchema, AgentFeatureSchema,
@@ -47,8 +47,7 @@ import {
type ScheduleCadence, type ScheduleCadence,
type UpdateScheduleInput, type UpdateScheduleInput,
} from "@getpaseo/protocol/schedule/types"; } from "@getpaseo/protocol/schedule/types";
import type { ProviderDefinition } from "./provider-registry.js"; import { resolveSnapshotCwd, type ProviderSnapshotManager } from "./provider-snapshot-manager.js";
import { resolveSnapshotCwd } from "./provider-snapshot-manager.js";
import { import {
AgentModelSchema, AgentModelSchema,
AgentProviderEnum, AgentProviderEnum,
@@ -88,7 +87,7 @@ export interface AgentMcpServerOptions {
terminalManager?: TerminalManager | null; terminalManager?: TerminalManager | null;
getDaemonTcpPort?: () => number | null; getDaemonTcpPort?: () => number | null;
scheduleService?: ScheduleService | null; scheduleService?: ScheduleService | null;
providerRegistry?: Record<AgentProvider, ProviderDefinition> | null; providerSnapshotManager: ProviderSnapshotManager;
github?: GitHubService; github?: GitHubService;
workspaceGitService?: Pick< workspaceGitService?: Pick<
WorkspaceGitService, WorkspaceGitService,
@@ -117,47 +116,6 @@ export interface AgentMcpServerOptions {
logger: Logger; logger: Logger;
} }
const CLAUDE_TO_CODEX_MODE: Record<string, string> = {
plan: "read-only",
default: "auto",
acceptEdits: "auto",
bypassPermissions: "full-access",
};
const CODEX_TO_CLAUDE_MODE: Record<string, string> = {
"read-only": "plan",
auto: "default",
"full-access": "bypassPermissions",
};
function mapModeAcrossProviders(
sourceMode: string,
sourceProvider: AgentProvider,
targetProvider: AgentProvider,
): string {
if (sourceProvider === targetProvider) {
return sourceMode;
}
if (sourceProvider === "claude" && targetProvider === "codex") {
const mapped = CLAUDE_TO_CODEX_MODE[sourceMode];
if (mapped) {
return mapped;
}
return "auto";
}
if (sourceProvider === "codex" && targetProvider === "claude") {
const mapped = CODEX_TO_CLAUDE_MODE[sourceMode];
if (mapped) {
return mapped;
}
return "default";
}
return sourceMode;
}
function addModelVisibleStructuredContent(result: CallToolResult): CallToolResult { function addModelVisibleStructuredContent(result: CallToolResult): CallToolResult {
if (result.structuredContent === undefined || result.content.length > 0) { if (result.structuredContent === undefined || result.content.length > 0) {
return result; return result;
@@ -257,48 +215,34 @@ function resolveAgentListActivityTime(agent: AgentListItemPayload): number {
); );
} }
function resolveRegisteredProviderIds(
agentManager: AgentManager,
providerRegistry: Record<AgentProvider, ProviderDefinition> | null | undefined,
): AgentProvider[] {
return providerRegistry ? Object.keys(providerRegistry) : agentManager.getRegisteredProviderIds();
}
interface ProviderSummary { interface ProviderSummary {
id: AgentProvider; id: AgentProvider;
label: string; label: string;
description: string; description: string;
enabled: boolean; enabled: boolean;
modes: ProviderDefinition["modes"]; modes: AgentMode[];
status: string; status: string;
error?: string; error?: string;
} }
async function resolveProviderSummary( function toProviderSummary(entry: {
provider: ProviderDefinition, provider: AgentProvider;
logger: Logger, label?: string;
): Promise<ProviderSummary> { description?: string;
const base = { enabled: boolean;
id: provider.id, modes?: AgentMode[];
label: provider.label, status: string;
description: provider.description, error?: string;
modes: provider.modes, }): ProviderSummary {
return {
id: entry.provider,
label: entry.label ?? entry.provider,
description: entry.description ?? "",
enabled: entry.enabled,
modes: entry.modes ?? [],
status: entry.status === "ready" ? "available" : entry.status,
...(entry.error ? { error: entry.error } : {}),
}; };
if (!provider.enabled) {
return { ...base, enabled: false, status: "unavailable" };
}
try {
const available = await provider.createClient(logger).isAvailable();
return { ...base, enabled: true, status: available ? "available" : "unavailable" };
} catch (availabilityError) {
return {
...base,
enabled: true,
status: "unavailable",
error:
availabilityError instanceof Error ? availabilityError.message : String(availabilityError),
};
}
} }
function compareAgentListItems(a: AgentListItemPayload, b: AgentListItemPayload): number { function compareAgentListItems(a: AgentListItemPayload, b: AgentListItemPayload): number {
@@ -539,7 +483,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
agentStorage, agentStorage,
terminalManager, terminalManager,
scheduleService, scheduleService,
providerRegistry, providerSnapshotManager,
callerAgentId, callerAgentId,
resolveSpeakHandler, resolveSpeakHandler,
resolveCallerContext, resolveCallerContext,
@@ -639,13 +583,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
return { return {
provider: resolvedProvider, provider: resolvedProvider,
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : callerAgent.cwd, cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : callerAgent.cwd,
...(callerAgent.currentModeId ...(callerAgent.currentModeId && callerAgent.provider === resolvedProvider
? { ? {
modeId: mapModeAcrossProviders( modeId: callerAgent.currentModeId,
callerAgent.currentModeId,
callerAgent.provider,
resolvedProvider,
),
} }
: {}), : {}),
...(resolvedModel ? { model: resolvedModel } : {}), ...(resolvedModel ? { model: resolvedModel } : {}),
@@ -927,7 +867,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
paseoHome: options.paseoHome, paseoHome: options.paseoHome,
workspaceGitService: options.workspaceGitService, workspaceGitService: options.workspaceGitService,
terminalManager, terminalManager,
providerRegistry, providerSnapshotManager,
createPaseoWorktree: options.createPaseoWorktree, createPaseoWorktree: options.createPaseoWorktree,
}, },
{ {
@@ -1249,7 +1189,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
const structuredSnapshot = buildStoredAgentPayload( const structuredSnapshot = buildStoredAgentPayload(
record, record,
resolveRegisteredProviderIds(agentManager, providerRegistry), providerSnapshotManager.listRegisteredProviderIds(),
); );
return { return {
content: [], content: [],
@@ -1296,7 +1236,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
); );
const liveIds = new Set(liveSnapshots.map((snapshot) => snapshot.id)); const liveIds = new Set(liveSnapshots.map((snapshot) => snapshot.id));
const storedRecords = await agentStorage.list(); const storedRecords = await agentStorage.list();
const registeredProviderIds = resolveRegisteredProviderIds(agentManager, providerRegistry); const registeredProviderIds = providerSnapshotManager.listRegisteredProviderIds();
const storedAgents = storedRecords const storedAgents = storedRecords
.filter((record) => !record.internal && !liveIds.has(record.id)) .filter((record) => !record.internal && !liveIds.has(record.id))
.filter((record) => includeArchived || !record.archivedAt) .filter((record) => includeArchived || !record.archivedAt)
@@ -1941,10 +1881,8 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
}, },
}, },
async () => { async () => {
const providers = await Promise.all( const providers = (await providerSnapshotManager.listProviders({ wait: true })).map(
Object.values(providerRegistry ?? {}).map((provider) => toProviderSummary,
resolveProviderSummary(provider, childLogger),
),
); );
return { return {
content: [], content: [],
@@ -1967,19 +1905,11 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
}, },
}, },
async ({ provider }) => { async ({ provider }) => {
if (!providerRegistry) { const models = await providerSnapshotManager.listModels({
throw new Error("Provider registry is not configured"); cwd: resolveSnapshotCwd(),
} provider,
wait: true,
const definition = providerRegistry[provider]; });
if (!definition) {
throw new Error(`Provider ${provider} is not configured`);
}
if (!definition.enabled) {
throw new Error(`Provider '${provider}' is disabled`);
}
const models = await definition.fetchModels({ cwd: resolveSnapshotCwd(), force: false });
return { return {
content: [], content: [],
structuredContent: ensureValidJson({ structuredContent: ensureValidJson({
@@ -2014,21 +1944,19 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
defaultProvider: provider, defaultProvider: provider,
}); });
const providerId = resolvedProviderModel.provider; const providerId = resolvedProviderModel.provider;
if (!providerRegistry) { const resolvedCwd = resolveScopedCwd(cwd, { required: true });
throw new Error("Provider registry is not configured"); const entry = await providerSnapshotManager.getProvider({
} cwd: resolvedCwd,
const definition = providerRegistry[providerId]; provider: providerId,
if (!definition) { wait: true,
throw new Error(`Provider ${providerId} is not configured`); });
} const summary = toProviderSummary(entry);
const summary = await resolveProviderSummary(definition, childLogger); if (!entry.enabled) {
if (!definition.enabled) {
throw new Error(`Provider '${providerId}' is disabled`); throw new Error(`Provider '${providerId}' is disabled`);
} }
if (summary.status !== "available") { if (entry.status !== "ready") {
throw new Error(summary.error ?? `Provider '${providerId}' is unavailable`); throw new Error(entry.error ?? `Provider '${providerId}' is unavailable`);
} }
const resolvedCwd = resolveScopedCwd(cwd, { required: true });
const selectedModel = settings?.model ?? resolvedProviderModel.model; const selectedModel = settings?.model ?? resolvedProviderModel.model;
const features = await agentManager.listDraftFeatures({ const features = await agentManager.listDraftFeatures({
provider: providerId, provider: providerId,
@@ -1,151 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createTestLogger } from "../../test-utils/test-logger.js";
import { resolveAgentModel } from "./model-resolver.js";
vi.mock("./provider-registry.js", () => ({
buildProviderRegistry: vi.fn(),
isProviderEnabled: vi.fn((definition: { enabled: boolean }) => definition.enabled),
}));
import { buildProviderRegistry } from "./provider-registry.js";
const mockedBuildProviderRegistry = vi.mocked(buildProviderRegistry);
const testLogger = createTestLogger();
const testLoggerWarn = vi.spyOn(testLogger, "warn");
type ProviderRegistryMock = ReturnType<typeof buildProviderRegistry>;
function makeMockRegistry(
entries: Record<string, { enabled: boolean; fetchModels: ReturnType<typeof vi.fn> }>,
): ProviderRegistryMock {
const registry: ProviderRegistryMock = Object.create(null);
for (const [key, val] of Object.entries(entries)) {
Reflect.set(registry, key, val);
}
return registry;
}
describe("resolveAgentModel", () => {
beforeEach(() => {
mockedBuildProviderRegistry.mockReset();
testLoggerWarn.mockClear();
});
it("returns the trimmed requested model when provided", async () => {
mockedBuildProviderRegistry.mockReturnValue(
makeMockRegistry({
claude: { enabled: true, fetchModels: vi.fn() },
codex: { enabled: true, fetchModels: vi.fn() },
opencode: { enabled: true, fetchModels: vi.fn() },
}),
);
const result = await resolveAgentModel({
provider: "codex",
requestedModel: " gpt-5.1 ",
cwd: "/tmp",
logger: testLogger,
});
expect(result).toBe("gpt-5.1");
expect(mockedBuildProviderRegistry).toHaveBeenCalledWith(testLogger);
});
it("uses the default model from the provider catalog when no model specified", async () => {
const fetchModels = vi.fn().mockResolvedValue([
{ id: "claude-3.5-haiku", isDefault: false },
{ id: "claude-3.5-sonnet", isDefault: true },
]);
mockedBuildProviderRegistry.mockReturnValue(
makeMockRegistry({
claude: { enabled: true, fetchModels },
codex: { enabled: true, fetchModels: vi.fn() },
opencode: { enabled: true, fetchModels: vi.fn() },
}),
);
const result = await resolveAgentModel({
provider: "claude",
cwd: "~/repo",
logger: testLogger,
});
expect(result).toBe("claude-3.5-sonnet");
expect(fetchModels).toHaveBeenCalledWith({
cwd: expect.stringMatching(/repo$/),
force: false,
});
});
it("falls back to the first model when none are flagged as default", async () => {
const fetchModels = vi.fn().mockResolvedValue([
{ id: "model-a", isDefault: false },
{ id: "model-b", isDefault: false },
]);
mockedBuildProviderRegistry.mockReturnValue(
makeMockRegistry({
claude: { enabled: true, fetchModels: vi.fn() },
codex: { enabled: true, fetchModels },
opencode: { enabled: true, fetchModels: vi.fn() },
}),
);
const result = await resolveAgentModel({ provider: "codex", logger: testLogger });
expect(result).toBe("model-a");
});
it("returns undefined when the catalog lookup fails", async () => {
const fetchModels = vi.fn().mockRejectedValue(new Error("boom"));
mockedBuildProviderRegistry.mockReturnValue(
makeMockRegistry({
claude: { enabled: true, fetchModels: vi.fn() },
codex: { enabled: true, fetchModels },
opencode: { enabled: true, fetchModels: vi.fn() },
}),
);
const result = await resolveAgentModel({ provider: "codex", logger: testLogger });
expect(result).toBeUndefined();
expect(testLoggerWarn).toHaveBeenCalled();
});
it("returns undefined for a disabled provider without fetching default models", async () => {
const fetchModels = vi.fn().mockResolvedValue([{ id: "model-a", isDefault: true }]);
mockedBuildProviderRegistry.mockReturnValue(
makeMockRegistry({
claude: { enabled: true, fetchModels: vi.fn() },
codex: { enabled: false, fetchModels },
opencode: { enabled: true, fetchModels: vi.fn() },
}),
);
const result = await resolveAgentModel({ provider: "codex", logger: testLogger });
expect(result).toBeUndefined();
expect(fetchModels).not.toHaveBeenCalled();
expect(testLoggerWarn).toHaveBeenCalled();
});
it("returns undefined for a requested model from a disabled provider", async () => {
const fetchModels = vi.fn();
mockedBuildProviderRegistry.mockReturnValue(
makeMockRegistry({
claude: { enabled: true, fetchModels: vi.fn() },
codex: { enabled: false, fetchModels },
opencode: { enabled: true, fetchModels: vi.fn() },
}),
);
const result = await resolveAgentModel({
provider: "codex",
requestedModel: "gpt-5.1",
logger: testLogger,
});
expect(result).toBeUndefined();
expect(fetchModels).not.toHaveBeenCalled();
expect(testLoggerWarn).toHaveBeenCalled();
});
});
@@ -1,42 +0,0 @@
import { buildProviderRegistry } from "./provider-registry.js";
import { resolveSnapshotCwd } from "./provider-snapshot-manager.js";
import type { AgentProvider } from "./agent-sdk-types.js";
import { expandTilde } from "../../utils/path.js";
import type { Logger } from "pino";
interface ResolveAgentModelOptions {
provider: AgentProvider;
requestedModel?: string | null;
cwd?: string;
logger: Logger;
}
export async function resolveAgentModel(
options: ResolveAgentModelOptions,
): Promise<string | undefined> {
try {
const providerRegistry = buildProviderRegistry(options.logger);
const providerDefinition = providerRegistry[options.provider];
if (!providerDefinition.enabled) {
throw new Error(`Provider '${options.provider}' is disabled`);
}
const trimmed = options.requestedModel?.trim();
if (trimmed) {
return trimmed;
}
const models = await providerDefinition.fetchModels({
cwd: resolveSnapshotCwd(options.cwd ? expandTilde(options.cwd) : undefined),
force: false,
});
const preferred = models.find((model) => model.isDefault) ?? models[0];
return preferred?.id;
} catch (error) {
options.logger.warn(
{ err: error, provider: options.provider },
"Failed to resolve default model",
);
return undefined;
}
}
@@ -2,6 +2,7 @@ import type { Logger } from "pino";
import type { import type {
AgentClient, AgentClient,
AgentCreateConfigUnattendedInput,
AgentMode, AgentMode,
AgentModelDefinition, AgentModelDefinition,
AgentPersistenceHandle, AgentPersistenceHandle,
@@ -13,7 +14,13 @@ import type {
ListModesOptions, ListModesOptions,
ListPersistedAgentsOptions, ListPersistedAgentsOptions,
PersistedAgentDescriptor, PersistedAgentDescriptor,
ResolveAgentCreateConfigInput,
ResolveAgentCreateConfigResult,
} from "./agent-sdk-types.js"; } from "./agent-sdk-types.js";
import {
isDefaultAgentCreateConfigUnattended,
resolveDefaultAgentCreateConfig,
} from "./create-agent-mode.js";
import { normalizeAgentModelDefinition } from "./agent-sdk-types.js"; import { normalizeAgentModelDefinition } from "./agent-sdk-types.js";
import type { WorkspaceGitService } from "../workspace-git-service.js"; import type { WorkspaceGitService } from "../workspace-git-service.js";
import type { import type {
@@ -28,7 +35,6 @@ import { CopilotACPAgentClient } from "./providers/copilot-acp-agent.js";
import { CursorACPAgentClient } from "./providers/cursor-acp-agent.js"; import { CursorACPAgentClient } from "./providers/cursor-acp-agent.js";
import { GenericACPAgentClient } from "./providers/generic-acp-agent.js"; import { GenericACPAgentClient } from "./providers/generic-acp-agent.js";
import { OpenCodeAgentClient } from "./providers/opencode-agent.js"; import { OpenCodeAgentClient } from "./providers/opencode-agent.js";
import { OpenCodeServerManager } from "./providers/opencode/server-manager.js";
import { PiRpcAgentClient } from "./providers/pi/agent.js"; import { PiRpcAgentClient } from "./providers/pi/agent.js";
import { MockLoadTestAgentClient } from "./providers/mock-load-test-agent.js"; import { MockLoadTestAgentClient } from "./providers/mock-load-test-agent.js";
import { MockSlowProviderClient } from "./providers/mock-slow-provider.js"; import { MockSlowProviderClient } from "./providers/mock-slow-provider.js";
@@ -57,6 +63,8 @@ export interface ProviderDefinition extends AgentProviderDefinition {
*/ */
derivedFromProviderId: string | null; derivedFromProviderId: string | null;
createClient: (logger: Logger) => AgentClient; createClient: (logger: Logger) => AgentClient;
resolveCreateConfig: (input: ResolveAgentCreateConfigInput) => ResolveAgentCreateConfigResult;
isCreateConfigUnattended: (input: AgentCreateConfigUnattendedInput) => boolean;
fetchModels: (options: ListModelsOptions) => Promise<AgentModelDefinition[]>; fetchModels: (options: ListModelsOptions) => Promise<AgentModelDefinition[]>;
fetchModes: (options: ListModesOptions) => Promise<AgentMode[]>; fetchModes: (options: ListModesOptions) => Promise<AgentMode[]>;
} }
@@ -414,6 +422,8 @@ function wrapClientProvider(
profileModelsAreAdditive, profileModelsAreAdditive,
}), }),
listModes: inner.listModes?.bind(inner), listModes: inner.listModes?.bind(inner),
resolveCreateConfig: inner.resolveCreateConfig?.bind(inner),
isCreateConfigUnattended: inner.isCreateConfigUnattended?.bind(inner),
listPersistedAgents: listPersistedAgents listPersistedAgents: listPersistedAgents
? async (options?: ListPersistedAgentsOptions) => ? async (options?: ListPersistedAgentsOptions) =>
(await listPersistedAgents(options)).map((descriptor) => (await listPersistedAgents(options)).map((descriptor) =>
@@ -438,6 +448,9 @@ function createRegistryEntry(
derivedFromProviderId: resolved.derivedFromProviderId, derivedFromProviderId: resolved.derivedFromProviderId,
createClient: (providerLogger: Logger) => createClient: (providerLogger: Logger) =>
createResolvedProviderClient(providerLogger, provider, resolved), createResolvedProviderClient(providerLogger, provider, resolved),
resolveCreateConfig: modelClient.resolveCreateConfig ?? resolveDefaultAgentCreateConfig,
isCreateConfigUnattended:
modelClient.isCreateConfigUnattended ?? isDefaultAgentCreateConfigUnattended,
fetchModels: async (options: ListModelsOptions) => fetchModels: async (options: ListModelsOptions) =>
mergeModels( mergeModels(
provider, provider,
@@ -673,5 +686,22 @@ export async function shutdownProviders(
logger: Logger, logger: Logger,
options?: BuildProviderRegistryOptions, options?: BuildProviderRegistryOptions,
): Promise<void> { ): Promise<void> {
await OpenCodeServerManager.getInstance(logger, options?.runtimeSettings?.opencode).shutdown(); const clients = createAllClients(logger, options);
await shutdownAgentClients(Object.values(clients), logger);
}
export async function shutdownAgentClients(
clients: Iterable<AgentClient>,
logger: Logger,
): Promise<void> {
await Promise.all(
Array.from(clients).map(async (client) => {
if (!client.shutdown) return;
try {
await client.shutdown();
} catch (error) {
logger.warn({ err: error, provider: client.provider }, "Provider client shutdown failed");
}
}),
);
} }
File diff suppressed because it is too large Load Diff
@@ -4,20 +4,91 @@ import { resolve } from "node:path";
import type { Logger } from "pino"; import type { Logger } from "pino";
import { expandTilde } from "../../utils/path.js";
import { withTimeout } from "../../utils/promise-timeout.js"; import { withTimeout } from "../../utils/promise-timeout.js";
import type { AgentProvider, ProviderSnapshotEntry } from "./agent-sdk-types.js"; import type {
import type { ProviderDefinition } from "./provider-registry.js"; AgentClient,
AgentMode,
AgentModelDefinition,
AgentProvider,
ProviderSnapshotEntry,
} from "./agent-sdk-types.js";
import type { ManagedAgent } from "./agent-manager.js";
import type { WorkspaceGitService } from "../workspace-git-service.js";
import type {
AgentProviderRuntimeSettingsMap,
ProviderOverride,
} from "./provider-launch-config.js";
import {
buildProviderRegistry,
shutdownAgentClients,
type ProviderDefinition,
} from "./provider-registry.js";
import { applyMutableProviderConfigToOverrides } from "../daemon-config-store.js";
import type { MutableDaemonConfig } from "../daemon-config-store.js";
const DEFAULT_REFRESH_TIMEOUT_MS = 30_000; const DEFAULT_REFRESH_TIMEOUT_MS = 30_000;
type ProviderSnapshotChangeListener = (entries: ProviderSnapshotEntry[], cwd: string) => void; type ProviderSnapshotChangeListener = (entries: ProviderSnapshotEntry[], cwd: string) => void;
interface ProviderSnapshotManagerOptions {
export interface ProviderSnapshotManagerOptions {
logger: Logger;
runtimeSettings?: AgentProviderRuntimeSettingsMap;
providerOverrides?: Record<string, ProviderOverride>;
workspaceGitService?: Pick<WorkspaceGitService, "resolveRepoRoot">;
isDev?: boolean;
extraClients?: Partial<Record<AgentProvider, AgentClient>>;
refreshTimeoutMs?: number; refreshTimeoutMs?: number;
} }
interface ProviderSnapshotRefreshOptions { interface ProviderSnapshotRefreshOptions {
cwd: string; cwd: string;
providers?: AgentProvider[]; providers?: AgentProvider[];
} }
interface ProviderSnapshotReadOptions {
cwd?: string | null;
providers?: AgentProvider[];
wait?: boolean;
}
interface ProviderSnapshotProviderOptions {
cwd?: string | null;
provider: AgentProvider;
wait?: boolean;
}
interface ResolveProviderCreateConfigOptions {
cwd?: string | null;
provider: AgentProvider;
requestedMode: string | undefined;
featureValues: Record<string, unknown> | undefined;
parent: ManagedAgent | null;
}
export interface ResolvedProviderCreateConfig {
modeId: string | undefined;
featureValues: Record<string, unknown> | undefined;
}
interface ResolveDefaultModelOptions {
provider: AgentProvider;
requestedModel?: string | null;
cwd?: string;
}
export interface ProviderDiagnosticResult {
provider: AgentProvider;
diagnostic: string;
}
export interface AgentManagerProviderState {
providerDefinitions: Partial<
Record<AgentProvider, { enabled: boolean; derivedFromProviderId: string | null }>
>;
clients: Partial<Record<AgentProvider, AgentClient>>;
}
interface ProviderLoadOptions { interface ProviderLoadOptions {
cwd: string; cwd: string;
providers: AgentProvider[]; providers: AgentProvider[];
@@ -33,15 +104,27 @@ export class ProviderSnapshotManager {
private readonly events = new EventEmitter(); private readonly events = new EventEmitter();
private destroyed = false; private destroyed = false;
private readonly refreshTimeoutMs: number; private readonly refreshTimeoutMs: number;
private readonly logger: Logger;
private readonly workspaceGitService?: Pick<WorkspaceGitService, "resolveRepoRoot">;
private readonly isDev: boolean;
private readonly extraClients: Partial<Record<AgentProvider, AgentClient>>;
private runtimeSettings: AgentProviderRuntimeSettingsMap | undefined;
private providerOverrides: Record<string, ProviderOverride> | undefined;
private readonly baseProviderOverrides: Record<string, ProviderOverride> | undefined;
private providerRegistry: Record<AgentProvider, ProviderDefinition>; private providerRegistry: Record<AgentProvider, ProviderDefinition>;
private providerClients: Record<AgentProvider, AgentClient>;
constructor( constructor(options: ProviderSnapshotManagerOptions) {
providerRegistry: Record<AgentProvider, ProviderDefinition>, this.logger = options.logger;
private readonly logger: Logger, this.workspaceGitService = options.workspaceGitService;
options: ProviderSnapshotManagerOptions = {}, this.isDev = options.isDev === true;
) { this.extraClients = options.extraClients ?? {};
this.providerRegistry = providerRegistry; this.runtimeSettings = options.runtimeSettings;
this.providerOverrides = options.providerOverrides;
this.baseProviderOverrides = options.providerOverrides;
this.refreshTimeoutMs = options.refreshTimeoutMs ?? DEFAULT_REFRESH_TIMEOUT_MS; this.refreshTimeoutMs = options.refreshTimeoutMs ?? DEFAULT_REFRESH_TIMEOUT_MS;
this.providerRegistry = this.buildRegistry();
this.providerClients = { ...this.extraClients } as Record<AgentProvider, AgentClient>;
} }
getSnapshot(cwd?: string): ProviderSnapshotEntry[] { getSnapshot(cwd?: string): ProviderSnapshotEntry[] {
@@ -118,6 +201,145 @@ export class ProviderSnapshotManager {
await this.refreshSnapshotForCwd(options); await this.refreshSnapshotForCwd(options);
} }
listRegisteredProviderIds(): AgentProvider[] {
return this.getProviderIds();
}
hasProvider(provider: AgentProvider): boolean {
return Object.prototype.hasOwnProperty.call(this.providerRegistry, provider);
}
getProviderLabel(provider: AgentProvider): string {
return this.providerRegistry[provider]?.label ?? provider;
}
getAgentManagerProviderState(): AgentManagerProviderState {
const providerDefinitions: AgentManagerProviderState["providerDefinitions"] = {};
const clients: AgentManagerProviderState["clients"] = {};
for (const [provider, definition] of Object.entries(this.providerRegistry)) {
providerDefinitions[provider] = {
enabled: definition.enabled,
derivedFromProviderId: definition.derivedFromProviderId,
};
if (definition.enabled) {
clients[provider] = this.ensureClient(provider, definition);
}
}
for (const [provider, client] of Object.entries(this.extraClients)) {
if (client) {
clients[provider] = client;
}
}
return { providerDefinitions, clients };
}
private ensureClient(provider: AgentProvider, definition: ProviderDefinition): AgentClient {
const existing = this.providerClients[provider];
if (existing) {
return existing;
}
const client = definition.createClient(this.logger);
this.providerClients[provider] = client;
return client;
}
async listProviders(input: ProviderSnapshotReadOptions = {}): Promise<ProviderSnapshotEntry[]> {
const cwd = resolveSnapshotCwd(input.cwd);
if (input.wait) {
await this.warmUpSnapshotForCwd({ cwd, providers: input.providers });
}
const providerFilter = input.providers ? new Set(input.providers) : null;
const entries = this.getSnapshot(cwd);
return providerFilter ? entries.filter((entry) => providerFilter.has(entry.provider)) : entries;
}
async getProvider(input: ProviderSnapshotProviderOptions): Promise<ProviderSnapshotEntry> {
const entry = (await this.listProviders({ ...input, providers: [input.provider] })).find(
(candidate) => candidate.provider === input.provider,
);
if (!entry) {
throw new Error(`Provider ${input.provider} is not configured`);
}
return entry;
}
async listModels(input: ProviderSnapshotProviderOptions): Promise<AgentModelDefinition[]> {
const entry = await this.getReadyProvider(input);
return entry.models ?? [];
}
async listModes(input: ProviderSnapshotProviderOptions): Promise<AgentMode[]> {
const entry = await this.getReadyProvider(input);
return entry.modes ?? [];
}
async resolveDefaultModel(input: ResolveDefaultModelOptions): Promise<string | undefined> {
try {
const trimmed = input.requestedModel?.trim();
if (trimmed) {
return trimmed;
}
const models = await this.listModels({
provider: input.provider,
cwd: input.cwd ? expandTilde(input.cwd) : undefined,
wait: true,
});
const preferred = models.find((model) => model.isDefault) ?? models[0];
return preferred?.id;
} catch (error) {
this.logger.warn({ err: error, provider: input.provider }, "Failed to resolve default model");
return undefined;
}
}
async resolveCreateConfig(
input: ResolveProviderCreateConfigOptions,
): Promise<ResolvedProviderCreateConfig> {
const entry = await this.getReadyProvider({
cwd: input.cwd,
provider: input.provider,
wait: true,
});
const definition = this.requireProvider(input.provider);
return definition.resolveCreateConfig({
provider: input.provider,
requestedMode: input.requestedMode,
featureValues: input.featureValues,
parent: input.parent ? this.resolveParent(input.parent) : null,
availableModes: entry.modes ?? [],
});
}
async getProviderDiagnostic(provider: AgentProvider): Promise<ProviderDiagnosticResult> {
const client = this.providerClients[provider];
if (!client) {
throw new Error(`Provider ${provider} is not configured`);
}
const diagnostic = client.getDiagnostic
? (await client.getDiagnostic()).diagnostic
: "No diagnostic available for this provider.";
return { provider, diagnostic };
}
applyMutableProviderConfig(
mutableProviders: MutableDaemonConfig["providers"] | undefined,
): AgentManagerProviderState {
this.providerOverrides = applyMutableProviderConfigToOverrides(
this.baseProviderOverrides,
mutableProviders,
);
this.providerRegistry = this.buildRegistry();
this.providerClients = { ...this.extraClients } as Record<AgentProvider, AgentClient>;
for (const cwd of this.snapshots.keys()) {
this.providerLoads.delete(cwd);
this.snapshots.set(cwd, this.reconcileSnapshotForRegistry(cwd));
this.emitChange(cwd);
}
return this.getAgentManagerProviderState();
}
on(event: "change", listener: ProviderSnapshotChangeListener): this { on(event: "change", listener: ProviderSnapshotChangeListener): this {
this.events.on(event, listener); this.events.on(event, listener);
return this; return this;
@@ -128,6 +350,17 @@ export class ProviderSnapshotManager {
return this; return this;
} }
async shutdown(): Promise<void> {
// Materialize a client per enabled provider so provider-owned resources
// (background processes, sockets, etc.) get a chance to release even when
// a given provider hasn't been touched yet during this daemon's lifetime.
const state = this.getAgentManagerProviderState();
const clients = Object.values(state.clients).filter(
(client): client is AgentClient => client !== undefined,
);
await shutdownAgentClients(clients, this.logger);
}
destroy(): void { destroy(): void {
this.destroyed = true; this.destroyed = true;
this.events.removeAllListeners(); this.events.removeAllListeners();
@@ -135,14 +368,51 @@ export class ProviderSnapshotManager {
this.providerLoads.clear(); this.providerLoads.clear();
} }
replaceRegistry(providerRegistry: Record<AgentProvider, ProviderDefinition>): void { private buildRegistry(): Record<AgentProvider, ProviderDefinition> {
this.providerRegistry = providerRegistry; return buildProviderRegistry(this.logger, {
runtimeSettings: this.runtimeSettings,
providerOverrides: this.providerOverrides,
workspaceGitService: this.workspaceGitService,
isDev: this.isDev,
});
}
for (const cwd of this.snapshots.keys()) { private resolveParent(parent: ManagedAgent) {
this.providerLoads.delete(cwd); const definition = this.requireProvider(parent.provider);
this.snapshots.set(cwd, this.reconcileSnapshotForRegistry(cwd)); return {
this.emitChange(cwd); provider: parent.provider,
modeId: parent.currentModeId,
isUnattended: definition.isCreateConfigUnattended({
modeId: parent.currentModeId,
config: parent.config,
features: parent.features,
availableModes: parent.availableModes ?? definition.modes ?? [],
}),
};
}
private async getReadyProvider(
input: ProviderSnapshotProviderOptions,
): Promise<ProviderSnapshotEntry> {
const entry = await this.getProvider(input);
if (!entry.enabled) {
throw new Error(`Provider '${entry.provider}' is disabled`);
} }
if (entry.status === "ready") {
return entry;
}
if (entry.status === "error") {
throw new Error(entry.error ?? `Failed to load provider '${entry.provider}'`);
}
throw new Error(`Provider '${entry.provider}' is not available`);
}
private requireProvider(provider: AgentProvider): ProviderDefinition {
const definition = this.providerRegistry[provider];
if (!definition) {
throw new Error(`Provider ${provider} is not configured`);
}
return definition;
} }
private createLoadingEntries(): Map<AgentProvider, ProviderSnapshotEntry> { private createLoadingEntries(): Map<AgentProvider, ProviderSnapshotEntry> {
@@ -322,7 +592,7 @@ export class ProviderSnapshotManager {
return; return;
} }
const client = definition.createClient(this.logger); const client = this.ensureClient(provider, definition);
const available = await withTimeout( const available = await withTimeout(
client.isAvailable(), client.isAvailable(),
this.refreshTimeoutMs, this.refreshTimeoutMs,
@@ -125,6 +125,44 @@ describe("OpenCode auto_accept feature", () => {
await session.close(); await session.close();
}); });
test("resolves legacy full-access for provider-driven child creation", () => {
const client = new OpenCodeAgentClient(createTestLogger());
expect(
client.resolveCreateConfig({
provider: "opencode",
requestedMode: "full-access",
featureValues: undefined,
parent: null,
availableModes: [
{ id: "build", label: "Build" },
{ id: "plan", label: "Plan" },
],
}),
).toEqual({ modeId: "build", featureValues: { auto_accept: true } });
});
test("inherits unattended callers as build plus auto accept", () => {
const client = new OpenCodeAgentClient(createTestLogger());
expect(
client.resolveCreateConfig({
provider: "opencode",
requestedMode: undefined,
featureValues: undefined,
parent: {
provider: "claude",
modeId: "bypassPermissions",
isUnattended: true,
},
availableModes: [
{ id: "build", label: "Build" },
{ id: "plan", label: "Plan" },
],
}),
).toEqual({ modeId: "build", featureValues: { auto_accept: true } });
});
test("auto-approves tool permissions when auto accept is enabled", async () => { test("auto-approves tool permissions when auto accept is enabled", async () => {
const { openCodeClient, runtime } = mockOpenCodeClient({ const { openCodeClient, runtime } = mockOpenCodeClient({
events: [toolPermissionEvent(), idleEvent()], events: [toolPermissionEvent(), idleEvent()],
@@ -37,6 +37,8 @@ import {
type AgentStreamEvent, type AgentStreamEvent,
type AgentTimelineItem, type AgentTimelineItem,
type AgentUsage, type AgentUsage,
type ResolveAgentCreateConfigInput,
type ResolveAgentCreateConfigResult,
type ListModelsOptions, type ListModelsOptions,
type ListModesOptions, type ListModesOptions,
type ListPersistedAgentsOptions, type ListPersistedAgentsOptions,
@@ -45,6 +47,10 @@ import {
type ToolCallDetail, type ToolCallDetail,
type ToolCallTimelineItem, type ToolCallTimelineItem,
} from "../agent-sdk-types.js"; } from "../agent-sdk-types.js";
import {
isDefaultAgentCreateConfigUnattended,
resolveDefaultAgentCreateConfig,
} from "../create-agent-mode.js";
import { import {
checkProviderLaunchAvailable, checkProviderLaunchAvailable,
createProviderEnvSpec, createProviderEnvSpec,
@@ -111,6 +117,55 @@ function isOpenCodeAutoAcceptEnabled(config: AgentSessionConfig): boolean {
return config.featureValues?.[OPENCODE_AUTO_ACCEPT_FEATURE_ID] === true; return config.featureValues?.[OPENCODE_AUTO_ACCEPT_FEATURE_ID] === true;
} }
function withOpenCodeAutoAcceptFeature(
featureValues: Record<string, unknown> | undefined,
enabled: boolean,
): Record<string, unknown> {
return {
...featureValues,
[OPENCODE_AUTO_ACCEPT_FEATURE_ID]: enabled,
};
}
function resolveOpenCodeCreateConfig(
input: ResolveAgentCreateConfigInput,
): ResolveAgentCreateConfigResult {
const legacyFullAccess = input.requestedMode === OPENCODE_LEGACY_FULL_ACCESS_MODE_ID;
const inheritsUnattended =
input.requestedMode === undefined && input.parent?.isUnattended === true;
const requestedMode = legacyFullAccess ? OPENCODE_BUILD_MODE_ID : input.requestedMode;
const featureValues =
legacyFullAccess ||
(inheritsUnattended && input.featureValues?.[OPENCODE_AUTO_ACCEPT_FEATURE_ID] === undefined)
? withOpenCodeAutoAcceptFeature(input.featureValues, true)
: input.featureValues;
if (inheritsUnattended && requestedMode === undefined) {
return { modeId: OPENCODE_BUILD_MODE_ID, featureValues };
}
const resolved = resolveDefaultAgentCreateConfig({
...input,
requestedMode,
featureValues,
});
return { ...resolved, featureValues };
}
function isOpenCodeCreateConfigUnattended(
input: Parameters<typeof isDefaultAgentCreateConfigUnattended>[0],
): boolean {
return (
isDefaultAgentCreateConfigUnattended(input) ||
input.config.featureValues?.[OPENCODE_AUTO_ACCEPT_FEATURE_ID] === true ||
input.features?.some(
(feature) =>
feature.id === OPENCODE_AUTO_ACCEPT_FEATURE_ID &&
(feature.value === true || feature.value === "true"),
) === true
);
}
function buildOpenCodeAutoAcceptFeature(config: AgentSessionConfig): AgentFeature { function buildOpenCodeAutoAcceptFeature(config: AgentSessionConfig): AgentFeature {
return { return {
type: "toggle", type: "toggle",
@@ -1147,6 +1202,8 @@ class ProductionOpenCodeRuntime implements OpenCodeRuntime {
export class OpenCodeAgentClient implements AgentClient { export class OpenCodeAgentClient implements AgentClient {
readonly provider = "opencode" as const; readonly provider = "opencode" as const;
readonly capabilities = OPENCODE_CAPABILITIES; readonly capabilities = OPENCODE_CAPABILITIES;
readonly resolveCreateConfig = resolveOpenCodeCreateConfig;
readonly isCreateConfigUnattended = isOpenCodeCreateConfigUnattended;
private readonly runtime: OpenCodeRuntime; private readonly runtime: OpenCodeRuntime;
private readonly logger: Logger; private readonly logger: Logger;
@@ -1403,6 +1460,10 @@ export class OpenCodeAgentClient implements AgentClient {
return availability.available; return availability.available;
} }
async shutdown(): Promise<void> {
await this.runtime.shutdown();
}
async getDiagnostic(): Promise<{ diagnostic: string }> { async getDiagnostic(): Promise<{ diagnostic: string }> {
try { try {
const launch = await resolveProviderLaunch({ const launch = await resolveProviderLaunch({
+11 -19
View File
@@ -99,11 +99,7 @@ import { AgentManager } from "./agent/agent-manager.js";
import { AgentStorage } from "./agent/agent-storage.js"; import { AgentStorage } from "./agent/agent-storage.js";
import { attachAgentStoragePersistence } from "./persistence-hooks.js"; import { attachAgentStoragePersistence } from "./persistence-hooks.js";
import { createAgentMcpServer } from "./agent/mcp-server.js"; import { createAgentMcpServer } from "./agent/mcp-server.js";
import { import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
buildProviderRegistry,
createClientsFromRegistry,
shutdownProviders,
} from "./agent/provider-registry.js";
import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js"; import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js";
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js"; import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry } from "./workspace-registry.js"; import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry } from "./workspace-registry.js";
@@ -516,18 +512,19 @@ export async function createPaseoDaemon(
github, github,
}, },
}); });
const providerRegistry = buildProviderRegistry(logger, { const providerSnapshotLogger = logger.child({ module: "provider-snapshot-manager" });
const providerSnapshotManager = new ProviderSnapshotManager({
logger: providerSnapshotLogger,
runtimeSettings: config.agentProviderSettings, runtimeSettings: config.agentProviderSettings,
providerOverrides: config.providerOverrides, providerOverrides: config.providerOverrides,
workspaceGitService, workspaceGitService,
isDev: config.isDev === true, isDev: config.isDev === true,
extraClients: config.agentClients,
}); });
const initialAgentManagerState = providerSnapshotManager.getAgentManagerProviderState();
const agentManager = new AgentManager({ const agentManager = new AgentManager({
clients: { clients: initialAgentManagerState.clients,
...createClientsFromRegistry(providerRegistry, logger), providerDefinitions: initialAgentManagerState.providerDefinitions,
...config.agentClients,
},
providerDefinitions: providerRegistry,
registry: agentStorage, registry: agentStorage,
appendSystemPrompt: config.appendSystemPrompt, appendSystemPrompt: config.appendSystemPrompt,
logger, logger,
@@ -670,7 +667,7 @@ export async function createPaseoDaemon(
terminalManager, terminalManager,
getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null), getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
scheduleService, scheduleService,
providerRegistry, providerSnapshotManager,
github, github,
workspaceGitService, workspaceGitService,
archiveWorkspaceRecord: archiveWorkspaceRecordExternal, archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
@@ -927,9 +924,6 @@ export async function createPaseoDaemon(
{ {
finalTimeoutMs: config.dictationFinalTimeoutMs, finalTimeoutMs: config.dictationFinalTimeoutMs,
}, },
config.agentProviderSettings,
config.providerOverrides,
config.isDev === true,
daemonVersion, daemonVersion,
(intent) => { (intent) => {
try { try {
@@ -953,6 +947,7 @@ export async function createPaseoDaemon(
workspaceGitService, workspaceGitService,
github, github,
config.pushNotificationSender, config.pushNotificationSender,
providerSnapshotManager,
{ {
listen: formatListenTarget(boundListenTarget ?? listenTarget), listen: formatListenTarget(boundListenTarget ?? listenTarget),
relay: { relay: {
@@ -1021,10 +1016,7 @@ export async function createPaseoDaemon(
await agentManager.flush().catch(() => undefined); await agentManager.flush().catch(() => undefined);
detachAgentStoragePersistence(); detachAgentStoragePersistence();
await agentStorage.flush().catch(() => undefined); await agentStorage.flush().catch(() => undefined);
await shutdownProviders(logger, { await providerSnapshotManager.shutdown();
runtimeSettings: config.agentProviderSettings,
providerOverrides: config.providerOverrides,
});
terminalManager.killAll(); terminalManager.killAll();
speechService.stop(); speechService.stop();
await scheduleService.stop().catch(() => undefined); await scheduleService.stop().catch(() => undefined);
@@ -5,7 +5,6 @@ import type {
AgentSessionConfig, AgentSessionConfig,
} from "./agent/agent-sdk-types.js"; } from "./agent/agent-sdk-types.js";
import type { AgentStorage, StoredAgentRecord } from "./agent/agent-storage.js"; import type { AgentStorage, StoredAgentRecord } from "./agent/agent-storage.js";
import { buildProviderRegistry } from "./agent/provider-registry.js";
interface LoggerLike { interface LoggerLike {
child(bindings: Record<string, unknown>): LoggerLike; child(bindings: Record<string, unknown>): LoggerLike;
@@ -24,18 +23,6 @@ interface BuildSessionConfigOptions {
validProviders?: Iterable<AgentProvider>; validProviders?: Iterable<AgentProvider>;
} }
type RegisteredProviders = ReturnType<typeof buildProviderRegistry> | Iterable<AgentProvider>;
function isProviderRegistry(
registeredProviders: RegisteredProviders,
): registeredProviders is ReturnType<typeof buildProviderRegistry> {
return (
typeof registeredProviders === "object" &&
registeredProviders !== null &&
!(Symbol.iterator in registeredProviders)
);
}
/** /**
* Attach AgentStorage persistence to an AgentManager instance so every * Attach AgentStorage persistence to an AgentManager instance so every
* agent_state snapshot is flushed to disk. * agent_state snapshot is flushed to disk.
@@ -118,29 +105,15 @@ export function extractTimestamps(record: StoredAgentRecord): {
}; };
} }
function hasRegisteredProvider(registeredProviders: RegisteredProviders, value: string): boolean {
if (isProviderRegistry(registeredProviders)) {
return Object.prototype.hasOwnProperty.call(registeredProviders, value);
}
return new Set(registeredProviders).has(value);
}
export function isRegisteredProvider(
providerRegistry: ReturnType<typeof buildProviderRegistry>,
value: string,
): boolean {
return hasRegisteredProvider(providerRegistry, value);
}
export function toAgentPersistenceHandle( export function toAgentPersistenceHandle(
registeredProviders: RegisteredProviders, registeredProviders: Iterable<AgentProvider>,
handle: StoredAgentRecord["persistence"], handle: StoredAgentRecord["persistence"],
): AgentPersistenceHandle | null { ): AgentPersistenceHandle | null {
if (!handle) { if (!handle) {
return null; return null;
} }
const provider = handle.provider; const provider = handle.provider;
if (!hasRegisteredProvider(registeredProviders, provider)) { if (!new Set(registeredProviders).has(provider)) {
return null; return null;
} }
if (!handle.sessionId) { if (!handle.sessionId) {
+49 -194
View File
@@ -13,15 +13,7 @@ import {
} from "@getpaseo/protocol/binary-frames/index"; } from "@getpaseo/protocol/binary-frames/index";
import { Session } from "./session.js"; import { Session } from "./session.js";
import { StructuredAgentFallbackError } from "./agent/agent-response-loop.js"; import { StructuredAgentFallbackError } from "./agent/agent-response-loop.js";
import type { import type { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
AgentClient,
AgentMode,
AgentModelDefinition,
ListModesOptions,
ListModelsOptions,
} from "./agent/agent-sdk-types.js";
import type { ProviderDefinition } from "./agent/provider-registry.js";
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
import type { SessionOptions } from "./session.js"; import type { SessionOptions } from "./session.js";
import type { import type {
SpeechToTextProvider, SpeechToTextProvider,
@@ -83,7 +75,6 @@ interface SessionHandlerInternals {
handleStashPopRequest(params: unknown): Promise<unknown>; handleStashPopRequest(params: unknown): Promise<unknown>;
createPaseoWorktree(params: unknown): Promise<unknown>; createPaseoWorktree(params: unknown): Promise<unknown>;
handleStartWorkspaceScriptRequest(params: unknown): Promise<unknown>; handleStartWorkspaceScriptRequest(params: unknown): Promise<unknown>;
getProviderRegistry(): unknown;
sttManager: { sttManager: {
transcribe(audio: Buffer, format: string): Promise<unknown>; transcribe(audio: Buffer, format: string): Promise<unknown>;
}; };
@@ -143,46 +134,6 @@ interface Deferred<T> {
reject: (reason?: unknown) => void; reject: (reason?: unknown) => void;
} }
const TEST_CAPABILITIES = {
supportsStreaming: false,
supportsSessionPersistence: false,
supportsDynamicModes: false,
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: false,
} as const;
function createTestProviderDefinition(overrides?: Partial<ProviderDefinition>): ProviderDefinition {
return {
id: "codex",
label: "Codex",
description: "Codex test provider",
enabled: true,
defaultModeId: null,
modes: [],
createClient: () =>
({
provider: "codex",
capabilities: TEST_CAPABILITIES,
async createSession() {
throw new Error("not implemented");
},
async resumeSession() {
throw new Error("not implemented");
},
async listModels() {
return [];
},
async isAvailable() {
return true;
},
}) satisfies AgentClient,
fetchModels: async () => [],
fetchModes: async () => [],
...overrides,
};
}
function deferred<T>(): Deferred<T> { function deferred<T>(): Deferred<T> {
let resolve!: (value: T) => void; let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void; let reject!: (reason?: unknown) => void;
@@ -357,7 +308,8 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session {
stt: options.stt ?? null, stt: options.stt ?? null,
tts: null, tts: null,
terminalManager: options.terminalManager ?? null, terminalManager: options.terminalManager ?? null,
providerSnapshotManager: options.providerSnapshotManager, providerSnapshotManager:
options.providerSnapshotManager ?? createProviderSnapshotManagerStub().manager,
scriptRouteStore: options.scriptRouteStore, scriptRouteStore: options.scriptRouteStore,
scriptRuntimeStore: options.scriptRuntimeStore, scriptRuntimeStore: options.scriptRuntimeStore,
getDaemonTcpPort: options.getDaemonTcpPort, getDaemonTcpPort: options.getDaemonTcpPort,
@@ -989,24 +941,10 @@ describe("session provider refresh cwd routing", () => {
expect(refreshSettingsSnapshot).not.toHaveBeenCalled(); expect(refreshSettingsSnapshot).not.toHaveBeenCalled();
}); });
test("provider snapshot requests pass cwd through to provider discovery", async () => { test("get_providers_snapshot_request forwards cwd to the provider authority", async () => {
const messages: unknown[] = []; const messages: unknown[] = [];
const workspaceCwd = resolvePath("/tmp/session-provider-snapshot"); const workspaceCwd = resolvePath("/tmp/session-provider-snapshot");
const fetchModels = vi.fn(async (options: ListModelsOptions) => [ const { manager: providerSnapshotManager, getSnapshot } = createProviderSnapshotManagerStub();
{
provider: "codex" as const,
id: `model:${options.cwd}`,
label: `model:${options.cwd}`,
},
]);
const providerDefinition = createTestProviderDefinition({
fetchModels,
fetchModes: vi.fn(async () => []),
});
const providerSnapshotManager = new ProviderSnapshotManager(
{ codex: providerDefinition },
pino({ level: "silent" }),
);
const session = createSessionForTest({ messages, providerSnapshotManager }); const session = createSessionForTest({ messages, providerSnapshotManager });
await session.handleMessage({ await session.handleMessage({
@@ -1015,27 +953,24 @@ describe("session provider refresh cwd routing", () => {
requestId: "snapshot-workspace", requestId: "snapshot-workspace",
}); });
await vi.waitFor(() => { expect(getSnapshot).toHaveBeenCalledWith(workspaceCwd);
expect(fetchModels).toHaveBeenCalledWith({
cwd: workspaceCwd,
force: false,
});
});
providerSnapshotManager.destroy();
}); });
test("normalizes legacy model and mode list requests without cwd to home", async () => { test("normalizes legacy model and mode list requests without cwd to home", async () => {
const messages: unknown[] = []; const messages: unknown[] = [];
const session = createSessionForTest({ messages }); const {
const fetchModels = vi.fn(async () => []); manager: providerSnapshotManager,
const fetchModes = vi.fn(async () => []); getSnapshot,
asSessionInternals(session).getProviderRegistry = () => ({ warmUpSnapshotForCwd,
codex: createTestProviderDefinition({ } = createProviderSnapshotManagerStub();
fetchModels, getSnapshot.mockReturnValue([
fetchModes, {
}), provider: "codex",
}); status: "loading",
enabled: true,
},
]);
const session = createSessionForTest({ messages, providerSnapshotManager });
await session.handleMessage({ await session.handleMessage({
type: "list_provider_models_request", type: "list_provider_models_request",
@@ -1048,8 +983,11 @@ describe("session provider refresh cwd routing", () => {
requestId: "modes-home", requestId: "modes-home",
}); });
expect(fetchModels).toHaveBeenCalledWith({ cwd: homedir(), force: false }); expect(getSnapshot).toHaveBeenCalledWith(homedir());
expect(fetchModes).toHaveBeenCalledWith({ cwd: homedir(), force: false }); expect(warmUpSnapshotForCwd).toHaveBeenCalledWith({
cwd: homedir(),
providers: ["codex"],
});
}); });
test("legacy model list request treats disabled snapshot entries as unavailable without warming", async () => { test("legacy model list request treats disabled snapshot entries as unavailable without warming", async () => {
@@ -1114,128 +1052,47 @@ describe("session provider refresh cwd routing", () => {
}); });
}); });
test("legacy model and mode list fallback treats disabled registry definitions as unavailable without fetching", async () => { test("list_provider_models_request awaits warmup and emits ready models", async () => {
const messages: unknown[] = []; const messages: unknown[] = [];
const session = createSessionForTest({ messages }); const warmupDeferred = deferred<void>();
const fetchModels = vi.fn(async () => [ const {
manager: providerSnapshotManager,
getSnapshot,
warmUpSnapshotForCwd,
} = createProviderSnapshotManagerStub();
getSnapshot.mockReturnValueOnce([
{ {
provider: "codex" as const, provider: "codex",
id: "should-not-fetch", status: "loading",
label: "Should not fetch", enabled: true,
}, },
]); ]);
const fetchModes = vi.fn(async () => [ getSnapshot.mockReturnValue([
{ {
id: "should-not-fetch", provider: "codex",
label: "Should not fetch", status: "ready",
enabled: true,
models: [{ provider: "codex", id: "gpt-5.4", label: "GPT-5.4" }],
modes: [],
fetchedAt: "2026-05-28T00:00:00.000Z",
}, },
]); ]);
asSessionInternals(session).getProviderRegistry = () => ({ warmUpSnapshotForCwd.mockReturnValue(warmupDeferred.promise);
codex: createTestProviderDefinition({
enabled: false,
fetchModels,
fetchModes,
}),
});
await session.handleMessage({
type: "list_provider_models_request",
provider: "codex",
requestId: "fallback-models-disabled",
});
await session.handleMessage({
type: "list_provider_modes_request",
provider: "codex",
requestId: "fallback-modes-disabled",
});
expect(fetchModels).not.toHaveBeenCalled();
expect(fetchModes).not.toHaveBeenCalled();
expect(messages).toContainEqual({
type: "list_provider_models_response",
payload: {
provider: "codex",
error: "Provider codex is disabled",
fetchedAt: expect.any(String),
requestId: "fallback-models-disabled",
},
});
expect(messages).toContainEqual({
type: "list_provider_modes_response",
payload: {
provider: "codex",
error: "Provider codex is disabled",
fetchedAt: expect.any(String),
requestId: "fallback-modes-disabled",
},
});
});
test("legacy model list request without cwd awaits loading snapshot without forced discovery", async () => {
const messages: unknown[] = [];
const models = deferred<AgentModelDefinition[]>();
const fetchModels = vi.fn(
async (options: ListModelsOptions): Promise<AgentModelDefinition[]> => {
expect(options.cwd).toBe(homedir());
return models.promise;
},
);
const fetchModes = vi.fn(async (_options: ListModesOptions): Promise<AgentMode[]> => []);
const providerDefinition = createTestProviderDefinition({
createClient: () =>
({
provider: "codex",
capabilities: TEST_CAPABILITIES,
async createSession() {
throw new Error("not implemented");
},
async resumeSession() {
throw new Error("not implemented");
},
async listModels(options: ListModelsOptions) {
return fetchModels(options);
},
async isAvailable() {
return true;
},
}) satisfies AgentClient,
fetchModels,
fetchModes,
});
const providerSnapshotManager = new ProviderSnapshotManager(
{ codex: providerDefinition },
pino({ level: "silent" }),
);
const session = createSessionForTest({ messages, providerSnapshotManager }); const session = createSessionForTest({ messages, providerSnapshotManager });
providerSnapshotManager.getSnapshot();
await vi.waitFor(() => {
expect(fetchModels).toHaveBeenCalledTimes(1);
});
const responsePromise = session.handleMessage({ const responsePromise = session.handleMessage({
type: "list_provider_models_request", type: "list_provider_models_request",
provider: "codex", provider: "codex",
requestId: "models-loading-home", requestId: "models-loading-home",
}); });
await Promise.resolve(); expect(warmUpSnapshotForCwd).toHaveBeenCalledWith({
cwd: homedir(),
expect(fetchModels).toHaveBeenCalledTimes(1); providers: ["codex"],
expect(fetchModels).toHaveBeenCalledWith({ cwd: homedir(), force: false }); });
expect(fetchModels).not.toHaveBeenCalledWith({ cwd: homedir(), force: true }); warmupDeferred.resolve();
models.resolve([
{
provider: "codex",
id: "gpt-5.4",
label: "GPT-5.4",
},
]);
await responsePromise; await responsePromise;
expect(fetchModels).toHaveBeenCalledTimes(1);
expect(fetchModels).not.toHaveBeenCalledWith({ cwd: homedir(), force: true });
expect(messages).toContainEqual({ expect(messages).toContainEqual({
type: "list_provider_models_response", type: "list_provider_models_response",
payload: { payload: {
@@ -1248,12 +1105,10 @@ describe("session provider refresh cwd routing", () => {
}, },
], ],
error: null, error: null,
fetchedAt: expect.any(String), fetchedAt: "2026-05-28T00:00:00.000Z",
requestId: "models-loading-home", requestId: "models-loading-home",
}, },
}); });
providerSnapshotManager.destroy();
}); });
}); });
+69 -196
View File
@@ -85,7 +85,6 @@ import type { ScriptHealthState } from "./script-health-monitor.js";
import { spawnWorkspaceScript } from "./worktree-bootstrap.js"; import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js"; import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
import type { DaemonConfigStore } from "./daemon-config-store.js"; import type { DaemonConfigStore } from "./daemon-config-store.js";
import { applyMutableProviderConfigToOverrides } from "./daemon-config-store.js";
import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-utils"; import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-utils";
import { getAgentStatusPriority } from "@getpaseo/protocol/agent-state-bucket"; import { getAgentStatusPriority } from "@getpaseo/protocol/agent-state-bucket";
import type { import type {
@@ -94,11 +93,6 @@ import type {
WorkspaceGitSnapshotOptions, WorkspaceGitSnapshotOptions,
} from "./workspace-git-service.js"; } from "./workspace-git-service.js";
import { buildProviderRegistry } from "./agent/provider-registry.js";
import type {
AgentProviderRuntimeSettingsMap,
ProviderOverride,
} from "./agent/provider-launch-config.js";
import { AgentManager } from "./agent/agent-manager.js"; import { AgentManager } from "./agent/agent-manager.js";
import { ProviderSnapshotManager, resolveSnapshotCwd } from "./agent/provider-snapshot-manager.js"; import { ProviderSnapshotManager, resolveSnapshotCwd } from "./agent/provider-snapshot-manager.js";
import type { import type {
@@ -587,7 +581,7 @@ export interface SessionOptions {
sttLanguage?: string; sttLanguage?: string;
tts: Resolvable<TextToSpeechProvider | null>; tts: Resolvable<TextToSpeechProvider | null>;
terminalManager: TerminalManager | null; terminalManager: TerminalManager | null;
providerSnapshotManager?: ProviderSnapshotManager; providerSnapshotManager: ProviderSnapshotManager;
scriptRouteStore?: ScriptRouteStore; scriptRouteStore?: ScriptRouteStore;
scriptRuntimeStore?: WorkspaceScriptRuntimeStore; scriptRuntimeStore?: WorkspaceScriptRuntimeStore;
workspaceSetupSnapshots?: Map<string, WorkspaceSetupSnapshot>; workspaceSetupSnapshots?: Map<string, WorkspaceSetupSnapshot>;
@@ -614,9 +608,6 @@ export interface SessionOptions {
sttLanguage?: string; sttLanguage?: string;
getSpeechReadiness?: () => SpeechReadinessSnapshot; getSpeechReadiness?: () => SpeechReadinessSnapshot;
}; };
agentProviderRuntimeSettings?: AgentProviderRuntimeSettingsMap;
providerOverrides?: Record<string, ProviderOverride>;
isDev?: boolean;
serverId?: string; serverId?: string;
daemonVersion?: string; daemonVersion?: string;
daemonRuntimeConfig?: { daemonRuntimeConfig?: {
@@ -794,7 +785,7 @@ export class Session {
} | null = null; } | null = null;
private readonly MOBILE_BACKGROUND_STREAM_GRACE_MS = 60_000; private readonly MOBILE_BACKGROUND_STREAM_GRACE_MS = 60_000;
private readonly terminalManager: TerminalManager | null; private readonly terminalManager: TerminalManager | null;
private readonly providerSnapshotManager: ProviderSnapshotManager | null; private readonly providerSnapshotManager: ProviderSnapshotManager;
private unsubscribeProviderSnapshotEvents: (() => void) | null = null; private unsubscribeProviderSnapshotEvents: (() => void) | null = null;
private readonly scriptRouteStore: ScriptRouteStore | null; private readonly scriptRouteStore: ScriptRouteStore | null;
private readonly scriptRuntimeStore: WorkspaceScriptRuntimeStore | null; private readonly scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
@@ -836,9 +827,6 @@ export class Session {
private unregisterVoiceCallerContext?: (agentId: string) => void; private unregisterVoiceCallerContext?: (agentId: string) => void;
private getSpeechReadiness?: () => SpeechReadinessSnapshot; private getSpeechReadiness?: () => SpeechReadinessSnapshot;
private readonly sttLanguage: string; private readonly sttLanguage: string;
private readonly agentProviderRuntimeSettings: AgentProviderRuntimeSettingsMap | undefined;
private readonly providerOverrides: Record<string, ProviderOverride> | undefined;
private readonly isDev: boolean;
private readonly serverId: string | undefined; private readonly serverId: string | undefined;
private readonly daemonVersion: string | undefined; private readonly daemonVersion: string | undefined;
private readonly daemonRuntimeConfig: SessionOptions["daemonRuntimeConfig"]; private readonly daemonRuntimeConfig: SessionOptions["daemonRuntimeConfig"];
@@ -885,9 +873,6 @@ export class Session {
voice, voice,
voiceBridge, voiceBridge,
dictation, dictation,
agentProviderRuntimeSettings,
providerOverrides,
isDev,
serverId, serverId,
daemonVersion, daemonVersion,
daemonRuntimeConfig, daemonRuntimeConfig,
@@ -957,7 +942,7 @@ export class Session {
this.terminalController.killTerminalsUnderPath(rootPath), this.terminalController.killTerminalsUnderPath(rootPath),
logger: this.sessionLogger, logger: this.sessionLogger,
}); });
this.providerSnapshotManager = providerSnapshotManager ?? null; this.providerSnapshotManager = providerSnapshotManager;
this.scriptRouteStore = scriptRouteStore ?? null; this.scriptRouteStore = scriptRouteStore ?? null;
this.scriptRuntimeStore = scriptRuntimeStore ?? null; this.scriptRuntimeStore = scriptRuntimeStore ?? null;
this.workspaceSetupSnapshots = workspaceSetupSnapshots ?? new Map(); this.workspaceSetupSnapshots = workspaceSetupSnapshots ?? new Map();
@@ -968,9 +953,6 @@ export class Session {
this.sttLanguage = sttLanguage ?? "en"; this.sttLanguage = sttLanguage ?? "en";
this.subscribeToOptionalManagers(); this.subscribeToOptionalManagers();
this.bindVoiceBridges({ voice, voiceBridge, dictation }); this.bindVoiceBridges({ voice, voiceBridge, dictation });
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
this.providerOverrides = providerOverrides;
this.isDev = isDev === true;
this.serverId = serverId; this.serverId = serverId;
this.daemonVersion = daemonVersion; this.daemonVersion = daemonVersion;
this.daemonRuntimeConfig = daemonRuntimeConfig; this.daemonRuntimeConfig = daemonRuntimeConfig;
@@ -1219,27 +1201,25 @@ export class Session {
*/ */
private subscribeToOptionalManagers(): void { private subscribeToOptionalManagers(): void {
this.terminalController.start(); this.terminalController.start();
if (this.providerSnapshotManager) { const handleProviderSnapshotChange = (entries: ProviderSnapshotEntry[], cwd: string) => {
const handleProviderSnapshotChange = (entries: ProviderSnapshotEntry[], cwd: string) => { // COMPAT(providersSnapshot): keep provider visibility gating for older clients.
// COMPAT(providersSnapshot): keep provider visibility gating for older clients. const visibleEntries = entries.filter((entry) =>
const visibleEntries = entries.filter((entry) => this.isProviderVisibleToClient(entry.provider),
this.isProviderVisibleToClient(entry.provider), );
); const snapshotCwd = cwd === resolveSnapshotCwd() ? undefined : cwd;
const snapshotCwd = cwd === resolveSnapshotCwd() ? undefined : cwd; this.emit({
this.emit({ type: "providers_snapshot_update",
type: "providers_snapshot_update", payload: {
payload: { ...(snapshotCwd ? { cwd: snapshotCwd } : {}),
...(snapshotCwd ? { cwd: snapshotCwd } : {}), entries: this.downgradeEntryModesForClient(visibleEntries),
entries: this.downgradeEntryModesForClient(visibleEntries), generatedAt: new Date().toISOString(),
generatedAt: new Date().toISOString(), },
}, });
}); };
}; this.providerSnapshotManager.on("change", handleProviderSnapshotChange);
this.providerSnapshotManager.on("change", handleProviderSnapshotChange); this.unsubscribeProviderSnapshotEvents = () => {
this.unsubscribeProviderSnapshotEvents = () => { this.providerSnapshotManager.off("change", handleProviderSnapshotChange);
this.providerSnapshotManager?.off("change", handleProviderSnapshotChange); };
};
}
} }
private bindVoiceBridges(params: { private bindVoiceBridges(params: {
@@ -1419,25 +1399,9 @@ export class Session {
return payload; return payload;
} }
private getProviderRegistry(): ReturnType<typeof buildProviderRegistry> {
return buildProviderRegistry(this.sessionLogger, {
runtimeSettings: this.agentProviderRuntimeSettings,
providerOverrides: applyMutableProviderConfigToOverrides(
this.providerOverrides,
this.daemonConfigStore.get().providers,
),
workspaceGitService: this.workspaceGitService,
isDev: this.isDev,
});
}
private getRegisteredProviderIds(): AgentProvider[] {
return Object.keys(this.getProviderRegistry());
}
private buildStoredAgentPayload( private buildStoredAgentPayload(
record: StoredAgentRecord, record: StoredAgentRecord,
registeredProviderIds = this.getRegisteredProviderIds(), registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds(),
): AgentSnapshotPayload { ): AgentSnapshotPayload {
return buildStoredAgentPayload(record, registeredProviderIds); return buildStoredAgentPayload(record, registeredProviderIds);
} }
@@ -3122,6 +3086,7 @@ export class Session {
logger: this.sessionLogger, logger: this.sessionLogger,
paseoHome: this.paseoHome, paseoHome: this.paseoHome,
workspaceGitService: this.workspaceGitService, workspaceGitService: this.workspaceGitService,
providerSnapshotManager: this.providerSnapshotManager,
}, },
{ {
kind: "session", kind: "session",
@@ -3378,11 +3343,11 @@ export class Session {
if (!record) { if (!record) {
throw new Error(`Agent not found: ${agentId}`); throw new Error(`Agent not found: ${agentId}`);
} }
const providerRegistry = this.getProviderRegistry(); const registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds();
if (!isStoredAgentProviderAvailable(record, Object.keys(providerRegistry))) { if (!isStoredAgentProviderAvailable(record, registeredProviderIds)) {
throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`); throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`);
} }
const handle = toAgentPersistenceHandle(providerRegistry, record.persistence); const handle = toAgentPersistenceHandle(registeredProviderIds, record.persistence);
if (!handle) { if (!handle) {
throw new Error(`Agent ${agentId} cannot be refreshed because it lacks persistence`); throw new Error(`Agent ${agentId} cannot be refreshed because it lacks persistence`);
} }
@@ -3601,47 +3566,6 @@ export class Session {
): Promise<void> { ): Promise<void> {
const cwd = resolveSnapshotCwd(msg.cwd ? expandTilde(msg.cwd) : undefined); const cwd = resolveSnapshotCwd(msg.cwd ? expandTilde(msg.cwd) : undefined);
const fetchedAt = new Date().toISOString(); const fetchedAt = new Date().toISOString();
const manager = this.providerSnapshotManager;
if (!manager) {
try {
const definition = this.getProviderRegistry()[msg.provider];
if (!definition.enabled) {
this.emitProviderDisabledResponse("models", msg.provider, msg.requestId, fetchedAt);
return;
}
const models = await definition.fetchModels({
cwd,
force: false,
});
this.emit({
type: "list_provider_models_response",
payload: {
provider: msg.provider,
models,
error: null,
fetchedAt,
requestId: msg.requestId,
},
});
} catch (error) {
this.sessionLogger.error(
{ err: error, provider: msg.provider },
`Failed to list models for ${msg.provider}`,
);
this.emit({
type: "list_provider_models_response",
payload: {
provider: msg.provider,
error: getErrorMessage(error),
fetchedAt,
requestId: msg.requestId,
},
});
}
return;
}
const entry = await this.getProviderSnapshotEntryForRead(cwd, msg.provider); const entry = await this.getProviderSnapshotEntryForRead(cwd, msg.provider);
@@ -3698,53 +3622,14 @@ export class Session {
): Promise<void> { ): Promise<void> {
const fetchedAt = new Date().toISOString(); const fetchedAt = new Date().toISOString();
const cwd = resolveSnapshotCwd(msg.cwd ? expandTilde(msg.cwd) : undefined); const cwd = resolveSnapshotCwd(msg.cwd ? expandTilde(msg.cwd) : undefined);
const manager = this.providerSnapshotManager; const entry = await this.getProviderSnapshotEntryForRead(cwd, msg.provider);
if (manager) {
const entry = await this.getProviderSnapshotEntryForRead(cwd, msg.provider);
if (!entry) {
this.emit({
type: "list_provider_modes_response",
payload: {
provider: msg.provider,
error: `Unknown provider: ${msg.provider}`,
fetchedAt,
requestId: msg.requestId,
},
});
return;
}
if (!entry.enabled) {
this.emitProviderDisabledResponse("modes", msg.provider, msg.requestId, fetchedAt);
return;
}
if (entry.status === "ready") {
this.emit({
type: "list_provider_modes_response",
payload: {
provider: msg.provider,
modes: this.downgradeModeIconsForClient(entry.modes ?? []),
error: null,
fetchedAt: entry.fetchedAt ?? fetchedAt,
requestId: msg.requestId,
},
});
return;
}
const errorMessage =
entry.status === "error"
? (entry.error ?? `Failed to list modes for ${msg.provider}`)
: `Provider ${msg.provider} is not available`;
if (!entry) {
this.emit({ this.emit({
type: "list_provider_modes_response", type: "list_provider_modes_response",
payload: { payload: {
provider: msg.provider, provider: msg.provider,
error: errorMessage, error: `Unknown provider: ${msg.provider}`,
fetchedAt, fetchedAt,
requestId: msg.requestId, requestId: msg.requestId,
}, },
@@ -3752,42 +3637,39 @@ export class Session {
return; return;
} }
try { if (!entry.enabled) {
const definition = this.getProviderRegistry()[msg.provider]; this.emitProviderDisabledResponse("modes", msg.provider, msg.requestId, fetchedAt);
if (!definition.enabled) { return;
this.emitProviderDisabledResponse("modes", msg.provider, msg.requestId, fetchedAt);
return;
}
const modes = await definition.fetchModes({
cwd,
force: false,
});
this.emit({
type: "list_provider_modes_response",
payload: {
provider: msg.provider,
modes: this.downgradeModeIconsForClient(modes),
error: null,
fetchedAt,
requestId: msg.requestId,
},
});
} catch (error) {
this.sessionLogger.error(
{ err: error, provider: msg.provider },
`Failed to list modes for ${msg.provider}`,
);
this.emit({
type: "list_provider_modes_response",
payload: {
provider: msg.provider,
error: getErrorMessage(error),
fetchedAt,
requestId: msg.requestId,
},
});
} }
if (entry.status === "ready") {
this.emit({
type: "list_provider_modes_response",
payload: {
provider: msg.provider,
modes: this.downgradeModeIconsForClient(entry.modes ?? []),
error: null,
fetchedAt: entry.fetchedAt ?? fetchedAt,
requestId: msg.requestId,
},
});
return;
}
const errorMessage =
entry.status === "error"
? (entry.error ?? `Failed to list modes for ${msg.provider}`)
: `Provider ${msg.provider} is not available`;
this.emit({
type: "list_provider_modes_response",
payload: {
provider: msg.provider,
error: errorMessage,
fetchedAt,
requestId: msg.requestId,
},
});
} }
private async getProviderSnapshotEntryForRead( private async getProviderSnapshotEntryForRead(
@@ -3795,10 +3677,6 @@ export class Session {
provider: AgentProvider, provider: AgentProvider,
): Promise<ProviderSnapshotEntry | undefined> { ): Promise<ProviderSnapshotEntry | undefined> {
const manager = this.providerSnapshotManager; const manager = this.providerSnapshotManager;
if (!manager) {
return undefined;
}
const findEntry = () => const findEntry = () =>
manager.getSnapshot(cwd).find((candidate) => candidate.provider === provider); manager.getSnapshot(cwd).find((candidate) => candidate.provider === provider);
@@ -3983,10 +3861,8 @@ export class Session {
): Promise<void> { ): Promise<void> {
// COMPAT(providersSnapshot): keep legacy provider-list RPCs alongside snapshot flow. // COMPAT(providersSnapshot): keep legacy provider-list RPCs alongside snapshot flow.
const entries = this.providerSnapshotManager const entries = this.providerSnapshotManager
? this.providerSnapshotManager .getSnapshot(msg.cwd ? expandTilde(msg.cwd) : undefined)
.getSnapshot(msg.cwd ? expandTilde(msg.cwd) : undefined) .filter((entry) => this.isProviderVisibleToClient(entry.provider));
.filter((entry) => this.isProviderVisibleToClient(entry.provider))
: [];
this.emit({ this.emit({
type: "get_providers_snapshot_response", type: "get_providers_snapshot_response",
@@ -4002,12 +3878,12 @@ export class Session {
msg: Extract<SessionInboundMessage, { type: "refresh_providers_snapshot_request" }>, msg: Extract<SessionInboundMessage, { type: "refresh_providers_snapshot_request" }>,
): Promise<void> { ): Promise<void> {
if (msg.cwd) { if (msg.cwd) {
await this.providerSnapshotManager?.refreshSnapshotForCwd({ await this.providerSnapshotManager.refreshSnapshotForCwd({
cwd: expandTilde(msg.cwd), cwd: expandTilde(msg.cwd),
providers: msg.providers, providers: msg.providers,
}); });
} else { } else {
await this.providerSnapshotManager?.refreshSettingsSnapshot({ await this.providerSnapshotManager.refreshSettingsSnapshot({
providers: msg.providers, providers: msg.providers,
}); });
} }
@@ -4024,10 +3900,7 @@ export class Session {
msg: Extract<SessionInboundMessage, { type: "provider_diagnostic_request" }>, msg: Extract<SessionInboundMessage, { type: "provider_diagnostic_request" }>,
): Promise<void> { ): Promise<void> {
try { try {
const client = this.getProviderRegistry()[msg.provider].createClient(this.sessionLogger); const { diagnostic } = await this.providerSnapshotManager.getProviderDiagnostic(msg.provider);
const diagnostic = client.getDiagnostic
? (await client.getDiagnostic()).diagnostic
: "No diagnostic available for this provider.";
this.emit({ this.emit({
type: "provider_diagnostic_response", type: "provider_diagnostic_response",
payload: { payload: {
@@ -6018,7 +5891,7 @@ export class Session {
// (excluding internal agents which are for ephemeral system tasks) // (excluding internal agents which are for ephemeral system tasks)
const registryRecords = await this.agentStorage.list(); const registryRecords = await this.agentStorage.list();
const liveIds = new Set(agentSnapshots.map((a) => a.id)); const liveIds = new Set(agentSnapshots.map((a) => a.id));
const registeredProviderIds = this.getRegisteredProviderIds(); const registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds();
const persistedAgents = registryRecords const persistedAgents = registryRecords
.filter((record) => !liveIds.has(record.id) && !record.internal) .filter((record) => !liveIds.has(record.id) && !record.internal)
.filter( .filter(
@@ -6984,7 +6857,7 @@ export class Session {
request, request,
agentManager: this.agentManager, agentManager: this.agentManager,
agentStorage: this.agentStorage, agentStorage: this.agentStorage,
providerRegistry: this.getProviderRegistry(), providerSnapshotManager: this.providerSnapshotManager,
}); });
this.emit({ this.emit({
type: "fetch_recent_provider_sessions_response", type: "fetch_recent_provider_sessions_response",
@@ -5,6 +5,7 @@ import { createBranchChangeRouteHandler } from "./script-route-branch-handler.js
import { ScriptRouteStore } from "./script-proxy.js"; import { ScriptRouteStore } from "./script-proxy.js";
import { Session, type SessionOptions } from "./session.js"; import { Session, type SessionOptions } from "./session.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js"; import { asInternals, createStub } from "./test-utils/class-mocks.js";
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js"; import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
import type { import type {
WorkspaceGitListener, WorkspaceGitListener,
@@ -231,6 +232,7 @@ function createSessionForWorkspaceGitWatchTests(options?: {
mcpBaseUrl: null, mcpBaseUrl: null,
stt: null, stt: null,
tts: null, tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null, terminalManager: null,
scriptRouteStore: options?.scriptRouteStore, scriptRouteStore: options?.scriptRouteStore,
scriptRuntimeStore: options?.scriptRuntimeStore, scriptRuntimeStore: options?.scriptRuntimeStore,
@@ -12,6 +12,7 @@ import { Session, type SessionOptions } from "./session.js";
import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
import { createNoopWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js"; import { createNoopWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js"; import { asInternals, createStub } from "./test-utils/class-mocks.js";
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
import { import {
createPersistedProjectRecord, createPersistedProjectRecord,
createPersistedWorkspaceRecord, createPersistedWorkspaceRecord,
@@ -162,6 +163,7 @@ function createHarness(input: {
mcpBaseUrl: null, mcpBaseUrl: null,
stt: null, stt: null,
tts: null, tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null, terminalManager: null,
}); });
@@ -38,6 +38,7 @@ import {
asDaemonConfigStore, asDaemonConfigStore,
asTerminalManager, asTerminalManager,
asSessionInternals, asSessionInternals,
createProviderSnapshotManagerStub,
isSessionOutboundMessage, isSessionOutboundMessage,
filterByType, filterByType,
findByType, findByType,
@@ -510,6 +511,7 @@ function createSessionForWorkspaceTests(
mcpBaseUrl: null, mcpBaseUrl: null,
stt: null, stt: null,
tts: null, tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null, terminalManager: null,
}), }),
); );
@@ -619,6 +621,7 @@ test("create_agent_request keeps requested child cwd when grouped under an exist
mcpBaseUrl: null, mcpBaseUrl: null,
stt: null, stt: null,
tts: null, tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null, terminalManager: null,
}), }),
); );
@@ -1000,6 +1003,7 @@ test("archive emits an authoritative agent_update upsert for subscribed clients"
mcpBaseUrl: null, mcpBaseUrl: null,
stt: null, stt: null,
tts: null, tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null, terminalManager: null,
}), }),
); );
@@ -1165,6 +1169,7 @@ test("close_items_request archives agents and kills terminals in one batch", asy
mcpBaseUrl: null, mcpBaseUrl: null,
stt: null, stt: null,
tts: null, tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: asTerminalManager({ terminalManager: asTerminalManager({
killTerminal, killTerminal,
subscribeTerminalsChanged: () => () => {}, subscribeTerminalsChanged: () => () => {},
@@ -1353,6 +1358,7 @@ test("close_items_request archives stored agents that are not currently loaded",
mcpBaseUrl: null, mcpBaseUrl: null,
stt: null, stt: null,
tts: null, tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null, terminalManager: null,
}), }),
); );
@@ -1502,6 +1508,7 @@ test("close_items_request continues after an archive failure", async () => {
mcpBaseUrl: null, mcpBaseUrl: null,
stt: null, stt: null,
tts: null, tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: asTerminalManager({ terminalManager: asTerminalManager({
killTerminal: killTerminalBestEffort, killTerminal: killTerminalBestEffort,
subscribeTerminalsChanged: () => () => {}, subscribeTerminalsChanged: () => () => {},
@@ -2368,6 +2375,7 @@ test("workspace update stream keeps persisted workspace visible after agents sto
mcpBaseUrl: null, mcpBaseUrl: null,
stt: null, stt: null,
tts: null, tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null, terminalManager: null,
}), }),
); );
@@ -7,6 +7,7 @@ import { Session } from "./session.js";
import type { SessionOptions } from "./session.js"; import type { SessionOptions } from "./session.js";
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js"; import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js"; import { asInternals, createStub } from "./test-utils/class-mocks.js";
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
interface SessionInternals { interface SessionInternals {
archiveAgentForClose(agentId: string): Promise<{ archivedAt: string }>; archiveAgentForClose(agentId: string): Promise<{ archivedAt: string }>;
@@ -135,6 +136,7 @@ describe("snapshot mutation ownership boundary", () => {
}, },
stt: null, stt: null,
tts: null, tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null, terminalManager: null,
}), }),
); );
@@ -1,6 +1,18 @@
import { vi } from "vitest"; import { vi } from "vitest";
import type { ProviderSnapshotEntry } from "../agent/agent-sdk-types.js"; import { getAgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
import type {
AgentMode,
AgentModelDefinition,
AgentProvider,
ProviderSnapshotEntry,
} from "../agent/agent-sdk-types.js";
import type {
AgentManagerProviderState,
ProviderDiagnosticResult,
ResolvedProviderCreateConfig,
} from "../agent/provider-snapshot-manager.js";
import { ProviderSnapshotManager } from "../agent/provider-snapshot-manager.js"; import { ProviderSnapshotManager } from "../agent/provider-snapshot-manager.js";
import type { SessionOptions } from "../session.js"; import type { SessionOptions } from "../session.js";
import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
@@ -130,28 +142,88 @@ export function findByType<T extends SessionOutboundMessage["type"]>(
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface ProviderSnapshotManagerSpies { export interface ProviderSnapshotManagerSpies {
getSnapshot: ReturnType<typeof vi.fn<[], ProviderSnapshotEntry[]>>; getSnapshot: ReturnType<typeof vi.fn<[cwd?: string], ProviderSnapshotEntry[]>>;
refreshSnapshotForCwd: ReturnType<typeof vi.fn<[], Promise<void>>>; refreshSnapshotForCwd: ReturnType<typeof vi.fn<[unknown], Promise<void>>>;
refreshSettingsSnapshot: ReturnType<typeof vi.fn<[], Promise<void>>>; refreshSettingsSnapshot: ReturnType<typeof vi.fn<[unknown], Promise<void>>>;
warmUpSnapshotForCwd: ReturnType<typeof vi.fn<[], Promise<void>>>; warmUpSnapshotForCwd: ReturnType<typeof vi.fn<[unknown], Promise<void>>>;
listRegisteredProviderIds: ReturnType<typeof vi.fn<[], AgentProvider[]>>;
hasProvider: ReturnType<typeof vi.fn<[AgentProvider], boolean>>;
getProviderLabel: ReturnType<typeof vi.fn<[AgentProvider], string>>;
getAgentManagerProviderState: ReturnType<typeof vi.fn<[], AgentManagerProviderState>>;
listProviders: ReturnType<typeof vi.fn<[unknown], Promise<ProviderSnapshotEntry[]>>>;
getProvider: ReturnType<typeof vi.fn<[unknown], Promise<ProviderSnapshotEntry>>>;
listModels: ReturnType<typeof vi.fn<[unknown], Promise<AgentModelDefinition[]>>>;
listModes: ReturnType<typeof vi.fn<[unknown], Promise<AgentMode[]>>>;
resolveCreateConfig: ReturnType<typeof vi.fn<[unknown], Promise<ResolvedProviderCreateConfig>>>;
resolveDefaultModel: ReturnType<typeof vi.fn<[unknown], Promise<string | undefined>>>;
getProviderDiagnostic: ReturnType<
typeof vi.fn<[AgentProvider], Promise<ProviderDiagnosticResult>>
>;
applyMutableProviderConfig: ReturnType<typeof vi.fn<[unknown], AgentManagerProviderState>>;
destroy: ReturnType<typeof vi.fn<[], void>>;
} }
export function createProviderSnapshotManagerStub(): { export function createProviderSnapshotManagerStub(): {
manager: ProviderSnapshotManager; manager: ProviderSnapshotManager;
} & ProviderSnapshotManagerSpies { } & ProviderSnapshotManagerSpies {
const getSnapshot = vi.fn<[], ProviderSnapshotEntry[]>(() => []); const getSnapshot = vi.fn<[cwd?: string], ProviderSnapshotEntry[]>(() => []);
const refreshSnapshotForCwd = vi.fn<[], Promise<void>>(async () => {}); const refreshSnapshotForCwd = vi.fn<[unknown], Promise<void>>(async () => {});
const refreshSettingsSnapshot = vi.fn<[], Promise<void>>(async () => {}); const refreshSettingsSnapshot = vi.fn<[unknown], Promise<void>>(async () => {});
const warmUpSnapshotForCwd = vi.fn<[], Promise<void>>(async () => {}); const warmUpSnapshotForCwd = vi.fn<[unknown], Promise<void>>(async () => {});
const listRegisteredProviderIds = vi.fn<[], AgentProvider[]>(() => []);
const hasProvider = vi.fn<[AgentProvider], boolean>(() => false);
const getProviderLabel = vi.fn<[AgentProvider], string>((provider) => {
try {
return getAgentProviderDefinition(provider).label;
} catch {
return provider;
}
});
const getAgentManagerProviderState = vi.fn<[], AgentManagerProviderState>(() => ({
providerDefinitions: {},
clients: {},
}));
const listProviders = vi.fn<[unknown], Promise<ProviderSnapshotEntry[]>>(async () => []);
const getProvider = vi.fn<[unknown], Promise<ProviderSnapshotEntry>>(async () => {
throw new Error("createProviderSnapshotManagerStub: getProvider not stubbed");
});
const listModels = vi.fn<[unknown], Promise<AgentModelDefinition[]>>(async () => []);
const listModes = vi.fn<[unknown], Promise<AgentMode[]>>(async () => []);
const resolveCreateConfig = vi.fn<[unknown], Promise<ResolvedProviderCreateConfig>>(async () => ({
modeId: undefined,
featureValues: undefined,
}));
const resolveDefaultModel = vi.fn<[unknown], Promise<string | undefined>>(async () => undefined);
const getProviderDiagnostic = vi.fn<[AgentProvider], Promise<ProviderDiagnosticResult>>(
async (provider) => ({ provider, diagnostic: "No diagnostic available for this provider." }),
);
const applyMutableProviderConfig = vi.fn<[unknown], AgentManagerProviderState>(() => ({
providerDefinitions: {},
clients: {},
}));
const on = vi.fn(); const on = vi.fn();
const off = vi.fn(); const off = vi.fn();
const destroy = vi.fn<[], void>();
const stub = { const stub = {
getSnapshot, getSnapshot,
refreshSnapshotForCwd, refreshSnapshotForCwd,
refreshSettingsSnapshot, refreshSettingsSnapshot,
warmUpSnapshotForCwd, warmUpSnapshotForCwd,
listRegisteredProviderIds,
hasProvider,
getProviderLabel,
getAgentManagerProviderState,
listProviders,
getProvider,
listModels,
listModes,
resolveCreateConfig,
resolveDefaultModel,
getProviderDiagnostic,
applyMutableProviderConfig,
on, on,
off, off,
destroy,
}; };
on.mockImplementation(() => stub); on.mockImplementation(() => stub);
off.mockImplementation(() => stub); off.mockImplementation(() => stub);
@@ -162,5 +234,18 @@ export function createProviderSnapshotManagerStub(): {
refreshSnapshotForCwd, refreshSnapshotForCwd,
refreshSettingsSnapshot, refreshSettingsSnapshot,
warmUpSnapshotForCwd, warmUpSnapshotForCwd,
listRegisteredProviderIds,
hasProvider,
getProviderLabel,
getAgentManagerProviderState,
listProviders,
getProvider,
listModels,
listModes,
resolveCreateConfig,
resolveDefaultModel,
getProviderDiagnostic,
applyMutableProviderConfig,
destroy,
}; };
} }
@@ -10,6 +10,7 @@ import type { LoopService } from "./loop-service.js";
import type { ScheduleService } from "./schedule/service.js"; import type { ScheduleService } from "./schedule/service.js";
import type { CheckoutDiffManager } from "./checkout-diff-manager.js"; import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js"; import { asInternals, createStub } from "./test-utils/class-mocks.js";
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
import type { PushNotificationSender, PushPayload } from "./push/notifications.js"; import type { PushNotificationSender, PushPayload } from "./push/notifications.js";
const wsModuleMock = vi.hoisted(() => { const wsModuleMock = vi.hoisted(() => {
@@ -108,9 +109,6 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
undefined, undefined,
undefined, undefined,
undefined, undefined,
undefined,
undefined,
false,
"1.2.3-test", "1.2.3-test",
undefined, undefined,
undefined, undefined,
@@ -138,6 +136,7 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
undefined, undefined,
undefined, undefined,
pushNotifications, pushNotifications,
createProviderSnapshotManagerStub().manager,
); );
return { server, agentManager, pushNotifications }; return { server, agentManager, pushNotifications };
@@ -10,6 +10,7 @@ import type { LoopService } from "./loop-service.js";
import type { ScheduleService } from "./schedule/service.js"; import type { ScheduleService } from "./schedule/service.js";
import type { CheckoutDiffManager } from "./checkout-diff-manager.js"; import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js"; import { asInternals, createStub } from "./test-utils/class-mocks.js";
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
import { import {
asUint8Array, asUint8Array,
decodeTerminalStreamFrame, decodeTerminalStreamFrame,
@@ -230,9 +231,6 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
: undefined, : undefined,
undefined, undefined,
undefined, undefined,
undefined,
undefined,
false,
TEST_DAEMON_VERSION, TEST_DAEMON_VERSION,
undefined, undefined,
undefined, undefined,
@@ -251,6 +249,16 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
})), })),
dispose: vi.fn(), dispose: vi.fn(),
}), }),
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
createProviderSnapshotManagerStub().manager,
); );
} }
+7 -43
View File
@@ -13,7 +13,6 @@ import type { LoopService } from "./loop-service.js";
import type { ScheduleService } from "./schedule/service.js"; import type { ScheduleService } from "./schedule/service.js";
import type { CheckoutDiffManager, CheckoutDiffMetrics } from "./checkout-diff-manager.js"; import type { CheckoutDiffManager, CheckoutDiffMetrics } from "./checkout-diff-manager.js";
import type { DaemonConfigStore, MutableDaemonConfig } from "./daemon-config-store.js"; import type { DaemonConfigStore, MutableDaemonConfig } from "./daemon-config-store.js";
import { applyMutableProviderConfigToOverrides } from "./daemon-config-store.js";
import { import {
type ServerInfoStatusPayload, type ServerInfoStatusPayload,
type WorkspaceSetupSnapshot, type WorkspaceSetupSnapshot,
@@ -30,12 +29,7 @@ import type { HostnamesConfig } from "./hostnames.js";
import { isHostnameAllowed } from "./hostnames.js"; import { isHostnameAllowed } from "./hostnames.js";
import { Session, type SessionLifecycleIntent, type SessionRuntimeMetrics } from "./session.js"; import { Session, type SessionLifecycleIntent, type SessionRuntimeMetrics } from "./session.js";
import type { AgentProvider } from "./agent/agent-sdk-types.js"; import type { AgentProvider } from "./agent/agent-sdk-types.js";
import type {
AgentProviderRuntimeSettingsMap,
ProviderOverride,
} from "./agent/provider-launch-config.js";
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js"; import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
import { buildProviderRegistry, createClientsFromRegistry } from "./agent/provider-registry.js";
import type { WorkspaceGitRuntimeSnapshot, WorkspaceGitService } from "./workspace-git-service.js"; import type { WorkspaceGitRuntimeSnapshot, WorkspaceGitService } from "./workspace-git-service.js";
import { buildWorkspaceGitMetadataFromSnapshot } from "./workspace-git-metadata.js"; import { buildWorkspaceGitMetadataFromSnapshot } from "./workspace-git-metadata.js";
import { PushTokenStore } from "./push/token-store.js"; import { PushTokenStore } from "./push/token-store.js";
@@ -373,9 +367,6 @@ export class VoiceAssistantWebSocketServer {
private readonly voiceSpeakHandlers = new Map<string, VoiceSpeakHandler>(); private readonly voiceSpeakHandlers = new Map<string, VoiceSpeakHandler>();
private readonly voiceCallerContexts = new Map<string, VoiceCallerContext>(); private readonly voiceCallerContexts = new Map<string, VoiceCallerContext>();
private readonly workspaceSetupSnapshots = new Map<string, WorkspaceSetupSnapshot>(); private readonly workspaceSetupSnapshots = new Map<string, WorkspaceSetupSnapshot>();
private agentProviderRuntimeSettings: AgentProviderRuntimeSettingsMap | undefined;
private providerOverrides: Record<string, ProviderOverride> | undefined;
private isDev!: boolean;
private readonly providerSnapshotManager: ProviderSnapshotManager; private readonly providerSnapshotManager: ProviderSnapshotManager;
private onLifecycleIntent!: ((intent: SessionLifecycleIntent) => void) | null; private onLifecycleIntent!: ((intent: SessionLifecycleIntent) => void) | null;
private onBranchChanged!: private onBranchChanged!:
@@ -404,9 +395,6 @@ export class VoiceAssistantWebSocketServer {
dictation?: { dictation?: {
finalTimeoutMs?: number; finalTimeoutMs?: number;
}, },
agentProviderRuntimeSettings?: AgentProviderRuntimeSettingsMap,
providerOverrides?: Record<string, ProviderOverride>,
isDev?: boolean,
daemonVersion?: string, daemonVersion?: string,
onLifecycleIntent?: (intent: SessionLifecycleIntent) => void, onLifecycleIntent?: (intent: SessionLifecycleIntent) => void,
projectRegistry?: ProjectRegistry, projectRegistry?: ProjectRegistry,
@@ -428,6 +416,7 @@ export class VoiceAssistantWebSocketServer {
workspaceGitService?: WorkspaceGitService, workspaceGitService?: WorkspaceGitService,
github?: GitHubService, github?: GitHubService,
pushNotificationSender?: PushNotificationSender, pushNotificationSender?: PushNotificationSender,
providerSnapshotManager?: ProviderSnapshotManager,
daemonRuntimeConfig?: { daemonRuntimeConfig?: {
listen: string | null; listen: string | null;
relay: { relay: {
@@ -470,9 +459,6 @@ export class VoiceAssistantWebSocketServer {
speech, speech,
terminalManager, terminalManager,
dictation, dictation,
agentProviderRuntimeSettings,
providerOverrides,
isDev,
onLifecycleIntent, onLifecycleIntent,
scriptRouteStore, scriptRouteStore,
scriptRuntimeStore, scriptRuntimeStore,
@@ -481,15 +467,10 @@ export class VoiceAssistantWebSocketServer {
getDaemonTcpHost, getDaemonTcpHost,
resolveScriptHealth, resolveScriptHealth,
}); });
const providerSnapshotLogger = this.logger.child({ module: "provider-snapshot-manager" }); if (!providerSnapshotManager) {
this.providerSnapshotManager = new ProviderSnapshotManager( throw new Error("providerSnapshotManager is required");
buildProviderRegistry(providerSnapshotLogger, { }
runtimeSettings: this.agentProviderRuntimeSettings, this.providerSnapshotManager = providerSnapshotManager;
providerOverrides: this.providerOverrides,
isDev: this.isDev,
}),
providerSnapshotLogger,
);
this.serverCapabilities = buildServerCapabilities({ this.serverCapabilities = buildServerCapabilities({
readiness: this.speech?.getReadiness() ?? null, readiness: this.speech?.getReadiness() ?? null,
}); });
@@ -498,18 +479,10 @@ export class VoiceAssistantWebSocketServer {
this.publishSpeechReadiness(snapshot); this.publishSpeechReadiness(snapshot);
}) ?? null; }) ?? null;
this.unsubscribeDaemonConfigChange = this.daemonConfigStore.onChange((config) => { this.unsubscribeDaemonConfigChange = this.daemonConfigStore.onChange((config) => {
this.providerOverrides = applyMutableProviderConfigToOverrides( const nextAgentManagerState = this.providerSnapshotManager.applyMutableProviderConfig(
this.providerOverrides,
config.providers, config.providers,
); );
const registry = buildProviderRegistry(providerSnapshotLogger, { this.agentManager.updateProviderRegistry(nextAgentManagerState);
runtimeSettings: this.agentProviderRuntimeSettings,
providerOverrides: this.providerOverrides,
isDev: this.isDev,
});
const clients = createClientsFromRegistry(registry, providerSnapshotLogger);
this.providerSnapshotManager.replaceRegistry(registry);
this.agentManager.updateProviderRegistry({ providerDefinitions: registry, clients });
this.broadcastDaemonConfigChanged(config); this.broadcastDaemonConfigChanged(config);
}); });
@@ -534,9 +507,6 @@ export class VoiceAssistantWebSocketServer {
speech: SpeechService | null | undefined; speech: SpeechService | null | undefined;
terminalManager: TerminalManager | null | undefined; terminalManager: TerminalManager | null | undefined;
dictation: { finalTimeoutMs?: number } | undefined; dictation: { finalTimeoutMs?: number } | undefined;
agentProviderRuntimeSettings: AgentProviderRuntimeSettingsMap | undefined;
providerOverrides: Record<string, ProviderOverride> | undefined;
isDev: boolean | undefined;
onLifecycleIntent: ((intent: SessionLifecycleIntent) => void) | undefined; onLifecycleIntent: ((intent: SessionLifecycleIntent) => void) | undefined;
scriptRouteStore: ScriptRouteStore | null | undefined; scriptRouteStore: ScriptRouteStore | null | undefined;
scriptRuntimeStore: WorkspaceScriptRuntimeStore | null | undefined; scriptRuntimeStore: WorkspaceScriptRuntimeStore | null | undefined;
@@ -550,9 +520,6 @@ export class VoiceAssistantWebSocketServer {
this.speech = params.speech ?? null; this.speech = params.speech ?? null;
this.terminalManager = params.terminalManager ?? null; this.terminalManager = params.terminalManager ?? null;
this.dictation = params.dictation ?? null; this.dictation = params.dictation ?? null;
this.agentProviderRuntimeSettings = params.agentProviderRuntimeSettings;
this.providerOverrides = params.providerOverrides;
this.isDev = params.isDev === true;
this.onLifecycleIntent = params.onLifecycleIntent ?? null; this.onLifecycleIntent = params.onLifecycleIntent ?? null;
this.scriptRouteStore = params.scriptRouteStore ?? null; this.scriptRouteStore = params.scriptRouteStore ?? null;
this.scriptRuntimeStore = params.scriptRuntimeStore ?? null; this.scriptRuntimeStore = params.scriptRuntimeStore ?? null;
@@ -941,9 +908,6 @@ export class VoiceAssistantWebSocketServer {
getSpeechReadiness: () => this.speech!.getReadiness(), getSpeechReadiness: () => this.speech!.getReadiness(),
} }
: undefined, : undefined,
agentProviderRuntimeSettings: this.agentProviderRuntimeSettings,
providerOverrides: this.providerOverrides,
isDev: this.isDev,
serverId: this.serverId, serverId: this.serverId,
daemonVersion: this.daemonVersion, daemonVersion: this.daemonVersion,
daemonRuntimeConfig: this.daemonRuntimeConfig, daemonRuntimeConfig: this.daemonRuntimeConfig,
@@ -11,6 +11,7 @@ import {
type SessionOutboundMessage, type SessionOutboundMessage,
} from "@getpaseo/protocol/messages"; } from "@getpaseo/protocol/messages";
import { Session, type SessionOptions } from "./session.js"; import { Session, type SessionOptions } from "./session.js";
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
import type { AgentTimelineRow } from "./agent/agent-manager.js"; import type { AgentTimelineRow } from "./agent/agent-manager.js";
import { handleCreatePaseoWorktreeRequest } from "./worktree-session.js"; import { handleCreatePaseoWorktreeRequest } from "./worktree-session.js";
@@ -294,6 +295,7 @@ function createSessionForWireCompatTest(options?: {
new EmptyDaemonConfigStore() as unknown as SessionOptions["daemonConfigStore"], new EmptyDaemonConfigStore() as unknown as SessionOptions["daemonConfigStore"],
stt: null, stt: null,
tts: null, tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null, terminalManager: null,
}); });