@narumitw/pi-subagents 1.0.1 → 2.0.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 +198 -188
- package/package.json +8 -8
- package/src/agents/built-ins.ts +13 -66
- package/src/agents/catalog.ts +19 -2
- package/src/agents/discovery.ts +31 -15
- package/src/auto-transport.ts +7 -1
- package/src/child-peer-bridge.ts +124 -0
- package/src/child-peer-tools.ts +132 -0
- package/src/completion-delivery.ts +19 -5
- package/src/completion-render.ts +189 -0
- package/src/completion-routing.ts +24 -0
- package/src/config-registration.ts +29 -4
- package/src/config-ui.ts +11 -17
- package/src/consult-registration.ts +3 -2
- package/src/consult-render.ts +1 -1
- package/src/create-stateful-transport.ts +15 -2
- package/src/execution-ui.ts +0 -72
- package/src/in-process-transport.ts +39 -7
- package/src/inspect-tool.ts +3 -1
- package/src/panel-planning.ts +2 -2
- package/src/panel-presets.ts +3 -0
- package/src/params.ts +1 -1
- package/src/peer-communication.ts +352 -0
- package/src/peer-transport.ts +49 -0
- package/src/persistence.ts +26 -1
- package/src/pi-args.ts +2 -0
- package/src/registry-types.ts +7 -0
- package/src/registry.ts +240 -41
- package/src/render.ts +2 -41
- package/src/result-contract.ts +20 -5
- package/src/rpc-transport.ts +56 -26
- package/src/runner.ts +13 -1
- package/src/settings.ts +4 -1
- package/src/spawn-idempotency.ts +2 -0
- package/src/stateful-agent-view.ts +3 -1
- package/src/stateful-guidance.ts +11 -11
- package/src/stateful-safety.ts +0 -45
- package/src/stateful-tool-params.ts +11 -3
- package/src/stateful.ts +359 -136
- package/src/subagents.ts +7 -9
- package/src/subprocess-transport.ts +49 -28
- package/src/task-path.ts +65 -0
- package/src/transport-ui.ts +0 -6
- package/src/transport.ts +2 -1
- package/src/usage-format.ts +42 -0
- package/src/workflow-ui.ts +4 -4
- package/src/automation-contract.ts +0 -709
- package/src/automation-planner.ts +0 -65
- package/src/automation-registration.ts +0 -137
- package/src/automation-tool.ts +0 -40
- package/src/automation.ts +0 -435
- package/src/execution-profiles.ts +0 -95
- package/src/workflow-plan-compiler.ts +0 -618
- package/src/workflow-plan-patch.ts +0 -636
- package/src/workflow-planning-benchmark.ts +0 -95
package/src/registry-types.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface AgentTurn {
|
|
|
36
36
|
|
|
37
37
|
export interface PersistedAgentCompletion {
|
|
38
38
|
completionId: string;
|
|
39
|
+
recipientId?: string;
|
|
40
|
+
recipientPath?: string;
|
|
39
41
|
runId: string;
|
|
40
42
|
generation: number;
|
|
41
43
|
task: string;
|
|
@@ -52,10 +54,13 @@ export interface AgentMailboxMessage {
|
|
|
52
54
|
createdAt: number;
|
|
53
55
|
readAt?: number;
|
|
54
56
|
deduplicationKey?: string;
|
|
57
|
+
completionId?: string;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
export interface ManagedAgent {
|
|
58
61
|
id: string;
|
|
62
|
+
taskName?: string;
|
|
63
|
+
taskPath?: string;
|
|
59
64
|
agent: string;
|
|
60
65
|
parentId?: string;
|
|
61
66
|
rootId: string;
|
|
@@ -108,6 +113,8 @@ export interface ManagedAgent {
|
|
|
108
113
|
|
|
109
114
|
export interface AgentRunInspectionSummary {
|
|
110
115
|
id: string;
|
|
116
|
+
taskName?: string;
|
|
117
|
+
taskPath?: string;
|
|
111
118
|
agent: string;
|
|
112
119
|
state: AgentLifecycleState;
|
|
113
120
|
createdAt: number;
|
package/src/registry.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isCapabilityGrantActive,
|
|
11
11
|
revokeCapabilityGrant,
|
|
12
12
|
} from "./capability-grant.js";
|
|
13
|
+
import { resolveCompletionRecipient } from "./completion-routing.js";
|
|
13
14
|
import type { TargetPolicyAudit } from "./cwd-policy.js";
|
|
14
15
|
import type { DelegationContract } from "./delegation-contract.js";
|
|
15
16
|
import {
|
|
@@ -40,8 +41,17 @@ import {
|
|
|
40
41
|
parseAnyStructuredSubagentResult,
|
|
41
42
|
type SubagentResultFormat,
|
|
42
43
|
} from "./result-contract.js";
|
|
44
|
+
import { safeTerminalLine } from "./safe-text.js";
|
|
43
45
|
import type { SemanticCompatibility, SemanticSnapshot } from "./semantic-snapshot.js";
|
|
44
46
|
import { resolveStatefulLimits } from "./stateful-limits.js";
|
|
47
|
+
import {
|
|
48
|
+
deriveTaskName,
|
|
49
|
+
joinTaskPath,
|
|
50
|
+
ROOT_TASK_PATH,
|
|
51
|
+
resolveTaskPath,
|
|
52
|
+
validateTaskName,
|
|
53
|
+
validateTaskPath,
|
|
54
|
+
} from "./task-path.js";
|
|
45
55
|
import { copyTurnTerminationReport } from "./timeout-checkpoint.js";
|
|
46
56
|
import { type AgentTurnRunner, normalizeTransport, type SubagentTransport } from "./transport.js";
|
|
47
57
|
import type { TransportTelemetry } from "./transport-types.js";
|
|
@@ -181,6 +191,7 @@ export class AgentRegistry {
|
|
|
181
191
|
{ maxAgents: this.maxAgents, maxDepth: this.maxDepth },
|
|
182
192
|
).map((record) => [record.id, record]),
|
|
183
193
|
);
|
|
194
|
+
const restorable: Array<{ record: ManagedAgent; rootId: string; depth: number }> = [];
|
|
184
195
|
for (const record of candidates.values()) {
|
|
185
196
|
if (record.parentId && !candidates.has(record.parentId)) continue;
|
|
186
197
|
if (record.parentId === record.id) continue;
|
|
@@ -198,12 +209,31 @@ export class AgentRegistry {
|
|
|
198
209
|
parentId = candidates.get(parentId)?.parentId;
|
|
199
210
|
}
|
|
200
211
|
const depth = seen.size - 1;
|
|
201
|
-
if (cyclic
|
|
212
|
+
if (!cyclic && depth <= this.maxDepth) restorable.push({ record, rootId, depth });
|
|
213
|
+
}
|
|
214
|
+
restorable.sort(
|
|
215
|
+
(left, right) =>
|
|
216
|
+
left.depth - right.depth ||
|
|
217
|
+
left.record.createdAt - right.record.createdAt ||
|
|
218
|
+
left.record.id.localeCompare(right.record.id),
|
|
219
|
+
);
|
|
220
|
+
const reservedPaths = new Set(
|
|
221
|
+
[...this.agents.values()].flatMap((agent) =>
|
|
222
|
+
agent.state !== "closed" && agent.taskPath ? [agent.taskPath] : [],
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
for (const { record, rootId, depth } of restorable) {
|
|
226
|
+
const parentPath = record.parentId
|
|
227
|
+
? this.agents.get(record.parentId)?.taskPath
|
|
228
|
+
: ROOT_TASK_PATH;
|
|
229
|
+
if (!parentPath) continue;
|
|
230
|
+
const identity = this.reserveRestoredIdentity(record, parentPath, reservedPaths);
|
|
202
231
|
for (const completion of record.pendingCompletions ?? []) {
|
|
203
232
|
this.lastCompletionAt = Math.max(this.lastCompletionAt, completion.createdAt);
|
|
204
233
|
}
|
|
205
234
|
this.agents.set(record.id, {
|
|
206
235
|
...record,
|
|
236
|
+
...identity,
|
|
207
237
|
state:
|
|
208
238
|
record.state === "running" || record.state === "starting" ? "interrupted" : record.state,
|
|
209
239
|
rootId,
|
|
@@ -233,14 +263,26 @@ export class AgentRegistry {
|
|
|
233
263
|
});
|
|
234
264
|
}
|
|
235
265
|
for (const agent of this.agents.values()) {
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
266
|
+
if (agent.parentId) {
|
|
267
|
+
const parent = this.agents.get(agent.parentId);
|
|
268
|
+
if (parent && !parent.children.includes(agent.id)) parent.children.push(agent.id);
|
|
269
|
+
}
|
|
270
|
+
for (const completion of agent.pendingCompletions ?? []) {
|
|
271
|
+
if (!completion.recipientId || !completion.recipientPath) {
|
|
272
|
+
Object.assign(completion, this.completionRecipient(agent));
|
|
273
|
+
}
|
|
274
|
+
this.enqueueCompletionMessage(
|
|
275
|
+
agent,
|
|
276
|
+
completion,
|
|
277
|
+
completion.output || completion.error || "(no output)",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
239
280
|
}
|
|
240
281
|
}
|
|
241
282
|
|
|
242
283
|
async spawn(input: {
|
|
243
284
|
agent: string;
|
|
285
|
+
taskName?: string;
|
|
244
286
|
task: string;
|
|
245
287
|
cwd: string;
|
|
246
288
|
agentScope?: "user" | "project" | "both";
|
|
@@ -296,8 +338,15 @@ export class AgentRegistry {
|
|
|
296
338
|
if (depth > this.maxDepth) throw new Error(`Subagent depth limit reached (${this.maxDepth})`);
|
|
297
339
|
const now = this.now();
|
|
298
340
|
const id = `sa_${randomUUID()}`;
|
|
341
|
+
const taskName = input.taskName ? validateTaskName(input.taskName) : deriveTaskName(id);
|
|
342
|
+
const taskPath = joinTaskPath(parent?.taskPath ?? ROOT_TASK_PATH, taskName);
|
|
343
|
+
if (this.findByTaskPath(taskPath)) {
|
|
344
|
+
throw new Error(`Canonical subagent task path is already retained: ${taskPath}`);
|
|
345
|
+
}
|
|
299
346
|
const record: ManagedAgent = {
|
|
300
347
|
id,
|
|
348
|
+
taskName,
|
|
349
|
+
taskPath,
|
|
301
350
|
agent: input.agent,
|
|
302
351
|
parentId: parent?.id,
|
|
303
352
|
rootId: parent?.rootId ?? id,
|
|
@@ -389,9 +438,7 @@ export class AgentRegistry {
|
|
|
389
438
|
) {
|
|
390
439
|
throw new Error(`Agent ${id} cannot accept follow-up while ${agent.state}`);
|
|
391
440
|
}
|
|
392
|
-
const unread = agent.mailbox.filter((message) => !message.readAt);
|
|
393
|
-
const readAt = this.now();
|
|
394
|
-
for (const message of unread) message.readAt = readAt;
|
|
441
|
+
const unread = agent.mailbox.filter((message) => !message.readAt).slice(-20);
|
|
395
442
|
agent.currentMailboxMessageIds = unread.map((message) => message.id);
|
|
396
443
|
this.startTurn(agent, boundedTask, options);
|
|
397
444
|
return this.copy(agent);
|
|
@@ -427,18 +474,13 @@ export class AgentRegistry {
|
|
|
427
474
|
if (deduplicationKey && deduplicationKey.length > 256) {
|
|
428
475
|
throw new Error("Subagent mailbox deduplication keys cannot exceed 256 characters");
|
|
429
476
|
}
|
|
430
|
-
const
|
|
477
|
+
const sender = senderId === "root" ? undefined : this.require(senderId);
|
|
478
|
+
if (sender?.state === "closed")
|
|
479
|
+
throw new Error(`Closed agent ${sender.id} cannot send messages`);
|
|
480
|
+
const recipient = this.require(recipientId, sender?.id);
|
|
431
481
|
if (recipient.state === "closed")
|
|
432
482
|
throw new Error(`Cannot message closed agent ${recipient.id}`);
|
|
433
|
-
|
|
434
|
-
const sender = this.require(senderId);
|
|
435
|
-
if (sender.state === "closed")
|
|
436
|
-
throw new Error(`Closed agent ${sender.id} cannot send messages`);
|
|
437
|
-
if (sender.rootId !== recipient.rootId) {
|
|
438
|
-
throw new Error("Subagent mailbox messages cannot cross agent trees");
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
const message = this.enqueueMessage(recipient, content, senderId, deduplicationKey);
|
|
483
|
+
const message = this.enqueueMessage(recipient, content, sender?.id ?? "root", deduplicationKey);
|
|
442
484
|
await this.changed();
|
|
443
485
|
return { ...message };
|
|
444
486
|
}
|
|
@@ -461,6 +503,58 @@ export class AgentRegistry {
|
|
|
461
503
|
return unread.map((message) => ({ ...message }));
|
|
462
504
|
}
|
|
463
505
|
|
|
506
|
+
async acknowledgeVisibleMessages(
|
|
507
|
+
id: string,
|
|
508
|
+
messageIds: readonly string[],
|
|
509
|
+
completionIds: readonly string[] = [],
|
|
510
|
+
visibleAt = this.now(),
|
|
511
|
+
): Promise<void> {
|
|
512
|
+
const agent = this.require(id);
|
|
513
|
+
const visibleMessageIds = new Set(messageIds);
|
|
514
|
+
const visibleCompletionIds = new Set(completionIds);
|
|
515
|
+
const changedMessages: AgentMailboxMessage[] = [];
|
|
516
|
+
const removedCompletions: Array<{
|
|
517
|
+
owner: ManagedAgent;
|
|
518
|
+
completion: NonNullable<ManagedAgent["pendingCompletions"]>[number];
|
|
519
|
+
}> = [];
|
|
520
|
+
for (const message of agent.mailbox) {
|
|
521
|
+
if (message.readAt === undefined && visibleMessageIds.has(message.id)) {
|
|
522
|
+
message.readAt = visibleAt;
|
|
523
|
+
changedMessages.push(message);
|
|
524
|
+
}
|
|
525
|
+
if (message.completionId && visibleMessageIds.has(message.id)) {
|
|
526
|
+
visibleCompletionIds.add(message.completionId);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
for (const owner of this.agents.values()) {
|
|
530
|
+
for (const completion of owner.pendingCompletions ?? []) {
|
|
531
|
+
if (!visibleCompletionIds.has(completion.completionId)) continue;
|
|
532
|
+
if (completion.recipientId !== agent.id && completion.recipientPath !== agent.taskPath) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
removedCompletions.push({ owner, completion });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (changedMessages.length === 0 && removedCompletions.length === 0) return;
|
|
539
|
+
for (const { owner, completion } of removedCompletions) {
|
|
540
|
+
owner.pendingCompletions = (owner.pendingCompletions ?? []).filter(
|
|
541
|
+
(candidate) => candidate.completionId !== completion.completionId,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
agent.updatedAt = Math.max(agent.updatedAt, visibleAt);
|
|
545
|
+
try {
|
|
546
|
+
await this.changed(true);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
for (const message of changedMessages) message.readAt = undefined;
|
|
549
|
+
for (const { owner, completion } of removedCompletions) {
|
|
550
|
+
owner.pendingCompletions = [...(owner.pendingCompletions ?? []), completion].sort(
|
|
551
|
+
(left, right) => left.createdAt - right.createdAt || left.generation - right.generation,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
464
558
|
async wait(
|
|
465
559
|
id: string,
|
|
466
560
|
timeoutMs = 30_000,
|
|
@@ -471,7 +565,8 @@ export class AgentRegistry {
|
|
|
471
565
|
}
|
|
472
566
|
if (signal?.aborted) throw waitAbortError();
|
|
473
567
|
const agent = this.require(id);
|
|
474
|
-
const
|
|
568
|
+
const agentId = agent.id;
|
|
569
|
+
const running = this.running.get(agentId);
|
|
475
570
|
if (!running) return { timedOut: false, agent: this.copy(agent) };
|
|
476
571
|
let timer: NodeJS.Timeout | undefined;
|
|
477
572
|
let onAbort: (() => void) | undefined;
|
|
@@ -487,7 +582,7 @@ export class AgentRegistry {
|
|
|
487
582
|
if (onAbort) signal?.removeEventListener("abort", onAbort);
|
|
488
583
|
if (result === "aborted") throw waitAbortError();
|
|
489
584
|
return result === "timeout"
|
|
490
|
-
? { timedOut: true, agent: this.copy(this.require(
|
|
585
|
+
? { timedOut: true, agent: this.copy(this.require(agentId)) }
|
|
491
586
|
: { timedOut: false, agent: this.copy(result) };
|
|
492
587
|
}
|
|
493
588
|
|
|
@@ -504,6 +599,7 @@ export class AgentRegistry {
|
|
|
504
599
|
|
|
505
600
|
async interrupt(id: string): Promise<ManagedAgent> {
|
|
506
601
|
const agent = this.require(id);
|
|
602
|
+
const agentId = agent.id;
|
|
507
603
|
if (agent.state !== "running" && agent.state !== "starting")
|
|
508
604
|
throw new Error(`Agent ${id} is not running`);
|
|
509
605
|
if (agent.capabilityGrant?.state === "active") {
|
|
@@ -517,11 +613,13 @@ export class AgentRegistry {
|
|
|
517
613
|
agent.executionPlan = rotateExecutionPlanGeneration(agent.executionPlan);
|
|
518
614
|
}
|
|
519
615
|
if (agent.state === "starting") {
|
|
520
|
-
const index = this.queue.findIndex((entry) => entry.agent.id ===
|
|
616
|
+
const index = this.queue.findIndex((entry) => entry.agent.id === agentId);
|
|
521
617
|
if (index >= 0) {
|
|
522
618
|
const [entry] = this.queue.splice(index, 1);
|
|
619
|
+
const recipient = this.completionRecipient(agent);
|
|
523
620
|
const persistedCompletion = {
|
|
524
621
|
completionId: `completion:${agent.id}:${randomUUID()}`,
|
|
622
|
+
...recipient,
|
|
525
623
|
runId: agent.currentRunId ?? `run:${agent.id}:${randomUUID()}`,
|
|
526
624
|
generation: agent.currentTurnGeneration ?? agent.turnGeneration ?? 1,
|
|
527
625
|
task: truncateUtf8(entry.task, 256).text,
|
|
@@ -531,6 +629,7 @@ export class AgentRegistry {
|
|
|
531
629
|
};
|
|
532
630
|
agent.state = "interrupted";
|
|
533
631
|
agent.pendingCompletions = [...(agent.pendingCompletions ?? []), persistedCompletion];
|
|
632
|
+
this.enqueueCompletionMessage(agent, persistedCompletion, persistedCompletion.error);
|
|
534
633
|
clearCurrentTurn(agent);
|
|
535
634
|
agent.updatedAt = this.now();
|
|
536
635
|
const persisted = await this.persistTerminalState().then(
|
|
@@ -542,14 +641,14 @@ export class AgentRegistry {
|
|
|
542
641
|
agent: this.copy(agent),
|
|
543
642
|
};
|
|
544
643
|
entry.resolve(agent);
|
|
545
|
-
this.running.delete(
|
|
644
|
+
this.running.delete(agentId);
|
|
546
645
|
if (persisted) await this.notifyTurnComplete(completion);
|
|
547
646
|
return this.copy(agent);
|
|
548
647
|
}
|
|
549
648
|
}
|
|
550
|
-
this.controllers.get(
|
|
551
|
-
await this.running.get(
|
|
552
|
-
return this.copy(this.require(
|
|
649
|
+
this.controllers.get(agentId)?.abort();
|
|
650
|
+
await this.running.get(agentId);
|
|
651
|
+
return this.copy(this.require(agentId));
|
|
553
652
|
}
|
|
554
653
|
|
|
555
654
|
async closeTree(id: string): Promise<ManagedAgent[]> {
|
|
@@ -574,6 +673,7 @@ export class AgentRegistry {
|
|
|
574
673
|
|
|
575
674
|
async close(id: string): Promise<ManagedAgent> {
|
|
576
675
|
const agent = this.require(id);
|
|
676
|
+
const agentId = agent.id;
|
|
577
677
|
if (agent.state === "closed") throw new Error(`Agent ${id} is already closed`);
|
|
578
678
|
if (agent.children.some((childId) => this.agents.get(childId)?.state !== "closed")) {
|
|
579
679
|
throw new Error(`Agent ${id} has active descendants; close the subtree instead`);
|
|
@@ -587,20 +687,20 @@ export class AgentRegistry {
|
|
|
587
687
|
}
|
|
588
688
|
}
|
|
589
689
|
if (agent.state === "starting") {
|
|
590
|
-
const index = this.queue.findIndex((entry) => entry.agent.id ===
|
|
690
|
+
const index = this.queue.findIndex((entry) => entry.agent.id === agentId);
|
|
591
691
|
if (index >= 0) {
|
|
592
692
|
const [entry] = this.queue.splice(index, 1);
|
|
593
693
|
entry.resolve(agent);
|
|
594
|
-
this.running.delete(
|
|
694
|
+
this.running.delete(agentId);
|
|
595
695
|
}
|
|
596
696
|
}
|
|
597
|
-
this.controllers.get(
|
|
598
|
-
await this.running.get(
|
|
697
|
+
this.controllers.get(agentId)?.abort();
|
|
698
|
+
await this.running.get(agentId)?.catch(() => undefined);
|
|
599
699
|
agent.state = "closed";
|
|
600
700
|
agent.updatedAt = this.now();
|
|
601
701
|
if (agent.parentId) {
|
|
602
702
|
const parent = this.agents.get(agent.parentId);
|
|
603
|
-
if (parent) parent.children = parent.children.filter((childId) => childId !==
|
|
703
|
+
if (parent) parent.children = parent.children.filter((childId) => childId !== agentId);
|
|
604
704
|
}
|
|
605
705
|
clearCurrentTurn(agent);
|
|
606
706
|
let releaseError: unknown;
|
|
@@ -697,7 +797,7 @@ export class AgentRegistry {
|
|
|
697
797
|
}
|
|
698
798
|
|
|
699
799
|
getInspection(id: string): AgentRunInspectionDetail | undefined {
|
|
700
|
-
const agent = this.
|
|
800
|
+
const agent = this.findReference(id);
|
|
701
801
|
if (!agent) return undefined;
|
|
702
802
|
return {
|
|
703
803
|
...this.inspectSummary(agent),
|
|
@@ -755,8 +855,12 @@ export class AgentRegistry {
|
|
|
755
855
|
.map((agent) => this.copy(agent));
|
|
756
856
|
}
|
|
757
857
|
|
|
758
|
-
get(
|
|
759
|
-
|
|
858
|
+
get(reference: string): ManagedAgent | undefined {
|
|
859
|
+
return this.resolveAgent(reference);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
resolveAgent(reference: string, senderId = "root"): ManagedAgent | undefined {
|
|
863
|
+
const agent = this.findReference(reference, senderId);
|
|
760
864
|
return agent ? this.copy(agent) : undefined;
|
|
761
865
|
}
|
|
762
866
|
|
|
@@ -892,8 +996,10 @@ export class AgentRegistry {
|
|
|
892
996
|
agent.state = "failed";
|
|
893
997
|
agent.error = "Capability grant expired or no longer matches the accepted plan";
|
|
894
998
|
agent.outcome = classifyStructuredOutcome("failed", "capability-grant-invalid");
|
|
999
|
+
const recipient = this.completionRecipient(agent);
|
|
895
1000
|
const persistedCompletion = {
|
|
896
1001
|
completionId: `completion:${agent.id}:${randomUUID()}`,
|
|
1002
|
+
...recipient,
|
|
897
1003
|
runId: agent.currentRunId ?? `run:${agent.id}:${randomUUID()}`,
|
|
898
1004
|
generation: agent.currentTurnGeneration ?? agent.turnGeneration ?? 1,
|
|
899
1005
|
task: truncateUtf8(task, 256).text,
|
|
@@ -902,6 +1008,7 @@ export class AgentRegistry {
|
|
|
902
1008
|
createdAt: this.completionCreatedAt(),
|
|
903
1009
|
};
|
|
904
1010
|
agent.pendingCompletions = [...(agent.pendingCompletions ?? []), persistedCompletion];
|
|
1011
|
+
this.enqueueCompletionMessage(agent, persistedCompletion, persistedCompletion.error ?? "");
|
|
905
1012
|
clearCurrentTurn(agent);
|
|
906
1013
|
agent.updatedAt = this.now();
|
|
907
1014
|
void this.persistTerminalState()
|
|
@@ -930,6 +1037,7 @@ export class AgentRegistry {
|
|
|
930
1037
|
let completionContent = "";
|
|
931
1038
|
let completionOutput = "";
|
|
932
1039
|
let completionError: string | undefined;
|
|
1040
|
+
const visibleMailboxMessageIds = [...(agent.currentMailboxMessageIds ?? [])];
|
|
933
1041
|
void this.transport
|
|
934
1042
|
.runTurn(this.copy(agent), task, controller.signal, (progress) => {
|
|
935
1043
|
agent.telemetry = {
|
|
@@ -942,6 +1050,14 @@ export class AgentRegistry {
|
|
|
942
1050
|
};
|
|
943
1051
|
})
|
|
944
1052
|
.then(async (outcome) => {
|
|
1053
|
+
if (visibleMailboxMessageIds.length > 0) {
|
|
1054
|
+
await this.acknowledgeVisibleMessages(
|
|
1055
|
+
agent.id,
|
|
1056
|
+
visibleMailboxMessageIds,
|
|
1057
|
+
[],
|
|
1058
|
+
this.now(),
|
|
1059
|
+
).catch(() => undefined);
|
|
1060
|
+
}
|
|
945
1061
|
const output = truncateUtf8(outcome.output, this.maxTurnOutputBytes).text;
|
|
946
1062
|
const error = outcome.error
|
|
947
1063
|
? truncateUtf8(outcome.error, this.maxTurnOutputBytes).text
|
|
@@ -1068,8 +1184,10 @@ export class AgentRegistry {
|
|
|
1068
1184
|
return agent;
|
|
1069
1185
|
})
|
|
1070
1186
|
.finally(async () => {
|
|
1187
|
+
const recipient = this.completionRecipient(agent);
|
|
1071
1188
|
const persistedCompletion = {
|
|
1072
1189
|
completionId: completionKey,
|
|
1190
|
+
...recipient,
|
|
1073
1191
|
runId,
|
|
1074
1192
|
generation: turnGeneration,
|
|
1075
1193
|
task: truncateUtf8(task, 256).text,
|
|
@@ -1078,12 +1196,7 @@ export class AgentRegistry {
|
|
|
1078
1196
|
createdAt: this.completionCreatedAt(),
|
|
1079
1197
|
};
|
|
1080
1198
|
agent.pendingCompletions = [...(agent.pendingCompletions ?? []), persistedCompletion];
|
|
1081
|
-
|
|
1082
|
-
const parent = this.agents.get(agent.parentId);
|
|
1083
|
-
if (parent && parent.state !== "closed") {
|
|
1084
|
-
this.enqueueMessage(parent, completionContent, agent.id, completionKey);
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1199
|
+
this.enqueueCompletionMessage(agent, persistedCompletion, completionContent);
|
|
1087
1200
|
clearCurrentTurn(agent);
|
|
1088
1201
|
agent.updatedAt = this.now();
|
|
1089
1202
|
const persisted = await this.persistTerminalState().then(
|
|
@@ -1113,11 +1226,38 @@ export class AgentRegistry {
|
|
|
1113
1226
|
}
|
|
1114
1227
|
}
|
|
1115
1228
|
|
|
1229
|
+
private completionRecipient(
|
|
1230
|
+
agent: ManagedAgent,
|
|
1231
|
+
): Pick<
|
|
1232
|
+
NonNullable<ManagedAgent["pendingCompletions"]>[number],
|
|
1233
|
+
"recipientId" | "recipientPath"
|
|
1234
|
+
> {
|
|
1235
|
+
return resolveCompletionRecipient(agent, (id) => this.agents.get(id));
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
private enqueueCompletionMessage(
|
|
1239
|
+
agent: ManagedAgent,
|
|
1240
|
+
completion: NonNullable<ManagedAgent["pendingCompletions"]>[number],
|
|
1241
|
+
content: string,
|
|
1242
|
+
): void {
|
|
1243
|
+
if (!completion.recipientId || completion.recipientId === "root") return;
|
|
1244
|
+
const recipient = this.agents.get(completion.recipientId);
|
|
1245
|
+
if (!recipient || recipient.state === "closed") return;
|
|
1246
|
+
this.enqueueMessage(
|
|
1247
|
+
recipient,
|
|
1248
|
+
content || "(no output)",
|
|
1249
|
+
agent.id,
|
|
1250
|
+
completion.completionId,
|
|
1251
|
+
completion.completionId,
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1116
1255
|
private enqueueMessage(
|
|
1117
1256
|
recipient: ManagedAgent,
|
|
1118
1257
|
content: string,
|
|
1119
1258
|
senderId: string,
|
|
1120
1259
|
deduplicationKey?: string,
|
|
1260
|
+
completionId?: string,
|
|
1121
1261
|
): AgentMailboxMessage {
|
|
1122
1262
|
if (deduplicationKey) {
|
|
1123
1263
|
const existing = recipient.mailbox.find(
|
|
@@ -1133,6 +1273,7 @@ export class AgentRegistry {
|
|
|
1133
1273
|
content: bounded.text,
|
|
1134
1274
|
createdAt: this.now(),
|
|
1135
1275
|
deduplicationKey,
|
|
1276
|
+
completionId,
|
|
1136
1277
|
};
|
|
1137
1278
|
recipient.mailbox.push(message);
|
|
1138
1279
|
recipient.mailbox = recipient.mailbox.slice(-this.maxMailboxMessages);
|
|
@@ -1154,12 +1295,68 @@ export class AgentRegistry {
|
|
|
1154
1295
|
return result;
|
|
1155
1296
|
}
|
|
1156
1297
|
|
|
1157
|
-
private require(
|
|
1158
|
-
const agent = this.
|
|
1159
|
-
if (!agent) throw new Error(`Unknown subagent: ${
|
|
1298
|
+
private require(reference: string, senderId = "root"): ManagedAgent {
|
|
1299
|
+
const agent = this.findReference(reference, senderId);
|
|
1300
|
+
if (!agent) throw new Error(`Unknown subagent: ${safeTerminalLine(reference, 256)}`);
|
|
1160
1301
|
return agent;
|
|
1161
1302
|
}
|
|
1162
1303
|
|
|
1304
|
+
private findReference(reference: string, senderId = "root"): ManagedAgent | undefined {
|
|
1305
|
+
const direct = this.agents.get(reference);
|
|
1306
|
+
if (direct) return direct;
|
|
1307
|
+
const sender =
|
|
1308
|
+
senderId === "root"
|
|
1309
|
+
? undefined
|
|
1310
|
+
: (this.agents.get(senderId) ?? this.findByTaskPath(senderId));
|
|
1311
|
+
if (senderId !== "root" && !sender) return undefined;
|
|
1312
|
+
let taskPath: string;
|
|
1313
|
+
try {
|
|
1314
|
+
taskPath = resolveTaskPath(sender?.taskPath ?? ROOT_TASK_PATH, reference);
|
|
1315
|
+
} catch {
|
|
1316
|
+
return undefined;
|
|
1317
|
+
}
|
|
1318
|
+
return this.findByTaskPath(taskPath);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
private findByTaskPath(taskPath: string): ManagedAgent | undefined {
|
|
1322
|
+
return [...this.agents.values()].find(
|
|
1323
|
+
(agent) => agent.state !== "closed" && agent.taskPath === taskPath,
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
private reserveRestoredIdentity(
|
|
1328
|
+
record: ManagedAgent,
|
|
1329
|
+
parentPath: string,
|
|
1330
|
+
reservedPaths: Set<string>,
|
|
1331
|
+
): Pick<ManagedAgent, "taskName" | "taskPath"> {
|
|
1332
|
+
let taskName: string | undefined;
|
|
1333
|
+
try {
|
|
1334
|
+
taskName = record.taskName ? validateTaskName(record.taskName) : undefined;
|
|
1335
|
+
} catch {
|
|
1336
|
+
taskName = undefined;
|
|
1337
|
+
}
|
|
1338
|
+
if (!taskName && record.taskPath) {
|
|
1339
|
+
try {
|
|
1340
|
+
const storedPath = validateTaskPath(record.taskPath);
|
|
1341
|
+
const storedName = storedPath.slice(storedPath.lastIndexOf("/") + 1);
|
|
1342
|
+
taskName = validateTaskName(storedName);
|
|
1343
|
+
} catch {
|
|
1344
|
+
taskName = undefined;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
taskName ??= deriveTaskName(record.id);
|
|
1348
|
+
let taskPath = joinTaskPath(parentPath, taskName);
|
|
1349
|
+
if (reservedPaths.has(taskPath)) {
|
|
1350
|
+
taskName = deriveTaskName(record.id);
|
|
1351
|
+
taskPath = joinTaskPath(parentPath, taskName);
|
|
1352
|
+
}
|
|
1353
|
+
if (reservedPaths.has(taskPath)) {
|
|
1354
|
+
throw new Error(`Cannot restore duplicate canonical subagent task path: ${taskPath}`);
|
|
1355
|
+
}
|
|
1356
|
+
reservedPaths.add(taskPath);
|
|
1357
|
+
return { taskName, taskPath };
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1163
1360
|
private retainedCount(): number {
|
|
1164
1361
|
return [...this.agents.values()].filter((agent) => agent.state !== "closed").length;
|
|
1165
1362
|
}
|
|
@@ -1272,6 +1469,8 @@ export class AgentRegistry {
|
|
|
1272
1469
|
}
|
|
1273
1470
|
return {
|
|
1274
1471
|
id: agent.id,
|
|
1472
|
+
taskName: agent.taskName,
|
|
1473
|
+
taskPath: agent.taskPath,
|
|
1275
1474
|
agent: agent.agent,
|
|
1276
1475
|
state: agent.state,
|
|
1277
1476
|
createdAt: agent.createdAt,
|
package/src/render.ts
CHANGED
|
@@ -7,12 +7,13 @@ import {
|
|
|
7
7
|
type ToolRenderResultOptions,
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
10
|
-
import type { AgentScope
|
|
10
|
+
import type { AgentScope } from "./agents/types.js";
|
|
11
11
|
import { renderPanelCall, renderPanelResult } from "./panel-render.js";
|
|
12
12
|
import { hasUsableAggregator, type SubagentParams } from "./params.js";
|
|
13
13
|
import { expansionHint, formatToolActivity, safeBlock, safeLine } from "./render-common.js";
|
|
14
14
|
import type { SingleResult, SubagentDetails } from "./runner.js";
|
|
15
15
|
import { getResultFinalOutput, isResultError } from "./runner-outcome.js";
|
|
16
|
+
import { formatUsageStats } from "./usage-format.js";
|
|
16
17
|
|
|
17
18
|
const COLLAPSED_ITEM_COUNT = 5;
|
|
18
19
|
|
|
@@ -25,46 +26,6 @@ function previewAgent(agent: unknown): string {
|
|
|
25
26
|
return safeLine(agent, "...", 256);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export function formatTokens(count: number): string {
|
|
29
|
-
if (count < 1000) return count.toString();
|
|
30
|
-
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
31
|
-
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
32
|
-
return `${(count / 1000000).toFixed(1)}M`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function formatUsageStats(
|
|
36
|
-
usage: {
|
|
37
|
-
input: number;
|
|
38
|
-
output: number;
|
|
39
|
-
cacheRead: number;
|
|
40
|
-
cacheWrite: number;
|
|
41
|
-
cost: number;
|
|
42
|
-
contextTokens?: number;
|
|
43
|
-
turns?: number;
|
|
44
|
-
},
|
|
45
|
-
model?: string,
|
|
46
|
-
thinkingLevel?: SubagentThinkingLevel,
|
|
47
|
-
actualProvider?: string,
|
|
48
|
-
actualModel?: string,
|
|
49
|
-
): string {
|
|
50
|
-
const parts: string[] = [];
|
|
51
|
-
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
52
|
-
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
53
|
-
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
54
|
-
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
55
|
-
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
56
|
-
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
57
|
-
if (usage.contextTokens && usage.contextTokens > 0)
|
|
58
|
-
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
59
|
-
const safeProvider = actualProvider ? safeLine(actualProvider, "", 256) : undefined;
|
|
60
|
-
const safeModel = actualModel ? safeLine(actualModel, "", 256) : undefined;
|
|
61
|
-
const actual =
|
|
62
|
-
safeProvider && safeModel ? `${safeProvider}/${safeModel}` : (safeModel ?? safeProvider);
|
|
63
|
-
if (actual ?? model) parts.push(actual ?? safeLine(model, "", 256));
|
|
64
|
-
if (thinkingLevel) parts.push(`requested-thinking:${safeLine(thinkingLevel, "", 128)}`);
|
|
65
|
-
return parts.join(" ");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
29
|
function formatResultUsageStats(result: SingleResult): string {
|
|
69
30
|
return formatUsageStats(
|
|
70
31
|
result.usage,
|
package/src/result-contract.ts
CHANGED
|
@@ -110,15 +110,30 @@ export function structuredResultInstruction(format: SubagentResultFormat | undef
|
|
|
110
110
|
].join(" ");
|
|
111
111
|
}
|
|
112
112
|
if (format === "structured-v2") {
|
|
113
|
+
const minimumResult = JSON.stringify({
|
|
114
|
+
version: "pi-subagents:result:v2",
|
|
115
|
+
status: "completed",
|
|
116
|
+
summary: "Concise outcome",
|
|
117
|
+
claims: [],
|
|
118
|
+
artifacts: [],
|
|
119
|
+
changes: [],
|
|
120
|
+
verification: [],
|
|
121
|
+
limitations: [],
|
|
122
|
+
unresolvedDependencies: [],
|
|
123
|
+
});
|
|
113
124
|
return [
|
|
114
125
|
"Return the final answer as one JSON object and no surrounding prose.",
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
"Minimum valid result:",
|
|
127
|
+
minimumResult,
|
|
128
|
+
"Keep every top-level field shown, including empty arrays.",
|
|
117
129
|
`status must be one of ${SUBAGENT_OUTCOME_STATUSES.join(", ")}.`,
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
'Claim item shape: {"claim":"Observed fact","classification":"observed","evidence":["path:line"]}.',
|
|
131
|
+
'Artifact item shape: {"id":"artifact-id","kind":"file","version":"optional","location":"optional","digest":"optional"}.',
|
|
132
|
+
'Change item shape: {"path":"path/to/file","summary":"What changed"}.',
|
|
133
|
+
'Verification item shape: {"status":"passed","summary":"What was checked","command":"optional","evidence":["optional"]}.',
|
|
134
|
+
"Claim classification must be observed, inferred, or unverified; verification status must be passed, failed, or not-run.",
|
|
120
135
|
"Use optional reasonCode for non-completed outcomes and optional provenance for taskId, inputArtifacts, and repositoryGeneration; executor-owned generation and plan identity are stamped after parsing.",
|
|
121
|
-
].join("
|
|
136
|
+
].join("\n");
|
|
122
137
|
}
|
|
123
138
|
return "";
|
|
124
139
|
}
|