@narumitw/pi-subagents 3.4.3 → 3.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ # Conversation presentation
2
+
3
+ Tool calls, results, completion messages, and child requests and responses follow Pi's global tool-output expansion state and the `app.tools.expand` binding (`Ctrl+O` by default).
4
+ The hint uses the host's configured keys and is omitted when the action is unbound or an older host does not supply key formatting.
5
+
6
+ In the conversation, collapsed tool calls show the operation, target, and a short task or message preview when applicable. Results show job state, send acknowledgement, or wait outcome instead of raw JSON. Inspection shows all state counts and up to five job rows, with failed, timed-out, and partial jobs first; additional rows and jobs no longer retained have separate counts. Expanded calls and results show full arguments and metadata. Wait results and completion notifications render the decoded result as Markdown, with errors and limitations shown separately.
7
+
8
+ Collapsed completion notifications show the job ID and terminal state. Child messages show the source job, request ID, and whether a reply is needed or a response was received. Expanding a child message retains its full security envelope and plain-text body. Unknown or malformed stored formats use a text fallback. These renderers use only the recorded payload, do not read current job state, and do not change data sent to the model or widget preferences.
9
+
10
+ Pi owns the standard tool shell and its background. Below three terminal columns, the host shell's minimum content width and padding can exceed the viewport; renderer content remains bounded.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "3.4.3",
3
+ "version": "3.4.4",
4
4
  "description": "Subagent jobs with asynchronous main-agent messaging for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,169 @@
