@dougbots/avenor-core 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 +151 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.js +854 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# @dougbots/avenor-core
|
|
2
|
+
|
|
3
|
+
Control socket client, supervisor lifecycle, and tool implementations for the avenor agent runner.
|
|
4
|
+
|
|
5
|
+
Avenor is a CLI daemon that runs AI agent sessions. This package manages that daemon's process and provides typed wrappers around its JSON-RPC control socket — the interface used to spawn runs, check status, follow up on sessions, and shut everything down cleanly.
|
|
6
|
+
|
|
7
|
+
It is a **private** workspace package consumed by `@dougbots/avenor-mcp` and `@dougbots/avenor-opencode`. It is not published to npm.
|
|
8
|
+
|
|
9
|
+
## Binary discovery
|
|
10
|
+
|
|
11
|
+
The avenor binary is located through a three-step fallback:
|
|
12
|
+
|
|
13
|
+
1. `AVENOR_BIN` environment variable
|
|
14
|
+
2. `avenor` on `PATH` (via `which avenor`)
|
|
15
|
+
3. `~/.botfiles/bin/avenor`
|
|
16
|
+
|
|
17
|
+
The exported helper `findAvenorBinary()` runs this discovery. The `Supervisor` calls it automatically if no explicit `binaryPath` is given.
|
|
18
|
+
|
|
19
|
+
## Core concepts
|
|
20
|
+
|
|
21
|
+
### Client
|
|
22
|
+
|
|
23
|
+
`dial(socketPath)` connects to an avenor control socket and returns a `Client`. The `Client` speaks JSON-RPC over a Unix socket and exposes methods like `spawn()`, `status()`, `list()`, `shutdown()`, `answerPermission()`, `prompt()`, and `cancel()`. It also provides an async iterable `events()` for streaming runtime events.
|
|
24
|
+
|
|
25
|
+
### Supervisor
|
|
26
|
+
|
|
27
|
+
`Supervisor.get()` is a singleton that ensures one avenor `stable` process is running, bound to a per-PID control socket. It starts the binary with `avenor stable --control-socket <path> --idle-timeout 30m`, retries the socket dial until it's ready, and registers signal handlers to clean up on exit.
|
|
28
|
+
|
|
29
|
+
Spawned runs are tracked in-memory (label → runtime ID, sentinel path, event log path) so follow-ups and status checks can resolve the right runtime.
|
|
30
|
+
|
|
31
|
+
## Tools
|
|
32
|
+
|
|
33
|
+
The package exports six self-contained tool functions. Each is designed for embedding in MCP servers or CLI handlers.
|
|
34
|
+
|
|
35
|
+
### `spawnTool`
|
|
36
|
+
|
|
37
|
+
Starts a new agent run.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { spawnTool } from '@dougbots/avenor-core'
|
|
41
|
+
|
|
42
|
+
const result = await spawnTool({
|
|
43
|
+
agent: 'codex',
|
|
44
|
+
prompt: 'Write a function to reverse a string',
|
|
45
|
+
label: 'reverse-string',
|
|
46
|
+
model: 'claude-sonnet-4',
|
|
47
|
+
})
|
|
48
|
+
// { run_id: '...', label: 'reverse-string', supervisor_id: '/path/to/socket' }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Accepts `agent`, `prompt`, `promptFile`, `label`, `dir`, `timeout`, `model`, `sessionId`, and `supervisorId` (to target a specific socket).
|
|
52
|
+
|
|
53
|
+
### `statusTool`
|
|
54
|
+
|
|
55
|
+
Reports status for one or all runs. When `runId` is omitted it lists every run across the supervisor.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { statusTool } from '@dougbots/avenor-core'
|
|
59
|
+
|
|
60
|
+
const status = await statusTool({ runId: 'reverse-string' })
|
|
61
|
+
// { run_id: 'reverse-string', label: 'reverse-string', status: 'running', ... }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Status values: `running`, `done`, `failed`, `timeout`, `killed`. If a permission request is pending, the result includes `pending_permission` with available options.
|
|
65
|
+
|
|
66
|
+
### `answerPermissionTool`
|
|
67
|
+
|
|
68
|
+
Answers a pending permission request for a run. If `requestId` is omitted, it looks up the currently pending request automatically.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { answerPermissionTool } from '@dougbots/avenor-core'
|
|
72
|
+
|
|
73
|
+
await answerPermissionTool({ runId: 'my-run', optionId: 'allow-once' })
|
|
74
|
+
// { ok: true }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `followUpTool`
|
|
78
|
+
|
|
79
|
+
Resumes a completed run by spawning a follow-up run on the same session, preserving conversation context.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { followUpTool } from '@dougbots/avenor-core'
|
|
83
|
+
|
|
84
|
+
await followUpTool({ runId: 'reverse-string', message: 'Now optimize it for speed' })
|
|
85
|
+
// { run_id: '...', label: 'reverse-string-followup' }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `eventsTool`
|
|
89
|
+
|
|
90
|
+
Reads recent events from a run's on-disk event log. Optionally filters by event type and limits the count (default 50, max 1000).
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { eventsTool } from '@dougbots/avenor-core'
|
|
94
|
+
|
|
95
|
+
const events = await eventsTool({ runId: 'my-run', types: ['tool_use', 'error'], limit: 20 })
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
⚠️ Only supported through the singleton `Supervisor` — passing an explicit `supervisorId` will throw.
|
|
99
|
+
|
|
100
|
+
### `shutdownTool`
|
|
101
|
+
|
|
102
|
+
Shuts down the avenor daemon. Pass `force: true` for an immediate kill instead of graceful termination. Cleans up sentinel files and event logs for tracked runs.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { shutdownTool } from '@dougbots/avenor-core'
|
|
106
|
+
|
|
107
|
+
const result = await shutdownTool({ force: false })
|
|
108
|
+
// { ok: true, cleaned_up: ['/path/to/sentinel.done', ...] }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Quick start
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { Supervisor, spawnTool, statusTool, shutdownTool } from '@dougbots/avenor-core'
|
|
115
|
+
|
|
116
|
+
// 1. The supervisor auto-starts avenor. Call get() early — it's a singleton.
|
|
117
|
+
const sup = await Supervisor.get()
|
|
118
|
+
|
|
119
|
+
// 2. Spawn a run
|
|
120
|
+
const { run_id } = await spawnTool({
|
|
121
|
+
agent: 'codex',
|
|
122
|
+
prompt: 'Review src/app.ts for security issues',
|
|
123
|
+
label: 'security-review',
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// 3. Poll until complete
|
|
127
|
+
let state = await statusTool({ runId: run_id })
|
|
128
|
+
while (state.status === 'running' || state.status === undefined) {
|
|
129
|
+
await new Promise(r => setTimeout(r, 2000))
|
|
130
|
+
state = await statusTool({ runId: run_id })
|
|
131
|
+
}
|
|
132
|
+
console.log(state) // { run_id, label, status: 'done', ... }
|
|
133
|
+
|
|
134
|
+
// 4. Clean up
|
|
135
|
+
await shutdownTool()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Exports
|
|
139
|
+
|
|
140
|
+
| Export | Kind | Description |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `dial` | function | Connect to a control socket, returning a `Client` |
|
|
143
|
+
| `Client` | class | JSON-RPC client for the avenor daemon |
|
|
144
|
+
| `Supervisor` | class | Singleton avenor process lifecycle manager |
|
|
145
|
+
| `findAvenorBinary` | function | Discover the avenor binary path |
|
|
146
|
+
| `spawnTool` | function | Start a new agent run |
|
|
147
|
+
| `statusTool` | function | Query run status (single or all) |
|
|
148
|
+
| `answerPermissionTool` | function | Answer a pending permission request |
|
|
149
|
+
| `followUpTool` | function | Resume a session with a follow-up prompt |
|
|
150
|
+
| `eventsTool` | function | Read recent events from a run's event log |
|
|
151
|
+
| `shutdownTool` | function | Stop the avenor daemon and clean up |
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as net from "node:net";
|
|
2
|
+
|
|
3
|
+
//#region src/client.d.ts
|
|
4
|
+
interface Event {
|
|
5
|
+
event: string;
|
|
6
|
+
session_id?: string;
|
|
7
|
+
runtime_id?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
interface SpawnParams {
|
|
11
|
+
agent: string;
|
|
12
|
+
prompt_file?: string;
|
|
13
|
+
prompt?: string;
|
|
14
|
+
dir?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
model?: string;
|
|
18
|
+
session_id?: string;
|
|
19
|
+
sentinel_file?: string;
|
|
20
|
+
on_event?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
declare class Client {
|
|
24
|
+
private socket;
|
|
25
|
+
private rl;
|
|
26
|
+
private nextID;
|
|
27
|
+
private pending;
|
|
28
|
+
private started;
|
|
29
|
+
private callTimeout;
|
|
30
|
+
private eventQueue;
|
|
31
|
+
private eventResolvers;
|
|
32
|
+
private eventDone;
|
|
33
|
+
private dropped;
|
|
34
|
+
private subscribed;
|
|
35
|
+
constructor(socket: net.Socket, opts?: {
|
|
36
|
+
callTimeoutMs?: number;
|
|
37
|
+
});
|
|
38
|
+
private startReadLoop;
|
|
39
|
+
private pushEvent;
|
|
40
|
+
call(method: string, params?: unknown): Promise<unknown>;
|
|
41
|
+
private subscribe;
|
|
42
|
+
events(): AsyncIterable<Event>;
|
|
43
|
+
status(runtimeId?: string): Promise<Record<string, unknown>>;
|
|
44
|
+
list(): Promise<Array<Record<string, unknown>>>;
|
|
45
|
+
spawn(params: SpawnParams): Promise<Record<string, unknown>>;
|
|
46
|
+
shutdown(mode: string): Promise<void>;
|
|
47
|
+
answerPermission(runtimeId: string, requestId: string, optionId: string): Promise<void>;
|
|
48
|
+
prompt(runtimeId: string, text: string): Promise<void>;
|
|
49
|
+
cancel(runtimeId?: string): Promise<void>;
|
|
50
|
+
close(): void;
|
|
51
|
+
}
|
|
52
|
+
declare function dial(socketPath: string, opts?: {
|
|
53
|
+
callTimeoutMs?: number;
|
|
54
|
+
}): Promise<Client>;
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/supervisor.d.ts
|
|
57
|
+
interface RunInfo {
|
|
58
|
+
label: string;
|
|
59
|
+
sentinelPath: string;
|
|
60
|
+
eventLogPath: string;
|
|
61
|
+
runtimeId?: string;
|
|
62
|
+
sessionId?: string;
|
|
63
|
+
}
|
|
64
|
+
interface SupervisorOptions {
|
|
65
|
+
binaryPath?: string;
|
|
66
|
+
callTimeoutMs?: number;
|
|
67
|
+
}
|
|
68
|
+
declare function findAvenorBinary(): string;
|
|
69
|
+
declare class Supervisor {
|
|
70
|
+
private static instance;
|
|
71
|
+
private static cleanupRegistered;
|
|
72
|
+
private static registerCleanup;
|
|
73
|
+
private client;
|
|
74
|
+
private childProcess;
|
|
75
|
+
private socketPath;
|
|
76
|
+
private crashed;
|
|
77
|
+
private crashCode;
|
|
78
|
+
private runs;
|
|
79
|
+
private binaryPath;
|
|
80
|
+
private callTimeoutMs;
|
|
81
|
+
private constructor();
|
|
82
|
+
static get(opts?: SupervisorOptions): Promise<Supervisor>;
|
|
83
|
+
private startProcess;
|
|
84
|
+
get supervisorId(): string;
|
|
85
|
+
static isCurrentInstance(supervisorId: string): boolean;
|
|
86
|
+
static currentInstance(): Supervisor | null;
|
|
87
|
+
getClient(): Client;
|
|
88
|
+
spawn(params: SpawnParams): Promise<RunInfo>;
|
|
89
|
+
close(): Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/tools/spawn.d.ts
|
|
93
|
+
declare function spawnTool(args: {
|
|
94
|
+
agent: string;
|
|
95
|
+
prompt?: string;
|
|
96
|
+
promptFile?: string;
|
|
97
|
+
label?: string;
|
|
98
|
+
dir?: string;
|
|
99
|
+
timeout?: string;
|
|
100
|
+
model?: string;
|
|
101
|
+
sessionId?: string;
|
|
102
|
+
supervisorId?: string;
|
|
103
|
+
}): Promise<{
|
|
104
|
+
run_id: string;
|
|
105
|
+
label: string;
|
|
106
|
+
supervisor_id: string;
|
|
107
|
+
}>;
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/tools/status.d.ts
|
|
110
|
+
interface StatusResult {
|
|
111
|
+
run_id: string;
|
|
112
|
+
label: string;
|
|
113
|
+
status: string;
|
|
114
|
+
phase?: string;
|
|
115
|
+
phase_label?: string;
|
|
116
|
+
pending_permission?: {
|
|
117
|
+
request_id: string;
|
|
118
|
+
description: string;
|
|
119
|
+
options: Array<{
|
|
120
|
+
option_id: string;
|
|
121
|
+
label: string;
|
|
122
|
+
kind: string;
|
|
123
|
+
}>;
|
|
124
|
+
};
|
|
125
|
+
session_id?: string;
|
|
126
|
+
stop_reason?: string;
|
|
127
|
+
}
|
|
128
|
+
declare function statusTool(args: {
|
|
129
|
+
runId?: string;
|
|
130
|
+
supervisorId?: string;
|
|
131
|
+
}): Promise<StatusResult | StatusResult[]>;
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/tools/answer-permission.d.ts
|
|
134
|
+
declare function answerPermissionTool(args: {
|
|
135
|
+
runId: string;
|
|
136
|
+
optionId: string;
|
|
137
|
+
requestId?: string;
|
|
138
|
+
supervisorId?: string;
|
|
139
|
+
}): Promise<{
|
|
140
|
+
ok: boolean;
|
|
141
|
+
}>;
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/tools/follow-up.d.ts
|
|
144
|
+
declare function followUpTool(args: {
|
|
145
|
+
runId: string;
|
|
146
|
+
message: string;
|
|
147
|
+
label?: string;
|
|
148
|
+
supervisorId?: string;
|
|
149
|
+
}): Promise<{
|
|
150
|
+
run_id: string;
|
|
151
|
+
label: string;
|
|
152
|
+
}>;
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/tools/events.d.ts
|
|
155
|
+
declare function eventsTool(args: {
|
|
156
|
+
runId: string;
|
|
157
|
+
types?: string[];
|
|
158
|
+
limit?: number;
|
|
159
|
+
supervisorId?: string;
|
|
160
|
+
}): Promise<Array<{
|
|
161
|
+
type: string;
|
|
162
|
+
ts: number;
|
|
163
|
+
[key: string]: unknown;
|
|
164
|
+
}>>;
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/tools/shutdown.d.ts
|
|
167
|
+
declare function shutdownTool(args: {
|
|
168
|
+
supervisorId?: string;
|
|
169
|
+
force?: boolean;
|
|
170
|
+
}): Promise<{
|
|
171
|
+
ok: boolean;
|
|
172
|
+
cleaned_up: string[];
|
|
173
|
+
}>;
|
|
174
|
+
//#endregion
|
|
175
|
+
export { type Client, type Event, type RunInfo, type SpawnParams, type StatusResult, Supervisor, type SupervisorOptions, answerPermissionTool, dial, eventsTool, findAvenorBinary, followUpTool, shutdownTool, spawnTool, statusTool };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
import * as net from "node:net";
|
|
2
|
+
import * as readline from "node:readline";
|
|
3
|
+
import { execSync, spawn } from "node:child_process";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import * as crypto from "node:crypto";
|
|
8
|
+
//#region src/client.ts
|
|
9
|
+
var Client = class {
|
|
10
|
+
socket;
|
|
11
|
+
rl;
|
|
12
|
+
nextID = 0;
|
|
13
|
+
pending = /* @__PURE__ */ new Map();
|
|
14
|
+
started = false;
|
|
15
|
+
callTimeout;
|
|
16
|
+
eventQueue = [];
|
|
17
|
+
eventResolvers = [];
|
|
18
|
+
eventDone = false;
|
|
19
|
+
dropped = 0;
|
|
20
|
+
subscribed = false;
|
|
21
|
+
constructor(socket, opts) {
|
|
22
|
+
this.socket = socket;
|
|
23
|
+
this.rl = readline.createInterface({
|
|
24
|
+
input: socket,
|
|
25
|
+
crlfDelay: Infinity
|
|
26
|
+
});
|
|
27
|
+
this.callTimeout = opts?.callTimeoutMs ?? 3e4;
|
|
28
|
+
this.socket.on("error", (err) => {
|
|
29
|
+
this.pushEvent({
|
|
30
|
+
event: "protocol-error",
|
|
31
|
+
message: err.message
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
startReadLoop() {
|
|
36
|
+
if (this.started) return;
|
|
37
|
+
this.started = true;
|
|
38
|
+
this.rl.on("line", (line) => {
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(line);
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (parsed.id !== void 0 && parsed.id !== null) {
|
|
46
|
+
const id = Number(parsed.id);
|
|
47
|
+
const pc = this.pending.get(id);
|
|
48
|
+
if (pc) {
|
|
49
|
+
clearTimeout(pc.timer);
|
|
50
|
+
this.pending.delete(id);
|
|
51
|
+
if (parsed.error) pc.reject(/* @__PURE__ */ new Error(`rpc error [${parsed.error.code}]: ${parsed.error.message}`));
|
|
52
|
+
else pc.resolve(parsed.result);
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (parsed.method === "event" && parsed.params) {
|
|
57
|
+
const ev = { ...parsed.params };
|
|
58
|
+
if (this.dropped > 0) {
|
|
59
|
+
const lag = this.dropped;
|
|
60
|
+
this.dropped = 0;
|
|
61
|
+
const lagEvent = {
|
|
62
|
+
event: "client.lagged",
|
|
63
|
+
dropped_count: lag
|
|
64
|
+
};
|
|
65
|
+
this.pushEvent(lagEvent);
|
|
66
|
+
}
|
|
67
|
+
this.pushEvent(ev);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
this.rl.on("close", () => {
|
|
71
|
+
for (const [, pc] of this.pending) {
|
|
72
|
+
clearTimeout(pc.timer);
|
|
73
|
+
pc.reject(/* @__PURE__ */ new Error("read response: connection closed"));
|
|
74
|
+
}
|
|
75
|
+
this.pending.clear();
|
|
76
|
+
this.eventDone = true;
|
|
77
|
+
for (const resolve of this.eventResolvers) resolve({
|
|
78
|
+
value: void 0,
|
|
79
|
+
done: true
|
|
80
|
+
});
|
|
81
|
+
this.eventResolvers = [];
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
pushEvent(ev) {
|
|
85
|
+
if (this.eventResolvers.length > 0) this.eventResolvers.shift()({
|
|
86
|
+
value: ev,
|
|
87
|
+
done: false
|
|
88
|
+
});
|
|
89
|
+
else if (this.eventQueue.length < 256) this.eventQueue.push(ev);
|
|
90
|
+
else this.dropped++;
|
|
91
|
+
}
|
|
92
|
+
async call(method, params) {
|
|
93
|
+
this.startReadLoop();
|
|
94
|
+
const id = ++this.nextID;
|
|
95
|
+
const req = {
|
|
96
|
+
jsonrpc: "2.0",
|
|
97
|
+
id,
|
|
98
|
+
method
|
|
99
|
+
};
|
|
100
|
+
if (params !== void 0) req.params = params;
|
|
101
|
+
const data = JSON.stringify(req) + "\n";
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const timer = setTimeout(() => {
|
|
104
|
+
this.pending.delete(id);
|
|
105
|
+
reject(/* @__PURE__ */ new Error("read response: timeout"));
|
|
106
|
+
}, this.callTimeout);
|
|
107
|
+
this.pending.set(id, {
|
|
108
|
+
resolve,
|
|
109
|
+
reject,
|
|
110
|
+
timer
|
|
111
|
+
});
|
|
112
|
+
this.socket.write(data, (err) => {
|
|
113
|
+
if (err) {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
this.pending.delete(id);
|
|
116
|
+
reject(/* @__PURE__ */ new Error(`write request: ${err.message}`));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async subscribe() {
|
|
122
|
+
if (this.subscribed) return;
|
|
123
|
+
this.subscribed = true;
|
|
124
|
+
await this.call("subscribe");
|
|
125
|
+
}
|
|
126
|
+
async *events() {
|
|
127
|
+
await this.subscribe();
|
|
128
|
+
while (true) {
|
|
129
|
+
if (this.eventDone) break;
|
|
130
|
+
if (this.eventQueue.length > 0) {
|
|
131
|
+
yield this.eventQueue.shift();
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const result = await new Promise((resolve) => {
|
|
135
|
+
this.eventResolvers.push(resolve);
|
|
136
|
+
});
|
|
137
|
+
if (result.done) break;
|
|
138
|
+
yield result.value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async status(runtimeId) {
|
|
142
|
+
const params = runtimeId ? { runtime_id: runtimeId } : void 0;
|
|
143
|
+
return this.call("status", params);
|
|
144
|
+
}
|
|
145
|
+
async list() {
|
|
146
|
+
return this.call("list");
|
|
147
|
+
}
|
|
148
|
+
async spawn(params) {
|
|
149
|
+
return this.call("spawn", params);
|
|
150
|
+
}
|
|
151
|
+
async shutdown(mode) {
|
|
152
|
+
await this.call("shutdown", { mode });
|
|
153
|
+
}
|
|
154
|
+
async answerPermission(runtimeId, requestId, optionId) {
|
|
155
|
+
await this.call("answer_permission", {
|
|
156
|
+
runtime_id: runtimeId,
|
|
157
|
+
request_id: requestId,
|
|
158
|
+
option_id: optionId
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async prompt(runtimeId, text) {
|
|
162
|
+
await this.call("prompt", {
|
|
163
|
+
runtime_id: runtimeId,
|
|
164
|
+
text
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async cancel(runtimeId) {
|
|
168
|
+
const params = runtimeId ? { runtime_id: runtimeId } : void 0;
|
|
169
|
+
await this.call("cancel", params);
|
|
170
|
+
}
|
|
171
|
+
close() {
|
|
172
|
+
this.rl.close();
|
|
173
|
+
this.socket.destroy();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
function dial(socketPath, opts) {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const socket = net.createConnection({ path: socketPath });
|
|
179
|
+
const onConnect = () => {
|
|
180
|
+
socket.removeListener("error", onError);
|
|
181
|
+
resolve(new Client(socket, opts));
|
|
182
|
+
};
|
|
183
|
+
const onError = (err) => {
|
|
184
|
+
socket.removeListener("connect", onConnect);
|
|
185
|
+
reject(/* @__PURE__ */ new Error(`dial control socket: ${err.message}`));
|
|
186
|
+
};
|
|
187
|
+
socket.on("connect", onConnect);
|
|
188
|
+
socket.on("error", onError);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/paths.ts
|
|
193
|
+
function avenorHome() {
|
|
194
|
+
return process.env.AVENOR_HOME || path.join(os.homedir(), ".avenor");
|
|
195
|
+
}
|
|
196
|
+
function socketsRoot() {
|
|
197
|
+
return path.join(avenorHome(), "sockets");
|
|
198
|
+
}
|
|
199
|
+
function runsRoot() {
|
|
200
|
+
return path.join(avenorHome(), "runs");
|
|
201
|
+
}
|
|
202
|
+
function ensureRunPaths(runId) {
|
|
203
|
+
const runDir = path.join(runsRoot(), runId);
|
|
204
|
+
fs.mkdirSync(runDir, {
|
|
205
|
+
recursive: true,
|
|
206
|
+
mode: 448
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
runDir,
|
|
210
|
+
sentinelPath: path.join(runDir, "sentinel.done"),
|
|
211
|
+
eventLogPath: path.join(runDir, "events.log")
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/supervisor.ts
|
|
216
|
+
function findAvenorBinary() {
|
|
217
|
+
const envBin = process.env.AVENOR_BIN;
|
|
218
|
+
if (envBin) try {
|
|
219
|
+
fs.accessSync(envBin, fs.constants.X_OK);
|
|
220
|
+
return envBin;
|
|
221
|
+
} catch {
|
|
222
|
+
throw new Error(`AVENOR_BIN path not executable: ${envBin}`);
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const binPath = execSync("which avenor", { encoding: "utf-8" }).trim();
|
|
226
|
+
if (binPath) {
|
|
227
|
+
fs.accessSync(binPath, fs.constants.X_OK);
|
|
228
|
+
return binPath;
|
|
229
|
+
}
|
|
230
|
+
} catch {}
|
|
231
|
+
const homeBin = path.join(os.homedir(), ".botfiles", "bin", "avenor");
|
|
232
|
+
try {
|
|
233
|
+
fs.accessSync(homeBin, fs.constants.X_OK);
|
|
234
|
+
return homeBin;
|
|
235
|
+
} catch {}
|
|
236
|
+
throw new Error("avenor binary not found. Set AVENOR_BIN=/path/to/avenor or ensure 'avenor' is on PATH.");
|
|
237
|
+
}
|
|
238
|
+
var Supervisor = class Supervisor {
|
|
239
|
+
static instance = null;
|
|
240
|
+
static cleanupRegistered = false;
|
|
241
|
+
static registerCleanup() {
|
|
242
|
+
if (Supervisor.cleanupRegistered) return;
|
|
243
|
+
Supervisor.cleanupRegistered = true;
|
|
244
|
+
const cleanup = () => {
|
|
245
|
+
if (Supervisor.instance?.childProcess) Supervisor.instance.childProcess.kill();
|
|
246
|
+
};
|
|
247
|
+
process.on("exit", cleanup);
|
|
248
|
+
process.on("SIGINT", () => {
|
|
249
|
+
cleanup();
|
|
250
|
+
process.exit();
|
|
251
|
+
});
|
|
252
|
+
process.on("SIGTERM", () => {
|
|
253
|
+
cleanup();
|
|
254
|
+
process.exit();
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
client = null;
|
|
258
|
+
childProcess = null;
|
|
259
|
+
socketPath;
|
|
260
|
+
crashed = false;
|
|
261
|
+
crashCode = null;
|
|
262
|
+
runs = /* @__PURE__ */ new Map();
|
|
263
|
+
binaryPath;
|
|
264
|
+
callTimeoutMs;
|
|
265
|
+
constructor(binaryPath, socketPath, opts) {
|
|
266
|
+
this.binaryPath = binaryPath;
|
|
267
|
+
this.socketPath = socketPath;
|
|
268
|
+
this.callTimeoutMs = opts?.callTimeoutMs ?? 3e4;
|
|
269
|
+
}
|
|
270
|
+
static async get(opts) {
|
|
271
|
+
if (Supervisor.instance && !Supervisor.instance.crashed) return Supervisor.instance;
|
|
272
|
+
Supervisor.registerCleanup();
|
|
273
|
+
const binaryPath = opts?.binaryPath ?? findAvenorBinary();
|
|
274
|
+
const socketDir = socketsRoot();
|
|
275
|
+
fs.mkdirSync(socketDir, {
|
|
276
|
+
recursive: true,
|
|
277
|
+
mode: 448
|
|
278
|
+
});
|
|
279
|
+
const socketPath = path.join(socketDir, `avenor-mcp-${process.pid}.sock`);
|
|
280
|
+
let socketExists = false;
|
|
281
|
+
try {
|
|
282
|
+
await fs.promises.access(socketPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
283
|
+
socketExists = true;
|
|
284
|
+
} catch {}
|
|
285
|
+
if (socketExists) try {
|
|
286
|
+
const probeClient = await dial(socketPath, { callTimeoutMs: 2e3 });
|
|
287
|
+
try {
|
|
288
|
+
await probeClient.status();
|
|
289
|
+
probeClient.close();
|
|
290
|
+
const client = await dial(socketPath);
|
|
291
|
+
const sup = new Supervisor(binaryPath, socketPath, opts);
|
|
292
|
+
sup.client = client;
|
|
293
|
+
Supervisor.instance = sup;
|
|
294
|
+
return sup;
|
|
295
|
+
} catch {
|
|
296
|
+
probeClient.close();
|
|
297
|
+
try {
|
|
298
|
+
await fs.promises.unlink(socketPath);
|
|
299
|
+
} catch {}
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
try {
|
|
303
|
+
await fs.promises.unlink(socketPath);
|
|
304
|
+
} catch {}
|
|
305
|
+
}
|
|
306
|
+
const sup = new Supervisor(binaryPath, socketPath, opts);
|
|
307
|
+
await sup.startProcess();
|
|
308
|
+
Supervisor.instance = sup;
|
|
309
|
+
return sup;
|
|
310
|
+
}
|
|
311
|
+
startProcess() {
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
let resolved = false;
|
|
314
|
+
this.childProcess = spawn(this.binaryPath, [
|
|
315
|
+
"stable",
|
|
316
|
+
"--control-socket",
|
|
317
|
+
this.socketPath,
|
|
318
|
+
"--idle-timeout",
|
|
319
|
+
"30m"
|
|
320
|
+
], { detached: false });
|
|
321
|
+
this.childProcess.on("exit", (code, signal) => {
|
|
322
|
+
if (!resolved) {
|
|
323
|
+
resolved = true;
|
|
324
|
+
reject(/* @__PURE__ */ new Error(`avenor exited with code ${code}${signal ? " signal " + signal : ""} during startup`));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (code !== 0 || signal) {
|
|
328
|
+
this.crashed = true;
|
|
329
|
+
this.crashCode = code ?? -1;
|
|
330
|
+
if (this.client) {
|
|
331
|
+
try {
|
|
332
|
+
this.client.close();
|
|
333
|
+
} catch {}
|
|
334
|
+
this.client = null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
this.childProcess.on("error", (err) => {
|
|
339
|
+
if (!resolved) {
|
|
340
|
+
resolved = true;
|
|
341
|
+
reject(/* @__PURE__ */ new Error(`failed to start avenor: ${err.message}`));
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
const timeout = setTimeout(() => {
|
|
345
|
+
if (!resolved) {
|
|
346
|
+
resolved = true;
|
|
347
|
+
reject(/* @__PURE__ */ new Error("timed out waiting for avenor socket"));
|
|
348
|
+
}
|
|
349
|
+
}, 1e4);
|
|
350
|
+
const retryDial = async () => {
|
|
351
|
+
while (!resolved) try {
|
|
352
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
353
|
+
this.client = await dial(this.socketPath, { callTimeoutMs: this.callTimeoutMs });
|
|
354
|
+
clearTimeout(timeout);
|
|
355
|
+
resolved = true;
|
|
356
|
+
resolve();
|
|
357
|
+
return;
|
|
358
|
+
} catch {}
|
|
359
|
+
};
|
|
360
|
+
retryDial();
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
get supervisorId() {
|
|
364
|
+
return this.socketPath;
|
|
365
|
+
}
|
|
366
|
+
static isCurrentInstance(supervisorId) {
|
|
367
|
+
return Supervisor.instance !== null && Supervisor.instance.supervisorId === supervisorId;
|
|
368
|
+
}
|
|
369
|
+
static currentInstance() {
|
|
370
|
+
return Supervisor.instance;
|
|
371
|
+
}
|
|
372
|
+
getClient() {
|
|
373
|
+
if (this.crashed) throw new Error(`avenor supervisor crashed (exit code ${this.crashCode}). Call avenor_shutdown to clean up and retry.`);
|
|
374
|
+
if (!this.client) throw new Error("supervisor not started");
|
|
375
|
+
return this.client;
|
|
376
|
+
}
|
|
377
|
+
async spawn(params) {
|
|
378
|
+
const client = this.getClient();
|
|
379
|
+
const runId = crypto.randomUUID();
|
|
380
|
+
const { sentinelPath, eventLogPath } = ensureRunPaths(runId);
|
|
381
|
+
const spawnParams = {
|
|
382
|
+
...params,
|
|
383
|
+
label: params.label ?? runId,
|
|
384
|
+
sentinel_file: sentinelPath,
|
|
385
|
+
on_event: eventLogPath
|
|
386
|
+
};
|
|
387
|
+
const result = await client.spawn(spawnParams);
|
|
388
|
+
const runInfo = {
|
|
389
|
+
label: spawnParams.label ?? runId,
|
|
390
|
+
sentinelPath: result.sentinel_file ?? sentinelPath,
|
|
391
|
+
eventLogPath: result.on_event ?? eventLogPath,
|
|
392
|
+
runtimeId: result.runtime_id,
|
|
393
|
+
sessionId: result.session_id
|
|
394
|
+
};
|
|
395
|
+
this.runs.set(runInfo.label, runInfo);
|
|
396
|
+
this.runs.set(runId, runInfo);
|
|
397
|
+
return runInfo;
|
|
398
|
+
}
|
|
399
|
+
async close() {
|
|
400
|
+
if (this.client) try {
|
|
401
|
+
await this.client.shutdown("graceful");
|
|
402
|
+
} catch {}
|
|
403
|
+
if (this.childProcess) {
|
|
404
|
+
this.childProcess.kill();
|
|
405
|
+
this.childProcess = null;
|
|
406
|
+
}
|
|
407
|
+
if (this.client) {
|
|
408
|
+
try {
|
|
409
|
+
this.client.close();
|
|
410
|
+
} catch {}
|
|
411
|
+
this.client = null;
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
await fs.promises.unlink(this.socketPath);
|
|
415
|
+
} catch {}
|
|
416
|
+
this.crashed = false;
|
|
417
|
+
this.crashCode = null;
|
|
418
|
+
this.runs.clear();
|
|
419
|
+
Supervisor.instance = null;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/tools/validate.ts
|
|
424
|
+
function validateRunId(id) {
|
|
425
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) throw new Error(`invalid run_id: ${id}`);
|
|
426
|
+
}
|
|
427
|
+
function validateTimeout(value) {
|
|
428
|
+
const trimmed = value.trim();
|
|
429
|
+
const match = /^(\d+)([smh]?)$/.exec(trimmed);
|
|
430
|
+
if (!match) throw new Error(`invalid timeout: ${value}`);
|
|
431
|
+
const amount = Number(match[1]);
|
|
432
|
+
const unit = match[2];
|
|
433
|
+
if (!Number.isFinite(amount) || amount <= 0) throw new Error(`invalid timeout: ${value}`);
|
|
434
|
+
if (unit === "m") return amount * 60;
|
|
435
|
+
if (unit === "h") return amount * 3600;
|
|
436
|
+
return amount;
|
|
437
|
+
}
|
|
438
|
+
function validateSupervisorSocketPath(supervisorId) {
|
|
439
|
+
const canonical = fs.realpathSync(supervisorId);
|
|
440
|
+
const root = fs.realpathSync(socketsRoot());
|
|
441
|
+
const relative = path.relative(root, canonical);
|
|
442
|
+
if (relative.startsWith("..") || path.isAbsolute(relative) || !/^avenor-mcp-[^/\\]+\.sock$/.test(path.basename(canonical))) throw new Error(`invalid supervisorId: ${supervisorId}`);
|
|
443
|
+
return canonical;
|
|
444
|
+
}
|
|
445
|
+
//#endregion
|
|
446
|
+
//#region src/tools/spawn.ts
|
|
447
|
+
async function spawnTool(args) {
|
|
448
|
+
const runId = crypto.randomUUID();
|
|
449
|
+
const label = args.label ?? runId;
|
|
450
|
+
if (args.supervisorId) {
|
|
451
|
+
const client = await dial(validateSupervisorSocketPath(args.supervisorId));
|
|
452
|
+
const { sentinelPath, eventLogPath } = ensureRunPaths(runId);
|
|
453
|
+
try {
|
|
454
|
+
return {
|
|
455
|
+
run_id: (await client.spawn({
|
|
456
|
+
agent: args.agent,
|
|
457
|
+
prompt: args.prompt,
|
|
458
|
+
prompt_file: args.promptFile,
|
|
459
|
+
label,
|
|
460
|
+
dir: args.dir,
|
|
461
|
+
timeout: args.timeout !== void 0 ? validateTimeout(args.timeout) : void 0,
|
|
462
|
+
model: args.model,
|
|
463
|
+
session_id: args.sessionId,
|
|
464
|
+
sentinel_file: sentinelPath,
|
|
465
|
+
on_event: eventLogPath
|
|
466
|
+
})).runtime_id ?? runId,
|
|
467
|
+
label,
|
|
468
|
+
supervisor_id: args.supervisorId
|
|
469
|
+
};
|
|
470
|
+
} finally {
|
|
471
|
+
client.close();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const sup = await Supervisor.get();
|
|
475
|
+
await sup.spawn({
|
|
476
|
+
agent: args.agent,
|
|
477
|
+
prompt: args.prompt,
|
|
478
|
+
prompt_file: args.promptFile,
|
|
479
|
+
label,
|
|
480
|
+
dir: args.dir,
|
|
481
|
+
timeout: args.timeout !== void 0 ? validateTimeout(args.timeout) : void 0,
|
|
482
|
+
model: args.model,
|
|
483
|
+
session_id: args.sessionId
|
|
484
|
+
});
|
|
485
|
+
return {
|
|
486
|
+
run_id: runId,
|
|
487
|
+
label,
|
|
488
|
+
supervisor_id: sup.supervisorId
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/tools/status.ts
|
|
493
|
+
function findRunByLabel$3(sup, runId) {
|
|
494
|
+
const runs = sup.runs;
|
|
495
|
+
const byKey = runs.get(runId);
|
|
496
|
+
if (byKey) return byKey;
|
|
497
|
+
for (const info of runs.values()) if (info.label === runId) return info;
|
|
498
|
+
}
|
|
499
|
+
async function parseSentinel$1(filePath) {
|
|
500
|
+
try {
|
|
501
|
+
const content = (await fs.promises.readFile(filePath, "utf-8")).trim();
|
|
502
|
+
if (!content) return null;
|
|
503
|
+
const lines = content.split("\n");
|
|
504
|
+
const result = {};
|
|
505
|
+
result._status = lines[0];
|
|
506
|
+
for (let i = 1; i < lines.length; i++) {
|
|
507
|
+
const line = lines[i].trim();
|
|
508
|
+
if (!line) continue;
|
|
509
|
+
const eq = line.indexOf("=");
|
|
510
|
+
if (eq > 0) result[line.substring(0, eq)] = line.substring(eq + 1);
|
|
511
|
+
}
|
|
512
|
+
return result;
|
|
513
|
+
} catch {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async function sentinelExists(filePath) {
|
|
518
|
+
try {
|
|
519
|
+
await fs.promises.access(filePath, fs.constants.R_OK);
|
|
520
|
+
return true;
|
|
521
|
+
} catch {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function translateStatus(rawPhase, sentinelData) {
|
|
526
|
+
const sentinelStatus = sentinelData?._status;
|
|
527
|
+
switch (rawPhase) {
|
|
528
|
+
case "idle":
|
|
529
|
+
case "running": return "running";
|
|
530
|
+
case "ended":
|
|
531
|
+
if (sentinelStatus) switch (sentinelStatus) {
|
|
532
|
+
case "DONE": return "done";
|
|
533
|
+
case "FAILED": return "failed";
|
|
534
|
+
case "TIMEOUT": return "timeout";
|
|
535
|
+
case "KILLED": return "killed";
|
|
536
|
+
case "BLOCKED": return "failed";
|
|
537
|
+
default: return "running";
|
|
538
|
+
}
|
|
539
|
+
return "running";
|
|
540
|
+
case "DONE": return "done";
|
|
541
|
+
case "FAILED": return "failed";
|
|
542
|
+
case "TIMEOUT": return "timeout";
|
|
543
|
+
case "KILLED": return "killed";
|
|
544
|
+
case "BLOCKED": return "failed";
|
|
545
|
+
default: return "running";
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function buildRunStatus(client, runInfo, liveStatus) {
|
|
549
|
+
let sentinel = null;
|
|
550
|
+
if (await sentinelExists(runInfo.sentinelPath)) sentinel = await parseSentinel$1(runInfo.sentinelPath);
|
|
551
|
+
const translated = translateStatus(liveStatus?.phase ?? liveStatus?.status ?? "running", sentinel);
|
|
552
|
+
const result = {
|
|
553
|
+
run_id: runInfo.label,
|
|
554
|
+
label: runInfo.label,
|
|
555
|
+
status: translated,
|
|
556
|
+
phase: liveStatus?.phase,
|
|
557
|
+
phase_label: liveStatus?.phase_label,
|
|
558
|
+
session_id: sentinel?.SESSION ?? liveStatus?.session_id ?? void 0,
|
|
559
|
+
stop_reason: sentinel?._status ?? void 0
|
|
560
|
+
};
|
|
561
|
+
if (liveStatus?.pending_permission) {
|
|
562
|
+
const pp = liveStatus.pending_permission;
|
|
563
|
+
result.pending_permission = {
|
|
564
|
+
request_id: pp.request_id ?? "",
|
|
565
|
+
description: pp.description ?? "",
|
|
566
|
+
options: Array.isArray(pp.options) ? pp.options.map((o) => ({
|
|
567
|
+
option_id: o.option_id ?? "",
|
|
568
|
+
label: o.label ?? "",
|
|
569
|
+
kind: o.kind ?? ""
|
|
570
|
+
})) : []
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
return result;
|
|
574
|
+
}
|
|
575
|
+
async function statusTool(args) {
|
|
576
|
+
if (args.supervisorId) {
|
|
577
|
+
const client = await dial(validateSupervisorSocketPath(args.supervisorId));
|
|
578
|
+
try {
|
|
579
|
+
if (args.runId) {
|
|
580
|
+
validateRunId(args.runId);
|
|
581
|
+
let liveStatus = null;
|
|
582
|
+
try {
|
|
583
|
+
liveStatus = await client.status(args.runId);
|
|
584
|
+
} catch {}
|
|
585
|
+
const sentinelPath = liveStatus?.sentinel_file ?? path.join(runsRoot(), args.runId, "sentinel.done");
|
|
586
|
+
let sentinel = null;
|
|
587
|
+
if (await sentinelExists(sentinelPath)) sentinel = await parseSentinel$1(sentinelPath);
|
|
588
|
+
return {
|
|
589
|
+
run_id: args.runId,
|
|
590
|
+
label: args.runId,
|
|
591
|
+
status: translateStatus(liveStatus?.phase ?? liveStatus?.status ?? "running", sentinel),
|
|
592
|
+
phase: liveStatus?.phase,
|
|
593
|
+
phase_label: liveStatus?.phase_label,
|
|
594
|
+
session_id: sentinel?.SESSION ?? liveStatus?.session_id ?? void 0,
|
|
595
|
+
stop_reason: sentinel?._status ?? void 0,
|
|
596
|
+
pending_permission: liveStatus?.pending_permission ? {
|
|
597
|
+
request_id: liveStatus.pending_permission.request_id ?? "",
|
|
598
|
+
description: liveStatus.pending_permission.description ?? "",
|
|
599
|
+
options: Array.isArray(liveStatus.pending_permission.options) ? liveStatus.pending_permission.options.map((o) => ({
|
|
600
|
+
option_id: o.option_id ?? "",
|
|
601
|
+
label: o.label ?? "",
|
|
602
|
+
kind: o.kind ?? ""
|
|
603
|
+
})) : []
|
|
604
|
+
} : void 0
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
return (await client.list()).map((entry) => ({
|
|
608
|
+
run_id: entry.runtime_id ?? entry.id ?? "",
|
|
609
|
+
label: entry.label ?? "",
|
|
610
|
+
status: translateStatus(entry.phase ?? entry.status ?? "running", null),
|
|
611
|
+
phase: entry.phase,
|
|
612
|
+
session_id: entry.session_id
|
|
613
|
+
}));
|
|
614
|
+
} finally {
|
|
615
|
+
client.close();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const sup = await Supervisor.get();
|
|
619
|
+
const client = sup.getClient();
|
|
620
|
+
if (args.runId) {
|
|
621
|
+
const runInfo = findRunByLabel$3(sup, args.runId);
|
|
622
|
+
if (!runInfo) throw new Error(`run not found: ${args.runId}`);
|
|
623
|
+
let liveStatus = null;
|
|
624
|
+
if (runInfo.runtimeId) try {
|
|
625
|
+
liveStatus = await client.status(runInfo.runtimeId);
|
|
626
|
+
} catch {}
|
|
627
|
+
return buildRunStatus(client, runInfo, liveStatus);
|
|
628
|
+
}
|
|
629
|
+
const list = await client.list();
|
|
630
|
+
const results = [];
|
|
631
|
+
for (const entry of list) {
|
|
632
|
+
const entryId = entry.runtime_id ?? entry.id ?? "";
|
|
633
|
+
const entryLabel = entry.label ?? entryId;
|
|
634
|
+
const runInfo = findRunByLabel$3(sup, entryId) ?? findRunByLabel$3(sup, entryLabel);
|
|
635
|
+
let liveStatus = null;
|
|
636
|
+
if (runInfo?.runtimeId) try {
|
|
637
|
+
liveStatus = await client.status(runInfo.runtimeId);
|
|
638
|
+
} catch {}
|
|
639
|
+
if (runInfo && liveStatus) results.push(await buildRunStatus(client, runInfo, liveStatus));
|
|
640
|
+
else results.push({
|
|
641
|
+
run_id: entryId,
|
|
642
|
+
label: entryLabel,
|
|
643
|
+
status: translateStatus(entry.phase ?? entry.status ?? "running", null),
|
|
644
|
+
phase: entry.phase,
|
|
645
|
+
session_id: entry.session_id
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
return results;
|
|
649
|
+
}
|
|
650
|
+
//#endregion
|
|
651
|
+
//#region src/tools/answer-permission.ts
|
|
652
|
+
function findRunByLabel$2(sup, runId) {
|
|
653
|
+
const runs = sup.runs;
|
|
654
|
+
const byKey = runs.get(runId);
|
|
655
|
+
if (byKey) return byKey;
|
|
656
|
+
for (const info of runs.values()) if (info.label === runId) return info;
|
|
657
|
+
}
|
|
658
|
+
async function answerPermissionTool(args) {
|
|
659
|
+
let client;
|
|
660
|
+
let sup = null;
|
|
661
|
+
let requestId = args.requestId;
|
|
662
|
+
if (args.supervisorId) client = await dial(validateSupervisorSocketPath(args.supervisorId));
|
|
663
|
+
else {
|
|
664
|
+
sup = await Supervisor.get();
|
|
665
|
+
client = sup.getClient();
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
const runtimeId = (sup ? findRunByLabel$2(sup, args.runId) : void 0)?.runtimeId ?? args.runId;
|
|
669
|
+
if (!requestId) {
|
|
670
|
+
const pp = (await client.status(runtimeId))?.pending_permission;
|
|
671
|
+
if (!pp?.request_id) throw new Error("no pending permission request for this run");
|
|
672
|
+
requestId = pp.request_id;
|
|
673
|
+
}
|
|
674
|
+
await client.answerPermission(runtimeId, requestId, args.optionId);
|
|
675
|
+
return { ok: true };
|
|
676
|
+
} finally {
|
|
677
|
+
if (args.supervisorId) client.close();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
//#endregion
|
|
681
|
+
//#region src/tools/follow-up.ts
|
|
682
|
+
function findRunByLabel$1(sup, runId) {
|
|
683
|
+
const runs = sup.runs;
|
|
684
|
+
const byKey = runs.get(runId);
|
|
685
|
+
if (byKey) return byKey;
|
|
686
|
+
for (const info of runs.values()) if (info.label === runId) return info;
|
|
687
|
+
}
|
|
688
|
+
async function parseSentinel(filePath) {
|
|
689
|
+
try {
|
|
690
|
+
const content = (await fs.promises.readFile(filePath, "utf-8")).trim();
|
|
691
|
+
if (!content) return null;
|
|
692
|
+
const lines = content.split("\n");
|
|
693
|
+
const result = {};
|
|
694
|
+
result._status = lines[0];
|
|
695
|
+
for (let i = 1; i < lines.length; i++) {
|
|
696
|
+
const line = lines[i].trim();
|
|
697
|
+
if (!line) continue;
|
|
698
|
+
const eq = line.indexOf("=");
|
|
699
|
+
if (eq > 0) result[line.substring(0, eq)] = line.substring(eq + 1);
|
|
700
|
+
}
|
|
701
|
+
return result;
|
|
702
|
+
} catch {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async function followUpTool(args) {
|
|
707
|
+
if (args.supervisorId) {
|
|
708
|
+
const client = await dial(validateSupervisorSocketPath(args.supervisorId));
|
|
709
|
+
try {
|
|
710
|
+
validateRunId(args.runId);
|
|
711
|
+
let liveStatus = null;
|
|
712
|
+
try {
|
|
713
|
+
liveStatus = await client.status(args.runId);
|
|
714
|
+
} catch {}
|
|
715
|
+
const sentinel = await parseSentinel(liveStatus?.sentinel_file ?? path.join(runsRoot(), args.runId, "sentinel.done"));
|
|
716
|
+
if (!sentinel?.SESSION) throw new Error("run has no session to resume");
|
|
717
|
+
const agent = liveStatus?.agent ?? "codex";
|
|
718
|
+
const followUpRunId = crypto.randomUUID();
|
|
719
|
+
const followUpLabel = args.label ?? `${args.runId}-followup`;
|
|
720
|
+
const { sentinelPath: followUpSentinelPath, eventLogPath } = ensureRunPaths(followUpRunId);
|
|
721
|
+
await client.spawn({
|
|
722
|
+
agent,
|
|
723
|
+
prompt: args.message,
|
|
724
|
+
label: followUpLabel,
|
|
725
|
+
session_id: sentinel.SESSION,
|
|
726
|
+
sentinel_file: followUpSentinelPath,
|
|
727
|
+
on_event: eventLogPath
|
|
728
|
+
});
|
|
729
|
+
return {
|
|
730
|
+
run_id: followUpRunId,
|
|
731
|
+
label: followUpLabel
|
|
732
|
+
};
|
|
733
|
+
} finally {
|
|
734
|
+
client.close();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const sup = await Supervisor.get();
|
|
738
|
+
const runInfo = findRunByLabel$1(sup, args.runId);
|
|
739
|
+
if (!runInfo) throw new Error(`run not found: ${args.runId}`);
|
|
740
|
+
const sentinel = await parseSentinel(runInfo.sentinelPath);
|
|
741
|
+
if (!sentinel?.SESSION) throw new Error("run has no session to resume");
|
|
742
|
+
const client = sup.getClient();
|
|
743
|
+
let agent = "codex";
|
|
744
|
+
if (runInfo.runtimeId) try {
|
|
745
|
+
agent = (await client.status(runInfo.runtimeId))?.agent ?? "codex";
|
|
746
|
+
} catch {}
|
|
747
|
+
const followUpLabel = args.label ?? `${runInfo.label}-followup`;
|
|
748
|
+
const followUpRun = await sup.spawn({
|
|
749
|
+
agent,
|
|
750
|
+
prompt: args.message,
|
|
751
|
+
session_id: sentinel.SESSION,
|
|
752
|
+
label: followUpLabel
|
|
753
|
+
});
|
|
754
|
+
return {
|
|
755
|
+
run_id: followUpRun.label,
|
|
756
|
+
label: followUpRun.label
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region src/tools/events.ts
|
|
761
|
+
function findRunByLabel(sup, runId) {
|
|
762
|
+
const runs = sup.runs;
|
|
763
|
+
const byKey = runs.get(runId);
|
|
764
|
+
if (byKey) return byKey;
|
|
765
|
+
for (const info of runs.values()) if (info.label === runId) return info;
|
|
766
|
+
}
|
|
767
|
+
async function eventsTool(args) {
|
|
768
|
+
if (args.supervisorId) throw new Error("eventsTool with explicit supervisorId not supported — event log path must be tracked by the singleton supervisor");
|
|
769
|
+
const runInfo = findRunByLabel(await Supervisor.get(), args.runId);
|
|
770
|
+
if (!runInfo) throw new Error(`run not found: ${args.runId}`);
|
|
771
|
+
const limit = Math.max(1, Math.min(1e3, Math.trunc(args.limit ?? 50)));
|
|
772
|
+
let raw;
|
|
773
|
+
try {
|
|
774
|
+
const stat = await fs.promises.stat(runInfo.eventLogPath);
|
|
775
|
+
const tailBytes = Math.min(stat.size, 256 * 1024);
|
|
776
|
+
const handle = await fs.promises.open(runInfo.eventLogPath, "r");
|
|
777
|
+
try {
|
|
778
|
+
const buffer = Buffer.alloc(tailBytes);
|
|
779
|
+
const start = Math.max(0, stat.size - tailBytes);
|
|
780
|
+
const { bytesRead } = await handle.read(buffer, 0, tailBytes, start);
|
|
781
|
+
raw = buffer.subarray(0, bytesRead).toString("utf-8");
|
|
782
|
+
} finally {
|
|
783
|
+
await handle.close();
|
|
784
|
+
}
|
|
785
|
+
} catch {
|
|
786
|
+
return [];
|
|
787
|
+
}
|
|
788
|
+
const lines = raw.split("\n").filter((line) => line.trim());
|
|
789
|
+
let events = [];
|
|
790
|
+
for (const line of lines) try {
|
|
791
|
+
events.push(JSON.parse(line));
|
|
792
|
+
} catch {}
|
|
793
|
+
if (args.types && args.types.length > 0) events = events.filter((e) => args.types.includes(e.type ?? e.event ?? ""));
|
|
794
|
+
if (events.length > limit) events = events.slice(events.length - limit);
|
|
795
|
+
return events;
|
|
796
|
+
}
|
|
797
|
+
//#endregion
|
|
798
|
+
//#region src/tools/shutdown.ts
|
|
799
|
+
async function shutdownTool(args) {
|
|
800
|
+
const cleanedUp = [];
|
|
801
|
+
if (args.supervisorId) {
|
|
802
|
+
const supervisorId = validateSupervisorSocketPath(args.supervisorId);
|
|
803
|
+
if (Supervisor.isCurrentInstance(supervisorId)) {
|
|
804
|
+
const sup = Supervisor.currentInstance();
|
|
805
|
+
if (!sup) throw new Error("supervisor not started");
|
|
806
|
+
await sup.getClient().shutdown(args.force ? "force" : "graceful");
|
|
807
|
+
const runs = sup.runs;
|
|
808
|
+
for (const info of runs.values()) {
|
|
809
|
+
try {
|
|
810
|
+
await fs.promises.unlink(info.sentinelPath);
|
|
811
|
+
cleanedUp.push(info.sentinelPath);
|
|
812
|
+
} catch {}
|
|
813
|
+
try {
|
|
814
|
+
await fs.promises.unlink(info.eventLogPath);
|
|
815
|
+
cleanedUp.push(info.eventLogPath);
|
|
816
|
+
} catch {}
|
|
817
|
+
}
|
|
818
|
+
await sup.close();
|
|
819
|
+
} else {
|
|
820
|
+
const client = await dial(supervisorId);
|
|
821
|
+
try {
|
|
822
|
+
await client.shutdown(args.force ? "force" : "graceful");
|
|
823
|
+
} catch {} finally {
|
|
824
|
+
client.close();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
ok: true,
|
|
829
|
+
cleaned_up: cleanedUp
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
const sup = await Supervisor.get();
|
|
833
|
+
try {
|
|
834
|
+
await sup.getClient().shutdown(args.force ? "force" : "graceful");
|
|
835
|
+
} catch {}
|
|
836
|
+
const runs = sup.runs;
|
|
837
|
+
for (const info of runs.values()) {
|
|
838
|
+
try {
|
|
839
|
+
fs.unlinkSync(info.sentinelPath);
|
|
840
|
+
cleanedUp.push(info.sentinelPath);
|
|
841
|
+
} catch {}
|
|
842
|
+
try {
|
|
843
|
+
fs.unlinkSync(info.eventLogPath);
|
|
844
|
+
cleanedUp.push(info.eventLogPath);
|
|
845
|
+
} catch {}
|
|
846
|
+
}
|
|
847
|
+
await sup.close();
|
|
848
|
+
return {
|
|
849
|
+
ok: true,
|
|
850
|
+
cleaned_up: cleanedUp
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
//#endregion
|
|
854
|
+
export { Supervisor, answerPermissionTool, dial, eventsTool, findAvenorBinary, followUpTool, shutdownTool, spawnTool, statusTool };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dougbots/avenor-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsdown",
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"test:e2e": "AVENOR_REAL_INTEGRATION=1 bun test",
|
|
23
|
+
"dev": "tsdown --watch"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/bun": "^1.3.12",
|
|
27
|
+
"@types/node": "^25.8.0",
|
|
28
|
+
"tsdown": "^0.22.0",
|
|
29
|
+
"typescript": "^6.0.3",
|
|
30
|
+
"unrun": "^0.3.0"
|
|
31
|
+
}
|
|
32
|
+
}
|