@ian-pascoe/pi-minimal-subagents 0.1.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/LICENSE +21 -0
- package/README.md +129 -0
- package/package.json +51 -0
- package/src/index.ts +1 -0
- package/src/minimal-subagents-capabilities.ts +118 -0
- package/src/minimal-subagents-config.ts +217 -0
- package/src/minimal-subagents-context.ts +70 -0
- package/src/minimal-subagents-coordinator.ts +1230 -0
- package/src/minimal-subagents-extension.ts +279 -0
- package/src/minimal-subagents-fork-lifecycle.ts +36 -0
- package/src/minimal-subagents-registry.ts +219 -0
- package/src/minimal-subagents-rendering.ts +717 -0
- package/src/minimal-subagents-sessions.ts +702 -0
- package/src/minimal-subagents-shutdown.ts +29 -0
- package/src/minimal-subagents-tool-schemas.ts +66 -0
- package/src/minimal-subagents-tools.ts +285 -0
- package/src/minimal-subagents-types.ts +305 -0
- package/src/minimal-subagents-ui.ts +326 -0
- package/src/minimal-subagents-usage.ts +24 -0
|
@@ -0,0 +1,1230 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { assembleImportedContext, contextContainsImages } from "./minimal-subagents-context.js";
|
|
4
|
+
import {
|
|
5
|
+
canAgentContractSpawn,
|
|
6
|
+
DEFAULT_MAX_SUBAGENT_DEPTH,
|
|
7
|
+
excludeCoordinatorTools,
|
|
8
|
+
getSubagentDepth,
|
|
9
|
+
resolveOrdinaryToolSelection,
|
|
10
|
+
} from "./minimal-subagents-capabilities.js";
|
|
11
|
+
import { createRegistryEvent } from "./minimal-subagents-registry.js";
|
|
12
|
+
import type {
|
|
13
|
+
AgentDetail,
|
|
14
|
+
AgentMessageResult,
|
|
15
|
+
AgentSessionFactory,
|
|
16
|
+
AgentSummary,
|
|
17
|
+
CallerSnapshot,
|
|
18
|
+
CancelResult,
|
|
19
|
+
ChildAgentRuntime,
|
|
20
|
+
CoordinatorDependencies,
|
|
21
|
+
CoordinatorMessage,
|
|
22
|
+
DeleteResult,
|
|
23
|
+
ForkSnapshot,
|
|
24
|
+
HierarchyStatusResult,
|
|
25
|
+
PersistedAgent,
|
|
26
|
+
PersistedDelivery,
|
|
27
|
+
RegistrySnapshot,
|
|
28
|
+
SpawnParameters,
|
|
29
|
+
SpawnResult,
|
|
30
|
+
StatusResult,
|
|
31
|
+
TurnId,
|
|
32
|
+
TurnResult,
|
|
33
|
+
} from "./minimal-subagents-types.js";
|
|
34
|
+
|
|
35
|
+
const FRIENDLY_AGENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
36
|
+
const RESERVED_AGENT_IDS = new Set(["root", "parent"]);
|
|
37
|
+
const RECENT_MESSAGE_LIMIT = 20;
|
|
38
|
+
const DEFAULT_AUTOMATIC_DELIVERY_GRACE_MS = 1_000;
|
|
39
|
+
|
|
40
|
+
interface MessageParameters {
|
|
41
|
+
agent_id?: string;
|
|
42
|
+
message: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface TurnWaiter {
|
|
46
|
+
callerId: string;
|
|
47
|
+
resolve: (result: TurnResult) => void;
|
|
48
|
+
reject: (error: Error) => void;
|
|
49
|
+
timeout?: ReturnType<typeof setTimeout>;
|
|
50
|
+
abortSignal?: AbortSignal;
|
|
51
|
+
abortListener?: () => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function agentDeliveryKey(agentId: string, turnId: string): string {
|
|
55
|
+
return `${agentId}\u0000${turnId}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function terminalTurnResult(
|
|
59
|
+
agentId: string,
|
|
60
|
+
turnId: string,
|
|
61
|
+
outcome: Awaited<ReturnType<ChildAgentRuntime["runPrompt"]>>,
|
|
62
|
+
): TurnResult {
|
|
63
|
+
return {
|
|
64
|
+
agent_id: agentId,
|
|
65
|
+
turn_id: turnId,
|
|
66
|
+
status: outcome.status,
|
|
67
|
+
output: outcome.output,
|
|
68
|
+
error: outcome.error,
|
|
69
|
+
usage: outcome.usage,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One root-owned coordinator for persistent nested Pi child sessions. */
|
|
74
|
+
export class MinimalSubagentsCoordinator {
|
|
75
|
+
private readonly agents = new Map<string, PersistedAgent>();
|
|
76
|
+
private readonly runtimes = new Map<string, ChildAgentRuntime>();
|
|
77
|
+
private readonly runtimeInitializations = new Map<string, Promise<ChildAgentRuntime>>();
|
|
78
|
+
private readonly importedMessages = new Map<string, AgentMessage[]>();
|
|
79
|
+
private readonly tombstones = new Set<string>();
|
|
80
|
+
private readonly pendingAgentIds = new Set<string>();
|
|
81
|
+
private readonly deliveries = new Map<string, PersistedDelivery>();
|
|
82
|
+
private readonly waiters = new Map<string, Set<TurnWaiter>>();
|
|
83
|
+
private readonly recipientQueues = new Map<string, Promise<void>>();
|
|
84
|
+
private readonly backgroundOperations = new Set<Promise<void>>();
|
|
85
|
+
private acceptingOperations = true;
|
|
86
|
+
private shutdownPromise?: Promise<void>;
|
|
87
|
+
|
|
88
|
+
constructor(private readonly dependencies: CoordinatorDependencies) {}
|
|
89
|
+
|
|
90
|
+
private get maxSubagentDepth(): number {
|
|
91
|
+
return this.dependencies.maxSubagentDepth ?? DEFAULT_MAX_SUBAGENT_DEPTH;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Return a serializable complete hierarchy checkpoint without process-local runtimes. */
|
|
95
|
+
snapshot(): RegistrySnapshot {
|
|
96
|
+
return {
|
|
97
|
+
agents: [...this.agents.values()].map((agent) => structuredClone(agent)),
|
|
98
|
+
tombstones: [...this.tombstones],
|
|
99
|
+
deliveries: [...this.deliveries.values()].map((delivery) => structuredClone(delivery)),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Persist a complete registry checkpoint for initial ownership or fork ownership. */
|
|
104
|
+
writeCheckpoint(): void {
|
|
105
|
+
this.dependencies.registry.append(
|
|
106
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "checkpoint", {
|
|
107
|
+
snapshot: this.snapshot(),
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Validate, persist, and schedule one asynchronous child turn. */
|
|
113
|
+
async spawn(
|
|
114
|
+
callerId: string,
|
|
115
|
+
parameters: SpawnParameters,
|
|
116
|
+
caller: CallerSnapshot,
|
|
117
|
+
): Promise<SpawnResult> {
|
|
118
|
+
this.assertAccepting();
|
|
119
|
+
this.assertCallerExists(callerId);
|
|
120
|
+
this.assertCallerMaySpawn(callerId);
|
|
121
|
+
if (parameters.task.trim().length === 0) {
|
|
122
|
+
throw new Error("Minimal subagents spawn validation: task must not be empty");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const friendlyId = parameters.agent_id ?? this.generateFriendlyId(callerId);
|
|
126
|
+
this.validateFriendlyId(friendlyId);
|
|
127
|
+
const agentId = this.buildChildAgentId(callerId, friendlyId);
|
|
128
|
+
if (this.tombstones.has(agentId)) {
|
|
129
|
+
throw new Error(`Minimal subagents agent ID is tombstoned: ${agentId}`);
|
|
130
|
+
}
|
|
131
|
+
if (this.agents.has(agentId) || this.pendingAgentIds.has(agentId)) {
|
|
132
|
+
throw new Error(`Minimal subagents duplicate agent ID: ${agentId}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sessionContext = parameters.session_context ?? "inherit";
|
|
136
|
+
const projectContext = parameters.project_context ?? "inherit";
|
|
137
|
+
const model = parameters.model ?? caller.model;
|
|
138
|
+
const requestedThinking = parameters.thinking_level ?? caller.thinkingLevel;
|
|
139
|
+
const thinkingLevel = this.dependencies.sessions.resolveThinkingLevel(model, requestedThinking);
|
|
140
|
+
const ordinaryTools = resolveOrdinaryToolSelection(parameters.tools, {
|
|
141
|
+
ordinaryTools: excludeCoordinatorTools(caller.ordinaryTools),
|
|
142
|
+
capabilityCeiling: excludeCoordinatorTools(caller.capabilityCeiling),
|
|
143
|
+
availableTools: excludeCoordinatorTools(caller.availableTools),
|
|
144
|
+
});
|
|
145
|
+
const committedMessages = structuredClone(caller.messages);
|
|
146
|
+
const imported = assembleImportedContext(sessionContext, committedMessages);
|
|
147
|
+
if (
|
|
148
|
+
contextContainsImages(imported.messages) &&
|
|
149
|
+
!this.dependencies.sessions.modelSupportsImages(model)
|
|
150
|
+
) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Minimal subagents spawn validation: model ${model} does not support image input`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const createdAt = this.now().toISOString();
|
|
157
|
+
const agent: PersistedAgent = {
|
|
158
|
+
agent_id: agentId,
|
|
159
|
+
friendly_id: friendlyId,
|
|
160
|
+
parent_id: callerId,
|
|
161
|
+
created_at: createdAt,
|
|
162
|
+
task: parameters.task,
|
|
163
|
+
latest_activity_at: createdAt,
|
|
164
|
+
spawn_entry_id: caller.spawnEntryId,
|
|
165
|
+
launch_contract: {
|
|
166
|
+
session_context: sessionContext,
|
|
167
|
+
project_context: projectContext,
|
|
168
|
+
model,
|
|
169
|
+
thinking_level: thinkingLevel,
|
|
170
|
+
tools: parameters.tools,
|
|
171
|
+
ordinary_tools: ordinaryTools,
|
|
172
|
+
delegation: parameters.delegation ?? "none",
|
|
173
|
+
},
|
|
174
|
+
capability_ceiling: [...ordinaryTools],
|
|
175
|
+
availability: "available",
|
|
176
|
+
missing_dependencies: [],
|
|
177
|
+
recent_messages: [],
|
|
178
|
+
};
|
|
179
|
+
this.pendingAgentIds.add(agentId);
|
|
180
|
+
let identity: ReturnType<AgentSessionFactory["createIdentity"]>;
|
|
181
|
+
try {
|
|
182
|
+
const missingDependencies =
|
|
183
|
+
await this.dependencies.sessions.resolveLaunchMissingDependencies(agent);
|
|
184
|
+
this.assertAccepting();
|
|
185
|
+
if (missingDependencies.length > 0) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Minimal subagents launch dependencies unavailable: ${missingDependencies.join(", ")}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
identity = this.dependencies.sessions.createIdentity(agent, imported.messages);
|
|
191
|
+
} finally {
|
|
192
|
+
this.pendingAgentIds.delete(agentId);
|
|
193
|
+
}
|
|
194
|
+
agent.session_file = identity.sessionFile;
|
|
195
|
+
agent.session_id = identity.sessionId;
|
|
196
|
+
this.agents.set(agentId, agent);
|
|
197
|
+
this.importedMessages.set(agentId, imported.messages);
|
|
198
|
+
this.dependencies.registry.append(
|
|
199
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "agent-created", { agent }),
|
|
200
|
+
);
|
|
201
|
+
const turnId = this.beginTurn(agent);
|
|
202
|
+
this.dependencies.notify?.({
|
|
203
|
+
type: "spawn",
|
|
204
|
+
agentId,
|
|
205
|
+
message: `Spawned ${agentId}`,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
this.trackBackgroundOperation(
|
|
209
|
+
this.initializeAndRunPrompt(
|
|
210
|
+
agentId,
|
|
211
|
+
turnId,
|
|
212
|
+
parameters.task,
|
|
213
|
+
imported.compact,
|
|
214
|
+
caller.model,
|
|
215
|
+
caller.thinkingLevel,
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
return { agent_id: agentId, turn_id: turnId, status: "running" };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Capture immutable launch defaults for a nested caller from its active child runtime. */
|
|
222
|
+
snapshotChildCaller(agentId: string, spawnEntryId: string): CallerSnapshot {
|
|
223
|
+
const agent = this.requireAgent(agentId);
|
|
224
|
+
const runtime = this.runtimes.get(agentId);
|
|
225
|
+
return {
|
|
226
|
+
messages: runtime?.snapshotCommittedMessages() ?? [],
|
|
227
|
+
model: agent.launch_contract.model,
|
|
228
|
+
thinkingLevel: agent.launch_contract.thinking_level,
|
|
229
|
+
ordinaryTools: [...agent.launch_contract.ordinary_tools],
|
|
230
|
+
capabilityCeiling: [...agent.capability_ceiling],
|
|
231
|
+
availableTools: [...agent.capability_ceiling],
|
|
232
|
+
spawnEntryId,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Send one steer-only coordination message to an authorized adjacent agent. */
|
|
237
|
+
async sendAgentMessage(
|
|
238
|
+
callerId: string,
|
|
239
|
+
parameters: MessageParameters,
|
|
240
|
+
sourceTurnId: string,
|
|
241
|
+
): Promise<AgentMessageResult> {
|
|
242
|
+
this.assertAccepting();
|
|
243
|
+
this.assertCallerExists(callerId);
|
|
244
|
+
const targetId = this.resolveMessageTarget(callerId, parameters.agent_id);
|
|
245
|
+
try {
|
|
246
|
+
await this.enqueueRecipientDelivery(targetId, async () => {
|
|
247
|
+
await this.deliverExplicitMessage(callerId, targetId, sourceTurnId, parameters.message);
|
|
248
|
+
});
|
|
249
|
+
return { agent_id: targetId, delivered: true };
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return {
|
|
252
|
+
agent_id: targetId,
|
|
253
|
+
delivered: false,
|
|
254
|
+
error: error instanceof Error ? error.message : String(error),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Wait for the exact active turn captured at invocation, or return the latest idle result. */
|
|
260
|
+
wait(
|
|
261
|
+
callerId: string,
|
|
262
|
+
agentId: string,
|
|
263
|
+
timeoutMs?: number,
|
|
264
|
+
signal?: AbortSignal,
|
|
265
|
+
): Promise<TurnResult> {
|
|
266
|
+
this.assertAccepting();
|
|
267
|
+
this.assertCallerExists(callerId);
|
|
268
|
+
this.assertCallerTargetsDirectChild(callerId, agentId, "wait");
|
|
269
|
+
const agent = this.requireUsableAgent(agentId, "wait");
|
|
270
|
+
if (!agent.active_turn_id) {
|
|
271
|
+
if (agent.latest_result) return Promise.resolve(structuredClone(agent.latest_result));
|
|
272
|
+
return Promise.reject(new Error(`Minimal subagents wait: ${agentId} has no turn to observe`));
|
|
273
|
+
}
|
|
274
|
+
const turnId = agent.active_turn_id;
|
|
275
|
+
const key = agentDeliveryKey(agentId, turnId);
|
|
276
|
+
|
|
277
|
+
return new Promise<TurnResult>((resolve, reject) => {
|
|
278
|
+
const waiter: TurnWaiter = { callerId, resolve, reject, abortSignal: signal };
|
|
279
|
+
let turnWaiters = this.waiters.get(key);
|
|
280
|
+
if (!turnWaiters) {
|
|
281
|
+
turnWaiters = new Set();
|
|
282
|
+
this.waiters.set(key, turnWaiters);
|
|
283
|
+
}
|
|
284
|
+
turnWaiters.add(waiter);
|
|
285
|
+
const stopWaiting = (error: Error) => {
|
|
286
|
+
this.removeWaiter(key, waiter);
|
|
287
|
+
reject(error);
|
|
288
|
+
};
|
|
289
|
+
if (timeoutMs !== undefined) {
|
|
290
|
+
waiter.timeout = setTimeout(
|
|
291
|
+
() =>
|
|
292
|
+
stopWaiting(
|
|
293
|
+
new Error(`Minimal subagents wait timed out for ${agentId} after ${timeoutMs}ms`),
|
|
294
|
+
),
|
|
295
|
+
timeoutMs,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
if (signal) {
|
|
299
|
+
waiter.abortListener = () =>
|
|
300
|
+
stopWaiting(new Error(`Minimal subagents wait cancelled for ${agentId}`));
|
|
301
|
+
if (signal.aborted) waiter.abortListener();
|
|
302
|
+
else signal.addEventListener("abort", waiter.abortListener, { once: true });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Return direct-child status authorized for one root or child caller. */
|
|
308
|
+
status(callerId: string, agentId?: string): StatusResult {
|
|
309
|
+
this.assertCallerExists(callerId);
|
|
310
|
+
if (agentId !== undefined) {
|
|
311
|
+
this.assertCallerTargetsDirectChild(callerId, agentId, "status");
|
|
312
|
+
return { agent: this.buildAgentDetail(this.requireAgent(agentId), false) };
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
parent_id: callerId,
|
|
316
|
+
agents: this.childrenOf(callerId).map((agent) => this.buildAgentSummary(agent, false)),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Return the complete root hierarchy for trusted UI and coordinator activity projections. */
|
|
321
|
+
inspectStatus(agentId?: string): HierarchyStatusResult {
|
|
322
|
+
if (agentId !== undefined) return { agent: this.buildAgentDetail(this.requireAgent(agentId)) };
|
|
323
|
+
return {
|
|
324
|
+
root_id: "root",
|
|
325
|
+
agents: this.childrenOf("root").map((agent) => this.buildAgentSummary(agent)),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Report whether root or one explicitly authorized child can create another agent. */
|
|
330
|
+
canAgentSpawn(callerId: string): boolean {
|
|
331
|
+
if (callerId === "root") return true;
|
|
332
|
+
const caller = this.agents.get(callerId);
|
|
333
|
+
return caller
|
|
334
|
+
? canAgentContractSpawn(
|
|
335
|
+
caller.agent_id,
|
|
336
|
+
caller.launch_contract.delegation,
|
|
337
|
+
this.maxSubagentDepth,
|
|
338
|
+
)
|
|
339
|
+
: false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Abort one caller-owned direct child turn, optionally including its subtree. */
|
|
343
|
+
async cancel(callerId: string, agentId: string, recursive = true): Promise<CancelResult> {
|
|
344
|
+
this.assertAccepting();
|
|
345
|
+
this.assertCallerCanManageAgent(callerId, agentId, "cancel");
|
|
346
|
+
const target = this.requireUsableAgent(agentId, "cancel");
|
|
347
|
+
const affected = recursive ? [target, ...this.descendantsOf(agentId)] : [target];
|
|
348
|
+
const cancelledTurnIds: string[] = [];
|
|
349
|
+
for (const agent of affected) {
|
|
350
|
+
const cancelledTurnId = await this.cancelActiveTurn(agent);
|
|
351
|
+
if (cancelledTurnId) cancelledTurnIds.push(cancelledTurnId);
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
agent_id: agentId,
|
|
355
|
+
recursive,
|
|
356
|
+
affected_agent_ids: affected.map((agent) => agent.agent_id),
|
|
357
|
+
cancelled_turn_ids: cancelledTurnIds,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Delete one caller-owned direct child session, optionally including its subtree post-order. */
|
|
362
|
+
async delete(callerId: string, agentId: string, recursive = true): Promise<DeleteResult> {
|
|
363
|
+
this.assertAccepting();
|
|
364
|
+
this.assertCallerCanManageAgent(callerId, agentId, "delete");
|
|
365
|
+
const target = this.requireAgent(agentId);
|
|
366
|
+
const descendants = this.descendantsOf(agentId);
|
|
367
|
+
if (!recursive && descendants.length > 0) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
`Minimal subagents delete: ${agentId} has descendants; use recursive deletion`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const ordered = recursive ? [...descendants].reverse().concat(target) : [target];
|
|
373
|
+
const result: DeleteResult = {
|
|
374
|
+
agent_id: agentId,
|
|
375
|
+
recursive,
|
|
376
|
+
deleted_agent_ids: [],
|
|
377
|
+
tombstoned_agent_ids: [],
|
|
378
|
+
trashed_session_files: [],
|
|
379
|
+
failures: [],
|
|
380
|
+
};
|
|
381
|
+
const failedAncestors = new Set<string>();
|
|
382
|
+
|
|
383
|
+
for (const agent of ordered) {
|
|
384
|
+
if (
|
|
385
|
+
[...failedAncestors].some(
|
|
386
|
+
(failedId) => agent.agent_id === failedId || failedId.startsWith(`${agent.agent_id}.`),
|
|
387
|
+
)
|
|
388
|
+
) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const runtime = this.runtimes.get(agent.agent_id);
|
|
392
|
+
try {
|
|
393
|
+
if (agent.active_turn_id) await this.cancelActiveTurn(agent);
|
|
394
|
+
runtime?.dispose();
|
|
395
|
+
this.runtimes.delete(agent.agent_id);
|
|
396
|
+
if (agent.session_file) {
|
|
397
|
+
await this.dependencies.sessions.trashSessionFile(agent.session_file);
|
|
398
|
+
result.trashed_session_files.push(agent.session_file);
|
|
399
|
+
}
|
|
400
|
+
this.agents.delete(agent.agent_id);
|
|
401
|
+
this.importedMessages.delete(agent.agent_id);
|
|
402
|
+
this.tombstones.add(agent.agent_id);
|
|
403
|
+
result.deleted_agent_ids.push(agent.agent_id);
|
|
404
|
+
result.tombstoned_agent_ids.push(agent.agent_id);
|
|
405
|
+
this.dependencies.registry.append(
|
|
406
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "agent-deleted", {
|
|
407
|
+
agent_ids: [agent.agent_id],
|
|
408
|
+
}),
|
|
409
|
+
);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
failedAncestors.add(agent.agent_id);
|
|
412
|
+
if (runtime && agent.session_file) {
|
|
413
|
+
try {
|
|
414
|
+
this.runtimes.set(
|
|
415
|
+
agent.agent_id,
|
|
416
|
+
await this.dependencies.sessions.restoreRuntime(agent),
|
|
417
|
+
);
|
|
418
|
+
} catch (restoreError) {
|
|
419
|
+
agent.availability = "unavailable";
|
|
420
|
+
agent.unavailable_reason = `Deletion recovery failed: ${
|
|
421
|
+
restoreError instanceof Error ? restoreError.message : String(restoreError)
|
|
422
|
+
}`;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
result.failures.push({
|
|
426
|
+
agent_id: agent.agent_id,
|
|
427
|
+
error: error instanceof Error ? error.message : String(error),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Restore non-deleted descendants, interrupt unfinished work, and reconcile pending successful output. */
|
|
435
|
+
async restore(snapshot: RegistrySnapshot): Promise<void> {
|
|
436
|
+
for (const runtime of this.runtimes.values()) runtime.dispose();
|
|
437
|
+
this.agents.clear();
|
|
438
|
+
this.runtimes.clear();
|
|
439
|
+
this.runtimeInitializations.clear();
|
|
440
|
+
this.importedMessages.clear();
|
|
441
|
+
this.pendingAgentIds.clear();
|
|
442
|
+
this.tombstones.clear();
|
|
443
|
+
this.deliveries.clear();
|
|
444
|
+
this.waiters.clear();
|
|
445
|
+
this.acceptingOperations = true;
|
|
446
|
+
this.shutdownPromise = undefined;
|
|
447
|
+
|
|
448
|
+
for (const agent of snapshot.agents) this.agents.set(agent.agent_id, structuredClone(agent));
|
|
449
|
+
for (const tombstone of snapshot.tombstones) this.tombstones.add(tombstone);
|
|
450
|
+
for (const delivery of snapshot.deliveries) {
|
|
451
|
+
this.deliveries.set(
|
|
452
|
+
agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id),
|
|
453
|
+
structuredClone(delivery),
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
for (const agent of this.agents.values()) {
|
|
458
|
+
if (agent.active_turn_id) {
|
|
459
|
+
const interrupted: TurnResult = {
|
|
460
|
+
agent_id: agent.agent_id,
|
|
461
|
+
turn_id: agent.active_turn_id,
|
|
462
|
+
status: "interrupted",
|
|
463
|
+
output: "",
|
|
464
|
+
error: "Turn interrupted because the owning Pi process exited",
|
|
465
|
+
};
|
|
466
|
+
this.settleTurn(agent, agent.active_turn_id, interrupted);
|
|
467
|
+
this.dependencies.notify?.({
|
|
468
|
+
type: "interruption",
|
|
469
|
+
agentId: agent.agent_id,
|
|
470
|
+
message: `Restored ${agent.agent_id} with an interrupted turn`,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
const previousAvailability = agent.availability;
|
|
474
|
+
const missing = agent.clone_error
|
|
475
|
+
? [agent.clone_error]
|
|
476
|
+
: await this.dependencies.sessions.resolveRestorationMissingDependencies(agent);
|
|
477
|
+
if (missing.length > 0 || !agent.session_file) {
|
|
478
|
+
agent.availability = "unavailable";
|
|
479
|
+
if (previousAvailability !== "unavailable")
|
|
480
|
+
agent.latest_activity_at = this.now().toISOString();
|
|
481
|
+
agent.missing_dependencies = missing.length > 0 ? missing : agent.missing_dependencies;
|
|
482
|
+
agent.unavailable_reason =
|
|
483
|
+
agent.clone_error ??
|
|
484
|
+
(missing.length > 0
|
|
485
|
+
? `Missing dependencies: ${missing.join(", ")}`
|
|
486
|
+
: (agent.unavailable_reason ?? `No persistent session exists for ${agent.agent_id}`));
|
|
487
|
+
this.dependencies.notify?.({
|
|
488
|
+
type: "unavailable",
|
|
489
|
+
agentId: agent.agent_id,
|
|
490
|
+
message: `${agent.agent_id} unavailable: ${agent.unavailable_reason}`,
|
|
491
|
+
});
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
this.runtimes.set(agent.agent_id, await this.dependencies.sessions.restoreRuntime(agent));
|
|
496
|
+
agent.availability = "available";
|
|
497
|
+
if (previousAvailability !== "available")
|
|
498
|
+
agent.latest_activity_at = this.now().toISOString();
|
|
499
|
+
agent.missing_dependencies = [];
|
|
500
|
+
agent.unavailable_reason = undefined;
|
|
501
|
+
this.dependencies.notify?.({
|
|
502
|
+
type: "restoration",
|
|
503
|
+
agentId: agent.agent_id,
|
|
504
|
+
message: `Restored ${agent.agent_id}`,
|
|
505
|
+
});
|
|
506
|
+
} catch (error) {
|
|
507
|
+
agent.availability = "unavailable";
|
|
508
|
+
if (previousAvailability !== "unavailable")
|
|
509
|
+
agent.latest_activity_at = this.now().toISOString();
|
|
510
|
+
agent.unavailable_reason = error instanceof Error ? error.message : String(error);
|
|
511
|
+
this.dependencies.notify?.({
|
|
512
|
+
type: "unavailable",
|
|
513
|
+
agentId: agent.agent_id,
|
|
514
|
+
message: `${agent.agent_id} unavailable: ${agent.unavailable_reason}`,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
await this.reconcileDeliveries(true);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Schedule delivery reconciliation as coordinator-owned work drained during shutdown. */
|
|
522
|
+
scheduleDeliveryReconciliation(replayMissing = false): void {
|
|
523
|
+
this.trackBackgroundOperation(this.reconcileDeliveries(replayMissing));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Reconcile durable destination evidence and replay only successful undelivered output. */
|
|
527
|
+
async reconcileDeliveries(replayMissing = false): Promise<void> {
|
|
528
|
+
for (const delivery of this.deliveries.values()) {
|
|
529
|
+
if (delivery.settled) continue;
|
|
530
|
+
const agent = this.agents.get(delivery.source_agent_id);
|
|
531
|
+
const result = delivery.result ?? agent?.latest_result;
|
|
532
|
+
if (!result || result.status !== "completed" || result.turn_id !== delivery.source_turn_id)
|
|
533
|
+
continue;
|
|
534
|
+
if (this.hasDeliveryEvidence(delivery)) {
|
|
535
|
+
this.settleDelivery(delivery);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (!replayMissing) continue;
|
|
539
|
+
delivery.path = "message";
|
|
540
|
+
this.dependencies.registry.append(
|
|
541
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pending", {
|
|
542
|
+
delivery,
|
|
543
|
+
}),
|
|
544
|
+
);
|
|
545
|
+
await this.deliverAutomaticResult(result, delivery);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Clone complete child leaves for root fork ownership without ever sharing source session paths. */
|
|
550
|
+
async prepareFork(sourceRootSessionFile: string): Promise<ForkSnapshot> {
|
|
551
|
+
this.acceptingOperations = false;
|
|
552
|
+
const activeRootChildren = this.childrenOf("root");
|
|
553
|
+
for (const child of activeRootChildren) await this.cancelDuringShutdown(child.agent_id);
|
|
554
|
+
await Promise.allSettled(this.runtimeInitializations.values());
|
|
555
|
+
await Promise.allSettled(this.backgroundOperations);
|
|
556
|
+
await Promise.allSettled(this.recipientQueues.values());
|
|
557
|
+
const forkAgents: PersistedAgent[] = [];
|
|
558
|
+
const failedSubtrees = new Set<string>();
|
|
559
|
+
|
|
560
|
+
for (const agent of this.agents.values()) {
|
|
561
|
+
const failedAncestor = [...failedSubtrees].find(
|
|
562
|
+
(failedId) => agent.agent_id === failedId || agent.agent_id.startsWith(`${failedId}.`),
|
|
563
|
+
);
|
|
564
|
+
if (failedAncestor) {
|
|
565
|
+
forkAgents.push(
|
|
566
|
+
this.createForkPlaceholder(agent, `Ancestor clone failed: ${failedAncestor}`),
|
|
567
|
+
);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const clone = await this.dependencies.sessions.cloneSession(agent);
|
|
572
|
+
forkAgents.push({
|
|
573
|
+
...structuredClone(agent),
|
|
574
|
+
session_file: clone.sessionFile,
|
|
575
|
+
session_id: clone.sessionId,
|
|
576
|
+
active_turn_id: undefined,
|
|
577
|
+
active_turn_started_at: undefined,
|
|
578
|
+
});
|
|
579
|
+
} catch (error) {
|
|
580
|
+
const cloneError = error instanceof Error ? error.message : String(error);
|
|
581
|
+
failedSubtrees.add(agent.agent_id);
|
|
582
|
+
forkAgents.push(this.createForkPlaceholder(agent, cloneError));
|
|
583
|
+
this.dependencies.notify?.({
|
|
584
|
+
type: "fork-clone-failure",
|
|
585
|
+
agentId: agent.agent_id,
|
|
586
|
+
message: `Fork clone failed for ${agent.agent_id}: ${cloneError}`,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
source_root_session_file: sourceRootSessionFile,
|
|
593
|
+
agents: forkAgents,
|
|
594
|
+
tombstones: [...this.tombstones],
|
|
595
|
+
deliveries: [...this.deliveries.values()].map((delivery) => structuredClone(delivery)),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Stop new operations and idempotently cancel and dispose every child runtime. */
|
|
600
|
+
shutdown(): Promise<void> {
|
|
601
|
+
if (this.shutdownPromise) return this.shutdownPromise;
|
|
602
|
+
this.acceptingOperations = false;
|
|
603
|
+
this.shutdownPromise = this.finishShutdown();
|
|
604
|
+
return this.shutdownPromise;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Let dynamic coordinator work settle before stopping acceptance and disposing child runtimes. */
|
|
608
|
+
shutdownAfterSettling(): Promise<void> {
|
|
609
|
+
if (this.shutdownPromise) return this.shutdownPromise;
|
|
610
|
+
if (!this.hasPendingOperations()) {
|
|
611
|
+
this.acceptingOperations = false;
|
|
612
|
+
this.shutdownPromise = this.finishShutdown();
|
|
613
|
+
return this.shutdownPromise;
|
|
614
|
+
}
|
|
615
|
+
this.shutdownPromise = (async () => {
|
|
616
|
+
await this.waitForSettledOperations();
|
|
617
|
+
this.acceptingOperations = false;
|
|
618
|
+
await this.finishShutdown();
|
|
619
|
+
})();
|
|
620
|
+
return this.shutdownPromise;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Wait until active turns, initialization, delivery, and recipient queues are all settled. */
|
|
624
|
+
async waitForSettledOperations(): Promise<void> {
|
|
625
|
+
while (this.hasPendingOperations()) {
|
|
626
|
+
const pending = [
|
|
627
|
+
...this.runtimeInitializations.values(),
|
|
628
|
+
...this.backgroundOperations,
|
|
629
|
+
...this.recipientQueues.values(),
|
|
630
|
+
];
|
|
631
|
+
if (pending.length === 0) {
|
|
632
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
await Promise.race(
|
|
636
|
+
pending.map((operation) =>
|
|
637
|
+
operation.then(
|
|
638
|
+
() => undefined,
|
|
639
|
+
() => undefined,
|
|
640
|
+
),
|
|
641
|
+
),
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private hasPendingOperations(): boolean {
|
|
647
|
+
return (
|
|
648
|
+
[...this.agents.values()].some((agent) => agent.active_turn_id !== undefined) ||
|
|
649
|
+
this.runtimeInitializations.size > 0 ||
|
|
650
|
+
this.backgroundOperations.size > 0 ||
|
|
651
|
+
this.recipientQueues.size > 0
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
private async finishShutdown(): Promise<void> {
|
|
656
|
+
const roots = this.childrenOf("root");
|
|
657
|
+
for (const child of roots) {
|
|
658
|
+
if (this.agents.has(child.agent_id)) await this.cancelDuringShutdown(child.agent_id);
|
|
659
|
+
}
|
|
660
|
+
await Promise.allSettled(this.runtimeInitializations.values());
|
|
661
|
+
await Promise.allSettled(this.backgroundOperations);
|
|
662
|
+
await Promise.allSettled(this.recipientQueues.values());
|
|
663
|
+
for (const runtime of this.runtimes.values()) runtime.dispose();
|
|
664
|
+
this.runtimes.clear();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private async initializeAndRunPrompt(
|
|
668
|
+
agentId: string,
|
|
669
|
+
turnId: string,
|
|
670
|
+
task: string,
|
|
671
|
+
compact: boolean,
|
|
672
|
+
callerModel: string,
|
|
673
|
+
callerThinkingLevel: CallerSnapshot["thinkingLevel"],
|
|
674
|
+
): Promise<void> {
|
|
675
|
+
const agent = this.agents.get(agentId);
|
|
676
|
+
if (!agent) return;
|
|
677
|
+
try {
|
|
678
|
+
const runtime = await this.ensureRuntime(agent);
|
|
679
|
+
if (this.agents.get(agentId) !== agent || agent.active_turn_id !== turnId) {
|
|
680
|
+
if (!this.agents.has(agentId) || !this.acceptingOperations) {
|
|
681
|
+
runtime.dispose();
|
|
682
|
+
this.runtimes.delete(agentId);
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const outcome = await runtime.runPrompt(task, compact, callerModel, callerThinkingLevel);
|
|
687
|
+
if (agent.active_turn_id !== turnId) return;
|
|
688
|
+
this.settleTurn(agent, turnId, terminalTurnResult(agentId, turnId, outcome));
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (this.agents.get(agentId) !== agent || agent.active_turn_id !== turnId) return;
|
|
691
|
+
this.settleTurn(agent, turnId, {
|
|
692
|
+
agent_id: agentId,
|
|
693
|
+
turn_id: turnId,
|
|
694
|
+
status: "failed",
|
|
695
|
+
output: "",
|
|
696
|
+
error: error instanceof Error ? error.message : String(error),
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private ensureRuntime(agent: PersistedAgent): Promise<ChildAgentRuntime> {
|
|
702
|
+
const current = this.runtimes.get(agent.agent_id);
|
|
703
|
+
if (current) return Promise.resolve(current);
|
|
704
|
+
const initializing = this.runtimeInitializations.get(agent.agent_id);
|
|
705
|
+
if (initializing) return initializing;
|
|
706
|
+
if (agent.clone_error || !agent.session_file || !agent.session_id) {
|
|
707
|
+
return Promise.reject(
|
|
708
|
+
new Error(agent.clone_error ?? `No persistent session exists for ${agent.agent_id}`),
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
const importedMessages = this.importedMessages.get(agent.agent_id);
|
|
712
|
+
const initialization = (
|
|
713
|
+
importedMessages
|
|
714
|
+
? this.dependencies.sessions.createRuntime({ agent, importedMessages })
|
|
715
|
+
: this.dependencies.sessions.restoreRuntime(agent)
|
|
716
|
+
)
|
|
717
|
+
.then((runtime) => {
|
|
718
|
+
if (this.agents.get(agent.agent_id) !== agent) {
|
|
719
|
+
runtime.dispose();
|
|
720
|
+
throw new Error(`Minimal subagents runtime replaced while opening ${agent.agent_id}`);
|
|
721
|
+
}
|
|
722
|
+
this.runtimes.set(agent.agent_id, runtime);
|
|
723
|
+
this.importedMessages.delete(agent.agent_id);
|
|
724
|
+
return runtime;
|
|
725
|
+
})
|
|
726
|
+
.finally(() => {
|
|
727
|
+
if (this.runtimeInitializations.get(agent.agent_id) === initialization) {
|
|
728
|
+
this.runtimeInitializations.delete(agent.agent_id);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
this.runtimeInitializations.set(agent.agent_id, initialization);
|
|
732
|
+
return initialization;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
private beginTurn(agent: PersistedAgent): TurnId {
|
|
736
|
+
const turnId = `${agent.agent_id}:turn-${randomUUID()}` as TurnId;
|
|
737
|
+
const startedAt = this.now().toISOString();
|
|
738
|
+
agent.active_turn_id = turnId;
|
|
739
|
+
agent.active_turn_started_at = startedAt;
|
|
740
|
+
agent.latest_activity_at = startedAt;
|
|
741
|
+
this.dependencies.registry.append(
|
|
742
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "turn-started", {
|
|
743
|
+
agent_id: agent.agent_id,
|
|
744
|
+
turn_id: turnId,
|
|
745
|
+
started_at: startedAt,
|
|
746
|
+
}),
|
|
747
|
+
);
|
|
748
|
+
return turnId;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private settleTurn(agent: PersistedAgent, turnId: string, result: TurnResult): void {
|
|
752
|
+
if (agent.active_turn_id !== turnId) return;
|
|
753
|
+
const settledAt = this.now();
|
|
754
|
+
const startedAt = agent.active_turn_started_at
|
|
755
|
+
? new Date(agent.active_turn_started_at).getTime()
|
|
756
|
+
: Number.NaN;
|
|
757
|
+
result = {
|
|
758
|
+
...result,
|
|
759
|
+
elapsed_ms:
|
|
760
|
+
result.elapsed_ms ??
|
|
761
|
+
(Number.isFinite(startedAt) ? Math.max(0, settledAt.getTime() - startedAt) : undefined),
|
|
762
|
+
};
|
|
763
|
+
agent.active_turn_id = undefined;
|
|
764
|
+
agent.active_turn_started_at = undefined;
|
|
765
|
+
agent.latest_activity_at = settledAt.toISOString();
|
|
766
|
+
agent.latest_result = structuredClone(result);
|
|
767
|
+
this.dependencies.registry.append(
|
|
768
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "turn-settled", { result }),
|
|
769
|
+
);
|
|
770
|
+
const waiterKey = agentDeliveryKey(agent.agent_id, turnId);
|
|
771
|
+
const turnWaiters = this.waiters.get(waiterKey);
|
|
772
|
+
const directParentWaited = [...(turnWaiters ?? [])].some(
|
|
773
|
+
(waiter) => waiter.callerId === agent.parent_id,
|
|
774
|
+
);
|
|
775
|
+
for (const waiter of turnWaiters ?? []) {
|
|
776
|
+
this.removeWaiter(waiterKey, waiter);
|
|
777
|
+
waiter.resolve(structuredClone(result));
|
|
778
|
+
}
|
|
779
|
+
if (result.status === "completed") {
|
|
780
|
+
const delivery: PersistedDelivery = {
|
|
781
|
+
source_agent_id: agent.agent_id,
|
|
782
|
+
source_turn_id: turnId,
|
|
783
|
+
destination_agent_id: agent.parent_id,
|
|
784
|
+
path: directParentWaited ? "wait" : "message",
|
|
785
|
+
settled: false,
|
|
786
|
+
result: structuredClone(result),
|
|
787
|
+
};
|
|
788
|
+
this.deliveries.set(waiterKey, delivery);
|
|
789
|
+
this.dependencies.registry.append(
|
|
790
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-pending", {
|
|
791
|
+
delivery,
|
|
792
|
+
}),
|
|
793
|
+
);
|
|
794
|
+
if (!directParentWaited) {
|
|
795
|
+
this.trackBackgroundOperation(this.deliverAutomaticResult(result, delivery));
|
|
796
|
+
}
|
|
797
|
+
this.dependencies.notify?.({
|
|
798
|
+
type: "completion",
|
|
799
|
+
agentId: agent.agent_id,
|
|
800
|
+
message: `${agent.agent_id} completed`,
|
|
801
|
+
});
|
|
802
|
+
} else if (result.status === "failed") {
|
|
803
|
+
this.dependencies.notify?.({
|
|
804
|
+
type: "failure",
|
|
805
|
+
agentId: agent.agent_id,
|
|
806
|
+
message: `${agent.agent_id} failed: ${result.error ?? "unknown error"}`,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private async deliverAutomaticResult(
|
|
812
|
+
result: TurnResult,
|
|
813
|
+
delivery: PersistedDelivery,
|
|
814
|
+
): Promise<void> {
|
|
815
|
+
const graceMs =
|
|
816
|
+
this.dependencies.automaticDeliveryGraceMs ?? DEFAULT_AUTOMATIC_DELIVERY_GRACE_MS;
|
|
817
|
+
if (graceMs > 0) await new Promise((resolve) => setTimeout(resolve, graceMs));
|
|
818
|
+
if (delivery.settled) return;
|
|
819
|
+
if (this.hasDeliveryEvidence(delivery)) {
|
|
820
|
+
this.settleDelivery(delivery);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
await this.enqueueRecipientDelivery(delivery.destination_agent_id, async () => {
|
|
825
|
+
const message: CoordinatorMessage = {
|
|
826
|
+
customType: "minimal-subagents.result",
|
|
827
|
+
content: result.output,
|
|
828
|
+
details: {
|
|
829
|
+
source_agent_id: delivery.source_agent_id,
|
|
830
|
+
destination_agent_id: delivery.destination_agent_id,
|
|
831
|
+
source_turn_id: result.turn_id,
|
|
832
|
+
status: result.status,
|
|
833
|
+
elapsed_ms: result.elapsed_ms,
|
|
834
|
+
usage: result.usage,
|
|
835
|
+
},
|
|
836
|
+
};
|
|
837
|
+
await this.deliverToRecipient(delivery.destination_agent_id, message);
|
|
838
|
+
});
|
|
839
|
+
} catch (error) {
|
|
840
|
+
delivery.error = error instanceof Error ? error.message : String(error);
|
|
841
|
+
this.dependencies.registry.append(
|
|
842
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-settled", {
|
|
843
|
+
source_agent_id: delivery.source_agent_id,
|
|
844
|
+
source_turn_id: delivery.source_turn_id,
|
|
845
|
+
error: delivery.error,
|
|
846
|
+
}),
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private async deliverExplicitMessage(
|
|
852
|
+
callerId: string,
|
|
853
|
+
targetId: string,
|
|
854
|
+
sourceTurnId: string,
|
|
855
|
+
content: string,
|
|
856
|
+
): Promise<void> {
|
|
857
|
+
const message: CoordinatorMessage = {
|
|
858
|
+
customType: "minimal-subagents.message",
|
|
859
|
+
content,
|
|
860
|
+
details: {
|
|
861
|
+
source_agent_id: callerId,
|
|
862
|
+
destination_agent_id: targetId,
|
|
863
|
+
source_turn_id: sourceTurnId,
|
|
864
|
+
},
|
|
865
|
+
};
|
|
866
|
+
if (targetId !== "root") {
|
|
867
|
+
const target = this.requireUsableAgent(targetId, "message");
|
|
868
|
+
target.recent_messages.push({
|
|
869
|
+
source_agent_id: callerId,
|
|
870
|
+
turn_id: sourceTurnId,
|
|
871
|
+
content,
|
|
872
|
+
});
|
|
873
|
+
if (target.recent_messages.length > RECENT_MESSAGE_LIMIT) target.recent_messages.shift();
|
|
874
|
+
}
|
|
875
|
+
await this.deliverToRecipient(targetId, message);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
private async deliverToRecipient(targetId: string, message: CoordinatorMessage): Promise<void> {
|
|
879
|
+
if (!this.acceptingOperations) {
|
|
880
|
+
throw new Error("Minimal subagents delivery stopped during coordinator shutdown");
|
|
881
|
+
}
|
|
882
|
+
if (targetId === "root") {
|
|
883
|
+
await this.dependencies.root.steerCoordinatorMessage(message);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const target = this.requireUsableAgent(targetId, "message");
|
|
887
|
+
const runtime = await this.ensureRuntime(target);
|
|
888
|
+
if (target.active_turn_id || runtime.isRunning) {
|
|
889
|
+
await runtime.steerCoordinatorMessage(message);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const turnId = this.beginTurn(target);
|
|
893
|
+
const runMessage = runtime
|
|
894
|
+
.runMessage(message)
|
|
895
|
+
.then((outcome) => {
|
|
896
|
+
if (target.active_turn_id === turnId) {
|
|
897
|
+
this.settleTurn(target, turnId, terminalTurnResult(target.agent_id, turnId, outcome));
|
|
898
|
+
}
|
|
899
|
+
})
|
|
900
|
+
.catch((error: unknown) => {
|
|
901
|
+
if (target.active_turn_id === turnId) {
|
|
902
|
+
this.settleTurn(target, turnId, {
|
|
903
|
+
agent_id: target.agent_id,
|
|
904
|
+
turn_id: turnId,
|
|
905
|
+
status: "failed",
|
|
906
|
+
output: "",
|
|
907
|
+
error: error instanceof Error ? error.message : String(error),
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
this.trackBackgroundOperation(runMessage);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private trackBackgroundOperation(operation: Promise<void>): void {
|
|
915
|
+
this.backgroundOperations.add(operation);
|
|
916
|
+
const cleanup = () => this.backgroundOperations.delete(operation);
|
|
917
|
+
void operation.then(cleanup, cleanup);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private enqueueRecipientDelivery(
|
|
921
|
+
targetId: string,
|
|
922
|
+
operation: () => Promise<void>,
|
|
923
|
+
): Promise<void> {
|
|
924
|
+
const previous = this.recipientQueues.get(targetId) ?? Promise.resolve();
|
|
925
|
+
const next = previous.catch(() => undefined).then(operation);
|
|
926
|
+
this.recipientQueues.set(targetId, next);
|
|
927
|
+
const cleanup = () => {
|
|
928
|
+
if (this.recipientQueues.get(targetId) === next) this.recipientQueues.delete(targetId);
|
|
929
|
+
};
|
|
930
|
+
void next.then(cleanup, cleanup);
|
|
931
|
+
return next;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
private buildChildAgentId(parentId: string, friendlyId: string): string {
|
|
935
|
+
if (parentId !== "root") return `${parentId}.${friendlyId}`;
|
|
936
|
+
const usesLegacyRootPrefix =
|
|
937
|
+
[...this.agents.values()].some(
|
|
938
|
+
(agent) => agent.parent_id === "root" && agent.agent_id.startsWith("root."),
|
|
939
|
+
) || [...this.tombstones].some((agentId) => agentId.startsWith("root."));
|
|
940
|
+
return usesLegacyRootPrefix ? `root.${friendlyId}` : friendlyId;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
private resolveMessageTarget(callerId: string, target?: string): string {
|
|
944
|
+
const resolved = target ?? (callerId === "root" ? undefined : "parent");
|
|
945
|
+
if (!resolved) throw new Error("Minimal subagents message: root caller must specify agent_id");
|
|
946
|
+
if (resolved === "*") throw new Error('Minimal subagents message target "*" is unsupported');
|
|
947
|
+
const caller = callerId === "root" ? undefined : this.requireAgent(callerId);
|
|
948
|
+
const targetId = resolved === "parent" ? caller?.parent_id : resolved;
|
|
949
|
+
if (!targetId) throw new Error("Minimal subagents message: root has no parent");
|
|
950
|
+
const targetAgent = targetId === "root" ? undefined : this.requireAgent(targetId);
|
|
951
|
+
const isDirectParent = caller?.parent_id === targetId;
|
|
952
|
+
const isDirectSibling =
|
|
953
|
+
caller !== undefined && targetAgent?.parent_id === caller.parent_id && targetId !== callerId;
|
|
954
|
+
const isDirectChild = targetAgent?.parent_id === callerId;
|
|
955
|
+
if (!isDirectParent && !isDirectSibling && !isDirectChild) {
|
|
956
|
+
throw new Error(
|
|
957
|
+
`Minimal subagents message authorization denied: ${callerId} cannot message ${targetId}`,
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
return targetId;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
private hasDeliveryEvidence(delivery: PersistedDelivery): boolean {
|
|
964
|
+
if (delivery.destination_agent_id === "root") {
|
|
965
|
+
return this.dependencies.root.hasDeliveryEvidence(
|
|
966
|
+
delivery.source_agent_id,
|
|
967
|
+
delivery.source_turn_id,
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
return (
|
|
971
|
+
this.runtimes
|
|
972
|
+
.get(delivery.destination_agent_id)
|
|
973
|
+
?.hasDeliveryEvidence(delivery.source_agent_id, delivery.source_turn_id) ?? false
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
private settleDelivery(delivery: PersistedDelivery): void {
|
|
978
|
+
delivery.settled = true;
|
|
979
|
+
delivery.error = undefined;
|
|
980
|
+
this.dependencies.registry.append(
|
|
981
|
+
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-settled", {
|
|
982
|
+
source_agent_id: delivery.source_agent_id,
|
|
983
|
+
source_turn_id: delivery.source_turn_id,
|
|
984
|
+
}),
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
private buildAgentSummary(agent: PersistedAgent, includeDescendants = true): AgentSummary {
|
|
989
|
+
const directChildren = this.childrenOf(agent.agent_id);
|
|
990
|
+
const children = includeDescendants
|
|
991
|
+
? directChildren.map((child) => this.buildAgentSummary(child))
|
|
992
|
+
: [];
|
|
993
|
+
const elapsed = agent.active_turn_started_at
|
|
994
|
+
? Math.max(0, this.now().getTime() - new Date(agent.active_turn_started_at).getTime())
|
|
995
|
+
: undefined;
|
|
996
|
+
return {
|
|
997
|
+
agent_id: agent.agent_id,
|
|
998
|
+
parent_id: agent.parent_id,
|
|
999
|
+
state: agent.active_turn_id ? "running" : "idle",
|
|
1000
|
+
availability: agent.availability,
|
|
1001
|
+
active_turn_id: agent.active_turn_id,
|
|
1002
|
+
latest_turn: agent.latest_result
|
|
1003
|
+
? { turn_id: agent.latest_result.turn_id, status: agent.latest_result.status }
|
|
1004
|
+
: undefined,
|
|
1005
|
+
model: agent.launch_contract.model,
|
|
1006
|
+
thinking_level: agent.launch_contract.thinking_level,
|
|
1007
|
+
tools: [...agent.launch_contract.ordinary_tools],
|
|
1008
|
+
elapsed_ms: elapsed ?? agent.latest_result?.elapsed_ms,
|
|
1009
|
+
latest_activity_at: agent.latest_activity_at ?? agent.created_at,
|
|
1010
|
+
task: agent.task,
|
|
1011
|
+
latest_activity: agent.active_turn_id
|
|
1012
|
+
? "turn running"
|
|
1013
|
+
: agent.latest_result
|
|
1014
|
+
? `turn ${agent.latest_result.status}`
|
|
1015
|
+
: "created",
|
|
1016
|
+
child_count: directChildren.length,
|
|
1017
|
+
children,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private buildAgentDetail(agent: PersistedAgent, includeDescendants = true): AgentDetail {
|
|
1022
|
+
const summary = this.buildAgentSummary(agent, includeDescendants);
|
|
1023
|
+
const runtimeUsage = this.runtimes.get(agent.agent_id)?.getUsage();
|
|
1024
|
+
return {
|
|
1025
|
+
...summary,
|
|
1026
|
+
session_file: agent.session_file,
|
|
1027
|
+
launch_contract: structuredClone(agent.launch_contract) as unknown as Record<string, unknown>,
|
|
1028
|
+
capability_ceiling: [...agent.capability_ceiling],
|
|
1029
|
+
spawn_entry_id: agent.spawn_entry_id,
|
|
1030
|
+
recent_messages: structuredClone(agent.recent_messages),
|
|
1031
|
+
latest_result: agent.latest_result ? structuredClone(agent.latest_result) : undefined,
|
|
1032
|
+
missing_dependencies: [...agent.missing_dependencies],
|
|
1033
|
+
unavailable_reason: agent.unavailable_reason,
|
|
1034
|
+
usage: runtimeUsage ?? agent.latest_result?.usage,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
private descendantsOf(agentId: string): PersistedAgent[] {
|
|
1039
|
+
const descendants: PersistedAgent[] = [];
|
|
1040
|
+
const queue = this.childrenOf(agentId);
|
|
1041
|
+
while (queue.length > 0) {
|
|
1042
|
+
const agent = queue.shift()!;
|
|
1043
|
+
descendants.push(agent);
|
|
1044
|
+
queue.push(...this.childrenOf(agent.agent_id));
|
|
1045
|
+
}
|
|
1046
|
+
return descendants;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
private childrenOf(parentId: string): PersistedAgent[] {
|
|
1050
|
+
return [...this.agents.values()].filter((agent) => agent.parent_id === parentId);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
private requireAgent(agentId: string): PersistedAgent {
|
|
1054
|
+
if (agentId === "root") throw new Error("Minimal subagents management target cannot be root");
|
|
1055
|
+
const agent = this.agents.get(agentId);
|
|
1056
|
+
if (!agent) throw new Error(`Minimal subagents unknown agent: ${agentId}`);
|
|
1057
|
+
return agent;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
private requireUsableAgent(agentId: string, operation: string): PersistedAgent {
|
|
1061
|
+
const agent = this.requireAgent(agentId);
|
|
1062
|
+
if (agent.availability === "unavailable") {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
agent.unavailable_reason ?? `Minimal subagents ${operation}: ${agentId} is unavailable`,
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
if (agent.clone_error || !agent.session_file) {
|
|
1068
|
+
throw new Error(
|
|
1069
|
+
`Minimal subagents ${operation}: ${agent.clone_error ?? `${agentId} has no child session`}`,
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
return agent;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
private assertCallerExists(callerId: string): void {
|
|
1076
|
+
if (callerId === "root") return;
|
|
1077
|
+
this.requireUsableAgent(callerId, "caller");
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
private assertCallerMaySpawn(callerId: string): void {
|
|
1081
|
+
if (callerId === "root") return;
|
|
1082
|
+
const depth = getSubagentDepth(callerId);
|
|
1083
|
+
if (depth >= this.maxSubagentDepth) {
|
|
1084
|
+
throw new Error(
|
|
1085
|
+
`Minimal subagents maximum delegation depth reached: ${callerId} (depth ${depth}, max ${this.maxSubagentDepth})`,
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
const caller = this.requireUsableAgent(callerId, "delegation");
|
|
1089
|
+
if (caller.launch_contract.delegation !== "fanout") {
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
`Minimal subagents delegation denied: ${callerId} is not authorized for fanout`,
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
private validateFriendlyId(friendlyId: string): void {
|
|
1097
|
+
if (!FRIENDLY_AGENT_ID_PATTERN.test(friendlyId) || RESERVED_AGENT_IDS.has(friendlyId)) {
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`Minimal subagents invalid friendly agent ID: ${JSON.stringify(friendlyId)}; expected ${FRIENDLY_AGENT_ID_PATTERN.source}`,
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private generateFriendlyId(parentId: string): string {
|
|
1105
|
+
let index = 1;
|
|
1106
|
+
while (true) {
|
|
1107
|
+
const friendlyId = `agent-${index}`;
|
|
1108
|
+
const agentId = this.buildChildAgentId(parentId, friendlyId);
|
|
1109
|
+
if (
|
|
1110
|
+
!this.agents.has(agentId) &&
|
|
1111
|
+
!this.pendingAgentIds.has(agentId) &&
|
|
1112
|
+
!this.tombstones.has(agentId)
|
|
1113
|
+
) {
|
|
1114
|
+
return friendlyId;
|
|
1115
|
+
}
|
|
1116
|
+
index++;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
private createForkPlaceholder(agent: PersistedAgent, cloneError: string): PersistedAgent {
|
|
1121
|
+
return {
|
|
1122
|
+
...structuredClone(agent),
|
|
1123
|
+
session_file: undefined,
|
|
1124
|
+
session_id: undefined,
|
|
1125
|
+
clone_error: cloneError,
|
|
1126
|
+
active_turn_id: undefined,
|
|
1127
|
+
active_turn_started_at: undefined,
|
|
1128
|
+
latest_activity_at: this.now().toISOString(),
|
|
1129
|
+
availability: "unavailable",
|
|
1130
|
+
missing_dependencies: [cloneError],
|
|
1131
|
+
unavailable_reason: cloneError,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
private removeWaiter(key: string, waiter: TurnWaiter): void {
|
|
1136
|
+
const turnWaiters = this.waiters.get(key);
|
|
1137
|
+
turnWaiters?.delete(waiter);
|
|
1138
|
+
if (turnWaiters?.size === 0) this.waiters.delete(key);
|
|
1139
|
+
if (waiter.timeout) clearTimeout(waiter.timeout);
|
|
1140
|
+
if (waiter.abortSignal && waiter.abortListener) {
|
|
1141
|
+
waiter.abortSignal.removeEventListener("abort", waiter.abortListener);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
private async cancelDuringShutdown(agentId: string): Promise<void> {
|
|
1146
|
+
const target = this.agents.get(agentId);
|
|
1147
|
+
if (!target) return;
|
|
1148
|
+
const affected = [target, ...this.descendantsOf(agentId)];
|
|
1149
|
+
for (const agent of affected) {
|
|
1150
|
+
if (!agent.active_turn_id) continue;
|
|
1151
|
+
const turnId = agent.active_turn_id;
|
|
1152
|
+
const runtime = this.runtimes.get(agent.agent_id);
|
|
1153
|
+
if (runtime) await runtime.abort();
|
|
1154
|
+
this.settleTurn(agent, turnId, {
|
|
1155
|
+
agent_id: agent.agent_id,
|
|
1156
|
+
turn_id: turnId,
|
|
1157
|
+
status: "cancelled",
|
|
1158
|
+
output: "",
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
private async cancelActiveTurn(agent: PersistedAgent): Promise<string | undefined> {
|
|
1164
|
+
if (!agent.active_turn_id) return undefined;
|
|
1165
|
+
const turnId = agent.active_turn_id;
|
|
1166
|
+
const runtime = this.runtimes.get(agent.agent_id);
|
|
1167
|
+
if (runtime) await runtime.abort();
|
|
1168
|
+
this.settleTurn(agent, turnId, {
|
|
1169
|
+
agent_id: agent.agent_id,
|
|
1170
|
+
turn_id: turnId,
|
|
1171
|
+
status: "cancelled",
|
|
1172
|
+
output: "",
|
|
1173
|
+
});
|
|
1174
|
+
this.dependencies.notify?.({
|
|
1175
|
+
type: "cancellation",
|
|
1176
|
+
agentId: agent.agent_id,
|
|
1177
|
+
message: `Cancelled ${agent.agent_id}`,
|
|
1178
|
+
});
|
|
1179
|
+
return turnId;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
private assertCallerTargetsDirectChild(
|
|
1183
|
+
callerId: string,
|
|
1184
|
+
targetAgentId: string,
|
|
1185
|
+
operation: "wait" | "status" | "cancel" | "delete",
|
|
1186
|
+
): void {
|
|
1187
|
+
if (targetAgentId === "root") {
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
`Minimal subagents ${operation} authorization denied: ${callerId} cannot target ${targetAgentId}`,
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
const target = this.requireAgent(targetAgentId);
|
|
1193
|
+
if (target.parent_id === callerId) return;
|
|
1194
|
+
throw new Error(
|
|
1195
|
+
`Minimal subagents ${operation} authorization denied: ${callerId} cannot target ${targetAgentId}`,
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
private assertCallerCanManageAgent(
|
|
1200
|
+
callerId: string,
|
|
1201
|
+
targetAgentId: string,
|
|
1202
|
+
operation: "cancel" | "delete",
|
|
1203
|
+
): void {
|
|
1204
|
+
this.assertCallerTargetsDirectChild(callerId, targetAgentId, operation);
|
|
1205
|
+
if (callerId === "root") return;
|
|
1206
|
+
const caller = this.agents.get(callerId);
|
|
1207
|
+
if (
|
|
1208
|
+
caller &&
|
|
1209
|
+
canAgentContractSpawn(
|
|
1210
|
+
caller.agent_id,
|
|
1211
|
+
caller.launch_contract.delegation,
|
|
1212
|
+
this.maxSubagentDepth,
|
|
1213
|
+
)
|
|
1214
|
+
) {
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
throw new Error(
|
|
1218
|
+
`Minimal subagents ${operation} authorization denied: ${callerId} cannot target ${targetAgentId}`,
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
private assertAccepting(): void {
|
|
1223
|
+
if (!this.acceptingOperations)
|
|
1224
|
+
throw new Error("Minimal subagents coordinator is shutting down");
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
private now(): Date {
|
|
1228
|
+
return this.dependencies.now?.() ?? new Date();
|
|
1229
|
+
}
|
|
1230
|
+
}
|