1
+ import { getMarkdownTheme, type KeybindingHints, type Theme, type ThemeColor } from "@earendil-works/pi-coding-agent";
2
+ import { type Component, Markdown, truncateToWidth } from "@earendil-works/pi-tui";
3
+ import { hardWrapTerminalDocument, sanitizeTerminalDocument } from "@narumitw/pi-tui-kit/terminal-document";
4
+ import { JOB_STATES, type SubagentJobState } from "./types.js";
5
+
6
+ export interface ConversationResult {
7
+ content: readonly { type: string; text?: string }[];
8
+ details?: unknown;
9
+ }
10
+
11
+ export type JobPayload = Record<string, unknown> & { jobId: string; state: SubagentJobState };
12
+
13
+ export function boundedView(render: (width: number) => string[], invalidate: () => void = () => {}): Component {
14
+ return {
15
+ render(width) {
16
+ if (!Number.isFinite(width) || width < 1) return [];
17
+ const columns = Math.floor(width);
18
+ return render(columns).map((line) => truncateToWidth(line, columns, ""));
19
+ },
20
+ invalidate,
21
+ };
22
+ }
23
+
24
+ export function conversationView(children: Component[]): Component {
25
+ return boundedView(
26
+ (width) => children.flatMap((child) => child.render(width)),
27
+ () => {
28
+ for (const child of children) child.invalidate();
29
+ },
30
+ );
31
+ }
32
+
33
+ function singleLine(text: string): string {
34
+ return sanitizeTerminalDocument(text).replace(/\s+/gu, " ").trim();
35
+ }
36
+
37
+ export function displayLine(theme: Theme, text: string, role: ThemeColor = "toolOutput"): Component {
38
+ const safe = singleLine(text);
39
+ return boundedView((width) => [theme.fg(role, truncateToWidth(safe, width, "…"))]);
40
+ }
41
+
42
+ export function displayHeading(theme: Theme, title: string, detail = "", role: ThemeColor = "toolTitle"): Component {
43
+ const safeTitle = singleLine(title);
44
+ const safeDetail = singleLine(detail);
45
+ return boundedView((width) => [
46
+ truncateToWidth(
47
+ theme.fg(role, theme.bold(safeTitle)) + (safeDetail ? theme.fg("muted", ` · ${safeDetail}`) : ""),
48
+ width,
49
+ "…",
50
+ ),
51
+ ]);
52
+ }
53
+
54
+ export function displayDocument(theme: Theme, text: string, role: ThemeColor = "toolOutput"): Component {
55
+ const safe = sanitizeTerminalDocument(text);
56
+ return boundedView((width) => hardWrapTerminalDocument(safe, width).map((line) => theme.fg(role, line)));
57
+ }
58
+
59
+ export function displayJson(theme: Theme, value: unknown): Component {
60
+ const text = JSON.stringify(
61
+ value,
62
+ (_key, item) => (typeof item === "string" ? sanitizeTerminalDocument(item) : item),
63
+ 2,
64
+ );
65
+ return displayDocument(theme, text ?? "");
66
+ }
67
+
68
+ export function displayMarkdown(theme: Theme, text: string, role: ThemeColor = "toolOutput"): Component {
69
+ const safe = sanitizeTerminalDocument(text);
70
+ const markdown = new Markdown(safe, 0, 0, getMarkdownTheme(), { color: (value) => theme.fg(role, value) });
71
+ return boundedView(
72
+ (width) => markdown.render(width),
73
+ () => markdown.invalidate(),
74
+ );
75
+ }
76
+
77
+ export function expansionHint(theme: Theme, hints: Partial<KeybindingHints>): Component {
78
+ const key = hints.keyText?.("app.tools.expand");
79
+ return conversationView(key ? [displayLine(theme, `(${key} to expand)`, "muted")] : []);
80
+ }
81
+
82
+ export function textContent(content: string | ConversationResult["content"]): string {
83
+ return typeof content === "string"
84
+ ? content
85
+ : content
86
+ .filter((part) => part.type === "text" && typeof part.text === "string")
87
+ .map((part) => part.text)
88
+ .join("\n");
89
+ }
90
+
91
+ export function asRecord(value: unknown): Record<string, unknown> | undefined {
92
+ return value !== null && typeof value === "object" && !Array.isArray(value)
93
+ ? (value as Record<string, unknown>)
94
+ : undefined;
95
+ }
96
+
97
+ export function parseRecord(text: string): Record<string, unknown> | undefined {
98
+ try {
99
+ return asRecord(JSON.parse(text));
100
+ } catch {
101
+ return undefined;
102
+ }
103
+ }
104
+
105
+ export function jobPayload(value: unknown): JobPayload | undefined {
106
+ const record = asRecord(value);
107
+ if (!record || typeof record.jobId !== "string" || !JOB_STATES.some((state) => state === record.state))
108
+ return undefined;
109
+ if (["result", "error"].some((key) => record[key] !== undefined && typeof record[key] !== "string")) return undefined;
110
+ if (
111
+ record.limitations !== undefined &&
112
+ (!Array.isArray(record.limitations) || record.limitations.some((value) => typeof value !== "string"))
113
+ )
114
+ return undefined;
115
+ if (["timedOut", "interrupted"].some((key) => record[key] !== undefined && typeof record[key] !== "boolean"))
116
+ return undefined;
117
+ return record as JobPayload;
118
+ }
119
+
120
+ export function stateLabel(state: SubagentJobState): string {
121
+ return state === "timed_out" ? "timed out" : state;
122
+ }
123
+
124
+ export function stateRole(state: SubagentJobState): ThemeColor {
125
+ switch (state) {
126
+ case "completed":
127
+ return "success";
128
+ case "failed":
129
+ case "timed_out":
130
+ return "error";
131
+ case "partial":
132
+ case "cancelled":
133
+ return "warning";
134
+ case "running":
135
+ return "accent";
136
+ case "queued":
137
+ return "muted";
138
+ }
139
+ }
140
+
141
+ export function jobSummary(theme: Theme, payload: JobPayload): Component {
142
+ const waitStatus = payload.timedOut === true ? " · wait timed out (job not cancelled)" : "";
143
+ const interrupted = payload.interrupted === true ? " · interrupted by subagent message" : "";
144
+ const error = typeof payload.error === "string" && payload.error ? ` · ${payload.error}` : "";
145
+ const limitations = Array.isArray(payload.limitations) ? payload.limitations.length : 0;
146
+ return displayLine(
147
+ theme,
148
+ `${payload.jobId} · ${stateLabel(payload.state)}${waitStatus}${interrupted}${error}${limitations ? ` · ${limitations} limitation${limitations === 1 ? "" : "s"}` : ""}`,
149
+ payload.state === "failed" || payload.state === "timed_out"
150
+ ? "error"
151
+ : payload.timedOut === true || payload.interrupted === true
152
+ ? "warning"
153
+ : stateRole(payload.state),
154
+ );
155
+ }
156
+
157
+ export function jobBody(theme: Theme, payload: JobPayload): Component {
158
+ const children: Component[] = [];
159
+ if (typeof payload.error === "string" && payload.error) {
160
+ children.push(displayDocument(theme, `Error: ${payload.error}`, "error"));
161
+ }
162
+ if (Array.isArray(payload.limitations)) {
163
+ for (const limitation of payload.limitations) {
164
+ if (typeof limitation === "string") children.push(displayDocument(theme, `Limitation: ${limitation}`, "warning"));
165
+ }
166
+ }
167
+ if (typeof payload.result === "string" && payload.result) children.push(displayMarkdown(theme, payload.result));
168
+ return conversationView(children);
169
+ }
@@ -1,54 +1,104 @@
1
- import { type ExtensionAPI, getMarkdownTheme } from "@earendil-works/pi-coding-agent";
2
- import { Box, type Component, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
3
- import { sanitizeTerminalText } from "./message-broker.js";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Box, type Component } from "@earendil-works/pi-tui";
3
+ import {
4
+ asRecord,
5
+ boundedView,
6
+ displayDocument,
7
+ displayHeading,
8
+ displayLine,
9
+ expansionHint,
10
+ jobBody,
11
+ jobPayload,
12
+ jobSummary,
13
+ parseRecord,
14
+ textContent,
15
+ } from "./conversation-view.js";
4
16
 
