@dougbots/avenor-pi 0.1.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 +159 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +618 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# @dougbots/avenor-pi
|
|
2
|
+
|
|
3
|
+
Pi extension that registers avenor agent runs as tools in your Pi session. When you need a sub-agent to handle a well-defined task — write code, run tests, search a codebase — avenor operates in the background while you keep your session focused.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
You need the `avenor` binary available. It is installed to `~/.botfiles/bin/avenor` by default.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Check it's on PATH
|
|
11
|
+
which avenor
|
|
12
|
+
|
|
13
|
+
# Or set explicitly
|
|
14
|
+
export AVENOR_BIN=~/.botfiles/bin/avenor
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### From npm (once published)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pi install npm:@dougbots/avenor-pi
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This writes to your global settings (`~/.pi/agent/settings.json`). Use `-l` for project-local installation (`.pi/settings.json`).
|
|
26
|
+
|
|
27
|
+
### From a git repo
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pi install git:github.com/sdougbrown/avenor@v0.1.0
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Pi will clone the repo and load the extension from `packages/pi`.
|
|
34
|
+
|
|
35
|
+
### Local development
|
|
36
|
+
|
|
37
|
+
**Quick test** — run pi with the extension directly:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd packages/pi
|
|
41
|
+
pi -e ./src/index.ts
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Symlink for auto-discovery** — link into your global extensions directory:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
mkdir -p ~/.pi/agent/extensions/avenor
|
|
48
|
+
ln -sf $(pwd)/packages/pi/src/index.ts ~/.pi/agent/extensions/avenor/index.ts
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Pi auto-discovers `~/.pi/agent/extensions/*/index.ts`. Changes are hot-reloadable with `/reload`.
|
|
52
|
+
|
|
53
|
+
**Local path in settings** — add the built dist directly:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Build first
|
|
57
|
+
cd packages/pi && bun run build
|
|
58
|
+
|
|
59
|
+
# Add to settings
|
|
60
|
+
pi install /path/to/avenor/packages/pi
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Manual settings.json
|
|
64
|
+
|
|
65
|
+
If you prefer to edit settings directly:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
// ~/.pi/agent/settings.json
|
|
69
|
+
{
|
|
70
|
+
"packages": ["npm:@dougbots/avenor-pi"]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Or for a local path:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"packages": ["/path/to/avenor/packages/pi"]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Package Structure
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
packages/pi/
|
|
86
|
+
├── package.json
|
|
87
|
+
├── tsdown.config.ts
|
|
88
|
+
├── src/
|
|
89
|
+
│ ├── index.ts # main extension (tools, commands, hooks, rendering)
|
|
90
|
+
│ ├── types.ts # shared types, status emoji mapping
|
|
91
|
+
│ └── watch.ts # EventFeedOverlay TUI component
|
|
92
|
+
└── dist/
|
|
93
|
+
└── index.js # built output (loaded by pi)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The `pi` key in `package.json` declares the extension entry point:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"pi": {
|
|
101
|
+
"extensions": ["./dist/index.js"]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Features
|
|
107
|
+
|
|
108
|
+
### Tools
|
|
109
|
+
|
|
110
|
+
Six tools for the LLM to dispatch and manage sub-agents:
|
|
111
|
+
|
|
112
|
+
| Tool | Description |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `avenor_spawn` | Dispatch an agent run (blocking or fire-and-forget) |
|
|
115
|
+
| `avenor_status` | Get status of a run or all runs |
|
|
116
|
+
| `avenor_answer_permission` | Answer a pending permission request |
|
|
117
|
+
| `avenor_follow_up` | Resume a completed run with a follow-up message |
|
|
118
|
+
| `avenor_events` | Read events from a run |
|
|
119
|
+
| `avenor_shutdown` | Shut down the avenor supervisor |
|
|
120
|
+
|
|
121
|
+
### Pi-specific features
|
|
122
|
+
|
|
123
|
+
Beyond the tools, the extension integrates with Pi's TUI and event system:
|
|
124
|
+
|
|
125
|
+
- **Status widget** — persistent widget showing all active runs with status, phase, and permission state
|
|
126
|
+
- **Footer status** — active runs shown in the Pi footer bar
|
|
127
|
+
- **Live progress** — blocking `avenor_spawn` calls stream progress updates via `onUpdate`
|
|
128
|
+
- **Context enrichment** — active sub-agents are automatically surfaced in the system prompt via `before_agent_start`
|
|
129
|
+
- **Custom rendering** — tool calls and results are rendered with status emojis and color-coded output
|
|
130
|
+
- **Commands** — interactive commands for run management:
|
|
131
|
+
- `/avenor-status` — show status of all runs
|
|
132
|
+
- `/avenor-watch <run_id>` — open a live event feed overlay
|
|
133
|
+
- `/avenor-cancel <run_id>` — cancel a running sub-agent
|
|
134
|
+
|
|
135
|
+
## Typical workflows
|
|
136
|
+
|
|
137
|
+
**Blocking (default):**
|
|
138
|
+
```
|
|
139
|
+
1. avenor_spawn → tool call shows live progress, blocks until done
|
|
140
|
+
2. tool call returns → structured result with status + session_id
|
|
141
|
+
3. avenor_events → inspect detailed output
|
|
142
|
+
4. avenor_follow_up → optionally iterate
|
|
143
|
+
5. avenor_shutdown → clean up when finished
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Parallel / fire-and-forget (`wait=false`):**
|
|
147
|
+
```
|
|
148
|
+
1. avenor_spawn × N → each returns run_id immediately
|
|
149
|
+
2. (status widget updates) → persistent widget shows all active runs
|
|
150
|
+
3. /avenor-watch <id> → open live event feed for a specific run
|
|
151
|
+
4. avenor_events → inspect output when done
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Dependencies
|
|
155
|
+
|
|
156
|
+
- **Peer:** `@earendil-works/pi-coding-agent` (Pi runtime, provided by pi)
|
|
157
|
+
- **Peer:** `@earendil-works/pi-tui` (TUI components, provided by pi)
|
|
158
|
+
- **Dependency:** `@dougbots/avenor-core` (supervisor, client, tool primitives)
|
|
159
|
+
- **Binary:** `avenor` must be available on `PATH` or at `AVENOR_BIN`
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { answerPermissionTool, eventsTool, followUpTool, shutdownTool, spawnTool, statusTool } from "@dougbots/avenor-core";
|
|
4
|
+
//#region src/watch.ts
|
|
5
|
+
var EventFeedOverlay = class {
|
|
6
|
+
_events = [];
|
|
7
|
+
_runId;
|
|
8
|
+
_onClose;
|
|
9
|
+
_maxLines = 50;
|
|
10
|
+
constructor(runId, onClose) {
|
|
11
|
+
this._runId = runId;
|
|
12
|
+
this._onClose = onClose ?? (() => {});
|
|
13
|
+
}
|
|
14
|
+
setOnClose(fn) {
|
|
15
|
+
this._onClose = fn;
|
|
16
|
+
}
|
|
17
|
+
setEvents(events) {
|
|
18
|
+
this._events = events.slice(-this._maxLines);
|
|
19
|
+
}
|
|
20
|
+
appendEvent(event) {
|
|
21
|
+
this._events.push(event);
|
|
22
|
+
if (this._events.length > this._maxLines) this._events = this._events.slice(-this._maxLines);
|
|
23
|
+
}
|
|
24
|
+
render(width) {
|
|
25
|
+
const lines = [];
|
|
26
|
+
const header = `📡 avenor watch: ${this._runId.slice(0, 8)}… (Esc to close)`;
|
|
27
|
+
lines.push(header);
|
|
28
|
+
lines.push("─".repeat(Math.min(width, header.length)));
|
|
29
|
+
if (this._events.length === 0) lines.push(" (waiting for events…)");
|
|
30
|
+
else for (const evt of this._events) {
|
|
31
|
+
const ts = new Date(evt.ts).toISOString().slice(11, 19);
|
|
32
|
+
const type = evt.type ?? "unknown";
|
|
33
|
+
const extra = this.summarizeEvent(evt);
|
|
34
|
+
lines.push(` ${ts} ${type}${extra ? ` — ${extra}` : ""}`);
|
|
35
|
+
}
|
|
36
|
+
return lines;
|
|
37
|
+
}
|
|
38
|
+
handleInput(data) {
|
|
39
|
+
if (data === "\x1B") this._onClose();
|
|
40
|
+
}
|
|
41
|
+
summarizeEvent(evt) {
|
|
42
|
+
if (evt.type === "tool.call" || evt.type === "tool.use") return `called ${String(evt.tool ?? evt.name ?? "")}`;
|
|
43
|
+
if (evt.type === "agent.message_chunk") return "thinking…";
|
|
44
|
+
if (evt.type?.startsWith("permission.")) return "permission requested";
|
|
45
|
+
if (evt.type === "tool.result") {
|
|
46
|
+
const result = evt.result;
|
|
47
|
+
if (typeof result === "string" && result.length < 80) return result;
|
|
48
|
+
return "completed";
|
|
49
|
+
}
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
invalidate() {}
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/types.ts
|
|
56
|
+
const TERMINAL_STATUSES = new Set([
|
|
57
|
+
"done",
|
|
58
|
+
"failed",
|
|
59
|
+
"timeout",
|
|
60
|
+
"killed"
|
|
61
|
+
]);
|
|
62
|
+
const STATUS_EMOJI = {
|
|
63
|
+
running: "🟢",
|
|
64
|
+
idle: "🟡",
|
|
65
|
+
done: "✅",
|
|
66
|
+
failed: "❌",
|
|
67
|
+
timeout: "⏱️",
|
|
68
|
+
killed: "💀",
|
|
69
|
+
permission: "🔒"
|
|
70
|
+
};
|
|
71
|
+
function statusEmoji(status) {
|
|
72
|
+
return STATUS_EMOJI[status] ?? "⚪";
|
|
73
|
+
}
|
|
74
|
+
function formatRunLine(entry) {
|
|
75
|
+
const emoji = statusEmoji(entry.status);
|
|
76
|
+
const perm = entry.pendingPermission ? ` — blocked: ${entry.permissionDescription ?? "permission"}` : "";
|
|
77
|
+
const phase = entry.phaseLabel ? ` (${entry.phaseLabel})` : "";
|
|
78
|
+
return `${emoji} ${entry.label} (${entry.agent}) — ${entry.status}${phase}${perm}`;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/index.ts
|
|
82
|
+
const POLL_INTERVAL_MS = 3e3;
|
|
83
|
+
function sleep(ms) {
|
|
84
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
85
|
+
}
|
|
86
|
+
function buildCompletionText(run, result) {
|
|
87
|
+
const lines = [`Sub-agent "${run.label}" finished with status **${result.status}**.`];
|
|
88
|
+
if (result.stop_reason && result.stop_reason.toUpperCase() !== result.status.toUpperCase()) lines.push(`Stop reason: ${result.stop_reason}`);
|
|
89
|
+
if (result.session_id) lines.push(`Session: \`${result.session_id}\``);
|
|
90
|
+
lines.push(`\nCall \`avenor_events\` with \`run_id: "${run.runId}"\` to review the output, or \`avenor_follow_up\` to iterate.`);
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
function summarizeEvent(evt) {
|
|
94
|
+
const type = String(evt.type ?? evt.event ?? "");
|
|
95
|
+
if (type === "tool.call" || type === "tool.use") return `called ${String(evt.tool ?? evt.name ?? type)}`;
|
|
96
|
+
if (type === "agent.message_chunk") return "thinking…";
|
|
97
|
+
if (type.startsWith("permission.")) return `permission requested`;
|
|
98
|
+
return type;
|
|
99
|
+
}
|
|
100
|
+
async function src_default(pi) {
|
|
101
|
+
const trackedRuns = /* @__PURE__ */ new Map();
|
|
102
|
+
let pollingActive = false;
|
|
103
|
+
let sessionCtx = null;
|
|
104
|
+
async function pollRuns() {
|
|
105
|
+
let raw;
|
|
106
|
+
try {
|
|
107
|
+
raw = await statusTool({});
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error("avenor pollRuns: statusTool failed", err);
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const results = Array.isArray(raw) ? raw : [raw];
|
|
113
|
+
const entries = [];
|
|
114
|
+
for (const r of results) {
|
|
115
|
+
const run = trackedRuns.get(r.run_id);
|
|
116
|
+
if (run) run.lastStatus = r;
|
|
117
|
+
entries.push({
|
|
118
|
+
runId: r.run_id,
|
|
119
|
+
label: r.label,
|
|
120
|
+
status: r.status,
|
|
121
|
+
phase: r.phase,
|
|
122
|
+
phaseLabel: r.phase_label,
|
|
123
|
+
agent: trackedRuns.get(r.run_id)?.agent ?? "unknown",
|
|
124
|
+
pendingPermission: !!r.pending_permission,
|
|
125
|
+
permissionDescription: r.pending_permission?.description
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return entries;
|
|
129
|
+
}
|
|
130
|
+
function renderStatusLines(entries) {
|
|
131
|
+
const active = entries.filter((e) => !TERMINAL_STATUSES.has(e.status));
|
|
132
|
+
if (active.length === 0) return [];
|
|
133
|
+
return active.map((e) => {
|
|
134
|
+
const emoji = statusEmoji(e.status);
|
|
135
|
+
const perm = e.pendingPermission ? ` 🔒` : "";
|
|
136
|
+
const phase = e.phaseLabel ? ` [${e.phaseLabel}]` : "";
|
|
137
|
+
return `${emoji} ${e.label} — ${e.status}${phase}${perm}`;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function updateWidget(entries) {
|
|
141
|
+
if (!sessionCtx) return;
|
|
142
|
+
const lines = renderStatusLines(entries);
|
|
143
|
+
if (lines.length > 0) {
|
|
144
|
+
sessionCtx.ui.setWidget("avenor-status", lines);
|
|
145
|
+
const parts = lines.map((l) => l.slice(0, 60));
|
|
146
|
+
sessionCtx.ui.setStatus("avenor", parts.join(" "));
|
|
147
|
+
} else {
|
|
148
|
+
sessionCtx.ui.setWidget("avenor-status", void 0);
|
|
149
|
+
sessionCtx.ui.setStatus("avenor", "");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function startPolling() {
|
|
153
|
+
if (pollingActive) return;
|
|
154
|
+
pollingActive = true;
|
|
155
|
+
const tick = async () => {
|
|
156
|
+
const prevStatuses = /* @__PURE__ */ new Map();
|
|
157
|
+
for (const [id, run] of trackedRuns) prevStatuses.set(id, run.lastStatus?.status);
|
|
158
|
+
const entries = await pollRuns();
|
|
159
|
+
updateWidget(entries);
|
|
160
|
+
for (const entry of entries) if (TERMINAL_STATUSES.has(entry.status)) {
|
|
161
|
+
if (trackedRuns.get(entry.runId)) {
|
|
162
|
+
const prevStatus = prevStatuses.get(entry.runId);
|
|
163
|
+
if (prevStatus && !TERMINAL_STATUSES.has(prevStatus)) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
|
|
164
|
+
trackedRuns.delete(entry.runId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (entries.some((e) => !TERMINAL_STATUSES.has(e.status))) setTimeout(tick, POLL_INTERVAL_MS);
|
|
168
|
+
else {
|
|
169
|
+
pollingActive = false;
|
|
170
|
+
sessionCtx?.ui.setStatus("avenor", "");
|
|
171
|
+
sessionCtx?.ui.setWidget("avenor-status", void 0);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
tick();
|
|
175
|
+
}
|
|
176
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
177
|
+
sessionCtx = ctx;
|
|
178
|
+
});
|
|
179
|
+
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
180
|
+
pollingActive = false;
|
|
181
|
+
sessionCtx = null;
|
|
182
|
+
});
|
|
183
|
+
pi.on("before_agent_start", async (event, _ctx) => {
|
|
184
|
+
if (trackedRuns.size === 0) return;
|
|
185
|
+
const active = (await pollRuns()).filter((e) => !TERMINAL_STATUSES.has(e.status));
|
|
186
|
+
if (active.length === 0) return;
|
|
187
|
+
return { message: {
|
|
188
|
+
customType: "avenor-active-runs",
|
|
189
|
+
content: `Active sub-agents:\n${active.map((e) => {
|
|
190
|
+
const perm = e.pendingPermission ? ` [blocked: ${e.permissionDescription ?? "permission"}]` : "";
|
|
191
|
+
const phase = e.phaseLabel ? ` (${e.phaseLabel})` : "";
|
|
192
|
+
return ` • ${e.label} (${e.agent}): ${e.status}${phase}${perm}`;
|
|
193
|
+
}).join("\n")}`,
|
|
194
|
+
display: true
|
|
195
|
+
} };
|
|
196
|
+
});
|
|
197
|
+
pi.registerMessageRenderer("avenor-active-runs", (message, options, theme) => {
|
|
198
|
+
const lines = message.content.split("\n");
|
|
199
|
+
let text = "";
|
|
200
|
+
for (let i = 0; i < lines.length; i++) {
|
|
201
|
+
if (i === 0) text += theme.fg("info", theme.bold(lines[i]));
|
|
202
|
+
else text += theme.fg("muted", lines[i]);
|
|
203
|
+
if (i < lines.length - 1) text += "\n";
|
|
204
|
+
}
|
|
205
|
+
return new Text(text, 0, 0);
|
|
206
|
+
});
|
|
207
|
+
pi.registerTool({
|
|
208
|
+
name: "avenor_spawn",
|
|
209
|
+
label: "Avenor Spawn",
|
|
210
|
+
description: "Dispatch an agent run via avenor. Blocks by default, showing live progress. Set wait=false for fire-and-forget.",
|
|
211
|
+
promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, events, follow-up, shutdown)",
|
|
212
|
+
promptGuidelines: [
|
|
213
|
+
"Use avenor_spawn to delegate well-defined tasks to sub-agents.",
|
|
214
|
+
"Use avenor_status to check progress of fire-and-forget runs.",
|
|
215
|
+
"Use avenor_events to inspect detailed output from a run.",
|
|
216
|
+
"Use avenor_follow_up to iterate on a completed run.",
|
|
217
|
+
"Use avenor_shutdown when done delegating work."
|
|
218
|
+
],
|
|
219
|
+
parameters: Type.Object({
|
|
220
|
+
agent: Type.String({ description: "Agent name (required, no default)" }),
|
|
221
|
+
prompt: Type.Optional(Type.String({ description: "Prompt text" })),
|
|
222
|
+
prompt_file: Type.Optional(Type.String({ description: "Path to prompt file" })),
|
|
223
|
+
dir: Type.Optional(Type.String({ description: "Working directory (defaults to session directory)" })),
|
|
224
|
+
label: Type.Optional(Type.String({ description: "Human-readable label for the run" })),
|
|
225
|
+
timeout: Type.Optional(Type.String({ description: "Timeout duration (e.g. 3600s)" })),
|
|
226
|
+
model: Type.Optional(Type.String({ description: "Model override" })),
|
|
227
|
+
backend: Type.Optional(Type.String({ description: "Backend override" })),
|
|
228
|
+
server_url: Type.Optional(Type.String({ description: "Backend server URL" })),
|
|
229
|
+
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
|
|
230
|
+
wait: Type.Optional(Type.Boolean({ description: "Block until complete (default true)" }))
|
|
231
|
+
}),
|
|
232
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
233
|
+
if (signal?.aborted) return { content: [{
|
|
234
|
+
type: "text",
|
|
235
|
+
text: "Cancelled"
|
|
236
|
+
}] };
|
|
237
|
+
const wait = params.wait ?? true;
|
|
238
|
+
const dir = params.dir ?? ctx.cwd;
|
|
239
|
+
const label = params.label ?? `${params.agent}-${Date.now()}`;
|
|
240
|
+
const result = await spawnTool({
|
|
241
|
+
agent: params.agent,
|
|
242
|
+
prompt: params.prompt,
|
|
243
|
+
promptFile: params.prompt_file,
|
|
244
|
+
label,
|
|
245
|
+
dir,
|
|
246
|
+
timeout: params.timeout,
|
|
247
|
+
model: params.model,
|
|
248
|
+
backend: params.backend,
|
|
249
|
+
serverUrl: params.server_url,
|
|
250
|
+
supervisorId: params.supervisor_id
|
|
251
|
+
});
|
|
252
|
+
trackedRuns.set(result.run_id, {
|
|
253
|
+
runId: result.run_id,
|
|
254
|
+
agent: params.agent,
|
|
255
|
+
label,
|
|
256
|
+
supervisorId: result.supervisor_id,
|
|
257
|
+
startTime: Date.now()
|
|
258
|
+
});
|
|
259
|
+
startPolling();
|
|
260
|
+
if (!wait) return {
|
|
261
|
+
content: [{
|
|
262
|
+
type: "text",
|
|
263
|
+
text: `Dispatched "${label}" (run_id: ${result.run_id}). Call avenor_status to check progress.`
|
|
264
|
+
}],
|
|
265
|
+
details: {
|
|
266
|
+
run_id: result.run_id,
|
|
267
|
+
label
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
onUpdate?.({
|
|
271
|
+
content: [{
|
|
272
|
+
type: "text",
|
|
273
|
+
text: `Starting ${label}…`
|
|
274
|
+
}],
|
|
275
|
+
details: {
|
|
276
|
+
status: "running",
|
|
277
|
+
run_id: result.run_id
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
let firstPoll = true;
|
|
281
|
+
while (!signal?.aborted) {
|
|
282
|
+
if (!firstPoll) await sleep(POLL_INTERVAL_MS);
|
|
283
|
+
firstPoll = false;
|
|
284
|
+
let raw;
|
|
285
|
+
try {
|
|
286
|
+
raw = await statusTool({
|
|
287
|
+
runId: result.run_id,
|
|
288
|
+
supervisorId: result.supervisor_id
|
|
289
|
+
});
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error("avenor spawn poll: statusTool failed", err);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const status = Array.isArray(raw) ? raw[0] : raw;
|
|
295
|
+
if (!status) continue;
|
|
296
|
+
if (status.pending_permission) onUpdate?.({
|
|
297
|
+
content: [{
|
|
298
|
+
type: "text",
|
|
299
|
+
text: `[blocked] ${label} — permission: ${status.pending_permission.description}`
|
|
300
|
+
}],
|
|
301
|
+
details: {
|
|
302
|
+
status: "permission",
|
|
303
|
+
run_id: result.run_id
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
else {
|
|
307
|
+
let lastAction = "";
|
|
308
|
+
if (!result.supervisor_id) try {
|
|
309
|
+
const eventsResult = await eventsTool({
|
|
310
|
+
runId: result.run_id,
|
|
311
|
+
limit: 3
|
|
312
|
+
});
|
|
313
|
+
const last = eventsResult.events[eventsResult.events.length - 1];
|
|
314
|
+
if (last) lastAction = summarizeEvent(last);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error("avenor spawn poll: eventsTool failed", err);
|
|
317
|
+
}
|
|
318
|
+
onUpdate?.({
|
|
319
|
+
content: [{
|
|
320
|
+
type: "text",
|
|
321
|
+
text: `${label}${status.phase_label ? ` (${status.phase_label})` : ""}${lastAction ? ` — ${lastAction}` : ""}`
|
|
322
|
+
}],
|
|
323
|
+
details: {
|
|
324
|
+
status: status.status,
|
|
325
|
+
run_id: result.run_id,
|
|
326
|
+
lastAction
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if (TERMINAL_STATUSES.has(status.status)) {
|
|
331
|
+
trackedRuns.delete(result.run_id);
|
|
332
|
+
return {
|
|
333
|
+
content: [{
|
|
334
|
+
type: "text",
|
|
335
|
+
text: buildCompletionText({
|
|
336
|
+
runId: result.run_id,
|
|
337
|
+
label
|
|
338
|
+
}, status)
|
|
339
|
+
}],
|
|
340
|
+
details: {
|
|
341
|
+
status: status.status,
|
|
342
|
+
run_id: result.run_id,
|
|
343
|
+
session_id: status.session_id
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
trackedRuns.delete(result.run_id);
|
|
349
|
+
return {
|
|
350
|
+
content: [{
|
|
351
|
+
type: "text",
|
|
352
|
+
text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status to check.`
|
|
353
|
+
}],
|
|
354
|
+
details: { run_id: result.run_id }
|
|
355
|
+
};
|
|
356
|
+
},
|
|
357
|
+
renderCall(args, theme) {
|
|
358
|
+
const agent = theme.fg("toolTitle", theme.bold("avenor_spawn "));
|
|
359
|
+
const agentName = theme.fg("accent", args.agent);
|
|
360
|
+
const label = args.label ? theme.fg("dim", ` "${args.label}"`) : "";
|
|
361
|
+
const wait = args.wait === false ? theme.fg("warning", " [async]") : "";
|
|
362
|
+
return new Text(agent + agentName + label + wait, 0, 0);
|
|
363
|
+
},
|
|
364
|
+
renderResult(result, { isPartial }, theme) {
|
|
365
|
+
if (isPartial) {
|
|
366
|
+
const emoji = statusEmoji(result.details?.status ?? "running");
|
|
367
|
+
return new Text(theme.fg("info", `${emoji} ${result.content?.[0]?.text ?? "Running…"}`), 0, 0);
|
|
368
|
+
}
|
|
369
|
+
const status = result.details?.status;
|
|
370
|
+
if (status && TERMINAL_STATUSES.has(status)) {
|
|
371
|
+
const emoji = statusEmoji(status);
|
|
372
|
+
const header = theme.fg(status === "done" ? "success" : "error", `${emoji} ${status}`);
|
|
373
|
+
const lines = (result.content?.[0]?.text ?? "").split("\n");
|
|
374
|
+
return new Text(header + "\n" + lines.map((l) => theme.fg("muted", ` ${l}`)).join("\n"), 0, 0);
|
|
375
|
+
}
|
|
376
|
+
return new Text((result.content?.[0]?.text ?? "").split("\n").map((l) => theme.fg("muted", l)).join("\n"), 0, 0);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
pi.registerTool({
|
|
380
|
+
name: "avenor_status",
|
|
381
|
+
label: "Avenor Status",
|
|
382
|
+
description: "Get status of a run or all runs. Surfaces pending permission requests.",
|
|
383
|
+
parameters: Type.Object({
|
|
384
|
+
run_id: Type.Optional(Type.String({ description: "Specific run ID to query" })),
|
|
385
|
+
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
|
|
386
|
+
}),
|
|
387
|
+
async execute(_toolCallId, params) {
|
|
388
|
+
const result = await statusTool({
|
|
389
|
+
runId: params.run_id,
|
|
390
|
+
supervisorId: params.supervisor_id
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
content: [{
|
|
394
|
+
type: "text",
|
|
395
|
+
text: JSON.stringify(result, null, 2)
|
|
396
|
+
}],
|
|
397
|
+
details: result
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
pi.registerTool({
|
|
402
|
+
name: "avenor_answer_permission",
|
|
403
|
+
label: "Avenor Answer Permission",
|
|
404
|
+
description: "Answer a pending permission request for a sub-agent. Pass option_id from avenor_status pending_permission.options.",
|
|
405
|
+
parameters: Type.Object({
|
|
406
|
+
run_id: Type.String({ description: "Run ID with the pending permission" }),
|
|
407
|
+
option_id: Type.String({ description: "allow_once | allow_always | deny" }),
|
|
408
|
+
request_id: Type.Optional(Type.String({ description: "Permission request ID (auto-discovered if omitted)" })),
|
|
409
|
+
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
|
|
410
|
+
}),
|
|
411
|
+
async execute(_toolCallId, params) {
|
|
412
|
+
const result = await answerPermissionTool({
|
|
413
|
+
runId: params.run_id,
|
|
414
|
+
optionId: params.option_id,
|
|
415
|
+
requestId: params.request_id,
|
|
416
|
+
supervisorId: params.supervisor_id
|
|
417
|
+
});
|
|
418
|
+
return {
|
|
419
|
+
content: [{
|
|
420
|
+
type: "text",
|
|
421
|
+
text: JSON.stringify(result, null, 2)
|
|
422
|
+
}],
|
|
423
|
+
details: result
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
pi.registerTool({
|
|
428
|
+
name: "avenor_follow_up",
|
|
429
|
+
label: "Avenor Follow Up",
|
|
430
|
+
description: "Resume a completed run with a follow-up message.",
|
|
431
|
+
parameters: Type.Object({
|
|
432
|
+
run_id: Type.String({ description: "Completed run ID to resume" }),
|
|
433
|
+
message: Type.String({ description: "Follow-up message text" }),
|
|
434
|
+
label: Type.Optional(Type.String({ description: "Override label (defaults to <original>-followup)" })),
|
|
435
|
+
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
|
|
436
|
+
}),
|
|
437
|
+
async execute(_toolCallId, params) {
|
|
438
|
+
const result = await followUpTool({
|
|
439
|
+
runId: params.run_id,
|
|
440
|
+
message: params.message,
|
|
441
|
+
label: params.label,
|
|
442
|
+
supervisorId: params.supervisor_id
|
|
443
|
+
});
|
|
444
|
+
return {
|
|
445
|
+
content: [{
|
|
446
|
+
type: "text",
|
|
447
|
+
text: JSON.stringify(result, null, 2)
|
|
448
|
+
}],
|
|
449
|
+
details: result
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
pi.registerTool({
|
|
454
|
+
name: "avenor_events",
|
|
455
|
+
label: "Avenor Events",
|
|
456
|
+
description: "Read events from a run. Filter by type. Returns last N events.",
|
|
457
|
+
parameters: Type.Object({
|
|
458
|
+
run_id: Type.String({ description: "Run ID to read events from" }),
|
|
459
|
+
types: Type.Optional(Type.Array(Type.String(), { description: "Filter by event types" })),
|
|
460
|
+
limit: Type.Optional(Type.Number({ description: "Max events to return (default 50)" })),
|
|
461
|
+
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
|
|
462
|
+
}),
|
|
463
|
+
async execute(_toolCallId, params) {
|
|
464
|
+
const result = await eventsTool({
|
|
465
|
+
runId: params.run_id,
|
|
466
|
+
types: params.types,
|
|
467
|
+
limit: params.limit,
|
|
468
|
+
supervisorId: params.supervisor_id
|
|
469
|
+
});
|
|
470
|
+
return {
|
|
471
|
+
content: [{
|
|
472
|
+
type: "text",
|
|
473
|
+
text: JSON.stringify(result, null, 2)
|
|
474
|
+
}],
|
|
475
|
+
details: result
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
pi.registerTool({
|
|
480
|
+
name: "avenor_shutdown",
|
|
481
|
+
label: "Avenor Shutdown",
|
|
482
|
+
description: "Shut down the avenor supervisor and clean up temp files.",
|
|
483
|
+
parameters: Type.Object({
|
|
484
|
+
supervisor_id: Type.Optional(Type.String({ description: "Supervisor to shut down (defaults to singleton)" })),
|
|
485
|
+
force: Type.Optional(Type.Boolean({ description: "Force shutdown instead of graceful" }))
|
|
486
|
+
}),
|
|
487
|
+
async execute(_toolCallId, params) {
|
|
488
|
+
const result = await shutdownTool({
|
|
489
|
+
supervisorId: params.supervisor_id,
|
|
490
|
+
force: params.force
|
|
491
|
+
});
|
|
492
|
+
return {
|
|
493
|
+
content: [{
|
|
494
|
+
type: "text",
|
|
495
|
+
text: JSON.stringify(result, null, 2)
|
|
496
|
+
}],
|
|
497
|
+
details: result
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
pi.registerCommand("avenor-status", {
|
|
502
|
+
description: "Show status of all avenor runs",
|
|
503
|
+
handler: async (_args, ctx) => {
|
|
504
|
+
const entries = await pollRuns();
|
|
505
|
+
if (entries.length === 0) {
|
|
506
|
+
ctx.ui.notify("No active avenor runs", "info");
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const lines = entries.map((e) => formatRunLine(e));
|
|
510
|
+
ctx.ui.setWidget("avenor-status-detail", lines);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
pi.registerCommand("avenor-watch", {
|
|
514
|
+
description: "Open a live event feed for a run",
|
|
515
|
+
getArgumentCompletions: (prefix) => {
|
|
516
|
+
const items = Array.from(trackedRuns.values()).filter((r) => r.label.startsWith(prefix) || r.runId.startsWith(prefix)).map((r) => ({
|
|
517
|
+
value: r.runId,
|
|
518
|
+
label: `${r.label} (${r.runId.slice(0, 8)})`
|
|
519
|
+
}));
|
|
520
|
+
return items.length > 0 ? items : null;
|
|
521
|
+
},
|
|
522
|
+
handler: async (args, ctx) => {
|
|
523
|
+
if (!ctx.hasUI) {
|
|
524
|
+
ctx.ui.notify("avenor-watch requires TUI mode", "warning");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
let runId = args?.trim();
|
|
528
|
+
if (!runId) {
|
|
529
|
+
const runList = Array.from(trackedRuns.values());
|
|
530
|
+
if (runList.length === 0) {
|
|
531
|
+
ctx.ui.notify("No tracked runs", "warning");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const choices = runList.map((r) => r.runId);
|
|
535
|
+
const labels = runList.map((r) => `${r.label} (${r.runId.slice(0, 8)})`);
|
|
536
|
+
const idx = await ctx.ui.select("Watch run:", labels);
|
|
537
|
+
if (idx === null) return;
|
|
538
|
+
runId = choices[idx];
|
|
539
|
+
}
|
|
540
|
+
let watchActive = true;
|
|
541
|
+
const overlay = new EventFeedOverlay(runId);
|
|
542
|
+
try {
|
|
543
|
+
const { events } = await eventsTool({
|
|
544
|
+
runId,
|
|
545
|
+
limit: 50
|
|
546
|
+
});
|
|
547
|
+
overlay.setEvents(events);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
console.error("avenor watch: failed to load initial events", err);
|
|
550
|
+
overlay.setEvents([]);
|
|
551
|
+
}
|
|
552
|
+
const handle = ctx.ui.custom((_tui, _theme, _kb, done) => {
|
|
553
|
+
overlay.setOnClose(() => {
|
|
554
|
+
watchActive = false;
|
|
555
|
+
done();
|
|
556
|
+
});
|
|
557
|
+
return overlay;
|
|
558
|
+
}, {
|
|
559
|
+
overlay: true,
|
|
560
|
+
overlayOptions: {
|
|
561
|
+
anchor: "right-center",
|
|
562
|
+
width: "45%",
|
|
563
|
+
minWidth: 60,
|
|
564
|
+
maxHeight: "70%"
|
|
565
|
+
},
|
|
566
|
+
onHandle: (h) => {
|
|
567
|
+
handle._inner = h;
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
const watchTick = async () => {
|
|
571
|
+
while (watchActive) {
|
|
572
|
+
await sleep(POLL_INTERVAL_MS);
|
|
573
|
+
try {
|
|
574
|
+
const { events } = await eventsTool({
|
|
575
|
+
runId,
|
|
576
|
+
limit: 50
|
|
577
|
+
});
|
|
578
|
+
overlay.setEvents(events);
|
|
579
|
+
handle.requestRender?.();
|
|
580
|
+
} catch (err) {
|
|
581
|
+
console.error("avenor watch poll: eventsTool failed", err);
|
|
582
|
+
watchActive = false;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
watchTick();
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
pi.registerCommand("avenor-cancel", {
|
|
590
|
+
description: "Cancel a running avenor sub-agent",
|
|
591
|
+
getArgumentCompletions: (prefix) => {
|
|
592
|
+
const items = Array.from(trackedRuns.values()).filter((r) => r.label.startsWith(prefix) || r.runId.startsWith(prefix)).map((r) => ({
|
|
593
|
+
value: r.runId,
|
|
594
|
+
label: `${r.label} (${r.runId.slice(0, 8)})`
|
|
595
|
+
}));
|
|
596
|
+
return items.length > 0 ? items : null;
|
|
597
|
+
},
|
|
598
|
+
handler: async (args, ctx) => {
|
|
599
|
+
if (!args?.trim()) {
|
|
600
|
+
ctx.ui.notify("Usage: /avenor-cancel <run_id>", "warning");
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const runId = args.trim();
|
|
604
|
+
const run = trackedRuns.get(runId);
|
|
605
|
+
const label = run?.label ?? runId;
|
|
606
|
+
if (!await ctx.ui.confirm("Cancel run", `Cancel "${label}" (${runId.slice(0, 8)})?`)) return;
|
|
607
|
+
try {
|
|
608
|
+
const { Supervisor } = await import("@dougbots/avenor-core");
|
|
609
|
+
await (await Supervisor.get()).getClient().cancel(run?.lastStatus?.runtime_id ?? runId);
|
|
610
|
+
ctx.ui.notify(`Cancelled "${label}"`, "info");
|
|
611
|
+
} catch (err) {
|
|
612
|
+
ctx.ui.notify(`Failed to cancel: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
//#endregion
|
|
618
|
+
export { src_default as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dougbots/avenor-pi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package"
|
|
7
|
+
],
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/sdougbrown/avenor",
|
|
11
|
+
"directory": "packages/pi"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"pi": {
|
|
28
|
+
"extensions": [
|
|
29
|
+
"./dist/index.js"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsdown",
|
|
34
|
+
"test": "bun test",
|
|
35
|
+
"dev": "tsdown --watch"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@dougbots/avenor-core": "^0.3.2"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
42
|
+
"@earendil-works/pi-tui": "*"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
46
|
+
"@earendil-works/pi-tui": "*",
|
|
47
|
+
"@types/bun": "^1.3.12",
|
|
48
|
+
"@types/node": "^25.8.0",
|
|
49
|
+
"tsdown": "^0.22.0",
|
|
50
|
+
"typebox": "^1.2.17",
|
|
51
|
+
"typescript": "^6.0.3"
|
|
52
|
+
}
|
|
53
|
+
}
|