@ian-pascoe/pi-minimal-subagents 0.2.2 → 0.3.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 +18 -8
- package/package.json +1 -1
- package/src/minimal-subagents-capabilities.ts +3 -8
- package/src/minimal-subagents-context.ts +72 -1
- package/src/minimal-subagents-coordinator.ts +133 -61
- package/src/minimal-subagents-extension.ts +67 -66
- package/src/minimal-subagents-fork-lifecycle.ts +4 -11
- package/src/minimal-subagents-registry-wire.ts +0 -1
- package/src/minimal-subagents-registry.ts +1 -9
- package/src/minimal-subagents-render-contract.ts +20 -10
- package/src/minimal-subagents-rendering.ts +55 -19
- package/src/minimal-subagents-sessions.ts +66 -46
- package/src/minimal-subagents-tools.ts +2 -2
- package/src/minimal-subagents-types.ts +29 -10
- package/src/minimal-subagents-ui.ts +4 -3
package/README.md
CHANGED
|
@@ -130,6 +130,11 @@ children: `subagent`, `agent_message`, `subagent_wait`, `subagent_status`,
|
|
|
130
130
|
only the three adjacent-coordination tools: `agent_message`, `subagent_wait`,
|
|
131
131
|
and `subagent_status`.
|
|
132
132
|
|
|
133
|
+
Targeted `subagent_status` includes `recent_activity`, a bounded tail of message
|
|
134
|
+
text, reasoning, tool calls, and tool results. It includes the current streaming
|
|
135
|
+
assistant message but omits image data. Timeout Wait Events include the same
|
|
136
|
+
detailed status snapshot.
|
|
137
|
+
|
|
133
138
|
The `subagent` `tools` argument distinguishes capability presets from exact
|
|
134
139
|
lists: `"read"` grants `read`, `grep`, `find`, and `ls`; `"modify"` adds
|
|
135
140
|
`bash`, `edit`, and `write`; an array such as `["read"]` grants exactly the
|
|
@@ -148,6 +153,10 @@ optional `turn_id` to address an older retained turn exactly. Without it, waits
|
|
|
148
153
|
select the oldest observable claimed or pending turn before the active/latest
|
|
149
154
|
turn. A caller may have only one outstanding wait for the same source turn; a
|
|
150
155
|
concurrent duplicate is rejected instead of competing for one Wait Event.
|
|
156
|
+
When `timeout_ms` expires, the wait returns an observational `event: "timeout"`
|
|
157
|
+
with the requested turn identity and the same detailed Child Agent status used
|
|
158
|
+
by targeted `subagent_status`. It removes only the waiter, leaving the child
|
|
159
|
+
running and all pending delivery unclaimed. Abort signals remain errors.
|
|
151
160
|
|
|
152
161
|
The persisted Delivery Ledger records Coordination Messages, terminal results,
|
|
153
162
|
globally increasing sequence, and wait ownership before delivery. Existing
|
|
@@ -155,14 +164,15 @@ items retain their sequence; gaps from skipped malformed records are valid.
|
|
|
155
164
|
Claims can name only active, latest, or retained turns. Wait-returned messages
|
|
156
165
|
retain individual delivery evidence; terminal wait ownership remains durable
|
|
157
166
|
across reloads, forks, and newer turns. Automatic fallback retains its ordered
|
|
158
|
-
queue reservation
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
queue reservation while batching queued messages from one source turn into one
|
|
168
|
+
Pi steer. Root-bound messages remain batchable while the root turn is active; a
|
|
169
|
+
pending terminal result absorbs them. Child sessions drain all available steers
|
|
170
|
+
before the next model call. Destination-session Delivery Evidence still settles
|
|
171
|
+
each batched ledger item independently, preventing duplicate delivery and
|
|
172
|
+
unbounded checkpoint growth. The pure Delivery Ledger state machine retains at
|
|
173
|
+
most 20 pending wait-only terminal results per source agent; Coordination Messages
|
|
174
|
+
are not removed by that terminal-retention limit. Delivered messages include
|
|
175
|
+
stable delivery, source-agent, and source-turn identities in persisted details.
|
|
166
176
|
|
|
167
177
|
Deleting a child first verifies its session header and persistent identity,
|
|
168
178
|
then uses the optional `trash` command when available and falls back to
|
package/package.json
CHANGED
|
@@ -39,11 +39,10 @@ export function buildEligibleModelIds(input: {
|
|
|
39
39
|
return [...new Set(source.map(({ provider, id }) => `${provider}/${id}`))];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/** Supplies inherited tools
|
|
42
|
+
/** Supplies inherited tools and the ancestor ceiling for exact tool resolution. */
|
|
43
43
|
export interface ToolResolutionContext {
|
|
44
44
|
ordinaryTools: readonly string[];
|
|
45
45
|
capabilityCeiling: readonly string[];
|
|
46
|
-
availableTools: readonly string[];
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
/** Resolve an exact ordinary-tool contract and reject missing or over-ceiling capabilities. */
|
|
@@ -69,13 +68,9 @@ export function resolveOrdinaryToolSelection(
|
|
|
69
68
|
`Minimal subagents ordinary tool selection: coordinator tools are injected separately and must not appear in tools: ${requestedCoordinatorTools.join(", ")}`,
|
|
70
69
|
);
|
|
71
70
|
}
|
|
72
|
-
|
|
71
|
+
// ponytail: availableTools and capabilityCeiling are identical at every production site,
|
|
72
|
+
// so availability and the ancestor ceiling are enforced by a single membership check.
|
|
73
73
|
const ceiling = new Set(context.capabilityCeiling);
|
|
74
|
-
const missing = uniqueRequested.filter((name) => !available.has(name));
|
|
75
|
-
if (missing.length > 0) {
|
|
76
|
-
throw new Error(`Minimal subagents tool resolution: unavailable tool: ${missing.join(", ")}`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
74
|
const exceeded = uniqueRequested.filter((name) => !ceiling.has(name));
|
|
80
75
|
if (exceeded.length > 0) {
|
|
81
76
|
throw new Error(`Minimal subagents capability ceiling exceeded: ${exceeded.join(", ")}`);
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type
|
|
2
|
+
import { contentText, type ImageContent, type TextContent } from "@earendil-works/pi-ai";
|
|
3
|
+
import { truncateTail } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { RecentAgentActivity, SessionContextMode } from "./minimal-subagents-types.js";
|
|
5
|
+
|
|
6
|
+
const RECENT_AGENT_ACTIVITY_LIMIT = 12;
|
|
7
|
+
const RECENT_AGENT_ACTIVITY_MAX_LINES = 20;
|
|
8
|
+
const RECENT_AGENT_ACTIVITY_MAX_BYTES = 2 * 1024;
|
|
3
9
|
|
|
4
10
|
/** Clone committed caller messages and exclude only the currently streaming assistant message. */
|
|
5
11
|
export function snapshotCommittedContext(
|
|
@@ -11,6 +17,71 @@ export function snapshotCommittedContext(
|
|
|
11
17
|
return structuredClone(committed);
|
|
12
18
|
}
|
|
13
19
|
|
|
20
|
+
function boundedRecentActivityContent(label: string, content: string): RecentAgentActivity {
|
|
21
|
+
const bounded = truncateTail(content, {
|
|
22
|
+
maxLines: RECENT_AGENT_ACTIVITY_MAX_LINES,
|
|
23
|
+
maxBytes: RECENT_AGENT_ACTIVITY_MAX_BYTES,
|
|
24
|
+
});
|
|
25
|
+
return { label, content: bounded.content, truncated: bounded.truncated };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function visibleMessageContent(content: string | readonly (TextContent | ImageContent)[]): string {
|
|
29
|
+
return contentText(content, "\n\n") || "(no text content)";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Build a bounded recent activity tail from message text, reasoning, and tool work. */
|
|
33
|
+
export function buildRecentAgentActivity(messages: readonly AgentMessage[]): RecentAgentActivity[] {
|
|
34
|
+
const activity: RecentAgentActivity[] = [];
|
|
35
|
+
for (const message of messages) {
|
|
36
|
+
if (message.role === "assistant") {
|
|
37
|
+
for (const content of message.content) {
|
|
38
|
+
if (content.type === "text" && content.text) {
|
|
39
|
+
activity.push(boundedRecentActivityContent("assistant message", content.text));
|
|
40
|
+
} else if (content.type === "thinking") {
|
|
41
|
+
activity.push(
|
|
42
|
+
boundedRecentActivityContent(
|
|
43
|
+
"reasoning",
|
|
44
|
+
content.thinking ||
|
|
45
|
+
(content.redacted ? "[redacted reasoning]" : "(no reasoning text)"),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
} else if (content.type === "toolCall") {
|
|
49
|
+
activity.push(
|
|
50
|
+
boundedRecentActivityContent(
|
|
51
|
+
`tool call ${content.name}`,
|
|
52
|
+
JSON.stringify(content.arguments, null, 2),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else if (message.role === "toolResult") {
|
|
58
|
+
activity.push(
|
|
59
|
+
boundedRecentActivityContent(
|
|
60
|
+
`tool result ${message.toolName}${message.isError ? " (error)" : ""}`,
|
|
61
|
+
visibleMessageContent(message.content),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
} else if (message.role === "user" || message.role === "custom") {
|
|
65
|
+
activity.push(
|
|
66
|
+
boundedRecentActivityContent(
|
|
67
|
+
`${message.role} message`,
|
|
68
|
+
visibleMessageContent(message.content),
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
} else if (message.role === "branchSummary" || message.role === "compactionSummary") {
|
|
72
|
+
activity.push(boundedRecentActivityContent(`${message.role} message`, message.summary));
|
|
73
|
+
} else if (message.role === "bashExecution") {
|
|
74
|
+
activity.push(
|
|
75
|
+
boundedRecentActivityContent(
|
|
76
|
+
`${message.role} message`,
|
|
77
|
+
`$ ${message.command}\n${message.output || "(no output)"}`,
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return activity.slice(-RECENT_AGENT_ACTIVITY_LIMIT);
|
|
83
|
+
}
|
|
84
|
+
|
|
14
85
|
/** Carries the selected caller messages and whether child preparation should compact them. */
|
|
15
86
|
export interface ImportedSubagentContext {
|
|
16
87
|
messages: AgentMessage[];
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
assembleImportedContext,
|
|
4
|
+
buildRecentAgentActivity,
|
|
5
|
+
contextContainsImages,
|
|
6
|
+
} from "./minimal-subagents-context.js";
|
|
3
7
|
import {
|
|
4
8
|
canAgentContractSpawn,
|
|
5
9
|
DEFAULT_MAX_SUBAGENT_DEPTH,
|
|
@@ -29,6 +33,7 @@ import {
|
|
|
29
33
|
type DeliveryLedgerTransition,
|
|
30
34
|
} from "./minimal-subagents-delivery-ledger.js";
|
|
31
35
|
import { addCoordinatorMessageEnvelope } from "./minimal-subagents-message-envelope.js";
|
|
36
|
+
import { unavailableAgent } from "./minimal-subagents-sessions.js";
|
|
32
37
|
import { createRegistryEvent } from "./minimal-subagents-registry.js";
|
|
33
38
|
import type {
|
|
34
39
|
AgentDetail,
|
|
@@ -51,7 +56,6 @@ import type {
|
|
|
51
56
|
SpawnParameters,
|
|
52
57
|
SpawnResult,
|
|
53
58
|
StatusResult,
|
|
54
|
-
TurnId,
|
|
55
59
|
TurnResult,
|
|
56
60
|
WaitMessageResult,
|
|
57
61
|
WaitResult,
|
|
@@ -110,6 +114,24 @@ function terminalWaitResult(result: TurnResult, messages: WaitMessageResult[] =
|
|
|
110
114
|
return messages.length === 0 ? terminal : { ...terminal, messages: structuredClone(messages) };
|
|
111
115
|
}
|
|
112
116
|
|
|
117
|
+
function combineCoordinatorMessages(messages: readonly CoordinatorMessage[]): CoordinatorMessage {
|
|
118
|
+
const latest = messages.at(-1);
|
|
119
|
+
if (!latest) throw new Error("Minimal subagents message batch must not be empty");
|
|
120
|
+
if (messages.length === 1) return latest;
|
|
121
|
+
const references = messages.flatMap(
|
|
122
|
+
(message) =>
|
|
123
|
+
message.details.messages ??
|
|
124
|
+
(message.details.delivery_id
|
|
125
|
+
? [{ delivery_id: message.details.delivery_id, message_id: message.details.message_id }]
|
|
126
|
+
: []),
|
|
127
|
+
);
|
|
128
|
+
return {
|
|
129
|
+
...latest,
|
|
130
|
+
content: messages.map((message) => message.content).join("\n\n"),
|
|
131
|
+
details: references.length > 0 ? { ...latest.details, messages: references } : latest.details,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
113
135
|
/** One root-owned coordinator for persistent nested Pi child sessions. */
|
|
114
136
|
export class MinimalSubagentsCoordinator {
|
|
115
137
|
private readonly agents = new Map<string, PersistedAgent>();
|
|
@@ -185,7 +207,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
185
207
|
const ordinaryTools = resolveOrdinaryToolSelection(parameters.tools, {
|
|
186
208
|
ordinaryTools: excludeCoordinatorTools(caller.ordinaryTools),
|
|
187
209
|
capabilityCeiling: excludeCoordinatorTools(caller.capabilityCeiling),
|
|
188
|
-
availableTools: excludeCoordinatorTools(caller.availableTools),
|
|
189
210
|
});
|
|
190
211
|
const committedMessages = structuredClone(caller.messages);
|
|
191
212
|
const imported = assembleImportedContext(sessionContext, committedMessages);
|
|
@@ -278,7 +299,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
278
299
|
thinkingLevel: agent.launch_contract.thinking_level,
|
|
279
300
|
ordinaryTools: [...agent.launch_contract.ordinary_tools],
|
|
280
301
|
capabilityCeiling: [...agent.capability_ceiling],
|
|
281
|
-
availableTools: [...agent.capability_ceiling],
|
|
282
302
|
spawnEntryId,
|
|
283
303
|
};
|
|
284
304
|
}
|
|
@@ -336,7 +356,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
336
356
|
}
|
|
337
357
|
}
|
|
338
358
|
|
|
339
|
-
/** Wait for one exact turn
|
|
359
|
+
/** Wait for one exact turn, returning detailed child status if the waiter times out. */
|
|
340
360
|
wait(
|
|
341
361
|
callerId: string,
|
|
342
362
|
agentId: string,
|
|
@@ -387,13 +407,16 @@ export class MinimalSubagentsCoordinator {
|
|
|
387
407
|
reject(error);
|
|
388
408
|
};
|
|
389
409
|
if (timeoutMs !== undefined) {
|
|
390
|
-
waiter.timeout = setTimeout(
|
|
391
|
-
()
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
410
|
+
waiter.timeout = setTimeout(() => {
|
|
411
|
+
this.removeWaiter(key, waiter);
|
|
412
|
+
resolve({
|
|
413
|
+
event: "timeout",
|
|
414
|
+
agent_id: agentId,
|
|
415
|
+
turn_id: turnId,
|
|
416
|
+
timeout_ms: timeoutMs,
|
|
417
|
+
agent: this.buildAgentDetail(agent, false),
|
|
418
|
+
});
|
|
419
|
+
}, timeoutMs);
|
|
397
420
|
}
|
|
398
421
|
if (signal) {
|
|
399
422
|
waiter.abortListener = () =>
|
|
@@ -474,7 +497,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
474
497
|
agent_id: agentId,
|
|
475
498
|
recursive,
|
|
476
499
|
deleted_agent_ids: [],
|
|
477
|
-
tombstoned_agent_ids: [],
|
|
478
500
|
trashed_session_files: [],
|
|
479
501
|
failures: [],
|
|
480
502
|
};
|
|
@@ -502,7 +524,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
502
524
|
this.pruneRecentMessageProjectionsForDeletedAgent(agent.agent_id);
|
|
503
525
|
this.tombstones.add(agent.agent_id);
|
|
504
526
|
result.deleted_agent_ids.push(agent.agent_id);
|
|
505
|
-
result.tombstoned_agent_ids.push(agent.agent_id);
|
|
506
527
|
this.dependencies.registry.append(
|
|
507
528
|
createRegistryEvent(this.dependencies.registry.rootSessionId, "agent-deleted", {
|
|
508
529
|
agent_ids: [agent.agent_id],
|
|
@@ -549,12 +570,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
549
570
|
this.runtimeInitializations.clear();
|
|
550
571
|
this.pendingAgentIds.clear();
|
|
551
572
|
this.tombstones.clear();
|
|
552
|
-
this.deliveryLedger = createDeliveryLedger();
|
|
553
573
|
this.waiters.clear();
|
|
554
|
-
this.pendingParentMessages.clear();
|
|
555
574
|
this.waitHandedDeliveryIds.clear();
|
|
556
|
-
this.recipientQueues.clear();
|
|
557
|
-
this.backgroundOperations.clear();
|
|
558
575
|
this.automaticDeliveryKeys.clear();
|
|
559
576
|
this.automaticCoordinationDeliveryIds.clear();
|
|
560
577
|
this.deliveryLedger = createDeliveryLedger({
|
|
@@ -898,9 +915,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
898
915
|
return initialization;
|
|
899
916
|
}
|
|
900
917
|
|
|
901
|
-
private beginTurn(agent: PersistedAgent):
|
|
902
|
-
|
|
903
|
-
const turnId = `${agent.agent_id}:turn-${randomUUID()}` as TurnId;
|
|
918
|
+
private beginTurn(agent: PersistedAgent): string {
|
|
919
|
+
const turnId = `${agent.agent_id}:turn-${randomUUID()}`;
|
|
904
920
|
const startedAt = this.now().toISOString();
|
|
905
921
|
agent.active_turn_id = turnId;
|
|
906
922
|
agent.active_turn_started_at = startedAt;
|
|
@@ -994,6 +1010,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
994
1010
|
if (this.automaticDeliveryKeys.has(deliveryKey)) return;
|
|
995
1011
|
this.automaticDeliveryKeys.add(deliveryKey);
|
|
996
1012
|
const graceMs = this.deliveryGraceMs();
|
|
1013
|
+
let batchedCoordinationDeliveries: PersistedCoordinationDelivery[] = [];
|
|
997
1014
|
try {
|
|
998
1015
|
await this.enqueueRecipientDelivery(delivery.destination_agent_id, async () => {
|
|
999
1016
|
if (delivery.destination_agent_id !== "root") {
|
|
@@ -1008,11 +1025,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1008
1025
|
this.settleDelivery(delivery);
|
|
1009
1026
|
return;
|
|
1010
1027
|
}
|
|
1011
|
-
if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery)) return;
|
|
1012
|
-
if (this.hasDeliveryEvidence(delivery)) {
|
|
1013
|
-
this.settleDelivery(delivery);
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
1028
|
const message: CoordinatorMessage = {
|
|
1017
1029
|
customType: "minimal-subagents.result",
|
|
1018
1030
|
content: result.output,
|
|
@@ -1026,11 +1038,28 @@ export class MinimalSubagentsCoordinator {
|
|
|
1026
1038
|
usage: result.usage,
|
|
1027
1039
|
},
|
|
1028
1040
|
};
|
|
1029
|
-
|
|
1030
|
-
|
|
1041
|
+
batchedCoordinationDeliveries = this.takePendingParentDeliveryBatch(
|
|
1042
|
+
deliveryKey,
|
|
1043
|
+
delivery.destination_agent_id,
|
|
1044
|
+
);
|
|
1045
|
+
for (const batchedDelivery of batchedCoordinationDeliveries) {
|
|
1046
|
+
this.waitHandedDeliveryIds.add(batchedDelivery.delivery_id);
|
|
1047
|
+
}
|
|
1048
|
+
await this.deliverToRecipient(
|
|
1049
|
+
delivery.destination_agent_id,
|
|
1050
|
+
combineCoordinatorMessages([
|
|
1051
|
+
...batchedCoordinationDeliveries.map((item) => item.message),
|
|
1052
|
+
message,
|
|
1053
|
+
]),
|
|
1054
|
+
() =>
|
|
1055
|
+
this.isTerminalDeliveryCurrent(delivery) &&
|
|
1056
|
+
batchedCoordinationDeliveries.every((item) => this.isCoordinationDeliveryCurrent(item)),
|
|
1031
1057
|
);
|
|
1032
1058
|
});
|
|
1033
1059
|
} catch (error) {
|
|
1060
|
+
for (const batchedDelivery of batchedCoordinationDeliveries) {
|
|
1061
|
+
this.waitHandedDeliveryIds.delete(batchedDelivery.delivery_id);
|
|
1062
|
+
}
|
|
1034
1063
|
if (!this.isTerminalDeliveryCurrent(delivery)) return;
|
|
1035
1064
|
const deliveryError = error instanceof Error ? error.message : String(error);
|
|
1036
1065
|
this.deliveryLedger = setTerminalDeliveryError(
|
|
@@ -1442,11 +1471,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1442
1471
|
elapsed_ms: elapsed ?? agent.latest_result?.elapsed_ms,
|
|
1443
1472
|
latest_activity_at: agent.latest_activity_at ?? agent.created_at,
|
|
1444
1473
|
task: agent.task,
|
|
1445
|
-
latest_activity: agent.active_turn_id
|
|
1446
|
-
? "turn running"
|
|
1447
|
-
: agent.latest_result
|
|
1448
|
-
? `turn ${agent.latest_result.status}`
|
|
1449
|
-
: "created",
|
|
1450
1474
|
child_count: directChildren.length,
|
|
1451
1475
|
children,
|
|
1452
1476
|
};
|
|
@@ -1454,7 +1478,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
1454
1478
|
|
|
1455
1479
|
private buildAgentDetail(agent: PersistedAgent, includeDescendants = true): AgentDetail {
|
|
1456
1480
|
const summary = this.buildAgentSummary(agent, includeDescendants);
|
|
1457
|
-
const
|
|
1481
|
+
const runtime = this.runtimes.get(agent.agent_id);
|
|
1482
|
+
const runtimeUsage = runtime?.getUsage();
|
|
1458
1483
|
return {
|
|
1459
1484
|
...summary,
|
|
1460
1485
|
session_file: agent.session_file,
|
|
@@ -1462,6 +1487,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1462
1487
|
capability_ceiling: [...agent.capability_ceiling],
|
|
1463
1488
|
spawn_entry_id: agent.spawn_entry_id,
|
|
1464
1489
|
recent_messages: structuredClone(agent.recent_messages),
|
|
1490
|
+
recent_activity: buildRecentAgentActivity(runtime?.snapshotActivityMessages() ?? []),
|
|
1465
1491
|
latest_result: agent.latest_result ? structuredClone(agent.latest_result) : undefined,
|
|
1466
1492
|
missing_dependencies: [...agent.missing_dependencies],
|
|
1467
1493
|
unavailable_reason: agent.unavailable_reason,
|
|
@@ -1585,19 +1611,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1585
1611
|
}
|
|
1586
1612
|
|
|
1587
1613
|
private createForkPlaceholder(agent: PersistedAgent, cloneError: string): PersistedAgent {
|
|
1588
|
-
return
|
|
1589
|
-
...structuredClone(agent),
|
|
1590
|
-
session_file: undefined,
|
|
1591
|
-
session_id: undefined,
|
|
1592
|
-
session_leaf_id: undefined,
|
|
1593
|
-
clone_error: cloneError,
|
|
1594
|
-
active_turn_id: undefined,
|
|
1595
|
-
active_turn_started_at: undefined,
|
|
1596
|
-
latest_activity_at: this.now().toISOString(),
|
|
1597
|
-
availability: "unavailable",
|
|
1598
|
-
missing_dependencies: [cloneError],
|
|
1599
|
-
unavailable_reason: cloneError,
|
|
1600
|
-
};
|
|
1614
|
+
return unavailableAgent(agent, cloneError, this.now().toISOString());
|
|
1601
1615
|
}
|
|
1602
1616
|
|
|
1603
1617
|
private rejectAllWaiters(error: Error): void {
|
|
@@ -1765,6 +1779,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1765
1779
|
)
|
|
1766
1780
|
return;
|
|
1767
1781
|
|
|
1782
|
+
let automaticBatch = [delivery];
|
|
1768
1783
|
const operation = this.enqueueRecipientDelivery(targetId, async () => {
|
|
1769
1784
|
if (targetId !== "root") {
|
|
1770
1785
|
await this.ensureRuntime(this.requireUsableAgent(targetId, "message"));
|
|
@@ -1786,25 +1801,63 @@ export class MinimalSubagentsCoordinator {
|
|
|
1786
1801
|
pending.cancelGrace?.();
|
|
1787
1802
|
pending.cancelGrace = undefined;
|
|
1788
1803
|
if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
|
|
1804
|
+
while (
|
|
1805
|
+
targetId === "root" &&
|
|
1806
|
+
!this.dependencies.root.isIdle() &&
|
|
1807
|
+
!findTerminalDelivery(
|
|
1808
|
+
this.deliveryLedger,
|
|
1809
|
+
message.details.source_agent_id,
|
|
1810
|
+
message.details.source_turn_id,
|
|
1811
|
+
) &&
|
|
1812
|
+
this.acceptingOperations &&
|
|
1813
|
+
!pending.claimed &&
|
|
1814
|
+
!turnClaimed()
|
|
1815
|
+
) {
|
|
1816
|
+
await Promise.race([
|
|
1817
|
+
pending.claimPromise,
|
|
1818
|
+
new Promise((resolve) => setTimeout(resolve, 25)),
|
|
1819
|
+
]);
|
|
1820
|
+
}
|
|
1789
1821
|
if (!this.acceptingOperations || pending.claimed || turnClaimed()) return;
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1822
|
+
const terminalDelivery = findTerminalDelivery(
|
|
1823
|
+
this.deliveryLedger,
|
|
1824
|
+
message.details.source_agent_id,
|
|
1825
|
+
message.details.source_turn_id,
|
|
1826
|
+
);
|
|
1827
|
+
if (
|
|
1828
|
+
terminalDelivery?.destination_agent_id === targetId &&
|
|
1829
|
+
terminalDelivery.path === "message"
|
|
1830
|
+
) {
|
|
1831
|
+
for (const queued of this.pendingParentMessages.get(key) ?? []) {
|
|
1832
|
+
queued.cancelGrace?.();
|
|
1833
|
+
queued.releaseClaim();
|
|
1834
|
+
}
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
automaticBatch = this.takePendingParentDeliveryBatch(key, targetId);
|
|
1838
|
+
for (const batchedDelivery of automaticBatch) {
|
|
1839
|
+
this.waitHandedDeliveryIds.add(batchedDelivery.delivery_id);
|
|
1840
|
+
}
|
|
1841
|
+
if (automaticBatch.length === 0) return;
|
|
1842
|
+
await this.deliverToRecipient(
|
|
1843
|
+
targetId,
|
|
1844
|
+
combineCoordinatorMessages(automaticBatch.map((item) => item.message)),
|
|
1845
|
+
() => automaticBatch.every((item) => this.isCoordinationDeliveryCurrent(item)),
|
|
1794
1846
|
);
|
|
1795
1847
|
});
|
|
1796
1848
|
void operation.catch((cause) => {
|
|
1797
|
-
this.removePendingParentMessage(key, pending);
|
|
1798
|
-
this.waitHandedDeliveryIds.delete(delivery.delivery_id);
|
|
1799
|
-
if (!this.isCoordinationDeliveryCurrent(delivery)) return;
|
|
1800
1849
|
const deliveryError = cause instanceof Error ? cause.message : String(cause);
|
|
1801
|
-
|
|
1802
|
-
this.
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1850
|
+
for (const batchedDelivery of automaticBatch) {
|
|
1851
|
+
this.waitHandedDeliveryIds.delete(batchedDelivery.delivery_id);
|
|
1852
|
+
if (!this.isCoordinationDeliveryCurrent(batchedDelivery)) continue;
|
|
1853
|
+
this.deliveryLedger = setCoordinationDeliveryError(
|
|
1854
|
+
this.deliveryLedger,
|
|
1855
|
+
batchedDelivery.delivery_id,
|
|
1856
|
+
deliveryError,
|
|
1857
|
+
);
|
|
1858
|
+
const current = findCoordinationDelivery(this.deliveryLedger, batchedDelivery.delivery_id);
|
|
1859
|
+
if (current) this.persistCoordinationDeliveryState(current);
|
|
1860
|
+
}
|
|
1808
1861
|
this.dependencies.notify?.({
|
|
1809
1862
|
type: "failure",
|
|
1810
1863
|
agentId: message.details.source_agent_id,
|
|
@@ -1815,6 +1868,25 @@ export class MinimalSubagentsCoordinator {
|
|
|
1815
1868
|
});
|
|
1816
1869
|
}
|
|
1817
1870
|
|
|
1871
|
+
private takePendingParentDeliveryBatch(
|
|
1872
|
+
key: string,
|
|
1873
|
+
targetId: string,
|
|
1874
|
+
): PersistedCoordinationDelivery[] {
|
|
1875
|
+
const batch = (this.pendingParentMessages.get(key) ?? []).filter(
|
|
1876
|
+
(pending) => pending.destinationAgentId === targetId && !pending.claimed,
|
|
1877
|
+
);
|
|
1878
|
+
for (const pending of batch) {
|
|
1879
|
+
pending.claimed = true;
|
|
1880
|
+
pending.cancelGrace?.();
|
|
1881
|
+
pending.releaseClaim();
|
|
1882
|
+
this.removePendingParentMessage(key, pending);
|
|
1883
|
+
}
|
|
1884
|
+
return batch.flatMap((pending) => {
|
|
1885
|
+
const delivery = findCoordinationDelivery(this.deliveryLedger, pending.deliveryId);
|
|
1886
|
+
return delivery ? [delivery] : [];
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1818
1890
|
private removePendingParentMessage(key: string, pending: PendingParentMessage): void {
|
|
1819
1891
|
const pendingMessages = this.pendingParentMessages.get(key);
|
|
1820
1892
|
if (!pendingMessages) return;
|