@dougbots/avenor-pi 0.10.0 → 0.10.2

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/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
@@ -118,7 +118,15 @@ interface ExtensionDeps {
118
118
  followUpTool: typeof followUpTool;
119
119
  inspectTool: typeof inspectTool;
120
120
  resultTool: typeof resultTool;
121
+ brokerReceiveTool: typeof brokerReceiveTool;
122
+ brokerReplyTool: typeof brokerReplyTool;
121
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;
122
130
  observeRun: typeof observeRun;
123
131
  dial: typeof dial;
124
132
  Supervisor: typeof Supervisor;
@@ -141,7 +149,11 @@ declare function isTerminalStatus(status: string | undefined): boolean;
141
149
  declare function renderStatusLines(entries: RunStatusEntry[]): string[];
142
150
  declare function compactWhitespace(text: string | undefined, limit?: number): string | undefined;
143
151
  declare function buildInspectPayload(result: InspectResult): Record<string, unknown>;
144
- 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>;
145
157
  declare const _default: (pi: ExtensionAPI) => Promise<void>;
146
158
  //#endregion
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 };
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, validateSpawnSelection } 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
@@ -1256,11 +1271,44 @@ const defaultDeps = {
1256
1271
  followUpTool,
1257
1272
  inspectTool,
1258
1273
  resultTool,
1274
+ brokerReceiveTool,
1275
+ brokerReplyTool,
1259
1276
  shutdownTool,
1277
+ workflowStatusTool,
1278
+ workflowWaitTool,
1279
+ workflowInspectTool,
1280
+ workflowEventsTool,
1281
+ workflowCompleteTool,
1282
+ workflowGateTool,
1260
1283
  observeRun,
1261
1284
  dial,
1262
1285
  Supervisor
1263
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
+ }
1264
1312
  function statusSupervisorId(requestedSupervisorId, spawnedSupervisorId) {
1265
1313
  return requestedSupervisorId ? spawnedSupervisorId : void 0;
1266
1314
  }
@@ -1387,8 +1435,19 @@ function toWatchRunRef(run) {
1387
1435
  supervisorId: run.supervisorId
1388
1436
  };
1389
1437
  }
