Skip to content

feat: show nested tool executions under their parent - #414

Merged
bubbleptr merged 2 commits into
feat/pi-099-builtin-extensionsfrom
feat/nested-tool-executions
Sep 30, 2026
Merged

bubbleptr merged 2 commits into
feat/pi-099-builtin-extensionsfrom
feat/nested-tool-executions

Conversation

@bubbleptr

@bubbleptr bubbleptr commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #412.

Summary

Pi 0.99 tools such as codemode start other tools through ctx.executeTool(). Pi emits those Nested Tool Executions as tool_execution_* events with parentToolCallId; they have no tool_call message part, so the Chain of Thought never showed them. In the Pi session log they exist only as a bounded nestedCalls record (no results) on the parent's toolResult message.

  • Event contract: tool AgentRuntimeEvent gains optional parentToolCallId; the normalizer sets it only for nested executions. The journal keeps start/end as before, so replay carries parentage too.
  • Live Chain of Thought (cot-view.ts): nested executions attach as children of the parent ChatToolItem (recursive, start order, one per-derivation index scoped to the Run) and never become steps of their own. The codemode detail pane shows the script instead of escaped {"code": ...} JSON.
  • ChatToolStep / ChatToolDetail: children render between args and result as per-call rows; the settled row shows N nested calls; the live label reads Running codemode › probe/echo….
  • Trajectory: nestedCalls from the toolResult payload is parsed defensively into TrajectoryStep.nestedCalls; the inspector Summary tab lists them (status, duration, args or "Arguments omitted (N bytes)", error) and notes when Pi kept a partial record.
  • MCP display names: mcp__<server>__<tool> is shown as server/tool, like Pi's TUI (display only; data, filters and kind lookups keep the raw name).
  • CONTEXT.md adds Nested Tool Execution; Design page variants, docs/self-built-ui.md, docs/design/chat.md, docs/design/workspace.md and the UI intent region table are updated.

Test plan

  • Normalizer: nested events carry parentToolCallId, top-level events have no key (red → green)
  • cot-view: codemode part with an MCP child and a failing bash child → one step, script args, two ordered children with state/args/output; running child is input-available (red → green)
  • trajectory-model: nestedCalls parsed, omitted-args kept as bytes, malformed entries dropped, complete: false preserved (red → green)
  • Inspector renders nested rows and the partial-record note (red → green)
  • chat-tool-step: children list, 2 nested calls, live Running codemode › probe/echo…, Used probe/echo
  • test:bundled-runtime against the real SDK: mcp__probe__echo end event arrives with parentToolCallId equal to the codemode call and ECHO:bundled in its result
  • bun run typecheck, bun run lint, bun run build clean; Design page screenshots checked
  • bun run test: all green except the pre-existing .cursor/** collection failure, fixed on main by 1a26a7b

Pi 0.99 tools such as codemode start other tools through ctx.executeTool(). Those executions carry parentToolCallId and have no tool_call part, so the Chain of Thought never showed them. Carry the parentage through the event contract and runtime model, render nested executions inside the parent's detail (live and replayed), read Pi's bounded nestedCalls record into the Trajectory inspector, and title MCP tools as server/tool.
@bubbleptr
bubbleptr added this pull request to stack #415 September 30, 2026 12:59

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

}: ChatToolStepProps) {
const { tools } = step;
const failed = tools.filter((tool) => tool.state === "output-error").length;
const nestedCount = tools.reduce((sum, tool) => sum + (tool.children?.length ?? 0), 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nested failures hidden in tool summary

When a parent tool succeeds after catching a nested failure, failed ignores the failed child. The collapsed row reports nested calls without showing their failure.

Learn more

A tool can execute another tool and catch its failure before returning successfully. The child then has output-error, while the parent's state is output-available. The summary counts only parents, although nestedItems retains each child's state. The failure remains visible only after expanding the row.

Example: A codemode script calls bash with exit 1, catches the error, and returns done. The collapsed row shows Used codemode · 1 nested call without 1 failed.

Recommended fix: Include failed descendants when computing failed in ChatToolStep, using the same recursive traversal as the nested count; preserve the existing top-level counts.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the latest commit: failed descendants now show as a separate N nested failed meta; the top-level failed count is unchanged because the parent itself succeeded.

}: ChatToolStepProps) {
const { tools } = step;
const failed = tools.filter((tool) => tool.state === "output-error").length;
const nestedCount = tools.reduce((sum, tool) => sum + (tool.children?.length ?? 0), 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Deeper nested calls omitted from summary

When a nested tool starts another tool, nestedCount counts only direct children. The settled count excludes deeper calls, and the live label names their ancestor instead.

Learn more

nestedItems builds a recursive children tree whenever a tool executes another tool. This summary counts only one level, and runningChild also stops at one level. The detail displays deeper calls, but the collapsed count and live status do not account for them.

Example: codemode executes tool A, which executes tool B. The row shows 1 nested call instead of two, and while B runs it names A.

Recommended fix: Traverse ChatToolItem.children recursively for the count and the deepest running child, retaining the current top-level tool label.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: the nested count covers all descendants, and the live label follows the deepest running execution.

Comment on lines +143 to +145
if (!calls.length) {
return undefined;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Empty partial records hide dropped calls

When nestedCalls has complete: false and no retained entries, the parser discards it. The inspector then hides Pi's partial-record warning.

Suggested change
if (!calls.length) {
return undefined;
}
if (!calls.length && complete !== false) {
return undefined;
}

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed as suggested: { calls: [], complete: false } is kept, so the inspector shows Nested calls (0) with the partial-record note.

Review follow-up: the tool step now counts all descendants, shows failed nested executions separately from top-level failures, names the deepest running execution while live, and keeps empty partial nestedCalls records so the inspector still warns.
@bubbleptr
bubbleptr merged commit e7fd5be into main Sep 30, 2026
1 check passed
@bubbleptr
bubbleptr deleted the feat/nested-tool-executions branch September 30, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant