From 6953ba6506421ff9e86ef3a701a348804ebcba24 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 30 Oct 2026 09:49:51 +0100 Subject: [PATCH] fix(server): put a tintinweb foreground answer in the child timeline A foreground @tintinweb/pi-subagents child has no session file, so its answer only exists in the Agent tool result. Emit that text as the child's assistant message when the child finishes. Tag every sub_agent producer's `log: ""` with COMPAT(subagent-log) so the shim removal finds them. --- .../server/src/server/agent/agent-sdk-types.ts | 3 ++- .../src/server/agent/providers/claude/agent.ts | 1 + .../providers/claude/sidechain-tracker.ts | 1 + .../agent/providers/codex/tool-call-mapper.ts | 2 ++ .../agent/providers/omp/tool-call-mapper.ts | 1 + .../opencode/tool-call-detail-parser.ts | 1 + .../pi/extensions/edxeth-pi-subagents/index.ts | 1 + .../extensions/gotgenes-pi-subagents/index.ts | 2 ++ .../pi/extensions/pi-subagents/index.ts | 1 + .../pi/extensions/subagent-fixture-test.ts | 2 -- .../tintinweb-pi-subagents/index.test.ts | 17 +++++++++++++++++ .../extensions/tintinweb-pi-subagents/index.ts | 18 +++++++++++++++++- 12 files changed, 46 insertions(+), 4 deletions(-) diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index 46e694e89b..6570392311 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -314,7 +314,8 @@ export type ToolCallDetail = description?: string; childSessionId?: string; // COMPAT(subagent-log): added in v0.14.1, remove after 2027-04-30. Apps before v0.14.1 - // require `log`; every producer sends it empty. Drop the field and those producers' `log: ""`. + // require `log`; every producer sends it empty and carries the same tag. Optional here because + // protocol timeline items, where `log` is optional, flow into this type. log?: string; actions?: Array<{ index: number; diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 3fa48c6c06..c675b1ed62 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -6276,6 +6276,7 @@ function buildClaudeSubagentCardDetail( type: "sub_agent", ...(facts.title ? { subAgentType: facts.title } : {}), ...(facts.description ? { description: facts.description } : {}), + // COMPAT(subagent-log): remove after 2027-04-30. log: "", actions: [], }; diff --git a/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts b/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts index f0596c22fd..4e52ea9f50 100644 --- a/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts +++ b/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts @@ -137,6 +137,7 @@ export class ClaudeSidechainTracker { type: "sub_agent", ...(state.subAgentType ? { subAgentType: state.subAgentType } : {}), ...(state.description ? { description: state.description } : {}), + // COMPAT(subagent-log): remove after 2027-04-30. log: "", actions: [], }; diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts index 64629bc5d9..4e77dc9367 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts @@ -950,6 +950,7 @@ function mapCollabAgentToolCallItem( type: "sub_agent", subAgentType: "Sub-agent", ...(item.prompt ? { description: item.prompt } : {}), + // COMPAT(subagent-log): remove after 2027-04-30. log: "", actions: [], }; @@ -1006,6 +1007,7 @@ function mapSubAgentActivityItem( type: "sub_agent", subAgentType: nativeName, description, + // COMPAT(subagent-log): remove after 2027-04-30. log: "", actions: [], }, diff --git a/packages/server/src/server/agent/providers/omp/tool-call-mapper.ts b/packages/server/src/server/agent/providers/omp/tool-call-mapper.ts index 8610996814..500123758a 100644 --- a/packages/server/src/server/agent/providers/omp/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/omp/tool-call-mapper.ts @@ -199,6 +199,7 @@ function mapOmpTaskDetail(args: unknown, result: OmpToolResult): ToolCallDetail argRecord.assignment, ), ...(childSessionId ? { childSessionId } : {}), + // COMPAT(subagent-log): remove after 2027-04-30. log: "", }; } diff --git a/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts b/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts index f8f7b2bfd2..b3eb432f86 100644 --- a/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts +++ b/packages/server/src/server/agent/providers/opencode/tool-call-detail-parser.ts @@ -199,6 +199,7 @@ function deriveOpencodeTaskDetail( ...(subAgentType ? { subAgentType } : {}), ...(description ? { description } : {}), ...(childSessionId ? { childSessionId } : {}), + // COMPAT(subagent-log): remove after 2027-04-30. log: "", actions: [], }; diff --git a/packages/server/src/server/agent/providers/pi/extensions/edxeth-pi-subagents/index.ts b/packages/server/src/server/agent/providers/pi/extensions/edxeth-pi-subagents/index.ts index 1b89c9407e..c58f0faf15 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/edxeth-pi-subagents/index.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/edxeth-pi-subagents/index.ts @@ -98,6 +98,7 @@ export const edxethPiSubagents: PiExtension = { detail: { type: "sub_agent", description, + // COMPAT(subagent-log): remove after 2027-04-30. log: "", }, subagents, diff --git a/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/index.ts b/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/index.ts index 5eef7433da..7d1b4c325c 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/index.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/gotgenes-pi-subagents/index.ts @@ -63,6 +63,7 @@ export const gotgenesPiSubagents: PiExtension = { type: "sub_agent" as const, subAgentType: title, description, + // COMPAT(subagent-log): remove after 2027-04-30. log: "", }; if (call.status === "running") @@ -117,6 +118,7 @@ export const gotgenesPiSubagents: PiExtension = { const detail = { type: "sub_agent" as const, description: details.success ? details.data.description : undefined, + // COMPAT(subagent-log): remove after 2027-04-30. log: "", }; if (call.status === "running" || !details.success) return { detail }; diff --git a/packages/server/src/server/agent/providers/pi/extensions/pi-subagents/index.ts b/packages/server/src/server/agent/providers/pi/extensions/pi-subagents/index.ts index 4ec882e11c..058171e8df 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/pi-subagents/index.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/pi-subagents/index.ts @@ -131,6 +131,7 @@ export const piSubagents: PiExtension = { type: "sub_agent" as const, subAgentType: args.data.agent, description: args.data.task, + // COMPAT(subagent-log): remove after 2027-04-30. log: "", }; const base = { diff --git a/packages/server/src/server/agent/providers/pi/extensions/subagent-fixture-test.ts b/packages/server/src/server/agent/providers/pi/extensions/subagent-fixture-test.ts index 8a9ca61d08..96ba771594 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/subagent-fixture-test.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/subagent-fixture-test.ts @@ -54,8 +54,6 @@ export async function verifySubagentFixture( try { await session.startTurn("Delegate work"); for (const event of fixture.events) pi.latestSession().emit(event); - const immediate = live.filter((event) => event.type === "provider_subagent"); - expect(immediate.some((event) => event.event.type === "timeline")).toBe(false); pi.latestSession().finishTurn(); const replay: AgentStreamEvent[] = []; for await (const event of streamPiHistory("pi", fixture.messages)) replay.push(event); diff --git a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.test.ts b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.test.ts index 65499183b8..60f3b12869 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.test.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.test.ts @@ -36,6 +36,11 @@ describe("@tintinweb/pi-subagents adapter", () => { }); expect(mapping?.subagents).toEqual([ expect.objectContaining({ id: "call-1", status: "completed" }), + { + type: "timeline", + id: "call-1", + item: { type: "assistant_message", text: "Wrapped up (turn limit)" }, + }, ]); expect(mapping?.detail).toEqual({ type: "sub_agent", @@ -53,6 +58,18 @@ describe("@tintinweb/pi-subagents adapter", () => { .filter((event) => event.event.type === "upsert") .map((event) => (event.event.type === "upsert" ? event.event.status : null)), ).toEqual(["running", "completed"]); + expect( + events.flatMap((event) => (event.event.type === "timeline" ? [event.event] : [])), + ).toEqual([ + { + type: "timeline", + id: "call_458926", + item: { + type: "assistant_message", + text: "Agent completed in 6.6s (1 tool uses, 18.8k token).\n\nThe workspace directory `/tmp/paseo-pi-extension-phase3/workspace` is empty (no files or hidden dotfiles).", + }, + }, + ]); }); test("uses a structured notification to complete a background child", async () => { const source = readSubagentFixture(new URL("./fixtures/background.json", import.meta.url)); diff --git a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.ts b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.ts index 101035ad89..6858dec8be 100644 --- a/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.ts +++ b/packages/server/src/server/agent/providers/pi/extensions/tintinweb-pi-subagents/index.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { extractTextFromToolResult } from "../../tool-call-mapper.js"; import { outputFileFromToolResult } from "../child-session.js"; import type { PiExtension, PiExtensionToolCall } from "../contract.js"; @@ -56,6 +57,7 @@ export const tintinwebPiSubagents: PiExtension = { type: "sub_agent" as const, subAgentType: title, description, + // COMPAT(subagent-log): remove after 2027-04-30. log: "", }; if (call.status === "running") @@ -83,6 +85,10 @@ export const tintinwebPiSubagents: PiExtension = { const file = outputFileFromToolResult(call.result); const childSessions = file && !readSessions.has(file) ? [{ id: call.callId, file }] : []; if (file) readSessions.add(file); + const childStatus = status(details.data.status); + // A foreground child has no session file; its answer is the tool result. + const answer = + !file && childStatus !== "running" ? extractTextFromToolResult(call.result)?.trim() : ""; return { detail, subagents: [ @@ -92,8 +98,17 @@ export const tintinwebPiSubagents: PiExtension = { title, description, toolCallId: call.callId, - status: status(details.data.status), + status: childStatus, }, + ...(answer + ? [ + { + type: "timeline" as const, + id: call.callId, + item: { type: "assistant_message" as const, text: answer }, + }, + ] + : []), ], childSessions, }; @@ -109,6 +124,7 @@ export const tintinwebPiSubagents: PiExtension = { const detail = { type: "sub_agent" as const, description: details.success ? details.data.description : undefined, + // COMPAT(subagent-log): remove after 2027-04-30. log: "", }; if (call.status === "running" || !details.success) return { detail };