pi-subagents 0.21.2 → 0.21.3
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
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.21.3] - 2026-04-30
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Debounce foreground `needs_attention` notices, make them non-triggering, and cancel them when the run finishes so stale chain-step alerts do not launch parent turns after completion.
|
|
9
|
+
|
|
5
10
|
## [0.21.2] - 2026-04-30
|
|
6
11
|
|
|
7
12
|
### Added
|
package/package.json
CHANGED
|
@@ -261,6 +261,8 @@ without forcing each step to rediscover everything.
|
|
|
261
261
|
|
|
262
262
|
### Async/background
|
|
263
263
|
|
|
264
|
+
Use async mode whenever the parent agent should keep working while a child runs. A normal foreground `subagent(...)` call blocks the parent until the child completes; it is appropriate when the next parent step depends on the child result. If you say you will "ask a reviewer while I continue auditing" or otherwise run local work in parallel with a child, launch with `async: true`. Do not end your turn immediately after launching that async child if you promised to keep working; continue the local inspection or other independent work, then check the async run when its result is needed.
|
|
265
|
+
|
|
264
266
|
```typescript
|
|
265
267
|
subagent({
|
|
266
268
|
agent: "worker",
|
|
@@ -269,6 +271,18 @@ subagent({
|
|
|
269
271
|
})
|
|
270
272
|
```
|
|
271
273
|
|
|
274
|
+
For review fanout where the parent continues a local audit:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
const run = subagent({
|
|
278
|
+
agent: "reviewer",
|
|
279
|
+
task: "Review the current diff for correctness issues. Do not edit files.",
|
|
280
|
+
async: true,
|
|
281
|
+
context: "fresh"
|
|
282
|
+
})
|
|
283
|
+
// Continue local inspection, then later call status with the returned id.
|
|
284
|
+
```
|
|
285
|
+
|
|
272
286
|
Inspect async runs with `subagent({ action: "status", id: "..." })`, `subagent({ action: "status" })` for active runs, or the `/subagents-status` slash command.
|
|
273
287
|
|
|
274
288
|
Use diagnostics when setup or child startup looks wrong:
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { controlNotificationKey, formatControlNoticeMessage } from "../runs/shared/subagent-control.ts";
|
|
3
|
+
import type { ControlEvent, SubagentState } from "../shared/types.ts";
|
|
4
|
+
|
|
5
|
+
export const SUBAGENT_CONTROL_MESSAGE_TYPE = "subagent_control_notice";
|
|
6
|
+
|
|
7
|
+
export interface SubagentControlMessageDetails {
|
|
8
|
+
event: ControlEvent;
|
|
9
|
+
source?: "foreground" | "async";
|
|
10
|
+
asyncDir?: string;
|
|
11
|
+
childIntercomTarget?: string;
|
|
12
|
+
noticeText?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function controlNoticeTarget(details: SubagentControlMessageDetails): string | undefined {
|
|
16
|
+
return details.childIntercomTarget;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatSubagentControlNotice(details: SubagentControlMessageDetails, content?: string): string {
|
|
20
|
+
return details.noticeText ?? content ?? formatControlNoticeMessage(details.event, controlNoticeTarget(details));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function noticeTimerKey(details: SubagentControlMessageDetails): string {
|
|
24
|
+
const childIntercomTarget = controlNoticeTarget(details);
|
|
25
|
+
return `${details.event.runId}:${controlNotificationKey(details.event, childIntercomTarget)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function clearPendingForegroundControlNotices(state: SubagentState, runId?: string): void {
|
|
29
|
+
const pending = state.pendingForegroundControlNotices;
|
|
30
|
+
if (!pending) return;
|
|
31
|
+
for (const [key, timer] of pending) {
|
|
32
|
+
if (runId !== undefined && !key.startsWith(`${runId}:`)) continue;
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
pending.delete(key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function deliverControlNotice(input: {
|
|
39
|
+
pi: Pick<ExtensionAPI, "sendMessage">;
|
|
40
|
+
visibleControlNotices: Set<string>;
|
|
41
|
+
details: SubagentControlMessageDetails;
|
|
42
|
+
}): void {
|
|
43
|
+
const childIntercomTarget = controlNoticeTarget(input.details);
|
|
44
|
+
const key = controlNotificationKey(input.details.event, childIntercomTarget);
|
|
45
|
+
if (input.visibleControlNotices.has(key)) return;
|
|
46
|
+
input.visibleControlNotices.add(key);
|
|
47
|
+
const noticeText = input.details.noticeText ?? formatControlNoticeMessage(input.details.event, childIntercomTarget);
|
|
48
|
+
input.pi.sendMessage(
|
|
49
|
+
{
|
|
50
|
+
customType: SUBAGENT_CONTROL_MESSAGE_TYPE,
|
|
51
|
+
content: noticeText,
|
|
52
|
+
display: true,
|
|
53
|
+
details: { ...input.details, childIntercomTarget, noticeText },
|
|
54
|
+
},
|
|
55
|
+
{ triggerTurn: input.details.source !== "foreground" },
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isForegroundNoticeStillActionable(state: SubagentState, details: SubagentControlMessageDetails): boolean {
|
|
60
|
+
const control = state.foregroundControls.get(details.event.runId);
|
|
61
|
+
if (!control) return false;
|
|
62
|
+
if (control.currentAgent && control.currentAgent !== details.event.agent) return false;
|
|
63
|
+
if (details.event.index !== undefined && control.currentIndex !== details.event.index) return false;
|
|
64
|
+
return control.currentActivityState === "needs_attention";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function handleSubagentControlNotice(input: {
|
|
68
|
+
pi: Pick<ExtensionAPI, "sendMessage">;
|
|
69
|
+
state: SubagentState;
|
|
70
|
+
visibleControlNotices: Set<string>;
|
|
71
|
+
details: SubagentControlMessageDetails;
|
|
72
|
+
foregroundDelayMs?: number;
|
|
73
|
+
}): void {
|
|
74
|
+
if (!input.details?.event || input.details.event.type === "active_long_running") return;
|
|
75
|
+
if (input.details.source !== "foreground") {
|
|
76
|
+
deliverControlNotice(input);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const pending = input.state.pendingForegroundControlNotices ?? new Map<string, ReturnType<typeof setTimeout>>();
|
|
81
|
+
input.state.pendingForegroundControlNotices = pending;
|
|
82
|
+
const timerKey = noticeTimerKey(input.details);
|
|
83
|
+
const existing = pending.get(timerKey);
|
|
84
|
+
if (existing) clearTimeout(existing);
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
pending.delete(timerKey);
|
|
87
|
+
if (!isForegroundNoticeStillActionable(input.state, input.details)) return;
|
|
88
|
+
deliverControlNotice(input);
|
|
89
|
+
}, input.foregroundDelayMs ?? 1000);
|
|
90
|
+
timer.unref?.();
|
|
91
|
+
pending.set(timerKey, timer);
|
|
92
|
+
}
|
package/src/extension/index.ts
CHANGED
|
@@ -25,7 +25,6 @@ import { renderWidget, renderSubagentResult, stopResultAnimations, stopWidgetAni
|
|
|
25
25
|
import { SubagentParams } from "./schemas.ts";
|
|
26
26
|
import { createSubagentExecutor } from "../runs/foreground/subagent-executor.ts";
|
|
27
27
|
import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
|
|
28
|
-
import { controlNotificationKey, formatControlNoticeMessage } from "../runs/shared/subagent-control.ts";
|
|
29
28
|
import { createResultWatcher } from "../runs/background/result-watcher.ts";
|
|
30
29
|
import { registerSlashCommands } from "../slash/slash-commands.ts";
|
|
31
30
|
import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.ts";
|
|
@@ -36,7 +35,6 @@ import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/back
|
|
|
36
35
|
import { SUBAGENT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
37
36
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
38
37
|
import {
|
|
39
|
-
type ControlEvent,
|
|
40
38
|
type Details,
|
|
41
39
|
type ExtensionConfig,
|
|
42
40
|
type SubagentState,
|
|
@@ -49,6 +47,13 @@ import {
|
|
|
49
47
|
SUBAGENT_CONTROL_EVENT,
|
|
50
48
|
WIDGET_KEY,
|
|
51
49
|
} from "../shared/types.ts";
|
|
50
|
+
import {
|
|
51
|
+
clearPendingForegroundControlNotices,
|
|
52
|
+
formatSubagentControlNotice,
|
|
53
|
+
handleSubagentControlNotice,
|
|
54
|
+
SUBAGENT_CONTROL_MESSAGE_TYPE,
|
|
55
|
+
type SubagentControlMessageDetails,
|
|
56
|
+
} from "./control-notices.ts";
|
|
52
57
|
|
|
53
58
|
/**
|
|
54
59
|
* Derive subagent session base directory from parent session file.
|
|
@@ -153,24 +158,6 @@ function createSlashResultComponent(
|
|
|
153
158
|
return container;
|
|
154
159
|
}
|
|
155
160
|
|
|
156
|
-
const SUBAGENT_CONTROL_MESSAGE_TYPE = "subagent_control_notice";
|
|
157
|
-
|
|
158
|
-
interface SubagentControlMessageDetails {
|
|
159
|
-
event: ControlEvent;
|
|
160
|
-
source?: "foreground" | "async";
|
|
161
|
-
asyncDir?: string;
|
|
162
|
-
childIntercomTarget?: string;
|
|
163
|
-
noticeText?: string;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function controlNoticeTarget(details: SubagentControlMessageDetails): string | undefined {
|
|
167
|
-
return details.childIntercomTarget;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function formatSubagentControlNotice(details: SubagentControlMessageDetails, content?: string): string {
|
|
171
|
-
return details.noticeText ?? content ?? formatControlNoticeMessage(details.event, controlNoticeTarget(details));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
161
|
function parseSubagentNotifyContent(content: string): SubagentNotifyDetails | undefined {
|
|
175
162
|
const lines = content.split("\n");
|
|
176
163
|
const header = lines[0] ?? "";
|
|
@@ -259,6 +246,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
259
246
|
asyncJobs: new Map(),
|
|
260
247
|
foregroundControls: new Map(),
|
|
261
248
|
lastForegroundControlId: null,
|
|
249
|
+
pendingForegroundControlNotices: new Map(),
|
|
262
250
|
cleanupTimers: new Map(),
|
|
263
251
|
lastUiContext: null,
|
|
264
252
|
poller: null,
|
|
@@ -283,6 +271,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
283
271
|
const runtimeCleanup = () => {
|
|
284
272
|
stopWidgetAnimation();
|
|
285
273
|
stopResultAnimations();
|
|
274
|
+
clearPendingForegroundControlNotices(state);
|
|
286
275
|
if (state.poller) {
|
|
287
276
|
clearInterval(state.poller);
|
|
288
277
|
state.poller = null;
|
|
@@ -497,22 +486,12 @@ DIAGNOSTICS:
|
|
|
497
486
|
const visibleControlNotices = existingVisibleControlNotices instanceof Set ? existingVisibleControlNotices as Set<string> : new Set<string>();
|
|
498
487
|
globalStore[controlNoticeSeenStoreKey] = visibleControlNotices;
|
|
499
488
|
const controlEventHandler = (payload: unknown) => {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const noticeText = details.noticeText ?? formatControlNoticeMessage(details.event, childIntercomTarget);
|
|
507
|
-
pi.sendMessage(
|
|
508
|
-
{
|
|
509
|
-
customType: SUBAGENT_CONTROL_MESSAGE_TYPE,
|
|
510
|
-
content: noticeText,
|
|
511
|
-
display: true,
|
|
512
|
-
details: { ...details, childIntercomTarget, noticeText },
|
|
513
|
-
},
|
|
514
|
-
{ triggerTurn: true },
|
|
515
|
-
);
|
|
489
|
+
handleSubagentControlNotice({
|
|
490
|
+
pi,
|
|
491
|
+
state,
|
|
492
|
+
visibleControlNotices,
|
|
493
|
+
details: payload as SubagentControlMessageDetails,
|
|
494
|
+
});
|
|
516
495
|
};
|
|
517
496
|
const eventUnsubscribes = [
|
|
518
497
|
pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, handleStarted),
|
|
@@ -547,6 +526,7 @@ DIAGNOSTICS:
|
|
|
547
526
|
state.currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
548
527
|
state.lastUiContext = ctx;
|
|
549
528
|
cleanupSessionArtifacts(ctx);
|
|
529
|
+
clearPendingForegroundControlNotices(state);
|
|
550
530
|
resetJobs(ctx);
|
|
551
531
|
restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
|
|
552
532
|
};
|
|
@@ -569,6 +549,7 @@ DIAGNOSTICS:
|
|
|
569
549
|
stopResultWatcher();
|
|
570
550
|
if (state.poller) clearInterval(state.poller);
|
|
571
551
|
state.poller = null;
|
|
552
|
+
clearPendingForegroundControlNotices(state);
|
|
572
553
|
for (const timer of state.cleanupTimers.values()) {
|
|
573
554
|
clearTimeout(timer);
|
|
574
555
|
}
|
|
@@ -10,6 +10,7 @@ import { executeChain } from "./chain-execution.ts";
|
|
|
10
10
|
import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts";
|
|
11
11
|
import { handleManagementAction } from "../../agents/agent-management.ts";
|
|
12
12
|
import { buildDoctorReport } from "../../extension/doctor.ts";
|
|
13
|
+
import { clearPendingForegroundControlNotices } from "../../extension/control-notices.ts";
|
|
13
14
|
import { runSync } from "./execution.ts";
|
|
14
15
|
import { resolveModelCandidate } from "../shared/model-fallback.ts";
|
|
15
16
|
import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
|
|
@@ -2067,6 +2068,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2067
2068
|
return toExecutionErrorResult(effectiveParams, error);
|
|
2068
2069
|
} finally {
|
|
2069
2070
|
if (foregroundControl) {
|
|
2071
|
+
clearPendingForegroundControlNotices(deps.state, runId);
|
|
2070
2072
|
deps.state.foregroundControls.delete(runId);
|
|
2071
2073
|
if (deps.state.lastForegroundControlId === runId) {
|
|
2072
2074
|
deps.state.lastForegroundControlId = null;
|
package/src/shared/types.ts
CHANGED
|
@@ -356,6 +356,7 @@ export interface SubagentState {
|
|
|
356
356
|
interrupt?: () => boolean;
|
|
357
357
|
}>;
|
|
358
358
|
lastForegroundControlId: string | null;
|
|
359
|
+
pendingForegroundControlNotices?: Map<string, ReturnType<typeof setTimeout>>;
|
|
359
360
|
cleanupTimers: Map<string, ReturnType<typeof setTimeout>>;
|
|
360
361
|
lastUiContext: ExtensionContext | null;
|
|
361
362
|
poller: NodeJS.Timeout | null;
|