@dougbots/avenor-core
v0.10.2
Published
Control socket client, supervisor lifecycle, and tool implementations for the avenor agent runner.
Readme
@dougbots/avenor-core
Control socket client, supervisor lifecycle, and tool implementations for the avenor agent runner.
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.
It is a private workspace package consumed by @dougbots/avenor-mcp and @dougbots/avenor-opencode. It is not published to npm.
Binary discovery
The avenor binary is located through a three-step fallback:
AVENOR_BINenvironment variableavenoronPATH(viawhich avenor)~/.botfiles/bin/avenor
The exported helper findAvenorBinary() runs this discovery. The Supervisor calls it automatically if no explicit binaryPath is given.
Core concepts
Client
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.
Supervisor
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.
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.
Tools
The package exports six self-contained tool functions. Each is designed for embedding in MCP servers or CLI handlers.
spawnTool
Starts a new agent run.
import { spawnTool } from '@dougbots/avenor-core'
const result = await spawnTool({
agent: 'codex',
prompt: 'Write a function to reverse a string',
label: 'reverse-string',
model: 'claude-sonnet-4',
backend: 'opencode-http',
serverUrl: 'http://127.0.0.1:4096',
})
// { run_id: '...', label: 'reverse-string', supervisor_id: '/path/to/socket' }Accepts agent, prompt, promptFile, label, dir, timeout, model, thinking, backend, rosterFile, rosterEntry, serverUrl, sessionId, and supervisorId (to target a specific socket).
Roster selection
rosterFile is the path to a top-level JSON roster map and rosterEntry is the key selected from it. Each entry must contain backend and at least one of agent or model:
{
"planner": {
"backend": "opencode-acp",
"model": "provider/planner"
}
}A direct roster spawn forwards both selectors and must not also provide agent, model, or backend. Direct mode without a roster allows each of those identity fields to be omitted independently, including a backend-only request; the selected runtime supplies defaults for omitted values. thinking is run-level, not a roster field, and is checked against the effective backend. The shared selector validation rejects missing selector pairs, mixed direct/roster fields, and unknown selector keys. Roster-file loading rejects deferred entry fields such as system; thinking remains a valid run-level option but is never read from a roster entry.
await spawnTool({
prompt: 'Analyze the repository',
dir: '/repo',
rosterFile: '/repo/roster.json',
rosterEntry: 'planner',
})Roster phase selection is resolved by Avenor's Go workflow runners. A roster entry may override backend/agent/model per phase. CLI workflow fallback and nested roster inheritance are CLI-only; stable nesting keeps its existing behavior. A resumed session must use the same effective backend as the original session.
statusTool
Reports status for one or all runs. When runId is omitted it lists every run across the supervisor.
import { statusTool } from '@dougbots/avenor-core'
const status = await statusTool({ runId: 'reverse-string', view: 'lifecycle' })
// { run_id: 'reverse-string', label: 'reverse-string', status: 'running', ... }Status values: running, done, failed, timeout, killed. If a permission request is pending, the result includes pending_permission with available options. The lifecycle view omits final output and diagnostic metadata; full remains the default for compatibility.
resultTool
Waits for one run to finish and returns its complete final output without transcript or event details.
import { resultTool } from '@dougbots/avenor-core'
const result = await resultTool({ runId: 'reverse-string', timeout: '5m' })
// { run_id: 'reverse-string', status: 'done', ready: true, output: '...' }Set wait: false for a non-blocking availability check. A pending permission returns immediately; an expired result wait returns ready: false and timed_out: true without stopping the run.
answerPermissionTool
Answers a pending permission request for a run. If requestId is omitted, it looks up the currently pending request automatically.
import { answerPermissionTool } from '@dougbots/avenor-core'
await answerPermissionTool({ runId: 'my-run', optionId: 'allow-once' })
// { ok: true }followUpTool
Resumes a completed run by spawning a follow-up run on the same session, preserving conversation context.
import { followUpTool } from '@dougbots/avenor-core'
await followUpTool({ runId: 'reverse-string', message: 'Now optimize it for speed' })
// { run_id: '...', label: 'reverse-string-followup' }eventsTool
Reads recent events from a run's on-disk event log. Optionally filters by event type and limits the count (default 50, max 1000).
import { eventsTool } from '@dougbots/avenor-core'
const events = await eventsTool({ runId: 'my-run', types: ['tool_use', 'error'], limit: 20 })⚠️ Only supported through the singleton Supervisor — passing an explicit supervisorId will throw.
shutdownTool
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.
import { shutdownTool } from '@dougbots/avenor-core'
const result = await shutdownTool({ force: false })
// { ok: true, cleaned_up: ['/path/to/sentinel.done', ...] }Quick start
import { Supervisor, resultTool, spawnTool, shutdownTool } from '@dougbots/avenor-core'
// 1. The supervisor auto-starts avenor. Call get() early — it's a singleton.
const sup = await Supervisor.get()
// 2. Spawn a run
const { run_id } = await spawnTool({
agent: 'codex',
prompt: 'Review src/app.ts for security issues',
label: 'security-review',
})
// 3. Wait for the complete final output
const result = await resultTool({ runId: run_id })
console.log(result) // { run_id, label, status: 'done', ready: true, output: '...' }
// 4. Clean up
await shutdownTool()Exports
| Export | Kind | Description |
|---|---|---|
| dial | function | Connect to a control socket, returning a Client |
| Client | class | JSON-RPC client for the avenor daemon |
| Supervisor | class | Singleton avenor process lifecycle manager |
| findAvenorBinary | function | Discover the avenor binary path |
| spawnTool | function | Start a new agent run |
| statusTool | function | Query run lifecycle status (single or all) |
| resultTool | function | Wait for and retrieve one run's complete final output |
| answerPermissionTool | function | Answer a pending permission request |
| followUpTool | function | Resume a session with a follow-up prompt |
| eventsTool | function | Read recent events from a run's event log |
| shutdownTool | function | Stop the avenor daemon and clean up |
