@dougbots/avenor-pi 0.3.6 → 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 +37 -2
  2. package/dist/index.js +1342 -597
  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,14 +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
+ };
545
+ function statusSupervisorId(requestedSupervisorId, spawnedSupervisorId) {
546
+ return requestedSupervisorId ? spawnedSupervisorId : void 0;
547
+ }
83
548
  function sleep(ms) {
84
549
  return new Promise((resolve) => setTimeout(resolve, ms));
85
550
  }
86
- 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) {
87
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));
88
566
  if (result.stop_reason && result.stop_reason.toUpperCase() !== result.status.toUpperCase()) lines.push(`Stop reason: ${result.stop_reason}`);
89
567
  if (result.session_id) lines.push(`Session: \`${result.session_id}\``);
90
- 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.`);
91
569
  return lines.join("\n");
92
570
  }
93
571
  function buildWaitingText(run, status) {
@@ -95,341 +573,598 @@ function buildWaitingText(run, status) {
95
573
  const lines = [`Sub-agent "${run.label}" (${run.agent}) is waiting for input.`];
96
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"`);
97
575
  else if (status.phase_label) lines.push(`Question: ${status.phase_label}`);
98
- 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.`);
99
577
  return lines.join("\n");
100
578
  }
101
- function summarizeEvent(evt) {
102
- const type = String(evt.type ?? evt.event ?? "");
103
- if (type === "tool.call" || type === "tool.use") return `called ${String(evt.tool ?? evt.name ?? type)}`;
104
- if (type === "agent.message_chunk") return "thinking…";
105
- if (type.startsWith("permission.")) return `permission requested`;
106
- return type;
107
- }
108
- async function src_default(pi) {
109
- const trackedRuns = /* @__PURE__ */ new Map();
110
- let pollingActive = false;
111
- let pollingGeneration = 0;
112
- let pollInFlight = null;
113
- let sessionCtx = null;
114
- async function pollRuns() {
115
- const liveMap = /* @__PURE__ */ new Map();
116
- try {
117
- const allLive = await statusTool({});
118
- const liveList = Array.isArray(allLive) ? allLive : [allLive];
119
- for (const result of liveList) liveMap.set(result.run_id, result);
120
- } catch (err) {
121
- 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
+ });
122
676
  }
123
- const entries = [];
124
- for (const run of trackedRuns.values()) {
125
- const live = liveMap.get(run.runId);
126
- if (live) {
127
- liveMap.delete(live.run_id);
128
- run.lastStatus = live;
129
- 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({
130
682
  runId: run.runId,
131
- label: run.label,
132
- status: live.status,
133
- phase: live.phase,
134
- phaseLabel: live.phase_label,
135
- agent: run.agent,
136
- pendingPermission: !!live.pending_permission,
137
- permissionDescription: live.pending_permission?.description
683
+ supervisorId: run.supervisorId,
684
+ limit: INSPECT_LIMIT
138
685
  });
139
- continue;
686
+ return finalOutputPreview(inspected.status, inspected.snapshot);
687
+ } catch {
688
+ return;
140
689
  }
690
+ }
691
+ async function pollRuns() {
692
+ const liveMap = /* @__PURE__ */ new Map();
141
693
  try {
142
- const raw = await statusTool({
143
- runId: run.runId,
144
- supervisorId: run.supervisorId
145
- });
146
- const result = Array.isArray(raw) ? raw[0] : raw;
147
- if (!result) throw new Error("status response was empty");
148
- run.lastStatus = result;
149
- entries.push({
150
- runId: run.runId,
151
- label: run.label,
152
- status: result.status,
153
- phase: result.phase,
154
- phaseLabel: result.phase_label,
155
- agent: run.agent,
156
- pendingPermission: !!result.pending_permission,
157
- permissionDescription: result.pending_permission?.description
158
- });
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);
159
697
  } catch (err) {
160
- console.error(`avenor pollRuns: statusTool failed for ${run.runId}`, err);
161
- const previous = run.lastStatus;
162
- entries.push({
163
- runId: run.runId,
164
- label: run.label,
165
- status: previous?.status ?? "unavailable",
166
- phase: previous?.phase,
167
- phaseLabel: previous?.phase_label,
168
- agent: run.agent,
169
- pendingPermission: !!previous?.pending_permission,
170
- permissionDescription: previous?.pending_permission?.description
171
- });
698
+ console.error("avenor pollRuns: failed to list singleton runs", err);
172
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
+ }
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;
173
762
  }
174
- for (const live of liveMap.values()) entries.push({
175
- runId: live.run_id,
176
- label: live.label,
177
- status: live.status,
178
- phase: live.phase,
179
- phaseLabel: live.phase_label,
180
- agent: "unknown",
181
- pendingPermission: !!live.pending_permission,
182
- permissionDescription: live.pending_permission?.description
183
- });
184
- return entries;
185
- }
186
- function renderStatusLines(entries) {
187
- const active = entries.filter((e) => !TERMINAL_STATUSES.has(e.status));
188
- if (active.length === 0) return [];
189
- return active.map((e) => {
190
- const emoji = statusEmoji(e.status);
191
- const perm = e.pendingPermission ? ` 🔒` : "";
192
- const phase = e.phaseLabel ? ` [${e.phaseLabel}]` : "";
193
- return `${emoji} ${e.label} — ${e.status}${phase}${perm}`;
194
- });
195
- }
196
- function updateWidget(entries) {
197
- if (!sessionCtx) return;
198
- const lines = renderStatusLines(entries);
199
- if (lines.length > 0) {
200
- sessionCtx.ui.setWidget("avenor-status", lines);
201
- const parts = lines.map((l) => l.slice(0, 60));
202
- sessionCtx.ui.setStatus("avenor", parts.join(" "));
203
- } else {
204
- sessionCtx.ui.setWidget("avenor-status", void 0);
205
- 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
+ }
206
773
  }
207
- }
208
- function startPolling() {
209
- if (pollingActive) return;
210
- pollingActive = true;
211
- const generation = ++pollingGeneration;
212
- const tick = async () => {
213
- if (!pollingActive || generation !== pollingGeneration) return;
214
- const prevStatuses = /* @__PURE__ */ new Map();
215
- for (const [id, run] of trackedRuns) prevStatuses.set(id, run.lastStatus?.status);
216
- const currentPoll = pollRuns();
217
- pollInFlight = currentPoll;
218
- const entries = await currentPoll.finally(() => {
219
- if (pollInFlight === currentPoll) pollInFlight = null;
220
- });
221
- if (!pollingActive || generation !== pollingGeneration) return;
222
- updateWidget(entries);
223
- for (const entry of entries) {
224
- const run = trackedRuns.get(entry.runId);
225
- if (!run) continue;
226
- if (TERMINAL_STATUSES.has(entry.status)) {
227
- const prevStatus = prevStatuses.get(entry.runId);
228
- if (!prevStatus || !TERMINAL_STATUSES.has(prevStatus)) {
229
- sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
230
- if (!run.blocking) try {
231
- 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({
232
813
  runId: run.runId,
233
- label: run.label
814
+ label: run.label,
815
+ agent: run.agent
234
816
  }, run.lastStatus), { deliverAs: "followUp" });
235
817
  } catch (err) {
236
- console.error("avenor tick: sendUserMessage failed", err);
818
+ console.error("avenor tick: sendUserMessage for permission failed", err);
237
819
  }
238
- }
239
- trackedRuns.delete(entry.runId);
240
- } else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
241
- run.permissionNotified = true;
242
- try {
243
- pi.sendUserMessage(buildWaitingText({
244
- runId: run.runId,
245
- label: run.label,
246
- agent: run.agent
247
- }, run.lastStatus), { deliverAs: "followUp" });
248
- } catch (err) {
249
- console.error("avenor tick: sendUserMessage for permission failed", err);
250
- }
251
- } else if (!entry.pendingPermission && run.permissionNotified) run.permissionNotified = false;
252
- }
253
- if (entries.some((e) => !TERMINAL_STATUSES.has(e.status)) && pollingActive && generation === pollingGeneration) setTimeout(() => {
254
- 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 {
255
829
  pollingActive = false;
256
- console.error("avenor polling loop failed", err);
257
- });
258
- }, POLL_INTERVAL_MS);
259
- else {
830
+ sessionCtx?.ui.setStatus("avenor", "");
831
+ sessionCtx?.ui.setWidget("avenor-status", void 0);
832
+ }
833
+ };
834
+ tick().catch((err) => {
260
835
  pollingActive = false;
261
- sessionCtx?.ui.setStatus("avenor", "");
262
- sessionCtx?.ui.setWidget("avenor-status", void 0);
263
- }
264
- };
265
- tick().catch((err) => {
836
+ console.error("avenor polling loop failed", err);
837
+ });
838
+ }
839
+ async function stopPolling() {
266
840
  pollingActive = false;
267
- console.error("avenor polling loop failed", err);
268
- });
269
- }
270
- async function stopPolling() {
271
- pollingActive = false;
272
- pollingGeneration++;
273
- await pollInFlight?.catch(() => {});
274
- sessionCtx?.ui.setStatus("avenor", "");
275
- sessionCtx?.ui.setWidget("avenor-status", void 0);
276
- }
277
- pi.on("session_start", async (_event, ctx) => {
278
- sessionCtx = ctx;
279
- });
280
- pi.on("session_shutdown", async (_event, _ctx) => {
281
- await stopPolling();
282
- sessionCtx = null;
283
- });
284
- pi.on("before_agent_start", async (event, _ctx) => {
285
- if (trackedRuns.size === 0) return;
286
- const active = (await pollRuns()).filter((e) => !TERMINAL_STATUSES.has(e.status));
287
- if (active.length === 0) return;
288
- return { message: {
289
- customType: "avenor-active-runs",
290
- content: `Active sub-agents:\n${active.map((e) => {
291
- const perm = e.pendingPermission ? ` [blocked: ${e.permissionDescription ?? "permission"}]` : "";
292
- const phase = e.phaseLabel ? ` (${e.phaseLabel})` : "";
293
- return ` • ${e.label} (${e.agent}): ${e.status}${phase}${perm}`;
294
- }).join("\n")}`,
295
- display: true
296
- } };
297
- });
298
- pi.registerMessageRenderer("avenor-active-runs", (message, options, theme) => {
299
- const lines = message.content.split("\n");
300
- let text = "";
301
- for (let i = 0; i < lines.length; i++) {
302
- if (i === 0) text += theme.fg("info", theme.bold(lines[i]));
303
- else text += theme.fg("muted", lines[i]);
304
- 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);
305
845
  }
306
- return new Text(text, 0, 0);
307
- });
308
- pi.registerTool({
309
- name: "avenor_spawn",
310
- label: "Avenor Spawn",
311
- description: "Dispatch an agent run via avenor. Blocks by default, showing live progress. Set wait=false for fire-and-forget.",
312
- promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, events, follow-up, shutdown)",
313
- promptGuidelines: [
314
- "Use avenor_spawn to delegate well-defined tasks to sub-agents.",
315
- "Use avenor_status to check progress of fire-and-forget runs.",
316
- "Use avenor_events to inspect detailed output from a run.",
317
- "Use avenor_follow_up to iterate on a completed run.",
318
- "Use avenor_shutdown when done delegating work."
319
- ],
320
- parameters: Type.Object({
321
- agent: Type.String({ description: "Agent name (required, no default)" }),
322
- prompt: Type.Optional(Type.String({ description: "Prompt text" })),
323
- prompt_file: Type.Optional(Type.String({ description: "Path to prompt file" })),
324
- dir: Type.Optional(Type.String({ description: "Working directory (defaults to session directory)" })),
325
- label: Type.Optional(Type.String({ description: "Human-readable label for the run" })),
326
- timeout: Type.Optional(Type.String({ description: "Timeout duration (e.g. 3600s)" })),
327
- model: Type.Optional(Type.String({ description: "Model override" })),
328
- backend: Type.Optional(Type.String({ description: "Backend override" })),
329
- server_url: Type.Optional(Type.String({ description: "Backend server URL" })),
330
- supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
331
- wait: Type.Optional(Type.Boolean({ description: "Block until complete (default true)" }))
332
- }),
333
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
334
- if (signal?.aborted) return { content: [{
335
- type: "text",
336
- text: "Cancelled"
337
- }] };
338
- const wait = params.wait ?? true;
339
- const dir = params.dir ?? ctx.cwd;
340
- const label = params.label ?? `${params.agent}-${Date.now()}`;
341
- const result = await spawnTool({
342
- agent: params.agent,
343
- prompt: params.prompt,
344
- promptFile: params.prompt_file,
345
- label,
346
- dir,
347
- timeout: params.timeout,
348
- model: params.model,
349
- backend: params.backend,
350
- serverUrl: params.server_url,
351
- supervisorId: params.supervisor_id
352
- });
353
- trackedRuns.set(result.run_id, {
354
- runId: result.run_id,
355
- agent: params.agent,
356
- label,
357
- supervisorId: result.supervisor_id,
358
- runtimeId: result.runtime_id,
359
- startTime: Date.now(),
360
- blocking: wait
361
- });
362
- startPolling();
363
- if (!wait) return {
364
- content: [{
365
- type: "text",
366
- text: `Dispatched "${label}" (run_id: ${result.run_id}). Call avenor_status to check progress.`
367
- }],
368
- details: {
369
- run_id: result.run_id,
370
- 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();
371
858
  }
372
- };
373
- onUpdate?.({
374
- content: [{
375
- type: "text",
376
- text: `Starting ${label}…`
377
- }],
378
- details: {
379
- status: "running",
380
- 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
+ });
381
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
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
+ })();
382
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) {
383
1002
  let firstPoll = true;
384
1003
  while (!signal?.aborted) {
385
1004
  if (!firstPoll) await sleep(POLL_INTERVAL_MS);
386
1005
  firstPoll = false;
387
- let raw;
388
1006
  try {
389
- raw = await statusTool({
390
- runId: result.run_id,
391
- supervisorId: result.supervisor_id
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
+ }
392
1023
  });
1024
+ if (status.status === "waiting" || isTerminalStatus(status.status)) return {
1025
+ status,
1026
+ aborted: false
1027
+ };
393
1028
  } catch (err) {
394
1029
  console.error("avenor spawn poll: statusTool failed", err);
395
- continue;
396
1030
  }
397
- const status = Array.isArray(raw) ? raw[0] : raw;
398
- if (!status) continue;
399
- 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 {
400
1126
  content: [{
401
1127
  type: "text",
402
- 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.`
403
1129
  }],
