Merge branch 'research/mcp-list-agents-summary'
This commit is contained in:
@@ -132,7 +132,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
args: {
|
args: {
|
||||||
cwd: agentCwd,
|
cwd: agentCwd,
|
||||||
title: "MCP e2e smoke",
|
title: "MCP e2e smoke",
|
||||||
provider: "claude",
|
provider: "claude/claude-test-model",
|
||||||
mode: "bypassPermissions",
|
mode: "bypassPermissions",
|
||||||
initialPrompt,
|
initialPrompt,
|
||||||
background: false,
|
background: false,
|
||||||
@@ -223,7 +223,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
args: {
|
args: {
|
||||||
cwd: agentCwd,
|
cwd: agentCwd,
|
||||||
title: "Injected MCP",
|
title: "Injected MCP",
|
||||||
provider: "claude",
|
provider: "claude/claude-test-model",
|
||||||
mode: "bypassPermissions",
|
mode: "bypassPermissions",
|
||||||
initialPrompt: "reply with done and stop",
|
initialPrompt: "reply with done and stop",
|
||||||
background: true,
|
background: true,
|
||||||
@@ -246,7 +246,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
args: {
|
args: {
|
||||||
cwd: disabledAgentCwd,
|
cwd: disabledAgentCwd,
|
||||||
title: "No injected MCP",
|
title: "No injected MCP",
|
||||||
provider: "claude",
|
provider: "claude/claude-test-model",
|
||||||
mode: "bypassPermissions",
|
mode: "bypassPermissions",
|
||||||
initialPrompt: "reply with done and stop",
|
initialPrompt: "reply with done and stop",
|
||||||
background: true,
|
background: true,
|
||||||
@@ -343,7 +343,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
args: {
|
args: {
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
title: "MCP worktree setup terminals",
|
title: "MCP worktree setup terminals",
|
||||||
provider: "claude",
|
provider: "claude/claude-test-model",
|
||||||
mode: "bypassPermissions",
|
mode: "bypassPermissions",
|
||||||
initialPrompt: "say done and stop",
|
initialPrompt: "say done and stop",
|
||||||
worktreeName: "mcp-worktree-setup-test",
|
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 { SerializableAgentConfig, StoredAgentRecord } from "./agent-storage.js";
|
||||||
import type {
|
import type {
|
||||||
AgentCapabilityFlags,
|
AgentCapabilityFlags,
|
||||||
@@ -31,6 +31,17 @@ function normalizeThinkingOptionId(value: string | null | undefined): string | n
|
|||||||
return normalized.length > 0 ? normalized : null;
|
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: {
|
export function resolveEffectiveThinkingOptionId(options: {
|
||||||
runtimeInfo?: AgentRuntimeInfo | null;
|
runtimeInfo?: AgentRuntimeInfo | null;
|
||||||
configuredThinkingOptionId?: string | null;
|
configuredThinkingOptionId?: string | null;
|
||||||
@@ -198,7 +209,29 @@ export function buildStoredAgentPayload(
|
|||||||
attentionReason: record.attentionReason ?? null,
|
attentionReason: record.attentionReason ?? null,
|
||||||
attentionTimestamp: record.attentionTimestamp ?? null,
|
attentionTimestamp: record.attentionTimestamp ?? null,
|
||||||
archivedAt: record.archivedAt ?? 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", {
|
const payload = await callToolStructured(topLevelClient, "create_agent", {
|
||||||
cwd,
|
cwd,
|
||||||
title: "Parity agent",
|
title: "Parity agent",
|
||||||
provider: "claude",
|
provider: "claude/claude-test-model",
|
||||||
initialPrompt: "say done and stop",
|
initialPrompt: "say done and stop",
|
||||||
mode: "bypassPermissions",
|
mode: "bypassPermissions",
|
||||||
background: true,
|
background: true,
|
||||||
@@ -133,7 +133,7 @@ describe("MCP parity end-to-end", () => {
|
|||||||
async function createChildAgent(args?: Partial<StructuredContent>): Promise<string> {
|
async function createChildAgent(args?: Partial<StructuredContent>): Promise<string> {
|
||||||
const payload = await callToolStructured(agentScopedClient, "create_agent", {
|
const payload = await callToolStructured(agentScopedClient, "create_agent", {
|
||||||
title: "Parity child",
|
title: "Parity child",
|
||||||
provider: "claude",
|
provider: "claude/claude-test-model",
|
||||||
initialPrompt: "say done and stop",
|
initialPrompt: "say done and stop",
|
||||||
background: true,
|
background: true,
|
||||||
...args,
|
...args,
|
||||||
@@ -207,7 +207,7 @@ describe("MCP parity end-to-end", () => {
|
|||||||
const parentPayload = await callToolStructured(topLevelClient, "create_agent", {
|
const parentPayload = await callToolStructured(topLevelClient, "create_agent", {
|
||||||
cwd: parentAgentCwd,
|
cwd: parentAgentCwd,
|
||||||
title: "MCP parity parent",
|
title: "MCP parity parent",
|
||||||
provider: "claude",
|
provider: "claude/claude-test-model",
|
||||||
initialPrompt: "say done and stop",
|
initialPrompt: "say done and stop",
|
||||||
mode: "bypassPermissions",
|
mode: "bypassPermissions",
|
||||||
background: true,
|
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;
|
let agentId: string | null = null;
|
||||||
try {
|
try {
|
||||||
agentId = await createTopLevelAgent({ model: "claude-test-model" });
|
agentId = await createTopLevelAgent({ provider: "claude/claude-test-model" });
|
||||||
const snapshot = daemonHandle.daemon.agentManager.getAgent(agentId);
|
const snapshot = daemonHandle.daemon.agentManager.getAgent(agentId);
|
||||||
expect(snapshot?.config.model).toBe("claude-test-model");
|
expect(snapshot?.config.model).toBe("claude-test-model");
|
||||||
} finally {
|
} 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 type { AgentManager, WaitForAgentResult } from "./agent-manager.js";
|
||||||
import {
|
import {
|
||||||
AgentPermissionRequestPayloadSchema,
|
AgentPermissionRequestPayloadSchema,
|
||||||
|
AgentListItemPayloadSchema,
|
||||||
AgentPermissionResponseSchema,
|
AgentPermissionResponseSchema,
|
||||||
AgentSnapshotPayloadSchema,
|
AgentSnapshotPayloadSchema,
|
||||||
} from "../messages.js";
|
} 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 { curateAgentActivity } from "./activity-curator.js";
|
||||||
import type { AgentStorage } from "./agent-storage.js";
|
import type { AgentStorage } from "./agent-storage.js";
|
||||||
import { ensureAgentLoaded } from "./agent-loading.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 { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
||||||
import { scheduleAgentMetadataGeneration } from "./agent-metadata-generator.js";
|
import { scheduleAgentMetadataGeneration } from "./agent-metadata-generator.js";
|
||||||
import type { VoiceCallerContext, VoiceSpeakHandler } from "../voice-types.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 type { TerminalManager } from "../../terminal/terminal-manager.js";
|
||||||
import { captureTerminalLines } from "../../terminal/terminal.js";
|
import { captureTerminalLines } from "../../terminal/terminal.js";
|
||||||
import { runAsyncWorktreeBootstrap } from "../worktree-bootstrap.js";
|
import { runAsyncWorktreeBootstrap } from "../worktree-bootstrap.js";
|
||||||
@@ -38,7 +44,7 @@ import {
|
|||||||
AgentStatusEnum,
|
AgentStatusEnum,
|
||||||
ProviderSummarySchema,
|
ProviderSummarySchema,
|
||||||
parseDurationString,
|
parseDurationString,
|
||||||
resolveProviderAndModel,
|
resolveRequiredProviderModel,
|
||||||
sanitizePermissionRequest,
|
sanitizePermissionRequest,
|
||||||
sendPromptToAgent,
|
sendPromptToAgent,
|
||||||
setupFinishNotification,
|
setupFinishNotification,
|
||||||
@@ -126,6 +132,68 @@ function mapModeAcrossProviders(
|
|||||||
|
|
||||||
type McpToolContext = RequestHandlerExtra<ServerRequest, ServerNotification>;
|
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: {
|
function resolveChildAgentCwd(params: {
|
||||||
parentCwd: string;
|
parentCwd: string;
|
||||||
requestedCwd?: string;
|
requestedCwd?: string;
|
||||||
@@ -257,7 +325,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
if (callerAgent) {
|
if (callerAgent) {
|
||||||
const hasProviderOverride = params?.provider !== undefined;
|
const hasProviderOverride = params?.provider !== undefined;
|
||||||
const resolvedProviderModel = hasProviderOverride
|
const resolvedProviderModel = hasProviderOverride
|
||||||
? resolveProviderAndModel({
|
? resolveScheduleProviderAndModel({
|
||||||
provider: params?.provider,
|
provider: params?.provider,
|
||||||
defaultProvider: callerAgent.provider,
|
defaultProvider: callerAgent.provider,
|
||||||
})
|
})
|
||||||
@@ -307,21 +375,34 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedProviderModel = resolveScheduleProviderAndModel({
|
||||||
|
provider: params?.provider,
|
||||||
|
defaultProvider: "claude",
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
type: "new-agent" as const,
|
type: "new-agent" as const,
|
||||||
config: (() => {
|
config: {
|
||||||
const resolvedProviderModel = resolveProviderAndModel({
|
provider: resolvedProviderModel.provider,
|
||||||
provider: params?.provider,
|
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : process.cwd(),
|
||||||
defaultProvider: "claude",
|
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
||||||
});
|
},
|
||||||
return {
|
|
||||||
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 = {
|
const agentToAgentInputSchema = {
|
||||||
cwd: z
|
cwd: z
|
||||||
.string()
|
.string()
|
||||||
@@ -333,10 +414,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
.min(1, "Title is required")
|
.min(1, "Title is required")
|
||||||
.max(60, "Title must be 60 characters or fewer")
|
.max(60, "Title must be 60 characters or fewer")
|
||||||
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
|
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
|
||||||
provider: AgentProviderEnum.optional().describe(
|
provider: ProviderModelInputSchema.describe(
|
||||||
"Optional agent implementation to spawn. Defaults to 'claude'.",
|
"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"),
|
thinking: z.string().optional().describe("Thinking option ID"),
|
||||||
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
|
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
|
||||||
initialPrompt: z
|
initialPrompt: z
|
||||||
@@ -370,10 +450,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
.min(1, "Title is required")
|
.min(1, "Title is required")
|
||||||
.max(60, "Title must be 60 characters or fewer")
|
.max(60, "Title must be 60 characters or fewer")
|
||||||
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
|
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
|
||||||
provider: AgentProviderEnum.optional().describe(
|
provider: ProviderModelInputSchema.describe(
|
||||||
"Optional agent implementation to spawn. Defaults to 'claude'.",
|
"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"),
|
thinking: z.string().optional().describe("Thinking option ID"),
|
||||||
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
|
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
|
||||||
initialPrompt: z
|
initialPrompt: z
|
||||||
@@ -421,8 +500,8 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createAgentInputSchema = callerAgentId ? agentToAgentInputSchema : topLevelInputSchema;
|
const createAgentInputSchema = callerAgentId ? agentToAgentInputSchema : topLevelInputSchema;
|
||||||
const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema);
|
const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema).strict();
|
||||||
const topLevelCreateAgentArgsSchema = z.object(topLevelInputSchema);
|
const topLevelCreateAgentArgsSchema = z.object(topLevelInputSchema).strict();
|
||||||
|
|
||||||
if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) {
|
if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) {
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
@@ -472,7 +551,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
{
|
{
|
||||||
title: "Create agent",
|
title: "Create agent",
|
||||||
description:
|
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,
|
inputSchema: createAgentInputSchema,
|
||||||
outputSchema: {
|
outputSchema: {
|
||||||
agentId: z.string(),
|
agentId: z.string(),
|
||||||
@@ -508,11 +587,12 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
|
|
||||||
if (callerAgentId) {
|
if (callerAgentId) {
|
||||||
const callerArgs = agentToAgentCreateAgentArgsSchema.parse(args);
|
const callerArgs = agentToAgentCreateAgentArgsSchema.parse(args);
|
||||||
provider = callerArgs.provider ?? "claude";
|
const resolvedProviderModel = resolveRequiredProviderModel(callerArgs.provider);
|
||||||
|
provider = resolvedProviderModel.provider;
|
||||||
|
model = resolvedProviderModel.model;
|
||||||
initialPrompt = callerArgs.initialPrompt;
|
initialPrompt = callerArgs.initialPrompt;
|
||||||
background = callerArgs.background ?? false;
|
background = callerArgs.background ?? false;
|
||||||
normalizedTitle = callerArgs.title.trim();
|
normalizedTitle = callerArgs.title.trim();
|
||||||
model = callerArgs.model;
|
|
||||||
thinking = callerArgs.thinking;
|
thinking = callerArgs.thinking;
|
||||||
labels = callerArgs.labels;
|
labels = callerArgs.labels;
|
||||||
notifyOnFinish = callerArgs.notifyOnFinish ?? false;
|
notifyOnFinish = callerArgs.notifyOnFinish ?? false;
|
||||||
@@ -533,11 +613,12 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const topLevelArgs = topLevelCreateAgentArgsSchema.parse(args);
|
const topLevelArgs = topLevelCreateAgentArgsSchema.parse(args);
|
||||||
provider = topLevelArgs.provider ?? "claude";
|
const resolvedProviderModel = resolveRequiredProviderModel(topLevelArgs.provider);
|
||||||
|
provider = resolvedProviderModel.provider;
|
||||||
|
model = resolvedProviderModel.model;
|
||||||
initialPrompt = topLevelArgs.initialPrompt;
|
initialPrompt = topLevelArgs.initialPrompt;
|
||||||
background = topLevelArgs.background ?? false;
|
background = topLevelArgs.background ?? false;
|
||||||
normalizedTitle = topLevelArgs.title.trim();
|
normalizedTitle = topLevelArgs.title.trim();
|
||||||
model = topLevelArgs.model;
|
|
||||||
thinking = topLevelArgs.thinking;
|
thinking = topLevelArgs.thinking;
|
||||||
labels = topLevelArgs.labels;
|
labels = topLevelArgs.labels;
|
||||||
notifyOnFinish = topLevelArgs.notifyOnFinish ?? false;
|
notifyOnFinish = topLevelArgs.notifyOnFinish ?? false;
|
||||||
@@ -929,15 +1010,29 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
"list_agents",
|
"list_agents",
|
||||||
{
|
{
|
||||||
title: "List agents",
|
title: "List agents",
|
||||||
description: "List all live agents managed by the server.",
|
description: "List recent agents as compact metadata.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
includeArchived: z.boolean().optional().default(false),
|
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: {
|
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 liveSnapshots = agentManager.listAgents();
|
||||||
const liveAgents = await Promise.all(
|
const liveAgents = await Promise.all(
|
||||||
liveSnapshots.map((snapshot) =>
|
liveSnapshots.map((snapshot) =>
|
||||||
@@ -950,10 +1045,17 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
.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)
|
||||||
.map((record) => buildStoredAgentPayload(record, requireProviderRegistry(), childLogger));
|
.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 {
|
return {
|
||||||
content: [],
|
content: [],
|
||||||
structuredContent: ensureValidJson({ agents: [...liveAgents, ...storedAgents] }),
|
structuredContent: ensureValidJson({ agents }),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1321,7 +1423,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
}
|
}
|
||||||
return { type: "agent" as const, agentId: callerAgentId };
|
return { type: "agent" as const, agentId: callerAgentId };
|
||||||
})()
|
})()
|
||||||
: resolveNewAgentScheduleTarget({ provider, cwd });
|
: (() => {
|
||||||
|
return resolveNewAgentScheduleTarget({ provider, cwd });
|
||||||
|
})();
|
||||||
|
|
||||||
const schedule = await scheduleService.create({
|
const schedule = await scheduleService.create({
|
||||||
prompt: prompt.trim(),
|
prompt: prompt.trim(),
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const AgentSelectOptionSchema = z
|
|||||||
label: z.string().nullish(),
|
label: z.string().nullish(),
|
||||||
description: z.string().nullish(),
|
description: z.string().nullish(),
|
||||||
isDefault: z.boolean().nullish(),
|
isDefault: z.boolean().nullish(),
|
||||||
metadata: z.record(z.unknown()).nullish(),
|
metadata: z.record(z.string(), z.unknown()).nullish(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export const AgentModelSchema = z
|
|||||||
label: z.string().nullish(),
|
label: z.string().nullish(),
|
||||||
description: z.string().nullish(),
|
description: z.string().nullish(),
|
||||||
isDefault: z.boolean().nullish(),
|
isDefault: z.boolean().nullish(),
|
||||||
metadata: z.record(z.unknown()).nullish(),
|
metadata: z.record(z.string(), z.unknown()).nullish(),
|
||||||
thinkingOptions: z.array(AgentSelectOptionSchema).nullish(),
|
thinkingOptions: z.array(AgentSelectOptionSchema).nullish(),
|
||||||
defaultThinkingOptionId: z.string().nullish(),
|
defaultThinkingOptionId: z.string().nullish(),
|
||||||
})
|
})
|
||||||
@@ -67,41 +67,24 @@ export interface ResolvedProviderModel {
|
|||||||
model: string | undefined;
|
model: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveProviderAndModel(params: {
|
export function resolveRequiredProviderModel(
|
||||||
provider?: string;
|
providerValue: string,
|
||||||
model?: string;
|
): Required<ResolvedProviderModel> {
|
||||||
defaultProvider: AgentProvider;
|
const providerInput = providerValue.trim();
|
||||||
}): 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
const slashIndex = providerInput.indexOf("/");
|
const slashIndex = providerInput.indexOf("/");
|
||||||
if (slashIndex === -1) {
|
if (slashIndex <= 0 || slashIndex === providerInput.length - 1) {
|
||||||
return {
|
throw new Error("provider must be provider/model, for example codex/gpt-5.4");
|
||||||
provider: providerInput as AgentProvider,
|
|
||||||
model: modelInput,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = providerInput.slice(0, slashIndex).trim();
|
const provider = providerInput.slice(0, slashIndex).trim();
|
||||||
const modelFromProvider = providerInput.slice(slashIndex + 1).trim();
|
const model = providerInput.slice(slashIndex + 1).trim();
|
||||||
if (!provider || !modelFromProvider) {
|
if (!provider || !model) {
|
||||||
throw new Error("provider must be <provider> or <provider>/<model>");
|
throw new Error("provider must be provider/model, for example codex/gpt-5.4");
|
||||||
}
|
|
||||||
|
|
||||||
if (modelInput && modelInput !== modelFromProvider) {
|
|
||||||
throw new Error(
|
|
||||||
`Conflicting model values provided: provider specifies ${modelFromProvider}, but model specifies ${modelInput}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provider: provider as AgentProvider,
|
provider: provider as AgentProvider,
|
||||||
model: modelInput ?? modelFromProvider,
|
model,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,3 +20,17 @@ export function resolvePathFromBase(baseCwd: string, requestedPath: string): str
|
|||||||
}
|
}
|
||||||
return resolve(baseCwd, trimmed);
|
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(),
|
callId: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
detail: ToolCallDetailPayloadSchema,
|
detail: ToolCallDetailPayloadSchema,
|
||||||
metadata: z.record(z.unknown()).optional(),
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -534,7 +534,7 @@ const AgentPersistenceHandleSchema: z.ZodType<AgentPersistenceHandle | null> = z
|
|||||||
provider: AgentProviderSchema,
|
provider: AgentProviderSchema,
|
||||||
sessionId: z.string(),
|
sessionId: z.string(),
|
||||||
nativeHandle: z.string().optional(),
|
nativeHandle: z.string().optional(),
|
||||||
metadata: z.record(z.unknown()).optional(),
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||||
})
|
})
|
||||||
.nullable();
|
.nullable();
|
||||||
|
|
||||||
@@ -544,7 +544,7 @@ const AgentRuntimeInfoSchema: z.ZodType<AgentRuntimeInfo> = z.object({
|
|||||||
model: z.string().nullable().optional(),
|
model: z.string().nullable().optional(),
|
||||||
thinkingOptionId: z.string().nullable().optional(),
|
thinkingOptionId: z.string().nullable().optional(),
|
||||||
modeId: 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({
|
export const AgentSnapshotPayloadSchema = z.object({
|
||||||
@@ -568,7 +568,7 @@ export const AgentSnapshotPayloadSchema = z.object({
|
|||||||
lastUsage: AgentUsageSchema.optional(),
|
lastUsage: AgentUsageSchema.optional(),
|
||||||
lastError: z.string().optional(),
|
lastError: z.string().optional(),
|
||||||
title: z.string().nullable(),
|
title: z.string().nullable(),
|
||||||
labels: z.record(z.string()).default({}),
|
labels: z.record(z.string(), z.string()).default({}),
|
||||||
requiresAttention: z.boolean().optional(),
|
requiresAttention: z.boolean().optional(),
|
||||||
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
|
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
|
||||||
attentionTimestamp: z.string().nullable().optional(),
|
attentionTimestamp: z.string().nullable().optional(),
|
||||||
@@ -577,6 +577,28 @@ export const AgentSnapshotPayloadSchema = z.object({
|
|||||||
|
|
||||||
export type AgentSnapshotPayload = z.infer<typeof AgentSnapshotPayloadSchema>;
|
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>;
|
export type AgentStreamEventPayload = z.infer<typeof AgentStreamEventPayloadSchema>;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { resolve, dirname, basename } from "path";
|
import { resolve, dirname, basename } from "path";
|
||||||
import { existsSync, realpathSync } from "fs";
|
import { existsSync, realpathSync } from "fs";
|
||||||
import { open as openFile, stat as statFile } from "fs/promises";
|
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 type { ParsedDiffFile } from "../server/utils/diff-highlighter.js";
|
||||||
import { parseAndHighlightDiff } from "../server/utils/diff-highlighter.js";
|
import { parseAndHighlightDiff } from "../server/utils/diff-highlighter.js";
|
||||||
import {
|
import {
|
||||||
|
|||||||
Reference in New Issue
Block a user