@ian-pascoe/pi-minimal-subagents 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -8
- package/package.json +1 -1
- package/src/minimal-subagents-capabilities.ts +3 -8
- package/src/minimal-subagents-context.ts +72 -1
- package/src/minimal-subagents-coordinator.ts +133 -61
- package/src/minimal-subagents-extension.ts +67 -66
- package/src/minimal-subagents-fork-lifecycle.ts +4 -11
- package/src/minimal-subagents-registry-wire.ts +0 -1
- package/src/minimal-subagents-registry.ts +1 -9
- package/src/minimal-subagents-render-contract.ts +20 -10
- package/src/minimal-subagents-rendering.ts +55 -19
- package/src/minimal-subagents-sessions.ts +66 -46
- package/src/minimal-subagents-tools.ts +2 -2
- package/src/minimal-subagents-types.ts +29 -10
- package/src/minimal-subagents-ui.ts +4 -3
|
@@ -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
|
}
|
|
@@ -99,6 +100,7 @@ function createRootConversationEndpoint(
|
|
|
99
100
|
},
|
|
100
101
|
);
|
|
101
102
|
},
|
|
103
|
+
isIdle: () => context.isIdle(),
|
|
102
104
|
hasDeliveryEvidence: (sourceAgentId, sourceTurnId, deliveryId) =>
|
|
103
105
|
findDeliveryEvidence(
|
|
104
106
|
context.sessionManager.getBranch(),
|
|
@@ -182,57 +184,84 @@ function orderForkAgentsParentFirst(agents: readonly PersistedAgent[]): Persiste
|
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
function unavailableForkAgent(agent: PersistedAgent, error: string): PersistedAgent {
|
|
185
|
-
return
|
|
186
|
-
...structuredClone(agent),
|
|
187
|
-
session_file: undefined,
|
|
188
|
-
session_id: undefined,
|
|
189
|
-
session_leaf_id: undefined,
|
|
190
|
-
clone_error: error,
|
|
191
|
-
active_turn_id: undefined,
|
|
192
|
-
active_turn_started_at: undefined,
|
|
193
|
-
availability: "unavailable",
|
|
194
|
-
missing_dependencies: [error],
|
|
195
|
-
unavailable_reason: error,
|
|
196
|
-
};
|
|
187
|
+
return unavailableAgent(agent, error);
|
|
197
188
|
}
|
|
198
189
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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,
|
|
204
207
|
context: ExtensionContext,
|
|
205
|
-
): Promise<
|
|
208
|
+
): Promise<PersistedAgent[]> {
|
|
206
209
|
const agents: PersistedAgent[] = [];
|
|
207
210
|
const failedSubtrees = new Set<string>();
|
|
208
|
-
for (const agent of orderForkAgentsParentFirst(
|
|
211
|
+
for (const agent of orderForkAgentsParentFirst(snapshotAgents)) {
|
|
209
212
|
const failedAncestor = [...failedSubtrees].find(
|
|
210
213
|
(agentId) => agent.agent_id === agentId || agent.agent_id.startsWith(`${agentId}.`),
|
|
211
214
|
);
|
|
212
|
-
|
|
213
|
-
|
|
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));
|
|
214
223
|
continue;
|
|
215
224
|
}
|
|
216
225
|
try {
|
|
217
|
-
const
|
|
218
|
-
if (!
|
|
226
|
+
const identity = await rebind(agent);
|
|
227
|
+
if (!identity.sessionLeafId) {
|
|
219
228
|
throw new Error(
|
|
220
229
|
`Minimal subagents fork recovery: no selected session leaf for ${agent.agent_id}`,
|
|
221
230
|
);
|
|
222
231
|
}
|
|
223
232
|
agents.push({
|
|
224
233
|
...structuredClone(agent),
|
|
225
|
-
session_file:
|
|
226
|
-
session_id:
|
|
227
|
-
session_leaf_id:
|
|
234
|
+
session_file: identity.sessionFile,
|
|
235
|
+
session_id: identity.sessionId,
|
|
236
|
+
session_leaf_id: identity.sessionLeafId,
|
|
228
237
|
});
|
|
229
238
|
} catch (error) {
|
|
230
239
|
const message = error instanceof Error ? error.message : String(error);
|
|
231
240
|
failedSubtrees.add(agent.agent_id);
|
|
232
241
|
agents.push(unavailableForkAgent(agent, message));
|
|
233
|
-
context.ui.notify(
|
|
242
|
+
context.ui.notify(`${options.notifyLabel} for ${agent.agent_id}: ${message}`, "error");
|
|
234
243
|
}
|
|
235
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
|
+
);
|
|
236
265
|
return {
|
|
237
266
|
...structuredClone(snapshot),
|
|
238
267
|
source_root_session_file: sourceRootSessionFile,
|
|
@@ -246,44 +275,16 @@ async function bindForkSnapshotToDestination(
|
|
|
246
275
|
snapshot: ForkSnapshot,
|
|
247
276
|
context: ExtensionContext,
|
|
248
277
|
): Promise<ForkSnapshot> {
|
|
249
|
-
const agents
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
agent.clone_error ?? `Ancestor ownership failed: ${failedAncestor ?? agent.agent_id}`,
|
|
260
|
-
),
|
|
261
|
-
);
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
try {
|
|
265
|
-
const owned = await sessionFactory.adoptForkSessionOwnership(
|
|
266
|
-
agent,
|
|
267
|
-
snapshot.source_root_session_id,
|
|
268
|
-
);
|
|
269
|
-
if (!owned.sessionLeafId) {
|
|
270
|
-
throw new Error(
|
|
271
|
-
`Minimal subagents fork ownership: no selected session leaf for ${agent.agent_id}`,
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
agents.push({
|
|
275
|
-
...structuredClone(agent),
|
|
276
|
-
session_file: owned.sessionFile,
|
|
277
|
-
session_id: owned.sessionId,
|
|
278
|
-
session_leaf_id: owned.sessionLeafId,
|
|
279
|
-
});
|
|
280
|
-
} catch (error) {
|
|
281
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
282
|
-
failedSubtrees.add(agent.agent_id);
|
|
283
|
-
agents.push(unavailableForkAgent(agent, message));
|
|
284
|
-
context.ui.notify(`Fork ownership failed for ${agent.agent_id}: ${message}`, "error");
|
|
285
|
-
}
|
|
286
|
-
}
|
|
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
|
+
);
|
|
287
288
|
return { ...structuredClone(snapshot), agents };
|
|
288
289
|
}
|
|
289
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
|
|
|
@@ -520,6 +533,8 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
520
533
|
private readonly modelById: ReadonlyMap<string, Model<any>>,
|
|
521
534
|
onSessionActivity?: () => void,
|
|
522
535
|
) {
|
|
536
|
+
// Keep this child-only; AgentSession.setSteeringMode would overwrite the user's global setting.
|
|
537
|
+
session.agent.steeringMode = "all";
|
|
523
538
|
this.unsubscribe = session.subscribe((event) => {
|
|
524
539
|
if (event.type !== "entry_appended") return;
|
|
525
540
|
if (
|
|
@@ -598,6 +613,11 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
598
613
|
return snapshotCommittedContext(this.session.messages, this.session.isStreaming);
|
|
599
614
|
}
|
|
600
615
|
|
|
616
|
+
snapshotActivityMessages(): AgentMessage[] {
|
|
617
|
+
const streamingMessage = this.session.state.streamingMessage;
|
|
618
|
+
return [...this.session.messages, ...(streamingMessage ? [streamingMessage] : [])];
|
|
619
|
+
}
|
|
620
|
+
|
|
601
621
|
hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean {
|
|
602
622
|
return findDeliveryEvidence(
|
|
603
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) {
|