pi-subagents 0.17.5 → 0.18.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 +16 -0
- package/README.md +19 -19
- package/agents/oracle-executor.md +1 -1
- package/agents/scout.md +1 -1
- package/async-execution.ts +22 -2
- package/async-job-tracker.ts +70 -7
- package/async-status.ts +37 -15
- package/chain-execution.ts +29 -4
- package/execution.ts +18 -30
- package/index.ts +118 -131
- package/install.mjs +2 -3
- package/intercom-bridge.ts +9 -0
- package/notify.ts +25 -6
- package/package.json +3 -6
- package/pi-args.ts +4 -0
- package/render.ts +15 -22
- package/result-watcher.ts +3 -5
- package/run-status.ts +134 -0
- package/schemas.ts +16 -12
- package/skills/pi-subagents/SKILL.md +21 -21
- package/slash-live-state.ts +0 -4
- package/subagent-control.ts +84 -42
- package/subagent-executor.ts +122 -11
- package/subagent-prompt-runtime.ts +6 -0
- package/subagent-runner.ts +118 -10
- package/subagents-status.ts +5 -1
- package/types.ts +29 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.18.0] - 2026-04-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added subagent control notifications so `needs_attention` signals push structured parent events, persist async control events to `events.jsonl`, show visible transcript notices for the user and parent agent, include proactive `nudge`/`status`/`interrupt` commands when a child appears blocked, and show each visible notice at most once per child run and attention state.
|
|
9
|
+
- Added stable child intercom session names for controlled subagents so needs-attention pings can tell the orchestrator which agent needs attention and how to message it when intercom is available.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Replaced the unreleased `starting`/`active`/`quiet`/`stalled`/`paused` activity labels with factual activity reporting and a single `needs_attention` control signal, keeping `paused` as lifecycle state only.
|
|
13
|
+
- Added `subagent({ action: "status", id })` and `subagent({ action: "status" })` as the control-surface status checks, replacing the separate `subagent_status(...)` tool.
|
|
14
|
+
- Adjusted bundled agent defaults: most builtins now use `openai-codex/gpt-5.5`, while `scout` uses `openai-codex/gpt-5.4-mini`.
|
|
15
|
+
- Removed the incomplete e2e suite and stale `@marcfargas/pi-test-harness` dev dependency; `test:all` now runs the maintained unit and integration suites.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Paused async runs now render `Background task paused` notifications instead of failed/completed copy, including after extension reloads with stale legacy listeners still present.
|
|
19
|
+
- Async status output no longer shows stale activity-age lines for paused or completed runs.
|
|
20
|
+
|
|
5
21
|
## [0.17.5] - 2026-04-23
|
|
6
22
|
|
|
7
23
|
### Added
|
package/README.md
CHANGED
|
@@ -48,8 +48,8 @@ Use `agentScope` parameter to control discovery: `"user"`, `"project"`, or `"bot
|
|
|
48
48
|
|
|
49
49
|
**Builtin agents:** The extension ships with ready-to-use agents — `scout`, `planner`, `worker`, `reviewer`, `context-builder`, `researcher`, `delegate`, `oracle`, and `oracle-executor`. They load at lowest priority so any user or project agent with the same name overrides them.
|
|
50
50
|
|
|
51
|
-
- `oracle` is a high-context advisory reviewer on `openai-codex/gpt-5.
|
|
52
|
-
- `oracle-executor` is a high-context implementation escalator on `openai-codex/gpt-5.
|
|
51
|
+
- `oracle` is a high-context advisory reviewer on `openai-codex/gpt-5.5`. It critiques direction, surfaces hidden risks, and proposes a concrete execution prompt, but it does not edit files directly.
|
|
52
|
+
- `oracle-executor` is a high-context implementation escalator on `openai-codex/gpt-5.5`. It is intended to run only after the main agent explicitly approves a course of action.
|
|
53
53
|
|
|
54
54
|
You can also override selected builtin fields without copying the whole agent. Builtin overrides are stored in settings under `subagents.agentOverrides`:
|
|
55
55
|
|
|
@@ -285,7 +285,7 @@ Add `--bg` at the end of any slash command to run in the background:
|
|
|
285
285
|
/parallel scout "scan frontend" -> scout "scan backend" -> scout "scan infra" --bg
|
|
286
286
|
```
|
|
287
287
|
|
|
288
|
-
Without `--bg`, the run is foreground: the tool call stays active and streams progress until completion. With `--bg`, the run is launched asynchronously: control returns immediately, and completion arrives later via notification. In both cases subagents run as separate processes. Check status with
|
|
288
|
+
Without `--bg`, the run is foreground: the tool call stays active and streams progress until completion. With `--bg`, the run is launched asynchronously: control returns immediately, and completion arrives later via notification. In both cases subagents run as separate processes. Check status with `subagent({ action: "status", id })`, or open the `/subagents-status` slash command for a read-only overlay listing active runs and recent completions.
|
|
289
289
|
|
|
290
290
|
### Forked Context Execution
|
|
291
291
|
|
|
@@ -649,16 +649,16 @@ These are the parameters the **LLM agent** passes when it calls the `subagent` t
|
|
|
649
649
|
], clarify: false, async: true }
|
|
650
650
|
```
|
|
651
651
|
|
|
652
|
-
**
|
|
652
|
+
**Programmatic status:**
|
|
653
653
|
```typescript
|
|
654
|
-
{ action: "
|
|
655
|
-
{ id: "a53ebe46" }
|
|
656
|
-
{ dir: "<tmpdir>/pi-subagents-<scope>/async-subagent-runs/a53ebe46-..." }
|
|
654
|
+
subagent({ action: "status" }) // active async runs only
|
|
655
|
+
subagent({ action: "status", id: "a53ebe46" }) // inspect one run
|
|
656
|
+
subagent({ action: "status", dir: "<tmpdir>/pi-subagents-<scope>/async-subagent-runs/a53ebe46-..." })
|
|
657
657
|
```
|
|
658
658
|
|
|
659
659
|
**/subagents-status slash command:**
|
|
660
660
|
|
|
661
|
-
Opens a small read-only overlay that shows active async runs plus recent completed
|
|
661
|
+
Opens a small read-only overlay that shows active async runs plus recent completed, failed, and paused runs. It auto-refreshes every 2 seconds while open, keeps the current run selected when possible, and uses `↑↓` to select a run plus `Esc` to close.
|
|
662
662
|
|
|
663
663
|
## Management Actions
|
|
664
664
|
|
|
@@ -798,11 +798,12 @@ Fallbacks are inherited from the selected agent for that step. There is no per-s
|
|
|
798
798
|
|
|
799
799
|
Fallbacks are inherited from the selected agent for that task. There is no per-task `fallbackModels` override in v1.
|
|
800
800
|
|
|
801
|
-
Status
|
|
801
|
+
Status commands:
|
|
802
802
|
|
|
803
|
-
|
|
|
803
|
+
| Command | Description |
|
|
804
804
|
|------|-------------|
|
|
805
|
-
| `
|
|
805
|
+
| `subagent({ action: "status" })` | List active async runs |
|
|
806
|
+
| `subagent({ action: "status", id })` | Inspect a foreground or async run by id or prefix |
|
|
806
807
|
|
|
807
808
|
## Worktree Isolation
|
|
808
809
|
|
|
@@ -1167,20 +1168,20 @@ When fallback is used in async/background mode, `status.json` and the final resu
|
|
|
1167
1168
|
For programmatic access:
|
|
1168
1169
|
|
|
1169
1170
|
```typescript
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1171
|
+
subagent({ action: "status" })
|
|
1172
|
+
subagent({ action: "status", id: "<id>" })
|
|
1173
|
+
subagent({ action: "status", dir: "<tmpdir>/pi-subagents-<scope>/async-subagent-runs/<id>" })
|
|
1173
1174
|
```
|
|
1174
1175
|
|
|
1175
|
-
For an interactive overview, run the `/subagents-status` slash command to open the overlay listing active runs and recent completed
|
|
1176
|
+
For an interactive overview, run the `/subagents-status` slash command to open the overlay listing active runs and recent completed, failed, and paused runs. The overlay auto-refreshes every 2 seconds while it is open and focuses on summary/status information, including the current output/session paths when available.
|
|
1176
1177
|
|
|
1177
1178
|
## Events
|
|
1178
1179
|
|
|
1179
1180
|
Async events:
|
|
1180
|
-
- `subagent:started`
|
|
1181
|
-
- `subagent:complete`
|
|
1181
|
+
- `subagent:async-started`
|
|
1182
|
+
- `subagent:async-complete`
|
|
1182
1183
|
|
|
1183
|
-
|
|
1184
|
+
The result watcher emits `subagent:async-complete`; `index.ts` registers the notification handler that consumes it.
|
|
1184
1185
|
|
|
1185
1186
|
## Files
|
|
1186
1187
|
|
|
@@ -1227,7 +1228,6 @@ Async events:
|
|
|
1227
1228
|
├── run-history.ts # Per-agent run recording (JSONL)
|
|
1228
1229
|
├── test/unit/ # Fast unit tests for pure modules
|
|
1229
1230
|
├── test/integration/ # Loader-based execution/integration tests
|
|
1230
|
-
├── test/e2e/ # End-to-end sandbox tests
|
|
1231
1231
|
├── test/support/ # Shared test loader, helpers, and mock pi harness
|
|
1232
1232
|
└── text-editor.ts # Shared text editor (word nav, paste)
|
|
1233
1233
|
```
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: oracle-executor
|
|
3
3
|
description: High-context implementation agent that executes only after main-agent approval
|
|
4
4
|
tools: read, grep, find, ls, bash, edit, write, intercom
|
|
5
|
-
model: openai-codex/gpt-5.5
|
|
5
|
+
model: openai-codex/gpt-5.5
|
|
6
6
|
thinking: high
|
|
7
7
|
systemPromptMode: replace
|
|
8
8
|
inheritProjectContext: true
|
package/agents/scout.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: scout
|
|
3
3
|
description: Fast codebase recon that returns compressed context for handoff
|
|
4
4
|
tools: read, grep, find, ls, bash, write
|
|
5
|
-
model: openai-codex/gpt-5.
|
|
5
|
+
model: openai-codex/gpt-5.4-mini
|
|
6
6
|
systemPromptMode: replace
|
|
7
7
|
inheritProjectContext: true
|
|
8
8
|
inheritSkills: false
|
package/async-execution.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type ResolvedControlConfig,
|
|
26
26
|
ASYNC_DIR,
|
|
27
27
|
RESULTS_DIR,
|
|
28
|
+
SUBAGENT_ASYNC_STARTED_EVENT,
|
|
28
29
|
TEMP_ROOT_DIR,
|
|
29
30
|
getAsyncConfigPath,
|
|
30
31
|
resolveChildMaxSubagentDepth,
|
|
@@ -77,6 +78,8 @@ export interface AsyncChainParams {
|
|
|
77
78
|
worktreeSetupHook?: string;
|
|
78
79
|
worktreeSetupHookTimeoutMs?: number;
|
|
79
80
|
controlConfig?: ResolvedControlConfig;
|
|
81
|
+
controlIntercomTarget?: string;
|
|
82
|
+
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
export interface AsyncSingleParams {
|
|
@@ -99,6 +102,8 @@ export interface AsyncSingleParams {
|
|
|
99
102
|
worktreeSetupHook?: string;
|
|
100
103
|
worktreeSetupHookTimeoutMs?: number;
|
|
101
104
|
controlConfig?: ResolvedControlConfig;
|
|
105
|
+
controlIntercomTarget?: string;
|
|
106
|
+
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
export interface AsyncExecutionResult {
|
|
@@ -182,6 +187,8 @@ export function executeAsyncChain(
|
|
|
182
187
|
worktreeSetupHook,
|
|
183
188
|
worktreeSetupHookTimeoutMs,
|
|
184
189
|
controlConfig,
|
|
190
|
+
controlIntercomTarget,
|
|
191
|
+
childIntercomTarget,
|
|
185
192
|
} = params;
|
|
186
193
|
const chainSkills = params.chainSkills ?? [];
|
|
187
194
|
const availableModels = params.availableModels;
|
|
@@ -280,6 +287,13 @@ export function executeAsyncChain(
|
|
|
280
287
|
}
|
|
281
288
|
return buildSeqStep(s as SequentialStep, nextSessionFile());
|
|
282
289
|
});
|
|
290
|
+
let childTargetIndex = 0;
|
|
291
|
+
const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
|
|
292
|
+
if ("parallel" in step) {
|
|
293
|
+
return step.parallel.map((task) => childIntercomTarget(task.agent, childTargetIndex++));
|
|
294
|
+
}
|
|
295
|
+
return [childIntercomTarget(step.agent, childTargetIndex++)];
|
|
296
|
+
}) : undefined;
|
|
283
297
|
|
|
284
298
|
let spawnResult: { pid?: number; error?: string } = {};
|
|
285
299
|
try {
|
|
@@ -302,6 +316,8 @@ export function executeAsyncChain(
|
|
|
302
316
|
worktreeSetupHook,
|
|
303
317
|
worktreeSetupHookTimeoutMs,
|
|
304
318
|
controlConfig,
|
|
319
|
+
controlIntercomTarget,
|
|
320
|
+
childIntercomTargets,
|
|
305
321
|
},
|
|
306
322
|
id,
|
|
307
323
|
runnerCwd,
|
|
@@ -320,7 +336,7 @@ export function executeAsyncChain(
|
|
|
320
336
|
const firstAgents = isParallelStep(firstStep)
|
|
321
337
|
? firstStep.parallel.map((t) => t.agent)
|
|
322
338
|
: [(firstStep as SequentialStep).agent];
|
|
323
|
-
ctx.pi.events.emit(
|
|
339
|
+
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
324
340
|
id,
|
|
325
341
|
pid: spawnResult.pid,
|
|
326
342
|
agent: firstAgents[0],
|
|
@@ -370,6 +386,8 @@ export function executeAsyncSingle(
|
|
|
370
386
|
worktreeSetupHook,
|
|
371
387
|
worktreeSetupHookTimeoutMs,
|
|
372
388
|
controlConfig,
|
|
389
|
+
controlIntercomTarget,
|
|
390
|
+
childIntercomTarget,
|
|
373
391
|
} = params;
|
|
374
392
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
375
393
|
const skillNames = params.skills ?? agentConfig.skills ?? [];
|
|
@@ -437,6 +455,8 @@ export function executeAsyncSingle(
|
|
|
437
455
|
worktreeSetupHook,
|
|
438
456
|
worktreeSetupHookTimeoutMs,
|
|
439
457
|
controlConfig,
|
|
458
|
+
controlIntercomTarget,
|
|
459
|
+
childIntercomTargets: childIntercomTarget ? [childIntercomTarget(agent, 0)] : undefined,
|
|
440
460
|
},
|
|
441
461
|
id,
|
|
442
462
|
runnerCwd,
|
|
@@ -451,7 +471,7 @@ export function executeAsyncSingle(
|
|
|
451
471
|
}
|
|
452
472
|
|
|
453
473
|
if (spawnResult.pid) {
|
|
454
|
-
ctx.pi.events.emit(
|
|
474
|
+
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
455
475
|
id,
|
|
456
476
|
pid: spawnResult.pid,
|
|
457
477
|
agent,
|
package/async-job-tracker.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { renderWidget } from "./render.ts";
|
|
5
|
+
import { formatControlNoticeMessage } from "./subagent-control.ts";
|
|
4
6
|
import {
|
|
7
|
+
type AsyncJobState,
|
|
8
|
+
type ControlEvent,
|
|
5
9
|
type SubagentState,
|
|
6
10
|
POLL_INTERVAL_MS,
|
|
11
|
+
SUBAGENT_CONTROL_EVENT,
|
|
12
|
+
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
7
13
|
} from "./types.ts";
|
|
8
14
|
import { readStatus } from "./utils.ts";
|
|
9
15
|
|
|
@@ -12,7 +18,7 @@ interface AsyncJobTrackerOptions {
|
|
|
12
18
|
pollIntervalMs?: number;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string, options: AsyncJobTrackerOptions = {}): {
|
|
21
|
+
export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: SubagentState, asyncDirRoot: string, options: AsyncJobTrackerOptions = {}): {
|
|
16
22
|
ensurePoller: () => void;
|
|
17
23
|
handleStarted: (data: unknown) => void;
|
|
18
24
|
handleComplete: (data: unknown) => void;
|
|
@@ -36,13 +42,67 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
36
42
|
}, completionRetentionMs);
|
|
37
43
|
state.cleanupTimers.set(asyncId, timer);
|
|
38
44
|
};
|
|
45
|
+
const emitNewControlEvents = (job: AsyncJobState) => {
|
|
46
|
+
const eventsPath = path.join(job.asyncDir, "events.jsonl");
|
|
47
|
+
let fd: number;
|
|
48
|
+
try {
|
|
49
|
+
fd = fs.openSync(eventsPath, "r");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
52
|
+
console.error(`Failed to open async control events for '${job.asyncDir}':`, error);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const stat = fs.fstatSync(fd);
|
|
57
|
+
const cursor = stat.size < (job.controlEventCursor ?? 0) ? 0 : (job.controlEventCursor ?? 0);
|
|
58
|
+
if (stat.size <= cursor) return;
|
|
59
|
+
const buffer = Buffer.alloc(stat.size - cursor);
|
|
60
|
+
fs.readSync(fd, buffer, 0, buffer.length, cursor);
|
|
61
|
+
const lastNewline = buffer.lastIndexOf(0x0a);
|
|
62
|
+
if (lastNewline === -1) return;
|
|
63
|
+
job.controlEventCursor = cursor + lastNewline + 1;
|
|
64
|
+
for (const line of buffer.subarray(0, lastNewline).toString("utf-8").split("\n")) {
|
|
65
|
+
if (!line.trim()) continue;
|
|
66
|
+
let parsed: unknown;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(line);
|
|
69
|
+
} catch {
|
|
70
|
+
// Ignore malformed completed records but keep the poller alive for later events.
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!parsed || typeof parsed !== "object" || (parsed as { type?: unknown }).type !== "subagent.control") continue;
|
|
74
|
+
const record = parsed as { event?: ControlEvent; channels?: string[]; childIntercomTarget?: string; noticeText?: string; intercom?: { to?: string; message?: string } };
|
|
75
|
+
if (!record.event || !Array.isArray(record.channels)) continue;
|
|
76
|
+
const payload = {
|
|
77
|
+
event: record.event,
|
|
78
|
+
source: "async" as const,
|
|
79
|
+
asyncDir: job.asyncDir,
|
|
80
|
+
childIntercomTarget: record.childIntercomTarget,
|
|
81
|
+
noticeText: record.noticeText ?? formatControlNoticeMessage(record.event, record.childIntercomTarget),
|
|
82
|
+
};
|
|
83
|
+
if (record.channels.includes("event")) {
|
|
84
|
+
pi.events.emit(SUBAGENT_CONTROL_EVENT, payload);
|
|
85
|
+
}
|
|
86
|
+
if (record.channels.includes("intercom") && record.intercom?.to && record.intercom.message) {
|
|
87
|
+
pi.events.emit(SUBAGENT_CONTROL_INTERCOM_EVENT, {
|
|
88
|
+
...payload,
|
|
89
|
+
to: record.intercom.to,
|
|
90
|
+
message: record.intercom.message,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`Failed to read async control events for '${job.asyncDir}':`, error);
|
|
96
|
+
} finally {
|
|
97
|
+
fs.closeSync(fd);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
39
100
|
|
|
40
101
|
const ensurePoller = () => {
|
|
41
102
|
if (state.poller) return;
|
|
42
103
|
state.poller = setInterval(() => {
|
|
43
|
-
if (!state.lastUiContext || !state.lastUiContext.hasUI) return;
|
|
44
104
|
if (state.asyncJobs.size === 0) {
|
|
45
|
-
rerenderWidget(state.lastUiContext, []);
|
|
105
|
+
if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext, []);
|
|
46
106
|
if (state.poller) {
|
|
47
107
|
clearInterval(state.poller);
|
|
48
108
|
state.poller = null;
|
|
@@ -52,11 +112,15 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
52
112
|
|
|
53
113
|
for (const job of state.asyncJobs.values()) {
|
|
54
114
|
try {
|
|
115
|
+
emitNewControlEvents(job);
|
|
55
116
|
const status = readStatus(job.asyncDir);
|
|
56
117
|
if (status) {
|
|
57
118
|
const previousStatus = job.status;
|
|
58
119
|
job.status = status.state;
|
|
59
120
|
job.activityState = status.activityState;
|
|
121
|
+
job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
|
|
122
|
+
job.currentTool = status.currentTool ?? job.currentTool;
|
|
123
|
+
job.currentToolStartedAt = status.currentToolStartedAt ?? job.currentToolStartedAt;
|
|
60
124
|
job.mode = status.mode;
|
|
61
125
|
job.currentStep = status.currentStep ?? job.currentStep;
|
|
62
126
|
job.stepsTotal = status.steps?.length ?? job.stepsTotal;
|
|
@@ -83,7 +147,7 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
83
147
|
}
|
|
84
148
|
}
|
|
85
149
|
|
|
86
|
-
rerenderWidget(state.lastUiContext);
|
|
150
|
+
if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
|
|
87
151
|
}, pollIntervalMs);
|
|
88
152
|
state.poller.unref?.();
|
|
89
153
|
};
|
|
@@ -103,16 +167,15 @@ export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string
|
|
|
103
167
|
asyncId: info.id,
|
|
104
168
|
asyncDir,
|
|
105
169
|
status: "queued",
|
|
106
|
-
activityState: "starting",
|
|
107
170
|
mode: info.chain ? "chain" : "single",
|
|
108
171
|
agents,
|
|
109
172
|
stepsTotal: agents?.length,
|
|
110
173
|
startedAt: now,
|
|
111
174
|
updatedAt: now,
|
|
112
175
|
});
|
|
176
|
+
ensurePoller();
|
|
113
177
|
if (state.lastUiContext) {
|
|
114
178
|
rerenderWidget(state.lastUiContext);
|
|
115
|
-
ensurePoller();
|
|
116
179
|
}
|
|
117
180
|
};
|
|
118
181
|
|
package/async-status.ts
CHANGED
|
@@ -10,6 +10,9 @@ export interface AsyncRunStepSummary {
|
|
|
10
10
|
agent: string;
|
|
11
11
|
status: string;
|
|
12
12
|
activityState?: ActivityState;
|
|
13
|
+
lastActivityAt?: number;
|
|
14
|
+
currentTool?: string;
|
|
15
|
+
currentToolStartedAt?: number;
|
|
13
16
|
durationMs?: number;
|
|
14
17
|
tokens?: TokenUsage;
|
|
15
18
|
skills?: string[];
|
|
@@ -23,6 +26,9 @@ export interface AsyncRunSummary {
|
|
|
23
26
|
asyncDir: string;
|
|
24
27
|
state: "queued" | "running" | "complete" | "failed" | "paused";
|
|
25
28
|
activityState?: ActivityState;
|
|
29
|
+
lastActivityAt?: number;
|
|
30
|
+
currentTool?: string;
|
|
31
|
+
currentToolStartedAt?: number;
|
|
26
32
|
mode: "single" | "chain";
|
|
27
33
|
cwd?: string;
|
|
28
34
|
startedAt: number;
|
|
@@ -78,27 +84,31 @@ function outputFileMtime(outputFile: string | undefined): number | undefined {
|
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): ActivityState
|
|
82
|
-
if (status.state
|
|
83
|
-
if (status.state !== "running") return status.activityState;
|
|
87
|
+
function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { activityState?: ActivityState; lastActivityAt?: number } {
|
|
88
|
+
if (status.state !== "running") return { activityState: status.activityState, lastActivityAt: status.lastActivityAt };
|
|
84
89
|
const outputPath = status.outputFile ? (path.isAbsolute(status.outputFile) ? status.outputFile : path.join(asyncDir, status.outputFile)) : undefined;
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
startedAt: status.startedAt,
|
|
90
|
+
const currentStep = typeof status.currentStep === "number" ? status.steps?.[status.currentStep] : undefined;
|
|
91
|
+
const lastActivityAt = status.lastActivityAt ?? outputFileMtime(outputPath) ?? currentStep?.lastActivityAt ?? currentStep?.startedAt ?? status.startedAt;
|
|
92
|
+
return {
|
|
89
93
|
lastActivityAt,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
activityState: status.activityState ?? deriveActivityState({
|
|
95
|
+
config: DEFAULT_CONTROL_CONFIG,
|
|
96
|
+
startedAt: status.startedAt,
|
|
97
|
+
lastActivityAt,
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
|
|
96
|
-
const activityState = deriveAsyncActivityState(asyncDir, status);
|
|
103
|
+
const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
|
|
97
104
|
return {
|
|
98
105
|
id: status.runId || path.basename(asyncDir),
|
|
99
106
|
asyncDir,
|
|
100
107
|
state: status.state,
|
|
101
108
|
activityState,
|
|
109
|
+
lastActivityAt,
|
|
110
|
+
currentTool: status.currentTool,
|
|
111
|
+
currentToolStartedAt: status.currentToolStartedAt,
|
|
102
112
|
mode: status.mode,
|
|
103
113
|
cwd: status.cwd,
|
|
104
114
|
startedAt: status.startedAt,
|
|
@@ -107,11 +117,15 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
|
|
|
107
117
|
currentStep: status.currentStep,
|
|
108
118
|
steps: (status.steps ?? []).map((step, index) => {
|
|
109
119
|
const stepActivityState = step.activityState ?? (step.status === "running" ? activityState : undefined);
|
|
120
|
+
const stepLastActivityAt = step.lastActivityAt ?? (step.status === "running" ? lastActivityAt : undefined);
|
|
110
121
|
return {
|
|
111
122
|
index,
|
|
112
123
|
agent: step.agent,
|
|
113
124
|
status: step.status,
|
|
114
125
|
...(stepActivityState ? { activityState: stepActivityState } : {}),
|
|
126
|
+
...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
|
|
127
|
+
...(step.currentTool ? { currentTool: step.currentTool } : {}),
|
|
128
|
+
...(step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
|
|
115
129
|
...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
|
|
116
130
|
...(step.tokens ? { tokens: step.tokens } : {}),
|
|
117
131
|
...(step.skills ? { skills: step.skills } : {}),
|
|
@@ -184,9 +198,17 @@ export function listAsyncRunsForOverlay(asyncDirRoot: string, recentLimit = 5):
|
|
|
184
198
|
};
|
|
185
199
|
}
|
|
186
200
|
|
|
201
|
+
function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number }): string | undefined {
|
|
202
|
+
if (input.currentTool && input.currentToolStartedAt) return `tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`;
|
|
203
|
+
if (!input.lastActivityAt) return input.activityState === "needs_attention" ? "needs attention" : undefined;
|
|
204
|
+
const elapsed = formatDuration(Math.max(0, Date.now() - input.lastActivityAt));
|
|
205
|
+
return input.activityState === "needs_attention" ? `no activity for ${elapsed}` : `active ${elapsed} ago`;
|
|
206
|
+
}
|
|
207
|
+
|
|
187
208
|
function formatStepLine(step: AsyncRunStepSummary): string {
|
|
188
|
-
const
|
|
189
|
-
const
|
|
209
|
+
const parts = [`${step.index + 1}. ${step.agent}`, step.status];
|
|
210
|
+
const activity = formatActivityFacts(step);
|
|
211
|
+
if (activity) parts.push(activity);
|
|
190
212
|
if (step.model) parts.push(step.model);
|
|
191
213
|
if (step.durationMs !== undefined) parts.push(formatDuration(step.durationMs));
|
|
192
214
|
if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
|
|
@@ -197,8 +219,8 @@ function formatRunHeader(run: AsyncRunSummary): string {
|
|
|
197
219
|
const stepCount = run.steps.length || 1;
|
|
198
220
|
const stepLabel = run.currentStep !== undefined ? `step ${run.currentStep + 1}/${stepCount}` : `steps ${stepCount}`;
|
|
199
221
|
const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
|
|
200
|
-
const
|
|
201
|
-
return `${run.id} | ${state} | ${run.mode} | ${stepLabel} | ${cwd}`;
|
|
222
|
+
const activity = formatActivityFacts(run);
|
|
223
|
+
return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${stepLabel} | ${cwd}`;
|
|
202
224
|
}
|
|
203
225
|
|
|
204
226
|
export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {
|
package/chain-execution.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
type AgentProgress,
|
|
45
45
|
type ArtifactConfig,
|
|
46
46
|
type ArtifactPaths,
|
|
47
|
+
type ControlEvent,
|
|
47
48
|
type Details,
|
|
48
49
|
type ResolvedControlConfig,
|
|
49
50
|
type SingleResult,
|
|
@@ -84,12 +85,17 @@ interface ParallelChainRunInput {
|
|
|
84
85
|
artifactsDir: string;
|
|
85
86
|
signal?: AbortSignal;
|
|
86
87
|
onUpdate?: (r: AgentToolResult<Details>) => void;
|
|
88
|
+
onControlEvent?: (event: ControlEvent) => void;
|
|
87
89
|
controlConfig: ResolvedControlConfig;
|
|
90
|
+
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
88
91
|
foregroundControl?: {
|
|
89
92
|
updatedAt: number;
|
|
90
93
|
currentAgent?: string;
|
|
91
94
|
currentIndex?: number;
|
|
92
95
|
currentActivityState?: ActivityState;
|
|
96
|
+
lastActivityAt?: number;
|
|
97
|
+
currentTool?: string;
|
|
98
|
+
currentToolStartedAt?: number;
|
|
93
99
|
interrupt?: () => boolean;
|
|
94
100
|
};
|
|
95
101
|
results: SingleResult[];
|
|
@@ -200,12 +206,12 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
200
206
|
if (input.foregroundControl) {
|
|
201
207
|
input.foregroundControl.currentAgent = task.agent;
|
|
202
208
|
input.foregroundControl.currentIndex = input.globalTaskIndex + taskIndex;
|
|
203
|
-
input.foregroundControl.currentActivityState =
|
|
209
|
+
input.foregroundControl.currentActivityState = undefined;
|
|
204
210
|
input.foregroundControl.updatedAt = Date.now();
|
|
205
211
|
input.foregroundControl.interrupt = () => {
|
|
206
212
|
if (interruptController.signal.aborted) return false;
|
|
207
213
|
interruptController.abort();
|
|
208
|
-
input.foregroundControl!.currentActivityState =
|
|
214
|
+
input.foregroundControl!.currentActivityState = undefined;
|
|
209
215
|
input.foregroundControl!.updatedAt = Date.now();
|
|
210
216
|
return true;
|
|
211
217
|
};
|
|
@@ -225,6 +231,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
225
231
|
outputPath,
|
|
226
232
|
maxSubagentDepth,
|
|
227
233
|
controlConfig: input.controlConfig,
|
|
234
|
+
onControlEvent: input.onControlEvent,
|
|
235
|
+
intercomSessionName: input.childIntercomTarget?.(task.agent, input.globalTaskIndex + taskIndex),
|
|
228
236
|
modelOverride: effectiveModel,
|
|
229
237
|
availableModels: input.availableModels,
|
|
230
238
|
preferredModelProvider: input.ctx.model?.provider,
|
|
@@ -238,6 +246,9 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
238
246
|
input.foregroundControl.currentAgent = task.agent;
|
|
239
247
|
input.foregroundControl.currentIndex = input.globalTaskIndex + taskIndex;
|
|
240
248
|
input.foregroundControl.currentActivityState = current?.activityState;
|
|
249
|
+
input.foregroundControl.lastActivityAt = current?.lastActivityAt;
|
|
250
|
+
input.foregroundControl.currentTool = current?.currentTool;
|
|
251
|
+
input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
|
|
241
252
|
input.foregroundControl.updatedAt = Date.now();
|
|
242
253
|
}
|
|
243
254
|
input.onUpdate?.({
|
|
@@ -287,12 +298,17 @@ export interface ChainExecutionParams {
|
|
|
287
298
|
includeProgress?: boolean;
|
|
288
299
|
clarify?: boolean;
|
|
289
300
|
onUpdate?: (r: AgentToolResult<Details>) => void;
|
|
301
|
+
onControlEvent?: (event: ControlEvent) => void;
|
|
290
302
|
controlConfig: ResolvedControlConfig;
|
|
303
|
+
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
291
304
|
foregroundControl?: {
|
|
292
305
|
updatedAt: number;
|
|
293
306
|
currentAgent?: string;
|
|
294
307
|
currentIndex?: number;
|
|
295
308
|
currentActivityState?: ActivityState;
|
|
309
|
+
lastActivityAt?: number;
|
|
310
|
+
currentTool?: string;
|
|
311
|
+
currentToolStartedAt?: number;
|
|
296
312
|
interrupt?: () => boolean;
|
|
297
313
|
};
|
|
298
314
|
chainSkills?: string[];
|
|
@@ -332,7 +348,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
332
348
|
includeProgress,
|
|
333
349
|
clarify,
|
|
334
350
|
onUpdate,
|
|
351
|
+
onControlEvent,
|
|
335
352
|
controlConfig,
|
|
353
|
+
childIntercomTarget,
|
|
336
354
|
foregroundControl,
|
|
337
355
|
chainSkills: chainSkillsParam,
|
|
338
356
|
chainDir: chainDirBase,
|
|
@@ -533,6 +551,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
533
551
|
chainAgents,
|
|
534
552
|
totalSteps,
|
|
535
553
|
controlConfig,
|
|
554
|
+
onControlEvent,
|
|
555
|
+
childIntercomTarget,
|
|
536
556
|
foregroundControl,
|
|
537
557
|
worktreeSetup,
|
|
538
558
|
maxSubagentDepth: params.maxSubagentDepth,
|
|
@@ -674,12 +694,12 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
674
694
|
if (foregroundControl) {
|
|
675
695
|
foregroundControl.currentAgent = seqStep.agent;
|
|
676
696
|
foregroundControl.currentIndex = globalTaskIndex;
|
|
677
|
-
foregroundControl.currentActivityState =
|
|
697
|
+
foregroundControl.currentActivityState = undefined;
|
|
678
698
|
foregroundControl.updatedAt = Date.now();
|
|
679
699
|
foregroundControl.interrupt = () => {
|
|
680
700
|
if (interruptController.signal.aborted) return false;
|
|
681
701
|
interruptController.abort();
|
|
682
|
-
foregroundControl.currentActivityState =
|
|
702
|
+
foregroundControl.currentActivityState = undefined;
|
|
683
703
|
foregroundControl.updatedAt = Date.now();
|
|
684
704
|
return true;
|
|
685
705
|
};
|
|
@@ -699,6 +719,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
699
719
|
outputPath,
|
|
700
720
|
maxSubagentDepth,
|
|
701
721
|
controlConfig,
|
|
722
|
+
onControlEvent,
|
|
723
|
+
intercomSessionName: childIntercomTarget?.(seqStep.agent, globalTaskIndex),
|
|
702
724
|
modelOverride: effectiveModel,
|
|
703
725
|
availableModels,
|
|
704
726
|
preferredModelProvider: ctx.model?.provider,
|
|
@@ -712,6 +734,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
712
734
|
foregroundControl.currentAgent = seqStep.agent;
|
|
713
735
|
foregroundControl.currentIndex = globalTaskIndex;
|
|
714
736
|
foregroundControl.currentActivityState = current?.activityState;
|
|
737
|
+
foregroundControl.lastActivityAt = current?.lastActivityAt;
|
|
738
|
+
foregroundControl.currentTool = current?.currentTool;
|
|
739
|
+
foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
|
|
715
740
|
foregroundControl.updatedAt = Date.now();
|
|
716
741
|
}
|
|
717
742
|
onUpdate({
|