@dougbots/avenor-pi 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -11,6 +11,12 @@ interface RunStatusEntry {
11
11
  agent: string;
12
12
  pendingPermission?: boolean;
13
13
  permissionDescription?: string;
14
+ /** OS PID of the spawned pi process (pi backend only), for discovering child supervisors. */
15
+ pid?: number;
16
+ /** Backend type, used to determine whether to look for child supervisors. */
17
+ backend?: string;
18
+ /** Count of active (non-terminal) runs on this run's child supervisor, if discovered. */
19
+ nestedCount?: number;
14
20
  }
15
21
  //#endregion
16
22
  //#region src/index.d.ts
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
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, followUpTool, inspectTool, observeRun, resultTool, shutdownTool, spawnTool, statusTool } from "@dougbots/avenor-core";
3
+ import * as path from "node:path";
4
+ import { Supervisor, answerPermissionTool, dial, eventsTool, followUpTool, inspectTool, observeRun, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool } from "@dougbots/avenor-core";
4
5
  //#region src/watch.ts
5
6
  const ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
6
7
  const TRANSCRIPT_SCROLL_STEP = 4;
@@ -552,6 +553,19 @@ function formatRunLine(entry) {
552
553
  const phase = entry.phaseLabel ? ` (${entry.phaseLabel})` : "";
553
554
  return `${emoji} ${entry.label} (${entry.agent}) — ${entry.status}${phase}${perm}`;
554
555
  }
556
+ /**
557
+ * decideCompletion determines whether to skip, defer, or send the completion
558
+ * steering message for a tracked run that has reached terminal status.
559
+ *
560
+ * The deferral prevents duplication when avenor_result consumes the result
561
+ * in the same turn: if the agent calls avenor_result between the defer and
562
+ * the next tick, the run is deleted from tracking and the send never fires.
563
+ */
564
+ function decideCompletion(run) {
565
+ if (run.blocking) return "skip";
566
+ if (!run.completionPending) return "defer";
567
+ return "send";
568
+ }
555
569
  //#endregion
556
570
  //#region src/agent-profile.ts
557
571
  const PROFILE_ENTRY_TYPE = "pi-agents:profile";
@@ -640,7 +654,8 @@ function renderStatusLines(entries) {
640
654
  const emoji = statusEmoji(e.status);
641
655
  const perm = e.pendingPermission ? " 🔒" : "";
642
656
  const phase = e.phaseLabel ? ` [${e.phaseLabel}]` : "";
643
- return `${emoji} ${e.label} — ${e.status}${phase}${perm}`;
657
+ const nested = e.nestedCount ? ` +${e.nestedCount}` : "";
658
+ return `${emoji} ${e.label} — ${e.status}${phase}${perm}${nested}`;
644
659
  });
645
660
  }
646
661
  function compactWhitespace(text, limit = 100) {
@@ -725,7 +740,8 @@ function createExtension(deps = defaultDeps) {
725
740
  startTime: current?.startTime ?? Date.now(),
726
741
  lastStatus: status ?? current?.lastStatus,
727
742
  blocking: current?.blocking ?? blocking,
728
- permissionNotified: current?.permissionNotified
743
+ permissionNotified: current?.permissionNotified,
744
+ completionPending: current?.completionPending
729
745
  });
730
746
  }
731
747
  async function resolveFinalOutput(run, status) {
@@ -742,6 +758,29 @@ function createExtension(deps = defaultDeps) {
742
758
  return;
743
759
  }
744
760
  }
761
+ /**
762
+ * Count active (non-terminal) runs on a child supervisor discovered via
763
+ * the spawned pi process's PID. The child pi process creates its own
764
+ * avenor supervisor at `avenor-mcp-<pid>.sock`; we dial it briefly to
765
+ * list runs and count those still active. Returns 0 on any failure
766
+ * (socket not yet ready, supervisor idle-shut-down, etc.).
767
+ */
768
+ async function countNestedRuns(pid) {
769
+ if (!pid) return 0;
770
+ const socketPath = path.join(socketsRoot(), `avenor-mcp-${pid}.sock`);
771
+ let client;
772
+ try {
773
+ client = await Promise.race([deps.dial(socketPath, { callTimeoutMs: 2e3 }), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("dial timeout")), 2e3))]);
774
+ return (await client.list()).filter((r) => {
775
+ const status = String(r.status ?? "running").toLowerCase();
776
+ return status === "running" || status === "idle";
777
+ }).length;
778
+ } catch {
779
+ return 0;
780
+ } finally {
781
+ client?.close();
782
+ }
783
+ }
745
784
  async function pollRuns() {
746
785
  const liveMap = /* @__PURE__ */ new Map();
747
786
  try {
@@ -765,7 +804,9 @@ function createExtension(deps = defaultDeps) {
765
804
  phaseLabel: live.phase_label,
766
805
  agent: run.agent,
767
806
  pendingPermission: !!live.pending_permission,
768
- permissionDescription: live.pending_permission?.description
807
+ permissionDescription: live.pending_permission?.description,
808
+ pid: live.pid,
809
+ backend: live.backend
769
810
  });
770
811
  continue;
771
812
  }
@@ -785,7 +826,9 @@ function createExtension(deps = defaultDeps) {
785
826
  phaseLabel: result.phase_label,
786
827
  agent: run.agent,
787
828
  pendingPermission: !!result.pending_permission,
788
- permissionDescription: result.pending_permission?.description
829
+ permissionDescription: result.pending_permission?.description,
830
+ pid: result.pid,
831
+ backend: result.backend
789
832
  });
