@ferris1225/pi-subagents 0.32.2 → 1.0.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/README.md +35 -16
- package/package.json +2 -2
- package/src/announcements.ts +59 -0
- package/src/dispatch.ts +1833 -1878
- package/src/fixloop.ts +1 -1
- package/src/format.ts +2 -1
- package/src/index.ts +4 -8
- package/src/models.ts +23 -35
- package/src/monitor.ts +29 -88
- package/src/rpc-run.ts +1 -26
- package/src/runtime.ts +284 -285
- package/src/setup.ts +4 -4
- package/src/spawn.ts +1 -6
- package/src/tools.ts +730 -748
- package/src/trajectory.ts +16 -207
- package/src/inspector-panel.ts +0 -363
- package/src/inspector.ts +0 -369
- package/src/widget.ts +0 -195
package/src/fixloop.ts
CHANGED
|
@@ -87,7 +87,7 @@ export function chainKeyFragments(result: SingleResult): string[] {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* Compact one-line outcome for a finished chain run,
|
|
90
|
+
* Compact one-line outcome for a finished chain run, retained so
|
|
91
91
|
* each round reads as what it did: a reviewer reports its verdict plus the
|
|
92
92
|
* key fragments of what it found ("fail · src/index.ts · render()"), a worker
|
|
93
93
|
* the fragments of what it changed. Failed runs and runs with nothing
|
package/src/format.ts
CHANGED
|
@@ -103,7 +103,8 @@ export function formatCompletionBlock(result: SingleResult, maxResultLines: numb
|
|
|
103
103
|
(result.forkChildRunIds?.length ?? 0) > 0 ? `fork children ${result.forkChildRunIds!.map((id) => `#${id}`).join(", ")}` : undefined,
|
|
104
104
|
].filter((value): value is string => Boolean(value));
|
|
105
105
|
const relationNote = relations.length > 0 ? ` · ${relations.join(" · ")}` : "";
|
|
106
|
-
const
|
|
106
|
+
const runNote = result.runId !== undefined ? ` · run #${result.runId}` : "";
|
|
107
|
+
const lines = [`### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}${fallbackNote}${startupRetryNote}${modelRetryNote}${runNote}`, "", `Task: ${formatTaskSummary(result.task, 80, false)}`, ""];
|
|
107
108
|
if (result.isolation === "worktree") {
|
|
108
109
|
const isolation =
|
|
109
110
|
result.integrationStatus === "integrated"
|
package/src/index.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* The heavy lifting lives in focused modules:
|
|
6
6
|
* - dispatch.ts — the `subagent` tool (spawn, auto-fix chain, vision model)
|
|
7
7
|
* - tools.ts — subagent_control / subagent_wait / status / stop
|
|
8
|
-
* -
|
|
9
|
-
* - runtime.ts
|
|
8
|
+
* - announcements.ts — session-start recovery and feature notices
|
|
9
|
+
* - runtime.ts — shared per-session state
|
|
10
10
|
*
|
|
11
11
|
* Also registers the `/subagents-setup` command and a `before_agent_start` hook
|
|
12
12
|
* that injects a delegation directive into the parent system prompt so the main
|
|
@@ -19,16 +19,15 @@
|
|
|
19
19
|
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { Text } from "@earendil-works/pi-tui";
|
|
21
21
|
import { discoverAgents } from "./agents.ts";
|
|
22
|
+
import { registerAnnouncements } from "./announcements.ts";
|
|
22
23
|
import { getConfigPath, loadConfig } from "./config.ts";
|
|
23
24
|
import { registerSubagentTool } from "./dispatch.ts";
|
|
24
25
|
import { matchRunIds } from "./format.ts";
|
|
25
|
-
import { registerInspectorCommand } from "./inspector-panel.ts";
|
|
26
26
|
import { buildDelegationDirective } from "./prompt.ts";
|
|
27
27
|
import { createRuntime } from "./runtime.ts";
|
|
28
28
|
import { runSetup } from "./setup.ts";
|
|
29
29
|
import { currentSubagentDepth } from "./spawn.ts";
|
|
30
30
|
import { registerLookupTools } from "./tools.ts";
|
|
31
|
-
import { registerWidget } from "./widget.ts";
|
|
32
31
|
|
|
33
32
|
export { matchRunIds };
|
|
34
33
|
|
|
@@ -64,7 +63,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
64
63
|
|
|
65
64
|
registerSubagentTool(pi, runtime);
|
|
66
65
|
registerLookupTools(pi, runtime);
|
|
67
|
-
registerInspectorCommand(pi, runtime);
|
|
68
66
|
|
|
69
67
|
pi.registerCommand("subagents-setup", {
|
|
70
68
|
description: "Configure pi-subagents: enabled agents, primary/backup model pools, and runtime settings",
|
|
@@ -73,9 +71,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
73
71
|
},
|
|
74
72
|
});
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
// one-time feature announcements after updates.
|
|
78
|
-
registerWidget(pi, runtime);
|
|
74
|
+
registerAnnouncements(pi, runtime);
|
|
79
75
|
|
|
80
76
|
// Proactive dispatch: inject the delegation directive into the parent system prompt.
|
|
81
77
|
pi.on("before_agent_start", async (event, ctx) => {
|
package/src/models.ts
CHANGED
|
@@ -72,18 +72,25 @@ export function currentModelRef(ctx: Pick<ModelContext, "model">): string | unde
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
75
|
+
* Current authenticated registry models narrowed by the session scope. Scope
|
|
76
|
+
* entries are a session snapshot, so they act only as a whitelist; the live
|
|
77
|
+
* registry remains the source of truth for availability and model metadata.
|
|
77
78
|
*/
|
|
78
|
-
export function
|
|
79
|
+
export function availableModelsInScope(ctx: ModelContext): readonly Model<Api>[] {
|
|
80
|
+
const models = ctx.modelRegistry.getAvailable();
|
|
79
81
|
// scopedModels was added after the declared Pi 0.80.6 minimum. Treat a
|
|
80
|
-
// missing field exactly like an empty scope and use the registry
|
|
82
|
+
// missing field exactly like an empty scope and use the full live registry.
|
|
81
83
|
const scopedModels = ctx.scopedModels ?? [];
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
84
|
+
if (scopedModels.length === 0) return models;
|
|
85
|
+
const scopedRefs = new Set(scopedModels.map((entry) => modelRef(entry.model)));
|
|
86
|
+
return models.filter((model) => scopedRefs.has(modelRef(model)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Model refs usable by setup, with an available current main model first. */
|
|
90
|
+
export function availableModelRefs(ctx: ModelContext): string[] {
|
|
91
|
+
const refs = [...new Set(availableModelsInScope(ctx).map(modelRef))];
|
|
85
92
|
const currentRef = currentModelRef(ctx);
|
|
86
|
-
if (!currentRef) return refs;
|
|
93
|
+
if (!currentRef || !refs.includes(currentRef)) return refs;
|
|
87
94
|
return [currentRef, ...refs.filter((ref) => ref !== currentRef)];
|
|
88
95
|
}
|
|
89
96
|
|
|
@@ -118,7 +125,9 @@ function modelCapabilities(model: ModelListEntry): string {
|
|
|
118
125
|
return capabilities.join(" + ");
|
|
119
126
|
}
|
|
120
127
|
|
|
121
|
-
/** Build the single searchable model list shared by primary/backup/vision picks.
|
|
128
|
+
/** Build the single searchable model list shared by primary/backup/vision picks.
|
|
129
|
+
* Only refs Pi currently reports as available are shown, which means providers
|
|
130
|
+
* without a configured API key/OAuth session never flood the setup picker. */
|
|
122
131
|
export function buildModelPickerItems(options: {
|
|
123
132
|
models: readonly ModelListEntry[];
|
|
124
133
|
availableRefs: readonly string[];
|
|
@@ -132,21 +141,16 @@ export function buildModelPickerItems(options: {
|
|
|
132
141
|
const byRef = new Map<string, ModelListEntry>();
|
|
133
142
|
for (const model of options.models) {
|
|
134
143
|
const ref = modelRef(model);
|
|
135
|
-
if (!byRef.has(ref)) byRef.set(ref, model);
|
|
144
|
+
if (available.has(ref) && !byRef.has(ref)) byRef.set(ref, model);
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
const refs = [...byRef.keys()]
|
|
139
|
-
.filter((ref) =>
|
|
140
|
-
options.slot !== "vision" ||
|
|
141
|
-
byRef.get(ref)?.input.includes("image") === true ||
|
|
142
|
-
ref === configuredRef,
|
|
143
|
-
)
|
|
148
|
+
.filter((ref) => options.slot !== "vision" || byRef.get(ref)?.input.includes("image") === true)
|
|
144
149
|
.sort((left, right) => {
|
|
145
150
|
const leftRank = left === configuredRef ? 0 : left === mainRef ? 1 : 2;
|
|
146
151
|
const rightRank = right === configuredRef ? 0 : right === mainRef ? 1 : 2;
|
|
147
152
|
return leftRank - rightRank || left.localeCompare(right);
|
|
148
153
|
});
|
|
149
|
-
if (configuredRef && !byRef.has(configuredRef)) refs.unshift(configuredRef);
|
|
150
154
|
|
|
151
155
|
const dynamic = options.slot === "backup"
|
|
152
156
|
? {
|
|
@@ -164,31 +168,15 @@ export function buildModelPickerItems(options: {
|
|
|
164
168
|
|
|
165
169
|
const items: ModelPickerItem[] = [dynamic];
|
|
166
170
|
for (const ref of refs) {
|
|
167
|
-
const model = byRef.get(ref)
|
|
168
|
-
const tags = [
|
|
171
|
+
const model = byRef.get(ref)!;
|
|
172
|
+
const tags = ["available"];
|
|
169
173
|
if (ref === configuredRef) tags.push("configured");
|
|
170
174
|
if (ref === mainRef) tags.push("current main");
|
|
171
|
-
if (!model) {
|
|
172
|
-
const compatibility = options.slot === "vision"
|
|
173
|
-
? "incompatible with vision (capability unknown)"
|
|
174
|
-
: undefined;
|
|
175
|
-
items.push({
|
|
176
|
-
value: ref,
|
|
177
|
-
label: ref,
|
|
178
|
-
description: [...tags, compatibility, "saved model reference"].filter(Boolean).join(" · "),
|
|
179
|
-
...(options.slot === "vision" ? { disabled: true } : {}),
|
|
180
|
-
});
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
175
|
const name = model.name.trim() && model.name !== model.id ? model.name.trim() : undefined;
|
|
184
|
-
const compatibility = options.slot === "vision" && !model.input.includes("image")
|
|
185
|
-
? "incompatible with vision"
|
|
186
|
-
: undefined;
|
|
187
176
|
items.push({
|
|
188
177
|
value: ref,
|
|
189
178
|
label: ref,
|
|
190
|
-
description: [name, modelCapabilities(model),
|
|
191
|
-
...(compatibility ? { disabled: true } : {}),
|
|
179
|
+
description: [name, modelCapabilities(model), ...tags].filter(Boolean).join(" · "),
|
|
192
180
|
});
|
|
193
181
|
}
|
|
194
182
|
return items;
|
package/src/monitor.ts
CHANGED
|
@@ -2,18 +2,17 @@
|
|
|
2
2
|
* Sub-agent monitor: a module-level singleton store that tracks subagent runs
|
|
3
3
|
* for the current turn.
|
|
4
4
|
*
|
|
5
|
-
* The store notifies
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* as soon as they finish: the tool result is the durable record in the main
|
|
10
|
-
* conversation, so a stale "done" row must not linger in the widget.
|
|
5
|
+
* The store notifies wait/status consumers on every mutation. Each run carries
|
|
6
|
+
* timing information plus a concise activity string ("thinking",
|
|
7
|
+
* "read src/index.ts", ...). Runs are removed after publication; tool results
|
|
8
|
+
* and the finished-run registry are the durable user-facing records.
|
|
11
9
|
*/
|
|
12
10
|
|
|
13
11
|
import { stripVTControlCharacters } from "node:util";
|
|
14
12
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import {
|
|
13
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
16
14
|
import type { UsageStats } from "./spawn.ts";
|
|
15
|
+
import { redactSensitiveText } from "./trajectory.ts";
|
|
17
16
|
import type { IsolationMode, WorktreeFinalizationStatus } from "./worktree.ts";
|
|
18
17
|
|
|
19
18
|
// ---------------------------------------------------------------------------
|
|
@@ -26,20 +25,6 @@ export function isRunActiveStatus(status: RunStatus): boolean {
|
|
|
26
25
|
return status === "queued" || status === "running" || status === "steering" || status === "interrupting";
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
/** Soft state-awareness signals, complementary to the hard idle-kill: a run may
|
|
30
|
-
* be alive (stdout streaming) yet "stuck thinking" (no tool running for a while),
|
|
31
|
-
* or simply taking a long time. Both are surfaced as widget annotations so the
|
|
32
|
-
* user can tell a healthy busy run from one that needs a nudge. */
|
|
33
|
-
export type ActivityState = "needs_attention" | "active_long_running";
|
|
34
|
-
|
|
35
|
-
/** A run with no tool running and no activity for this long is "needs attention"
|
|
36
|
-
* (the model may be stuck between turns). Below the idle-kill threshold so the
|
|
37
|
-
* soft signal always fires before the hard kill. */
|
|
38
|
-
export const NEEDS_ATTENTION_AFTER_MS = 60_000;
|
|
39
|
-
/** A run whose total elapsed time exceeds this is "long-running": still active
|
|
40
|
-
* but worth flagging so the user can decide whether to wait or steer. */
|
|
41
|
-
export const ACTIVE_LONG_RUNNING_AFTER_MS = 240_000;
|
|
42
|
-
|
|
43
28
|
export interface RunView {
|
|
44
29
|
id: number;
|
|
45
30
|
agent: string;
|
|
@@ -77,15 +62,14 @@ export interface RunView {
|
|
|
77
62
|
groupId?: string;
|
|
78
63
|
/** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
|
|
79
64
|
relationLabel?: string;
|
|
80
|
-
/** Free-form note
|
|
65
|
+
/** Free-form orchestration note (e.g. "auto-fix chain running"). */
|
|
81
66
|
annotation?: string;
|
|
82
|
-
/** One-line outcome summary of a finished chain run
|
|
83
|
-
* each auto-fix round reads as what it did: a reviewer reports its verdict
|
|
67
|
+
/** One-line outcome summary of a finished chain run: a reviewer reports its verdict
|
|
84
68
|
* plus key fragments of what it found ("fail · src/index.ts · render()"), a
|
|
85
69
|
* worker the fragments of what it changed. Unset for non-chain runs. */
|
|
86
70
|
summary?: string;
|
|
87
|
-
/** True when a finished run
|
|
88
|
-
*
|
|
71
|
+
/** True when a finished run remains in monitor state (e.g. an auto-fix
|
|
72
|
+
* parent whose chain is still running). beginTurn preserves
|
|
89
73
|
* retained runs so they are not swept between turns. */
|
|
90
74
|
retained?: boolean;
|
|
91
75
|
}
|
|
@@ -107,7 +91,7 @@ const TASK_SUMMARY_ELLIPSIS = "…";
|
|
|
107
91
|
/** Columns reserved at the END of a truncated summary so the distinguishing
|
|
108
92
|
* keywords (paths, symbols, ...) survive; the head gets the rest. */
|
|
109
93
|
const TASK_SUMMARY_TAIL_MAX = 28;
|
|
110
|
-
/** Tail share of a non-default maxWidth (narrow
|
|
94
|
+
/** Tail share of a non-default maxWidth (narrow summaries keep a usable tail). */
|
|
111
95
|
const TASK_SUMMARY_TAIL_SHARE = 0.35;
|
|
112
96
|
const TASK_SUMMARY_TAIL_MIN = 8;
|
|
113
97
|
const TASK_SUMMARY_KEY_SEP = " · ";
|
|
@@ -305,63 +289,18 @@ export function formatElapsed(run: RunView, now: number = Date.now()): string {
|
|
|
305
289
|
return formatDuration(end - run.startedAt);
|
|
306
290
|
}
|
|
307
291
|
|
|
308
|
-
/** Strip the provider prefix from a "provider/model-id" reference for compact
|
|
309
|
-
* widget display ("anthropic/claude-sonnet-4" → "claude-sonnet-4"). A bare id is
|
|
310
|
-
* left unchanged. */
|
|
311
|
-
export function compactModelRef(model: string | undefined): string {
|
|
312
|
-
if (!model) return "";
|
|
313
|
-
const slash = model.lastIndexOf("/");
|
|
314
|
-
return slash >= 0 ? model.slice(slash + 1) : model;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/** Human-readable label for a soft activity-state annotation. */
|
|
318
|
-
export function activityStateLabel(state: ActivityState): string {
|
|
319
|
-
return state === "needs_attention" ? "idle" : "long-running";
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/** Derive the soft activity state of a run at render time: needs_attention
|
|
323
|
-
* (no tool running, idle past the threshold) takes priority over
|
|
324
|
-
* active_long_running (total elapsed past its threshold). Both are suppressed
|
|
325
|
-
* for non-running runs. */
|
|
326
|
-
export function deriveActivityState(run: RunView, now: number = Date.now()): ActivityState | undefined {
|
|
327
|
-
if (run.status !== "running" && run.status !== "steering") return undefined;
|
|
328
|
-
if (!run.currentTool) {
|
|
329
|
-
const since = run.lastActivityAt ?? run.startedAt ?? now;
|
|
330
|
-
if (now - since >= NEEDS_ATTENTION_AFTER_MS) return "needs_attention";
|
|
331
|
-
}
|
|
332
|
-
if (run.startedAt !== undefined && now - run.startedAt >= ACTIVE_LONG_RUNNING_AFTER_MS) {
|
|
333
|
-
return "active_long_running";
|
|
334
|
-
}
|
|
335
|
-
return undefined;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/** Left/right split a widget line so the right side (status, elapsed) is always
|
|
339
|
-
* visible and the left side (title) clips on overflow instead of pushing it off.
|
|
340
|
-
* `left`/`right` may carry ANSI styling; widths are measured display-column-wise. */
|
|
341
|
-
export function rightAlign(left: string, right: string, width: number): string {
|
|
342
|
-
const rightWidth = visibleWidth(right);
|
|
343
|
-
const leftMax = Math.max(0, width - rightWidth - 1);
|
|
344
|
-
const leftClipped = truncateToWidth(left, leftMax);
|
|
345
|
-
const gap = Math.max(1, width - visibleWidth(leftClipped) - rightWidth);
|
|
346
|
-
return truncateToWidth(`${leftClipped}${" ".repeat(gap)}${right}`, width);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/** Concatenate `left` and `right` (no center padding) and clip the combined line
|
|
350
|
-
* to `width`. Unlike {@link rightAlign}, the right side trails the left side
|
|
351
|
-
* instead of being pinned to the far edge, so a short header leaves no gap in
|
|
352
|
-
* the middle. `right` should carry its own leading separator (e.g. ` \u00b7 `);
|
|
353
|
-
* pass an empty string when there is nothing to append. Styled strings are
|
|
354
|
-
* measured by display width. */
|
|
355
|
-
export function compactLine(left: string, right: string, width: number): string {
|
|
356
|
-
return truncateToWidth(`${left}${right}`, width);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
292
|
/** Max length of the argument target inside a formatted activity line. */
|
|
360
293
|
export const ACTIVITY_TARGET_MAX = 60;
|
|
361
294
|
|
|
295
|
+
/** Monitor activity is returned to the parent model and rendered in the terminal,
|
|
296
|
+
* so treat every live string as untrusted before it reaches store state. */
|
|
297
|
+
function sanitizeActivityText(value: string): string {
|
|
298
|
+
return redactSensitiveText(value).replace(/\s+/g, " ").trim();
|
|
299
|
+
}
|
|
300
|
+
|
|
362
301
|
function shortTarget(value: unknown): string {
|
|
363
302
|
if (typeof value !== "string") return "";
|
|
364
|
-
const oneLine = value
|
|
303
|
+
const oneLine = sanitizeActivityText(value);
|
|
365
304
|
// Slice by code point so emoji / CJK-ext never leave a lone surrogate.
|
|
366
305
|
const chars = [...oneLine];
|
|
367
306
|
return chars.length > ACTIVITY_TARGET_MAX ? `${chars.slice(0, ACTIVITY_TARGET_MAX - 1).join("")}…` : oneLine;
|
|
@@ -410,7 +349,8 @@ export function formatToolActivity(toolName: string, args: unknown): string {
|
|
|
410
349
|
default:
|
|
411
350
|
target = pick("path", "command", "query", "pattern", "url", "file", "task");
|
|
412
351
|
}
|
|
413
|
-
|
|
352
|
+
const safeToolName = sanitizeActivityText(toolName) || "tool";
|
|
353
|
+
return target ? `${safeToolName} ${target}` : safeToolName;
|
|
414
354
|
}
|
|
415
355
|
|
|
416
356
|
// ---------------------------------------------------------------------------
|
|
@@ -491,7 +431,7 @@ export class MonitorStore {
|
|
|
491
431
|
setActivity(id: number, text: string): void {
|
|
492
432
|
const run = this.find(id);
|
|
493
433
|
if (!run) return;
|
|
494
|
-
run.activity = text;
|
|
434
|
+
run.activity = sanitizeActivityText(text) || undefined;
|
|
495
435
|
run.lastActivityAt = Date.now();
|
|
496
436
|
this.notify();
|
|
497
437
|
}
|
|
@@ -502,9 +442,10 @@ export class MonitorStore {
|
|
|
502
442
|
recordToolStart(id: number, toolName: string, activity: string): void {
|
|
503
443
|
const run = this.find(id);
|
|
504
444
|
if (!run) return;
|
|
445
|
+
const safeToolName = sanitizeActivityText(toolName) || "tool";
|
|
505
446
|
run.toolCount = (run.toolCount ?? 0) + 1;
|
|
506
|
-
run.currentTool =
|
|
507
|
-
run.activity = activity;
|
|
447
|
+
run.currentTool = safeToolName;
|
|
448
|
+
run.activity = sanitizeActivityText(activity) || safeToolName;
|
|
508
449
|
run.lastActivityAt = Date.now();
|
|
509
450
|
this.notify();
|
|
510
451
|
}
|
|
@@ -516,11 +457,11 @@ export class MonitorStore {
|
|
|
516
457
|
if (!run) return;
|
|
517
458
|
run.currentTool = undefined;
|
|
518
459
|
run.lastActivityAt = Date.now();
|
|
519
|
-
if (isError) run.activity = `✗ ${toolName} failed`;
|
|
460
|
+
if (isError) run.activity = `✗ ${sanitizeActivityText(toolName) || "tool"} failed`;
|
|
520
461
|
this.notify();
|
|
521
462
|
}
|
|
522
463
|
|
|
523
|
-
/** Set
|
|
464
|
+
/** Set an orchestration note on the run (e.g. auto-fix chain running). */
|
|
524
465
|
setAnnotation(id: number, text: string): void {
|
|
525
466
|
const run = this.find(id);
|
|
526
467
|
if (!run) return;
|
|
@@ -536,7 +477,7 @@ export class MonitorStore {
|
|
|
536
477
|
this.notify();
|
|
537
478
|
}
|
|
538
479
|
|
|
539
|
-
/**
|
|
480
|
+
/** Keep a finished chain step in status state until its group settles. */
|
|
540
481
|
setRetained(id: number, retained: boolean): void {
|
|
541
482
|
const run = this.find(id);
|
|
542
483
|
if (!run) return;
|
|
@@ -624,7 +565,7 @@ export class MonitorStore {
|
|
|
624
565
|
this.notify();
|
|
625
566
|
}
|
|
626
567
|
|
|
627
|
-
/** Remove a run
|
|
568
|
+
/** Remove a run after publication. Returns the removed run. */
|
|
628
569
|
removeRun(id: number): RunView | undefined {
|
|
629
570
|
const index = this.runs.findIndex((r) => r.id === id);
|
|
630
571
|
if (index === -1) return undefined;
|
|
@@ -698,7 +639,7 @@ export function statusIcon(status: RunStatus, theme: Theme): string {
|
|
|
698
639
|
}
|
|
699
640
|
}
|
|
700
641
|
|
|
701
|
-
/** User-facing status label
|
|
642
|
+
/** User-facing status label used by tool/status rendering. */
|
|
702
643
|
export function statusLabel(status: RunStatus): string {
|
|
703
644
|
switch (status) {
|
|
704
645
|
case "queued":
|
package/src/rpc-run.ts
CHANGED
|
@@ -96,15 +96,6 @@ export type SubagentLiveEvent =
|
|
|
96
96
|
| { kind: "thinking" }
|
|
97
97
|
| { kind: "text" };
|
|
98
98
|
|
|
99
|
-
/** Carries the actual streamed payload (dropped by the plain text/thinking
|
|
100
|
-
* live events). Emitted in addition to the aliveness events so monitor
|
|
101
|
-
* observers stay unchanged while the inspector can apply its rolling budget
|
|
102
|
-
* without silently losing part of a transport delta. */
|
|
103
|
-
export interface SubagentRecordEvent {
|
|
104
|
-
kind: "thinking" | "text";
|
|
105
|
-
delta: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
99
|
export type RpcControlPhase =
|
|
109
100
|
| "queued"
|
|
110
101
|
| "starting"
|
|
@@ -413,15 +404,13 @@ export interface RunRpcAttemptOptions {
|
|
|
413
404
|
prompt: string;
|
|
414
405
|
signal?: AbortSignal;
|
|
415
406
|
onLive?: (event: SubagentLiveEvent) => void;
|
|
416
|
-
/** Receives the raw streamed deltas; observer errors are swallowed. */
|
|
417
|
-
onRecord?: (event: SubagentRecordEvent) => void;
|
|
418
407
|
env?: NodeJS.ProcessEnv;
|
|
419
408
|
control?: RpcRunControl;
|
|
420
409
|
}
|
|
421
410
|
|
|
422
411
|
/** Run one persistent RPC child until a stable `agent_settled` or control action. */
|
|
423
412
|
export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise<RpcSingleResult> {
|
|
424
|
-
const { agent, agentName, task, thinkingLevel, idleTimeoutMs, signal, onLive,
|
|
413
|
+
const { agent, agentName, task, thinkingLevel, idleTimeoutMs, signal, onLive, control } = options;
|
|
425
414
|
const args: string[] = ["--mode", "rpc", "--exclude-tools", "subagent,subagent_control"];
|
|
426
415
|
if (options.sessionDir && options.sessionId) {
|
|
427
416
|
args.push("--session-dir", options.sessionDir);
|
|
@@ -506,14 +495,6 @@ export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise
|
|
|
506
495
|
}
|
|
507
496
|
};
|
|
508
497
|
|
|
509
|
-
const emitRecord = (event: SubagentRecordEvent): void => {
|
|
510
|
-
try {
|
|
511
|
-
onRecord?.(event);
|
|
512
|
-
} catch {
|
|
513
|
-
/* record observers must never break protocol handling */
|
|
514
|
-
}
|
|
515
|
-
};
|
|
516
|
-
|
|
517
498
|
const setAttemptPhase = (phase: RpcControlPhase): void => {
|
|
518
499
|
if (attemptToken !== undefined) control?.updateAttemptPhase(attemptToken, phase);
|
|
519
500
|
switch (phase) {
|
|
@@ -806,12 +787,6 @@ export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise
|
|
|
806
787
|
const type = event.assistantMessageEvent?.type;
|
|
807
788
|
if (type === "thinking_delta" || type === "text_delta") {
|
|
808
789
|
emit({ kind: type === "thinking_delta" ? "thinking" : "text" });
|
|
809
|
-
const delta = event.assistantMessageEvent?.delta;
|
|
810
|
-
if (typeof delta === "string" && delta) {
|
|
811
|
-
// TranscriptBuffer owns the complete rolling-budget decision. Truncating
|
|
812
|
-
// one RPC delta here would silently lose text without setting its flag.
|
|
813
|
-
emitRecord({ kind: type === "thinking_delta" ? "thinking" : "text", delta });
|
|
814
|
-
}
|
|
815
790
|
}
|
|
816
791
|
}
|
|
817
792
|
|