@ian-pascoe/pi-minimal-subagents 0.19.3 → 0.20.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 +12 -0
- package/package.json +2 -2
- package/src/minimal-subagents-coordinator.ts +43 -6
- package/src/minimal-subagents-extension.ts +7 -1
- package/src/minimal-subagents-render-contract.ts +22 -0
- package/src/minimal-subagents-rendering.ts +22 -16
- package/src/minimal-subagents-result-text.ts +28 -7
- package/src/minimal-subagents-sessions.ts +16 -1
- package/src/minimal-subagents-tools.ts +7 -2
- package/src/minimal-subagents-types.ts +24 -9
package/README.md
CHANGED
|
@@ -387,6 +387,18 @@ Launch Contract; targeted `subagent_status` returns the full detail.
|
|
|
387
387
|
It removes only the waiter, leaving the child
|
|
388
388
|
running and all pending delivery unclaimed. Abort signals remain errors.
|
|
389
389
|
|
|
390
|
+
A wait that would block also ends early when a Queued Steer is pending for the
|
|
391
|
+
caller's session, because Pi hands a steer to an agent only after every tool call in
|
|
392
|
+
the current batch returns. It returns an `event: "steered"` Wait Event with the
|
|
393
|
+
requested turn identity and the same compact progress snapshot as a timeout, and,
|
|
394
|
+
like a timeout, removes only its waiter: nothing is claimed, the child is not
|
|
395
|
+
cancelled, and later messages and the terminal result keep automatic fallback.
|
|
396
|
+
A wait that starts while a steer is pending returns `steered` at once, but a
|
|
397
|
+
retained result or pending Coordination Message is still returned first. Only steers
|
|
398
|
+
count (a user's mid-run message, a parent's Coordination Message into a busy child, or
|
|
399
|
+
another child's result steered into the caller); follow-ups never do. A child
|
|
400
|
+
session's steer clears when its next turn starts or its run ends.
|
|
401
|
+
|
|
390
402
|
The persisted Delivery Ledger records Coordination Messages, terminal results,
|
|
391
403
|
globally increasing sequence, and wait ownership before delivery. Existing items
|
|
392
404
|
retain their sequence; gaps from skipped malformed records are valid. Claims can
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ian-pascoe/pi-minimal-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Persistent nested subagents with bounded delegation for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"provenance": true
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ian-pascoe/pi-utils": "^0.
|
|
34
|
+
"@ian-pascoe/pi-utils": "^0.10.0",
|
|
35
35
|
"minimatch": "^10.2.6"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
@@ -69,7 +69,8 @@ import type {
|
|
|
69
69
|
StatusResult,
|
|
70
70
|
TurnResult,
|
|
71
71
|
WaitMessageResult,
|
|
72
|
-
|
|
72
|
+
WaitProgressSnapshot,
|
|
73
|
+
WaitSteeredResult,
|
|
73
74
|
AutomaticDelivery,
|
|
74
75
|
WaitDeliveredTurnResult,
|
|
75
76
|
WaitResult,
|
|
@@ -106,6 +107,8 @@ interface TurnWaiter {
|
|
|
106
107
|
timeout?: ReturnType<typeof setTimeout>;
|
|
107
108
|
abortSignal?: AbortSignal;
|
|
108
109
|
abortListener?: () => void;
|
|
110
|
+
steerSignal?: AbortSignal;
|
|
111
|
+
steerListener?: () => void;
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
interface PendingParentMessage {
|
|
@@ -489,13 +492,19 @@ export class MinimalSubagentsCoordinator {
|
|
|
489
492
|
}
|
|
490
493
|
}
|
|
491
494
|
|
|
492
|
-
/**
|
|
495
|
+
/**
|
|
496
|
+
* Wait for one exact turn, returning compact child progress if the waiter times out. Once
|
|
497
|
+
* `steerSignal` aborts, a Queued Steer is pending for the caller, so a wait that would block
|
|
498
|
+
* ends with a `steered` Steer Yield instead, claiming nothing and leaving the child running;
|
|
499
|
+
* `signal` still rejects as a cancellation.
|
|
500
|
+
*/
|
|
493
501
|
wait(
|
|
494
502
|
callerId: string,
|
|
495
503
|
agentId: string,
|
|
496
504
|
timeoutMs?: number,
|
|
497
505
|
signal?: AbortSignal,
|
|
498
506
|
requestedTurnId?: string,
|
|
507
|
+
steerSignal?: AbortSignal,
|
|
499
508
|
): Promise<WaitResult> {
|
|
500
509
|
this.assertAccepting();
|
|
501
510
|
this.assertCallerExists(callerId);
|
|
@@ -538,8 +547,18 @@ export class MinimalSubagentsCoordinator {
|
|
|
538
547
|
);
|
|
539
548
|
}
|
|
540
549
|
|
|
550
|
+
if (steerSignal?.aborted && !signal?.aborted) {
|
|
551
|
+
return Promise.resolve(this.steeredWaitResult(agent, turnId));
|
|
552
|
+
}
|
|
553
|
+
|
|
541
554
|
return new Promise<WaitResult>((resolve, reject) => {
|
|
542
|
-
const waiter: TurnWaiter = {
|
|
555
|
+
const waiter: TurnWaiter = {
|
|
556
|
+
callerId,
|
|
557
|
+
resolve,
|
|
558
|
+
reject,
|
|
559
|
+
abortSignal: signal,
|
|
560
|
+
steerSignal,
|
|
561
|
+
};
|
|
543
562
|
let turnWaiters = this.waiters.get(key);
|
|
544
563
|
if (!turnWaiters) {
|
|
545
564
|
turnWaiters = new Set();
|
|
@@ -562,6 +581,13 @@ export class MinimalSubagentsCoordinator {
|
|
|
562
581
|
});
|
|
563
582
|
}, timeoutMs);
|
|
564
583
|
}
|
|
584
|
+
if (steerSignal) {
|
|
585
|
+
waiter.steerListener = () => {
|
|
586
|
+
this.removeWaiter(key, waiter);
|
|
587
|
+
resolve(this.steeredWaitResult(agent, turnId));
|
|
588
|
+
};
|
|
589
|
+
steerSignal.addEventListener("abort", waiter.steerListener, { once: true });
|
|
590
|
+
}
|
|
565
591
|
if (signal) {
|
|
566
592
|
waiter.abortListener = () =>
|
|
567
593
|
stopWaiting(new Error(`Minimal subagents wait cancelled for ${agentId}`));
|
|
@@ -571,6 +597,16 @@ export class MinimalSubagentsCoordinator {
|
|
|
571
597
|
});
|
|
572
598
|
}
|
|
573
599
|
|
|
600
|
+
/** The Steer Yield: the timeout's progress snapshot, with nothing claimed or cancelled. */
|
|
601
|
+
private steeredWaitResult(agent: PersistedAgent, turnId: string): WaitSteeredResult {
|
|
602
|
+
return {
|
|
603
|
+
event: "steered",
|
|
604
|
+
agent_id: agent.agent_id,
|
|
605
|
+
turn_id: turnId,
|
|
606
|
+
...this.buildWaitTimeoutProgress(agent),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
574
610
|
/** Return direct-child status for a child caller; the root may inspect any descendant. */
|
|
575
611
|
status(callerId: string, agentId?: string): StatusResult {
|
|
576
612
|
this.assertCallerExists(callerId);
|
|
@@ -1918,9 +1954,7 @@ export class MinimalSubagentsCoordinator {
|
|
|
1918
1954
|
}
|
|
1919
1955
|
|
|
1920
1956
|
/** The compact progress a timed-out wait reports; `subagent_status` has the full detail. */
|
|
1921
|
-
private buildWaitTimeoutProgress(
|
|
1922
|
-
agent: PersistedAgent,
|
|
1923
|
-
): Omit<WaitTimeoutResult, "event" | "agent_id" | "turn_id" | "timeout_ms"> {
|
|
1957
|
+
private buildWaitTimeoutProgress(agent: PersistedAgent): WaitProgressSnapshot {
|
|
1924
1958
|
const summary = this.buildAgentSummary(agent, false);
|
|
1925
1959
|
const runtime = this.runtimes.get(agent.agent_id);
|
|
1926
1960
|
const messages =
|
|
@@ -2099,6 +2133,9 @@ export class MinimalSubagentsCoordinator {
|
|
|
2099
2133
|
if (waiter.abortSignal && waiter.abortListener) {
|
|
2100
2134
|
waiter.abortSignal.removeEventListener("abort", waiter.abortListener);
|
|
2101
2135
|
}
|
|
2136
|
+
if (waiter.steerSignal && waiter.steerListener) {
|
|
2137
|
+
waiter.steerSignal.removeEventListener("abort", waiter.steerListener);
|
|
2138
|
+
}
|
|
2102
2139
|
}
|
|
2103
2140
|
|
|
2104
2141
|
private resolveWaiterWithMessage(
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type TurnEndEvent,
|
|
21
21
|
} from "@earendil-works/pi-coding-agent";
|
|
22
22
|
import { isKeyRelease, isKeyRepeat, matchesKey } from "@earendil-works/pi-tui";
|
|
23
|
+
import { raiseQueuedSteer, trackQueuedSteers } from "@ian-pascoe/pi-utils/queued-steer";
|
|
23
24
|
import { expandMessageOnClick, noticeText } from "@ian-pascoe/pi-utils/ui";
|
|
24
25
|
import {
|
|
25
26
|
createSubagentAccessBranchRecord,
|
|
@@ -189,7 +190,11 @@ function createRootConversationEndpoint(
|
|
|
189
190
|
return {
|
|
190
191
|
async queueCoordinatorMessage(message) {
|
|
191
192
|
// A running root takes the message as a clearable steer; an idle root starts a turn with it.
|
|
192
|
-
if (!context.isIdle())
|
|
193
|
+
if (!context.isIdle()) {
|
|
194
|
+
onSteer();
|
|
195
|
+
// Pi hands the steer over only after the running tool batch ends, so waits there yield.
|
|
196
|
+
raiseQueuedSteer(context.sessionManager.getSessionId());
|
|
197
|
+
}
|
|
193
198
|
pi.sendMessage(
|
|
194
199
|
{
|
|
195
200
|
customType: message.customType,
|
|
@@ -548,6 +553,7 @@ export class MinimalSubagentsLifecycleController {
|
|
|
548
553
|
* registered only while a result waits in the root's steer queue (`watchRootTurnEnds`).
|
|
549
554
|
*/
|
|
550
555
|
register(): void {
|
|
556
|
+
trackQueuedSteers(this.pi);
|
|
551
557
|
const rootTools = createCoordinatorToolDefinitions({
|
|
552
558
|
coordinator: this.coordinatorOperations,
|
|
553
559
|
callerId: "root",
|
|
@@ -163,6 +163,16 @@ const WaitTimeoutDetailsSchema = Type.Object({
|
|
|
163
163
|
/** Pre-compact timeout results carried the full child status. */
|
|
164
164
|
agent: Type.Optional(RenderStatusAgentSchema),
|
|
165
165
|
});
|
|
166
|
+
const WaitSteeredDetailsSchema = Type.Object({
|
|
167
|
+
event: Type.Literal("steered"),
|
|
168
|
+
agent_id: Type.String(),
|
|
169
|
+
turn_id: Type.String(),
|
|
170
|
+
state: Type.Optional(Type.String()),
|
|
171
|
+
elapsed_ms: Type.Optional(Type.Number()),
|
|
172
|
+
latest_activity_at: Type.Optional(Type.String()),
|
|
173
|
+
total_tokens: Type.Optional(Type.Number()),
|
|
174
|
+
recent_activity_labels: Type.Optional(Type.Array(Type.String())),
|
|
175
|
+
});
|
|
166
176
|
const StatusDetailsSchema = Type.Union([
|
|
167
177
|
Type.Object({
|
|
168
178
|
parent_id: Type.Optional(Type.String()),
|
|
@@ -331,6 +341,17 @@ export const CoordinatorToolOutputSchemas = {
|
|
|
331
341
|
recent_activity_labels: Type.Array(Type.String()),
|
|
332
342
|
...CoordinatorWaitSourceOutputSchema,
|
|
333
343
|
}),
|
|
344
|
+
Type.Object({
|
|
345
|
+
event: Type.Literal("steered"),
|
|
346
|
+
agent_id: Type.String(),
|
|
347
|
+
turn_id: Type.String(),
|
|
348
|
+
state: Type.Union([Type.Literal("running"), Type.Literal("idle")]),
|
|
349
|
+
elapsed_ms: Type.Optional(Type.Number()),
|
|
350
|
+
latest_activity_at: Type.Optional(Type.String()),
|
|
351
|
+
total_tokens: Type.Optional(Type.Number()),
|
|
352
|
+
recent_activity_labels: Type.Array(Type.String()),
|
|
353
|
+
...CoordinatorWaitSourceOutputSchema,
|
|
354
|
+
}),
|
|
334
355
|
]),
|
|
335
356
|
subagent_status: Type.Union([
|
|
336
357
|
Type.Object({
|
|
@@ -351,6 +372,7 @@ const WaitRenderDetailsSchema = Type.Union([
|
|
|
351
372
|
WaitMessageDetailsSchema,
|
|
352
373
|
WaitTurnDetailsSchema,
|
|
353
374
|
WaitTimeoutDetailsSchema,
|
|
375
|
+
WaitSteeredDetailsSchema,
|
|
354
376
|
]);
|
|
355
377
|
|
|
356
378
|
/** Partial `subagent_wait` updates: elapsed wait time and, while the child runs, its progress. */
|
|
@@ -597,13 +597,20 @@ function alreadyDeliveredText(details: { delivery_pending?: boolean }): string {
|
|
|
597
597
|
return details.delivery_pending ? DELIVERY_PENDING_TEXT : ALREADY_DELIVERED_TEXT;
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
-
/**
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
):
|
|
600
|
+
/** A wait that ended early with a progress snapshot: a timeout or a Steer Yield. */
|
|
601
|
+
type WaitEarlyEndRenderDetails = Extract<WaitRenderDetails, { event: "timeout" | "steered" }>;
|
|
602
|
+
|
|
603
|
+
function isWaitProgress(details: WaitRenderDetails): details is WaitEarlyEndRenderDetails {
|
|
604
|
+
return details.event === "timeout" || details.event === "steered";
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** The activity labels of a timeout or steered result, including pre-compact results that carried the full status. */
|
|
608
|
+
function timeoutActivityLabels(details: WaitEarlyEndRenderDetails): readonly string[] {
|
|
604
609
|
return (
|
|
605
610
|
details.recent_activity_labels ??
|
|
606
|
-
latestProgressActivity(
|
|
611
|
+
latestProgressActivity(
|
|
612
|
+
("agent" in details ? details.agent?.recent_activity : undefined) ?? [],
|
|
613
|
+
).map((entry) => entry.label)
|
|
607
614
|
);
|
|
608
615
|
}
|
|
609
616
|
|
|
@@ -614,7 +621,7 @@ function collapsedWaitBody(
|
|
|
614
621
|
theme: MinimalSubagentsRenderTheme,
|
|
615
622
|
): Component | undefined {
|
|
616
623
|
if (details.event === "message") return collapsedText(theme, details.message);
|
|
617
|
-
if (details
|
|
624
|
+
if (isWaitProgress(details)) {
|
|
618
625
|
const labels = timeoutActivityLabels(details);
|
|
619
626
|
return labels.length > 0
|
|
620
627
|
? collapsedText(theme, theme.fg("dim", labels.join(" \u00b7 ")))
|
|
@@ -643,18 +650,18 @@ function renderWaitResult(
|
|
|
643
650
|
? "message"
|
|
644
651
|
: details.event === "timeout"
|
|
645
652
|
? "timed out"
|
|
646
|
-
: details.
|
|
653
|
+
: details.event === "steered"
|
|
654
|
+
? "steered"
|
|
655
|
+
: details.status;
|
|
647
656
|
const duration = formatSubagentDuration(
|
|
648
657
|
details.event === "timeout" ? details.timeout_ms : details.elapsed_ms,
|
|
649
658
|
);
|
|
650
|
-
const usage = details
|
|
659
|
+
const usage = isWaitProgress(details) ? undefined : details.usage;
|
|
651
660
|
const tokens = formatSubagentTokenCount(
|
|
652
|
-
details
|
|
661
|
+
isWaitProgress(details) ? details.total_tokens : usage?.totalTokens,
|
|
653
662
|
);
|
|
654
663
|
const drainedMessageCount =
|
|
655
|
-
details.event === "message" || details.
|
|
656
|
-
? 0
|
|
657
|
-
: (details.messages?.length ?? 0);
|
|
664
|
+
details.event === "message" || isWaitProgress(details) ? 0 : (details.messages?.length ?? 0);
|
|
658
665
|
const metrics = [
|
|
659
666
|
duration,
|
|
660
667
|
tokens ? `${tokens} tokens` : undefined,
|
|
@@ -674,10 +681,9 @@ function renderWaitResult(
|
|
|
674
681
|
container.addChild(renderLabelValue(theme, "Message ID", details.message_id));
|
|
675
682
|
return container;
|
|
676
683
|
}
|
|
677
|
-
if (details
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
);
|
|
684
|
+
if (isWaitProgress(details)) {
|
|
685
|
+
const legacyState = "agent" in details ? details.agent?.state : undefined;
|
|
686
|
+
container.addChild(renderLabelValue(theme, "State", details.state ?? legacyState ?? "unknown"));
|
|
681
687
|
if (details.latest_activity_at) {
|
|
682
688
|
container.addChild(renderLabelValue(theme, "Latest activity", details.latest_activity_at));
|
|
683
689
|
}
|
|
@@ -17,7 +17,9 @@ import type {
|
|
|
17
17
|
ToolSelection,
|
|
18
18
|
WaitDeliveredTurnResult,
|
|
19
19
|
WaitMessageResult,
|
|
20
|
+
WaitProgressSnapshot,
|
|
20
21
|
WaitResult,
|
|
22
|
+
WaitSteeredResult,
|
|
21
23
|
WaitTimeoutResult,
|
|
22
24
|
WaitTurnResult,
|
|
23
25
|
} from "./minimal-subagents-types.js";
|
|
@@ -157,12 +159,8 @@ function formatWaitTurnText(result: WaitTurnResult): string {
|
|
|
157
159
|
return `${lines.join("\n")}\n\n${result.output || "(no output)"}`;
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const lines = [
|
|
163
|
-
`Wait timed out after ${formatSubagentDuration(result.timeout_ms) ?? `${result.timeout_ms}ms`}; ${result.agent_id} is ${result.state} on turn ${result.turn_id} and was not cancelled.`,
|
|
164
|
-
];
|
|
165
|
-
const progress = joinParts([
|
|
162
|
+
function waitProgressText(result: WaitProgressSnapshot): string {
|
|
163
|
+
return joinParts([
|
|
166
164
|
result.elapsed_ms === undefined
|
|
167
165
|
? undefined
|
|
168
166
|
: `elapsed ${formatSubagentDuration(result.elapsed_ms)}`,
|
|
@@ -171,6 +169,27 @@ function formatWaitTimeoutText(result: WaitTimeoutResult): string {
|
|
|
171
169
|
? undefined
|
|
172
170
|
: `${formatSubagentTokenCount(result.total_tokens)} tokens`,
|
|
173
171
|
]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The observational timeout snapshot; the wait never cancels the child. */
|
|
175
|
+
function formatWaitTimeoutText(result: WaitTimeoutResult): string {
|
|
176
|
+
const lines = [
|
|
177
|
+
`Wait timed out after ${formatSubagentDuration(result.timeout_ms) ?? `${result.timeout_ms}ms`}; ${result.agent_id} is ${result.state} on turn ${result.turn_id} and was not cancelled.`,
|
|
178
|
+
];
|
|
179
|
+
const progress = waitProgressText(result);
|
|
180
|
+
if (progress) lines.push(progress);
|
|
181
|
+
if (result.recent_activity_labels.length > 0) {
|
|
182
|
+
lines.push(`recent activity: ${result.recent_activity_labels.join(", ")}`);
|
|
183
|
+
}
|
|
184
|
+
return lines.join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The Steer Yield snapshot: a Queued Steer ended the wait, and the child was not cancelled. */
|
|
188
|
+
function formatWaitSteeredText(result: WaitSteeredResult): string {
|
|
189
|
+
const lines = [
|
|
190
|
+
`Wait ended early: a queued steer is waiting for you. ${result.agent_id} is ${result.state} on turn ${result.turn_id} and was not cancelled; wait again to resume.`,
|
|
191
|
+
];
|
|
192
|
+
const progress = waitProgressText(result);
|
|
174
193
|
if (progress) lines.push(progress);
|
|
175
194
|
if (result.recent_activity_labels.length > 0) {
|
|
176
195
|
lines.push(`recent activity: ${result.recent_activity_labels.join(", ")}`);
|
|
@@ -178,7 +197,7 @@ function formatWaitTimeoutText(result: WaitTimeoutResult): string {
|
|
|
178
197
|
return lines.join("\n");
|
|
179
198
|
}
|
|
180
199
|
|
|
181
|
-
/** Any Wait Event: a message, a settled or already-delivered turn, or a
|
|
200
|
+
/** Any Wait Event: a message, a settled or already-delivered turn, a timeout, or a steer yield. */
|
|
182
201
|
export function formatWaitResultText(result: WaitResult): string {
|
|
183
202
|
if ("already_delivered" in result) return formatWaitDeliveredText(result);
|
|
184
203
|
switch (result.event) {
|
|
@@ -188,6 +207,8 @@ export function formatWaitResultText(result: WaitResult): string {
|
|
|
188
207
|
return formatWaitTurnText(result);
|
|
189
208
|
case "timeout":
|
|
190
209
|
return formatWaitTimeoutText(result);
|
|
210
|
+
case "steered":
|
|
211
|
+
return formatWaitSteeredText(result);
|
|
191
212
|
}
|
|
192
213
|
}
|
|
193
214
|
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
readAgentLineage,
|
|
28
28
|
type AgentLineage,
|
|
29
29
|
} from "@ian-pascoe/pi-utils/agent-lineage";
|
|
30
|
+
import { raiseQueuedSteer, settleQueuedSteers } from "@ian-pascoe/pi-utils/queued-steer";
|
|
30
31
|
import type { Static, TSchema } from "typebox";
|
|
31
32
|
import { Value } from "typebox/value";
|
|
32
33
|
import {
|
|
@@ -525,11 +526,12 @@ export function findDeliveryEvidence(
|
|
|
525
526
|
))
|
|
526
527
|
);
|
|
527
528
|
}
|
|
528
|
-
// A message, timeout, or already-delivered wait result never carried the turn's terminal result.
|
|
529
|
+
// A message, timeout, steer yield, or already-delivered wait result never carried the turn's terminal result.
|
|
529
530
|
if (
|
|
530
531
|
waitToolResult &&
|
|
531
532
|
(details.event === "message" ||
|
|
532
533
|
details.event === "timeout" ||
|
|
534
|
+
details.event === "steered" ||
|
|
533
535
|
details.already_delivered === true)
|
|
534
536
|
)
|
|
535
537
|
return false;
|
|
@@ -631,6 +633,11 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
631
633
|
// Keep this child-only; AgentSession.setSteeringMode would overwrite the user's global setting.
|
|
632
634
|
session.agent.steeringMode = "all";
|
|
633
635
|
this.unsubscribe = session.subscribe((event) => {
|
|
636
|
+
// Child sessions get no Pi `input` events, so this runtime settles its own Queued Steers.
|
|
637
|
+
if (event.type === "turn_start" || event.type === "agent_end") {
|
|
638
|
+
settleQueuedSteers(this.sessionId);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
634
641
|
if (event.type !== "entry_appended") return;
|
|
635
642
|
if (
|
|
636
643
|
event.entry.type === "custom_message" ||
|
|
@@ -641,6 +648,10 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
641
648
|
});
|
|
642
649
|
}
|
|
643
650
|
|
|
651
|
+
private get sessionId(): string {
|
|
652
|
+
return this.session.sessionManager.getSessionId();
|
|
653
|
+
}
|
|
654
|
+
|
|
644
655
|
get sessionLeafId(): string | undefined {
|
|
645
656
|
return this.session.sessionManager.getLeafId() ?? undefined;
|
|
646
657
|
}
|
|
@@ -660,6 +671,8 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
660
671
|
}
|
|
661
672
|
|
|
662
673
|
runMessage(message: CoordinatorMessage): Promise<RuntimeTurnOutcome> {
|
|
674
|
+
// An idle child starts a turn with the message; a streaming child queues it as a steer.
|
|
675
|
+
if (this.session.isStreaming) raiseQueuedSteer(this.sessionId);
|
|
663
676
|
return this.captureTurn(() =>
|
|
664
677
|
this.session.sendCustomMessage(
|
|
665
678
|
{
|
|
@@ -682,6 +695,7 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
682
695
|
details: message.details,
|
|
683
696
|
timestamp: Date.now(),
|
|
684
697
|
});
|
|
698
|
+
raiseQueuedSteer(this.sessionId);
|
|
685
699
|
}
|
|
686
700
|
|
|
687
701
|
async abort(): Promise<void> {
|
|
@@ -691,6 +705,7 @@ class PiChildAgentRuntime implements ChildAgentRuntime {
|
|
|
691
705
|
|
|
692
706
|
async dispose(): Promise<void> {
|
|
693
707
|
this.unsubscribe();
|
|
708
|
+
settleQueuedSteers(this.sessionId);
|
|
694
709
|
try {
|
|
695
710
|
await this.observer?.dispose();
|
|
696
711
|
// Let child extensions release services such as MCP server connections.
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Container, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
13
13
|
import { acceptNullForOptionalArguments } from "@ian-pascoe/pi-utils/null-optional-arguments";
|
|
14
|
+
import { watchQueuedSteer } from "@ian-pascoe/pi-utils/queued-steer";
|
|
14
15
|
import { appendDurationFooter, callDurationFooter } from "@ian-pascoe/pi-utils/ui";
|
|
15
16
|
import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
|
|
16
17
|
import { withTroubleshootingHint } from "./troubleshooting-skill.js";
|
|
@@ -359,7 +360,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
359
360
|
name: "subagent_wait",
|
|
360
361
|
label: "Subagent Wait",
|
|
361
362
|
description:
|
|
362
|
-
"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. If it was handed to you but is not yet in your conversation, delivery_pending=true and it arrives as a separate message; do not reread. Timeout returns a compact event=timeout (state, elapsed_ms, latest_activity_at, total_tokens, last few activity labels; use subagent_status for detail) and never cancels the child.",
|
|
363
|
+
"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. If it was handed to you but is not yet in your conversation, delivery_pending=true and it arrives as a separate message; do not reread. Timeout returns a compact event=timeout (state, elapsed_ms, latest_activity_at, total_tokens, last few activity labels; use subagent_status for detail) and never cancels the child. A queued steer for you returns event=steered with the same snapshot, immediately if one is already pending; it claims nothing and never cancels the child, so wait again to resume.",
|
|
363
364
|
promptSnippet: "Wait for one direct child's exact turn",
|
|
364
365
|
parameters: options.schemas.subagent_wait,
|
|
365
366
|
annotations: {
|
|
@@ -368,7 +369,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
368
369
|
idempotentHint: false,
|
|
369
370
|
openWorldHint: false,
|
|
370
371
|
},
|
|
371
|
-
async execute(_toolCallId, parameters, signal, onUpdate) {
|
|
372
|
+
async execute(_toolCallId, parameters, signal, onUpdate, context) {
|
|
372
373
|
const startedAt = Date.now();
|
|
373
374
|
const updateWaitingResult = () => {
|
|
374
375
|
const details: WaitProgressDetails = {
|
|
@@ -389,6 +390,8 @@ export function createCoordinatorToolDefinitions(
|
|
|
389
390
|
updateWaitingResult();
|
|
390
391
|
const waitingInterval = setInterval(updateWaitingResult, 1_000);
|
|
391
392
|
waitingInterval.unref?.();
|
|
393
|
+
// Pi hands a Queued Steer over only after this tool call returns, so end the wait for it.
|
|
394
|
+
const steerWatch = watchQueuedSteer(context.sessionManager.getSessionId());
|
|
392
395
|
try {
|
|
393
396
|
return await runCoordinatorToolActivity(options, async () => {
|
|
394
397
|
const result = await options.coordinator.wait(
|
|
@@ -397,6 +400,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
397
400
|
parameters.timeout_ms,
|
|
398
401
|
signal,
|
|
399
402
|
parameters.turn_id,
|
|
403
|
+
steerWatch.signal,
|
|
400
404
|
);
|
|
401
405
|
const details = {
|
|
402
406
|
...result,
|
|
@@ -410,6 +414,7 @@ export function createCoordinatorToolDefinitions(
|
|
|
410
414
|
};
|
|
411
415
|
});
|
|
412
416
|
} finally {
|
|
417
|
+
steerWatch.dispose();
|
|
413
418
|
clearInterval(waitingInterval);
|
|
414
419
|
}
|
|
415
420
|
},
|
|
@@ -119,13 +119,8 @@ export interface WaitDeliveredTurnResult extends Pick<
|
|
|
119
119
|
messages?: WaitMessageResult[];
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/**
|
|
123
|
-
export interface
|
|
124
|
-
event: "timeout";
|
|
125
|
-
agent_id: string;
|
|
126
|
-
turn_id: string;
|
|
127
|
-
/** Requested timeout duration that expired. */
|
|
128
|
-
timeout_ms: number;
|
|
122
|
+
/** The compact, bounded child progress snapshot an observational wait ending early reports. */
|
|
123
|
+
export interface WaitProgressSnapshot {
|
|
129
124
|
state: AgentState;
|
|
130
125
|
elapsed_ms?: number;
|
|
131
126
|
latest_activity_at?: string;
|
|
@@ -135,12 +130,32 @@ export interface WaitTimeoutResult {
|
|
|
135
130
|
recent_activity_labels: string[];
|
|
136
131
|
}
|
|
137
132
|
|
|
138
|
-
/** Reports
|
|
133
|
+
/** Reports an observational wait timeout with a compact, bounded child progress snapshot. */
|
|
134
|
+
export interface WaitTimeoutResult extends WaitProgressSnapshot {
|
|
135
|
+
event: "timeout";
|
|
136
|
+
agent_id: string;
|
|
137
|
+
turn_id: string;
|
|
138
|
+
/** Requested timeout duration that expired. */
|
|
139
|
+
timeout_ms: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Reports a Steer Yield: the wait ended early because a Queued Steer is pending for the caller,
|
|
144
|
+
* with the same progress snapshot as a timeout. The child keeps running and nothing is claimed.
|
|
145
|
+
*/
|
|
146
|
+
export interface WaitSteeredResult extends WaitProgressSnapshot {
|
|
147
|
+
event: "steered";
|
|
148
|
+
agent_id: string;
|
|
149
|
+
turn_id: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Reports one message, terminal turn, timeout, or steer yield returned by subagent_wait. */
|
|
139
153
|
export type WaitResult =
|
|
140
154
|
| WaitMessageResult
|
|
141
155
|
| WaitTurnResult
|
|
142
156
|
| WaitDeliveredTurnResult
|
|
143
|
-
| WaitTimeoutResult
|
|
157
|
+
| WaitTimeoutResult
|
|
158
|
+
| WaitSteeredResult;
|
|
144
159
|
|
|
145
160
|
/** Provides bounded hierarchy, usage, and best-known Runtime Profile data for one persistent agent. */
|
|
146
161
|
export interface AgentSummary extends RuntimeProfile {
|