@oai404iao/pi-subagent 0.2.0 → 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 -2
- package/config.example.json +1 -0
- package/config.schema.json +5 -0
- package/package.json +5 -2
- package/src/catalog.ts +10 -7
- package/src/config.ts +6 -0
- package/src/coordinator.ts +176 -59
- package/src/descriptor.ts +15 -2
- package/src/index.ts +9 -8
- package/src/providers.ts +2 -0
- package/src/render.ts +2 -2
- package/src/schemas.ts +5 -2
- package/src/types.ts +19 -9
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Compatibility: Pi 0.84.2 or newer; tested against 0.84.2.
|
|
|
18
18
|
- `fork`: one-shot child seeded through the parent's latest completed turn
|
|
19
19
|
- **Two lifecycles**
|
|
20
20
|
- foreground one-shot runs return the child's final answer
|
|
21
|
-
- background continuable runs return a
|
|
21
|
+
- background continuable runs return a durable agent id immediately
|
|
22
22
|
- **Foreground-only policy** that removes background scheduling and lifecycle controls
|
|
23
23
|
- **Independent context and session** for every child
|
|
24
24
|
- **Bundled presets without filesystem writes by default**, with opt-in
|
|
@@ -242,6 +242,7 @@ See [`config.example.json`](config.example.json) and [`config.schema.json`](conf
|
|
|
242
242
|
"defaultBackground": true,
|
|
243
243
|
"reportDelivery": "wakeup",
|
|
244
244
|
"inheritExtensions": false,
|
|
245
|
+
"openAIIdentity": false,
|
|
245
246
|
"maxOutputBytes": 51200
|
|
246
247
|
}
|
|
247
248
|
```
|
|
@@ -255,10 +256,21 @@ See [`config.example.json`](config.example.json) and [`config.schema.json`](conf
|
|
|
255
256
|
| `defaultBackground` | `true` | Default scheduling for fresh `subagent` calls when background execution is enabled. |
|
|
256
257
|
| `reportDelivery` | `wakeup` | `wakeup` starts/queues a parent turn; `quiet` waits for the parent's next turn. |
|
|
257
258
|
| `inheritExtensions` | `false` | Load other Pi extensions in child runtimes. This package filters itself out; explicit agent tool ceilings still apply. |
|
|
259
|
+
| `openAIIdentity` | `false` | For OpenAI Responses child models, inject only the named `pi-codex-minimal-tools` identity lifecycle inline. Codex Session/Thread/Turn/Window ids remain owned and serialized by that package. |
|
|
258
260
|
| `maxOutputBytes` | `51200` | Cap for parent-visible foreground output, reports, and settlement notices. Full output remains in the child session. |
|
|
259
261
|
|
|
260
262
|
Invalid configuration and unknown child tool names fail loud before the child's first model request.
|
|
261
263
|
|
|
264
|
+
`openAIIdentity` and `inheritExtensions` are independent. The former adds only
|
|
265
|
+
the lightweight Codex identity lifecycle even when normal extension inheritance
|
|
266
|
+
is disabled. Enable `inheritExtensions` as well when the child should receive
|
|
267
|
+
the complete separately installed Codex extension tool surface such as
|
|
268
|
+
`web_search` and `apply_patch`.
|
|
269
|
+
|
|
270
|
+
The Codex adapter is an optional package dependency. If an installation omits
|
|
271
|
+
optional dependencies, `openAIIdentity: true` fails before the child starts
|
|
272
|
+
with an actionable missing-adapter error.
|
|
273
|
+
|
|
262
274
|
### Foreground-only mode
|
|
263
275
|
|
|
264
276
|
```json
|
|
@@ -301,7 +313,11 @@ The caller waits for one isolated child run. Only the child's last non-empty ass
|
|
|
301
313
|
|
|
302
314
|
### Continuable
|
|
303
315
|
|
|
304
|
-
The start tool resolves at prompt preflight acceptance and returns the
|
|
316
|
+
The start tool resolves at prompt preflight acceptance and returns the child's durable
|
|
317
|
+
agent id (UUIDv7). Agent ids are independent of Pi session (file) ids: they are
|
|
318
|
+
generated once per subagent, recorded in the child's session as `pi-subagent/agent`,
|
|
319
|
+
and chained through `parentAgentId` in the descriptor, so children stay addressable
|
|
320
|
+
even when a parent session is forked or re-created. When an activation settles:
|
|
305
321
|
|
|
306
322
|
1. the runtime sends the parent a settlement notice with the stop reason and closing message;
|
|
307
323
|
2. the child runtime is disposed once its owned descendants are done;
|
package/config.example.json
CHANGED
package/config.schema.json
CHANGED
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
"description": "Load the parent's other Pi extensions in child runtimes. pi-subagent itself is filtered out and explicit agent tool ceilings still apply.",
|
|
51
51
|
"default": false
|
|
52
52
|
},
|
|
53
|
+
"openAIIdentity": {
|
|
54
|
+
"type": "boolean",
|
|
55
|
+
"description": "Inject the pi-codex-minimal-tools identity lifecycle as a named inline extension for OpenAI Responses child models.",
|
|
56
|
+
"default": false
|
|
57
|
+
},
|
|
53
58
|
"maxOutputBytes": {
|
|
54
59
|
"type": "integer",
|
|
55
60
|
"minimum": 1024,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oai404iao/pi-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Durable, continuable subagents for Pi with spawn/fork providers and lifecycle controls.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
"@earendil-works/pi-tui": ">=0.84.2",
|
|
38
38
|
"typebox": "*"
|
|
39
39
|
},
|
|
40
|
+
"optionalDependencies": {
|
|
41
|
+
"@oai404iao/pi-codex-minimal-tools": "1.4.0"
|
|
42
|
+
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"@earendil-works/pi-agent-core": "^0.84.2",
|
|
42
45
|
"@earendil-works/pi-ai": "^0.84.2",
|
|
@@ -84,5 +87,5 @@
|
|
|
84
87
|
"optional": true
|
|
85
88
|
}
|
|
86
89
|
},
|
|
87
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "a93559be8f8b02739310c492b36270b13607fbf9"
|
|
88
91
|
}
|
package/src/catalog.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { SessionView } from "./providers.ts";
|
|
|
4
4
|
import type { CatalogDiagnostic, SubagentDescriptor } from "./types.ts";
|
|
5
5
|
|
|
6
6
|
export interface PersistedDescriptor {
|
|
7
|
-
|
|
7
|
+
agentId: string;
|
|
8
8
|
sessionFile: string;
|
|
9
9
|
descriptor: SubagentDescriptor;
|
|
10
10
|
}
|
|
@@ -44,7 +44,7 @@ export async function readPersistedCatalog(session: SessionView): Promise<Persis
|
|
|
44
44
|
if (headerParent !== folded.descriptor.parentSessionFile) {
|
|
45
45
|
diagnostics.push({
|
|
46
46
|
kind: "diagnostic",
|
|
47
|
-
|
|
47
|
+
piSessionId: manager.getSessionId(),
|
|
48
48
|
reason: "corrupt",
|
|
49
49
|
sessionFile: info.path,
|
|
50
50
|
...(headerParent ? { parentSessionFile: headerParent } : {}),
|
|
@@ -53,7 +53,7 @@ export async function readPersistedCatalog(session: SessionView): Promise<Persis
|
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
descriptors.push({
|
|
56
|
-
|
|
56
|
+
agentId: folded.descriptor.agentId,
|
|
57
57
|
sessionFile: info.path,
|
|
58
58
|
descriptor: folded.descriptor,
|
|
59
59
|
});
|
|
@@ -61,7 +61,7 @@ export async function readPersistedCatalog(session: SessionView): Promise<Persis
|
|
|
61
61
|
const headerParent = manager.getHeader()?.parentSession;
|
|
62
62
|
diagnostics.push({
|
|
63
63
|
kind: "diagnostic",
|
|
64
|
-
|
|
64
|
+
piSessionId: manager.getSessionId(),
|
|
65
65
|
reason: "corrupt",
|
|
66
66
|
sessionFile: info.path,
|
|
67
67
|
...(headerParent ? { parentSessionFile: headerParent } : {}),
|
|
@@ -71,7 +71,7 @@ export async function readPersistedCatalog(session: SessionView): Promise<Persis
|
|
|
71
71
|
} catch (error) {
|
|
72
72
|
diagnostics.push({
|
|
73
73
|
kind: "diagnostic",
|
|
74
|
-
|
|
74
|
+
piSessionId: info.id,
|
|
75
75
|
reason: "unavailable",
|
|
76
76
|
sessionFile: info.path,
|
|
77
77
|
...(info.parentSessionPath ? { parentSessionFile: info.parentSessionPath } : {}),
|
|
@@ -82,8 +82,11 @@ export async function readPersistedCatalog(session: SessionView): Promise<Persis
|
|
|
82
82
|
|
|
83
83
|
descriptors.sort(
|
|
84
84
|
(left, right) =>
|
|
85
|
-
left.descriptor.createdAt.localeCompare(right.descriptor.createdAt) ||
|
|
85
|
+
left.descriptor.createdAt.localeCompare(right.descriptor.createdAt) ||
|
|
86
|
+
left.agentId.localeCompare(right.agentId),
|
|
87
|
+
);
|
|
88
|
+
diagnostics.sort((left, right) =>
|
|
89
|
+
left.piSessionId.localeCompare(right.piSessionId),
|
|
86
90
|
);
|
|
87
|
-
diagnostics.sort((left, right) => left.id.localeCompare(right.id));
|
|
88
91
|
return { descriptors, diagnostics };
|
|
89
92
|
}
|
package/src/config.ts
CHANGED
|
@@ -13,6 +13,7 @@ export const DEFAULT_SETTINGS: Readonly<SubagentSettings> = {
|
|
|
13
13
|
defaultBackground: true,
|
|
14
14
|
reportDelivery: "wakeup",
|
|
15
15
|
inheritExtensions: false,
|
|
16
|
+
openAIIdentity: false,
|
|
16
17
|
maxOutputBytes: 50 * 1024,
|
|
17
18
|
};
|
|
18
19
|
|
|
@@ -25,6 +26,7 @@ const CONFIG_KEYS = new Set([
|
|
|
25
26
|
"defaultBackground",
|
|
26
27
|
"reportDelivery",
|
|
27
28
|
"inheritExtensions",
|
|
29
|
+
"openAIIdentity",
|
|
28
30
|
"maxOutputBytes",
|
|
29
31
|
]);
|
|
30
32
|
|
|
@@ -147,6 +149,10 @@ function applyConfig(
|
|
|
147
149
|
config.inheritExtensions === undefined
|
|
148
150
|
? settings.inheritExtensions
|
|
149
151
|
: parseBoolean(config.inheritExtensions, "inheritExtensions", source),
|
|
152
|
+
openAIIdentity:
|
|
153
|
+
config.openAIIdentity === undefined
|
|
154
|
+
? settings.openAIIdentity
|
|
155
|
+
: parseBoolean(config.openAIIdentity, "openAIIdentity", source),
|
|
150
156
|
maxOutputBytes:
|
|
151
157
|
config.maxOutputBytes === undefined
|
|
152
158
|
? settings.maxOutputBytes
|
package/src/coordinator.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { join, relative, resolve } from "node:path";
|
|
3
2
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
4
|
-
import type
|
|
3
|
+
import { type Model, uuidv7 } from "@earendil-works/pi-ai";
|
|
5
4
|
import {
|
|
6
5
|
type AgentSessionEvent,
|
|
7
6
|
type AgentToolResult,
|
|
8
7
|
type AgentToolUpdateCallback,
|
|
9
8
|
type ExtensionAPI,
|
|
10
9
|
type ExtensionContext,
|
|
10
|
+
type InlineExtension,
|
|
11
11
|
type ModelRegistry,
|
|
12
12
|
type ToolDefinition,
|
|
13
13
|
type CreateAgentSessionRuntimeFactory,
|
|
@@ -75,6 +75,10 @@ import {
|
|
|
75
75
|
|
|
76
76
|
const REPORT_CUSTOM_TYPE = "pi-subagent/report";
|
|
77
77
|
const SETTLED_CUSTOM_TYPE = "pi-subagent/settled";
|
|
78
|
+
const AGENT_CUSTOM_TYPE = "pi-subagent/agent";
|
|
79
|
+
const LINEAGE_CUSTOM_TYPE = "pi-subagent/lineage";
|
|
80
|
+
const AGENT_ID_PATTERN =
|
|
81
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
78
82
|
const MAX_TRACE_ITEMS = 100;
|
|
79
83
|
const MAX_TRACE_TEXT = 4000;
|
|
80
84
|
const BACKGROUND_CONTROL_TOOLS = new Set([
|
|
@@ -107,7 +111,7 @@ export type DelegationOutcome =
|
|
|
107
111
|
| { kind: "foreground"; details: DelegationDetails; result: SubagentRunResult };
|
|
108
112
|
|
|
109
113
|
interface ParentRef {
|
|
110
|
-
|
|
114
|
+
agentId: string;
|
|
111
115
|
depth: number;
|
|
112
116
|
cwd: string;
|
|
113
117
|
sessionManager: SessionView;
|
|
@@ -125,8 +129,8 @@ interface ParentRef {
|
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
interface Activation {
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
agentId: string;
|
|
133
|
+
runId: string;
|
|
130
134
|
descriptor: SubagentDescriptor;
|
|
131
135
|
parent: ParentRef;
|
|
132
136
|
runtime: AgentSessionRuntime;
|
|
@@ -158,7 +162,7 @@ interface CreateActivationOptions {
|
|
|
158
162
|
}
|
|
159
163
|
|
|
160
164
|
interface CatalogRecord {
|
|
161
|
-
|
|
165
|
+
agentId: string;
|
|
162
166
|
descriptor: SubagentDescriptor;
|
|
163
167
|
sessionFile?: string;
|
|
164
168
|
active?: Activation;
|
|
@@ -191,6 +195,41 @@ function errorText(error: unknown): string {
|
|
|
191
195
|
return error instanceof Error ? error.message : String(error);
|
|
192
196
|
}
|
|
193
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Read the durable pi-subagent control id recorded for a session, if any.
|
|
200
|
+
*
|
|
201
|
+
* The latest entry wins because a copied/forked Pi session may contain older
|
|
202
|
+
* identity checkpoints.
|
|
203
|
+
*/
|
|
204
|
+
function readAgentId(session: Pick<SessionView, "getEntries">): string | undefined {
|
|
205
|
+
const entries = session.getEntries();
|
|
206
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
207
|
+
const entry = entries[index];
|
|
208
|
+
if (!entry) continue;
|
|
209
|
+
if (entry.type !== "custom" || entry.customType !== AGENT_CUSTOM_TYPE) continue;
|
|
210
|
+
const data = entry.data as { agentId?: unknown } | undefined;
|
|
211
|
+
if (
|
|
212
|
+
typeof data?.agentId === "string"
|
|
213
|
+
&& AGENT_ID_PATTERN.test(data.agentId)
|
|
214
|
+
) {
|
|
215
|
+
return data.agentId;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Resolve the provider-neutral control id for a session, creating it on first
|
|
223
|
+
* use. In-memory SessionManager instances retain custom entries too.
|
|
224
|
+
*/
|
|
225
|
+
function ensureAgentId(session: SessionView): string {
|
|
226
|
+
const existing = readAgentId(session);
|
|
227
|
+
if (existing) return existing;
|
|
228
|
+
const agentId = uuidv7();
|
|
229
|
+
session.appendCustomEntry(AGENT_CUSTOM_TYPE, { agentId });
|
|
230
|
+
return agentId;
|
|
231
|
+
}
|
|
232
|
+
|
|
194
233
|
function stopReasonHeadline(reason: SubagentStopReason): string {
|
|
195
234
|
switch (reason) {
|
|
196
235
|
case "completed":
|
|
@@ -213,6 +252,7 @@ function makeRuntimeSettings(descriptor: SubagentDescriptor): SubagentSettings {
|
|
|
213
252
|
defaultBackground: descriptor.runtime.defaultBackground,
|
|
214
253
|
reportDelivery: descriptor.runtime.reportDelivery,
|
|
215
254
|
inheritExtensions: descriptor.runtime.inheritExtensions,
|
|
255
|
+
openAIIdentity: descriptor.runtime.openAIIdentity,
|
|
216
256
|
maxOutputBytes: descriptor.runtime.maxOutputBytes,
|
|
217
257
|
};
|
|
218
258
|
}
|
|
@@ -222,6 +262,29 @@ function isPathInside(parent: string, child: string): boolean {
|
|
|
222
262
|
return rel === "" || (!rel.startsWith("..") && !rel.startsWith("/"));
|
|
223
263
|
}
|
|
224
264
|
|
|
265
|
+
function isOpenAIResponsesModel(model: Model<any>): boolean {
|
|
266
|
+
return model.api === "openai-responses"
|
|
267
|
+
|| model.api === "openai-codex-responses";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function loadCodexIdentityInlineExtension(
|
|
271
|
+
parentSessionManager: SessionView,
|
|
272
|
+
): Promise<InlineExtension> {
|
|
273
|
+
try {
|
|
274
|
+
const integration = await import(
|
|
275
|
+
"@oai404iao/pi-codex-minimal-tools/subagent-inline"
|
|
276
|
+
);
|
|
277
|
+
return integration.createCodexSubagentInlineExtension({
|
|
278
|
+
parentSessionManager,
|
|
279
|
+
});
|
|
280
|
+
} catch (error) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
"openAIIdentity requires @oai404iao/pi-codex-minimal-tools with its subagent-inline export",
|
|
283
|
+
{ cause: error },
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
225
288
|
export class SubagentCoordinator {
|
|
226
289
|
private readonly providers = new ProviderRegistry();
|
|
227
290
|
private readonly active = new Map<string, Activation>();
|
|
@@ -278,10 +341,15 @@ export class SubagentCoordinator {
|
|
|
278
341
|
const descriptor = folded.kind === "valid" ? folded.descriptor : undefined;
|
|
279
342
|
const modelRuntime = runtimeFromRegistry(ctx.modelRegistry);
|
|
280
343
|
return {
|
|
281
|
-
|
|
344
|
+
agentId: readAgentId(ctx.sessionManager) ?? ensureAgentId(
|
|
345
|
+
ctx.sessionManager as unknown as SessionView,
|
|
346
|
+
),
|
|
282
347
|
depth: descriptor?.depth ?? 0,
|
|
283
348
|
cwd: ctx.cwd,
|
|
284
|
-
|
|
349
|
+
// ExtensionContext narrows the live SessionManager to a read-only
|
|
350
|
+
// view; appending the agent entry through the same instance keeps
|
|
351
|
+
// the in-memory tree and the session file consistent.
|
|
352
|
+
sessionManager: ctx.sessionManager as unknown as SessionView,
|
|
285
353
|
modelRuntime,
|
|
286
354
|
model: ctx.model,
|
|
287
355
|
thinkingLevel: ctx.thinkingLevel ?? "off",
|
|
@@ -362,12 +430,17 @@ export class SubagentCoordinator {
|
|
|
362
430
|
const model = this.resolveModel(parent, agent);
|
|
363
431
|
const thinkingLevel = agent.thinking ?? parent.thinkingLevel;
|
|
364
432
|
const prepared = await provider.prepare(parent, mode);
|
|
433
|
+
parent.agentId = ensureAgentId(parent.sessionManager);
|
|
434
|
+
const openAIIdentityEnabled =
|
|
435
|
+
settings.openAIIdentity && isOpenAIResponsesModel(model);
|
|
365
436
|
const descriptor: SubagentDescriptor = {
|
|
366
|
-
version:
|
|
437
|
+
version: 2,
|
|
367
438
|
mode,
|
|
368
439
|
provider: providerName,
|
|
369
440
|
label: input.description.trim(),
|
|
370
|
-
|
|
441
|
+
agentId: uuidv7(),
|
|
442
|
+
parentAgentId: parent.agentId,
|
|
443
|
+
parentPiSessionId: parent.sessionManager.getSessionId(),
|
|
371
444
|
...(parent.sessionManager.getSessionFile()
|
|
372
445
|
? { parentSessionFile: parent.sessionManager.getSessionFile() }
|
|
373
446
|
: {}),
|
|
@@ -385,9 +458,25 @@ export class SubagentCoordinator {
|
|
|
385
458
|
defaultBackground: settings.defaultBackground,
|
|
386
459
|
reportDelivery: settings.reportDelivery,
|
|
387
460
|
inheritExtensions: settings.inheritExtensions,
|
|
461
|
+
openAIIdentity: openAIIdentityEnabled,
|
|
388
462
|
maxOutputBytes: settings.maxOutputBytes,
|
|
389
463
|
},
|
|
390
464
|
};
|
|
465
|
+
prepared.sessionManager.appendCustomEntry(AGENT_CUSTOM_TYPE, {
|
|
466
|
+
agentId: descriptor.agentId,
|
|
467
|
+
});
|
|
468
|
+
prepared.sessionManager.appendCustomEntry(LINEAGE_CUSTOM_TYPE, {
|
|
469
|
+
version: 1,
|
|
470
|
+
agentId: descriptor.agentId,
|
|
471
|
+
parentAgentId: descriptor.parentAgentId,
|
|
472
|
+
parentPiSessionId: descriptor.parentPiSessionId,
|
|
473
|
+
relation: descriptor.provider,
|
|
474
|
+
agentName: descriptor.agent.name,
|
|
475
|
+
openAIIdentity: openAIIdentityEnabled,
|
|
476
|
+
...(descriptor.parentSessionFile
|
|
477
|
+
? { parentSessionFile: descriptor.parentSessionFile }
|
|
478
|
+
: {}),
|
|
479
|
+
});
|
|
391
480
|
|
|
392
481
|
let activation: Activation | undefined;
|
|
393
482
|
try {
|
|
@@ -399,7 +488,7 @@ export class SubagentCoordinator {
|
|
|
399
488
|
onUpdate,
|
|
400
489
|
});
|
|
401
490
|
if (mode === "continuable" && parent.activation) {
|
|
402
|
-
parent.activation.ownedChildren.add(activation.
|
|
491
|
+
parent.activation.ownedChildren.add(activation.agentId);
|
|
403
492
|
}
|
|
404
493
|
const started = this.startPrompt(activation, input.prompt, signal, mode === "continuable");
|
|
405
494
|
await started.accepted;
|
|
@@ -456,7 +545,7 @@ export class SubagentCoordinator {
|
|
|
456
545
|
prepared: coldPrepared,
|
|
457
546
|
isNew: false,
|
|
458
547
|
});
|
|
459
|
-
if (parent.activation) parent.activation.ownedChildren.add(activation.
|
|
548
|
+
if (parent.activation) parent.activation.ownedChildren.add(activation.agentId);
|
|
460
549
|
} else {
|
|
461
550
|
this.assertDirectParent(parent, activation.descriptor);
|
|
462
551
|
}
|
|
@@ -484,7 +573,7 @@ export class SubagentCoordinator {
|
|
|
484
573
|
const target = this.active.get(targetId);
|
|
485
574
|
if (!target) return;
|
|
486
575
|
if (!(await this.isDescendantOf(parent, target.descriptor))) {
|
|
487
|
-
throw new Error(`subagent ${targetId} is not a live descendant of ${parent.
|
|
576
|
+
throw new Error(`subagent ${targetId} is not a live descendant of ${parent.agentId}`);
|
|
488
577
|
}
|
|
489
578
|
target.status = "failed";
|
|
490
579
|
void target.runtime.session.abort().catch((error) => {
|
|
@@ -495,16 +584,16 @@ export class SubagentCoordinator {
|
|
|
495
584
|
async list(parent: ParentRef, scope: "children" | "descendants"): Promise<CatalogEntry[]> {
|
|
496
585
|
const catalog = await this.catalogRecords(parent);
|
|
497
586
|
const records = catalog.records;
|
|
498
|
-
const byId = new Map(records.map((record) => [record.
|
|
587
|
+
const byId = new Map(records.map((record) => [record.agentId, record]));
|
|
499
588
|
const children: CatalogChild[] = [];
|
|
500
589
|
for (const record of records) {
|
|
501
590
|
if (record.descriptor.mode !== "continuable") continue;
|
|
502
|
-
const distance = this.distanceFrom(parent.
|
|
591
|
+
const distance = this.distanceFrom(parent.agentId, record.descriptor, byId);
|
|
503
592
|
if (distance === undefined || (scope === "children" && distance !== 1)) continue;
|
|
504
593
|
children.push({
|
|
505
594
|
kind: "child",
|
|
506
|
-
|
|
507
|
-
|
|
595
|
+
agentId: record.agentId,
|
|
596
|
+
parentAgentId: record.descriptor.parentAgentId,
|
|
508
597
|
depth: distance,
|
|
509
598
|
descriptor: record.descriptor,
|
|
510
599
|
...(record.sessionFile ? { sessionFile: record.sessionFile } : {}),
|
|
@@ -517,7 +606,8 @@ export class SubagentCoordinator {
|
|
|
517
606
|
}
|
|
518
607
|
children.sort(
|
|
519
608
|
(left, right) =>
|
|
520
|
-
left.descriptor.createdAt.localeCompare(right.descriptor.createdAt) ||
|
|
609
|
+
left.descriptor.createdAt.localeCompare(right.descriptor.createdAt) ||
|
|
610
|
+
left.agentId.localeCompare(right.agentId),
|
|
521
611
|
);
|
|
522
612
|
const parentFile = parent.sessionManager.getSessionFile();
|
|
523
613
|
const diagnostics = parentFile
|
|
@@ -533,7 +623,7 @@ export class SubagentCoordinator {
|
|
|
533
623
|
throw new Error("report is available only to continuable subagents");
|
|
534
624
|
}
|
|
535
625
|
const truncated = truncateUtf8(output, child.descriptor.runtime.maxOutputBytes);
|
|
536
|
-
const content = `Background subagent ${child.
|
|
626
|
+
const content = `Background subagent ${child.agentId} reported:\n\n${truncated.text}${
|
|
537
627
|
truncated.truncated ? `\n\n[Report truncated; ${truncated.omittedBytes} bytes omitted.]` : ""
|
|
538
628
|
}`;
|
|
539
629
|
await child.parent.deliver(
|
|
@@ -541,7 +631,7 @@ export class SubagentCoordinator {
|
|
|
541
631
|
content,
|
|
542
632
|
{
|
|
543
633
|
kind: "report",
|
|
544
|
-
|
|
634
|
+
childAgentId: child.agentId,
|
|
545
635
|
label: child.descriptor.label,
|
|
546
636
|
...(truncated.truncated ? { truncated: true } : {}),
|
|
547
637
|
},
|
|
@@ -653,7 +743,7 @@ export class SubagentCoordinator {
|
|
|
653
743
|
text: `message queued as the next turn for subagent ${params.subagent_id}`,
|
|
654
744
|
},
|
|
655
745
|
],
|
|
656
|
-
details: { kind: "control", action: "send",
|
|
746
|
+
details: { kind: "control", action: "send", agentId: params.subagent_id } satisfies ControlDetails,
|
|
657
747
|
};
|
|
658
748
|
},
|
|
659
749
|
});
|
|
@@ -670,7 +760,7 @@ export class SubagentCoordinator {
|
|
|
670
760
|
await this.interrupt(this.parentForActivation(activation), params.agent_id);
|
|
671
761
|
return {
|
|
672
762
|
content: [{ type: "text", text: `interrupt requested for agent ${params.agent_id}` }],
|
|
673
|
-
details: { kind: "control", action: "interrupt",
|
|
763
|
+
details: { kind: "control", action: "interrupt", agentId: params.agent_id } satisfies ControlDetails,
|
|
674
764
|
};
|
|
675
765
|
},
|
|
676
766
|
});
|
|
@@ -706,7 +796,7 @@ export class SubagentCoordinator {
|
|
|
706
796
|
await this.report(activation, params.output);
|
|
707
797
|
return {
|
|
708
798
|
content: [{ type: "text", text: `report accepted by the agent that started you` }],
|
|
709
|
-
details: { kind: "control", action: "report",
|
|
799
|
+
details: { kind: "control", action: "report", agentId: activation.agentId } satisfies ControlDetails,
|
|
710
800
|
};
|
|
711
801
|
},
|
|
712
802
|
});
|
|
@@ -717,7 +807,7 @@ export class SubagentCoordinator {
|
|
|
717
807
|
outcomeToolResult(outcome: DelegationOutcome): AgentToolResult<DelegationDetails> {
|
|
718
808
|
if (outcome.kind === "continuable") {
|
|
719
809
|
return {
|
|
720
|
-
content: [{ type: "text", text: `started subagent ${outcome.details.
|
|
810
|
+
content: [{ type: "text", text: `started subagent ${outcome.details.agentId}` }],
|
|
721
811
|
details: outcome.details,
|
|
722
812
|
};
|
|
723
813
|
}
|
|
@@ -740,10 +830,12 @@ export class SubagentCoordinator {
|
|
|
740
830
|
if (entries.length === 0) return "(no subagents)";
|
|
741
831
|
return entries
|
|
742
832
|
.map((entry) => {
|
|
743
|
-
if (entry.kind === "diagnostic")
|
|
833
|
+
if (entry.kind === "diagnostic") {
|
|
834
|
+
return `${entry.piSessionId} [diagnostic: ${entry.reason}]`;
|
|
835
|
+
}
|
|
744
836
|
const location =
|
|
745
|
-
scope === "descendants" ? ` parent=${entry.
|
|
746
|
-
return `${entry.
|
|
837
|
+
scope === "descendants" ? ` parent=${entry.parentAgentId} depth=${entry.depth}` : "";
|
|
838
|
+
return `${entry.agentId} [${entry.status}]${location} — ${entry.descriptor.label} (${entry.descriptor.agent.name})`;
|
|
747
839
|
})
|
|
748
840
|
.join("\n");
|
|
749
841
|
}
|
|
@@ -801,6 +893,14 @@ export class SubagentCoordinator {
|
|
|
801
893
|
`cannot materialize subagent: model ${descriptor.model.provider}/${descriptor.model.id} is unavailable`,
|
|
802
894
|
);
|
|
803
895
|
}
|
|
896
|
+
const extensionFactories: InlineExtension[] =
|
|
897
|
+
descriptor.runtime.openAIIdentity && isOpenAIResponsesModel(model)
|
|
898
|
+
? [
|
|
899
|
+
await loadCodexIdentityInlineExtension(
|
|
900
|
+
options.parent.sessionManager,
|
|
901
|
+
),
|
|
902
|
+
]
|
|
903
|
+
: [];
|
|
804
904
|
|
|
805
905
|
const appendSystemPrompt = [
|
|
806
906
|
descriptor.agent.systemPrompt,
|
|
@@ -836,10 +936,13 @@ export class SubagentCoordinator {
|
|
|
836
936
|
noExtensions: !descriptor.runtime.inheritExtensions,
|
|
837
937
|
noThemes: true,
|
|
838
938
|
appendSystemPrompt,
|
|
939
|
+
extensionFactories,
|
|
839
940
|
extensionsOverride: (base) => ({
|
|
840
941
|
...base,
|
|
841
942
|
extensions: base.extensions.filter(
|
|
842
|
-
(extension) =>
|
|
943
|
+
(extension) =>
|
|
944
|
+
extension.resolvedPath.startsWith("<inline:")
|
|
945
|
+
|| !isPathInside(this.packageRoot, extension.resolvedPath),
|
|
843
946
|
),
|
|
844
947
|
}),
|
|
845
948
|
},
|
|
@@ -935,8 +1038,8 @@ export class SubagentCoordinator {
|
|
|
935
1038
|
}
|
|
936
1039
|
|
|
937
1040
|
activation = {
|
|
938
|
-
|
|
939
|
-
|
|
1041
|
+
agentId: descriptor.agentId,
|
|
1042
|
+
runId: uuidv7(),
|
|
940
1043
|
descriptor,
|
|
941
1044
|
parent: options.parent,
|
|
942
1045
|
runtime,
|
|
@@ -954,7 +1057,7 @@ export class SubagentCoordinator {
|
|
|
954
1057
|
disposed: false,
|
|
955
1058
|
};
|
|
956
1059
|
activation.unsubscribe = runtime.session.subscribe((event) => this.observe(activation!, event));
|
|
957
|
-
this.active.set(activation.
|
|
1060
|
+
this.active.set(activation.agentId, activation);
|
|
958
1061
|
return activation;
|
|
959
1062
|
} catch (error) {
|
|
960
1063
|
await runtime.dispose().catch(() => {});
|
|
@@ -968,7 +1071,7 @@ export class SubagentCoordinator {
|
|
|
968
1071
|
signal: AbortSignal | undefined,
|
|
969
1072
|
detachAtAcceptance: boolean,
|
|
970
1073
|
): { accepted: Promise<void>; result: Promise<SubagentRunResult> } {
|
|
971
|
-
if (activation.currentRun) throw new Error(`subagent ${activation.
|
|
1074
|
+
if (activation.currentRun) throw new Error(`subagent ${activation.agentId} is already running`);
|
|
972
1075
|
if (signal?.aborted) throw signal.reason ?? new Error("subagent start aborted");
|
|
973
1076
|
activation.pendingSettlement = undefined;
|
|
974
1077
|
activation.status = "running";
|
|
@@ -1111,7 +1214,7 @@ export class SubagentCoordinator {
|
|
|
1111
1214
|
const closing = truncated.text.trim()
|
|
1112
1215
|
? `Its closing message:\n\n${truncated.text}`
|
|
1113
1216
|
: "It left no closing message.";
|
|
1114
|
-
const content = `Background subagent ${activation.
|
|
1217
|
+
const content = `Background subagent ${activation.agentId} ${stopReasonHeadline(result.stopReason)} and will do no further work unless you send it more.\n\n${closing}${
|
|
1115
1218
|
truncated.truncated ? `\n\n[Closing message truncated; ${truncated.omittedBytes} bytes omitted.]` : ""
|
|
1116
1219
|
}`;
|
|
1117
1220
|
await activation.parent.deliver(
|
|
@@ -1119,7 +1222,7 @@ export class SubagentCoordinator {
|
|
|
1119
1222
|
content,
|
|
1120
1223
|
{
|
|
1121
1224
|
kind: "settled",
|
|
1122
|
-
|
|
1225
|
+
childAgentId: activation.agentId,
|
|
1123
1226
|
label: activation.descriptor.label,
|
|
1124
1227
|
stopReason: result.stopReason,
|
|
1125
1228
|
...(truncated.truncated ? { truncated: true } : {}),
|
|
@@ -1130,7 +1233,7 @@ export class SubagentCoordinator {
|
|
|
1130
1233
|
|
|
1131
1234
|
private parentForActivation(activation: Activation): ParentRef {
|
|
1132
1235
|
return {
|
|
1133
|
-
|
|
1236
|
+
agentId: activation.agentId,
|
|
1134
1237
|
depth: activation.descriptor.depth,
|
|
1135
1238
|
cwd: activation.descriptor.cwd,
|
|
1136
1239
|
sessionManager: activation.runtime.session.sessionManager,
|
|
@@ -1140,7 +1243,7 @@ export class SubagentCoordinator {
|
|
|
1140
1243
|
projectTrusted: activation.parent.projectTrusted,
|
|
1141
1244
|
activation,
|
|
1142
1245
|
deliver: async (customType, content, details, delivery) => {
|
|
1143
|
-
if (activation.disposed) throw new Error(`parent subagent ${activation.
|
|
1246
|
+
if (activation.disposed) throw new Error(`parent subagent ${activation.agentId} is no longer resident`);
|
|
1144
1247
|
const session = activation.runtime.session;
|
|
1145
1248
|
if (delivery === "quiet") {
|
|
1146
1249
|
await session.sendCustomMessage(
|
|
@@ -1170,7 +1273,8 @@ export class SubagentCoordinator {
|
|
|
1170
1273
|
const truncated = truncateUtf8(output, activation.descriptor.runtime.maxOutputBytes);
|
|
1171
1274
|
const sessionFile = activation.runtime.session.sessionFile;
|
|
1172
1275
|
return {
|
|
1173
|
-
|
|
1276
|
+
agentId: activation.agentId,
|
|
1277
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
1174
1278
|
...(sessionFile ? { sessionFile } : {}),
|
|
1175
1279
|
output: truncated.truncated
|
|
1176
1280
|
? `${truncated.text}\n\n[Output truncated; ${truncated.omittedBytes} bytes omitted.${
|
|
@@ -1224,22 +1328,24 @@ export class SubagentCoordinator {
|
|
|
1224
1328
|
if (activation.published) return;
|
|
1225
1329
|
activation.published = true;
|
|
1226
1330
|
this.pi.events.emit("pi-subagent:start", {
|
|
1227
|
-
runId: activation.
|
|
1228
|
-
|
|
1331
|
+
runId: activation.runId,
|
|
1332
|
+
agentId: activation.agentId,
|
|
1333
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
1334
|
+
parentAgentId: activation.descriptor.parentAgentId,
|
|
1229
1335
|
provider: activation.descriptor.provider,
|
|
1230
1336
|
mode: activation.descriptor.mode,
|
|
1231
|
-
parentId: activation.descriptor.parentSessionId,
|
|
1232
1337
|
});
|
|
1233
1338
|
}
|
|
1234
1339
|
|
|
1235
1340
|
private emitEnd(activation: Activation, result: SubagentRunResult): void {
|
|
1236
1341
|
if (!activation.published) return;
|
|
1237
1342
|
this.pi.events.emit("pi-subagent:end", {
|
|
1238
|
-
runId: activation.
|
|
1239
|
-
|
|
1343
|
+
runId: activation.runId,
|
|
1344
|
+
agentId: activation.agentId,
|
|
1345
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
1346
|
+
parentAgentId: activation.descriptor.parentAgentId,
|
|
1240
1347
|
provider: activation.descriptor.provider,
|
|
1241
1348
|
mode: activation.descriptor.mode,
|
|
1242
|
-
parentId: activation.descriptor.parentSessionId,
|
|
1243
1349
|
stopReason: result.stopReason,
|
|
1244
1350
|
output: result.output,
|
|
1245
1351
|
});
|
|
@@ -1248,7 +1354,8 @@ export class SubagentCoordinator {
|
|
|
1248
1354
|
private detailsOf(activation: Activation, result?: SubagentRunResult): DelegationDetails {
|
|
1249
1355
|
return {
|
|
1250
1356
|
kind: "delegation",
|
|
1251
|
-
|
|
1357
|
+
agentId: activation.agentId,
|
|
1358
|
+
piSessionId: activation.runtime.session.sessionId,
|
|
1252
1359
|
provider: activation.descriptor.provider,
|
|
1253
1360
|
mode: activation.descriptor.mode,
|
|
1254
1361
|
agent: activation.descriptor.agent.name,
|
|
@@ -1302,14 +1409,14 @@ export class SubagentCoordinator {
|
|
|
1302
1409
|
try {
|
|
1303
1410
|
await activation.runtime.dispose();
|
|
1304
1411
|
} finally {
|
|
1305
|
-
if (this.active.get(activation.
|
|
1412
|
+
if (this.active.get(activation.agentId) === activation) this.active.delete(activation.agentId);
|
|
1306
1413
|
}
|
|
1307
1414
|
}
|
|
1308
1415
|
|
|
1309
1416
|
private async releaseParentOwnership(activation: Activation): Promise<void> {
|
|
1310
1417
|
const owner = activation.parent.activation;
|
|
1311
1418
|
if (!owner) return;
|
|
1312
|
-
owner.ownedChildren.delete(activation.
|
|
1419
|
+
owner.ownedChildren.delete(activation.agentId);
|
|
1313
1420
|
if (
|
|
1314
1421
|
!owner.currentRun &&
|
|
1315
1422
|
owner.ownedChildren.size === 0 &&
|
|
@@ -1321,9 +1428,9 @@ export class SubagentCoordinator {
|
|
|
1321
1428
|
}
|
|
1322
1429
|
|
|
1323
1430
|
private assertDirectParent(parent: ParentRef, descriptor: SubagentDescriptor): void {
|
|
1324
|
-
if (descriptor.
|
|
1431
|
+
if (descriptor.parentAgentId !== parent.agentId) {
|
|
1325
1432
|
throw new Error(
|
|
1326
|
-
`subagent ${descriptor.label} is not a direct child of ${parent.
|
|
1433
|
+
`subagent ${descriptor.label} is not a direct child of ${parent.agentId}; message was not delivered`,
|
|
1327
1434
|
);
|
|
1328
1435
|
}
|
|
1329
1436
|
}
|
|
@@ -1333,7 +1440,7 @@ export class SubagentCoordinator {
|
|
|
1333
1440
|
childId: string,
|
|
1334
1441
|
): Promise<{ descriptor: SubagentDescriptor; sessionFile: string } | undefined> {
|
|
1335
1442
|
const catalog = await readPersistedCatalog(parent.sessionManager);
|
|
1336
|
-
const entry = catalog.descriptors.find((candidate) => candidate.
|
|
1443
|
+
const entry = catalog.descriptors.find((candidate) => candidate.agentId === childId);
|
|
1337
1444
|
return entry ? { descriptor: entry.descriptor, sessionFile: entry.sessionFile } : undefined;
|
|
1338
1445
|
}
|
|
1339
1446
|
|
|
@@ -1341,16 +1448,19 @@ export class SubagentCoordinator {
|
|
|
1341
1448
|
const persisted = await readPersistedCatalog(parent.sessionManager);
|
|
1342
1449
|
const records = new Map<string, CatalogRecord>();
|
|
1343
1450
|
for (const item of persisted.descriptors) {
|
|
1344
|
-
records.set(item.
|
|
1345
|
-
|
|
1451
|
+
records.set(item.agentId, {
|
|
1452
|
+
agentId: item.agentId,
|
|
1346
1453
|
descriptor: item.descriptor,
|
|
1347
1454
|
sessionFile: item.sessionFile,
|
|
1348
1455
|
});
|
|
1349
1456
|
}
|
|
1457
|
+
const activeSessionIds = new Set(
|
|
1458
|
+
[...this.active.values()].map((activation) => activation.runtime.session.sessionId),
|
|
1459
|
+
);
|
|
1350
1460
|
for (const activation of this.active.values()) {
|
|
1351
1461
|
if (activation.descriptor.cwd !== parent.cwd) continue;
|
|
1352
|
-
records.set(activation.
|
|
1353
|
-
|
|
1462
|
+
records.set(activation.agentId, {
|
|
1463
|
+
agentId: activation.agentId,
|
|
1354
1464
|
descriptor: activation.descriptor,
|
|
1355
1465
|
...(activation.runtime.session.sessionFile
|
|
1356
1466
|
? { sessionFile: activation.runtime.session.sessionFile }
|
|
@@ -1360,7 +1470,9 @@ export class SubagentCoordinator {
|
|
|
1360
1470
|
}
|
|
1361
1471
|
return {
|
|
1362
1472
|
records: [...records.values()],
|
|
1363
|
-
diagnostics: persisted.diagnostics.filter(
|
|
1473
|
+
diagnostics: persisted.diagnostics.filter(
|
|
1474
|
+
(diagnostic) => !activeSessionIds.has(diagnostic.piSessionId),
|
|
1475
|
+
),
|
|
1364
1476
|
};
|
|
1365
1477
|
}
|
|
1366
1478
|
|
|
@@ -1369,7 +1481,7 @@ export class SubagentCoordinator {
|
|
|
1369
1481
|
descriptor: SubagentDescriptor,
|
|
1370
1482
|
byId: Map<string, CatalogRecord>,
|
|
1371
1483
|
): number | undefined {
|
|
1372
|
-
let parentId = descriptor.
|
|
1484
|
+
let parentId = descriptor.parentAgentId;
|
|
1373
1485
|
let distance = 1;
|
|
1374
1486
|
const visited = new Set<string>();
|
|
1375
1487
|
while (true) {
|
|
@@ -1378,16 +1490,21 @@ export class SubagentCoordinator {
|
|
|
1378
1490
|
visited.add(parentId);
|
|
1379
1491
|
const parent = byId.get(parentId);
|
|
1380
1492
|
if (!parent) return undefined;
|
|
1381
|
-
parentId = parent.descriptor.
|
|
1493
|
+
parentId = parent.descriptor.parentAgentId;
|
|
1382
1494
|
distance++;
|
|
1383
1495
|
}
|
|
1384
1496
|
}
|
|
1385
1497
|
|
|
1386
1498
|
private async isDescendantOf(parent: ParentRef, descriptor: SubagentDescriptor): Promise<boolean> {
|
|
1387
1499
|
const { records } = await this.catalogRecords(parent);
|
|
1388
|
-
const byId = new Map(records.map((record) => [record.
|
|
1389
|
-
return this.distanceFrom(parent.
|
|
1500
|
+
const byId = new Map(records.map((record) => [record.agentId, record]));
|
|
1501
|
+
return this.distanceFrom(parent.agentId, descriptor, byId) !== undefined;
|
|
1390
1502
|
}
|
|
1391
1503
|
}
|
|
1392
1504
|
|
|
1393
|
-
export {
|
|
1505
|
+
export {
|
|
1506
|
+
REPORT_CUSTOM_TYPE,
|
|
1507
|
+
SETTLED_CUSTOM_TYPE,
|
|
1508
|
+
AGENT_CUSTOM_TYPE,
|
|
1509
|
+
LINEAGE_CUSTOM_TYPE,
|
|
1510
|
+
};
|
package/src/descriptor.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
} from "./types.ts";
|
|
12
12
|
|
|
13
13
|
export const DESCRIPTOR_CUSTOM_TYPE = "pi-subagent/descriptor";
|
|
14
|
-
export const DESCRIPTOR_VERSION =
|
|
14
|
+
export const DESCRIPTOR_VERSION = 2;
|
|
15
15
|
|
|
16
16
|
const THINKING_LEVELS = new Set<ThinkingLevel>(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
17
17
|
const AGENT_SOURCES = new Set<AgentSource>(["bundled", "user", "project"]);
|
|
@@ -20,6 +20,8 @@ const PROVIDERS = new Set<SubagentProviderName>(["spawn", "fork"]);
|
|
|
20
20
|
const REPORT_DELIVERIES = new Set<ReportDelivery>(["wakeup", "quiet"]);
|
|
21
21
|
const AGENT_SCOPES = new Set<AgentScope>(["user", "project", "both"]);
|
|
22
22
|
const AGENT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
23
|
+
/** UUIDv7 with the standard RFC 9562 variant bits (version 7, variant 10xx). */
|
|
24
|
+
const AGENT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
23
25
|
|
|
24
26
|
type UnknownRecord = Record<string, unknown>;
|
|
25
27
|
|
|
@@ -46,6 +48,14 @@ function optionalString(value: unknown, field: string): string | undefined {
|
|
|
46
48
|
return value === undefined ? undefined : string(value, field);
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
function agentId(value: unknown, field: string): string {
|
|
52
|
+
const parsed = string(value, field);
|
|
53
|
+
if (!AGENT_ID_PATTERN.test(parsed)) {
|
|
54
|
+
throw new Error(`${field} must be a UUIDv7 agent id`);
|
|
55
|
+
}
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
|
|
49
59
|
function limitedString(value: unknown, field: string, maxLength: number): string {
|
|
50
60
|
const parsed = string(value, field);
|
|
51
61
|
if (parsed.length > maxLength) throw new Error(`${field} exceeds ${maxLength} characters`);
|
|
@@ -135,7 +145,9 @@ export function parseDescriptor(value: unknown): SubagentDescriptor {
|
|
|
135
145
|
mode,
|
|
136
146
|
provider,
|
|
137
147
|
label: limitedString(input.label, "label", 200),
|
|
138
|
-
|
|
148
|
+
agentId: agentId(input.agentId, "agentId"),
|
|
149
|
+
parentAgentId: agentId(input.parentAgentId, "parentAgentId"),
|
|
150
|
+
parentPiSessionId: string(input.parentPiSessionId, "parentPiSessionId"),
|
|
139
151
|
...(optionalString(input.parentSessionFile, "parentSessionFile")
|
|
140
152
|
? { parentSessionFile: input.parentSessionFile as string }
|
|
141
153
|
: {}),
|
|
@@ -162,6 +174,7 @@ export function parseDescriptor(value: unknown): SubagentDescriptor {
|
|
|
162
174
|
defaultBackground: boolean(runtime.defaultBackground, "runtime.defaultBackground"),
|
|
163
175
|
reportDelivery,
|
|
164
176
|
inheritExtensions: boolean(runtime.inheritExtensions, "runtime.inheritExtensions"),
|
|
177
|
+
openAIIdentity: boolean(runtime.openAIIdentity, "runtime.openAIIdentity"),
|
|
165
178
|
maxOutputBytes: boundedInteger(
|
|
166
179
|
runtime.maxOutputBytes,
|
|
167
180
|
"runtime.maxOutputBytes",
|
package/src/index.ts
CHANGED
|
@@ -78,11 +78,11 @@ function registerDelegationTool(
|
|
|
78
78
|
"This foreground-only tool waits for the child and returns its final answer. " +
|
|
79
79
|
"Independent sibling calls may still execute in parallel."
|
|
80
80
|
: defaultBackground
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
? "Delegate a complete standalone task to a fresh child with its own Pi session and context. " +
|
|
82
|
+
"Background mode is continuable and returns a durable agent id; use send_message for later FIFO turns. " +
|
|
83
|
+
"Start independent children together in one assistant message."
|
|
84
|
+
: "Delegate a complete standalone task to a fresh child with its own Pi session and context. " +
|
|
85
|
+
"This tool waits for the result by default; set run_in_background to true to return a durable agent id.";
|
|
86
86
|
const promptGuidelines = !enableRunInBackground
|
|
87
87
|
? [
|
|
88
88
|
"Use subagent for focused independent work and give it a complete standalone prompt.",
|
|
@@ -214,7 +214,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
214
214
|
name: "send_message",
|
|
215
215
|
label: "Send Message",
|
|
216
216
|
description:
|
|
217
|
-
"Queue a message as a direct continuable child's next FIFO turn. If it is inactive, its persisted session is cold-resumed. " +
|
|
217
|
+
"Queue a message as a direct continuable child's next FIFO turn. If it is inactive, its persisted session is cold-resumed by agent id. " +
|
|
218
218
|
"This call returns acceptance only, never the child's answer.",
|
|
219
219
|
promptSnippet: "Send a later FIFO turn to a direct continuable subagent",
|
|
220
220
|
executionMode: "parallel",
|
|
@@ -230,7 +230,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
230
230
|
text: `message queued as the next turn for subagent ${params.subagent_id}`,
|
|
231
231
|
},
|
|
232
232
|
],
|
|
233
|
-
details: { kind: "control", action: "send",
|
|
233
|
+
details: { kind: "control", action: "send", agentId: params.subagent_id } satisfies ControlDetails,
|
|
234
234
|
};
|
|
235
235
|
},
|
|
236
236
|
});
|
|
@@ -250,7 +250,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
250
250
|
await coordinator.interrupt(parent, params.agent_id);
|
|
251
251
|
return {
|
|
252
252
|
content: [{ type: "text", text: `interrupt requested for agent ${params.agent_id}` }],
|
|
253
|
-
details: { kind: "control", action: "interrupt",
|
|
253
|
+
details: { kind: "control", action: "interrupt", agentId: params.agent_id } satisfies ControlDetails,
|
|
254
254
|
};
|
|
255
255
|
},
|
|
256
256
|
});
|
|
@@ -307,6 +307,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
307
307
|
: "foreground-first";
|
|
308
308
|
const sections = [
|
|
309
309
|
`Mode: ${schedulingMode}`,
|
|
310
|
+
`OpenAI identity inline: ${sessionSettings.openAIIdentity ? "enabled" : "disabled"}`,
|
|
310
311
|
sessionSettings.syncBundledAgents
|
|
311
312
|
? `Bundled presets: synchronized to ${agentSync?.userAgentsDir ?? coordinator.getUserAgentsDir()}`
|
|
312
313
|
: "Bundled presets: package defaults (no filesystem sync)",
|
package/src/providers.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface SessionView {
|
|
|
9
9
|
getSessionDir(): string;
|
|
10
10
|
getSessionFile(): string | undefined;
|
|
11
11
|
getSessionId(): string;
|
|
12
|
+
getEntries(): SessionEntry[];
|
|
13
|
+
appendCustomEntry(customType: string, data?: unknown): string;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export interface ProviderParent {
|
package/src/render.ts
CHANGED
|
@@ -46,7 +46,7 @@ export function renderDelegationResult(
|
|
|
46
46
|
const header = `${icon} ${theme.fg("toolTitle", theme.bold(details.agent))} ${theme.fg(
|
|
47
47
|
"muted",
|
|
48
48
|
`[${details.provider}/${details.mode}]`,
|
|
49
|
-
)} ${theme.fg("dim", details.
|
|
49
|
+
)} ${theme.fg("dim", details.agentId)}`;
|
|
50
50
|
|
|
51
51
|
if (!options.expanded) {
|
|
52
52
|
const lines = [header, theme.fg("muted", details.label)];
|
|
@@ -102,7 +102,7 @@ export function renderParentMessage(
|
|
|
102
102
|
theme.fg("accent", icon) +
|
|
103
103
|
" " +
|
|
104
104
|
theme.fg("toolTitle", theme.bold(`subagent ${kind}`)) +
|
|
105
|
-
theme.fg("muted", ` ${details?.
|
|
105
|
+
theme.fg("muted", ` ${details?.childAgentId ?? "unknown"}${label}`),
|
|
106
106
|
outputPad,
|
|
107
107
|
0,
|
|
108
108
|
);
|
package/src/schemas.ts
CHANGED
|
@@ -87,7 +87,10 @@ export function forkDelegationParameters(agentNames?: readonly string[]) {
|
|
|
87
87
|
|
|
88
88
|
export const SendMessageParameters = Type.Object(
|
|
89
89
|
{
|
|
90
|
-
subagent_id: Type.String({
|
|
90
|
+
subagent_id: Type.String({
|
|
91
|
+
description: "Durable agent id of a direct continuable child",
|
|
92
|
+
minLength: 1,
|
|
93
|
+
}),
|
|
91
94
|
message: Type.String({
|
|
92
95
|
description: "Message to enqueue as the child's next FIFO turn",
|
|
93
96
|
minLength: 1,
|
|
@@ -99,7 +102,7 @@ export const SendMessageParameters = Type.Object(
|
|
|
99
102
|
export const InterruptParameters = Type.Object(
|
|
100
103
|
{
|
|
101
104
|
agent_id: Type.String({
|
|
102
|
-
description: "
|
|
105
|
+
description: "Agent id of a live child or deeper descendant whose current turn should stop",
|
|
103
106
|
minLength: 1,
|
|
104
107
|
}),
|
|
105
108
|
},
|
package/src/types.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface SubagentSettings {
|
|
|
16
16
|
defaultBackground: boolean;
|
|
17
17
|
reportDelivery: ReportDelivery;
|
|
18
18
|
inheritExtensions: boolean;
|
|
19
|
+
openAIIdentity: boolean;
|
|
19
20
|
maxOutputBytes: number;
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -53,15 +54,22 @@ export interface SubagentRuntimeSnapshot {
|
|
|
53
54
|
defaultBackground: boolean;
|
|
54
55
|
reportDelivery: ReportDelivery;
|
|
55
56
|
inheritExtensions: boolean;
|
|
57
|
+
openAIIdentity: boolean;
|
|
56
58
|
maxOutputBytes: number;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
export interface SubagentDescriptor {
|
|
60
|
-
version:
|
|
62
|
+
version: 2;
|
|
61
63
|
mode: SubagentMode;
|
|
62
64
|
provider: SubagentProviderName;
|
|
63
65
|
label: string;
|
|
64
|
-
|
|
66
|
+
/** Durable pi-subagent control identity, independent of provider wire ids. */
|
|
67
|
+
agentId: string;
|
|
68
|
+
/** Durable control identity of the delegating pi-subagent. */
|
|
69
|
+
parentAgentId: string;
|
|
70
|
+
/** Pi session lookup key of the delegating agent; never used as a wire id. */
|
|
71
|
+
parentPiSessionId: string;
|
|
72
|
+
/** Path of the parent's session file at delegation time (attribute, not identity). */
|
|
65
73
|
parentSessionFile?: string;
|
|
66
74
|
depth: number;
|
|
67
75
|
cwd: string;
|
|
@@ -77,7 +85,8 @@ export interface SubagentUsage extends Usage {
|
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
export interface SubagentRunResult {
|
|
80
|
-
|
|
88
|
+
agentId: string;
|
|
89
|
+
piSessionId?: string;
|
|
81
90
|
sessionFile?: string;
|
|
82
91
|
output: string;
|
|
83
92
|
stopReason: SubagentStopReason;
|
|
@@ -92,7 +101,8 @@ export interface TraceItem {
|
|
|
92
101
|
|
|
93
102
|
export interface DelegationDetails {
|
|
94
103
|
kind: "delegation";
|
|
95
|
-
|
|
104
|
+
agentId: string;
|
|
105
|
+
piSessionId?: string;
|
|
96
106
|
provider: SubagentProviderName;
|
|
97
107
|
mode: SubagentMode;
|
|
98
108
|
agent: string;
|
|
@@ -109,13 +119,13 @@ export interface DelegationDetails {
|
|
|
109
119
|
export interface ControlDetails {
|
|
110
120
|
kind: "control";
|
|
111
121
|
action: "send" | "interrupt" | "list" | "report";
|
|
112
|
-
|
|
122
|
+
agentId?: string;
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
export interface CatalogChild {
|
|
116
126
|
kind: "child";
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
agentId: string;
|
|
128
|
+
parentAgentId: string;
|
|
119
129
|
depth: number;
|
|
120
130
|
descriptor: SubagentDescriptor;
|
|
121
131
|
sessionFile?: string;
|
|
@@ -124,7 +134,7 @@ export interface CatalogChild {
|
|
|
124
134
|
|
|
125
135
|
export interface CatalogDiagnostic {
|
|
126
136
|
kind: "diagnostic";
|
|
127
|
-
|
|
137
|
+
piSessionId: string;
|
|
128
138
|
reason: "corrupt" | "unavailable";
|
|
129
139
|
sessionFile?: string;
|
|
130
140
|
parentSessionFile?: string;
|
|
@@ -135,7 +145,7 @@ export type CatalogEntry = CatalogChild | CatalogDiagnostic;
|
|
|
135
145
|
|
|
136
146
|
export interface ParentMessageDetails {
|
|
137
147
|
kind: "report" | "settled";
|
|
138
|
-
|
|
148
|
+
childAgentId: string;
|
|
139
149
|
label: string;
|
|
140
150
|
stopReason?: SubagentStopReason;
|
|
141
151
|
truncated?: boolean;
|