@ferris1225/pi-subagents 4.1.21 → 4.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -18
- package/package.json +55 -55
- package/src/announcements.ts +26 -0
- package/src/config.ts +50 -2
- package/src/dispatch.ts +26 -8
- package/src/monitor.ts +114 -4
- package/src/recovery.ts +145 -145
- package/src/rpc-run.ts +993 -993
- package/src/setup.ts +12 -15
- package/src/widget.ts +247 -134
package/README.md
CHANGED
|
@@ -223,28 +223,44 @@ threads that had already finished keep only their delivered result.
|
|
|
223
223
|
|
|
224
224
|
## Live status and results
|
|
225
225
|
|
|
226
|
-
The TUI widget renders one
|
|
227
|
-
right-aligned `#id`, padded agent name, then the task label — so
|
|
228
|
-
starts at the same column, with the live activity dimmed after
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
The TUI widget renders one line per participant in fixed identity columns —
|
|
227
|
+
status icon, right-aligned `#id`, padded agent name, then the task label — so
|
|
228
|
+
every label starts at the same column, with the live activity dimmed after
|
|
229
|
+
` — ` and the rest of the telemetry flowing inline after ` · `: the worktree
|
|
230
|
+
badge, the token flow in the footer vocabulary (`↑` input, `↓` output,
|
|
231
|
+
`R`/`W` cache read/write), cost, the full `provider/model/thinking` ref, the
|
|
232
|
+
wait state, and an elapsed time that always carries seconds. The first line is
|
|
233
|
+
the parent session itself — what the current model is doing right now while
|
|
234
|
+
its agent loop runs. A managed workflow (the automatic review / fix / re-review
|
|
235
|
+
chain) renders as a tree: the parent line carries the workflow-wide token/cost
|
|
236
|
+
totals and total elapsed, and every stage gets its own `├`/`└`-connected row
|
|
237
|
+
with its own model, token flow, and elapsed — settled stages keep the
|
|
238
|
+
telemetry frozen at settlement, the live stage shows its child's model and
|
|
239
|
+
current activity. A live run renders two lines: what it is — agent, task,
|
|
240
|
+
token flow, cost, provider/model, elapsed — and, dim under the label column,
|
|
241
|
+
what it is doing right now:
|
|
233
242
|
|
|
234
243
|
```text
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
244
|
+
● pi subagent Implement the login redirect fix · openai/gpt-5/max · 12m06s
|
|
245
|
+
◆ #12 worker src/cache.ts · wt:a91f3c · ↑5.2k ↓41.0k R210.0k W6.1k $1.9400 · 12m06s
|
|
246
|
+
├ ✓ implement · ↑1.0k ↓12.0k R40.0k W1.2k $0.5100 · xai/grok-4/xhigh · 2m41s
|
|
247
|
+
├ ! review · ↑0.9k ↓6.0k R38.0k W0.9k $0.3300 · openai/gpt-5 · 1m12s
|
|
248
|
+
├ ● review fix — edit src/auth.ts · ↑0.2k ↓3.0k R12.0k $0.1200 · openai/gpt-5/medium · 41s
|
|
249
|
+
└ ○ re-review
|
|
250
|
+
● #15 explorer src/models.ts · ↑1.2k ↓8.4k R31.0k W1.1k $0.0900 · openai/gpt-5-mini · 3m07s
|
|
251
|
+
↳ grep fallback
|
|
252
|
+
○ #23 worker src/config.ts · repo lane
|
|
253
|
+
○ #24 worker ↻ tests/config.test.ts · queued · 5m02s
|
|
240
254
|
```
|
|
241
255
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
256
|
+
Telemetry drops leftmost-first when a row runs out of width (badge, wait
|
|
257
|
+
state, usage, model) while the elapsed survives every width. Queued rows state
|
|
258
|
+
what they actually wait for — `queued` for a free process slot, `repo lane`
|
|
259
|
+
for shared-checkout write serialization, or `starting` — and a resumed thread
|
|
260
|
+
carries a dim `↻` in its agent column with its cumulative time. The widget is
|
|
261
|
+
capped at ten lines: when many runs are live, extra roots collapse into a
|
|
262
|
+
`… +N more` marker, and an oversized stage chain keeps a window anchored on
|
|
263
|
+
the live stage so the editor keeps its space.
|
|
248
264
|
|
|
249
265
|
Completions resume the main agent on their own, with a compact block of at most 40
|
|
250
266
|
lines by default; longer output lands unchanged in a Markdown artifact whose path
|
|
@@ -292,6 +308,7 @@ strength per agent. Everything else is config-file only, stored at
|
|
|
292
308
|
```json
|
|
293
309
|
{
|
|
294
310
|
"enabledAgents": ["explorer", "worker", "cleaner", "documenter", "synthesizer", "reviewer"],
|
|
311
|
+
"knownAgents": ["explorer", "worker", "cleaner", "documenter", "synthesizer", "reviewer"],
|
|
295
312
|
"agentModels": { "explorer": "anthropic/claude-haiku-4-5" },
|
|
296
313
|
"agentThinkingLevels": { "reviewer": "high" },
|
|
297
314
|
"notifyOnReviewPass": false,
|
|
@@ -304,6 +321,7 @@ strength per agent. Everything else is config-file only, stored at
|
|
|
304
321
|
| Field | Meaning |
|
|
305
322
|
| --------------------- | --------------------------------------------------------------------------------- |
|
|
306
323
|
| `enabledAgents` | Agents available for discovery and delegation. `[]` disables all. |
|
|
324
|
+
| `knownAgents` | Built-ins this config has seen; automatic bookkeeping — never edit it. |
|
|
307
325
|
| `agentModels` | Optional `provider/model-id` per agent; missing = current main model. |
|
|
308
326
|
| `agentThinkingLevels` | Optional manual level per agent; missing = Auto. |
|
|
309
327
|
| `notifyOnReviewPass` | Deliver a standalone passing gate without waking the main agent. Default `false`. |
|
|
@@ -318,6 +336,13 @@ start, model overrides pi no longer reports are removed with a one-time notice.
|
|
|
318
336
|
pi's own session compaction fails mid-thread, a notice surfaces the error and the
|
|
319
337
|
automatic retry instead of failing quietly.
|
|
320
338
|
|
|
339
|
+
Agents shipped by a newer package version turn themselves on at the next
|
|
340
|
+
session: a built-in the config has never seen is adopted into `enabledAgents`
|
|
341
|
+
and follows explorer's configured model and thinking level — the fast lane
|
|
342
|
+
these light roles need — while an agent you disabled stays disabled
|
|
343
|
+
(`knownAgents` is what tells the two cases apart). Enabling a role in
|
|
344
|
+
`/subagents-setup` adopts the same explorer route.
|
|
345
|
+
|
|
321
346
|
## Custom agents
|
|
322
347
|
|
|
323
348
|
Built-ins ship with the package. Add or replace them with Markdown files:
|
package/package.json
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "4.1.
|
|
4
|
-
"description": "A managed sub-agent team for pi: specialized roles, pre-commit documentation sync, retained threads, auto-fix chains, model fallback, and Git worktree isolation.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"publishConfig": {
|
|
8
|
-
"access": "public"
|
|
9
|
-
},
|
|
10
|
-
"keywords": [
|
|
11
|
-
"pi-package",
|
|
12
|
-
"pi-extension",
|
|
13
|
-
"subagent",
|
|
14
|
-
"sub-agent",
|
|
15
|
-
"delegation",
|
|
16
|
-
"code-cleanup",
|
|
17
|
-
"dead-code"
|
|
18
|
-
],
|
|
19
|
-
"files": [
|
|
20
|
-
"src",
|
|
21
|
-
"agents",
|
|
22
|
-
"README.md",
|
|
23
|
-
"LICENSE"
|
|
24
|
-
],
|
|
25
|
-
"pi": {
|
|
26
|
-
"extensions": [
|
|
27
|
-
"./src/index.ts"
|
|
28
|
-
]
|
|
29
|
-
},
|
|
30
|
-
"scripts": {
|
|
31
|
-
"check": "tsc --noEmit",
|
|
32
|
-
"test": "vitest run tests",
|
|
33
|
-
"prepack": "npm run check && npm test"
|
|
34
|
-
},
|
|
35
|
-
"peerDependencies": {
|
|
36
|
-
"@earendil-works/pi-agent-core": ">=0.84.4",
|
|
37
|
-
"@earendil-works/pi-ai": ">=0.84.4",
|
|
38
|
-
"@earendil-works/pi-coding-agent": ">=0.84.4",
|
|
39
|
-
"@earendil-works/pi-tui": ">=0.84.4",
|
|
40
|
-
"typebox": "*"
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"@earendil-works/pi-agent-core": "^0.84.4",
|
|
44
|
-
"@earendil-works/pi-ai": "^0.84.4",
|
|
45
|
-
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
46
|
-
"@earendil-works/pi-tui": "^0.84.4",
|
|
47
|
-
"@types/node": "^22.10.0",
|
|
48
|
-
"typebox": "^1.3.9",
|
|
49
|
-
"typescript": "^5.9.0",
|
|
50
|
-
"vitest": "^4.1.0"
|
|
51
|
-
},
|
|
52
|
-
"engines": {
|
|
53
|
-
"node": ">=22.19.0"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@ferris1225/pi-subagents",
|
|
3
|
+
"version": "4.1.24",
|
|
4
|
+
"description": "A managed sub-agent team for pi: specialized roles, pre-commit documentation sync, retained threads, auto-fix chains, model fallback, and Git worktree isolation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pi-package",
|
|
12
|
+
"pi-extension",
|
|
13
|
+
"subagent",
|
|
14
|
+
"sub-agent",
|
|
15
|
+
"delegation",
|
|
16
|
+
"code-cleanup",
|
|
17
|
+
"dead-code"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"agents",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"pi": {
|
|
26
|
+
"extensions": [
|
|
27
|
+
"./src/index.ts"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"check": "tsc --noEmit",
|
|
32
|
+
"test": "vitest run tests",
|
|
33
|
+
"prepack": "npm run check && npm test"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@earendil-works/pi-agent-core": ">=0.84.4",
|
|
37
|
+
"@earendil-works/pi-ai": ">=0.84.4",
|
|
38
|
+
"@earendil-works/pi-coding-agent": ">=0.84.4",
|
|
39
|
+
"@earendil-works/pi-tui": ">=0.84.4",
|
|
40
|
+
"typebox": "*"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@earendil-works/pi-agent-core": "^0.84.4",
|
|
44
|
+
"@earendil-works/pi-ai": "^0.84.4",
|
|
45
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
46
|
+
"@earendil-works/pi-tui": "^0.84.4",
|
|
47
|
+
"@types/node": "^22.10.0",
|
|
48
|
+
"typebox": "^1.3.9",
|
|
49
|
+
"typescript": "^5.9.0",
|
|
50
|
+
"vitest": "^4.1.0"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=22.19.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/announcements.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { loadConfig, saveConfig } from "./config.ts";
|
|
6
6
|
import { availableModelsInScope, filterUnavailableModelOverrides } from "./models.ts";
|
|
7
|
+
import { formatToolActivity, monitor } from "./monitor.ts";
|
|
7
8
|
import { announceRecoveryRecords } from "./recovery.ts";
|
|
8
9
|
import type { SubagentRuntime } from "./runtime.ts";
|
|
9
10
|
import { installActiveRunsWidget } from "./widget.ts";
|
|
@@ -35,7 +36,32 @@ async function migrateUnavailableAgentModels(
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/** Track the parent pi session itself as the widget's first row: what the
|
|
40
|
+
* current model is doing while its agent loop runs. Same activity vocabulary
|
|
41
|
+
* as subagent rows (thinking / responding / tool + target), fed by the
|
|
42
|
+
* session's own extension events; the row disappears when the loop settles. */
|
|
43
|
+
function trackMainActivity(pi: ExtensionAPI): void {
|
|
44
|
+
pi.on("agent_start", () => monitor.setMainAgentActive(true));
|
|
45
|
+
pi.on("agent_end", () => monitor.setMainAgentActive(false));
|
|
46
|
+
pi.on("agent_settled", () => monitor.setMainAgentActive(false));
|
|
47
|
+
pi.on("model_select", (event) => monitor.setMainModel(event.model?.id));
|
|
48
|
+
pi.on("thinking_level_select", (event) => monitor.setMainThinking(event.level));
|
|
49
|
+
pi.on("message_update", (event) => {
|
|
50
|
+
if (event.message.role !== "assistant") return;
|
|
51
|
+
const kind = event.assistantMessageEvent.type;
|
|
52
|
+
if (kind === "text_start" || kind === "text_delta") monitor.setMainActivity("responding");
|
|
53
|
+
else if (kind === "thinking_start" || kind === "thinking_delta") monitor.setMainActivity("thinking");
|
|
54
|
+
});
|
|
55
|
+
pi.on("tool_execution_start", (event) =>
|
|
56
|
+
monitor.recordMainToolStart(event.toolName, formatToolActivity(event.toolName, event.args)));
|
|
57
|
+
pi.on("tool_execution_end", (event) => monitor.recordMainToolEnd(event.toolName, event.isError));
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
export function registerAnnouncements(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
61
|
+
// Registered at extension load (not session_start) so a model selection
|
|
62
|
+
// made during restore is already captured when the widget appears.
|
|
63
|
+
trackMainActivity(pi);
|
|
64
|
+
|
|
39
65
|
pi.on("session_start", async (_event, ctx) => {
|
|
40
66
|
if (!existsSync(runtime.configPath)) {
|
|
41
67
|
ctx.ui.notify(
|
package/src/config.ts
CHANGED
|
@@ -47,6 +47,11 @@ export const IDLE_TIMEOUT_SEC_LIMIT = 600;
|
|
|
47
47
|
export interface SubagentsConfig {
|
|
48
48
|
/** Agent names that are discoverable and injected. Fresh-install default: every built-in agent. */
|
|
49
49
|
enabledAgents: string[];
|
|
50
|
+
/** Built-in names this config has already surfaced. A shipped agent outside
|
|
51
|
+
* this set is new in an upgrade: loadConfig enables it instead of leaving it
|
|
52
|
+
* dark behind a stale allow-list. Bookkeeping only — maintained automatically,
|
|
53
|
+
* and it is what keeps an explicit disable from being undone. */
|
|
54
|
+
knownAgents: string[];
|
|
50
55
|
/** Per-agent model override, keyed by agent name, as "provider/model-id". */
|
|
51
56
|
agentModels: Record<string, string>;
|
|
52
57
|
/** Optional per-agent thinking preference. Runtime clamps it to the effective model's supported levels. */
|
|
@@ -74,6 +79,7 @@ export interface SubagentsConfig {
|
|
|
74
79
|
|
|
75
80
|
export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
76
81
|
enabledAgents: [...DEFAULT_ENABLED_AGENTS],
|
|
82
|
+
knownAgents: [...BUILTIN_AGENT_NAMES],
|
|
77
83
|
agentModels: {},
|
|
78
84
|
agentThinkingLevels: {},
|
|
79
85
|
notifyOnReviewPass: false,
|
|
@@ -123,6 +129,20 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
123
129
|
config.enabledAgents = [...new Set(names.map((name) => name.trim()))];
|
|
124
130
|
}
|
|
125
131
|
|
|
132
|
+
// Known-agent bookkeeping starts empty for a parsed record (not the fresh
|
|
133
|
+
// default) so loadConfig can still tell which shipped agents this config
|
|
134
|
+
// has never seen. Every enabled name was necessarily surfaced.
|
|
135
|
+
config.knownAgents = [];
|
|
136
|
+
if (Array.isArray(raw.knownAgents)) {
|
|
137
|
+
const names = raw.knownAgents.filter(
|
|
138
|
+
(name): name is string => typeof name === "string" && name.trim().length > 0,
|
|
139
|
+
);
|
|
140
|
+
config.knownAgents = [...new Set(names.map((name) => name.trim()))];
|
|
141
|
+
}
|
|
142
|
+
for (const name of config.enabledAgents) {
|
|
143
|
+
if (!config.knownAgents.includes(name)) config.knownAgents.push(name);
|
|
144
|
+
}
|
|
145
|
+
|
|
126
146
|
if (isRecord(raw.agentModels)) {
|
|
127
147
|
for (const [rawKey, value] of Object.entries(raw.agentModels)) {
|
|
128
148
|
const key = rawKey.trim();
|
|
@@ -173,11 +193,39 @@ function defaultConfig(): SubagentsConfig {
|
|
|
173
193
|
};
|
|
174
194
|
}
|
|
175
195
|
|
|
196
|
+
/**
|
|
197
|
+
* A shipped agent the config has never recorded is new in this release; the
|
|
198
|
+
* stale allow-list must not keep it dark. Enable it and adopt explorer's
|
|
199
|
+
* configured model and thinking level, so an upgrade surfaces the new role on
|
|
200
|
+
* the fast light-task lane instead of silently spending the main model.
|
|
201
|
+
*/
|
|
202
|
+
function adoptNewBuiltins(config: SubagentsConfig): SubagentsConfig {
|
|
203
|
+
const known = new Set(config.knownAgents);
|
|
204
|
+
const fresh = BUILTIN_AGENT_NAMES.filter((name) => !known.has(name));
|
|
205
|
+
if (fresh.length === 0) return config;
|
|
206
|
+
const agentModels = { ...config.agentModels };
|
|
207
|
+
const agentThinkingLevels = { ...config.agentThinkingLevels };
|
|
208
|
+
for (const name of fresh) {
|
|
209
|
+
if (!agentModels[name] && config.agentModels.explorer) agentModels[name] = config.agentModels.explorer;
|
|
210
|
+
if (!agentThinkingLevels[name] && config.agentThinkingLevels.explorer) {
|
|
211
|
+
agentThinkingLevels[name] = config.agentThinkingLevels.explorer;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
...config,
|
|
216
|
+
enabledAgents: [...config.enabledAgents, ...fresh],
|
|
217
|
+
knownAgents: [...known, ...fresh],
|
|
218
|
+
agentModels,
|
|
219
|
+
agentThinkingLevels,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
176
223
|
/**
|
|
177
224
|
* Load config. A missing file is a normal state and yields the defaults (not an error).
|
|
178
225
|
* A corrupt file also falls back to defaults rather than throwing, so startup never breaks.
|
|
179
226
|
* A file from an older version (missing newer keys or holding extra keys) is
|
|
180
|
-
* normalized and persisted back, so the on-disk config stays current.
|
|
227
|
+
* normalized and persisted back, so the on-disk config stays current. Built-in
|
|
228
|
+
* agents the file has never seen are adopted: enabled with explorer's route.
|
|
181
229
|
*/
|
|
182
230
|
export async function loadConfig(configPath: string = getConfigPath()): Promise<SubagentsConfig> {
|
|
183
231
|
let text: string;
|
|
@@ -195,7 +243,7 @@ export async function loadConfig(configPath: string = getConfigPath()): Promise<
|
|
|
195
243
|
return defaultConfig();
|
|
196
244
|
}
|
|
197
245
|
|
|
198
|
-
const config = normalizeConfig(parsed);
|
|
246
|
+
const config = adoptNewBuiltins(normalizeConfig(parsed));
|
|
199
247
|
|
|
200
248
|
// Schema upgrade: persist the normalized shape when the file gained fields
|
|
201
249
|
// (new version) or dropped invalid ones.
|
package/src/dispatch.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
monitor,
|
|
31
31
|
statusIcon,
|
|
32
32
|
type RunChainMeta,
|
|
33
|
+
type RunView,
|
|
33
34
|
type RunWaitReason,
|
|
34
35
|
type WorkflowStage,
|
|
35
36
|
type WorkflowStageStatus,
|
|
@@ -239,18 +240,21 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
239
240
|
const environmentRef: { current: DispatchEnvironment | undefined } = { current: undefined };
|
|
240
241
|
|
|
241
242
|
// Finished runs leave the active monitor immediately. Their final findings
|
|
242
|
-
// are sent as a custom message that starts a follow-up turn.
|
|
243
|
+
// are sent as a custom message that starts a follow-up turn. Returns the
|
|
244
|
+
// removed row so workflow callers can freeze its exact elapsed time onto
|
|
245
|
+
// the stage projection before the row is gone.
|
|
243
246
|
const finishRun = (
|
|
244
247
|
runId: number,
|
|
245
248
|
status: "done" | "failed",
|
|
246
249
|
opts?: { silent?: boolean },
|
|
247
|
-
):
|
|
250
|
+
): RunView | undefined => {
|
|
248
251
|
monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
|
|
249
252
|
const run = monitor.removeRun(runId);
|
|
250
|
-
if (!run) return; // already finished — stay idempotent
|
|
251
|
-
if (opts?.silent || !runtime.sessionActive) return;
|
|
253
|
+
if (!run) return undefined; // already finished — stay idempotent
|
|
254
|
+
if (opts?.silent || !runtime.sessionActive) return run;
|
|
252
255
|
const icon = status === "done" ? "✓" : "✗";
|
|
253
256
|
environmentRef.current?.ctx.ui.notify(`${icon} #${run.id} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
|
|
257
|
+
return run;
|
|
254
258
|
};
|
|
255
259
|
|
|
256
260
|
// Live sub-agent activity → concise one-line status ("thinking",
|
|
@@ -354,7 +358,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
354
358
|
agentOverride?: AgentConfig;
|
|
355
359
|
session?: { sessionId: string; sessionDir: string };
|
|
356
360
|
} = {},
|
|
357
|
-
): Promise<{ runId: number; result: SingleResult }> => {
|
|
361
|
+
): Promise<{ runId: number; result: SingleResult; elapsedMs?: number }> => {
|
|
358
362
|
const discoveredAgent = request.agents.find((candidate) => candidate.name === agentName);
|
|
359
363
|
if (!discoveredAgent) {
|
|
360
364
|
throw new Error(`Managed workflow requires enabled agent "${agentName}", but discovery did not provide it.`);
|
|
@@ -411,11 +415,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
411
415
|
runtime.retainSession(result);
|
|
412
416
|
monitor.setModel(runId, result.model, result.modelFallbackFrom);
|
|
413
417
|
monitor.setThinking(runId, result.thinking);
|
|
414
|
-
finishRun(runId, isFailedResult(result) ? "failed" : "done", { silent: true });
|
|
418
|
+
const finished = finishRun(runId, isFailedResult(result) ? "failed" : "done", { silent: true });
|
|
415
419
|
runtime.registerRunResult(runId, result);
|
|
416
|
-
return { runId, result };
|
|
420
|
+
return { runId, result, elapsedMs: finished?.elapsedMs };
|
|
417
421
|
} catch (error) {
|
|
418
|
-
finishRun(runId, "failed", { silent: true });
|
|
422
|
+
const finished = finishRun(runId, "failed", { silent: true });
|
|
419
423
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
420
424
|
const crashed: SingleResult = {
|
|
421
425
|
...queuedResult(route.agent, task, thinkingLevel),
|
|
@@ -473,6 +477,11 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
473
477
|
agent: initialStepResult.agent,
|
|
474
478
|
relation: initialStageRelation,
|
|
475
479
|
status: workflowStageStatus(initialStepResult),
|
|
480
|
+
// The initial stage is the parent's own run; freeze its telemetry now,
|
|
481
|
+
// before the reopened parent row starts counting workflow-wide time.
|
|
482
|
+
model: initialStepResult.model,
|
|
483
|
+
usage: initialStepResult.usage,
|
|
484
|
+
elapsedMs: monitor.getElapsedMs(request.parentRunId),
|
|
476
485
|
}];
|
|
477
486
|
let reviewStage: WorkflowStage | undefined;
|
|
478
487
|
if (enabled("reviewer")) {
|
|
@@ -484,6 +493,14 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
484
493
|
};
|
|
485
494
|
publishWorkflowStages();
|
|
486
495
|
|
|
496
|
+
/** Freeze the settled step's telemetry onto its stage: once the child row
|
|
497
|
+
* leaves the monitor, this snapshot is the only per-stage record. */
|
|
498
|
+
const settleStage = (stage: WorkflowStage, step: { result: SingleResult; elapsedMs?: number }): void => {
|
|
499
|
+
stage.model = step.result.model;
|
|
500
|
+
stage.usage = step.result.usage;
|
|
501
|
+
if (step.elapsedMs !== undefined) stage.elapsedMs = step.elapsedMs;
|
|
502
|
+
};
|
|
503
|
+
|
|
487
504
|
const launchStep = async (
|
|
488
505
|
agentName: string,
|
|
489
506
|
task: string,
|
|
@@ -506,6 +523,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
506
523
|
parentRunId: request.parentRunId,
|
|
507
524
|
}, stageOptions);
|
|
508
525
|
stage.status = workflowStageStatus(step.result, relation);
|
|
526
|
+
settleStage(stage, step);
|
|
509
527
|
publishWorkflowStages();
|
|
510
528
|
request.rememberLatest(step.result);
|
|
511
529
|
steps.push({ ...step, relation });
|
package/src/monitor.ts
CHANGED
|
@@ -38,6 +38,11 @@ export interface WorkflowStage {
|
|
|
38
38
|
agent: string;
|
|
39
39
|
relation: string;
|
|
40
40
|
status: WorkflowStageStatus;
|
|
41
|
+
/** Telemetry snapshot frozen when the stage settled (the live child row
|
|
42
|
+
* leaves the monitor at that moment); the active stage reads its live child. */
|
|
43
|
+
model?: string;
|
|
44
|
+
usage?: UsageStats;
|
|
45
|
+
elapsedMs?: number;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
export function isRunActiveStatus(status: RunStatus): boolean {
|
|
@@ -111,6 +116,17 @@ export interface RunChainMeta {
|
|
|
111
116
|
waitReason?: RunWaitReason;
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
/** Ephemeral activity of the parent pi model while its agent loop runs: the
|
|
120
|
+
* live model/thinking ref and a one-line "what is it doing now". Not a run —
|
|
121
|
+
* no id, usage, or chain machinery; the view disappears when the loop settles. */
|
|
122
|
+
export interface MainActivity {
|
|
123
|
+
model?: string;
|
|
124
|
+
thinking?: string;
|
|
125
|
+
activity?: string;
|
|
126
|
+
/** Epoch ms when the current agent loop started. */
|
|
127
|
+
activeSince: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
// ---------------------------------------------------------------------------
|
|
115
131
|
// Formatting helpers
|
|
116
132
|
// ---------------------------------------------------------------------------
|
|
@@ -283,20 +299,43 @@ export function runLabel(task: string): string {
|
|
|
283
299
|
: `${takeGraphemes(chars, RUN_LABEL_MAX - 1)}${TASK_SUMMARY_ELLIPSIS}`;
|
|
284
300
|
}
|
|
285
301
|
|
|
302
|
+
/** Narrow an already-extracted run label to a smaller budget, keeping its
|
|
303
|
+
* tail: runLabel tail-weights path fragments because the filename is the
|
|
304
|
+
* recognisable part, and a second squeeze must not trade that tail away.
|
|
305
|
+
* Grapheme-safe. */
|
|
306
|
+
export function shrinkRunLabel(text: string, maxWidth: number): string {
|
|
307
|
+
if (maxWidth <= 0) return "";
|
|
308
|
+
if (visibleWidth(text) <= maxWidth) return text;
|
|
309
|
+
const chars = [...graphemeSegmenter.segment(text)].map((s) => s.segment);
|
|
310
|
+
return `${TASK_SUMMARY_ELLIPSIS}${tailGraphemes(chars, maxWidth - 1)}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
286
313
|
function formatTokens(count: number): string {
|
|
287
314
|
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
288
315
|
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
289
316
|
return String(count);
|
|
290
317
|
}
|
|
291
318
|
|
|
292
|
-
|
|
319
|
+
/** Token flow in the pi-footer vocabulary: ↑input ↓output, R cache-read,
|
|
320
|
+
* W cache-write; zero components are omitted. No cost — callers place it as
|
|
321
|
+
* its own droppable part. */
|
|
322
|
+
export function formatUsageTokens(usage: UsageStats | undefined): string | undefined {
|
|
323
|
+
if (!usage) return undefined;
|
|
293
324
|
const parts: string[] = [];
|
|
294
325
|
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
295
326
|
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
296
327
|
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
297
328
|
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
298
|
-
|
|
299
|
-
|
|
329
|
+
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function formatUsageCompact(usage: UsageStats): string {
|
|
333
|
+
return [formatUsageTokens(usage), usageCostPart(usage)].filter(Boolean).join(" ");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Cost as its own droppable part: `$0.0421`, undefined when nothing accrued. */
|
|
337
|
+
export function usageCostPart(usage: UsageStats | undefined): string | undefined {
|
|
338
|
+
return usage?.cost ? `$${usage.cost.toFixed(4)}` : undefined;
|
|
300
339
|
}
|
|
301
340
|
|
|
302
341
|
/** Aggregate usage across several runs (chain steps or a completion group). */
|
|
@@ -319,9 +358,11 @@ export function formatDuration(ms: number): string {
|
|
|
319
358
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
320
359
|
const minutes = Math.floor(totalSeconds / 60);
|
|
321
360
|
const seconds = totalSeconds % 60;
|
|
361
|
+
// Seconds stay visible at every magnitude: a long-running chain is judged
|
|
362
|
+
// by whether it is still moving, and "5s ago" is exactly that signal.
|
|
322
363
|
if (minutes < 60) return `${minutes}m${String(seconds).padStart(2, "0")}s`;
|
|
323
364
|
const hours = Math.floor(minutes / 60);
|
|
324
|
-
return `${hours}h${String(minutes % 60).padStart(2, "0")}m`;
|
|
365
|
+
return `${hours}h${String(minutes % 60).padStart(2, "0")}m${String(seconds).padStart(2, "0")}s`;
|
|
325
366
|
}
|
|
326
367
|
|
|
327
368
|
/** Cumulative active time across generations; parked gaps never count. */
|
|
@@ -453,6 +494,71 @@ export class MonitorStore {
|
|
|
453
494
|
private runs: RunView[] = [];
|
|
454
495
|
private nextId = 1;
|
|
455
496
|
private subscribers = new Set<() => void>();
|
|
497
|
+
private mainModel?: string;
|
|
498
|
+
private mainThinking?: string;
|
|
499
|
+
private mainActivity?: string;
|
|
500
|
+
private mainActiveSince?: number;
|
|
501
|
+
|
|
502
|
+
// --- parent pi model activity ------------------------------------------
|
|
503
|
+
// Fed by the parent session's extension events (agent loop, streaming,
|
|
504
|
+
// tool executions); rendered as the widget's first line. Change-guarded so
|
|
505
|
+
// per-token streaming deltas do not flood subscribers.
|
|
506
|
+
|
|
507
|
+
setMainModel(model?: string): void {
|
|
508
|
+
if (!model || this.mainModel === model) return;
|
|
509
|
+
this.mainModel = model;
|
|
510
|
+
this.notify();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
setMainThinking(thinking?: string): void {
|
|
514
|
+
if (!thinking || this.mainThinking === thinking) return;
|
|
515
|
+
this.mainThinking = thinking;
|
|
516
|
+
this.notify();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
setMainActivity(text: string): void {
|
|
520
|
+
const activity = sanitizeActivityText(text) || undefined;
|
|
521
|
+
if (!activity || this.mainActivity === activity) return;
|
|
522
|
+
this.mainActivity = activity;
|
|
523
|
+
this.notify();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Record the main model starting a tool; the activity shows the tool's
|
|
527
|
+
* most telling argument, same vocabulary as subagent rows. */
|
|
528
|
+
recordMainToolStart(toolName: string, activity: string): void {
|
|
529
|
+
const safeToolName = sanitizeActivityText(toolName) || "tool";
|
|
530
|
+
this.setMainActivity(activity || safeToolName);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Record a failed main-model tool; successful completions keep their last
|
|
534
|
+
* activity until the next model event supplies a better description. */
|
|
535
|
+
recordMainToolEnd(toolName: string, isError: boolean): void {
|
|
536
|
+
if (isError) this.setMainActivity(`✗ ${sanitizeActivityText(toolName) || "tool"} failed`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Track the parent agent loop: started at agent_start, cleared when the
|
|
540
|
+
* loop settles (agent_end / agent_settled). */
|
|
541
|
+
setMainAgentActive(active: boolean): void {
|
|
542
|
+
if ((this.mainActiveSince !== undefined) === active) return;
|
|
543
|
+
this.mainActiveSince = active ? Date.now() : undefined;
|
|
544
|
+
this.mainActivity = undefined;
|
|
545
|
+
this.notify();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Live view of the parent model while its agent loop runs; undefined when idle. */
|
|
549
|
+
getMainActivity(): MainActivity | undefined {
|
|
550
|
+
if (this.mainActiveSince === undefined) return undefined;
|
|
551
|
+
return {
|
|
552
|
+
...(this.mainModel ? { model: this.mainModel } : {}),
|
|
553
|
+
...(this.mainThinking ? { thinking: this.mainThinking } : {}),
|
|
554
|
+
...(this.mainActivity ? { activity: this.mainActivity } : {}),
|
|
555
|
+
activeSince: this.mainActiveSince,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
isMainAgentActive(): boolean {
|
|
560
|
+
return this.mainActiveSince !== undefined;
|
|
561
|
+
}
|
|
456
562
|
|
|
457
563
|
beginTurn(): void {
|
|
458
564
|
// Clear finished runs from a previous turn, but keep active and parked
|
|
@@ -719,6 +825,10 @@ export class MonitorStore {
|
|
|
719
825
|
* finishRun calls from the old session remain safe no-ops. */
|
|
720
826
|
clear(): void {
|
|
721
827
|
this.runs = [];
|
|
828
|
+
this.mainModel = undefined;
|
|
829
|
+
this.mainThinking = undefined;
|
|
830
|
+
this.mainActivity = undefined;
|
|
831
|
+
this.mainActiveSince = undefined;
|
|
722
832
|
this.notify();
|
|
723
833
|
}
|
|
724
834
|
|