@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.
- package/CHANGELOG.md +6 -0
- package/README.md +5 -4
- package/dist/index.ts +317 -34
- package/dist/index.ts.map +4 -4
- package/docs/conversation-display.md +10 -0
- package/package.json +1 -1
- package/src/conversation-view.ts +169 -0
- package/src/message-renderers.ts +90 -40
- package/src/tool-renderers.ts +156 -0
- package/src/tools.ts +6 -0
|
@@ -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
|
@@ -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
|
+
}
|
package/src/message-renderers.ts
CHANGED
|
@@ -1,54 +1,104 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Box, type Component
|
|
3
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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",
|