From 1c24dd28858c86dd123b3230d82fff87a114b9d3 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 17:48:21 +0800 Subject: [PATCH 01/12] fix(credentials): add fallback to ~/.super-multica for custom data dirs When SMC_DATA_DIR is set (e.g., for E2E tests), the credentials lookup now falls back to ~/.super-multica/credentials.json5 if the custom data dir doesn't have its own credentials file. This mirrors the existing fallback pattern in auth-store.ts and removes the need for the SMC_CREDENTIALS_PATH workaround in E2E tests. Lookup order: 1. SMC_CREDENTIALS_PATH env var (explicit override) 2. {DATA_DIR}/credentials.json5 (current data dir) 3. ~/.super-multica/credentials.json5 (default location fallback) Co-Authored-By: Claude Opus 4.6 --- packages/core/src/agent/credentials.ts | 30 ++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/core/src/agent/credentials.ts b/packages/core/src/agent/credentials.ts index 6e6f8363..d93446b9 100644 --- a/packages/core/src/agent/credentials.ts +++ b/packages/core/src/agent/credentials.ts @@ -36,6 +36,7 @@ export type CredentialsConfig = { }; const DEFAULT_CREDENTIALS_PATH = join(DATA_DIR, "credentials.json5"); +const FALLBACK_CREDENTIALS_PATH = join(homedir(), ".super-multica", "credentials.json5"); function expandHome(value: string): string { if (value === "~") return homedir(); @@ -53,9 +54,34 @@ function isTestEnv(): boolean { ); } +/** + * Resolve the credentials file path. + * + * Lookup order: + * 1. SMC_CREDENTIALS_PATH env var (explicit override) + * 2. {DATA_DIR}/credentials.json5 (current data dir, respects SMC_DATA_DIR) + * 3. ~/.super-multica/credentials.json5 (default location fallback — + * allows E2E tests and other custom SMC_DATA_DIR setups to + * share the production credentials) + */ export function getCredentialsPath(): string { - const raw = process.env.SMC_CREDENTIALS_PATH ?? DEFAULT_CREDENTIALS_PATH; - return expandHome(raw); + // Explicit env override — use as-is + if (process.env.SMC_CREDENTIALS_PATH) { + return expandHome(process.env.SMC_CREDENTIALS_PATH); + } + + // Primary: current DATA_DIR + if (existsSync(DEFAULT_CREDENTIALS_PATH)) { + return DEFAULT_CREDENTIALS_PATH; + } + + // Fallback: default ~/.super-multica location when using a custom data dir + if (DEFAULT_CREDENTIALS_PATH !== FALLBACK_CREDENTIALS_PATH && existsSync(FALLBACK_CREDENTIALS_PATH)) { + return FALLBACK_CREDENTIALS_PATH; + } + + // Return primary path even if it doesn't exist (for error messages / creation) + return DEFAULT_CREDENTIALS_PATH; } export class CredentialManager { From 755ed5e9dee6bd9fbd57a3c9b79594954bf24915 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 18:06:42 +0800 Subject: [PATCH 02/12] feat(run-log): add result metadata to tool_end events Enrich tool_end events with result_chars, result_summary, and error_type fields. Since run-log.jsonl is append-only and never compacted, this preserves tool result metadata that would otherwise be lost when session.jsonl undergoes compaction. New fields: - result_chars: total character count of result content - result_summary: short tool-specific summary (e.g. "10 results", "12.5KB", "finance/get_price_snapshot") - error_type: error category for tool errors (e.g. "fetch_failed") Co-Authored-By: Claude Opus 4.6 --- packages/core/src/agent/run-log.ts | 5 ++- packages/core/src/agent/runner.ts | 62 +++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packages/core/src/agent/run-log.ts b/packages/core/src/agent/run-log.ts index e8e3b446..18bf5c80 100644 --- a/packages/core/src/agent/run-log.ts +++ b/packages/core/src/agent/run-log.ts @@ -25,7 +25,10 @@ * - `tool_start` — Tool execution begins. * Fields: tool (name), args (first 500 chars of JSON) * - `tool_end` — Tool execution completes. - * Fields: tool (name), duration_ms, is_error + * Fields: tool (name), duration_ms, is_error, result_chars, result_summary?, error_type? + * result_chars: total character count of result content (survives session compaction) + * result_summary: short tool-specific summary (e.g. "10 results", "12.5KB", "finance/get_price_snapshot") + * error_type: error category when tool returned an error (e.g. "fetch_failed", "ssrf_blocked") * * ### Context Management — Preflight (before LLM call) * - `preflight_compact_start` — Preflight compaction triggered. diff --git a/packages/core/src/agent/runner.ts b/packages/core/src/agent/runner.ts index e561213d..ba652939 100644 --- a/packages/core/src/agent/runner.ts +++ b/packages/core/src/agent/runner.ts @@ -85,6 +85,51 @@ export function isRotatableError(reason: AuthProfileFailureReason): boolean { return reason === "auth" || reason === "rate_limit" || reason === "billing" || reason === "timeout"; } +// ── Run-log result extraction helpers ────────────────────────────────────── +// Lightweight extractors for tool_end metadata. These mirror the patterns in +// cli/output.ts but are kept separate to avoid CLI-specific dependencies. + +function extractRunLogResultText(result: unknown): string | undefined { + if (!result || typeof result !== "object") return undefined; + const msg = result as { content?: Array<{ type: string; text?: string }> }; + if (Array.isArray(msg.content)) { + for (const c of msg.content) { + if (c.type === "text" && c.text) return c.text; + } + } + return undefined; +} + +function extractRunLogResultDetails(result: unknown): Record | null { + const text = extractRunLogResultText(result); + if (text) { + try { return JSON.parse(text) as Record; } catch { /* non-JSON result */ } + } + const withDetails = result as { details?: unknown }; + if (withDetails?.details && typeof withDetails.details === "object") { + return withDetails.details as Record; + } + return null; +} + +function formatRunLogToolSummary(tool: string, details: Record | null): string | undefined { + if (!details) return undefined; + if (details.error) return `error: ${details.message || details.error}`; + switch (tool) { + case "web_search": return `${details.count ?? 0} results`; + case "web_fetch": { + const parts: string[] = []; + if (typeof details.length === "number") parts.push(`${(details.length as number / 1024).toFixed(1)}KB`); + if (details.cached) parts.push("cached"); + return parts.join(", ") || undefined; + } + case "data": return `${details.domain}/${details.action}`; + case "glob": return `${details.count ?? 0} files`; + case "exec": return details.exitCode !== undefined ? `exit ${details.exitCode}` : undefined; + default: return undefined; + } +} + export class Agent { private readonly agent: PiAgentCore; private output; @@ -746,11 +791,24 @@ export class Agent { const startTime = this.toolStartTimes.get(toolName); const duration_ms = startTime ? Date.now() - startTime : undefined; this.toolStartTimes.delete(toolName); - this.runLog.log("tool_end", { + + // Extract result metadata for run-log persistence (survives session compaction) + const result = (event as any).result; + const resultText = extractRunLogResultText(result); + const resultChars = resultText?.length ?? 0; + const details = extractRunLogResultDetails(result); + + const toolEndData: Record = { tool: toolName, duration_ms, is_error: (event as any).isError ?? false, - }); + result_chars: resultChars, + result_summary: formatRunLogToolSummary(toolName, details), + }; + if (details?.error) { + toolEndData.error_type = String(details.error); + } + this.runLog.log("tool_end", toolEndData); } } From b007ddffc8b8f9a162be3be888a1ef68489808d9 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 18:06:51 +0800 Subject: [PATCH 03/12] feat(cli): initialize Hub in run mode for full agent capabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Always initialize Hub in CLI run mode to match the Desktop environment where Hub is always active. This enables sessions_spawn (sub-agent creation), cron tasks, channel plugins, and other Hub-dependent features during E2E testing. Hub constructor is non-blocking — gateway connection failures are handled gracefully with auto-reconnect. hub.shutdown() in finally block ensures clean teardown on exit. Co-Authored-By: Claude Opus 4.6 --- apps/cli/src/commands/run.ts | 66 +++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/apps/cli/src/commands/run.ts b/apps/cli/src/commands/run.ts index 1b915d0d..c5fb6916 100644 --- a/apps/cli/src/commands/run.ts +++ b/apps/cli/src/commands/run.ts @@ -7,7 +7,7 @@ */ import { join } from "node:path"; -import { Agent } from "@multica/core"; +import { Agent, Hub } from "@multica/core"; import type { AgentOptions } from "@multica/core"; import type { ToolsConfig } from "@multica/core"; import { DATA_DIR } from "@multica/utils"; @@ -192,35 +192,45 @@ export async function runCommand(args: string[]): Promise { const enableRunLog = opts.runLog || !!process.env.MULTICA_RUN_LOG; - const agent = new Agent({ - profileId: opts.profile, - provider: opts.provider, - model: opts.model, - apiKey: opts.apiKey, - baseUrl: opts.baseUrl, - systemPrompt: opts.system, - thinkingLevel: opts.thinking as any, - reasoningMode: opts.reasoning as AgentOptions["reasoningMode"], - cwd: opts.cwd, - sessionId: opts.session, - debug: opts.debug, - enableRunLog, - tools: toolsConfig, - }); + // Initialize Hub to enable full agent capabilities (sub-agents, channels, cron). + // Matches Desktop environment where Hub is always active. + // Gateway connection failures are non-blocking (auto-reconnect with backoff). + const gatewayUrl = process.env.GATEWAY_URL || "http://localhost:3000"; + const hub = new Hub(gatewayUrl); - const sessionDir = join(DATA_DIR, "sessions", agent.sessionId); + try { + const agent = new Agent({ + profileId: opts.profile, + provider: opts.provider, + model: opts.model, + apiKey: opts.apiKey, + baseUrl: opts.baseUrl, + systemPrompt: opts.system, + thinkingLevel: opts.thinking as any, + reasoningMode: opts.reasoning as AgentOptions["reasoningMode"], + cwd: opts.cwd, + sessionId: opts.session, + debug: opts.debug, + enableRunLog, + tools: toolsConfig, + }); - // If it's a newly created session, notify user of sessionId - if (!opts.session) { - console.error(`[session: ${agent.sessionId}]`); - } - if (enableRunLog) { - console.error(`[session-dir: ${sessionDir}]`); - } + const sessionDir = join(DATA_DIR, "sessions", agent.sessionId); - const result = await agent.run(finalPrompt); - if (result.error) { - console.error(`Error: ${result.error}`); - process.exitCode = 1; + // If it's a newly created session, notify user of sessionId + if (!opts.session) { + console.error(`[session: ${agent.sessionId}]`); + } + if (enableRunLog) { + console.error(`[session-dir: ${sessionDir}]`); + } + + const result = await agent.run(finalPrompt); + if (result.error) { + console.error(`Error: ${result.error}`); + process.exitCode = 1; + } + } finally { + hub.shutdown(); } } From 02ed09b77b15bddbec5c49d6d9e7e22d0fd48743 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 18:47:37 +0800 Subject: [PATCH 04/12] fix(tools): use boolean error flag in web_fetch and web_search error responses Return error: true (boolean) with code field instead of error: "string_code" to match ToolErrorPayload convention. Also update runner.ts formatRunLogToolSummary to prefer details.code for error categorization. Co-Authored-By: Claude Opus 4.6 --- packages/core/src/agent/tools/web/web-fetch.ts | 6 ++++-- packages/core/src/agent/tools/web/web-search.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/core/src/agent/tools/web/web-fetch.ts b/packages/core/src/agent/tools/web/web-fetch.ts index 32a1e079..476e9400 100644 --- a/packages/core/src/agent/tools/web/web-fetch.ts +++ b/packages/core/src/agent/tools/web/web-fetch.ts @@ -343,12 +343,14 @@ export function createWebFetchTool(): AgentTool } catch (error) { if (error instanceof SsrfBlockedError) { return jsonResult({ - error: "ssrf_blocked", + error: true, + code: "ssrf_blocked", message: error.message, }); } return jsonResult({ - error: "fetch_failed", + error: true, + code: "fetch_failed", message: error instanceof Error ? error.message : String(error), }); } diff --git a/packages/core/src/agent/tools/web/web-search.ts b/packages/core/src/agent/tools/web/web-search.ts index 5d2bece1..5dff6ed8 100644 --- a/packages/core/src/agent/tools/web/web-search.ts +++ b/packages/core/src/agent/tools/web/web-search.ts @@ -135,7 +135,8 @@ export function createWebSearchTool(): AgentTool Date: Sun, 15 Feb 2026 18:47:44 +0800 Subject: [PATCH 05/12] fix(tools): show findings and full runId in sessions_list list view Grouped runs now display findings for completed sub-agents (up to 4000 chars). Ungrouped runs increased truncation from 200 to 4000 chars. All status lines include full runId for subsequent API queries. Co-Authored-By: Claude Opus 4.6 --- .../src/agent/tools/sessions-list.test.ts | 49 +++++++++++++++++++ .../core/src/agent/tools/sessions-list.ts | 13 +++-- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/packages/core/src/agent/tools/sessions-list.test.ts b/packages/core/src/agent/tools/sessions-list.test.ts index 230d8997..637a8537 100644 --- a/packages/core/src/agent/tools/sessions-list.test.ts +++ b/packages/core/src/agent/tools/sessions-list.test.ts @@ -47,6 +47,8 @@ describe("sessions_list tool", () => { startedAt: now - 60000, endedAt: now - 30000, outcome: { status: "ok" }, + findings: "All tests passed successfully.", + findingsCaptured: true, }), ); seedSubagentRunForTests( @@ -56,6 +58,8 @@ describe("sessions_list tool", () => { startedAt: now - 60000, endedAt: now, outcome: { status: "error", error: "timeout" }, + findings: "Lint check timed out.", + findingsCaptured: true, }), ); @@ -71,6 +75,13 @@ describe("sessions_list tool", () => { expect((text as { text: string }).text).toContain("Code Review"); expect((text as { text: string }).text).toContain("Test Analysis"); expect((text as { text: string }).text).toContain("Lint Check"); + // Verify full runId is shown for completed runs + expect((text as { text: string }).text).toContain("id:run-aaa"); + expect((text as { text: string }).text).toContain("id:run-bbb"); + expect((text as { text: string }).text).toContain("id:run-ccc"); + // Verify findings are shown for completed runs + expect((text as { text: string }).text).toContain("All tests passed successfully."); + expect((text as { text: string }).text).toContain("Lint check timed out."); expect(result.details!.runs).toHaveLength(3); expect(result.details!.runs[0]!.status).toBe("running"); @@ -141,6 +152,44 @@ describe("sessions_list tool", () => { expect(result.details).toEqual({ runs: [] }); }); + it("shows findings for grouped completed runs", async () => { + const now = Date.now(); + const groupId = "group-001"; + seedSubagentRunForTests( + makeRecord({ + runId: "run-g1", + label: "Bull Case Research", + startedAt: now - 60000, + endedAt: now - 10000, + outcome: { status: "ok" }, + findings: "AI infrastructure capex growing 40% YoY.", + findingsCaptured: true, + groupId, + }), + ); + seedSubagentRunForTests( + makeRecord({ + runId: "run-g2", + label: "Bear Case Research", + startedAt: now - 60000, + endedAt: now - 5000, + outcome: { status: "ok" }, + findings: "Valuation risk: forward P/E above historical average.", + findingsCaptured: true, + groupId, + }), + ); + + const tool = createSessionsListTool({ sessionId: "parent-001" }); + const result = await tool.execute("call-1", {}); + + const text = (result.content[0] as { text: string }).text; + expect(text).toContain("id:run-g1"); + expect(text).toContain("id:run-g2"); + expect(text).toContain("AI infrastructure capex growing 40% YoY."); + expect(text).toContain("Valuation risk: forward P/E above historical average."); + }); + it("shows findings status for running task", async () => { const now = Date.now(); seedSubagentRunForTests( diff --git a/packages/core/src/agent/tools/sessions-list.ts b/packages/core/src/agent/tools/sessions-list.ts index 93ef896c..8905b943 100644 --- a/packages/core/src/agent/tools/sessions-list.ts +++ b/packages/core/src/agent/tools/sessions-list.ts @@ -212,10 +212,13 @@ export function createSessionsListTool( const status = resolveStatus(r); if (status === "running") { const elapsed = r.startedAt ? formatElapsed(now - r.startedAt) : "just spawned"; - statusLines.push(` ${idx}. [RUNNING] "${displayName}" (${elapsed})`); + statusLines.push(` ${idx}. [RUNNING] "${displayName}" (${elapsed}) id:${r.runId}`); } else { const elapsed = r.startedAt && r.endedAt ? formatElapsed(r.endedAt - r.startedAt) : ""; - statusLines.push(` ${idx}. [${status.toUpperCase()}] "${displayName}" (${elapsed})`); + const findings = r.findingsCaptured + ? (r.findings ? r.findings.slice(0, 4000) + (r.findings.length > 4000 ? "…" : "") : "(no output)") + : "(findings not yet captured)"; + statusLines.push(` ${idx}. [${status.toUpperCase()}] "${displayName}" (${elapsed}) id:${r.runId}\n Findings: ${findings}`); } } } @@ -227,13 +230,13 @@ export function createSessionsListTool( const status = resolveStatus(r); if (status === "running") { const elapsed = r.startedAt ? formatElapsed(now - r.startedAt) : "just spawned"; - statusLines.push(` ${idx}. [RUNNING] "${displayName}" (${elapsed})`); + statusLines.push(` ${idx}. [RUNNING] "${displayName}" (${elapsed}) id:${r.runId}`); } else { const elapsed = r.startedAt && r.endedAt ? formatElapsed(r.endedAt - r.startedAt) : ""; const findings = r.findingsCaptured - ? (r.findings ? r.findings.slice(0, 200) + (r.findings.length > 200 ? "…" : "") : "(no output)") + ? (r.findings ? r.findings.slice(0, 4000) + (r.findings.length > 4000 ? "…" : "") : "(no output)") : "(findings not yet captured)"; - statusLines.push(` ${idx}. [${status.toUpperCase()}] "${displayName}" (${elapsed})\n Findings: ${findings}`); + statusLines.push(` ${idx}. [${status.toUpperCase()}] "${displayName}" (${elapsed}) id:${r.runId}\n Findings: ${findings}`); } } From a254daff013ce39f0d185d9057329fab0774302f Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 18:47:53 +0800 Subject: [PATCH 06/12] feat(agent): enable parallel tool execution via pi-agent-core patch Replace sequential for+await tool dispatch with Promise.allSettled for parallel execution. All tool_execution_start events emit immediately, tools run concurrently, results are processed in original order. Also fix run-log toolStartTimes to key by toolCallId instead of toolName to prevent collisions with parallel same-name tools. Co-Authored-By: Claude Opus 4.6 --- package.json | 3 + packages/core/src/agent/runner.ts | 12 +-- .../@mariozechner__pi-agent-core@0.52.9.patch | 80 +++++++++++++++++++ pnpm-lock.yaml | 35 ++++---- 4 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 patches/@mariozechner__pi-agent-core@0.52.9.patch diff --git a/package.json b/package.json index 80b868f4..d39e6145 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,9 @@ "overrides": { "@types/react": "catalog:", "@types/react-dom": "catalog:" + }, + "patchedDependencies": { + "@mariozechner/pi-agent-core@0.52.9": "patches/@mariozechner__pi-agent-core@0.52.9.patch" } }, "devDependencies": { diff --git a/packages/core/src/agent/runner.ts b/packages/core/src/agent/runner.ts index ba652939..c9f81f03 100644 --- a/packages/core/src/agent/runner.ts +++ b/packages/core/src/agent/runner.ts @@ -114,7 +114,7 @@ function extractRunLogResultDetails(result: unknown): Record | function formatRunLogToolSummary(tool: string, details: Record | null): string | undefined { if (!details) return undefined; - if (details.error) return `error: ${details.message || details.error}`; + if (details.error) return `error: ${details.code || details.message || details.error}`; switch (tool) { case "web_search": return `${details.count ?? 0} results`; case "web_fetch": { @@ -780,17 +780,19 @@ export class Agent { private handleRunLogEvent(event: AgentEvent) { if (event.type === "tool_execution_start") { + const toolCallId = (event as any).toolCallId ?? "unknown"; const toolName = (event as any).toolName ?? "unknown"; - this.toolStartTimes.set(toolName, Date.now()); + this.toolStartTimes.set(toolCallId, Date.now()); this.runLog.log("tool_start", { tool: toolName, args: JSON.stringify((event as any).args ?? {}).slice(0, 500), }); } else if (event.type === "tool_execution_end") { + const toolCallId = (event as any).toolCallId ?? "unknown"; const toolName = (event as any).toolName ?? "unknown"; - const startTime = this.toolStartTimes.get(toolName); + const startTime = this.toolStartTimes.get(toolCallId); const duration_ms = startTime ? Date.now() - startTime : undefined; - this.toolStartTimes.delete(toolName); + this.toolStartTimes.delete(toolCallId); // Extract result metadata for run-log persistence (survives session compaction) const result = (event as any).result; @@ -806,7 +808,7 @@ export class Agent { result_summary: formatRunLogToolSummary(toolName, details), }; if (details?.error) { - toolEndData.error_type = String(details.error); + toolEndData.error_type = details.code ? String(details.code) : String(details.error); } this.runLog.log("tool_end", toolEndData); } diff --git a/patches/@mariozechner__pi-agent-core@0.52.9.patch b/patches/@mariozechner__pi-agent-core@0.52.9.patch new file mode 100644 index 00000000..e570a653 --- /dev/null +++ b/patches/@mariozechner__pi-agent-core@0.52.9.patch @@ -0,0 +1,80 @@ +diff --git a/dist/agent-loop.js b/dist/agent-loop.js +index b26d45753809da14df6409354e8c537684b9acd7..931399cc12bafea940fe99ab74ae288d71506e52 100644 +--- a/dist/agent-loop.js ++++ b/dist/agent-loop.js +@@ -206,17 +206,18 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn + */ + async function executeToolCalls(tools, assistantMessage, signal, stream, getSteeringMessages) { + const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall"); +- const results = []; +- let steeringMessages; +- for (let index = 0; index < toolCalls.length; index++) { +- const toolCall = toolCalls[index]; +- const tool = tools?.find((t) => t.name === toolCall.name); ++ // Emit all tool_execution_start events immediately ++ for (const toolCall of toolCalls) { + stream.push({ + type: "tool_execution_start", + toolCallId: toolCall.id, + toolName: toolCall.name, + args: toolCall.arguments, + }); ++ } ++ // Execute all tools in parallel ++ const settled = await Promise.allSettled(toolCalls.map(async (toolCall) => { ++ const tool = tools?.find((t) => t.name === toolCall.name); + let result; + let isError = false; + try { +@@ -240,6 +241,27 @@ async function executeToolCalls(tools, assistantMessage, signal, stream, getStee + }; + isError = true; + } ++ return { result, isError }; ++ })); ++ // Process results IN ORIGINAL ORDER (critical for LLM context) ++ const results = []; ++ let steeringMessages; ++ for (let i = 0; i < settled.length; i++) { ++ const entry = settled[i]; ++ const toolCall = toolCalls[i]; ++ let result; ++ let isError; ++ if (entry.status === "fulfilled") { ++ result = entry.value.result; ++ isError = entry.value.isError; ++ } ++ else { ++ result = { ++ content: [{ type: "text", text: entry.reason instanceof Error ? entry.reason.message : String(entry.reason) }], ++ details: {}, ++ }; ++ isError = true; ++ } + stream.push({ + type: "tool_execution_end", + toolCallId: toolCall.id, +@@ -259,17 +281,12 @@ async function executeToolCalls(tools, assistantMessage, signal, stream, getStee + results.push(toolResultMessage); + stream.push({ type: "message_start", message: toolResultMessage }); + stream.push({ type: "message_end", message: toolResultMessage }); +- // Check for steering messages - skip remaining tools if user interrupted +- if (getSteeringMessages) { +- const steering = await getSteeringMessages(); +- if (steering.length > 0) { +- steeringMessages = steering; +- const remainingCalls = toolCalls.slice(index + 1); +- for (const skipped of remainingCalls) { +- results.push(skipToolCall(skipped, stream)); +- } +- break; +- } ++ } ++ // Check steering messages once after all tools complete ++ if (getSteeringMessages) { ++ const steering = await getSteeringMessages(); ++ if (steering.length > 0) { ++ steeringMessages = steering; + } + } + return { toolResults: results, steeringMessages }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0221107..18c13e8b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,13 +68,18 @@ overrides: '@types/react': ^19.2.0 '@types/react-dom': ^19.2.0 +patchedDependencies: + '@mariozechner/pi-agent-core@0.52.9': + hash: befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00 + path: patches/@mariozechner__pi-agent-core@0.52.9.patch + importers: .: dependencies: '@mariozechner/pi-agent-core': specifier: 'catalog:' - version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) + version: 0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-ai': specifier: 'catalog:' version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) @@ -591,7 +596,7 @@ importers: dependencies: '@mariozechner/pi-agent-core': specifier: 'catalog:' - version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) + version: 0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-ai': specifier: 'catalog:' version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) @@ -690,7 +695,7 @@ importers: devDependencies: '@mariozechner/pi-agent-core': specifier: 'catalog:' - version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) + version: 0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-ai': specifier: 'catalog:' version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) @@ -12498,7 +12503,7 @@ snapshots: std-env: 3.10.0 yoctocolors: 2.1.2 - '@mariozechner/pi-agent-core@0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)': + '@mariozechner/pi-agent-core@0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)': dependencies: '@mariozechner/pi-ai': 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) transitivePeerDependencies: @@ -12537,7 +12542,7 @@ snapshots: '@mariozechner/pi-coding-agent@0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)': dependencies: '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-agent-core': 0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-ai': 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-tui': 0.52.9 '@silvia-odwyer/photon-node': 0.3.4 @@ -15944,9 +15949,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.6.1)) globals: 16.5.0 @@ -15961,8 +15966,8 @@ snapshots: '@next/eslint-plugin-next': 16.1.6 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) @@ -15984,7 +15989,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -15995,18 +16000,18 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -16019,7 +16024,7 @@ snapshots: - supports-color - typescript - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -16030,7 +16035,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 From d162ba98a9b6894ea8f3f24e49015e4c31d795af Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 19:53:03 +0800 Subject: [PATCH 07/12] fix(agent): pass sessionId to tools for sub-agent session tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toolsOptions spread `options` which had sessionId undefined for auto-generated sessions. This caused sessions_list and sessions_spawn to fail with "No session ID available" — breaking sub-agent orchestration. Co-Authored-By: Claude Opus 4.6 --- packages/core/src/agent/runner.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/src/agent/runner.ts b/packages/core/src/agent/runner.ts index c9f81f03..48bc355d 100644 --- a/packages/core/src/agent/runner.ts +++ b/packages/core/src/agent/runner.ts @@ -393,9 +393,12 @@ export class Agent { const profileToolsConfig = this.profile?.getToolsConfig(); const mergedToolsConfig = mergeToolsConfig(profileToolsConfig, options.tools); const profileDir = this.profile?.getProfileDir(); + // Use this.sessionId (which may be auto-generated) instead of options.sessionId + // (which may be undefined). Without this, sessions_list and sessions_spawn + // can't find sub-agent runs because they have no session context. this.toolsOptions = mergedToolsConfig - ? { ...options, cwd: effectiveCwd, tools: mergedToolsConfig, profileDir, provider: this.resolvedProvider } - : { ...options, cwd: effectiveCwd, profileDir, provider: this.resolvedProvider }; + ? { ...options, sessionId: this.sessionId, cwd: effectiveCwd, tools: mergedToolsConfig, profileDir, provider: this.resolvedProvider } + : { ...options, sessionId: this.sessionId, cwd: effectiveCwd, profileDir, provider: this.resolvedProvider }; const tools = resolveTools(this.toolsOptions); if (this.debug) { From 691e33e71ed177c0358eb3db8aa7e3d0731f1604 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 19:53:12 +0800 Subject: [PATCH 08/12] fix(tools): auto-create group when custom groupId is provided LLM often invents custom groupId strings that don't exist in the registry, causing "group not found" errors. Now auto-creates the group instead, matching the behavior when `next` is provided. Co-Authored-By: Claude Opus 4.6 --- .../src/agent/tools/sessions-spawn.test.ts | 23 ++++++++++++++++++- .../core/src/agent/tools/sessions-spawn.ts | 19 +++++++-------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/core/src/agent/tools/sessions-spawn.test.ts b/packages/core/src/agent/tools/sessions-spawn.test.ts index 9eaff1a7..0a0d80a9 100644 --- a/packages/core/src/agent/tools/sessions-spawn.test.ts +++ b/packages/core/src/agent/tools/sessions-spawn.test.ts @@ -1,7 +1,11 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, beforeEach } from "vitest"; import { createSessionsSpawnTool } from "./sessions-spawn.js"; +import { getSubagentGroup, resetSubagentRegistryForTests } from "../subagent/registry.js"; describe("sessions_spawn tool", () => { + beforeEach(() => { + resetSubagentRegistryForTests(); + }); it("has correct name and description", () => { const tool = createSessionsSpawnTool({ isSubagent: false, sessionId: "test-session" }); expect(tool.name).toBe("sessions_spawn"); @@ -24,6 +28,23 @@ describe("sessions_spawn tool", () => { expect(firstContent.text).toContain("not allowed"); }); + it("auto-creates group when custom groupId is provided", async () => { + const tool = createSessionsSpawnTool({ isSubagent: false, sessionId: "parent-session" }); + + // Should not error — the group is auto-created + await tool.execute( + "call-group", + { task: "research topic", label: "Research", groupId: "my-custom-group" } as any, + new AbortController().signal, + ); + + // Verify group was created in the registry + const group = getSubagentGroup("my-custom-group"); + expect(group).toBeDefined(); + expect(group!.groupId).toBe("my-custom-group"); + expect(group!.label).toBe("Group: Research"); + }); + it("fails gracefully when Hub is not initialized", async () => { const tool = createSessionsSpawnTool({ isSubagent: false, sessionId: "parent-session" }); diff --git a/packages/core/src/agent/tools/sessions-spawn.ts b/packages/core/src/agent/tools/sessions-spawn.ts index 63e615bb..030fbe27 100644 --- a/packages/core/src/agent/tools/sessions-spawn.ts +++ b/packages/core/src/agent/tools/sessions-spawn.ts @@ -126,19 +126,20 @@ export function createSessionsSpawnTool( const runId = uuidv7(); const childSessionId = uuidv7(); - // Validate groupId if provided + // Auto-create group when groupId is provided but doesn't exist yet, + // or when `next` is provided without a groupId. if (groupId) { const existingGroup = getSubagentGroup(groupId); if (!existingGroup) { - return { - content: [{ type: "text", text: `Error: group not found: ${groupId}. Use the groupId returned by a previous sessions_spawn call.` }], - details: { status: "error", error: `group not found: ${groupId}` }, - }; + // LLM provided a custom groupId — auto-create the group + createSubagentGroup({ + groupId, + requesterSessionId, + label: label ? `Group: ${label}` : undefined, + next, + }); } - } - - // Auto-create group when `next` is provided without an existing groupId - if (!groupId && next) { + } else if (next) { groupId = uuidv7(); createSubagentGroup({ groupId, From b394b0ccf9635261b17b3af88d0d249b63a96bd1 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 19:53:18 +0800 Subject: [PATCH 09/12] fix(skills): use python3 and inject skill directory path into prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SKILL.md: python → python3 (macOS has no `python` binary) - skills/index.ts: inject skill directory path so agent can resolve relative paths like scripts/recalc.py to absolute paths Co-Authored-By: Claude Opus 4.6 --- packages/core/src/agent/skills/index.ts | 6 ++++++ skills/xlsx/SKILL.md | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/core/src/agent/skills/index.ts b/packages/core/src/agent/skills/index.ts index e7343eb2..0a84f076 100644 --- a/packages/core/src/agent/skills/index.ts +++ b/packages/core/src/agent/skills/index.ts @@ -5,6 +5,7 @@ * Compatible with OpenClaw/AgentSkills specification */ +import { dirname } from "path"; import type { Skill, SkillManagerOptions, SkillsConfig, SkillCommandSpec, SkillInvocationResult } from "./types.js"; import { loadAllSkills, getProfileSkillsDir, initializeManagedSkills, getManagedSkillsDir } from "./loader.js"; import { @@ -332,6 +333,11 @@ export class SkillManager { parts.push(`## ${emoji} ${name} (${id})`); parts.push(`${desc}\n`); + // Include skill directory path so the agent can resolve relative paths + // (e.g., scripts/recalc.py → /absolute/path/to/skill/scripts/recalc.py) + const skillDir = dirname(skill.filePath); + parts.push(`**Skill directory**: \`${skillDir}\`\n`); + // Include full instructions if (skill.instructions) { parts.push(skill.instructions); diff --git a/skills/xlsx/SKILL.md b/skills/xlsx/SKILL.md index 8a617e58..dfdb1f6a 100644 --- a/skills/xlsx/SKILL.md +++ b/skills/xlsx/SKILL.md @@ -151,7 +151,7 @@ This applies to ALL calculations - totals, percentages, ratios, differences, etc 4. **Save**: Write to file 5. **Recalculate formulas (MANDATORY IF USING FORMULAS)**: Use the scripts/recalc.py script ```bash - python scripts/recalc.py output.xlsx + python3 scripts/recalc.py output.xlsx ``` 6. **Verify and fix any errors**: - The script returns JSON with error details @@ -224,12 +224,12 @@ wb.save('modified.xlsx') Excel files created or modified by openpyxl contain formulas as strings but not calculated values. Use the provided `scripts/recalc.py` script to recalculate formulas: ```bash -python scripts/recalc.py [timeout_seconds] +python3 scripts/recalc.py [timeout_seconds] ``` Example: ```bash -python scripts/recalc.py output.xlsx 30 +python3 scripts/recalc.py output.xlsx 30 ``` The script: From e39f9a5dfe510261e13805754fcc8d8ad1e68361 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 19:53:26 +0800 Subject: [PATCH 10/12] feat(cli): wait for sub-agents and output findings in run mode In CLI mode, the parent Agent is not registered with the Hub, so the normal announce flow can't deliver sub-agent results. Added polling mechanism that waits for sub-agents to complete and prints their findings directly to stdout. Co-Authored-By: Claude Opus 4.6 --- apps/cli/src/commands/run.ts | 47 +++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/apps/cli/src/commands/run.ts b/apps/cli/src/commands/run.ts index c5fb6916..157baeab 100644 --- a/apps/cli/src/commands/run.ts +++ b/apps/cli/src/commands/run.ts @@ -7,7 +7,7 @@ */ import { join } from "node:path"; -import { Agent, Hub } from "@multica/core"; +import { Agent, Hub, listSubagentRuns } from "@multica/core"; import type { AgentOptions } from "@multica/core"; import type { ToolsConfig } from "@multica/core"; import { DATA_DIR } from "@multica/utils"; @@ -230,7 +230,52 @@ export async function runCommand(args: string[]): Promise { console.error(`Error: ${result.error}`); process.exitCode = 1; } + + // Wait for sub-agents to complete and parent to process their results. + // Without this, CLI exits before sub-agent announcements are delivered. + await waitForSubagents(agent); } finally { hub.shutdown(); } } + +/** + * Wait for any running sub-agents to complete, then output their findings. + * + * In CLI mode, the parent Agent is not registered with the Hub, so the normal + * announce flow (Hub → writeInternal) can't deliver results. Instead, we poll + * the registry and print findings directly once all sub-agents finish. + * + * Max wait: 30 minutes (matches default sub-agent timeout). + */ +async function waitForSubagents(agent: Agent): Promise { + const MAX_WAIT_MS = 30 * 60 * 1000; + const POLL_INTERVAL_MS = 2000; + const start = Date.now(); + + const allRuns = listSubagentRuns(agent.sessionId); + if (allRuns.length === 0) return; + + // Phase 1: Wait for all sub-agent runs to finish + while (Date.now() - start < MAX_WAIT_MS) { + const runs = listSubagentRuns(agent.sessionId); + const running = runs.filter((r) => !r.endedAt); + if (running.length === 0) break; + console.error(dim(`[waiting for ${running.length} sub-agent(s)...]`)); + await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS)); + } + + // Phase 2: Output sub-agent findings directly (bypasses Hub announce flow) + const completedRuns = listSubagentRuns(agent.sessionId).filter((r) => r.endedAt); + if (completedRuns.length === 0) return; + + console.error(dim(`[${completedRuns.length} sub-agent(s) completed]`)); + + for (const run of completedRuns) { + const displayName = run.label || run.task.slice(0, 60); + const status = run.outcome?.status ?? "unknown"; + const findings = run.findings || "(no output)"; + console.log(`\n--- Sub-agent: ${displayName} [${status}] ---`); + console.log(findings); + } +} From 084657868fff96d207789d88c1a212833d0b51b6 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 20:43:37 +0800 Subject: [PATCH 11/12] revert(agent): remove parallel tool execution patch, keep serial Co-Authored-By: Claude Opus 4.6 --- package.json | 3 --- packages/core/src/agent/runner.ts | 8 +++----- pnpm-lock.yaml | 15 +++++---------- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index d39e6145..80b868f4 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,6 @@ "overrides": { "@types/react": "catalog:", "@types/react-dom": "catalog:" - }, - "patchedDependencies": { - "@mariozechner/pi-agent-core@0.52.9": "patches/@mariozechner__pi-agent-core@0.52.9.patch" } }, "devDependencies": { diff --git a/packages/core/src/agent/runner.ts b/packages/core/src/agent/runner.ts index 48bc355d..da3112a1 100644 --- a/packages/core/src/agent/runner.ts +++ b/packages/core/src/agent/runner.ts @@ -783,19 +783,17 @@ export class Agent { private handleRunLogEvent(event: AgentEvent) { if (event.type === "tool_execution_start") { - const toolCallId = (event as any).toolCallId ?? "unknown"; const toolName = (event as any).toolName ?? "unknown"; - this.toolStartTimes.set(toolCallId, Date.now()); + this.toolStartTimes.set(toolName, Date.now()); this.runLog.log("tool_start", { tool: toolName, args: JSON.stringify((event as any).args ?? {}).slice(0, 500), }); } else if (event.type === "tool_execution_end") { - const toolCallId = (event as any).toolCallId ?? "unknown"; const toolName = (event as any).toolName ?? "unknown"; - const startTime = this.toolStartTimes.get(toolCallId); + const startTime = this.toolStartTimes.get(toolName); const duration_ms = startTime ? Date.now() - startTime : undefined; - this.toolStartTimes.delete(toolCallId); + this.toolStartTimes.delete(toolName); // Extract result metadata for run-log persistence (survives session compaction) const result = (event as any).result; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18c13e8b..cc765ee9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,18 +68,13 @@ overrides: '@types/react': ^19.2.0 '@types/react-dom': ^19.2.0 -patchedDependencies: - '@mariozechner/pi-agent-core@0.52.9': - hash: befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00 - path: patches/@mariozechner__pi-agent-core@0.52.9.patch - importers: .: dependencies: '@mariozechner/pi-agent-core': specifier: 'catalog:' - version: 0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) + version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-ai': specifier: 'catalog:' version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) @@ -596,7 +591,7 @@ importers: dependencies: '@mariozechner/pi-agent-core': specifier: 'catalog:' - version: 0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) + version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-ai': specifier: 'catalog:' version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) @@ -695,7 +690,7 @@ importers: devDependencies: '@mariozechner/pi-agent-core': specifier: 'catalog:' - version: 0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) + version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-ai': specifier: 'catalog:' version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) @@ -12503,7 +12498,7 @@ snapshots: std-env: 3.10.0 yoctocolors: 2.1.2 - '@mariozechner/pi-agent-core@0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)': + '@mariozechner/pi-agent-core@0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)': dependencies: '@mariozechner/pi-ai': 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) transitivePeerDependencies: @@ -12542,7 +12537,7 @@ snapshots: '@mariozechner/pi-coding-agent@0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)': dependencies: '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.52.9(patch_hash=befbe3b9fd1a6d3e13dfa4927d9f0addd2f3b3454bd9d407c16dacb9a71c0f00)(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-agent-core': 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-ai': 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-tui': 0.52.9 '@silvia-odwyer/photon-node': 0.3.4 From a443d3009d5b1c761e0b03247d5a6e9c02bfb956 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sun, 15 Feb 2026 20:45:32 +0800 Subject: [PATCH 12/12] chore: remove leftover patch file from parallel execution revert Co-Authored-By: Claude Opus 4.6 --- .../@mariozechner__pi-agent-core@0.52.9.patch | 80 ------------------- 1 file changed, 80 deletions(-) delete mode 100644 patches/@mariozechner__pi-agent-core@0.52.9.patch diff --git a/patches/@mariozechner__pi-agent-core@0.52.9.patch b/patches/@mariozechner__pi-agent-core@0.52.9.patch deleted file mode 100644 index e570a653..00000000 --- a/patches/@mariozechner__pi-agent-core@0.52.9.patch +++ /dev/null @@ -1,80 +0,0 @@ -diff --git a/dist/agent-loop.js b/dist/agent-loop.js -index b26d45753809da14df6409354e8c537684b9acd7..931399cc12bafea940fe99ab74ae288d71506e52 100644 ---- a/dist/agent-loop.js -+++ b/dist/agent-loop.js -@@ -206,17 +206,18 @@ async function streamAssistantResponse(context, config, signal, stream, streamFn - */ - async function executeToolCalls(tools, assistantMessage, signal, stream, getSteeringMessages) { - const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall"); -- const results = []; -- let steeringMessages; -- for (let index = 0; index < toolCalls.length; index++) { -- const toolCall = toolCalls[index]; -- const tool = tools?.find((t) => t.name === toolCall.name); -+ // Emit all tool_execution_start events immediately -+ for (const toolCall of toolCalls) { - stream.push({ - type: "tool_execution_start", - toolCallId: toolCall.id, - toolName: toolCall.name, - args: toolCall.arguments, - }); -+ } -+ // Execute all tools in parallel -+ const settled = await Promise.allSettled(toolCalls.map(async (toolCall) => { -+ const tool = tools?.find((t) => t.name === toolCall.name); - let result; - let isError = false; - try { -@@ -240,6 +241,27 @@ async function executeToolCalls(tools, assistantMessage, signal, stream, getStee - }; - isError = true; - } -+ return { result, isError }; -+ })); -+ // Process results IN ORIGINAL ORDER (critical for LLM context) -+ const results = []; -+ let steeringMessages; -+ for (let i = 0; i < settled.length; i++) { -+ const entry = settled[i]; -+ const toolCall = toolCalls[i]; -+ let result; -+ let isError; -+ if (entry.status === "fulfilled") { -+ result = entry.value.result; -+ isError = entry.value.isError; -+ } -+ else { -+ result = { -+ content: [{ type: "text", text: entry.reason instanceof Error ? entry.reason.message : String(entry.reason) }], -+ details: {}, -+ }; -+ isError = true; -+ } - stream.push({ - type: "tool_execution_end", - toolCallId: toolCall.id, -@@ -259,17 +281,12 @@ async function executeToolCalls(tools, assistantMessage, signal, stream, getStee - results.push(toolResultMessage); - stream.push({ type: "message_start", message: toolResultMessage }); - stream.push({ type: "message_end", message: toolResultMessage }); -- // Check for steering messages - skip remaining tools if user interrupted -- if (getSteeringMessages) { -- const steering = await getSteeringMessages(); -- if (steering.length > 0) { -- steeringMessages = steering; -- const remainingCalls = toolCalls.slice(index + 1); -- for (const skipped of remainingCalls) { -- results.push(skipToolCall(skipped, stream)); -- } -- break; -- } -+ } -+ // Check steering messages once after all tools complete -+ if (getSteeringMessages) { -+ const steering = await getSteeringMessages(); -+ if (steering.length > 0) { -+ steeringMessages = steering; - } - } - return { toolResults: results, steeringMessages };