@tranhoangnguyen0310/pi-flow-external 5.0.1-external.0 → 5.1.0-external.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/CHANGELOG.md +8 -0
- package/README.md +2 -2
- package/docs/supervision-and-runtime.md +1 -1
- package/docs/workflows.md +1 -1
- package/package.json +1 -1
- package/src/core/run-registry.ts +75 -1
- package/src/external-help.ts +1 -1
- package/src/external-runs.ts +9 -4
- package/src/pi-subagent.ts +180 -3
- package/src/prompts.ts +2 -2
- package/src/types.ts +7 -0
- package/src/workflow/source.ts +1 -1
- package/src/workflow/tool.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-flow external are documented here.
|
|
4
4
|
|
|
5
|
+
## [5.1.0-external.0] - 2026-10-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Live background run status bar (`pi-flow-runs`), completion notifications, and debounced follow-up wake messages (`pi-flow-background-settled`) for UI sessions:
|
|
9
|
+
- Active background root runs (`external_delegate` and `external_workflow` launched with `background: true`) display a live footer indicator (`pi-flow N bg (...)`) without colliding with usage reporting (`pi-flow`) or double-counting workflow child tasks.
|
|
10
|
+
- When an uncollected background root run settles across a turn boundary, UI sessions emit a completion notice and queue a debounced follow-up wake message (`deliverAs: "followUp"`) carrying the redacted `PublicRun` summary and `external_runs` inspection command.
|
|
11
|
+
- Wake delivery is automatically suppressed for foreground runs, workflow children, caller cancellations (`external_runs cancel`), closed or switched sessions, and runs already collected in the turn via `external_runs wait` or `inspect`.
|
|
12
|
+
|
|
5
13
|
## [5.0.1-external.0] - 2026-10-19
|
|
6
14
|
|
|
7
15
|
### Changed
|
package/README.md
CHANGED
|
@@ -149,12 +149,12 @@ Both `external_delegate` and `external_workflow` support these modes:
|
|
|
149
149
|
| | Foreground (default) | Background (`background: true`) |
|
|
150
150
|
|---|---|---|
|
|
151
151
|
| Original call returns | When the work finishes or fails | After registration, with a run ID; work may already have finished |
|
|
152
|
-
| Progress | Live execution card | A static launch receipt; inspect with `/external runs`
|
|
152
|
+
| Progress | Live execution card | A static launch receipt plus a live status bar count (`pi-flow N bg`); inspect with `/external runs` |
|
|
153
153
|
| Next step | Pi checks the returned outcome | Pi keeps the run ID and does independent work before collecting, or reports the ID and switches tasks/ends its turn if left running across turns |
|
|
154
154
|
|
|
155
155
|
> **You:** "Have Claude audit the API in the background while we review the release checklist. Then bring me the findings."
|
|
156
156
|
|
|
157
|
-
- **
|
|
157
|
+
- **Status bar & completion wake-up:** UI sessions show active background runs in the footer status bar, notify on completion, and automatically queue a follow-up wake message when an uncollected background run finishes across turns. If Pi already collected the run in the same turn, no duplicate message is sent.
|
|
158
158
|
- **Keep Pi open:** background work belongs to its originating session, not a persistent service. Orderly shutdown requests cancellation with bounded cleanup; restart does not adopt unfinished work.
|
|
159
159
|
- **Stopping is different from waiting:** interrupting the original foreground call cancels it. Interrupting `external_runs wait` only stops waiting; use `/external runs` or `external_runs cancel` to stop background work.
|
|
160
160
|
|
|
@@ -101,7 +101,7 @@ return { run, view, encoding, text, checklist };
|
|
|
101
101
|
- **Foreground interruption:** interrupting the original blocking call cancels that work.
|
|
102
102
|
- **Wait interruption:** interrupting `external_runs wait` only stops waiting; it does not cancel the selected runs. Use `external_runs cancel` to stop background work. Cancellation requests are not proof of completed cleanup; check settlement.
|
|
103
103
|
- **Workflow cancellation:** cancelling the root stops its active children. Cancelling one child throws a catchable child error; a script that handles it may continue.
|
|
104
|
-
- **
|
|
104
|
+
- **Status bar & completion wake-up:** ending a turn does not stop background work; it continues in the owning session. UI sessions display active background root runs in the status bar (`pi-flow-runs`), show a completion notification, and queue a debounced follow-up wake message (`pi-flow-background-settled`) when an uncollected background root run settles across turns. Runs already collected in the turn or cancelled by the caller do not send a wake message.
|
|
105
105
|
- **Not a persistent service:** keep the owning Pi session open. Orderly shutdown requests cancellation and waits for bounded cleanup. Crashes or force-kills leave unfinished evidence `interrupted_or_uncertain`; restarting Pi never adopts live orphan work.
|
|
106
106
|
|
|
107
107
|
---
|
package/docs/workflows.md
CHANGED
|
@@ -54,7 +54,7 @@ Put the same roadmap in `meta.phases`, an optional ordered array of `{ title, de
|
|
|
54
54
|
|
|
55
55
|
**A plan is intent, not proof of execution.** The briefing is an agent instruction, not a runtime-verified chat message. Runtime validation checks phase metadata, not whether the script follows its prose. Child states and activity are the source of truth for observed execution. Do not invent completion estimates or call a quiet agent healthy merely because a spinner moves.
|
|
56
56
|
|
|
57
|
-
Choose foreground by default when you need the result next, including parallel workflows. Use background when you have independent local work to do first, or when the user asks to leave the workflow running across turns; it does not make children more parallel. While a foreground call runs, the live card carries progress; the parent cannot reliably send chat updates from inside that call. A background receipt is not a live monitor,
|
|
57
|
+
Choose foreground by default when you need the result next, including parallel workflows. Use background when you have independent local work to do first, or when the user asks to leave the workflow running across turns; it does not make children more parallel. While a foreground call runs, the live card carries progress; the parent cannot reliably send chat updates from inside that call. A background receipt is not a live monitor, though UI sessions show active background runs in the status bar and wake the session with a follow-up message when an uncollected background run settles across turns. Keep the owning session open and collect results after independent work finishes, or report the run ID and switch tasks/end the turn when the user wants to check back later. See [execution and collection](supervision-and-runtime.md#foreground-background--lifecycle) for the complete lifecycle and interruption rules.
|
|
58
58
|
|
|
59
59
|
The recorded purpose and phase details remain accessible in explicit launch inspection, during execution and after completion:
|
|
60
60
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tranhoangnguyen0310/pi-flow-external",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0-external.0",
|
|
4
4
|
"description": "External Claude Code, Codex CLI, Antigravity, Grok Build CLI, Muse Code, and OpenCode delegation for pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
package/src/core/run-registry.ts
CHANGED
|
@@ -20,17 +20,29 @@ export interface RegisteredRunEntry {
|
|
|
20
20
|
sessionId: string;
|
|
21
21
|
project: string;
|
|
22
22
|
workflowRunId?: string;
|
|
23
|
+
background?: boolean;
|
|
23
24
|
state: RegisteredRunState;
|
|
24
25
|
observation?: unknown;
|
|
25
26
|
outcome?: RegisteredRunOutcome;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
export interface BackgroundRunEvent {
|
|
30
|
+
type: "started" | "settled";
|
|
31
|
+
entry: RegisteredRunEntry;
|
|
32
|
+
sessionVersion: number;
|
|
33
|
+
suppressed: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
export interface RegisteredRunHandle<T> {
|
|
29
37
|
result: Promise<T>;
|
|
30
38
|
terminal: Promise<RegisteredRunOutcome>;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
interface InternalEntry extends RegisteredRunEntry {
|
|
42
|
+
sessionVersion: number;
|
|
43
|
+
background: boolean;
|
|
44
|
+
collected: boolean;
|
|
45
|
+
userCancelled: boolean;
|
|
34
46
|
controller: AbortController;
|
|
35
47
|
terminal: Promise<RegisteredRunOutcome>;
|
|
36
48
|
resolveTerminal: (outcome: RegisteredRunOutcome) => void;
|
|
@@ -45,6 +57,7 @@ export interface StartRegisteredRun<T> {
|
|
|
45
57
|
sessionVersion?: number;
|
|
46
58
|
project: string;
|
|
47
59
|
workflowRunId?: string;
|
|
60
|
+
background?: boolean;
|
|
48
61
|
signal?: AbortSignal;
|
|
49
62
|
run: (signal: AbortSignal) => Promise<T> | T;
|
|
50
63
|
outcome?: (result: T, signal: AbortSignal) => Omit<RegisteredRunOutcome, "runId" | "kind" | "settledAt">;
|
|
@@ -62,8 +75,10 @@ const DEFAULT_COMPLETED_LIMIT = 100;
|
|
|
62
75
|
export class RunRegistry {
|
|
63
76
|
private readonly entries = new Map<string, InternalEntry>();
|
|
64
77
|
private readonly completed: string[] = [];
|
|
78
|
+
private readonly collectedIds = new Set<string>();
|
|
65
79
|
private readonly sessionVersions = new Map<string, number>();
|
|
66
80
|
private readonly closedSessions = new Set<string>();
|
|
81
|
+
private readonly backgroundListeners = new Set<(event: BackgroundRunEvent) => void>();
|
|
67
82
|
|
|
68
83
|
constructor(private readonly completedLimit = DEFAULT_COMPLETED_LIMIT, private readonly shutdownGraceMs = 4_000) {
|
|
69
84
|
if (!Number.isInteger(completedLimit) || completedLimit < 0) {
|
|
@@ -97,8 +112,12 @@ export class RunRegistry {
|
|
|
97
112
|
runId: params.runId,
|
|
98
113
|
kind: params.kind,
|
|
99
114
|
sessionId: params.sessionId,
|
|
115
|
+
sessionVersion: params.sessionVersion ?? this.sessionVersion(params.sessionId),
|
|
100
116
|
project: params.project,
|
|
101
117
|
...(params.workflowRunId ? { workflowRunId: params.workflowRunId } : {}),
|
|
118
|
+
background: params.background === true,
|
|
119
|
+
collected: false,
|
|
120
|
+
userCancelled: false,
|
|
102
121
|
state: "running",
|
|
103
122
|
controller,
|
|
104
123
|
terminal,
|
|
@@ -107,6 +126,9 @@ export class RunRegistry {
|
|
|
107
126
|
listeners: new Set(),
|
|
108
127
|
};
|
|
109
128
|
this.entries.set(entry.runId, entry);
|
|
129
|
+
if (entry.background && !entry.workflowRunId) {
|
|
130
|
+
this.emitBackground({ type: "started", entry: this.publicEntry(entry), sessionVersion: entry.sessionVersion, suppressed: false });
|
|
131
|
+
}
|
|
110
132
|
|
|
111
133
|
const abortFromCaller = () => controller.abort(params.signal?.reason);
|
|
112
134
|
if (params.signal?.aborted) abortFromCaller();
|
|
@@ -168,6 +190,35 @@ export class RunRegistry {
|
|
|
168
190
|
.map((entry) => this.publicEntry(entry));
|
|
169
191
|
}
|
|
170
192
|
|
|
193
|
+
listBackgroundRoots(sessionId: string): RegisteredRunEntry[] {
|
|
194
|
+
return [...this.entries.values()]
|
|
195
|
+
.filter((entry) => entry.sessionId === sessionId && entry.background && !entry.workflowRunId && entry.state === "running")
|
|
196
|
+
.map((entry) => this.publicEntry(entry));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
onBackgroundEvent(listener: (event: BackgroundRunEvent) => void): () => void {
|
|
200
|
+
this.backgroundListeners.add(listener);
|
|
201
|
+
return () => this.backgroundListeners.delete(listener);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
markCollected(runId: string): void {
|
|
205
|
+
if (this.collectedIds.size >= 512 && !this.collectedIds.has(runId)) {
|
|
206
|
+
const oldest = this.collectedIds.values().next().value;
|
|
207
|
+
if (oldest) this.collectedIds.delete(oldest);
|
|
208
|
+
}
|
|
209
|
+
this.collectedIds.add(runId);
|
|
210
|
+
const entry = this.entries.get(runId);
|
|
211
|
+
if (entry) entry.collected = true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
isCollected(runId: string): boolean {
|
|
215
|
+
return this.collectedIds.has(runId) || Boolean(this.entries.get(runId)?.collected);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
isSessionActive(sessionId: string, sessionVersion?: number): boolean {
|
|
219
|
+
return !this.closedSessions.has(sessionId) && (sessionVersion === undefined || this.sessionVersion(sessionId) === sessionVersion);
|
|
220
|
+
}
|
|
221
|
+
|
|
171
222
|
update(runId: string, observation: unknown): void {
|
|
172
223
|
const entry = this.entries.get(runId);
|
|
173
224
|
if (entry && entry.state === "running") entry.observation = observation;
|
|
@@ -176,6 +227,8 @@ export class RunRegistry {
|
|
|
176
227
|
cancel(runId: string, reason = "run cancelled"): "requested" | "terminal" | "unknown" {
|
|
177
228
|
const entry = this.entries.get(runId);
|
|
178
229
|
if (!entry) return "unknown";
|
|
230
|
+
entry.userCancelled = true;
|
|
231
|
+
this.markCollected(runId);
|
|
179
232
|
if (entry.state === "terminal") return "terminal";
|
|
180
233
|
entry.controller.abort(new Error(reason));
|
|
181
234
|
return "requested";
|
|
@@ -242,7 +295,17 @@ export class RunRegistry {
|
|
|
242
295
|
entry.resolveTerminal(outcome);
|
|
243
296
|
for (const listener of [...entry.listeners]) listener(outcome);
|
|
244
297
|
entry.listeners.clear();
|
|
245
|
-
|
|
298
|
+
const isCurrent = this.entries.get(entry.runId) === entry;
|
|
299
|
+
if (entry.background && !entry.workflowRunId) {
|
|
300
|
+
const suppressed =
|
|
301
|
+
!isCurrent ||
|
|
302
|
+
entry.collected ||
|
|
303
|
+
this.collectedIds.has(entry.runId) ||
|
|
304
|
+
entry.userCancelled ||
|
|
305
|
+
!this.isSessionActive(entry.sessionId, entry.sessionVersion);
|
|
306
|
+
this.emitBackground({ type: "settled", entry: this.publicEntry(entry), sessionVersion: entry.sessionVersion, suppressed });
|
|
307
|
+
}
|
|
308
|
+
if (!isCurrent) return;
|
|
246
309
|
this.completed.push(entry.runId);
|
|
247
310
|
while (this.completed.length > this.completedLimit) {
|
|
248
311
|
const evicted = this.completed.shift();
|
|
@@ -250,6 +313,16 @@ export class RunRegistry {
|
|
|
250
313
|
}
|
|
251
314
|
}
|
|
252
315
|
|
|
316
|
+
private emitBackground(event: BackgroundRunEvent): void {
|
|
317
|
+
for (const listener of [...this.backgroundListeners]) {
|
|
318
|
+
try {
|
|
319
|
+
listener(event);
|
|
320
|
+
} catch {
|
|
321
|
+
// Never allow a UI or notification listener error to corrupt registry state.
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
253
326
|
private publicEntry(entry: InternalEntry): RegisteredRunEntry {
|
|
254
327
|
return {
|
|
255
328
|
runId: entry.runId,
|
|
@@ -257,6 +330,7 @@ export class RunRegistry {
|
|
|
257
330
|
sessionId: entry.sessionId,
|
|
258
331
|
project: entry.project,
|
|
259
332
|
...(entry.workflowRunId ? { workflowRunId: entry.workflowRunId } : {}),
|
|
333
|
+
...(entry.background ? { background: true } : {}),
|
|
260
334
|
state: entry.state,
|
|
261
335
|
...(entry.observation !== undefined ? { observation: entry.observation } : {}),
|
|
262
336
|
...(entry.outcome ? { outcome: entry.outcome } : {}),
|
package/src/external-help.ts
CHANGED
|
@@ -99,7 +99,7 @@ Before calling external_workflow, explain the purpose, assignments, parallel/seq
|
|
|
99
99
|
|
|
100
100
|
APIs: agent(prompt, { description, label, role, harness, subagent_type, permission, max_budget_usd, resume, context, schema, phase }); parallel(thunks); pipeline(items, ...stages); phase(title); log(message). Globals: args and cwd. Agent options other than the profile selector are optional. "description" is the common task-name option shared with the direct external_delegate tool; "label" is a compatible alias — set only one, or both to the same value. Choose role/harness or legacy exact subagent_type, never both. Use unique descriptions/labels and clear task prompts. An agent() succeeds with its value or throws ChildRunError { runId, outcome, message, outputRef, diagnosticsRef }; catch optional failures explicitly. Uncaught failures terminate the workflow and drain siblings. Helpers never convert failures to null. Context options: {mode:"none"} (default), {mode:"recent",turns:N} (positive integer, includes current user turn), or {mode:"full"} (available context after compaction). All children share invocation-time context/settings; earlier child results must still be passed explicitly. Context excludes thinking/system instructions and pending calls; images and snapshots over 1 MiB fail without truncation. Context sharing cannot be combined with resume. Use schema for results that control branching or aggregation. Imports, filesystem globals, Date APIs, and Math.random() are unavailable.
|
|
101
101
|
|
|
102
|
-
Execution mode: foreground is blocking by default when you need the result next, including parallel workflows. Pass background:true when you have independent local work (read, edit, bash) that does not touch files the workflow may change, or on an explicit background request; do that independent work first and never launch background then immediately wait. Ending a turn does not stop a background run: if the user asks to leave the workflow running, switch tasks, or check back later, report the runId and work on the other task or end your turn without waiting; otherwise retain the ID and collect it after your independent work finishes. Both modes support parallel([() => agent(...), ...]) under the same maxConcurrentSubagents cap; sequential awaits stay serial. Background changes neither permissions nor budget enforcement. Its stable handle means accepted, not completed; the receipt is not a live monitor
|
|
102
|
+
Execution mode: foreground is blocking by default when you need the result next, including parallel workflows. Pass background:true when you have independent local work (read, edit, bash) that does not touch files the workflow may change, or on an explicit background request; do that independent work first and never launch background then immediately wait. Ending a turn does not stop a background run: if the user asks to leave the workflow running, switch tasks, or check back later, report the runId and work on the other task or end your turn without waiting; otherwise retain the ID and collect it after your independent work finishes. Both modes support parallel([() => agent(...), ...]) under the same maxConcurrentSubagents cap; sequential awaits stay serial. Background changes neither permissions nor budget enforcement. Its stable handle means accepted, not completed; the receipt is not a live monitor, though UI sessions show active background runs in the status bar and wake the session with a follow-up message when an uncollected background run settles across turns. Keep the session open: orderly shutdown requests cancellation with bounded cleanup; crashes leave uncertain evidence and restart does not adopt work. Topic usage contains the worked collection guidance.
|
|
103
103
|
|
|
104
104
|
Supervision: external_runs selects targets with runIds. list takes optional workflowRunId, cursor, workflowCursor, and limit. inspect with one id reads summary/output/diagnostics/final/launch and pages with cursor and limitBytes; several ids (up to 20) batch summary only. wait takes up to 100 runIds in mode any or all; check ok for the operation, then each completed run's outcome and pending IDs. It never cancels pending work and can return early on a failed workflow even in all mode. If settlement is already known, skip wait. Use inline successful output when available; otherwise inspect final and follow nextCursor. Failures need diagnostics/output, not final. Follow nextWorkflowCursor for workflow listings too. Foreground-call interruption cancels that call; interrupting wait only stops waiting. Use cancel with one id and an optional reason to stop background work. Cancelling a workflow stops active children; cancelling one child is a catchable workflow error. There is no live steering.
|
|
105
105
|
|
package/src/external-runs.ts
CHANGED
|
@@ -953,6 +953,7 @@ export function createExternalRunsTool(
|
|
|
953
953
|
}
|
|
954
954
|
}
|
|
955
955
|
const served = entries.slice(startIndex, index);
|
|
956
|
+
for (const entry of served) if (!entry.run.live) options.registry.markCollected(entry.run.runId);
|
|
956
957
|
const nextCursor = index < entries.length ? encodeBatchCursor(scope, runIds, index) : undefined;
|
|
957
958
|
const payload = { entries: served.map((entry) => entry.legacy), ...(nextCursor ? { nextCursor } : {}) };
|
|
958
959
|
return { ...result(JSON.stringify(payload), payload), data: { mode: "batch", entries: served.map((entry) => entry.run), ...(nextCursor ? { nextCursor } : {}) } };
|
|
@@ -962,12 +963,15 @@ export function createExternalRunsTool(
|
|
|
962
963
|
assertRunId(params.runId);
|
|
963
964
|
const runId = params.runId;
|
|
964
965
|
const view = params.view ?? "summary";
|
|
965
|
-
const single = (text: string, encoding: "json" | "text", details: Record<string, unknown>, extra: { finalAvailable?: boolean; outputStatus?: string } = {}, contentText = text) => ({
|
|
966
|
-
...result(contentText, details),
|
|
967
|
-
data: { mode: "single", runId, view, page: { text, encoding, complete: typeof details.nextCursor !== "string", ...(typeof details.nextCursor === "string" ? { nextCursor: details.nextCursor } : {}) }, ...compact(extra) },
|
|
968
|
-
});
|
|
969
966
|
const entry = options.registry.get(runId);
|
|
970
967
|
if (entry) assertOwned(entry, sessionId, project);
|
|
968
|
+
const single = (text: string, encoding: "json" | "text", details: Record<string, unknown>, extra: { finalAvailable?: boolean; outputStatus?: string } = {}, contentText = text) => {
|
|
969
|
+
if (!entry || entry.state === "terminal") options.registry.markCollected(runId);
|
|
970
|
+
return {
|
|
971
|
+
...result(contentText, details),
|
|
972
|
+
data: { mode: "single", runId, view, page: { text, encoding, complete: typeof details.nextCursor !== "string", ...(typeof details.nextCursor === "string" ? { nextCursor: details.nextCursor } : {}) }, ...compact(extra) },
|
|
973
|
+
};
|
|
974
|
+
};
|
|
971
975
|
const isWorkflowId = WORKFLOW_ID.test(runId);
|
|
972
976
|
const workflowDir = getSessionWorkflowDir(ctx);
|
|
973
977
|
const historical = !entry && isWorkflowId ? await loadOwnedJournal(workflowDir, runId, project) : undefined;
|
|
@@ -1162,6 +1166,7 @@ export function createExternalRunsTool(
|
|
|
1162
1166
|
const outcomeByRunId = new Map(outcomes.map((outcome) => [outcome.runId, outcome] as const));
|
|
1163
1167
|
const settled = known.flatMap((target) => {
|
|
1164
1168
|
const outcome = outcomeByRunId.get(target.runId);
|
|
1169
|
+
if (outcome) options.registry.markCollected(target.runId);
|
|
1165
1170
|
return outcome ? [{ target, outcome }] : [];
|
|
1166
1171
|
});
|
|
1167
1172
|
const projected = await collectOutcomes(settled.map(({ outcome }) => outcome), runsDirectory, params.limitBytes);
|
package/src/pi-subagent.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ExpectedFlowError } from "./core/errors.ts";
|
|
2
2
|
import { agentOutputSchema, agentReceipt } from "./contract/agent.ts";
|
|
3
3
|
import type { PublicError } from "./contract/envelope.ts";
|
|
4
|
+
import { liveAgentRun, liveWorkflowRun, type PublicRun } from "./contract/run.ts";
|
|
4
5
|
import { getRunRecord } from "./core/run-inspection.ts";
|
|
5
6
|
import { redactSecrets } from "./core/run-record.ts";
|
|
6
7
|
import {
|
|
@@ -66,6 +67,9 @@ const DEFAULT_SUBAGENT_TIMEOUT_MS = DEFAULT_EXTERNAL_SETTINGS.subagentTimeoutMs;
|
|
|
66
67
|
const MAX_CONCURRENT_SUBAGENTS_FLAG = "max-concurrent-subagents";
|
|
67
68
|
const SUBAGENT_TIMEOUT_MS_FLAG = "subagent-timeout-ms";
|
|
68
69
|
const STATUS_KEY = "pi-flow";
|
|
70
|
+
const BG_STATUS_KEY = "pi-flow-runs";
|
|
71
|
+
const BACKGROUND_SETTLED_CUSTOM_TYPE = "pi-flow-background-settled";
|
|
72
|
+
const DEFAULT_BACKGROUND_WAKE_DEBOUNCE_MS = 150;
|
|
69
73
|
|
|
70
74
|
export const agentToolParameters = Type.Object({
|
|
71
75
|
context: Type.Optional(parentContextSchema),
|
|
@@ -76,7 +80,7 @@ export const agentToolParameters = Type.Object({
|
|
|
76
80
|
description: "The task briefing to send to the subagent.",
|
|
77
81
|
}),
|
|
78
82
|
background: Type.Optional(Type.Boolean({
|
|
79
|
-
description: "Defaults to false: wait for settlement. True returns a stable session-owned run handle after registration; work may already be settled. Pass true when you have independent local work to do first (do it, then call external_runs wait), or when the user asks to leave work running across turns (report the runId and do not call external_runs wait this turn).
|
|
83
|
+
description: "Defaults to false: wait for settlement. True returns a stable session-owned run handle after registration; work may already be settled. Pass true when you have independent local work to do first (do it, then call external_runs wait), or when the user asks to leave work running across turns (report the runId and do not call external_runs wait this turn). UI sessions show active background runs in the status bar and wake the session when an uncollected run settles across turns. Keep the owning session open.",
|
|
80
84
|
})),
|
|
81
85
|
role: Type.Optional(Type.String({
|
|
82
86
|
minLength: 1,
|
|
@@ -162,6 +166,7 @@ interface CreateAgentToolOptions {
|
|
|
162
166
|
/** Effective default harness for render paths: cwd only, no trust signal. */
|
|
163
167
|
getDefaultHarness: (cwd: string) => string;
|
|
164
168
|
updateStatus: (ctx: ExtensionContext, toolCallId: string, usage: SubagentUsage) => void;
|
|
169
|
+
onSessionContext?: (ctx: ExtensionContext, sessionId: string) => void;
|
|
165
170
|
}
|
|
166
171
|
|
|
167
172
|
const PROGRESS_STATUSES: SubagentProgressNode["status"][] = ["queued", "running", "done", "error", "aborted"];
|
|
@@ -284,6 +289,32 @@ function formatUsageStatus(totals: SubagentUsage, theme: Theme): string {
|
|
|
284
289
|
return `${theme.fg("dim", "pi-flow ")}${theme.fg("dim", formatUsage(totals))}`;
|
|
285
290
|
}
|
|
286
291
|
|
|
292
|
+
function formatBackgroundRunsStatus(roots: RegisteredRunEntry[], theme?: Theme): string | undefined {
|
|
293
|
+
if (roots.length === 0) return undefined;
|
|
294
|
+
const ids = roots.slice(0, 2).map((r) => r.runId.slice(-8)).join(", ") + (roots.length > 2 ? ", …" : "");
|
|
295
|
+
const text = `pi-flow ${roots.length} bg (${ids})`;
|
|
296
|
+
return typeof theme?.fg === "function" ? theme.fg("accent", text) : text;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function buildBackgroundWakePayload(entries: RegisteredRunEntry[]): { text: string; runs: PublicRun[] } {
|
|
300
|
+
const runs = entries.map((entry) =>
|
|
301
|
+
(entry.kind === "workflow" ? liveWorkflowRun(entry, { inlineBudget: 0 }) : liveAgentRun(entry, { inlineBudget: 0 })).run,
|
|
302
|
+
);
|
|
303
|
+
const lines = runs.map((run) => {
|
|
304
|
+
const label = run.task.name ?? run.task.description ?? run.kind;
|
|
305
|
+
const outcome = run.state.outcome ?? run.state.status;
|
|
306
|
+
const view = outcome === "succeeded" ? "final" : "diagnostics";
|
|
307
|
+
const failedChildren = run.children?.failed ? ` (${run.children.failed} handled child failure(s))` : "";
|
|
308
|
+
return `- ${run.runId} (${run.kind} "${label}"): ${outcome}${failedChildren} — inspect with external_runs { action: "inspect", runIds: ["${run.runId}"], view: "${view}" }`;
|
|
309
|
+
});
|
|
310
|
+
const rawText = [
|
|
311
|
+
"Background run(s) settled:",
|
|
312
|
+
...lines,
|
|
313
|
+
"Use external_runs inspect (view: \"final\" for succeeded runs, \"diagnostics\" or \"output\" for failed runs) to read the results and report back to the user.",
|
|
314
|
+
].join("\n");
|
|
315
|
+
return redactSecrets({ text: rawText, runs }) as { text: string; runs: PublicRun[] };
|
|
316
|
+
}
|
|
317
|
+
|
|
287
318
|
function publishUsageStatus(ctx: ExtensionContext, state: SubagentUsageStatusState): void {
|
|
288
319
|
const totals = getUsageTotals(state);
|
|
289
320
|
if (totals.input === 0 && totals.output === 0 && totals.cacheRead === 0 && totals.cacheWrite === 0) {
|
|
@@ -454,6 +485,7 @@ function createAgentTool(
|
|
|
454
485
|
|
|
455
486
|
const queuedAt = Date.now();
|
|
456
487
|
const sessionId = ctx.sessionManager?.getSessionId?.() ?? `unpersisted:${toolCallId}`;
|
|
488
|
+
options.onSessionContext?.(ctx, sessionId);
|
|
457
489
|
const sessionVersion = state.registry.sessionVersion(sessionId);
|
|
458
490
|
const project = resolve(ctx.cwd);
|
|
459
491
|
// modelRegistry must survive into the execution context: the pi backend
|
|
@@ -595,6 +627,7 @@ function createAgentTool(
|
|
|
595
627
|
sessionId,
|
|
596
628
|
sessionVersion,
|
|
597
629
|
project,
|
|
630
|
+
background,
|
|
598
631
|
...(background ? {} : { signal }),
|
|
599
632
|
run: executeRun,
|
|
600
633
|
outcome: (result, runSignal) => {
|
|
@@ -776,6 +809,118 @@ export function createSubagentExtension(options: SubagentExtensionOptions = {}):
|
|
|
776
809
|
return rootState;
|
|
777
810
|
};
|
|
778
811
|
const usageStatusState = createUsageStatusState();
|
|
812
|
+
const wakeDebounceMs =
|
|
813
|
+
typeof options.backgroundWakeDebounceMs === "number" &&
|
|
814
|
+
Number.isInteger(options.backgroundWakeDebounceMs) &&
|
|
815
|
+
options.backgroundWakeDebounceMs >= 0
|
|
816
|
+
? options.backgroundWakeDebounceMs
|
|
817
|
+
: DEFAULT_BACKGROUND_WAKE_DEBOUNCE_MS;
|
|
818
|
+
let activeSessionId: string | undefined;
|
|
819
|
+
let activeUiCtx: ExtensionContext | undefined;
|
|
820
|
+
let inAgentTurn = false;
|
|
821
|
+
let wakeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
822
|
+
const pendingWakeBySession = new Map<string, Map<string, { entry: RegisteredRunEntry; sessionVersion: number }>>();
|
|
823
|
+
|
|
824
|
+
const rememberSessionContext = (ctx: ExtensionContext, sessionId = ctx.sessionManager?.getSessionId?.()) => {
|
|
825
|
+
if (sessionId && !sessionId.startsWith("unpersisted:")) {
|
|
826
|
+
if (activeSessionId !== sessionId) activeUiCtx = ctx;
|
|
827
|
+
activeSessionId = sessionId;
|
|
828
|
+
} else if (!activeSessionId && sessionId) {
|
|
829
|
+
activeSessionId = sessionId;
|
|
830
|
+
}
|
|
831
|
+
if (ctx.hasUI || !activeUiCtx) activeUiCtx = ctx;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
const updateBackgroundStatus = (sessionId = activeSessionId) => {
|
|
835
|
+
if (!sessionId || sessionId !== activeSessionId || !activeUiCtx?.hasUI || typeof activeUiCtx.ui?.setStatus !== "function") return;
|
|
836
|
+
const roots = rootState.registry.listBackgroundRoots(sessionId);
|
|
837
|
+
try {
|
|
838
|
+
activeUiCtx.ui.setStatus(BG_STATUS_KEY, formatBackgroundRunsStatus(roots, activeUiCtx.ui.theme));
|
|
839
|
+
} catch {
|
|
840
|
+
// Ignore stale or disposed UI contexts.
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
const clearWakeTimer = () => {
|
|
845
|
+
if (wakeTimer) {
|
|
846
|
+
clearTimeout(wakeTimer);
|
|
847
|
+
wakeTimer = undefined;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const flushPendingWakes = (sessionId: string) => {
|
|
852
|
+
if (inAgentTurn || sessionId !== activeSessionId) return;
|
|
853
|
+
const pending = pendingWakeBySession.get(sessionId);
|
|
854
|
+
if (!pending || pending.size === 0) return;
|
|
855
|
+
pendingWakeBySession.delete(sessionId);
|
|
856
|
+
const uncollected = [...pending.values()].filter(
|
|
857
|
+
(item) =>
|
|
858
|
+
!rootState.registry.isCollected(item.entry.runId) &&
|
|
859
|
+
rootState.registry.isSessionActive(sessionId, item.sessionVersion),
|
|
860
|
+
);
|
|
861
|
+
if (uncollected.length === 0) return;
|
|
862
|
+
const payload = buildBackgroundWakePayload(uncollected.map((item) => item.entry));
|
|
863
|
+
try {
|
|
864
|
+
pi.sendMessage(
|
|
865
|
+
{
|
|
866
|
+
customType: BACKGROUND_SETTLED_CUSTOM_TYPE,
|
|
867
|
+
content: [{ type: "text", text: payload.text }],
|
|
868
|
+
display: true,
|
|
869
|
+
details: { runs: payload.runs },
|
|
870
|
+
},
|
|
871
|
+
{ triggerTurn: true, deliverAs: "followUp" },
|
|
872
|
+
);
|
|
873
|
+
for (const item of uncollected) rootState.registry.markCollected(item.entry.runId);
|
|
874
|
+
} catch {
|
|
875
|
+
if (activeSessionId === sessionId) {
|
|
876
|
+
activeSessionId = undefined;
|
|
877
|
+
activeUiCtx = undefined;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const scheduleWakeFlush = (sessionId: string) => {
|
|
883
|
+
if (inAgentTurn || sessionId !== activeSessionId) return;
|
|
884
|
+
const pending = pendingWakeBySession.get(sessionId);
|
|
885
|
+
if (!pending || pending.size === 0) return;
|
|
886
|
+
clearWakeTimer();
|
|
887
|
+
wakeTimer = setTimeout(() => {
|
|
888
|
+
wakeTimer = undefined;
|
|
889
|
+
flushPendingWakes(sessionId);
|
|
890
|
+
}, wakeDebounceMs);
|
|
891
|
+
(wakeTimer as { unref?: () => void }).unref?.();
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
rootState.registry.onBackgroundEvent((event) => {
|
|
895
|
+
updateBackgroundStatus(event.entry.sessionId);
|
|
896
|
+
if (event.type !== "settled") return;
|
|
897
|
+
if (event.suppressed) {
|
|
898
|
+
pendingWakeBySession.get(event.entry.sessionId)?.delete(event.entry.runId);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (
|
|
902
|
+
activeUiCtx?.hasUI &&
|
|
903
|
+
activeSessionId === event.entry.sessionId &&
|
|
904
|
+
typeof activeUiCtx.ui?.notify === "function"
|
|
905
|
+
) {
|
|
906
|
+
const outcome = event.entry.outcome?.outcome ?? "settled";
|
|
907
|
+
const label = event.entry.kind === "workflow" ? "Background workflow" : "Background subagent";
|
|
908
|
+
activeUiCtx.ui.notify(
|
|
909
|
+
redactSecrets(`${label} ${event.entry.runId} ${outcome}.`) as string,
|
|
910
|
+
outcome === "succeeded" ? "info" : "warning",
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
const shouldWake = options.backgroundWake ?? Boolean(activeUiCtx?.hasUI);
|
|
914
|
+
if (!shouldWake) return;
|
|
915
|
+
let sessionPending = pendingWakeBySession.get(event.entry.sessionId);
|
|
916
|
+
if (!sessionPending) {
|
|
917
|
+
sessionPending = new Map();
|
|
918
|
+
pendingWakeBySession.set(event.entry.sessionId, sessionPending);
|
|
919
|
+
}
|
|
920
|
+
sessionPending.set(event.entry.runId, { entry: event.entry, sessionVersion: event.sessionVersion });
|
|
921
|
+
scheduleWakeFlush(event.entry.sessionId);
|
|
922
|
+
});
|
|
923
|
+
|
|
779
924
|
const toolOptions: CreateAgentToolOptions = {
|
|
780
925
|
getLimiter: () => syncMaxConcurrentSubagents().limiter,
|
|
781
926
|
getThinkingLevel: () => pi.getThinkingLevel(),
|
|
@@ -788,6 +933,7 @@ export function createSubagentExtension(options: SubagentExtensionOptions = {}):
|
|
|
788
933
|
}
|
|
789
934
|
updateUsageStatus(usageStatusState, ctx, toolCallId, usage);
|
|
790
935
|
},
|
|
936
|
+
onSessionContext: rememberSessionContext,
|
|
791
937
|
};
|
|
792
938
|
|
|
793
939
|
pi.registerTool(createAgentTool(syncMaxConcurrentSubagents, toolOptions));
|
|
@@ -856,12 +1002,30 @@ export function createSubagentExtension(options: SubagentExtensionOptions = {}):
|
|
|
856
1002
|
}
|
|
857
1003
|
updateUsageStatus(usageStatusState, ctx, toolCallId, usage);
|
|
858
1004
|
},
|
|
1005
|
+
onSessionContext: rememberSessionContext,
|
|
859
1006
|
}),
|
|
860
1007
|
);
|
|
861
1008
|
}
|
|
862
1009
|
|
|
1010
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
1011
|
+
if (!activeSessionId) rememberSessionContext(ctx);
|
|
1012
|
+
inAgentTurn = true;
|
|
1013
|
+
clearWakeTimer();
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
1017
|
+
if (!activeSessionId) rememberSessionContext(ctx);
|
|
1018
|
+
inAgentTurn = false;
|
|
1019
|
+
if (activeSessionId) scheduleWakeFlush(activeSessionId);
|
|
1020
|
+
});
|
|
1021
|
+
|
|
863
1022
|
pi.on("session_start", (_event, ctx) => {
|
|
864
|
-
|
|
1023
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1024
|
+
rememberSessionContext(ctx, sessionId);
|
|
1025
|
+
inAgentTurn = false;
|
|
1026
|
+
clearWakeTimer();
|
|
1027
|
+
pendingWakeBySession.clear();
|
|
1028
|
+
rootState.registry.openSession(sessionId);
|
|
865
1029
|
syncMaxConcurrentSubagents();
|
|
866
1030
|
usageStatusState.calls.clear();
|
|
867
1031
|
usageStatusState.latestCacheHitRate = undefined;
|
|
@@ -869,12 +1033,24 @@ export function createSubagentExtension(options: SubagentExtensionOptions = {}):
|
|
|
869
1033
|
void pruneRunRecords(runRecordsDirectory(), rootState.maxRunRecords).catch(() => undefined);
|
|
870
1034
|
if (ctx.hasUI) {
|
|
871
1035
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
1036
|
+
ctx.ui.setStatus(BG_STATUS_KEY, undefined);
|
|
872
1037
|
}
|
|
873
1038
|
});
|
|
874
1039
|
|
|
875
1040
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1041
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1042
|
+
inAgentTurn = false;
|
|
1043
|
+
clearWakeTimer();
|
|
1044
|
+
pendingWakeBySession.delete(sessionId);
|
|
1045
|
+
if (activeSessionId === sessionId) {
|
|
1046
|
+
activeSessionId = undefined;
|
|
1047
|
+
activeUiCtx = undefined;
|
|
1048
|
+
}
|
|
876
1049
|
for (const controller of readinessControllers) controller.abort(new Error('Session shutting down'));
|
|
877
|
-
const cleanup = await rootState.registry.shutdownSession(
|
|
1050
|
+
const cleanup = await rootState.registry.shutdownSession(sessionId);
|
|
1051
|
+
if (ctx.hasUI) {
|
|
1052
|
+
ctx.ui.setStatus(BG_STATUS_KEY, undefined);
|
|
1053
|
+
}
|
|
878
1054
|
if (cleanup.pending.length && ctx.hasUI) {
|
|
879
1055
|
const shown = cleanup.pending.slice(0, 10);
|
|
880
1056
|
ctx.ui.notify(`External run cleanup could not be confirmed for ${shown.join(", ")}${cleanup.pending.length > shown.length ? ` and ${cleanup.pending.length - shown.length} more` : ""}; their evidence is interrupted or uncertain.`, "warning");
|
|
@@ -882,6 +1058,7 @@ export function createSubagentExtension(options: SubagentExtensionOptions = {}):
|
|
|
882
1058
|
});
|
|
883
1059
|
|
|
884
1060
|
pi.on("before_agent_start", (event, ctx) => {
|
|
1061
|
+
if (!activeSessionId) rememberSessionContext(ctx);
|
|
885
1062
|
const tools = pi.getAllTools();
|
|
886
1063
|
if (!tools.some((tool) => tool.name === EXTERNAL_DELEGATE_TOOL)) {
|
|
887
1064
|
return;
|
package/src/prompts.ts
CHANGED
|
@@ -100,7 +100,7 @@ Give every child an objective, the context it needs, the changes it may make, a
|
|
|
100
100
|
|
|
101
101
|
Choose a fresh child or resume on purpose. Omit context for independent work. context {mode:"recent", turns:N} shares the last N user turns, including the current turn; {mode:"full"} shares the available post-compaction conversation. Snapshots drop thinking, system instructions, and pending calls, reject images and payloads over 1 MiB, and are sent to that harness. resume continues the same CLI child and cannot be combined with sharing. A named pi-* harness has no persisted session and no enforced budget. Prefer resume for the same task and a new child for an independent review.
|
|
102
102
|
|
|
103
|
-
Default to foreground (blocking) when you need the result next, including parallel workflows. When you have independent local work (read, edit, bash) that does not touch files the delegated run may change, or the user asks for background execution, pass background:true, do that other work first, and only call external_runs wait once you actually need the result; never background then immediately wait. Ending a turn does not stop a background run: if the user asks to leave it running, switch tasks, or check back later, report the runId and end your turn or work on the other task without waiting. Otherwise retain the runId and collect it in this turn after your independent work finishes. Foreground has a live card; a background receipt is not a live monitor and
|
|
103
|
+
Default to foreground (blocking) when you need the result next, including parallel workflows. When you have independent local work (read, edit, bash) that does not touch files the delegated run may change, or the user asks for background execution, pass background:true, do that other work first, and only call external_runs wait once you actually need the result; never background then immediately wait. Ending a turn does not stop a background run: if the user asks to leave it running, switch tasks, or check back later, report the runId and end your turn or work on the other task without waiting. Otherwise retain the runId and collect it in this turn after your independent work finishes. Foreground has a live card; a background receipt is not a live monitor, though UI sessions show active background runs in the status bar and wake the session with a follow-up message when an uncollected background run settles across turns. Use external_runs wait only when needed; check each run's outcome and pending IDs. Read inline successful output or inspect view final; inspect failures with diagnostics. Interrupting foreground cancels that call; interrupting wait only stops waiting. Explicit cancel stops background work. Keep the owning session open: orderly shutdown requests cancellation; restart does not adopt work. Prefer one external_workflow with parallel() for coordinated parallel tasks; all children share maxConcurrentSubagents. Sequential awaits stay serial. Report milestones. Retry a failed run, or switch its model or harness, only when the user authorized that step. agy alone may make one disclosed infrastructure retry. Nested agents may start in another workspace.
|
|
104
104
|
|
|
105
105
|
Catalog availability reflects enabled harnesses and built-in/authored roles, not CLI installation or authentication. Disabled harnesses reject calls without fallback; manage them through /external config. Harness and role lifecycle commands live under /external config harness and /external config role. Role selection never falls back. external_help topic usage is the playbook; roles, permissions, and workflow are the references.`;
|
|
106
106
|
}
|
|
@@ -215,7 +215,7 @@ Tell each child the objective, the context it needs, what it may change, the del
|
|
|
215
215
|
|
|
216
216
|
Choose fresh context or resume deliberately. Omit context, or pass {"mode":"none"}, for independent work such as a review. {"mode":"recent","turns":N} and {"mode":"full"} share parent conversation and cannot be combined with resume. resume continues one prior CLI run that recorded a session id, on the same backend: ${EXTERNAL_HARNESSES.join(", ")}. A named pi-* harness has no persisted session, so resume does not continue it. Prefer resume for the same task and a new child when you want an independent judgment.
|
|
217
217
|
|
|
218
|
-
Choose foreground by default when you need the result next, even for a long or parallel workflow: the original call waits for settlement and its live card carries progress. Pass background:true when you have independent local work (read, edit, bash) that does not touch files the delegated run may change, or when the user asks for background execution. Do that independent work first and never follow a background launch with an immediate external_runs wait. Ending a turn does not stop a background run: if the user asks to leave the workflow running, attend to another task, or check back later, report the runId and proceed with the other task or end your turn without waiting. Otherwise keep the ID and collect/check the outcome after your independent work finishes. The returned handle means accepted, not completed; the run may already be settled.
|
|
218
|
+
Choose foreground by default when you need the result next, even for a long or parallel workflow: the original call waits for settlement and its live card carries progress. Pass background:true when you have independent local work (read, edit, bash) that does not touch files the delegated run may change, or when the user asks for background execution. Do that independent work first and never follow a background launch with an immediate external_runs wait. Ending a turn does not stop a background run: if the user asks to leave the workflow running, attend to another task, or check back later, report the runId and proceed with the other task or end your turn without waiting. Otherwise keep the ID and collect/check the outcome after your independent work finishes. The returned handle means accepted, not completed; the run may already be settled. UI sessions display active background runs in the status bar and send a follow-up wake message when an uncollected background run settles across turns. Keep Pi open: orderly shutdown requests cancellation with bounded cleanup; crashes leave uncertain evidence and restart does not adopt work.
|
|
219
219
|
|
|
220
220
|
Unless the user deferred collection, after independent work use external_runs wait if settlement is not already known. When the user later asks about a deferred run, inspect its runId first and wait only if it is still running. Check ok for the operation, then each completed run's state.outcome and pending IDs; a failed workflow can return wait early even in mode all. Use successful output when delivery is inline (null/false/0 are valid); otherwise inspect view final, following cursor pages. For failures inspect diagnostics or output; final is empty without verified success. A successful workflow may still have children.failed when its script caught child errors; read the reports. Interrupting the original foreground call cancels its work. Interrupting wait only stops waiting; cancel explicitly to stop background work. Cancelling a workflow stops active children; cancelling one child throws a catchable child error. Both modes share permissions, budget enforcement, and concurrency limits. A failed or aborted run stays failed. Retry it, or switch model or harness, only when the user authorized that step. agy may make one disclosed infrastructure retry; that exception is not yours to extend.
|
|
221
221
|
|
package/src/types.ts
CHANGED
|
@@ -76,6 +76,13 @@ export interface SubagentExtensionOptions {
|
|
|
76
76
|
* one product, two entry points. Set to false for a subagents-only surface.
|
|
77
77
|
*/
|
|
78
78
|
workflow?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Automatically wake the parent session with a follow-up message when an
|
|
81
|
+
* uncollected background root run settles across a turn. Defaults to `ctx.hasUI`.
|
|
82
|
+
*/
|
|
83
|
+
backgroundWake?: boolean;
|
|
84
|
+
/** Debounce window in milliseconds for batching background completion wake-ups (default: 150). */
|
|
85
|
+
backgroundWakeDebounceMs?: number;
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
export type FlowExtensionOptions = SubagentExtensionOptions;
|
package/src/workflow/source.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type { WorkflowCachedAgentResult, WorkflowMetaPhase } from "./types.ts";
|
|
|
20
20
|
|
|
21
21
|
export const workflowToolParameters = Type.Object({
|
|
22
22
|
background: Type.Optional(Type.Boolean({
|
|
23
|
-
description: "Defaults to false: wait for settlement. True returns a stable session-owned workflow handle after registration; work may already be settled. Pass true when you have independent local work to do first (do it, then call external_runs wait), or when the user asks to leave work running across turns (report the runId and do not call external_runs wait this turn).
|
|
23
|
+
description: "Defaults to false: wait for settlement. True returns a stable session-owned workflow handle after registration; work may already be settled. Pass true when you have independent local work to do first (do it, then call external_runs wait), or when the user asks to leave work running across turns (report the runId and do not call external_runs wait this turn). UI sessions show active background runs in the status bar and wake the session when an uncollected run settles across turns. Keep the owning session open.",
|
|
24
24
|
})),
|
|
25
25
|
script: Type.Optional(
|
|
26
26
|
Type.String({
|
package/src/workflow/tool.ts
CHANGED
|
@@ -47,6 +47,7 @@ export interface CreateWorkflowToolOptions {
|
|
|
47
47
|
getThinkingLevel: () => ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
48
48
|
getSubagentTimeoutMs: () => number;
|
|
49
49
|
updateStatus: (ctx: ExtensionContext, toolCallId: string, usage: SubagentUsage) => void;
|
|
50
|
+
onSessionContext?: (ctx: ExtensionContext, sessionId: string) => void;
|
|
50
51
|
getDefaultPermission: () => PermissionTier;
|
|
51
52
|
getDefaultHarness: (ctx: ExtensionContext) => string;
|
|
52
53
|
getDefaultMaxBudgetUsd: () => number | undefined;
|
|
@@ -146,6 +147,7 @@ export function createWorkflowTool(
|
|
|
146
147
|
return await withContract<WorkflowToolDetails>({ tool: EXTERNAL_WORKFLOW_TOOL, action: "run", failureData: { run: null }, redact: true }, async () => {
|
|
147
148
|
const parentMessages = captureParentContext(ctx.sessionManager);
|
|
148
149
|
const sessionId = ctx.sessionManager?.getSessionId?.() ?? `unpersisted:${toolCallId}`;
|
|
150
|
+
options.onSessionContext?.(ctx, sessionId);
|
|
149
151
|
const sessionVersion = options.registry.sessionVersion(sessionId);
|
|
150
152
|
const project = resolve(ctx.cwd);
|
|
151
153
|
// modelRegistry must survive into the execution context: the pi backend
|
|
@@ -615,6 +617,7 @@ export function createWorkflowTool(
|
|
|
615
617
|
sessionId,
|
|
616
618
|
sessionVersion,
|
|
617
619
|
project,
|
|
620
|
+
background,
|
|
618
621
|
...(background ? {} : { signal }),
|
|
619
622
|
run: executeWorkflow,
|
|
620
623
|
outcome: (result, workflowSignal) => ({
|