@ian-pascoe/pi-minimal-subagents 0.14.0 → 0.14.1
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 +13 -7
- package/package.json +1 -1
- package/src/minimal-subagents-coordinator.ts +44 -1
- package/src/minimal-subagents-render-contract.ts +8 -0
- package/src/minimal-subagents-rendering.ts +9 -1
- 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 +18 -1
package/README.md
CHANGED
|
@@ -40,7 +40,9 @@ 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;
|
|
@@ -336,12 +338,16 @@ intermediate Wait Event containing a Coordination Message before the child turn
|
|
|
336
338
|
settles. That event claims only its message, so later unconsumed messages and the
|
|
337
339
|
terminal result retain automatic fallback. If the turn has already settled, a
|
|
338
340
|
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.
|
|
341
|
+
again for a settled turn you claimed returns the same result while it is
|
|
342
|
+
retained. Pass optional `turn_id` to address a retained turn exactly, including
|
|
343
|
+
one whose result you already claimed (a claimed result stays retained until
|
|
344
|
+
Delivery Evidence settles it). Without it, waits skip claimed turns and turns
|
|
345
|
+
whose result was already delivered to you automatically, and select the oldest
|
|
346
|
+
remaining observable turn, falling back to the active, then latest, turn. When
|
|
347
|
+
that fallback reaches a result already delivered to you automatically, whether
|
|
348
|
+
still queued or settled, the wait returns `event: "turn"` with
|
|
349
|
+
`already_delivered: true`, the turn identity, and status, but no output; it does
|
|
350
|
+
not claim the turn, and `turn_id` rereads the full result. A
|
|
345
351
|
caller may have only one outstanding wait for the same source turn; a
|
|
346
352
|
concurrent duplicate is rejected instead of competing for one Wait Event.
|
|
347
353
|
When `timeout_ms` expires, the wait returns an observational `event: "timeout"`
|
package/package.json
CHANGED
|
@@ -69,6 +69,7 @@ import type {
|
|
|
69
69
|
StatusResult,
|
|
70
70
|
TurnResult,
|
|
71
71
|
WaitMessageResult,
|
|
72
|
+
WaitDeliveredTurnResult,
|
|
72
73
|
WaitResult,
|
|
73
74
|
} from "./minimal-subagents-types.js";
|
|
74
75
|
|
|
@@ -145,6 +146,27 @@ function terminalWaitResult(result: TurnResult, messages: WaitMessageResult[] =
|
|
|
145
146
|
return messages.length === 0 ? terminal : { ...terminal, messages: structuredClone(messages) };
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
/** A terminal Wait Event naming an already delivered result without repeating its output. */
|
|
150
|
+
function alreadyDeliveredWaitResult(
|
|
151
|
+
result: TurnResult,
|
|
152
|
+
messages: WaitMessageResult[],
|
|
153
|
+
): WaitDeliveredTurnResult {
|
|
154
|
+
const delivered: WaitDeliveredTurnResult = {
|
|
155
|
+
event: "turn",
|
|
156
|
+
agent_id: result.agent_id,
|
|
157
|
+
turn_id: result.turn_id,
|
|
158
|
+
status: result.status,
|
|
159
|
+
already_delivered: true,
|
|
160
|
+
};
|
|
161
|
+
if (messages.length > 0) delivered.messages = structuredClone(messages);
|
|
162
|
+
return delivered;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The message ID of an automatic result message, which is also its Delivery Evidence key. */
|
|
166
|
+
function automaticResultMessageId(sourceAgentId: string, sourceTurnId: string): string {
|
|
167
|
+
return `result:${sourceAgentId}:${sourceTurnId}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
148
170
|
function combineCoordinatorMessages(messages: readonly CoordinatorMessage[]): CoordinatorMessage {
|
|
149
171
|
const latest = messages.at(-1);
|
|
150
172
|
if (!latest) throw new Error("Minimal subagents message batch must not be empty");
|
|
@@ -486,6 +508,11 @@ export class MinimalSubagentsCoordinator {
|
|
|
486
508
|
(agent.latest_result?.turn_id === turnId ? agent.latest_result : undefined);
|
|
487
509
|
if (retainedResult) {
|
|
488
510
|
const messages = this.drainPendingParentMessages(callerId, agentId, turnId);
|
|
511
|
+
// A default wait falls back to the latest turn, whose result may already have been delivered
|
|
512
|
+
// automatically; repeating its output would deliver it twice. An explicit turn_id rereads it.
|
|
513
|
+
if (requestedTurnId === undefined && this.wasAlreadyDelivered(callerId, retainedResult)) {
|
|
514
|
+
return Promise.resolve(alreadyDeliveredWaitResult(retainedResult, messages));
|
|
515
|
+
}
|
|
489
516
|
this.claimTerminalDelivery(callerId, retainedResult);
|
|
490
517
|
return Promise.resolve(terminalWaitResult(retainedResult, messages));
|
|
491
518
|
}
|
|
@@ -1335,7 +1362,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1335
1362
|
source_agent_id: delivery.source_agent_id,
|
|
1336
1363
|
destination_agent_id: delivery.destination_agent_id,
|
|
1337
1364
|
source_turn_id: result.turn_id,
|
|
1338
|
-
message_id:
|
|
1365
|
+
message_id: automaticResultMessageId(delivery.source_agent_id, result.turn_id),
|
|
1339
1366
|
status: result.status,
|
|
1340
1367
|
elapsed_ms: result.elapsed_ms,
|
|
1341
1368
|
usage: result.usage,
|
|
@@ -1737,6 +1764,22 @@ export class MinimalSubagentsCoordinator {
|
|
|
1737
1764
|
);
|
|
1738
1765
|
}
|
|
1739
1766
|
|
|
1767
|
+
/**
|
|
1768
|
+
* Whether this result was already delivered to the caller automatically: it is handed, or the
|
|
1769
|
+
* caller's branch holds its automatic result message. A wait's own tool result does not count.
|
|
1770
|
+
*/
|
|
1771
|
+
private wasAlreadyDelivered(callerId: string, result: TurnResult): boolean {
|
|
1772
|
+
return (
|
|
1773
|
+
this.handedTerminalKeys.has(deliveryTurnKey(result.agent_id, result.turn_id)) ||
|
|
1774
|
+
this.hasRecipientDeliveryEvidence(
|
|
1775
|
+
callerId,
|
|
1776
|
+
result.agent_id,
|
|
1777
|
+
result.turn_id,
|
|
1778
|
+
automaticResultMessageId(result.agent_id, result.turn_id),
|
|
1779
|
+
)
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1740
1783
|
private hasDeliveryEvidence(delivery: PersistedDelivery): boolean {
|
|
1741
1784
|
return this.hasRecipientDeliveryEvidence(
|
|
1742
1785
|
delivery.destination_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({
|
|
@@ -300,6 +301,13 @@ export const CoordinatorToolOutputSchemas = {
|
|
|
300
301
|
messages: Type.Optional(Type.Array(CoordinatorWaitMessageOutputSchema)),
|
|
301
302
|
...CoordinatorWaitSourceOutputSchema,
|
|
302
303
|
}),
|
|
304
|
+
Type.Object({
|
|
305
|
+
...Type.Pick(CoordinatorTurnOutputSchema, ["agent_id", "turn_id", "status"]).properties,
|
|
306
|
+
event: Type.Literal("turn"),
|
|
307
|
+
already_delivered: Type.Literal(true),
|
|
308
|
+
messages: Type.Optional(Type.Array(CoordinatorWaitMessageOutputSchema)),
|
|
309
|
+
...CoordinatorWaitSourceOutputSchema,
|
|
310
|
+
}),
|
|
303
311
|
Type.Object({
|
|
304
312
|
event: Type.Literal("timeout"),
|
|
305
313
|
agent_id: Type.String(),
|
|
@@ -582,6 +582,9 @@ 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
|
+
|
|
585
588
|
/** The collapsed body of a settled wait: what the child said, or why it stopped. */
|
|
586
589
|
function collapsedWaitBody(
|
|
587
590
|
details: WaitRenderDetails,
|
|
@@ -593,6 +596,9 @@ function collapsedWaitBody(
|
|
|
593
596
|
const activity = latestProgressActivity(details.agent.recent_activity ?? []);
|
|
594
597
|
return activity.length > 0 ? new ActivityLines(activity, theme) : undefined;
|
|
595
598
|
}
|
|
599
|
+
if (details.already_delivered) {
|
|
600
|
+
return collapsedTextPreview(theme.fg("muted", ALREADY_DELIVERED_TEXT), theme);
|
|
601
|
+
}
|
|
596
602
|
const output = details.output ?? "";
|
|
597
603
|
if (status === "completed") {
|
|
598
604
|
return output ? collapsedMarkdownPreview(output, theme) : undefined;
|
|
@@ -669,7 +675,9 @@ function renderWaitResult(
|
|
|
669
675
|
);
|
|
670
676
|
}
|
|
671
677
|
const output = details.output ?? "";
|
|
672
|
-
if (
|
|
678
|
+
if (details.already_delivered) {
|
|
679
|
+
appendTextSection(container, theme, "Output", ALREADY_DELIVERED_TEXT);
|
|
680
|
+
} else if (status === "completed") {
|
|
673
681
|
if (output.length > 0) {
|
|
674
682
|
appendComponentSection(
|
|
675
683
|
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 event=timeout with detailed child status 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,6 +98,19 @@ export interface WaitTurnResult extends TurnResult {
|
|
|
98
98
|
messages?: WaitMessageResult[];
|
|
99
99
|
}
|
|
100
100
|
|
|
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
|
+
|
|
101
114
|
/** Reports an observational wait timeout with the current detailed child status. */
|
|
102
115
|
export interface WaitTimeoutResult {
|
|
103
116
|
event: "timeout";
|
|
@@ -110,7 +123,11 @@ export interface WaitTimeoutResult {
|
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
/** Reports one message, terminal turn, or timeout returned by subagent_wait. */
|
|
113
|
-
export type WaitResult =
|
|
126
|
+
export type WaitResult =
|
|
127
|
+
| WaitMessageResult
|
|
128
|
+
| WaitTurnResult
|
|
129
|
+
| WaitDeliveredTurnResult
|
|
130
|
+
| WaitTimeoutResult;
|
|
114
131
|
|
|
115
132
|
/** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
|
|
116
133
|
export interface AgentSummary extends RuntimeProfile {
|