From eafb357ebd1518e9ea645a1a8c0a71ee94f1ac68 Mon Sep 17 00:00:00 2001 From: Jake Ruesink Date: Mon, 18 May 2026 16:35:26 -0500 Subject: [PATCH 1/5] Implement v1 manager task tools for multi-task coordination --- packages/core/src/index.ts | 4 ++- packages/core/src/tools.ts | 31 ++++++++++++++++++- packages/core/src/types.ts | 14 +++++++++ packages/mcp/src/server.ts | 63 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 2 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9ed0a98..4b3404f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,7 +7,7 @@ export { searchMemory, getMemory, listCollections, DEFAULT_QMD_URL } from "./mem export type { MemorySearchHit, MemorySearchResult, GetMemoryResult, SearchMemoryOpts, CollectionListing } from "./memory.ts"; export { classifyError } from "./errors.ts"; export { emptyArtifacts, processEvent, extractPatternsFromSummary, deriveNextStep } from "./artifacts.ts"; -export { runTask, checkTask, listSessions } from "./tools.ts"; +export { runTask, checkTask, listSessions, listTasks } from "./tools.ts"; export type { Artifacts, CheckMode, @@ -24,4 +24,6 @@ export type { LogEntry, RunTaskResult, TaskInput, + TaskSummary, + TaskStatus, } from "./types.ts"; diff --git a/packages/core/src/tools.ts b/packages/core/src/tools.ts index 6b2c650..fa3b1a6 100644 --- a/packages/core/src/tools.ts +++ b/packages/core/src/tools.ts @@ -4,6 +4,7 @@ import type { CheckTaskResult, ContinuationState, RunTaskResult, + TaskSummary, TaskInput, } from "./types.ts"; @@ -18,12 +19,41 @@ export function runTask(pool: GatewayPool, input: TaskInput): RunTaskResult { pool.rememberJob(job.jobId, entry.agent.id); return { jobId: job.jobId, + taskId: job.jobId, sessionKey: job.sessionKey, status: "running", agent: entry.agent.id, }; } +function mapTaskStatus(status: string): TaskSummary["status"] { + if (status === "running") return "running"; + if (status === "completed" || status === "completed_no_summary") return "done"; + return "failed"; +} + +export function listTasks(pool: GatewayPool): TaskSummary[] { + const items: TaskSummary[] = []; + for (const entry of pool.allEntries()) { + for (const session of entry.sessions.listSessions()) { + const job = entry.sessions.getLatestJobForSession(session.sessionKey); + if (!job) continue; + items.push({ + taskId: job.jobId, + jobId: job.jobId, + sessionKey: job.sessionKey, + agent: entry.agent.id, + status: mapTaskStatus(job.status), + startedAt: job.startedAt, + lastEventAt: job.lastEventAt, + summary: job.summary ?? session.lastSummary, + error: job.error, + }); + } + } + return items; +} + function notFound(): CheckTaskResult { return { found: false }; } @@ -70,4 +100,3 @@ export function listSessions(pool: GatewayPool): ContinuationState[] { } return all; } - diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 7491d43..3d85ed3 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -37,6 +37,7 @@ export type ErrorInfo = { export type LogEntry = { ts: number; type: string; text: string }; export type JobStatus = "running" | "completed" | "completed_no_summary" | "error"; +export type TaskStatus = "queued" | "running" | "blocked" | "needs-human" | "done" | "failed"; export type Job = { jobId: string; @@ -111,12 +112,25 @@ export type CheckMode = "poll" | "wait"; export type RunTaskResult = { jobId: string; + taskId?: string; sessionKey: string; status: "running"; /** ClawConnect agent alias the task was dispatched to. */ agent?: string; }; +export type TaskSummary = { + taskId: string; + jobId: string; + sessionKey: string; + agent?: string; + status: TaskStatus; + startedAt: number; + lastEventAt: number; + summary?: string; + error?: string; +}; + export type CheckTaskOpts = { jobId?: string; sessionKey?: string; diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index 8f4e3e6..453358e 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -4,6 +4,7 @@ import { GatewayPool, runTask, checkTask, + listTasks, listSessions, agentBlurb, agentDescriptor, @@ -17,6 +18,7 @@ import type { CheckTaskResult, ContinuationState, RunTaskResult, + TaskSummary, } from "@clawconnect/core"; // ── Provider config ───────────────────────────────────────────────────────── @@ -36,6 +38,7 @@ export type ProviderConfig = { formatRunTask?: (result: RunTaskResult) => McpToolResponse; formatCheckTask?: (result: CheckTaskResult) => McpToolResponse; formatListSessions?: (result: ContinuationState[]) => McpToolResponse; + formatListTasks?: (result: TaskSummary[]) => McpToolResponse; }; // ── Default formatters (optimized for agentic use / Claude Code) ──────────── @@ -129,6 +132,12 @@ function defaultFormatListSessions(result: ContinuationState[]): McpToolResponse }; } +function defaultFormatListTasks(result: TaskSummary[]): McpToolResponse { + return { + content: [{ type: "text", text: JSON.stringify({ tasks: result }) }], + }; +} + // ── Server factory ────────────────────────────────────────────────────────── export function createMcpServer(config: { registry: AgentRegistry; provider?: ProviderConfig }) { @@ -144,6 +153,7 @@ export function createMcpServer(config: { registry: AgentRegistry; provider?: Pr const fmtRun = provider.formatRunTask ?? defaultFormatRunTask; const fmtCheck = provider.formatCheckTask ?? defaultFormatCheckTask; const fmtList = provider.formatListSessions ?? defaultFormatListSessions; + const fmtListTasks = provider.formatListTasks ?? defaultFormatListTasks; const agentIds = config.registry.agents.map((a) => a.id); const agentBlurbs = config.registry.agents.map(agentBlurb).join("; "); @@ -208,6 +218,59 @@ Pass the jobId returned by run_task. Available agents: ${agentList}.`, }, ); + server.tool( + "list_tasks", + `List manager-friendly task summaries across agents. This is task-level coordination (what needs attention), not low-level session debugging.`, + { + view: z.enum(["active", "all"]).optional().describe('Optional preset. "active" returns running tasks only.'), + }, + { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + async ({ view }) => { + const tasks = listTasks(pool); + const filtered = view === "active" ? tasks.filter((t) => t.status === "running") : tasks; + return fmtListTasks(filtered); + }, + ); + + server.tool( + "get_task", + `Inspect a task by taskId/jobId and optionally include updates/logs/artifacts for richer manager status.`, + { + taskId: z.string().describe("Task identifier (same as jobId in v1)"), + include: z.array(z.enum(["summary", "updates", "artifacts", "diagnostics"])).optional(), + mode: z.enum(["poll", "wait"]).optional().describe('Uses check semantics: "wait" blocks up to timeout; "poll" returns on updates'), + }, + { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + async ({ taskId, include, mode }) => { + const result = await checkTask(pool, { jobId: taskId, mode: (mode as CheckMode) ?? defaultMode }); + if (!result.found) return defaultFormatCheckTask(result); + const snapshot = result.snapshot; + return { + content: [ + { + type: "text", + text: JSON.stringify({ + taskId: snapshot.jobId, + jobId: snapshot.jobId, + sessionKey: snapshot.sessionKey, + agent: snapshot.agent, + status: snapshot.status, + startedAt: snapshot.startedAt, + lastEventAt: snapshot.lastEventAt, + summary: include?.length ? (include.includes("summary") ? snapshot.summary : undefined) : snapshot.summary, + updates: include?.includes("updates") ? snapshot.logs : undefined, + artifacts: include?.includes("artifacts") ? snapshot.artifacts : undefined, + diagnostics: include?.includes("diagnostics") + ? { error: snapshot.error, errorInfo: snapshot.errorInfo, continuationState: snapshot.continuationState } + : undefined, + }), + }, + ], + ...(result.isError ? { isError: true } : {}), + }; + }, + ); + server.tool( "list_sessions", `List active OpenClaw sessions across configured agents. Shows agent, session keys, last job status, and recommended next steps. Available agents: ${agentList}.`, From 521d3975cb5b660e92c2841a1c9c2fdd49990eee Mon Sep 17 00:00:00 2001 From: Jake Ruesink Date: Mon, 18 May 2026 23:08:17 -0500 Subject: [PATCH 2/5] Add session inspection tool and refine task status/details --- packages/core/src/index.ts | 4 ++- packages/core/src/tools.ts | 51 ++++++++++++++++++++++++++++++++++++-- packages/core/src/types.ts | 18 ++++++++++++++ packages/mcp/src/server.ts | 28 ++++++++++++++++++++- 4 files changed, 97 insertions(+), 4 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4b3404f..b852a59 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,7 +7,7 @@ export { searchMemory, getMemory, listCollections, DEFAULT_QMD_URL } from "./mem export type { MemorySearchHit, MemorySearchResult, GetMemoryResult, SearchMemoryOpts, CollectionListing } from "./memory.ts"; export { classifyError } from "./errors.ts"; export { emptyArtifacts, processEvent, extractPatternsFromSummary, deriveNextStep } from "./artifacts.ts"; -export { runTask, checkTask, listSessions, listTasks } from "./tools.ts"; +export { runTask, checkTask, listSessions, listTasks, getSession } from "./tools.ts"; export type { Artifacts, CheckMode, @@ -23,6 +23,8 @@ export type { JobStatus, LogEntry, RunTaskResult, + SessionInspectMode, + SessionInspectResult, TaskInput, TaskSummary, TaskStatus, diff --git a/packages/core/src/tools.ts b/packages/core/src/tools.ts index fa3b1a6..4545d85 100644 --- a/packages/core/src/tools.ts +++ b/packages/core/src/tools.ts @@ -4,6 +4,8 @@ import type { CheckTaskResult, ContinuationState, RunTaskResult, + SessionInspectMode, + SessionInspectResult, TaskSummary, TaskInput, } from "./types.ts"; @@ -32,6 +34,14 @@ function mapTaskStatus(status: string): TaskSummary["status"] { return "failed"; } +function deriveTaskStatus(job: { status: string; error?: string; artifacts: { needsHumanDecision: boolean } }): TaskSummary["status"] { + if (job.status === "running") return "running"; + if (job.status === "completed" || job.status === "completed_no_summary") return "done"; + if (job.artifacts.needsHumanDecision) return "needs-human"; + if (job.error?.includes("session busy")) return "blocked"; + return mapTaskStatus(job.status); +} + export function listTasks(pool: GatewayPool): TaskSummary[] { const items: TaskSummary[] = []; for (const entry of pool.allEntries()) { @@ -43,10 +53,10 @@ export function listTasks(pool: GatewayPool): TaskSummary[] { jobId: job.jobId, sessionKey: job.sessionKey, agent: entry.agent.id, - status: mapTaskStatus(job.status), + status: deriveTaskStatus(job), startedAt: job.startedAt, lastEventAt: job.lastEventAt, - summary: job.summary ?? session.lastSummary, + summary: job.summary, error: job.error, }); } @@ -100,3 +110,40 @@ export function listSessions(pool: GatewayPool): ContinuationState[] { } return all; } + +export function getSession( + pool: GatewayPool, + opts: { sessionId: string; mode?: SessionInspectMode; limit?: number; after?: number; agent?: string }, +): SessionInspectResult { + let entry = opts.agent ? pool.forAgent(opts.agent) : pool.forSession(opts.sessionId); + if (!entry) { + for (const candidate of pool.allEntries()) { + if (candidate.sessions.getSessionState(opts.sessionId)) { + entry = candidate; + break; + } + } + } + if (!entry) return { found: false }; + const job = entry.sessions.getLatestJobForSession(opts.sessionId); + if (!job) return { found: false }; + + const mode = opts.mode ?? "snapshot"; + const limit = Math.max(1, Math.min(200, opts.limit ?? 50)); + const after = Math.max(0, opts.after ?? 0); + const events = job.logs.slice(after, after + limit); + + return { + found: true, + sessionKey: job.sessionKey, + agent: entry.agent.id, + jobId: job.jobId, + status: job.status, + startedAt: job.startedAt, + lastEventAt: job.lastEventAt, + summary: job.summary, + error: job.error, + ...(mode === "snapshot" ? {} : { events }), + ...(mode === "tail" ? { nextAfter: after + events.length } : {}), + }; +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3d85ed3..1e1366d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -131,6 +131,24 @@ export type TaskSummary = { error?: string; }; +export type SessionInspectMode = "snapshot" | "events" | "tail"; + +export type SessionInspectResult = + | { found: false } + | { + found: true; + sessionKey: string; + agent?: string; + jobId: string; + status: JobStatus; + startedAt: number; + lastEventAt: number; + summary?: string; + error?: string; + events?: LogEntry[]; + nextAfter?: number; + }; + export type CheckTaskOpts = { jobId?: string; sessionKey?: string; diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index 453358e..09049ff 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -4,6 +4,7 @@ import { GatewayPool, runTask, checkTask, + getSession, listTasks, listSessions, agentBlurb, @@ -19,6 +20,7 @@ import type { ContinuationState, RunTaskResult, TaskSummary, + SessionInspectResult, } from "@clawconnect/core"; // ── Provider config ───────────────────────────────────────────────────────── @@ -39,6 +41,7 @@ export type ProviderConfig = { formatCheckTask?: (result: CheckTaskResult) => McpToolResponse; formatListSessions?: (result: ContinuationState[]) => McpToolResponse; formatListTasks?: (result: TaskSummary[]) => McpToolResponse; + formatGetSession?: (result: SessionInspectResult) => McpToolResponse; }; // ── Default formatters (optimized for agentic use / Claude Code) ──────────── @@ -138,6 +141,11 @@ function defaultFormatListTasks(result: TaskSummary[]): McpToolResponse { }; } +function defaultFormatGetSession(result: SessionInspectResult): McpToolResponse { + if (!result.found) return { content: [{ type: "text", text: "Session not found." }], isError: true }; + return { content: [{ type: "text", text: JSON.stringify(result) }] }; +} + // ── Server factory ────────────────────────────────────────────────────────── export function createMcpServer(config: { registry: AgentRegistry; provider?: ProviderConfig }) { @@ -154,6 +162,7 @@ export function createMcpServer(config: { registry: AgentRegistry; provider?: Pr const fmtCheck = provider.formatCheckTask ?? defaultFormatCheckTask; const fmtList = provider.formatListSessions ?? defaultFormatListSessions; const fmtListTasks = provider.formatListTasks ?? defaultFormatListTasks; + const fmtGetSession = provider.formatGetSession ?? defaultFormatGetSession; const agentIds = config.registry.agents.map((a) => a.id); const agentBlurbs = config.registry.agents.map(agentBlurb).join("; "); @@ -257,7 +266,7 @@ Pass the jobId returned by run_task. Available agents: ${agentList}.`, status: snapshot.status, startedAt: snapshot.startedAt, lastEventAt: snapshot.lastEventAt, - summary: include?.length ? (include.includes("summary") ? snapshot.summary : undefined) : snapshot.summary, + summary: include ? (include.includes("summary") ? snapshot.summary : undefined) : snapshot.summary, updates: include?.includes("updates") ? snapshot.logs : undefined, artifacts: include?.includes("artifacts") ? snapshot.artifacts : undefined, diagnostics: include?.includes("diagnostics") @@ -271,6 +280,23 @@ Pass the jobId returned by run_task. Available agents: ${agentList}.`, }, ); + server.tool( + "get_session", + `Inspect one session for debugging ("what exactly happened?"). Use mode="snapshot" for current state, "events" for bounded event retrieval, or "tail" for cursor-based tailing.`, + { + sessionId: z.string().describe("Session key to inspect"), + mode: z.enum(["snapshot", "events", "tail"]).optional(), + limit: z.number().int().positive().max(200).optional().describe("Max events to return for events/tail modes"), + after: z.number().int().nonnegative().optional().describe("Zero-based event cursor; for tail mode use returned nextAfter"), + agent: agentEnum.optional().describe(`${agentDescription} Usually inferred from sessionId.`), + }, + { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + async ({ sessionId, mode, limit, after, agent }) => { + const result = getSession(pool, { sessionId, mode, limit, after, agent }); + return fmtGetSession(result); + }, + ); + server.tool( "list_sessions", `List active OpenClaw sessions across configured agents. Shows agent, session keys, last job status, and recommended next steps. Available agents: ${agentList}.`, From 92454d2a515679832e8d42cee2d9ef2ed64d73aa Mon Sep 17 00:00:00 2001 From: Jake Ruesink Date: Mon, 18 May 2026 23:21:38 -0500 Subject: [PATCH 3/5] fix(tools,mcp): preserve explicit task statuses in mapTaskStatus; broaden active view - mapTaskStatus now preserves needs-human, blocked, and queued instead of collapsing them to failed. deriveTaskStatus already handled these before falling through, but the fallback path no longer loses semantics. - list_tasks(view='active') now includes all non-terminal statuses (queued, running, blocked, needs-human) instead of only running. - Add isActiveTaskStatus predicate with ACTIVE_STATUSES set for clarity. Review feedback addressed: - CodeRabbit: mapTaskStatus can collapse needs-human/blocked to failed - CodeRabbit: active view should mean non-terminal, not just running --- packages/core/src/tools.ts | 3 +++ packages/mcp/src/server.ts | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/core/src/tools.ts b/packages/core/src/tools.ts index 4545d85..36c7bc1 100644 --- a/packages/core/src/tools.ts +++ b/packages/core/src/tools.ts @@ -31,6 +31,9 @@ export function runTask(pool: GatewayPool, input: TaskInput): RunTaskResult { function mapTaskStatus(status: string): TaskSummary["status"] { if (status === "running") return "running"; if (status === "completed" || status === "completed_no_summary") return "done"; + if (status === "needs-human") return "needs-human"; + if (status === "blocked") return "blocked"; + if (status === "queued") return "queued"; return "failed"; } diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index 09049ff..15cc4a9 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -23,6 +23,18 @@ import type { SessionInspectResult, } from "@clawconnect/core"; +/** Non-terminal task statuses — tasks that still need attention. */ +const ACTIVE_STATUSES: ReadonlySet = new Set([ + "queued", + "running", + "blocked", + "needs-human", +]); + +function isActiveTaskStatus(status: TaskSummary["status"]): boolean { + return ACTIVE_STATUSES.has(status); +} + // ── Provider config ───────────────────────────────────────────────────────── type McpToolResponse = { @@ -231,12 +243,12 @@ Pass the jobId returned by run_task. Available agents: ${agentList}.`, "list_tasks", `List manager-friendly task summaries across agents. This is task-level coordination (what needs attention), not low-level session debugging.`, { - view: z.enum(["active", "all"]).optional().describe('Optional preset. "active" returns running tasks only.'), + view: z.enum(["active", "all"]).optional().describe('Optional preset. "active" returns non-terminal tasks (queued, running, blocked, needs-human) that still need attention.'), }, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, async ({ view }) => { const tasks = listTasks(pool); - const filtered = view === "active" ? tasks.filter((t) => t.status === "running") : tasks; + const filtered = view === "active" ? tasks.filter((t) => isActiveTaskStatus(t.status)) : tasks; return fmtListTasks(filtered); }, ); From 4abe959578abeef52a83dfc64653e228bfd22b94 Mon Sep 17 00:00:00 2001 From: Jake Ruesink Date: Tue, 19 May 2026 00:01:16 -0500 Subject: [PATCH 4/5] fix(chatgpt): wire get_task, list_tasks, get_session into connector The new manager-style tools (get_task, list_tasks, get_session) were only registered in packages/mcp/src/server.ts (MCP SDK server). The ChatGPT connector has its own hand-rolled JSON-RPC handler and was never updated. Root cause: ChatGPT connector's buildTools() and tools/call handler had no knowledge of get_task / list_tasks / get_session. Calls would hit the "Unknown tool" error path, or the tools were invisible via tools/list. Changes: - Import listTasks, getSession, TaskSummary from @clawconnect/core - Add list_tasks, get_task, get_session tool definitions to buildTools() - Add tools/call handlers with proper scope filtering - get_task.include is respected: undefined = summary only, [] = core only, specific includes add only those fields --- apps/chatgpt/src/index.ts | 158 +++++++++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 1 deletion(-) diff --git a/apps/chatgpt/src/index.ts b/apps/chatgpt/src/index.ts index df15d58..1cfe7f8 100644 --- a/apps/chatgpt/src/index.ts +++ b/apps/chatgpt/src/index.ts @@ -24,13 +24,15 @@ import { runTask, checkTask, listSessions, + listTasks, + getSession, agentBlurb, agentDescriptor, searchMemory, getMemory, listCollections, } from "@clawconnect/core"; -import type { AgentEntry, AgentRegistry, CheckMode } from "@clawconnect/core"; +import type { AgentEntry, AgentRegistry, CheckMode, TaskSummary } from "@clawconnect/core"; // Widget UI is temporarily disabled to keep the surface focused on // run_task / check_task. Re-enable by restoring the widget imports and @@ -144,6 +146,14 @@ function resolveScope(url: URL): Scope { return { allowedIds: allowed, defaultId, serverName }; } +/** Non-terminal task statuses — tasks that still need attention. */ +const ACTIVE_STATUSES: ReadonlySet = new Set([ + "queued", + "running", + "blocked", + "needs-human", +]); + const AGENTS_BY_ID = new Map(registry.agents.map((a) => [a.id, a])); function blurbsFor(ids: string[]): string { @@ -296,6 +306,63 @@ Pass the jobId returned by run_task. Available agents: ${list}.`, inputSchema: { type: "object", properties: {} }, annotations: { title: "List Collections", readOnlyHint: true, idempotentHint: true, openWorldHint: false }, }, + { + name: "list_tasks", + description: `List manager-friendly task summaries across agents. This is task-level coordination (what needs attention), not low-level session debugging.`, + inputSchema: { + type: "object", + properties: { + view: { + type: "string", + enum: ["active", "all"], + description: 'Optional preset. "active" returns non-terminal tasks (queued, running, blocked, needs-human) that still need attention.', + }, + }, + }, + annotations: { title: "List Tasks", readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + }, + { + name: "get_task", + description: `Inspect a task by taskId/jobId and optionally include updates/logs/artifacts for richer manager status.`, + inputSchema: { + type: "object", + properties: { + taskId: { type: "string", description: "Task identifier (same as jobId in v1)" }, + include: { + type: "array", + items: { type: "string", enum: ["summary", "updates", "artifacts", "diagnostics"] }, + description: 'Which extra fields to include beyond core fields. Omit for summary by default. Pass [] for core fields only.', + }, + mode: { + type: "string", + enum: ["poll", "wait"], + description: 'Uses check semantics: "wait" blocks up to timeout; "poll" returns on updates', + }, + }, + required: ["taskId"], + }, + annotations: { title: "Get Task", readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + }, + { + name: "get_session", + description: `Inspect one session for debugging ("what exactly happened?"). Use mode="snapshot" for current state, "events" for bounded event retrieval, or "tail" for cursor-based tailing.`, + inputSchema: { + type: "object", + properties: { + sessionId: { type: "string", description: "Session key to inspect" }, + mode: { + type: "string", + enum: ["snapshot", "events", "tail"], + description: "Inspection mode: snapshot (default), events, or tail", + }, + limit: { type: "number", description: "Max events to return for events/tail modes (1–200)" }, + after: { type: "number", description: "Zero-based event cursor; for tail mode use returned nextAfter" }, + agent: { ...agentProp, description: `${agentProp.description} Usually inferred from sessionId.` }, + }, + required: ["sessionId"], + }, + annotations: { title: "Get Session", readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + }, ]; } @@ -515,6 +582,95 @@ const server = createServer(async (req, res) => { content: [{ type: "text", text: JSON.stringify({ collections }) }], structuredContent: { collections }, }); + } else if (name === "list_tasks") { + const tasks = listTasks(pool); + const scoped = tasks.filter((t) => !t.agent || scope.allowedIds.includes(t.agent)); + const view = typeof args.view === "string" ? args.view : undefined; + const filtered = view === "active" ? scoped.filter((t) => ACTIVE_STATUSES.has(t.status)) : scoped; + respond({ + content: [{ type: "text", text: JSON.stringify({ tasks: filtered }) }], + structuredContent: { tasks: filtered }, + }); + } else if (name === "get_task") { + const taskId = typeof args.taskId === "string" ? args.taskId : ""; + const include = Array.isArray(args.include) ? (args.include as string[]) : undefined; + const mode = (typeof args.mode === "string" ? args.mode : undefined) as CheckMode | undefined; + const result = await checkTask(pool, { jobId: taskId, mode: mode ?? "wait" }); + + if (!result.found) { + respond({ + content: [{ type: "text", text: "Task not found. The server may have restarted." }], + structuredContent: { taskId, status: "error", error: "Task not found." }, + isError: true, + }); + } else if (result.snapshot.agent && !scope.allowedIds.includes(result.snapshot.agent)) { + respond({ + content: [{ type: "text", text: "Task not found." }], + structuredContent: { taskId, status: "error", error: "Task not found." }, + isError: true, + }); + } else { + const s = result.snapshot; + const payload: Record = { + taskId: s.jobId, + jobId: s.jobId, + sessionKey: s.sessionKey, + agent: s.agent, + status: s.status, + startedAt: s.startedAt, + lastEventAt: s.lastEventAt, + }; + // include logic: undefined = summary by default; [] = core only; ["x"] = x only + if (include === undefined || include.includes("summary")) { + payload.summary = s.summary; + } + if (include?.includes("updates")) { + payload.updates = s.logs; + } + if (include?.includes("artifacts")) { + payload.artifacts = s.artifacts; + } + if (include?.includes("diagnostics")) { + payload.diagnostics = { error: s.error, errorInfo: s.errorInfo, continuationState: s.continuationState }; + } + respond({ + content: [{ type: "text", text: JSON.stringify(payload) }], + structuredContent: payload, + ...(result.isError ? { isError: true } : {}), + }); + } + } else if (name === "get_session") { + const sessionId = typeof args.sessionId === "string" ? args.sessionId : ""; + const sessionMode = typeof args.mode === "string" ? args.mode : undefined; + const limit = args.limit !== undefined ? Number(args.limit) : undefined; + const after = args.after !== undefined ? Number(args.after) : undefined; + const sessionAgent = typeof args.agent === "string" && args.agent ? args.agent : undefined; + + if (sessionAgent && !scope.allowedIds.includes(sessionAgent)) { + respond({ + content: [{ type: "text", text: `Agent "${sessionAgent}" is not available on this connection.` }], + isError: true, + }); + } else { + const result = getSession(pool, { sessionId, mode: sessionMode as any, limit, after, agent: sessionAgent }); + if (!result.found) { + respond({ + content: [{ type: "text", text: "Session not found." }], + structuredContent: { sessionId, found: false }, + isError: true, + }); + } else if (result.agent && !scope.allowedIds.includes(result.agent)) { + respond({ + content: [{ type: "text", text: "Session not found." }], + isError: true, + }); + } else { + respond({ + content: [{ type: "text", text: JSON.stringify(result) }], + structuredContent: result, + }); + } + } } else { respondError(-32601, `Unknown tool: ${name}`); } From d94cd1ef1e24e805e846391b30a8a69ef301c22e Mon Sep 17 00:00:00 2001 From: Jake Ruesink Date: Tue, 19 May 2026 00:26:13 -0500 Subject: [PATCH 5/5] refactor: replace get_task include array with detail enum preset Replace include?: ('summary'|'updates'|'artifacts'|'diagnostics')[] with detail?: 'core'|'summary'|'updates'|'artifacts'|'diagnostics'|'full'|'fullWithDiagnostics' Avoids non-empty array tool-bridge issues by using a single enum value instead of an array. Omitted detail defaults to 'summary' (same behavior). Applied in packages/mcp/src/server.ts and apps/chatgpt/src/index.ts. pnpm ready passes cleanly. --- apps/chatgpt/src/index.ts | 24 +++++++++++++----------- packages/mcp/src/server.ts | 26 +++++++++++++++++--------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/apps/chatgpt/src/index.ts b/apps/chatgpt/src/index.ts index 1cfe7f8..4607b51 100644 --- a/apps/chatgpt/src/index.ts +++ b/apps/chatgpt/src/index.ts @@ -323,15 +323,16 @@ Pass the jobId returned by run_task. Available agents: ${list}.`, }, { name: "get_task", - description: `Inspect a task by taskId/jobId and optionally include updates/logs/artifacts for richer manager status.`, + description: `Inspect a task by taskId/jobId with a detail preset controlling which fields are returned.`, inputSchema: { type: "object", properties: { taskId: { type: "string", description: "Task identifier (same as jobId in v1)" }, - include: { - type: "array", - items: { type: "string", enum: ["summary", "updates", "artifacts", "diagnostics"] }, - description: 'Which extra fields to include beyond core fields. Omit for summary by default. Pass [] for core fields only.', + detail: { + type: "string", + enum: ["core", "summary", "updates", "artifacts", "diagnostics", "full", "fullWithDiagnostics"], + description: + 'Detail preset. Omit for summary. core=ids+status only; summary=+summary; updates=+logs; artifacts=+artifacts; diagnostics=+error info; full=core+summary+updates+artifacts; fullWithDiagnostics=full+diagnostics', }, mode: { type: "string", @@ -593,7 +594,7 @@ const server = createServer(async (req, res) => { }); } else if (name === "get_task") { const taskId = typeof args.taskId === "string" ? args.taskId : ""; - const include = Array.isArray(args.include) ? (args.include as string[]) : undefined; + const detail = typeof args.detail === "string" ? args.detail : undefined; const mode = (typeof args.mode === "string" ? args.mode : undefined) as CheckMode | undefined; const result = await checkTask(pool, { jobId: taskId, mode: mode ?? "wait" }); @@ -611,6 +612,8 @@ const server = createServer(async (req, res) => { }); } else { const s = result.snapshot; + const d = detail ?? "summary"; + const has = (field: string) => d === field || d === "full" || d === "fullWithDiagnostics"; const payload: Record = { taskId: s.jobId, jobId: s.jobId, @@ -620,17 +623,16 @@ const server = createServer(async (req, res) => { startedAt: s.startedAt, lastEventAt: s.lastEventAt, }; - // include logic: undefined = summary by default; [] = core only; ["x"] = x only - if (include === undefined || include.includes("summary")) { + if (d === "summary" || has("summary")) { payload.summary = s.summary; } - if (include?.includes("updates")) { + if (has("updates")) { payload.updates = s.logs; } - if (include?.includes("artifacts")) { + if (has("artifacts")) { payload.artifacts = s.artifacts; } - if (include?.includes("diagnostics")) { + if (d === "diagnostics" || d === "fullWithDiagnostics") { payload.diagnostics = { error: s.error, errorInfo: s.errorInfo, continuationState: s.continuationState }; } respond({ diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index 15cc4a9..18e529f 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -255,17 +255,24 @@ Pass the jobId returned by run_task. Available agents: ${agentList}.`, server.tool( "get_task", - `Inspect a task by taskId/jobId and optionally include updates/logs/artifacts for richer manager status.`, + `Inspect a task by taskId/jobId with a detail preset controlling which fields are returned.`, { taskId: z.string().describe("Task identifier (same as jobId in v1)"), - include: z.array(z.enum(["summary", "updates", "artifacts", "diagnostics"])).optional(), + detail: z + .enum(["core", "summary", "updates", "artifacts", "diagnostics", "full", "fullWithDiagnostics"]) + .optional() + .describe( + "Detail preset. Omit for summary. core=ids+status only; summary=+summary; updates=+logs; artifacts=+artifacts; diagnostics=+error info; full=core+summary+updates+artifacts; fullWithDiagnostics=full+diagnostics", + ), mode: z.enum(["poll", "wait"]).optional().describe('Uses check semantics: "wait" blocks up to timeout; "poll" returns on updates'), }, { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, - async ({ taskId, include, mode }) => { + async ({ taskId, detail, mode }) => { const result = await checkTask(pool, { jobId: taskId, mode: (mode as CheckMode) ?? defaultMode }); if (!result.found) return defaultFormatCheckTask(result); const snapshot = result.snapshot; + const d = detail ?? "summary"; + const has = (field: string) => d === field || d === "full" || d === "fullWithDiagnostics"; return { content: [ { @@ -278,12 +285,13 @@ Pass the jobId returned by run_task. Available agents: ${agentList}.`, status: snapshot.status, startedAt: snapshot.startedAt, lastEventAt: snapshot.lastEventAt, - summary: include ? (include.includes("summary") ? snapshot.summary : undefined) : snapshot.summary, - updates: include?.includes("updates") ? snapshot.logs : undefined, - artifacts: include?.includes("artifacts") ? snapshot.artifacts : undefined, - diagnostics: include?.includes("diagnostics") - ? { error: snapshot.error, errorInfo: snapshot.errorInfo, continuationState: snapshot.continuationState } - : undefined, + summary: d === "summary" || has("summary") ? snapshot.summary : undefined, + updates: has("updates") ? snapshot.logs : undefined, + artifacts: has("artifacts") ? snapshot.artifacts : undefined, + diagnostics: + d === "diagnostics" || d === "fullWithDiagnostics" + ? { error: snapshot.error, errorInfo: snapshot.errorInfo, continuationState: snapshot.continuationState } + : undefined, }), }, ],