@dougbots/avenor-pi 0.3.7 → 0.4.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +36 -2
  2. package/dist/index.js +1339 -598
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,56 +1,500 @@
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(() => {});
35
230
  }
36
- return lines;
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();
300
+ }
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
+ async function openRunInspector(ctx, options) {
473
+ if (!ctx.hasUI) {
474
+ ctx.ui.notify("avenor-watch requires TUI mode", "warning");
475
+ return;
476
+ }
477
+ let runId = options.initialRunId?.trim();
478
+ if (!runId) {
479
+ if (options.trackedRuns.length === 0) {
480
+ ctx.ui.notify("No tracked runs", "warning");
481
+ return;
482
+ }
483
+ const selected = await ctx.ui.select("Watch run:", options.trackedRuns.map((run) => run.runId));
484
+ if (!selected) return;
485
+ runId = selected;
486
+ }
487
+ const tracked = options.trackedRuns.find((run) => run.runId === runId);
488
+ await ctx.ui.custom((tui, theme, keybindings, done) => new RunInspectorOverlay(tui, theme, keybindings, options.deps, tracked ?? { runId }, () => done(), options.onTrackRun), {
489
+ overlay: true,
490
+ overlayOptions: {
491
+ anchor: "center",
492
+ width: "95%",
493
+ maxHeight: "95%",
494
+ minWidth: 80
495
+ }
496
+ });
497
+ }
54
498
  //#endregion
55
499
  //#region src/types.ts