404
1130
  details: {
405
- 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",
406
1142
  run_id: result.run_id
407
1143
  }
408
1144
  });
409
- else {
410
- let lastAction = "";
411
- if (!params.supervisor_id) try {
412
- const eventsResult = await eventsTool({
413
- runId: result.run_id,
414
- limit: 3
415
- });
416
- const last = eventsResult.events[eventsResult.events.length - 1];
417
- if (last) lastAction = summarizeEvent(last);
418
- } catch (err) {
419
- console.error("avenor spawn poll: eventsTool failed", err);
420
- }
421
- onUpdate?.({
1145
+ const waitResult = await waitForRun(runRef, signal, onUpdate);
1146
+ if (waitResult.aborted) {
1147
+ trackedRuns.delete(result.run_id);
1148
+ return {
422
1149
  content: [{
423
1150
  type: "text",
424
- 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.`
425
1152
  }],
426
- details: {
427
- status: status.status,
428
- run_id: result.run_id,
429
- lastAction
430
- }
431
- });
1153
+ details: { run_id: result.run_id }
1154
+ };
432
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);
433
1168
  if (status.status === "waiting") {
434
1169
  const tracked = trackedRuns.get(result.run_id);
435
1170
  if (tracked) tracked.permissionNotified = true;
@@ -450,7 +1185,11 @@ async function src_default(pi) {
450
1185
  }
451
1186
  };
452
1187
  }
453
- 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);
454
1193
  trackedRuns.delete(result.run_id);
455
1194
  return {
456
1195
  content: [{
@@ -458,286 +1197,292 @@ async function src_default(pi) {
458
1197
  text: buildCompletionText({
459
1198
  runId: result.run_id,
460
1199
  label
461
- }, status)
1200
+ }, status, finalOutput)
462
1201
  }],
463
1202
  details: {
464
1203
  status: status.status,
465
1204
  run_id: result.run_id,
466
- session_id: status.session_id
1205
+ session_id: status.session_id,
1206
+ final_output: finalOutput
467
1207
  }
468
1208
  };
469
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);
470
1240
  }
471
- trackedRuns.delete(result.run_id);
472
- return {
473
- content: [{
474
- type: "text",
475
- text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status to check.`
476
- }],
477
- details: { run_id: result.run_id }
478
- };
479
- },
480
- renderCall(args, theme) {
481
- const agent = theme.fg("toolTitle", theme.bold("avenor_spawn "));
482
- const agentName = theme.fg("accent", args.agent);
483
- const label = args.label ? theme.fg("dim", ` "${args.label}"`) : "";
484
- const wait = args.wait === false ? theme.fg("warning", " [async]") : "";
485
- return new Text(agent + agentName + label + wait, 0, 0);
486
- },
487
- renderResult(result, { isPartial }, theme) {
488
- if (isPartial) {
489
- const emoji = statusEmoji(result.details?.status ?? "running");
490
- return new Text(theme.fg("info", `${emoji} ${result.content?.[0]?.text ?? "Running…"}`), 0, 0);
491
- }
492
- const status = result.details?.status;
493
- if (status && TERMINAL_STATUSES.has(status)) {
494
- const emoji = statusEmoji(status);
495
- const header = theme.fg(status === "done" ? "success" : "error", `${emoji} ${status}`);
496
- const lines = (result.content?.[0]?.text ?? "").split("\n");
497
- 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
+ };
498
1262
  }
499
- return new Text((result.content?.[0]?.text ?? "").split("\n").map((l) => theme.fg("muted", l)).join("\n"), 0, 0);
500
- }
501
- });
502
- pi.registerTool({
503
- name: "avenor_status",
504
- label: "Avenor Status",
505
- description: "Get status of a run or all runs. Surfaces pending permission requests.",
506
- parameters: Type.Object({
507
- run_id: Type.Optional(Type.String({ description: "Specific run ID to query" })),
508
- supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
509
- }),
510
- async execute(_toolCallId, params) {
511
- const result = await statusTool({
512
- runId: params.run_id,
513
- supervisorId: params.supervisor_id
514
- });
515
- return {
516
- content: [{
517
- type: "text",
518
- text: JSON.stringify(result, null, 2)
519
- }],
520
- details: result
521
- };
522
- }
523
- });
524
- pi.registerTool({
525
- name: "avenor_answer_permission",
526
- label: "Avenor Answer Permission",
527
- description: "Answer a pending permission request for a sub-agent. Pass option_id from avenor_status pending_permission.options.",
528
- parameters: Type.Object({
529
- run_id: Type.String({ description: "Run ID with the pending permission" }),
530
- option_id: Type.String({ description: "allow_once | allow_always | deny" }),
531
- request_id: Type.Optional(Type.String({ description: "Permission request ID (auto-discovered if omitted)" })),
532
- supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
533
- }),
534
- async execute(_toolCallId, params) {
535
- const result = await answerPermissionTool({
536
- runId: params.run_id,
537
- optionId: params.option_id,
538
- requestId: params.request_id,
539
- supervisorId: params.supervisor_id
540
- });
541
- return {
542
- content: [{
543
- type: "text",
544
- text: JSON.stringify(result, null, 2)
545
- }],
546
- details: result
547
- };
548
- }
549
- });
550
- pi.registerTool({
551
- name: "avenor_follow_up",
552
- label: "Avenor Follow Up",
553
- description: "Resume a completed run with a follow-up message.",
554
- parameters: Type.Object({
555
- run_id: Type.String({ description: "Completed run ID to resume" }),
556
- message: Type.String({ description: "Follow-up message text" }),
557
- label: Type.Optional(Type.String({ description: "Override label (defaults to <original>-followup)" })),
558
- supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
559
- }),
560
- async execute(_toolCallId, params) {
561
- const result = await followUpTool({
562
- runId: params.run_id,
563
- message: params.message,
564
- label: params.label,
565
- supervisorId: params.supervisor_id
566
- });
567
- return {
568
- content: [{
569
- type: "text",
570
- text: JSON.stringify(result, null, 2)
571
- }],
572
- details: result
573
- };
574
- }
575
- });
576
- pi.registerTool({
577
- name: "avenor_events",
578
- label: "Avenor Events",
579
- description: "Read events from a run. Filter by type. Returns last N events.",
580
- parameters: Type.Object({
581
- run_id: Type.String({ description: "Run ID to read events from" }),
582
- types: Type.Optional(Type.Array(Type.String(), { description: "Filter by event types" })),
583
- limit: Type.Optional(Type.Number({ description: "Max events to return (default 50)" })),
584
- supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
585
- }),
586
- async execute(_toolCallId, params) {
587
- const result = await eventsTool({
588
- runId: params.run_id,
589
- types: params.types,
590
- limit: params.limit,
591
- supervisorId: params.supervisor_id
592
- });
593
- return {
594
- content: [{
595
- type: "text",
596
- text: JSON.stringify(result, null, 2)
597
- }],
598
- details: result
599
- };
600
- }
601
- });
602
- pi.registerTool({
603
- name: "avenor_shutdown",
604
- label: "Avenor Shutdown",
605
- description: "Shut down the avenor supervisor and clean up temp files.",
606
- parameters: Type.Object({
607
- supervisor_id: Type.Optional(Type.String({ description: "Supervisor to shut down (defaults to singleton)" })),
608
- force: Type.Optional(Type.Boolean({ description: "Force shutdown instead of graceful" }))
609
- }),
610
- async execute(_toolCallId, params) {
611
- await stopPolling();
612
- trackedRuns.clear();
613
- const result = await shutdownTool({
614
- supervisorId: params.supervisor_id,
615
- force: params.force
616
- });
617
- return {
618
- content: [{
619
- type: "text",
620
- text: JSON.stringify(result, null, 2)
621
- }],
622
- details: result
623
- };
624
- }
625
- });
626
- pi.registerCommand("avenor-status", {
627
- description: "Show status of all avenor runs",
628
- handler: async (_args, ctx) => {
629
- const entries = await pollRuns();
630
- if (entries.length === 0) {
631
- ctx.ui.notify("No active avenor runs", "info");
632
- 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
+ };
633
1288
  }
634
- const lines = entries.map((e) => formatRunLine(e));
635
- ctx.ui.setWidget("avenor-status-detail", lines);
636
- }
637
- });
638
- pi.registerCommand("avenor-watch", {
639
- description: "Open a live event feed for a run",
640
- getArgumentCompletions: (prefix) => {
641
- const items = Array.from(trackedRuns.values()).filter((r) => r.label.startsWith(prefix) || r.runId.startsWith(prefix)).map((r) => ({
642
- value: r.runId,
643
- label: `${r.label} (${r.runId.slice(0, 8)})`
644
- }));
645
- return items.length > 0 ? items : null;
646
- },
647
- handler: async (args, ctx) => {
648
- if (!ctx.hasUI) {
649
- ctx.ui.notify("avenor-watch requires TUI mode", "warning");
650
- 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
+ };
651
1314
  }
652
- let runId = args?.trim();
653
- if (!runId) {
654
- const runList = Array.from(trackedRuns.values());
655
- if (runList.length === 0) {
656
- ctx.ui.notify("No tracked runs", "warning");
657
- 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);
658
1352
  }
659
- const choices = runList.map((r) => r.runId);
660
- const labels = runList.map((r) => `${r.label} (${r.runId.slice(0, 8)})`);
661
- const idx = await ctx.ui.select("Watch run:", labels);
662
- if (idx === null) return;
663
- runId = choices[idx];
1353
+ startPolling();
1354
+ return {
1355
+ content: [{
1356
+ type: "text",
1357
+ text: JSON.stringify(result, null, 2)
1358
+ }],
1359
+ details: result
1360
+ };
664
1361
  }
665
- let watchActive = true;
666
- const overlay = new EventFeedOverlay(runId);
667
- try {
668
- const { events } = await eventsTool({
669
- runId,
670
- 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
671
1379
  });
672
- overlay.setEvents(events);
673
- } catch (err) {
674
- console.error("avenor watch: failed to load initial events", err);
675
- overlay.setEvents([]);
1380
+ return {
1381
+ content: [{
1382
+ type: "text",
1383
+ text: JSON.stringify(result, null, 2)
1384
+ }],
1385
+ details: result
1386
+ };
676
1387
  }
677
- const handle = ctx.ui.custom((_tui, _theme, _kb, done) => {
678
- overlay.setOnClose(() => {
679
- watchActive = false;
680
- 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
681
1403
  });
682
- return overlay;
683
- }, {
684
- overlay: true,
685
- overlayOptions: {
686
- anchor: "right-center",
687
- width: "45%",
688
- minWidth: 60,
689
- maxHeight: "70%"
690
- },
691
- onHandle: (h) => {
692
- 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;
693
1420
  }
694
- });
695
- const watchTick = async () => {
696
- while (watchActive) {
697
- await sleep(POLL_INTERVAL_MS);
698
- try {
699
- const { events } = await eventsTool({
700
- runId,
701
- limit: 50
702
- });
703
- overlay.setEvents(events);
704
- handle.requestRender?.();
705
- } catch (err) {
706
- console.error("avenor watch poll: eventsTool failed", err);
707
- 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();
708
1444
  }
709
- }
710
- };
711
- watchTick();
712
- }
713
- });
714
- pi.registerCommand("avenor-cancel", {
715
- description: "Cancel a running avenor sub-agent",
716
- getArgumentCompletions: (prefix) => {
717
- const items = Array.from(trackedRuns.values()).filter((r) => r.label.startsWith(prefix) || r.runId.startsWith(prefix)).map((r) => ({
718
- value: r.runId,
719
- label: `${r.label} (${r.runId.slice(0, 8)})`
720
- }));
721
- return items.length > 0 ? items : null;
722
- },
723
- handler: async (args, ctx) => {
724
- if (!args?.trim()) {
725
- ctx.ui.notify("Usage: /avenor-cancel <run_id>", "warning");
726
- return;
1445
+ });
727
1446
  }
728
- const runId = args.trim();
729
- const run = trackedRuns.get(runId);
730
- const label = run?.label ?? runId;
731
- if (!await ctx.ui.confirm("Cancel run", `Cancel "${label}" (${runId.slice(0, 8)})?`)) return;
732
- try {
733
- const { Supervisor } = await import("@dougbots/avenor-core");
734
- await (await Supervisor.get()).getClient().cancel(run?.lastStatus?.runtime_id ?? run?.runtimeId ?? runId);
735
- ctx.ui.notify(`Cancelled "${label}"`, "info");
736
- } catch (err) {
737
- 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
+ }
738
1482
  }
739
- }
740
- });
1483
+ });
1484
+ };
741
1485
  }
1486
+ var src_default = createExtension();
742
1487
  //#endregion
743
- export { src_default as default };
1488
+ export { buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, src_default as default, isTerminalStatus, renderStatusLines, statusSupervisorId };