5
17
  export const COMPLETION_MESSAGE_TYPE = "pi-subagents-completion";
6
18
  export const BROKER_MESSAGE_TYPE = "pi-subagents-message";
19
+ const COMPLETION_PREFIX = "Subagent job completion:\n";
20
+
21
+ type BrokerDetails = { kind: "request" | "response"; jobId: string; requestId: string };
22
+
23
+ function brokerDetails(value: unknown): BrokerDetails | undefined {
24
+ const details = asRecord(value);
25
+ return details &&
26
+ (details.kind === "request" || details.kind === "response") &&
27
+ typeof details.jobId === "string" &&
28
+ typeof details.requestId === "string"
29
+ ? (details as BrokerDetails)
30
+ : undefined;
31
+ }
32
+
33
+ function legacyBrokerDetails(content: string): BrokerDetails | undefined {
34
+ const [type, protocol, request, job] = content.split("\n", 4);
35
+ if (
36
+ protocol !== "Protocol: pi-subagents:main-message:v1" ||
37
+ !request?.startsWith("Request ID: ") ||
38
+ !job?.startsWith("Job ID: ")
39
+ )
40
+ return undefined;
41
+ const kind =
42
+ type === "Message Type: SUBAGENT_REQUEST"
43
+ ? "request"
44
+ : type === "Message Type: SUBAGENT_RESPONSE"
45
+ ? "response"
46
+ : undefined;
47
+ return brokerDetails({ kind, requestId: request.slice("Request ID: ".length), jobId: job.slice("Job ID: ".length) });
48
+ }
7
49
 