56
500
  const TERMINAL_STATUSES = new Set([
@@ -80,17 +524,48 @@ function formatRunLine(entry) {
80
524
  //#endregion
81
525
  //#region src/index.ts
82
526
  const POLL_INTERVAL_MS = 3e3;
527
+ const INSPECT_LIMIT = 128;
528
+ const COMPLETION_OUTPUT_CHARS = 600;
529
+ const INSPECT_JSON_STRING_CHARS = 600;
530
+ const INSPECT_JSON_ARRAY_ITEMS = 24;
531
+ const INSPECT_JSON_OBJECT_KEYS = 24;
532
+ const INSPECT_JSON_DEPTH = 8;
533
+ const defaultDeps = {
534
+ spawnTool,
535
+ statusTool,
536
+ eventsTool,
537
+ answerPermissionTool,
538
+ followUpTool,
539
+ inspectTool,
540
+ shutdownTool,
541
+ observeRun,
542
+ dial,
543
+ Supervisor
544
+ };
83
545
  function statusSupervisorId(requestedSupervisorId, spawnedSupervisorId) {
84
546
  return requestedSupervisorId ? spawnedSupervisorId : void 0;
85
547
  }
86
548
  function sleep(ms) {
87
549
  return new Promise((resolve) => setTimeout(resolve, ms));
88
550
  }
89
- function buildCompletionText(run, result) {
551
+ function normalizeMultilinePreview(text, limit = COMPLETION_OUTPUT_CHARS) {
552
+ if (!text) return void 0;
553
+ const clean = sanitizeText(text).trim();
554
+ if (!clean) return void 0;
555
+ return clean.length <= limit ? clean : `${clean.slice(0, Math.max(0, limit - 1))}…`;
556
+ }
557
+ function quotedLines(text) {
558
+ const preview = normalizeMultilinePreview(text);
559
+ if (!preview) return [];
560
+ return preview.split("\n").slice(0, 8).map((line) => `> ${line}`);
561
+ }
562
+ function buildCompletionText(run, result, finalOutput) {
90
563
  const lines = [`Sub-agent "${run.label}" finished with status **${result.status}**.`];
564
+ const output = normalizeMultilinePreview(finalOutput ?? result.final_output);
565
+ if (output) lines.push("", "Final output:", ...quotedLines(output));
91
566
  if (result.stop_reason && result.stop_reason.toUpperCase() !== result.status.toUpperCase()) lines.push(`Stop reason: ${result.stop_reason}`);
92
567
  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.`);
568
+ lines.push("", `Call \`avenor_inspect\` with \`run_id: "${run.runId}"\` for the bounded transcript/snapshot, or \`avenor_follow_up\` to iterate.`);
94
569
  return lines.join("\n");
95
570
  }
96
571
  function buildWaitingText(run, status) {
@@ -98,342 +573,598 @@ function buildWaitingText(run, status) {
98
573
  const lines = [`Sub-agent "${run.label}" (${run.agent}) is waiting for input.`];
99
574
  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
575
  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.`);
576
+ if (!perm) lines.push(`Use \`avenor_follow_up\` with run_id "${run.runId}" to respond, or \`avenor_inspect\` to review context first.`);
102
577
  return lines.join("\n");
103
578
  }
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);
579
+ function isTerminalStatus(status) {
580
+ return !!status && TERMINAL_STATUSES.has(status);
581
+ }
582
+ function renderStatusLines(entries) {
583
+ const active = entries.filter((e) => !isTerminalStatus(e.status));
584
+ if (active.length === 0) return [];
585
+ return active.map((e) => {
586
+ const emoji = statusEmoji(e.status);
587
+ const perm = e.pendingPermission ? " 🔒" : "";
588
+ const phase = e.phaseLabel ? ` [${e.phaseLabel}]` : "";
589
+ return `${emoji} ${e.label} — ${e.status}${phase}${perm}`;
590
+ });
591
+ }
592
+ function compactWhitespace(text, limit = 100) {
593
+ const clipped = clipText(text, limit);
594
+ return clipped ? clipped.replace(/\s+/g, " ").trim() : void 0;
595
+ }
596
+ function snapshotProgressText(label, snapshot) {
597
+ if (snapshot.pending_permission) return `[blocked] ${label} — permission: ${snapshot.pending_permission.description}`;
598
+ const phase = snapshot.phase_label ? ` (${snapshot.phase_label})` : snapshot.phase ? ` (${snapshot.phase})` : "";
599
+ const liveTool = snapshot.live_tools[snapshot.live_tools.length - 1];
600
+ if (liveTool) {
601
+ const toolName = liveTool.title ?? liveTool.kind ?? "tool";
602
+ const preview = compactWhitespace(liveTool.preview, 80);
603
+ return `${label}${phase} — ${toolName}${preview ? `: ${preview}` : ""}`;
604
+ }
605
+ const last = snapshot.transcript[snapshot.transcript.length - 1];
606
+ const transcriptWidth = 160;
607
+ const rendered = last ? compactWhitespace(renderTranscriptLines({
608
+ ...snapshot,
609
+ transcript: [last],
610
+ live_tools: [],
611
+ pending_permission: void 0
612
+ }, transcriptWidth, {
613
+ fg: (_color, text) => text,
614
+ bg: (_color, text) => text,
615
+ bold: (text) => text,
616
+ italic: (text) => text
617
+ })[0], 80) : void 0;
618
+ return `${label}${phase}${rendered ? ` — ${rendered}` : ""}`;
619
+ }
620
+ function boundJsonValue(value, depth = 0) {
621
+ if (depth >= INSPECT_JSON_DEPTH) return "[truncated]";
622
+ if (typeof value === "string") {
623
+ const clean = sanitizeText(value);
624
+ return clean.length <= INSPECT_JSON_STRING_CHARS ? clean : `${clean.slice(0, INSPECT_JSON_STRING_CHARS - 1)}…`;
625
+ }
626
+ if (Array.isArray(value)) return value.slice(Math.max(0, value.length - INSPECT_JSON_ARRAY_ITEMS)).map((item) => boundJsonValue(item, depth + 1));
627
+ if (!value || typeof value !== "object") return value;
628
+ const result = {};
629
+ for (const [key, item] of Object.entries(value).slice(0, INSPECT_JSON_OBJECT_KEYS)) result[key] = boundJsonValue(item, depth + 1);
630
+ return result;
631
+ }
632
+ function buildInspectPayload(result) {
633
+ const finalOutput = finalOutputPreview(result.status, result.snapshot);
634
+ return boundJsonValue({
635
+ run_id: result.run_id,
636
+ label: result.label,
637
+ status: result.status,
638
+ snapshot: result.snapshot,
639
+ transcript: result.transcript,
640
+ tools: result.tools,
641
+ live_tools: result.live_tools,
642
+ permissions: result.permissions,
643
+ pending_permission: result.pending_permission,
644
+ final_output: finalOutput
645
+ });
646
+ }
647
+ function toWatchRunRef(run) {
648
+ return {
649
+ runId: run.runId,
650
+ label: run.label,
651
+ agent: run.agent,
652
+ runtimeId: run.runtimeId,
653
+ supervisorId: run.supervisorId
654
+ };
655
+ }
656
+ function createExtension(deps = defaultDeps) {
657
+ return async function(pi) {
658
+ const trackedRuns = /* @__PURE__ */ new Map();
659
+ let pollingActive = false;
660
+ let pollingGeneration = 0;
661
+ let pollInFlight = null;
662
+ let sessionCtx = null;
663
+ function upsertTrackedRun(run, status, blocking = false) {
664
+ const current = trackedRuns.get(run.runId);
665
+ trackedRuns.set(run.runId, {
666
+ runId: run.runId,
667
+ agent: run.agent ?? status?.agent ?? current?.agent ?? "unknown",
668
+ label: run.label ?? status?.label ?? current?.label ?? run.runId,
669
+ supervisorId: run.supervisorId ?? current?.supervisorId,
670
+ runtimeId: run.runtimeId ?? status?.runtime_id ?? current?.runtimeId,
671
+ startTime: current?.startTime ?? Date.now(),
672
+ lastStatus: status ?? current?.lastStatus,
673
+ blocking: current?.blocking ?? blocking,
674
+ permissionNotified: current?.permissionNotified
675
+ });
125
676
  }
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({
677
+ async function resolveFinalOutput(run, status) {
678
+ const preview = finalOutputPreview(status);
679
+ if (preview) return preview;
680
+ try {
681
+ const inspected = await deps.inspectTool({
133
682
  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
683
+ supervisorId: run.supervisorId,
684
+ limit: INSPECT_LIMIT
141
685
  });
142
- continue;
686
+ return finalOutputPreview(inspected.status, inspected.snapshot);
687
+ } catch {
688
+ return;
143
689
  }
690
+ }
691
+ async function pollRuns() {
692
+ const liveMap = /* @__PURE__ */ new Map();
144
693
  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
- });
694
+ const allLive = await deps.statusTool({});
695
+ const liveList = Array.isArray(allLive) ? allLive : [allLive];
696
+ for (const result of liveList) liveMap.set(result.run_id, result);
162
697
  } 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
- });
698
+ console.error("avenor pollRuns: failed to list singleton runs", err);
699
+ }
700
+ const entries = [];
701
+ for (const run of trackedRuns.values()) {
702
+ const live = liveMap.get(run.runId);
703
+ if (live) {
704
+ liveMap.delete(live.run_id);
705
+ run.lastStatus = live;
706
+ entries.push({
707
+ runId: run.runId,
708
+ label: run.label,
709
+ status: live.status,
710
+ phase: live.phase,
711
+ phaseLabel: live.phase_label,
712
+ agent: run.agent,
713
+ pendingPermission: !!live.pending_permission,
714
+ permissionDescription: live.pending_permission?.description
715
+ });
716
+ continue;
717
+ }
718
+ try {
719
+ const raw = await deps.statusTool({
720
+ runId: run.runId,
721
+ supervisorId: run.supervisorId
722
+ });
723
+ const result = Array.isArray(raw) ? raw[0] : raw;
724
+ if (!result) throw new Error("status response was empty");
725
+ run.lastStatus = result;
726
+ entries.push({
727
+ runId: run.runId,
728
+ label: run.label,
729
+ status: result.status,
730
+ phase: result.phase,
731
+ phaseLabel: result.phase_label,
732
+ agent: run.agent,
733
+ pendingPermission: !!result.pending_permission,
734
+ permissionDescription: result.pending_permission?.description
735
+ });
736
+ } catch (err) {
737
+ console.error(`avenor pollRuns: statusTool failed for ${run.runId}`, err);
738
+ const previous = run.lastStatus;
739
+ entries.push({
740
+ runId: run.runId,
741
+ label: run.label,
742
+ status: previous?.status ?? "unavailable",
743
+ phase: previous?.phase,
744
+ phaseLabel: previous?.phase_label,
745
+ agent: run.agent,
746
+ pendingPermission: !!previous?.pending_permission,
747
+ permissionDescription: previous?.pending_permission?.description
748
+ });
749
+ }
175
750
  }
751
+ for (const live of liveMap.values()) entries.push({
752
+ runId: live.run_id,
753
+ label: live.label,
754
+ status: live.status,
755
+ phase: live.phase,
756
+ phaseLabel: live.phase_label,
757
+ agent: live.agent ?? "unknown",
758
+ pendingPermission: !!live.pending_permission,
759
+ permissionDescription: live.pending_permission?.description
760
+ });
761
+ return entries;
176
762
  }
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", "");
763
+ function updateWidget(entries) {
764
+ if (!sessionCtx) return;
765
+ const lines = renderStatusLines(entries);
766
+ if (lines.length > 0) {
767
+ sessionCtx.ui.setWidget("avenor-status", lines);
768
+ sessionCtx.ui.setStatus("avenor", lines.map((line) => line.slice(0, 60)).join(" "));
769
+ } else {
770
+ sessionCtx.ui.setWidget("avenor-status", void 0);
771
+ sessionCtx.ui.setStatus("avenor", "");
772
+ }
209
773
  }
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({
774
+ function startPolling() {
775
+ if (pollingActive) return;
776
+ pollingActive = true;
777
+ const generation = ++pollingGeneration;
778
+ const tick = async () => {
779
+ if (!pollingActive || generation !== pollingGeneration) return;
780
+ const prevStatuses = /* @__PURE__ */ new Map();
781
+ for (const [id, run] of trackedRuns) prevStatuses.set(id, run.lastStatus?.status);
782
+ const currentPoll = pollRuns();
783
+ pollInFlight = currentPoll;
784
+ const entries = await currentPoll.finally(() => {
785
+ if (pollInFlight === currentPoll) pollInFlight = null;
786
+ });
787
+ if (!pollingActive || generation !== pollingGeneration) return;
788
+ updateWidget(entries);
789
+ for (const entry of entries) {
790
+ const run = trackedRuns.get(entry.runId);
791
+ if (!run) continue;
792
+ if (isTerminalStatus(entry.status)) {
793
+ const prevStatus = prevStatuses.get(entry.runId);
794
+ if (!prevStatus || !isTerminalStatus(prevStatus)) {
795
+ sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
796
+ if (!run.blocking && run.lastStatus) {
797
+ const finalOutput = await resolveFinalOutput(run, run.lastStatus);
798
+ try {
799
+ pi.sendUserMessage(buildCompletionText({
800
+ runId: run.runId,
801
+ label: run.label
802
+ }, run.lastStatus, finalOutput), { deliverAs: "followUp" });
803
+ } catch (err) {
804
+ console.error("avenor tick: sendUserMessage failed", err);
805
+ }
806
+ }
807
+ }
808
+ trackedRuns.delete(entry.runId);
809
+ } else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
810
+ run.permissionNotified = true;
811
+ if (run.lastStatus) try {
812
+ pi.sendUserMessage(buildWaitingText({
235
813
  runId: run.runId,
236
- label: run.label
814
+ label: run.label,
815
+ agent: run.agent
237
816
  }, run.lastStatus), { deliverAs: "followUp" });
238
817
  } catch (err) {
239
- console.error("avenor tick: sendUserMessage failed", err);
818
+ console.error("avenor tick: sendUserMessage for permission failed", err);
240
819
  }
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) => {
820
+ } else if (!entry.pendingPermission && run.permissionNotified) run.permissionNotified = false;
821
+ }
822
+ if (entries.some((entry) => !isTerminalStatus(entry.status)) && pollingActive && generation === pollingGeneration) setTimeout(() => {
823
+ tick().catch((err) => {
824
+ pollingActive = false;
825
+ console.error("avenor polling loop failed", err);
826
+ });
827
+ }, POLL_INTERVAL_MS);
828
+ else {
258
829
  pollingActive = false;
259
- console.error("avenor polling loop failed", err);
260
- });
261
- }, POLL_INTERVAL_MS);
262
- else {
830
+ sessionCtx?.ui.setStatus("avenor", "");
831
+ sessionCtx?.ui.setWidget("avenor-status", void 0);
832
+ }
833
+ };
834
+ tick().catch((err) => {
263
835
  pollingActive = false;
264
- sessionCtx?.ui.setStatus("avenor", "");
265
- sessionCtx?.ui.setWidget("avenor-status", void 0);
266
- }
267
- };
268
- tick().catch((err) => {
836
+ console.error("avenor polling loop failed", err);
837
+ });
838
+ }
839
+ async function stopPolling() {
269
840
  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";
841
+ pollingGeneration++;
842
+ await pollInFlight?.catch(() => {});
843
+ sessionCtx?.ui.setStatus("avenor", "");
844
+ sessionCtx?.ui.setWidget("avenor-status", void 0);
308
845
  }
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
846
+ function createWatchDeps() {
847
+ async function resolveSupervisorSocket(run) {
848
+ if (run.supervisorId) return run.supervisorId;
849
+ return (await deps.Supervisor.get()).supervisorId;
850
+ }
851
+ async function withClient(run, work) {
852
+ const socket = await resolveSupervisorSocket(run);
853
+ const client = await deps.dial(socket);
854
+ try {
855
+ return await work(client);
856
+ } finally {
857
+ client.close();
375
858
  }
376
- };
377
- onUpdate?.({
378
- content: [{
379
- type: "text",
380
- text: `Starting ${label}…`
381
- }],
382
- details: {
383
- status: "running",
384
- run_id: result.run_id
859
+ }
860
+ return {
861
+ inspectRun(run) {
862
+ return deps.inspectTool({
863
+ runId: run.runId,
864
+ supervisorId: run.supervisorId,
865
+ limit: INSPECT_LIMIT
866
+ });
867
+ },
868
+ async observeRun(run, afterSeq, initialSnapshot) {
869
+ const runtimeId = run.runtimeId;
870
+ if (!runtimeId) return null;
871
+ const socket = await resolveSupervisorSocket(run);
872
+ const client = await deps.dial(socket);
873
+ return deps.observeRun(client, {
874
+ runtime_id: runtimeId,
875
+ replay: true,
876
+ after_seq: afterSeq,
877
+ initial_snapshot: initialSnapshot,
878
+ close_client: true
879
+ });
880
+ },
881
+ cancelRun(run, runtimeId) {
882
+ return withClient(run, async (client) => {
883
+ await client.cancel(runtimeId);
884
+ });
885
+ },
886
+ promptRun(run, runtimeId, text) {
887
+ return withClient(run, async (client) => {
888
+ await client.prompt(runtimeId, text);
889
+ });
890
+ },
891
+ followUpRun(run, text) {
892
+ return deps.followUpTool({
893
+ runId: run.runId,
894
+ message: text,
895
+ supervisorId: run.supervisorId
896
+ });
385
897
  }
898
+ };
899
+ }
900
+ async function followUpTrackedRun(run, text) {
901
+ const result = await deps.followUpTool({
902
+ runId: run.runId,
903
+ message: text,
904
+ supervisorId: run.supervisorId
386
905
  });
906
+ try {
907
+ const rawStatus = await deps.statusTool({
908
+ runId: result.run_id,
909
+ supervisorId: run.supervisorId
910
+ });
911
+ const status = Array.isArray(rawStatus) ? rawStatus[0] : rawStatus;
912
+ upsertTrackedRun({
913
+ runId: result.run_id,
914
+ label: result.label,
915
+ agent: status?.agent ?? run.agent,
916
+ runtimeId: status?.runtime_id,
917
+ supervisorId: run.supervisorId
918
+ }, status, false);
919
+ startPolling();
920
+ } catch {
921
+ upsertTrackedRun({
922
+ runId: result.run_id,
923
+ label: result.label,
924
+ agent: run.agent,
925
+ supervisorId: run.supervisorId
926
+ }, void 0, false);
927
+ startPolling();
928
+ }
929
+ return result;
930
+ }
931
+ async function waitForRunWithObserver(run, signal, onUpdate) {
932
+ let observer = null;
933
+ let unsubscribe;
934
+ let lastUpdate = "";
935
+ let abortListener;
936
+ const emit = (snapshot) => {
937
+ const text = snapshotProgressText(run.label ?? run.runId, snapshot);
938
+ if (text === lastUpdate) return;
939
+ lastUpdate = text;
940
+ onUpdate?.({
941
+ content: [{
942
+ type: "text",
943
+ text
944
+ }],
945
+ details: {
946
+ status: snapshot.pending_permission ? "waiting" : snapshot.ended ? "done" : "running",
947
+ run_id: run.runId
948
+ }
949
+ });
950
+ };
951
+ const waitPromise = new Promise((resolve) => {
952
+ const complete = () => resolve("settled");
953
+ (async () => {
954
+ try {
955
+ observer = await createWatchDeps().observeRun(run);
956
+ if (!observer) {
957
+ complete();
958
+ return;
959
+ }
960
+ const maybeComplete = (snapshot) => {
961
+ emit(snapshot);
962
+ if (snapshot.pending_permission || snapshot.ended) complete();
963
+ };
964
+ unsubscribe = observer.subscribe((snapshot) => {
965
+ maybeComplete(snapshot);
966
+ });
967
+ const current = observer.snapshot();
968
+ if (current.last_event || current.transcript.length > 0 || current.pending_permission || current.ended) maybeComplete(current);
969
+ } catch {
970
+ complete();
971
+ }
972
+ })();
973
+ });
974
+ const abortPromise = signal ? new Promise((resolve) => {
975
+ if (signal.aborted) {
976
+ resolve("aborted");
977
+ return;
978
+ }
979
+ const onAbort = () => resolve("aborted");
980
+ abortListener = onAbort;
981
+ signal.addEventListener("abort", onAbort, { once: true });
982
+ }) : void 0;
983
+ const outcome = abortPromise ? await Promise.race([waitPromise, abortPromise]) : await waitPromise;
984
+ if (signal && abortListener) signal.removeEventListener("abort", abortListener);
985
+ unsubscribe?.();
986
+ await observer?.close().catch(() => {});
987
+ if (outcome === "aborted") return { aborted: true };
988
+ try {
989
+ const raw = await deps.statusTool({
990
+ runId: run.runId,
991
+ supervisorId: run.supervisorId
992
+ });
993
+ return {
994
+ status: Array.isArray(raw) ? raw[0] : raw,
995
+ aborted: false
996
+ };
997
+ } catch {
998
+ return { aborted: false };
999
+ }
1000
+ }
1001
+ async function waitForRunByPolling(run, signal, onUpdate) {
387
1002
  let firstPoll = true;
388
1003
  while (!signal?.aborted) {
389
1004
  if (!firstPoll) await sleep(POLL_INTERVAL_MS);
390
1005
  firstPoll = false;
391
- let raw;
392
1006
  try {
393
- raw = await statusTool({
394
- runId: result.run_id,
395
- supervisorId
1007
+ const raw = await deps.statusTool({
1008
+ runId: run.runId,
1009
+ supervisorId: run.supervisorId
1010
+ });
1011
+ const status = Array.isArray(raw) ? raw[0] : raw;
1012
+ if (!status) continue;
1013
+ 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})` : ""}`;
1014
+ onUpdate?.({
1015
+ content: [{
1016
+ type: "text",
1017
+ text
1018
+ }],
1019
+ details: {
1020
+ status: status.status,
1021
+ run_id: run.runId
1022
+ }
396
1023
  });
1024
+ if (status.status === "waiting" || isTerminalStatus(status.status)) return {
1025
+ status,
1026
+ aborted: false
1027
+ };
397
1028
  } catch (err) {
398
1029
  console.error("avenor spawn poll: statusTool failed", err);
399
- continue;
400
1030
  }
401
- const status = Array.isArray(raw) ? raw[0] : raw;
402
- if (!status) continue;
403
- if (status.pending_permission) onUpdate?.({
1031
+ }
1032
+ return { aborted: true };
1033
+ }
1034
+ async function waitForRun(run, signal, onUpdate) {
1035
+ const observed = await waitForRunWithObserver(run, signal, onUpdate);
1036
+ if (observed.status || observed.aborted) return observed;
1037
+ return waitForRunByPolling(run, signal, onUpdate);
1038
+ }
1039
+ pi.on("session_start", async (_event, ctx) => {
1040
+ sessionCtx = ctx;
1041
+ });
1042
+ pi.on("session_shutdown", async () => {
1043
+ await stopPolling();
1044
+ sessionCtx = null;
1045
+ });
1046
+ pi.on("before_agent_start", async () => {
1047
+ if (trackedRuns.size === 0) return;
1048
+ const active = (await pollRuns()).filter((entry) => !isTerminalStatus(entry.status));
1049
+ if (active.length === 0) return;
1050
+ return { message: {
1051
+ customType: "avenor-active-runs",
1052
+ content: `Active sub-agents:\n${active.map((entry) => {
1053
+ const perm = entry.pendingPermission ? ` [blocked: ${entry.permissionDescription ?? "permission"}]` : "";
1054
+ const phase = entry.phaseLabel ? ` (${entry.phaseLabel})` : "";
1055
+ return ` • ${entry.label} (${entry.agent}): ${entry.status}${phase}${perm}`;
1056
+ }).join("\n")}`,
1057
+ display: true
1058
+ } };
1059
+ });
1060
+ pi.registerMessageRenderer("avenor-active-runs", (message, _options, theme) => {
1061
+ const lines = message.content.split("\n");
1062
+ let text = "";
1063
+ for (let i = 0; i < lines.length; i++) {
1064
+ text += i === 0 ? theme.fg("accent", theme.bold(lines[i] ?? "")) : theme.fg("muted", lines[i] ?? "");
1065
+ if (i < lines.length - 1) text += "\n";
1066
+ }
1067
+ return new Text(text, 0, 0);
1068
+ });
1069
+ pi.registerTool({
1070
+ name: "avenor_spawn",
1071
+ label: "Avenor Spawn",
1072
+ description: "Dispatch an agent run via avenor. Blocks by default, showing live progress. Set wait=false for fire-and-forget.",
1073
+ promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, inspect, events, follow-up, shutdown)",
1074
+ promptGuidelines: [
1075
+ "Use avenor_spawn to delegate well-defined tasks to sub-agents.",
1076
+ "Use avenor_status to check progress of fire-and-forget runs.",
1077
+ "Use avenor_inspect to review a bounded transcript, tools, and final output from a run.",
1078
+ "Use avenor_events only when raw event payloads are specifically needed.",
1079
+ "Use avenor_follow_up to iterate on a completed run.",
1080
+ "Use avenor_shutdown when done delegating work."
1081
+ ],
1082
+ parameters: Type.Object({
1083
+ agent: Type.String({ description: "Agent name (required, no default)" }),
1084
+ prompt: Type.Optional(Type.String({ description: "Prompt text" })),
1085
+ prompt_file: Type.Optional(Type.String({ description: "Path to prompt file" })),
1086
+ dir: Type.Optional(Type.String({ description: "Working directory (defaults to session directory)" })),
1087
+ label: Type.Optional(Type.String({ description: "Human-readable label for the run" })),
1088
+ timeout: Type.Optional(Type.String({ description: "Timeout duration (e.g. 3600s)" })),
1089
+ model: Type.Optional(Type.String({ description: "Model override" })),
1090
+ backend: Type.Optional(Type.String({ description: "Backend override" })),
1091
+ server_url: Type.Optional(Type.String({ description: "Backend server URL" })),
1092
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
1093
+ wait: Type.Optional(Type.Boolean({ description: "Block until complete (default true)" }))
1094
+ }),
1095
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
1096
+ if (signal?.aborted) return { content: [{
1097
+ type: "text",
1098
+ text: "Cancelled"
1099
+ }] };
1100
+ const wait = params.wait ?? true;
1101
+ const dir = params.dir ?? ctx.cwd;
1102
+ const label = params.label ?? `${params.agent}-${Date.now()}`;
1103
+ const result = await deps.spawnTool({
1104
+ agent: params.agent,
1105
+ prompt: params.prompt,
1106
+ promptFile: params.prompt_file,
1107
+ label,
1108
+ dir,
1109
+ timeout: params.timeout,
1110
+ model: params.model,
1111
+ backend: params.backend,
1112
+ serverUrl: params.server_url,
1113
+ supervisorId: params.supervisor_id
1114
+ });
1115
+ const supervisorId = statusSupervisorId(params.supervisor_id, result.supervisor_id);
1116
+ const runRef = {
1117
+ runId: result.run_id,
1118
+ agent: params.agent,
1119
+ label,
1120
+ supervisorId,
1121
+ runtimeId: result.runtime_id
1122
+ };
1123
+ upsertTrackedRun(runRef, void 0, wait);
1124
+ startPolling();
1125
+ if (!wait) return {
404
1126
  content: [{
405
1127
  type: "text",
406
- text: `[blocked] ${label} — permission: ${status.pending_permission.description}`
1128
+ text: `Dispatched "${label}" (run_id: ${result.run_id}). Call avenor_status to check progress or avenor_inspect for a bounded transcript snapshot.`
407
1129
  }],
408
1130
  details: {
409
- status: "permission",
1131
+ run_id: result.run_id,
1132
+ label
1133
+ }
1134
+ };
1135
+ onUpdate?.({
1136
+ content: [{
1137
+ type: "text",
1138
+ text: `Starting ${label}…`
1139
+ }],
1140
+ details: {
1141
+ status: "running",
410
1142
  run_id: result.run_id
411
1143
  }
412
1144
  });
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?.({
1145
+ const waitResult = await waitForRun(runRef, signal, onUpdate);
1146
+ if (waitResult.aborted) {
1147
+ trackedRuns.delete(result.run_id);
1148
+ return {
426
1149
  content: [{
427
1150
  type: "text",
428
- text: `${label}${status.phase_label ? ` (${status.phase_label})` : ""}${lastAction ? ` — ${lastAction}` : ""}`
1151
+ text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status or avenor_inspect to check.`
429
1152
  }],
430
- details: {
431
- status: status.status,
432
- run_id: result.run_id,
433
- lastAction
434
- }
435
- });
1153
+ details: { run_id: result.run_id }
1154
+ };
436
1155
  }
1156
+ const status = waitResult.status;
1157
+ if (!status) return {
1158
+ content: [{
1159
+ type: "text",
1160
+ text: `Sub-agent "${label}" is still running. Use avenor_status or avenor_inspect to check progress.`
1161
+ }],
1162
+ details: {
1163
+ status: "running",
1164
+ run_id: result.run_id
1165
+ }
1166
+ };
1167
+ upsertTrackedRun(runRef, status, wait);
437
1168
  if (status.status === "waiting") {
438
1169
  const tracked = trackedRuns.get(result.run_id);
439
1170
  if (tracked) tracked.permissionNotified = true;
@@ -454,7 +1185,11 @@ async function src_default(pi) {
454
1185
  }
455
1186
  };
456
1187
  }
457
- if (TERMINAL_STATUSES.has(status.status)) {
1188
+ if (isTerminalStatus(status.status)) {
1189
+ const finalOutput = await resolveFinalOutput({
1190
+ runId: result.run_id,
1191
+ supervisorId
1192
+ }, status);
458
1193
  trackedRuns.delete(result.run_id);
459
1194
  return {
460
1195
  content: [{
@@ -462,286 +1197,292 @@ async function src_default(pi) {
462
1197
  text: buildCompletionText({
463
1198
  runId: result.run_id,
464
1199
  label
465
- }, status)
1200
+ }, status, finalOutput)
466
1201
  }],
467
1202
  details: {
468
1203
  status: status.status,
469
1204
  run_id: result.run_id,
470
- session_id: status.session_id
1205
+ session_id: status.session_id,
1206
+ final_output: finalOutput
471
1207
  }
472
1208
  };
473
1209
  }
1210
+ return {
1211
+ content: [{
1212
+ type: "text",
1213
+ text: `${label} is ${status.status}. Use avenor_status or avenor_inspect to check progress.`
1214
+ }],
1215
+ details: {
1216
+ status: status.status,
1217
+ run_id: result.run_id
1218
+ }
1219
+ };
1220
+ },
1221
+ renderCall(args, theme) {
1222
+ const agent = theme.fg("toolTitle", theme.bold("avenor_spawn "));
1223
+ const agentName = theme.fg("accent", args.agent);
1224
+ const label = args.label ? theme.fg("dim", ` "${args.label}"`) : "";
1225
+ const wait = args.wait === false ? theme.fg("warning", " [async]") : "";
1226
+ return new Text(agent + agentName + label + wait, 0, 0);
1227
+ },
1228
+ renderResult(result, { isPartial }, theme) {
1229
+ if (isPartial) {
1230
+ const status = result.details?.status ?? "running";
1231
+ const emoji = statusEmoji(String(status));
1232
+ return new Text(theme.fg("accent", `${emoji} ${result.content?.[0]?.text ?? "Running…"}`), 0, 0);
1233
+ }
1234
+ const status = String(result.details?.status ?? "");
1235
+ if (status && isTerminalStatus(status)) {
1236
+ const emoji = statusEmoji(status);
1237
+ 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);
1238
+ }
1239
+ return new Text((result.content?.[0]?.text ?? "").split("\n").map((line) => theme.fg("muted", line)).join("\n"), 0, 0);
474
1240
  }
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);
1241
+ });
1242
+ pi.registerTool({
1243
+ name: "avenor_status",
1244
+ label: "Avenor Status",
1245
+ description: "Get status of a run or all runs. Surfaces pending permission requests.",
1246
+ parameters: Type.Object({
1247
+ run_id: Type.Optional(Type.String({ description: "Specific run ID to query" })),
1248
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
1249
+ }),
1250
+ async execute(_toolCallId, params) {
1251
+ const result = await deps.statusTool({
1252
+ runId: params.run_id,
1253
+ supervisorId: params.supervisor_id
1254
+ });
1255
+ return {
1256
+ content: [{
1257
+ type: "text",
1258
+ text: JSON.stringify(result, null, 2)
1259
+ }],
1260
+ details: result
1261
+ };
502
1262
  }
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;
1263
+ });
1264
+ pi.registerTool({
1265
+ name: "avenor_inspect",
1266
+ label: "Avenor Inspect",
1267
+ description: "Inspect a run with bounded snapshot, transcript, tool, permission, and final output details.",
1268
+ parameters: Type.Object({
1269
+ run_id: Type.String({ description: "Run ID to inspect" }),
1270
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
1271
+ limit: Type.Optional(Type.Number({ description: "Replay event limit (default 128)" })),
1272
+ after_seq: Type.Optional(Type.Number({ description: "Only include events after this sequence number" }))
1273
+ }),
1274
+ async execute(_toolCallId, params) {
1275
+ const payload = buildInspectPayload(await deps.inspectTool({
1276
+ runId: params.run_id,
1277
+ supervisorId: params.supervisor_id,
1278
+ limit: params.limit,
1279
+ after_seq: params.after_seq
1280
+ }));
1281
+ return {
1282
+ content: [{
1283
+ type: "text",
1284
+ text: JSON.stringify(payload, null, 2)
1285
+ }],
1286
+ details: payload
1287
+ };
637
1288
  }
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;
1289
+ });
1290
+ pi.registerTool({
1291
+ name: "avenor_answer_permission",
1292
+ label: "Avenor Answer Permission",
1293
+ description: "Answer a pending permission request for a sub-agent. Pass option_id from avenor_status pending_permission.options.",
1294
+ parameters: Type.Object({
1295
+ run_id: Type.String({ description: "Run ID with the pending permission" }),
1296
+ option_id: Type.String({ description: "allow_once | allow_always | deny" }),
1297
+ request_id: Type.Optional(Type.String({ description: "Permission request ID (auto-discovered if omitted)" })),
1298
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
1299
+ }),
1300
+ async execute(_toolCallId, params) {
1301
+ const result = await deps.answerPermissionTool({
1302
+ runId: params.run_id,
1303
+ optionId: params.option_id,
1304
+ requestId: params.request_id,
1305
+ supervisorId: params.supervisor_id
1306
+ });
1307
+ return {
1308
+ content: [{
1309
+ type: "text",
1310
+ text: JSON.stringify(result, null, 2)
1311
+ }],
1312
+ details: result
1313
+ };
655
1314
  }
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;
1315
+ });
1316
+ pi.registerTool({
1317
+ name: "avenor_follow_up",
1318
+ label: "Avenor Follow Up",
1319
+ description: "Resume a completed run with a follow-up message.",
1320
+ parameters: Type.Object({
1321
+ run_id: Type.String({ description: "Completed run ID to resume" }),
1322
+ message: Type.String({ description: "Follow-up message text" }),
1323
+ label: Type.Optional(Type.String({ description: "Override label (defaults to <original>-followup)" })),
1324
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
1325
+ }),
1326
+ async execute(_toolCallId, params) {
1327
+ const result = await deps.followUpTool({
1328
+ runId: params.run_id,
1329
+ message: params.message,
1330
+ label: params.label,
1331
+ supervisorId: params.supervisor_id
1332
+ });
1333
+ try {
1334
+ const rawStatus = await deps.statusTool({
1335
+ runId: result.run_id,
1336
+ supervisorId: params.supervisor_id
1337
+ });
1338
+ const status = Array.isArray(rawStatus) ? rawStatus[0] : rawStatus;
1339
+ upsertTrackedRun({
1340
+ runId: result.run_id,
1341
+ label: result.label,
1342
+ agent: status?.agent,
1343
+ runtimeId: status?.runtime_id,
1344
+ supervisorId: params.supervisor_id
1345
+ }, status, false);
1346
+ } catch {
1347
+ upsertTrackedRun({
1348
+ runId: result.run_id,
1349
+ label: result.label,
1350
+ supervisorId: params.supervisor_id
1351
+ }, void 0, false);
662
1352
  }
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];
1353
+ startPolling();
1354
+ return {
1355
+ content: [{
1356
+ type: "text",
1357
+ text: JSON.stringify(result, null, 2)
1358
+ }],
1359
+ details: result
1360
+ };
668
1361
  }
669
- let watchActive = true;
670
- const overlay = new EventFeedOverlay(runId);
671
- try {
672
- const { events } = await eventsTool({
673
- runId,
674
- limit: 50
1362
+ });
1363
+ pi.registerTool({
1364
+ name: "avenor_events",
1365
+ label: "Avenor Events",
1366
+ description: "Read events from a run. Filter by type. Returns last N events.",
1367
+ parameters: Type.Object({
1368
+ run_id: Type.String({ description: "Run ID to read events from" }),
1369
+ types: Type.Optional(Type.Array(Type.String(), { description: "Filter by event types" })),
1370
+ limit: Type.Optional(Type.Number({ description: "Max events to return (default 50)" })),
1371
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
1372
+ }),
1373
+ async execute(_toolCallId, params) {
1374
+ const result = await deps.eventsTool({
1375
+ runId: params.run_id,
1376
+ types: params.types,
1377
+ limit: params.limit,
1378
+ supervisorId: params.supervisor_id
675
1379
  });
676
- overlay.setEvents(events);
677
- } catch (err) {
678
- console.error("avenor watch: failed to load initial events", err);
679
- overlay.setEvents([]);
1380
+ return {
1381
+ content: [{
1382
+ type: "text",
1383
+ text: JSON.stringify(result, null, 2)
1384
+ }],
1385
+ details: result
1386
+ };
680
1387
  }
681
- const handle = ctx.ui.custom((_tui, _theme, _kb, done) => {
682
- overlay.setOnClose(() => {
683
- watchActive = false;
684
- done();
1388
+ });
1389
+ pi.registerTool({
1390
+ name: "avenor_shutdown",
1391
+ label: "Avenor Shutdown",
1392
+ description: "Shut down the avenor supervisor and clean up temp files.",
1393
+ parameters: Type.Object({
1394
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor to shut down (defaults to singleton)" })),
1395
+ force: Type.Optional(Type.Boolean({ description: "Force shutdown instead of graceful" }))
1396
+ }),
1397
+ async execute(_toolCallId, params) {
1398
+ await stopPolling();
1399
+ trackedRuns.clear();
1400
+ const result = await deps.shutdownTool({
1401
+ supervisorId: params.supervisor_id,
1402
+ force: params.force
685
1403
  });
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;
1404
+ return {
1405
+ content: [{
1406
+ type: "text",
1407
+ text: JSON.stringify(result, null, 2)
1408
+ }],
1409
+ details: result
1410
+ };
1411
+ }
1412
+ });
1413
+ pi.registerCommand("avenor-status", {
1414
+ description: "Show status of all avenor runs",
1415
+ handler: async (_args, ctx) => {
1416
+ const entries = await pollRuns();
1417
+ if (entries.length === 0) {
1418
+ ctx.ui.notify("No active avenor runs", "info");
1419
+ return;
697
1420
  }
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;
1421
+ ctx.ui.setWidget("avenor-status-detail", entries.map((entry) => formatRunLine(entry)));
1422
+ }
1423
+ });
1424
+ pi.registerCommand("avenor-watch", {
1425
+ description: "Open a live run inspector for an avenor run",
1426
+ getArgumentCompletions: (prefix) => {
1427
+ const items = Array.from(trackedRuns.values()).filter((run) => run.label.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => ({
1428
+ value: run.runId,
1429
+ label: `${run.label} (${run.runId.slice(0, 8)})`
1430
+ }));
1431
+ return items.length > 0 ? items : null;
1432
+ },
1433
+ handler: async (args, ctx) => {
1434
+ await openRunInspector(ctx, {
1435
+ trackedRuns: Array.from(trackedRuns.values()).map(toWatchRunRef),
1436
+ initialRunId: args?.trim() || void 0,
1437
+ deps: {
1438
+ ...createWatchDeps(),
1439
+ followUpRun: (run, text) => followUpTrackedRun(run, text)
1440
+ },
1441
+ onTrackRun: (run, status) => {
1442
+ upsertTrackedRun(run, status, false);
1443
+ if (status && !isTerminalStatus(status.status)) startPolling();
712
1444
  }
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;
1445
+ });
731
1446
  }
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");
1447
+ });
1448
+ pi.registerCommand("avenor-cancel", {
1449
+ description: "Cancel a running avenor sub-agent",
1450
+ getArgumentCompletions: (prefix) => {
1451
+ const items = Array.from(trackedRuns.values()).filter((run) => run.label.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => ({
1452
+ value: run.runId,
1453
+ label: `${run.label} (${run.runId.slice(0, 8)})`
1454
+ }));
1455
+ return items.length > 0 ? items : null;
1456
+ },
1457
+ handler: async (args, ctx) => {
1458
+ if (!args?.trim()) {
1459
+ ctx.ui.notify("Usage: /avenor-cancel <run_id>", "warning");
1460
+ return;
1461
+ }
1462
+ const runId = args.trim();
1463
+ const run = trackedRuns.get(runId);
1464
+ const label = run?.label ?? runId;
1465
+ const runtimeId = run?.runtimeId ?? run?.lastStatus?.runtime_id;
1466
+ if (!runtimeId) {
1467
+ ctx.ui.notify(`No runtime id available for "${label}"`, "warning");
1468
+ return;
1469
+ }
1470
+ if (!await ctx.ui.confirm("Cancel run", `Cancel "${label}" (${runId.slice(0, 8)})?`)) return;
1471
+ try {
1472
+ await createWatchDeps().cancelRun(toWatchRunRef(run ?? {
1473
+ runId,
1474
+ agent: "unknown",
1475
+ label,
1476
+ startTime: Date.now()
1477
+ }), runtimeId);
1478
+ ctx.ui.notify(`Cancelled "${label}"`, "info");
1479
+ } catch (err) {
1480
+ ctx.ui.notify(`Failed to cancel: ${err instanceof Error ? err.message : String(err)}`, "error");
1481
+ }
742
1482
  }
743
- }
744
- });
1483
+ });
1484
+ };
745
1485
  }
1486
+ var src_default = createExtension();
746
1487
  //#endregion
747
- export { src_default as default, statusSupervisorId };
1488
+ export { buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, src_default as default, isTerminalStatus, renderStatusLines, statusSupervisorId };