@dougbots/avenor-pi 0.10.3 → 0.11.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
@@ -1,6 +1,13 @@
1
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
+ //#region src/subagent-broker.d.ts
5
+ interface SubAgentEnv {
6
+ brokerUrl: string;
7
+ runId: string;
8
+ token: string;
9
+ }
10
+ //#endregion
4
11
  //#region src/types.d.ts
5
12
  interface RunStatusEntry {
6
13
  runId: string;
@@ -152,6 +159,8 @@ declare function buildInspectPayload(result: InspectResult): Record<string, unkn
152
159
  interface ExtensionOptions {
153
160
  /** Polling cadence; injectable so tests can drive deterministic ticks. */
154
161
  pollIntervalMs?: number;
162
+ /** Overrides broker-credential autodetection for sub-agent broker mode (tests). */
163
+ subAgentEnv?: SubAgentEnv | null;
155
164
  }
156
165
  declare function createExtension(deps?: ExtensionDeps, options?: ExtensionOptions): (pi: ExtensionAPI) => Promise<void>;
157
166
  declare const _default: (pi: ExtensionAPI) => Promise<void>;
package/dist/index.js CHANGED
@@ -1,9 +1,78 @@
1
1
  import { Input, Key, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
2
  import { Type } from "typebox";
3
+ import { createHash, randomUUID } from "node:crypto";
3
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
5
  import * as fs from "node:fs";
5
6
  import * as path from "node:path";
6
- import { createHash } from "node:crypto";
7
+ //#region src/subagent-broker.ts
8
+ function readSubAgentEnv(env = process.env) {
9
+ const brokerUrl = env.AVENOR_BROKER_URL ?? "";
10
+ const runId = env.AVENOR_RUN_ID ?? "";
11
+ const token = env.AVENOR_BROKER_TOKEN ?? "";
12
+ if (brokerUrl && runId && token) return {
13
+ brokerUrl,
14
+ runId,
15
+ token
16
+ };
17
+ return null;
18
+ }
19
+ /** Parse a broker control message that is an agent_message into an InboundAsk. */
20
+ function parseControlMessage(msg) {
21
+ if (msg.type !== "agent_message") return null;
22
+ let payload = msg.payload;
23
+ if (typeof payload === "string") try {
24
+ payload = JSON.parse(payload);
25
+ } catch {
26
+ return null;
27
+ }
28
+ const p = payload;
29
+ if (!p || typeof p.message !== "string" || !p.message) return null;
30
+ const messageId = String(p.id ?? "");
31
+ if (!messageId) return null;
32
+ const fromRunId = String(p.from_run_id ?? p.from ?? "");
33
+ if (!fromRunId) return null;
34
+ return {
35
+ from_run_id: fromRunId,
36
+ message_id: messageId,
37
+ message: p.message
38
+ };
39
+ }
40
+ function formatInboundAskBody(ask) {
41
+ return `**📨 Inbound ask**${ask.from_run_id ? ` from \`${ask.from_run_id}\`` : ""}\n\n${ask.message}\n\nTo answer, use avenor_reply({ reply_to_message_id: ${JSON.stringify(ask.message_id)}, message: "..." }).`;
42
+ }
43
+ async function postBroker(env, path, body, fetchImpl = fetch) {
44
+ const res = await fetchImpl(`${env.brokerUrl}${path}`, {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json" },
47
+ body: JSON.stringify({
48
+ run_id: env.runId,
49
+ token: env.token,
50
+ ...body
51
+ })
52
+ });
53
+ if (!res.ok) throw new Error(`broker ${path}: ${res.status} ${(await res.text()).trim()}`);
54
+ return res.json();
55
+ }
56
+ /**
57
+ * Resolve which pending ask to answer. Prefers an explicit message id (and its
58
+ * asker); otherwise the most recent pending ask matching fromRunId.
59
+ */
60
+ function resolveReplyTarget(pending, fromRunId, replyToMessageId) {
61
+ if (replyToMessageId) {
62
+ const hit = pending.get(replyToMessageId);
63
+ if (!hit) return null;
64
+ const from = fromRunId ?? hit.from_run_id;
65
+ if (!from) return null;
66
+ return {
67
+ from_run_id: from,
68
+ message_id: replyToMessageId
69
+ };
70
+ }
71
+ let match;
72
+ for (const [, entry] of pending) if (!fromRunId || entry.from_run_id === fromRunId) match = entry;
73
+ return match ?? null;
74
+ }
75
+ //#endregion
7
76
  //#region src/watch.ts
8
77
  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;
9
78
  const TRANSCRIPT_SCROLL_STEP = 4;
@@ -1457,6 +1526,49 @@ function createExtension(deps = defaultDeps, options = {}) {
1457
1526
  let pollingErrorCount = 0;
1458
1527
  const pollingErrors = [];
1459
1528
  let lastStatusEntries = [];
1529
+ const subAgentEnv = options.subAgentEnv !== void 0 ? options.subAgentEnv : readSubAgentEnv();
1530
+ const isSubAgentBrokerMode = subAgentEnv !== null;
1531
+ let subAgentPollTimer = null;
1532
+ let subAgentPollGeneration = 0;
1533
+ const subAgentPendingAsks = /* @__PURE__ */ new Map();
1534
+ function stopSubAgentPoll() {
1535
+ subAgentPollGeneration++;
1536
+ if (subAgentPollTimer) {
1537
+ clearTimeout(subAgentPollTimer);
1538
+ subAgentPollTimer = null;
1539
+ }
1540
+ }
1541
+ async function startSubAgentPoll(ctx) {
1542
+ if (!subAgentEnv) return;
1543
+ stopSubAgentPoll();
1544
+ const generation = subAgentPollGeneration;
1545
+ const intervalMs = options.pollIntervalMs ?? 2e3;
1546
+ let consecutiveFailures = 0;
1547
+ const tick = async () => {
1548
+ try {
1549
+ const msgs = await postBroker(subAgentEnv, "/poll-control", {}) ?? [];
1550
+ if (consecutiveFailures >= 3) console.error("[avenor] sub-agent broker poll recovered");
1551
+ consecutiveFailures = 0;
1552
+ for (const msg of msgs) {
1553
+ const ask = parseControlMessage(msg);
1554
+ if (!ask) continue;
1555
+ subAgentPendingAsks.set(ask.message_id, {
1556
+ from_run_id: ask.from_run_id,
1557
+ message_id: ask.message_id
1558
+ });
1559
+ await ctx.sendUserMessage(formatInboundAskBody(ask), { deliverAs: "steer" });
1560
+ }
1561
+ } catch (error) {
1562
+ consecutiveFailures++;
1563
+ if (consecutiveFailures === 3) console.error("[avenor] sub-agent broker poll failing:", error instanceof Error ? error.message : error);
1564
+ }
1565
+ if (generation !== subAgentPollGeneration) return;
1566
+ subAgentPollTimer = setTimeout(() => void tick(), intervalMs);
1567
+ subAgentPollTimer.unref?.();
1568
+ };
1569
+ subAgentPollTimer = setTimeout(() => void tick(), intervalMs);
1570
+ subAgentPollTimer.unref?.();
1571
+ }
1460
1572
  function recordPollingError(source, message, error, runId) {
1461
1573
  pollingErrorCount++;
1462
1574
  const payload = createPollErrorPayload({
@@ -1925,11 +2037,14 @@ function createExtension(deps = defaultDeps, options = {}) {
1925
2037
  pollingErrorCount = 0;
1926
2038
  pollingErrors.length = 0;
1927
2039
  startHostAskPoll(ctx);
2040
+ startSubAgentPoll(ctx);
1928
2041
  });
1929
2042
  pi.on("session_shutdown", async () => {
1930
2043
  await stopPolling();
1931
2044
  stopHostAskPoll();
2045
+ stopSubAgentPoll();
1932
2046
  hostPendingAsks.clear();
2047
+ subAgentPendingAsks.clear();
1933
2048
  sessionCtx = null;
1934
2049
  });
1935
2050
  pi.on("before_agent_start", async () => {
@@ -1962,12 +2077,14 @@ function createExtension(deps = defaultDeps, options = {}) {
1962
2077
  promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, result, inspect, events, follow-up, shutdown)",
1963
2078
  promptGuidelines: [
1964
2079
  "Use avenor_spawn to delegate well-defined tasks to sub-agents.",
1965
- "Use avenor_status with view \"lifecycle\" only to check progress or pending permissions.",
1966
- "Use avenor_result to wait for and retrieve a sub-agent final result.",
2080
+ "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.",
1967
2082
  "Use avenor_inspect to review a bounded transcript and tool activity from a run.",
1968
- "Use avenor_events only when raw event payloads are specifically needed.",
2083
+ "Use avenor_events only when you specifically need raw event payloads.",
1969
2084
  "Use avenor_follow_up to iterate on a completed run.",
1970
- "Use avenor_shutdown when done delegating work."
2085
+ "Use avenor_shutdown when done delegating work.",
2086
+ "When roster_entry is set, the roster selects agent, model, and backend; do not pass those fields alongside it.",
2087
+ "If a roster spawn fails validation or resolution, do not retry the roster: respawn without roster_file/roster_entry. Direct selectors (agent, model, backend) are optional; omission uses runtime defaults."
1971
2088
  ],
1972
2089
  parameters: Type.Object({
1973
2090
  agent: Type.Optional(Type.String({ description: "Optional agent name; omission uses the supplied model or runtime defaults" })),
@@ -1988,7 +2105,7 @@ function createExtension(deps = defaultDeps, options = {}) {
1988
2105
  ], { description: "Thinking level; omission uses the backend default" })),
1989
2106
  backend: Type.Optional(Type.String({ description: "Backend override (defaults to pi for direct mode)" })),
1990
2107
  roster_file: Type.Optional(Type.String({ description: "Path to the roster map" })),
1991
- roster_entry: Type.Optional(Type.String({ description: "Roster entry to select" })),
2108
+ roster_entry: Type.Optional(Type.String({ description: "Roster entry to select. Requires roster_file unless a roster context is configured; disables agent, model, and backend" })),
1992
2109
  server_url: Type.Optional(Type.String({ description: "Backend server URL" })),
1993
2110
  supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
1994
2111
  wait: Type.Optional(Type.Boolean({ description: "Block until complete (default true)" }))
@@ -2216,10 +2333,10 @@ function createExtension(deps = defaultDeps, options = {}) {
2216
2333
  pi.registerTool({
2217
2334
  name: "avenor_result",
2218
2335
  label: "Avenor Result",
2219
- description: "Wait for a run to finish and return its complete final output without transcript or event details.",
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.",
2220
2337
  parameters: Type.Object({
2221
- run_id: Type.String({ description: "Run ID to await" }),
2222
- wait: Type.Optional(Type.Boolean({ description: "Wait for a terminal result (default true)" })),
2338
+ run_id: Type.String({ description: "Run ID or label" }),
2339
+ wait: Type.Optional(Type.Boolean({ description: "Wait for a terminal result (default true); false retrieves the current state without blocking" })),
2223
2340
  timeout: Type.Optional(Type.String({ description: "Maximum time to wait (e.g. 30s, 5m)" })),
2224
2341
  supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
2225
2342
  }),
@@ -2426,6 +2543,39 @@ function createExtension(deps = defaultDeps, options = {}) {
2426
2543
  supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
2427
2544
  }),
2428
2545
  async execute(_toolCallId, params) {
2546
+ if (isSubAgentBrokerMode && subAgentEnv) {
2547
+ const target = resolveReplyTarget(subAgentPendingAsks, params.from_run_id, params.reply_to_message_id);
2548
+ if (!target) return {
2549
+ content: [{
2550
+ type: "text",
2551
+ text: "No pending ask found to reply to. Pass from_run_id and/or reply_to_message_id."
2552
+ }],
2553
+ details: { error: true }
2554
+ };
2555
+ subAgentPendingAsks.delete(target.message_id);
2556
+ try {
2557
+ await postBroker(subAgentEnv, "/send", {
2558
+ from_run_id: subAgentEnv.runId,
2559
+ to_run_id: target.from_run_id,
2560
+ type: "agent_message",
2561
+ payload: {
2562
+ id: randomUUID(),
2563
+ from_run_id: subAgentEnv.runId,
2564
+ to_run_id: target.from_run_id,
2565
+ message: params.message,
2566
+ role: "agent",
2567
+ reply_to: target.message_id
2568
+ }
2569
+ });
2570
+ } catch (error) {
2571
+ subAgentPendingAsks.set(target.message_id, target);
2572
+ throw error;
2573
+ }
2574
+ return { content: [{
2575
+ type: "text",
2576
+ text: "Reply sent"
2577
+ }] };
2578
+ }
2429
2579
  let fromRunId = params.from_run_id;
2430
2580
  let messageId = params.reply_to_message_id;
2431
2581
  if (!messageId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-pi",
3
- "version": "0.10.3",
3
+ "version": "0.11.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.10.3"
38
+ "@dougbots/avenor-core": "^0.11.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@earendil-works/pi-coding-agent": ">=0.74.0",