@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 +9 -0
- package/dist/index.js +159 -9
- package/package.json +2 -2
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
|
-
|
|
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
|
|
1966
|
-
"Use avenor_result to
|
|
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
|
|
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: "
|
|
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
|
|
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.
|
|
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.
|
|
38
|
+
"@dougbots/avenor-core": "^0.11.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@earendil-works/pi-coding-agent": ">=0.74.0",
|