@ian-pascoe/pi-minimal-subagents 0.2.3 → 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 +9 -0
- 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 +23 -45
- package/src/minimal-subagents-extension.ts +66 -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 +64 -46
- package/src/minimal-subagents-tools.ts +2 -2
- package/src/minimal-subagents-types.ts +26 -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
|
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,
|
|
@@ -203,7 +207,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
203
207
|
const ordinaryTools = resolveOrdinaryToolSelection(parameters.tools, {
|
|
204
208
|
ordinaryTools: excludeCoordinatorTools(caller.ordinaryTools),
|
|
205
209
|
capabilityCeiling: excludeCoordinatorTools(caller.capabilityCeiling),
|
|
206
|
-
availableTools: excludeCoordinatorTools(caller.availableTools),
|
|
207
210
|
});
|
|
208
211
|
const committedMessages = structuredClone(caller.messages);
|
|
209
212
|
const imported = assembleImportedContext(sessionContext, committedMessages);
|
|
@@ -296,7 +299,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
296
299
|
thinkingLevel: agent.launch_contract.thinking_level,
|
|
297
300
|
ordinaryTools: [...agent.launch_contract.ordinary_tools],
|
|
298
301
|
capabilityCeiling: [...agent.capability_ceiling],
|
|
299
|
-
availableTools: [...agent.capability_ceiling],
|
|
300
302
|
spawnEntryId,
|
|
301
303
|
};
|
|
302
304
|
}
|
|
@@ -354,7 +356,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
354
356
|
}
|
|
355
357
|
}
|
|
356
358
|
|
|
357
|
-
/** Wait for one exact turn
|
|
359
|
+
/** Wait for one exact turn, returning detailed child status if the waiter times out. */
|
|
358
360
|
wait(
|
|
359
361
|
callerId: string,
|
|
360
362
|
agentId: string,
|
|
@@ -405,13 +407,16 @@ export class MinimalSubagentsCoordinator {
|
|
|
405
407
|
reject(error);
|
|
406
408
|
};
|
|
407
409
|
if (timeoutMs !== undefined) {
|
|
408
|
-
waiter.timeout = setTimeout(
|
|
409
|
-
()
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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);
|
|
415
420
|
}
|
|
416
421
|
if (signal) {
|
|
417
422
|
waiter.abortListener = () =>
|
|
@@ -492,7 +497,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
492
497
|
agent_id: agentId,
|
|
493
498
|
recursive,
|
|
494
499
|
deleted_agent_ids: [],
|
|
495
|
-
tombstoned_agent_ids: [],
|
|
496
500
|
trashed_session_files: [],
|
|
497
501
|
failures: [],
|
|
498
502
|
};
|
|
@@ -520,7 +524,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
520
524
|
this.pruneRecentMessageProjectionsForDeletedAgent(agent.agent_id);
|
|
521
525
|
this.tombstones.add(agent.agent_id);
|
|
522
526
|
result.deleted_agent_ids.push(agent.agent_id);
|
|
523
|
-
result.tombstoned_agent_ids.push(agent.agent_id);
|
|
524
527
|
this.dependencies.registry.append(
|
|
525
528
|
createRegistryEvent(this.dependencies.registry.rootSessionId, "agent-deleted", {
|
|
526
529
|
agent_ids: [agent.agent_id],
|
|
@@ -567,12 +570,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
567
570
|
this.runtimeInitializations.clear();
|
|
568
571
|
this.pendingAgentIds.clear();
|
|
569
572
|
this.tombstones.clear();
|
|
570
|
-
this.deliveryLedger = createDeliveryLedger();
|
|
571
573
|
this.waiters.clear();
|
|
572
|
-
this.pendingParentMessages.clear();
|
|
573
574
|
this.waitHandedDeliveryIds.clear();
|
|
574
|
-
this.recipientQueues.clear();
|
|
575
|
-
this.backgroundOperations.clear();
|
|
576
575
|
this.automaticDeliveryKeys.clear();
|
|
577
576
|
this.automaticCoordinationDeliveryIds.clear();
|
|
578
577
|
this.deliveryLedger = createDeliveryLedger({
|
|
@@ -916,9 +915,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
916
915
|
return initialization;
|
|
917
916
|
}
|
|
918
917
|
|
|
919
|
-
private beginTurn(agent: PersistedAgent):
|
|
920
|
-
|
|
921
|
-
const turnId = `${agent.agent_id}:turn-${randomUUID()}` as TurnId;
|
|
918
|
+
private beginTurn(agent: PersistedAgent): string {
|
|
919
|
+
const turnId = `${agent.agent_id}:turn-${randomUUID()}`;
|
|
922
920
|
const startedAt = this.now().toISOString();
|
|
923
921
|
agent.active_turn_id = turnId;
|
|
924
922
|
agent.active_turn_started_at = startedAt;
|
|
@@ -1027,11 +1025,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1027
1025
|
this.settleDelivery(delivery);
|
|
1028
1026
|
return;
|
|
1029
1027
|
}
|
|
1030
|
-
if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery)) return;
|
|
1031
|
-
if (this.hasDeliveryEvidence(delivery)) {
|
|
1032
|
-
this.settleDelivery(delivery);
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
1028
|
const message: CoordinatorMessage = {
|
|
1036
1029
|
customType: "minimal-subagents.result",
|
|
1037
1030
|
content: result.output,
|
|
@@ -1478,11 +1471,6 @@ export class MinimalSubagentsCoordinator {
|
|
|
1478
1471
|
elapsed_ms: elapsed ?? agent.latest_result?.elapsed_ms,
|
|
1479
1472
|
latest_activity_at: agent.latest_activity_at ?? agent.created_at,
|
|
1480
1473
|
task: agent.task,
|
|
1481
|
-
latest_activity: agent.active_turn_id
|
|
1482
|
-
? "turn running"
|
|
1483
|
-
: agent.latest_result
|
|
1484
|
-
? `turn ${agent.latest_result.status}`
|
|
1485
|
-
: "created",
|
|
1486
1474
|
child_count: directChildren.length,
|
|
1487
1475
|
children,
|
|
1488
1476
|
};
|
|
@@ -1490,7 +1478,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
1490
1478
|
|
|
1491
1479
|
private buildAgentDetail(agent: PersistedAgent, includeDescendants = true): AgentDetail {
|
|
1492
1480
|
const summary = this.buildAgentSummary(agent, includeDescendants);
|
|
1493
|
-
const
|
|
1481
|
+
const runtime = this.runtimes.get(agent.agent_id);
|
|
1482
|
+
const runtimeUsage = runtime?.getUsage();
|
|
1494
1483
|
return {
|
|
1495
1484
|
...summary,
|
|
1496
1485
|
session_file: agent.session_file,
|
|
@@ -1498,6 +1487,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1498
1487
|
capability_ceiling: [...agent.capability_ceiling],
|
|
1499
1488
|
spawn_entry_id: agent.spawn_entry_id,
|
|
1500
1489
|
recent_messages: structuredClone(agent.recent_messages),
|
|
1490
|
+
recent_activity: buildRecentAgentActivity(runtime?.snapshotActivityMessages() ?? []),
|
|
1501
1491
|
latest_result: agent.latest_result ? structuredClone(agent.latest_result) : undefined,
|
|
1502
1492
|
missing_dependencies: [...agent.missing_dependencies],
|
|
1503
1493
|
unavailable_reason: agent.unavailable_reason,
|
|
@@ -1621,19 +1611,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1621
1611
|
}
|
|
1622
1612
|
|
|
1623
1613
|
private createForkPlaceholder(agent: PersistedAgent, cloneError: string): PersistedAgent {
|
|
1624
|
-
return
|
|
1625
|
-
...structuredClone(agent),
|
|
1626
|
-
session_file: undefined,
|
|
1627
|
-
session_id: undefined,
|
|
1628
|
-
session_leaf_id: undefined,
|
|
1629
|
-
clone_error: cloneError,
|
|
1630
|
-
active_turn_id: undefined,
|
|
1631
|
-
active_turn_started_at: undefined,
|
|
1632
|
-
latest_activity_at: this.now().toISOString(),
|
|
1633
|
-
availability: "unavailable",
|
|
1634
|
-
missing_dependencies: [cloneError],
|
|
1635
|
-
unavailable_reason: cloneError,
|
|
1636
|
-
};
|
|
1614
|
+
return unavailableAgent(agent, cloneError, this.now().toISOString());
|
|
1637
1615
|
}
|
|
1638
1616
|
|
|
1639
1617
|
private rejectAllWaiters(error: Error): void {
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
findDeliveryEvidence,
|
|
40
40
|
PiAgentSessionFactory,
|
|
41
41
|
type PiAgentSessionFactoryOptions,
|
|
42
|
+
unavailableAgent,
|
|
42
43
|
} from "./minimal-subagents-sessions.js";
|
|
43
44
|
import { shutdownMinimalSubagentsSession } from "./minimal-subagents-shutdown.js";
|
|
44
45
|
import { createCoordinatorToolDefinitions } from "./minimal-subagents-tools.js";
|
|
@@ -53,6 +54,7 @@ import type {
|
|
|
53
54
|
CoordinatorNotification,
|
|
54
55
|
ForkSnapshot,
|
|
55
56
|
PersistedAgent,
|
|
57
|
+
PersistedSessionIdentity,
|
|
56
58
|
RegistrySnapshot,
|
|
57
59
|
RootConversationEndpoint,
|
|
58
60
|
} from "./minimal-subagents-types.js";
|
|
@@ -75,7 +77,6 @@ function rootCallerSnapshot(pi: ExtensionAPI, context: ExtensionContext): Caller
|
|
|
75
77
|
thinkingLevel: context.thinkingLevel ?? pi.getThinkingLevel(),
|
|
76
78
|
ordinaryTools: activeTools,
|
|
77
79
|
capabilityCeiling: availableTools,
|
|
78
|
-
availableTools,
|
|
79
80
|
spawnEntryId: context.sessionManager.getLeafId() ?? "root",
|
|
80
81
|
};
|
|
81
82
|
}
|
|
@@ -183,57 +184,84 @@ function orderForkAgentsParentFirst(agents: readonly PersistedAgent[]): Persiste
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
function unavailableForkAgent(agent: PersistedAgent, error: string): PersistedAgent {
|
|
186
|
-
return
|
|
187
|
-
...structuredClone(agent),
|
|
188
|
-
session_file: undefined,
|
|
189
|
-
session_id: undefined,
|
|
190
|
-
session_leaf_id: undefined,
|
|
191
|
-
clone_error: error,
|
|
192
|
-
active_turn_id: undefined,
|
|
193
|
-
active_turn_started_at: undefined,
|
|
194
|
-
availability: "unavailable",
|
|
195
|
-
missing_dependencies: [error],
|
|
196
|
-
unavailable_reason: error,
|
|
197
|
-
};
|
|
187
|
+
return unavailableAgent(agent, error);
|
|
198
188
|
}
|
|
199
189
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
190
|
+
/** One per-agent fork rebind step returning the destination session identity. */
|
|
191
|
+
type ForkAgentRebind = (agent: PersistedAgent) => Promise<PersistedSessionIdentity>;
|
|
192
|
+
|
|
193
|
+
interface ForkRebindOptions {
|
|
194
|
+
/** Require an existing clone session before rebinding (ownership pass only). */
|
|
195
|
+
readonly requiresCloneSession: boolean;
|
|
196
|
+
/** Message for agents skipped because an ancestor's rebind failed. */
|
|
197
|
+
readonly skippedMessage: (agent: PersistedAgent, failedAncestor: string) => string;
|
|
198
|
+
/** Human label used in failure notifications. */
|
|
199
|
+
readonly notifyLabel: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Walk fork agents parent-first, rebind each session, and quarantine failed subtrees. */
|
|
203
|
+
async function rebindForkAgents(
|
|
204
|
+
snapshotAgents: readonly PersistedAgent[],
|
|
205
|
+
rebind: ForkAgentRebind,
|
|
206
|
+
options: ForkRebindOptions,
|
|
205
207
|
context: ExtensionContext,
|
|
206
|
-
): Promise<
|
|
208
|
+
): Promise<PersistedAgent[]> {
|
|
207
209
|
const agents: PersistedAgent[] = [];
|
|
208
210
|
const failedSubtrees = new Set<string>();
|
|
209
|
-
for (const agent of orderForkAgentsParentFirst(
|
|
211
|
+
for (const agent of orderForkAgentsParentFirst(snapshotAgents)) {
|
|
210
212
|
const failedAncestor = [...failedSubtrees].find(
|
|
211
213
|
(agentId) => agent.agent_id === agentId || agent.agent_id.startsWith(`${agentId}.`),
|
|
212
214
|
);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
const missingCloneSession =
|
|
216
|
+
options.requiresCloneSession && (!agent.session_file || !agent.session_id);
|
|
217
|
+
if (failedAncestor !== undefined || missingCloneSession) {
|
|
218
|
+
const message =
|
|
219
|
+
failedAncestor !== undefined
|
|
220
|
+
? options.skippedMessage(agent, failedAncestor)
|
|
221
|
+
: (agent.clone_error ?? `Ancestor ownership failed: ${agent.agent_id}`);
|
|
222
|
+
agents.push(unavailableForkAgent(agent, message));
|
|
215
223
|
continue;
|
|
216
224
|
}
|
|
217
225
|
try {
|
|
218
|
-
const
|
|
219
|
-
if (!
|
|
226
|
+
const identity = await rebind(agent);
|
|
227
|
+
if (!identity.sessionLeafId) {
|
|
220
228
|
throw new Error(
|
|
221
229
|
`Minimal subagents fork recovery: no selected session leaf for ${agent.agent_id}`,
|
|
222
230
|
);
|
|
223
231
|
}
|
|
224
232
|
agents.push({
|
|
225
233
|
...structuredClone(agent),
|
|
226
|
-
session_file:
|
|
227
|
-
session_id:
|
|
228
|
-
session_leaf_id:
|
|
234
|
+
session_file: identity.sessionFile,
|
|
235
|
+
session_id: identity.sessionId,
|
|
236
|
+
session_leaf_id: identity.sessionLeafId,
|
|
229
237
|
});
|
|
230
238
|
} catch (error) {
|
|
231
239
|
const message = error instanceof Error ? error.message : String(error);
|
|
232
240
|
failedSubtrees.add(agent.agent_id);
|
|
233
241
|
agents.push(unavailableForkAgent(agent, message));
|
|
234
|
-
context.ui.notify(
|
|
242
|
+
context.ui.notify(`${options.notifyLabel} for ${agent.agent_id}: ${message}`, "error");
|
|
235
243
|
}
|
|
236
244
|
}
|
|
245
|
+
return agents;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function cloneSelectedForkSessions(
|
|
249
|
+
sessionFactory: AgentSessionFactory,
|
|
250
|
+
snapshot: RegistrySnapshot,
|
|
251
|
+
sourceRootSessionFile: string,
|
|
252
|
+
sourceRootSessionId: string,
|
|
253
|
+
context: ExtensionContext,
|
|
254
|
+
): Promise<ForkSnapshot> {
|
|
255
|
+
const agents = await rebindForkAgents(
|
|
256
|
+
snapshot.agents,
|
|
257
|
+
(agent) => sessionFactory.cloneForkSourceSession(agent, sourceRootSessionId),
|
|
258
|
+
{
|
|
259
|
+
requiresCloneSession: false,
|
|
260
|
+
skippedMessage: (_agent, failedAncestor) => `Ancestor clone failed: ${failedAncestor}`,
|
|
261
|
+
notifyLabel: "Fork recovery clone failed",
|
|
262
|
+
},
|
|
263
|
+
context,
|
|
264
|
+
);
|
|
237
265
|
return {
|
|
238
266
|
...structuredClone(snapshot),
|
|
239
267
|
source_root_session_file: sourceRootSessionFile,
|
|
@@ -247,44 +275,16 @@ async function bindForkSnapshotToDestination(
|
|
|
247
275
|
snapshot: ForkSnapshot,
|
|
248
276
|
context: ExtensionContext,
|
|
249
277
|
): Promise<ForkSnapshot> {
|
|
250
|
-
const agents
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
agent.clone_error ?? `Ancestor ownership failed: ${failedAncestor ?? agent.agent_id}`,
|
|
261
|
-
),
|
|
262
|
-
);
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
try {
|
|
266
|
-
const owned = await sessionFactory.adoptForkSessionOwnership(
|
|
267
|
-
agent,
|
|
268
|
-
snapshot.source_root_session_id,
|
|
269
|
-
);
|
|
270
|
-
if (!owned.sessionLeafId) {
|
|
271
|
-
throw new Error(
|
|
272
|
-
`Minimal subagents fork ownership: no selected session leaf for ${agent.agent_id}`,
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
agents.push({
|
|
276
|
-
...structuredClone(agent),
|
|
277
|
-
session_file: owned.sessionFile,
|
|
278
|
-
session_id: owned.sessionId,
|
|
279
|
-
session_leaf_id: owned.sessionLeafId,
|
|
280
|
-
});
|
|
281
|
-
} catch (error) {
|
|
282
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
283
|
-
failedSubtrees.add(agent.agent_id);
|
|
284
|
-
agents.push(unavailableForkAgent(agent, message));
|
|
285
|
-
context.ui.notify(`Fork ownership failed for ${agent.agent_id}: ${message}`, "error");
|
|
286
|
-
}
|
|
287
|
-
}
|
|
278
|
+
const agents = await rebindForkAgents(
|
|
279
|
+
snapshot.agents,
|
|
280
|
+
(agent) => sessionFactory.adoptForkSessionOwnership(agent, snapshot.source_root_session_id),
|
|
281
|
+
{
|
|
282
|
+
requiresCloneSession: true,
|
|
283
|
+
skippedMessage: () => "Ancestor ownership failed",
|
|
284
|
+
notifyLabel: "Fork ownership failed",
|
|
285
|
+
},
|
|
286
|
+
context,
|
|
287
|
+
);
|
|
288
288
|
return { ...structuredClone(snapshot), agents };
|
|
289
289
|
}
|
|
290
290
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
1
|
import type { ForkSnapshot } from "./minimal-subagents-types.js";
|
|
2
|
+
import { canonicalPath } from "./minimal-subagents-sessions.js";
|
|
4
3
|
|
|
5
4
|
declare global {
|
|
6
5
|
// eslint-disable-next-line no-var -- A process-global handoff must be visible to replacement extension instances.
|
|
@@ -12,11 +11,6 @@ function forkSnapshotStore(): Map<string, ForkSnapshot> {
|
|
|
12
11
|
return globalThis.minimalSubagentsForkSnapshots;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
function canonicalSessionFile(sessionFile: string): string {
|
|
16
|
-
const absolutePath = resolve(sessionFile);
|
|
17
|
-
return existsSync(absolutePath) ? realpathSync(absolutePath) : absolutePath;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
14
|
/** Prove a process-loss fork destination was derived from the expected canonical source file. */
|
|
21
15
|
export function isForkDestinationForSource(
|
|
22
16
|
destinationHeader: { parentSession?: string } | null,
|
|
@@ -24,14 +18,13 @@ export function isForkDestinationForSource(
|
|
|
24
18
|
): boolean {
|
|
25
19
|
return (
|
|
26
20
|
destinationHeader?.parentSession !== undefined &&
|
|
27
|
-
|
|
28
|
-
canonicalSessionFile(previousSessionFile)
|
|
21
|
+
canonicalPath(destinationHeader.parentSession) === canonicalPath(previousSessionFile)
|
|
29
22
|
);
|
|
30
23
|
}
|
|
31
24
|
|
|
32
25
|
/** Retain a complete pre-fork hierarchy across Pi extension-instance replacement. */
|
|
33
26
|
export function rememberForkSnapshot(snapshot: ForkSnapshot): void {
|
|
34
|
-
const canonicalSourceFile =
|
|
27
|
+
const canonicalSourceFile = canonicalPath(snapshot.source_root_session_file);
|
|
35
28
|
const retained = structuredClone(snapshot);
|
|
36
29
|
retained.source_root_session_file = canonicalSourceFile;
|
|
37
30
|
forkSnapshotStore().set(canonicalSourceFile, retained);
|
|
@@ -39,7 +32,7 @@ export function rememberForkSnapshot(snapshot: ForkSnapshot): void {
|
|
|
39
32
|
|
|
40
33
|
/** Consume the pre-fork hierarchy once when the destination root session starts. */
|
|
41
34
|
export function takeForkSnapshot(previousSessionFile: string): ForkSnapshot | undefined {
|
|
42
|
-
const key =
|
|
35
|
+
const key = canonicalPath(previousSessionFile);
|
|
43
36
|
const snapshot = forkSnapshotStore().get(key);
|
|
44
37
|
if (snapshot) forkSnapshotStore().delete(key);
|
|
45
38
|
return snapshot ? structuredClone(snapshot) : undefined;
|
|
@@ -102,7 +102,6 @@ export const RegistryAgentWireSchema = Type.Object({
|
|
|
102
102
|
missing_dependencies: Type.Array(NonEmptyStringSchema),
|
|
103
103
|
unavailable_reason: Type.Optional(NonEmptyStringSchema),
|
|
104
104
|
recent_messages: Type.Array(RegistryRecentMessageWireSchema),
|
|
105
|
-
deleted: Type.Optional(Type.Boolean()),
|
|
106
105
|
});
|
|
107
106
|
|
|
108
107
|
/** Parses one terminal Delivery Ledger item without applying ownership semantics. */
|
|
@@ -399,7 +399,6 @@ function parseRegistryAgent(value: RegistryAgentWire, version: 1 | 2): Persisted
|
|
|
399
399
|
agent.latest_result = cloneRegistryTurnResult(value.latest_result);
|
|
400
400
|
}
|
|
401
401
|
if (value.unavailable_reason !== undefined) agent.unavailable_reason = value.unavailable_reason;
|
|
402
|
-
if (value.deleted !== undefined) agent.deleted = value.deleted;
|
|
403
402
|
return agent;
|
|
404
403
|
}
|
|
405
404
|
|
|
@@ -620,12 +619,6 @@ function validateRegistrySnapshot(
|
|
|
620
619
|
"tombstone agent ID is not canonical",
|
|
621
620
|
);
|
|
622
621
|
}
|
|
623
|
-
if (agents.some((agent) => agent.deleted === true)) {
|
|
624
|
-
return invalidRegistrySnapshot(
|
|
625
|
-
"invalid-agent-hierarchy",
|
|
626
|
-
"live snapshot agents cannot be deleted",
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
622
|
const agentIds = new Set(agents.map((agent) => agent.agent_id));
|
|
630
623
|
if (tombstones.some((agentId) => agentIds.has(agentId))) {
|
|
631
624
|
return invalidRegistrySnapshot(
|
|
@@ -1245,8 +1238,7 @@ export function replayRegistryEntries(
|
|
|
1245
1238
|
if (
|
|
1246
1239
|
event.agent.active_turn_id ||
|
|
1247
1240
|
event.agent.latest_result ||
|
|
1248
|
-
event.agent.recent_messages.length > 0
|
|
1249
|
-
event.agent.deleted
|
|
1241
|
+
event.agent.recent_messages.length > 0
|
|
1250
1242
|
) {
|
|
1251
1243
|
reportDiagnostic(
|
|
1252
1244
|
diagnostics,
|
|
@@ -5,15 +5,9 @@ import type {
|
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Type, type Static } from "typebox";
|
|
7
7
|
import { Value } from "typebox/value";
|
|
8
|
+
import { COORDINATOR_TOOL_NAMES } from "./minimal-subagents-capabilities.js";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
export type CoordinatorToolName =
|
|
11
|
-
| "subagent"
|
|
12
|
-
| "agent_message"
|
|
13
|
-
| "subagent_wait"
|
|
14
|
-
| "subagent_status"
|
|
15
|
-
| "subagent_cancel"
|
|
16
|
-
| "subagent_delete";
|
|
10
|
+
export type CoordinatorToolName = (typeof COORDINATOR_TOOL_NAMES)[number];
|
|
17
11
|
|
|
18
12
|
const RenderUsageSchema = Type.Object({
|
|
19
13
|
input: Type.Number(),
|
|
@@ -55,6 +49,11 @@ const RenderRecentMessageSchema = Type.Object({
|
|
|
55
49
|
source_agent_id: Type.Optional(Type.String()),
|
|
56
50
|
content: Type.Optional(Type.String()),
|
|
57
51
|
});
|
|
52
|
+
const RenderRecentActivitySchema = Type.Object({
|
|
53
|
+
label: Type.String(),
|
|
54
|
+
content: Type.String(),
|
|
55
|
+
truncated: Type.Boolean(),
|
|
56
|
+
});
|
|
58
57
|
|
|
59
58
|
/** Structurally parsed hierarchy status used only by transcript presentation. */
|
|
60
59
|
export const RenderStatusAgentSchema = Type.Object({
|
|
@@ -80,6 +79,7 @@ export const RenderStatusAgentSchema = Type.Object({
|
|
|
80
79
|
capability_ceiling: Type.Optional(Type.Array(Type.String())),
|
|
81
80
|
spawn_entry_id: Type.Optional(Type.String()),
|
|
82
81
|
recent_messages: Type.Optional(Type.Array(RenderRecentMessageSchema)),
|
|
82
|
+
recent_activity: Type.Optional(Type.Array(RenderRecentActivitySchema)),
|
|
83
83
|
latest_result: Type.Optional(RenderTurnResultSchema),
|
|
84
84
|
missing_dependencies: Type.Optional(Type.Array(Type.String())),
|
|
85
85
|
unavailable_reason: Type.Optional(Type.String()),
|
|
@@ -148,6 +148,13 @@ const WaitTurnDetailsSchema = Type.Object({
|
|
|
148
148
|
usage: Type.Optional(RenderUsageSchema),
|
|
149
149
|
messages: Type.Optional(Type.Array(WaitMessageDetailsSchema)),
|
|
150
150
|
});
|
|
151
|
+
const WaitTimeoutDetailsSchema = Type.Object({
|
|
152
|
+
event: Type.Literal("timeout"),
|
|
153
|
+
agent_id: Type.String(),
|
|
154
|
+
turn_id: Type.String(),
|
|
155
|
+
timeout_ms: Type.Number(),
|
|
156
|
+
agent: RenderStatusAgentSchema,
|
|
157
|
+
});
|
|
151
158
|
const StatusDetailsSchema = Type.Union([
|
|
152
159
|
Type.Object({
|
|
153
160
|
parent_id: Type.Optional(Type.String()),
|
|
@@ -165,7 +172,6 @@ const DeleteDetailsSchema = Type.Object({
|
|
|
165
172
|
agent_id: Type.String(),
|
|
166
173
|
recursive: Type.Boolean(),
|
|
167
174
|
deleted_agent_ids: Type.Array(Type.String()),
|
|
168
|
-
tombstoned_agent_ids: Type.Array(Type.String()),
|
|
169
175
|
trashed_session_files: Type.Array(Type.String()),
|
|
170
176
|
failures: Type.Array(
|
|
171
177
|
Type.Object({
|
|
@@ -179,7 +185,11 @@ const MessageRenderDetailsSchema = Type.Union([
|
|
|
179
185
|
CurrentMessageDetailsSchema,
|
|
180
186
|
LegacyMessageDetailsSchema,
|
|
181
187
|
]);
|
|
182
|
-
const WaitRenderDetailsSchema = Type.Union([
|
|
188
|
+
const WaitRenderDetailsSchema = Type.Union([
|
|
189
|
+
WaitMessageDetailsSchema,
|
|
190
|
+
WaitTurnDetailsSchema,
|
|
191
|
+
WaitTimeoutDetailsSchema,
|
|
192
|
+
]);
|
|
183
193
|
|
|
184
194
|
export type SpawnCallArguments = Static<typeof SpawnCallArgumentsSchema>;
|
|
185
195
|
export type MessageCallArguments = Static<typeof MessageCallArgumentsSchema>;
|
|
@@ -62,7 +62,8 @@ type SubagentPresentationStatus =
|
|
|
62
62
|
| "delivered"
|
|
63
63
|
| "delivered-via-wait"
|
|
64
64
|
| "queued"
|
|
65
|
-
| "message"
|
|
65
|
+
| "message"
|
|
66
|
+
| "timed out";
|
|
66
67
|
|
|
67
68
|
type SubagentStatusPresentation = { readonly symbol: string; readonly color: ThemeColor };
|
|
68
69
|
|
|
@@ -79,6 +80,7 @@ const SUBAGENT_STATUS_PRESENTATION = {
|
|
|
79
80
|
"delivered-via-wait": { symbol: "→", color: "accent" },
|
|
80
81
|
queued: { symbol: "↗", color: "accent" },
|
|
81
82
|
message: { symbol: "→", color: "accent" },
|
|
83
|
+
"timed out": { symbol: "!", color: "warning" },
|
|
82
84
|
} satisfies { readonly [Status in SubagentPresentationStatus]: SubagentStatusPresentation };
|
|
83
85
|
|
|
84
86
|
function coordinatorMessageText(content: RenderableCoordinatorMessage["content"]): string {
|
|
@@ -96,11 +98,21 @@ function toolResultText(result: AgentToolResult<unknown>): string {
|
|
|
96
98
|
return text?.type === "text" ? text.text : "";
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
/** Shared unavailable → running → latest-turn → idle status ladder for one subagent. */
|
|
102
|
+
export function subagentStatusLadder(agent: {
|
|
103
|
+
readonly availability?: string;
|
|
104
|
+
readonly state?: string;
|
|
105
|
+
readonly latest_turn?: { readonly status?: string };
|
|
106
|
+
}): string {
|
|
107
|
+
if (agent.availability === "unavailable") return "unavailable";
|
|
108
|
+
if (agent.state === "running") return "running";
|
|
109
|
+
return agent.latest_turn?.status ?? "idle";
|
|
110
|
+
}
|
|
111
|
+
|
|
99
112
|
function subagentStatusPresentation(status: string): SubagentStatusPresentation {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return SUBAGENT_STATUS_PRESENTATION.idle;
|
|
113
|
+
// SAFETY: unknown statuses fall back to the idle presentation below.
|
|
114
|
+
const known = status as keyof typeof SUBAGENT_STATUS_PRESENTATION;
|
|
115
|
+
return SUBAGENT_STATUS_PRESENTATION[known] ?? SUBAGENT_STATUS_PRESENTATION.idle;
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
/** Render the shared semantic symbol and color for one subagent status. */
|
|
@@ -338,10 +350,19 @@ function renderWaitResult(
|
|
|
338
350
|
? "waiting"
|
|
339
351
|
: details.event === "message"
|
|
340
352
|
? "message"
|
|
341
|
-
: details.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
353
|
+
: details.event === "timeout"
|
|
354
|
+
? "timed out"
|
|
355
|
+
: details.status;
|
|
356
|
+
const duration = formatSubagentDuration(
|
|
357
|
+
details.event === "timeout" ? details.timeout_ms : details.elapsed_ms,
|
|
358
|
+
);
|
|
359
|
+
const tokens = formatSubagentTokenCount(
|
|
360
|
+
details.event === "timeout" ? undefined : details.usage?.totalTokens,
|
|
361
|
+
);
|
|
362
|
+
const drainedMessageCount =
|
|
363
|
+
details.event === "message" || details.event === "timeout"
|
|
364
|
+
? 0
|
|
365
|
+
: (details.messages?.length ?? 0);
|
|
345
366
|
const metrics = [
|
|
346
367
|
duration,
|
|
347
368
|
tokens ? `${tokens} tokens` : undefined,
|
|
@@ -359,6 +380,15 @@ function renderWaitResult(
|
|
|
359
380
|
container.addChild(renderLabelValue(theme, "Message ID", details.message_id));
|
|
360
381
|
return container;
|
|
361
382
|
}
|
|
383
|
+
if (details.event === "timeout") {
|
|
384
|
+
appendComponentSection(
|
|
385
|
+
container,
|
|
386
|
+
theme,
|
|
387
|
+
"Child status",
|
|
388
|
+
renderDetailedStatusAgent(details.agent, options, theme),
|
|
389
|
+
);
|
|
390
|
+
return container;
|
|
391
|
+
}
|
|
362
392
|
if (details.messages && details.messages.length > 0) {
|
|
363
393
|
appendTextSection(
|
|
364
394
|
container,
|
|
@@ -402,9 +432,7 @@ function countDirectStatusAgents(agents: readonly RenderStatusAgent[]): DirectSt
|
|
|
402
432
|
}
|
|
403
433
|
|
|
404
434
|
function statusAgentPresentation(agent: RenderStatusAgent): string {
|
|
405
|
-
|
|
406
|
-
if (agent.state === "running") return "running";
|
|
407
|
-
return agent.latest_turn?.status ?? "idle";
|
|
435
|
+
return subagentStatusLadder(agent);
|
|
408
436
|
}
|
|
409
437
|
|
|
410
438
|
function renderDirectStatusRows(
|
|
@@ -484,6 +512,20 @@ function renderDetailedStatusAgent(
|
|
|
484
512
|
if (agent.unavailable_reason) {
|
|
485
513
|
appendTextSection(container, theme, "Unavailable reason", agent.unavailable_reason);
|
|
486
514
|
}
|
|
515
|
+
const recentActivity = agent.recent_activity ?? [];
|
|
516
|
+
if (recentActivity.length > 0) {
|
|
517
|
+
appendTextSection(
|
|
518
|
+
container,
|
|
519
|
+
theme,
|
|
520
|
+
"Recent activity",
|
|
521
|
+
recentActivity
|
|
522
|
+
.map(
|
|
523
|
+
(activity) =>
|
|
524
|
+
`${activity.label}${activity.truncated ? " (truncated)" : ""}\n${activity.content}`,
|
|
525
|
+
)
|
|
526
|
+
.join("\n\n"),
|
|
527
|
+
);
|
|
528
|
+
}
|
|
487
529
|
const recentMessages = agent.recent_messages ?? [];
|
|
488
530
|
if (recentMessages.length > 0) {
|
|
489
531
|
appendTextSection(
|
|
@@ -577,7 +619,7 @@ function renderDeleteResult(
|
|
|
577
619
|
const status = details.failures.length > 0 ? "failed" : "completed";
|
|
578
620
|
const metrics = [
|
|
579
621
|
`${details.deleted_agent_ids.length} agents deleted`,
|
|
580
|
-
`${details.
|
|
622
|
+
`${details.deleted_agent_ids.length} tombstones`,
|
|
581
623
|
details.failures.length > 0 ? `${details.failures.length} failed` : undefined,
|
|
582
624
|
].filter((metric): metric is string => metric !== undefined);
|
|
583
625
|
const summary = renderSubagentSummary(theme, status, details.agent_id, metrics);
|
|
@@ -594,12 +636,6 @@ function renderDeleteResult(
|
|
|
594
636
|
"Deleted agents",
|
|
595
637
|
details.deleted_agent_ids.join("\n") || "(none)",
|
|
596
638
|
);
|
|
597
|
-
appendTextSection(
|
|
598
|
-
container,
|
|
599
|
-
theme,
|
|
600
|
-
"Tombstones",
|
|
601
|
-
details.tombstoned_agent_ids.join("\n") || "(none)",
|
|
602
|
-
);
|
|
603
639
|
appendTextSection(
|
|
604
640
|
container,
|
|
605
641
|
theme,
|
|
@@ -131,11 +131,35 @@ export function buildDepthBoundSubagentPrompt(
|
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
/** Canonicalize one path, resolving symlinks only when the target exists. */
|
|
135
|
+
export function canonicalPath(path: string): string {
|
|
135
136
|
const absolutePath = resolve(path);
|
|
136
137
|
return existsSync(absolutePath) ? realpathSync(absolutePath) : absolutePath;
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
/** Build the shared unavailable-agent projection used by fork recovery and ownership binding. */
|
|
141
|
+
export function unavailableAgent(
|
|
142
|
+
agent: PersistedAgent,
|
|
143
|
+
error: string,
|
|
144
|
+
latestActivityAt?: string,
|
|
145
|
+
): PersistedAgent {
|
|
146
|
+
// SAFETY: the spread preserves every required PersistedAgent field; only optional session fields are cleared.
|
|
147
|
+
const unavailable = {
|
|
148
|
+
...structuredClone(agent),
|
|
149
|
+
session_file: undefined,
|
|
150
|
+
session_id: undefined,
|
|
151
|
+
session_leaf_id: undefined,
|
|
152
|
+
clone_error: error,
|
|
153
|
+
active_turn_id: undefined,
|
|
154
|
+
active_turn_started_at: undefined,
|
|
155
|
+
availability: "unavailable",
|
|
156
|
+
missing_dependencies: [error],
|
|
157
|
+
unavailable_reason: error,
|
|
158
|
+
} as PersistedAgent;
|
|
159
|
+
if (latestActivityAt !== undefined) unavailable.latest_activity_at = latestActivityAt;
|
|
160
|
+
return unavailable;
|
|
161
|
+
}
|
|
162
|
+
|
|
139
163
|
function appendImportedMessage(sessionManager: SessionManager, message: AgentMessage): void {
|
|
140
164
|
if (message.role === "compactionSummary") {
|
|
141
165
|
sessionManager.appendCustomMessageEntry(
|
|
@@ -439,54 +463,40 @@ function assistantText(message: AgentMessage | undefined): string {
|
|
|
439
463
|
.join("\n");
|
|
440
464
|
}
|
|
441
465
|
|
|
442
|
-
/**
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
466
|
+
/** Collect finalized turn messages and reduce them to one runtime outcome. */
|
|
467
|
+
function collectChildTurnOutcome(
|
|
468
|
+
messages: readonly AgentMessage[],
|
|
469
|
+
aborted: boolean,
|
|
470
|
+
): RuntimeTurnOutcome {
|
|
471
|
+
const finalAssistant = [...messages].reverse().find((message) => message.role === "assistant");
|
|
472
|
+
if (!finalAssistant || finalAssistant.role !== "assistant") {
|
|
473
|
+
return {
|
|
474
|
+
status: aborted ? "cancelled" : "failed",
|
|
475
|
+
output: "",
|
|
476
|
+
error: "No terminal assistant response",
|
|
477
|
+
};
|
|
451
478
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
479
|
+
if (finalAssistant.stopReason === "aborted") {
|
|
480
|
+
return {
|
|
481
|
+
status: "cancelled",
|
|
482
|
+
output: assistantText(finalAssistant),
|
|
483
|
+
error: finalAssistant.errorMessage,
|
|
484
|
+
usage: sumUsage(messages),
|
|
485
|
+
};
|
|
455
486
|
}
|
|
456
|
-
|
|
457
|
-
toOutcome(aborted: boolean): RuntimeTurnOutcome {
|
|
458
|
-
const finalAssistant = [...this.messages]
|
|
459
|
-
.reverse()
|
|
460
|
-
.find((message) => message.role === "assistant");
|
|
461
|
-
if (!finalAssistant || finalAssistant.role !== "assistant") {
|
|
462
|
-
return {
|
|
463
|
-
status: aborted ? "cancelled" : "failed",
|
|
464
|
-
output: "",
|
|
465
|
-
error: "No terminal assistant response",
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
if (finalAssistant.stopReason === "aborted") {
|
|
469
|
-
return {
|
|
470
|
-
status: "cancelled",
|
|
471
|
-
output: assistantText(finalAssistant),
|
|
472
|
-
error: finalAssistant.errorMessage,
|
|
473
|
-
usage: sumUsage(this.messages),
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
if (finalAssistant.stopReason === "error") {
|
|
477
|
-
return {
|
|
478
|
-
status: "failed",
|
|
479
|
-
output: assistantText(finalAssistant),
|
|
480
|
-
error: finalAssistant.errorMessage ?? "Provider request failed",
|
|
481
|
-
usage: sumUsage(this.messages),
|
|
482
|
-
};
|
|
483
|
-
}
|
|
487
|
+
if (finalAssistant.stopReason === "error") {
|
|
484
488
|
return {
|
|
485
|
-
status: "
|
|
489
|
+
status: "failed",
|
|
486
490
|
output: assistantText(finalAssistant),
|
|
487
|
-
|
|
491
|
+
error: finalAssistant.errorMessage ?? "Provider request failed",
|
|
492
|
+
usage: sumUsage(messages),
|
|
488
493
|
};
|
|
489
494
|
}
|
|
495
|
+
return {
|
|
496
|
+
status: "completed",
|
|
497
|
+
output: assistantText(finalAssistant),
|
|
498
|
+
usage: sumUsage(messages),
|
|
499
|
+
};
|
|
490
500
|
}
|
|
491
501
|
|
|
492
502
|
/** Run one child operation while retaining its finalized outcome across compaction. */
|
|
@@ -495,10 +505,13 @@ export async function captureChildTurnOutcome(
|
|
|
495
505
|
operation: () => Promise<void>,
|
|
496
506
|
isAborted: () => boolean,
|
|
497
507
|
): Promise<RuntimeTurnOutcome> {
|
|
498
|
-
const
|
|
508
|
+
const messages: AgentMessage[] = [];
|
|
509
|
+
const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
510
|
+
if (event.type === "message_end") messages.push(event.message);
|
|
511
|
+
});
|
|
499
512
|
try {
|
|
500
513
|
await operation();
|
|
501
|
-
return
|
|
514
|
+
return collectChildTurnOutcome(messages, isAborted());
|
|
502
515
|
} catch (error) {
|
|
503
516
|
return {
|
|
504
517
|
status: isAborted() ? "cancelled" : "failed",
|
|
@@ -506,7 +519,7 @@ export async function captureChildTurnOutcome(
|
|
|
506
519
|
error: error instanceof Error ? error.message : String(error),
|
|
507
520
|
};
|
|
508
521
|
} finally {
|
|
509
|
-
|
|
522
|
+
unsubscribe();
|
|
510
523
|
}
|
|
511
524
|
}
|
|
512
525
|
|
|
@@ -600,6 +613,11 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
600
613
|
return snapshotCommittedContext(this.session.messages, this.session.isStreaming);
|
|
601
614
|
}
|
|
602
615
|
|
|
616
|
+
snapshotActivityMessages(): AgentMessage[] {
|
|
617
|
+
const streamingMessage = this.session.state.streamingMessage;
|
|
618
|
+
return [...this.session.messages, ...(streamingMessage ? [streamingMessage] : [])];
|
|
619
|
+
}
|
|
620
|
+
|
|
603
621
|
hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean {
|
|
604
622
|
return findDeliveryEvidence(
|
|
605
623
|
this.session.sessionManager.getBranch(),
|
|
@@ -202,7 +202,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
202
202
|
name: "subagent_wait",
|
|
203
203
|
label: "Subagent Wait",
|
|
204
204
|
description:
|
|
205
|
-
"Wait for one direct child's oldest observable turn, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. An already settled turn returns event=turn once with queued messages in messages. Timeout never cancels the child.",
|
|
205
|
+
"Wait for one direct child's oldest observable turn, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. An already settled turn returns event=turn once with queued messages in messages. Timeout returns event=timeout with detailed child status and never cancels the child.",
|
|
206
206
|
promptSnippet: "Wait for one direct child's exact turn",
|
|
207
207
|
parameters: options.schemas.subagent_wait,
|
|
208
208
|
async execute(_toolCallId, parameters, signal, onUpdate) {
|
|
@@ -248,7 +248,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
248
248
|
name: "subagent_status",
|
|
249
249
|
label: "Subagent Status",
|
|
250
250
|
description:
|
|
251
|
-
"List direct children when agent_id is omitted, or inspect one direct child's launch contract, result, usage, and
|
|
251
|
+
"List direct children when agent_id is omitted, or inspect one direct child's launch contract, result, usage, dependencies, and bounded recent activity including message text and reasoning.",
|
|
252
252
|
promptSnippet: "Inspect direct child state",
|
|
253
253
|
parameters: options.schemas.subagent_status,
|
|
254
254
|
async execute(_toolCallId, parameters) {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
3
3
|
|
|
4
|
-
/** Stable identity for one prompt and its complete assistant/tool loop. */
|
|
5
|
-
export type TurnId = string & { readonly __turnId: unique symbol };
|
|
6
|
-
|
|
7
4
|
/** Controls how much committed caller conversation enters a new child session. */
|
|
8
5
|
export type SessionContextMode = "inherit" | "compact" | "omit";
|
|
9
6
|
/** Controls whether child resource discovery includes project instructions, skills, and prompts. */
|
|
@@ -81,8 +78,19 @@ export interface WaitTurnResult extends TurnResult {
|
|
|
81
78
|
messages?: WaitMessageResult[];
|
|
82
79
|
}
|
|
83
80
|
|
|
84
|
-
/** Reports
|
|
85
|
-
export
|
|
81
|
+
/** Reports an observational wait timeout with the current detailed child status. */
|
|
82
|
+
export interface WaitTimeoutResult {
|
|
83
|
+
event: "timeout";
|
|
84
|
+
agent_id: string;
|
|
85
|
+
turn_id: string;
|
|
86
|
+
/** Requested timeout duration that expired. */
|
|
87
|
+
timeout_ms: number;
|
|
88
|
+
/** Detailed child status captured when the timeout callback won. */
|
|
89
|
+
agent: AgentDetail;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Reports one message, terminal turn, or timeout returned by subagent_wait. */
|
|
93
|
+
export type WaitResult = WaitMessageResult | WaitTurnResult | WaitTimeoutResult;
|
|
86
94
|
|
|
87
95
|
/** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
|
|
88
96
|
export interface AgentSummary extends RuntimeProfile {
|
|
@@ -94,7 +102,6 @@ export interface AgentSummary extends RuntimeProfile {
|
|
|
94
102
|
latest_turn?: Pick<TurnResult, "turn_id" | "status">;
|
|
95
103
|
tools: string[];
|
|
96
104
|
elapsed_ms?: number;
|
|
97
|
-
latest_activity?: string;
|
|
98
105
|
latest_activity_at?: string;
|
|
99
106
|
task?: string;
|
|
100
107
|
child_count: number;
|
|
@@ -108,13 +115,22 @@ export interface RecentAgentMessage {
|
|
|
108
115
|
content: string;
|
|
109
116
|
}
|
|
110
117
|
|
|
111
|
-
/**
|
|
118
|
+
/** Reports one labeled, bounded child activity item without image data. */
|
|
119
|
+
export interface RecentAgentActivity {
|
|
120
|
+
label: string;
|
|
121
|
+
content: string;
|
|
122
|
+
truncated: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Extends summary status with launch, dependency, recent-message, and recent-work diagnostics. */
|
|
112
126
|
export interface AgentDetail extends AgentSummary {
|
|
113
127
|
session_file?: string;
|
|
114
128
|
launch_contract: LaunchContract;
|
|
115
129
|
capability_ceiling: string[];
|
|
116
130
|
spawn_entry_id: string;
|
|
117
131
|
recent_messages: RecentAgentMessage[];
|
|
132
|
+
/** The 12 most recent work items, each capped at 20 lines and 2 KiB. */
|
|
133
|
+
recent_activity: RecentAgentActivity[];
|
|
118
134
|
latest_result?: TurnResult;
|
|
119
135
|
missing_dependencies: string[];
|
|
120
136
|
unavailable_reason?: string;
|
|
@@ -142,7 +158,6 @@ export interface DeleteResult {
|
|
|
142
158
|
agent_id: string;
|
|
143
159
|
recursive: boolean;
|
|
144
160
|
deleted_agent_ids: string[];
|
|
145
|
-
tombstoned_agent_ids: string[];
|
|
146
161
|
trashed_session_files: string[];
|
|
147
162
|
failures: Array<{ agent_id: string; error: string }>;
|
|
148
163
|
}
|
|
@@ -163,7 +178,6 @@ export interface CallerSnapshot {
|
|
|
163
178
|
thinkingLevel: ThinkingLevel;
|
|
164
179
|
ordinaryTools: string[];
|
|
165
180
|
capabilityCeiling: string[];
|
|
166
|
-
availableTools: string[];
|
|
167
181
|
spawnEntryId: string;
|
|
168
182
|
}
|
|
169
183
|
|
|
@@ -209,7 +223,10 @@ export interface ChildAgentRuntime {
|
|
|
209
223
|
dispose(): void;
|
|
210
224
|
/** Return the live Runtime Profile, or undefined when the SDK session has no model. */
|
|
211
225
|
getRuntimeProfile(): RuntimeProfile | undefined;
|
|
226
|
+
/** Clone committed child transcript messages while excluding the streaming assistant tail. */
|
|
212
227
|
snapshotCommittedMessages(): AgentMessage[];
|
|
228
|
+
/** Clone child transcript messages including the current streaming assistant tail. */
|
|
229
|
+
snapshotActivityMessages(): AgentMessage[];
|
|
213
230
|
hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean;
|
|
214
231
|
getUsage(): Usage | undefined;
|
|
215
232
|
}
|
|
@@ -276,7 +293,6 @@ export interface PersistedAgent {
|
|
|
276
293
|
missing_dependencies: string[];
|
|
277
294
|
unavailable_reason?: string;
|
|
278
295
|
recent_messages: RecentAgentMessage[];
|
|
279
|
-
deleted?: boolean;
|
|
280
296
|
}
|
|
281
297
|
|
|
282
298
|
/** Records which conversation path owns one successful terminal result. */
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
formatSubagentDuration,
|
|
16
16
|
renderSubagentStatusLabel,
|
|
17
17
|
renderSubagentStatusSymbol,
|
|
18
|
+
subagentStatusLadder,
|
|
18
19
|
} from "./minimal-subagents-rendering.js";
|
|
19
20
|
import type {
|
|
20
21
|
AgentSummary,
|
|
@@ -65,9 +66,9 @@ function flattenAgentHierarchy(agents: readonly AgentSummary[]): FlattenedAgentS
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
function agentTerminalStatus(agent: AgentSummary): TurnStatus | "idle" | "unavailable" {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return
|
|
69
|
+
const status = subagentStatusLadder(agent);
|
|
70
|
+
// SAFETY: the shared ladder emits exactly the unavailable/running/TurnStatus/idle vocabulary.
|
|
71
|
+
return status as TurnStatus | "idle" | "unavailable";
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
function terminalTimestamp(agent: AgentSummary): number {
|