Merge branch 'research/mcp-list-agents-summary'
This commit is contained in:
@@ -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";
|
||||
@@ -38,7 +44,7 @@ import {
|
||||
AgentStatusEnum,
|
||||
ProviderSummarySchema,
|
||||
parseDurationString,
|
||||
resolveProviderAndModel,
|
||||
resolveRequiredProviderModel,
|
||||
sanitizePermissionRequest,
|
||||
sendPromptToAgent,
|
||||
setupFinishNotification,
|
||||
@@ -126,6 +132,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;
|
||||
@@ -257,7 +325,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,
|
||||
})
|
||||
@@ -307,21 +375,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()
|
||||
@@ -333,10 +414,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
|
||||
@@ -370,10 +450,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
|
||||
@@ -421,8 +500,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(
|
||||
@@ -472,7 +551,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(),
|
||||
@@ -508,11 +587,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;
|
||||
@@ -533,11 +613,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;
|
||||
@@ -929,15 +1010,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) =>
|
||||
@@ -950,10 +1045,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 }),
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -1321,7 +1423,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(),
|
||||
|
||||
Reference in New Issue
Block a user