@ian-pascoe/pi-minimal-subagents 0.13.0 → 0.14.1
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 +66 -25
- package/package.json +2 -2
- package/src/minimal-subagents-config.ts +11 -6
- package/src/minimal-subagents-coordinator.ts +233 -10
- package/src/minimal-subagents-delivery-ledger.ts +14 -13
- package/src/minimal-subagents-extension.ts +59 -2
- package/src/minimal-subagents-registry-wire.ts +1 -0
- package/src/minimal-subagents-registry.ts +3 -0
- package/src/minimal-subagents-render-contract.ts +11 -0
- package/src/minimal-subagents-rendering.ts +12 -1
- package/src/minimal-subagents-session-wire.ts +1 -0
- package/src/minimal-subagents-sessions.ts +8 -1
- package/src/minimal-subagents-tool-schemas.ts +8 -0
- package/src/minimal-subagents-tools.ts +27 -4
- package/src/minimal-subagents-types.ts +24 -1
package/README.md
CHANGED
|
@@ -39,9 +39,15 @@ From this package checkout, load the source directly with
|
|
|
39
39
|
tool-resolution `warnings`. Spawns run sequentially, so a `subagent_wait` or
|
|
40
40
|
`agent_message` in the same tool batch can target the new child.
|
|
41
41
|
2. `subagent_wait` claims the turn's result, or leave it unclaimed and the final
|
|
42
|
-
response arrives automatically as a steer message.
|
|
42
|
+
response arrives automatically as a steer message. Without `turn_id`, a wait
|
|
43
|
+
skips turns whose result you already claimed or received automatically, and
|
|
44
|
+
never repeats an automatically delivered result: it reports
|
|
45
|
+
`already_delivered: true` instead of the output.
|
|
43
46
|
3. `agent_message` continues an idle child: the result reports
|
|
44
|
-
`disposition: "started-turn"` with the new `turn_id
|
|
47
|
+
`disposition: "started-turn"` with the new `turn_id`. A plain
|
|
48
|
+
`subagent_wait({ agent_id })` targets that new turn, not the first result;
|
|
49
|
+
pass `turn_id` to re-read a turn whose result is still retained, claimed or
|
|
50
|
+
not.
|
|
45
51
|
4. `subagent_status` inspects children; `subagent_cancel` stops work but keeps
|
|
46
52
|
the session; `subagent_delete` removes it. Deleted IDs cannot be reused.
|
|
47
53
|
|
|
@@ -93,10 +99,10 @@ source branch, and a new branch without an override follows settings.
|
|
|
93
99
|
|
|
94
100
|
## Model roles
|
|
95
101
|
|
|
96
|
-
`minimalSubagents.modelRoles` gives the parent agent advisory
|
|
102
|
+
`minimalSubagents.modelRoles` gives the parent agent named, advisory choices of
|
|
97
103
|
eligible models. The extension defines no roles itself, performs no task
|
|
98
|
-
classification, and does not route launches
|
|
99
|
-
|
|
104
|
+
classification, and does not route launches: the parent names the role it wants
|
|
105
|
+
with the optional `role` argument of `subagent`.
|
|
100
106
|
|
|
101
107
|
```json
|
|
102
108
|
{
|
|
@@ -113,8 +119,9 @@ ordinary `model` and `thinking_level` arguments separately.
|
|
|
113
119
|
```
|
|
114
120
|
|
|
115
121
|
A recognized final suffix (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`,
|
|
116
|
-
or `max`) is
|
|
117
|
-
|
|
122
|
+
or `max`) is the role's `thinking_level`, not part of the canonical model.
|
|
123
|
+
Unsuffixed roles leave thinking selection independent: the child uses the
|
|
124
|
+
caller's thinking level unless `thinking_level` is passed.
|
|
118
125
|
Role names and hints are trimmed, single-line text. Names may be up to 64
|
|
119
126
|
characters and hints up to 500 characters. Models use canonical
|
|
120
127
|
`provider/model` IDs and must be available under the effective `enabledModels`
|
|
@@ -125,6 +132,27 @@ preference. A thinking level pinned in `enabledModels` neither supplies nor
|
|
|
125
132
|
constrains a role preference, and normal spawn-time model-capability clamping
|
|
126
133
|
still applies.
|
|
127
134
|
|
|
135
|
+
### Launching with a role
|
|
136
|
+
|
|
137
|
+
`subagent({ task, role: "design" })` launches the child with the `design` role's
|
|
138
|
+
model and, when the role has a suffix, its thinking level. An explicit `model` or
|
|
139
|
+
`thinking_level` overrides the role's value independently, so
|
|
140
|
+
`{ role: "design", thinking_level: "low" }` keeps the role's model with a
|
|
141
|
+
different thinking level. The role is recorded even when explicit `model` and
|
|
142
|
+
`thinking_level` override all of its values. An unknown `role` fails before any
|
|
143
|
+
child is created and lists the configured role names. The Launch Contract
|
|
144
|
+
records the resolved `model` and `thinking_level` together with the `role` used
|
|
145
|
+
(`role` is absent when none was named), and `subagent_status` and the expanded
|
|
146
|
+
`subagent` result show it. Recording the name does not tie the child to the role: later settings
|
|
147
|
+
changes never alter an existing Launch Contract.
|
|
148
|
+
|
|
149
|
+
Roles are read when the session starts, so editing `modelRoles` mid-session
|
|
150
|
+
changes nothing until a reload. The `role` parameter is a plain string rather
|
|
151
|
+
than an enum of the configured names, so the tool definition stays
|
|
152
|
+
byte-identical when `modelRoles` changes. Only the role list in the system
|
|
153
|
+
prompt (the `subagent` prompt guidelines) changes, and only at reload. The
|
|
154
|
+
`model` enum is separate: it already follows the eligible models.
|
|
155
|
+
|
|
128
156
|
Global and project roles merge by name in settings order. Expanded role
|
|
129
157
|
objects merge by field; a project string replaces the whole global entry. A
|
|
130
158
|
project can remove one inherited role with `null`, or clear all inherited
|
|
@@ -310,10 +338,17 @@ intermediate Wait Event containing a Coordination Message before the child turn
|
|
|
310
338
|
settles. That event claims only its message, so later unconsumed messages and the
|
|
311
339
|
terminal result retain automatic fallback. If the turn has already settled, a
|
|
312
340
|
wait returns its terminal result with queued messages in `messages`; waiting
|
|
313
|
-
again for
|
|
314
|
-
optional `turn_id` to address
|
|
315
|
-
|
|
316
|
-
|
|
341
|
+
again for a settled turn you claimed returns the same result while it is
|
|
342
|
+
retained. Pass optional `turn_id` to address a retained turn exactly, including
|
|
343
|
+
one whose result you already claimed (a claimed result stays retained until
|
|
344
|
+
Delivery Evidence settles it). Without it, waits skip claimed turns and turns
|
|
345
|
+
whose result was already delivered to you automatically, and select the oldest
|
|
346
|
+
remaining observable turn, falling back to the active, then latest, turn. When
|
|
347
|
+
that fallback reaches a result already delivered to you automatically, whether
|
|
348
|
+
still queued or settled, the wait returns `event: "turn"` with
|
|
349
|
+
`already_delivered: true`, the turn identity, and status, but no output; it does
|
|
350
|
+
not claim the turn, and `turn_id` rereads the full result. A
|
|
351
|
+
caller may have only one outstanding wait for the same source turn; a
|
|
317
352
|
concurrent duplicate is rejected instead of competing for one Wait Event.
|
|
318
353
|
When `timeout_ms` expires, the wait returns an observational `event: "timeout"`
|
|
319
354
|
with the requested turn identity and the same detailed Child Agent status used
|
|
@@ -321,20 +356,26 @@ by targeted `subagent_status`. It removes only the waiter, leaving the child
|
|
|
321
356
|
running and all pending delivery unclaimed. Abort signals remain errors.
|
|
322
357
|
|
|
323
358
|
The persisted Delivery Ledger records Coordination Messages, terminal results,
|
|
324
|
-
globally increasing sequence, and wait ownership before delivery. Existing
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
pending terminal result absorbs them.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
359
|
+
globally increasing sequence, and wait ownership before delivery. Existing items
|
|
360
|
+
retain their sequence; gaps from skipped malformed records are valid. Claims can
|
|
361
|
+
name only active, latest, or retained turns. Wait-returned messages retain
|
|
362
|
+
individual delivery evidence; terminal wait ownership remains durable across
|
|
363
|
+
reloads, forks, and newer turns. Automatic fallback retains its ordered queue
|
|
364
|
+
reservation while batching queued messages from one source turn into one Pi
|
|
365
|
+
steer. Root-bound messages remain batchable while the root turn is active; a
|
|
366
|
+
pending terminal result absorbs them. If Pi discards a root-bound steer before
|
|
367
|
+
consuming it, as Esc does, the next root turn boundary reporting an empty Pi
|
|
368
|
+
queue releases the result: a default wait selects it again at once, and
|
|
369
|
+
automatic fallback re-sends it, ahead of newer results, into the root run if it
|
|
370
|
+
is still going, or, after an abort such as Esc, when the root's next run starts.
|
|
371
|
+
Recovery never starts a root turn by itself. Child sessions drain all available
|
|
372
|
+
steers before the next model call. Destination-session Delivery Evidence still
|
|
373
|
+
settles each batched ledger item independently, preventing duplicate delivery
|
|
374
|
+
and unbounded checkpoint growth. The pure Delivery Ledger state machine retains
|
|
375
|
+
at most 20 pending wait-only terminal results per source agent; Coordination
|
|
376
|
+
Messages are not removed by that terminal-retention limit. Delivered messages
|
|
377
|
+
include stable delivery, source-agent, and source-turn identities in persisted
|
|
378
|
+
details.
|
|
338
379
|
|
|
339
380
|
Deleting a child first verifies its session header and persistent identity,
|
|
340
381
|
then uses the optional `trash` command when available and falls back to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-minimal-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Persistent nested subagents with bounded delegation for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
]
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
|
-
"test": "vitest run
|
|
56
|
+
"test": "vitest run",
|
|
57
57
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -45,6 +45,16 @@ const ModelRolesSettingSchema = Type.Union([ModelRoleEntriesSchema, Type.Null()]
|
|
|
45
45
|
|
|
46
46
|
type ModelRoleThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
47
47
|
|
|
48
|
+
/** Whether `name` is trimmed single-line text of at most 64 characters, as every role name must be. */
|
|
49
|
+
export function isModelRoleName(name: string): boolean {
|
|
50
|
+
return (
|
|
51
|
+
name.length > 0 &&
|
|
52
|
+
name === name.trim() &&
|
|
53
|
+
!/[\r\n]/.test(name) &&
|
|
54
|
+
name.length <= MODEL_ROLE_NAME_MAX_LENGTH
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
/** Describes one user-authored advisory model role shown to subagent callers. */
|
|
49
59
|
export interface MinimalSubagentsModelRole {
|
|
50
60
|
name: string;
|
|
@@ -294,12 +304,7 @@ function parseModelRoles(
|
|
|
294
304
|
const roles: MinimalSubagentsModelRole[] = [];
|
|
295
305
|
for (const [name, entry] of entries) {
|
|
296
306
|
const path = `${entry.scope} minimalSubagents.modelRoles.${name}`;
|
|
297
|
-
if (
|
|
298
|
-
name.length === 0 ||
|
|
299
|
-
name !== name.trim() ||
|
|
300
|
-
/[\r\n]/.test(name) ||
|
|
301
|
-
name.length > MODEL_ROLE_NAME_MAX_LENGTH
|
|
302
|
-
) {
|
|
307
|
+
if (!isModelRoleName(name)) {
|
|
303
308
|
warnings.push(`${path}: role name must be trimmed single-line text up to 64 characters`);
|
|
304
309
|
continue;
|
|
305
310
|
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
claimDeliveryLedgerTurn,
|
|
24
24
|
createDeliveryLedger,
|
|
25
25
|
deliveryLedgerSnapshot,
|
|
26
|
+
deliveryTurnKey,
|
|
26
27
|
findCoordinationDelivery,
|
|
27
28
|
findTerminalDelivery,
|
|
28
29
|
isDeliveryLedgerTurnClaimed,
|
|
@@ -42,6 +43,7 @@ import { withTroubleshootingHint } from "./troubleshooting-skill.js";
|
|
|
42
43
|
import { addCoordinatorMessageEnvelope } from "./minimal-subagents-message-envelope.js";
|
|
43
44
|
import { unavailableAgent } from "./minimal-subagents-sessions.js";
|
|
44
45
|
import { createRegistryEvent } from "./minimal-subagents-registry.js";
|
|
46
|
+
import type { MinimalSubagentsModelRole } from "./minimal-subagents-config.js";
|
|
45
47
|
import type {
|
|
46
48
|
ActiveTurnProgress,
|
|
47
49
|
AgentDetail,
|
|
@@ -67,6 +69,7 @@ import type {
|
|
|
67
69
|
StatusResult,
|
|
68
70
|
TurnResult,
|
|
69
71
|
WaitMessageResult,
|
|
72
|
+
WaitDeliveredTurnResult,
|
|
70
73
|
WaitResult,
|
|
71
74
|
} from "./minimal-subagents-types.js";
|
|
72
75
|
|
|
@@ -109,6 +112,16 @@ interface PendingParentMessage {
|
|
|
109
112
|
cancelGrace?: () => void;
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
/** One terminal result queued to its parent, until Delivery Evidence settles it or Pi discards it. */
|
|
116
|
+
interface TerminalHandOff {
|
|
117
|
+
/** Hand-off order, compared with the root's last ended message at a turn boundary. */
|
|
118
|
+
order: number;
|
|
119
|
+
/** Whether the result entered Pi's clearable steer queue instead of starting a root turn. */
|
|
120
|
+
queued: boolean;
|
|
121
|
+
/** Coordination Messages batched into the same steer. */
|
|
122
|
+
coordinationDeliveryIds: readonly string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
112
125
|
function agentDeliveryKey(agentId: string, turnId: string): string {
|
|
113
126
|
return `${agentId}\u0000${turnId}`;
|
|
114
127
|
}
|
|
@@ -133,6 +146,27 @@ function terminalWaitResult(result: TurnResult, messages: WaitMessageResult[] =
|
|
|
133
146
|
return messages.length === 0 ? terminal : { ...terminal, messages: structuredClone(messages) };
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
/** A terminal Wait Event naming an already delivered result without repeating its output. */
|
|
150
|
+
function alreadyDeliveredWaitResult(
|
|
151
|
+
result: TurnResult,
|
|
152
|
+
messages: WaitMessageResult[],
|
|
153
|
+
): WaitDeliveredTurnResult {
|
|
154
|
+
const delivered: WaitDeliveredTurnResult = {
|
|
155
|
+
event: "turn",
|
|
156
|
+
agent_id: result.agent_id,
|
|
157
|
+
turn_id: result.turn_id,
|
|
158
|
+
status: result.status,
|
|
159
|
+
already_delivered: true,
|
|
160
|
+
};
|
|
161
|
+
if (messages.length > 0) delivered.messages = structuredClone(messages);
|
|
162
|
+
return delivered;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The message ID of an automatic result message, which is also its Delivery Evidence key. */
|
|
166
|
+
function automaticResultMessageId(sourceAgentId: string, sourceTurnId: string): string {
|
|
167
|
+
return `result:${sourceAgentId}:${sourceTurnId}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
136
170
|
function combineCoordinatorMessages(messages: readonly CoordinatorMessage[]): CoordinatorMessage {
|
|
137
171
|
const latest = messages.at(-1);
|
|
138
172
|
if (!latest) throw new Error("Minimal subagents message batch must not be empty");
|
|
@@ -162,9 +196,18 @@ export class MinimalSubagentsCoordinator {
|
|
|
162
196
|
private readonly waiters = new Map<string, Set<TurnWaiter>>();
|
|
163
197
|
private readonly pendingParentMessages = new Map<string, PendingParentMessage[]>();
|
|
164
198
|
private readonly recipientQueues = new Map<string, Promise<unknown>>();
|
|
165
|
-
|
|
199
|
+
/** In-flight automatic result hand-offs, each with the lifecycle epoch that owns it. */
|
|
200
|
+
private readonly automaticDeliveryKeys = new Map<string, number>();
|
|
166
201
|
private readonly automaticCoordinationDeliveryIds = new Set<string>();
|
|
167
202
|
private readonly waitHandedDeliveryIds = new Set<string>();
|
|
203
|
+
/** Terminal results already queued to their parent, until Delivery Evidence settles them. */
|
|
204
|
+
private readonly handedTerminalKeys = new Map<string, TerminalHandOff>();
|
|
205
|
+
/** Monotonic order of hand-offs, compared against the root's last ended message. */
|
|
206
|
+
private handOffOrder = 0;
|
|
207
|
+
/** The latest hand-off order a root `message_end` has seen; later hand-offs may postdate Pi's queue snapshot. */
|
|
208
|
+
private rootMessageEndOrder = 0;
|
|
209
|
+
/** Root results Pi discarded, awaiting the root's next run; each keeps its Delivery Ledger sequence. */
|
|
210
|
+
private readonly discardedRootResultKeys = new Set<string>();
|
|
168
211
|
private readonly backgroundOperations = new Set<Promise<void>>();
|
|
169
212
|
private acceptingOperations = true;
|
|
170
213
|
private lifecycleEpoch = 0;
|
|
@@ -222,8 +265,10 @@ export class MinimalSubagentsCoordinator {
|
|
|
222
265
|
|
|
223
266
|
const sessionContext = parameters.session_context ?? "omit";
|
|
224
267
|
const projectContext = parameters.project_context ?? "inherit";
|
|
225
|
-
const
|
|
226
|
-
const
|
|
268
|
+
const role = parameters.role === undefined ? undefined : this.requireModelRole(parameters.role);
|
|
269
|
+
const model = parameters.model ?? role?.model ?? caller.model;
|
|
270
|
+
const requestedThinking =
|
|
271
|
+
parameters.thinking_level ?? role?.thinkingLevel ?? caller.thinkingLevel;
|
|
227
272
|
const thinkingLevel = this.dependencies.sessions.resolveThinkingLevel(model, requestedThinking);
|
|
228
273
|
const {
|
|
229
274
|
ordinaryTools,
|
|
@@ -268,6 +313,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
268
313
|
missing_dependencies: [],
|
|
269
314
|
recent_messages: [],
|
|
270
315
|
};
|
|
316
|
+
// Added after `delegation`, matching the key order of replayed Launch Contracts.
|
|
317
|
+
if (role) agent.launch_contract.role = role.name;
|
|
271
318
|
this.pendingAgentIds.add(agentId);
|
|
272
319
|
let identity: ReturnType<AgentSessionFactory["createIdentity"]>;
|
|
273
320
|
try {
|
|
@@ -352,6 +399,19 @@ export class MinimalSubagentsCoordinator {
|
|
|
352
399
|
return result;
|
|
353
400
|
}
|
|
354
401
|
|
|
402
|
+
private requireModelRole(name: string): MinimalSubagentsModelRole {
|
|
403
|
+
const roles = this.dependencies.modelRoles ?? [];
|
|
404
|
+
const role = roles.find((candidate) => candidate.name === name);
|
|
405
|
+
if (role) return role;
|
|
406
|
+
throw new Error(
|
|
407
|
+
`Minimal subagents spawn validation: unknown role ${JSON.stringify(name)}; ${
|
|
408
|
+
roles.length === 0
|
|
409
|
+
? "no model roles are configured"
|
|
410
|
+
: `configured roles: ${roles.map((candidate) => candidate.name).join(", ")}`
|
|
411
|
+
}`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
355
415
|
/** Capture immutable launch defaults for a nested caller from its active child runtime. */
|
|
356
416
|
snapshotChildCaller(agentId: string, spawnEntryId: string): CallerSnapshot {
|
|
357
417
|
const agent = this.requireAgent(agentId);
|
|
@@ -448,6 +508,11 @@ export class MinimalSubagentsCoordinator {
|
|
|
448
508
|
(agent.latest_result?.turn_id === turnId ? agent.latest_result : undefined);
|
|
449
509
|
if (retainedResult) {
|
|
450
510
|
const messages = this.drainPendingParentMessages(callerId, agentId, turnId);
|
|
511
|
+
// A default wait falls back to the latest turn, whose result may already have been delivered
|
|
512
|
+
// automatically; repeating its output would deliver it twice. An explicit turn_id rereads it.
|
|
513
|
+
if (requestedTurnId === undefined && this.wasAlreadyDelivered(callerId, retainedResult)) {
|
|
514
|
+
return Promise.resolve(alreadyDeliveredWaitResult(retainedResult, messages));
|
|
515
|
+
}
|
|
451
516
|
this.claimTerminalDelivery(callerId, retainedResult);
|
|
452
517
|
return Promise.resolve(terminalWaitResult(retainedResult, messages));
|
|
453
518
|
}
|
|
@@ -456,7 +521,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
456
521
|
if (agent.active_turn_id !== turnId) {
|
|
457
522
|
return Promise.reject(
|
|
458
523
|
new Error(
|
|
459
|
-
`Minimal subagents wait: turn ${turnId} is unknown or no longer retained for ${agentId}; omit turn_id to wait for its oldest observable turn`,
|
|
524
|
+
`Minimal subagents wait: turn ${turnId} is unknown or no longer retained for ${agentId}; omit turn_id to wait for its oldest observable turn you have neither claimed nor received`,
|
|
460
525
|
),
|
|
461
526
|
);
|
|
462
527
|
}
|
|
@@ -720,6 +785,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
720
785
|
this.waiters.clear();
|
|
721
786
|
this.waitHandedDeliveryIds.clear();
|
|
722
787
|
this.automaticDeliveryKeys.clear();
|
|
788
|
+
this.handedTerminalKeys.clear();
|
|
789
|
+
this.discardedRootResultKeys.clear();
|
|
723
790
|
this.automaticCoordinationDeliveryIds.clear();
|
|
724
791
|
this.deliveryLedger = createDeliveryLedger({
|
|
725
792
|
deliveries: snapshot.deliveries,
|
|
@@ -864,6 +931,102 @@ export class MinimalSubagentsCoordinator {
|
|
|
864
931
|
await Promise.allSettled(scheduled);
|
|
865
932
|
}
|
|
866
933
|
|
|
934
|
+
/** Note that a root message ended; results handed before it reached Pi's queue before the next turn boundary. */
|
|
935
|
+
recordRootMessageEnd(): void {
|
|
936
|
+
this.rootMessageEndOrder = this.handOffOrder;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Release results Pi discarded from the root's steer queue, as Esc does.
|
|
941
|
+
*
|
|
942
|
+
* Call only at a root turn boundary whose pending-message snapshot is empty. Pi persists a
|
|
943
|
+
* consumed steer before that boundary, so a queued result handed before the turn's last message
|
|
944
|
+
* either has Delivery Evidence and settles, or was discarded. A discarded result is selectable
|
|
945
|
+
* by a default wait again at once, with its batched Coordination Messages; automatic fallback
|
|
946
|
+
* re-sends it when the root's next run starts (`deliverDiscardedRootResults`).
|
|
947
|
+
*/
|
|
948
|
+
requeueDiscardedRootResults(): void {
|
|
949
|
+
if (!this.acceptingOperations) return;
|
|
950
|
+
const candidates = this.deliveryLedger.terminalDeliveries
|
|
951
|
+
.filter((delivery) => {
|
|
952
|
+
if (delivery.destination_agent_id !== "root") return false;
|
|
953
|
+
const handOff = this.handedTerminalKeys.get(
|
|
954
|
+
deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id),
|
|
955
|
+
);
|
|
956
|
+
return handOff?.queued === true && handOff.order <= this.rootMessageEndOrder;
|
|
957
|
+
})
|
|
958
|
+
.sort(
|
|
959
|
+
(left, right) =>
|
|
960
|
+
(left.sequence ?? Number.MAX_SAFE_INTEGER) - (right.sequence ?? Number.MAX_SAFE_INTEGER),
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
for (const delivery of candidates) {
|
|
964
|
+
const handedKey = deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id);
|
|
965
|
+
const handOff = this.handedTerminalKeys.get(handedKey);
|
|
966
|
+
if (!handOff) continue;
|
|
967
|
+
const batched = handOff.coordinationDeliveryIds.flatMap((deliveryId) => {
|
|
968
|
+
const item = findCoordinationDelivery(this.deliveryLedger, deliveryId);
|
|
969
|
+
return item ? [item] : [];
|
|
970
|
+
});
|
|
971
|
+
if (this.hasDeliveryEvidence(delivery)) {
|
|
972
|
+
for (const item of batched) {
|
|
973
|
+
if (this.hasCoordinationDeliveryEvidence(item)) this.settleCoordinationDelivery(item);
|
|
974
|
+
}
|
|
975
|
+
this.settleDelivery(delivery);
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
this.handedTerminalKeys.delete(handedKey);
|
|
980
|
+
this.discardedRootResultKeys.add(handedKey);
|
|
981
|
+
for (const item of batched) {
|
|
982
|
+
this.waitHandedDeliveryIds.delete(item.delivery_id);
|
|
983
|
+
this.queuePendingParentMessage("root", item.message, item);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/** Whether a root result waits in Pi's clearable steer queue, so turn boundaries must be watched. */
|
|
989
|
+
hasQueuedRootHandOffs(): boolean {
|
|
990
|
+
for (const handOff of this.handedTerminalKeys.values()) if (handOff.queued) return true;
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Queue results Pi discarded into a root run that just started, in Delivery Ledger sequence.
|
|
996
|
+
*
|
|
997
|
+
* Recovery never starts a root turn by itself, so Esc stays respected: the results wait, still
|
|
998
|
+
* selectable by a default wait, until the user or another delivery starts the root's next run.
|
|
999
|
+
*/
|
|
1000
|
+
deliverDiscardedRootResults(): void {
|
|
1001
|
+
if (
|
|
1002
|
+
this.discardedRootResultKeys.size === 0 ||
|
|
1003
|
+
!this.acceptingOperations ||
|
|
1004
|
+
this.dependencies.root.isIdle()
|
|
1005
|
+
)
|
|
1006
|
+
return;
|
|
1007
|
+
const discarded = this.deliveryLedger.terminalDeliveries
|
|
1008
|
+
.filter((delivery) =>
|
|
1009
|
+
this.discardedRootResultKeys.has(
|
|
1010
|
+
deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id),
|
|
1011
|
+
),
|
|
1012
|
+
)
|
|
1013
|
+
.sort(
|
|
1014
|
+
(left, right) =>
|
|
1015
|
+
(left.sequence ?? Number.MAX_SAFE_INTEGER) - (right.sequence ?? Number.MAX_SAFE_INTEGER),
|
|
1016
|
+
);
|
|
1017
|
+
this.discardedRootResultKeys.clear();
|
|
1018
|
+
for (const delivery of discarded) {
|
|
1019
|
+
const result = delivery.result ?? this.agents.get(delivery.source_agent_id)?.latest_result;
|
|
1020
|
+
if (
|
|
1021
|
+
delivery.path !== "message" ||
|
|
1022
|
+
result?.status !== "completed" ||
|
|
1023
|
+
result.turn_id !== delivery.source_turn_id
|
|
1024
|
+
)
|
|
1025
|
+
continue;
|
|
1026
|
+
this.trackBackgroundOperation(this.deliverAutomaticResult(result, delivery, true));
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
867
1030
|
/** Clone complete child leaves for root fork ownership without ever sharing source session paths. */
|
|
868
1031
|
async prepareFork(sourceRootSessionFile: string): Promise<ForkSnapshot> {
|
|
869
1032
|
const activeRootChildren = this.childrenOf("root");
|
|
@@ -1157,17 +1320,27 @@ export class MinimalSubagentsCoordinator {
|
|
|
1157
1320
|
if (result.status !== "completed") this.removeSettledEmptyTurnClaim(agent.agent_id, turnId);
|
|
1158
1321
|
}
|
|
1159
1322
|
|
|
1323
|
+
/**
|
|
1324
|
+
* Hand one terminal result to its parent through automatic fallback.
|
|
1325
|
+
*
|
|
1326
|
+
* By default it waits its turn in the recipient queue, then the wait-claim grace period. A
|
|
1327
|
+
* `resume` hand-off re-sends a root result Pi discarded: its grace period has passed and its
|
|
1328
|
+
* sequence precedes everything still queued, so it is handed at once, synchronously up to Pi.
|
|
1329
|
+
*/
|
|
1160
1330
|
private async deliverAutomaticResult(
|
|
1161
1331
|
result: TurnResult,
|
|
1162
1332
|
delivery: PersistedDelivery,
|
|
1333
|
+
resume = false,
|
|
1163
1334
|
): Promise<void> {
|
|
1164
1335
|
const deliveryKey = agentDeliveryKey(delivery.source_agent_id, delivery.source_turn_id);
|
|
1336
|
+
const handedKey = deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id);
|
|
1165
1337
|
if (this.automaticDeliveryKeys.has(deliveryKey)) return;
|
|
1166
|
-
this.
|
|
1167
|
-
|
|
1338
|
+
const epoch = this.lifecycleEpoch;
|
|
1339
|
+
this.automaticDeliveryKeys.set(deliveryKey, epoch);
|
|
1340
|
+
const graceMs = resume ? 0 : this.deliveryGraceMs();
|
|
1168
1341
|
let batchedCoordinationDeliveries: PersistedCoordinationDelivery[] = [];
|
|
1169
1342
|
try {
|
|
1170
|
-
|
|
1343
|
+
const handOff = async () => {
|
|
1171
1344
|
if (delivery.destination_agent_id !== "root") {
|
|
1172
1345
|
await this.ensureRuntime(
|
|
1173
1346
|
this.requireUsableAgent(delivery.destination_agent_id, "message"),
|
|
@@ -1175,6 +1348,8 @@ export class MinimalSubagentsCoordinator {
|
|
|
1175
1348
|
if (!this.isTerminalDeliveryCurrent(delivery)) return;
|
|
1176
1349
|
}
|
|
1177
1350
|
if (graceMs > 0) await new Promise((resolve) => setTimeout(resolve, graceMs));
|
|
1351
|
+
// A branch change during the grace period abandons this hand-off; restore replays it.
|
|
1352
|
+
if (epoch !== this.lifecycleEpoch) return;
|
|
1178
1353
|
if (!this.acceptingOperations || this.shouldStopAutomaticTerminalDelivery(delivery)) return;
|
|
1179
1354
|
if (this.hasDeliveryEvidence(delivery)) {
|
|
1180
1355
|
this.settleDelivery(delivery);
|
|
@@ -1187,7 +1362,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1187
1362
|
source_agent_id: delivery.source_agent_id,
|
|
1188
1363
|
destination_agent_id: delivery.destination_agent_id,
|
|
1189
1364
|
source_turn_id: result.turn_id,
|
|
1190
|
-
message_id:
|
|
1365
|
+
message_id: automaticResultMessageId(delivery.source_agent_id, result.turn_id),
|
|
1191
1366
|
status: result.status,
|
|
1192
1367
|
elapsed_ms: result.elapsed_ms,
|
|
1193
1368
|
usage: result.usage,
|
|
@@ -1200,6 +1375,13 @@ export class MinimalSubagentsCoordinator {
|
|
|
1200
1375
|
for (const batchedDelivery of batchedCoordinationDeliveries) {
|
|
1201
1376
|
this.waitHandedDeliveryIds.add(batchedDelivery.delivery_id);
|
|
1202
1377
|
}
|
|
1378
|
+
// Installed before delivery so a settlement inside it removes the marker. Delivery up to
|
|
1379
|
+
// Pi's `sendMessage` is synchronous, so the idle check predicts a steer versus a new turn.
|
|
1380
|
+
this.handedTerminalKeys.set(handedKey, {
|
|
1381
|
+
order: ++this.handOffOrder,
|
|
1382
|
+
queued: delivery.destination_agent_id === "root" && !this.dependencies.root.isIdle(),
|
|
1383
|
+
coordinationDeliveryIds: batchedCoordinationDeliveries.map((item) => item.delivery_id),
|
|
1384
|
+
});
|
|
1203
1385
|
await this.deliverToRecipient(
|
|
1204
1386
|
delivery.destination_agent_id,
|
|
1205
1387
|
combineCoordinatorMessages([
|
|
@@ -1210,8 +1392,14 @@ export class MinimalSubagentsCoordinator {
|
|
|
1210
1392
|
this.isTerminalDeliveryCurrent(delivery) &&
|
|
1211
1393
|
batchedCoordinationDeliveries.every((item) => this.isCoordinationDeliveryCurrent(item)),
|
|
1212
1394
|
);
|
|
1213
|
-
}
|
|
1395
|
+
};
|
|
1396
|
+
await (resume
|
|
1397
|
+
? handOff()
|
|
1398
|
+
: this.enqueueRecipientDelivery(delivery.destination_agent_id, handOff));
|
|
1214
1399
|
} catch (error) {
|
|
1400
|
+
// A hand-off abandoned by a branch change must not touch the restored branch's state.
|
|
1401
|
+
if (epoch !== this.lifecycleEpoch) return;
|
|
1402
|
+
this.handedTerminalKeys.delete(handedKey);
|
|
1215
1403
|
for (const batchedDelivery of batchedCoordinationDeliveries) {
|
|
1216
1404
|
this.waitHandedDeliveryIds.delete(batchedDelivery.delivery_id);
|
|
1217
1405
|
}
|
|
@@ -1231,7 +1419,10 @@ export class MinimalSubagentsCoordinator {
|
|
|
1231
1419
|
}),
|
|
1232
1420
|
);
|
|
1233
1421
|
} finally {
|
|
1234
|
-
|
|
1422
|
+
// Restore's replay may own the key again; release it only from the call that holds it.
|
|
1423
|
+
if (this.automaticDeliveryKeys.get(deliveryKey) === epoch) {
|
|
1424
|
+
this.automaticDeliveryKeys.delete(deliveryKey);
|
|
1425
|
+
}
|
|
1235
1426
|
}
|
|
1236
1427
|
}
|
|
1237
1428
|
|
|
@@ -1317,6 +1508,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1317
1508
|
sourceAgentId,
|
|
1318
1509
|
destinationAgentId,
|
|
1319
1510
|
waitHandedDeliveryIds: this.waitHandedDeliveryIds,
|
|
1511
|
+
handedTurnKeys: this.handedTerminalKeys,
|
|
1320
1512
|
activeTurnId: agent?.active_turn_id,
|
|
1321
1513
|
latestResultTurnId: agent?.latest_result?.turn_id,
|
|
1322
1514
|
});
|
|
@@ -1532,6 +1724,19 @@ export class MinimalSubagentsCoordinator {
|
|
|
1532
1724
|
}),
|
|
1533
1725
|
);
|
|
1534
1726
|
}
|
|
1727
|
+
this.dropStaleHandedTerminalKeys();
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/** Forget handed markers whose terminal delivery left the ledger by settlement, pruning, or deletion. */
|
|
1731
|
+
private dropStaleHandedTerminalKeys(): void {
|
|
1732
|
+
const pending = new Set(
|
|
1733
|
+
this.deliveryLedger.terminalDeliveries.map((delivery) =>
|
|
1734
|
+
deliveryTurnKey(delivery.source_agent_id, delivery.source_turn_id),
|
|
1735
|
+
),
|
|
1736
|
+
);
|
|
1737
|
+
for (const key of this.handedTerminalKeys.keys()) {
|
|
1738
|
+
if (!pending.has(key)) this.handedTerminalKeys.delete(key);
|
|
1739
|
+
}
|
|
1535
1740
|
}
|
|
1536
1741
|
|
|
1537
1742
|
private isTerminalDeliveryCurrent(delivery: PersistedDelivery): boolean {
|
|
@@ -1559,6 +1764,22 @@ export class MinimalSubagentsCoordinator {
|
|
|
1559
1764
|
);
|
|
1560
1765
|
}
|
|
1561
1766
|
|
|
1767
|
+
/**
|
|
1768
|
+
* Whether this result was already delivered to the caller automatically: it is handed, or the
|
|
1769
|
+
* caller's branch holds its automatic result message. A wait's own tool result does not count.
|
|
1770
|
+
*/
|
|
1771
|
+
private wasAlreadyDelivered(callerId: string, result: TurnResult): boolean {
|
|
1772
|
+
return (
|
|
1773
|
+
this.handedTerminalKeys.has(deliveryTurnKey(result.agent_id, result.turn_id)) ||
|
|
1774
|
+
this.hasRecipientDeliveryEvidence(
|
|
1775
|
+
callerId,
|
|
1776
|
+
result.agent_id,
|
|
1777
|
+
result.turn_id,
|
|
1778
|
+
automaticResultMessageId(result.agent_id, result.turn_id),
|
|
1779
|
+
)
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1562
1783
|
private hasDeliveryEvidence(delivery: PersistedDelivery): boolean {
|
|
1563
1784
|
return this.hasRecipientDeliveryEvidence(
|
|
1564
1785
|
delivery.destination_agent_id,
|
|
@@ -1607,6 +1828,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1607
1828
|
delivery.source_agent_id,
|
|
1608
1829
|
delivery.source_turn_id,
|
|
1609
1830
|
).ledger;
|
|
1831
|
+
this.dropStaleHandedTerminalKeys();
|
|
1610
1832
|
this.dependencies.registry.append(
|
|
1611
1833
|
createRegistryEvent(this.dependencies.registry.rootSessionId, "delivery-settled", {
|
|
1612
1834
|
source_agent_id: delivery.source_agent_id,
|
|
@@ -1672,6 +1894,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1672
1894
|
private pruneDeliveryStateForDeletedAgent(agentId: string): void {
|
|
1673
1895
|
const previousCoordinationDeliveries = this.deliveryLedger.coordinationDeliveries;
|
|
1674
1896
|
this.deliveryLedger = pruneDeliveryLedgerAgents(this.deliveryLedger, [agentId]).ledger;
|
|
1897
|
+
this.dropStaleHandedTerminalKeys();
|
|
1675
1898
|
for (const delivery of previousCoordinationDeliveries) {
|
|
1676
1899
|
if (!this.isCoordinationDeliveryCurrent(delivery)) {
|
|
1677
1900
|
this.waitHandedDeliveryIds.delete(delivery.delivery_id);
|
|
@@ -45,16 +45,19 @@ export interface AddedCoordinationDelivery extends DeliveryLedgerTransition {
|
|
|
45
45
|
delivery: PersistedCoordinationDelivery;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/** Describes caller-visible inputs used to select the oldest observable source turn. */
|
|
48
|
+
/** Describes caller-visible inputs used to select the oldest observable source turn the destination has neither claimed nor received. */
|
|
49
49
|
export interface SelectObservableDeliveryTurnOptions {
|
|
50
50
|
sourceAgentId: string;
|
|
51
51
|
destinationAgentId: string;
|
|
52
52
|
waitHandedDeliveryIds: ReadonlySet<string>;
|
|
53
|
+
/** `deliveryTurnKey`s of terminal results already queued to the destination automatically. */
|
|
54
|
+
handedTurnKeys?: { has(key: string): boolean };
|
|
53
55
|
activeTurnId?: string;
|
|
54
56
|
latestResultTurnId?: string;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
/** Compound source-agent and source-turn key shared by claims and process-local handed markers. */
|
|
60
|
+
export function deliveryTurnKey(sourceAgentId: string, sourceTurnId: string): string {
|
|
58
61
|
return `${sourceAgentId}\u0000${sourceTurnId}`;
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -420,19 +423,20 @@ export function releaseEmptyDeliveryLedgerTurn(
|
|
|
420
423
|
: releaseDeliveryLedgerTurn(ledger, sourceAgentId, sourceTurnId);
|
|
421
424
|
}
|
|
422
425
|
|
|
423
|
-
/**
|
|
426
|
+
/**
|
|
427
|
+
* Select the oldest sequenced observable source turn whose terminal result the caller has neither
|
|
428
|
+
* claimed nor been handed automatically, falling back to the active, then latest, turn.
|
|
429
|
+
*/
|
|
424
430
|
export function selectObservableDeliveryTurn(
|
|
425
431
|
ledger: DeliveryLedger,
|
|
426
432
|
options: SelectObservableDeliveryTurnOptions,
|
|
427
433
|
): string | undefined {
|
|
428
|
-
const candidates = new Map<string,
|
|
434
|
+
const candidates = new Map<string, number>();
|
|
429
435
|
const addCandidate = (turnId: string, sequence: number) => {
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
sequence,
|
|
433
|
-
};
|
|
436
|
+
const turnKey = deliveryTurnKey(options.sourceAgentId, turnId);
|
|
437
|
+
if (ledger.waitClaimedTurns.includes(turnKey) || options.handedTurnKeys?.has(turnKey)) return;
|
|
434
438
|
const existing = candidates.get(turnId);
|
|
435
|
-
if (
|
|
439
|
+
if (existing === undefined || sequence < existing) candidates.set(turnId, sequence);
|
|
436
440
|
};
|
|
437
441
|
for (const delivery of ledger.coordinationDeliveries) {
|
|
438
442
|
if (
|
|
@@ -451,10 +455,7 @@ export function selectObservableDeliveryTurn(
|
|
|
451
455
|
addCandidate(delivery.source_turn_id, delivery.sequence ?? Number.MAX_SAFE_INTEGER);
|
|
452
456
|
}
|
|
453
457
|
}
|
|
454
|
-
const retained = [...candidates.entries()].sort((left, right) =>
|
|
455
|
-
if (left[1].claimed !== right[1].claimed) return left[1].claimed ? -1 : 1;
|
|
456
|
-
return left[1].sequence - right[1].sequence;
|
|
457
|
-
})[0]?.[0];
|
|
458
|
+
const retained = [...candidates.entries()].sort((left, right) => left[1] - right[1])[0]?.[0];
|
|
458
459
|
return retained ?? options.activeTurnId ?? options.latestResultTurnId;
|
|
459
460
|
}
|
|
460
461
|
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type SessionShutdownEvent,
|
|
18
18
|
type SessionStartEvent,
|
|
19
19
|
type SessionTreeEvent,
|
|
20
|
+
type TurnEndEvent,
|
|
20
21
|
} from "@earendil-works/pi-coding-agent";
|
|
21
22
|
import { isKeyRelease, isKeyRepeat, matchesKey } from "@earendil-works/pi-tui";
|
|
22
23
|
import {
|
|
@@ -182,9 +183,12 @@ function rootCallerSnapshot(pi: ExtensionAPI, context: ExtensionContext): Caller
|
|
|
182
183
|
function createRootConversationEndpoint(
|
|
183
184
|
pi: ExtensionAPI,
|
|
184
185
|
context: ExtensionContext,
|
|
186
|
+
onSteer: () => void,
|
|
185
187
|
): RootConversationEndpoint {
|
|
186
188
|
return {
|
|
187
189
|
async queueCoordinatorMessage(message) {
|
|
190
|
+
// A running root takes the message as a clearable steer; an idle root starts a turn with it.
|
|
191
|
+
if (!context.isIdle()) onSteer();
|
|
188
192
|
pi.sendMessage(
|
|
189
193
|
{
|
|
190
194
|
customType: message.customType,
|
|
@@ -517,6 +521,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
517
521
|
private uiController: MinimalSubagentsUiController | undefined;
|
|
518
522
|
private statusPanelController: MinimalSubagentsStatusPanelController | undefined;
|
|
519
523
|
private viewerShortcut: ReturnType<typeof installViewerShortcut> | undefined;
|
|
524
|
+
private unwatchRootTurnEnds: (() => void) | undefined;
|
|
520
525
|
private accessSession: ActiveSubagentAccessSession | undefined;
|
|
521
526
|
private preparedFork:
|
|
522
527
|
| { sourceSessionFile: string; selectedBranchSnapshot: RegistrySnapshot }
|
|
@@ -528,7 +533,10 @@ export class MinimalSubagentsLifecycleController {
|
|
|
528
533
|
private readonly effects: MinimalSubagentsLifecycleEffects,
|
|
529
534
|
) {}
|
|
530
535
|
|
|
531
|
-
/**
|
|
536
|
+
/**
|
|
537
|
+
* Register stable tools, renderers, and the six static Pi lifecycle event handlers. `turn_end` is
|
|
538
|
+
* registered only while a result waits in the root's steer queue (`watchRootTurnEnds`).
|
|
539
|
+
*/
|
|
532
540
|
register(): void {
|
|
533
541
|
const rootTools = createCoordinatorToolDefinitions({
|
|
534
542
|
coordinator: this.coordinatorOperations,
|
|
@@ -552,6 +560,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
552
560
|
this.pi.on("session_before_fork", (event, context) => this.prepareSessionFork(event, context));
|
|
553
561
|
this.pi.on("session_tree", (event, context) => this.restoreSessionTree(event, context));
|
|
554
562
|
this.pi.on("message_end", (event, context) => this.reconcileMessageDelivery(event, context));
|
|
563
|
+
this.pi.on("agent_start", () => this.deliverDiscardedRootResults());
|
|
555
564
|
this.pi.on("session_shutdown", (event, context) => this.shutdownSession(event, context));
|
|
556
565
|
}
|
|
557
566
|
|
|
@@ -645,9 +654,10 @@ export class MinimalSubagentsLifecycleController {
|
|
|
645
654
|
});
|
|
646
655
|
activeCoordinator = new MinimalSubagentsCoordinator({
|
|
647
656
|
sessions: sessionFactory,
|
|
648
|
-
root: createRootConversationEndpoint(this.pi, context),
|
|
657
|
+
root: createRootConversationEndpoint(this.pi, context, () => this.watchRootTurnEnds()),
|
|
649
658
|
maxSubagentDepth: minimalSubagentsConfig.maxSubagentDepth,
|
|
650
659
|
toolsets: minimalSubagentsConfig.toolsets,
|
|
660
|
+
modelRoles: minimalSubagentsConfig.modelRoles,
|
|
651
661
|
registry: {
|
|
652
662
|
rootSessionId,
|
|
653
663
|
append: (registryEvent) => this.pi.appendEntry(REGISTRY_ENTRY_TYPE, registryEvent),
|
|
@@ -925,16 +935,63 @@ export class MinimalSubagentsLifecycleController {
|
|
|
925
935
|
_context: ExtensionContext,
|
|
926
936
|
): Promise<void> {
|
|
927
937
|
if (!this.coordinator) return;
|
|
938
|
+
this.coordinator.recordRootMessageEnd();
|
|
928
939
|
if (event.message.role === "toolResult" || event.message.role === "custom") {
|
|
929
940
|
await this.coordinator.reconcileDeliveries();
|
|
930
941
|
this.uiController?.refresh();
|
|
931
942
|
}
|
|
932
943
|
}
|
|
933
944
|
|
|
945
|
+
/**
|
|
946
|
+
* Watch root turn boundaries while a result waits in the root's steer queue. Pi builds a boundary
|
|
947
|
+
* preview for every `turn_end` while any handler listens, so the handler is removed again once no
|
|
948
|
+
* queued result remains.
|
|
949
|
+
*/
|
|
950
|
+
private watchRootTurnEnds(): void {
|
|
951
|
+
this.unwatchRootTurnEnds ??= this.pi.on("turn_end", (event, context) =>
|
|
952
|
+
this.requeueDiscardedRootResults(event, context),
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private stopWatchingRootTurnEnds(): void {
|
|
957
|
+
this.unwatchRootTurnEnds?.();
|
|
958
|
+
this.unwatchRootTurnEnds = undefined;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Pi gives extensions no queue-cleared event, so Esc's discarded steers are found here: the turn
|
|
963
|
+
* boundary's pending-message preview is empty. Returns nothing, so the transcript, system prompt,
|
|
964
|
+
* and tools are unchanged.
|
|
965
|
+
*
|
|
966
|
+
* A queue clear without an abort (dequeue, RPC `clear_queue`) leaves the run going, so released
|
|
967
|
+
* results are steered back at once and Pi's poll after this boundary takes them in the same run.
|
|
968
|
+
* Esc aborts the run, so its released results wait for the root's next run.
|
|
969
|
+
*/
|
|
970
|
+
private requeueDiscardedRootResults(event: TurnEndEvent, context: ExtensionContext): void {
|
|
971
|
+
if (!this.coordinator) {
|
|
972
|
+
this.stopWatchingRootTurnEnds();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (event.context.pendingMessages.length === 0) {
|
|
976
|
+
this.coordinator.requeueDiscardedRootResults();
|
|
977
|
+
if (context.signal && !context.signal.aborted) {
|
|
978
|
+
this.coordinator.deliverDiscardedRootResults();
|
|
979
|
+
}
|
|
980
|
+
this.uiController?.refresh();
|
|
981
|
+
}
|
|
982
|
+
if (!this.coordinator.hasQueuedRootHandOffs()) this.stopWatchingRootTurnEnds();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/** Re-send discarded results into a root run that just started; recovery never starts one. */
|
|
986
|
+
private deliverDiscardedRootResults(): void {
|
|
987
|
+
this.coordinator?.deliverDiscardedRootResults();
|
|
988
|
+
}
|
|
989
|
+
|
|
934
990
|
private async shutdownSession(
|
|
935
991
|
event: SessionShutdownEvent,
|
|
936
992
|
context: ExtensionContext,
|
|
937
993
|
): Promise<void> {
|
|
994
|
+
this.stopWatchingRootTurnEnds();
|
|
938
995
|
this.viewerShortcut?.dispose();
|
|
939
996
|
this.viewerShortcut = undefined;
|
|
940
997
|
this.statusPanelController?.dispose();
|
|
@@ -56,6 +56,7 @@ const ToolSelectionWireSchema = Type.Union([
|
|
|
56
56
|
|
|
57
57
|
/** Parses persisted immutable Launch Contracts without coercion. */
|
|
58
58
|
export const RegistryLaunchContractWireSchema = Type.Object({
|
|
59
|
+
role: Type.Optional(NonEmptyStringSchema),
|
|
59
60
|
session_context: Type.Union([
|
|
60
61
|
Type.Literal("inherit"),
|
|
61
62
|
Type.Literal("compact"),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { COORDINATOR_TOOL_NAMES } from "./minimal-subagents-capabilities.js";
|
|
4
|
+
import { isModelRoleName } from "./minimal-subagents-config.js";
|
|
4
5
|
import { normalizeMinimalSubagentsUsage } from "./minimal-subagents-usage.js";
|
|
5
6
|
import {
|
|
6
7
|
createDeliveryLedger,
|
|
@@ -304,6 +305,7 @@ function parseRegistryLaunchContract(
|
|
|
304
305
|
): LaunchContract | undefined {
|
|
305
306
|
if (
|
|
306
307
|
!isCanonicalModelId(value.model) ||
|
|
308
|
+
(value.role !== undefined && !isModelRoleName(value.role)) ||
|
|
307
309
|
!isUniqueNonEmptyStringArray(value.ordinary_tools) ||
|
|
308
310
|
!excludesCoordinatorToolNames(value.ordinary_tools) ||
|
|
309
311
|
(Array.isArray(value.tools) &&
|
|
@@ -320,6 +322,7 @@ function parseRegistryLaunchContract(
|
|
|
320
322
|
ordinary_tools: [...value.ordinary_tools],
|
|
321
323
|
};
|
|
322
324
|
if (value.delegation !== undefined) launchContract.delegation = value.delegation;
|
|
325
|
+
if (value.role !== undefined) launchContract.role = value.role;
|
|
323
326
|
return launchContract;
|
|
324
327
|
}
|
|
325
328
|
|
|
@@ -23,6 +23,7 @@ const RenderUsageSchema = Type.Object({
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
const RenderLaunchContractSchema = Type.Object({
|
|
26
|
+
role: Type.Optional(Type.String()),
|
|
26
27
|
session_context: Type.Optional(Type.String()),
|
|
27
28
|
project_context: Type.Optional(Type.String()),
|
|
28
29
|
model: Type.Optional(Type.String()),
|
|
@@ -87,6 +88,7 @@ const SpawnCallArgumentsSchema = Type.Object({
|
|
|
87
88
|
agent_id: Type.Optional(Type.String()),
|
|
88
89
|
session_context: Type.Optional(Type.String()),
|
|
89
90
|
project_context: Type.Optional(Type.String()),
|
|
91
|
+
role: Type.Optional(Type.String()),
|
|
90
92
|
model: Type.Optional(Type.String()),
|
|
91
93
|
thinking_level: Type.Optional(Type.String()),
|
|
92
94
|
delegation: Type.Optional(Type.String()),
|
|
@@ -144,6 +146,7 @@ const WaitTurnDetailsSchema = Type.Object({
|
|
|
144
146
|
error: Type.Optional(Type.String()),
|
|
145
147
|
elapsed_ms: Type.Optional(Type.Number()),
|
|
146
148
|
usage: Type.Optional(RenderUsageSchema),
|
|
149
|
+
already_delivered: Type.Optional(Type.Boolean()),
|
|
147
150
|
messages: Type.Optional(Type.Array(WaitMessageDetailsSchema)),
|
|
148
151
|
});
|
|
149
152
|
const WaitTimeoutDetailsSchema = Type.Object({
|
|
@@ -219,6 +222,7 @@ const CoordinatorAgentDetailOutputSchema = Type.Object({
|
|
|
219
222
|
...CoordinatorAgentSummaryOutputSchema.properties,
|
|
220
223
|
session_file: Type.Optional(Type.String()),
|
|
221
224
|
launch_contract: Type.Object({
|
|
225
|
+
role: Type.Optional(Type.String()),
|
|
222
226
|
session_context: Type.String(),
|
|
223
227
|
project_context: Type.String(),
|
|
224
228
|
model: Type.String(),
|
|
@@ -297,6 +301,13 @@ export const CoordinatorToolOutputSchemas = {
|
|
|
297
301
|
messages: Type.Optional(Type.Array(CoordinatorWaitMessageOutputSchema)),
|
|
298
302
|
...CoordinatorWaitSourceOutputSchema,
|
|
299
303
|
}),
|
|
304
|
+
Type.Object({
|
|
305
|
+
...Type.Pick(CoordinatorTurnOutputSchema, ["agent_id", "turn_id", "status"]).properties,
|
|
306
|
+
event: Type.Literal("turn"),
|
|
307
|
+
already_delivered: Type.Literal(true),
|
|
308
|
+
messages: Type.Optional(Type.Array(CoordinatorWaitMessageOutputSchema)),
|
|
309
|
+
...CoordinatorWaitSourceOutputSchema,
|
|
310
|
+
}),
|
|
300
311
|
Type.Object({
|
|
301
312
|
event: Type.Literal("timeout"),
|
|
302
313
|
agent_id: Type.String(),
|
|
@@ -445,6 +445,7 @@ function spawnCallLaunchSummary(args: SpawnCallArguments): string[] {
|
|
|
445
445
|
? undefined
|
|
446
446
|
: `tools ${Array.isArray(args.tools) ? args.tools.join(", ") || "none" : args.tools}`;
|
|
447
447
|
return [
|
|
448
|
+
args.role ? `role ${args.role}` : undefined,
|
|
448
449
|
profile,
|
|
449
450
|
tools,
|
|
450
451
|
args.session_context && args.session_context !== "omit"
|
|
@@ -507,6 +508,7 @@ function renderSpawnResult(
|
|
|
507
508
|
`delegation ${launchContract?.delegation ?? args.delegation ?? "none"}`,
|
|
508
509
|
`session context ${launchContract?.session_context ?? args.session_context ?? "omit"}`,
|
|
509
510
|
`project context ${launchContract?.project_context ?? args.project_context ?? "inherit"}`,
|
|
511
|
+
launchContract?.role ? `role ${launchContract.role}` : undefined,
|
|
510
512
|
resolvedModel ? `model ${resolvedModel}` : undefined,
|
|
511
513
|
resolvedThinking ? `thinking ${resolvedThinking}` : undefined,
|
|
512
514
|
].filter((value): value is string => value !== undefined);
|
|
@@ -580,6 +582,9 @@ function renderWaitProgress(
|
|
|
580
582
|
return container;
|
|
581
583
|
}
|
|
582
584
|
|
|
585
|
+
/** Shown instead of the output of a result already delivered to the parent automatically. */
|
|
586
|
+
const ALREADY_DELIVERED_TEXT = "Already delivered automatically; wait with turn_id to reread it.";
|
|
587
|
+
|
|
583
588
|
/** The collapsed body of a settled wait: what the child said, or why it stopped. */
|
|
584
589
|
function collapsedWaitBody(
|
|
585
590
|
details: WaitRenderDetails,
|
|
@@ -591,6 +596,9 @@ function collapsedWaitBody(
|
|
|
591
596
|
const activity = latestProgressActivity(details.agent.recent_activity ?? []);
|
|
592
597
|
return activity.length > 0 ? new ActivityLines(activity, theme) : undefined;
|
|
593
598
|
}
|
|
599
|
+
if (details.already_delivered) {
|
|
600
|
+
return collapsedTextPreview(theme.fg("muted", ALREADY_DELIVERED_TEXT), theme);
|
|
601
|
+
}
|
|
594
602
|
const output = details.output ?? "";
|
|
595
603
|
if (status === "completed") {
|
|
596
604
|
return output ? collapsedMarkdownPreview(output, theme) : undefined;
|
|
@@ -667,7 +675,9 @@ function renderWaitResult(
|
|
|
667
675
|
);
|
|
668
676
|
}
|
|
669
677
|
const output = details.output ?? "";
|
|
670
|
-
if (
|
|
678
|
+
if (details.already_delivered) {
|
|
679
|
+
appendTextSection(container, theme, "Output", ALREADY_DELIVERED_TEXT);
|
|
680
|
+
} else if (status === "completed") {
|
|
671
681
|
if (output.length > 0) {
|
|
672
682
|
appendComponentSection(
|
|
673
683
|
container,
|
|
@@ -770,6 +780,7 @@ function renderDetailedStatusAgent(
|
|
|
770
780
|
const launchValues = [
|
|
771
781
|
`session context ${launchContract.session_context ?? "inherit"}`,
|
|
772
782
|
`project context ${launchContract.project_context ?? "inherit"}`,
|
|
783
|
+
...(launchContract.role ? [`role ${launchContract.role}`] : []),
|
|
773
784
|
`model ${launchContract.model ?? agent.model ?? "unknown"}`,
|
|
774
785
|
`thinking ${launchContract.thinking_level ?? agent.thinking_level ?? "unknown"}`,
|
|
775
786
|
`delegation ${launchContract.delegation ?? "none"}`,
|
|
@@ -36,6 +36,7 @@ const DeliveryEvidenceMessageSchema = Type.Object({
|
|
|
36
36
|
/** Parses durable custom-message and wait-tool Delivery Evidence details. */
|
|
37
37
|
export const DeliveryEvidenceDetailsSchema = Type.Object({
|
|
38
38
|
event: Type.Optional(Type.String()),
|
|
39
|
+
already_delivered: Type.Optional(Type.Boolean()),
|
|
39
40
|
source_agent_id: Type.String(),
|
|
40
41
|
source_turn_id: Type.String(),
|
|
41
42
|
delivery_id: Type.Optional(Type.String()),
|
|
@@ -503,7 +503,14 @@ export function findDeliveryEvidence(
|
|
|
503
503
|
))
|
|
504
504
|
);
|
|
505
505
|
}
|
|
506
|
-
|
|
506
|
+
// A message, timeout, or already-delivered wait result never carried the turn's terminal result.
|
|
507
|
+
if (
|
|
508
|
+
waitToolResult &&
|
|
509
|
+
(details.event === "message" ||
|
|
510
|
+
details.event === "timeout" ||
|
|
511
|
+
details.already_delivered === true)
|
|
512
|
+
)
|
|
513
|
+
return false;
|
|
507
514
|
return details.source_agent_id === sourceAgentId && details.source_turn_id === sourceTurnId;
|
|
508
515
|
});
|
|
509
516
|
}
|
|
@@ -26,6 +26,13 @@ const ToolSelectionSchema = Type.Union(
|
|
|
26
26
|
'Configurable presets: "read" adds readToolset; "modify" adds readToolset plus modifyToolset. Defaults: read, grep, find, ls; modify additionally grants bash, edit, write. Configured baseToolset applies to all selections, including "none" and exact arrays. Omit to inherit the caller\'s active ordinary tools plus base. All grants stay within the caller\'s capability ceiling; coordinator tools are injected separately.',
|
|
27
27
|
},
|
|
28
28
|
);
|
|
29
|
+
// Deliberately a plain string rather than an enum of the configured roles: settings can change, and
|
|
30
|
+
// the tool definition must stay byte-identical for the session so provider prompt caches survive.
|
|
31
|
+
const ModelRoleSchema = Type.String({
|
|
32
|
+
minLength: 1,
|
|
33
|
+
description:
|
|
34
|
+
"Name of a configured model role (minimalSubagents.modelRoles) listed in the Subagent tool guidelines. Resolves to the role's model and, when the role has one, its thinking level. An explicit model or thinking_level overrides the role's value. An unknown role is rejected with the configured role names.",
|
|
35
|
+
});
|
|
29
36
|
const FRIENDLY_AGENT_ID_PATTERN = "^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$";
|
|
30
37
|
const CANONICAL_AGENT_ID_PATTERN =
|
|
31
38
|
"^(?:root\\.)?[A-Za-z0-9][A-Za-z0-9_-]{0,63}(?:\\.[A-Za-z0-9][A-Za-z0-9_-]{0,63})*$";
|
|
@@ -55,6 +62,7 @@ export function createCoordinatorToolSchemas(modelIds: readonly string[]) {
|
|
|
55
62
|
),
|
|
56
63
|
session_context: Type.Optional(SessionContextSchema),
|
|
57
64
|
project_context: Type.Optional(ProjectContextSchema),
|
|
65
|
+
role: Type.Optional(ModelRoleSchema),
|
|
58
66
|
model: Type.Optional(explicitModelSchema),
|
|
59
67
|
thinking_level: Type.Optional(ThinkingLevelSchema),
|
|
60
68
|
tools: Type.Optional(ToolSelectionSchema),
|
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
DeleteResult,
|
|
39
39
|
SpawnResult,
|
|
40
40
|
StatusResult,
|
|
41
|
+
WaitDeliveredTurnResult,
|
|
41
42
|
WaitResult,
|
|
42
43
|
} from "./minimal-subagents-types.js";
|
|
43
44
|
|
|
@@ -82,8 +83,11 @@ function buildModelRolePromptGuidelines(
|
|
|
82
83
|
return ` - ${role.name} → model=${role.model}${thinkingGuidance}${role.hint ? ` — ${role.hint}` : ""}`;
|
|
83
84
|
});
|
|
84
85
|
return [
|
|
85
|
-
[
|
|
86
|
-
|
|
86
|
+
[
|
|
87
|
+
"Configured model roles are guidance, not constraints. Pass role to launch with a role's model and thinking_level; an explicit model or thinking_level overrides the role's value:",
|
|
88
|
+
...roleLines,
|
|
89
|
+
].join("\n"),
|
|
90
|
+
"Choose a role or model based on the task. A listed thinking_level is a preference, not a constraint. Callers choose thinking_level independently for roles without one.",
|
|
87
91
|
];
|
|
88
92
|
}
|
|
89
93
|
|
|
@@ -194,6 +198,22 @@ function structuredToolResult<TDetails extends CoordinatorToolResultDetails>(
|
|
|
194
198
|
};
|
|
195
199
|
}
|
|
196
200
|
|
|
201
|
+
/**
|
|
202
|
+
* One line saying an already-delivered result is not repeated and how to reread it, followed by
|
|
203
|
+
* any Coordination Messages the wait drained, which the parent has not seen yet.
|
|
204
|
+
*/
|
|
205
|
+
function alreadyDeliveredContent(
|
|
206
|
+
result: WaitDeliveredTurnResult,
|
|
207
|
+
): AgentToolResult<WaitResult>["content"] {
|
|
208
|
+
const notice = `Result of ${result.agent_id} turn ${result.turn_id} (${result.status}) was already delivered automatically; call subagent_wait with turn_id "${result.turn_id}" to reread it.`;
|
|
209
|
+
if (!result.messages) return [{ type: "text", text: notice }];
|
|
210
|
+
const messages = truncateHead(JSON.stringify({ messages: result.messages }, null, 2), {
|
|
211
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
212
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
213
|
+
});
|
|
214
|
+
return [{ type: "text", text: `${notice}\n${messages.content}` }];
|
|
215
|
+
}
|
|
216
|
+
|
|
197
217
|
/**
|
|
198
218
|
* Report a failed operation that still carries declared output: the model sees an error and
|
|
199
219
|
* codemode scripts receive `structuredContent` instead of a data-less rejection.
|
|
@@ -310,7 +330,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
310
330
|
name: "subagent_wait",
|
|
311
331
|
label: "Subagent Wait",
|
|
312
332
|
description:
|
|
313
|
-
"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. A settled turn returns event=turn, with any queued messages in messages; waiting again returns
|
|
333
|
+
"Wait for one direct child's oldest observable turn whose result you have neither claimed nor received, or select an exact retained turn_id. An active child may first return event=message; later unconsumed items still fall back automatically. A settled turn returns event=turn, with any queued messages in messages; waiting again returns a result you claimed in full. Without turn_id, a result already delivered to you automatically returns event=turn with already_delivered=true and no output; pass its turn_id to reread it. Timeout returns event=timeout with detailed child status and never cancels the child.",
|
|
314
334
|
promptSnippet: "Wait for one direct child's exact turn",
|
|
315
335
|
parameters: options.schemas.subagent_wait,
|
|
316
336
|
annotations: {
|
|
@@ -355,7 +375,10 @@ export function createCoordinatorToolDefinitions(
|
|
|
355
375
|
source_turn_id: result.turn_id,
|
|
356
376
|
};
|
|
357
377
|
return {
|
|
358
|
-
|
|
378
|
+
content:
|
|
379
|
+
"already_delivered" in result
|
|
380
|
+
? alreadyDeliveredContent(result)
|
|
381
|
+
: structuredToolResult(result).content,
|
|
359
382
|
details,
|
|
360
383
|
structuredContent: JSON.parse(JSON.stringify(details)),
|
|
361
384
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { MinimalSubagentsModelRole } from "./minimal-subagents-config.js";
|
|
4
5
|
|
|
5
6
|
/** Controls how much committed caller conversation enters a new child session. */
|
|
6
7
|
export type SessionContextMode = "inherit" | "compact" | "omit";
|
|
@@ -35,6 +36,8 @@ export interface SpawnParameters {
|
|
|
35
36
|
agent_id?: string;
|
|
36
37
|
session_context?: SessionContextMode;
|
|
37
38
|
project_context?: ProjectContextMode;
|
|
39
|
+
/** Configured model role whose model and thinking level apply unless overridden below. */
|
|
40
|
+
role?: string;
|
|
38
41
|
model?: string;
|
|
39
42
|
thinking_level?: ThinkingLevel;
|
|
40
43
|
tools?: ToolSelection;
|
|
@@ -95,6 +98,19 @@ export interface WaitTurnResult extends TurnResult {
|
|
|
95
98
|
messages?: WaitMessageResult[];
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Reports that a default wait selected a terminal turn whose result was already delivered
|
|
103
|
+
* automatically, handed or settled, so the output is not repeated; an explicit `turn_id` rereads it.
|
|
104
|
+
*/
|
|
105
|
+
export interface WaitDeliveredTurnResult extends Pick<
|
|
106
|
+
TurnResult,
|
|
107
|
+
"agent_id" | "turn_id" | "status"
|
|
108
|
+
> {
|
|
109
|
+
event: "turn";
|
|
110
|
+
already_delivered: true;
|
|
111
|
+
messages?: WaitMessageResult[];
|
|
112
|
+
}
|
|
113
|
+
|
|
98
114
|
/** Reports an observational wait timeout with the current detailed child status. */
|
|
99
115
|
export interface WaitTimeoutResult {
|
|
100
116
|
event: "timeout";
|
|
@@ -107,7 +123,11 @@ export interface WaitTimeoutResult {
|
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
/** Reports one message, terminal turn, or timeout returned by subagent_wait. */
|
|
110
|
-
export type WaitResult =
|
|
126
|
+
export type WaitResult =
|
|
127
|
+
| WaitMessageResult
|
|
128
|
+
| WaitTurnResult
|
|
129
|
+
| WaitDeliveredTurnResult
|
|
130
|
+
| WaitTimeoutResult;
|
|
111
131
|
|
|
112
132
|
/** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
|
|
113
133
|
export interface AgentSummary extends RuntimeProfile {
|
|
@@ -199,6 +219,8 @@ export interface DeleteResult {
|
|
|
199
219
|
|
|
200
220
|
/** Persists immutable context, model, thinking, and ordinary-tool launch choices. */
|
|
201
221
|
export interface LaunchContract extends RuntimeProfile {
|
|
222
|
+
/** Configured model role the caller named at launch; absent when none was used. */
|
|
223
|
+
role?: string;
|
|
202
224
|
session_context: SessionContextMode;
|
|
203
225
|
project_context: ProjectContextMode;
|
|
204
226
|
tools: ToolSelection | undefined;
|
|
@@ -424,6 +446,7 @@ export interface CoordinatorDependencies {
|
|
|
424
446
|
root: RootConversationEndpoint;
|
|
425
447
|
maxSubagentDepth?: number;
|
|
426
448
|
toolsets?: MinimalSubagentsToolsets;
|
|
449
|
+
modelRoles?: readonly MinimalSubagentsModelRole[];
|
|
427
450
|
now?: () => Date;
|
|
428
451
|
automaticDeliveryGraceMs?: number;
|
|
429
452
|
notify?: (notification: CoordinatorNotification) => void;
|