@ian-pascoe/pi-minimal-subagents 0.14.0 → 0.15.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 +25 -13
- package/package.json +1 -1
- package/src/minimal-subagents-coordinator.ts +102 -5
- package/src/minimal-subagents-render-contract.ts +20 -2
- package/src/minimal-subagents-rendering.ts +34 -9
- package/src/minimal-subagents-session-wire.ts +1 -0
- package/src/minimal-subagents-sessions.ts +7 -2
- package/src/minimal-subagents-tools.ts +22 -2
- package/src/minimal-subagents-types.ts +26 -4
package/README.md
CHANGED
|
@@ -40,14 +40,18 @@ From this package checkout, load the source directly with
|
|
|
40
40
|
`agent_message` in the same tool batch can target the new child.
|
|
41
41
|
2. `subagent_wait` claims the turn's result, or leave it unclaimed and the final
|
|
42
42
|
response arrives automatically as a steer message. Without `turn_id`, a wait
|
|
43
|
-
skips turns whose result you already claimed or received automatically
|
|
43
|
+
skips turns whose result you already claimed or received automatically, and
|
|
44
|
+
never repeats an automatically delivered result: it reports
|
|
45
|
+
`already_delivered: true` instead of the output.
|
|
44
46
|
3. `agent_message` continues an idle child: the result reports
|
|
45
47
|
`disposition: "started-turn"` with the new `turn_id`. A plain
|
|
46
48
|
`subagent_wait({ agent_id })` targets that new turn, not the first result;
|
|
47
49
|
pass `turn_id` to re-read a turn whose result is still retained, claimed or
|
|
48
50
|
not.
|
|
49
51
|
4. `subagent_status` inspects children; `subagent_cancel` stops work but keeps
|
|
50
|
-
the session; `subagent_delete` removes it. Deleted IDs cannot be reused
|
|
52
|
+
the session; `subagent_delete` removes it. Deleted IDs cannot be reused, and
|
|
53
|
+
spawning with an existing `agent_id` fails with that child's state and the
|
|
54
|
+
next step (`agent_message`, `subagent_delete`, or another `agent_id`).
|
|
51
55
|
|
|
52
56
|
## Configuration
|
|
53
57
|
|
|
@@ -277,10 +281,11 @@ children: `subagent`, `agent_message`, `subagent_wait`, `subagent_status`,
|
|
|
277
281
|
only the three adjacent-coordination tools: `agent_message`, `subagent_wait`,
|
|
278
282
|
and `subagent_status`.
|
|
279
283
|
|
|
280
|
-
|
|
284
|
+
A running child's `latest_activity_at` advances with the timestamps of its latest
|
|
285
|
+
messages and tool results. Targeted `subagent_status` includes `recent_activity`, a bounded tail of message
|
|
281
286
|
text, reasoning, tool calls, and tool results. It includes the current streaming
|
|
282
|
-
assistant message but omits image data. Timeout Wait Events
|
|
283
|
-
|
|
287
|
+
assistant message but omits image data. Timeout Wait Events carry only a compact
|
|
288
|
+
progress snapshot. Children target only direct children; the Root Agent
|
|
284
289
|
may inspect any descendant. Model-facing status reports a `child_count` but no
|
|
285
290
|
nested `children` summaries. Reported `usage` always includes `cacheWrite1h`
|
|
286
291
|
and `reasoning`, as `0` when the provider reports none. `subagent_cancel`
|
|
@@ -336,17 +341,24 @@ intermediate Wait Event containing a Coordination Message before the child turn
|
|
|
336
341
|
settles. That event claims only its message, so later unconsumed messages and the
|
|
337
342
|
terminal result retain automatic fallback. If the turn has already settled, a
|
|
338
343
|
wait returns its terminal result with queued messages in `messages`; waiting
|
|
339
|
-
again for
|
|
340
|
-
optional `turn_id` to address a retained turn exactly, including
|
|
341
|
-
result you already claimed (a claimed result stays retained until
|
|
342
|
-
Evidence settles it). Without it, waits skip claimed turns and turns
|
|
343
|
-
result was already delivered to you automatically, and select the oldest
|
|
344
|
-
remaining observable turn, falling back to the active, then latest, turn.
|
|
344
|
+
again for a settled turn you claimed returns the same result while it is
|
|
345
|
+
retained. Pass optional `turn_id` to address a retained turn exactly, including
|
|
346
|
+
one whose result you already claimed (a claimed result stays retained until
|
|
347
|
+
Delivery Evidence settles it). Without it, waits skip claimed turns and turns
|
|
348
|
+
whose result was already delivered to you automatically, and select the oldest
|
|
349
|
+
remaining observable turn, falling back to the active, then latest, turn. When
|
|
350
|
+
that fallback reaches a result already delivered to you automatically, whether
|
|
351
|
+
still queued or settled, the wait returns `event: "turn"` with
|
|
352
|
+
`already_delivered: true`, the turn identity, and status, but no output; it does
|
|
353
|
+
not claim the turn, and `turn_id` rereads the full result. A
|
|
345
354
|
caller may have only one outstanding wait for the same source turn; a
|
|
346
355
|
concurrent duplicate is rejected instead of competing for one Wait Event.
|
|
347
356
|
When `timeout_ms` expires, the wait returns an observational `event: "timeout"`
|
|
348
|
-
with the requested turn identity and
|
|
349
|
-
|
|
357
|
+
with the requested turn identity and a compact, bounded progress snapshot
|
|
358
|
+
(`state`, `elapsed_ms`, `latest_activity_at`, `total_tokens`, and the labels of
|
|
359
|
+
the last three Recent Activity items as `recent_activity_labels`), under about 1 KB. It omits the task and
|
|
360
|
+
Launch Contract; targeted `subagent_status` returns the full detail.
|
|
361
|
+
It removes only the waiter, leaving the child
|
|
350
362
|
running and all pending delivery unclaimed. Abort signals remain errors.
|
|
351
363
|
|
|
352
364
|
The persisted Delivery Ledger records Coordination Messages, terminal results,
|
package/package.json
CHANGED
|
@@ -69,11 +69,17 @@ import type {
|
|
|
69
69
|
StatusResult,
|
|
70
70
|
TurnResult,
|
|
71
71
|
WaitMessageResult,
|
|
72
|
+
WaitTimeoutResult,
|
|
73
|
+
WaitDeliveredTurnResult,
|
|
72
74
|
WaitResult,
|
|
73
75
|
} from "./minimal-subagents-types.js";
|
|
74
76
|
|
|
75
77
|
const FRIENDLY_AGENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
76
78
|
const RESERVED_AGENT_IDS = new Set(["root", "parent"]);
|
|
79
|
+
/** Recent Activity labels a timed-out wait reports. */
|
|
80
|
+
const WAIT_TIMEOUT_ACTIVITY_LIMIT = 3;
|
|
81
|
+
/** Characters kept of each label so a timed-out wait stays bounded whatever a tool is named. */
|
|
82
|
+
const WAIT_TIMEOUT_LABEL_MAX_LENGTH = 80;
|
|
77
83
|
const RECENT_MESSAGE_LIMIT = 20;
|
|
78
84
|
const DEFAULT_AUTOMATIC_DELIVERY_GRACE_MS = 1_000;
|
|
79
85
|
|
|
@@ -145,6 +151,27 @@ function terminalWaitResult(result: TurnResult, messages: WaitMessageResult[] =
|
|
|
145
151
|
return messages.length === 0 ? terminal : { ...terminal, messages: structuredClone(messages) };
|
|
146
152
|
}
|
|
147
153
|
|
|
154
|
+
/** A terminal Wait Event naming an already delivered result without repeating its output. */
|
|
155
|
+
function alreadyDeliveredWaitResult(
|
|
156
|
+
result: TurnResult,
|
|
157
|
+
messages: WaitMessageResult[],
|
|
158
|
+
): WaitDeliveredTurnResult {
|
|
159
|
+
const delivered: WaitDeliveredTurnResult = {
|
|
160
|
+
event: "turn",
|
|
161
|
+
agent_id: result.agent_id,
|
|
162
|
+
turn_id: result.turn_id,
|
|
163
|
+
status: result.status,
|
|
164
|
+
already_delivered: true,
|
|
165
|
+
};
|
|
166
|
+
if (messages.length > 0) delivered.messages = structuredClone(messages);
|
|
167
|
+
return delivered;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The message ID of an automatic result message, which is also its Delivery Evidence key. */
|
|
171
|
+
function automaticResultMessageId(sourceAgentId: string, sourceTurnId: string): string {
|
|
172
|
+
return `result:${sourceAgentId}:${sourceTurnId}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
148
175
|
function combineCoordinatorMessages(messages: readonly CoordinatorMessage[]): CoordinatorMessage {
|
|
149
176
|
const latest = messages.at(-1);
|
|
150
177
|
if (!latest) throw new Error("Minimal subagents message batch must not be empty");
|
|
@@ -238,7 +265,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
238
265
|
);
|
|
239
266
|
}
|
|
240
267
|
if (this.agents.has(agentId) || this.pendingAgentIds.has(agentId)) {
|
|
241
|
-
throw new Error(
|
|
268
|
+
throw new Error(this.duplicateAgentIdMessage(agentId));
|
|
242
269
|
}
|
|
243
270
|
|
|
244
271
|
const sessionContext = parameters.session_context ?? "omit";
|
|
@@ -459,7 +486,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
459
486
|
}
|
|
460
487
|
}
|
|
461
488
|
|
|
462
|
-
/** Wait for one exact turn, returning
|
|
489
|
+
/** Wait for one exact turn, returning compact child progress if the waiter times out. */
|
|
463
490
|
wait(
|
|
464
491
|
callerId: string,
|
|
465
492
|
agentId: string,
|
|
@@ -486,6 +513,11 @@ export class MinimalSubagentsCoordinator {
|
|
|
486
513
|
(agent.latest_result?.turn_id === turnId ? agent.latest_result : undefined);
|
|
487
514
|
if (retainedResult) {
|
|
488
515
|
const messages = this.drainPendingParentMessages(callerId, agentId, turnId);
|
|
516
|
+
// A default wait falls back to the latest turn, whose result may already have been delivered
|
|
517
|
+
// automatically; repeating its output would deliver it twice. An explicit turn_id rereads it.
|
|
518
|
+
if (requestedTurnId === undefined && this.wasAlreadyDelivered(callerId, retainedResult)) {
|
|
519
|
+
return Promise.resolve(alreadyDeliveredWaitResult(retainedResult, messages));
|
|
520
|
+
}
|
|
489
521
|
this.claimTerminalDelivery(callerId, retainedResult);
|
|
490
522
|
return Promise.resolve(terminalWaitResult(retainedResult, messages));
|
|
491
523
|
}
|
|
@@ -519,7 +551,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
519
551
|
agent_id: agentId,
|
|
520
552
|
turn_id: turnId,
|
|
521
553
|
timeout_ms: timeoutMs,
|
|
522
|
-
|
|
554
|
+
...this.buildWaitTimeoutProgress(agent),
|
|
523
555
|
});
|
|
524
556
|
}, timeoutMs);
|
|
525
557
|
}
|
|
@@ -1335,7 +1367,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1335
1367
|
source_agent_id: delivery.source_agent_id,
|
|
1336
1368
|
destination_agent_id: delivery.destination_agent_id,
|
|
1337
1369
|
source_turn_id: result.turn_id,
|
|
1338
|
-
message_id:
|
|
1370
|
+
message_id: automaticResultMessageId(delivery.source_agent_id, result.turn_id),
|
|
1339
1371
|
status: result.status,
|
|
1340
1372
|
elapsed_ms: result.elapsed_ms,
|
|
1341
1373
|
usage: result.usage,
|
|
@@ -1737,6 +1769,22 @@ export class MinimalSubagentsCoordinator {
|
|
|
1737
1769
|
);
|
|
1738
1770
|
}
|
|
1739
1771
|
|
|
1772
|
+
/**
|
|
1773
|
+
* Whether this result was already delivered to the caller automatically: it is handed, or the
|
|
1774
|
+
* caller's branch holds its automatic result message. A wait's own tool result does not count.
|
|
1775
|
+
*/
|
|
1776
|
+
private wasAlreadyDelivered(callerId: string, result: TurnResult): boolean {
|
|
1777
|
+
return (
|
|
1778
|
+
this.handedTerminalKeys.has(deliveryTurnKey(result.agent_id, result.turn_id)) ||
|
|
1779
|
+
this.hasRecipientDeliveryEvidence(
|
|
1780
|
+
callerId,
|
|
1781
|
+
result.agent_id,
|
|
1782
|
+
result.turn_id,
|
|
1783
|
+
automaticResultMessageId(result.agent_id, result.turn_id),
|
|
1784
|
+
)
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1740
1788
|
private hasDeliveryEvidence(delivery: PersistedDelivery): boolean {
|
|
1741
1789
|
return this.hasRecipientDeliveryEvidence(
|
|
1742
1790
|
delivery.destination_agent_id,
|
|
@@ -1795,6 +1843,20 @@ export class MinimalSubagentsCoordinator {
|
|
|
1795
1843
|
this.removeSettledEmptyTurnClaim(delivery.source_agent_id, delivery.source_turn_id);
|
|
1796
1844
|
}
|
|
1797
1845
|
|
|
1846
|
+
/** Name an existing agent ID's state and the next steps that are valid for that state. */
|
|
1847
|
+
private duplicateAgentIdMessage(agentId: string): string {
|
|
1848
|
+
const prefix = `Minimal subagents duplicate agent ID: ${agentId}`;
|
|
1849
|
+
const existing = this.agents.get(agentId);
|
|
1850
|
+
if (!existing) {
|
|
1851
|
+
return `${prefix} is still being created; retry shortly or choose another agent_id`;
|
|
1852
|
+
}
|
|
1853
|
+
if (existing.availability === "unavailable") {
|
|
1854
|
+
return `${prefix} already exists (unavailable). Use subagent_delete to remove it (a deleted ID cannot be reused), or choose another agent_id.`;
|
|
1855
|
+
}
|
|
1856
|
+
const state = existing.active_turn_id ? "running" : "idle";
|
|
1857
|
+
return `${prefix} already exists (${state}). Use agent_message to continue it, subagent_delete to remove it (a deleted ID cannot be reused), or choose another agent_id.`;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1798
1860
|
private buildAgentSummary(agent: PersistedAgent, includeDescendants = true): AgentSummary {
|
|
1799
1861
|
const directChildren = this.childrenOf(agent.agent_id);
|
|
1800
1862
|
const elapsed = agent.active_turn_started_at
|
|
@@ -1817,7 +1879,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1817
1879
|
...runtimeProfile,
|
|
1818
1880
|
tools: runtime?.getActiveToolNames?.() ?? [...agent.launch_contract.ordinary_tools],
|
|
1819
1881
|
elapsed_ms: elapsed ?? agent.latest_result?.elapsed_ms,
|
|
1820
|
-
latest_activity_at:
|
|
1882
|
+
latest_activity_at: this.latestActivityAt(agent, runtime),
|
|
1821
1883
|
task: agent.task,
|
|
1822
1884
|
child_count: directChildren.length,
|
|
1823
1885
|
};
|
|
@@ -1827,6 +1889,41 @@ export class MinimalSubagentsCoordinator {
|
|
|
1827
1889
|
return summary;
|
|
1828
1890
|
}
|
|
1829
1891
|
|
|
1892
|
+
/**
|
|
1893
|
+
* The persisted activity time, advanced by the timestamps of the messages and tool results a
|
|
1894
|
+
* running child has produced since. Only running children can have newer unpersisted activity.
|
|
1895
|
+
*/
|
|
1896
|
+
private latestActivityAt(agent: PersistedAgent, runtime: ChildAgentRuntime | undefined): string {
|
|
1897
|
+
const persisted = agent.latest_activity_at ?? agent.created_at;
|
|
1898
|
+
if (!agent.active_turn_id || !runtime) return persisted;
|
|
1899
|
+
const persistedMs = Date.parse(persisted);
|
|
1900
|
+
let latestMs = persistedMs;
|
|
1901
|
+
for (const { timestamp } of runtime.snapshotActivityMessages()) {
|
|
1902
|
+
if (Number.isFinite(timestamp) && timestamp > latestMs) latestMs = timestamp;
|
|
1903
|
+
}
|
|
1904
|
+
return latestMs > persistedMs ? new Date(latestMs).toISOString() : persisted;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/** The compact progress a timed-out wait reports; `subagent_status` has the full detail. */
|
|
1908
|
+
private buildWaitTimeoutProgress(
|
|
1909
|
+
agent: PersistedAgent,
|
|
1910
|
+
): Omit<WaitTimeoutResult, "event" | "agent_id" | "turn_id" | "timeout_ms"> {
|
|
1911
|
+
const summary = this.buildAgentSummary(agent, false);
|
|
1912
|
+
const runtime = this.runtimes.get(agent.agent_id);
|
|
1913
|
+
const messages =
|
|
1914
|
+
runtime?.snapshotActivityMessages() ?? this.inspectTranscript(agent.agent_id).messages;
|
|
1915
|
+
const usage = runtime?.getUsage() ?? agent.latest_result?.usage;
|
|
1916
|
+
return {
|
|
1917
|
+
state: summary.state,
|
|
1918
|
+
elapsed_ms: summary.elapsed_ms,
|
|
1919
|
+
latest_activity_at: summary.latest_activity_at,
|
|
1920
|
+
total_tokens: usage?.totalTokens,
|
|
1921
|
+
recent_activity_labels: buildRecentAgentActivity(messages)
|
|
1922
|
+
.slice(-WAIT_TIMEOUT_ACTIVITY_LIMIT)
|
|
1923
|
+
.map((activity) => activity.label.slice(0, WAIT_TIMEOUT_LABEL_MAX_LENGTH)),
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1830
1927
|
private buildAgentDetail(agent: PersistedAgent, includeDescendants = true): AgentDetail {
|
|
1831
1928
|
const summary = this.buildAgentSummary(agent, includeDescendants);
|
|
1832
1929
|
const runtime = this.runtimes.get(agent.agent_id);
|
|
@@ -146,6 +146,7 @@ const WaitTurnDetailsSchema = Type.Object({
|
|
|
146
146
|
error: Type.Optional(Type.String()),
|
|
147
147
|
elapsed_ms: Type.Optional(Type.Number()),
|
|
148
148
|
usage: Type.Optional(RenderUsageSchema),
|
|
149
|
+
already_delivered: Type.Optional(Type.Boolean()),
|
|
149
150
|
messages: Type.Optional(Type.Array(WaitMessageDetailsSchema)),
|
|
150
151
|
});
|
|
151
152
|
const WaitTimeoutDetailsSchema = Type.Object({
|
|
@@ -153,7 +154,13 @@ const WaitTimeoutDetailsSchema = Type.Object({
|
|
|
153
154
|
agent_id: Type.String(),
|
|
154
155
|
turn_id: Type.String(),
|
|
155
156
|
timeout_ms: Type.Number(),
|
|
156
|
-
|
|
157
|
+
state: Type.Optional(Type.String()),
|
|
158
|
+
elapsed_ms: Type.Optional(Type.Number()),
|
|
159
|
+
latest_activity_at: Type.Optional(Type.String()),
|
|
160
|
+
total_tokens: Type.Optional(Type.Number()),
|
|
161
|
+
recent_activity_labels: Type.Optional(Type.Array(Type.String())),
|
|
162
|
+
/** Pre-compact timeout results carried the full child status. */
|
|
163
|
+
agent: Type.Optional(RenderStatusAgentSchema),
|
|
157
164
|
});
|
|
158
165
|
const StatusDetailsSchema = Type.Union([
|
|
159
166
|
Type.Object({
|
|
@@ -300,12 +307,23 @@ export const CoordinatorToolOutputSchemas = {
|
|
|
300
307
|
messages: Type.Optional(Type.Array(CoordinatorWaitMessageOutputSchema)),
|
|
301
308
|
...CoordinatorWaitSourceOutputSchema,
|
|
302
309
|
}),
|
|
310
|
+
Type.Object({
|
|
311
|
+
...Type.Pick(CoordinatorTurnOutputSchema, ["agent_id", "turn_id", "status"]).properties,
|
|
312
|
+
event: Type.Literal("turn"),
|
|
313
|
+
already_delivered: Type.Literal(true),
|
|
314
|
+
messages: Type.Optional(Type.Array(CoordinatorWaitMessageOutputSchema)),
|
|
315
|
+
...CoordinatorWaitSourceOutputSchema,
|
|
316
|
+
}),
|
|
303
317
|
Type.Object({
|
|
304
318
|
event: Type.Literal("timeout"),
|
|
305
319
|
agent_id: Type.String(),
|
|
306
320
|
turn_id: Type.String(),
|
|
307
321
|
timeout_ms: Type.Number(),
|
|
308
|
-
|
|
322
|
+
state: Type.Union([Type.Literal("running"), Type.Literal("idle")]),
|
|
323
|
+
elapsed_ms: Type.Optional(Type.Number()),
|
|
324
|
+
latest_activity_at: Type.Optional(Type.String()),
|
|
325
|
+
total_tokens: Type.Optional(Type.Number()),
|
|
326
|
+
recent_activity_labels: Type.Array(Type.String()),
|
|
309
327
|
...CoordinatorWaitSourceOutputSchema,
|
|
310
328
|
}),
|
|
311
329
|
]),
|
|
@@ -582,6 +582,19 @@ function renderWaitProgress(
|
|
|
582
582
|
return container;
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
+
/** Shown instead of the output of a result already delivered to the parent automatically. */
|
|
586
|
+
const ALREADY_DELIVERED_TEXT = "Already delivered automatically; wait with turn_id to reread it.";
|
|
587
|
+
|
|
588
|
+
/** The activity labels of a timeout result, including pre-compact results that carried the full status. */
|
|
589
|
+
function timeoutActivityLabels(
|
|
590
|
+
details: Extract<WaitRenderDetails, { event: "timeout" }>,
|
|
591
|
+
): readonly string[] {
|
|
592
|
+
return (
|
|
593
|
+
details.recent_activity_labels ??
|
|
594
|
+
latestProgressActivity(details.agent?.recent_activity ?? []).map((entry) => entry.label)
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
585
598
|
/** The collapsed body of a settled wait: what the child said, or why it stopped. */
|
|
586
599
|
function collapsedWaitBody(
|
|
587
600
|
details: WaitRenderDetails,
|
|
@@ -590,8 +603,13 @@ function collapsedWaitBody(
|
|
|
590
603
|
): Component | undefined {
|
|
591
604
|
if (details.event === "message") return collapsedTextPreview(details.message, theme);
|
|
592
605
|
if (details.event === "timeout") {
|
|
593
|
-
const
|
|
594
|
-
return
|
|
606
|
+
const labels = timeoutActivityLabels(details);
|
|
607
|
+
return labels.length > 0
|
|
608
|
+
? collapsedTextPreview(theme.fg("dim", labels.join(" \u00b7 ")), theme)
|
|
609
|
+
: undefined;
|
|
610
|
+
}
|
|
611
|
+
if (details.already_delivered) {
|
|
612
|
+
return collapsedTextPreview(theme.fg("muted", ALREADY_DELIVERED_TEXT), theme);
|
|
595
613
|
}
|
|
596
614
|
const output = details.output ?? "";
|
|
597
615
|
if (status === "completed") {
|
|
@@ -621,7 +639,9 @@ function renderWaitResult(
|
|
|
621
639
|
details.event === "timeout" ? details.timeout_ms : details.elapsed_ms,
|
|
622
640
|
);
|
|
623
641
|
const usage = details.event === "timeout" ? undefined : details.usage;
|
|
624
|
-
const tokens = formatSubagentTokenCount(
|
|
642
|
+
const tokens = formatSubagentTokenCount(
|
|
643
|
+
details.event === "timeout" ? details.total_tokens : usage?.totalTokens,
|
|
644
|
+
);
|
|
625
645
|
const drainedMessageCount =
|
|
626
646
|
details.event === "message" || details.event === "timeout"
|
|
627
647
|
? 0
|
|
@@ -652,12 +672,15 @@ function renderWaitResult(
|
|
|
652
672
|
return container;
|
|
653
673
|
}
|
|
654
674
|
if (details.event === "timeout") {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
theme,
|
|
658
|
-
"Child status",
|
|
659
|
-
renderDetailedStatusAgent(details.agent, options, theme),
|
|
675
|
+
container.addChild(
|
|
676
|
+
renderLabelValue(theme, "State", details.state ?? details.agent?.state ?? "unknown"),
|
|
660
677
|
);
|
|
678
|
+
if (details.latest_activity_at) {
|
|
679
|
+
container.addChild(renderLabelValue(theme, "Latest activity", details.latest_activity_at));
|
|
680
|
+
}
|
|
681
|
+
const labels = timeoutActivityLabels(details);
|
|
682
|
+
if (labels.length > 0)
|
|
683
|
+
appendTextSection(container, theme, "Recent activity", labels.join("\n"));
|
|
661
684
|
return container;
|
|
662
685
|
}
|
|
663
686
|
if (details.messages && details.messages.length > 0) {
|
|
@@ -669,7 +692,9 @@ function renderWaitResult(
|
|
|
669
692
|
);
|
|
670
693
|
}
|
|
671
694
|
const output = details.output ?? "";
|
|
672
|
-
if (
|
|
695
|
+
if (details.already_delivered) {
|
|
696
|
+
appendTextSection(container, theme, "Output", ALREADY_DELIVERED_TEXT);
|
|
697
|
+
} else if (status === "completed") {
|
|
673
698
|
if (output.length > 0) {
|
|
674
699
|
appendComponentSection(
|
|
675
700
|
container,
|
|
@@ -36,6 +36,7 @@ const DeliveryEvidenceMessageSchema = Type.Object({
|
|
|
36
36
|
/** Parses durable custom-message and wait-tool Delivery Evidence details. */
|
|
37
37
|
export const DeliveryEvidenceDetailsSchema = Type.Object({
|
|
38
38
|
event: Type.Optional(Type.String()),
|
|
39
|
+
already_delivered: Type.Optional(Type.Boolean()),
|
|
39
40
|
source_agent_id: Type.String(),
|
|
40
41
|
source_turn_id: Type.String(),
|
|
41
42
|
delivery_id: Type.Optional(Type.String()),
|
|
@@ -503,8 +503,13 @@ export function findDeliveryEvidence(
|
|
|
503
503
|
))
|
|
504
504
|
);
|
|
505
505
|
}
|
|
506
|
-
// A message or
|
|
507
|
-
if (
|
|
506
|
+
// A message, timeout, or already-delivered wait result never carried the turn's terminal result.
|
|
507
|
+
if (
|
|
508
|
+
waitToolResult &&
|
|
509
|
+
(details.event === "message" ||
|
|
510
|
+
details.event === "timeout" ||
|
|
511
|
+
details.already_delivered === true)
|
|
512
|
+
)
|
|
508
513
|
return false;
|
|
509
514
|
return details.source_agent_id === sourceAgentId && details.source_turn_id === sourceTurnId;
|
|
510
515
|
});
|
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
DeleteResult,
|
|
39
39
|
SpawnResult,
|
|
40
40
|
StatusResult,
|
|
41
|
+
WaitDeliveredTurnResult,
|
|
41
42
|
WaitResult,
|
|
42
43
|
} from "./minimal-subagents-types.js";
|
|
43
44
|
|
|
@@ -197,6 +198,22 @@ function structuredToolResult<TDetails extends CoordinatorToolResultDetails>(
|
|
|
197
198
|
};
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
/**
|
|
202
|
+
* One line saying an already-delivered result is not repeated and how to reread it, followed by
|
|
203
|
+
* any Coordination Messages the wait drained, which the parent has not seen yet.
|
|
204
|
+
*/
|
|
205
|
+
function alreadyDeliveredContent(
|
|
206
|
+
result: WaitDeliveredTurnResult,
|
|
207
|
+
): AgentToolResult<WaitResult>["content"] {
|
|
208
|
+
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.`;
|
|
209
|
+
if (!result.messages) return [{ type: "text", text: notice }];
|
|
210
|
+
const messages = truncateHead(JSON.stringify({ messages: result.messages }, null, 2), {
|
|
211
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
212
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
213
|
+
});
|
|
214
|
+
return [{ type: "text", text: `${notice}\n${messages.content}` }];
|
|
215
|
+
}
|
|
216
|
+
|
|
200
217
|
/**
|
|
201
218
|
* Report a failed operation that still carries declared output: the model sees an error and
|
|
202
219
|
* codemode scripts receive `structuredContent` instead of a data-less rejection.
|
|
@@ -313,7 +330,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
313
330
|
name: "subagent_wait",
|
|
314
331
|
label: "Subagent Wait",
|
|
315
332
|
description:
|
|
316
|
-
"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
|
|
333
|
+
"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.",
|
|
317
334
|
promptSnippet: "Wait for one direct child's exact turn",
|
|
318
335
|
parameters: options.schemas.subagent_wait,
|
|
319
336
|
annotations: {
|
|
@@ -358,7 +375,10 @@ export function createCoordinatorToolDefinitions(
|
|
|
358
375
|
source_turn_id: result.turn_id,
|
|
359
376
|
};
|
|
360
377
|
return {
|
|
361
|
-
|
|
378
|
+
content:
|
|
379
|
+
"already_delivered" in result
|
|
380
|
+
? alreadyDeliveredContent(result)
|
|
381
|
+
: structuredToolResult(result).content,
|
|
362
382
|
details,
|
|
363
383
|
structuredContent: JSON.parse(JSON.stringify(details)),
|
|
364
384
|
};
|
|
@@ -98,19 +98,41 @@ export interface WaitTurnResult extends TurnResult {
|
|
|
98
98
|
messages?: WaitMessageResult[];
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Reports that a default wait selected a terminal turn whose result was already delivered
|
|
103
|
+
* automatically, handed or settled, so the output is not repeated; an explicit `turn_id` rereads it.
|
|
104
|
+
*/
|
|
105
|
+
export interface WaitDeliveredTurnResult extends Pick<
|
|
106
|
+
TurnResult,
|
|
107
|
+
"agent_id" | "turn_id" | "status"
|
|
108
|
+
> {
|
|
109
|
+
event: "turn";
|
|
110
|
+
already_delivered: true;
|
|
111
|
+
messages?: WaitMessageResult[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Reports an observational wait timeout with a compact, bounded child progress snapshot. */
|
|
102
115
|
export interface WaitTimeoutResult {
|
|
103
116
|
event: "timeout";
|
|
104
117
|
agent_id: string;
|
|
105
118
|
turn_id: string;
|
|
106
119
|
/** Requested timeout duration that expired. */
|
|
107
120
|
timeout_ms: number;
|
|
108
|
-
|
|
109
|
-
|
|
121
|
+
state: AgentState;
|
|
122
|
+
elapsed_ms?: number;
|
|
123
|
+
latest_activity_at?: string;
|
|
124
|
+
/** Total tokens the child has used, when usage is known. */
|
|
125
|
+
total_tokens?: number;
|
|
126
|
+
/** Labels of the child's last few Recent Activity items, oldest first. */
|
|
127
|
+
recent_activity_labels: string[];
|
|
110
128
|
}
|
|
111
129
|
|
|
112
130
|
/** Reports one message, terminal turn, or timeout returned by subagent_wait. */
|
|
113
|
-
export type WaitResult =
|
|
131
|
+
export type WaitResult =
|
|
132
|
+
| WaitMessageResult
|
|
133
|
+
| WaitTurnResult
|
|
134
|
+
| WaitDeliveredTurnResult
|
|
135
|
+
| WaitTimeoutResult;
|
|
114
136
|
|
|
115
137
|
/** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
|
|
116
138
|
export interface AgentSummary extends RuntimeProfile {
|