mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createTestLogger } from "../../test-utils/test-logger.js";
|
||||
import { resolveAgentModel } from "./model-resolver.js";
|
||||
|
||||
vi.mock("./provider-registry.js", () => ({
|
||||
buildProviderRegistry: vi.fn(),
|
||||
isProviderEnabled: vi.fn((definition: { enabled: boolean }) => definition.enabled),
|
||||
}));
|
||||
|
||||
import { buildProviderRegistry } from "./provider-registry.js";
|
||||
|
||||
const mockedBuildProviderRegistry = vi.mocked(buildProviderRegistry);
|
||||
const testLogger = createTestLogger();
|
||||
const testLoggerWarn = vi.spyOn(testLogger, "warn");
|
||||
type ProviderRegistryMock = ReturnType<typeof buildProviderRegistry>;
|
||||
|
||||
function makeMockRegistry(
|
||||
entries: Record<string, { enabled: boolean; fetchModels: ReturnType<typeof vi.fn> }>,
|
||||
): ProviderRegistryMock {
|
||||
const registry: ProviderRegistryMock = Object.create(null);
|
||||
for (const [key, val] of Object.entries(entries)) {
|
||||
Reflect.set(registry, key, val);
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
|
||||
describe("resolveAgentModel", () => {
|
||||
beforeEach(() => {
|
||||
mockedBuildProviderRegistry.mockReset();
|
||||
testLoggerWarn.mockClear();
|
||||
});
|
||||
|
||||
it("returns the trimmed requested model when provided", async () => {
|
||||
mockedBuildProviderRegistry.mockReturnValue(
|
||||
makeMockRegistry({
|
||||
claude: { enabled: true, fetchModels: vi.fn() },
|
||||
codex: { enabled: true, fetchModels: vi.fn() },
|
||||
opencode: { enabled: true, fetchModels: vi.fn() },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await resolveAgentModel({
|
||||
provider: "codex",
|
||||
requestedModel: " gpt-5.1 ",
|
||||
cwd: "/tmp",
|
||||
logger: testLogger,
|
||||
});
|
||||
|
||||
expect(result).toBe("gpt-5.1");
|
||||
expect(mockedBuildProviderRegistry).toHaveBeenCalledWith(testLogger);
|
||||
});
|
||||
|
||||
it("uses the default model from the provider catalog when no model specified", async () => {
|
||||
const fetchModels = vi.fn().mockResolvedValue([
|
||||
{ id: "claude-3.5-haiku", isDefault: false },
|
||||
{ id: "claude-3.5-sonnet", isDefault: true },
|
||||
]);
|
||||
mockedBuildProviderRegistry.mockReturnValue(
|
||||
makeMockRegistry({
|
||||
claude: { enabled: true, fetchModels },
|
||||
codex: { enabled: true, fetchModels: vi.fn() },
|
||||
opencode: { enabled: true, fetchModels: vi.fn() },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await resolveAgentModel({
|
||||
provider: "claude",
|
||||
cwd: "~/repo",
|
||||
logger: testLogger,
|
||||
});
|
||||
|
||||
expect(result).toBe("claude-3.5-sonnet");
|
||||
expect(fetchModels).toHaveBeenCalledWith({
|
||||
cwd: expect.stringMatching(/repo$/),
|
||||
force: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the first model when none are flagged as default", async () => {
|
||||
const fetchModels = vi.fn().mockResolvedValue([
|
||||
{ id: "model-a", isDefault: false },
|
||||
{ id: "model-b", isDefault: false },
|
||||
]);
|
||||
mockedBuildProviderRegistry.mockReturnValue(
|
||||
makeMockRegistry({
|
||||
claude: { enabled: true, fetchModels: vi.fn() },
|
||||
codex: { enabled: true, fetchModels },
|
||||
opencode: { enabled: true, fetchModels: vi.fn() },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await resolveAgentModel({ provider: "codex", logger: testLogger });
|
||||
|
||||
expect(result).toBe("model-a");
|
||||
});
|
||||
|
||||
it("returns undefined when the catalog lookup fails", async () => {
|
||||
const fetchModels = vi.fn().mockRejectedValue(new Error("boom"));
|
||||
mockedBuildProviderRegistry.mockReturnValue(
|
||||
makeMockRegistry({
|
||||
claude: { enabled: true, fetchModels: vi.fn() },
|
||||
codex: { enabled: true, fetchModels },
|
||||
opencode: { enabled: true, fetchModels: vi.fn() },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await resolveAgentModel({ provider: "codex", logger: testLogger });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(testLoggerWarn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns undefined for a disabled provider without fetching default models", async () => {
|
||||
const fetchModels = vi.fn().mockResolvedValue([{ id: "model-a", isDefault: true }]);
|
||||
mockedBuildProviderRegistry.mockReturnValue(
|
||||
makeMockRegistry({
|
||||
claude: { enabled: true, fetchModels: vi.fn() },
|
||||
codex: { enabled: false, fetchModels },
|
||||
opencode: { enabled: true, fetchModels: vi.fn() },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await resolveAgentModel({ provider: "codex", logger: testLogger });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(fetchModels).not.toHaveBeenCalled();
|
||||
expect(testLoggerWarn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns undefined for a requested model from a disabled provider", async () => {
|
||||
const fetchModels = vi.fn();
|
||||
mockedBuildProviderRegistry.mockReturnValue(
|
||||
makeMockRegistry({
|
||||
claude: { enabled: true, fetchModels: vi.fn() },
|
||||
codex: { enabled: false, fetchModels },
|
||||
opencode: { enabled: true, fetchModels: vi.fn() },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await resolveAgentModel({
|
||||
provider: "codex",
|
||||
requestedModel: "gpt-5.1",
|
||||
logger: testLogger,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(fetchModels).not.toHaveBeenCalled();
|
||||
expect(testLoggerWarn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
import { buildProviderRegistry } from "./provider-registry.js";
|
||||
import { resolveSnapshotCwd } from "./provider-snapshot-manager.js";
|
||||
import type { AgentProvider } from "./agent-sdk-types.js";
|
||||
import { expandTilde } from "../../utils/path.js";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
interface ResolveAgentModelOptions {
|
||||
provider: AgentProvider;
|
||||
requestedModel?: string | null;
|
||||
cwd?: string;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export async function resolveAgentModel(
|
||||
options: ResolveAgentModelOptions,
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const providerRegistry = buildProviderRegistry(options.logger);
|
||||
const providerDefinition = providerRegistry[options.provider];
|
||||
if (!providerDefinition.enabled) {
|
||||
throw new Error(`Provider '${options.provider}' is disabled`);
|
||||
}
|
||||
|
||||
const trimmed = options.requestedModel?.trim();
|
||||
if (trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
const models = await providerDefinition.fetchModels({
|
||||
cwd: resolveSnapshotCwd(options.cwd ? expandTilde(options.cwd) : undefined),
|
||||
force: false,
|
||||
});
|
||||
const preferred = models.find((model) => model.isDefault) ?? models[0];
|
||||
return preferred?.id;
|
||||
} catch (error) {
|
||||
options.logger.warn(
|
||||
{ err: error, provider: options.provider },
|
||||
"Failed to resolve default model",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { Logger } from "pino";
|
||||
|
||||
import type {
|
||||
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");
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,20 +4,91 @@ import { resolve } from "node:path";
|
||||
|
||||
import type { Logger } from "pino";
|
||||
|
||||
import { 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,
|
||||
|
||||
@@ -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()],
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user