@dougbots/avenor-pi 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -112,7 +112,7 @@ Available tools for LLM sub-agent management:
112
112
 
113
113
  | Tool | Description |
114
114
  |---|---|
115
- | `avenor_spawn` | Dispatch an agent run (blocking or fire-and-forget). Uses the `pi` backend by default; accepts `agent`, `model`, `prompt`, and `dir`. |
115
+ | `avenor_spawn` | Dispatch an agent run (blocking or fire-and-forget). Uses the `pi` backend by default in direct mode; accepts optional `agent`, `model`, `prompt`, `dir`, `roster_file`, and `roster_entry`. |
116
116
  | `avenor_status` | Get status of a run or all runs; use `view="lifecycle"` for compact progress and permission checks |
117
117
  | `avenor_result` | Wait for a run and return its complete final output without transcript details |
118
118
  | `avenor_inspect` | Review a bounded transcript, tool activity, permissions, and final output |
@@ -121,17 +121,33 @@ Available tools for LLM sub-agent management:
121
121
  | `avenor_events` | Read events from a run |
122
122
  | `avenor_shutdown` | Shut down the avenor supervisor |
123
123
 
124
+ ### Direct and roster spawns
125
+
126
+ Direct mode permits `agent`, `model`, and `backend` to be omitted independently, including a backend-only request; omitted values use runtime defaults. Pi supplies `backend: "pi"` for direct tool calls when no backend is given. Roster mode uses both `roster_file` and `roster_entry`, and leaves backend selection to the roster entry instead of applying Pi's direct default:
127
+
128
+ ```json
129
+ {
130
+ "prompt": "Review the repository",
131
+ "roster_file": "/repo/roster.json",
132
+ "roster_entry": "reviewer"
133
+ }
134
+ ```
135
+
136
+ Each roster entry must contain `backend` and at least one of `agent` or `model`. Do not combine roster mode with direct `agent`, `model`, or `backend` overrides. Roster entries currently do not support `system` or `thinking`; `thinking` remains a run-level option and is validated against the effective backend. A resumed session must stay on its original backend. Use an absolute `roster_file` path when the Pi process and target project have different working directories.
137
+
124
138
  ### Pi-specific features
125
139
 
126
140
  Beyond the tools, the extension integrates with Pi's TUI and event system:
127
141
 
128
142
  - **Status widget** — persistent widget showing all active runs with status, phase, and permission state
129
- - **Footer status** — active runs shown in the Pi footer bar
143
+ - **Footer status** — active runs and polling error counts shown in the Pi footer bar
144
+ - **Polling diagnostics** — bounded polling errors are emitted on `avenor:poll:error` for companion extensions
130
145
  - **Live progress** — blocking `avenor_spawn` calls stream progress updates via `onUpdate`
131
146
  - **Context enrichment** — active sub-agents are automatically surfaced in the system prompt via `before_agent_start`
132
147
  - **Custom rendering** — The Pi extension renders all Avenor tool calls and results as themed, bounded summaries. Results have collapsed and expanded views.
133
148
  - **Commands** — interactive commands for run management:
134
149
  - `/avenor-status` — show status of all runs
150
+ - `/avenor-errors` — show and clear recent polling errors
135
151
  - `/avenor-watch <run_id>` — open a live event feed overlay
136
152
  - `/avenor-cancel <run_id>` — cancel a running sub-agent
137
153
 
