From 94b57f34add2de608ec8c41e99f4ce9c2e14ba9c Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:17:12 +0200 Subject: [PATCH] fix(pi): render delegated tasks with lifecycle status (#2891) Pi left task and direct subagent tool calls in the unknown detail shape, so clients rendered raw request and result payloads. Normalize execution calls at the provider boundary so the existing task card owns their running and terminal states. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../server/agent/providers/pi/agent.test.ts | 55 +++++++++++++++++++ .../providers/pi/tool-call-mapper.test.ts | 31 +++++++++++ .../agent/providers/pi/tool-call-mapper.ts | 28 ++++++++++ 3 files changed, 114 insertions(+) diff --git a/packages/server/src/server/agent/providers/pi/agent.test.ts b/packages/server/src/server/agent/providers/pi/agent.test.ts index c3fad836d..d6b0f9377 100644 --- a/packages/server/src/server/agent/providers/pi/agent.test.ts +++ b/packages/server/src/server/agent/providers/pi/agent.test.ts @@ -553,6 +553,61 @@ describe("PiRpcAgentSession", () => { ]); }); + test("streams Pi task calls as sub-agent cards with lifecycle status", async () => { + const { pi, session, events } = await createSession(); + const fakeSession = pi.latestSession(); + + await session.startTurn("delegate this"); + fakeSession.emit({ + type: "tool_execution_start", + toolCallId: "task-1", + toolName: "task", + args: { + agent: "explore", + task: "Trace the Pi provider tool mapper", + }, + }); + fakeSession.emit({ + type: "tool_execution_end", + toolCallId: "task-1", + toolName: "task", + result: { content: [{ type: "text", text: "Found the mapper." }] }, + isError: false, + }); + fakeSession.finishTurn(); + + await events.nextTurnCompletion(); + + expect(events.timelineItems()).toEqual([ + { + type: "tool_call", + callId: "task-1", + name: "task", + status: "running", + detail: { + type: "sub_agent", + subAgentType: "explore", + description: "Trace the Pi provider tool mapper", + log: "", + }, + error: null, + }, + { + type: "tool_call", + callId: "task-1", + name: "task", + status: "completed", + detail: { + type: "sub_agent", + subAgentType: "explore", + description: "Trace the Pi provider tool mapper", + log: "Found the mapper.", + }, + error: null, + }, + ]); + }); + test("keeps one generated message id when Pi omits message start and response id", async () => { const { pi, session, events } = await createSession(); const fakeSession = pi.latestSession(); diff --git a/packages/server/src/server/agent/providers/pi/tool-call-mapper.test.ts b/packages/server/src/server/agent/providers/pi/tool-call-mapper.test.ts index dd2213ed1..224e86176 100644 --- a/packages/server/src/server/agent/providers/pi/tool-call-mapper.test.ts +++ b/packages/server/src/server/agent/providers/pi/tool-call-mapper.test.ts @@ -136,6 +136,37 @@ describe("Pi tool call mapper", () => { }); }); + test("maps task calls to sub-agent detail while running", () => { + const toolCall = parseToolArgs("task", { + agent: "explore", + task: "Trace the Pi provider tool mapper", + }); + + expect(mapToolDetail(toolCall, null)).toEqual({ + type: "sub_agent", + subAgentType: "explore", + description: "Trace the Pi provider tool mapper", + log: "", + }); + }); + + test("maps completed subagent calls with task input to sub-agent detail", () => { + const toolCall = parseToolArgs("subagent", { + agent: "reviewer", + task: "Review the Pi mapper change", + }); + const result = parseToolResult({ + content: [{ type: "text", text: "The mapper change preserves provider status." }], + }); + + expect(mapToolDetail(toolCall, result)).toEqual({ + type: "sub_agent", + subAgentType: "reviewer", + description: "Review the Pi mapper change", + log: "The mapper change preserves provider status.", + }); + }); + test("normalizes Pi MCP proxy calls from requested tool args while running", () => { const toolCall = parseToolArgs("mcp", { tool: "paseo_list_models", diff --git a/packages/server/src/server/agent/providers/pi/tool-call-mapper.ts b/packages/server/src/server/agent/providers/pi/tool-call-mapper.ts index b53ed3993..499627377 100644 --- a/packages/server/src/server/agent/providers/pi/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/pi/tool-call-mapper.ts @@ -340,6 +340,10 @@ export function resolveToolCallName(toolCall: PiTrackedToolCall, result?: PiTool export function mapToolDetail(toolCall: PiTrackedToolCall, result?: PiToolResult): ToolCallDetail { const parsedResult = result ?? null; + if (isTaskToolCall(toolCall)) { + return mapTaskToolDetail(toolCall.args, parsedResult); + } + switch (toolCall.kind) { case "bash": { const summary = resolveToolCallOutput(parsedResult); @@ -388,6 +392,30 @@ export function mapToolDetail(toolCall: PiTrackedToolCall, result?: PiToolResult } } +function isTaskToolCall(toolCall: PiTrackedToolCall): boolean { + if (toolCall.toolName === "task") { + return true; + } + if (toolCall.toolName !== "subagent" || !isRecord(toolCall.args)) { + return false; + } + return ( + toolCall.args.action === undefined && + (readNonEmptyString(toolCall.args.agent) !== undefined || + readNonEmptyString(toolCall.args.task) !== undefined) + ); +} + +function mapTaskToolDetail(args: unknown, result: PiToolResult): ToolCallDetail { + const argRecord = isRecord(args) ? args : {}; + return { + type: "sub_agent", + subAgentType: readNonEmptyString(argRecord.agent), + description: readNonEmptyString(argRecord.task), + log: extractTextFromToolResult(result)?.trim() ?? "", + }; +} + function mapWriteToolDetail(args: WriteToolInput, result: PiToolResult): ToolCallDetail { if (result && typeof result !== "string" && result.details && "xdev" in result.details) { const xdev = XdevExecuteDetailsSchema.safeParse(result.details.xdev);