@dougbots/avenor-pi 0.3.7 → 0.4.1

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +36 -2
  2. package/dist/index.js +1370 -598
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,58 +1,533 @@
1
- import { Text } from "@earendil-works/pi-tui";
1
+ import { Input, Key, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
2
  import { Type } from "typebox";
3
- import { answerPermissionTool, eventsTool, followUpTool, shutdownTool, spawnTool, statusTool } from "@dougbots/avenor-core";
3
+ import { Supervisor, answerPermissionTool, dial, eventsTool, followUpTool, inspectTool, observeRun, shutdownTool, spawnTool, statusTool } from "@dougbots/avenor-core";
4
4
  //#region src/watch.ts
5
- var EventFeedOverlay = class {
6
- _events = [];
7
- _runId;
8
- _onClose;
9
- _maxLines = 50;
10
- constructor(runId, onClose) {
11
- this._runId = runId;
12
- this._onClose = onClose ?? (() => {});
5
+ const ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
6
+ const TRANSCRIPT_SCROLL_STEP = 4;
7
+ const ACTION_STATUS_CHARS = 180;
8
+ const PREVIEW_CHARS = 240;
9
+ const TOOL_PREVIEW_CHARS = 120;
10
+ const FINAL_OUTPUT_CHARS = 600;
11
+ const TRANSCRIPT_PADDING = 2;
12
+ const INSPECT_ERROR_PREFIX = "error: ";
13
+ const TERMINAL_STATUSES$1 = new Set([
14
+ "done",
15
+ "failed",
16
+ "timeout",
17
+ "killed"
18
+ ]);
19
+ function emptySnapshot() {
20
+ return {
21
+ identity: {},
22
+ limits: {
23
+ transcript_entries: 64,
24
+ text_chars: 4e3,
25
+ tool_preview_chars: 1e3,
26
+ tools: 16,
27
+ permissions: 16,
28
+ legacy_keys: 128
29
+ },
30
+ ended: false,
31
+ assistant_text: "",
32
+ thought_text: "",
33
+ user_text: "",
34
+ transcript: [],
35
+ tools: [],
36
+ live_tools: [],
37
+ permissions: [],
38
+ _seen_seq_by_scope: {},
39
+ _recent_legacy_keys: []
40
+ };
41
+ }
42
+ function sanitizeText(text) {
43
+ return text.replace(ANSI_PATTERN, "").replaceAll(" ", " ").replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
44
+ }
45
+ function clipText(text, limit) {
46
+ if (!text) return void 0;
47
+ const clean = sanitizeText(text).trim();
48
+ if (!clean) return void 0;
49
+ if (clean.length <= limit) return clean;
50
+ return `${clean.slice(0, Math.max(0, limit - 1))}…`;
51
+ }
52
+ function compactWhitespace$1(text, limit) {
53
+ const clean = clipText(text, limit);
54
+ if (!clean) return void 0;
55
+ return clean.replace(/\s+/g, " ").trim() || void 0;
56
+ }
57
+ function padLine(text, width) {
58
+ const clipped = truncateToWidth(text, width);
59
+ return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
60
+ }
61
+ function wrapPrefixed(text, width, firstPrefix, nextPrefix = " ".repeat(Math.max(0, visibleWidth(firstPrefix)))) {
62
+ const clean = clipText(text, PREVIEW_CHARS);
63
+ if (!clean) return [];
64
+ return wrapTextWithAnsi(clean, Math.max(10, width - Math.max(visibleWidth(firstPrefix), visibleWidth(nextPrefix)))).map((line, index) => truncateToWidth(`${index === 0 ? firstPrefix : nextPrefix}${line}`, width));
65
+ }
66
+ function renderKv(theme, label, value, width) {
67
+ if (!value) return [];
68
+ return wrapPrefixed(value, width, theme.fg("dim", `${label}: `));
69
+ }
70
+ function usageSummary(usage) {
71
+ if (!usage) return void 0;
72
+ const order = [
73
+ "input_tokens",
74
+ "output_tokens",
75
+ "total_tokens",
76
+ "tokens"
77
+ ];
78
+ const parts = [];
79
+ for (const key of order) {
80
+ const value = usage[key];
81
+ if (typeof value === "number" && Number.isFinite(value)) parts.push(`${key}=${value}`);
13
82
  }
14
- setOnClose(fn) {
15
- this._onClose = fn;
83
+ if (parts.length === 0) {
84
+ const entries = Object.entries(usage).filter(([, value]) => typeof value === "number" || typeof value === "string" || typeof value === "boolean").slice(0, 4).map(([key, value]) => `${key}=${String(value)}`);
85
+ if (entries.length === 0) return void 0;
86
+ return entries.join(" · ");
16
87
  }
17
- setEvents(events) {
18
- this._events = events.slice(-this._maxLines);
88
+ return parts.join(" · ");
89
+ }
90
+ function finalOutputPreview(status, snapshot) {
91
+ return clipText(status?.final_output ?? snapshot?.final_output ?? snapshot?.assistant_text, FINAL_OUTPUT_CHARS);
92
+ }
93
+ function currentStatusWord(status, snapshot) {
94
+ const raw = status?.status?.toLowerCase();
95
+ if (raw && TERMINAL_STATUSES$1.has(raw)) return raw;
96
+ if (snapshot.pending_permission) return "waiting";
97
+ if (snapshot.ended) {
98
+ const stop = snapshot.stop_reason?.toLowerCase();
99
+ if (stop && TERMINAL_STATUSES$1.has(stop)) return stop;
100
+ return raw && raw !== "running" ? raw : "done";
19
101
  }
20
- appendEvent(event) {
21
- this._events.push(event);
22
- if (this._events.length > this._maxLines) this._events = this._events.slice(-this._maxLines);
102
+ return raw ?? "running";
103
+ }
104
+ function isPromptable(status, snapshot) {
105
+ const current = currentStatusWord(status, snapshot);
106
+ return !TERMINAL_STATUSES$1.has(current) && current !== "unavailable";
107
+ }
108
+ function isFollowUpable(status, snapshot) {
109
+ return TERMINAL_STATUSES$1.has(currentStatusWord(status, snapshot)) || snapshot.ended;
110
+ }
111
+ function transcriptLabel(entry, theme) {
112
+ switch (entry.kind) {
113
+ case "assistant": return theme.fg("text", "A ");
114
+ case "thought": return theme.fg("muted", "~ ");
115
+ case "user": return theme.fg("accent", "> ");
116
+ case "tool": return theme.fg("toolTitle", `→ ${entry.title ?? "tool"}${entry.status ? ` [${entry.status}]` : ""} `);
117
+ case "permission": return theme.fg("warning", "🔒 ");
118
+ case "session": return theme.fg("success", "■ ");
119
+ case "status": return theme.fg("dim", "· ");
23
120
  }
24
- render(width) {
25
- const lines = [];
26
- const header = `📡 avenor watch: ${this._runId.slice(0, 8)}… (Esc to close)`;
27
- lines.push(header);
28
- lines.push("─".repeat(Math.min(width, header.length)));
29
- if (this._events.length === 0) lines.push(" (waiting for events…)");
30
- else for (const evt of this._events) {
31
- const ts = new Date(evt.ts).toISOString().slice(11, 19);
32
- const type = evt.type ?? "unknown";
33
- const extra = this.summarizeEvent(evt);
34
- lines.push(` ${ts} ${type}${extra ? ` — ${extra}` : ""}`);
121
+ }
122
+ function transcriptText(entry) {
123
+ if (entry.kind === "tool") return compactWhitespace$1(entry.text, TOOL_PREVIEW_CHARS) ?? entry.title;
124
+ if (entry.kind === "session") return compactWhitespace$1(entry.text, PREVIEW_CHARS) ?? entry.status;
125
+ return compactWhitespace$1(entry.text, PREVIEW_CHARS);
126
+ }
127
+ function renderTranscriptLines(snapshot, width, theme) {
128
+ const out = [];
129
+ for (const entry of snapshot.transcript) {
130
+ const text = transcriptText(entry);
131
+ if (!text) continue;
132
+ const prefix = transcriptLabel(entry, theme);
133
+ out.push(...wrapPrefixed(text, width, prefix));
134
+ }
135
+ if (snapshot.live_tools.length > 0) {
136
+ if (out.length > 0) out.push("");
137
+ out.push(theme.fg("dim", "live tools:"));
138
+ for (const tool of snapshot.live_tools) out.push(...renderToolState(tool, width, theme));
139
+ }
140
+ if (snapshot.pending_permission) {
141
+ if (out.length > 0) out.push("");
142
+ out.push(...wrapPrefixed(snapshot.pending_permission.description, width, theme.fg("warning", "pending: ")));
143
+ }
144
+ return out;
145
+ }
146
+ function renderToolState(tool, width, theme) {
147
+ const preview = compactWhitespace$1(tool.preview, TOOL_PREVIEW_CHARS);
148
+ const firstPrefix = `${theme.fg("toolTitle", `→ ${tool.title ?? tool.kind ?? "tool"} `)}${(tool.live ? theme.fg("warning", "[running] ") : tool.status && `${theme.fg("dim", `[${tool.status}] `)}`) ?? ""}`;
149
+ return wrapPrefixed(preview ?? "(no preview)", width, firstPrefix);
150
+ }
151
+ function helpText(state) {
152
+ const actions = [
153
+ "↑/↓ scroll",
154
+ "pgup/pgdn page",
155
+ "home/end jump",
156
+ "esc close"
157
+ ];
158
+ if (state.status && isPromptable(state.status, state.snapshot)) actions.unshift("enter send prompt", "ctrl-c cancel run");
159
+ else if (isFollowUpable(state.status, state.snapshot)) actions.unshift("enter follow up");
160
+ return actions.join(" · ");
161
+ }
162
+ var RunInspectorOverlay = class {
163
+ tui;
164
+ theme;
165
+ keybindings;
166
+ deps;
167
+ done;
168
+ onTrackRun;
169
+ input = new Input();
170
+ state;
171
+ observer = null;
172
+ observerUnsubscribe = null;
173
+ refreshPending = false;
174
+ scrollOffset = 0;
175
+ closed = false;
176
+ actionPending = false;
177
+ actionError;
178
+ _focused = false;
179
+ get focused() {
180
+ return this._focused;
181
+ }
182
+ set focused(value) {
183
+ this._focused = value;
184
+ this.input.focused = value;
185
+ }
186
+ constructor(tui, theme, keybindings, deps, run, done, onTrackRun) {
187
+ this.tui = tui;
188
+ this.theme = theme;
189
+ this.keybindings = keybindings;
190
+ this.deps = deps;
191
+ this.done = done;
192
+ this.onTrackRun = onTrackRun;
193
+ this.state = {
194
+ run,
195
+ snapshot: emptySnapshot(),
196
+ loading: true
197
+ };
198
+ this.input.onSubmit = (value) => {
199
+ const text = value.trim();
200
+ if (!text) return;
201
+ this.input.setValue("");
202
+ this.submit(text);
203
+ };
204
+ this.reload(true);
205
+ }
206
+ requestRender() {
207
+ this.tui.requestRender();
208
+ }
209
+ close() {
210
+ if (this.closed) return;
211
+ this.closed = true;
212
+ this.cleanup();
213
+ this.done();
214
+ }
215
+ dispose() {
216
+ if (this.closed) return;
217
+ this.closed = true;
218
+ this.cleanup();
219
+ }
220
+ invalidate() {
221
+ this.input.invalidate();
222
+ }
223
+ async cleanup() {
224
+ this.observerUnsubscribe?.();
225
+ this.observerUnsubscribe = null;
226
+ if (this.observer) {
227
+ const observer = this.observer;
228
+ this.observer = null;
229
+ await observer.close().catch(() => {});
230
+ }
231
+ }
232
+ async reload(withObserver) {
233
+ this.state = {
234
+ ...this.state,
235
+ loading: true,
236
+ error: void 0,
237
+ actionMessage: void 0
238
+ };
239
+ this.requestRender();
240
+ try {
241
+ const result = await this.deps.inspectRun(this.state.run);
242
+ if (this.closed) return;
243
+ const nextRun = {
244
+ ...this.state.run,
245
+ runId: result.status.run_id ?? this.state.run.runId,
246
+ label: result.label ?? this.state.run.label,
247
+ agent: result.status.agent ?? this.state.run.agent,
248
+ runtimeId: result.status.runtime_id ?? result.snapshot.identity.runtime_id ?? this.state.run.runtimeId
249
+ };
250
+ this.state = {
251
+ run: nextRun,
252
+ status: result.status,
253
+ snapshot: result.snapshot,
254
+ loading: false
255
+ };
256
+ this.onTrackRun?.(nextRun, result.status);
257
+ this.requestRender();
258
+ await this.cleanup();
259
+ if (!withObserver) return;
260
+ if (TERMINAL_STATUSES$1.has(currentStatusWord(result.status, result.snapshot))) return;
261
+ if (!(nextRun.runtimeId ?? result.snapshot.identity.runtime_id)) return;
262
+ const observer = await this.deps.observeRun(nextRun, result.snapshot.latest_seq, result.snapshot);
263
+ if (this.closed || !observer) {
264
+ await observer?.close().catch(() => {});
265
+ return;
266
+ }
267
+ const applySnapshot = (snapshot) => {
268
+ this.state = {
269
+ ...this.state,
270
+ snapshot: {
271
+ ...snapshot,
272
+ identity: {
273
+ ...snapshot.identity,
274
+ run_id: snapshot.identity.run_id ?? this.state.status?.run_id ?? this.state.run.runId
275
+ }
276
+ }
277
+ };
278
+ if (snapshot.ended && !this.refreshPending) {
279
+ this.refreshPending = true;
280
+ this.reload(false).finally(() => {
281
+ this.refreshPending = false;
282
+ });
283
+ }
284
+ this.requestRender();
285
+ };
286
+ this.observer = observer;
287
+ this.observerUnsubscribe = observer.subscribe((snapshot) => {
288
+ applySnapshot(snapshot);
289
+ });
290
+ const current = observer.snapshot();
291
+ if (current.last_event || current.transcript.length > 0 || current.pending_permission || current.ended) applySnapshot(current);
292
+ } catch (error) {
293
+ if (this.closed) return;
294
+ this.state = {
295
+ ...this.state,
296
+ loading: false,
297
+ error: error instanceof Error ? error.message : String(error)
298
+ };
299
+ this.requestRender();
35
300
  }
36
- return lines;
301
+ }
302
+ runAction(work, message) {
303
+ if (this.actionPending) return;
304
+ this.actionPending = true;
305
+ this.actionError = void 0;
306
+ if (message) this.state = {
307
+ ...this.state,
308
+ actionMessage: clipText(message, ACTION_STATUS_CHARS)
309
+ };
310
+ this.requestRender();
311
+ work().catch((error) => {
312
+ this.actionError = error instanceof Error ? error.message : String(error);
313
+ }).finally(() => {
314
+ this.actionPending = false;
315
+ this.requestRender();
316
+ });
317
+ }
318
+ submit(text) {
319
+ const runtimeId = this.state.run.runtimeId ?? this.state.status?.runtime_id ?? this.state.snapshot.identity.runtime_id;
320
+ if (this.state.status && isPromptable(this.state.status, this.state.snapshot) && runtimeId) {
321
+ this.runAction(async () => {
322
+ await this.deps.promptRun(this.state.run, runtimeId, text);
323
+ this.state = {
324
+ ...this.state,
325
+ actionMessage: clipText(`Queued prompt: ${text}`, ACTION_STATUS_CHARS)
326
+ };
327
+ }, "sending prompt…");
328
+ return;
329
+ }
330
+ if (!isFollowUpable(this.state.status, this.state.snapshot)) return;
331
+ this.runAction(async () => {
332
+ const next = await this.deps.followUpRun(this.state.run, text);
333
+ this.state = {
334
+ ...this.state,
335
+ run: {
336
+ ...this.state.run,
337
+ runId: next.run_id,
338
+ label: next.label,
339
+ runtimeId: void 0
340
+ },
341
+ actionMessage: clipText(`Started follow-up run ${next.label}`, ACTION_STATUS_CHARS)
342
+ };
343
+ this.scrollOffset = 0;
344
+ await this.reload(true);
345
+ }, "starting follow-up…");
346
+ }
347
+ cancelCurrentRun() {
348
+ const runtimeId = this.state.run.runtimeId ?? this.state.status?.runtime_id ?? this.state.snapshot.identity.runtime_id;
349
+ if (!runtimeId) return;
350
+ this.runAction(async () => {
351
+ await this.deps.cancelRun(this.state.run, runtimeId);
352
+ this.state = {
353
+ ...this.state,
354
+ actionMessage: "cancel requested"
355
+ };
356
+ }, "cancelling run…");
37
357
  }
38
358
  handleInput(data) {
39
- if (data === "\x1B") this._onClose();
359
+ if (matchesKey(data, Key.escape)) {
360
+ this.close();
361
+ return;
362
+ }
363
+ if (matchesKey(data, Key.up)) {
364
+ this.scrollOffset += TRANSCRIPT_SCROLL_STEP;
365
+ this.requestRender();
366
+ return;
367
+ }
368
+ if (matchesKey(data, Key.down)) {
369
+ this.scrollOffset = Math.max(0, this.scrollOffset - TRANSCRIPT_SCROLL_STEP);
370
+ this.requestRender();
371
+ return;
372
+ }
373
+ if (matchesKey(data, Key.pageUp)) {
374
+ this.scrollOffset += this.viewportHeight();
375
+ this.requestRender();
376
+ return;
377
+ }
378
+ if (matchesKey(data, Key.pageDown)) {
379
+ this.scrollOffset = Math.max(0, this.scrollOffset - this.viewportHeight());
380
+ this.requestRender();
381
+ return;
382
+ }
383
+ if (matchesKey(data, Key.home)) {
384
+ this.scrollOffset = Number.MAX_SAFE_INTEGER;
385
+ this.requestRender();
386
+ return;
387
+ }
388
+ if (matchesKey(data, Key.end)) {
389
+ this.scrollOffset = 0;
390
+ this.requestRender();
391
+ return;
392
+ }
393
+ if (matchesKey(data, Key.ctrl("c")) && this.state.status && isPromptable(this.state.status, this.state.snapshot)) {
394
+ this.cancelCurrentRun();
395
+ return;
396
+ }
397
+ this.input.handleInput(data);
398
+ this.requestRender();
40
399
  }
41
- summarizeEvent(evt) {
42
- if (evt.type === "tool.call" || evt.type === "tool.use") return `called ${String(evt.tool ?? evt.name ?? "")}`;
43
- if (evt.type === "agent.message_chunk") return "thinking…";
44
- if (evt.type?.startsWith("permission.")) return "permission requested";
45
- if (evt.type === "tool.result") {
46
- const result = evt.result;
47
- if (typeof result === "string" && result.length < 80) return result;
48
- return "completed";
400
+ viewportHeight() {
401
+ const rows = this.tui.terminal.rows || 30;
402
+ return Math.max(8, rows - 15);
403
+ }
404
+ render(width) {
405
+ const theme = this.theme;
406
+ const lines = [];
407
+ const horizontal = theme.fg("borderAccent", "─".repeat(Math.max(1, width)));
408
+ const innerWidth = Math.max(10, width);
409
+ lines.push(horizontal);
410
+ const title = this.state.run.label ?? this.state.run.runId;
411
+ const statusWord = currentStatusWord(this.state.status, this.state.snapshot);
412
+ const heading = `${theme.fg("accent", theme.bold(title))} ${theme.fg("dim", `(${this.state.run.runId})`)} ${theme.fg(statusWord === "done" ? "success" : statusWord === "failed" ? "error" : statusWord === "waiting" ? "warning" : "muted", statusWord)}`;
413
+ lines.push(truncateToWidth(heading, innerWidth));
414
+ const phase = this.state.snapshot.phase_label ?? this.state.status?.phase_label ?? this.state.snapshot.phase ?? this.state.status?.phase;
415
+ const summary = [
416
+ phase ? `phase=${phase}` : void 0,
417
+ this.state.snapshot.latest_seq !== void 0 ? `seq=${this.state.snapshot.latest_seq}` : void 0,
418
+ this.state.snapshot.identity.agent ?? this.state.status?.agent,
419
+ this.state.snapshot.identity.model ?? this.state.status?.model,
420
+ this.state.snapshot.identity.backend ?? this.state.status?.backend
421
+ ].filter((value) => !!value).join(" · ");
422
+ lines.push(...renderKv(theme, "status", summary || void 0, innerWidth));
423
+ const identity = [
424
+ this.state.snapshot.identity.runtime_id ?? this.state.status?.runtime_id,
425
+ this.state.snapshot.identity.session_id ?? this.state.status?.session_id,
426
+ this.state.snapshot.identity.dir ?? this.state.status?.dir
427
+ ].filter((value) => !!value).join(" · ");
428
+ lines.push(...renderKv(theme, "identity", identity || void 0, innerWidth));
429
+ lines.push(...renderKv(theme, "usage", usageSummary(this.state.snapshot.usage ?? this.state.status?.usage), innerWidth));
430
+ const finalOutput = finalOutputPreview(this.state.status, this.state.snapshot);
431
+ if (finalOutput && (this.state.snapshot.ended || TERMINAL_STATUSES$1.has(statusWord))) lines.push(...wrapPrefixed(finalOutput, innerWidth, theme.fg("success", "final: ")));
432
+ if (this.state.snapshot.pending_permission) {
433
+ lines.push(...wrapPrefixed(this.state.snapshot.pending_permission.description, innerWidth, theme.fg("warning", "permission: ")));
434
+ const optionText = this.state.snapshot.pending_permission.options.map((option) => `${option.option_id} (${option.label})`).join(", ");
435
+ lines.push(...wrapPrefixed(optionText, innerWidth, theme.fg("dim", "options: ")));
436
+ } else if (this.state.status?.pending_permission) {
437
+ lines.push(...wrapPrefixed(this.state.status.pending_permission.description, innerWidth, theme.fg("warning", "permission: ")));
438
+ const optionText = this.state.status.pending_permission.options.map((option) => `${option.option_id} (${option.label})`).join(", ");
439
+ lines.push(...wrapPrefixed(optionText, innerWidth, theme.fg("dim", "options: ")));
49
440
  }
50
- return "";
441
+ const liveTools = this.state.snapshot.live_tools.slice(-4);
442
+ if (liveTools.length > 0) {
443
+ lines.push(theme.fg("dim", "live tools:"));
444
+ for (const tool of liveTools) lines.push(...renderToolState(tool, innerWidth, theme));
445
+ }
446
+ if (this.state.loading) lines.push(theme.fg("muted", "loading…"));
447
+ if (this.state.actionMessage) lines.push(...wrapPrefixed(this.state.actionMessage, innerWidth, theme.fg("accent", "action: ")));
448
+ if (this.actionError) lines.push(...wrapPrefixed(this.actionError, innerWidth, theme.fg("error", INSPECT_ERROR_PREFIX)));
449
+ else if (this.state.error) lines.push(...wrapPrefixed(this.state.error, innerWidth, theme.fg("error", INSPECT_ERROR_PREFIX)));
450
+ lines.push(horizontal);
451
+ lines.push(theme.fg("dim", "transcript:"));
452
+ const transcript = renderTranscriptLines(this.state.snapshot, Math.max(10, innerWidth - TRANSCRIPT_PADDING), theme).map((line) => truncateToWidth(` ${line}`, innerWidth));
453
+ const viewportHeight = this.viewportHeight();
454
+ const scrollNotice = this.scrollOffset > 0 ? truncateToWidth(theme.fg("dim", `… ${this.scrollOffset} line${this.scrollOffset === 1 ? "" : "s"} below`), innerWidth) : void 0;
455
+ const transcriptCapacity = Math.max(1, viewportHeight - (scrollNotice ? 1 : 0));
456
+ const maxOffset = Math.max(0, transcript.length - transcriptCapacity);
457
+ if (this.scrollOffset > maxOffset) this.scrollOffset = maxOffset;
458
+ const end = transcript.length - this.scrollOffset;
459
+ const start = Math.max(0, end - transcriptCapacity);
460
+ const body = transcript.slice(start, end);
461
+ while (body.length < transcriptCapacity) body.push("");
462
+ if (scrollNotice) body.push(scrollNotice);
463
+ for (const line of body) lines.push(padLine(line, innerWidth));
464
+ lines.push(horizontal);
465
+ lines.push(...this.input.render(innerWidth).map((line) => truncateToWidth(line, innerWidth)));
466
+ const promptHint = this.state.status && isPromptable(this.state.status, this.state.snapshot) ? "send prompt to running/waiting run" : isFollowUpable(this.state.status, this.state.snapshot) ? "start a follow-up run from this transcript" : "read-only view";
467
+ lines.push(truncateToWidth(theme.fg("dim", `${promptHint} · ${helpText(this.state)}`), innerWidth));
468
+ lines.push(horizontal);
469
+ return lines.map((line) => truncateToWidth(line, innerWidth));
51
470
  }
52
- invalidate() {}
53
471
  };
472
+ function matchingWatchRuns(runs, selector) {
473
+ const query = selector.trim().toLowerCase();
474
+ if (!query) return [];
475
+ const byId = runs.find((run) => run.runId.toLowerCase() === query);
476
+ if (byId) return [byId];
477
+ return runs.filter((run) => run.label?.toLowerCase() === query || run.agent?.toLowerCase() === query);
478
+ }
479
+ function watchRunChoice(run) {
480
+ return `${sanitizeText(run.label ?? run.runId)}${run.agent ? ` · ${sanitizeText(run.agent)}` : ""} · ${sanitizeText(run.runId).slice(0, 8)}`;
481
+ }
482
+ async function selectWatchRun(ctx, runs) {
483
+ const choices = runs.map(watchRunChoice);
484
+ const selected = await ctx.ui.select("Watch run:", choices);
485
+ if (!selected) return void 0;
486
+ return runs.find((_run, index) => choices[index] === selected);
487
+ }
488
+ async function openRunInspector(ctx, options) {
489
+ if (!ctx.hasUI) {
490
+ ctx.ui.notify("avenor-watch requires TUI mode", "warning");
491
+ return;
492
+ }
493
+ const selector = options.initialRunId?.trim();
494
+ let tracked;
495
+ let runId;
496
+ if (selector) {
497
+ const matches = matchingWatchRuns(options.trackedRuns, selector);
498
+ tracked = matches.length === 1 ? matches[0] : matches.length > 1 ? await selectWatchRun(ctx, matches) : void 0;
499
+ if (matches.length > 1 && !tracked) return;
500
+ runId = tracked?.runId ?? selector;
501
+ } else {
502
+ if (options.trackedRuns.length === 0) {
503
+ ctx.ui.notify("No tracked runs", "warning");
504
+ return;
505
+ }
506
+ tracked = await selectWatchRun(ctx, options.trackedRuns);
507
+ if (!tracked) return;
508
+ runId = tracked.runId;
509
+ }
510
+ await ctx.ui.custom((tui, theme, keybindings, done) => new RunInspectorOverlay(tui, theme, keybindings, options.deps, tracked ?? { runId }, () => done(), options.onTrackRun), {
511
+ overlay: true,
512
+ overlayOptions: {
513
+ anchor: "center",
514
+ width: "95%",
515
+ maxHeight: "95%",
516
+ minWidth: 80
517
+ }
518
+ });
519
+ }
54
520
  //#endregion
55
521
  //#region src/types.ts
522
+ function findLiveStatusForTrackedRun(run, liveStatuses) {
523
+ const knownRunIds = new Set([run.runId, run.lastStatus?.run_id].filter((id) => !!id));
524
+ let runtimeMatch;
525
+ for (const status of liveStatuses) {
526
+ if (knownRunIds.has(status.run_id)) return status;
527
+ if (!run.supervisorId && run.runtimeId === status.runtime_id) runtimeMatch = status;
528
+ }
529
+ return runtimeMatch;
530
+ }
56
531
  const TERMINAL_STATUSES = new Set([
57
532
  "done",
58
533
  "failed",
@@ -80,17 +555,48 @@ function formatRunLine(entry) {
80
555
  //#endregion
81
556
  //#region src/index.ts
82
557
  const POLL_INTERVAL_MS = 3e3;
558
+ const INSPECT_LIMIT = 128;
559
+ const COMPLETION_OUTPUT_CHARS = 600;
560
+ const INSPECT_JSON_STRING_CHARS = 600;
561
+ const INSPECT_JSON_ARRAY_ITEMS = 24;
562
+ const INSPECT_JSON_OBJECT_KEYS = 24;
563
+ const INSPECT_JSON_DEPTH = 8;
564
+ const defaultDeps = {
565
+ spawnTool,
566
+ statusTool,
567
+ eventsTool,
568
+ answerPermissionTool,
569
+ followUpTool,
570
+ inspectTool,
571
+ shutdownTool,
572
+ observeRun,
573
+ dial,
574
+ Supervisor
575
+ };
83
576
  function statusSupervisorId(requestedSupervisorId, spawnedSupervisorId) {
84
577
  return requestedSupervisorId ? spawnedSupervisorId : void 0;
85
578
  }
86
579
  function sleep(ms) {
87
580
  return new Promise((resolve) => setTimeout(resolve, ms));
88
581
  }
89
- function buildCompletionText(run, result) {
582
+ function normalizeMultilinePreview(text, limit = COMPLETION_OUTPUT_CHARS) {
583
+ if (!text) return void 0;
584
+ const clean = sanitizeText(text).trim();
585
+ if (!clean) return void 0;
586
+ return clean.length <= limit ? clean : `${clean.slice(0, Math.max(0, limit - 1))}…`;
587
+ }
588
+ function quotedLines(text) {
589
+ const preview = normalizeMultilinePreview(text);
590
+ if (!preview) return [];
591
+ return preview.split("\n").slice(0, 8).map((line) => `> ${line}`);
592
+ }
593
+ function buildCompletionText(run, result, finalOutput) {
90
594
  const lines = [`Sub-agent "${run.label}" finished with status **${result.status}**.`];
595
+ const output = normalizeMultilinePreview(finalOutput ?? result.final_output);
596
+ if (output) lines.push("", "Final output:", ...quotedLines(output));
91
597
  if (result.stop_reason && result.stop_reason.toUpperCase() !== result.status.toUpperCase()) lines.push(`Stop reason: ${result.stop_reason}`);
92
598
  if (result.session_id) lines.push(`Session: \`${result.session_id}\``);
93
- lines.push(`\nCall \`avenor_events\` with \`run_id: "${run.runId}"\` to review the output, or \`avenor_follow_up\` to iterate.`);
599
+ lines.push("", `Call \`avenor_inspect\` with \`run_id: "${run.runId}"\` for the bounded transcript/snapshot, or \`avenor_follow_up\` to iterate.`);
94
600
  return lines.join("\n");
95
601
  }
96
602
  function buildWaitingText(run, status) {
@@ -98,342 +604,598 @@ function buildWaitingText(run, status) {
98
604
  const lines = [`Sub-agent "${run.label}" (${run.agent}) is waiting for input.`];
99
605
  if (perm) lines.push(`Permission request: ${perm.description}`, `To answer: call \`avenor_answer_permission\` with run_id "${run.runId}", option_id "allow_once" | "allow_always" | "deny"`);
100
606
  else if (status.phase_label) lines.push(`Question: ${status.phase_label}`);
101
- if (!perm) lines.push(`Use \`avenor_follow_up\` with run_id "${run.runId}" to respond, or \`avenor_status\` to re-check.`);
607
+ if (!perm) lines.push(`Use \`avenor_follow_up\` with run_id "${run.runId}" to respond, or \`avenor_inspect\` to review context first.`);
102
608
  return lines.join("\n");
103
609
  }
104
- function summarizeEvent(evt) {
105
- const type = String(evt.type ?? evt.event ?? "");
106
- if (type === "tool.call" || type === "tool.use") return `called ${String(evt.tool ?? evt.name ?? type)}`;
107
- if (type === "agent.message_chunk") return "thinking…";
108
- if (type.startsWith("permission.")) return `permission requested`;
109
- return type;
110
- }
111
- async function src_default(pi) {
112
- const trackedRuns = /* @__PURE__ */ new Map();
113
- let pollingActive = false;
114
- let pollingGeneration = 0;
115
- let pollInFlight = null;
116
- let sessionCtx = null;
117
- async function pollRuns() {
118
- const liveMap = /* @__PURE__ */ new Map();
119
- try {
120
- const allLive = await statusTool({});
121
- const liveList = Array.isArray(allLive) ? allLive : [allLive];
122
- for (const result of liveList) liveMap.set(result.run_id, result);
123
- } catch (err) {
124
- console.error("avenor pollRuns: failed to list singleton runs", err);
610
+ function isTerminalStatus(status) {
611
+ return !!status && TERMINAL_STATUSES.has(status);
612
+ }
613
+ function renderStatusLines(entries) {
614
+ const active = entries.filter((e) => !isTerminalStatus(e.status));
615
+ if (active.length === 0) return [];
616
+ return active.map((e) => {
617
+ const emoji = statusEmoji(e.status);
618
+ const perm = e.pendingPermission ? " 🔒" : "";
619
+ const phase = e.phaseLabel ? ` [${e.phaseLabel}]` : "";
620
+ return `${emoji} ${e.label} — ${e.status}${phase}${perm}`;
621
+ });
622
+ }
623
+ function compactWhitespace(text, limit = 100) {
624
+ const clipped = clipText(text, limit);
625
+ return clipped ? clipped.replace(/\s+/g, " ").trim() : void 0;
626
+ }
627
+ function snapshotProgressText(label, snapshot) {
628
+ if (snapshot.pending_permission) return `[blocked] ${label} — permission: ${snapshot.pending_permission.description}`;
629
+ const phase = snapshot.phase_label ? ` (${snapshot.phase_label})` : snapshot.phase ? ` (${snapshot.phase})` : "";
630
+ const liveTool = snapshot.live_tools[snapshot.live_tools.length - 1];
631
+ if (liveTool) {
632
+ const toolName = liveTool.title ?? liveTool.kind ?? "tool";
633
+ const preview = compactWhitespace(liveTool.preview, 80);
634
+ return `${label}${phase} — ${toolName}${preview ? `: ${preview}` : ""}`;
635
+ }
636
+ const last = snapshot.transcript[snapshot.transcript.length - 1];
637
+ const transcriptWidth = 160;
638
+ const rendered = last ? compactWhitespace(renderTranscriptLines({
639
+ ...snapshot,
640
+ transcript: [last],
641
+ live_tools: [],
642
+ pending_permission: void 0
643
+ }, transcriptWidth, {
644
+ fg: (_color, text) => text,
645
+ bg: (_color, text) => text,
646
+ bold: (text) => text,
647
+ italic: (text) => text
648
+ })[0], 80) : void 0;
649
+ return `${label}${phase}${rendered ? ` — ${rendered}` : ""}`;
650
+ }
651
+ function boundJsonValue(value, depth = 0) {
652
+ if (depth >= INSPECT_JSON_DEPTH) return "[truncated]";
653
+ if (typeof value === "string") {
654
+ const clean = sanitizeText(value);
655
+ return clean.length <= INSPECT_JSON_STRING_CHARS ? clean : `${clean.slice(0, INSPECT_JSON_STRING_CHARS - 1)}…`;
656
+ }
657
+ if (Array.isArray(value)) return value.slice(Math.max(0, value.length - INSPECT_JSON_ARRAY_ITEMS)).map((item) => boundJsonValue(item, depth + 1));
658
+ if (!value || typeof value !== "object") return value;
659
+ const result = {};
660
+ for (const [key, item] of Object.entries(value).slice(0, INSPECT_JSON_OBJECT_KEYS)) result[key] = boundJsonValue(item, depth + 1);
661
+ return result;
662
+ }
663
+ function buildInspectPayload(result) {
664
+ const finalOutput = finalOutputPreview(result.status, result.snapshot);
665
+ return boundJsonValue({
666
+ run_id: result.run_id,
667
+ label: result.label,
668
+ status: result.status,
669
+ snapshot: result.snapshot,
670
+ transcript: result.transcript,
671
+ tools: result.tools,
672
+ live_tools: result.live_tools,
673
+ permissions: result.permissions,
674
+ pending_permission: result.pending_permission,
675
+ final_output: finalOutput
676
+ });
677
+ }
678
+ function toWatchRunRef(run) {
679
+ return {
680
+ runId: run.runId,
681
+ label: run.label,
682
+ agent: run.agent,
683
+ runtimeId: run.runtimeId,
684
+ supervisorId: run.supervisorId
685
+ };
686
+ }
687
+ function createExtension(deps = defaultDeps) {
688
+ return async function(pi) {
689
+ const trackedRuns = /* @__PURE__ */ new Map();
690
+ let pollingActive = false;
691
+ let pollingGeneration = 0;
692
+ let pollInFlight = null;
693
+ let sessionCtx = null;
694
+ function upsertTrackedRun(run, status, blocking = false) {
695
+ const current = trackedRuns.get(run.runId);
696
+ trackedRuns.set(run.runId, {
697
+ runId: run.runId,
698
+ agent: run.agent ?? status?.agent ?? current?.agent ?? "unknown",
699
+ label: run.label ?? status?.label ?? current?.label ?? run.runId,
700
+ supervisorId: run.supervisorId ?? current?.supervisorId,
701
+ runtimeId: run.runtimeId ?? status?.runtime_id ?? current?.runtimeId,
702
+ startTime: current?.startTime ?? Date.now(),
703
+ lastStatus: status ?? current?.lastStatus,
704
+ blocking: current?.blocking ?? blocking,
705
+ permissionNotified: current?.permissionNotified
706
+ });
125
707
  }
126
- const entries = [];
127
- for (const run of trackedRuns.values()) {
128
- const live = liveMap.get(run.runId);
129
- if (live) {
130
- liveMap.delete(live.run_id);
131
- run.lastStatus = live;
132
- entries.push({
708
+ async function resolveFinalOutput(run, status) {
709
+ const preview = finalOutputPreview(status);
710
+ if (preview) return preview;
711
+ try {
712
+ const inspected = await deps.inspectTool({
133
713
  runId: run.runId,
134
- label: run.label,
135
- status: live.status,
136
- phase: live.phase,
137
- phaseLabel: live.phase_label,
138
- agent: run.agent,
139
- pendingPermission: !!live.pending_permission,
140
- permissionDescription: live.pending_permission?.description
714
+ supervisorId: run.supervisorId,
715
+ limit: INSPECT_LIMIT
141
716
  });
142
- continue;
717
+ return finalOutputPreview(inspected.status, inspected.snapshot);
718
+ } catch {
719
+ return;
143
720
  }
721
+ }
722
+ async function pollRuns() {
723
+ const liveMap = /* @__PURE__ */ new Map();
144
724
  try {
145
- const raw = await statusTool({
146
- runId: run.runId,
147
- supervisorId: run.supervisorId
148
- });
149
- const result = Array.isArray(raw) ? raw[0] : raw;
150
- if (!result) throw new Error("status response was empty");
151
- run.lastStatus = result;
152
- entries.push({
153
- runId: run.runId,
154
- label: run.label,
155
- status: result.status,
156
- phase: result.phase,
157
- phaseLabel: result.phase_label,
158
- agent: run.agent,
159
- pendingPermission: !!result.pending_permission,
160
- permissionDescription: result.pending_permission?.description
161
- });
725
+ const allLive = await deps.statusTool({});
726
+ const liveList = Array.isArray(allLive) ? allLive : [allLive];
727
+ for (const result of liveList) liveMap.set(result.run_id, result);
162
728
  } catch (err) {
163
- console.error(`avenor pollRuns: statusTool failed for ${run.runId}`, err);
164
- const previous = run.lastStatus;
165
- entries.push({
166
- runId: run.runId,
167
- label: run.label,
168
- status: previous?.status ?? "unavailable",
169
- phase: previous?.phase,
170
- phaseLabel: previous?.phase_label,
171
- agent: run.agent,
172
- pendingPermission: !!previous?.pending_permission,
173
- permissionDescription: previous?.pending_permission?.description
174
- });
729
+ console.error("avenor pollRuns: failed to list singleton runs", err);
730
+ }
731
+ const entries = [];
732
+ for (const run of trackedRuns.values()) {
733
+ const live = findLiveStatusForTrackedRun(run, liveMap.values());
734
+ if (live) {
735
+ liveMap.delete(live.run_id);
736
+ run.lastStatus = live;
737
+ entries.push({
738
+ runId: run.runId,
739
+ label: run.label,
740
+ status: live.status,
741
+ phase: live.phase,
742
+ phaseLabel: live.phase_label,
743
+ agent: run.agent,
744
+ pendingPermission: !!live.pending_permission,
745
+ permissionDescription: live.pending_permission?.description
746
+ });
747
+ continue;
748
+ }
749
+ try {
750
+ const raw = await deps.statusTool({
751
+ runId: run.runId,
752
+ supervisorId: run.supervisorId
753
+ });
754
+ const result = Array.isArray(raw) ? raw[0] : raw;
755
+ if (!result) throw new Error("status response was empty");
756
+ run.lastStatus = result;
757
+ entries.push({
758
+ runId: run.runId,
759
+ label: run.label,
760
+ status: result.status,
761
+ phase: result.phase,
762
+ phaseLabel: result.phase_label,
763
+ agent: run.agent,
764
+ pendingPermission: !!result.pending_permission,
765
+ permissionDescription: result.pending_permission?.description
766
+ });
767
+ } catch (err) {
768
+ console.error(`avenor pollRuns: statusTool failed for ${run.runId}`, err);
769
+ const previous = run.lastStatus;
770
+ entries.push({
771
+ runId: run.runId,
772
+ label: run.label,
773
+ status: previous?.status ?? "unavailable",
774
+ phase: previous?.phase,
775
+ phaseLabel: previous?.phase_label,
776
+ agent: run.agent,
777
+ pendingPermission: !!previous?.pending_permission,
778
+ permissionDescription: previous?.pending_permission?.description
779
+ });
780
+ }
175
781
  }
782
+ for (const live of liveMap.values()) entries.push({
783
+ runId: live.run_id,
784
+ label: live.label,
785
+ status: live.status,
786
+ phase: live.phase,
787
+ phaseLabel: live.phase_label,
788
+ agent: live.agent ?? "unknown",
789
+ pendingPermission: !!live.pending_permission,
790
+ permissionDescription: live.pending_permission?.description
791
+ });
792
+ return entries;
176
793
  }
177
- for (const live of liveMap.values()) entries.push({
178
- runId: live.run_id,
179
- label: live.label,
180
- status: live.status,
181
- phase: live.phase,
182
- phaseLabel: live.phase_label,
183
- agent: "unknown",
184
- pendingPermission: !!live.pending_permission,
185
- permissionDescription: live.pending_permission?.description
186
- });
187
- return entries;
188
- }
189
- function renderStatusLines(entries) {
190
- const active = entries.filter((e) => !TERMINAL_STATUSES.has(e.status));
191
- if (active.length === 0) return [];
192
- return active.map((e) => {
193
- const emoji = statusEmoji(e.status);
194
- const perm = e.pendingPermission ? ` 🔒` : "";
195
- const phase = e.phaseLabel ? ` [${e.phaseLabel}]` : "";
196
- return `${emoji} ${e.label} — ${e.status}${phase}${perm}`;
197
- });
198
- }
199
- function updateWidget(entries) {
200
- if (!sessionCtx) return;
201
- const lines = renderStatusLines(entries);
202
- if (lines.length > 0) {
203
- sessionCtx.ui.setWidget("avenor-status", lines);
204
- const parts = lines.map((l) => l.slice(0, 60));
205
- sessionCtx.ui.setStatus("avenor", parts.join(" "));
206
- } else {
207
- sessionCtx.ui.setWidget("avenor-status", void 0);
208
- sessionCtx.ui.setStatus("avenor", "");
794
+ function updateWidget(entries) {
795
+ if (!sessionCtx) return;
796
+ const lines = renderStatusLines(entries);
797
+ if (lines.length > 0) {
798
+ sessionCtx.ui.setWidget("avenor-status", lines);
799
+ sessionCtx.ui.setStatus("avenor", lines.map((line) => line.slice(0, 60)).join(" "));
800
+ } else {
801
+ sessionCtx.ui.setWidget("avenor-status", void 0);
802
+ sessionCtx.ui.setStatus("avenor", "");
803
+ }
209
804
  }
210
- }
211
- function startPolling() {
212
- if (pollingActive) return;
213
- pollingActive = true;
214
- const generation = ++pollingGeneration;
215
- const tick = async () => {
216
- if (!pollingActive || generation !== pollingGeneration) return;
217
- const prevStatuses = /* @__PURE__ */ new Map();
218
- for (const [id, run] of trackedRuns) prevStatuses.set(id, run.lastStatus?.status);
219
- const currentPoll = pollRuns();
220
- pollInFlight = currentPoll;
221
- const entries = await currentPoll.finally(() => {
222
- if (pollInFlight === currentPoll) pollInFlight = null;
223
- });
224
- if (!pollingActive || generation !== pollingGeneration) return;
225
- updateWidget(entries);
226
- for (const entry of entries) {
227
- const run = trackedRuns.get(entry.runId);
228
- if (!run) continue;
229
- if (TERMINAL_STATUSES.has(entry.status)) {
230
- const prevStatus = prevStatuses.get(entry.runId);
231
- if (!prevStatus || !TERMINAL_STATUSES.has(prevStatus)) {
232
- sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
233
- if (!run.blocking) try {
234
- pi.sendUserMessage(buildCompletionText({
805
+ function startPolling() {
806
+ if (pollingActive) return;
807
+ pollingActive = true;
808
+ const generation = ++pollingGeneration;
809
+ const tick = async () => {
810
+ if (!pollingActive || generation !== pollingGeneration) return;
811
+ const prevStatuses = /* @__PURE__ */ new Map();
812
+ for (const [id, run] of trackedRuns) prevStatuses.set(id, run.lastStatus?.status);
813
+ const currentPoll = pollRuns();
814
+ pollInFlight = currentPoll;
815
+ const entries = await currentPoll.finally(() => {
816
+ if (pollInFlight === currentPoll) pollInFlight = null;
817
+ });
818
+ if (!pollingActive || generation !== pollingGeneration) return;
819
+ updateWidget(entries);
820
+ for (const entry of entries) {
821
+ const run = trackedRuns.get(entry.runId);
822
+ if (!run) continue;
823
+ if (isTerminalStatus(entry.status)) {
824
+ const prevStatus = prevStatuses.get(entry.runId);
825
+ if (!prevStatus || !isTerminalStatus(prevStatus)) {
826
+ sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
827
+ if (!run.blocking && run.lastStatus) {
828
+ const finalOutput = await resolveFinalOutput(run, run.lastStatus);
829
+ try {
830
+ pi.sendUserMessage(buildCompletionText({
831
+ runId: run.runId,
832
+ label: run.label
833
+ }, run.lastStatus, finalOutput), { deliverAs: "followUp" });
834
+ } catch (err) {
835
+ console.error("avenor tick: sendUserMessage failed", err);
836
+ }
837
+ }
838
+ }
839
+ trackedRuns.delete(entry.runId);
840
+ } else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
841
+ run.permissionNotified = true;
842
+ if (run.lastStatus) try {
843
+ pi.sendUserMessage(buildWaitingText({
235
844
  runId: run.runId,
236
- label: run.label
845
+ label: run.label,
846
+ agent: run.agent
237
847
  }, run.lastStatus), { deliverAs: "followUp" });
238
848
  } catch (err) {
239
- console.error("avenor tick: sendUserMessage failed", err);
849
+ console.error("avenor tick: sendUserMessage for permission failed", err);
240
850
  }
241
- }
242
- trackedRuns.delete(entry.runId);
243
- } else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
244
- run.permissionNotified = true;
245
- try {
246
- pi.sendUserMessage(buildWaitingText({
247
- runId: run.runId,
248
- label: run.label,
249
- agent: run.agent
250
- }, run.lastStatus), { deliverAs: "followUp" });
251
- } catch (err) {
252
- console.error("avenor tick: sendUserMessage for permission failed", err);
253
- }
254
- } else if (!entry.pendingPermission && run.permissionNotified) run.permissionNotified = false;
255
- }
256
- if (entries.some((e) => !TERMINAL_STATUSES.has(e.status)) && pollingActive && generation === pollingGeneration) setTimeout(() => {
257
- tick().catch((err) => {
851
+ } else if (!entry.pendingPermission && run.permissionNotified) run.permissionNotified = false;
852
+ }
853
+ if (entries.some((entry) => !isTerminalStatus(entry.status)) && pollingActive && generation === pollingGeneration) setTimeout(() => {
854
+ tick().catch((err) => {
855
+ pollingActive = false;
856
+ console.error("avenor polling loop failed", err);
857
+ });
858
+ }, POLL_INTERVAL_MS);
859
+ else {
258
860
  pollingActive = false;
259
- console.error("avenor polling loop failed", err);
260
- });
261
- }, POLL_INTERVAL_MS);
262
- else {
861
+ sessionCtx?.ui.setStatus("avenor", "");
862
+ sessionCtx?.ui.setWidget("avenor-status", void 0);
863
+ }
864
+ };
865
+ tick().catch((err) => {
263
866
  pollingActive = false;
264
- sessionCtx?.ui.setStatus("avenor", "");
265
- sessionCtx?.ui.setWidget("avenor-status", void 0);
266
- }
267
- };
268
- tick().catch((err) => {
867
+ console.error("avenor polling loop failed", err);
868
+ });
869
+ }
870
+ async function stopPolling() {
269
871
  pollingActive = false;
270
- console.error("avenor polling loop failed", err);
271
- });
272
- }
273
- async function stopPolling() {
274
- pollingActive = false;
275
- pollingGeneration++;
276
- await pollInFlight?.catch(() => {});
277
- sessionCtx?.ui.setStatus("avenor", "");
278
- sessionCtx?.ui.setWidget("avenor-status", void 0);
279
- }
280
- pi.on("session_start", async (_event, ctx) => {
281
- sessionCtx = ctx;
282
- });
283
- pi.on("session_shutdown", async (_event, _ctx) => {
284
- await stopPolling();
285
- sessionCtx = null;
286
- });
287
- pi.on("before_agent_start", async (event, _ctx) => {
288
- if (trackedRuns.size === 0) return;
289
- const active = (await pollRuns()).filter((e) => !TERMINAL_STATUSES.has(e.status));
290
- if (active.length === 0) return;
291
- return { message: {
292
- customType: "avenor-active-runs",
293
- content: `Active sub-agents:\n${active.map((e) => {
294
- const perm = e.pendingPermission ? ` [blocked: ${e.permissionDescription ?? "permission"}]` : "";
295
- const phase = e.phaseLabel ? ` (${e.phaseLabel})` : "";
296
- return ` • ${e.label} (${e.agent}): ${e.status}${phase}${perm}`;
297
- }).join("\n")}`,
298
- display: true
299
- } };
300
- });
301
- pi.registerMessageRenderer("avenor-active-runs", (message, options, theme) => {
302
- const lines = message.content.split("\n");
303
- let text = "";
304
- for (let i = 0; i < lines.length; i++) {
305
- if (i === 0) text += theme.fg("info", theme.bold(lines[i]));
306
- else text += theme.fg("muted", lines[i]);
307
- if (i < lines.length - 1) text += "\n";
872
+ pollingGeneration++;
873
+ await pollInFlight?.catch(() => {});
874
+ sessionCtx?.ui.setStatus("avenor", "");
875
+ sessionCtx?.ui.setWidget("avenor-status", void 0);
308
876
  }
309
- return new Text(text, 0, 0);
310
- });
311
- pi.registerTool({
312
- name: "avenor_spawn",
313
- label: "Avenor Spawn",
314
- description: "Dispatch an agent run via avenor. Blocks by default, showing live progress. Set wait=false for fire-and-forget.",
315
- promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, events, follow-up, shutdown)",
316
- promptGuidelines: [
317
- "Use avenor_spawn to delegate well-defined tasks to sub-agents.",
318
- "Use avenor_status to check progress of fire-and-forget runs.",
319
- "Use avenor_events to inspect detailed output from a run.",
320
- "Use avenor_follow_up to iterate on a completed run.",
321
- "Use avenor_shutdown when done delegating work."
322
- ],
323
- parameters: Type.Object({
324
- agent: Type.String({ description: "Agent name (required, no default)" }),
325
- prompt: Type.Optional(Type.String({ description: "Prompt text" })),
326
- prompt_file: Type.Optional(Type.String({ description: "Path to prompt file" })),
327
- dir: Type.Optional(Type.String({ description: "Working directory (defaults to session directory)" })),
328
- label: Type.Optional(Type.String({ description: "Human-readable label for the run" })),
329
- timeout: Type.Optional(Type.String({ description: "Timeout duration (e.g. 3600s)" })),
330
- model: Type.Optional(Type.String({ description: "Model override" })),
331
- backend: Type.Optional(Type.String({ description: "Backend override" })),
332
- server_url: Type.Optional(Type.String({ description: "Backend server URL" })),
333
- supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
334
- wait: Type.Optional(Type.Boolean({ description: "Block until complete (default true)" }))
335
- }),
336
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
337
- if (signal?.aborted) return { content: [{
338
- type: "text",
339
- text: "Cancelled"
340
- }] };
341
- const wait = params.wait ?? true;
342
- const dir = params.dir ?? ctx.cwd;
343
- const label = params.label ?? `${params.agent}-${Date.now()}`;
344
- const result = await spawnTool({
345
- agent: params.agent,
346
- prompt: params.prompt,
347
- promptFile: params.prompt_file,
348
- label,
349
- dir,
350
- timeout: params.timeout,
351
- model: params.model,
352
- backend: params.backend,
353
- serverUrl: params.server_url,
354
- supervisorId: params.supervisor_id
355
- });
356
- const supervisorId = statusSupervisorId(params.supervisor_id, result.supervisor_id);
357
- trackedRuns.set(result.run_id, {
358
- runId: result.run_id,
359
- agent: params.agent,
360
- label,
361
- supervisorId,
362
- runtimeId: result.runtime_id,
363
- startTime: Date.now(),
364
- blocking: wait
365
- });
366
- startPolling();
367
- if (!wait) return {
368
- content: [{
369
- type: "text",
370
- text: `Dispatched "${label}" (run_id: ${result.run_id}). Call avenor_status to check progress.`
371
- }],
372
- details: {
373
- run_id: result.run_id,
374
- label
877
+ function createWatchDeps() {
878
+ async function resolveSupervisorSocket(run) {
879
+ if (run.supervisorId) return run.supervisorId;
880
+ return (await deps.Supervisor.get()).supervisorId;
881
+ }
882
+ async function withClient(run, work) {
883
+ const socket = await resolveSupervisorSocket(run);
884
+ const client = await deps.dial(socket);
885
+ try {
886
+ return await work(client);
887
+ } finally {
888
+ client.close();
375
889
  }
376
- };
377
- onUpdate?.({
378
- content: [{
379
- type: "text",
380
- text: `Starting ${label}…`
381
- }],
382
- details: {
383
- status: "running",
384
- run_id: result.run_id
890
+ }
891
+ return {
892
+ inspectRun(run) {
893
+ return deps.inspectTool({
894
+ runId: run.runId,
895
+ supervisorId: run.supervisorId,
896
+ limit: INSPECT_LIMIT
897
+ });
898
+ },
899
+ async observeRun(run, afterSeq, initialSnapshot) {
900
+ const runtimeId = run.runtimeId;
901
+ if (!runtimeId) return null;
902
+ const socket = await resolveSupervisorSocket(run);
903
+ const client = await deps.dial(socket);
904
+ return deps.observeRun(client, {
905
+ runtime_id: runtimeId,
906
+ replay: true,
907
+ after_seq: afterSeq,
908
+ initial_snapshot: initialSnapshot,
909
+ close_client: true
910
+ });
911
+ },
912
+ cancelRun(run, runtimeId) {
913
+ return withClient(run, async (client) => {
914
+ await client.cancel(runtimeId);
915
+ });
916
+ },
917
+ promptRun(run, runtimeId, text) {
918
+ return withClient(run, async (client) => {
919
+ await client.prompt(runtimeId, text);
920
+ });
921
+ },
922
+ followUpRun(run, text) {
923
+ return deps.followUpTool({
924
+ runId: run.runId,
925
+ message: text,
926
+ supervisorId: run.supervisorId
927
+ });
385
928
  }
929
+ };
930
+ }
931
+ async function followUpTrackedRun(run, text) {
932
+ const result = await deps.followUpTool({
933
+ runId: run.runId,
934
+ message: text,
935
+ supervisorId: run.supervisorId
936
+ });
937
+ try {
938
+ const rawStatus = await deps.statusTool({
939
+ runId: result.run_id,
940
+ supervisorId: run.supervisorId
941
+ });
942
+ const status = Array.isArray(rawStatus) ? rawStatus[0] : rawStatus;
943
+ upsertTrackedRun({
944
+ runId: result.run_id,
945
+ label: result.label,
946
+ agent: status?.agent ?? run.agent,
947
+ runtimeId: status?.runtime_id,
948
+ supervisorId: run.supervisorId
949
+ }, status, false);
950
+ startPolling();
951
+ } catch {
952
+ upsertTrackedRun({
953
+ runId: result.run_id,
954
+ label: result.label,
955
+ agent: run.agent,
956
+ supervisorId: run.supervisorId
957
+ }, void 0, false);
958
+ startPolling();
959
+ }
960
+ return result;
961
+ }
962
+ async function waitForRunWithObserver(run, signal, onUpdate) {
963
+ let observer = null;
964
+ let unsubscribe;
965
+ let lastUpdate = "";
966
+ let abortListener;
967
+ const emit = (snapshot) => {
968
+ const text = snapshotProgressText(run.label ?? run.runId, snapshot);
969
+ if (text === lastUpdate) return;
970
+ lastUpdate = text;
971
+ onUpdate?.({
972
+ content: [{
973
+ type: "text",
974
+ text
975
+ }],
976
+ details: {
977
+ status: snapshot.pending_permission ? "waiting" : snapshot.ended ? "done" : "running",
978
+ run_id: run.runId
979
+ }
980
+ });
981
+ };
982
+ const waitPromise = new Promise((resolve) => {
983
+ const complete = () => resolve("settled");
984
+ (async () => {
985
+ try {
986
+ observer = await createWatchDeps().observeRun(run);
987
+ if (!observer) {
988
+ complete();
989
+ return;
990
+ }
991
+ const maybeComplete = (snapshot) => {
992
+ emit(snapshot);
993
+ if (snapshot.pending_permission || snapshot.ended) complete();
994
+ };
995
+ unsubscribe = observer.subscribe((snapshot) => {
996
+ maybeComplete(snapshot);
997
+ });
998
+ const current = observer.snapshot();
999
+ if (current.last_event || current.transcript.length > 0 || current.pending_permission || current.ended) maybeComplete(current);
1000
+ } catch {
1001
+ complete();
1002
+ }
1003
+ })();
386
1004
  });
1005
+ const abortPromise = signal ? new Promise((resolve) => {
1006
+ if (signal.aborted) {
1007
+ resolve("aborted");
1008
+ return;
1009
+ }
1010
+ const onAbort = () => resolve("aborted");
1011
+ abortListener = onAbort;
1012
+ signal.addEventListener("abort", onAbort, { once: true });
1013
+ }) : void 0;
1014
+ const outcome = abortPromise ? await Promise.race([waitPromise, abortPromise]) : await waitPromise;
1015
+ if (signal && abortListener) signal.removeEventListener("abort", abortListener);
1016
+ unsubscribe?.();
1017
+ await observer?.close().catch(() => {});
1018
+ if (outcome === "aborted") return { aborted: true };
1019
+ try {
1020
+ const raw = await deps.statusTool({
1021
+ runId: run.runId,
1022
+ supervisorId: run.supervisorId
1023
+ });
1024
+ return {
1025
+ status: Array.isArray(raw) ? raw[0] : raw,
1026
+ aborted: false
1027
+ };
1028
+ } catch {
1029
+ return { aborted: false };
1030
+ }
1031
+ }
1032
+ async function waitForRunByPolling(run, signal, onUpdate) {
387
1033
  let firstPoll = true;
388
1034
  while (!signal?.aborted) {
389
1035
  if (!firstPoll) await sleep(POLL_INTERVAL_MS);
390
1036
  firstPoll = false;
391
- let raw;
392
1037
  try {
393
- raw = await statusTool({
394
- runId: result.run_id,
395
- supervisorId
1038
+ const raw = await deps.statusTool({
1039
+ runId: run.runId,
1040
+ supervisorId: run.supervisorId
1041
+ });
1042
+ const status = Array.isArray(raw) ? raw[0] : raw;
1043
+ if (!status) continue;
1044
+ const text = status.pending_permission ? `[blocked] ${run.label ?? run.runId} — permission: ${status.pending_permission.description}` : `${run.label ?? run.runId}${status.phase_label ? ` (${status.phase_label})` : ""}`;
1045
+ onUpdate?.({
1046
+ content: [{
1047
+ type: "text",
1048
+ text
1049
+ }],
1050
+ details: {
1051
+ status: status.status,
1052
+ run_id: run.runId
1053
+ }
396
1054
  });
1055
+ if (status.status === "waiting" || isTerminalStatus(status.status)) return {
1056
+ status,
1057
+ aborted: false
1058
+ };
397
1059
  } catch (err) {
398
1060
  console.error("avenor spawn poll: statusTool failed", err);
399
- continue;
400
1061
  }
401
- const status = Array.isArray(raw) ? raw[0] : raw;
402
- if (!status) continue;
403
- if (status.pending_permission) onUpdate?.({
1062
+ }
1063
+ return { aborted: true };
1064
+ }
1065
+ async function waitForRun(run, signal, onUpdate) {
1066
+ const observed = await waitForRunWithObserver(run, signal, onUpdate);
1067
+ if (observed.status || observed.aborted) return observed;
1068
+ return waitForRunByPolling(run, signal, onUpdate);
1069
+ }
1070
+ pi.on("session_start", async (_event, ctx) => {
1071
+ sessionCtx = ctx;
1072
+ });
1073
+ pi.on("session_shutdown", async () => {
1074
+ await stopPolling();
1075
+ sessionCtx = null;
1076
+ });
1077
+ pi.on("before_agent_start", async () => {
1078
+ if (trackedRuns.size === 0) return;
1079
+ const active = (await pollRuns()).filter((entry) => !isTerminalStatus(entry.status));
1080
+ if (active.length === 0) return;
1081
+ return { message: {
1082
+ customType: "avenor-active-runs",
1083
+ content: `Active sub-agents:\n${active.map((entry) => {
1084
+ const perm = entry.pendingPermission ? ` [blocked: ${entry.permissionDescription ?? "permission"}]` : "";
1085
+ const phase = entry.phaseLabel ? ` (${entry.phaseLabel})` : "";
1086
+ return ` • ${entry.label} (${entry.agent}): ${entry.status}${phase}${perm}`;
1087
+ }).join("\n")}`,
1088
+ display: true
1089
+ } };
1090
+ });
1091
+ pi.registerMessageRenderer("avenor-active-runs", (message, _options, theme) => {
1092
+ const lines = message.content.split("\n");
1093
+ let text = "";
1094
+ for (let i = 0; i < lines.length; i++) {
1095
+ text += i === 0 ? theme.fg("accent", theme.bold(lines[i] ?? "")) : theme.fg("muted", lines[i] ?? "");
1096
+ if (i < lines.length - 1) text += "\n";
1097
+ }
1098
+ return new Text(text, 0, 0);
1099
+ });
1100
+ pi.registerTool({
1101
+ name: "avenor_spawn",
1102
+ label: "Avenor Spawn",
1103
+ description: "Dispatch an agent run via avenor. Blocks by default, showing live progress. Set wait=false for fire-and-forget.",
1104
+ promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, inspect, events, follow-up, shutdown)",
1105
+ promptGuidelines: [
1106
+ "Use avenor_spawn to delegate well-defined tasks to sub-agents.",
1107
+ "Use avenor_status to check progress of fire-and-forget runs.",
1108
+ "Use avenor_inspect to review a bounded transcript, tools, and final output from a run.",
1109
+ "Use avenor_events only when raw event payloads are specifically needed.",
1110
+ "Use avenor_follow_up to iterate on a completed run.",
1111
+ "Use avenor_shutdown when done delegating work."
1112
+ ],
1113
+ parameters: Type.Object({
1114
+ agent: Type.String({ description: "Agent name (required, no default)" }),
1115
+ prompt: Type.Optional(Type.String({ description: "Prompt text" })),
1116
+ prompt_file: Type.Optional(Type.String({ description: "Path to prompt file" })),
1117
+ dir: Type.Optional(Type.String({ description: "Working directory (defaults to session directory)" })),
1118
+ label: Type.Optional(Type.String({ description: "Human-readable label for the run" })),
1119
+ timeout: Type.Optional(Type.String({ description: "Timeout duration (e.g. 3600s)" })),
1120
+ model: Type.Optional(Type.String({ description: "Model override" })),
1121
+ backend: Type.Optional(Type.String({ description: "Backend override" })),
1122
+ server_url: Type.Optional(Type.String({ description: "Backend server URL" })),
1123
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
1124
+ wait: Type.Optional(Type.Boolean({ description: "Block until complete (default true)" }))
1125
+ }),
1126
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
1127
+ if (signal?.aborted) return { content: [{
1128
+ type: "text",
1129
+ text: "Cancelled"
1130
+ }] };
1131
+ const wait = params.wait ?? true;
1132
+ const dir = params.dir ?? ctx.cwd;
1133
+ const label = params.label ?? `${params.agent}-${Date.now()}`;
1134
+ const result = await deps.spawnTool({
1135
+ agent: params.agent,
1136
+ prompt: params.prompt,
1137
+ promptFile: params.prompt_file,
1138
+ label,
1139
+ dir,
1140
+ timeout: params.timeout,
1141
+ model: params.model,
1142
+ backend: params.backend,
1143
+ serverUrl: params.server_url,
1144
+ supervisorId: params.supervisor_id
1145
+ });
1146
+ const supervisorId = statusSupervisorId(params.supervisor_id, result.supervisor_id);
1147
+ const runRef = {
1148
+ runId: result.run_id,
1149
+ agent: params.agent,
1150
+ label,
1151
+ supervisorId,
1152
+ runtimeId: result.runtime_id
1153
+ };
1154
+ upsertTrackedRun(runRef, void 0, wait);
1155
+ startPolling();
1156
+ if (!wait) return {
1157
+ content: [{
1158
+ type: "text",
1159
+ text: `Dispatched "${label}" (run_id: ${result.run_id}). Call avenor_status to check progress or avenor_inspect for a bounded transcript snapshot.`
1160
+ }],
1161
+ details: {
1162
+ run_id: result.run_id,
1163
+ label
1164
+ }
1165
+ };
1166
+ onUpdate?.({
404
1167
  content: [{
405
1168
  type: "text",
406
- text: `[blocked] ${label} — permission: ${status.pending_permission.description}`
1169
+ text: `Starting ${label}…`
407
1170
  }],
408
1171
  details: {
409
- status: "permission",
1172
+ status: "running",
410
1173
  run_id: result.run_id
411
1174
  }
412
1175
  });
413
- else {
414
- let lastAction = "";
415
- if (!params.supervisor_id) try {
416
- const eventsResult = await eventsTool({
417
- runId: result.run_id,
418
- limit: 3
419
- });
420
- const last = eventsResult.events[eventsResult.events.length - 1];
421
- if (last) lastAction = summarizeEvent(last);
422
- } catch (err) {
423
- console.error("avenor spawn poll: eventsTool failed", err);
424
- }
425
- onUpdate?.({
1176
+ const waitResult = await waitForRun(runRef, signal, onUpdate);
1177
+ if (waitResult.aborted) {
1178
+ trackedRuns.delete(result.run_id);
1179
+ return {
426
1180
  content: [{
427
1181
  type: "text",
428
- text: `${label}${status.phase_label ? ` (${status.phase_label})` : ""}${lastAction ? ` — ${lastAction}` : ""}`
1182
+ text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status or avenor_inspect to check.`
429
1183
  }],
430
- details: {
431
- status: status.status,
432
- run_id: result.run_id,
433
- lastAction
434
- }
435
- });
1184
+ details: { run_id: result.run_id }
1185
+ };
436
1186
  }
1187
+ const status = waitResult.status;
1188
+ if (!status) return {
1189
+ content: [{
1190
+ type: "text",
1191
+ text: `Sub-agent "${label}" is still running. Use avenor_status or avenor_inspect to check progress.`
1192
+ }],
1193
+ details: {
1194
+ status: "running",
1195
+ run_id: result.run_id
1196
+ }
1197
+ };
1198
+ upsertTrackedRun(runRef, status, wait);
437
1199
  if (status.status === "waiting") {
438
1200
  const tracked = trackedRuns.get(result.run_id);
439
1201
  if (tracked) tracked.permissionNotified = true;
@@ -454,7 +1216,11 @@ async function src_default(pi) {
454
1216
  }
455
1217
  };
456
1218
  }
457
- if (TERMINAL_STATUSES.has(status.status)) {
1219
+ if (isTerminalStatus(status.status)) {
1220
+ const finalOutput = await resolveFinalOutput({
1221
+ runId: result.run_id,
1222
+ supervisorId
1223
+ }, status);
458
1224
  trackedRuns.delete(result.run_id);
459
1225
  return {
460
1226
  content: [{
@@ -462,286 +1228,292 @@ async function src_default(pi) {
462
1228
  text: buildCompletionText({
463
1229
  runId: result.run_id,
464
1230
  label
465
- }, status)
1231
+ }, status, finalOutput)
466
1232
  }],
467
1233
  details: {
468
1234
  status: status.status,
469
1235
  run_id: result.run_id,
470
- session_id: status.session_id
1236
+ session_id: status.session_id,
1237
+ final_output: finalOutput
471
1238
  }
472
1239
  };
473
1240
  }
1241
+ return {
1242
+ content: [{
1243
+ type: "text",
1244
+ text: `${label} is ${status.status}. Use avenor_status or avenor_inspect to check progress.`
1245
+ }],
1246
+ details: {
1247
+ status: status.status,
1248
+ run_id: result.run_id
1249
+ }
1250
+ };
1251
+ },
1252
+ renderCall(args, theme) {
1253
+ const agent = theme.fg("toolTitle", theme.bold("avenor_spawn "));
1254
+ const agentName = theme.fg("accent", args.agent);
1255
+ const label = args.label ? theme.fg("dim", ` "${args.label}"`) : "";
1256
+ const wait = args.wait === false ? theme.fg("warning", " [async]") : "";
1257
+ return new Text(agent + agentName + label + wait, 0, 0);
1258
+ },
1259
+ renderResult(result, { isPartial }, theme) {
1260
+ if (isPartial) {
1261
+ const status = result.details?.status ?? "running";
1262
+ const emoji = statusEmoji(String(status));
1263
+ return new Text(theme.fg("accent", `${emoji} ${result.content?.[0]?.text ?? "Running…"}`), 0, 0);
1264
+ }
1265
+ const status = String(result.details?.status ?? "");
1266
+ if (status && isTerminalStatus(status)) {
1267
+ const emoji = statusEmoji(status);
1268
+ return new Text(`${theme.fg(status === "done" ? "success" : "error", `${emoji} ${status}`)}\n${(result.content?.[0]?.text ?? "").split("\n").map((line) => theme.fg("muted", ` ${line}`)).join("\n")}`, 0, 0);
1269
+ }
1270
+ return new Text((result.content?.[0]?.text ?? "").split("\n").map((line) => theme.fg("muted", line)).join("\n"), 0, 0);
474
1271
  }
475
- trackedRuns.delete(result.run_id);
476
- return {
477
- content: [{
478
- type: "text",
479
- text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status to check.`
480
- }],
481
- details: { run_id: result.run_id }
482
- };
483
- },
484
- renderCall(args, theme) {
485
- const agent = theme.fg("toolTitle", theme.bold("avenor_spawn "));
486
- const agentName = theme.fg("accent", args.agent);
487
- const label = args.label ? theme.fg("dim", ` "${args.label}"`) : "";
488
- const wait = args.wait === false ? theme.fg("warning", " [async]") : "";
489
- return new Text(agent + agentName + label + wait, 0, 0);
490
- },
491
- renderResult(result, { isPartial }, theme) {
492
- if (isPartial) {
493
- const emoji = statusEmoji(result.details?.status ?? "running");
494
- return new Text(theme.fg("info", `${emoji} ${result.content?.[0]?.text ?? "Running…"}`), 0, 0);
495
- }
496
- const status = result.details?.status;
497
- if (status && TERMINAL_STATUSES.has(status)) {
498
- const emoji = statusEmoji(status);
499
- const header = theme.fg(status === "done" ? "success" : "error", `${emoji} ${status}`);
500
- const lines = (result.content?.[0]?.text ?? "").split("\n");
501
- return new Text(header + "\n" + lines.map((l) => theme.fg("muted", ` ${l}`)).join("\n"), 0, 0);
1272
+ });
1273
+ pi.registerTool({
1274
+ name: "avenor_status",
1275
+ label: "Avenor Status",
1276
+ description: "Get status of a run or all runs. Surfaces pending permission requests.",
1277
+ parameters: Type.Object({
1278
+ run_id: Type.Optional(Type.String({ description: "Specific run ID to query" })),
1279
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
1280
+ }),
1281
+ async execute(_toolCallId, params) {
1282
+ const result = await deps.statusTool({
1283
+ runId: params.run_id,
1284
+ supervisorId: params.supervisor_id
1285
+ });
1286
+ return {
1287
+ content: [{
1288
+ type: "text",
1289
+ text: JSON.stringify(result, null, 2)
1290
+ }],
1291
+ details: result
1292
+ };
502
1293
  }
503
- return new Text((result.content?.[0]?.text ?? "").split("\n").map((l) => theme.fg("muted", l)).join("\n"), 0, 0);
504
- }
505
- });
506
- pi.registerTool({
507
- name: "avenor_status",
508
- label: "Avenor Status",
509
- description: "Get status of a run or all runs. Surfaces pending permission requests.",
510
- parameters: Type.Object({
511
- run_id: Type.Optional(Type.String({ description: "Specific run ID to query" })),
512
- supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
513
- }),
514
- async execute(_toolCallId, params) {
515
- const result = await statusTool({
516
- runId: params.run_id,
517
- supervisorId: params.supervisor_id
518
- });
519
- return {
520
- content: [{
521
- type: "text",
522
- text: JSON.stringify(result, null, 2)
523
- }],
524
- details: result
525
- };
526
- }
527
- });
528
- pi.registerTool({
529
- name: "avenor_answer_permission",
530
- label: "Avenor Answer Permission",
531
- description: "Answer a pending permission request for a sub-agent. Pass option_id from avenor_status pending_permission.options.",
532
- parameters: Type.Object({
533
- run_id: Type.String({ description: "Run ID with the pending permission" }),
534
- option_id: Type.String({ description: "allow_once | allow_always | deny" }),
535
- request_id: Type.Optional(Type.String({ description: "Permission request ID (auto-discovered if omitted)" })),
536
- supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
537
- }),
538
- async execute(_toolCallId, params) {
539
- const result = await answerPermissionTool({
540
- runId: params.run_id,
541
- optionId: params.option_id,
542
- requestId: params.request_id,
543
- supervisorId: params.supervisor_id
544
- });
545
- return {
546
- content: [{
547
- type: "text",
548
- text: JSON.stringify(result, null, 2)
549
- }],
550
- details: result
551
- };
552
- }
553
- });
554
- pi.registerTool({
555
- name: "avenor_follow_up",
556
- label: "Avenor Follow Up",
557
- description: "Resume a completed run with a follow-up message.",
558
- parameters: Type.Object({
559
- run_id: Type.String({ description: "Completed run ID to resume" }),
560
- message: Type.String({ description: "Follow-up message text" }),
561
- label: Type.Optional(Type.String({ description: "Override label (defaults to <original>-followup)" })),
562
- supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
563
- }),
564
- async execute(_toolCallId, params) {
565
- const result = await followUpTool({
566
- runId: params.run_id,
567
- message: params.message,
568
- label: params.label,
569
- supervisorId: params.supervisor_id
570
- });
571
- return {
572
- content: [{
573
- type: "text",
574
- text: JSON.stringify(result, null, 2)
575
- }],
576
- details: result
577
- };
578
- }
579
- });
580
- pi.registerTool({
581
- name: "avenor_events",
582
- label: "Avenor Events",
583
- description: "Read events from a run. Filter by type. Returns last N events.",
584
- parameters: Type.Object({
585
- run_id: Type.String({ description: "Run ID to read events from" }),
586
- types: Type.Optional(Type.Array(Type.String(), { description: "Filter by event types" })),
587
- limit: Type.Optional(Type.Number({ description: "Max events to return (default 50)" })),
588
- supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
589
- }),
590
- async execute(_toolCallId, params) {
591
- const result = await eventsTool({
592
- runId: params.run_id,
593
- types: params.types,
594
- limit: params.limit,
595
- supervisorId: params.supervisor_id
596
- });
597
- return {
598
- content: [{
599
- type: "text",
600
- text: JSON.stringify(result, null, 2)
601
- }],
602
- details: result
603
- };
604
- }
605
- });
606
- pi.registerTool({
607
- name: "avenor_shutdown",
608
- label: "Avenor Shutdown",
609
- description: "Shut down the avenor supervisor and clean up temp files.",
610
- parameters: Type.Object({
611
- supervisor_id: Type.Optional(Type.String({ description: "Supervisor to shut down (defaults to singleton)" })),
612
- force: Type.Optional(Type.Boolean({ description: "Force shutdown instead of graceful" }))
613
- }),
614
- async execute(_toolCallId, params) {
615
- await stopPolling();
616
- trackedRuns.clear();
617
- const result = await shutdownTool({
618
- supervisorId: params.supervisor_id,
619
- force: params.force
620
- });
621
- return {
622
- content: [{
623
- type: "text",
624
- text: JSON.stringify(result, null, 2)
625
- }],
626
- details: result
627
- };
628
- }
629
- });
630
- pi.registerCommand("avenor-status", {
631
- description: "Show status of all avenor runs",
632
- handler: async (_args, ctx) => {
633
- const entries = await pollRuns();
634
- if (entries.length === 0) {
635
- ctx.ui.notify("No active avenor runs", "info");
636
- return;
1294
+ });
1295
+ pi.registerTool({
1296
+ name: "avenor_inspect",
1297
+ label: "Avenor Inspect",
1298
+ description: "Inspect a run with bounded snapshot, transcript, tool, permission, and final output details.",
1299
+ parameters: Type.Object({
1300
+ run_id: Type.String({ description: "Run ID to inspect" }),
1301
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
1302
+ limit: Type.Optional(Type.Number({ description: "Replay event limit (default 128)" })),
1303
+ after_seq: Type.Optional(Type.Number({ description: "Only include events after this sequence number" }))
1304
+ }),
1305
+ async execute(_toolCallId, params) {
1306
+ const payload = buildInspectPayload(await deps.inspectTool({
1307
+ runId: params.run_id,
1308
+ supervisorId: params.supervisor_id,
1309
+ limit: params.limit,
1310
+ after_seq: params.after_seq
1311
+ }));
1312
+ return {
1313
+ content: [{
1314
+ type: "text",
1315
+ text: JSON.stringify(payload, null, 2)
1316
+ }],
1317
+ details: payload
1318
+ };
637
1319
  }
638
- const lines = entries.map((e) => formatRunLine(e));
639
- ctx.ui.setWidget("avenor-status-detail", lines);
640
- }
641
- });
642
- pi.registerCommand("avenor-watch", {
643
- description: "Open a live event feed for a run",
644
- getArgumentCompletions: (prefix) => {
645
- const items = Array.from(trackedRuns.values()).filter((r) => r.label.startsWith(prefix) || r.runId.startsWith(prefix)).map((r) => ({
646
- value: r.runId,
647
- label: `${r.label} (${r.runId.slice(0, 8)})`
648
- }));
649
- return items.length > 0 ? items : null;
650
- },
651
- handler: async (args, ctx) => {
652
- if (!ctx.hasUI) {
653
- ctx.ui.notify("avenor-watch requires TUI mode", "warning");
654
- return;
1320
+ });
1321
+ pi.registerTool({
1322
+ name: "avenor_answer_permission",
1323
+ label: "Avenor Answer Permission",
1324
+ description: "Answer a pending permission request for a sub-agent. Pass option_id from avenor_status pending_permission.options.",
1325
+ parameters: Type.Object({
1326
+ run_id: Type.String({ description: "Run ID with the pending permission" }),
1327
+ option_id: Type.String({ description: "allow_once | allow_always | deny" }),
1328
+ request_id: Type.Optional(Type.String({ description: "Permission request ID (auto-discovered if omitted)" })),
1329
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
1330
+ }),
1331
+ async execute(_toolCallId, params) {
1332
+ const result = await deps.answerPermissionTool({
1333
+ runId: params.run_id,
1334
+ optionId: params.option_id,
1335
+ requestId: params.request_id,
1336
+ supervisorId: params.supervisor_id
1337
+ });
1338
+ return {
1339
+ content: [{
1340
+ type: "text",
1341
+ text: JSON.stringify(result, null, 2)
1342
+ }],
1343
+ details: result
1344
+ };
655
1345
  }
656
- let runId = args?.trim();
657
- if (!runId) {
658
- const runList = Array.from(trackedRuns.values());
659
- if (runList.length === 0) {
660
- ctx.ui.notify("No tracked runs", "warning");
661
- return;
1346
+ });
1347
+ pi.registerTool({
1348
+ name: "avenor_follow_up",
1349
+ label: "Avenor Follow Up",
1350
+ description: "Resume a completed run with a follow-up message.",
1351
+ parameters: Type.Object({
1352
+ run_id: Type.String({ description: "Completed run ID to resume" }),
1353
+ message: Type.String({ description: "Follow-up message text" }),
1354
+ label: Type.Optional(Type.String({ description: "Override label (defaults to <original>-followup)" })),
1355
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
1356
+ }),
1357
+ async execute(_toolCallId, params) {
1358
+ const result = await deps.followUpTool({
1359
+ runId: params.run_id,
1360
+ message: params.message,
1361
+ label: params.label,
1362
+ supervisorId: params.supervisor_id
1363
+ });
1364
+ try {
1365
+ const rawStatus = await deps.statusTool({
1366
+ runId: result.run_id,
1367
+ supervisorId: params.supervisor_id
1368
+ });
1369
+ const status = Array.isArray(rawStatus) ? rawStatus[0] : rawStatus;
1370
+ upsertTrackedRun({
1371
+ runId: result.run_id,
1372
+ label: result.label,
1373
+ agent: status?.agent,
1374
+ runtimeId: status?.runtime_id,
1375
+ supervisorId: params.supervisor_id
1376
+ }, status, false);
1377
+ } catch {
1378
+ upsertTrackedRun({
1379
+ runId: result.run_id,
1380
+ label: result.label,
1381
+ supervisorId: params.supervisor_id
1382
+ }, void 0, false);
662
1383
  }
663
- const choices = runList.map((r) => r.runId);
664
- const labels = runList.map((r) => `${r.label} (${r.runId.slice(0, 8)})`);
665
- const idx = await ctx.ui.select("Watch run:", labels);
666
- if (idx === null) return;
667
- runId = choices[idx];
1384
+ startPolling();
1385
+ return {
1386
+ content: [{
1387
+ type: "text",
1388
+ text: JSON.stringify(result, null, 2)
1389
+ }],
1390
+ details: result
1391
+ };
668
1392
  }
669
- let watchActive = true;
670
- const overlay = new EventFeedOverlay(runId);
671
- try {
672
- const { events } = await eventsTool({
673
- runId,
674
- limit: 50
1393
+ });
1394
+ pi.registerTool({
1395
+ name: "avenor_events",
1396
+ label: "Avenor Events",
1397
+ description: "Read events from a run. Filter by type. Returns last N events.",
1398
+ parameters: Type.Object({
1399
+ run_id: Type.String({ description: "Run ID to read events from" }),
1400
+ types: Type.Optional(Type.Array(Type.String(), { description: "Filter by event types" })),
1401
+ limit: Type.Optional(Type.Number({ description: "Max events to return (default 50)" })),
1402
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
1403
+ }),
1404
+ async execute(_toolCallId, params) {
1405
+ const result = await deps.eventsTool({
1406
+ runId: params.run_id,
1407
+ types: params.types,
1408
+ limit: params.limit,
1409
+ supervisorId: params.supervisor_id
675
1410
  });
676
- overlay.setEvents(events);
677
- } catch (err) {
678
- console.error("avenor watch: failed to load initial events", err);
679
- overlay.setEvents([]);
1411
+ return {
1412
+ content: [{
1413
+ type: "text",
1414
+ text: JSON.stringify(result, null, 2)
1415
+ }],
1416
+ details: result
1417
+ };
680
1418
  }
681
- const handle = ctx.ui.custom((_tui, _theme, _kb, done) => {
682
- overlay.setOnClose(() => {
683
- watchActive = false;
684
- done();
1419
+ });
1420
+ pi.registerTool({
1421
+ name: "avenor_shutdown",
1422
+ label: "Avenor Shutdown",
1423
+ description: "Shut down the avenor supervisor and clean up temp files.",
1424
+ parameters: Type.Object({
1425
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor to shut down (defaults to singleton)" })),
1426
+ force: Type.Optional(Type.Boolean({ description: "Force shutdown instead of graceful" }))
1427
+ }),
1428
+ async execute(_toolCallId, params) {
1429
+ await stopPolling();
1430
+ trackedRuns.clear();
1431
+ const result = await deps.shutdownTool({
1432
+ supervisorId: params.supervisor_id,
1433
+ force: params.force
685
1434
  });
686
- return overlay;
687
- }, {
688
- overlay: true,
689
- overlayOptions: {
690
- anchor: "right-center",
691
- width: "45%",
692
- minWidth: 60,
693
- maxHeight: "70%"
694
- },
695
- onHandle: (h) => {
696
- handle._inner = h;
1435
+ return {
1436
+ content: [{
1437
+ type: "text",
1438
+ text: JSON.stringify(result, null, 2)
1439
+ }],
1440
+ details: result
1441
+ };
1442
+ }
1443
+ });
1444
+ pi.registerCommand("avenor-status", {
1445
+ description: "Show status of all avenor runs",
1446
+ handler: async (_args, ctx) => {
1447
+ const entries = await pollRuns();
1448
+ if (entries.length === 0) {
1449
+ ctx.ui.notify("No active avenor runs", "info");
1450
+ return;
697
1451
  }
698
- });
699
- const watchTick = async () => {
700
- while (watchActive) {
701
- await sleep(POLL_INTERVAL_MS);
702
- try {
703
- const { events } = await eventsTool({
704
- runId,
705
- limit: 50
706
- });
707
- overlay.setEvents(events);
708
- handle.requestRender?.();
709
- } catch (err) {
710
- console.error("avenor watch poll: eventsTool failed", err);
711
- watchActive = false;
1452
+ ctx.ui.setWidget("avenor-status-detail", entries.map((entry) => formatRunLine(entry)));
1453
+ }
1454
+ });
1455
+ pi.registerCommand("avenor-watch", {
1456
+ description: "Open a live run inspector by run ID, label, or agent name",
1457
+ getArgumentCompletions: (prefix) => {
1458
+ const items = Array.from(trackedRuns.values()).filter((run) => run.label.startsWith(prefix) || run.agent.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => ({
1459
+ value: run.runId,
1460
+ label: `${sanitizeText(run.label)} (${sanitizeText(run.agent)}, ${sanitizeText(run.runId).slice(0, 8)})`
1461
+ }));
1462
+ return items.length > 0 ? items : null;
1463
+ },
1464
+ handler: async (args, ctx) => {
1465
+ await openRunInspector(ctx, {
1466
+ trackedRuns: Array.from(trackedRuns.values()).map(toWatchRunRef),
1467
+ initialRunId: args?.trim() || void 0,
1468
+ deps: {
1469
+ ...createWatchDeps(),
1470
+ followUpRun: (run, text) => followUpTrackedRun(run, text)
1471
+ },
1472
+ onTrackRun: (run, status) => {
1473
+ upsertTrackedRun(run, status, false);
1474
+ if (status && !isTerminalStatus(status.status)) startPolling();
712
1475
  }
713
- }
714
- };
715
- watchTick();
716
- }
717
- });
718
- pi.registerCommand("avenor-cancel", {
719
- description: "Cancel a running avenor sub-agent",
720
- getArgumentCompletions: (prefix) => {
721
- const items = Array.from(trackedRuns.values()).filter((r) => r.label.startsWith(prefix) || r.runId.startsWith(prefix)).map((r) => ({
722
- value: r.runId,
723
- label: `${r.label} (${r.runId.slice(0, 8)})`
724
- }));
725
- return items.length > 0 ? items : null;
726
- },
727
- handler: async (args, ctx) => {
728
- if (!args?.trim()) {
729
- ctx.ui.notify("Usage: /avenor-cancel <run_id>", "warning");
730
- return;
1476
+ });
731
1477
  }
732
- const runId = args.trim();
733
- const run = trackedRuns.get(runId);
734
- const label = run?.label ?? runId;
735
- if (!await ctx.ui.confirm("Cancel run", `Cancel "${label}" (${runId.slice(0, 8)})?`)) return;
736
- try {
737
- const { Supervisor } = await import("@dougbots/avenor-core");
738
- await (await Supervisor.get()).getClient().cancel(run?.lastStatus?.runtime_id ?? run?.runtimeId ?? runId);
739
- ctx.ui.notify(`Cancelled "${label}"`, "info");
740
- } catch (err) {
741
- ctx.ui.notify(`Failed to cancel: ${err instanceof Error ? err.message : String(err)}`, "error");
1478
+ });
1479
+ pi.registerCommand("avenor-cancel", {
1480
+ description: "Cancel a running avenor sub-agent",
1481
+ getArgumentCompletions: (prefix) => {
1482
+ const items = Array.from(trackedRuns.values()).filter((run) => run.label.startsWith(prefix) || run.agent.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => ({
1483
+ value: run.runId,
1484
+ label: `${sanitizeText(run.label)} (${sanitizeText(run.agent)}, ${sanitizeText(run.runId).slice(0, 8)})`
1485
+ }));
1486
+ return items.length > 0 ? items : null;
1487
+ },
1488
+ handler: async (args, ctx) => {
1489
+ if (!args?.trim()) {
1490
+ ctx.ui.notify("Usage: /avenor-cancel <run_id>", "warning");
1491
+ return;
1492
+ }
1493
+ const runId = args.trim();
1494
+ const run = trackedRuns.get(runId);
1495
+ const label = run?.label ?? runId;
1496
+ const runtimeId = run?.runtimeId ?? run?.lastStatus?.runtime_id;
1497
+ if (!runtimeId) {
1498
+ ctx.ui.notify(`No runtime id available for "${label}"`, "warning");
1499
+ return;
1500
+ }
1501
+ if (!await ctx.ui.confirm("Cancel run", `Cancel "${label}" (${runId.slice(0, 8)})?`)) return;
1502
+ try {
1503
+ await createWatchDeps().cancelRun(toWatchRunRef(run ?? {
1504
+ runId,
1505
+ agent: "unknown",
1506
+ label,
1507
+ startTime: Date.now()
1508
+ }), runtimeId);
1509
+ ctx.ui.notify(`Cancelled "${label}"`, "info");
1510
+ } catch (err) {
1511
+ ctx.ui.notify(`Failed to cancel: ${err instanceof Error ? err.message : String(err)}`, "error");
1512
+ }
742
1513
  }
743
- }
744
- });
1514
+ });
1515
+ };
745
1516
  }
1517
+ var src_default = createExtension();
746
1518
  //#endregion
747
- export { src_default as default, statusSupervisorId };
1519
+ export { buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, src_default as default, isTerminalStatus, renderStatusLines, statusSupervisorId };