@dougbots/avenor-pi 0.11.0 → 0.12.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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
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";
1
+ import { InspectResult, StatusResult, Supervisor, answerPermissionTool, brokerReceiveTool, brokerReplyTool, dial, eventsTool, followUpTool, inspectTool, observeRun, resultTool, shutdownTool, spawnTool, statusTool, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool } from "@dougbots/avenor-core";
2
2
  import { EventBus, ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
 
4
4
  //#region src/subagent-broker.d.ts
@@ -134,6 +134,8 @@ interface ExtensionDeps {
134
134
  workflowEventsTool: typeof workflowEventsTool;
135
135
  workflowCompleteTool: typeof workflowCompleteTool;
136
136
  workflowGateTool: typeof workflowGateTool;
137
+ workflowControllerStatusTool: typeof workflowControllerStatusTool;
138
+ workflowReadyTool: typeof workflowReadyTool;
137
139
  observeRun: typeof observeRun;
138
140
  dial: typeof dial;
139
141
  Supervisor: typeof Supervisor;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Input, Key, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
2
  import { Type } from "typebox";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- 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
+ import { Supervisor, THINKING_LEVELS, answerPermissionTool, askTool, brokerReceiveTool, brokerReplyTool, cancelTool, dial, eventsTool, extractEventText, followUpTool, inspectTool, observeRun, peersTool, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool } from "@dougbots/avenor-core";
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  //#region src/subagent-broker.ts
@@ -1230,6 +1230,66 @@ function shutdownLines(details, args, expanded) {
1230
1230
  ...paths.length > CLEANUP_ITEMS ? [itemMarker(paths.length - CLEANUP_ITEMS)] : []
1231
1231
  ];
1232
1232
  }
1233
+ function controllerLeaderLine(leader) {
1234
+ if (!leader) return "Leader: none";
1235
+ const owner = leader.is_this_process === true ? "this process" : `other owner (${scalar(leader.owner_id)})`;
1236
+ const expires = optionalScalar(leader.expires_at);
1237
+ return `Leader: ${owner}${expires ? ` — lease expires ${expires}` : ""}`;
1238
+ }
1239
+ function controllerStatusLines(details, args, expanded) {
1240
+ const status = asRecord(details);
1241
+ if (!status || !stringValue(status.controller_id) || !stringValue(status.desired_state)) return void 0;
1242
+ const controllerId = selected(status, "controller_id", args.controller_id);
1243
+ const desiredState = selected(status, "desired_state");
1244
+ const inflight = numberValue(status.inflight) ?? 0;
1245
+ const maxInflight = numberValue(status.max_inflight);
1246
+ const blocked = asRecord(status.capacity_blocked);
1247
+ const blockedDetail = blocked ? optionalScalar(blocked.detail) : void 0;
1248
+ const lines = [`Controller: ${controllerId} — ${desiredState}`, controllerLeaderLine(asRecord(status.leader))];
1249
+ const candidates = numberValue(status.candidate_count);
1250
+ if (candidates !== void 0) lines.push(`Candidates: ${candidates} (advisory)`);
1251
+ lines.push(`In-flight: ${inflight}${maxInflight !== void 0 ? ` of ${maxInflight}` : ""}`);
1252
+ const lastReconcile = optionalScalar(status.last_reconcile);
1253
+ if (lastReconcile) lines.push(`Last reconcile: ${lastReconcile}`);
1254
+ if (blocked) lines.push(`Capacity blocked: ${selected(blocked, "source")}${blockedDetail ? ` — ${blockedDetail}` : ""}`);
1255
+ const nextPoll = optionalScalar(status.next_poll_at);
1256
+ if (nextPoll) lines.push(`Next external poll: ${nextPoll}`);
1257
+ lines.push(desiredState === "disabled" ? `Guidance: Run \`avenor workflow controller enable ${controllerId}\` to start dispatch and polling.` : `Guidance: Call avenor_workflow_controller_status without controller_id to review all controllers.`);
1258
+ if (!expanded) return lines;
1259
+ const supervisor = stringValue(args.supervisor_id) ? scalar(args.supervisor_id) : "singleton";
1260
+ return [
1261
+ ...lines,
1262
+ `Supervisor: ${supervisor}`,
1263
+ ...maxInflight !== void 0 ? [`Max in-flight: ${maxInflight}`] : [],
1264
+ ...optionalNumber(status.revision) !== void 0 ? [`Revision: ${optionalNumber(status.revision)}`] : [],
1265
+ ...optionalNumber(status.owner_epoch) !== void 0 ? [`Owner epoch: ${optionalNumber(status.owner_epoch)}`] : []
1266
+ ];
1267
+ }
1268
+ function controllerListLines(details, args, expanded) {
1269
+ const result = asRecord(details);
1270
+ if (!result || !Array.isArray(result.controllers) || result.controllers.some((controller) => !asRecord(controller))) return void 0;
1271
+ const controllers = result.controllers.map((controller) => controller);
1272
+ stringValue(args.supervisor_id) && scalar(args.supervisor_id);
1273
+ if (controllers.length === 0) return ["No workflow controllers configured."];
1274
+ const displayed = controllers.slice(0, STATUS_ROWS);
1275
+ const lines = [
1276
+ `Workflow controllers — ${controllers.length}`,
1277
+ ...displayed.map(controllerListRow),
1278
+ ...controllers.length > displayed.length ? [itemMarker(controllers.length - displayed.length)] : []
1279
+ ];
1280
+ if (!expanded) return lines;
1281
+ for (const controller of displayed) {
1282
+ const leader = asRecord(controller.leader);
1283
+ const ownerEpoch = leader ? numberValue(leader.owner_epoch) : void 0;
1284
+ lines.push(`${selected(controller, "controller_id")}:`, controllerLeaderLine(leader), ...ownerEpoch !== void 0 ? [`Owner epoch: ${ownerEpoch}`] : []);
1285
+ }
1286
+ return lines;
1287
+ }
1288
+ function controllerListRow(controller) {
1289
+ const leader = asRecord(controller.leader);
1290
+ const owner = leader ? leader.is_this_process === true ? "this process" : `other owner (${scalar(leader.owner_id)})` : "none";
1291
+ return `${selected(controller, "controller_id")} — ${selected(controller, "desired_state")} — leader: ${owner}`;
1292
+ }
1233
1293
  function render(tool, result, options, theme, build) {
1234
1294
  try {
1235
1295
  return linesText(options.isPartial ? partial(result) : build() ?? fallback(tool), theme);
@@ -1322,8 +1382,20 @@ function renderEventsResult(result, options, theme, args) {
1322
1382
  function renderShutdownResult(result, options, theme, args) {
1323
1383
  return render("shutdown", result, options, theme, () => shutdownLines(result.details, args, options.expanded));
1324
1384
  }
1385
+ function renderWorkflowControllerStatusCall(args, theme) {
1386
+ const pieces = stringValue(args.controller_id) ? [`avenor_workflow_controller_status controller_id ${quoted(args.controller_id)}`] : ["avenor_workflow_controller_status (all controllers)"];
1387
+ const supervisor = optionalQuoted(args.supervisor_id);
1388
+ if (supervisor) pieces.push(`supervisor_id ${supervisor}`);
1389
+ return linesText([pieces.join(" ")], theme);
1390
+ }
1391
+ function renderWorkflowControllerStatusResult(result, options, theme, args) {
1392
+ return render("workflow controller status", result, options, theme, () => controllerStatusLines(result.details, args, options.expanded) ?? controllerListLines(result.details, args, options.expanded));
1393
+ }
1325
1394
  //#endregion
1326
1395
  //#region src/index.ts
1396
+ function asStatusResult(raw) {
1397
+ return raw;
1398
+ }
1327
1399
  const POLL_INTERVAL_MS = 3e3;
1328
1400
  const INSPECT_LIMIT = 128;
1329
1401
  const COMPLETION_OUTPUT_CHARS = 600;
@@ -1349,6 +1421,8 @@ const defaultDeps = {
1349
1421
  workflowEventsTool,
1350
1422
  workflowCompleteTool,
1351
1423
  workflowGateTool,
1424
+ workflowControllerStatusTool,
1425
+ workflowReadyTool,
1352
1426
  observeRun,
1353
1427
  dial,
1354
1428
  Supervisor
@@ -1526,6 +1600,88 @@ function createExtension(deps = defaultDeps, options = {}) {
1526
1600
  let pollingErrorCount = 0;
1527
1601
  const pollingErrors = [];
1528
1602
  let lastStatusEntries = [];
1603
+ const pendingWaits = /* @__PURE__ */ new Set();
1604
+ function interruptPendingWaits(stop, excludeRunKey) {
1605
+ for (const wait of [...pendingWaits]) {
1606
+ if (wait.runKey === excludeRunKey) continue;
1607
+ pendingWaits.delete(wait);
1608
+ wait.interrupt(stop);
1609
+ }
1610
+ }
1611
+ function registerPendingWait(runKey, interrupt) {
1612
+ const wait = {
1613
+ runKey,
1614
+ interrupt
1615
+ };
1616
+ pendingWaits.add(wait);
1617
+ return wait;
1618
+ }
1619
+ function createInterruptibleWait(runKey, signal) {
1620
+ const controller = new AbortController();
1621
+ let stopped;
1622
+ const onSignalAbort = () => controller.abort();
1623
+ if (signal?.aborted) controller.abort();
1624
+ else signal?.addEventListener("abort", onSignalAbort);
1625
+ const pending = registerPendingWait(runKey, (stop) => {
1626
+ if (controller.signal.aborted) return;
1627
+ stopped = stop;
1628
+ controller.abort();
1629
+ });
1630
+ return {
1631
+ signal: controller.signal,
1632
+ get stopped() {
1633
+ return stopped;
1634
+ },
1635
+ dispose() {
1636
+ pendingWaits.delete(pending);
1637
+ signal?.removeEventListener("abort", onSignalAbort);
1638
+ }
1639
+ };
1640
+ }
1641
+ /** Build the early return for a wait interrupted by a sibling stopping
1642
+ * point. When the awaited run itself has since reached a stopping point
1643
+ * (possible when the caller omitted supervisor_id and the registry key
1644
+ * diverged), deliver its real state instead of an interruption notice. */
1645
+ async function resultAfterInterrupt(params, stop, tracked) {
1646
+ const runId = tracked?.runId ?? params.run_id;
1647
+ const supervisorId = tracked?.supervisorId ?? params.supervisor_id;
1648
+ let current;
1649
+ try {
1650
+ current = asStatusResult(await deps.statusTool({
1651
+ runId,
1652
+ supervisorId,
1653
+ view: "full"
1654
+ }));
1655
+ } catch {}
1656
+ if (current !== void 0 && (isTerminalStatus(current.status) || current.status === "waiting")) try {
1657
+ return {
1658
+ ...await deps.resultTool({
1659
+ runId,
1660
+ supervisorId,
1661
+ wait: true,
1662
+ timeout: params.timeout,
1663
+ signal: params.signal
1664
+ }),
1665
+ interrupted_by: {
1666
+ run_id: stop.runId,
1667
+ label: stop.label,
1668
+ status: stop.status
1669
+ }
1670
+ };
1671
+ } catch {}
1672
+ return {
1673
+ run_id: current?.run_id ?? runId,
1674
+ label: current?.label ?? runId,
1675
+ status: current?.status ?? "running",
1676
+ ready: current !== void 0 && isTerminalStatus(current.status),
1677
+ ...current?.pending_permission && { pending_permission: current.pending_permission },
1678
+ interrupted_by: {
1679
+ run_id: stop.runId,
1680
+ label: stop.label,
1681
+ status: stop.status
1682
+ }
1683
+ };
1684
+ }
1529
1685
  const subAgentEnv = options.subAgentEnv !== void 0 ? options.subAgentEnv : readSubAgentEnv();
1530
1686
  const isSubAgentBrokerMode = subAgentEnv !== null;
1531
1687
  let subAgentPollTimer = null;
@@ -1643,7 +1799,7 @@ function createExtension(deps = defaultDeps, options = {}) {
1643
1799
  const liveMap = /* @__PURE__ */ new Map();
1644
1800
  try {
1645
1801
  const allLive = await deps.statusTool({});
1646
- const liveList = Array.isArray(allLive) ? allLive : [allLive];
1802
+ const liveList = "runs" in allLive ? allLive.runs : [allLive];
1647
1803
  for (const result of liveList) liveMap.set(result.run_id, result);
1648
1804
  } catch (err) {
1649
1805
  recordPollingError("singleton-list", "failed to list singleton runs", err);
@@ -1673,11 +1829,10 @@ function createExtension(deps = defaultDeps, options = {}) {
1673
1829
  continue;
1674
1830
  }
1675
1831
  try {
1676
- const raw = await deps.statusTool({
1832
+ const result = asStatusResult(await deps.statusTool({
1677
1833
  runId: run.runId,
1678
1834
  supervisorId: run.supervisorId
1679
- });
1680
- const result = Array.isArray(raw) ? raw[0] : raw;
1835
+ }));
1681
1836
  if (!result) throw new Error("status response was empty");
1682
1837
  run.lastStatus = result;
1683
1838
  run.agent = result.agent ?? run.agent;
@@ -1760,7 +1915,10 @@ function createExtension(deps = defaultDeps, options = {}) {
1760
1915
  const tick = async () => {
1761
1916
  if (!pollingActive || generation !== pollingGeneration) return;
1762
1917
  const prevStatuses = /* @__PURE__ */ new Map();
1763
- for (const [id, run] of trackedRuns) prevStatuses.set(id, run.lastStatus?.status);
1918
+ for (const [id, run] of trackedRuns) prevStatuses.set(id, {
1919
+ status: run.lastStatus?.status,
1920
+ pendingPermission: !!run.lastStatus?.pending_permission
1921
+ });
1764
1922
  const currentPoll = pollRuns();
1765
1923
  pollInFlight = currentPoll;
1766
1924
  const entries = await currentPoll.finally(() => {
@@ -1773,13 +1931,19 @@ function createExtension(deps = defaultDeps, options = {}) {
1773
1931
  const entryKey = trackedRunKey(entry.supervisorId, entry.runId);
1774
1932
  const run = trackedRuns.get(entryKey);
1775
1933
  if (!run) continue;
1934
+ const prevStatus = prevStatuses.get(entryKey);
1935
+ const wasStopped = prevStatus?.status !== void 0 && isTerminalStatus(prevStatus.status) || !!prevStatus?.pendingPermission;
1936
+ if ((entry.pendingPermission || isTerminalStatus(entry.status)) && !wasStopped) interruptPendingWaits({
1937
+ runId: entry.runId,
1938
+ label: entry.label,
1939
+ status: isTerminalStatus(entry.status) ? entry.status : "waiting"
1940
+ }, entryKey);
1776
1941
  if (isTerminalStatus(entry.status)) {
1777
1942
  if (decideCompletion(run) === "skip") {
1778
1943
  if (run.consumed) trackedRuns.delete(entryKey);
1779
1944
  continue;
1780
1945
  }
1781
- const prevStatus = prevStatuses.get(entryKey);
1782
- if (!(prevStatus && isTerminalStatus(prevStatus))) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
1946
+ if (!(prevStatus?.status !== void 0 && isTerminalStatus(prevStatus.status))) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
1783
1947
  const finalOutput = await resolveFinalOutput(run, run.lastStatus);
1784
1948
  try {
1785
1949
  pi.sendUserMessage(buildCompletionText({
@@ -1899,11 +2063,10 @@ function createExtension(deps = defaultDeps, options = {}) {
1899
2063
  supervisorId: run.supervisorId
1900
2064
  });
1901
2065
  try {
1902
- const rawStatus = await deps.statusTool({
2066
+ const status = asStatusResult(await deps.statusTool({
1903
2067
  runId: result.run_id,
1904
2068
  supervisorId: run.supervisorId
1905
- });
1906
- const status = Array.isArray(rawStatus) ? rawStatus[0] : rawStatus;
2069
+ }));
1907
2070
  upsertTrackedRun({
1908
2071
  runId: result.run_id,
1909
2072
  label: result.label,
@@ -1981,12 +2144,11 @@ function createExtension(deps = defaultDeps, options = {}) {
1981
2144
  await observer?.close().catch(() => {});
1982
2145
  if (outcome === "aborted") return { aborted: true };
1983
2146
  try {
1984
- const raw = await deps.statusTool({
1985
- runId: run.runId,
1986
- supervisorId: run.supervisorId
1987
- });
1988
2147
  return {
1989
- status: Array.isArray(raw) ? raw[0] : raw,
2148
+ status: asStatusResult(await deps.statusTool({
2149
+ runId: run.runId,
2150
+ supervisorId: run.supervisorId
2151
+ })),
1990
2152
  aborted: false
1991
2153
  };
1992
2154
  } catch {
@@ -1999,11 +2161,10 @@ function createExtension(deps = defaultDeps, options = {}) {
1999
2161
  if (!firstPoll) await sleep(pollIntervalMs);
2000
2162
  firstPoll = false;
2001
2163
  try {
2002
- const raw = await deps.statusTool({
2164
+ const status = asStatusResult(await deps.statusTool({
2003
2165
  runId: run.runId,
2004
2166
  supervisorId: run.supervisorId
2005
- });
2006
- const status = Array.isArray(raw) ? raw[0] : raw;
2167
+ }));
2007
2168
  if (!status) continue;
2008
2169
  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})` : ""}`;
2009
2170
  onUpdate?.({
@@ -2045,6 +2206,14 @@ function createExtension(deps = defaultDeps, options = {}) {
2045
2206
  stopSubAgentPoll();
2046
2207
  hostPendingAsks.clear();
2047
2208
  subAgentPendingAsks.clear();
2209
+ for (const wait of [...pendingWaits]) {
2210
+ pendingWaits.delete(wait);
2211
+ wait.interrupt({
2212
+ runId: "",
2213
+ status: "shutdown",
2214
+ synthetic: true
2215
+ });
2216
+ }
2048
2217
  sessionCtx = null;
2049
2218
  });
2050
2219
  pi.on("before_agent_start", async () => {
@@ -2073,12 +2242,12 @@ function createExtension(deps = defaultDeps, options = {}) {
2073
2242
  pi.registerTool({
2074
2243
  name: "avenor_spawn",
2075
2244
  label: "Avenor Spawn",
2076
- 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.`,
2245
+ 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. With several blocking spawns in parallel, the first stopping point (permission or completion) interrupts the other waits.`,
2077
2246
  promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, result, inspect, events, follow-up, shutdown)",
2078
2247
  promptGuidelines: [
2079
2248
  "Use avenor_spawn to delegate well-defined tasks to sub-agents.",
2080
2249
  "Use avenor_status (view \"lifecycle\") only to check progress or pending permissions.",
2081
- "Use avenor_result to retrieve the final result of a sub-agent; it waits by default, but set wait=false for runs that already completed.",
2250
+ "Use avenor_result to retrieve the final result of a sub-agent; it waits by default, but set wait=false for runs that already completed. When awaiting multiple runs, the first stopping point (permission or completion) interrupts the other waits.",
2082
2251
  "Use avenor_inspect to review a bounded transcript and tool activity from a run.",
2083
2252
  "Use avenor_events only when you specifically need raw event payloads.",
2084
2253
  "Use avenor_follow_up to iterate on a completed run.",
@@ -2180,21 +2349,56 @@ function createExtension(deps = defaultDeps, options = {}) {
2180
2349
  run_id: result.run_id
2181
2350
  }
2182
2351
  });
2183
- const waitResult = await waitForRun(runRef, signal, onUpdate);
2352
+ const blockingWait = createInterruptibleWait(trackedRunKey(supervisorId, result.run_id), signal);
2353
+ let waitResult;
2354
+ try {
2355
+ waitResult = await waitForRun(runRef, blockingWait.signal, onUpdate);
2356
+ } finally {
2357
+ blockingWait.dispose();
2358
+ }
2359
+ let waitStatus = waitResult.status;
2184
2360
  if (waitResult.aborted) {
2185
- trackedRuns.delete(trackedRunKey(supervisorId, result.run_id));
2186
- return {
2187
- content: [{
2188
- type: "text",
2189
- text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status or avenor_inspect to check.`
2190
- }],
2191
- details: {
2192
- run_id: result.run_id,
2193
- ...spawnIdentityMetadata(hostParams)
2361
+ const stop = blockingWait.stopped;
2362
+ if (stop && !stop.synthetic) {
2363
+ const probed = asStatusResult(await deps.statusTool({
2364
+ runId: result.run_id,
2365
+ supervisorId
2366
+ }).catch(() => void 0));
2367
+ if (probed && (isTerminalStatus(probed.status) || probed.status === "waiting")) waitStatus = probed;
2368
+ else {
2369
+ const tracked = getTrackedRun(result.run_id, supervisorId);
2370
+ if (tracked) tracked.blocking = false;
2371
+ return {
2372
+ content: [{
2373
+ type: "text",
2374
+ text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted: "${stop.label ?? stop.runId}" reached ${stop.status === "waiting" ? "a permission request" : stop.status}. Handle that first; this run's final result is delivered automatically.`
2375
+ }],
2376
+ details: {
2377
+ run_id: result.run_id,
2378
+ interrupted_by: {
2379
+ run_id: stop.runId,
2380
+ label: stop.label,
2381
+ status: stop.status
2382
+ },
2383
+ ...spawnIdentityMetadata(hostParams)
2384
+ }
2385
+ };
2194
2386
  }
2195
- };
2387
+ } else {
2388
+ trackedRuns.delete(trackedRunKey(supervisorId, result.run_id));
2389
+ return {
2390
+ content: [{
2391
+ type: "text",
2392
+ text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status or avenor_inspect to check.`
2393
+ }],
2394
+ details: {
2395
+ run_id: result.run_id,
2396
+ ...spawnIdentityMetadata(hostParams)
2397
+ }
2398
+ };
2399
+ }
2196
2400
  }
2197
- const status = waitResult.status;
2401
+ const status = waitStatus;
2198
2402
  if (!status) return {
2199
2403
  content: [{
2200
2404
  type: "text",
@@ -2333,7 +2537,7 @@ function createExtension(deps = defaultDeps, options = {}) {
2333
2537
  pi.registerTool({
2334
2538
  name: "avenor_result",
2335
2539
  label: "Avenor Result",
2336
- description: "Retrieve the complete final output of a run without transcript or event details. Waits by default; set wait=false to retrieve an already-completed run without blocking.",
2540
+ description: `Retrieve the complete final output of a run without transcript or event details. Waits by default; set wait=false to retrieve an already-completed run without blocking. When several runs are pending, the first stopping point (a permission request or completion on any run) interrupts other blocking waits so they can be handled; re-issue the interrupted wait afterwards.`,
2337
2541
  parameters: Type.Object({
2338
2542
  run_id: Type.String({ description: "Run ID or label" }),
2339
2543
  wait: Type.Optional(Type.Boolean({ description: "Wait for a terminal result (default true); false retrieves the current state without blocking" })),
@@ -2345,14 +2549,21 @@ function createExtension(deps = defaultDeps, options = {}) {
2345
2549
  const previousBlocking = tracked?.blocking;
2346
2550
  if (tracked) tracked.blocking = true;
2347
2551
  let completed = false;
2552
+ const blockingWait = params.wait ?? true ? createInterruptibleWait(trackedRunKey(tracked?.supervisorId ?? params.supervisor_id, tracked?.runId ?? params.run_id), signal) : void 0;
2348
2553
  try {
2349
- const result = await deps.resultTool({
2350
- runId: params.run_id,
2351
- supervisorId: params.supervisor_id,
2352
- wait: params.wait,
2353
- timeout: params.timeout,
2354
- signal
2355
- });
2554
+ let result;
2555
+ try {
2556
+ result = await deps.resultTool({
2557
+ runId: params.run_id,
2558
+ supervisorId: params.supervisor_id,
2559
+ wait: params.wait,
2560
+ timeout: params.timeout,
2561
+ signal: blockingWait?.signal ?? signal
2562
+ });
2563
+ } catch (err) {
2564
+ if (!blockingWait?.stopped || blockingWait.stopped.synthetic) throw err;
2565
+ result = await resultAfterInterrupt(params, blockingWait.stopped, tracked);
2566
+ }
2356
2567
  completed = result.ready;
2357
2568
  if (completed) {
2358
2569
  if (tracked) tracked.consumed = true;
@@ -2374,6 +2585,7 @@ function createExtension(deps = defaultDeps, options = {}) {
2374
2585
  details: result
2375
2586
  };
2376
2587
  } finally {
2588
+ blockingWait?.dispose();
2377
2589
  if (!completed && tracked && getTrackedRun(params.run_id, params.supervisor_id) === tracked) tracked.blocking = previousBlocking ?? false;
2378
2590
  }
2379
2591
  },
@@ -2477,11 +2689,10 @@ function createExtension(deps = defaultDeps, options = {}) {
2477
2689
  supervisorId: params.supervisor_id
2478
2690
  });
2479
2691
  try {
2480
- const rawStatus = await deps.statusTool({
2692
+ const status = asStatusResult(await deps.statusTool({
2481
2693
  runId: result.run_id,
2482
2694
  supervisorId: params.supervisor_id
2483
- });
2484
- const status = Array.isArray(rawStatus) ? rawStatus[0] : rawStatus;
2695
+ }));
2485
2696
  upsertTrackedRun({
2486
2697
  runId: result.run_id,
2487
2698
  label: result.label,
@@ -2518,16 +2729,18 @@ function createExtension(deps = defaultDeps, options = {}) {
2518
2729
  pi.registerTool({
2519
2730
  name: "avenor_ask",
2520
2731
  label: "Avenor Ask",
2521
- description: "Ask another agent run a question and block until it replies.",
2732
+ description: "Ask another agent run a question and block until it replies. An optional timeout_ms bounds the wait; a failed ask reports its message_id and whether it is still pending (withdrawable via avenor_cancel) or already withdrawn.",
2522
2733
  parameters: Type.Object({
2523
2734
  to_run_id: Type.String({ description: "Target run ID" }),
2524
2735
  message: Type.String({ description: "Question to ask the other agent" }),
2736
+ timeout_ms: Type.Optional(Type.Integer({ description: "Optional timeout in milliseconds for the ask" })),
2525
2737
  supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
2526
2738
  }),
2527
2739
  async execute(_toolCallId, params) {
2528
2740
  return askTool({
2529
2741
  toRunId: params.to_run_id,
2530
2742
  message: params.message,
2743
+ timeoutMs: params.timeout_ms,
2531
2744
  supervisorId: params.supervisor_id
2532
2745
  });
2533
2746
  }
@@ -2885,6 +3098,45 @@ function createExtension(deps = defaultDeps, options = {}) {
2885
3098
  };
2886
3099
  }
2887
3100
  });
3101
+ pi.registerTool({
3102
+ name: "avenor_workflow_controller_status",
3103
+ label: "Avenor Workflow Controller Status",
3104
+ description: "Show workflow controller status. With controller_id, returns that controller's full status; without it, lists all controllers with summary state. Create, enable, and disable are CLI-only (avenor workflow controller ...).",
3105
+ parameters: Type.Object({
3106
+ controller_id: Type.Optional(Type.String({ description: "Controller ID; omit to list all controllers" })),
3107
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
3108
+ }),
3109
+ async execute(_toolCallId, params) {
3110
+ const result = await deps.workflowControllerStatusTool({
3111
+ controllerId: params.controller_id,
3112
+ supervisorId: params.supervisor_id
3113
+ });
3114
+ const details = { ...result };
3115
+ if (params.controller_id) try {
3116
+ const ready = await deps.workflowReadyTool({
3117
+ controllerId: params.controller_id,
3118
+ supervisorId: params.supervisor_id
3119
+ });
3120
+ if (Array.isArray(ready.candidates)) details.candidate_count = ready.candidates.length;
3121
+ } catch {}
3122
+ return {
3123
+ content: [{
3124
+ type: "text",
3125
+ text: JSON.stringify(result, null, 2)
3126
+ }],
3127
+ details
3128
+ };
3129
+ },
3130
+ renderCall(args, theme, context) {
3131
+ return renderWorkflowControllerStatusCall(context.args, theme);
3132
+ },
3133
+ renderResult(result, { expanded, isPartial }, theme, context) {
3134
+ return renderWorkflowControllerStatusResult(result, {
3135
+ expanded,
3136
+ isPartial
3137
+ }, theme, context.args);
3138
+ }
3139
+ });
2888
3140
  pi.registerCommand("avenor-status", {
2889
3141
  description: "Show status of all avenor runs",
2890
3142
  handler: async (_args, ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-pi",
3
- "version": "0.11.0",
3
+ "version": "0.12.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.11.0"
38
+ "@dougbots/avenor-core": "^0.12.1"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@earendil-works/pi-coding-agent": ">=0.74.0",