790
833
  } catch (err) {
791
834
  console.error(`avenor pollRuns: statusTool failed for ${run.runId}`, err);
@@ -798,7 +841,9 @@ function createExtension(deps = defaultDeps) {
798
841
  phaseLabel: previous?.phase_label,
799
842
  agent: run.agent,
800
843
  pendingPermission: !!previous?.pending_permission,
801
- permissionDescription: previous?.pending_permission?.description
844
+ permissionDescription: previous?.pending_permission?.description,
845
+ pid: previous?.pid,
846
+ backend: previous?.backend
802
847
  });
803
848
  }
804
849
  }
@@ -810,8 +855,16 @@ function createExtension(deps = defaultDeps) {
810
855
  phaseLabel: live.phase_label,
811
856
  agent: live.agent ?? "unknown",
812
857
  pendingPermission: !!live.pending_permission,
813
- permissionDescription: live.pending_permission?.description
858
+ permissionDescription: live.pending_permission?.description,
859
+ pid: live.pid,
860
+ backend: live.backend
814
861
  });
862
+ const nestedPids = entries.filter((e) => e.backend === "pi" && e.pid && !isTerminalStatus(e.status)).map((e) => e.pid);
863
+ if (nestedPids.length > 0) {
864
+ const nestedCounts = await Promise.all(nestedPids.map((pid) => countNestedRuns(pid)));
865
+ let idx = 0;
866
+ for (const entry of entries) if (entry.backend === "pi" && entry.pid && !isTerminalStatus(entry.status)) entry.nestedCount = nestedCounts[idx++] ?? 0;
867
+ }
815
868
  return entries;
816
869
  }
817
870
  function updateWidget(entries) {
@@ -819,7 +872,11 @@ function createExtension(deps = defaultDeps) {
819
872
  const lines = renderStatusLines(entries);
820
873
  if (lines.length > 0) {
821
874
  sessionCtx.ui.setWidget("avenor-status", lines);
822
- sessionCtx.ui.setStatus("avenor", lines.map((line) => line.slice(0, 60)).join(" "));
875
+ const statusStr = lines.map((line) => line.slice(0, 60)).join(" ");
876
+ const nestedTotal = entries.filter((e) => !isTerminalStatus(e.status)).reduce((sum, e) => sum + (e.nestedCount ?? 0), 0);
877
+ const total = lines.length + nestedTotal;
878
+ const statusWithTotal = nestedTotal > 0 ? `${statusStr} total:${total}` : statusStr;
879
+ sessionCtx.ui.setStatus("avenor", statusWithTotal);
823
880
  } else {
824
881
  sessionCtx.ui.setWidget("avenor-status", void 0);
825
882
  sessionCtx.ui.setStatus("avenor", "");
@@ -845,19 +902,22 @@ function createExtension(deps = defaultDeps) {
845
902
  if (!run) continue;
846
903
  if (isTerminalStatus(entry.status)) {
847
904
  const prevStatus = prevStatuses.get(entry.runId);
848
- if (!prevStatus || !isTerminalStatus(prevStatus)) {
849
- sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
850
- if (!run.blocking && run.lastStatus) {
851
- const finalOutput = await resolveFinalOutput(run, run.lastStatus);
852
- try {
853
- pi.sendUserMessage(buildCompletionText({
854
- runId: run.runId,
855
- label: run.label
856
- }, run.lastStatus, finalOutput), { deliverAs: "followUp" });
857
- } catch (err) {
858
- console.error("avenor tick: sendUserMessage failed", err);
859
- }
860
- }
905
+ if (!(prevStatus && isTerminalStatus(prevStatus))) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
906
+ if (run.blocking) continue;
907
+ const action = decideCompletion(run);
908
+ if (action === "defer") {
909
+ run.completionPending = true;
910
+ continue;
911
+ }
912
+ if (action === "skip") continue;
913
+ const finalOutput = await resolveFinalOutput(run, run.lastStatus);
914
+ try {
915
+ pi.sendUserMessage(buildCompletionText({
916
+ runId: run.runId,
917
+ label: run.label
918
+ }, run.lastStatus, finalOutput), { deliverAs: "followUp" });
919
+ } catch (err) {
920
+ console.error("avenor tick: sendUserMessage failed", err);
861
921
  }
862
922
  trackedRuns.delete(entry.runId);
863
923
  } else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
@@ -1398,14 +1458,16 @@ function createExtension(deps = defaultDeps) {
1398
1458
  run_id: Type.String({ description: "Run ID with the pending permission" }),
1399
1459
  option_id: Type.String({ description: "allow_once | allow_always | deny" }),
1400
1460
  request_id: Type.Optional(Type.String({ description: "Permission request ID (auto-discovered if omitted)" })),
1401
- supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
1461
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
1462
+ message: Type.Optional(Type.String({ description: "Optional write-in response text. Required only when the selected option declares requiresMessage: true." }))
1402
1463
  }),
1403
1464
  async execute(_toolCallId, params) {
1404
1465
  const result = await deps.answerPermissionTool({
1405
1466
  runId: params.run_id,
1406
1467
  optionId: params.option_id,
1407
1468
  requestId: params.request_id,
1408
- supervisorId: params.supervisor_id
1469
+ supervisorId: params.supervisor_id,
1470
+ message: params.message
1409
1471
  });
1410
1472
  return {
1411
1473
  content: [{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-pi",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -35,7 +35,7 @@
35
35
  "dev": "tsdown --watch"
36
36
  },
37
37
  "dependencies": {
38
- "@dougbots/avenor-core": "^0.7.0"
38
+ "@dougbots/avenor-core": "^0.8.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@earendil-works/pi-coding-agent": "*",