feat: improve provider settings and model selection

This commit is contained in:
Mohamed Boudra
2026-04-24 21:42:47 +07:00
parent d3f9655e33
commit 4b7199f53b
24 changed files with 1384 additions and 514 deletions
+3 -3
View File
@@ -61,9 +61,9 @@
"@getpaseo/highlight": "0.1.63-beta.1",
"@getpaseo/relay": "0.1.63-beta.1",
"@isaacs/ttlcache": "^2.1.4",
"@mariozechner/pi-agent-core": "^0.67.68",
"@mariozechner/pi-ai": "^0.67.68",
"@mariozechner/pi-coding-agent": "^0.67.68",
"@mariozechner/pi-agent-core": "^0.70.2",
"@mariozechner/pi-ai": "^0.70.2",
"@mariozechner/pi-coding-agent": "^0.70.2",
"@modelcontextprotocol/sdk": "^1.20.1",
"@opencode-ai/sdk": "1.2.6",
"@sctg/sentencepiece-js": "^1.1.0",
@@ -7,6 +7,7 @@ import {
buildConfigOverrides,
buildSessionConfig,
extractTimestamps,
isStoredAgentProviderAvailable,
toAgentPersistenceHandle,
} from "../persistence-hooks.js";
@@ -40,7 +41,11 @@ export async function ensureAgentLoaded(
}
const validProviders = deps.validProviders ?? deps.agentManager.getRegisteredProviderIds();
const handle = toAgentPersistenceHandle(deps.logger, validProviders, record.persistence);
if (!isStoredAgentProviderAvailable(record, validProviders)) {
throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`);
}
const handle = toAgentPersistenceHandle(validProviders, record.persistence);
let snapshot: ManagedAgent;
if (handle) {
@@ -54,7 +59,6 @@ export async function ensureAgentLoaded(
} else {
const config = buildSessionConfig(record, {
validProviders,
logger: deps.logger,
});
if (!config) {
throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`);
@@ -78,6 +78,11 @@ class TestAgentClient implements AgentClient {
id: "gpt-5.4-mini",
label: "GPT-5.4 Mini",
},
{
provider: "codex",
id: "gpt-5.2-codex",
label: "GPT-5.2 Codex",
},
];
}
@@ -787,6 +792,86 @@ test("createAgent fails when cwd does not exist", async () => {
).rejects.toThrow("Working directory does not exist");
});
test("createAgent reports configured providers when provider is unknown", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents");
const storage = new AgentStorage(storagePath, logger);
const manager = new AgentManager({
clients: {
codex: new TestAgentClient(),
},
registry: storage,
logger,
});
await expect(
manager.createAgent({
provider: "missing-provider",
cwd: workdir,
}),
).rejects.toThrow("Unknown provider 'missing-provider'. Configured providers: codex.");
});
test("createAgent reports available providers when selected provider is unavailable", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents");
const storage = new AgentStorage(storagePath, logger);
class UnavailableCodexClient extends TestAgentClient {
override async isAvailable(): Promise<boolean> {
return false;
}
}
const manager = new AgentManager({
clients: {
codex: new UnavailableCodexClient(),
claude: new TestAgentClient(),
},
registry: storage,
logger,
});
await expect(
manager.createAgent({
provider: "codex",
cwd: workdir,
}),
).rejects.toThrow(
"Provider 'codex' is not available. Available providers: claude. Use one of those providers, or install/configure 'codex'.",
);
});
test("createAgent passes explicit model strings through to the provider", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents");
const storage = new AgentStorage(storagePath, logger);
class CaptureModelClient extends TestAgentClient {
lastConfig: AgentSessionConfig | null = null;
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
this.lastConfig = config;
return new TestAgentSession(config);
}
}
const client = new CaptureModelClient();
const manager = new AgentManager({
clients: {
codex: client,
},
registry: storage,
logger,
});
await manager.createAgent({
provider: "codex",
cwd: workdir,
model: "not-a-real-model",
});
expect(client.lastConfig?.model).toBe("not-a-real-model");
});
test("resumeAgentFromPersistence keeps metadata config, applies overrides, and passes launch env", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-resume-"));
const storagePath = join(workdir, "agents");
@@ -61,6 +61,10 @@ interface TimeoutOptions {
onLateError?: (error: unknown) => void;
}
function formatProviderList(providers: readonly string[]): string {
return providers.length > 0 ? providers.join(", ") : "none";
}
export { AGENT_LIFECYCLE_STATUSES, type AgentLifecycleStatus };
export type {
AgentTimelineCursor,
@@ -716,13 +720,9 @@ export class AgentManager {
};
const normalizedConfig = await this.normalizeConfig(injectedConfig);
const launchContext = this.buildLaunchContext(resolvedAgentId);
const client = this.requireClient(normalizedConfig.provider);
const available = await client.isAvailable();
if (!available) {
throw new Error(
`Provider '${normalizedConfig.provider}' is not available. Please ensure the CLI is installed.`,
);
}
const client = await this.requireAvailableClient({
provider: normalizedConfig.provider,
});
const session = await client.createSession(normalizedConfig, launchContext);
return this.registerSession(session, normalizedConfig, resolvedAgentId, {
labels: options?.labels,
@@ -3079,6 +3079,37 @@ export class AgentManager {
};
}
private async requireAvailableClient(options: { provider: AgentProvider }): Promise<AgentClient> {
const client = this.clients.get(options.provider);
if (!client) {
const configuredProviders = Array.from(this.clients.keys());
throw new Error(
`Unknown provider '${options.provider}'. Configured providers: ${formatProviderList(
configuredProviders,
)}.`,
);
}
let unavailableReason: string | null = null;
try {
const available = await client.isAvailable();
if (available) {
return client;
}
} catch (error) {
unavailableReason = error instanceof Error ? error.message : String(error);
}
const availableProviders = (await this.listProviderAvailability())
.filter((entry) => entry.available)
.map((entry) => entry.provider);
const providerList = formatProviderList(availableProviders);
const reason = unavailableReason ? ` Reason: ${unavailableReason}.` : "";
throw new Error(
`Provider '${options.provider}' is not available.${reason} Available providers: ${providerList}. Use one of those providers, or install/configure '${options.provider}'.`,
);
}
private requireClient(provider: AgentProvider): AgentClient {
const client = this.clients.get(provider);
if (!client) {
@@ -7,16 +7,14 @@ import type {
AgentMode,
AgentPermissionRequest,
AgentPersistenceHandle,
AgentProvider,
AgentSessionConfig,
AgentRuntimeInfo,
AgentUsage,
} from "./agent-sdk-types.js";
import type { ManagedAgent } from "./agent-manager.js";
import type { JsonValue } from "../json-utils.js";
import type { Logger } from "pino";
import { buildProviderRegistry } from "./provider-registry.js";
import { coerceAgentProvider, toAgentPersistenceHandle } from "../persistence-hooks.js";
import { isStoredAgentProviderAvailable, toAgentPersistenceHandle } from "../persistence-hooks.js";
export type { ManagedAgent };
interface ProjectionOptions {
@@ -143,15 +141,11 @@ export function toAgentPayload(
return payload;
}
function buildStoredRuntimeInfo(
record: StoredAgentRecord,
providerRegistry: ReturnType<typeof buildProviderRegistry>,
logger: Logger,
): AgentRuntimeInfo | undefined {
function buildStoredRuntimeInfo(record: StoredAgentRecord): AgentRuntimeInfo | undefined {
if (!record.runtimeInfo) return undefined;
const ri = record.runtimeInfo;
const runtimeInfo: AgentRuntimeInfo = {
provider: coerceAgentProvider(logger, providerRegistry, ri.provider, record.id),
provider: ri.provider,
sessionId: ri.sessionId,
};
if (Object.prototype.hasOwnProperty.call(ri, "model")) {
@@ -169,10 +163,19 @@ function buildStoredRuntimeInfo(
return runtimeInfo;
}
function buildStoredPersistenceHandle(
record: StoredAgentRecord,
validProviders: Iterable<AgentProvider>,
): AgentPersistenceHandle | null {
if (!isStoredAgentProviderAvailable(record, validProviders)) {
return null;
}
return toAgentPersistenceHandle(validProviders, record.persistence);
}
export function buildStoredAgentPayload(
record: StoredAgentRecord,
providerRegistry: ReturnType<typeof buildProviderRegistry>,
logger: Logger,
validProviders: Iterable<AgentProvider>,
): AgentSnapshotPayload {
const defaultCapabilities = {
supportsStreaming: false,
@@ -187,12 +190,13 @@ export function buildStoredAgentPayload(
const updatedAt = new Date(resolveStoredAgentPayloadUpdatedAt(record));
const lastUserMessageAt = record.lastUserMessageAt ? new Date(record.lastUserMessageAt) : null;
const provider = coerceAgentProvider(logger, providerRegistry, record.provider, record.id);
const runtimeInfo = buildStoredRuntimeInfo(record, providerRegistry, logger);
const runtimeInfo = buildStoredRuntimeInfo(record);
const providerAvailable = isStoredAgentProviderAvailable(record, validProviders);
const persistence = buildStoredPersistenceHandle(record, validProviders);
return {
id: record.id,
provider,
provider: record.provider,
cwd: record.cwd,
model: record.config?.model ?? null,
thinkingOptionId: record.config?.thinkingOptionId ?? null,
@@ -209,13 +213,14 @@ export function buildStoredAgentPayload(
currentModeId: record.lastModeId ?? null,
availableModes: [],
pendingPermissions: [],
persistence: toAgentPersistenceHandle(logger, providerRegistry, record.persistence),
persistence,
title: record.title ?? record.config?.title ?? null,
requiresAttention: record.requiresAttention ?? false,
attentionReason: record.attentionReason ?? null,
attentionTimestamp: record.attentionTimestamp ?? null,
archivedAt: record.archivedAt ?? null,
labels: normalizeLabels(record.labels),
...(providerAvailable ? {} : { providerUnavailable: true }),
};
}
@@ -238,6 +243,7 @@ export function toAgentListItemPayload(agent: AgentSnapshotPayload): AgentListIt
attentionReason: agent.attentionReason ?? null,
attentionTimestamp: agent.attentionTimestamp ?? null,
labels: agent.labels,
...(agent.providerUnavailable ? { providerUnavailable: true } : {}),
};
}
@@ -141,13 +141,32 @@ function createTestDeps(): TestDeps {
}
function createProviderDefinition(overrides: Partial<ProviderDefinition>): ProviderDefinition {
const provider = (overrides.id ?? "claude") as AgentProvider;
return {
id: "claude",
id: provider,
label: "Claude",
description: "Test provider",
defaultModeId: "default",
modes: [],
createClient: vi.fn() as ProviderDefinition["createClient"],
createClient: vi.fn(() => ({
provider,
capabilities: {
supportsStreaming: false,
supportsSessionPersistence: false,
supportsDynamicModes: false,
supportsMcpServers: false,
supportsReasoningStream: false,
supportsToolInvocations: false,
},
createSession: async () => {
throw new Error("createSession is not used by this MCP provider test");
},
resumeSession: async () => {
throw new Error("resumeSession is not used by this MCP provider test");
},
listModels: vi.fn().mockResolvedValue([]),
isAvailable: vi.fn().mockResolvedValue(true),
})),
fetchModels: vi.fn().mockResolvedValue([]),
fetchModes: vi.fn().mockResolvedValue([]),
...overrides,
@@ -913,7 +932,7 @@ describe("create_agent MCP tool", () => {
describe("create_schedule MCP tool", () => {
const logger = createTestLogger();
it("preserves default new-agent schedule behavior without requiring provider", async () => {
it("requires provider for new-agent schedules", async () => {
const { agentManager, agentStorage } = createTestDeps();
const create = vi.fn(async (input: CreateScheduleInput) => createStoredSchedule(input));
const server = await createAgentMcpServer({
@@ -924,31 +943,14 @@ describe("create_schedule MCP tool", () => {
});
const tool = registeredTool(server, "create_schedule");
const response = await tool.callback({
prompt: "say hello",
every: "5m",
name: "Default schedule",
});
expect(create).toHaveBeenCalledWith(
expect.objectContaining({
await expect(
tool.callback({
prompt: "say hello",
target: {
type: "new-agent",
config: {
provider: "claude",
cwd: process.cwd(),
},
},
every: "5m",
name: "Default schedule",
}),
);
expect(response.structuredContent.target).toEqual({
type: "new-agent",
config: {
provider: "claude",
cwd: process.cwd(),
},
});
).rejects.toThrow("provider is required when target is new-agent");
expect(create).not.toHaveBeenCalled();
});
it("keeps create_schedule provider overrides compatible with provider and provider/model forms", async () => {
@@ -1035,11 +1037,13 @@ describe("provider listing MCP tool", () => {
{
id: "claude",
label: "Claude",
status: "available",
modes: [{ id: "default", label: "Default", description: "Built-in mode" }],
},
{
id: "zai",
label: "ZAI",
status: "available",
modes: [{ id: "default", label: "Default", description: "Custom mode" }],
},
],
+61 -27
View File
@@ -22,6 +22,7 @@ import {
import { curateAgentActivity } from "./activity-curator.js";
import type { AgentStorage } from "./agent-storage.js";
import { ensureAgentLoaded } from "./agent-loading.js";
import { isStoredAgentProviderAvailable } from "../persistence-hooks.js";
import {
appendTimelineItemIfAgentKnown,
emitLiveTimelineItemIfAgentKnown,
@@ -159,6 +160,15 @@ function resolveAgentListActivityTime(agent: AgentListItemPayload): number {
);
}
function resolveRegisteredProviderIds(
agentManager: AgentManager,
providerRegistry: Record<AgentProvider, ProviderDefinition> | null | undefined,
): AgentProvider[] {
return providerRegistry
? (Object.keys(providerRegistry) as AgentProvider[])
: agentManager.getRegisteredProviderIds();
}
function compareAgentListItems(a: AgentListItemPayload, b: AgentListItemPayload): number {
const attentionDelta =
Number(b.requiresAttention ?? false) - Number(a.requiresAttention ?? false);
@@ -289,13 +299,6 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
version: "2.0.0",
});
const requireProviderRegistry = (): Record<AgentProvider, ProviderDefinition> => {
if (!providerRegistry) {
throw new Error("Provider registry is required to load stored agent records");
}
return providerRegistry;
};
const resolveCallerAgent = () => {
if (!callerAgentId) {
return null;
@@ -389,6 +392,10 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
};
const resolveNewAgentScheduleTarget = (params?: { provider?: string; cwd?: string }) => {
if (!params?.provider?.trim()) {
throw new Error("provider is required when target is new-agent");
}
const callerAgent = resolveCallerAgent();
if (callerAgent) {
return {
@@ -399,7 +406,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
const resolvedProviderModel = resolveScheduleProviderAndModel({
provider: params?.provider,
defaultProvider: "claude",
defaultProvider: params.provider as AgentProvider,
});
return {
type: "new-agent" as const,
@@ -675,7 +682,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
{
title: "Create agent",
description:
"Create an agent tied to a working directory. Requires provider/model, for example codex/gpt-5.4. Optionally run an initial prompt immediately or create a git worktree for the agent.",
"Create an agent tied to a working directory. Requires provider/model, for example codex/gpt-5.4. Do not guess; call list_providers and list_models first if uncertain. Optionally run an initial prompt immediately or create a git worktree for the agent.",
inputSchema: createAgentInputSchema,
outputSchema: {
agentId: z.string(),
@@ -1052,8 +1059,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
const structuredSnapshot = buildStoredAgentPayload(
record,
requireProviderRegistry(),
childLogger,
resolveRegisteredProviderIds(agentManager, providerRegistry),
);
return {
content: [],
@@ -1100,10 +1106,15 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
);
const liveIds = new Set(liveSnapshots.map((snapshot) => snapshot.id));
const storedRecords = await agentStorage.list();
const registeredProviderIds = resolveRegisteredProviderIds(agentManager, providerRegistry);
const storedAgents = storedRecords
.filter((record) => !record.internal && !liveIds.has(record.id))
.filter((record) => includeArchived || !record.archivedAt)
.map((record) => buildStoredAgentPayload(record, requireProviderRegistry(), childLogger));
.filter(
(record) =>
includeArchived || isStoredAgentProviderAvailable(record, registeredProviderIds),
)
.map((record) => buildStoredAgentPayload(record, registeredProviderIds));
const agents = [...liveAgents, ...storedAgents]
.map(toAgentListItemPayload)
.filter((agent) => !requestedCwd || isSameOrDescendantPath(requestedCwd, agent.cwd))
@@ -1631,26 +1642,49 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
"list_providers",
{
title: "List providers",
description: "List available agent providers and their modes.",
description: "List configured agent providers, availability, and their modes.",
inputSchema: {},
outputSchema: {
providers: z.array(ProviderSummarySchema),
},
},
async () => ({
content: [],
structuredContent: ensureValidJson({
providers: Object.values(providerRegistry ?? {}).map((provider) => ({
id: provider.id,
label: provider.label,
modes: provider.modes.map((mode) => ({
id: mode.id,
label: mode.label,
...(mode.description ? { description: mode.description } : {}),
})),
})),
}),
}),
async () => {
const providers = await Promise.all(
Object.values(providerRegistry ?? {}).map(async (provider) => {
let status = "unknown";
let error: string | undefined;
try {
status = (await provider.createClient(childLogger).isAvailable())
? "available"
: "unavailable";
} catch (availabilityError) {
status = "unavailable";
error =
availabilityError instanceof Error
? availabilityError.message
: String(availabilityError);
}
const summary = {
id: provider.id,
label: provider.label,
status,
modes: provider.modes.map((mode) => ({
id: mode.id,
label: mode.label,
...(mode.description ? { description: mode.description } : {}),
})),
};
if (error) {
return Object.assign(summary, { error });
}
return summary;
}),
);
return {
content: [],
structuredContent: ensureValidJson({ providers }),
};
},
);
server.registerTool(
@@ -1047,6 +1047,49 @@ describe("ProviderSnapshotManager", () => {
manager.destroy();
});
test("replaceRegistry removes providers that were disabled at runtime", async () => {
const { registry } = createRegistry([
createMockProvider({
provider: "codex",
fetchModels: async () => [createModel("codex", "gpt-5.2")],
}),
createMockProvider({
provider: "claude",
fetchModels: async () => [createModel("claude", "sonnet")],
}),
]);
const manager = new ProviderSnapshotManager(registry, createTestLogger());
const listener = vi.fn<(entries: ProviderSnapshotEntry[], cwd: string) => void>();
manager.on("change", listener);
manager.getSnapshot(projectCwd);
await vi.waitFor(() => {
expect(getProviderEntry(manager.getSnapshot(projectCwd), "claude")?.status).toBe("ready");
expect(getProviderEntry(manager.getSnapshot(projectCwd), "codex")?.status).toBe("ready");
});
const { registry: nextRegistry } = createRegistry([
createMockProvider({
provider: "codex",
fetchModels: async () => [createModel("codex", "gpt-5.2")],
}),
]);
manager.replaceRegistry(nextRegistry);
expect(manager.getSnapshot(projectCwd).map((entry) => entry.provider)).toEqual(["codex"]);
expect(listener).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
provider: "codex",
}),
]),
homedir(),
);
manager.destroy();
});
test("snapshot includes user-defined providers from the registry", async () => {
const { registry } = createRegistry([
createMockProvider({ provider: "claude" }),
@@ -39,12 +39,14 @@ export class ProviderSnapshotManager {
private readonly ttlMs: number;
private readonly refreshTimeoutMs: number;
private readonly now: () => number;
private providerRegistry: Record<AgentProvider, ProviderDefinition>;
constructor(
private readonly providerRegistry: Record<AgentProvider, ProviderDefinition>,
providerRegistry: Record<AgentProvider, ProviderDefinition>,
private readonly logger: Logger,
options: ProviderSnapshotManagerOptions = {},
) {
this.providerRegistry = providerRegistry;
this.ttlMs = options.ttlMs ?? DEFAULT_SNAPSHOT_TTL_MS;
this.refreshTimeoutMs = options.refreshTimeoutMs ?? DEFAULT_REFRESH_TIMEOUT_MS;
this.now = options.now ?? Date.now;
@@ -144,6 +146,18 @@ export class ProviderSnapshotManager {
this.providerLoads.clear();
}
replaceRegistry(providerRegistry: Record<AgentProvider, ProviderDefinition>): void {
this.providerRegistry = providerRegistry;
for (const cwd of this.snapshots.keys()) {
this.providerLoads.delete(cwd);
this.lastCheckedAts.delete(cwd);
this.snapshots.set(cwd, this.createLoadingEntries());
this.emitChange(cwd);
void this.warmUp(cwd);
}
}
private createLoadingEntries(): Map<AgentProvider, ProviderSnapshotEntry> {
const entries = new Map<AgentProvider, ProviderSnapshotEntry>();
for (const provider of this.getProviderIds()) {
@@ -1,5 +1,5 @@
import { describe, expect, test, vi } from "vitest";
import type { PromptResponse, SessionUpdate } from "@agentclientprotocol/sdk";
import type { PromptResponse, SessionConfigOption, SessionUpdate } from "@agentclientprotocol/sdk";
import {
ACPAgentClient,
@@ -21,6 +21,18 @@ interface ACPSessionInternals {
translateSessionUpdate(update: SessionUpdate): unknown;
}
interface ACPModelSelectionInternals {
sessionId: string | null;
connection: {
setSessionConfigOption: (input: {
sessionId: string;
configId: string;
value: string;
}) => Promise<void>;
};
configOptions: SessionConfigOption[];
}
function createSession(): ACPAgentSession {
return new ACPAgentSession(
{
@@ -44,6 +56,32 @@ function createSession(): ACPAgentSession {
);
}
test("ACP setModel forwards model ids that are absent from the advertised catalog", async () => {
const session = createSession();
const setSessionConfigOption = vi.fn(async () => undefined);
const internals = session as unknown as ACPModelSelectionInternals;
internals.sessionId = "session-1";
internals.connection = { setSessionConfigOption };
internals.configOptions = [
{
id: "model-option",
name: "Model",
category: "model",
type: "select",
currentValue: "sonnet",
options: [{ value: "sonnet", name: "Sonnet" }],
},
];
await session.setModel("new-provider-model");
expect(setSessionConfigOption).toHaveBeenCalledWith({
sessionId: "session-1",
configId: "model-option",
value: "new-provider-model",
});
});
describe("createLoggedNdJsonStream", () => {
test("routes malformed ACP stdout through the provider logger instead of console.error", async () => {
const input = new TransformStream<Uint8Array, Uint8Array>();
@@ -71,6 +109,10 @@ describe("createLoggedNdJsonStream", () => {
expect(parsed.value).toEqual({ jsonrpc: "2.0", method: "ok", params: {} });
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
err: {
type: "SyntaxError",
message: "ACP stdout line was not valid JSON",
},
provider: "gemini",
}),
"ACP agent emitted non-JSON stdout; ignoring line",
@@ -82,6 +124,39 @@ describe("createLoggedNdJsonStream", () => {
reader.releaseLock();
consoleError.mockRestore();
});
test("does not log terminal control sequences from malformed ACP stdout", async () => {
const input = new TransformStream<Uint8Array, Uint8Array>();
const output = new TransformStream<Uint8Array, Uint8Array>();
const logger = {
warn: vi.fn(),
};
const stream = createLoggedNdJsonStream(output.writable, input.readable, {
logger: logger as unknown as ReturnType<typeof createTestLogger>,
provider: "gemini",
});
const reader = stream.readable.getReader();
const writer = input.writable.getWriter();
await writer.write(new TextEncoder().encode('\u001b[1G\u001b[0JEn\n{"ok":true}\n'));
const parsed = await reader.read();
expect(parsed.value).toEqual({ ok: true });
expect(JSON.stringify(logger.warn.mock.calls)).not.toContain("\u001b");
expect(JSON.stringify(logger.warn.mock.calls)).not.toContain("[1G");
expect(logger.warn.mock.calls[0]?.[0]).toEqual({
err: {
type: "SyntaxError",
message: "ACP stdout line was not valid JSON",
},
provider: "gemini",
});
await writer.close();
reader.releaseLock();
});
});
describe("mapACPUsage", () => {
@@ -113,6 +113,13 @@ const COPILOT_AUTOPILOT_MODE = "https://agentclientprotocol.com/protocol/session
// NO_BROWSER is honored by Gemini CLI; other ACP agents ignore it.
const PROBE_ENV: Record<string, string> = { NO_BROWSER: "true" };
function summarizeMalformedACPStdoutError(error: unknown): { type: string; message: string } {
return {
type: error instanceof Error ? error.name : typeof error,
message: "ACP stdout line was not valid JSON",
};
}
export function createLoggedNdJsonStream(
output: WritableStream<Uint8Array>,
input: ReadableStream<Uint8Array>,
@@ -151,7 +158,7 @@ export function createLoggedNdJsonStream(
} catch (error) {
options.logger.warn(
{
err: error,
err: summarizeMalformedACPStdoutError(error),
provider: options.provider,
},
"ACP agent emitted non-JSON stdout; ignoring line",
@@ -696,7 +703,6 @@ export class ACPAgentSession implements AgentSession, ACPClient {
private sessionId: string | null = null;
private currentMode: string | null = null;
private availableModes: AgentMode[];
private availableModels: AgentModelDefinition[] = [];
private currentModel: string | null = null;
private thinkingOptionId: string | null = null;
private currentTitle: string | null = null;
@@ -1065,11 +1071,6 @@ export class ACPAgentSession implements AgentSession, ACPClient {
return;
}
const modelExists = this.availableModels.some((model) => model.id === modelId);
if (!modelExists && this.availableModels.length > 0) {
throw new Error(`Unknown ${this.provider} model '${modelId}'`);
}
if ("unstable_setSessionModel" in this.connection) {
try {
await this.connection.unstable_setSessionModel({
@@ -1467,7 +1468,6 @@ export class ACPAgentSession implements AgentSession, ACPClient {
const modeInfo = deriveModesFromACP(this.defaultModes, transformed.modes, this.configOptions);
this.availableModes = modeInfo.modes;
this.availableModels = this.deriveAvailableModels(transformed.models);
this.currentMode = modeInfo.currentModeId ?? this.currentMode;
this.currentModel =
@@ -1598,24 +1598,12 @@ export class ACPAgentSession implements AgentSession, ACPClient {
this.configOptions = update.configOptions;
const modeInfo = deriveModesFromACP(this.defaultModes, null, this.configOptions);
this.availableModes = modeInfo.modes;
this.availableModels = this.deriveAvailableModels(null);
this.currentMode = modeInfo.currentModeId ?? this.currentMode;
this.currentModel = deriveCurrentConfigValue(this.configOptions, "model") ?? this.currentModel;
this.thinkingOptionId =
deriveCurrentConfigValue(this.configOptions, "thought_level") ?? this.thinkingOptionId;
}
private deriveAvailableModels(
models: SessionModelState | null | undefined,
): AgentModelDefinition[] {
const availableModels = deriveModelDefinitionsFromACP(
this.provider,
models,
this.configOptions,
);
return this.modelTransformer ? this.modelTransformer(availableModels) : availableModels;
}
private handleSessionInfoUpdate(update: SessionInfoUpdate): void {
if ("title" in update) {
this.currentTitle = update.title ?? null;
@@ -38,6 +38,7 @@ function createPiSession(prompt: () => Promise<void>): PiDirectSessionAdapter {
abort: vi.fn(),
dispose: vi.fn(),
getSessionStats: vi.fn(() => ({})),
setModel: vi.fn(),
setThinkingLevel: vi.fn(),
};
}
@@ -47,7 +48,13 @@ function createPiModel(provider: string, id: string): Model<Api> {
provider,
id,
name: id,
api: "openai-completions",
baseUrl: "https://example.invalid/v1",
reasoning: true,
input: ["text"],
cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192,
} as Model<Api>;
}
@@ -76,6 +83,37 @@ describe("PiDirectAgentSession", () => {
},
]);
});
test("setModel creates a minimal model for new ids under a known provider", async () => {
const sdkSession = createPiSession(async () => undefined);
const session = new PiDirectAgentSession(
sdkSession,
{
find: vi.fn(() => undefined),
getAll: vi.fn(() => [createPiModel("openrouter", "known-model")]),
},
{
provider: "pi",
cwd: "/tmp/paseo-pi-test",
},
);
await session.setModel("openrouter/blabal");
expect(sdkSession.setModel).toHaveBeenCalledWith({
id: "blabal",
name: "blabal",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://example.invalid/v1",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 16384,
compat: undefined,
});
});
});
describe("PiDirectAgentClient", () => {
@@ -787,7 +787,13 @@ function findModelInRegistry(
parsedReference: PiModelReference,
): Model<Api> | undefined {
if (parsedReference.provider) {
return registry.find(parsedReference.provider, parsedReference.id);
return (
registry.find(parsedReference.provider, parsedReference.id) ??
createProviderModelFallback(registry, {
provider: parsedReference.provider,
id: parsedReference.id,
})
);
}
return registry.getAll().find((entry) => {
@@ -798,6 +804,32 @@ function findModelInRegistry(
});
}
function createProviderModelFallback(
registry: PiDirectModelRegistry,
parsedReference: { provider: string; id: string },
): Model<Api> | undefined {
const providerDefault = registry
.getAll()
.find((model) => model.provider === parsedReference.provider);
if (!providerDefault) {
return undefined;
}
return {
id: parsedReference.id,
name: parsedReference.id,
api: providerDefault.api,
provider: parsedReference.provider,
baseUrl: providerDefault.baseUrl,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 16384,
compat: providerDefault.compat,
};
}
export class PiDirectAgentSession implements AgentSession {
readonly provider = PI_PROVIDER;
readonly capabilities = PI_CAPABILITIES;
+9
View File
@@ -225,6 +225,15 @@ export async function createPaseoDaemon(
config.paseoHome,
{
mcp: { injectIntoAgents: config.mcpInjectIntoAgents ?? true },
providers: Object.fromEntries(
Object.entries(config.providerOverrides ?? {}).map(([providerId, override]) => [
providerId,
{
...(override.enabled !== undefined ? { enabled: override.enabled } : {}),
...(override.additionalModels ? { additionalModels: override.additionalModels } : {}),
},
]),
),
},
logger,
);
@@ -0,0 +1,140 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import { DaemonConfigStore, applyMutableProviderConfigToOverrides } from "./daemon-config-store.js";
import { loadPersistedConfig } from "./persisted-config.js";
describe("applyMutableProviderConfigToOverrides", () => {
test("merges mutable provider fields onto provider overrides", () => {
expect(
applyMutableProviderConfigToOverrides(
{
gemini: {
extends: "acp",
label: "Gemini",
command: ["gemini", "--acp"],
},
},
{
gemini: { enabled: false },
claude: {
additionalModels: [
{
id: "claude-custom",
label: "claude-custom",
},
],
},
},
),
).toEqual({
gemini: {
extends: "acp",
label: "Gemini",
command: ["gemini", "--acp"],
enabled: false,
},
claude: {
additionalModels: [
{
id: "claude-custom",
label: "claude-custom",
},
],
},
});
});
});
describe("DaemonConfigStore", () => {
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs) {
rmSync(dir, { recursive: true, force: true });
}
});
test("patch persists provider enabled flags into config.json", () => {
const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-"));
tempDirs.push(paseoHome);
const initial = loadPersistedConfig(paseoHome);
initial.agents = {
providers: {
gemini: {
extends: "acp",
label: "Gemini",
command: ["gemini", "--acp"],
},
},
};
const configPath = path.join(paseoHome, "config.json");
// Reuse the validated serializer through the store path by seeding the file directly.
// This keeps the test focused on the merge behavior.
const seeded = JSON.stringify(initial, null, 2) + "\n";
writeFileSync(configPath, seeded);
const store = new DaemonConfigStore(
paseoHome,
{
mcp: { injectIntoAgents: false },
providers: {},
},
undefined,
);
store.patch({
providers: {
gemini: { enabled: false },
},
});
const persisted = loadPersistedConfig(paseoHome);
expect(persisted.agents?.providers?.gemini).toEqual({
extends: "acp",
label: "Gemini",
command: ["gemini", "--acp"],
enabled: false,
});
});
test("patch persists provider additional models into config.json", () => {
const paseoHome = mkdtempSync(path.join(tmpdir(), "paseo-daemon-config-store-"));
tempDirs.push(paseoHome);
const store = new DaemonConfigStore(
paseoHome,
{
mcp: { injectIntoAgents: false },
providers: {},
},
undefined,
);
store.patch({
providers: {
claude: {
additionalModels: [
{
id: "claude-custom",
label: "claude-custom",
},
],
},
},
});
const persisted = loadPersistedConfig(paseoHome);
expect(persisted.agents?.providers?.claude).toEqual({
additionalModels: [
{
id: "claude-custom",
label: "claude-custom",
},
],
});
});
});
@@ -9,6 +9,7 @@ export type { MutableDaemonConfig, MutableDaemonConfigPatch } from "../shared/me
type MutableDaemonConfig = import("../shared/messages.js").MutableDaemonConfig;
type MutableDaemonConfigPatch = import("../shared/messages.js").MutableDaemonConfigPatch;
type ProviderOverride = import("./agent/provider-launch-config.js").ProviderOverride;
interface LoggerLike {
child(bindings: Record<string, unknown>): LoggerLike;
@@ -57,6 +58,30 @@ function isEqualValue(a: unknown, b: unknown): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
export function applyMutableProviderConfigToOverrides(
baseOverrides: Record<string, ProviderOverride> | undefined,
mutableProviders: MutableDaemonConfig["providers"] | undefined,
): Record<string, ProviderOverride> | undefined {
if (!baseOverrides && (!mutableProviders || Object.keys(mutableProviders).length === 0)) {
return undefined;
}
const nextOverrides: Record<string, ProviderOverride> = { ...baseOverrides };
for (const [providerId, providerConfig] of Object.entries(mutableProviders ?? {})) {
nextOverrides[providerId] = {
...nextOverrides[providerId],
};
if (providerConfig.enabled !== undefined) {
nextOverrides[providerId].enabled = providerConfig.enabled;
}
if (providerConfig.additionalModels !== undefined) {
nextOverrides[providerId].additionalModels = providerConfig.additionalModels;
}
}
return nextOverrides;
}
export class DaemonConfigStore {
private current: MutableDaemonConfig;
private readonly paseoHome: string;
@@ -148,6 +173,14 @@ function mergeMutableConfigIntoPersistedConfig(params: {
mutable: MutableDaemonConfig;
}): PersistedConfig {
const { persisted, mutable } = params;
const providerOverrides = applyMutableProviderConfigToOverrides(
persisted.agents?.providers as Record<string, ProviderOverride> | undefined,
mutable.providers,
);
const persistedAgents = persisted.agents as
| ({ providers?: Record<string, ProviderOverride> } & Record<string, unknown>)
| undefined;
return {
...persisted,
daemon: {
@@ -157,5 +190,12 @@ function mergeMutableConfigIntoPersistedConfig(params: {
injectIntoAgents: mutable.mcp.injectIntoAgents,
},
},
};
agents:
providerOverrides && Object.keys(providerOverrides).length > 0
? {
...persistedAgents,
providers: providerOverrides,
}
: persisted.agents,
} as PersistedConfig;
}
@@ -1,13 +1,10 @@
import type { Logger } from "pino";
import { describe, expect, test, vi } from "vitest";
import { describe, expect, test } from "vitest";
import type { StoredAgentRecord } from "./agent/agent-storage.js";
import { buildConfigOverrides, buildSessionConfig } from "./persistence-hooks.js";
const testLogger = {
child: () => testLogger,
error: vi.fn(),
warn: vi.fn(),
} as unknown as Logger;
import {
buildConfigOverrides,
buildSessionConfig,
toAgentPersistenceHandle,
} from "./persistence-hooks.js";
function createRecord(overrides?: Partial<StoredAgentRecord>): StoredAgentRecord {
const now = new Date().toISOString();
@@ -127,12 +124,16 @@ describe("persistence hooks", () => {
expect(
buildSessionConfig(record, {
validProviders: ["claude", "codex"],
logger: testLogger,
}),
).toBeNull();
expect(testLogger.warn).toHaveBeenCalledWith(
{ agentId: "agent-missing-provider", provider: "zai" },
"Skipping persisted agent with unknown provider 'zai'",
);
});
test("toAgentPersistenceHandle rejects handles for unavailable providers", () => {
const handle = toAgentPersistenceHandle(["claude", "codex"], {
provider: "gemini",
sessionId: "session-123",
});
expect(handle).toBeNull();
});
});
@@ -13,8 +13,6 @@ interface LoggerLike {
warn(...args: unknown[]): void;
}
const DEFAULT_AGENT_PROVIDER = "claude";
function getLogger(logger: LoggerLike): LoggerLike {
return logger.child({ module: "persistence" });
}
@@ -24,7 +22,6 @@ type AgentManagerStateSource = Pick<AgentManager, "subscribe">;
interface BuildSessionConfigOptions {
validProviders?: Iterable<AgentProvider>;
logger?: LoggerLike;
}
type RegisteredProviders = ReturnType<typeof buildProviderRegistry> | Iterable<AgentProvider>;
@@ -85,10 +82,6 @@ export function buildSessionConfig(
const validProviders = options?.validProviders;
const isValidProvider = validProviders ? new Set(validProviders).has(record.provider) : true;
if (!isValidProvider) {
options?.logger?.warn(
{ agentId: record.id, provider: record.provider },
`Skipping persisted agent with unknown provider '${record.provider}'`,
);
return null;
}
const overrides = buildConfigOverrides(record);
@@ -106,6 +99,13 @@ export function buildSessionConfig(
};
}
export function isStoredAgentProviderAvailable(
record: StoredAgentRecord,
validProviders?: Iterable<AgentProvider>,
): boolean {
return buildSessionConfig(record, { validProviders }) !== null;
}
export function extractTimestamps(record: StoredAgentRecord): {
createdAt: Date;
updatedAt: Date;
@@ -134,24 +134,7 @@ export function isRegisteredProvider(
return hasRegisteredProvider(providerRegistry, value);
}
export function coerceAgentProvider(
logger: LoggerLike,
providerRegistry: ReturnType<typeof buildProviderRegistry>,
value: string,
agentId?: string,
): AgentProvider {
if (isRegisteredProvider(providerRegistry, value)) {
return value;
}
logger.warn(
{ value, agentId, defaultProvider: DEFAULT_AGENT_PROVIDER },
`Unknown provider '${value}' for agent ${agentId ?? "unknown"}; defaulting to '${DEFAULT_AGENT_PROVIDER}'`,
);
return DEFAULT_AGENT_PROVIDER;
}
export function toAgentPersistenceHandle(
logger: LoggerLike,
registeredProviders: RegisteredProviders,
handle: StoredAgentRecord["persistence"],
): AgentPersistenceHandle | null {
@@ -160,11 +143,9 @@ export function toAgentPersistenceHandle(
}
const provider = handle.provider;
if (!hasRegisteredProvider(registeredProviders, provider)) {
logger.warn({ provider }, `Ignoring persistence handle with unknown provider '${provider}'`);
return null;
}
if (!handle.sessionId) {
logger.warn("Ignoring persistence handle missing sessionId");
return null;
}
return {
+9 -3
View File
@@ -221,7 +221,13 @@ function createSessionForTest(options?: {
checkoutDiffManager: checkoutDiffManager as unknown as SessionOptions["checkoutDiffManager"],
github: github as unknown as SessionOptions["github"],
workspaceGitService: workspaceGitService as unknown as SessionOptions["workspaceGitService"],
daemonConfigStore: {} as unknown as SessionOptions["daemonConfigStore"],
daemonConfigStore: {
get: vi.fn(() => ({
mcp: { injectIntoAgents: false },
providers: {},
})),
onChange: vi.fn(() => () => {}),
} as unknown as SessionOptions["daemonConfigStore"],
stt: null,
tts: null,
terminalManager: (options?.terminalManager ?? null) as SessionOptions["terminalManager"],
@@ -357,12 +363,12 @@ describe("session provider refresh cwd routing", () => {
const session = createSessionForTest({ messages });
const fetchModels = vi.fn(async () => []);
const fetchModes = vi.fn(async () => []);
(session as unknown as { providerRegistry: unknown }).providerRegistry = {
(session as unknown as { getProviderRegistry: () => unknown }).getProviderRegistry = () => ({
codex: {
fetchModels,
fetchModes,
},
};
});
await session.handleMessage({
type: "list_provider_models_request",
+42 -18
View File
@@ -68,6 +68,7 @@ import {
import {
buildConfigOverrides,
extractTimestamps,
isStoredAgentProviderAvailable,
toAgentPersistenceHandle,
} from "./persistence-hooks.js";
import { ensureAgentLoaded } from "./agent/agent-loading.js";
@@ -81,6 +82,7 @@ import type { ScriptHealthState } from "./script-health-monitor.js";
import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
import type { DaemonConfigStore } from "./daemon-config-store.js";
import { applyMutableProviderConfigToOverrides } from "./daemon-config-store.js";
import type { WorkspaceGitRuntimeSnapshot, WorkspaceGitService } from "./workspace-git-service.js";
import { buildProviderRegistry } from "./agent/provider-registry.js";
@@ -801,7 +803,6 @@ export class Session {
private readonly mcpBaseUrl: string | null;
private readonly downloadTokenStore: DownloadTokenStore;
private readonly pushTokenStore: PushTokenStore;
private readonly providerRegistry: ReturnType<typeof buildProviderRegistry>;
private unsubscribeAgentEvents: (() => void) | null = null;
private agentUpdatesSubscription: AgentUpdatesSubscriptionState | null = null;
private workspaceUpdatesSubscription: WorkspaceUpdatesSubscriptionState | null = null;
@@ -947,12 +948,6 @@ export class Session {
this.providerOverrides = providerOverrides;
this.isDev = isDev === true;
this.abortController = new AbortController();
this.providerRegistry = buildProviderRegistry(this.sessionLogger, {
runtimeSettings: this.agentProviderRuntimeSettings,
providerOverrides: this.providerOverrides,
workspaceGitService: this.workspaceGitService,
isDev: this.isDev,
});
this.initializePerSessionManagers({ tts, stt, dictation });
@@ -1360,8 +1355,27 @@ export class Session {
return payload;
}
private buildStoredAgentPayload(record: StoredAgentRecord): AgentSnapshotPayload {
return buildStoredAgentPayload(record, this.providerRegistry, this.sessionLogger);
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()) as AgentProvider[];
}
private buildStoredAgentPayload(
record: StoredAgentRecord,
registeredProviderIds = this.getRegisteredProviderIds(),
): AgentSnapshotPayload {
return buildStoredAgentPayload(record, registeredProviderIds);
}
private isProviderVisibleToClient(provider: string): boolean {
@@ -3082,11 +3096,13 @@ export class Session {
if (!record) {
throw new Error(`Agent not found: ${agentId}`);
}
const handle = toAgentPersistenceHandle(
this.sessionLogger,
this.providerRegistry,
record.persistence,
);
const providerRegistry = this.getProviderRegistry();
if (
!isStoredAgentProviderAvailable(record, Object.keys(providerRegistry) as AgentProvider[])
) {
throw new Error(`Agent ${agentId} references unavailable provider '${record.provider}'`);
}
const handle = toAgentPersistenceHandle(providerRegistry, record.persistence);
if (!handle) {
throw new Error(`Agent ${agentId} cannot be refreshed because it lacks persistence`);
}
@@ -3183,7 +3199,7 @@ export class Session {
if (!manager) {
try {
const models = await this.providerRegistry[msg.provider].fetchModels({
const models = await this.getProviderRegistry()[msg.provider].fetchModels({
cwd,
force: false,
});
@@ -3315,7 +3331,7 @@ export class Session {
}
try {
const modes = await this.providerRegistry[msg.provider].fetchModes({
const modes = await this.getProviderRegistry()[msg.provider].fetchModes({
cwd,
force: false,
});
@@ -3497,7 +3513,7 @@ export class Session {
msg: Extract<SessionInboundMessage, { type: "provider_diagnostic_request" }>,
): Promise<void> {
try {
const client = this.providerRegistry[msg.provider].createClient(this.sessionLogger);
const client = this.getProviderRegistry()[msg.provider].createClient(this.sessionLogger);
const diagnostic = client.getDiagnostic
? (await client.getDiagnostic()).diagnostic
: "No diagnostic available for this provider.";
@@ -5334,6 +5350,7 @@ export class Session {
*/
private async listAgentPayloads(filter?: {
labels?: Record<string, string>;
includeUnavailablePersisted?: boolean;
}): Promise<AgentSnapshotPayload[]> {
// Get live agents with session modes
const agentSnapshots = this.agentManager.listAgents();
@@ -5345,9 +5362,15 @@ export class Session {
// (excluding internal agents which are for ephemeral system tasks)
const registryRecords = await this.agentStorage.list();
const liveIds = new Set(agentSnapshots.map((a) => a.id));
const registeredProviderIds = this.getRegisteredProviderIds();
const persistedAgents = registryRecords
.filter((record) => !liveIds.has(record.id) && !record.internal)
.map((record) => this.buildStoredAgentPayload(record));
.filter(
(record) =>
filter?.includeUnavailablePersisted === true ||
isStoredAgentProviderAvailable(record, registeredProviderIds),
)
.map((record) => this.buildStoredAgentPayload(record, registeredProviderIds));
let agents = [...liveAgents, ...persistedAgents];
@@ -5710,6 +5733,7 @@ export class Session {
let agents = await this.listAgentPayloads({
labels: filter?.labels,
includeUnavailablePersisted: request.type === "fetch_agent_history_request",
});
const activePlacementsByCwd =
scope === "active" ? await this.buildActiveProjectPlacementsByWorkspaceCwd() : null;
@@ -13,6 +13,7 @@ import type { LoopService } from "./loop-service.js";
import type { ScheduleService } from "./schedule/service.js";
import type { CheckoutDiffManager, CheckoutDiffMetrics } from "./checkout-diff-manager.js";
import type { DaemonConfigStore, MutableDaemonConfig } from "./daemon-config-store.js";
import { applyMutableProviderConfigToOverrides } from "./daemon-config-store.js";
import {
type ServerInfoStatusPayload,
type SessionOutboundMessage,
@@ -492,6 +493,17 @@ export class VoiceAssistantWebSocketServer {
this.publishSpeechReadiness(snapshot);
}) ?? null;
this.unsubscribeDaemonConfigChange = this.daemonConfigStore.onChange((config) => {
this.providerOverrides = applyMutableProviderConfigToOverrides(
this.providerOverrides,
config.providers,
);
this.providerSnapshotManager.replaceRegistry(
buildProviderRegistry(providerSnapshotLogger, {
runtimeSettings: this.agentProviderRuntimeSettings,
providerOverrides: this.providerOverrides,
isDev: this.isDev,
}),
);
this.broadcastDaemonConfigChanged(config);
});
+22
View File
@@ -51,6 +51,22 @@ import {
// Mutable daemon config schemas (shared between server store and client)
// ---------------------------------------------------------------------------
const MutableDaemonProviderModelSchema = z
.object({
id: z.string().min(1),
label: z.string().min(1),
description: z.string().optional(),
isDefault: z.boolean().optional(),
})
.passthrough();
const MutableDaemonProviderConfigSchema = z
.object({
enabled: z.boolean().optional(),
additionalModels: z.array(MutableDaemonProviderModelSchema).optional(),
})
.passthrough();
export const MutableDaemonConfigSchema = z
.object({
mcp: z
@@ -58,12 +74,16 @@ export const MutableDaemonConfigSchema = z
injectIntoAgents: z.boolean(),
})
.passthrough(),
providers: z.record(z.string(), MutableDaemonProviderConfigSchema).default({}),
})
.passthrough();
export const MutableDaemonConfigPatchSchema = z
.object({
mcp: MutableDaemonConfigSchema.shape.mcp.partial().optional(),
providers: z
.record(z.string(), MutableDaemonProviderConfigSchema.partial().passthrough())
.optional(),
})
.partial()
.passthrough();
@@ -578,6 +598,7 @@ export const AgentSnapshotPayloadSchema = z.object({
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
attentionTimestamp: z.string().nullable().optional(),
archivedAt: z.string().nullable().optional(),
providerUnavailable: z.boolean().optional(),
});
export type AgentSnapshotPayload = z.infer<typeof AgentSnapshotPayloadSchema>;
@@ -600,6 +621,7 @@ export const AgentListItemPayloadSchema = z.object({
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
attentionTimestamp: z.string().nullable().optional(),
labels: z.record(z.string(), z.string()).default({}),
providerUnavailable: z.boolean().optional(),
});
export type AgentListItemPayload = z.infer<typeof AgentListItemPayloadSchema>;