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