@dougbots/avenor-pi 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -1,4 +1,4 @@
1
- import { InspectResult, StatusResult, Supervisor, answerPermissionTool, dial, eventsTool, followUpTool, inspectTool, observeRun, resultTool, shutdownTool, spawnTool, statusTool } from "@dougbots/avenor-core";
1
+ import { InspectResult, StatusResult, Supervisor, answerPermissionTool, brokerReceiveTool, brokerReplyTool, dial, eventsTool, followUpTool, inspectTool, observeRun, resultTool, shutdownTool, spawnTool, statusTool, workflowCompleteTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowStatusTool, workflowWaitTool } from "@dougbots/avenor-core";
2
2
  import { EventBus, ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
 
4
4
  //#region src/types.d.ts
@@ -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 {
@@ -90,12 +118,29 @@ interface ExtensionDeps {
90
118
  followUpTool: typeof followUpTool;
91
119
  inspectTool: typeof inspectTool;
92
120
  resultTool: typeof resultTool;
121
+ brokerReceiveTool: typeof brokerReceiveTool;
122
+ brokerReplyTool: typeof brokerReplyTool;
93
123
  shutdownTool: typeof shutdownTool;
124
+ workflowStatusTool: typeof workflowStatusTool;
125
+ workflowWaitTool: typeof workflowWaitTool;
126
+ workflowInspectTool: typeof workflowInspectTool;
127
+ workflowEventsTool: typeof workflowEventsTool;
128
+ workflowCompleteTool: typeof workflowCompleteTool;
129
+ workflowGateTool: typeof workflowGateTool;
94
130
  observeRun: typeof observeRun;
95
131
  dial: typeof dial;
96
132
  Supervisor: typeof Supervisor;
97
133
  }
98
134
  declare function statusSupervisorId(requestedSupervisorId: string | undefined, spawnedSupervisorId: string): string | undefined;
135
+ type SpawnHostParams = {
136
+ agent?: string;
137
+ model?: string;
138
+ backend?: string;
139
+ roster_file?: string;
140
+ roster_entry?: string;
141
+ };
142
+ /** Project shared spawn identity into Pi tool metadata without resolving rosters here. */
143
+ 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
144
  declare function buildCompletionText(run: {
100
145
  runId: string;
101
146
  label: string;
@@ -104,7 +149,11 @@ declare function isTerminalStatus(status: string | undefined): boolean;
104
149
  declare function renderStatusLines(entries: RunStatusEntry[]): string[];
105
150
  declare function compactWhitespace(text: string | undefined, limit?: number): string | undefined;
106
151
  declare function buildInspectPayload(result: InspectResult): Record<string, unknown>;
107
- declare function createExtension(deps?: ExtensionDeps): (pi: ExtensionAPI) => Promise<void>;
152
+ interface ExtensionOptions {
153
+ /** Polling cadence; injectable so tests can drive deterministic ticks. */
154
+ pollIntervalMs?: number;
155
+ }
156
+ declare function createExtension(deps?: ExtensionDeps, options?: ExtensionOptions): (pi: ExtensionAPI) => Promise<void>;
108
157
  declare const _default: (pi: ExtensionAPI) => Promise<void>;
109
158
  //#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 };
159
+ export { type AvenorEventBus, type AvenorEventChannel, type AvenorEventMap, type AvenorRunStatus, CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, ExtensionDeps, ExtensionOptions, 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, THINKING_LEVELS, answerPermissionTool, askTool, brokerReceiveTool, brokerReplyTool, cancelTool, dial, eventsTool, extractEventText, followUpTool, inspectTool, observeRun, peersTool, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection, workflowCompleteTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowStatusTool, workflowWaitTool } 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";
@@ -475,12 +475,12 @@ var RunInspectorOverlay = class {
475
475
  function matchingWatchRuns(runs, selector) {
476
476
  const query = selector.trim().toLowerCase();
477
477
  if (!query) return [];
478
- const byId = runs.find((run) => run.runId.toLowerCase() === query);
479
- if (byId) return [byId];
478
+ const byId = runs.filter((run) => run.runId.toLowerCase() === query);
479
+ if (byId.length > 0) return byId;
480
480
  return runs.filter((run) => run.label?.toLowerCase() === query || run.agent?.toLowerCase() === query);
481
481
  }
482
482
  function watchRunChoice(run) {
483
- return `${sanitizeText(run.label ?? run.runId)}${run.agent ? ` · ${sanitizeText(run.agent)}` : ""} · ${sanitizeText(run.runId).slice(0, 8)}`;
483
+ return `${sanitizeText(run.label ?? run.runId)}${run.agent ? ` · ${sanitizeText(run.agent)}` : ""}${run.supervisorId ? ` · ${sanitizeText(run.supervisorId)}` : ""} · ${sanitizeText(run.runId).slice(0, 8)}`;
484
484
  }
485
485
  async function selectWatchRun(ctx, runs) {
486
486
  const choices = runs.map(watchRunChoice);
@@ -522,7 +522,20 @@ async function openRunInspector(ctx, options) {
522
522
  }
523
523
  //#endregion
524
524
  //#region src/types.ts
525
- function findLiveStatusForTrackedRun(run, liveStatuses) {
525
+ /**
526
+ * Find a live status for a tracked run from the unqualified live-status list.
527
+ *
528
+ * `statusTool({})` only describes the current singleton supervisor, so it is
529
+ * authoritative solely for runs that live in the singleton namespace. Runs on
530
+ * another supervisor must be polled through their own supervisor; matching
531
+ * them against this list could correlate a run with a colliding id (for
532
+ * example a shared `rt_1`) owned by a different supervisor.
533
+ *
534
+ * @param singletonScope true when the tracked run belongs to the singleton
535
+ * supervisor (no explicit socket, or a socket equal to the current instance).
536
+ */
537
+ function findLiveStatusForTrackedRun(run, liveStatuses, singletonScope) {
538
+ if (!singletonScope) return void 0;
526
539
  const knownRunIds = new Set([run.runId, run.lastStatus?.run_id].filter((id) => !!id));
527
540
  let runtimeMatch;
528
541
  for (const status of liveStatuses) {
@@ -556,16 +569,18 @@ function formatRunLine(entry) {
556
569
  return `${emoji} ${entry.label} (${entry.agent}) — ${entry.status}${phase}${perm}`;
557
570
  }
558
571
  /**
559
- * decideCompletion determines whether to skip, defer, or send the completion
572
+ * decideCompletion determines whether to skip or send the completion
560
573
  * steering message for a tracked run that has reached terminal status.
561
574
  *
562
- * The deferral prevents duplication when avenor_result consumes the result
563
- * in the same turn: if the agent calls avenor_result between the defer and
564
- * the next tick, the run is deleted from tracking and the send never fires.
575
+ * The message is delivered on the same terminal tick — never deferred to a
576
+ * later cycle — so a run that is the last live one still gets its completion.
577
+ * `blocking` suppresses it while avenor_result is awaiting (it will deliver
578
+ * the result itself and set `consumed`), and `consumed` suppresses it once
579
+ * avenor_result has already returned the result successfully.
565
580
  */
566
581
  function decideCompletion(run) {
567
582
  if (run.blocking) return "skip";
568
- if (!run.completionPending) return "defer";
583
+ if (run.consumed) return "skip";
569
584
  return "send";
570
585
  }
571
586
  //#endregion
@@ -718,7 +733,15 @@ function resolveAgentProfile(entries, environment = process.env) {
718
733
  const CHANNEL_POLL_COMPLETED = "avenor:poll:completed";
719
734
  /** Emitted when the extension observes a terminal run result. */
720
735
  const CHANNEL_RUN_TERMINAL = "avenor:run:terminal";
721
- /** Derive a stable non-connectable scope from a local supervisor socket. */
736
+ /** Emitted when a best-effort polling operation fails. */
737
+ const CHANNEL_POLL_ERROR = "avenor:poll:error";
738
+ /** Maximum sanitized error text exposed to companion extensions. */
739
+ const POLL_ERROR_TEXT_CHARS = 600;
740
+ function sanitizeAndBoundText(value) {
741
+ const text = sanitizeText(String(value)).trim();
742
+ if (!text) return "unknown error";
743
+ return text.length <= POLL_ERROR_TEXT_CHARS ? text : `${text.slice(0, POLL_ERROR_TEXT_CHARS - 1)}…`;
744
+ }
722
745
  function createSupervisorKey(supervisorId) {
723
746
  if (!supervisorId) return "singleton";
724
747
  return `supervisor:${createHash("sha256").update(supervisorId).digest("hex").slice(0, 16)}`;
@@ -769,6 +792,17 @@ function createRunTerminalPayload(payload) {
769
792
  ...payload.backend !== void 0 && { backend: payload.backend }
770
793
  });
771
794
  }
795
+ /** Copy, bound, sanitize, and freeze a polling error before publishing it. */
796
+ function createPollErrorPayload(payload) {
797
+ return Object.freeze({
798
+ source: payload.source,
799
+ ...payload.runId !== void 0 && { runId: payload.runId },
800
+ message: sanitizeAndBoundText(payload.message),
801
+ error: sanitizeAndBoundText(payload.error),
802
+ count: payload.count,
803
+ timestamp: payload.timestamp
804
+ });
805
+ }
772
806
  //#endregion
773
807
  //#region src/render.ts
774
808
  const FINAL_OUTPUT_CHARS = 600;
@@ -1031,9 +1065,11 @@ function eventsLines(details, args, expanded) {
1031
1065
  const displayedTypes = types.slice(0, COLLECTION_ITEMS).map((type) => scalar(type)).join(", ");
1032
1066
  const lines = [`Events: ${runId} — ${events.length} events`, ...types.length > 0 ? [`Filter: ${displayedTypes}${types.length > COLLECTION_ITEMS ? ` ${itemMarker(types.length - COLLECTION_ITEMS)}` : ""}`] : []];
1033
1067
  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;
1068
+ if (!expanded) {
1069
+ lines.push(`Preview: ${eventRow(events[0])}`);
1070
+ if (events.length > 1) lines.push(itemMarker(events.length - 1));
1071
+ return lines;
1072
+ }
1037
1073
  const displayed = events.slice(0, STATUS_ROWS);
1038
1074
  lines.push(...displayed.map(eventRow));
1039
1075
  if (events.length > displayed.length) lines.push(itemMarker(events.length - displayed.length));
@@ -1226,6 +1262,7 @@ const INSPECT_JSON_STRING_CHARS = 600;
1226
1262
  const INSPECT_JSON_ARRAY_ITEMS = 24;
1227
1263
  const INSPECT_JSON_OBJECT_KEYS = 24;
1228
1264
  const INSPECT_JSON_DEPTH = 8;
1265
+ const MAX_POLL_ERRORS = 100;
1229
1266
  const defaultDeps = {
1230
1267
  spawnTool,
1231
1268
  statusTool,
@@ -1234,14 +1271,60 @@ const defaultDeps = {
1234
1271
  followUpTool,
1235
1272
  inspectTool,
1236
1273
  resultTool,
1274
+ brokerReceiveTool,
1275
+ brokerReplyTool,
1237
1276
  shutdownTool,
1277
+ workflowStatusTool,
1278
+ workflowWaitTool,
1279
+ workflowInspectTool,
1280
+ workflowEventsTool,
1281
+ workflowCompleteTool,
1282
+ workflowGateTool,
1238
1283
  observeRun,
1239
1284
  dial,
1240
1285
  Supervisor
1241
1286
  };
1287
+ let hostPollTimer = null;
1288
+ const hostPendingAsks = /* @__PURE__ */ new Map();
1289
+ function stopHostAskPoll() {
1290
+ if (hostPollTimer) {
1291
+ clearInterval(hostPollTimer);
1292
+ hostPollTimer = null;
1293
+ }
1294
+ }
1295
+ async function startHostAskPoll(ctx) {
1296
+ stopHostAskPoll();
1297
+ hostPollTimer = setInterval(async () => {
1298
+ try {
1299
+ const { asks } = await deps.brokerReceiveTool({});
1300
+ for (const ask of asks) {
1301
+ hostPendingAsks.set(ask.message_id, {
1302
+ from_run_id: ask.from_run_id,
1303
+ message_id: ask.message_id
1304
+ });
1305
+ const body = `**📨 Ask from sub-agent** (\`${ask.role && ask.role !== "agent" ? `${ask.from_run_id} (${ask.role})` : ask.from_run_id}\`)\n\n${ask.message}\n\nTo answer, use the intercom tool: avenor_reply({ from_run_id: ${JSON.stringify(ask.from_run_id)}, reply_to_message_id: ${JSON.stringify(ask.message_id)}, message: "..." })`;
1306
+ await ctx.sendUserMessage(body, { deliverAs: "steer" });
1307
+ }
1308
+ } catch (error) {}
1309
+ }, 2e3);
1310
+ hostPollTimer.unref?.();
1311
+ }
1242
1312
  function statusSupervisorId(requestedSupervisorId, spawnedSupervisorId) {
1243
1313
  return requestedSupervisorId ? spawnedSupervisorId : void 0;
1244
1314
  }
1315
+ /** Project shared spawn identity into Pi tool metadata without resolving rosters here. */
1316
+ function spawnIdentityMetadata(params, status) {
1317
+ const metadata = {};
1318
+ const add = (key, value) => {
1319
+ if (typeof value === "string" && value.length > 0) metadata[key] = value;
1320
+ };
1321
+ add("roster_file", status?.roster_file ?? params.roster_file);
1322
+ add("roster_entry", status?.roster_entry ?? params.roster_entry);
1323
+ add("effective_agent", status?.effective_agent ?? status?.agent ?? params.agent);
1324
+ add("effective_model", status?.effective_model ?? status?.model ?? params.model);
1325
+ add("effective_backend", status?.effective_backend ?? status?.backend ?? params.backend);
1326
+ return metadata;
1327
+ }
1245
1328
  function sleep(ms) {
1246
1329
  return new Promise((resolve) => setTimeout(resolve, ms));
1247
1330
  }
@@ -1267,7 +1350,8 @@ function buildCompletionText(run, result, finalOutput) {
1267
1350
  }
1268
1351
  function buildWaitingText(run, status) {
1269
1352
  const perm = status.pending_permission;
1270
- const lines = [`Sub-agent "${run.label}" (${run.agent}) is waiting for input.`];
1353
+ const agent = run.agent ?? status.effective_agent ?? status.agent ?? "roster";
1354
+ const lines = [`Sub-agent "${run.label}" (${agent}) is waiting for input.`];
1271
1355
  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
1356
  else if (status.phase_label) lines.push(`Question: ${status.phase_label}`);
1273
1357
  if (!perm) lines.push(`Use \`avenor_follow_up\` with run_id "${run.runId}" to respond, or \`avenor_inspect\` to review context first.`);
@@ -1351,30 +1435,51 @@ function toWatchRunRef(run) {
1351
1435
  supervisorId: run.supervisorId
1352
1436
  };
1353
1437
  }
1354
- function createExtension(deps = defaultDeps) {
1438
+ /**
1439
+ * Namespace a run id by its supervisor so runs on different supervisors can
1440
+ * never collide or be correlated, even when they share runtime ids (e.g.
1441
+ * `rt_1`). An undefined supervisorId denotes the singleton namespace. The
1442
+ * NUL separator cannot appear in socket paths or generated run ids, so the
1443
+ * singleton and each explicit supervisor stay disjoint.
1444
+ */
1445
+ function trackedRunKey(supervisorId, runId) {
1446
+ return `${supervisorId ?? ""}\u0000${runId}`;
1447
+ }
1448
+ function createExtension(deps = defaultDeps, options = {}) {
1355
1449
  return async function(pi) {
1450
+ const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
1356
1451
  let telemetryGeneration = 0;
1357
1452
  const trackedRuns = /* @__PURE__ */ new Map();
1358
1453
  let pollingActive = false;
1359
1454
  let pollingGeneration = 0;
1360
1455
  let pollInFlight = null;
1361
1456
  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);
1457
+ let pollingErrorCount = 0;
1458
+ const pollingErrors = [];
1459
+ let lastStatusEntries = [];
1460
+ function recordPollingError(source, message, error, runId) {
1461
+ pollingErrorCount++;
1462
+ const payload = createPollErrorPayload({
1463
+ source,
1464
+ runId,
1465
+ message,
1466
+ error,
1467
+ count: pollingErrorCount,
1468
+ timestamp: Date.now()
1469
+ });
1470
+ pollingErrors.push(payload);
1471
+ if (pollingErrors.length > MAX_POLL_ERRORS) pollingErrors.shift();
1472
+ emitAvenorEvent(pi.events, CHANNEL_POLL_ERROR, payload);
1370
1473
  }
1371
- function clearRunPollingFailures(run) {
1372
- clearPollingFailure(`run-status:${run.supervisorId ?? "singleton"}:${run.runId}`);
1373
- clearPollingFailure(`spawn-status:${run.supervisorId ?? "singleton"}:${run.runId}`);
1474
+ function clearPollingErrors() {
1475
+ pollingErrorCount = 0;
1476
+ pollingErrors.length = 0;
1477
+ updateWidget(lastStatusEntries);
1374
1478
  }
1375
1479
  function upsertTrackedRun(run, status, blocking = false) {
1376
- const current = trackedRuns.get(run.runId);
1377
- trackedRuns.set(run.runId, {
1480
+ const key = trackedRunKey(run.supervisorId, run.runId);
1481
+ const current = trackedRuns.get(key);
1482
+ trackedRuns.set(key, {
1378
1483
  runId: run.runId,
1379
1484
  agent: run.agent ?? status?.agent ?? current?.agent ?? "unknown",
1380
1485
  label: run.label ?? status?.label ?? current?.label ?? run.runId,
@@ -1384,9 +1489,29 @@ function createExtension(deps = defaultDeps) {
1384
1489
  lastStatus: status ?? current?.lastStatus,
1385
1490
  blocking: current?.blocking ?? blocking,
1386
1491
  permissionNotified: current?.permissionNotified,
1387
- completionPending: current?.completionPending
1492
+ consumed: current?.consumed
1388
1493
  });
1389
1494
  }
1495
+ /**
1496
+ * Look a run up in the per-supervisor tracking namespace. When a
1497
+ * supervisor is supplied, resolve exactly. Without one, prefer the
1498
+ * singleton namespace and only fall back to a run id walk when it is
1499
+ * unambiguous across supervisors. Duplicate run ids across supervisors
1500
+ * without an explicit supervisor return undefined so callers never bind
1501
+ * flags or telemetry to the wrong supervisor's run.
1502
+ */
1503
+ function getTrackedRun(runId, supervisorId) {
1504
+ if (supervisorId !== void 0) return trackedRuns.get(trackedRunKey(supervisorId, runId));
1505
+ const singleton = trackedRuns.get(trackedRunKey(void 0, runId));
1506
+ if (singleton) return singleton;
1507
+ let match;
1508
+ for (const run of trackedRuns.values()) {
1509
+ if (run.runId !== runId) continue;
1510
+ if (match) return void 0;
1511
+ match = run;
1512
+ }
1513
+ return match;
1514
+ }
1390
1515
  async function resolveFinalOutput(run, status) {
1391
1516
  const preview = finalOutputPreview(status);
1392
1517
  if (preview) return preview;
@@ -1402,22 +1527,23 @@ function createExtension(deps = defaultDeps) {
1402
1527
  }
1403
1528
  }
1404
1529
  async function pollRuns() {
1530
+ const errorCountAtPollStart = pollingErrorCount;
1405
1531
  const liveMap = /* @__PURE__ */ new Map();
1406
1532
  try {
1407
1533
  const allLive = await deps.statusTool({});
1408
1534
  const liveList = Array.isArray(allLive) ? allLive : [allLive];
1409
1535
  for (const result of liveList) liveMap.set(result.run_id, result);
1410
- clearPollingFailure("singleton-list");
1411
1536
  } catch (err) {
1412
- reportPollingFailure("singleton-list", "avenor pollRuns: failed to list singleton runs", err);
1537
+ recordPollingError("singleton-list", "failed to list singleton runs", err);
1413
1538
  }
1414
1539
  const entries = [];
1415
1540
  for (const run of trackedRuns.values()) {
1416
- const live = findLiveStatusForTrackedRun(run, liveMap.values());
1541
+ const singletonScope = !run.supervisorId || Boolean(deps.Supervisor.isCurrentInstance?.(run.supervisorId));
1542
+ const live = singletonScope ? findLiveStatusForTrackedRun(run, liveMap.values(), singletonScope) : void 0;
1417
1543
  if (live) {
1418
1544
  liveMap.delete(live.run_id);
1419
- clearPollingFailure(`run-status:${run.supervisorId ?? "singleton"}:${run.runId}`);
1420
1545
  run.lastStatus = live;
1546
+ run.agent = live.agent ?? run.agent;
1421
1547
  entries.push({
1422
1548
  runId: run.runId,
1423
1549
  supervisorId: run.supervisorId,
@@ -1426,7 +1552,7 @@ function createExtension(deps = defaultDeps) {
1426
1552
  status: live.status,
1427
1553
  phase: live.phase,
1428
1554
  phaseLabel: live.phase_label,
1429
- agent: run.agent,
1555
+ agent: run.agent ?? "unknown",
1430
1556
  pendingPermission: !!live.pending_permission,
1431
1557
  permissionDescription: live.pending_permission?.description,
1432
1558
  pid: live.pid,
@@ -1441,8 +1567,8 @@ function createExtension(deps = defaultDeps) {
1441
1567
  });
1442
1568
  const result = Array.isArray(raw) ? raw[0] : raw;
1443
1569
  if (!result) throw new Error("status response was empty");
1444
- clearPollingFailure(`run-status:${run.supervisorId ?? "singleton"}:${run.runId}`);
1445
1570
  run.lastStatus = result;
1571
+ run.agent = result.agent ?? run.agent;
1446
1572
  entries.push({
1447
1573
  runId: run.runId,
1448
1574
  supervisorId: run.supervisorId,
@@ -1451,14 +1577,14 @@ function createExtension(deps = defaultDeps) {
1451
1577
  status: result.status,
1452
1578
  phase: result.phase,
1453
1579
  phaseLabel: result.phase_label,
1454
- agent: run.agent,
1580
+ agent: run.agent ?? "unknown",
1455
1581
  pendingPermission: !!result.pending_permission,
1456
1582
  permissionDescription: result.pending_permission?.description,
1457
1583
  pid: result.pid,
1458
1584
  backend: result.backend
1459
1585
  });
1460
1586
  } catch (err) {
1461
- reportPollingFailure(`run-status:${run.supervisorId ?? "singleton"}:${run.runId}`, `avenor pollRuns: statusTool failed for ${run.runId}`, err);
1587
+ recordPollingError("run-status", "statusTool failed while polling a run", err, run.runId);
1462
1588
  const previous = run.lastStatus;
1463
1589
  entries.push({
1464
1590
  runId: run.runId,
@@ -1468,7 +1594,7 @@ function createExtension(deps = defaultDeps) {
1468
1594
  status: previous?.status ?? "unavailable",
1469
1595
  phase: previous?.phase,
1470
1596
  phaseLabel: previous?.phase_label,
1471
- agent: run.agent,
1597
+ agent: run.agent ?? previous?.agent ?? "unknown",
1472
1598
  pendingPermission: !!previous?.pending_permission,
1473
1599
  permissionDescription: previous?.pending_permission?.description,
1474
1600
  pid: previous?.pid,
@@ -1495,21 +1621,24 @@ function createExtension(deps = defaultDeps) {
1495
1621
  let idx = 0;
1496
1622
  for (const entry of entries) if (entry.backend === "pi" && entry.pid && !isTerminalStatus(entry.status)) entry.nestedCount = nestedCounts[idx++] ?? 0;
1497
1623
  }
1624
+ if (pollingErrorCount === errorCountAtPollStart) pollingErrorCount = 0;
1498
1625
  return entries;
1499
1626
  }
1500
1627
  function updateWidget(entries) {
1628
+ lastStatusEntries = entries;
1501
1629
  if (!sessionCtx) return;
1502
1630
  const lines = renderStatusLines(entries);
1631
+ const errorSuffix = pollingErrorCount > 0 ? `errors:${pollingErrorCount}` : "";
1503
1632
  if (lines.length > 0) {
1504
1633
  sessionCtx.ui.setWidget("avenor-status", lines);
1505
1634
  const statusStr = lines.map((line) => line.slice(0, 60)).join(" ");
1506
1635
  const nestedTotal = entries.filter((e) => !isTerminalStatus(e.status)).reduce((sum, e) => sum + (e.nestedCount ?? 0), 0);
1507
1636
  const total = lines.length + nestedTotal;
1508
- const statusWithTotal = nestedTotal > 0 ? `${statusStr} total:${total}` : statusStr;
1637
+ const statusWithTotal = [nestedTotal > 0 ? `${statusStr} total:${total}` : statusStr, errorSuffix].filter(Boolean).join(" ");
1509
1638
  sessionCtx.ui.setStatus("avenor", statusWithTotal);
1510
1639
  } else {
1511
1640
  sessionCtx.ui.setWidget("avenor-status", void 0);
1512
- sessionCtx.ui.setStatus("avenor", "");
1641
+ sessionCtx.ui.setStatus("avenor", errorSuffix);
1513
1642
  }
1514
1643
  }
1515
1644
  function startPolling() {
@@ -1529,18 +1658,16 @@ function createExtension(deps = defaultDeps) {
1529
1658
  updateWidget(entries);
1530
1659
  emitAvenorEvent(pi.events, CHANNEL_POLL_COMPLETED, createPollCompletedPayload(entries, Date.now(), ++telemetryGeneration));
1531
1660
  for (const entry of entries) {
1532
- const run = trackedRuns.get(entry.runId);
1661
+ const entryKey = trackedRunKey(entry.supervisorId, entry.runId);
1662
+ const run = trackedRuns.get(entryKey);
1533
1663
  if (!run) continue;
1534
1664
  if (isTerminalStatus(entry.status)) {
1535
- const prevStatus = prevStatuses.get(entry.runId);
1536
- if (!(prevStatus && isTerminalStatus(prevStatus))) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
1537
- if (run.blocking) continue;
1538
- const action = decideCompletion(run);
1539
- if (action === "defer") {
1540
- run.completionPending = true;
1665
+ if (decideCompletion(run) === "skip") {
1666
+ if (run.consumed) trackedRuns.delete(entryKey);
1541
1667
  continue;
1542
1668
  }
1543
- if (action === "skip") continue;
1669
+ const prevStatus = prevStatuses.get(entryKey);
1670
+ if (!(prevStatus && isTerminalStatus(prevStatus))) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
1544
1671
  const finalOutput = await resolveFinalOutput(run, run.lastStatus);
1545
1672
  try {
1546
1673
  pi.sendUserMessage(buildCompletionText({
@@ -1560,8 +1687,7 @@ function createExtension(deps = defaultDeps) {
1560
1687
  nestedCount: entry.nestedCount,
1561
1688
  backend: entry.backend
1562
1689
  }));
1563
- clearRunPollingFailures(run);
1564
- trackedRuns.delete(entry.runId);
1690
+ trackedRuns.delete(entryKey);
1565
1691
  } else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
1566
1692
  run.permissionNotified = true;
1567
1693
  if (run.lastStatus) try {
@@ -1580,12 +1706,10 @@ function createExtension(deps = defaultDeps) {
1580
1706
  pollingActive = false;
1581
1707
  console.error("avenor polling loop failed", err);
1582
1708
  });
1583
- }, POLL_INTERVAL_MS);
1709
+ }, pollIntervalMs);
1584
1710
  else {
1585
1711
  pollingActive = false;
1586
- reportedPollingFailures.clear();
1587
- sessionCtx?.ui.setStatus("avenor", "");
1588
- sessionCtx?.ui.setWidget("avenor-status", void 0);
1712
+ updateWidget([]);
1589
1713
  }
1590
1714
  };
1591
1715
  tick().catch((err) => {
@@ -1596,10 +1720,11 @@ function createExtension(deps = defaultDeps) {
1596
1720
  async function stopPolling() {
1597
1721
  pollingActive = false;
1598
1722
  pollingGeneration++;
1599
- reportedPollingFailures.clear();
1600
1723
  await pollInFlight?.catch(() => {});
1601
- sessionCtx?.ui.setStatus("avenor", "");
1602
- sessionCtx?.ui.setWidget("avenor-status", void 0);
1724
+ pollingErrorCount = 0;
1725
+ pollingErrors.length = 0;
1726
+ lastStatusEntries = [];
1727
+ updateWidget([]);
1603
1728
  }
1604
1729
  function createWatchDeps() {
1605
1730
  async function resolveSupervisorSocket(run) {
@@ -1759,7 +1884,7 @@ function createExtension(deps = defaultDeps) {
1759
1884
  async function waitForRunByPolling(run, signal, onUpdate) {
1760
1885
  let firstPoll = true;
1761
1886
  while (!signal?.aborted) {
1762
- if (!firstPoll) await sleep(POLL_INTERVAL_MS);
1887
+ if (!firstPoll) await sleep(pollIntervalMs);
1763
1888
  firstPoll = false;
1764
1889
  try {
1765
1890
  const raw = await deps.statusTool({
@@ -1769,7 +1894,6 @@ function createExtension(deps = defaultDeps) {
1769
1894
  const status = Array.isArray(raw) ? raw[0] : raw;
1770
1895
  if (!status) continue;
1771
1896
  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
1897
  onUpdate?.({
1774
1898
  content: [{
1775
1899
  type: "text",
@@ -1785,7 +1909,7 @@ function createExtension(deps = defaultDeps) {
1785
1909
  aborted: false
1786
1910
  };
1787
1911
  } catch (err) {
1788
- reportPollingFailure(`spawn-status:${run.supervisorId ?? "singleton"}:${run.runId}`, "avenor spawn poll: statusTool failed", err);
1912
+ recordPollingError("spawn-status", "statusTool failed while waiting for a run", err, run.runId);
1789
1913
  }
1790
1914
  }
1791
1915
  return { aborted: true };
@@ -1797,9 +1921,15 @@ function createExtension(deps = defaultDeps) {
1797
1921
  }
1798
1922
  pi.on("session_start", async (_event, ctx) => {
1799
1923
  sessionCtx = ctx;
1924
+ lastStatusEntries = [];
1925
+ pollingErrorCount = 0;
1926
+ pollingErrors.length = 0;
1927
+ startHostAskPoll(ctx);
1800
1928
  });
1801
1929
  pi.on("session_shutdown", async () => {
1802
1930
  await stopPolling();
1931
+ stopHostAskPoll();
1932
+ hostPendingAsks.clear();
1803
1933
  sessionCtx = null;
1804
1934
  });
1805
1935
  pi.on("before_agent_start", async () => {
@@ -1828,7 +1958,7 @@ function createExtension(deps = defaultDeps) {
1828
1958
  pi.registerTool({
1829
1959
  name: "avenor_spawn",
1830
1960
  label: "Avenor Spawn",
1831
- description: "Dispatch an agent run via avenor. Optional thinking accepts off, minimal, low, medium, high, xhigh, or max; unsupported backends reject explicit values. Blocks by default, showing live progress. Set wait=false for fire-and-forget.",
1961
+ description: `Dispatch an agent run via avenor. Optional thinking accepts ${[...THINKING_LEVELS].slice(0, -1).join(", ")}, or ${THINKING_LEVELS[THINKING_LEVELS.length - 1]}; unsupported backends reject explicit values. Blocks by default, showing live progress. Set wait=false for fire-and-forget.`,
1832
1962
  promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, result, inspect, events, follow-up, shutdown)",
1833
1963
  promptGuidelines: [
1834
1964
  "Use avenor_spawn to delegate well-defined tasks to sub-agents.",
@@ -1840,7 +1970,7 @@ function createExtension(deps = defaultDeps) {
1840
1970
  "Use avenor_shutdown when done delegating work."
1841
1971
  ],
1842
1972
  parameters: Type.Object({
1843
- agent: Type.String({ description: "Agent name (required, no default)" }),
1973
+ agent: Type.Optional(Type.String({ description: "Optional agent name; omission uses the supplied model or runtime defaults" })),
1844
1974
  prompt: Type.Optional(Type.String({ description: "Prompt text" })),
1845
1975
  prompt_file: Type.Optional(Type.String({ description: "Path to prompt file" })),
1846
1976
  dir: Type.Optional(Type.String({ description: "Working directory (defaults to session directory)" })),
@@ -1856,7 +1986,9 @@ function createExtension(deps = defaultDeps) {
1856
1986
  Type.Literal("xhigh"),
1857
1987
  Type.Literal("max")
1858
1988
  ], { description: "Thinking level; omission uses the backend default" })),
1859
- backend: Type.Optional(Type.String({ description: "Backend override (defaults to pi)" })),
1989
+ backend: Type.Optional(Type.String({ description: "Backend override (defaults to pi for direct mode)" })),
1990
+ roster_file: Type.Optional(Type.String({ description: "Path to the roster map" })),
1991
+ roster_entry: Type.Optional(Type.String({ description: "Roster entry to select" })),
1860
1992
  server_url: Type.Optional(Type.String({ description: "Backend server URL" })),
1861
1993
  supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
1862
1994
  wait: Type.Optional(Type.Boolean({ description: "Block until complete (default true)" }))
@@ -1866,25 +1998,39 @@ function createExtension(deps = defaultDeps) {
1866
1998
  type: "text",
1867
1999
  text: "Cancelled"
1868
2000
  }] };
2001
+ validateSpawnSelection({
2002
+ agent: params.agent,
2003
+ model: params.model,
2004
+ backend: params.backend,
2005
+ roster_file: params.roster_file,
2006
+ roster_entry: params.roster_entry
2007
+ });
1869
2008
  const wait = params.wait ?? true;
1870
2009
  const dir = params.dir ?? ctx.cwd;
1871
- const label = params.label ?? `${params.agent}-${Date.now()}`;
1872
- const backend = params.backend ?? "pi";
2010
+ const label = params.label ?? `${params.agent ?? params.roster_entry ?? params.model ?? "avenor"}-${Date.now()}`;
2011
+ const rosterMode = Boolean(params.roster_entry);
2012
+ const backend = rosterMode ? params.backend : params.backend ?? "pi";
1873
2013
  const entries = ctx.sessionManager?.getEntries?.();
1874
- const agentProfile = backend === "pi" ? resolveAgentProfile(entries) : void 0;
2014
+ const agentProfile = rosterMode || backend === "pi" ? resolveAgentProfile(entries) : void 0;
2015
+ const hostParams = {
2016
+ ...params,
2017
+ backend
2018
+ };
1875
2019
  const result = await deps.spawnTool({
1876
- agent: params.agent,
1877
- prompt: params.prompt,
1878
- promptFile: params.prompt_file,
2020
+ ...params.agent !== void 0 ? { agent: params.agent } : {},
2021
+ ...params.prompt !== void 0 ? { prompt: params.prompt } : {},
2022
+ ...params.prompt_file !== void 0 ? { promptFile: params.prompt_file } : {},
1879
2023
  label,
1880
2024
  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
2025
+ ...params.timeout !== void 0 ? { timeout: params.timeout } : {},
2026
+ ...params.model !== void 0 ? { model: params.model } : {},
2027
+ ...params.thinking !== void 0 ? { thinking: params.thinking } : {},
2028
+ ...backend !== void 0 ? { backend } : {},
2029
+ ...params.roster_file !== void 0 ? { rosterFile: params.roster_file } : {},
2030
+ ...params.roster_entry !== void 0 ? { rosterEntry: params.roster_entry } : {},
2031
+ ...agentProfile !== void 0 ? { agentProfile } : {},
2032
+ ...params.server_url !== void 0 ? { serverUrl: params.server_url } : {},
2033
+ ...params.supervisor_id !== void 0 ? { supervisorId: params.supervisor_id } : {}
1888
2034
  });
1889
2035
  const supervisorId = statusSupervisorId(params.supervisor_id, result.supervisor_id);
1890
2036
  const runRef = {
@@ -1903,7 +2049,8 @@ function createExtension(deps = defaultDeps) {
1903
2049
  }],
1904
2050
  details: {
1905
2051
  run_id: result.run_id,
1906
- label
2052
+ label,
2053
+ ...spawnIdentityMetadata(hostParams)
1907
2054
  }
1908
2055
  };
1909
2056
  onUpdate?.({
@@ -1918,14 +2065,16 @@ function createExtension(deps = defaultDeps) {
1918
2065
  });
1919
2066
  const waitResult = await waitForRun(runRef, signal, onUpdate);
1920
2067
  if (waitResult.aborted) {
1921
- clearRunPollingFailures(runRef);
1922
- trackedRuns.delete(result.run_id);
2068
+ trackedRuns.delete(trackedRunKey(supervisorId, result.run_id));
1923
2069
  return {
1924
2070
  content: [{
1925
2071
  type: "text",
1926
2072
  text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status or avenor_inspect to check.`
1927
2073
  }],
1928
- details: { run_id: result.run_id }
2074
+ details: {
2075
+ run_id: result.run_id,
2076
+ ...spawnIdentityMetadata(hostParams)
2077
+ }
1929
2078
  };
1930
2079
  }
1931
2080
  const status = waitResult.status;
@@ -1936,12 +2085,13 @@ function createExtension(deps = defaultDeps) {
1936
2085
  }],
1937
2086
  details: {
1938
2087
  status: "running",
1939
- run_id: result.run_id
2088
+ run_id: result.run_id,
2089
+ ...spawnIdentityMetadata(hostParams)
1940
2090
  }
1941
2091
  };
1942
2092
  upsertTrackedRun(runRef, status, wait);
1943
2093
  if (status.status === "waiting") {
1944
- const tracked = trackedRuns.get(result.run_id);
2094
+ const tracked = getTrackedRun(result.run_id, supervisorId);
1945
2095
  if (tracked) tracked.permissionNotified = true;
1946
2096
  return {
1947
2097
  content: [{
@@ -1956,7 +2106,8 @@ function createExtension(deps = defaultDeps) {
1956
2106
  status: "waiting",
1957
2107
  run_id: result.run_id,
1958
2108
  session_id: status.session_id,
1959
- ...status.pending_permission && { pending_permission: status.pending_permission }
2109
+ ...status.pending_permission && { pending_permission: status.pending_permission },
2110
+ ...spawnIdentityMetadata(hostParams, status)
1960
2111
  }
1961
2112
  };
1962
2113
  }
@@ -1974,8 +2125,7 @@ function createExtension(deps = defaultDeps) {
1974
2125
  status: status.status,
1975
2126
  backend: status.backend
1976
2127
  }));
1977
- clearRunPollingFailures(runRef);
1978
- trackedRuns.delete(result.run_id);
2128
+ trackedRuns.delete(trackedRunKey(supervisorId, result.run_id));
1979
2129
  return {
1980
2130
  content: [{
1981
2131
  type: "text",
@@ -1988,7 +2138,8 @@ function createExtension(deps = defaultDeps) {
1988
2138
  status: status.status,
1989
2139
  run_id: result.run_id,
1990
2140
  session_id: status.session_id,
1991
- final_output: finalOutput
2141
+ final_output: finalOutput,
2142
+ ...spawnIdentityMetadata(hostParams, status)
1992
2143
  }
1993
2144
  };
1994
2145
  }
@@ -1999,13 +2150,14 @@ function createExtension(deps = defaultDeps) {
1999
2150
  }],
2000
2151
  details: {
2001
2152
  status: status.status,
2002
- run_id: result.run_id
2153
+ run_id: result.run_id,
2154
+ ...spawnIdentityMetadata(hostParams, status)
2003
2155
  }
2004
2156
  };
2005
2157
  },
2006
2158
  renderCall(args, theme) {
2007
2159
  const agent = theme.fg("toolTitle", theme.bold("avenor_spawn "));
2008
- const agentName = theme.fg("accent", args.agent);
2160
+ const agentName = theme.fg("accent", args.agent ?? args.roster_entry ?? args.model ?? "roster");
2009
2161
  const label = args.label ? theme.fg("dim", ` "${args.label}"`) : "";
2010
2162
  const wait = args.wait === false ? theme.fg("warning", " [async]") : "";
2011
2163
  return new Text(agent + agentName + label + wait, 0, 0);
@@ -2072,7 +2224,7 @@ function createExtension(deps = defaultDeps) {
2072
2224
  supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
2073
2225
  }),
2074
2226
  async execute(_toolCallId, params, signal) {
2075
- const tracked = trackedRuns.get(params.run_id);
2227
+ const tracked = getTrackedRun(params.run_id, params.supervisor_id);
2076
2228
  const previousBlocking = tracked?.blocking;
2077
2229
  if (tracked) tracked.blocking = true;
2078
2230
  let completed = false;
@@ -2086,6 +2238,7 @@ function createExtension(deps = defaultDeps) {
2086
2238
  });
2087
2239
  completed = result.ready;
2088
2240
  if (completed) {
2241
+ if (tracked) tracked.consumed = true;
2089
2242
  emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
2090
2243
  runId: params.run_id,
2091
2244
  supervisorId: tracked?.supervisorId ?? params.supervisor_id,
@@ -2094,11 +2247,7 @@ function createExtension(deps = defaultDeps) {
2094
2247
  agent: tracked?.agent ?? "unknown",
2095
2248
  status: result.status ?? "done"
2096
2249
  }));
2097
- clearRunPollingFailures({
2098
- runId: params.run_id,
2099
- supervisorId: tracked?.supervisorId ?? params.supervisor_id
2100
- });
2101
- trackedRuns.delete(params.run_id);
2250
+ if (tracked) trackedRuns.delete(trackedRunKey(tracked.supervisorId, tracked.runId));
2102
2251
  } else if (tracked && result.status === "waiting") tracked.permissionNotified = true;
2103
2252
  return {
2104
2253
  content: [{
@@ -2108,7 +2257,7 @@ function createExtension(deps = defaultDeps) {
2108
2257
  details: result
2109
2258
  };
2110
2259
  } finally {
2111
- if (!completed && tracked && trackedRuns.get(params.run_id) === tracked) tracked.blocking = previousBlocking ?? false;
2260
+ if (!completed && tracked && getTrackedRun(params.run_id, params.supervisor_id) === tracked) tracked.blocking = previousBlocking ?? false;
2112
2261
  }
2113
2262
  },
2114
2263
  renderCall(args, theme, context) {
@@ -2249,6 +2398,94 @@ function createExtension(deps = defaultDeps) {
2249
2398
  }, theme, context.args);
2250
2399
  }
2251
2400
  });
2401
+ pi.registerTool({
2402
+ name: "avenor_ask",
2403
+ label: "Avenor Ask",
2404
+ description: "Ask another agent run a question and block until it replies.",
2405
+ parameters: Type.Object({
2406
+ to_run_id: Type.String({ description: "Target run ID" }),
2407
+ message: Type.String({ description: "Question to ask the other agent" })
2408
+ }),
2409
+ async execute(_toolCallId, params) {
2410
+ return askTool({
2411
+ toRunId: params.to_run_id,
2412
+ message: params.message
2413
+ });
2414
+ }
2415
+ });
2416
+ pi.registerTool({
2417
+ name: "avenor_reply",
2418
+ label: "Avenor Reply (host)",
2419
+ description: "Reply to an inbound ask received from a sub-agent. Answer the most recent ask from a given sub-agent, or reply to a specific message_id from avenor_ask.",
2420
+ parameters: Type.Object({
2421
+ from_run_id: Type.Optional(Type.String({ description: "Sub-agent run id to reply to (the asker)" })),
2422
+ reply_to_message_id: Type.Optional(Type.String({ description: "The ask's broker message id to answer specifically" })),
2423
+ message: Type.String({ description: "Your answer" }),
2424
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
2425
+ }),
2426
+ async execute(_toolCallId, params) {
2427
+ let fromRunId = params.from_run_id;
2428
+ let messageId = params.reply_to_message_id;
2429
+ if (!messageId) {
2430
+ let match;
2431
+ for (const [, pending] of hostPendingAsks) if (pending.from_run_id === fromRunId && (!match || pending.message_id > match.message_id)) match = pending;
2432
+ if (match) messageId = match.message_id;
2433
+ }
2434
+ if (!fromRunId || !messageId) return {
2435
+ content: [{
2436
+ type: "text",
2437
+ text: "No pending ask found to reply to. Pass from_run_id and/or reply_to_message_id."
2438
+ }],
2439
+ details: { error: true }
2440
+ };
2441
+ await deps.brokerReplyTool({
2442
+ toRunId: fromRunId,
2443
+ replyTo: messageId,
2444
+ message: params.message,
2445
+ supervisorId: params.supervisor_id
2446
+ });
2447
+ hostPendingAsks.delete(messageId);
2448
+ return { content: [{
2449
+ type: "text",
2450
+ text: "Reply sent"
2451
+ }] };
2452
+ }
2453
+ });
2454
+ pi.registerTool({
2455
+ name: "avenor_peers",
2456
+ label: "Avenor Peers",
2457
+ description: "List all connected agent sessions with their status and metadata.",
2458
+ parameters: Type.Object({}),
2459
+ async execute() {
2460
+ const result = await peersTool({});
2461
+ if (result.peers.length === 0) return { content: [{
2462
+ type: "text",
2463
+ text: "No other agent sessions connected."
2464
+ }] };
2465
+ return { content: [{
2466
+ type: "text",
2467
+ text: `**Active sessions:**\n${result.peers.map((p) => {
2468
+ const parts = [p.label ? `${p.label} (${p.run_id})` : p.run_id];
2469
+ const tags = [
2470
+ p.status,
2471
+ p.backend,
2472
+ p.model
2473
+ ].filter(Boolean);
2474
+ if (tags.length) parts.push(`[${tags.join(", ")}]`);
2475
+ return ` ${parts.join(" ")}`;
2476
+ }).join("\n")}`
2477
+ }] };
2478
+ }
2479
+ });
2480
+ pi.registerTool({
2481
+ name: "avenor_cancel",
2482
+ label: "Avenor Cancel",
2483
+ description: "Cancel a pending ask by its message ID.",
2484
+ parameters: Type.Object({ message_id: Type.String({ description: "Message ID of the pending ask to cancel" }) }),
2485
+ async execute(_toolCallId, params) {
2486
+ return cancelTool({ messageId: params.message_id });
2487
+ }
2488
+ });
2252
2489
  pi.registerTool({
2253
2490
  name: "avenor_events",
2254
2491
  label: "Avenor Events",
@@ -2293,8 +2530,20 @@ function createExtension(deps = defaultDeps) {
2293
2530
  force: Type.Optional(Type.Boolean({ description: "Force shutdown instead of graceful" }))
2294
2531
  }),
2295
2532
  async execute(_toolCallId, params) {
2296
- await stopPolling();
2297
- trackedRuns.clear();
2533
+ const target = params.supervisor_id;
2534
+ const targetIsSingleton = target === void 0 || Boolean(deps.Supervisor.isCurrentInstance?.(target));
2535
+ let removed = 0;
2536
+ for (const [key, run] of trackedRuns) if (targetIsSingleton ? !run.supervisorId || Boolean(deps.Supervisor.isCurrentInstance?.(run.supervisorId)) : run.supervisorId === target) {
2537
+ trackedRuns.delete(key);
2538
+ removed++;
2539
+ }
2540
+ if (removed > 0 && trackedRuns.size === 0) await stopPolling();
2541
+ else if (removed > 0) {
2542
+ pollingActive = false;
2543
+ pollingGeneration++;
2544
+ await pollInFlight?.catch(() => {});
2545
+ startPolling();
2546
+ }
2298
2547
  const result = await deps.shutdownTool({
2299
2548
  supervisorId: params.supervisor_id,
2300
2549
  force: params.force
@@ -2317,10 +2566,178 @@ function createExtension(deps = defaultDeps) {
2317
2566
  }, theme, context.args);
2318
2567
  }
2319
2568
  });
2569
+ pi.registerTool({
2570
+ name: "avenor_workflow_status",
2571
+ label: "Avenor Workflow Status",
2572
+ description: "Get lightweight status for a workflow instance",
2573
+ parameters: Type.Object({
2574
+ workflow_id: Type.String({ description: "Workflow ID" }),
2575
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
2576
+ }),
2577
+ async execute(_toolCallId, params) {
2578
+ const result = await deps.workflowStatusTool({
2579
+ workflowId: params.workflow_id,
2580
+ supervisorId: params.supervisor_id
2581
+ });
2582
+ return {
2583
+ content: [{
2584
+ type: "text",
2585
+ text: JSON.stringify(result, null, 2)
2586
+ }],
2587
+ details: result
2588
+ };
2589
+ }
2590
+ });
2591
+ pi.registerTool({
2592
+ name: "avenor_workflow_wait",
2593
+ label: "Avenor Workflow Wait",
2594
+ description: "Wait for a workflow to reach a terminal state or until timeout",
2595
+ parameters: Type.Object({
2596
+ workflow_id: Type.String({ description: "Workflow ID" }),
2597
+ timeout: Type.Optional(Type.String({ description: "Timeout (e.g. 30s, 5m)" })),
2598
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
2599
+ }),
2600
+ async execute(_toolCallId, params) {
2601
+ const result = await deps.workflowWaitTool({
2602
+ workflowId: params.workflow_id,
2603
+ timeout: params.timeout,
2604
+ supervisorId: params.supervisor_id
2605
+ });
2606
+ return {
2607
+ content: [{
2608
+ type: "text",
2609
+ text: JSON.stringify(result, null, 2)
2610
+ }],
2611
+ details: result
2612
+ };
2613
+ }
2614
+ });
2615
+ pi.registerTool({
2616
+ name: "avenor_workflow_inspect",
2617
+ label: "Avenor Workflow Inspect",
2618
+ description: "Return the full instance detail for a workflow",
2619
+ parameters: Type.Object({
2620
+ workflow_id: Type.String({ description: "Workflow ID" }),
2621
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
2622
+ }),
2623
+ async execute(_toolCallId, params) {
2624
+ const result = await deps.workflowInspectTool({
2625
+ workflowId: params.workflow_id,
2626
+ supervisorId: params.supervisor_id
2627
+ });
2628
+ return {
2629
+ content: [{
2630
+ type: "text",
2631
+ text: JSON.stringify(result, null, 2)
2632
+ }],
2633
+ details: result
2634
+ };
2635
+ }
2636
+ });
2637
+ pi.registerTool({
2638
+ name: "avenor_workflow_events",
2639
+ label: "Avenor Workflow Events",
2640
+ description: "Read log events from a workflow instance's event log",
2641
+ parameters: Type.Object({
2642
+ workflow_id: Type.String({ description: "Workflow ID" }),
2643
+ after_seq: Type.Optional(Type.Number({ description: "Only include events after this sequence number" })),
2644
+ limit: Type.Optional(Type.Number({ description: "Max events to return (default 50)" })),
2645
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
2646
+ }),
2647
+ async execute(_toolCallId, params) {
2648
+ const result = await deps.workflowEventsTool({
2649
+ workflowId: params.workflow_id,
2650
+ afterSeq: params.after_seq,
2651
+ limit: params.limit,
2652
+ supervisorId: params.supervisor_id
2653
+ });
2654
+ return {
2655
+ content: [{
2656
+ type: "text",
2657
+ text: JSON.stringify(result, null, 2)
2658
+ }],
2659
+ details: result
2660
+ };
2661
+ }
2662
+ });
2663
+ pi.registerTool({
2664
+ name: "avenor_workflow_complete",
2665
+ label: "Avenor Workflow Complete",
2666
+ description: "Atomically complete a machine/external handoff activation",
2667
+ parameters: Type.Object({
2668
+ workflow_id: Type.String({ description: "Workflow ID" }),
2669
+ node_id: Type.String({ description: "Node ID" }),
2670
+ activation_id: Type.String({ description: "Activation ID" }),
2671
+ attempt_id: Type.String({ description: "Attempt ID" }),
2672
+ lease_id: Type.String({ description: "Lease ID" }),
2673
+ owner_token: Type.String({ description: "Owner token" }),
2674
+ outcome: Type.String({ description: "Outcome" }),
2675
+ outputs: Type.Optional(Type.Unknown({ description: "Outputs" })),
2676
+ artifacts: Type.Optional(Type.Unknown({ description: "Artifacts" })),
2677
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
2678
+ }),
2679
+ async execute(_toolCallId, params) {
2680
+ const { supervisor_id, ...rest } = params;
2681
+ const result = await deps.workflowCompleteTool({
2682
+ ...rest,
2683
+ supervisorId: supervisor_id
2684
+ });
2685
+ return {
2686
+ content: [{
2687
+ type: "text",
2688
+ text: JSON.stringify(result, null, 2)
2689
+ }],
2690
+ details: result
2691
+ };
2692
+ }
2693
+ });
2694
+ pi.registerTool({
2695
+ name: "avenor_workflow_gate",
2696
+ label: "Avenor Workflow Gate",
2697
+ description: "Record a gate decision on a parked awaiting_gate activation",
2698
+ parameters: Type.Object({
2699
+ workflow_id: Type.String({ description: "Workflow ID" }),
2700
+ node_id: Type.String({ description: "Node ID" }),
2701
+ gate_id: Type.String({ description: "Gate ID" }),
2702
+ activation_id: Type.String({ description: "Activation ID" }),
2703
+ operation: Type.Union([
2704
+ Type.Literal("satisfy"),
2705
+ Type.Literal("reject"),
2706
+ Type.Literal("waive"),
2707
+ Type.Literal("external_result")
2708
+ ], { description: "Gate operation" }),
2709
+ actor: Type.Optional(Type.String({ description: "Actor" })),
2710
+ reason: Type.Optional(Type.String({ description: "Reason" })),
2711
+ outcome: Type.Optional(Type.String({ description: "Outcome" })),
2712
+ subject: Type.Optional(Type.Unknown({ description: "Subject" })),
2713
+ poll_id: Type.Optional(Type.String({ description: "Poll ID" })),
2714
+ source: Type.Optional(Type.String({ description: "Source" })),
2715
+ result: Type.Optional(Type.String({ description: "Result" })),
2716
+ response_hash: Type.Optional(Type.String({ description: "Response hash" })),
2717
+ observed_at: Type.Optional(Type.String({ description: "Observed at" })),
2718
+ evidence_ids: Type.Optional(Type.Array(Type.String(), { description: "Evidence IDs" })),
2719
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
2720
+ }),
2721
+ async execute(_toolCallId, params) {
2722
+ const { supervisor_id, ...rest } = params;
2723
+ const result = await deps.workflowGateTool({
2724
+ ...rest,
2725
+ supervisorId: supervisor_id
2726
+ });
2727
+ return {
2728
+ content: [{
2729
+ type: "text",
2730
+ text: JSON.stringify(result, null, 2)
2731
+ }],
2732
+ details: result
2733
+ };
2734
+ }
2735
+ });
2320
2736
  pi.registerCommand("avenor-status", {
2321
2737
  description: "Show status of all avenor runs",
2322
2738
  handler: async (_args, ctx) => {
2323
2739
  const entries = await pollRuns();
2740
+ updateWidget(entries);
2324
2741
  if (entries.length === 0) {
2325
2742
  ctx.ui.notify("No active avenor runs", "info");
2326
2743
  return;
@@ -2328,12 +2745,28 @@ function createExtension(deps = defaultDeps) {
2328
2745
  ctx.ui.setWidget("avenor-status-detail", entries.map((entry) => formatRunLine(entry)));
2329
2746
  }
2330
2747
  });
2748
+ pi.registerCommand("avenor-errors", {
2749
+ description: "Show and clear recent Avenor polling errors",
2750
+ handler: async (_args, ctx) => {
2751
+ if (pollingErrors.length === 0) {
2752
+ ctx.ui.setWidget("avenor-errors", void 0);
2753
+ ctx.ui.notify("No recorded avenor polling errors", "info");
2754
+ return;
2755
+ }
2756
+ const errorLines = pollingErrors.map((error) => {
2757
+ const run = error.runId ? ` [${error.runId}]` : "";
2758
+ return `${new Date(error.timestamp).toLocaleTimeString()} ${error.message}${run}: ${error.error}`;
2759
+ });
2760
+ ctx.ui.setWidget("avenor-errors", errorLines);
2761
+ clearPollingErrors();
2762
+ }
2763
+ });
2331
2764
  pi.registerCommand("avenor-watch", {
2332
2765
  description: "Open a live run inspector by run ID, label, or agent name",
2333
2766
  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) => ({
2767
+ const items = Array.from(trackedRuns.values()).filter((run) => run.label.startsWith(prefix) || run.agent?.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => ({
2335
2768
  value: run.runId,
2336
- label: `${sanitizeText(run.label)} (${sanitizeText(run.agent)}, ${sanitizeText(run.runId).slice(0, 8)})`
2769
+ label: `${sanitizeText(run.label)} (${sanitizeText(run.agent ?? "roster")}, ${sanitizeText(run.runId).slice(0, 8)})`
2337
2770
  }));
2338
2771
  return items.length > 0 ? items : null;
2339
2772
  },
@@ -2355,10 +2788,13 @@ function createExtension(deps = defaultDeps) {
2355
2788
  pi.registerCommand("avenor-cancel", {
2356
2789
  description: "Cancel a running avenor sub-agent",
2357
2790
  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) => ({
2359
- value: run.runId,
2360
- label: `${sanitizeText(run.label)} (${sanitizeText(run.agent)}, ${sanitizeText(run.runId).slice(0, 8)})`
2361
- }));
2791
+ const runs = Array.from(trackedRuns.values());
2792
+ const items = runs.filter((run) => run.label.startsWith(prefix) || run.agent?.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => {
2793
+ return {
2794
+ value: runs.some((other) => other !== run && other.runId === run.runId) ? `${run.supervisorId ?? ""}::${run.runId}` : run.runId,
2795
+ label: `${sanitizeText(run.label)} (${sanitizeText(run.agent ?? "roster")}, ${sanitizeText(run.runId).slice(0, 8)})`
2796
+ };
2797
+ });
2362
2798
  return items.length > 0 ? items : null;
2363
2799
  },
2364
2800
  handler: async (args, ctx) => {
@@ -2366,8 +2802,11 @@ function createExtension(deps = defaultDeps) {
2366
2802
  ctx.ui.notify("Usage: /avenor-cancel <run_id>", "warning");
2367
2803
  return;
2368
2804
  }
2369
- const runId = args.trim();
2370
- const run = trackedRuns.get(runId);
2805
+ const raw = args.trim();
2806
+ const sep = raw.lastIndexOf("::");
2807
+ const supervisorId = sep >= 0 ? raw.slice(0, sep) || void 0 : void 0;
2808
+ const runId = sep >= 0 ? raw.slice(sep + 2) : raw;
2809
+ const run = getTrackedRun(runId, supervisorId);
2371
2810
  const label = run?.label ?? runId;
2372
2811
  const runtimeId = run?.runtimeId ?? run?.lastStatus?.runtime_id;
2373
2812
  if (!runtimeId) {
@@ -2392,4 +2831,4 @@ function createExtension(deps = defaultDeps) {
2392
2831
  }
2393
2832
  var src_default = createExtension();
2394
2833
  //#endregion
2395
- export { CHANNEL_POLL_COMPLETED, CHANNEL_RUN_TERMINAL, buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, createPollCompletedPayload, createRunTerminalPayload, src_default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, statusSupervisorId };
2834
+ 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.1",
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.1"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@earendil-works/pi-coding-agent": ">=0.74.0",