package/dist/index.d.ts CHANGED
@@ -28,6 +28,31 @@ interface RunStatusEntry {
28
28
  declare const CHANNEL_POLL_COMPLETED: "avenor:poll:completed";
29
29
  /** Emitted when the extension observes a terminal run result. */
30
30
  declare const CHANNEL_RUN_TERMINAL: "avenor:run:terminal";
31
+ /** Emitted when a best-effort polling operation fails. */
32
+ declare const CHANNEL_POLL_ERROR: "avenor:poll:error";
33
+ type PollErrorSource = 'singleton-list' | 'run-status' | 'spawn-status';
34
+ interface PollErrorPayload {
35
+ /** Polling operation that failed. */
36
+ readonly source: PollErrorSource;
37
+ /** Run associated with the failure, when the operation was run-specific. */
38
+ readonly runId?: string;
39
+ /** Human-readable operation description. */
40
+ readonly message: string;
41
+ /** Bounded, sanitized error text suitable for debug consumers. */
42
+ readonly error: string;
43
+ /** Number of polling errors observed by this extension instance. */
44
+ readonly count: number;
45
+ /** Wall-clock timestamp in epoch milliseconds. */
46
+ readonly timestamp: number;
47
+ }
48
+ interface PollErrorPayloadInput {
49
+ readonly source: PollErrorSource;
50
+ readonly runId?: string;
51
+ readonly message: string;
52
+ readonly error: unknown;
53
+ readonly count: number;
54
+ readonly timestamp: number;
55
+ }
31
56
  type AvenorRunStatus = Readonly<Omit<RunStatusEntry, 'supervisorId' | 'pid' | 'permissionDescription'> & {
32
57
  supervisorKey: string;
33
58
  }>;
@@ -65,6 +90,7 @@ interface RunTerminalPayloadInput {
65
90
  interface AvenorEventMap {
66
91
  [CHANNEL_POLL_COMPLETED]: PollCompletedPayload;
67
92
  [CHANNEL_RUN_TERMINAL]: RunTerminalPayload;
93
+ [CHANNEL_POLL_ERROR]: PollErrorPayload;
68
94
  }
69
95
  type AvenorEventChannel = keyof AvenorEventMap;
70
96
  type AvenorEventBus = Pick<EventBus, 'emit' | 'on'>;
@@ -80,6 +106,8 @@ declare function onAvenorEvent<K extends AvenorEventChannel>(bus: Pick<EventBus,
80
106
  declare function createPollCompletedPayload(entries: readonly RunStatusEntry[], timestamp: number, generation: number): PollCompletedPayload;
81
107
  /** Copy and freeze a terminal payload before publishing it. */
82
108
  declare function createRunTerminalPayload(payload: RunTerminalPayloadInput): RunTerminalPayload;
109
+ /** Copy, bound, sanitize, and freeze a polling error before publishing it. */
110
+ declare function createPollErrorPayload(payload: PollErrorPayloadInput): PollErrorPayload;
83
111
  //#endregion
84
112
  //#region src/index.d.ts
85
113
  interface ExtensionDeps {
@@ -96,6 +124,15 @@ interface ExtensionDeps {
96
124
  Supervisor: typeof Supervisor;
97
125
  }
98
126
  declare function statusSupervisorId(requestedSupervisorId: string | undefined, spawnedSupervisorId: string): string | undefined;
127
+ type SpawnHostParams = {
128
+ agent?: string;
129
+ model?: string;
130
+ backend?: string;
131
+ roster_file?: string;
132
+ roster_entry?: string;
133
+ };
134
+ /** Project shared spawn identity into Pi tool metadata without resolving rosters here. */
135
+ declare function spawnIdentityMetadata(params: SpawnHostParams, status?: Pick<StatusResult, 'roster_file' | 'roster_entry' | 'agent' | 'model' | 'backend' | 'effective_agent' | 'effective_model' | 'effective_backend'>): Record<string, unknown>;
99
136
  declare function buildCompletionText(run: {
100
137
  runId: string;
101
138
  label: string;
@@ -107,4 +144,4 @@ declare function buildInspectPayload(result: InspectResult): Record<string, unkn
107
144
  declare function createExtension(deps?: ExtensionDeps): (pi: ExtensionAPI) => Promise<void>;
108
145
  declare const _default: (pi: ExtensionAPI) => Promise<void>;
109
146
  //#endregion
110
- export { type AvenorEventBus, type AvenorEventChannel, type AvenorEventMap, type AvenorRunStatus, CHANNEL_POLL_COMPLETED, CHANNEL_RUN_TERMINAL, ExtensionDeps, type PollCompletedPayload, type RunTerminalPayload, type RunTerminalPayloadInput, buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, createPollCompletedPayload, createRunTerminalPayload, _default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, statusSupervisorId };
147
+ export { type AvenorEventBus, type AvenorEventChannel, type AvenorEventMap, type AvenorRunStatus, CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, ExtensionDeps, type PollCompletedPayload, type PollErrorPayload, type PollErrorPayloadInput, type PollErrorSource, type RunTerminalPayload, type RunTerminalPayloadInput, buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, createPollCompletedPayload, createPollErrorPayload, createRunTerminalPayload, _default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, spawnIdentityMetadata, statusSupervisorId };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Input, Key, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
2
  import { Type } from "typebox";
3
- import { Supervisor, answerPermissionTool, dial, eventsTool, extractEventText, followUpTool, inspectTool, observeRun, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool } from "@dougbots/avenor-core";
3
+ import { Supervisor, answerPermissionTool, dial, eventsTool, extractEventText, followUpTool, inspectTool, observeRun, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection } from "@dougbots/avenor-core";
4
4
  import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import { createHash } from "node:crypto";
@@ -718,7 +718,15 @@ function resolveAgentProfile(entries, environment = process.env) {
718
718
  const CHANNEL_POLL_COMPLETED = "avenor:poll:completed";
719
719
  /** Emitted when the extension observes a terminal run result. */
720
720
  const CHANNEL_RUN_TERMINAL = "avenor:run:terminal";
721
- /** Derive a stable non-connectable scope from a local supervisor socket. */
721
+ /** Emitted when a best-effort polling operation fails. */
722
+ const CHANNEL_POLL_ERROR = "avenor:poll:error";
723
+ /** Maximum sanitized error text exposed to companion extensions. */
724
+ const POLL_ERROR_TEXT_CHARS = 600;
725
+ function sanitizeAndBoundText(value) {
726
+ const text = sanitizeText(String(value)).trim();
727
+ if (!text) return "unknown error";
728
+ return text.length <= POLL_ERROR_TEXT_CHARS ? text : `${text.slice(0, POLL_ERROR_TEXT_CHARS - 1)}…`;
729
+ }
722
730
  function createSupervisorKey(supervisorId) {
723
731
  if (!supervisorId) return "singleton";
724
732
  return `supervisor:${createHash("sha256").update(supervisorId).digest("hex").slice(0, 16)}`;
@@ -769,6 +777,17 @@ function createRunTerminalPayload(payload) {
769
777
  ...payload.backend !== void 0 && { backend: payload.backend }
770
778
  });
771
779
  }
780
+ /** Copy, bound, sanitize, and freeze a polling error before publishing it. */
781
+ function createPollErrorPayload(payload) {
782
+ return Object.freeze({
783
+ source: payload.source,
784
+ ...payload.runId !== void 0 && { runId: payload.runId },
785
+ message: sanitizeAndBoundText(payload.message),
786
+ error: sanitizeAndBoundText(payload.error),
787
+ count: payload.count,
788
+ timestamp: payload.timestamp
789
+ });
790
+ }
772
791
  //#endregion
773
792
  //#region src/render.ts
774
793
  const FINAL_OUTPUT_CHARS = 600;
@@ -1031,9 +1050,11 @@ function eventsLines(details, args, expanded) {
1031
1050
  const displayedTypes = types.slice(0, COLLECTION_ITEMS).map((type) => scalar(type)).join(", ");
1032
1051
  const lines = [`Events: ${runId} — ${events.length} events`, ...types.length > 0 ? [`Filter: ${displayedTypes}${types.length > COLLECTION_ITEMS ? ` ${itemMarker(types.length - COLLECTION_ITEMS)}` : ""}`] : []];
1033
1052
  if (events.length === 0) return [...lines, "No events."];
1034
- lines.push(`Preview: ${eventRow(events[0])}`);
1035
- if (events.length > 1) lines.push(itemMarker(events.length - 1));
1036
- if (!expanded) return lines;
1053
+ if (!expanded) {
1054
+ lines.push(`Preview: ${eventRow(events[0])}`);
1055
+ if (events.length > 1) lines.push(itemMarker(events.length - 1));
1056
+ return lines;
1057
+ }
1037
1058
  const displayed = events.slice(0, STATUS_ROWS);
1038
1059
  lines.push(...displayed.map(eventRow));
1039
1060
  if (events.length > displayed.length) lines.push(itemMarker(events.length - displayed.length));
@@ -1226,6 +1247,7 @@ const INSPECT_JSON_STRING_CHARS = 600;
1226
1247
  const INSPECT_JSON_ARRAY_ITEMS = 24;
1227
1248
  const INSPECT_JSON_OBJECT_KEYS = 24;
1228
1249
  const INSPECT_JSON_DEPTH = 8;
1250
+ const MAX_POLL_ERRORS = 100;
1229
1251
  const defaultDeps = {
1230
1252
  spawnTool,
1231
1253
  statusTool,
@@ -1242,6 +1264,19 @@ const defaultDeps = {
1242
1264
  function statusSupervisorId(requestedSupervisorId, spawnedSupervisorId) {
1243
1265
  return requestedSupervisorId ? spawnedSupervisorId : void 0;
1244
1266
  }
1267
+ /** Project shared spawn identity into Pi tool metadata without resolving rosters here. */
1268
+ function spawnIdentityMetadata(params, status) {
1269
+ const metadata = {};
1270
+ const add = (key, value) => {
1271
+ if (typeof value === "string" && value.length > 0) metadata[key] = value;
1272
+ };
1273
+ add("roster_file", status?.roster_file ?? params.roster_file);
1274
+ add("roster_entry", status?.roster_entry ?? params.roster_entry);
1275
+ add("effective_agent", status?.effective_agent ?? status?.agent ?? params.agent);
1276
+ add("effective_model", status?.effective_model ?? status?.model ?? params.model);
1277
+ add("effective_backend", status?.effective_backend ?? status?.backend ?? params.backend);
1278
+ return metadata;
1279
+ }
1245
1280
  function sleep(ms) {
1246
1281
  return new Promise((resolve) => setTimeout(resolve, ms));
1247
1282
  }
@@ -1267,7 +1302,8 @@ function buildCompletionText(run, result, finalOutput) {
1267
1302
  }
1268
1303
  function buildWaitingText(run, status) {
1269
1304
  const perm = status.pending_permission;
1270
- const lines = [`Sub-agent "${run.label}" (${run.agent}) is waiting for input.`];
1305
+ const agent = run.agent ?? status.effective_agent ?? status.agent ?? "roster";
1306
+ const lines = [`Sub-agent "${run.label}" (${agent}) is waiting for input.`];
1271
1307
  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"`);
1272
1308
  else if (status.phase_label) lines.push(`Question: ${status.phase_label}`);
1273
1309
  if (!perm) lines.push(`Use \`avenor_follow_up\` with run_id "${run.runId}" to respond, or \`avenor_inspect\` to review context first.`);
@@ -1359,18 +1395,27 @@ function createExtension(deps = defaultDeps) {
1359
1395
  let pollingGeneration = 0;
1360
1396
  let pollInFlight = null;
1361
1397
  let sessionCtx = null;
1362
- const reportedPollingFailures = /* @__PURE__ */ new Set();
1363
- function reportPollingFailure(key, message, error) {
1364
- if (reportedPollingFailures.has(key)) return;
1365
- reportedPollingFailures.add(key);
1366
- console.error(message, error);
1367
- }
1368
- function clearPollingFailure(key) {
1369
- reportedPollingFailures.delete(key);
1398
+ let pollingErrorCount = 0;
1399
+ const pollingErrors = [];
1400
+ let lastStatusEntries = [];
1401
+ function recordPollingError(source, message, error, runId) {
1402
+ pollingErrorCount++;
1403
+ const payload = createPollErrorPayload({
1404
+ source,
1405
+ runId,
1406
+ message,
1407
+ error,
1408
+ count: pollingErrorCount,
1409
+ timestamp: Date.now()
1410
+ });
1411
+ pollingErrors.push(payload);
1412
+ if (pollingErrors.length > MAX_POLL_ERRORS) pollingErrors.shift();
1413
+ emitAvenorEvent(pi.events, CHANNEL_POLL_ERROR, payload);
1370
1414
  }
1371
- function clearRunPollingFailures(run) {
1372
- clearPollingFailure(`run-status:${run.supervisorId ?? "singleton"}:${run.runId}`);
1373
- clearPollingFailure(`spawn-status:${run.supervisorId ?? "singleton"}:${run.runId}`);
1415
+ function clearPollingErrors() {
1416
+ pollingErrorCount = 0;
1417
+ pollingErrors.length = 0;
1418
+ updateWidget(lastStatusEntries);
1374
1419
  }
1375
1420
  function upsertTrackedRun(run, status, blocking = false) {
1376
1421
  const current = trackedRuns.get(run.runId);
@@ -1402,22 +1447,22 @@ function createExtension(deps = defaultDeps) {
1402
1447
  }
1403
1448
  }
1404
1449
  async function pollRuns() {
1450
+ const errorCountAtPollStart = pollingErrorCount;
1405
1451
  const liveMap = /* @__PURE__ */ new Map();
1406
1452
  try {
1407
1453
  const allLive = await deps.statusTool({});
1408
1454
  const liveList = Array.isArray(allLive) ? allLive : [allLive];
1409
1455
  for (const result of liveList) liveMap.set(result.run_id, result);
1410
- clearPollingFailure("singleton-list");
1411
1456
  } catch (err) {
1412
- reportPollingFailure("singleton-list", "avenor pollRuns: failed to list singleton runs", err);
1457
+ recordPollingError("singleton-list", "failed to list singleton runs", err);
1413
1458
  }
1414
1459
  const entries = [];
1415
1460
  for (const run of trackedRuns.values()) {
1416
1461
  const live = findLiveStatusForTrackedRun(run, liveMap.values());
1417
1462
  if (live) {
1418
1463
  liveMap.delete(live.run_id);
1419
- clearPollingFailure(`run-status:${run.supervisorId ?? "singleton"}:${run.runId}`);
1420
1464
  run.lastStatus = live;
1465
+ run.agent = live.agent ?? run.agent;
1421
1466
  entries.push({
1422
1467
  runId: run.runId,
1423
1468
  supervisorId: run.supervisorId,
@@ -1426,7 +1471,7 @@ function createExtension(deps = defaultDeps) {
1426
1471
  status: live.status,
1427
1472
  phase: live.phase,
1428
1473
  phaseLabel: live.phase_label,
1429
- agent: run.agent,
1474
+ agent: run.agent ?? "unknown",
1430
1475
  pendingPermission: !!live.pending_permission,
1431
1476
  permissionDescription: live.pending_permission?.description,
1432
1477
  pid: live.pid,
@@ -1441,8 +1486,8 @@ function createExtension(deps = defaultDeps) {
1441
1486
  });
1442
1487
  const result = Array.isArray(raw) ? raw[0] : raw;
1443
1488
  if (!result) throw new Error("status response was empty");
1444
- clearPollingFailure(`run-status:${run.supervisorId ?? "singleton"}:${run.runId}`);
1445
1489
  run.lastStatus = result;
1490
+ run.agent = result.agent ?? run.agent;
1446
1491
  entries.push({
1447
1492
  runId: run.runId,
1448
1493
  supervisorId: run.supervisorId,
@@ -1451,14 +1496,14 @@ function createExtension(deps = defaultDeps) {
1451
1496
  status: result.status,
1452
1497
  phase: result.phase,
1453
1498
  phaseLabel: result.phase_label,
1454
- agent: run.agent,
1499
+ agent: run.agent ?? "unknown",
1455
1500
  pendingPermission: !!result.pending_permission,
1456
1501
  permissionDescription: result.pending_permission?.description,
1457
1502
  pid: result.pid,
1458
1503
  backend: result.backend
1459
1504
  });
1460
1505
  } catch (err) {
1461
- reportPollingFailure(`run-status:${run.supervisorId ?? "singleton"}:${run.runId}`, `avenor pollRuns: statusTool failed for ${run.runId}`, err);
1506
+ recordPollingError("run-status", "statusTool failed while polling a run", err, run.runId);
1462
1507
  const previous = run.lastStatus;
1463
1508
  entries.push({
1464
1509
  runId: run.runId,
@@ -1468,7 +1513,7 @@ function createExtension(deps = defaultDeps) {
1468
1513
  status: previous?.status ?? "unavailable",
1469
1514
  phase: previous?.phase,
1470
1515
  phaseLabel: previous?.phase_label,
1471
- agent: run.agent,
1516
+ agent: run.agent ?? previous?.agent ?? "unknown",
1472
1517
  pendingPermission: !!previous?.pending_permission,
1473
1518
  permissionDescription: previous?.pending_permission?.description,
1474
1519
  pid: previous?.pid,
@@ -1495,21 +1540,24 @@ function createExtension(deps = defaultDeps) {
1495
1540
  let idx = 0;
1496
1541
  for (const entry of entries) if (entry.backend === "pi" && entry.pid && !isTerminalStatus(entry.status)) entry.nestedCount = nestedCounts[idx++] ?? 0;
1497
1542
  }
1543
+ if (pollingErrorCount === errorCountAtPollStart) pollingErrorCount = 0;
1498
1544
  return entries;
1499
1545
  }
1500
1546
  function updateWidget(entries) {
1547
+ lastStatusEntries = entries;
1501
1548
  if (!sessionCtx) return;
1502
1549
  const lines = renderStatusLines(entries);
1550
+ const errorSuffix = pollingErrorCount > 0 ? `errors:${pollingErrorCount}` : "";
1503
1551
  if (lines.length > 0) {
1504
1552
  sessionCtx.ui.setWidget("avenor-status", lines);
1505
1553
  const statusStr = lines.map((line) => line.slice(0, 60)).join(" ");
1506
1554
  const nestedTotal = entries.filter((e) => !isTerminalStatus(e.status)).reduce((sum, e) => sum + (e.nestedCount ?? 0), 0);
1507
1555
  const total = lines.length + nestedTotal;
1508
- const statusWithTotal = nestedTotal > 0 ? `${statusStr} total:${total}` : statusStr;
1556
+ const statusWithTotal = [nestedTotal > 0 ? `${statusStr} total:${total}` : statusStr, errorSuffix].filter(Boolean).join(" ");
1509
1557
  sessionCtx.ui.setStatus("avenor", statusWithTotal);
1510
1558
  } else {
1511
1559
  sessionCtx.ui.setWidget("avenor-status", void 0);
1512
- sessionCtx.ui.setStatus("avenor", "");
1560
+ sessionCtx.ui.setStatus("avenor", errorSuffix);
1513
1561
  }
1514
1562
  }
1515
1563
  function startPolling() {
@@ -1560,7 +1608,6 @@ function createExtension(deps = defaultDeps) {
1560
1608
  nestedCount: entry.nestedCount,
1561
1609
  backend: entry.backend
1562
1610
  }));
1563
- clearRunPollingFailures(run);
1564
1611
  trackedRuns.delete(entry.runId);
1565
1612
  } else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
1566
1613
  run.permissionNotified = true;
@@ -1583,9 +1630,7 @@ function createExtension(deps = defaultDeps) {
1583
1630
  }, POLL_INTERVAL_MS);
1584
1631
  else {
1585
1632
  pollingActive = false;
1586
- reportedPollingFailures.clear();
1587
- sessionCtx?.ui.setStatus("avenor", "");
1588
- sessionCtx?.ui.setWidget("avenor-status", void 0);
1633
+ updateWidget([]);
1589
1634
  }
1590
1635
  };
1591
1636
  tick().catch((err) => {
@@ -1596,10 +1641,11 @@ function createExtension(deps = defaultDeps) {
1596
1641
  async function stopPolling() {
1597
1642
  pollingActive = false;
1598
1643
  pollingGeneration++;
1599
- reportedPollingFailures.clear();
1600
1644
  await pollInFlight?.catch(() => {});
1601
- sessionCtx?.ui.setStatus("avenor", "");
1602
- sessionCtx?.ui.setWidget("avenor-status", void 0);
1645
+ pollingErrorCount = 0;
1646
+ pollingErrors.length = 0;
1647
+ lastStatusEntries = [];
1648
+ updateWidget([]);
1603
1649
  }
1604
1650
  function createWatchDeps() {
1605
1651
  async function resolveSupervisorSocket(run) {
@@ -1769,7 +1815,6 @@ function createExtension(deps = defaultDeps) {
1769
1815
  const status = Array.isArray(raw) ? raw[0] : raw;
1770
1816
  if (!status) continue;
1771
1817
  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})` : ""}`;
1772
- clearPollingFailure(`spawn-status:${run.supervisorId ?? "singleton"}:${run.runId}`);
1773
1818
  onUpdate?.({
1774
1819
  content: [{
1775
1820
  type: "text",
@@ -1785,7 +1830,7 @@ function createExtension(deps = defaultDeps) {
1785
1830
  aborted: false
1786
1831
  };
1787
1832
  } catch (err) {
1788
- reportPollingFailure(`spawn-status:${run.supervisorId ?? "singleton"}:${run.runId}`, "avenor spawn poll: statusTool failed", err);
1833
+ recordPollingError("spawn-status", "statusTool failed while waiting for a run", err, run.runId);
1789
1834
  }
1790
1835
  }
1791
1836
  return { aborted: true };
@@ -1797,6 +1842,9 @@ function createExtension(deps = defaultDeps) {
1797
1842
  }
1798
1843
  pi.on("session_start", async (_event, ctx) => {
1799
1844
  sessionCtx = ctx;
1845
+ lastStatusEntries = [];
1846
+ pollingErrorCount = 0;
1847
+ pollingErrors.length = 0;
1800
1848
  });
1801
1849
  pi.on("session_shutdown", async () => {
1802
1850
  await stopPolling();
@@ -1840,7 +1888,7 @@ function createExtension(deps = defaultDeps) {
1840
1888
  "Use avenor_shutdown when done delegating work."
1841
1889
  ],
1842
1890
  parameters: Type.Object({
1843
- agent: Type.String({ description: "Agent name (required, no default)" }),
1891
+ agent: Type.Optional(Type.String({ description: "Optional agent name; omission uses the supplied model or runtime defaults" })),
1844
1892
  prompt: Type.Optional(Type.String({ description: "Prompt text" })),
1845
1893
  prompt_file: Type.Optional(Type.String({ description: "Path to prompt file" })),
1846
1894
  dir: Type.Optional(Type.String({ description: "Working directory (defaults to session directory)" })),
@@ -1856,7 +1904,9 @@ function createExtension(deps = defaultDeps) {
1856
1904
  Type.Literal("xhigh"),
1857
1905
  Type.Literal("max")
1858
1906
  ], { description: "Thinking level; omission uses the backend default" })),
1859
- backend: Type.Optional(Type.String({ description: "Backend override (defaults to pi)" })),
1907
+ backend: Type.Optional(Type.String({ description: "Backend override (defaults to pi for direct mode)" })),
1908
+ roster_file: Type.Optional(Type.String({ description: "Path to the roster map" })),
1909
+ roster_entry: Type.Optional(Type.String({ description: "Roster entry to select" })),
1860
1910
  server_url: Type.Optional(Type.String({ description: "Backend server URL" })),
1861
1911
  supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
1862
1912
  wait: Type.Optional(Type.Boolean({ description: "Block until complete (default true)" }))
@@ -1866,25 +1916,39 @@ function createExtension(deps = defaultDeps) {
1866
1916
  type: "text",
1867
1917
  text: "Cancelled"
1868
1918
  }] };
1919
+ validateSpawnSelection({
1920
+ agent: params.agent,
1921
+ model: params.model,
1922
+ backend: params.backend,
1923
+ roster_file: params.roster_file,
1924
+ roster_entry: params.roster_entry
1925
+ });
1869
1926
  const wait = params.wait ?? true;
1870
1927
  const dir = params.dir ?? ctx.cwd;
1871
- const label = params.label ?? `${params.agent}-${Date.now()}`;
1872
- const backend = params.backend ?? "pi";
1928
+ const label = params.label ?? `${params.agent ?? params.roster_entry ?? params.model ?? "avenor"}-${Date.now()}`;
1929
+ const rosterMode = Boolean(params.roster_entry);
1930
+ const backend = rosterMode ? params.backend : params.backend ?? "pi";
1873
1931
  const entries = ctx.sessionManager?.getEntries?.();
1874
- const agentProfile = backend === "pi" ? resolveAgentProfile(entries) : void 0;
1932
+ const agentProfile = rosterMode || backend === "pi" ? resolveAgentProfile(entries) : void 0;
1933
+ const hostParams = {
1934
+ ...params,
1935
+ backend
1936
+ };
1875
1937
  const result = await deps.spawnTool({
1876
- agent: params.agent,
1877
- prompt: params.prompt,
1878
- promptFile: params.prompt_file,
1938
+ ...params.agent !== void 0 ? { agent: params.agent } : {},
1939
+ ...params.prompt !== void 0 ? { prompt: params.prompt } : {},
1940
+ ...params.prompt_file !== void 0 ? { promptFile: params.prompt_file } : {},
1879
1941
  label,
1880
1942
  dir,
1881
- timeout: params.timeout,
1882
- model: params.model,
1883
- thinking: params.thinking,
1884
- backend,
1885
- agentProfile,
1886
- serverUrl: params.server_url,
1887
- supervisorId: params.supervisor_id
1943
+ ...params.timeout !== void 0 ? { timeout: params.timeout } : {},
1944
+ ...params.model !== void 0 ? { model: params.model } : {},
1945
+ ...params.thinking !== void 0 ? { thinking: params.thinking } : {},
1946
+ ...backend !== void 0 ? { backend } : {},
1947
+ ...params.roster_file !== void 0 ? { rosterFile: params.roster_file } : {},
1948
+ ...params.roster_entry !== void 0 ? { rosterEntry: params.roster_entry } : {},
1949
+ ...agentProfile !== void 0 ? { agentProfile } : {},
1950
+ ...params.server_url !== void 0 ? { serverUrl: params.server_url } : {},
1951
+ ...params.supervisor_id !== void 0 ? { supervisorId: params.supervisor_id } : {}
1888
1952
  });
1889
1953
  const supervisorId = statusSupervisorId(params.supervisor_id, result.supervisor_id);
1890
1954
  const runRef = {
@@ -1903,7 +1967,8 @@ function createExtension(deps = defaultDeps) {
1903
1967
  }],
1904
1968
  details: {
1905
1969
  run_id: result.run_id,
1906
- label
1970
+ label,
1971
+ ...spawnIdentityMetadata(hostParams)
1907
1972
  }
1908
1973
  };
1909
1974
  onUpdate?.({
@@ -1918,14 +1983,16 @@ function createExtension(deps = defaultDeps) {
1918
1983
  });
1919
1984
  const waitResult = await waitForRun(runRef, signal, onUpdate);
1920
1985
  if (waitResult.aborted) {
1921
- clearRunPollingFailures(runRef);
1922
1986
  trackedRuns.delete(result.run_id);
1923
1987
  return {
1924
1988
  content: [{
1925
1989
  type: "text",
1926
1990
  text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status or avenor_inspect to check.`
1927
1991
  }],
1928
- details: { run_id: result.run_id }
1992
+ details: {
1993
+ run_id: result.run_id,
1994
+ ...spawnIdentityMetadata(hostParams)
1995
+ }
1929
1996
  };
1930
1997
  }
1931
1998
  const status = waitResult.status;
@@ -1936,7 +2003,8 @@ function createExtension(deps = defaultDeps) {
1936
2003
  }],
1937
2004
  details: {
1938
2005
  status: "running",
1939
- run_id: result.run_id
2006
+ run_id: result.run_id,
2007
+ ...spawnIdentityMetadata(hostParams)
1940
2008
  }
1941
2009
  };
1942
2010
  upsertTrackedRun(runRef, status, wait);
@@ -1956,7 +2024,8 @@ function createExtension(deps = defaultDeps) {
1956
2024
  status: "waiting",
1957
2025
  run_id: result.run_id,
1958
2026
  session_id: status.session_id,
1959
- ...status.pending_permission && { pending_permission: status.pending_permission }
2027
+ ...status.pending_permission && { pending_permission: status.pending_permission },
2028
+ ...spawnIdentityMetadata(hostParams, status)
1960
2029
  }
1961
2030
  };
1962
2031
  }
@@ -1974,7 +2043,6 @@ function createExtension(deps = defaultDeps) {
1974
2043
  status: status.status,
1975
2044
  backend: status.backend
1976
2045
  }));
1977
- clearRunPollingFailures(runRef);
1978
2046
  trackedRuns.delete(result.run_id);
1979
2047
  return {
1980
2048
  content: [{
@@ -1988,7 +2056,8 @@ function createExtension(deps = defaultDeps) {
1988
2056
  status: status.status,
1989
2057
  run_id: result.run_id,
1990
2058
  session_id: status.session_id,
1991
- final_output: finalOutput
2059
+ final_output: finalOutput,
2060
+ ...spawnIdentityMetadata(hostParams, status)
1992
2061
  }
1993
2062
  };
1994
2063
  }
@@ -1999,13 +2068,14 @@ function createExtension(deps = defaultDeps) {
1999
2068
  }],
2000
2069
  details: {
2001
2070
  status: status.status,
2002
- run_id: result.run_id
2071
+ run_id: result.run_id,
2072
+ ...spawnIdentityMetadata(hostParams, status)
2003
2073
  }
2004
2074
  };
2005
2075
  },
2006
2076
  renderCall(args, theme) {
2007
2077
  const agent = theme.fg("toolTitle", theme.bold("avenor_spawn "));
2008
- const agentName = theme.fg("accent", args.agent);
2078
+ const agentName = theme.fg("accent", args.agent ?? args.roster_entry ?? args.model ?? "roster");
2009
2079
  const label = args.label ? theme.fg("dim", ` "${args.label}"`) : "";
2010
2080
  const wait = args.wait === false ? theme.fg("warning", " [async]") : "";
2011
2081
  return new Text(agent + agentName + label + wait, 0, 0);
@@ -2094,10 +2164,6 @@ function createExtension(deps = defaultDeps) {
2094
2164
  agent: tracked?.agent ?? "unknown",
2095
2165
  status: result.status ?? "done"
2096
2166
  }));
2097
- clearRunPollingFailures({
2098
- runId: params.run_id,
2099
- supervisorId: tracked?.supervisorId ?? params.supervisor_id
2100
- });
2101
2167
  trackedRuns.delete(params.run_id);
2102
2168
  } else if (tracked && result.status === "waiting") tracked.permissionNotified = true;
2103
2169
  return {
@@ -2321,6 +2387,7 @@ function createExtension(deps = defaultDeps) {
2321
2387
  description: "Show status of all avenor runs",
2322
2388
  handler: async (_args, ctx) => {
2323
2389
  const entries = await pollRuns();
2390
+ updateWidget(entries);
2324
2391
  if (entries.length === 0) {
2325
2392
  ctx.ui.notify("No active avenor runs", "info");
2326
2393
  return;
@@ -2328,12 +2395,28 @@ function createExtension(deps = defaultDeps) {
2328
2395
  ctx.ui.setWidget("avenor-status-detail", entries.map((entry) => formatRunLine(entry)));
2329
2396
  }
2330
2397
  });
2398
+ pi.registerCommand("avenor-errors", {
2399
+ description: "Show and clear recent Avenor polling errors",
2400
+ handler: async (_args, ctx) => {
2401
+ if (pollingErrors.length === 0) {
2402
+ ctx.ui.setWidget("avenor-errors", void 0);
2403
+ ctx.ui.notify("No recorded avenor polling errors", "info");
2404
+ return;
2405
+ }
2406
+ const errorLines = pollingErrors.map((error) => {
2407
+ const run = error.runId ? ` [${error.runId}]` : "";
2408
+ return `${new Date(error.timestamp).toLocaleTimeString()} ${error.message}${run}: ${error.error}`;
2409
+ });
2410
+ ctx.ui.setWidget("avenor-errors", errorLines);
2411
+ clearPollingErrors();
2412
+ }
2413
+ });
2331
2414
  pi.registerCommand("avenor-watch", {
2332
2415
  description: "Open a live run inspector by run ID, label, or agent name",
2333
2416
  getArgumentCompletions: (prefix) => {
2334
- const items = Array.from(trackedRuns.values()).filter((run) => run.label.startsWith(prefix) || run.agent.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => ({
2417
+ const items = Array.from(trackedRuns.values()).filter((run) => run.label.startsWith(prefix) || run.agent?.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => ({
2335
2418
  value: run.runId,
2336
- label: `${sanitizeText(run.label)} (${sanitizeText(run.agent)}, ${sanitizeText(run.runId).slice(0, 8)})`
2419
+ label: `${sanitizeText(run.label)} (${sanitizeText(run.agent ?? "roster")}, ${sanitizeText(run.runId).slice(0, 8)})`
2337
2420
  }));
2338
2421
  return items.length > 0 ? items : null;
2339
2422
  },
@@ -2355,9 +2438,9 @@ function createExtension(deps = defaultDeps) {
2355
2438
  pi.registerCommand("avenor-cancel", {
2356
2439
  description: "Cancel a running avenor sub-agent",
2357
2440
  getArgumentCompletions: (prefix) => {
2358
- const items = Array.from(trackedRuns.values()).filter((run) => run.label.startsWith(prefix) || run.agent.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => ({
2441
+ const items = Array.from(trackedRuns.values()).filter((run) => run.label.startsWith(prefix) || run.agent?.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => ({
2359
2442
  value: run.runId,
2360
- label: `${sanitizeText(run.label)} (${sanitizeText(run.agent)}, ${sanitizeText(run.runId).slice(0, 8)})`
2443
+ label: `${sanitizeText(run.label)} (${sanitizeText(run.agent ?? "roster")}, ${sanitizeText(run.runId).slice(0, 8)})`
2361
2444
  }));
2362
2445
  return items.length > 0 ? items : null;
2363
2446
  },
@@ -2392,4 +2475,4 @@ function createExtension(deps = defaultDeps) {
2392
2475
  }
2393
2476
  var src_default = createExtension();
2394
2477
  //#endregion
2395
- export { CHANNEL_POLL_COMPLETED, CHANNEL_RUN_TERMINAL, buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, createPollCompletedPayload, createRunTerminalPayload, src_default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, statusSupervisorId };
2478
+ export { CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, createPollCompletedPayload, createPollErrorPayload, createRunTerminalPayload, src_default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, spawnIdentityMetadata, statusSupervisorId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-pi",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -35,7 +35,7 @@
35
35
  "dev": "tsdown --watch"
36
36
  },
37
37
  "dependencies": {
38
- "@dougbots/avenor-core": "^0.9.0"
38
+ "@dougbots/avenor-core": "^0.10.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@earendil-works/pi-coding-agent": ">=0.74.0",