1390
- 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 = {}) {
1391
1449
  return async function(pi) {
1450
+ const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
1392
1451
  let telemetryGeneration = 0;
1393
1452
  const trackedRuns = /* @__PURE__ */ new Map();
1394
1453
  let pollingActive = false;
@@ -1418,8 +1477,9 @@ function createExtension(deps = defaultDeps) {
1418
1477
  updateWidget(lastStatusEntries);
1419
1478
  }
1420
1479
  function upsertTrackedRun(run, status, blocking = false) {
1421
- const current = trackedRuns.get(run.runId);
1422
- trackedRuns.set(run.runId, {
1480
+ const key = trackedRunKey(run.supervisorId, run.runId);
1481
+ const current = trackedRuns.get(key);
1482
+ trackedRuns.set(key, {
1423
1483
  runId: run.runId,
1424
1484
  agent: run.agent ?? status?.agent ?? current?.agent ?? "unknown",
1425
1485
  label: run.label ?? status?.label ?? current?.label ?? run.runId,
@@ -1429,9 +1489,29 @@ function createExtension(deps = defaultDeps) {
1429
1489
  lastStatus: status ?? current?.lastStatus,
1430
1490
  blocking: current?.blocking ?? blocking,
1431
1491
  permissionNotified: current?.permissionNotified,
1432
- completionPending: current?.completionPending
1492
+ consumed: current?.consumed
1433
1493
  });
1434
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
+ }
1435
1515
  async function resolveFinalOutput(run, status) {
1436
1516
  const preview = finalOutputPreview(status);
1437
1517
  if (preview) return preview;
@@ -1458,7 +1538,8 @@ function createExtension(deps = defaultDeps) {
1458
1538
  }
1459
1539
  const entries = [];
1460
1540
  for (const run of trackedRuns.values()) {
1461
- 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;
1462
1543
  if (live) {
1463
1544
  liveMap.delete(live.run_id);
1464
1545
  run.lastStatus = live;
@@ -1577,18 +1658,16 @@ function createExtension(deps = defaultDeps) {
1577
1658
  updateWidget(entries);
1578
1659
  emitAvenorEvent(pi.events, CHANNEL_POLL_COMPLETED, createPollCompletedPayload(entries, Date.now(), ++telemetryGeneration));
1579
1660
  for (const entry of entries) {
1580
- const run = trackedRuns.get(entry.runId);
1661
+ const entryKey = trackedRunKey(entry.supervisorId, entry.runId);
1662
+ const run = trackedRuns.get(entryKey);
1581
1663
  if (!run) continue;
1582
1664
  if (isTerminalStatus(entry.status)) {
1583
- const prevStatus = prevStatuses.get(entry.runId);
1584
- if (!(prevStatus && isTerminalStatus(prevStatus))) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
1585
- if (run.blocking) continue;
1586
- const action = decideCompletion(run);
1587
- if (action === "defer") {
1588
- run.completionPending = true;
1665
+ if (decideCompletion(run) === "skip") {
1666
+ if (run.consumed) trackedRuns.delete(entryKey);
1589
1667
  continue;
1590
1668
  }
1591
- 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");
1592
1671
  const finalOutput = await resolveFinalOutput(run, run.lastStatus);
1593
1672
  try {
1594
1673
  pi.sendUserMessage(buildCompletionText({
@@ -1608,7 +1687,7 @@ function createExtension(deps = defaultDeps) {
1608
1687
  nestedCount: entry.nestedCount,
1609
1688
  backend: entry.backend
1610
1689
  }));
1611
- trackedRuns.delete(entry.runId);
1690
+ trackedRuns.delete(entryKey);
1612
1691
  } else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
1613
1692
  run.permissionNotified = true;
1614
1693
  if (run.lastStatus) try {
@@ -1627,7 +1706,7 @@ function createExtension(deps = defaultDeps) {
1627
1706
  pollingActive = false;
1628
1707
  console.error("avenor polling loop failed", err);
1629
1708
  });
1630
- }, POLL_INTERVAL_MS);
1709
+ }, pollIntervalMs);
1631
1710
  else {
1632
1711
  pollingActive = false;
1633
1712
  updateWidget([]);
@@ -1805,7 +1884,7 @@ function createExtension(deps = defaultDeps) {
1805
1884
  async function waitForRunByPolling(run, signal, onUpdate) {
1806
1885
  let firstPoll = true;
1807
1886
  while (!signal?.aborted) {
1808
- if (!firstPoll) await sleep(POLL_INTERVAL_MS);
1887
+ if (!firstPoll) await sleep(pollIntervalMs);
1809
1888
  firstPoll = false;
1810
1889
  try {
1811
1890
  const raw = await deps.statusTool({
@@ -1845,9 +1924,12 @@ function createExtension(deps = defaultDeps) {
1845
1924
  lastStatusEntries = [];
1846
1925
  pollingErrorCount = 0;
1847
1926
  pollingErrors.length = 0;
1927
+ startHostAskPoll(ctx);
1848
1928
  });
1849
1929
  pi.on("session_shutdown", async () => {
1850
1930
  await stopPolling();
1931
+ stopHostAskPoll();
1932
+ hostPendingAsks.clear();
1851
1933
  sessionCtx = null;
1852
1934
  });
1853
1935
  pi.on("before_agent_start", async () => {
@@ -1876,7 +1958,7 @@ function createExtension(deps = defaultDeps) {
1876
1958
  pi.registerTool({
1877
1959
  name: "avenor_spawn",
1878
1960
  label: "Avenor Spawn",
1879
- 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.`,
1880
1962
  promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, result, inspect, events, follow-up, shutdown)",
1881
1963
  promptGuidelines: [
1882
1964
  "Use avenor_spawn to delegate well-defined tasks to sub-agents.",
@@ -1983,7 +2065,7 @@ function createExtension(deps = defaultDeps) {
1983
2065
  });
1984
2066
  const waitResult = await waitForRun(runRef, signal, onUpdate);
1985
2067
  if (waitResult.aborted) {
1986
- trackedRuns.delete(result.run_id);
2068
+ trackedRuns.delete(trackedRunKey(supervisorId, result.run_id));
1987
2069
  return {
1988
2070
  content: [{
1989
2071
  type: "text",
@@ -2009,7 +2091,7 @@ function createExtension(deps = defaultDeps) {
2009
2091
  };
2010
2092
  upsertTrackedRun(runRef, status, wait);
2011
2093
  if (status.status === "waiting") {
2012
- const tracked = trackedRuns.get(result.run_id);
2094
+ const tracked = getTrackedRun(result.run_id, supervisorId);
2013
2095
  if (tracked) tracked.permissionNotified = true;
2014
2096
  return {
2015
2097
  content: [{
@@ -2043,7 +2125,7 @@ function createExtension(deps = defaultDeps) {
2043
2125
  status: status.status,
2044
2126
  backend: status.backend
2045
2127
  }));
2046
- trackedRuns.delete(result.run_id);
2128
+ trackedRuns.delete(trackedRunKey(supervisorId, result.run_id));
2047
2129
  return {
2048
2130
  content: [{
2049
2131
  type: "text",
@@ -2142,7 +2224,7 @@ function createExtension(deps = defaultDeps) {
2142
2224
  supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
2143
2225
  }),
2144
2226
  async execute(_toolCallId, params, signal) {
2145
- const tracked = trackedRuns.get(params.run_id);
2227
+ const tracked = getTrackedRun(params.run_id, params.supervisor_id);
2146
2228
  const previousBlocking = tracked?.blocking;
2147
2229
  if (tracked) tracked.blocking = true;
2148
2230
  let completed = false;
@@ -2156,6 +2238,7 @@ function createExtension(deps = defaultDeps) {
2156
2238
  });
2157
2239
  completed = result.ready;
2158
2240
  if (completed) {
2241
+ if (tracked) tracked.consumed = true;
2159
2242
  emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
2160
2243
  runId: params.run_id,
2161
2244
  supervisorId: tracked?.supervisorId ?? params.supervisor_id,
@@ -2164,7 +2247,7 @@ function createExtension(deps = defaultDeps) {
2164
2247
  agent: tracked?.agent ?? "unknown",
2165
2248
  status: result.status ?? "done"
2166
2249
  }));
2167
- trackedRuns.delete(params.run_id);
2250
+ if (tracked) trackedRuns.delete(trackedRunKey(tracked.supervisorId, tracked.runId));
2168
2251
  } else if (tracked && result.status === "waiting") tracked.permissionNotified = true;
2169
2252
  return {
2170
2253
  content: [{
@@ -2174,7 +2257,7 @@ function createExtension(deps = defaultDeps) {
2174
2257
  details: result
2175
2258
  };
2176
2259
  } finally {
2177
- 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;
2178
2261
  }
2179
2262
  },
2180
2263
  renderCall(args, theme, context) {
@@ -2315,6 +2398,96 @@ function createExtension(deps = defaultDeps) {
2315
2398
  }, theme, context.args);
2316
2399
  }
2317
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
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
2409
+ }),
2410
+ async execute(_toolCallId, params) {
2411
+ return askTool({
2412
+ toRunId: params.to_run_id,
2413
+ message: params.message,
2414
+ supervisorId: params.supervisor_id
2415
+ });
2416
+ }
2417
+ });
2418
+ pi.registerTool({
2419
+ name: "avenor_reply",
2420
+ label: "Avenor Reply (host)",
2421
+ 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.",
2422
+ parameters: Type.Object({
2423
+ from_run_id: Type.Optional(Type.String({ description: "Sub-agent run id to reply to (the asker)" })),
2424
+ reply_to_message_id: Type.Optional(Type.String({ description: "The ask's broker message id to answer specifically" })),
2425
+ message: Type.String({ description: "Your answer" }),
2426
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
2427
+ }),
2428
+ async execute(_toolCallId, params) {
2429
+ let fromRunId = params.from_run_id;
2430
+ let messageId = params.reply_to_message_id;
2431
+ if (!messageId) {
2432
+ let match;
2433
+ for (const [, pending] of hostPendingAsks) if (pending.from_run_id === fromRunId && (!match || pending.message_id > match.message_id)) match = pending;
2434
+ if (match) messageId = match.message_id;
2435
+ }
2436
+ if (!fromRunId || !messageId) return {
2437
+ content: [{
2438
+ type: "text",
2439
+ text: "No pending ask found to reply to. Pass from_run_id and/or reply_to_message_id."
2440
+ }],
2441
+ details: { error: true }
2442
+ };
2443
+ await deps.brokerReplyTool({
2444
+ toRunId: fromRunId,
2445
+ replyTo: messageId,
2446
+ message: params.message,
2447
+ supervisorId: params.supervisor_id
2448
+ });
2449
+ hostPendingAsks.delete(messageId);
2450
+ return { content: [{
2451
+ type: "text",
2452
+ text: "Reply sent"
2453
+ }] };
2454
+ }
2455
+ });
2456
+ pi.registerTool({
2457
+ name: "avenor_peers",
2458
+ label: "Avenor Peers",
2459
+ description: "List all connected agent sessions with their status and metadata.",
2460
+ parameters: Type.Object({}),
2461
+ async execute() {
2462
+ const result = await peersTool({});
2463
+ if (result.peers.length === 0) return { content: [{
2464
+ type: "text",
2465
+ text: "No other agent sessions connected."
2466
+ }] };
2467
+ return { content: [{
2468
+ type: "text",
2469
+ text: `**Active sessions:**\n${result.peers.map((p) => {
2470
+ const parts = [p.label ? `${p.label} (${p.run_id})` : p.run_id];
2471
+ const tags = [
2472
+ p.status,
2473
+ p.backend,
2474
+ p.model
2475
+ ].filter(Boolean);
2476
+ if (tags.length) parts.push(`[${tags.join(", ")}]`);
2477
+ return ` ${parts.join(" ")}`;
2478
+ }).join("\n")}`
2479
+ }] };
2480
+ }
2481
+ });
2482
+ pi.registerTool({
2483
+ name: "avenor_cancel",
2484
+ label: "Avenor Cancel",
2485
+ description: "Cancel a pending ask by its message ID.",
2486
+ parameters: Type.Object({ message_id: Type.String({ description: "Message ID of the pending ask to cancel" }) }),
2487
+ async execute(_toolCallId, params) {
2488
+ return cancelTool({ messageId: params.message_id });
2489
+ }
2490
+ });
2318
2491
  pi.registerTool({
2319
2492
  name: "avenor_events",
2320
2493
  label: "Avenor Events",
@@ -2359,8 +2532,20 @@ function createExtension(deps = defaultDeps) {
2359
2532
  force: Type.Optional(Type.Boolean({ description: "Force shutdown instead of graceful" }))
2360
2533
  }),
2361
2534
  async execute(_toolCallId, params) {
2362
- await stopPolling();
2363
- trackedRuns.clear();
2535
+ const target = params.supervisor_id;
2536
+ const targetIsSingleton = target === void 0 || Boolean(deps.Supervisor.isCurrentInstance?.(target));
2537
+ let removed = 0;
2538
+ for (const [key, run] of trackedRuns) if (targetIsSingleton ? !run.supervisorId || Boolean(deps.Supervisor.isCurrentInstance?.(run.supervisorId)) : run.supervisorId === target) {
2539
+ trackedRuns.delete(key);
2540
+ removed++;
2541
+ }
2542
+ if (removed > 0 && trackedRuns.size === 0) await stopPolling();
2543
+ else if (removed > 0) {
2544
+ pollingActive = false;
2545
+ pollingGeneration++;
2546
+ await pollInFlight?.catch(() => {});
2547
+ startPolling();
2548
+ }
2364
2549
  const result = await deps.shutdownTool({
2365
2550
  supervisorId: params.supervisor_id,
2366
2551
  force: params.force
@@ -2383,6 +2568,173 @@ function createExtension(deps = defaultDeps) {
2383
2568
  }, theme, context.args);
2384
2569
  }
2385
2570
  });
2571
+ pi.registerTool({
2572
+ name: "avenor_workflow_status",
2573
+ label: "Avenor Workflow Status",
2574
+ description: "Get lightweight status for a workflow instance",
2575
+ parameters: Type.Object({
2576
+ workflow_id: Type.String({ description: "Workflow ID" }),
2577
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
2578
+ }),
2579
+ async execute(_toolCallId, params) {
2580
+ const result = await deps.workflowStatusTool({
2581
+ workflowId: params.workflow_id,
2582
+ supervisorId: params.supervisor_id
2583
+ });
2584
+ return {
2585
+ content: [{
2586
+ type: "text",
2587
+ text: JSON.stringify(result, null, 2)
2588
+ }],
2589
+ details: result
2590
+ };
2591
+ }
2592
+ });
2593
+ pi.registerTool({
2594
+ name: "avenor_workflow_wait",
2595
+ label: "Avenor Workflow Wait",
2596
+ description: "Wait for a workflow to reach a terminal state or until timeout",
2597
+ parameters: Type.Object({
2598
+ workflow_id: Type.String({ description: "Workflow ID" }),
2599
+ timeout: Type.Optional(Type.String({ description: "Timeout (e.g. 30s, 5m)" })),
2600
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
2601
+ }),
2602
+ async execute(_toolCallId, params) {
2603
+ const result = await deps.workflowWaitTool({
2604
+ workflowId: params.workflow_id,
2605
+ timeout: params.timeout,
2606
+ supervisorId: params.supervisor_id
2607
+ });
2608
+ return {
2609
+ content: [{
2610
+ type: "text",
2611
+ text: JSON.stringify(result, null, 2)
2612
+ }],
2613
+ details: result
2614
+ };
2615
+ }
2616
+ });
2617
+ pi.registerTool({
2618
+ name: "avenor_workflow_inspect",
2619
+ label: "Avenor Workflow Inspect",
2620
+ description: "Return the full instance detail for a workflow",
2621
+ parameters: Type.Object({
2622
+ workflow_id: Type.String({ description: "Workflow ID" }),
2623
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
2624
+ }),
2625
+ async execute(_toolCallId, params) {
2626
+ const result = await deps.workflowInspectTool({
2627
+ workflowId: params.workflow_id,
2628
+ supervisorId: params.supervisor_id
2629
+ });
2630
+ return {
2631
+ content: [{
2632
+ type: "text",
2633
+ text: JSON.stringify(result, null, 2)
2634
+ }],
2635
+ details: result
2636
+ };
2637
+ }
2638
+ });
2639
+ pi.registerTool({
2640
+ name: "avenor_workflow_events",
2641
+ label: "Avenor Workflow Events",
2642
+ description: "Read log events from a workflow instance's event log",
2643
+ parameters: Type.Object({
2644
+ workflow_id: Type.String({ description: "Workflow ID" }),
2645
+ after_seq: Type.Optional(Type.Number({ description: "Only include events after this sequence number" })),
2646
+ limit: Type.Optional(Type.Number({ description: "Max events to return (default 50)" })),
2647
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
2648
+ }),
2649
+ async execute(_toolCallId, params) {
2650
+ const result = await deps.workflowEventsTool({
2651
+ workflowId: params.workflow_id,
2652
+ afterSeq: params.after_seq,
2653
+ limit: params.limit,
2654
+ supervisorId: params.supervisor_id
2655
+ });
2656
+ return {
2657
+ content: [{
2658
+ type: "text",
2659
+ text: JSON.stringify(result, null, 2)
2660
+ }],
2661
+ details: result
2662
+ };
2663
+ }
2664
+ });
2665
+ pi.registerTool({
2666
+ name: "avenor_workflow_complete",
2667
+ label: "Avenor Workflow Complete",
2668
+ description: "Atomically complete a machine/external handoff activation",
2669
+ parameters: Type.Object({
2670
+ workflow_id: Type.String({ description: "Workflow ID" }),
2671
+ node_id: Type.String({ description: "Node ID" }),
2672
+ activation_id: Type.String({ description: "Activation ID" }),
2673
+ attempt_id: Type.String({ description: "Attempt ID" }),
2674
+ lease_id: Type.String({ description: "Lease ID" }),
2675
+ owner_token: Type.String({ description: "Owner token" }),
2676
+ outcome: Type.String({ description: "Outcome" }),
2677
+ outputs: Type.Optional(Type.Unknown({ description: "Outputs" })),
2678
+ artifacts: Type.Optional(Type.Unknown({ description: "Artifacts" })),
2679
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
2680
+ }),
2681
+ async execute(_toolCallId, params) {
2682
+ const { supervisor_id, ...rest } = params;
2683
+ const result = await deps.workflowCompleteTool({
2684
+ ...rest,
2685
+ supervisorId: supervisor_id
2686
+ });
2687
+ return {
2688
+ content: [{
2689
+ type: "text",
2690
+ text: JSON.stringify(result, null, 2)
2691
+ }],
2692
+ details: result
2693
+ };
2694
+ }
2695
+ });
2696
+ pi.registerTool({
2697
+ name: "avenor_workflow_gate",
2698
+ label: "Avenor Workflow Gate",
2699
+ description: "Record a gate decision on a parked awaiting_gate activation",
2700
+ parameters: Type.Object({
2701
+ workflow_id: Type.String({ description: "Workflow ID" }),
2702
+ node_id: Type.String({ description: "Node ID" }),
2703
+ gate_id: Type.String({ description: "Gate ID" }),
2704
+ activation_id: Type.String({ description: "Activation ID" }),
2705
+ operation: Type.Union([
2706
+ Type.Literal("satisfy"),
2707
+ Type.Literal("reject"),
2708
+ Type.Literal("waive"),
2709
+ Type.Literal("external_result")
2710
+ ], { description: "Gate operation" }),
2711
+ actor: Type.Optional(Type.String({ description: "Actor" })),
2712
+ reason: Type.Optional(Type.String({ description: "Reason" })),
2713
+ outcome: Type.Optional(Type.String({ description: "Outcome" })),
2714
+ subject: Type.Optional(Type.Unknown({ description: "Subject" })),
2715
+ poll_id: Type.Optional(Type.String({ description: "Poll ID" })),
2716
+ source: Type.Optional(Type.String({ description: "Source" })),
2717
+ result: Type.Optional(Type.String({ description: "Result" })),
2718
+ response_hash: Type.Optional(Type.String({ description: "Response hash" })),
2719
+ observed_at: Type.Optional(Type.String({ description: "Observed at" })),
2720
+ evidence_ids: Type.Optional(Type.Array(Type.String(), { description: "Evidence IDs" })),
2721
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
2722
+ }),
2723
+ async execute(_toolCallId, params) {
2724
+ const { supervisor_id, ...rest } = params;
2725
+ const result = await deps.workflowGateTool({
2726
+ ...rest,
2727
+ supervisorId: supervisor_id
2728
+ });
2729
+ return {
2730
+ content: [{
2731
+ type: "text",
2732
+ text: JSON.stringify(result, null, 2)
2733
+ }],
2734
+ details: result
2735
+ };
2736
+ }
2737
+ });
2386
2738
  pi.registerCommand("avenor-status", {
2387
2739
  description: "Show status of all avenor runs",
2388
2740
  handler: async (_args, ctx) => {
@@ -2438,10 +2790,13 @@ function createExtension(deps = defaultDeps) {
2438
2790
  pi.registerCommand("avenor-cancel", {
2439
2791
  description: "Cancel a running avenor sub-agent",
2440
2792
  getArgumentCompletions: (prefix) => {
2441
- const items = Array.from(trackedRuns.values()).filter((run) => run.label.startsWith(prefix) || run.agent?.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => ({
2442
- value: run.runId,
2443
- label: `${sanitizeText(run.label)} (${sanitizeText(run.agent ?? "roster")}, ${sanitizeText(run.runId).slice(0, 8)})`
2444
- }));
2793
+ const runs = Array.from(trackedRuns.values());
2794
+ const items = runs.filter((run) => run.label.startsWith(prefix) || run.agent?.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => {
2795
+ return {
2796
+ value: runs.some((other) => other !== run && other.runId === run.runId) ? `${run.supervisorId ?? ""}::${run.runId}` : run.runId,
2797
+ label: `${sanitizeText(run.label)} (${sanitizeText(run.agent ?? "roster")}, ${sanitizeText(run.runId).slice(0, 8)})`
2798
+ };
2799
+ });
2445
2800
  return items.length > 0 ? items : null;
2446
2801
  },
2447
2802
  handler: async (args, ctx) => {
@@ -2449,8 +2804,11 @@ function createExtension(deps = defaultDeps) {
2449
2804
  ctx.ui.notify("Usage: /avenor-cancel <run_id>", "warning");
2450
2805
  return;
2451
2806
  }
2452
- const runId = args.trim();
2453
- const run = trackedRuns.get(runId);
2807
+ const raw = args.trim();
2808
+ const sep = raw.lastIndexOf("::");
2809
+ const supervisorId = sep >= 0 ? raw.slice(0, sep) || void 0 : void 0;
2810
+ const runId = sep >= 0 ? raw.slice(sep + 2) : raw;
2811
+ const run = getTrackedRun(runId, supervisorId);
2454
2812
  const label = run?.label ?? runId;
2455
2813
  const runtimeId = run?.runtimeId ?? run?.lastStatus?.runtime_id;
2456
2814
  if (!runtimeId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-pi",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
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.10.0"
38
+ "@dougbots/avenor-core": "^0.10.2"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@earendil-works/pi-coding-agent": ">=0.74.0",