@dougbots/avenor-pi 0.16.0 → 0.17.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/README.md +58 -3
- package/dist/index.d.ts +37 -2
- package/dist/index.js +420 -103
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -14,6 +14,10 @@ which avenor
|
|
|
14
14
|
export AVENOR_BIN=~/.botfiles/bin/avenor
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
You also need Pi 1.4.0 or newer (`pi --version`). Busy-session delivery depends on
|
|
18
|
+
the `agent_before_settle` hook, so a notice raised while you were busy needs that
|
|
19
|
+
version to reach you. A missing notification is not a lost run.
|
|
20
|
+
|
|
17
21
|
## Installation
|
|
18
22
|
|
|
19
23
|
### From npm (once published)
|
|
@@ -112,7 +116,7 @@ Available tools for LLM sub-agent management:
|
|
|
112
116
|
|
|
113
117
|
| Tool | Description |
|
|
114
118
|
|---|---|
|
|
115
|
-
| `avenor_spawn` | Dispatch an agent run (blocking or fire-and-forget). Uses the `pi` backend by default in direct mode; accepts optional `agent`, `model`, `prompt`, `dir`, `roster_file`, and `roster_entry`. |
|
|
119
|
+
| `avenor_spawn` | Dispatch an agent run (blocking or fire-and-forget). Uses the `pi` backend by default in direct mode; accepts optional `agent`, `model`, `prompt`, `dir`, `roster_file`, and `roster_entry`. In host mode, spawns to this process's singleton supervisor (the default) receive a parent broker note for `avenor_message`. Non-local and nested spawns need an address supplied in the brief; their asks are not forwarded to the parent. |
|
|
116
120
|
| `avenor_status` | Get status of a run or all runs; use `view="lifecycle"` for compact progress and permission checks |
|
|
117
121
|
| `avenor_result` | Wait for a run and return its complete final output without transcript details |
|
|
118
122
|
| `avenor_inspect` | Review a bounded transcript, tool activity, permissions, and final output |
|
|
@@ -144,8 +148,9 @@ Each roster entry must contain `backend` and at least one of `agent` or `model`.
|
|
|
144
148
|
Beyond the tools, the extension integrates with Pi's TUI and event system:
|
|
145
149
|
|
|
146
150
|
- **Status widget** — persistent widget showing all active runs with status, phase, and permission state
|
|
147
|
-
- **Footer status** — active runs
|
|
148
|
-
- **
|
|
151
|
+
- **Footer status** — the `avenor` status key carries active runs, the live agent-tree total, the unread notice count, and the current poll-failure count (see [Completion notices](#completion-notices))
|
|
152
|
+
- **Completion notices** — one notice per finished run, delivered immediately when your session is idle and batched into a single continuation when it was busy
|
|
153
|
+
- **Polling diagnostics** — bounded polling errors are emitted on `avenor:poll:error` for companion extensions; a completion-notice delivery that exhausts its retries is emitted separately on `avenor:delivery:error`
|
|
149
154
|
- **Live progress** — blocking `avenor_spawn` calls stream progress updates via `onUpdate`
|
|
150
155
|
- **Context enrichment** — active sub-agents are automatically surfaced in the system prompt via `before_agent_start`
|
|
151
156
|
- **Custom rendering** — The Pi extension renders all Avenor tool calls and results as themed, bounded summaries. Results have collapsed and expanded views.
|
|
@@ -155,6 +160,56 @@ Beyond the tools, the extension integrates with Pi's TUI and event system:
|
|
|
155
160
|
- `/avenor-watch <run_id>` — open a live event feed overlay
|
|
156
161
|
- `/avenor-cancel <run_id>` — cancel a running sub-agent
|
|
157
162
|
|
|
163
|
+
### Completion notices
|
|
164
|
+
|
|
165
|
+
A fire-and-forget spawn (`wait: false`) promises one thing: you hear about the
|
|
166
|
+
run when it ends. Each finished run produces one notice — label, terminal
|
|
167
|
+
status, bounded output preview — and where it lands depends on your session at
|
|
168
|
+
that moment.
|
|
169
|
+
|
|
170
|
+
- **Idle.** The notice is sent as a user message and starts its own turn. It
|
|
171
|
+
counts as unread until Pi confirms the message landed, and while compaction is
|
|
172
|
+
running the send waits for it to finish. A send that is accepted but never
|
|
173
|
+
confirmed is sent at most three times; once that budget is spent the notice
|
|
174
|
+
stays unread for the next settlement and the failure is emitted on
|
|
175
|
+
`avenor:delivery:error`.
|
|
176
|
+
- **Busy.** The notice is queued and the footer shows `unread:N`. The next
|
|
177
|
+
settlement that completes normally appends each queued notice as an
|
|
178
|
+
`avenor-completion` custom message under a single continuation, so three runs
|
|
179
|
+
finishing while you work mean three messages and one wake-up. A settlement
|
|
180
|
+
that ends in abort or error delivers nothing; the notices wait.
|
|
181
|
+
|
|
182
|
+
Every terminal status queues a notice — `done`, `failed`, `timeout`, `killed`.
|
|
183
|
+
|
|
184
|
+
`avenor_result` and a blocking `avenor_spawn` (`wait: true`) own delivery while
|
|
185
|
+
they run; `avenor_result` takes the label you spawned with, so you need not keep
|
|
186
|
+
the id. A `ready` result consumes the run: the queued notice is dropped,
|
|
187
|
+
`unread:N` falls by one, and nothing arrives for it later. A result that is not
|
|
188
|
+
`ready` — interrupted by a sibling's stopping point, timed out, or reporting a
|
|
189
|
+
pending permission — consumes nothing, and its notice arrives on the normal
|
|
190
|
+
schedule. A blocking spawn that reaches completion returns the result inline and
|
|
191
|
+
removes the notice; the footer can show a transient `unread:N` for that run while
|
|
192
|
+
the wait is in flight.
|
|
193
|
+
|
|
194
|
+
#### Footer status
|
|
195
|
+
|
|
196
|
+
One string under the status key `avenor`, in this order:
|
|
197
|
+
|
|
198
|
+
| Token | Meaning |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `🟢 label — running [phase] 🔒 +2` | one per non-terminal run: status emoji, label, status, phase label, `🔒` for a pending permission, `+N` for that run's active descendants |
|
|
201
|
+
| `total:N` | direct active runs plus discovered active descendants; present only when active descendants exist |
|
|
202
|
+
| `unread:N` | notices not yet delivered: queued for a settlement, or in flight for an idle session |
|
|
203
|
+
| `errors:N` | recent poll failures; cleared by a cycle that adds no new error |
|
|
204
|
+
|
|
205
|
+
Missing `unread:N` or `errors:N` means zero. Without `total:N`, count the active run lines. `unread:N` counts notices,
|
|
206
|
+
not runs, and can stay set after the last run exits — an otherwise idle session
|
|
207
|
+
can still advertise `unread:1` until you consume or receive that notice.
|
|
208
|
+
`total:N` tracks live runs and disappears with them. `errors:N` outlives them
|
|
209
|
+
too: it clears when a later poll cycle adds no new error, so it counts recent
|
|
210
|
+
failures rather than a consecutive streak. The bounded history behind
|
|
211
|
+
`/avenor-errors` is longer than `errors:N`; running that command clears both.
|
|
212
|
+
|
|
158
213
|
### Tool-result channels
|
|
159
214
|
|
|
160
215
|
Pi returns JSON model content in `content[0].text` and structured `details` for
|
package/dist/index.d.ts
CHANGED
|
@@ -37,7 +37,11 @@ declare const CHANNEL_POLL_COMPLETED: "avenor:poll:completed";
|
|
|
37
37
|
declare const CHANNEL_RUN_TERMINAL: "avenor:run:terminal";
|
|
38
38
|
/** Emitted when a best-effort polling operation fails. */
|
|
39
39
|
declare const CHANNEL_POLL_ERROR: "avenor:poll:error";
|
|
40
|
+
/** Emitted when a completion-notice delivery fails permanently. */
|
|
41
|
+
declare const CHANNEL_DELIVERY_ERROR: "avenor:delivery:error";
|
|
40
42
|
type PollErrorSource = 'singleton-list' | 'run-status' | 'spawn-status';
|
|
43
|
+
/** Delivery paths whose retries can be exhausted. */
|
|
44
|
+
type DeliveryErrorSource = 'idle-send';
|
|
41
45
|
interface PollErrorPayload {
|
|
42
46
|
/** Polling operation that failed. */
|
|
43
47
|
readonly source: PollErrorSource;
|
|
@@ -60,6 +64,31 @@ interface PollErrorPayloadInput {
|
|
|
60
64
|
readonly count: number;
|
|
61
65
|
readonly timestamp: number;
|
|
62
66
|
}
|
|
67
|
+
interface DeliveryErrorPayload {
|
|
68
|
+
/** Delivery path that failed. */
|
|
69
|
+
readonly source: DeliveryErrorSource;
|
|
70
|
+
/** Run whose completion notice is stuck unread. */
|
|
71
|
+
readonly runId: string;
|
|
72
|
+
/** Label of the run, as shown in completion notices. */
|
|
73
|
+
readonly label: string;
|
|
74
|
+
/** Human-readable failure description. */
|
|
75
|
+
readonly message: string;
|
|
76
|
+
/** Bounded, sanitized error text suitable for debug consumers. */
|
|
77
|
+
readonly error: string;
|
|
78
|
+
/** Delivery attempts made for this notice. */
|
|
79
|
+
readonly attempts: number;
|
|
80
|
+
/** Wall-clock timestamp in epoch milliseconds. */
|
|
81
|
+
readonly timestamp: number;
|
|
82
|
+
}
|
|
83
|
+
interface DeliveryErrorPayloadInput {
|
|
84
|
+
readonly source: DeliveryErrorSource;
|
|
85
|
+
readonly runId: string;
|
|
86
|
+
readonly label: string;
|
|
87
|
+
readonly message: string;
|
|
88
|
+
readonly error: unknown;
|
|
89
|
+
readonly attempts: number;
|
|
90
|
+
readonly timestamp: number;
|
|
91
|
+
}
|
|
63
92
|
type AvenorRunStatus = Readonly<Omit<RunStatusEntry, 'supervisorId' | 'pid' | 'permissionDescription'> & {
|
|
64
93
|
supervisorKey: string;
|
|
65
94
|
}>;
|
|
@@ -79,7 +108,7 @@ interface RunTerminalPayload {
|
|
|
79
108
|
readonly label: string;
|
|
80
109
|
readonly agent: string;
|
|
81
110
|
readonly status: string;
|
|
82
|
-
/** Number of active descendants
|
|
111
|
+
/** Number of active descendants last seen for the run, if available. */
|
|
83
112
|
readonly nestedCount?: number;
|
|
84
113
|
readonly backend?: string;
|
|
85
114
|
}
|
|
@@ -98,6 +127,7 @@ interface AvenorEventMap {
|
|
|
98
127
|
[CHANNEL_POLL_COMPLETED]: PollCompletedPayload;
|
|
99
128
|
[CHANNEL_RUN_TERMINAL]: RunTerminalPayload;
|
|
100
129
|
[CHANNEL_POLL_ERROR]: PollErrorPayload;
|
|
130
|
+
[CHANNEL_DELIVERY_ERROR]: DeliveryErrorPayload;
|
|
101
131
|
}
|
|
102
132
|
type AvenorEventChannel = keyof AvenorEventMap;
|
|
103
133
|
type AvenorEventBus = Pick<EventBus, 'emit' | 'on'>;
|
|
@@ -113,6 +143,8 @@ declare function onAvenorEvent<K extends AvenorEventChannel>(bus: Pick<EventBus,
|
|
|
113
143
|
declare function createPollCompletedPayload(entries: readonly RunStatusEntry[], timestamp: number, generation: number): PollCompletedPayload;
|
|
114
144
|
/** Copy and freeze a terminal payload before publishing it. */
|
|
115
145
|
declare function createRunTerminalPayload(payload: RunTerminalPayloadInput): RunTerminalPayload;
|
|
146
|
+
/** Copy, bound, sanitize, and freeze a delivery failure before publishing it. */
|
|
147
|
+
declare function createDeliveryErrorPayload(payload: DeliveryErrorPayloadInput): DeliveryErrorPayload;
|
|
116
148
|
/** Copy, bound, sanitize, and freeze a polling error before publishing it. */
|
|
117
149
|
declare function createPollErrorPayload(payload: PollErrorPayloadInput): PollErrorPayload;
|
|
118
150
|
//#endregion
|
|
@@ -167,10 +199,13 @@ declare function buildInspectPayload(result: InspectResult): Record<string, unkn
|
|
|
167
199
|
interface ExtensionOptions {
|
|
168
200
|
/** Polling cadence; injectable so tests can drive deterministic ticks. */
|
|
169
201
|
pollIntervalMs?: number;
|
|
202
|
+
/** How long an unconfirmed idle completion send waits before re-arming;
|
|
203
|
+
* injectable so tests can exercise the timeout without fixed sleeps. */
|
|
204
|
+
idleSendTimeoutMs?: number;
|
|
170
205
|
/** Overrides broker-credential autodetection for sub-agent broker mode (tests). */
|
|
171
206
|
subAgentEnv?: SubAgentEnv | null;
|
|
172
207
|
}
|
|
173
208
|
declare function createExtension(deps?: ExtensionDeps, options?: ExtensionOptions): (pi: ExtensionAPI) => Promise<void>;
|
|
174
209
|
declare const _default: (pi: ExtensionAPI) => Promise<void>;
|
|
175
210
|
//#endregion
|
|
176
|
-
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 };
|
|
211
|
+
export { type AvenorEventBus, type AvenorEventChannel, type AvenorEventMap, type AvenorRunStatus, CHANNEL_DELIVERY_ERROR, CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, type DeliveryErrorPayload, type DeliveryErrorPayloadInput, type DeliveryErrorSource, ExtensionDeps, ExtensionOptions, type PollCompletedPayload, type PollErrorPayload, type PollErrorPayloadInput, type PollErrorSource, type RunTerminalPayload, type RunTerminalPayloadInput, buildCompletionText, buildInspectPayload, compactWhitespace, createDeliveryErrorPayload, 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, THINKING_LEVELS, answerPermissionTool, attachOwner, boundFinalOutput, brokerReceiveTool, createChildMessageProvider, createHostMessageProvider, dial, eventsTool, extractEventText, extractReplyMessage, extractTurnText, getSupervisorClient, inspectTool, installAskClaims, isGenerationMismatchError, killTool, messageTool, normalizeAskTimeoutMs, normalizeTurnStopReason, observeRun, peersDetails, peersTool, renderPeerList, resultOutput, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection, workflowCancelTool, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool } from "@dougbots/avenor-core";
|
|
3
|
+
import { SUPERVISOR_RUN_ID, Supervisor, THINKING_LEVELS, answerPermissionTool, attachOwner, boundFinalOutput, brokerReceiveTool, createChildMessageProvider, createHostMessageProvider, dial, eventsTool, extractEventText, extractReplyMessage, extractTurnText, getSupervisorClient, inspectTool, installAskClaims, isClosedSocketError, isGenerationMismatchError, killTool, messageTool, normalizeAskTimeoutMs, normalizeTurnStopReason, observeRun, peersDetails, peersTool, renderPeerList, resultOutput, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection, workflowCancelTool, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, 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";
|
|
@@ -106,6 +106,14 @@ async function postBroker(env, path, body, fetchImpl = fetch, signal) {
|
|
|
106
106
|
if (!res.ok) throw await codedError(path, res);
|
|
107
107
|
return res.json();
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Publish the harness's own user-defined session name for this run, or clear it
|
|
111
|
+
* with an empty name. The broker stores it apart from any model-set rename and
|
|
112
|
+
* prefers it, so peers see the name the human gave the session.
|
|
113
|
+
*/
|
|
114
|
+
async function publishHarnessName(env, name, fetchImpl = fetch) {
|
|
115
|
+
await postBroker(env, "/session_name", { name: name ?? "" }, fetchImpl);
|
|
116
|
+
}
|
|
109
117
|
/** Authenticated GET; broker credentials travel in headers, never the URL. */
|
|
110
118
|
async function getBroker(env, path, fetchImpl = fetch, signal) {
|
|
111
119
|
const res = await fetchImpl(`${env.brokerUrl}${path}`, {
|
|
@@ -536,7 +544,13 @@ function createBrokerChildBroker(env, fetchImpl = fetch) {
|
|
|
536
544
|
}
|
|
537
545
|
},
|
|
538
546
|
async peers() {
|
|
539
|
-
|
|
547
|
+
const body = await getBroker(env, "/sessions", fetchImpl);
|
|
548
|
+
const peers = peerRecords(body);
|
|
549
|
+
const detail = body !== null && typeof body === "object" ? body.peers_error : void 0;
|
|
550
|
+
return typeof detail === "string" && detail ? {
|
|
551
|
+
peers,
|
|
552
|
+
detail
|
|
553
|
+
} : { peers };
|
|
540
554
|
},
|
|
541
555
|
async rename(req) {
|
|
542
556
|
try {
|
|
@@ -1136,21 +1150,6 @@ function formatRunLine(entry) {
|
|
|
1136
1150
|
const phase = entry.phaseLabel ? ` (${entry.phaseLabel})` : "";
|
|
1137
1151
|
return `${emoji} ${entry.label} (${entry.agent}) — ${entry.status}${phase}${perm}`;
|
|
1138
1152
|
}
|
|
1139
|
-
/**
|
|
1140
|
-
* decideCompletion determines whether to skip or send the completion
|
|
1141
|
-
* steering message for a tracked run that has reached terminal status.
|
|
1142
|
-
*
|
|
1143
|
-
* The message is delivered on the same terminal tick — never deferred to a
|
|
1144
|
-
* later cycle — so a run that is the last live one still gets its completion.
|
|
1145
|
-
* `blocking` suppresses it while avenor_result is awaiting (it will deliver
|
|
1146
|
-
* the result itself and set `consumed`), and `consumed` suppresses it once
|
|
1147
|
-
* avenor_result has already returned the result successfully.
|
|
1148
|
-
*/
|
|
1149
|
-
function decideCompletion(run) {
|
|
1150
|
-
if (run.blocking) return "skip";
|
|
1151
|
-
if (run.consumed) return "skip";
|
|
1152
|
-
return "send";
|
|
1153
|
-
}
|
|
1154
1153
|
//#endregion
|
|
1155
1154
|
//#region src/nested-count.ts
|
|
1156
1155
|
const NESTED_DIAL_TIMEOUT_MS = 2e3;
|
|
@@ -1303,6 +1302,8 @@ const CHANNEL_POLL_COMPLETED = "avenor:poll:completed";
|
|
|
1303
1302
|
const CHANNEL_RUN_TERMINAL = "avenor:run:terminal";
|
|
1304
1303
|
/** Emitted when a best-effort polling operation fails. */
|
|
1305
1304
|
const CHANNEL_POLL_ERROR = "avenor:poll:error";
|
|
1305
|
+
/** Emitted when a completion-notice delivery fails permanently. */
|
|
1306
|
+
const CHANNEL_DELIVERY_ERROR = "avenor:delivery:error";
|
|
1306
1307
|
/** Maximum sanitized error text exposed to companion extensions. */
|
|
1307
1308
|
const POLL_ERROR_TEXT_CHARS = 600;
|
|
1308
1309
|
function sanitizeAndBoundText(value) {
|
|
@@ -1360,6 +1361,18 @@ function createRunTerminalPayload(payload) {
|
|
|
1360
1361
|
...payload.backend !== void 0 && { backend: payload.backend }
|
|
1361
1362
|
});
|
|
1362
1363
|
}
|
|
1364
|
+
/** Copy, bound, sanitize, and freeze a delivery failure before publishing it. */
|
|
1365
|
+
function createDeliveryErrorPayload(payload) {
|
|
1366
|
+
return Object.freeze({
|
|
1367
|
+
source: payload.source,
|
|
1368
|
+
runId: payload.runId,
|
|
1369
|
+
label: payload.label,
|
|
1370
|
+
message: sanitizeAndBoundText(payload.message),
|
|
1371
|
+
error: sanitizeAndBoundText(payload.error),
|
|
1372
|
+
attempts: payload.attempts,
|
|
1373
|
+
timestamp: payload.timestamp
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1363
1376
|
/** Copy, bound, sanitize, and freeze a polling error before publishing it. */
|
|
1364
1377
|
function createPollErrorPayload(payload) {
|
|
1365
1378
|
return Object.freeze({
|
|
@@ -1920,6 +1933,8 @@ function asStatusResult(raw) {
|
|
|
1920
1933
|
return raw;
|
|
1921
1934
|
}
|
|
1922
1935
|
const POLL_INTERVAL_MS = 3e3;
|
|
1936
|
+
const IDLE_SEND_TIMEOUT_MS = 1e4;
|
|
1937
|
+
const MAX_IDLE_SEND_ATTEMPTS = 3;
|
|
1923
1938
|
const INSPECT_LIMIT = 128;
|
|
1924
1939
|
const COMPLETION_OUTPUT_CHARS = 600;
|
|
1925
1940
|
const INSPECT_JSON_STRING_CHARS = 600;
|
|
@@ -2092,6 +2107,7 @@ function trackedRunKey(supervisorId, runId) {
|
|
|
2092
2107
|
function createExtension(deps = defaultDeps, options = {}) {
|
|
2093
2108
|
return async function(pi) {
|
|
2094
2109
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
2110
|
+
const idleSendTimeoutMs = options.idleSendTimeoutMs ?? IDLE_SEND_TIMEOUT_MS;
|
|
2095
2111
|
let telemetryGeneration = 0;
|
|
2096
2112
|
const trackedRuns = /* @__PURE__ */ new Map();
|
|
2097
2113
|
let pollingActive = false;
|
|
@@ -2101,6 +2117,9 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2101
2117
|
let pollingErrorCount = 0;
|
|
2102
2118
|
const pollingErrors = [];
|
|
2103
2119
|
let lastStatusEntries = [];
|
|
2120
|
+
let sessionGeneration = 0;
|
|
2121
|
+
let compacting = false;
|
|
2122
|
+
let lastTurnSocketClosed = false;
|
|
2104
2123
|
const pendingWaits = /* @__PURE__ */ new Set();
|
|
2105
2124
|
/** The agent-facing prose clause for a stopping point. A synthetic status
|
|
2106
2125
|
* such as `asking` has no run-status spelling, so it must not appear raw in
|
|
@@ -2173,12 +2192,16 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2173
2192
|
};
|
|
2174
2193
|
}
|
|
2175
2194
|
/** Build the early return for a wait interrupted by a sibling stopping
|
|
2176
|
-
* point. When the awaited run itself has since reached a stopping point
|
|
2177
|
-
*
|
|
2178
|
-
*
|
|
2195
|
+
* point. When the awaited run itself has since reached a stopping point,
|
|
2196
|
+
* deliver its real state instead of an interruption notice. The probe and
|
|
2197
|
+
* redelivery use the caller's original run reference, matching the non-
|
|
2198
|
+
* interrupted path: a label may resolve canonically to a newer run than
|
|
2199
|
+
* the one this call first bound to, and the reconciliation below handles
|
|
2200
|
+
* that divergence the same way. A caller that omitted supervisor_id still
|
|
2201
|
+
* probes through the bound run's supervisor. */
|
|
2179
2202
|
async function resultAfterInterrupt(params, stop, tracked) {
|
|
2180
|
-
const runId =
|
|
2181
|
-
const supervisorId = tracked?.supervisorId
|
|
2203
|
+
const runId = params.run_id;
|
|
2204
|
+
const supervisorId = params.supervisor_id ?? tracked?.supervisorId;
|
|
2182
2205
|
let current;
|
|
2183
2206
|
try {
|
|
2184
2207
|
current = asStatusResult(await deps.statusTool({
|
|
@@ -2228,6 +2251,35 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2228
2251
|
subAgentPollTimer = null;
|
|
2229
2252
|
}
|
|
2230
2253
|
}
|
|
2254
|
+
let publishedSessionName = null;
|
|
2255
|
+
let publishChain = Promise.resolve();
|
|
2256
|
+
let refusedSessionName = null;
|
|
2257
|
+
function publishSessionName(ctx) {
|
|
2258
|
+
if (!subAgentEnv) return publishChain;
|
|
2259
|
+
const env = subAgentEnv;
|
|
2260
|
+
const manager = ctx.sessionManager;
|
|
2261
|
+
if (!manager || typeof manager.getSessionName !== "function") return publishChain;
|
|
2262
|
+
const name = manager.getSessionName();
|
|
2263
|
+
if (publishedSessionName && publishedSessionName.name === name) return publishChain;
|
|
2264
|
+
if (refusedSessionName === name) return publishChain;
|
|
2265
|
+
const attempt = { name };
|
|
2266
|
+
publishedSessionName = attempt;
|
|
2267
|
+
publishChain = publishChain.then(async () => {
|
|
2268
|
+
try {
|
|
2269
|
+
await publishHarnessName(env, name);
|
|
2270
|
+
refusedSessionName = null;
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
if (error instanceof BrokerHttpError && error.status === 400 && error.code === "invalid-name") {
|
|
2273
|
+
refusedSessionName = name;
|
|
2274
|
+
console.error("[avenor] session_name_set refused:", error.message);
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
if (publishedSessionName === attempt) publishedSessionName = null;
|
|
2278
|
+
console.error("[avenor] session_name_set failed:", error instanceof Error ? error.message : error);
|
|
2279
|
+
}
|
|
2280
|
+
});
|
|
2281
|
+
return publishChain;
|
|
2282
|
+
}
|
|
2231
2283
|
async function startSubAgentPoll() {
|
|
2232
2284
|
if (!subAgentEnv) return;
|
|
2233
2285
|
stopSubAgentPoll();
|
|
@@ -2312,6 +2364,40 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2312
2364
|
if (pollingErrors.length > MAX_POLL_ERRORS) pollingErrors.shift();
|
|
2313
2365
|
emitAvenorEvent(pi.events, CHANNEL_POLL_ERROR, payload);
|
|
2314
2366
|
}
|
|
2367
|
+
/** A delivery that exhausted its retries is not a polling failure: emit
|
|
2368
|
+
* it on its own channel so `avenor:poll:error` consumers keep a pure
|
|
2369
|
+
* polling contract. The unread notice is the persistent user-facing
|
|
2370
|
+
* surface; this event is the companion-extension signal. */
|
|
2371
|
+
function recordDeliveryError(run, attempts) {
|
|
2372
|
+
emitAvenorEvent(pi.events, CHANNEL_DELIVERY_ERROR, createDeliveryErrorPayload({
|
|
2373
|
+
source: "idle-send",
|
|
2374
|
+
runId: run.runId,
|
|
2375
|
+
label: run.label,
|
|
2376
|
+
message: "idle completion send was never confirmed",
|
|
2377
|
+
error: /* @__PURE__ */ new Error(`no message_end after ${attempts} attempts`),
|
|
2378
|
+
attempts,
|
|
2379
|
+
timestamp: Date.now()
|
|
2380
|
+
}));
|
|
2381
|
+
console.error(`[avenor] idle completion send for run "${run.label}" was never confirmed after ${attempts} attempts; the notice stays unread`);
|
|
2382
|
+
}
|
|
2383
|
+
/** Latch the per-notice retry budget as spent and report the exhausted
|
|
2384
|
+
* delivery. Idempotent, so the timeout re-arm and the compaction
|
|
2385
|
+
* hand-back cannot double-report the same notice. */
|
|
2386
|
+
function capIdleSend(run) {
|
|
2387
|
+
if (!run.idleSend || run.idleSend.capped) return;
|
|
2388
|
+
run.idleSend.capped = true;
|
|
2389
|
+
recordDeliveryError(run, run.idleSend.attempts);
|
|
2390
|
+
}
|
|
2391
|
+
/** Hand an unconfirmed idle send back to the poll: clear the in-flight
|
|
2392
|
+
* mark and latch the budget when it is spent. Shared by the poll-tick
|
|
2393
|
+
* timeout and the compaction hand-back, which must stay in lockstep. */
|
|
2394
|
+
function rearmIdleSend(run) {
|
|
2395
|
+
const idle = run.idleSend;
|
|
2396
|
+
if (!idle) return;
|
|
2397
|
+
idle.inFlight = false;
|
|
2398
|
+
idle.sentAt = void 0;
|
|
2399
|
+
if (idle.attempts >= MAX_IDLE_SEND_ATTEMPTS) capIdleSend(run);
|
|
2400
|
+
}
|
|
2315
2401
|
function clearPollingErrors() {
|
|
2316
2402
|
pollingErrorCount = 0;
|
|
2317
2403
|
pollingErrors.length = 0;
|
|
@@ -2320,38 +2406,75 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2320
2406
|
function upsertTrackedRun(run, status, blocking = false) {
|
|
2321
2407
|
const key = trackedRunKey(run.supervisorId, run.runId);
|
|
2322
2408
|
const current = trackedRuns.get(key);
|
|
2323
|
-
|
|
2409
|
+
const tracked = current ?? {
|
|
2324
2410
|
runId: run.runId,
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2411
|
+
startTime: Date.now(),
|
|
2412
|
+
agent: "unknown",
|
|
2413
|
+
label: run.label ?? run.runId
|
|
2414
|
+
};
|
|
2415
|
+
tracked.runId = run.runId;
|
|
2416
|
+
tracked.agent = run.agent ?? status?.agent ?? current?.agent ?? tracked.agent;
|
|
2417
|
+
tracked.label = run.label ?? status?.label ?? current?.label ?? tracked.label;
|
|
2418
|
+
tracked.supervisorId = run.supervisorId ?? current?.supervisorId;
|
|
2419
|
+
tracked.runtimeId = run.runtimeId ?? status?.runtime_id ?? current?.runtimeId;
|
|
2420
|
+
if (!current) tracked.startTime = Date.now();
|
|
2421
|
+
tracked.lastStatus = status ?? current?.lastStatus;
|
|
2422
|
+
tracked.blocking = current?.blocking ?? blocking;
|
|
2423
|
+
trackedRuns.set(key, tracked);
|
|
2424
|
+
}
|
|
2425
|
+
function findTrackedByLabel(label, supervisorId) {
|
|
2426
|
+
let match;
|
|
2427
|
+
for (const run of trackedRuns.values()) {
|
|
2428
|
+
if (supervisorId !== void 0 && run.supervisorId !== supervisorId) continue;
|
|
2429
|
+
if (run.label !== label) continue;
|
|
2430
|
+
if (match) return void 0;
|
|
2431
|
+
match = run;
|
|
2432
|
+
}
|
|
2433
|
+
return match;
|
|
2335
2434
|
}
|
|
2336
2435
|
/**
|
|
2337
|
-
*
|
|
2338
|
-
* supervisor is supplied, resolve exactly. Without one, prefer the
|
|
2436
|
+
* Resolve a canonical run id in the per-supervisor tracking namespace.
|
|
2437
|
+
* When a supervisor is supplied, resolve exactly. Without one, prefer the
|
|
2339
2438
|
* singleton namespace and only fall back to a run id walk when it is
|
|
2340
2439
|
* unambiguous across supervisors. Duplicate run ids across supervisors
|
|
2341
2440
|
* without an explicit supervisor return undefined so callers never bind
|
|
2342
2441
|
* flags or telemetry to the wrong supervisor's run.
|
|
2343
2442
|
*/
|
|
2344
|
-
function
|
|
2345
|
-
if (supervisorId !== void 0)
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
let match;
|
|
2349
|
-
for (const run of trackedRuns.values()) {
|
|
2350
|
-
if (run.runId !== runId) continue;
|
|
2351
|
-
if (match) return void 0;
|
|
2352
|
-
match = run;
|
|
2443
|
+
function trackedRunsById(runId, supervisorId) {
|
|
2444
|
+
if (supervisorId !== void 0) {
|
|
2445
|
+
const exact = trackedRuns.get(trackedRunKey(supervisorId, runId));
|
|
2446
|
+
return exact ? [exact] : [];
|
|
2353
2447
|
}
|
|
2354
|
-
|
|
2448
|
+
const singleton = trackedRuns.get(trackedRunKey(void 0, runId));
|
|
2449
|
+
if (singleton) return [singleton];
|
|
2450
|
+
const matches = [];
|
|
2451
|
+
for (const run of trackedRuns.values()) if (run.runId === runId) matches.push(run);
|
|
2452
|
+
return matches;
|
|
2453
|
+
}
|
|
2454
|
+
function getTrackedRunById(runId, supervisorId) {
|
|
2455
|
+
const matches = trackedRunsById(runId, supervisorId);
|
|
2456
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
2457
|
+
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Resolve a user-supplied run reference. A canonical identity always wins;
|
|
2460
|
+
* a label is only a fallback, and an ambiguous canonical id never falls
|
|
2461
|
+
* through to a label.
|
|
2462
|
+
*/
|
|
2463
|
+
function getTrackedRun(runId, supervisorId) {
|
|
2464
|
+
const exact = getTrackedRunById(runId, supervisorId);
|
|
2465
|
+
if (exact) return exact;
|
|
2466
|
+
if (trackedRunsById(runId, supervisorId).length > 1) return void 0;
|
|
2467
|
+
return findTrackedByLabel(runId, supervisorId);
|
|
2468
|
+
}
|
|
2469
|
+
/** Resolve the tracked run a successful result belongs to from the
|
|
2470
|
+
* canonical run id the result tool reports, accepting a runtime-id alias
|
|
2471
|
+
* for the label-resolved run. */
|
|
2472
|
+
function reconcileResultRun(tracked, result, supervisorId) {
|
|
2473
|
+
const runId = result.run_id;
|
|
2474
|
+
if (!runId) return tracked;
|
|
2475
|
+
if (tracked && (tracked.runId === runId || tracked.runtimeId === runId)) return tracked;
|
|
2476
|
+
const namespace = tracked?.supervisorId ?? supervisorId;
|
|
2477
|
+
return trackedRuns.get(trackedRunKey(namespace, runId));
|
|
2355
2478
|
}
|
|
2356
2479
|
async function resolveFinalOutput(run, status) {
|
|
2357
2480
|
const preview = finalOutputPreview(status);
|
|
@@ -2367,6 +2490,67 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2367
2490
|
return;
|
|
2368
2491
|
}
|
|
2369
2492
|
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Fire-and-forget delivery. Pi's extension-facing `sendUserMessage` returns
|
|
2495
|
+
* void and the runtime swallows async rejections, so a send is only known to
|
|
2496
|
+
* have landed when the user message comes back through `message_end`. Only a
|
|
2497
|
+
* synchronous throw is observable here.
|
|
2498
|
+
*/
|
|
2499
|
+
function sendUserMessageLogged(text, failureLog) {
|
|
2500
|
+
try {
|
|
2501
|
+
pi.sendUserMessage(text, { deliverAs: "followUp" });
|
|
2502
|
+
} catch (err) {
|
|
2503
|
+
console.error(failureLog, err);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
function clearPendingNotices() {
|
|
2507
|
+
for (const run of trackedRuns.values()) {
|
|
2508
|
+
run.pendingNotice = void 0;
|
|
2509
|
+
run.idleSend = void 0;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
/**
|
|
2513
|
+
* Emit the single terminal event for a tracked run. Every terminal path
|
|
2514
|
+
* (poll, blocking spawn, avenor_result) goes through here so the event-once
|
|
2515
|
+
* guard and payload shape stay identical. A tracked run emits at most once;
|
|
2516
|
+
* an untracked run emits with the canonical result identity, unguarded.
|
|
2517
|
+
*
|
|
2518
|
+
* @returns whether the event was emitted, so callers can gate one-time
|
|
2519
|
+
* side effects.
|
|
2520
|
+
*/
|
|
2521
|
+
function emitRunTerminal(input) {
|
|
2522
|
+
const { run } = input;
|
|
2523
|
+
if (!run) {
|
|
2524
|
+
if (!input.runId) return false;
|
|
2525
|
+
emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
|
|
2526
|
+
runId: input.runId,
|
|
2527
|
+
supervisorId: input.supervisorId,
|
|
2528
|
+
runtimeId: input.runtimeId,
|
|
2529
|
+
label: input.label,
|
|
2530
|
+
agent: input.agent,
|
|
2531
|
+
status: input.status,
|
|
2532
|
+
nestedCount: input.nestedCount,
|
|
2533
|
+
backend: input.backend
|
|
2534
|
+
}));
|
|
2535
|
+
return true;
|
|
2536
|
+
}
|
|
2537
|
+
if (run.terminalEventEmitted) return false;
|
|
2538
|
+
run.terminalEventEmitted = true;
|
|
2539
|
+
emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
|
|
2540
|
+
runId: run.runId,
|
|
2541
|
+
supervisorId: run.supervisorId,
|
|
2542
|
+
runtimeId: input.runtimeId,
|
|
2543
|
+
label: input.label,
|
|
2544
|
+
agent: input.agent,
|
|
2545
|
+
status: input.status,
|
|
2546
|
+
nestedCount: input.nestedCount,
|
|
2547
|
+
backend: input.backend
|
|
2548
|
+
}));
|
|
2549
|
+
return true;
|
|
2550
|
+
}
|
|
2551
|
+
function sendCompletionNotice(notice) {
|
|
2552
|
+
sendUserMessageLogged(notice.text, "avenor tick: sendUserMessage failed");
|
|
2553
|
+
}
|
|
2370
2554
|
async function pollRuns() {
|
|
2371
2555
|
const errorCountAtPollStart = pollingErrorCount;
|
|
2372
2556
|
const liveMap = /* @__PURE__ */ new Map();
|
|
@@ -2459,7 +2643,11 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2459
2643
|
if (nestedPids.length > 0) {
|
|
2460
2644
|
const nestedCounts = await Promise.all(nestedPids.map((pid) => countNestedRuns(pid, deps.dial)));
|
|
2461
2645
|
let idx = 0;
|
|
2462
|
-
for (const entry of entries) if (entry.backend === "pi" && entry.pid && !isTerminalStatus(entry.status))
|
|
2646
|
+
for (const entry of entries) if (entry.backend === "pi" && entry.pid && !isTerminalStatus(entry.status)) {
|
|
2647
|
+
entry.nestedCount = nestedCounts[idx++] ?? 0;
|
|
2648
|
+
const tracked = trackedRuns.get(trackedRunKey(entry.supervisorId, entry.runId));
|
|
2649
|
+
if (tracked) tracked.nestedCount = entry.nestedCount;
|
|
2650
|
+
}
|
|
2463
2651
|
}
|
|
2464
2652
|
if (pollingErrorCount === errorCountAtPollStart) pollingErrorCount = 0;
|
|
2465
2653
|
return entries;
|
|
@@ -2468,17 +2656,24 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2468
2656
|
lastStatusEntries = entries;
|
|
2469
2657
|
if (!sessionCtx) return;
|
|
2470
2658
|
const lines = renderStatusLines(entries);
|
|
2659
|
+
let unread = 0;
|
|
2660
|
+
for (const run of trackedRuns.values()) if (run.pendingNotice) unread++;
|
|
2471
2661
|
const errorSuffix = pollingErrorCount > 0 ? `errors:${pollingErrorCount}` : "";
|
|
2662
|
+
const unreadSuffix = unread > 0 ? `unread:${unread}` : "";
|
|
2472
2663
|
if (lines.length > 0) {
|
|
2473
2664
|
sessionCtx.ui.setWidget("avenor-status", lines);
|
|
2474
2665
|
const statusStr = lines.map((line) => line.slice(0, 60)).join(" ");
|
|
2475
2666
|
const nestedTotal = entries.filter((e) => !isTerminalStatus(e.status)).reduce((sum, e) => sum + (e.nestedCount ?? 0), 0);
|
|
2476
2667
|
const total = lines.length + nestedTotal;
|
|
2477
|
-
const statusWithTotal = [
|
|
2668
|
+
const statusWithTotal = [
|
|
2669
|
+
nestedTotal > 0 ? `${statusStr} total:${total}` : statusStr,
|
|
2670
|
+
unreadSuffix,
|
|
2671
|
+
errorSuffix
|
|
2672
|
+
].filter(Boolean).join(" ");
|
|
2478
2673
|
sessionCtx.ui.setStatus("avenor", statusWithTotal);
|
|
2479
2674
|
} else {
|
|
2480
2675
|
sessionCtx.ui.setWidget("avenor-status", void 0);
|
|
2481
|
-
sessionCtx.ui.setStatus("avenor", errorSuffix);
|
|
2676
|
+
sessionCtx.ui.setStatus("avenor", [unreadSuffix, errorSuffix].filter(Boolean).join(" "));
|
|
2482
2677
|
}
|
|
2483
2678
|
}
|
|
2484
2679
|
function startPolling() {
|
|
@@ -2499,12 +2694,20 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2499
2694
|
});
|
|
2500
2695
|
if (!pollingActive || generation !== pollingGeneration) return;
|
|
2501
2696
|
updateWidget(entries);
|
|
2697
|
+
let widgetDirty = false;
|
|
2698
|
+
const sessionAtPollStart = sessionGeneration;
|
|
2502
2699
|
emitAvenorEvent(pi.events, CHANNEL_POLL_COMPLETED, createPollCompletedPayload(entries, Date.now(), ++telemetryGeneration));
|
|
2503
2700
|
for (const entry of entries) {
|
|
2504
2701
|
const entryKey = trackedRunKey(entry.supervisorId, entry.runId);
|
|
2505
2702
|
const run = trackedRuns.get(entryKey);
|
|
2506
2703
|
if (!run) continue;
|
|
2507
2704
|
const prevStatus = prevStatuses.get(entryKey);
|
|
2705
|
+
const idle = run.idleSend;
|
|
2706
|
+
if (idle?.inFlight && idle.sentAt !== void 0 && Date.now() - idle.sentAt >= idleSendTimeoutMs) {
|
|
2707
|
+
rearmIdleSend(run);
|
|
2708
|
+
widgetDirty = true;
|
|
2709
|
+
}
|
|
2710
|
+
if (run.idleSend?.inFlight && isTerminalStatus(entry.status)) continue;
|
|
2508
2711
|
const wasStopped = prevStatus?.status !== void 0 && isTerminalStatus(prevStatus.status) || !!prevStatus?.pendingPermission;
|
|
2509
2712
|
if ((entry.pendingPermission || isTerminalStatus(entry.status)) && !wasStopped) interruptPendingWaits({
|
|
2510
2713
|
runId: entry.runId,
|
|
@@ -2512,45 +2715,61 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2512
2715
|
status: isTerminalStatus(entry.status) ? entry.status : "waiting"
|
|
2513
2716
|
}, entryKey);
|
|
2514
2717
|
if (isTerminalStatus(entry.status)) {
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
if (
|
|
2520
|
-
|
|
2521
|
-
try {
|
|
2522
|
-
pi.sendUserMessage(buildCompletionText({
|
|
2523
|
-
runId: run.runId,
|
|
2524
|
-
label: run.label
|
|
2525
|
-
}, run.lastStatus, finalOutput), { deliverAs: "followUp" });
|
|
2526
|
-
} catch (err) {
|
|
2527
|
-
console.error("avenor tick: sendUserMessage failed", err);
|
|
2528
|
-
}
|
|
2529
|
-
emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
|
|
2530
|
-
runId: entry.runId,
|
|
2531
|
-
supervisorId: entry.supervisorId,
|
|
2718
|
+
const finalOutput = run.pendingNotice ? void 0 : await resolveFinalOutput(run, run.lastStatus);
|
|
2719
|
+
if (sessionAtPollStart !== sessionGeneration) continue;
|
|
2720
|
+
if (trackedRuns.get(entryKey) !== run) continue;
|
|
2721
|
+
const wasTerminalBefore = prevStatus?.status !== void 0 && isTerminalStatus(prevStatus.status);
|
|
2722
|
+
if (emitRunTerminal({
|
|
2723
|
+
run,
|
|
2532
2724
|
runtimeId: entry.runtimeId,
|
|
2533
2725
|
label: entry.label,
|
|
2534
2726
|
agent: entry.agent,
|
|
2535
2727
|
status: entry.status,
|
|
2536
|
-
nestedCount: entry.nestedCount,
|
|
2728
|
+
nestedCount: entry.nestedCount ?? run.nestedCount,
|
|
2537
2729
|
backend: entry.backend
|
|
2538
|
-
}))
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
run.
|
|
2542
|
-
|
|
2543
|
-
|
|
2730
|
+
})) {
|
|
2731
|
+
if (!wasTerminalBefore && !run.blocking) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
|
|
2732
|
+
}
|
|
2733
|
+
const notice = run.pendingNotice ?? {
|
|
2734
|
+
status: entry.status,
|
|
2735
|
+
text: buildCompletionText({
|
|
2544
2736
|
runId: run.runId,
|
|
2545
|
-
label: run.label
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2737
|
+
label: run.label
|
|
2738
|
+
}, run.lastStatus, finalOutput)
|
|
2739
|
+
};
|
|
2740
|
+
if (run.blocking) {
|
|
2741
|
+
if (!run.pendingNotice) {
|
|
2742
|
+
run.pendingNotice = notice;
|
|
2743
|
+
widgetDirty = true;
|
|
2744
|
+
}
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
if (sessionCtx?.isIdle() === true && !compacting && !run.idleSend?.capped) {
|
|
2748
|
+
run.pendingNotice = notice;
|
|
2749
|
+
run.idleSend = {
|
|
2750
|
+
inFlight: true,
|
|
2751
|
+
sentAt: Date.now(),
|
|
2752
|
+
attempts: (run.idleSend?.attempts ?? 0) + 1,
|
|
2753
|
+
capped: false
|
|
2754
|
+
};
|
|
2755
|
+
widgetDirty = true;
|
|
2756
|
+
sendCompletionNotice(notice);
|
|
2757
|
+
continue;
|
|
2758
|
+
} else if (!run.pendingNotice) {
|
|
2759
|
+
run.pendingNotice = notice;
|
|
2760
|
+
widgetDirty = true;
|
|
2550
2761
|
}
|
|
2762
|
+
} else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
|
|
2763
|
+
run.permissionNotified = true;
|
|
2764
|
+
if (run.lastStatus) sendUserMessageLogged(buildWaitingText({
|
|
2765
|
+
runId: run.runId,
|
|
2766
|
+
label: run.label,
|
|
2767
|
+
agent: run.agent
|
|
2768
|
+
}, run.lastStatus), "avenor tick: sendUserMessage for permission failed");
|
|
2551
2769
|
} else if (!entry.pendingPermission && run.permissionNotified) run.permissionNotified = false;
|
|
2552
2770
|
}
|
|
2553
|
-
if (
|
|
2771
|
+
if (widgetDirty) updateWidget(entries);
|
|
2772
|
+
if ((entries.some((entry) => !isTerminalStatus(entry.status)) || Array.from(trackedRuns.values()).some((run) => run.idleSend?.inFlight)) && pollingActive && generation === pollingGeneration) setTimeout(() => {
|
|
2554
2773
|
tick().catch((err) => {
|
|
2555
2774
|
pollingActive = false;
|
|
2556
2775
|
console.error("avenor polling loop failed", err);
|
|
@@ -2750,6 +2969,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2750
2969
|
const binding = hostProviders.get(key);
|
|
2751
2970
|
hostProviders.delete(key);
|
|
2752
2971
|
hostAskClaims.delete(key);
|
|
2972
|
+
if (key === "") lastTurnSocketClosed = false;
|
|
2753
2973
|
if (!binding || binding.isSingleton || !binding.client) return;
|
|
2754
2974
|
try {
|
|
2755
2975
|
binding.client.close();
|
|
@@ -2758,6 +2978,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2758
2978
|
function disposeHostProviders() {
|
|
2759
2979
|
for (const key of [...hostProviders.keys()]) disposeHostProvider(key);
|
|
2760
2980
|
hostAskClaims.clear();
|
|
2981
|
+
lastTurnSocketClosed = false;
|
|
2761
2982
|
}
|
|
2762
2983
|
async function defaultHostProviderFor(supervisorId) {
|
|
2763
2984
|
const key = supervisorId ?? "";
|
|
@@ -2828,6 +3049,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2828
3049
|
}
|
|
2829
3050
|
pi.on("turn_end", async (event) => {
|
|
2830
3051
|
if (isSubAgentBrokerMode) return;
|
|
3052
|
+
if (lastTurnSocketClosed) return;
|
|
2831
3053
|
if (!deps.Supervisor.currentInstance?.()) return;
|
|
2832
3054
|
const turn = extractTurnText([event.message]);
|
|
2833
3055
|
try {
|
|
@@ -2836,14 +3058,22 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2836
3058
|
const bounded = boundFinalOutput(turn.text);
|
|
2837
3059
|
await provider.recordLastTurn(bounded, normalizeTurnStopReason(turn.stopReason), turn.text.length > bounded.length);
|
|
2838
3060
|
} catch (error) {
|
|
3061
|
+
if (isClosedSocketError(error)) lastTurnSocketClosed = true;
|
|
2839
3062
|
console.error("[avenor] session_last_set failed:", error instanceof Error ? error.message : error);
|
|
2840
3063
|
}
|
|
2841
3064
|
});
|
|
2842
3065
|
pi.on("session_start", async (_event, ctx) => {
|
|
2843
3066
|
sessionCtx = ctx;
|
|
3067
|
+
sessionGeneration++;
|
|
3068
|
+
compacting = false;
|
|
3069
|
+
clearPendingNotices();
|
|
3070
|
+
if (trackedRuns.size > 0) startPolling();
|
|
2844
3071
|
lastStatusEntries = [];
|
|
3072
|
+
lastTurnSocketClosed = false;
|
|
2845
3073
|
pollingErrorCount = 0;
|
|
2846
3074
|
pollingErrors.length = 0;
|
|
3075
|
+
publishedSessionName = null;
|
|
3076
|
+
publishSessionName(ctx);
|
|
2847
3077
|
if (!isSubAgentBrokerMode) startHostAskPoll();
|
|
2848
3078
|
startSubAgentPoll();
|
|
2849
3079
|
});
|
|
@@ -2853,6 +3083,8 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2853
3083
|
disposeHostProviders();
|
|
2854
3084
|
hostPendingAsks.clear();
|
|
2855
3085
|
subAgentPendingAsks.clear();
|
|
3086
|
+
sessionGeneration++;
|
|
3087
|
+
clearPendingNotices();
|
|
2856
3088
|
await stopPolling();
|
|
2857
3089
|
for (const wait of [...pendingWaits]) {
|
|
2858
3090
|
pendingWaits.delete(wait);
|
|
@@ -2864,7 +3096,36 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2864
3096
|
}
|
|
2865
3097
|
sessionCtx = null;
|
|
2866
3098
|
});
|
|
2867
|
-
pi.on("
|
|
3099
|
+
pi.on("message_end", (event) => {
|
|
3100
|
+
if (event.message?.role !== "user") return;
|
|
3101
|
+
const text = extractTurnText([event.message]).text;
|
|
3102
|
+
if (text === "") return;
|
|
3103
|
+
const matches = [...trackedRuns].filter(([, run]) => run.pendingNotice?.text === text);
|
|
3104
|
+
let targets = matches.filter(([, run]) => run.idleSend?.inFlight);
|
|
3105
|
+
if (targets.length === 0) targets = matches.filter(([, run]) => run.idleSend !== void 0);
|
|
3106
|
+
for (const [key, run] of targets) {
|
|
3107
|
+
run.pendingNotice = void 0;
|
|
3108
|
+
run.idleSend = void 0;
|
|
3109
|
+
trackedRuns.delete(key);
|
|
3110
|
+
}
|
|
3111
|
+
if (targets.length > 0) updateWidget(lastStatusEntries);
|
|
3112
|
+
});
|
|
3113
|
+
pi.on("session_before_compact", () => {
|
|
3114
|
+
compacting = true;
|
|
3115
|
+
});
|
|
3116
|
+
const resumeAfterCompaction = () => {
|
|
3117
|
+
compacting = false;
|
|
3118
|
+
for (const run of trackedRuns.values()) {
|
|
3119
|
+
if (run.permissionNotified && run.lastStatus?.pending_permission) run.permissionNotified = false;
|
|
3120
|
+
if (!run.idleSend) continue;
|
|
3121
|
+
rearmIdleSend(run);
|
|
3122
|
+
}
|
|
3123
|
+
if (Array.from(trackedRuns.values()).some((run) => run.pendingNotice || !run.permissionNotified && !!run.lastStatus?.pending_permission)) startPolling();
|
|
3124
|
+
};
|
|
3125
|
+
pi.on("session_compact", resumeAfterCompaction);
|
|
3126
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
3127
|
+
publishSessionName(ctx);
|
|
3128
|
+
compacting = false;
|
|
2868
3129
|
if (trackedRuns.size === 0) return;
|
|
2869
3130
|
const active = (await pollRuns()).filter((entry) => !isTerminalStatus(entry.status));
|
|
2870
3131
|
if (active.length === 0) return;
|
|
@@ -2878,6 +3139,38 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2878
3139
|
display: true
|
|
2879
3140
|
} };
|
|
2880
3141
|
});
|
|
3142
|
+
pi.on("agent_before_settle", async (event) => {
|
|
3143
|
+
if (event.outcome !== "completed") return;
|
|
3144
|
+
const deliverable = [];
|
|
3145
|
+
for (const run of trackedRuns.values()) {
|
|
3146
|
+
if (!run.pendingNotice) continue;
|
|
3147
|
+
if (run.idleSend?.inFlight) continue;
|
|
3148
|
+
deliverable.push({
|
|
3149
|
+
run,
|
|
3150
|
+
notice: run.pendingNotice
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
if (deliverable.length === 0) return;
|
|
3154
|
+
for (const { run } of deliverable) {
|
|
3155
|
+
run.pendingNotice = void 0;
|
|
3156
|
+
trackedRuns.delete(trackedRunKey(run.supervisorId, run.runId));
|
|
3157
|
+
}
|
|
3158
|
+
updateWidget(lastStatusEntries);
|
|
3159
|
+
return {
|
|
3160
|
+
entries: [...event.entries, ...deliverable.map(({ run, notice }) => ({
|
|
3161
|
+
type: "custom_message",
|
|
3162
|
+
customType: "avenor-completion",
|
|
3163
|
+
content: notice.text,
|
|
3164
|
+
display: true,
|
|
3165
|
+
details: {
|
|
3166
|
+
run_id: run.runId,
|
|
3167
|
+
supervisor_id: run.supervisorId,
|
|
3168
|
+
status: notice.status
|
|
3169
|
+
}
|
|
3170
|
+
}))],
|
|
3171
|
+
continue: true
|
|
3172
|
+
};
|
|
3173
|
+
});
|
|
2881
3174
|
pi.registerMessageRenderer("avenor-active-runs", (message, _options, theme) => {
|
|
2882
3175
|
const lines = message.content.split("\n");
|
|
2883
3176
|
let text = "";
|
|
@@ -2950,6 +3243,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2950
3243
|
...params,
|
|
2951
3244
|
backend
|
|
2952
3245
|
};
|
|
3246
|
+
const parentRunId = isSubAgentBrokerMode ? void 0 : !params.supervisor_id || Boolean(deps.Supervisor.isCurrentInstance?.(params.supervisor_id)) ? SUPERVISOR_RUN_ID : void 0;
|
|
2953
3247
|
const result = await deps.spawnTool({
|
|
2954
3248
|
...params.agent !== void 0 ? { agent: params.agent } : {},
|
|
2955
3249
|
...params.prompt !== void 0 ? { prompt: params.prompt } : {},
|
|
@@ -2964,7 +3258,8 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2964
3258
|
...params.roster_entry !== void 0 ? { rosterEntry: params.roster_entry } : {},
|
|
2965
3259
|
...agentProfile !== void 0 ? { agentProfile } : {},
|
|
2966
3260
|
...params.server_url !== void 0 ? { serverUrl: params.server_url } : {},
|
|
2967
|
-
...params.supervisor_id !== void 0 ? { supervisorId: params.supervisor_id } : {}
|
|
3261
|
+
...params.supervisor_id !== void 0 ? { supervisorId: params.supervisor_id } : {},
|
|
3262
|
+
...parentRunId !== void 0 ? { parent_run_id: parentRunId } : {}
|
|
2968
3263
|
});
|
|
2969
3264
|
const supervisorId = statusSupervisorId(params.supervisor_id, result.supervisor_id);
|
|
2970
3265
|
const runRef = {
|
|
@@ -3014,7 +3309,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3014
3309
|
}).catch(() => void 0));
|
|
3015
3310
|
if (probed && (isTerminalStatus(probed.status) || probed.status === "waiting")) waitStatus = probed;
|
|
3016
3311
|
else {
|
|
3017
|
-
const tracked =
|
|
3312
|
+
const tracked = getTrackedRunById(result.run_id, supervisorId);
|
|
3018
3313
|
if (tracked) tracked.blocking = false;
|
|
3019
3314
|
return {
|
|
3020
3315
|
content: [{
|
|
@@ -3033,7 +3328,8 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3033
3328
|
};
|
|
3034
3329
|
}
|
|
3035
3330
|
} else {
|
|
3036
|
-
|
|
3331
|
+
const runKey = trackedRunKey(supervisorId, result.run_id);
|
|
3332
|
+
trackedRuns.delete(runKey);
|
|
3037
3333
|
return {
|
|
3038
3334
|
content: [{
|
|
3039
3335
|
type: "text",
|
|
@@ -3058,10 +3354,13 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3058
3354
|
...spawnIdentityMetadata(hostParams)
|
|
3059
3355
|
}
|
|
3060
3356
|
};
|
|
3061
|
-
upsertTrackedRun(runRef, status, wait);
|
|
3357
|
+
if (trackedRuns.has(trackedRunKey(supervisorId, result.run_id))) upsertTrackedRun(runRef, status, wait);
|
|
3062
3358
|
if (status.status === "waiting") {
|
|
3063
|
-
const tracked =
|
|
3064
|
-
if (tracked)
|
|
3359
|
+
const tracked = getTrackedRunById(result.run_id, supervisorId);
|
|
3360
|
+
if (tracked) {
|
|
3361
|
+
tracked.permissionNotified = true;
|
|
3362
|
+
tracked.blocking = false;
|
|
3363
|
+
}
|
|
3065
3364
|
return {
|
|
3066
3365
|
content: [{
|
|
3067
3366
|
type: "text",
|
|
@@ -3085,16 +3384,19 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3085
3384
|
runId: result.run_id,
|
|
3086
3385
|
supervisorId
|
|
3087
3386
|
}, status);
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3387
|
+
const runKey = trackedRunKey(supervisorId, result.run_id);
|
|
3388
|
+
const tracked = trackedRuns.get(runKey);
|
|
3389
|
+
emitRunTerminal({
|
|
3390
|
+
run: tracked,
|
|
3091
3391
|
runtimeId: status.runtime_id ?? runRef.runtimeId,
|
|
3092
3392
|
label,
|
|
3093
3393
|
agent: params.agent,
|
|
3094
3394
|
status: status.status,
|
|
3395
|
+
nestedCount: tracked?.nestedCount,
|
|
3095
3396
|
backend: status.backend
|
|
3096
|
-
})
|
|
3097
|
-
trackedRuns.delete(
|
|
3397
|
+
});
|
|
3398
|
+
trackedRuns.delete(runKey);
|
|
3399
|
+
updateWidget(lastStatusEntries);
|
|
3098
3400
|
return {
|
|
3099
3401
|
content: [{
|
|
3100
3402
|
type: "text",
|
|
@@ -3112,6 +3414,8 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3112
3414
|
}
|
|
3113
3415
|
};
|
|
3114
3416
|
}
|
|
3417
|
+
const fallthrough = getTrackedRunById(result.run_id, supervisorId);
|
|
3418
|
+
if (fallthrough) fallthrough.blocking = false;
|
|
3115
3419
|
return {
|
|
3116
3420
|
content: [{
|
|
3117
3421
|
type: "text",
|
|
@@ -3197,6 +3501,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3197
3501
|
const previousBlocking = tracked?.blocking;
|
|
3198
3502
|
if (tracked) tracked.blocking = true;
|
|
3199
3503
|
let completed = false;
|
|
3504
|
+
let resultRun = tracked;
|
|
3200
3505
|
const blockingWait = params.wait ?? true ? createInterruptibleWait(trackedRunKey(tracked?.supervisorId ?? params.supervisor_id, tracked?.runId ?? params.run_id), signal) : void 0;
|
|
3201
3506
|
try {
|
|
3202
3507
|
let result;
|
|
@@ -3213,18 +3518,30 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3213
3518
|
result = await resultAfterInterrupt(params, blockingWait.stopped, tracked);
|
|
3214
3519
|
}
|
|
3215
3520
|
completed = result.ready;
|
|
3521
|
+
resultRun = reconcileResultRun(tracked, result, params.supervisor_id);
|
|
3216
3522
|
if (completed) {
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3523
|
+
const idMatches = trackedRunsById(params.run_id, params.supervisor_id).length;
|
|
3524
|
+
const labelMatches = [...trackedRuns.values()].filter((run) => (params.supervisor_id === void 0 || run.supervisorId === params.supervisor_id) && run.label === params.run_id).length;
|
|
3525
|
+
emitRunTerminal({
|
|
3526
|
+
run: resultRun,
|
|
3527
|
+
runId: idMatches > 1 || idMatches === 0 && labelMatches > 1 ? void 0 : result.run_id ?? params.run_id,
|
|
3528
|
+
supervisorId: params.supervisor_id,
|
|
3529
|
+
runtimeId: resultRun?.runtimeId ?? result.runtime_id,
|
|
3530
|
+
label: resultRun?.label ?? result.label ?? params.run_id,
|
|
3531
|
+
agent: resultRun?.agent ?? "unknown",
|
|
3532
|
+
status: result.status ?? "done",
|
|
3533
|
+
nestedCount: resultRun?.nestedCount,
|
|
3534
|
+
backend: resultRun?.lastStatus?.backend
|
|
3535
|
+
});
|
|
3536
|
+
if (resultRun) {
|
|
3537
|
+
trackedRuns.delete(trackedRunKey(resultRun.supervisorId, resultRun.runId));
|
|
3538
|
+
updateWidget(lastStatusEntries);
|
|
3539
|
+
if (tracked && resultRun !== tracked) {
|
|
3540
|
+
const trackedKey = trackedRunKey(tracked.supervisorId, tracked.runId);
|
|
3541
|
+
if (trackedRuns.get(trackedKey) === tracked) tracked.blocking = previousBlocking ?? false;
|
|
3542
|
+
}
|
|
3543
|
+
} else if (tracked && trackedRuns.get(trackedRunKey(tracked.supervisorId, tracked.runId)) === tracked) tracked.blocking = previousBlocking ?? false;
|
|
3544
|
+
} else if (resultRun && result.status === "waiting") resultRun.permissionNotified = true;
|
|
3228
3545
|
return {
|
|
3229
3546
|
content: [{
|
|
3230
3547
|
type: "text",
|
|
@@ -3234,7 +3551,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3234
3551
|
};
|
|
3235
3552
|
} finally {
|
|
3236
3553
|
blockingWait?.dispose();
|
|
3237
|
-
if (!completed && tracked &&
|
|
3554
|
+
if (!completed && tracked && trackedRuns.get(trackedRunKey(tracked.supervisorId, tracked.runId)) === tracked) tracked.blocking = previousBlocking ?? false;
|
|
3238
3555
|
}
|
|
3239
3556
|
},
|
|
3240
3557
|
renderCall(args, theme, context) {
|
|
@@ -3824,4 +4141,4 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3824
4141
|
}
|
|
3825
4142
|
var src_default = createExtension();
|
|
3826
4143
|
//#endregion
|
|
3827
|
-
export { CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, createPollCompletedPayload, createPollErrorPayload, createRunTerminalPayload, src_default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, spawnIdentityMetadata, statusSupervisorId };
|
|
4144
|
+
export { CHANNEL_DELIVERY_ERROR, CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, buildCompletionText, buildInspectPayload, compactWhitespace, createDeliveryErrorPayload, createExtension, createPollCompletedPayload, createPollErrorPayload, createRunTerminalPayload, src_default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, spawnIdentityMetadata, statusSupervisorId };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dougbots/avenor-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -35,15 +35,15 @@
|
|
|
35
35
|
"dev": "tsdown --watch"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@dougbots/avenor-core": "^0.
|
|
38
|
+
"@dougbots/avenor-core": "^0.17.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-coding-agent": ">=
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=1.4.0",
|
|
42
42
|
"@earendil-works/pi-tui": "*"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@earendil-works/pi-coding-agent": "
|
|
46
|
-
"@earendil-works/pi-tui": "
|
|
45
|
+
"@earendil-works/pi-coding-agent": "^1.4.0",
|
|
46
|
+
"@earendil-works/pi-tui": "^1.4.0",
|
|
47
47
|
"@types/bun": "^1.3.12",
|
|
48
48
|
"@types/node": "^25.8.0",
|
|
49
49
|
"tsdown": "^0.22.0",
|