@ian-pascoe/pi-minimal-subagents 0.17.0 → 0.18.0
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/README.md +19 -4
- package/package.json +2 -2
- package/skills/pi-minimal-subagents/SKILL.md +1 -1
- package/src/minimal-subagents-coordinator.ts +19 -6
- package/src/minimal-subagents-render-contract.ts +2 -0
- package/src/minimal-subagents-rendering.ts +8 -3
- package/src/minimal-subagents-result-text.ts +408 -0
- package/src/minimal-subagents-tool-schemas.ts +6 -0
- package/src/minimal-subagents-tools.ts +75 -73
- package/src/minimal-subagents-types.ts +8 -0
package/README.md
CHANGED
|
@@ -42,7 +42,9 @@ From this package checkout, load the source directly with
|
|
|
42
42
|
response arrives automatically as a steer message. Without `turn_id`, a wait
|
|
43
43
|
skips turns whose result you already claimed or received automatically, and
|
|
44
44
|
never repeats an automatically delivered result: it reports
|
|
45
|
-
`already_delivered: true` instead of the output.
|
|
45
|
+
`already_delivered: true` instead of the output. If the result is handed to Pi but
|
|
46
|
+
not yet in your conversation, it also sets `delivery_pending: true` and the
|
|
47
|
+
result arrives as a separate message; no reread needed.
|
|
46
48
|
3. `agent_message` continues an idle child: the result reports
|
|
47
49
|
`disposition: "started-turn"` with the new `turn_id`. A plain
|
|
48
50
|
`subagent_wait({ agent_id })` targets that new turn, not the first result;
|
|
@@ -288,11 +290,22 @@ assistant message but omits image data. Timeout Wait Events carry only a compact
|
|
|
288
290
|
progress snapshot. Children target only direct children; the Root Agent
|
|
289
291
|
may inspect any descendant. Model-facing status reports a `child_count` but no
|
|
290
292
|
nested `children` summaries. Reported `usage` always includes `cacheWrite1h`
|
|
291
|
-
and `reasoning`, as `0` when the provider reports none.
|
|
292
|
-
|
|
293
|
+
and `reasoning`, as `0` when the provider reports none. `structuredContent`
|
|
294
|
+
rounds every `usage.cost` field to six decimal places of USD;
|
|
293
295
|
exact values stay in session data. `subagent_cancel`
|
|
294
296
|
lists in `affected_agent_ids` only agents whose active turns it cancelled.
|
|
295
297
|
|
|
298
|
+
Coordinator Tools give the model compact text, not JSON: a spawn is one line
|
|
299
|
+
naming the tool preset and count rather than the tool list; a settled wait is
|
|
300
|
+
status, elapsed time, one token and cost total, then the output; cancel and
|
|
301
|
+
delete are one line each, and a cancel that found nothing running says so.
|
|
302
|
+
Targeted `subagent_status` opens with a summary line, lists active tools once,
|
|
303
|
+
describes the Launch Contract grant and capability ceiling relative to them,
|
|
304
|
+
and previews the task, latest output, and last three Recent Activity items;
|
|
305
|
+
`verbose: true` adds the full task, output, ceiling, usage breakdown, recent
|
|
306
|
+
messages, and Recent Activity. Codemode scripts receive the complete record in
|
|
307
|
+
`structuredContent`, and the transcript renderer reads `details`.
|
|
308
|
+
|
|
296
309
|
`session_context` defaults to `omit`: the child starts with only its system
|
|
297
310
|
prompt and task, so the task should be a self-contained brief covering the goal,
|
|
298
311
|
target files and non-goals, the change or question, and acceptance criteria.
|
|
@@ -352,7 +365,9 @@ remaining observable turn, falling back to the active, then latest, turn. When
|
|
|
352
365
|
that fallback reaches a result already delivered to you automatically, whether
|
|
353
366
|
still queued or settled, the wait returns `event: "turn"` with
|
|
354
367
|
`already_delivered: true`, the turn identity, and status, but no output; it does
|
|
355
|
-
not claim the turn, and `turn_id` rereads the full result. A
|
|
368
|
+
not claim the turn, and `turn_id` rereads the full result. A result handed but
|
|
369
|
+
not yet recorded in your branch also carries `delivery_pending: true`: it will
|
|
370
|
+
arrive as a separate message (redelivered if Pi discards the queued steer), so no reread is needed. A
|
|
356
371
|
caller may have only one outstanding wait for the same source turn; a
|
|
357
372
|
concurrent duplicate is rejected instead of competing for one Wait Event.
|
|
358
373
|
When `timeout_ms` expires, the wait returns an observational `event: "timeout"`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-minimal-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Persistent nested subagents with bounded delegation for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"provenance": true
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ian-pascoe/pi-utils": "^0.
|
|
34
|
+
"@ian-pascoe/pi-utils": "^0.6.0",
|
|
35
35
|
"minimatch": "^10.2.6"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
@@ -9,7 +9,7 @@ disable-model-invocation: true
|
|
|
9
9
|
|
|
10
10
|
1. Read the relevant access, spawn, capability, delivery, or recovery section of [`../../README.md`](../../README.md).
|
|
11
11
|
2. For Subagent Access or missing Root Agent Coordinator Tools, run `/subagents status` first and record the effective source plus actual active-tool count.
|
|
12
|
-
3. From the direct parent, use `subagent_status` to capture the Child Agent's Launch Contract, effective tools, Runtime Profile, dependencies, result, and Recent Activity.
|
|
12
|
+
3. From the direct parent, use `subagent_status` with `verbose: true` to capture the Child Agent's Launch Contract, effective tools, Runtime Profile, dependencies, result, and Recent Activity.
|
|
13
13
|
4. Classify the fault as Subagent Access, launch resolution, adjacency, ordinary-tool ceiling, delivery state, or Registry recovery.
|
|
14
14
|
5. For access changes, use the narrowest `/subagents enable|disable|reset` scope. For other configuration, identify the effective layer, make one scoped edit, validate JSON, and reload Pi.
|
|
15
15
|
6. Repeat the same spawn or coordination operation. Finish when it succeeds or one named access, launch, adjacency, capability, delivery, or recovery boundary is evidenced.
|
|
@@ -70,6 +70,7 @@ import type {
|
|
|
70
70
|
TurnResult,
|
|
71
71
|
WaitMessageResult,
|
|
72
72
|
WaitTimeoutResult,
|
|
73
|
+
AutomaticDelivery,
|
|
73
74
|
WaitDeliveredTurnResult,
|
|
74
75
|
WaitResult,
|
|
75
76
|
} from "./minimal-subagents-types.js";
|
|
@@ -155,6 +156,7 @@ function terminalWaitResult(result: TurnResult, messages: WaitMessageResult[] =
|
|
|
155
156
|
function alreadyDeliveredWaitResult(
|
|
156
157
|
result: TurnResult,
|
|
157
158
|
messages: WaitMessageResult[],
|
|
159
|
+
delivery: AutomaticDelivery,
|
|
158
160
|
): WaitDeliveredTurnResult {
|
|
159
161
|
const delivered: WaitDeliveredTurnResult = {
|
|
160
162
|
event: "turn",
|
|
@@ -163,6 +165,7 @@ function alreadyDeliveredWaitResult(
|
|
|
163
165
|
status: result.status,
|
|
164
166
|
already_delivered: true,
|
|
165
167
|
};
|
|
168
|
+
if (delivery === "handed") delivered.delivery_pending = true;
|
|
166
169
|
if (messages.length > 0) delivered.messages = structuredClone(messages);
|
|
167
170
|
return delivered;
|
|
168
171
|
}
|
|
@@ -515,8 +518,12 @@ export class MinimalSubagentsCoordinator {
|
|
|
515
518
|
const messages = this.drainPendingParentMessages(callerId, agentId, turnId);
|
|
516
519
|
// A default wait falls back to the latest turn, whose result may already have been delivered
|
|
517
520
|
// automatically; repeating its output would deliver it twice. An explicit turn_id rereads it.
|
|
518
|
-
|
|
519
|
-
|
|
521
|
+
const delivery =
|
|
522
|
+
requestedTurnId === undefined
|
|
523
|
+
? this.automaticDelivery(callerId, retainedResult)
|
|
524
|
+
: undefined;
|
|
525
|
+
if (delivery) {
|
|
526
|
+
return Promise.resolve(alreadyDeliveredWaitResult(retainedResult, messages, delivery));
|
|
520
527
|
}
|
|
521
528
|
this.claimTerminalDelivery(callerId, retainedResult);
|
|
522
529
|
return Promise.resolve(terminalWaitResult(retainedResult, messages));
|
|
@@ -1772,17 +1779,23 @@ export class MinimalSubagentsCoordinator {
|
|
|
1772
1779
|
/**
|
|
1773
1780
|
* Whether this result was already delivered to the caller automatically: it is handed, or the
|
|
1774
1781
|
* caller's branch holds its automatic result message. A wait's own tool result does not count.
|
|
1782
|
+
* "recorded" means the branch already holds it; "handed" means it is queued to Pi but not yet
|
|
1783
|
+
* recorded, so it still arrives as a separate message (redelivered if Pi discarded the queued steer).
|
|
1775
1784
|
*/
|
|
1776
|
-
private
|
|
1777
|
-
|
|
1778
|
-
this.handedTerminalKeys.has(deliveryTurnKey(result.agent_id, result.turn_id)) ||
|
|
1785
|
+
private automaticDelivery(callerId: string, result: TurnResult): AutomaticDelivery | undefined {
|
|
1786
|
+
if (
|
|
1779
1787
|
this.hasRecipientDeliveryEvidence(
|
|
1780
1788
|
callerId,
|
|
1781
1789
|
result.agent_id,
|
|
1782
1790
|
result.turn_id,
|
|
1783
1791
|
automaticResultMessageId(result.agent_id, result.turn_id),
|
|
1784
1792
|
)
|
|
1785
|
-
)
|
|
1793
|
+
) {
|
|
1794
|
+
return "recorded";
|
|
1795
|
+
}
|
|
1796
|
+
return this.handedTerminalKeys.has(deliveryTurnKey(result.agent_id, result.turn_id))
|
|
1797
|
+
? "handed"
|
|
1798
|
+
: undefined;
|
|
1786
1799
|
}
|
|
1787
1800
|
|
|
1788
1801
|
private hasDeliveryEvidence(delivery: PersistedDelivery): boolean {
|
|
@@ -147,6 +147,7 @@ const WaitTurnDetailsSchema = Type.Object({
|
|
|
147
147
|
elapsed_ms: Type.Optional(Type.Number()),
|
|
148
148
|
usage: Type.Optional(RenderUsageSchema),
|
|
149
149
|
already_delivered: Type.Optional(Type.Boolean()),
|
|
150
|
+
delivery_pending: Type.Optional(Type.Boolean()),
|
|
150
151
|
messages: Type.Optional(Type.Array(WaitMessageDetailsSchema)),
|
|
151
152
|
});
|
|
152
153
|
const WaitTimeoutDetailsSchema = Type.Object({
|
|
@@ -314,6 +315,7 @@ export const CoordinatorToolOutputSchemas = {
|
|
|
314
315
|
...Type.Pick(CoordinatorTurnOutputSchema, ["agent_id", "turn_id", "status"]).properties,
|
|
315
316
|
event: Type.Literal("turn"),
|
|
316
317
|
already_delivered: Type.Literal(true),
|
|
318
|
+
delivery_pending: Type.Optional(Type.Literal(true)),
|
|
317
319
|
messages: Type.Optional(Type.Array(CoordinatorWaitMessageOutputSchema)),
|
|
318
320
|
...CoordinatorWaitSourceOutputSchema,
|
|
319
321
|
}),
|
|
@@ -379,7 +379,7 @@ function latestProgressActivity(activity: readonly RenderRecentActivity[]): Rend
|
|
|
379
379
|
}
|
|
380
380
|
|
|
381
381
|
/** Format a turn's cost for summary rows; sub-cent costs keep enough precision to be non-zero. */
|
|
382
|
-
function formatSubagentCost(usage: Usage | undefined): string | undefined {
|
|
382
|
+
export function formatSubagentCost(usage: Usage | undefined): string | undefined {
|
|
383
383
|
const total = usage?.cost.total;
|
|
384
384
|
if (total === undefined || !(total > 0)) return undefined;
|
|
385
385
|
return `$${total.toFixed(total < 0.01 ? 4 : 2)}`;
|
|
@@ -591,6 +591,11 @@ function renderWaitProgress(
|
|
|
591
591
|
|
|
592
592
|
/** Shown instead of the output of a result already delivered to the parent automatically. */
|
|
593
593
|
const ALREADY_DELIVERED_TEXT = "Already delivered automatically; wait with turn_id to reread it.";
|
|
594
|
+
const DELIVERY_PENDING_TEXT = "Handed to you automatically; it arrives as a separate message.";
|
|
595
|
+
|
|
596
|
+
function alreadyDeliveredText(details: { delivery_pending?: boolean }): string {
|
|
597
|
+
return details.delivery_pending ? DELIVERY_PENDING_TEXT : ALREADY_DELIVERED_TEXT;
|
|
598
|
+
}
|
|
594
599
|
|
|
595
600
|
/** The activity labels of a timeout result, including pre-compact results that carried the full status. */
|
|
596
601
|
function timeoutActivityLabels(
|
|
@@ -616,7 +621,7 @@ function collapsedWaitBody(
|
|
|
616
621
|
: undefined;
|
|
617
622
|
}
|
|
618
623
|
if (details.already_delivered) {
|
|
619
|
-
return collapsedText(theme, theme.fg("muted",
|
|
624
|
+
return collapsedText(theme, theme.fg("muted", alreadyDeliveredText(details)));
|
|
620
625
|
}
|
|
621
626
|
const output = details.output ?? "";
|
|
622
627
|
if (status === "completed") {
|
|
@@ -691,7 +696,7 @@ function renderWaitResult(
|
|
|
691
696
|
}
|
|
692
697
|
const output = details.output ?? "";
|
|
693
698
|
if (details.already_delivered) {
|
|
694
|
-
appendTextSection(container, theme, "Output",
|
|
699
|
+
appendTextSection(container, theme, "Output", alreadyDeliveredText(details));
|
|
695
700
|
} else if (status === "completed") {
|
|
696
701
|
if (output.length > 0) {
|
|
697
702
|
appendComponentSection(
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
formatSubagentCost,
|
|
4
|
+
formatSubagentDuration,
|
|
5
|
+
formatSubagentPreview,
|
|
6
|
+
formatSubagentTokenCount,
|
|
7
|
+
formatSubagentUsage,
|
|
8
|
+
} from "./minimal-subagents-rendering.js";
|
|
9
|
+
import type {
|
|
10
|
+
AgentDetail,
|
|
11
|
+
AgentMessageResult,
|
|
12
|
+
AgentSummary,
|
|
13
|
+
CancelResult,
|
|
14
|
+
DeleteResult,
|
|
15
|
+
SpawnResult,
|
|
16
|
+
StatusResult,
|
|
17
|
+
ToolSelection,
|
|
18
|
+
WaitDeliveredTurnResult,
|
|
19
|
+
WaitMessageResult,
|
|
20
|
+
WaitResult,
|
|
21
|
+
WaitTimeoutResult,
|
|
22
|
+
WaitTurnResult,
|
|
23
|
+
} from "./minimal-subagents-types.js";
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* Model-facing text for Coordinator Tool results. Each renderer states the facts a model acts on
|
|
27
|
+
* in a few lines; `details` and `structuredContent` keep the complete record, so nothing dropped
|
|
28
|
+
* here is lost to codemode scripts or the transcript renderer.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** Width of the one-line task preview in summaries. */
|
|
32
|
+
const TASK_PREVIEW_WIDTH = 120;
|
|
33
|
+
/** Width of the one-line latest-result preview in a non-verbose status. */
|
|
34
|
+
const RESULT_PREVIEW_WIDTH = 200;
|
|
35
|
+
/** Width of each one-line Recent Activity preview in a non-verbose status. */
|
|
36
|
+
const ACTIVITY_PREVIEW_WIDTH = 120;
|
|
37
|
+
/** Recent Activity items a non-verbose status previews, newest last. */
|
|
38
|
+
const STATUS_ACTIVITY_PREVIEW_COUNT = 3;
|
|
39
|
+
|
|
40
|
+
function joinParts(parts: readonly (string | undefined)[]): string {
|
|
41
|
+
return parts.filter((part): part is string => Boolean(part)).join(" · ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function profile(model: string, thinkingLevel: string): string {
|
|
45
|
+
return `${model} (${thinkingLevel})`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One `tokens · cost` total, or undefined when usage is unknown. */
|
|
49
|
+
function usageTotal(usage: Usage | undefined): string | undefined {
|
|
50
|
+
if (!usage) return undefined;
|
|
51
|
+
return joinParts([
|
|
52
|
+
`${formatSubagentTokenCount(usage.totalTokens) ?? "0"} tokens`,
|
|
53
|
+
formatSubagentCost(usage),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function plural(count: number, singular: string, pluralForm = `${singular}s`): string {
|
|
58
|
+
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function listOrNone(values: readonly string[]): string {
|
|
62
|
+
return values.length === 0 ? "none" : values.join(", ");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function without(values: readonly string[], excluded: readonly string[]): string[] {
|
|
66
|
+
const excludedSet = new Set(excluded);
|
|
67
|
+
return values.filter((value) => !excludedSet.has(value));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Describe the granted tools relative to the requested selection, without repeating a preset's list. */
|
|
71
|
+
function spawnToolsSummary(
|
|
72
|
+
granted: readonly string[],
|
|
73
|
+
requested: ToolSelection | undefined,
|
|
74
|
+
): string {
|
|
75
|
+
const count = plural(granted.length, "tool");
|
|
76
|
+
if (requested === undefined) return `tools: inherited, ${count}`;
|
|
77
|
+
if (!Array.isArray(requested)) return `tools: ${requested} preset, ${count}`;
|
|
78
|
+
const added = without(granted, requested);
|
|
79
|
+
const missing = without(requested, granted);
|
|
80
|
+
const diff = [
|
|
81
|
+
added.length > 0 ? `+${added.join(", +")}` : undefined,
|
|
82
|
+
missing.length > 0 ? `-${missing.join(", -")}` : undefined,
|
|
83
|
+
].filter(Boolean);
|
|
84
|
+
return `tools: your list, ${count}${diff.length > 0 ? ` (${diff.join("; ")})` : ""}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** One launch line plus any tool-resolution warnings. */
|
|
88
|
+
export function formatSpawnResultText(
|
|
89
|
+
result: SpawnResult,
|
|
90
|
+
requestedTools: ToolSelection | undefined,
|
|
91
|
+
): string {
|
|
92
|
+
const lines = [
|
|
93
|
+
joinParts([
|
|
94
|
+
`Spawned ${result.agent_id} (turn ${result.turn_id}, ${result.status})`,
|
|
95
|
+
profile(result.model, result.thinking_level),
|
|
96
|
+
spawnToolsSummary(result.tools, requestedTools),
|
|
97
|
+
`delegation ${result.delegation}`,
|
|
98
|
+
]),
|
|
99
|
+
];
|
|
100
|
+
for (const warning of result.warnings ?? []) lines.push(`warning: ${warning}`);
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** One line naming how the message was delivered, or why it failed. */
|
|
105
|
+
export function formatAgentMessageResultText(result: AgentMessageResult): string {
|
|
106
|
+
switch (result.disposition) {
|
|
107
|
+
case "delivered-via-wait":
|
|
108
|
+
return `Message delivered to ${result.agent_id} through its active wait.`;
|
|
109
|
+
case "queued":
|
|
110
|
+
return `Message queued into ${result.agent_id}'s active turn.`;
|
|
111
|
+
case "started-turn":
|
|
112
|
+
return `${result.agent_id} was idle; the message started turn ${result.turn_id ?? "(unknown)"}. Wait on it with subagent_wait.`;
|
|
113
|
+
case "failed":
|
|
114
|
+
return `Message to ${result.agent_id} failed: ${result.error ?? "unknown error"}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function coordinationMessagesText(
|
|
119
|
+
agentId: string,
|
|
120
|
+
messages: readonly WaitMessageResult[] | undefined,
|
|
121
|
+
): string | undefined {
|
|
122
|
+
if (!messages || messages.length === 0) return undefined;
|
|
123
|
+
return messages.map((message) => `Message from ${agentId}: ${message.message}`).join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* One line saying an already-delivered result is not repeated and how to reread it, or that a
|
|
128
|
+
* handed result is still to arrive as a separate message, followed by any Coordination Messages
|
|
129
|
+
* the wait drained, which the parent has not seen yet.
|
|
130
|
+
*/
|
|
131
|
+
function formatWaitDeliveredText(result: WaitDeliveredTurnResult): string {
|
|
132
|
+
const subject = `Result of ${result.agent_id} turn ${result.turn_id} (${result.status})`;
|
|
133
|
+
const notice = result.delivery_pending
|
|
134
|
+
? `${subject} was handed to you automatically and arrives as a separate message; no reread is needed.`
|
|
135
|
+
: `${subject} was already delivered automatically; call subagent_wait with turn_id "${result.turn_id}" to reread it.`;
|
|
136
|
+
const messages = coordinationMessagesText(result.agent_id, result.messages);
|
|
137
|
+
return messages ? `${notice}\n${messages}` : notice;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A Coordination Message returned before its source turn settled. */
|
|
141
|
+
function formatWaitMessageText(result: WaitMessageResult): string {
|
|
142
|
+
return `Message from ${result.agent_id} (turn ${result.turn_id} still running):\n${result.message}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Status and elapsed time, one token/cost total, drained messages, then the turn's output. */
|
|
146
|
+
function formatWaitTurnText(result: WaitTurnResult): string {
|
|
147
|
+
const header = joinParts([
|
|
148
|
+
`${result.agent_id} turn ${result.turn_id} ${result.status}`,
|
|
149
|
+
formatSubagentDuration(result.elapsed_ms),
|
|
150
|
+
]);
|
|
151
|
+
const total = usageTotal(result.usage);
|
|
152
|
+
const lines = [header];
|
|
153
|
+
if (total) lines.push(`usage: ${total}`);
|
|
154
|
+
if (result.error) lines.push(`error: ${result.error}`);
|
|
155
|
+
const messages = coordinationMessagesText(result.agent_id, result.messages);
|
|
156
|
+
if (messages) lines.push(messages);
|
|
157
|
+
return `${lines.join("\n")}\n\n${result.output || "(no output)"}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The observational timeout snapshot; the wait never cancels the child. */
|
|
161
|
+
function formatWaitTimeoutText(result: WaitTimeoutResult): string {
|
|
162
|
+
const lines = [
|
|
163
|
+
`Wait timed out after ${formatSubagentDuration(result.timeout_ms) ?? `${result.timeout_ms}ms`}; ${result.agent_id} is ${result.state} on turn ${result.turn_id} and was not cancelled.`,
|
|
164
|
+
];
|
|
165
|
+
const progress = joinParts([
|
|
166
|
+
result.elapsed_ms === undefined
|
|
167
|
+
? undefined
|
|
168
|
+
: `elapsed ${formatSubagentDuration(result.elapsed_ms)}`,
|
|
169
|
+
result.latest_activity_at ? `last activity ${result.latest_activity_at}` : undefined,
|
|
170
|
+
result.total_tokens === undefined
|
|
171
|
+
? undefined
|
|
172
|
+
: `${formatSubagentTokenCount(result.total_tokens)} tokens`,
|
|
173
|
+
]);
|
|
174
|
+
if (progress) lines.push(progress);
|
|
175
|
+
if (result.recent_activity_labels.length > 0) {
|
|
176
|
+
lines.push(`recent activity: ${result.recent_activity_labels.join(", ")}`);
|
|
177
|
+
}
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Any Wait Event: a message, a settled or already-delivered turn, or a timeout. */
|
|
182
|
+
export function formatWaitResultText(result: WaitResult): string {
|
|
183
|
+
if ("already_delivered" in result) return formatWaitDeliveredText(result);
|
|
184
|
+
switch (result.event) {
|
|
185
|
+
case "message":
|
|
186
|
+
return formatWaitMessageText(result);
|
|
187
|
+
case "turn":
|
|
188
|
+
return formatWaitTurnText(result);
|
|
189
|
+
case "timeout":
|
|
190
|
+
return formatWaitTimeoutText(result);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function turnSummary(agent: AgentSummary): string {
|
|
195
|
+
if (agent.active_turn_id) return `turn ${agent.active_turn_id}`;
|
|
196
|
+
if (agent.latest_turn)
|
|
197
|
+
return `latest turn ${agent.latest_turn.turn_id} ${agent.latest_turn.status}`;
|
|
198
|
+
return "no turns";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function stateSummary(agent: AgentSummary): string {
|
|
202
|
+
return agent.availability === "unavailable" ? `${agent.state}, unavailable` : agent.state;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** One line per direct child; nested summaries and tool lists stay in `details`. */
|
|
206
|
+
function formatStatusListText(parentId: string, agents: readonly AgentSummary[]): string {
|
|
207
|
+
if (agents.length === 0) return `${parentId} has no children.`;
|
|
208
|
+
const lines = [`${plural(agents.length, "child", "children")} of ${parentId}:`];
|
|
209
|
+
for (const agent of agents) {
|
|
210
|
+
lines.push(
|
|
211
|
+
`- ${joinParts([
|
|
212
|
+
`${agent.agent_id} ${stateSummary(agent)}`,
|
|
213
|
+
turnSummary(agent),
|
|
214
|
+
profile(agent.model, agent.thinking_level),
|
|
215
|
+
formatSubagentDuration(agent.elapsed_ms),
|
|
216
|
+
agent.child_count > 0 ? plural(agent.child_count, "child", "children") : undefined,
|
|
217
|
+
agent.task ? `task: ${formatSubagentPreview(agent.task, TASK_PREVIEW_WIDTH)}` : undefined,
|
|
218
|
+
])}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return lines.join("\n");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Active tools, the Launch Contract grant, and the capability ceiling, each listed at most once:
|
|
226
|
+
* the grant and ceiling are described relative to the list before them.
|
|
227
|
+
*/
|
|
228
|
+
function toolLines(agent: AgentDetail, verbose: boolean): string[] {
|
|
229
|
+
const active = agent.tools;
|
|
230
|
+
const granted = agent.launch_contract.ordinary_tools;
|
|
231
|
+
const ceiling = agent.capability_ceiling;
|
|
232
|
+
const lines = [`active tools (${active.length}): ${listOrNone(active)}`];
|
|
233
|
+
|
|
234
|
+
const inactiveGrants = without(granted, active);
|
|
235
|
+
const adapterTools = without(active, granted);
|
|
236
|
+
if (inactiveGrants.length === 0 && adapterTools.length === 0) {
|
|
237
|
+
lines.push("granted tools: same as active");
|
|
238
|
+
} else {
|
|
239
|
+
const differences = [
|
|
240
|
+
inactiveGrants.length > 0
|
|
241
|
+
? `plus ${inactiveGrants.join(", ")} (granted but not active: codemode-only or undeclared)`
|
|
242
|
+
: undefined,
|
|
243
|
+
adapterTools.length > 0
|
|
244
|
+
? `minus ${adapterTools.join(", ")} (active from a runtime adapter, not granted)`
|
|
245
|
+
: undefined,
|
|
246
|
+
].filter(Boolean);
|
|
247
|
+
lines.push(`granted tools (${granted.length}): active ${differences.join("; ")}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const beyondGrant = without(ceiling, granted);
|
|
251
|
+
if (beyondGrant.length === 0) {
|
|
252
|
+
lines.push("capability ceiling: same as granted");
|
|
253
|
+
} else if (verbose) {
|
|
254
|
+
lines.push(`capability ceiling (${ceiling.length}): granted plus ${beyondGrant.join(", ")}`);
|
|
255
|
+
} else {
|
|
256
|
+
lines.push(
|
|
257
|
+
`capability ceiling: ${ceiling.length} tools, ${beyondGrant.length} beyond the grant`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return lines;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function launchLine(agent: AgentDetail): string {
|
|
264
|
+
const contract = agent.launch_contract;
|
|
265
|
+
const tools =
|
|
266
|
+
contract.tools === undefined
|
|
267
|
+
? "inherited tools"
|
|
268
|
+
: Array.isArray(contract.tools)
|
|
269
|
+
? "explicit tool list"
|
|
270
|
+
: `${contract.tools} preset`;
|
|
271
|
+
const launchedProfile =
|
|
272
|
+
contract.model !== agent.model || contract.thinking_level !== agent.thinking_level
|
|
273
|
+
? `launched as ${profile(contract.model, contract.thinking_level)}`
|
|
274
|
+
: undefined;
|
|
275
|
+
return `launch: ${joinParts([
|
|
276
|
+
contract.role ? `role ${contract.role}` : undefined,
|
|
277
|
+
launchedProfile,
|
|
278
|
+
tools,
|
|
279
|
+
`session context ${contract.session_context}`,
|
|
280
|
+
`project context ${contract.project_context}`,
|
|
281
|
+
`delegation ${contract.delegation ?? "none"}`,
|
|
282
|
+
])}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function indent(text: string): string {
|
|
286
|
+
return text
|
|
287
|
+
.split("\n")
|
|
288
|
+
.map((line) => ` ${line}`)
|
|
289
|
+
.join("\n");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* A summary line, then the Launch Contract, tools, latest result, and Recent Activity. Without
|
|
294
|
+
* `verbose` the task, result, and activity are one-line previews and the ceiling is a count.
|
|
295
|
+
*/
|
|
296
|
+
function formatStatusDetailText(agent: AgentDetail, verbose: boolean): string {
|
|
297
|
+
const lines = [
|
|
298
|
+
joinParts([
|
|
299
|
+
`${agent.agent_id} ${stateSummary(agent)}`,
|
|
300
|
+
turnSummary(agent),
|
|
301
|
+
profile(agent.model, agent.thinking_level),
|
|
302
|
+
agent.elapsed_ms === undefined
|
|
303
|
+
? undefined
|
|
304
|
+
: `elapsed ${formatSubagentDuration(agent.elapsed_ms)}`,
|
|
305
|
+
agent.latest_activity_at ? `last activity ${agent.latest_activity_at}` : undefined,
|
|
306
|
+
usageTotal(agent.usage),
|
|
307
|
+
]),
|
|
308
|
+
joinParts([`parent ${agent.parent_id}`, plural(agent.child_count, "child", "children")]),
|
|
309
|
+
];
|
|
310
|
+
if (agent.unavailable_reason) lines.push(`unavailable: ${agent.unavailable_reason}`);
|
|
311
|
+
if (agent.missing_dependencies.length > 0) {
|
|
312
|
+
lines.push(`missing dependencies: ${agent.missing_dependencies.join(", ")}`);
|
|
313
|
+
}
|
|
314
|
+
if (agent.task) {
|
|
315
|
+
lines.push(
|
|
316
|
+
verbose
|
|
317
|
+
? `task:\n${indent(agent.task)}`
|
|
318
|
+
: `task: ${formatSubagentPreview(agent.task, TASK_PREVIEW_WIDTH)}`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
lines.push(launchLine(agent), ...toolLines(agent, verbose));
|
|
322
|
+
|
|
323
|
+
const latest = agent.latest_result;
|
|
324
|
+
if (latest) {
|
|
325
|
+
// The summary line already names an idle child's latest turn.
|
|
326
|
+
const heading = joinParts([
|
|
327
|
+
agent.active_turn_id
|
|
328
|
+
? `latest result: turn ${latest.turn_id} ${latest.status}`
|
|
329
|
+
: "latest result",
|
|
330
|
+
formatSubagentDuration(latest.elapsed_ms),
|
|
331
|
+
]);
|
|
332
|
+
if (latest.error) lines.push(`${heading}\n error: ${latest.error}`);
|
|
333
|
+
else lines.push(heading);
|
|
334
|
+
if (latest.output) {
|
|
335
|
+
lines.push(
|
|
336
|
+
verbose
|
|
337
|
+
? indent(latest.output)
|
|
338
|
+
: ` ${formatSubagentPreview(latest.output, RESULT_PREVIEW_WIDTH)}`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (verbose) {
|
|
344
|
+
const usage = formatSubagentUsage(agent.usage);
|
|
345
|
+
if (usage) lines.push(`usage: ${usage}`);
|
|
346
|
+
if (agent.session_file) lines.push(`session file: ${agent.session_file}`);
|
|
347
|
+
lines.push(`spawn entry: ${agent.spawn_entry_id}`);
|
|
348
|
+
if (agent.recent_messages.length > 0) {
|
|
349
|
+
lines.push(`recent messages (${agent.recent_messages.length}):`);
|
|
350
|
+
for (const message of agent.recent_messages) {
|
|
351
|
+
lines.push(
|
|
352
|
+
`- ${message.source_agent_id} (${message.turn_id}):\n${indent(message.content)}`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (agent.recent_activity.length > 0) {
|
|
357
|
+
lines.push(`recent activity (${agent.recent_activity.length}):`);
|
|
358
|
+
for (const activity of agent.recent_activity) {
|
|
359
|
+
const label = `${activity.label}${activity.truncated ? " (truncated)" : ""}`;
|
|
360
|
+
lines.push(`- ${label}:\n${indent(activity.content)}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return lines.join("\n");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const activity = agent.recent_activity.slice(-STATUS_ACTIVITY_PREVIEW_COUNT);
|
|
367
|
+
if (activity.length > 0) {
|
|
368
|
+
lines.push(
|
|
369
|
+
`recent activity (last ${activity.length} of ${agent.recent_activity.length}):`,
|
|
370
|
+
...activity.map(
|
|
371
|
+
(item) => `- ${item.label}: ${formatSubagentPreview(item.content, ACTIVITY_PREVIEW_WIDTH)}`,
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
lines.push(
|
|
376
|
+
"Pass verbose: true for the full task, latest output, capability ceiling, recent messages, and recent activity.",
|
|
377
|
+
);
|
|
378
|
+
return lines.join("\n");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** A direct-children list or one child's detail. */
|
|
382
|
+
export function formatStatusResultText(result: StatusResult, verbose = false): string {
|
|
383
|
+
return "agent" in result
|
|
384
|
+
? formatStatusDetailText(result.agent, verbose)
|
|
385
|
+
: formatStatusListText(result.parent_id, result.agents);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** One line naming the cancelled turns, or that no turn was running. */
|
|
389
|
+
export function formatCancelResultText(result: CancelResult): string {
|
|
390
|
+
if (result.cancelled_turn_ids.length === 0) {
|
|
391
|
+
return `Nothing cancelled: ${result.agent_id} had no active turn${result.recursive ? ", nor did any descendant" : ""}.`;
|
|
392
|
+
}
|
|
393
|
+
return `Cancelled ${plural(result.cancelled_turn_ids.length, "active turn")}: ${result.cancelled_turn_ids.join(", ")}; sessions are kept.`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** One line naming the deleted agents, plus one line per failure. */
|
|
397
|
+
export function formatDeleteResultText(result: DeleteResult): string {
|
|
398
|
+
const lines =
|
|
399
|
+
result.deleted_agent_ids.length === 0
|
|
400
|
+
? [`Deleted no agents under ${result.agent_id}.`]
|
|
401
|
+
: [
|
|
402
|
+
`Deleted ${result.deleted_agent_ids.join(", ")}; ${plural(result.trashed_session_files.length, "session file")} moved to trash.`,
|
|
403
|
+
];
|
|
404
|
+
for (const failure of result.failures) {
|
|
405
|
+
lines.push(`failed: ${failure.agent_id}: ${failure.error}`);
|
|
406
|
+
}
|
|
407
|
+
return lines.join("\n");
|
|
408
|
+
}
|
|
@@ -87,6 +87,12 @@ export function createCoordinatorToolSchemas(modelIds: readonly string[]) {
|
|
|
87
87
|
agent_id: Type.Optional(
|
|
88
88
|
canonicalAgentIdSchema("Direct child canonical agent ID; the root may name any descendant"),
|
|
89
89
|
),
|
|
90
|
+
verbose: Type.Optional(
|
|
91
|
+
Type.Boolean({
|
|
92
|
+
description:
|
|
93
|
+
"With agent_id: include the full task, latest output, capability ceiling, usage breakdown, recent messages, and all recent activity",
|
|
94
|
+
}),
|
|
95
|
+
),
|
|
90
96
|
}),
|
|
91
97
|
subagent_cancel: Type.Object({
|
|
92
98
|
agent_id: canonicalAgentIdSchema("Direct child canonical agent ID"),
|
|
@@ -10,11 +10,20 @@ import {
|
|
|
10
10
|
type ToolRenderResultOptions,
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Container, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
13
|
+
import { acceptNullForOptionalArguments } from "@ian-pascoe/pi-utils/null-optional-arguments";
|
|
13
14
|
import { appendDurationFooter, callDurationFooter } from "@ian-pascoe/pi-utils/ui";
|
|
14
15
|
import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
|
|
15
16
|
import { withTroubleshootingHint } from "./troubleshooting-skill.js";
|
|
16
17
|
import type { MinimalSubagentsModelRole } from "./minimal-subagents-config.js";
|
|
17
18
|
import { roundMinimalSubagentsUsageCosts } from "./minimal-subagents-usage.js";
|
|
19
|
+
import {
|
|
20
|
+
formatAgentMessageResultText,
|
|
21
|
+
formatCancelResultText,
|
|
22
|
+
formatDeleteResultText,
|
|
23
|
+
formatSpawnResultText,
|
|
24
|
+
formatStatusResultText,
|
|
25
|
+
formatWaitResultText,
|
|
26
|
+
} from "./minimal-subagents-result-text.js";
|
|
18
27
|
import {
|
|
19
28
|
renderCoordinatorToolCall,
|
|
20
29
|
renderCoordinatorToolResult,
|
|
@@ -40,7 +49,6 @@ import type {
|
|
|
40
49
|
DeleteResult,
|
|
41
50
|
SpawnResult,
|
|
42
51
|
StatusResult,
|
|
43
|
-
WaitDeliveredTurnResult,
|
|
44
52
|
WaitResult,
|
|
45
53
|
} from "./minimal-subagents-types.js";
|
|
46
54
|
|
|
@@ -202,49 +210,40 @@ function createCoordinatorToolRendering(
|
|
|
202
210
|
* Serialize a tool result with every nested `usage` cost rounded for presentation. Exact values
|
|
203
211
|
* stay in `details`, the Registry, and session data, which never pass through here.
|
|
204
212
|
*/
|
|
205
|
-
function stringifyWithRoundedUsageCosts(
|
|
206
|
-
result: CoordinatorToolResultDetails,
|
|
207
|
-
space?: number,
|
|
208
|
-
): string {
|
|
213
|
+
function stringifyWithRoundedUsageCosts(result: CoordinatorToolResultDetails): string {
|
|
209
214
|
// `JSON.stringify` types replacer values as `any`; a usage is recognized by key and `cost` object.
|
|
210
|
-
return JSON.stringify(
|
|
211
|
-
|
|
212
|
-
(key, value) =>
|
|
213
|
-
key === "usage" && value?.cost ? roundMinimalSubagentsUsageCosts(value) : value,
|
|
214
|
-
space,
|
|
215
|
+
return JSON.stringify(result, (key, value) =>
|
|
216
|
+
key === "usage" && value?.cost ? roundMinimalSubagentsUsageCosts(value) : value,
|
|
215
217
|
);
|
|
216
218
|
}
|
|
217
219
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// Presentation only: `details` keeps the exact values the session persists.
|
|
222
|
-
const json = stringifyWithRoundedUsageCosts(result, 2);
|
|
223
|
-
const truncated = truncateHead(json, {
|
|
220
|
+
/** Model-facing text, truncated like Pi's built-in tool output. */
|
|
221
|
+
function textContent(text: string): AgentToolResult<unknown>["content"] {
|
|
222
|
+
const truncated = truncateHead(text, {
|
|
224
223
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
225
224
|
maxLines: DEFAULT_MAX_LINES,
|
|
226
225
|
});
|
|
226
|
+
return [{ type: "text" as const, text: truncated.content }];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The model reads compact `text`; codemode scripts and the transcript renderer keep the complete
|
|
231
|
+
* record in `structuredContent` (costs rounded) and `details` (exact).
|
|
232
|
+
*/
|
|
233
|
+
function structuredToolResult<TDetails extends CoordinatorToolResultDetails>(
|
|
234
|
+
result: TDetails,
|
|
235
|
+
text: string,
|
|
236
|
+
): AgentToolResult<TDetails> {
|
|
227
237
|
return {
|
|
228
|
-
content:
|
|
238
|
+
content: textContent(text),
|
|
229
239
|
details: result,
|
|
230
|
-
structuredContent: JSON.parse(
|
|
240
|
+
structuredContent: JSON.parse(stringifyWithRoundedUsageCosts(result)),
|
|
231
241
|
};
|
|
232
242
|
}
|
|
233
243
|
|
|
234
|
-
/**
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
*/
|
|
238
|
-
function alreadyDeliveredContent(
|
|
239
|
-
result: WaitDeliveredTurnResult,
|
|
240
|
-
): AgentToolResult<WaitResult>["content"] {
|
|
241
|
-
const notice = `Result of ${result.agent_id} turn ${result.turn_id} (${result.status}) was already delivered automatically; call subagent_wait with turn_id "${result.turn_id}" to reread it.`;
|
|
242
|
-
if (!result.messages) return [{ type: "text", text: notice }];
|
|
243
|
-
const messages = truncateHead(JSON.stringify({ messages: result.messages }, null, 2), {
|
|
244
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
245
|
-
maxLines: DEFAULT_MAX_LINES,
|
|
246
|
-
});
|
|
247
|
-
return [{ type: "text", text: `${notice}\n${messages.content}` }];
|
|
244
|
+
/** One wait result as model-facing text. */
|
|
245
|
+
function waitResultText(result: WaitResult): AgentToolResult<WaitResult>["content"] {
|
|
246
|
+
return textContent(formatWaitResultText(result));
|
|
248
247
|
}
|
|
249
248
|
|
|
250
249
|
/**
|
|
@@ -254,26 +253,19 @@ function alreadyDeliveredContent(
|
|
|
254
253
|
function failedStructuredToolResult<TDetails extends CoordinatorToolResultDetails>(
|
|
255
254
|
prefix: string,
|
|
256
255
|
result: TDetails,
|
|
256
|
+
resultText: string,
|
|
257
257
|
options: { troubleshootingHint: boolean },
|
|
258
258
|
): AgentToolResult<TDetails> & { isError: true } {
|
|
259
|
-
const
|
|
260
|
-
const text = `${prefix}:\n${textOf(success)}`;
|
|
259
|
+
const text = `${prefix}:\n${resultText}`;
|
|
261
260
|
return {
|
|
262
|
-
...
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
text: options.troubleshootingHint ? withTroubleshootingHint(text) : text,
|
|
267
|
-
},
|
|
268
|
-
],
|
|
261
|
+
...structuredToolResult(
|
|
262
|
+
result,
|
|
263
|
+
options.troubleshootingHint ? withTroubleshootingHint(text) : text,
|
|
264
|
+
),
|
|
269
265
|
isError: true,
|
|
270
266
|
};
|
|
271
267
|
}
|
|
272
268
|
|
|
273
|
-
function textOf(result: AgentToolResult<unknown>): string {
|
|
274
|
-
return result.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
|
|
275
|
-
}
|
|
276
|
-
|
|
277
269
|
function callerSourceTurnId(
|
|
278
270
|
coordinator: CoordinatorToolOperations,
|
|
279
271
|
callerId: string,
|
|
@@ -319,7 +311,10 @@ export function createCoordinatorToolDefinitions(
|
|
|
319
311
|
const status = options.coordinator.inspectStatus(result.agent_id);
|
|
320
312
|
const details: SpawnResultDetails = { ...result };
|
|
321
313
|
if ("agent" in status) details.agent = status.agent;
|
|
322
|
-
return {
|
|
314
|
+
return {
|
|
315
|
+
...structuredToolResult(result, formatSpawnResultText(result, parameters.tools)),
|
|
316
|
+
details,
|
|
317
|
+
};
|
|
323
318
|
});
|
|
324
319
|
},
|
|
325
320
|
...createCoordinatorToolRendering(options, "subagent"),
|
|
@@ -348,12 +343,13 @@ export function createCoordinatorToolDefinitions(
|
|
|
348
343
|
},
|
|
349
344
|
callerSourceTurnId(options.coordinator, options.callerId, toolCallId),
|
|
350
345
|
);
|
|
346
|
+
const text = formatAgentMessageResultText(result);
|
|
351
347
|
return result.disposition === "failed"
|
|
352
|
-
? failedStructuredToolResult("Minimal subagents message delivery failed", result, {
|
|
348
|
+
? failedStructuredToolResult("Minimal subagents message delivery failed", result, text, {
|
|
353
349
|
// The coordinator already appends the hint to `error`.
|
|
354
350
|
troubleshootingHint: false,
|
|
355
351
|
})
|
|
356
|
-
: structuredToolResult(result);
|
|
352
|
+
: structuredToolResult(result, text);
|
|
357
353
|
});
|
|
358
354
|
},
|
|
359
355
|
...createCoordinatorToolRendering(options, "agent_message"),
|
|
@@ -363,7 +359,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
363
359
|
name: "subagent_wait",
|
|
364
360
|
label: "Subagent Wait",
|
|
365
361
|
description:
|
|
366
|
-
"Wait for one direct child's oldest observable turn whose result you have neither claimed nor received, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. A settled turn returns event=turn, with any queued messages in messages; waiting again returns a result you claimed in full. Without turn_id, a result already delivered to you automatically returns event=turn with already_delivered=true and no output; pass its turn_id to reread it. Timeout returns a compact event=timeout (state, elapsed_ms, latest_activity_at, total_tokens, last few activity labels; use subagent_status for detail) and never cancels the child.",
|
|
362
|
+
"Wait for one direct child's oldest observable turn whose result you have neither claimed nor received, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. A settled turn returns event=turn, with any queued messages in messages; waiting again returns a result you claimed in full. Without turn_id, a result already delivered to you automatically returns event=turn with already_delivered=true and no output; pass its turn_id to reread it. If it was handed to you but is not yet in your conversation, delivery_pending=true and it arrives as a separate message; do not reread. Timeout returns a compact event=timeout (state, elapsed_ms, latest_activity_at, total_tokens, last few activity labels; use subagent_status for detail) and never cancels the child.",
|
|
367
363
|
promptSnippet: "Wait for one direct child's exact turn",
|
|
368
364
|
parameters: options.schemas.subagent_wait,
|
|
369
365
|
annotations: {
|
|
@@ -408,10 +404,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
408
404
|
source_turn_id: result.turn_id,
|
|
409
405
|
};
|
|
410
406
|
return {
|
|
411
|
-
content:
|
|
412
|
-
"already_delivered" in result
|
|
413
|
-
? alreadyDeliveredContent(result)
|
|
414
|
-
: structuredToolResult(result).content,
|
|
407
|
+
content: waitResultText(result),
|
|
415
408
|
details,
|
|
416
409
|
structuredContent: JSON.parse(stringifyWithRoundedUsageCosts(details)),
|
|
417
410
|
};
|
|
@@ -427,7 +420,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
427
420
|
name: "subagent_status",
|
|
428
421
|
label: "Subagent Status",
|
|
429
422
|
description:
|
|
430
|
-
"List direct children when agent_id is omitted, or inspect one direct child's launch contract, result, usage, dependencies, and bounded recent activity including message text and reasoning. The root may inspect any descendant.",
|
|
423
|
+
"List direct children when agent_id is omitted, or inspect one direct child's launch contract, result, usage, dependencies, and bounded recent activity including message text and reasoning. Without verbose, the task, latest output, and recent activity are short previews. The root may inspect any descendant.",
|
|
431
424
|
promptSnippet: "Inspect direct child state",
|
|
432
425
|
parameters: options.schemas.subagent_status,
|
|
433
426
|
annotations: {
|
|
@@ -437,9 +430,13 @@ export function createCoordinatorToolDefinitions(
|
|
|
437
430
|
openWorldHint: false,
|
|
438
431
|
},
|
|
439
432
|
async execute(_toolCallId, parameters) {
|
|
440
|
-
return runCoordinatorToolActivity(options, () =>
|
|
441
|
-
|
|
442
|
-
|
|
433
|
+
return runCoordinatorToolActivity(options, () => {
|
|
434
|
+
const result = options.coordinator.status(options.callerId, parameters.agent_id);
|
|
435
|
+
return structuredToolResult(
|
|
436
|
+
result,
|
|
437
|
+
formatStatusResultText(result, parameters.verbose ?? false),
|
|
438
|
+
);
|
|
439
|
+
});
|
|
443
440
|
},
|
|
444
441
|
...createCoordinatorToolRendering(options, "subagent_status"),
|
|
445
442
|
});
|
|
@@ -458,15 +455,14 @@ export function createCoordinatorToolDefinitions(
|
|
|
458
455
|
openWorldHint: false,
|
|
459
456
|
},
|
|
460
457
|
async execute(_toolCallId, parameters) {
|
|
461
|
-
return runCoordinatorToolActivity(options, async () =>
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
);
|
|
458
|
+
return runCoordinatorToolActivity(options, async () => {
|
|
459
|
+
const result = await options.coordinator.cancel(
|
|
460
|
+
options.callerId,
|
|
461
|
+
parameters.agent_id,
|
|
462
|
+
parameters.recursive ?? true,
|
|
463
|
+
);
|
|
464
|
+
return structuredToolResult(result, formatCancelResultText(result));
|
|
465
|
+
});
|
|
470
466
|
},
|
|
471
467
|
...createCoordinatorToolRendering(options, "subagent_cancel"),
|
|
472
468
|
});
|
|
@@ -491,13 +487,17 @@ export function createCoordinatorToolDefinitions(
|
|
|
491
487
|
parameters.agent_id,
|
|
492
488
|
parameters.recursive ?? true,
|
|
493
489
|
);
|
|
490
|
+
const text = formatDeleteResultText(result);
|
|
494
491
|
if (result.failures.length > 0) {
|
|
495
492
|
options.onAttention?.(`Deletion partially failed for ${parameters.agent_id}`);
|
|
496
|
-
return failedStructuredToolResult(
|
|
497
|
-
|
|
498
|
-
|
|
493
|
+
return failedStructuredToolResult(
|
|
494
|
+
"Minimal subagents deletion partially failed",
|
|
495
|
+
result,
|
|
496
|
+
text,
|
|
497
|
+
{ troubleshootingHint: true },
|
|
498
|
+
);
|
|
499
499
|
}
|
|
500
|
-
return structuredToolResult(result);
|
|
500
|
+
return structuredToolResult(result, text);
|
|
501
501
|
});
|
|
502
502
|
},
|
|
503
503
|
...createCoordinatorToolRendering(options, "subagent_delete"),
|
|
@@ -512,7 +512,9 @@ export function createCoordinatorToolDefinitions(
|
|
|
512
512
|
Object.assign(deleteTool, { outputSchema: CoordinatorToolOutputSchemas.subagent_delete }),
|
|
513
513
|
];
|
|
514
514
|
const allowFanoutTools = options.allowFanoutTools ?? options.callerId === "root";
|
|
515
|
-
return
|
|
516
|
-
|
|
517
|
-
|
|
515
|
+
return (
|
|
516
|
+
allowFanoutTools
|
|
517
|
+
? coordinatorTools
|
|
518
|
+
: coordinatorTools.filter((tool) => NON_SPAWNING_CHILD_COORDINATOR_TOOL_NAMES.has(tool.name))
|
|
519
|
+
).map((tool) => acceptNullForOptionalArguments(tool));
|
|
518
520
|
}
|
|
@@ -98,6 +98,9 @@ export interface WaitTurnResult extends TurnResult {
|
|
|
98
98
|
messages?: WaitMessageResult[];
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/** How a result reached the caller automatically: handed to Pi but not yet in its branch, or recorded there. */
|
|
102
|
+
export type AutomaticDelivery = "handed" | "recorded";
|
|
103
|
+
|
|
101
104
|
/**
|
|
102
105
|
* Reports that a default wait selected a terminal turn whose result was already delivered
|
|
103
106
|
* automatically, handed or settled, so the output is not repeated; an explicit `turn_id` rereads it.
|
|
@@ -108,6 +111,11 @@ export interface WaitDeliveredTurnResult extends Pick<
|
|
|
108
111
|
> {
|
|
109
112
|
event: "turn";
|
|
110
113
|
already_delivered: true;
|
|
114
|
+
/**
|
|
115
|
+
* Set when the result is handed to Pi but not yet recorded in the caller's branch, so it will
|
|
116
|
+
* arrive as a separate message and needs no reread.
|
|
117
|
+
*/
|
|
118
|
+
delivery_pending?: true;
|
|
111
119
|
messages?: WaitMessageResult[];
|
|
112
120
|
}
|
|
113
121
|
|