8
50
  function createMessageBox(children: Component[], outputPad: number, background: (text: string) => string): Component {
9
- return {
10
- render(width) {
11
- if (width <= 0) return [];
12
- const maxPadding = Math.floor((width - 1) / 2);
13
- const box = new Box(Math.min(Math.max(0, outputPad), maxPadding), 1, background);
51
+ return boundedView(
52
+ (width) => {
53
+ const padding = Number.isFinite(outputPad) ? Math.max(0, Math.floor(outputPad)) : 0;
54
+ const box = new Box(Math.min(padding, Math.floor((width - 1) / 2)), 1, background);
14
55
  for (const child of children) box.addChild(child);
15
56
  return box.render(width);
16
57
  },
17
- invalidate() {
58
+ () => {
18
59
  for (const child of children) child.invalidate();
19
60
  },
20
- };
61
+ );
21
62
  }
22
63
 
23
64
  export function registerMessageRenderers(pi: ExtensionAPI): void {
24
- for (const [messageType, summary] of [
25
- [COMPLETION_MESSAGE_TYPE, "Subagent job completion"],
26
- [BROKER_MESSAGE_TYPE, "Subagent message"],
27
- ] as const) {
28
- pi.registerMessageRenderer(messageType, (message, options, theme) => {
29
- const children: Component[] = [
30
- new Text(theme.fg("customMessageLabel", theme.bold(`[${messageType}]`)), 0, 0),
31
- new Spacer(1),
32
- ];
33
- if (!options.expanded) {
34
- const expansionKey = options.keyText?.("app.tools.expand");
35
- const hint = expansionKey ? theme.fg("muted", ` (${expansionKey} to expand)`) : "";
36
- children.push(new Text(theme.fg("customMessageText", summary) + hint, 0, 0));
37
- } else {
38
- const content =
39
- typeof message.content === "string"
40
- ? message.content
41
- : message.content
42
- .filter((part) => part.type === "text")
43
- .map((part) => part.text)
44
- .join("\n");
45
- children.push(
46
- new Markdown(sanitizeTerminalText(content), 0, 0, getMarkdownTheme(), {
47
- color: (text) => theme.fg("customMessageText", text),
48
- }),
49
- );
50
- }
51
- return createMessageBox(children, options.outputPad, (text) => theme.bg("customMessageBg", text));
52
- });
53
- }
65
+ pi.registerMessageRenderer(COMPLETION_MESSAGE_TYPE, (message, options, theme) => {
66
+ const content = textContent(message.content);
67
+ const payload =
68
+ jobPayload(message.details) ??
69
+ jobPayload(
70
+ parseRecord(content.startsWith(COMPLETION_PREFIX) ? content.slice(COMPLETION_PREFIX.length) : content),
71
+ );
72
+ const children = [displayHeading(theme, "Subagent job completion", "", "customMessageLabel")];
73
+ if (payload) children.push(jobSummary(theme, payload));
74
+ if (options.expanded) {
75
+ children.push(payload ? jobBody(theme, payload) : displayDocument(theme, content, "customMessageText"));
76
+ } else {
77
+ children.push(expansionHint(theme, options));
78
+ }
79
+ return createMessageBox(children, options.outputPad, (text) => theme.bg("customMessageBg", text));
80
+ });
81
+
82
+ pi.registerMessageRenderer(BROKER_MESSAGE_TYPE, (message, options, theme) => {
83
+ const content = textContent(message.content);
84
+ const details = brokerDetails(message.details) ?? legacyBrokerDetails(content);
85
+ const title = details ? `Subagent ${details.kind}` : "Subagent message";
86
+ const source = details ? `from ${details.jobId}` : "";
87
+ const children = [displayHeading(theme, title, source, "customMessageLabel")];
88
+ if (details) {
89
+ children.push(
90
+ displayLine(
91
+ theme,
92
+ `${details.requestId} · ${details.kind === "request" ? "needs reply" : "response received"}`,
93
+ "muted",
94
+ ),
95
+ );
96
+ }
97
+ if (options.expanded) {
98
+ children.push(displayDocument(theme, content, "customMessageText"));
99
+ } else {
100
+ children.push(expansionHint(theme, options));
101
+ }
102
+ return createMessageBox(children, options.outputPad, (text) => theme.bg("customMessageBg", text));
103
+ });
54
104
  }
@@ -0,0 +1,156 @@
1
+ import type { Theme, ToolRenderers } from "@earendil-works/pi-coding-agent";
2
+ import type { Component } from "@earendil-works/pi-tui";
3
+ import {
4
+ asRecord,
5
+ type ConversationResult,
6
+ conversationView,
7
+ displayDocument,
8
+ displayHeading,
9
+ displayJson,
10
+ displayLine,
11
+ expansionHint,
12
+ jobBody,
13
+ jobPayload,
14
+ jobSummary,
15
+ parseRecord,
16
+ stateLabel,
17
+ textContent,
18
+ } from "./conversation-view.js";
19
+ import { JOB_STATES } from "./types.js";
20
+
21
+ export type SubagentToolAction = "spawn" | "inspect" | "cancel" | "wait" | "send";
22
+
23
+ const LABELS = { spawn: "Spawn", inspect: "Inspect", cancel: "Cancel", wait: "Wait", send: "Send" } as const;
24
+ const PENDING = {
25
+ spawn: "Starting subagent…",
26
+ inspect: "Inspecting jobs…",
27
+ cancel: "Cancelling job…",
28
+ wait: "Waiting for job or subagent message…",
29
+ send: "Sending message…",
30
+ } as const;
31
+ const INSPECT_PREVIEW_JOBS = 5;
32
+ const STATE_PRIORITY = { failed: 0, timed_out: 1, partial: 2, running: 3, queued: 4, cancelled: 5, completed: 6 };
33
+
34
+ // Pi owns the default shell. Its minimum content cell and padding can exceed a
35
+ // viewport narrower than three cells; these renderers bound their inner content.
36
+ export function subagentToolRenderers(action: SubagentToolAction): ToolRenderers {
37
+ return {
38
+ renderCall(input, theme, context) {
39
+ const args = asRecord(input) ?? {};
40
+ const target =
41
+ action === "send"
42
+ ? typeof args.requestId === "string"
43
+ ? `reply to ${args.requestId}`
44
+ : typeof args.recipient === "string"
45
+ ? `to ${args.recipient}`
46
+ : ""
47
+ : typeof args.jobId === "string"
48
+ ? args.jobId
49
+ : "";
50
+ const children = [displayHeading(theme, `Subagent · ${LABELS[action]}`, target)];
51
+ if (context.expanded) {
52
+ for (const [key, value] of Object.entries(args)) {
53
+ if (typeof value === "string") {
54
+ children.push(displayDocument(theme, `${key}: ${value}`, "muted"));
55
+ } else if (value !== undefined) {
56
+ children.push(displayLine(theme, `${key}:`, "muted"), displayJson(theme, value));
57
+ }
58
+ }
59
+ } else {
60
+ const preview = action === "spawn" ? args.task : action === "send" ? args.message : undefined;
61
+ if (typeof preview === "string") children.push(displayLine(theme, preview, "muted"));
62
+ }
63
+ return conversationView(children);
64
+ },
65
+ renderResult(result, { expanded, isPartial }, theme, context) {
66
+ const raw = textContent(result.content);
67
+ const children: Component[] = [];
68
+ if (context.isError) {
69
+ children.push(
70
+ displayLine(theme, `${LABELS[action]} failed`, "error"),
71
+ expanded ? displayDocument(theme, raw, "error") : displayLine(theme, raw, "error"),
72
+ );
73
+ } else if (isPartial) {
74
+ return displayLine(theme, PENDING[action], "muted");
75
+ } else {
76
+ const payload = toolPayload(action, result);
77
+ if (!payload) {
78
+ children.push(
79
+ expanded
80
+ ? displayDocument(theme, raw || "No result returned.")
81
+ : displayLine(theme, raw || "No result returned."),
82
+ );
83
+ } else if (action === "inspect") {
84
+ children.push(...inspectSummary(theme, payload, expanded));
85
+ if (expanded) children.push(displayJson(theme, payload));
86
+ } else if (action === "send") {
87
+ const accepted = payload.accepted === true;
88
+ const reply = typeof asRecord(context.args)?.requestId === "string";
89
+ children.push(
90
+ displayLine(
91
+ theme,
92
+ `${reply ? "Reply" : "Request"} ${accepted ? "accepted" : "not accepted"}${payload.duplicate === true ? " · duplicate" : ""} · ${payload.requestId}`,
93
+ accepted ? "success" : "warning",
94
+ ),
95
+ );
96
+ if (expanded) children.push(displayJson(theme, payload));
97
+ } else {
98
+ const job = jobPayload(payload);
99
+ if (job) children.push(jobSummary(theme, job));
100
+ if (expanded) {
101
+ if (action === "wait" && job) {
102
+ const { result: _result, error: _error, limitations: _limitations, ...metadata } = job;
103
+ children.push(displayJson(theme, metadata), jobBody(theme, job));
104
+ } else {
105
+ children.push(displayJson(theme, payload));
106
+ }
107
+ }
108
+ }
109
+ }
110
+ if (!expanded) children.push(expansionHint(theme, context));
111
+ return conversationView(children);
112
+ },
113
+ };
114
+ }
115
+
116
+ function toolPayload(action: SubagentToolAction, result: ConversationResult): Record<string, unknown> | undefined {
117
+ for (const value of [result.details, parseRecord(textContent(result.content))]) {
118
+ const record = asRecord(value);
119
+ if (!record) continue;
120
+ if (action === "inspect") {
121
+ if (Array.isArray(record.jobs) && record.jobs.every((job) => jobPayload(job))) return record;
122
+ } else if (action === "send") {
123
+ if (typeof record.requestId === "string" && typeof record.accepted === "boolean") return record;
124
+ } else if (jobPayload(record)) {
125
+ return record;
126
+ }
127
+ }
128
+ return undefined;
129
+ }
130
+
131
+ function inspectSummary(theme: Theme, payload: Record<string, unknown>, expanded: boolean): Component[] {
132
+ const jobs = (payload.jobs as unknown[]).map(jobPayload).filter((job) => job !== undefined);
133
+ const counts = JOB_STATES.map((state) => {
134
+ const count = jobs.filter((job) => job.state === state).length;
135
+ return count ? `${count} ${stateLabel(state)}` : undefined;
136
+ }).filter(Boolean);
137
+ const children = [
138
+ displayLine(
139
+ theme,
140
+ jobs.length ? `${jobs.length} job${jobs.length === 1 ? "" : "s"} · ${counts.join(" · ")}` : "No retained jobs.",
141
+ "muted",
142
+ ),
143
+ ];
144
+ if (!expanded) {
145
+ const sorted = [...jobs].sort((left, right) => STATE_PRIORITY[left.state] - STATE_PRIORITY[right.state]);
146
+ for (const job of sorted.slice(0, INSPECT_PREVIEW_JOBS)) children.push(jobSummary(theme, job));
147
+ if (jobs.length > INSPECT_PREVIEW_JOBS) {
148
+ children.push(displayLine(theme, `${jobs.length - INSPECT_PREVIEW_JOBS} more jobs`, "muted"));
149
+ }
150
+ }
151
+ const omitted = asRecord(payload.omitted)?.jobs;
152
+ if (typeof omitted === "number" && omitted > 0) {
153
+ children.push(displayLine(theme, `${omitted} expired or removed jobs not retained`, "muted"));
154
+ }
155
+ return children;
156
+ }
package/src/tools.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  } from "./resource-attachments.js";
26
26
  import { type RuntimeDependencies, type SubagentModelOverrides, SubagentRuntime } from "./runtime.js";
27
27
  import { prepareTimeoutArguments, resolveTimeoutMs } from "./timeout.js";
28
+ import { subagentToolRenderers } from "./tool-renderers.js";
28
29
  import {
29
30
  CHILD_CORE_TOOL_NAMES,
30
31
  DEFAULT_SUBAGENT_TOOLS,
@@ -205,6 +206,7 @@ export function registerSubagentTools(
205
206
  pi.registerTool({
206
207
  name: "subagent_spawn",
207
208
  label: "Subagent · Spawn",
209
+ ...subagentToolRenderers("spawn"),
208
210
  description:
209
211
  "Use subagent_spawn to start one Pi subagent job and return its jobId immediately. The task defines the child's specialization; selected core tools and explicit trusted local skills or extensions define its initial capabilities. The job may ask the main agent questions and publishes one asynchronous completion when terminal.",
210
212
  promptSnippet: "Use subagent_spawn to start one Pi subagent job",
@@ -272,6 +274,7 @@ export function registerSubagentTools(
272
274
  pi.registerTool({
273
275
  name: "subagent_inspect",
274
276
  label: "Subagent · Inspect",
277
+ ...subagentToolRenderers("inspect"),
275
278
  description:
276
279
  "Use subagent_inspect to return one privacy-filtered snapshot of retained jobs without exposing task text, complete child output, prompts, selected tools, context, credentials, or broker messages.",
277
280
  promptSnippet: "Use subagent_inspect to inspect retained subagent jobs",
@@ -286,6 +289,7 @@ export function registerSubagentTools(
286
289
  pi.registerTool({
287
290
  name: "subagent_cancel",
288
291
  label: "Subagent · Cancel",
292
+ ...subagentToolRenderers("cancel"),
289
293
  description:
290
294
  "Use subagent_cancel to idempotently cancel one queued or running job and release its process, timer, broker credentials, and temporary resources. Terminal jobs remain unchanged.",
291
295
  promptSnippet: "Use subagent_cancel to cancel one active subagent job",
@@ -299,6 +303,7 @@ export function registerSubagentTools(
299
303
  pi.registerTool({
300
304
  name: "subagent_wait",
301
305
  label: "Subagent · Wait",
306
+ ...subagentToolRenderers("wait"),
302
307
  description:
303
308
  "Use subagent_wait to wait for one job to become terminal. An incoming child request or response interrupts the wait without cancelling the job. A timeout or caller cancellation stops only this wait.",
304
309
  promptSnippet: "Use subagent_wait to wait for one subagent job or incoming message",
@@ -313,6 +318,7 @@ export function registerSubagentTools(
313
318
  pi.registerTool({
314
319
  name: "subagent_send",
315
320
  label: "Subagent · Send",
321
+ ...subagentToolRenderers("send"),
316
322
  description:
317
323
  "Use subagent_send to send one request to an active job or answer one pending child request. For a new request, provide recipient and omit requestId. To answer a request, provide requestId and omit recipient. Provide exactly one of recipient or requestId. An accepted new request interrupts any active child response wait so delivery can proceed without consuming the child's original request.",
318
324
  promptSnippet: "Use subagent_send to send or answer one subagent message",