feat: require provider/model format and return compact list items
This commit is contained in:
@@ -151,7 +151,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
args: {
|
||||
cwd: agentCwd,
|
||||
title: "MCP e2e smoke",
|
||||
provider: "claude",
|
||||
provider: "claude/claude-test-model",
|
||||
mode: "bypassPermissions",
|
||||
initialPrompt,
|
||||
background: false,
|
||||
@@ -242,7 +242,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
args: {
|
||||
cwd: agentCwd,
|
||||
title: "Injected MCP",
|
||||
provider: "claude",
|
||||
provider: "claude/claude-test-model",
|
||||
mode: "bypassPermissions",
|
||||
initialPrompt: "reply with done and stop",
|
||||
background: true,
|
||||
@@ -265,7 +265,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
args: {
|
||||
cwd: disabledAgentCwd,
|
||||
title: "No injected MCP",
|
||||
provider: "claude",
|
||||
provider: "claude/claude-test-model",
|
||||
mode: "bypassPermissions",
|
||||
initialPrompt: "reply with done and stop",
|
||||
background: true,
|
||||
@@ -362,7 +362,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
args: {
|
||||
cwd: repoRoot,
|
||||
title: "MCP worktree setup terminals",
|
||||
provider: "claude",
|
||||
provider: "claude/claude-test-model",
|
||||
mode: "bypassPermissions",
|
||||
initialPrompt: "say done and stop",
|
||||
worktreeName: "mcp-worktree-setup-test",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AgentSnapshotPayload } from "../messages.js";
|
||||
import type { AgentListItemPayload, AgentSnapshotPayload } from "../messages.js";
|
||||
import type { SerializableAgentConfig, StoredAgentRecord } from "./agent-storage.js";
|
||||
import type {
|
||||
AgentCapabilityFlags,
|
||||
@@ -31,6 +31,17 @@ function normalizeThinkingOptionId(value: string | null | undefined): string | n
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function normalizeLabels(labels: Record<string, unknown> | undefined): Record<string, string> {
|
||||
if (!labels) {
|
||||
return {};
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(labels).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === "string",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveEffectiveThinkingOptionId(options: {
|
||||
runtimeInfo?: AgentRuntimeInfo | null;
|
||||
configuredThinkingOptionId?: string | null;
|
||||
@@ -198,7 +209,29 @@ export function buildStoredAgentPayload(
|
||||
attentionReason: record.attentionReason ?? null,
|
||||
attentionTimestamp: record.attentionTimestamp ?? null,
|
||||
archivedAt: record.archivedAt ?? null,
|
||||
labels: record.labels,
|
||||
labels: normalizeLabels(record.labels),
|
||||
};
|
||||
}
|
||||
|
||||
export function toAgentListItemPayload(agent: AgentSnapshotPayload): AgentListItemPayload {
|
||||
return {
|
||||
id: agent.id,
|
||||
shortId: agent.id.slice(0, 7),
|
||||
title: agent.title,
|
||||
provider: agent.provider,
|
||||
model: agent.runtimeInfo?.model ?? agent.model,
|
||||
thinkingOptionId: agent.thinkingOptionId,
|
||||
effectiveThinkingOptionId: agent.effectiveThinkingOptionId,
|
||||
status: agent.status,
|
||||
cwd: agent.cwd,
|
||||
createdAt: agent.createdAt,
|
||||
updatedAt: agent.updatedAt,
|
||||
lastUserMessageAt: agent.lastUserMessageAt,
|
||||
archivedAt: agent.archivedAt ?? null,
|
||||
requiresAttention: agent.requiresAttention ?? false,
|
||||
attentionReason: agent.attentionReason ?? null,
|
||||
attentionTimestamp: agent.attentionTimestamp ?? null,
|
||||
labels: agent.labels,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ describe("MCP parity end-to-end", () => {
|
||||
const payload = await callToolStructured(topLevelClient, "create_agent", {
|
||||
cwd,
|
||||
title: "Parity agent",
|
||||
provider: "claude",
|
||||
provider: "claude/claude-test-model",
|
||||
initialPrompt: "say done and stop",
|
||||
mode: "bypassPermissions",
|
||||
background: true,
|
||||
@@ -133,7 +133,7 @@ describe("MCP parity end-to-end", () => {
|
||||
async function createChildAgent(args?: Partial<StructuredContent>): Promise<string> {
|
||||
const payload = await callToolStructured(agentScopedClient, "create_agent", {
|
||||
title: "Parity child",
|
||||
provider: "claude",
|
||||
provider: "claude/claude-test-model",
|
||||
initialPrompt: "say done and stop",
|
||||
background: true,
|
||||
...args,
|
||||
@@ -207,7 +207,7 @@ describe("MCP parity end-to-end", () => {
|
||||
const parentPayload = await callToolStructured(topLevelClient, "create_agent", {
|
||||
cwd: parentAgentCwd,
|
||||
title: "MCP parity parent",
|
||||
provider: "claude",
|
||||
provider: "claude/claude-test-model",
|
||||
initialPrompt: "say done and stop",
|
||||
mode: "bypassPermissions",
|
||||
background: true,
|
||||
@@ -294,10 +294,10 @@ describe("MCP parity end-to-end", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("create_agent accepts model param", async () => {
|
||||
test("create_agent accepts provider/model syntax", async () => {
|
||||
let agentId: string | null = null;
|
||||
try {
|
||||
agentId = await createTopLevelAgent({ model: "claude-test-model" });
|
||||
agentId = await createTopLevelAgent({ provider: "claude/claude-test-model" });
|
||||
const snapshot = daemonHandle.daemon.agentManager.getAgent(agentId);
|
||||
expect(snapshot?.config.model).toBe("claude-test-model");
|
||||
} finally {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,16 @@ import type { AgentProvider } from "./agent-sdk-types.js";
|
||||
import type { AgentManager, WaitForAgentResult } from "./agent-manager.js";
|
||||
import {
|
||||
AgentPermissionRequestPayloadSchema,
|
||||
AgentListItemPayloadSchema,
|
||||
AgentPermissionResponseSchema,
|
||||
AgentSnapshotPayloadSchema,
|
||||
} from "../messages.js";
|
||||
import { buildStoredAgentPayload, toAgentPayload } from "./agent-projections.js";
|
||||
import type { AgentListItemPayload } from "../messages.js";
|
||||
import {
|
||||
buildStoredAgentPayload,
|
||||
toAgentListItemPayload,
|
||||
toAgentPayload,
|
||||
} from "./agent-projections.js";
|
||||
import { curateAgentActivity } from "./activity-curator.js";
|
||||
import type { AgentStorage } from "./agent-storage.js";
|
||||
import { ensureAgentLoaded } from "./agent-loading.js";
|
||||
@@ -24,7 +30,7 @@ import { deletePaseoWorktree, type WorktreeConfig } from "../../utils/worktree.j
|
||||
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
||||
import { scheduleAgentMetadataGeneration } from "./agent-metadata-generator.js";
|
||||
import type { VoiceCallerContext, VoiceSpeakHandler } from "../voice-types.js";
|
||||
import { expandUserPath, resolvePathFromBase } from "../path-utils.js";
|
||||
import { expandUserPath, isSameOrDescendantPath, resolvePathFromBase } from "../path-utils.js";
|
||||
import type { TerminalManager } from "../../terminal/terminal-manager.js";
|
||||
import { captureTerminalLines } from "../../terminal/terminal.js";
|
||||
import { runAsyncWorktreeBootstrap } from "../worktree-bootstrap.js";
|
||||
@@ -37,7 +43,7 @@ import {
|
||||
AgentStatusEnum,
|
||||
ProviderSummarySchema,
|
||||
parseDurationString,
|
||||
resolveProviderAndModel,
|
||||
resolveRequiredProviderModel,
|
||||
sanitizePermissionRequest,
|
||||
sendPromptToAgent,
|
||||
setupFinishNotification,
|
||||
@@ -125,6 +131,68 @@ function mapModeAcrossProviders(
|
||||
|
||||
type McpToolContext = RequestHandlerExtra<ServerRequest, ServerNotification>;
|
||||
|
||||
function parseTimestamp(value: string | null | undefined): number {
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
const parsed = Date.parse(value);
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}
|
||||
|
||||
function resolveAgentListActivityTime(agent: AgentListItemPayload): number {
|
||||
return Math.max(
|
||||
parseTimestamp(agent.updatedAt),
|
||||
parseTimestamp(agent.lastUserMessageAt),
|
||||
parseTimestamp(agent.attentionTimestamp),
|
||||
parseTimestamp(agent.archivedAt),
|
||||
parseTimestamp(agent.createdAt),
|
||||
);
|
||||
}
|
||||
|
||||
function compareAgentListItems(a: AgentListItemPayload, b: AgentListItemPayload): number {
|
||||
const attentionDelta =
|
||||
Number(b.requiresAttention ?? false) - Number(a.requiresAttention ?? false);
|
||||
if (attentionDelta !== 0) {
|
||||
return attentionDelta;
|
||||
}
|
||||
|
||||
const statusOrder = {
|
||||
running: 0,
|
||||
initializing: 1,
|
||||
idle: 2,
|
||||
error: 3,
|
||||
closed: 4,
|
||||
} as Record<string, number>;
|
||||
const statusDelta = (statusOrder[a.status] ?? 999) - (statusOrder[b.status] ?? 999);
|
||||
if (statusDelta !== 0) {
|
||||
return statusDelta;
|
||||
}
|
||||
|
||||
return resolveAgentListActivityTime(b) - resolveAgentListActivityTime(a);
|
||||
}
|
||||
|
||||
function resolveScheduleProviderAndModel(params: {
|
||||
provider?: string;
|
||||
defaultProvider: AgentProvider;
|
||||
}): { provider: AgentProvider; model?: string } {
|
||||
const providerInput = params.provider?.trim() || params.defaultProvider;
|
||||
const slashIndex = providerInput.indexOf("/");
|
||||
if (slashIndex === -1) {
|
||||
return { provider: providerInput as AgentProvider };
|
||||
}
|
||||
|
||||
const provider = providerInput.slice(0, slashIndex).trim();
|
||||
const model = providerInput.slice(slashIndex + 1).trim();
|
||||
if (!provider || !model) {
|
||||
throw new Error("provider must be <provider> or <provider>/<model>");
|
||||
}
|
||||
|
||||
return {
|
||||
provider: provider as AgentProvider,
|
||||
model,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveChildAgentCwd(params: {
|
||||
parentCwd: string;
|
||||
requestedCwd?: string;
|
||||
@@ -256,7 +324,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
if (callerAgent) {
|
||||
const hasProviderOverride = params?.provider !== undefined;
|
||||
const resolvedProviderModel = hasProviderOverride
|
||||
? resolveProviderAndModel({
|
||||
? resolveScheduleProviderAndModel({
|
||||
provider: params?.provider,
|
||||
defaultProvider: callerAgent.provider,
|
||||
})
|
||||
@@ -306,21 +374,34 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedProviderModel = resolveScheduleProviderAndModel({
|
||||
provider: params?.provider,
|
||||
defaultProvider: "claude",
|
||||
});
|
||||
return {
|
||||
type: "new-agent" as const,
|
||||
config: (() => {
|
||||
const resolvedProviderModel = resolveProviderAndModel({
|
||||
provider: params?.provider,
|
||||
defaultProvider: "claude",
|
||||
});
|
||||
return {
|
||||
provider: resolvedProviderModel.provider,
|
||||
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : process.cwd(),
|
||||
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
||||
};
|
||||
})(),
|
||||
config: {
|
||||
provider: resolvedProviderModel.provider,
|
||||
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : process.cwd(),
|
||||
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
const ProviderModelInputSchema = AgentProviderEnum.trim()
|
||||
.refine((value) => value.includes("/"), {
|
||||
message: "provider must be provider/model, for example codex/gpt-5.4",
|
||||
})
|
||||
.refine(
|
||||
(value) => {
|
||||
try {
|
||||
resolveRequiredProviderModel(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ message: "provider must be provider/model, for example codex/gpt-5.4" },
|
||||
);
|
||||
const agentToAgentInputSchema = {
|
||||
cwd: z
|
||||
.string()
|
||||
@@ -332,10 +413,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
.min(1, "Title is required")
|
||||
.max(60, "Title must be 60 characters or fewer")
|
||||
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
|
||||
provider: AgentProviderEnum.optional().describe(
|
||||
"Optional agent implementation to spawn. Defaults to 'claude'.",
|
||||
provider: ProviderModelInputSchema.describe(
|
||||
"Required provider/model pair, for example codex/gpt-5.4.",
|
||||
),
|
||||
model: z.string().optional().describe("Model to use (e.g. claude-sonnet-4-20250514)"),
|
||||
thinking: z.string().optional().describe("Thinking option ID"),
|
||||
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
|
||||
initialPrompt: z
|
||||
@@ -369,10 +449,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
.min(1, "Title is required")
|
||||
.max(60, "Title must be 60 characters or fewer")
|
||||
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
|
||||
provider: AgentProviderEnum.optional().describe(
|
||||
"Optional agent implementation to spawn. Defaults to 'claude'.",
|
||||
provider: ProviderModelInputSchema.describe(
|
||||
"Required provider/model pair, for example codex/gpt-5.4.",
|
||||
),
|
||||
model: z.string().optional().describe("Model to use (e.g. claude-sonnet-4-20250514)"),
|
||||
thinking: z.string().optional().describe("Thinking option ID"),
|
||||
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
|
||||
initialPrompt: z
|
||||
@@ -420,8 +499,8 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
};
|
||||
|
||||
const createAgentInputSchema = callerAgentId ? agentToAgentInputSchema : topLevelInputSchema;
|
||||
const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema);
|
||||
const topLevelCreateAgentArgsSchema = z.object(topLevelInputSchema);
|
||||
const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema).strict();
|
||||
const topLevelCreateAgentArgsSchema = z.object(topLevelInputSchema).strict();
|
||||
|
||||
if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) {
|
||||
server.registerTool(
|
||||
@@ -471,7 +550,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
{
|
||||
title: "Create agent",
|
||||
description:
|
||||
"Create a new Claude or Codex agent tied to a working directory. 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. Optionally run an initial prompt immediately or create a git worktree for the agent.",
|
||||
inputSchema: createAgentInputSchema,
|
||||
outputSchema: {
|
||||
agentId: z.string(),
|
||||
@@ -507,11 +586,12 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
|
||||
if (callerAgentId) {
|
||||
const callerArgs = agentToAgentCreateAgentArgsSchema.parse(args);
|
||||
provider = callerArgs.provider ?? "claude";
|
||||
const resolvedProviderModel = resolveRequiredProviderModel(callerArgs.provider);
|
||||
provider = resolvedProviderModel.provider;
|
||||
model = resolvedProviderModel.model;
|
||||
initialPrompt = callerArgs.initialPrompt;
|
||||
background = callerArgs.background ?? false;
|
||||
normalizedTitle = callerArgs.title.trim();
|
||||
model = callerArgs.model;
|
||||
thinking = callerArgs.thinking;
|
||||
labels = callerArgs.labels;
|
||||
notifyOnFinish = callerArgs.notifyOnFinish ?? false;
|
||||
@@ -532,11 +612,12 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
}
|
||||
} else {
|
||||
const topLevelArgs = topLevelCreateAgentArgsSchema.parse(args);
|
||||
provider = topLevelArgs.provider ?? "claude";
|
||||
const resolvedProviderModel = resolveRequiredProviderModel(topLevelArgs.provider);
|
||||
provider = resolvedProviderModel.provider;
|
||||
model = resolvedProviderModel.model;
|
||||
initialPrompt = topLevelArgs.initialPrompt;
|
||||
background = topLevelArgs.background ?? false;
|
||||
normalizedTitle = topLevelArgs.title.trim();
|
||||
model = topLevelArgs.model;
|
||||
thinking = topLevelArgs.thinking;
|
||||
labels = topLevelArgs.labels;
|
||||
notifyOnFinish = topLevelArgs.notifyOnFinish ?? false;
|
||||
@@ -928,15 +1009,29 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
"list_agents",
|
||||
{
|
||||
title: "List agents",
|
||||
description: "List all live agents managed by the server.",
|
||||
description: "List recent agents as compact metadata.",
|
||||
inputSchema: {
|
||||
includeArchived: z.boolean().optional().default(false),
|
||||
cwd: z.string().optional(),
|
||||
sinceHours: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.max(24 * 30)
|
||||
.optional()
|
||||
.default(48),
|
||||
statuses: z.array(AgentStatusEnum).optional(),
|
||||
limit: z.number().int().positive().max(200).optional().default(50),
|
||||
},
|
||||
outputSchema: {
|
||||
agents: z.array(AgentSnapshotPayloadSchema),
|
||||
agents: z.array(AgentListItemPayloadSchema),
|
||||
},
|
||||
},
|
||||
async ({ includeArchived }) => {
|
||||
async ({ includeArchived = false, cwd, sinceHours = 48, statuses, limit = 50 }) => {
|
||||
const callerCwd = callerAgentId ? resolveCallerAgent()?.cwd : undefined;
|
||||
const requestedCwd = cwd?.trim() ? expandUserPath(cwd) : callerCwd;
|
||||
const statusFilter = statuses && statuses.length > 0 ? new Set(statuses) : null;
|
||||
const sinceMs = Date.now() - sinceHours * 60 * 60 * 1000;
|
||||
const liveSnapshots = agentManager.listAgents();
|
||||
const liveAgents = await Promise.all(
|
||||
liveSnapshots.map((snapshot) =>
|
||||
@@ -949,10 +1044,17 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
.filter((record) => !record.internal && !liveIds.has(record.id))
|
||||
.filter((record) => includeArchived || !record.archivedAt)
|
||||
.map((record) => buildStoredAgentPayload(record, requireProviderRegistry(), childLogger));
|
||||
const agents = [...liveAgents, ...storedAgents]
|
||||
.map(toAgentListItemPayload)
|
||||
.filter((agent) => !requestedCwd || isSameOrDescendantPath(requestedCwd, agent.cwd))
|
||||
.filter((agent) => !statusFilter || statusFilter.has(agent.status))
|
||||
.filter((agent) => !agent.archivedAt || resolveAgentListActivityTime(agent) >= sinceMs)
|
||||
.sort(compareAgentListItems)
|
||||
.slice(0, limit);
|
||||
|
||||
return {
|
||||
content: [],
|
||||
structuredContent: ensureValidJson({ agents: [...liveAgents, ...storedAgents] }),
|
||||
structuredContent: ensureValidJson({ agents }),
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -1304,7 +1406,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
}
|
||||
return { type: "agent" as const, agentId: callerAgentId };
|
||||
})()
|
||||
: resolveNewAgentScheduleTarget({ provider, cwd });
|
||||
: (() => {
|
||||
return resolveNewAgentScheduleTarget({ provider, cwd });
|
||||
})();
|
||||
|
||||
const schedule = await scheduleService.create({
|
||||
prompt: prompt.trim(),
|
||||
|
||||
@@ -42,7 +42,7 @@ export const AgentSelectOptionSchema = z
|
||||
label: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
isDefault: z.boolean().nullish(),
|
||||
metadata: z.record(z.unknown()).nullish(),
|
||||
metadata: z.record(z.string(), z.unknown()).nullish(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
@@ -53,7 +53,7 @@ export const AgentModelSchema = z
|
||||
label: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
isDefault: z.boolean().nullish(),
|
||||
metadata: z.record(z.unknown()).nullish(),
|
||||
metadata: z.record(z.string(), z.unknown()).nullish(),
|
||||
thinkingOptions: z.array(AgentSelectOptionSchema).nullish(),
|
||||
defaultThinkingOptionId: z.string().nullish(),
|
||||
})
|
||||
@@ -67,41 +67,24 @@ export interface ResolvedProviderModel {
|
||||
model: string | undefined;
|
||||
}
|
||||
|
||||
export function resolveProviderAndModel(params: {
|
||||
provider?: string;
|
||||
model?: string;
|
||||
defaultProvider: AgentProvider;
|
||||
}): ResolvedProviderModel {
|
||||
const providerInput = params.provider?.trim() || params.defaultProvider;
|
||||
const modelInput = params.model?.trim();
|
||||
|
||||
if (params.model !== undefined && !modelInput) {
|
||||
throw new Error("model cannot be empty");
|
||||
}
|
||||
|
||||
export function resolveRequiredProviderModel(
|
||||
providerValue: string,
|
||||
): Required<ResolvedProviderModel> {
|
||||
const providerInput = providerValue.trim();
|
||||
const slashIndex = providerInput.indexOf("/");
|
||||
if (slashIndex === -1) {
|
||||
return {
|
||||
provider: providerInput as AgentProvider,
|
||||
model: modelInput,
|
||||
};
|
||||
if (slashIndex <= 0 || slashIndex === providerInput.length - 1) {
|
||||
throw new Error("provider must be provider/model, for example codex/gpt-5.4");
|
||||
}
|
||||
|
||||
const provider = providerInput.slice(0, slashIndex).trim();
|
||||
const modelFromProvider = providerInput.slice(slashIndex + 1).trim();
|
||||
if (!provider || !modelFromProvider) {
|
||||
throw new Error("provider must be <provider> or <provider>/<model>");
|
||||
}
|
||||
|
||||
if (modelInput && modelInput !== modelFromProvider) {
|
||||
throw new Error(
|
||||
`Conflicting model values provided: provider specifies ${modelFromProvider}, but model specifies ${modelInput}`,
|
||||
);
|
||||
const model = providerInput.slice(slashIndex + 1).trim();
|
||||
if (!provider || !model) {
|
||||
throw new Error("provider must be provider/model, for example codex/gpt-5.4");
|
||||
}
|
||||
|
||||
return {
|
||||
provider: provider as AgentProvider,
|
||||
model: modelInput ?? modelFromProvider,
|
||||
model,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,3 +20,17 @@ export function resolvePathFromBase(baseCwd: string, requestedPath: string): str
|
||||
}
|
||||
return resolve(baseCwd, trimmed);
|
||||
}
|
||||
|
||||
export function isSameOrDescendantPath(basePath: string, candidatePath: string): boolean {
|
||||
let normalizedBase = basePath.replace(/\\/g, "/").replace(/\/$/, "");
|
||||
let normalizedCandidate = candidatePath.replace(/\\/g, "/").replace(/\/$/, "");
|
||||
|
||||
if (/^[a-zA-Z]:\//.test(normalizedBase) || /^[a-zA-Z]:\//.test(normalizedCandidate)) {
|
||||
normalizedBase = normalizedBase.toLowerCase();
|
||||
normalizedCandidate = normalizedCandidate.toLowerCase();
|
||||
}
|
||||
|
||||
return (
|
||||
normalizedCandidate === normalizedBase || normalizedCandidate.startsWith(normalizedBase + "/")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -397,7 +397,7 @@ const ToolCallBasePayloadSchema = z
|
||||
callId: z.string(),
|
||||
name: z.string(),
|
||||
detail: ToolCallDetailPayloadSchema,
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -534,7 +534,7 @@ const AgentPersistenceHandleSchema: z.ZodType<AgentPersistenceHandle | null> = z
|
||||
provider: AgentProviderSchema,
|
||||
sessionId: z.string(),
|
||||
nativeHandle: z.string().optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
.nullable();
|
||||
|
||||
@@ -544,7 +544,7 @@ const AgentRuntimeInfoSchema: z.ZodType<AgentRuntimeInfo> = z.object({
|
||||
model: z.string().nullable().optional(),
|
||||
thinkingOptionId: z.string().nullable().optional(),
|
||||
modeId: z.string().nullable().optional(),
|
||||
extra: z.record(z.unknown()).optional(),
|
||||
extra: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
|
||||
export const AgentSnapshotPayloadSchema = z.object({
|
||||
@@ -568,7 +568,7 @@ export const AgentSnapshotPayloadSchema = z.object({
|
||||
lastUsage: AgentUsageSchema.optional(),
|
||||
lastError: z.string().optional(),
|
||||
title: z.string().nullable(),
|
||||
labels: z.record(z.string()).default({}),
|
||||
labels: z.record(z.string(), z.string()).default({}),
|
||||
requiresAttention: z.boolean().optional(),
|
||||
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
|
||||
attentionTimestamp: z.string().nullable().optional(),
|
||||
@@ -577,6 +577,28 @@ export const AgentSnapshotPayloadSchema = z.object({
|
||||
|
||||
export type AgentSnapshotPayload = z.infer<typeof AgentSnapshotPayloadSchema>;
|
||||
|
||||
export const AgentListItemPayloadSchema = z.object({
|
||||
id: z.string(),
|
||||
shortId: z.string(),
|
||||
title: z.string().nullable(),
|
||||
provider: AgentProviderSchema,
|
||||
model: z.string().nullable(),
|
||||
thinkingOptionId: z.string().nullable().optional(),
|
||||
effectiveThinkingOptionId: z.string().nullable().optional(),
|
||||
status: AgentStatusSchema,
|
||||
cwd: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
lastUserMessageAt: z.string().nullable(),
|
||||
archivedAt: z.string().nullable().optional(),
|
||||
requiresAttention: z.boolean().optional(),
|
||||
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
|
||||
attentionTimestamp: z.string().nullable().optional(),
|
||||
labels: z.record(z.string(), z.string()).default({}),
|
||||
});
|
||||
|
||||
export type AgentListItemPayload = z.infer<typeof AgentListItemPayloadSchema>;
|
||||
|
||||
export type AgentStreamEventPayload = z.infer<typeof AgentStreamEventPayloadSchema>;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolve, dirname, basename } from "path";
|
||||
import { existsSync, realpathSync } from "fs";
|
||||
import { open as openFile, stat as statFile } from "fs/promises";
|
||||
import { TTLCache } from "@isaacs/ttlcache";
|
||||
import TTLCache from "@isaacs/ttlcache";
|
||||
import type { ParsedDiffFile } from "../server/utils/diff-highlighter.js";
|
||||
import { parseAndHighlightDiff } from "../server/utils/diff-highlighter.js";
|
||||
import {
|
||||
|
||||
Reference in New Issue
Block a user