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

View File

@@ -48,6 +48,7 @@ export interface AgentMode {
description?: string;
icon?: string;
colorTier?: string;
isUnattended?: boolean;
}
export type ProviderStatus = "ready" | "loading" | "error" | "unavailable";
@@ -93,6 +94,32 @@ export interface ProviderSnapshotEntry {
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 {
type: "toggle";
id: string;
@@ -590,6 +617,8 @@ export interface AgentClient {
): Promise<AgentSession>;
listModels(options: ListModelsOptions): Promise<AgentModelDefinition[]>;
listModes?(options: ListModesOptions): Promise<AgentMode[]>;
resolveCreateConfig?(input: ResolveAgentCreateConfigInput): ResolveAgentCreateConfigResult;
isCreateConfigUnattended?(input: AgentCreateConfigUnattendedInput): boolean;
listCommands?(config: AgentSessionConfig): Promise<AgentSlashCommand[]>;
listFeatures?(config: AgentSessionConfig): Promise<AgentFeature[]>;
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.
*/
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>;
}

View File

@@ -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 {
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)}`,
);
}
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;
}

View File

@@ -16,17 +16,14 @@ import type { AgentAttachment, FirstAgentContext, GitSetupOptions } from "../../
import type { AgentManager, ManagedAgent } from "../agent-manager.js";
import { scheduleAgentMetadataGeneration } from "../agent-metadata-generator.js";
import type {
AgentProvider,
AgentPromptContentBlock,
AgentPromptInput,
AgentRunOptions,
AgentSessionConfig,
} from "../agent-sdk-types.js";
import type { AgentStorage } from "../agent-storage.js";
import { getAgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
import type { ProviderDefinition } from "../provider-registry.js";
import type { ProviderSnapshotManager } from "../provider-snapshot-manager.js";
import { setupFinishNotification, startCreatedAgentInitialPrompt } from "../agent-prompt.js";
import { resolveAndValidateCreateAgentMode } from "../create-agent-mode.js";
import { resolveClientMessageId } from "../../client-message-id.js";
import { resolveRequiredProviderModel } from "../mcp-shared.js";
import {
@@ -34,51 +31,6 @@ import {
emitLiveTimelineItemIfAgentKnown,
} 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 {
workspaceId: string;
}
@@ -98,7 +50,7 @@ interface CreateAgentCommandDependencies {
"getSnapshot" | "listWorktrees" | "resolveRepoRoot"
>;
terminalManager?: TerminalManager | null;
providerRegistry?: Record<AgentProvider, ProviderDefinition> | null;
providerSnapshotManager: ProviderSnapshotManager;
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
}
@@ -293,22 +245,14 @@ async function resolveMcpCreateAgent(
initialPrompt: input.initialPrompt,
});
const parentForResolve = parentAgent
? {
provider: parentAgent.provider,
modeId: parentAgent.currentModeId,
isUnattended: isAgentInUnattendedState(dependencies, parentAgent),
}
: null;
const { mode: resolvedMode, features: resolvedFeatures } = resolveCreateModeAndFeatures(
dependencies,
{
const { modeId: resolvedMode, featureValues: resolvedFeatures } =
await dependencies.providerSnapshotManager.resolveCreateConfig({
cwd: resolvedCwd,
provider,
requestedMode: input.mode,
parent: parentForResolve,
features: input.features,
},
);
featureValues: input.features,
parent: parentAgent,
});
const labels = mergeLabels(
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(
dependencies: CreateAgentCommandDependencies,
resolved: ResolvedCreateAgent,
@@ -565,47 +471,3 @@ function mergeLabels(
};
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);
}

View File

@@ -208,7 +208,7 @@ test("listImportableProviderSessions filters, sorts, limits, and projects import
}),
agentManager,
agentStorage,
providerRegistry: { codex: { label: "Codex" } },
providerSnapshotManager: { getProviderLabel: () => "Codex" },
});
expect(listImportablePersistedAgents).toHaveBeenCalledWith({
@@ -274,7 +274,7 @@ test("listImportableProviderSessions filters out metadata generation sessions",
agentStorage: {
list: async () => [],
} satisfies Pick<AgentStorage, "list">,
providerRegistry: { codex: { label: "Codex" } },
providerSnapshotManager: { getProviderLabel: () => "Codex" },
});
expect(result.entries).toHaveLength(1);
@@ -309,7 +309,7 @@ test("listImportableProviderSessions keeps realpath-equivalent cwd matches", asy
agentStorage: {
list: async () => [],
} satisfies Pick<AgentStorage, "list">,
providerRegistry: { pi: { label: "Pi" } },
providerSnapshotManager: { getProviderLabel: () => "Pi" },
});
expect(result.entries.map((entry) => entry.providerHandleId)).toEqual(["pi-handle"]);
@@ -326,7 +326,7 @@ test("listImportableProviderSessions rejects invalid since values", async () =>
agentStorage: {
list: async () => [],
} satisfies Pick<AgentStorage, "list">,
providerRegistry: {},
providerSnapshotManager: { getProviderLabel: () => "" },
}),
).rejects.toMatchObject(
new ImportSessionsRequestError("invalid_since", "Invalid recent provider sessions since"),

View File

@@ -1,6 +1,6 @@
import type { z } from "zod";
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 { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
import type {
@@ -49,7 +49,7 @@ export interface ListImportableProviderSessionsInput {
request: FetchRecentProviderSessionsRequestMessage;
agentManager: Pick<AgentManager, "listAgents" | "listImportablePersistedAgents">;
agentStorage: Pick<AgentStorage, "list">;
providerRegistry: Record<string, Pick<ProviderDefinition, "label"> | undefined>;
providerSnapshotManager: Pick<ProviderSnapshotManager, "getProviderLabel">;
}
export interface ListImportableProviderSessionsResult {
@@ -99,7 +99,7 @@ export function normalizeImportAgentRequest(
export async function listImportableProviderSessions(
input: ListImportableProviderSessionsInput,
): Promise<ListImportableProviderSessionsResult> {
const { request, agentManager, agentStorage, providerRegistry } = input;
const { request, agentManager, agentStorage, providerSnapshotManager } = input;
const limit = request.limit ?? 20;
const sinceTimestamp = parseRecentProviderSessionsSince(request.since);
const providerFilter = request.providers ? new Set(request.providers) : undefined;
@@ -140,7 +140,7 @@ export async function listImportableProviderSessions(
.slice(0, limit)
.map((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

View File

@@ -9,7 +9,7 @@ import type {
ServerRequest,
} 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 {
AgentFeatureSchema,
@@ -47,8 +47,7 @@ import {
type ScheduleCadence,
type UpdateScheduleInput,
} from "@getpaseo/protocol/schedule/types";
import type { ProviderDefinition } from "./provider-registry.js";
import { resolveSnapshotCwd } from "./provider-snapshot-manager.js";
import { resolveSnapshotCwd, type ProviderSnapshotManager } from "./provider-snapshot-manager.js";
import {
AgentModelSchema,
AgentProviderEnum,
@@ -88,7 +87,7 @@ export interface AgentMcpServerOptions {
terminalManager?: TerminalManager | null;
getDaemonTcpPort?: () => number | null;
scheduleService?: ScheduleService | null;
providerRegistry?: Record<AgentProvider, ProviderDefinition> | null;
providerSnapshotManager: ProviderSnapshotManager;
github?: GitHubService;
workspaceGitService?: Pick<
WorkspaceGitService,
@@ -117,47 +116,6 @@ export interface AgentMcpServerOptions {
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 {
if (result.structuredContent === undefined || result.content.length > 0) {
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 {
id: AgentProvider;
label: string;
description: string;
enabled: boolean;
modes: ProviderDefinition["modes"];
modes: AgentMode[];
status: string;
error?: string;
}
async function resolveProviderSummary(
provider: ProviderDefinition,
logger: Logger,
): Promise<ProviderSummary> {
const base = {
id: provider.id,
label: provider.label,
description: provider.description,
modes: provider.modes,
function toProviderSummary(entry: {
provider: AgentProvider;
label?: string;
description?: string;
enabled: boolean;
modes?: AgentMode[];
status: string;
error?: string;
}): 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 {
@@ -539,7 +483,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
agentStorage,
terminalManager,
scheduleService,
providerRegistry,
providerSnapshotManager,
callerAgentId,
resolveSpeakHandler,
resolveCallerContext,
@@ -639,13 +583,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
return {
provider: resolvedProvider,
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : callerAgent.cwd,
...(callerAgent.currentModeId
...(callerAgent.currentModeId && callerAgent.provider === resolvedProvider
? {
modeId: mapModeAcrossProviders(
callerAgent.currentModeId,
callerAgent.provider,
resolvedProvider,
),
modeId: callerAgent.currentModeId,
}
: {}),
...(resolvedModel ? { model: resolvedModel } : {}),
@@ -927,7 +867,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
paseoHome: options.paseoHome,
workspaceGitService: options.workspaceGitService,
terminalManager,
providerRegistry,
providerSnapshotManager,
createPaseoWorktree: options.createPaseoWorktree,
},
{
@@ -1249,7 +1189,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
const structuredSnapshot = buildStoredAgentPayload(
record,
resolveRegisteredProviderIds(agentManager, providerRegistry),
providerSnapshotManager.listRegisteredProviderIds(),
);
return {
content: [],
@@ -1296,7 +1236,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
);
const liveIds = new Set(liveSnapshots.map((snapshot) => snapshot.id));
const storedRecords = await agentStorage.list();
const registeredProviderIds = resolveRegisteredProviderIds(agentManager, providerRegistry);
const registeredProviderIds = providerSnapshotManager.listRegisteredProviderIds();
const storedAgents = storedRecords
.filter((record) => !record.internal && !liveIds.has(record.id))
.filter((record) => includeArchived || !record.archivedAt)
@@ -1941,10 +1881,8 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
},
},
async () => {
const providers = await Promise.all(
Object.values(providerRegistry ?? {}).map((provider) =>
resolveProviderSummary(provider, childLogger),
),
const providers = (await providerSnapshotManager.listProviders({ wait: true })).map(
toProviderSummary,
);
return {
content: [],
@@ -1967,19 +1905,11 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
},
},
async ({ provider }) => {
if (!providerRegistry) {
throw new Error("Provider registry is not configured");
}
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 });
const models = await providerSnapshotManager.listModels({
cwd: resolveSnapshotCwd(),
provider,
wait: true,
});
return {
content: [],
structuredContent: ensureValidJson({
@@ -2014,21 +1944,19 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
defaultProvider: provider,
});
const providerId = resolvedProviderModel.provider;
if (!providerRegistry) {
throw new Error("Provider registry is not configured");
}
const definition = providerRegistry[providerId];
if (!definition) {
throw new Error(`Provider ${providerId} is not configured`);
}
const summary = await resolveProviderSummary(definition, childLogger);
if (!definition.enabled) {
const resolvedCwd = resolveScopedCwd(cwd, { required: true });
const entry = await providerSnapshotManager.getProvider({
cwd: resolvedCwd,
provider: providerId,
wait: true,
});
const summary = toProviderSummary(entry);
if (!entry.enabled) {
throw new Error(`Provider '${providerId}' is disabled`);
}
if (summary.status !== "available") {
throw new Error(summary.error ?? `Provider '${providerId}' is unavailable`);
if (entry.status !== "ready") {
throw new Error(entry.error ?? `Provider '${providerId}' is unavailable`);
}
const resolvedCwd = resolveScopedCwd(cwd, { required: true });
const selectedModel = settings?.model ?? resolvedProviderModel.model;
const features = await agentManager.listDraftFeatures({
provider: providerId,

View File

@@ -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();
});
});

View File

@@ -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;
}
}

View File

@@ -2,6 +2,7 @@ import type { Logger } from "pino";
import type {
AgentClient,
AgentCreateConfigUnattendedInput,
AgentMode,
AgentModelDefinition,
AgentPersistenceHandle,
@@ -13,7 +14,13 @@ import type {
ListModesOptions,
ListPersistedAgentsOptions,
PersistedAgentDescriptor,
ResolveAgentCreateConfigInput,
ResolveAgentCreateConfigResult,
} from "./agent-sdk-types.js";
import {
isDefaultAgentCreateConfigUnattended,
resolveDefaultAgentCreateConfig,
} from "./create-agent-mode.js";
import { normalizeAgentModelDefinition } from "./agent-sdk-types.js";
import type { WorkspaceGitService } from "../workspace-git-service.js";
import type {
@@ -28,7 +35,6 @@ import { CopilotACPAgentClient } from "./providers/copilot-acp-agent.js";
import { CursorACPAgentClient } from "./providers/cursor-acp-agent.js";
import { GenericACPAgentClient } from "./providers/generic-acp-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 { MockLoadTestAgentClient } from "./providers/mock-load-test-agent.js";
import { MockSlowProviderClient } from "./providers/mock-slow-provider.js";
@@ -57,6 +63,8 @@ export interface ProviderDefinition extends AgentProviderDefinition {
*/
derivedFromProviderId: string | null;
createClient: (logger: Logger) => AgentClient;
resolveCreateConfig: (input: ResolveAgentCreateConfigInput) => ResolveAgentCreateConfigResult;
isCreateConfigUnattended: (input: AgentCreateConfigUnattendedInput) => boolean;
fetchModels: (options: ListModelsOptions) => Promise<AgentModelDefinition[]>;
fetchModes: (options: ListModesOptions) => Promise<AgentMode[]>;
}
@@ -414,6 +422,8 @@ function wrapClientProvider(
profileModelsAreAdditive,
}),
listModes: inner.listModes?.bind(inner),
resolveCreateConfig: inner.resolveCreateConfig?.bind(inner),
isCreateConfigUnattended: inner.isCreateConfigUnattended?.bind(inner),
listPersistedAgents: listPersistedAgents
? async (options?: ListPersistedAgentsOptions) =>
(await listPersistedAgents(options)).map((descriptor) =>
@@ -438,6 +448,9 @@ function createRegistryEntry(
derivedFromProviderId: resolved.derivedFromProviderId,
createClient: (providerLogger: Logger) =>
createResolvedProviderClient(providerLogger, provider, resolved),
resolveCreateConfig: modelClient.resolveCreateConfig ?? resolveDefaultAgentCreateConfig,
isCreateConfigUnattended:
modelClient.isCreateConfigUnattended ?? isDefaultAgentCreateConfigUnattended,
fetchModels: async (options: ListModelsOptions) =>
mergeModels(
provider,
@@ -673,5 +686,22 @@ export async function shutdownProviders(
logger: Logger,
options?: BuildProviderRegistryOptions,
): 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");
}
}),
);
}

View File

@@ -4,20 +4,91 @@ import { resolve } from "node:path";
import type { Logger } from "pino";
import { expandTilde } from "../../utils/path.js";
import { withTimeout } from "../../utils/promise-timeout.js";
import type { AgentProvider, ProviderSnapshotEntry } from "./agent-sdk-types.js";
import type { ProviderDefinition } from "./provider-registry.js";
import type {
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;
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;
}
interface ProviderSnapshotRefreshOptions {
cwd: string;
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 {
cwd: string;
providers: AgentProvider[];
@@ -33,15 +104,27 @@ export class ProviderSnapshotManager {
private readonly events = new EventEmitter();
private destroyed = false;
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 providerClients: Record<AgentProvider, AgentClient>;
constructor(
providerRegistry: Record<AgentProvider, ProviderDefinition>,
private readonly logger: Logger,
options: ProviderSnapshotManagerOptions = {},
) {
this.providerRegistry = providerRegistry;
constructor(options: ProviderSnapshotManagerOptions) {
this.logger = options.logger;
this.workspaceGitService = options.workspaceGitService;
this.isDev = options.isDev === true;
this.extraClients = options.extraClients ?? {};
this.runtimeSettings = options.runtimeSettings;
this.providerOverrides = options.providerOverrides;
this.baseProviderOverrides = options.providerOverrides;
this.refreshTimeoutMs = options.refreshTimeoutMs ?? DEFAULT_REFRESH_TIMEOUT_MS;
this.providerRegistry = this.buildRegistry();
this.providerClients = { ...this.extraClients } as Record<AgentProvider, AgentClient>;
}
getSnapshot(cwd?: string): ProviderSnapshotEntry[] {
@@ -118,6 +201,145 @@ export class ProviderSnapshotManager {
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 {
this.events.on(event, listener);
return this;
@@ -128,6 +350,17 @@ export class ProviderSnapshotManager {
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 {
this.destroyed = true;
this.events.removeAllListeners();
@@ -135,14 +368,51 @@ export class ProviderSnapshotManager {
this.providerLoads.clear();
}
replaceRegistry(providerRegistry: Record<AgentProvider, ProviderDefinition>): void {
this.providerRegistry = providerRegistry;
private buildRegistry(): Record<AgentProvider, ProviderDefinition> {
return buildProviderRegistry(this.logger, {
runtimeSettings: this.runtimeSettings,
providerOverrides: this.providerOverrides,
workspaceGitService: this.workspaceGitService,
isDev: this.isDev,
});
}
for (const cwd of this.snapshots.keys()) {
this.providerLoads.delete(cwd);
this.snapshots.set(cwd, this.reconcileSnapshotForRegistry(cwd));
this.emitChange(cwd);
private resolveParent(parent: ManagedAgent) {
const definition = this.requireProvider(parent.provider);
return {
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> {
@@ -322,7 +592,7 @@ export class ProviderSnapshotManager {
return;
}
const client = definition.createClient(this.logger);
const client = this.ensureClient(provider, definition);
const available = await withTimeout(
client.isAvailable(),
this.refreshTimeoutMs,

View File

@@ -125,6 +125,44 @@ describe("OpenCode auto_accept feature", () => {
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 () => {
const { openCodeClient, runtime } = mockOpenCodeClient({
events: [toolPermissionEvent(), idleEvent()],

View File

@@ -37,6 +37,8 @@ import {
type AgentStreamEvent,
type AgentTimelineItem,
type AgentUsage,
type ResolveAgentCreateConfigInput,
type ResolveAgentCreateConfigResult,
type ListModelsOptions,
type ListModesOptions,
type ListPersistedAgentsOptions,
@@ -45,6 +47,10 @@ import {
type ToolCallDetail,
type ToolCallTimelineItem,
} from "../agent-sdk-types.js";
import {
isDefaultAgentCreateConfigUnattended,
resolveDefaultAgentCreateConfig,
} from "../create-agent-mode.js";
import {
checkProviderLaunchAvailable,
createProviderEnvSpec,
@@ -111,6 +117,55 @@ function isOpenCodeAutoAcceptEnabled(config: AgentSessionConfig): boolean {
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 {
return {
type: "toggle",
@@ -1147,6 +1202,8 @@ class ProductionOpenCodeRuntime implements OpenCodeRuntime {
export class OpenCodeAgentClient implements AgentClient {
readonly provider = "opencode" as const;
readonly capabilities = OPENCODE_CAPABILITIES;
readonly resolveCreateConfig = resolveOpenCodeCreateConfig;
readonly isCreateConfigUnattended = isOpenCodeCreateConfigUnattended;
private readonly runtime: OpenCodeRuntime;
private readonly logger: Logger;
@@ -1403,6 +1460,10 @@ export class OpenCodeAgentClient implements AgentClient {
return availability.available;
}
async shutdown(): Promise<void> {
await this.runtime.shutdown();
}
async getDiagnostic(): Promise<{ diagnostic: string }> {
try {
const launch = await resolveProviderLaunch({

View File

@@ -99,11 +99,7 @@ import { AgentManager } from "./agent/agent-manager.js";
import { AgentStorage } from "./agent/agent-storage.js";
import { attachAgentStoragePersistence } from "./persistence-hooks.js";
import { createAgentMcpServer } from "./agent/mcp-server.js";
import {
buildProviderRegistry,
createClientsFromRegistry,
shutdownProviders,
} from "./agent/provider-registry.js";
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js";
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
import { FileBackedProjectRegistry, FileBackedWorkspaceRegistry } from "./workspace-registry.js";
@@ -516,18 +512,19 @@ export async function createPaseoDaemon(
github,
},
});
const providerRegistry = buildProviderRegistry(logger, {
const providerSnapshotLogger = logger.child({ module: "provider-snapshot-manager" });
const providerSnapshotManager = new ProviderSnapshotManager({
logger: providerSnapshotLogger,
runtimeSettings: config.agentProviderSettings,
providerOverrides: config.providerOverrides,
workspaceGitService,
isDev: config.isDev === true,
extraClients: config.agentClients,
});
const initialAgentManagerState = providerSnapshotManager.getAgentManagerProviderState();
const agentManager = new AgentManager({
clients: {
...createClientsFromRegistry(providerRegistry, logger),
...config.agentClients,
},
providerDefinitions: providerRegistry,
clients: initialAgentManagerState.clients,
providerDefinitions: initialAgentManagerState.providerDefinitions,
registry: agentStorage,
appendSystemPrompt: config.appendSystemPrompt,
logger,
@@ -670,7 +667,7 @@ export async function createPaseoDaemon(
terminalManager,
getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
scheduleService,
providerRegistry,
providerSnapshotManager,
github,
workspaceGitService,
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
@@ -927,9 +924,6 @@ export async function createPaseoDaemon(
{
finalTimeoutMs: config.dictationFinalTimeoutMs,
},
config.agentProviderSettings,
config.providerOverrides,
config.isDev === true,
daemonVersion,
(intent) => {
try {
@@ -953,6 +947,7 @@ export async function createPaseoDaemon(
workspaceGitService,
github,
config.pushNotificationSender,
providerSnapshotManager,
{
listen: formatListenTarget(boundListenTarget ?? listenTarget),
relay: {
@@ -1021,10 +1016,7 @@ export async function createPaseoDaemon(
await agentManager.flush().catch(() => undefined);
detachAgentStoragePersistence();
await agentStorage.flush().catch(() => undefined);
await shutdownProviders(logger, {
runtimeSettings: config.agentProviderSettings,
providerOverrides: config.providerOverrides,
});
await providerSnapshotManager.shutdown();
terminalManager.killAll();
speechService.stop();
await scheduleService.stop().catch(() => undefined);

View File

@@ -5,7 +5,6 @@ import type {
AgentSessionConfig,
} from "./agent/agent-sdk-types.js";
import type { AgentStorage, StoredAgentRecord } from "./agent/agent-storage.js";
import { buildProviderRegistry } from "./agent/provider-registry.js";
interface LoggerLike {
child(bindings: Record<string, unknown>): LoggerLike;
@@ -24,18 +23,6 @@ interface BuildSessionConfigOptions {
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
* 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(
registeredProviders: RegisteredProviders,
registeredProviders: Iterable<AgentProvider>,
handle: StoredAgentRecord["persistence"],
): AgentPersistenceHandle | null {
if (!handle) {
return null;
}
const provider = handle.provider;
if (!hasRegisteredProvider(registeredProviders, provider)) {
if (!new Set(registeredProviders).has(provider)) {
return null;
}
if (!handle.sessionId) {

View File

@@ -13,15 +13,7 @@ import {
} from "@getpaseo/protocol/binary-frames/index";
import { Session } from "./session.js";
import { StructuredAgentFallbackError } from "./agent/agent-response-loop.js";
import type {
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 { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
import type { SessionOptions } from "./session.js";
import type {
SpeechToTextProvider,
@@ -83,7 +75,6 @@ interface SessionHandlerInternals {
handleStashPopRequest(params: unknown): Promise<unknown>;
createPaseoWorktree(params: unknown): Promise<unknown>;
handleStartWorkspaceScriptRequest(params: unknown): Promise<unknown>;
getProviderRegistry(): unknown;
sttManager: {
transcribe(audio: Buffer, format: string): Promise<unknown>;
};
@@ -143,46 +134,6 @@ interface Deferred<T> {
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> {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
@@ -357,7 +308,8 @@ function createSessionForTest(options: SessionForTestOptions = {}): Session {
stt: options.stt ?? null,
tts: null,
terminalManager: options.terminalManager ?? null,
providerSnapshotManager: options.providerSnapshotManager,
providerSnapshotManager:
options.providerSnapshotManager ?? createProviderSnapshotManagerStub().manager,
scriptRouteStore: options.scriptRouteStore,
scriptRuntimeStore: options.scriptRuntimeStore,
getDaemonTcpPort: options.getDaemonTcpPort,
@@ -989,24 +941,10 @@ describe("session provider refresh cwd routing", () => {
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 workspaceCwd = resolvePath("/tmp/session-provider-snapshot");
const fetchModels = vi.fn(async (options: ListModelsOptions) => [
{
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 { manager: providerSnapshotManager, getSnapshot } = createProviderSnapshotManagerStub();
const session = createSessionForTest({ messages, providerSnapshotManager });
await session.handleMessage({
@@ -1015,27 +953,24 @@ describe("session provider refresh cwd routing", () => {
requestId: "snapshot-workspace",
});
await vi.waitFor(() => {
expect(fetchModels).toHaveBeenCalledWith({
cwd: workspaceCwd,
force: false,
});
});
providerSnapshotManager.destroy();
expect(getSnapshot).toHaveBeenCalledWith(workspaceCwd);
});
test("normalizes legacy model and mode list requests without cwd to home", async () => {
const messages: unknown[] = [];
const session = createSessionForTest({ messages });
const fetchModels = vi.fn(async () => []);
const fetchModes = vi.fn(async () => []);
asSessionInternals(session).getProviderRegistry = () => ({
codex: createTestProviderDefinition({
fetchModels,
fetchModes,
}),
});
const {
manager: providerSnapshotManager,
getSnapshot,
warmUpSnapshotForCwd,
} = createProviderSnapshotManagerStub();
getSnapshot.mockReturnValue([
{
provider: "codex",
status: "loading",
enabled: true,
},
]);
const session = createSessionForTest({ messages, providerSnapshotManager });
await session.handleMessage({
type: "list_provider_models_request",
@@ -1048,8 +983,11 @@ describe("session provider refresh cwd routing", () => {
requestId: "modes-home",
});
expect(fetchModels).toHaveBeenCalledWith({ cwd: homedir(), force: false });
expect(fetchModes).toHaveBeenCalledWith({ cwd: homedir(), force: false });
expect(getSnapshot).toHaveBeenCalledWith(homedir());
expect(warmUpSnapshotForCwd).toHaveBeenCalledWith({
cwd: homedir(),
providers: ["codex"],
});
});
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 session = createSessionForTest({ messages });
const fetchModels = vi.fn(async () => [
const warmupDeferred = deferred<void>();
const {
manager: providerSnapshotManager,
getSnapshot,
warmUpSnapshotForCwd,
} = createProviderSnapshotManagerStub();
getSnapshot.mockReturnValueOnce([
{
provider: "codex" as const,
id: "should-not-fetch",
label: "Should not fetch",
provider: "codex",
status: "loading",
enabled: true,
},
]);
const fetchModes = vi.fn(async () => [
getSnapshot.mockReturnValue([
{
id: "should-not-fetch",
label: "Should not fetch",
provider: "codex",
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 = () => ({
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" }),
);
warmUpSnapshotForCwd.mockReturnValue(warmupDeferred.promise);
const session = createSessionForTest({ messages, providerSnapshotManager });
providerSnapshotManager.getSnapshot();
await vi.waitFor(() => {
expect(fetchModels).toHaveBeenCalledTimes(1);
});
const responsePromise = session.handleMessage({
type: "list_provider_models_request",
provider: "codex",
requestId: "models-loading-home",
});
await Promise.resolve();
expect(fetchModels).toHaveBeenCalledTimes(1);
expect(fetchModels).toHaveBeenCalledWith({ cwd: homedir(), force: false });
expect(fetchModels).not.toHaveBeenCalledWith({ cwd: homedir(), force: true });
models.resolve([
{
provider: "codex",
id: "gpt-5.4",
label: "GPT-5.4",
},
]);
expect(warmUpSnapshotForCwd).toHaveBeenCalledWith({
cwd: homedir(),
providers: ["codex"],
});
warmupDeferred.resolve();
await responsePromise;
expect(fetchModels).toHaveBeenCalledTimes(1);
expect(fetchModels).not.toHaveBeenCalledWith({ cwd: homedir(), force: true });
expect(messages).toContainEqual({
type: "list_provider_models_response",
payload: {
@@ -1248,12 +1105,10 @@ describe("session provider refresh cwd routing", () => {
},
],
error: null,
fetchedAt: expect.any(String),
fetchedAt: "2026-05-28T00:00:00.000Z",
requestId: "models-loading-home",
},
});
providerSnapshotManager.destroy();
});
});

View File

@@ -85,7 +85,6 @@ import type { ScriptHealthState } from "./script-health-monitor.js";
import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-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 { getAgentStatusPriority } from "@getpaseo/protocol/agent-state-bucket";
import type {
@@ -94,11 +93,6 @@ import type {
WorkspaceGitSnapshotOptions,
} 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 { ProviderSnapshotManager, resolveSnapshotCwd } from "./agent/provider-snapshot-manager.js";
import type {
@@ -587,7 +581,7 @@ export interface SessionOptions {
sttLanguage?: string;
tts: Resolvable<TextToSpeechProvider | null>;
terminalManager: TerminalManager | null;
providerSnapshotManager?: ProviderSnapshotManager;
providerSnapshotManager: ProviderSnapshotManager;
scriptRouteStore?: ScriptRouteStore;
scriptRuntimeStore?: WorkspaceScriptRuntimeStore;
workspaceSetupSnapshots?: Map<string, WorkspaceSetupSnapshot>;
@@ -614,9 +608,6 @@ export interface SessionOptions {
sttLanguage?: string;
getSpeechReadiness?: () => SpeechReadinessSnapshot;
};
agentProviderRuntimeSettings?: AgentProviderRuntimeSettingsMap;
providerOverrides?: Record<string, ProviderOverride>;
isDev?: boolean;
serverId?: string;
daemonVersion?: string;
daemonRuntimeConfig?: {
@@ -794,7 +785,7 @@ export class Session {
} | null = null;
private readonly MOBILE_BACKGROUND_STREAM_GRACE_MS = 60_000;
private readonly terminalManager: TerminalManager | null;
private readonly providerSnapshotManager: ProviderSnapshotManager | null;
private readonly providerSnapshotManager: ProviderSnapshotManager;
private unsubscribeProviderSnapshotEvents: (() => void) | null = null;
private readonly scriptRouteStore: ScriptRouteStore | null;
private readonly scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
@@ -836,9 +827,6 @@ export class Session {
private unregisterVoiceCallerContext?: (agentId: string) => void;
private getSpeechReadiness?: () => SpeechReadinessSnapshot;
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 daemonVersion: string | undefined;
private readonly daemonRuntimeConfig: SessionOptions["daemonRuntimeConfig"];
@@ -885,9 +873,6 @@ export class Session {
voice,
voiceBridge,
dictation,
agentProviderRuntimeSettings,
providerOverrides,
isDev,
serverId,
daemonVersion,
daemonRuntimeConfig,
@@ -957,7 +942,7 @@ export class Session {
this.terminalController.killTerminalsUnderPath(rootPath),
logger: this.sessionLogger,
});
this.providerSnapshotManager = providerSnapshotManager ?? null;
this.providerSnapshotManager = providerSnapshotManager;
this.scriptRouteStore = scriptRouteStore ?? null;
this.scriptRuntimeStore = scriptRuntimeStore ?? null;
this.workspaceSetupSnapshots = workspaceSetupSnapshots ?? new Map();
@@ -968,9 +953,6 @@ export class Session {
this.sttLanguage = sttLanguage ?? "en";
this.subscribeToOptionalManagers();
this.bindVoiceBridges({ voice, voiceBridge, dictation });
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
this.providerOverrides = providerOverrides;
this.isDev = isDev === true;
this.serverId = serverId;
this.daemonVersion = daemonVersion;
this.daemonRuntimeConfig = daemonRuntimeConfig;
@@ -1219,27 +1201,25 @@ export class Session {
*/
private subscribeToOptionalManagers(): void {
this.terminalController.start();
if (this.providerSnapshotManager) {
const handleProviderSnapshotChange = (entries: ProviderSnapshotEntry[], cwd: string) => {
// COMPAT(providersSnapshot): keep provider visibility gating for older clients.
const visibleEntries = entries.filter((entry) =>
this.isProviderVisibleToClient(entry.provider),
);
const snapshotCwd = cwd === resolveSnapshotCwd() ? undefined : cwd;
this.emit({
type: "providers_snapshot_update",
payload: {
...(snapshotCwd ? { cwd: snapshotCwd } : {}),
entries: this.downgradeEntryModesForClient(visibleEntries),
generatedAt: new Date().toISOString(),
},
});
};
this.providerSnapshotManager.on("change", handleProviderSnapshotChange);
this.unsubscribeProviderSnapshotEvents = () => {
this.providerSnapshotManager?.off("change", handleProviderSnapshotChange);
};
}
const handleProviderSnapshotChange = (entries: ProviderSnapshotEntry[], cwd: string) => {
// COMPAT(providersSnapshot): keep provider visibility gating for older clients.
const visibleEntries = entries.filter((entry) =>
this.isProviderVisibleToClient(entry.provider),
);
const snapshotCwd = cwd === resolveSnapshotCwd() ? undefined : cwd;
this.emit({
type: "providers_snapshot_update",
payload: {
...(snapshotCwd ? { cwd: snapshotCwd } : {}),
entries: this.downgradeEntryModesForClient(visibleEntries),
generatedAt: new Date().toISOString(),
},
});
};
this.providerSnapshotManager.on("change", handleProviderSnapshotChange);
this.unsubscribeProviderSnapshotEvents = () => {
this.providerSnapshotManager.off("change", handleProviderSnapshotChange);
};
}
private bindVoiceBridges(params: {
@@ -1419,25 +1399,9 @@ export class Session {
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(
record: StoredAgentRecord,
registeredProviderIds = this.getRegisteredProviderIds(),
registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds(),
): AgentSnapshotPayload {
return buildStoredAgentPayload(record, registeredProviderIds);
}
@@ -3122,6 +3086,7 @@ export class Session {
logger: this.sessionLogger,
paseoHome: this.paseoHome,
workspaceGitService: this.workspaceGitService,
providerSnapshotManager: this.providerSnapshotManager,
},
{
kind: "session",
@@ -3378,11 +3343,11 @@ export class Session {
if (!record) {
throw new Error(`Agent not found: ${agentId}`);
}
const providerRegistry = this.getProviderRegistry();
if (!isStoredAgentProviderAvailable(record, Object.keys(providerRegistry))) {
const registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds();
if (!isStoredAgentProviderAvailable(record, registeredProviderIds)) {
throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`);
}
const handle = toAgentPersistenceHandle(providerRegistry, record.persistence);
const handle = toAgentPersistenceHandle(registeredProviderIds, record.persistence);
if (!handle) {
throw new Error(`Agent ${agentId} cannot be refreshed because it lacks persistence`);
}
@@ -3601,47 +3566,6 @@ export class Session {
): Promise<void> {
const cwd = resolveSnapshotCwd(msg.cwd ? expandTilde(msg.cwd) : undefined);
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);
@@ -3698,53 +3622,14 @@ export class Session {
): Promise<void> {
const fetchedAt = new Date().toISOString();
const cwd = resolveSnapshotCwd(msg.cwd ? expandTilde(msg.cwd) : undefined);
const manager = this.providerSnapshotManager;
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`;
const entry = await this.getProviderSnapshotEntryForRead(cwd, msg.provider);
if (!entry) {
this.emit({
type: "list_provider_modes_response",
payload: {
provider: msg.provider,
error: errorMessage,
error: `Unknown provider: ${msg.provider}`,
fetchedAt,
requestId: msg.requestId,
},
@@ -3752,42 +3637,39 @@ export class Session {
return;
}
try {
const definition = this.getProviderRegistry()[msg.provider];
if (!definition.enabled) {
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.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`;
this.emit({
type: "list_provider_modes_response",
payload: {
provider: msg.provider,
error: errorMessage,
fetchedAt,
requestId: msg.requestId,
},
});
}
private async getProviderSnapshotEntryForRead(
@@ -3795,10 +3677,6 @@ export class Session {
provider: AgentProvider,
): Promise<ProviderSnapshotEntry | undefined> {
const manager = this.providerSnapshotManager;
if (!manager) {
return undefined;
}
const findEntry = () =>
manager.getSnapshot(cwd).find((candidate) => candidate.provider === provider);
@@ -3983,10 +3861,8 @@ export class Session {
): Promise<void> {
// COMPAT(providersSnapshot): keep legacy provider-list RPCs alongside snapshot flow.
const entries = this.providerSnapshotManager
? this.providerSnapshotManager
.getSnapshot(msg.cwd ? expandTilde(msg.cwd) : undefined)
.filter((entry) => this.isProviderVisibleToClient(entry.provider))
: [];
.getSnapshot(msg.cwd ? expandTilde(msg.cwd) : undefined)
.filter((entry) => this.isProviderVisibleToClient(entry.provider));
this.emit({
type: "get_providers_snapshot_response",
@@ -4002,12 +3878,12 @@ export class Session {
msg: Extract<SessionInboundMessage, { type: "refresh_providers_snapshot_request" }>,
): Promise<void> {
if (msg.cwd) {
await this.providerSnapshotManager?.refreshSnapshotForCwd({
await this.providerSnapshotManager.refreshSnapshotForCwd({
cwd: expandTilde(msg.cwd),
providers: msg.providers,
});
} else {
await this.providerSnapshotManager?.refreshSettingsSnapshot({
await this.providerSnapshotManager.refreshSettingsSnapshot({
providers: msg.providers,
});
}
@@ -4024,10 +3900,7 @@ export class Session {
msg: Extract<SessionInboundMessage, { type: "provider_diagnostic_request" }>,
): Promise<void> {
try {
const client = this.getProviderRegistry()[msg.provider].createClient(this.sessionLogger);
const diagnostic = client.getDiagnostic
? (await client.getDiagnostic()).diagnostic
: "No diagnostic available for this provider.";
const { diagnostic } = await this.providerSnapshotManager.getProviderDiagnostic(msg.provider);
this.emit({
type: "provider_diagnostic_response",
payload: {
@@ -6018,7 +5891,7 @@ export class Session {
// (excluding internal agents which are for ephemeral system tasks)
const registryRecords = await this.agentStorage.list();
const liveIds = new Set(agentSnapshots.map((a) => a.id));
const registeredProviderIds = this.getRegisteredProviderIds();
const registeredProviderIds = this.providerSnapshotManager.listRegisteredProviderIds();
const persistedAgents = registryRecords
.filter((record) => !liveIds.has(record.id) && !record.internal)
.filter(
@@ -6984,7 +6857,7 @@ export class Session {
request,
agentManager: this.agentManager,
agentStorage: this.agentStorage,
providerRegistry: this.getProviderRegistry(),
providerSnapshotManager: this.providerSnapshotManager,
});
this.emit({
type: "fetch_recent_provider_sessions_response",

View File

@@ -5,6 +5,7 @@ import { createBranchChangeRouteHandler } from "./script-route-branch-handler.js
import { ScriptRouteStore } from "./script-proxy.js";
import { Session, type SessionOptions } from "./session.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 type {
WorkspaceGitListener,
@@ -231,6 +232,7 @@ function createSessionForWorkspaceGitWatchTests(options?: {
mcpBaseUrl: null,
stt: null,
tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null,
scriptRouteStore: options?.scriptRouteStore,
scriptRuntimeStore: options?.scriptRuntimeStore,

View File

@@ -12,6 +12,7 @@ import { Session, type SessionOptions } from "./session.js";
import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
import { createNoopWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js";
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
import {
createPersistedProjectRecord,
createPersistedWorkspaceRecord,
@@ -162,6 +163,7 @@ function createHarness(input: {
mcpBaseUrl: null,
stt: null,
tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null,
});

View File

@@ -38,6 +38,7 @@ import {
asDaemonConfigStore,
asTerminalManager,
asSessionInternals,
createProviderSnapshotManagerStub,
isSessionOutboundMessage,
filterByType,
findByType,
@@ -510,6 +511,7 @@ function createSessionForWorkspaceTests(
mcpBaseUrl: null,
stt: null,
tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null,
}),
);
@@ -619,6 +621,7 @@ test("create_agent_request keeps requested child cwd when grouped under an exist
mcpBaseUrl: null,
stt: null,
tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null,
}),
);
@@ -1000,6 +1003,7 @@ test("archive emits an authoritative agent_update upsert for subscribed clients"
mcpBaseUrl: null,
stt: null,
tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null,
}),
);
@@ -1165,6 +1169,7 @@ test("close_items_request archives agents and kills terminals in one batch", asy
mcpBaseUrl: null,
stt: null,
tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: asTerminalManager({
killTerminal,
subscribeTerminalsChanged: () => () => {},
@@ -1353,6 +1358,7 @@ test("close_items_request archives stored agents that are not currently loaded",
mcpBaseUrl: null,
stt: null,
tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null,
}),
);
@@ -1502,6 +1508,7 @@ test("close_items_request continues after an archive failure", async () => {
mcpBaseUrl: null,
stt: null,
tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: asTerminalManager({
killTerminal: killTerminalBestEffort,
subscribeTerminalsChanged: () => () => {},
@@ -2368,6 +2375,7 @@ test("workspace update stream keeps persisted workspace visible after agents sto
mcpBaseUrl: null,
stt: null,
tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null,
}),
);

View File

@@ -7,6 +7,7 @@ import { Session } from "./session.js";
import type { SessionOptions } from "./session.js";
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js";
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
interface SessionInternals {
archiveAgentForClose(agentId: string): Promise<{ archivedAt: string }>;
@@ -135,6 +136,7 @@ describe("snapshot mutation ownership boundary", () => {
},
stt: null,
tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null,
}),
);

View File

@@ -1,6 +1,18 @@
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 type { SessionOptions } from "../session.js";
import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
@@ -130,28 +142,88 @@ export function findByType<T extends SessionOutboundMessage["type"]>(
// ---------------------------------------------------------------------------
export interface ProviderSnapshotManagerSpies {
getSnapshot: ReturnType<typeof vi.fn<[], ProviderSnapshotEntry[]>>;
refreshSnapshotForCwd: ReturnType<typeof vi.fn<[], Promise<void>>>;
refreshSettingsSnapshot: ReturnType<typeof vi.fn<[], Promise<void>>>;
warmUpSnapshotForCwd: ReturnType<typeof vi.fn<[], Promise<void>>>;
getSnapshot: ReturnType<typeof vi.fn<[cwd?: string], ProviderSnapshotEntry[]>>;
refreshSnapshotForCwd: ReturnType<typeof vi.fn<[unknown], Promise<void>>>;
refreshSettingsSnapshot: ReturnType<typeof vi.fn<[unknown], 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(): {
manager: ProviderSnapshotManager;
} & ProviderSnapshotManagerSpies {
const getSnapshot = vi.fn<[], ProviderSnapshotEntry[]>(() => []);
const refreshSnapshotForCwd = vi.fn<[], Promise<void>>(async () => {});
const refreshSettingsSnapshot = vi.fn<[], Promise<void>>(async () => {});
const warmUpSnapshotForCwd = vi.fn<[], Promise<void>>(async () => {});
const getSnapshot = vi.fn<[cwd?: string], ProviderSnapshotEntry[]>(() => []);
const refreshSnapshotForCwd = vi.fn<[unknown], Promise<void>>(async () => {});
const refreshSettingsSnapshot = vi.fn<[unknown], 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 off = vi.fn();
const destroy = vi.fn<[], void>();
const stub = {
getSnapshot,
refreshSnapshotForCwd,
refreshSettingsSnapshot,
warmUpSnapshotForCwd,
listRegisteredProviderIds,
hasProvider,
getProviderLabel,
getAgentManagerProviderState,
listProviders,
getProvider,
listModels,
listModes,
resolveCreateConfig,
resolveDefaultModel,
getProviderDiagnostic,
applyMutableProviderConfig,
on,
off,
destroy,
};
on.mockImplementation(() => stub);
off.mockImplementation(() => stub);
@@ -162,5 +234,18 @@ export function createProviderSnapshotManagerStub(): {
refreshSnapshotForCwd,
refreshSettingsSnapshot,
warmUpSnapshotForCwd,
listRegisteredProviderIds,
hasProvider,
getProviderLabel,
getAgentManagerProviderState,
listProviders,
getProvider,
listModels,
listModes,
resolveCreateConfig,
resolveDefaultModel,
getProviderDiagnostic,
applyMutableProviderConfig,
destroy,
};
}

View File

@@ -10,6 +10,7 @@ import type { LoopService } from "./loop-service.js";
import type { ScheduleService } from "./schedule/service.js";
import type { CheckoutDiffManager } from "./checkout-diff-manager.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";
const wsModuleMock = vi.hoisted(() => {
@@ -108,9 +109,6 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
undefined,
undefined,
undefined,
undefined,
undefined,
false,
"1.2.3-test",
undefined,
undefined,
@@ -138,6 +136,7 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
undefined,
undefined,
pushNotifications,
createProviderSnapshotManagerStub().manager,
);
return { server, agentManager, pushNotifications };

View File

@@ -10,6 +10,7 @@ import type { LoopService } from "./loop-service.js";
import type { ScheduleService } from "./schedule/service.js";
import type { CheckoutDiffManager } from "./checkout-diff-manager.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js";
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
import {
asUint8Array,
decodeTerminalStreamFrame,
@@ -230,9 +231,6 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
: undefined,
undefined,
undefined,
undefined,
undefined,
false,
TEST_DAEMON_VERSION,
undefined,
undefined,
@@ -251,6 +249,16 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
})),
dispose: vi.fn(),
}),
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
createProviderSnapshotManagerStub().manager,
);
}

View File

@@ -13,7 +13,6 @@ import type { LoopService } from "./loop-service.js";
import type { ScheduleService } from "./schedule/service.js";
import type { CheckoutDiffManager, CheckoutDiffMetrics } from "./checkout-diff-manager.js";
import type { DaemonConfigStore, MutableDaemonConfig } from "./daemon-config-store.js";
import { applyMutableProviderConfigToOverrides } from "./daemon-config-store.js";
import {
type ServerInfoStatusPayload,
type WorkspaceSetupSnapshot,
@@ -30,12 +29,7 @@ import type { HostnamesConfig } from "./hostnames.js";
import { isHostnameAllowed } from "./hostnames.js";
import { Session, type SessionLifecycleIntent, type SessionRuntimeMetrics } from "./session.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 { buildProviderRegistry, createClientsFromRegistry } from "./agent/provider-registry.js";
import type { WorkspaceGitRuntimeSnapshot, WorkspaceGitService } from "./workspace-git-service.js";
import { buildWorkspaceGitMetadataFromSnapshot } from "./workspace-git-metadata.js";
import { PushTokenStore } from "./push/token-store.js";
@@ -373,9 +367,6 @@ export class VoiceAssistantWebSocketServer {
private readonly voiceSpeakHandlers = new Map<string, VoiceSpeakHandler>();
private readonly voiceCallerContexts = new Map<string, VoiceCallerContext>();
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 onLifecycleIntent!: ((intent: SessionLifecycleIntent) => void) | null;
private onBranchChanged!:
@@ -404,9 +395,6 @@ export class VoiceAssistantWebSocketServer {
dictation?: {
finalTimeoutMs?: number;
},
agentProviderRuntimeSettings?: AgentProviderRuntimeSettingsMap,
providerOverrides?: Record<string, ProviderOverride>,
isDev?: boolean,
daemonVersion?: string,
onLifecycleIntent?: (intent: SessionLifecycleIntent) => void,
projectRegistry?: ProjectRegistry,
@@ -428,6 +416,7 @@ export class VoiceAssistantWebSocketServer {
workspaceGitService?: WorkspaceGitService,
github?: GitHubService,
pushNotificationSender?: PushNotificationSender,
providerSnapshotManager?: ProviderSnapshotManager,
daemonRuntimeConfig?: {
listen: string | null;
relay: {
@@ -470,9 +459,6 @@ export class VoiceAssistantWebSocketServer {
speech,
terminalManager,
dictation,
agentProviderRuntimeSettings,
providerOverrides,
isDev,
onLifecycleIntent,
scriptRouteStore,
scriptRuntimeStore,
@@ -481,15 +467,10 @@ export class VoiceAssistantWebSocketServer {
getDaemonTcpHost,
resolveScriptHealth,
});
const providerSnapshotLogger = this.logger.child({ module: "provider-snapshot-manager" });
this.providerSnapshotManager = new ProviderSnapshotManager(
buildProviderRegistry(providerSnapshotLogger, {
runtimeSettings: this.agentProviderRuntimeSettings,
providerOverrides: this.providerOverrides,
isDev: this.isDev,
}),
providerSnapshotLogger,
);
if (!providerSnapshotManager) {
throw new Error("providerSnapshotManager is required");
}
this.providerSnapshotManager = providerSnapshotManager;
this.serverCapabilities = buildServerCapabilities({
readiness: this.speech?.getReadiness() ?? null,
});
@@ -498,18 +479,10 @@ export class VoiceAssistantWebSocketServer {
this.publishSpeechReadiness(snapshot);
}) ?? null;
this.unsubscribeDaemonConfigChange = this.daemonConfigStore.onChange((config) => {
this.providerOverrides = applyMutableProviderConfigToOverrides(
this.providerOverrides,
const nextAgentManagerState = this.providerSnapshotManager.applyMutableProviderConfig(
config.providers,
);
const registry = buildProviderRegistry(providerSnapshotLogger, {
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.agentManager.updateProviderRegistry(nextAgentManagerState);
this.broadcastDaemonConfigChanged(config);
});
@@ -534,9 +507,6 @@ export class VoiceAssistantWebSocketServer {
speech: SpeechService | null | undefined;
terminalManager: TerminalManager | null | undefined;
dictation: { finalTimeoutMs?: number } | undefined;
agentProviderRuntimeSettings: AgentProviderRuntimeSettingsMap | undefined;
providerOverrides: Record<string, ProviderOverride> | undefined;
isDev: boolean | undefined;
onLifecycleIntent: ((intent: SessionLifecycleIntent) => void) | undefined;
scriptRouteStore: ScriptRouteStore | null | undefined;
scriptRuntimeStore: WorkspaceScriptRuntimeStore | null | undefined;
@@ -550,9 +520,6 @@ export class VoiceAssistantWebSocketServer {
this.speech = params.speech ?? null;
this.terminalManager = params.terminalManager ?? 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.scriptRouteStore = params.scriptRouteStore ?? null;
this.scriptRuntimeStore = params.scriptRuntimeStore ?? null;
@@ -941,9 +908,6 @@ export class VoiceAssistantWebSocketServer {
getSpeechReadiness: () => this.speech!.getReadiness(),
}
: undefined,
agentProviderRuntimeSettings: this.agentProviderRuntimeSettings,
providerOverrides: this.providerOverrides,
isDev: this.isDev,
serverId: this.serverId,
daemonVersion: this.daemonVersion,
daemonRuntimeConfig: this.daemonRuntimeConfig,

View File

@@ -11,6 +11,7 @@ import {
type SessionOutboundMessage,
} from "@getpaseo/protocol/messages";
import { Session, type SessionOptions } from "./session.js";
import { createProviderSnapshotManagerStub } from "./test-utils/session-stubs.js";
import type { AgentTimelineRow } from "./agent/agent-manager.js";
import { handleCreatePaseoWorktreeRequest } from "./worktree-session.js";
@@ -294,6 +295,7 @@ function createSessionForWireCompatTest(options?: {
new EmptyDaemonConfigStore() as unknown as SessionOptions["daemonConfigStore"],
stt: null,
tts: null,
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
terminalManager: null,
});