@automatalabs/acp-agents 0.1.1 → 0.2.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 +89 -0
- package/dist/acp-client.d.ts +15 -1
- package/dist/acp-client.d.ts.map +1 -1
- package/dist/acp-client.js +58 -7
- package/dist/events.d.ts +93 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +69 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/pool.d.ts +8 -1
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +4 -1
- package/dist/runner.d.ts +20 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +34 -1
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @automatalabs/acp-agents
|
|
2
|
+
|
|
3
|
+
Low-level building block: the [Agent Client Protocol](https://agentclientprotocol.com) (ACP) client plus Claude and Codex backends that implement the `AgentRunner` seam from `@automatalabs/shared-types`. It spawns `claude-agent-acp` / `codex-acp` as child processes, drives one subagent turn to completion over ACP, and returns structured output or text.
|
|
4
|
+
|
|
5
|
+
This is the layer `@automatalabs/workflows` and `@automatalabs/mcp-server` are built on.
|
|
6
|
+
|
|
7
|
+
## Most users want `@automatalabs/workflows`
|
|
8
|
+
|
|
9
|
+
If you are orchestrating a workflow, use [`@automatalabs/workflows`](../workflows) instead — it re-exports `createAcpRunner` and wires it into the engine for you. Reach for this package directly only when you want to drive a **single** Claude/Codex agent over ACP yourself.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @automatalabs/acp-agents
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Standalone use: drive one agent
|
|
16
|
+
|
|
17
|
+
`createAcpRunner().run(prompt, options)` runs a single agent to completion. Pass a [typebox](https://github.com/sinclairzx81/typebox) `schema` to get a validated object back (typed as `Static<typeof schema>`); omit it to get the final assistant text as a `string`. The backend (Claude vs Codex) is selected from `model` / `tier`. Call `dispose()` when you're done to tear down the pooled child processes.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createAcpRunner } from "@automatalabs/acp-agents";
|
|
21
|
+
import { Type } from "typebox";
|
|
22
|
+
|
|
23
|
+
const runner = createAcpRunner();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Structured output: result is typed and validated against the schema.
|
|
27
|
+
const review = await runner.run("Review the diff and summarize risk.", {
|
|
28
|
+
schema: Type.Object({
|
|
29
|
+
risk: Type.Union([Type.Literal("low"), Type.Literal("high")]),
|
|
30
|
+
summary: Type.String(),
|
|
31
|
+
}),
|
|
32
|
+
model: "anthropic/claude-sonnet-4", // routes to the Claude backend
|
|
33
|
+
cwd: "/abs/path/to/worktree", // ACP session/new { cwd } — absolute
|
|
34
|
+
});
|
|
35
|
+
console.log(review.risk, review.summary);
|
|
36
|
+
|
|
37
|
+
// No schema: result is the final assistant text.
|
|
38
|
+
const text = await runner.run("Explain this repo in one paragraph.", {
|
|
39
|
+
model: "openai/gpt-5", // routes to the Codex backend
|
|
40
|
+
cwd: "/abs/path/to/worktree",
|
|
41
|
+
});
|
|
42
|
+
console.log(text);
|
|
43
|
+
} finally {
|
|
44
|
+
await runner.dispose();
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`run()` accepts the full `RunOptions` seam: `schema`, `model`, `tier`, `cwd`, `instructions`, `label`, `signal` (cancellation), `toolNames` / `disallowedToolNames`, `mcpServers`, `onUsage`, `onModelResolved`, `onModelFallback`, and `onHistory`. See `@automatalabs/shared-types` for the field-by-field contract.
|
|
49
|
+
|
|
50
|
+
## Listening in: live ACP events
|
|
51
|
+
|
|
52
|
+
`AcpAgentRunner` is also a typed event bus — `runner.on(name, listener)` bubbles up the live ACP stream of every run (streaming text, tool calls, usage, permissions). Event names are the ACP `sessionUpdate` discriminants (`agent_message_chunk`, `tool_call`, `usage_update`, …) plus the cross-cutting `session_update` (catch-all), `permission_request`, `raw_message`, `session_open` / `session_close`, and `backend_error`. Each payload carries a `{ sessionId, backendId, label?, runId? }` context envelope (a pooled runner multiplexes many runs at once). `on()` / `once()` return an unsubscribe thunk; `off()` and `removeAllListeners()` round it out. Listeners are best-effort observers — a throwing listener never affects the run.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const off = runner.on("agent_message_chunk", (e) => {
|
|
56
|
+
if (e.content.type === "text") process.stdout.write(e.content.text);
|
|
57
|
+
});
|
|
58
|
+
runner.on("tool_call", (e) => console.error(`[${e.label}] ${e.title}`));
|
|
59
|
+
// … run() …
|
|
60
|
+
off();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The full event map (`AcpRunnerEventMap`) and helpers (`TypedEventEmitter`) are exported here and re-exported from `@automatalabs/workflows`.
|
|
64
|
+
|
|
65
|
+
## Key exports
|
|
66
|
+
|
|
67
|
+
From [`src/index.ts`](./src/index.ts):
|
|
68
|
+
|
|
69
|
+
- **`createAcpRunner(options?)`** — factory returning an `AcpAgentRunner` (this is what `@automatalabs/workflows` injects into the engine).
|
|
70
|
+
- **`AcpAgentRunner`** — the `AgentRunner` implementation; `run(prompt, options)` and `dispose()`.
|
|
71
|
+
- **`selectBackend({ model, tier })`** — the cross-provider routing rule: which backend a spec maps to.
|
|
72
|
+
- **`ClaudeBackend` / `CodexBackend`** — the two backend strategies (spawn config + per-backend schema wiring).
|
|
73
|
+
- **`toJsonSchema(schema)` / `toStrictJsonSchema(schema)`** — turn a typebox schema into the on-the-wire shapes: a plain JSON Schema for Claude `outputFormat`, and an OpenAI-strict-normalized schema for Codex `outputSchema`.
|
|
74
|
+
|
|
75
|
+
Also exported: `AcpAgentPool` / `resolvePoolSize`, `PooledConnection` / `SessionHandle`, `decidePermission`, `UsageAccumulator`, `resolveStructuredOutput` / `extractValidated` / `findJsonBlock` / `validateValue`, `errorText` / `mapThrownError`, and the event surface `TypedEventEmitter` / `AcpRunnerEventMap` / `AcpEventName` / `AcpEventListener` / `AcpEventContext` / `AcpSessionUpdate` (+ the per-event payload types), plus their associated types.
|
|
76
|
+
|
|
77
|
+
## Environment overrides
|
|
78
|
+
|
|
79
|
+
| Variable | Effect |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `AGENTPRISM_DEFAULT_BACKEND` | Backend when `model`/`tier` don't pick one (`codex` selects Codex; anything else is Claude). |
|
|
82
|
+
| `AGENTPRISM_ACP_POOL_SIZE` | Long-lived processes to keep per backend (default `1`). |
|
|
83
|
+
| `AGENTPRISM_CLAUDE_ACP_CMD` / `AGENTPRISM_CLAUDE_ACP_ARGS` | Override the command (and args) used to spawn the Claude ACP server. |
|
|
84
|
+
| `AGENTPRISM_CODEX_ACP_CMD` / `AGENTPRISM_CODEX_ACP_ARGS` | Override the command (and args) used to spawn the Codex ACP server. |
|
|
85
|
+
| `AGENTPRISM_CODEX_ACP_BIN` | Override only the resolved Codex ACP bin path (keeps the default node launcher). |
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
Apache-2.0
|
package/dist/acp-client.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ClientSideConnection, type PromptResponse, type SessionConfigOption, ty
|
|
|
2
2
|
import type { TSchema } from "typebox";
|
|
3
3
|
import { type AgentHistoryEntry, type McpServerConfig } from "@automatalabs/shared-types";
|
|
4
4
|
import type { Backend, BackendId, StructuredSource } from "./backend.js";
|
|
5
|
+
import { type AcpEventSink } from "./events.js";
|
|
5
6
|
import { type ToolPolicy } from "./permissions.js";
|
|
6
7
|
import { UsageAccumulator } from "./usage.js";
|
|
7
8
|
interface RawResultSuccess {
|
|
@@ -13,12 +14,16 @@ interface RawResultSuccess {
|
|
|
13
14
|
* and the tool policy used to auto-answer permission requests for THIS session. */
|
|
14
15
|
declare class SessionState {
|
|
15
16
|
readonly policy: ToolPolicy;
|
|
17
|
+
readonly label?: string | undefined;
|
|
18
|
+
readonly runId?: string | undefined;
|
|
16
19
|
readonly textChunks: string[];
|
|
17
20
|
readonly history: AgentHistoryEntry[];
|
|
18
21
|
readonly usage: UsageAccumulator;
|
|
19
22
|
rawResultSuccess: RawResultSuccess | undefined;
|
|
20
23
|
private turnStartIndex;
|
|
21
|
-
|
|
24
|
+
/** `label`/`runId` are carried here ONLY so the MultiplexClient can stamp them onto emitted
|
|
25
|
+
* events as context — they never affect routing or the wire request. */
|
|
26
|
+
constructor(policy: ToolPolicy, label?: string | undefined, runId?: string | undefined);
|
|
22
27
|
/** Mark the start of a new turn so currentTurnText()/structured_output read only this turn. */
|
|
23
28
|
beginTurn(): void;
|
|
24
29
|
currentTurnText(): string;
|
|
@@ -37,10 +42,16 @@ export interface AcpSessionOptions {
|
|
|
37
42
|
/** Engine run id, stamped onto session/new `_meta` (META_KEYS.runId) as a correlation id.
|
|
38
43
|
* Omitted => no runId `_meta` is stamped (the request `_meta` is whatever the backend set). */
|
|
39
44
|
runId?: string;
|
|
45
|
+
/** `RunOptions.label`, propagated onto emitted events as context. NOT sent on the wire. */
|
|
46
|
+
label?: string;
|
|
40
47
|
}
|
|
41
48
|
/** Notified by a PooledConnection when its process dies, so the pool can drop it. */
|
|
42
49
|
export interface PooledConnectionDeps {
|
|
43
50
|
onDead(connection: PooledConnection): void;
|
|
51
|
+
/** Optional typed event sink. When present, every ACP notification / permission request /
|
|
52
|
+
* session lifecycle change on this connection is bubbled up through it (additive observability;
|
|
53
|
+
* it is invoked AFTER the drain accumulation and never affects the run). */
|
|
54
|
+
onEvent?: AcpEventSink;
|
|
44
55
|
}
|
|
45
56
|
/**
|
|
46
57
|
* One long-lived ACP server subprocess + its held ACP client connection. Initialized ONCE and
|
|
@@ -55,6 +66,9 @@ export declare class PooledConnection {
|
|
|
55
66
|
private readonly child;
|
|
56
67
|
private readonly client;
|
|
57
68
|
private readonly onDead;
|
|
69
|
+
private readonly onEvent;
|
|
70
|
+
/** Set true at the start of dispose() so the graceful-shutdown death is NOT reported as a crash. */
|
|
71
|
+
private disposing;
|
|
58
72
|
/** Resolves once `initialize` completed (or rejects if the process died first). */
|
|
59
73
|
private readonly ready;
|
|
60
74
|
/** Resolves when the process dies; `race()` turns it into a thrown, descriptive error. */
|
package/dist/acp-client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"acp-client.d.ts","sourceRoot":"","sources":["../src/acp-client.ts"],"names":[],"mappings":"AAwBA,OAAO,EACL,oBAAoB,EAOpB,KAAK,cAAc,EAGnB,KAAK,mBAAmB,EAGxB,KAAK,mBAAmB,EACzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAa,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AACrG,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAiB9C,UAAU,gBAAgB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;oFACoF;AACpF,cAAM,YAAY;
|
|
1
|
+
{"version":3,"file":"acp-client.d.ts","sourceRoot":"","sources":["../src/acp-client.ts"],"names":[],"mappings":"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"}
|
package/dist/acp-client.js
CHANGED
|
@@ -24,6 +24,7 @@ import { spawn } from "node:child_process";
|
|
|
24
24
|
import { Readable, Writable } from "node:stream";
|
|
25
25
|
import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
|
|
26
26
|
import { META_KEYS } from "@automatalabs/shared-types";
|
|
27
|
+
import { emitSessionUpdate } from "./events.js";
|
|
27
28
|
import { decidePermission } from "./permissions.js";
|
|
28
29
|
import { UsageAccumulator } from "./usage.js";
|
|
29
30
|
/** A benign client identity. NOT JetBrains/IntelliJ 2026.1 — that exact identity makes
|
|
@@ -42,13 +43,19 @@ const DISPOSE_SIGKILL_GRACE_MS = 2_000;
|
|
|
42
43
|
* and the tool policy used to auto-answer permission requests for THIS session. */
|
|
43
44
|
class SessionState {
|
|
44
45
|
policy;
|
|
46
|
+
label;
|
|
47
|
+
runId;
|
|
45
48
|
textChunks = [];
|
|
46
49
|
history = [];
|
|
47
50
|
usage = new UsageAccumulator();
|
|
48
51
|
rawResultSuccess;
|
|
49
52
|
turnStartIndex = 0;
|
|
50
|
-
|
|
53
|
+
/** `label`/`runId` are carried here ONLY so the MultiplexClient can stamp them onto emitted
|
|
54
|
+
* events as context — they never affect routing or the wire request. */
|
|
55
|
+
constructor(policy, label, runId) {
|
|
51
56
|
this.policy = policy;
|
|
57
|
+
this.label = label;
|
|
58
|
+
this.runId = runId;
|
|
52
59
|
}
|
|
53
60
|
/** Mark the start of a new turn so currentTurnText()/structured_output read only this turn. */
|
|
54
61
|
beginTurn() {
|
|
@@ -103,22 +110,48 @@ class SessionState {
|
|
|
103
110
|
* permission request to the per-session SessionState by `sessionId`, so one process can serve
|
|
104
111
|
* many concurrent sessions without their streams crossing. */
|
|
105
112
|
class MultiplexClient {
|
|
113
|
+
backendId;
|
|
114
|
+
onEvent;
|
|
106
115
|
sessions = new Map();
|
|
116
|
+
/** `backendId` stamps event context; `onEvent` (optional) bubbles every notification, permission
|
|
117
|
+
* request and session lifecycle change up to the runner's typed bus. */
|
|
118
|
+
constructor(backendId, onEvent) {
|
|
119
|
+
this.backendId = backendId;
|
|
120
|
+
this.onEvent = onEvent;
|
|
121
|
+
}
|
|
122
|
+
contextFor(sessionId, state) {
|
|
123
|
+
return { sessionId, backendId: this.backendId, label: state?.label, runId: state?.runId };
|
|
124
|
+
}
|
|
107
125
|
register(sessionId, state) {
|
|
108
126
|
this.sessions.set(sessionId, state);
|
|
127
|
+
this.onEvent?.("session_open", this.contextFor(sessionId, state));
|
|
109
128
|
}
|
|
110
129
|
unregister(sessionId) {
|
|
130
|
+
const state = this.sessions.get(sessionId);
|
|
111
131
|
this.sessions.delete(sessionId);
|
|
132
|
+
if (state)
|
|
133
|
+
this.onEvent?.("session_close", this.contextFor(sessionId, state));
|
|
112
134
|
}
|
|
113
135
|
requestPermission(params) {
|
|
114
136
|
const state = this.sessions.get(params.sessionId);
|
|
115
137
|
// Unknown/closed session: refuse rather than silently allow a tool we can't attribute.
|
|
116
138
|
if (!state)
|
|
117
139
|
return { outcome: { outcome: "cancelled" } };
|
|
118
|
-
|
|
140
|
+
const outcome = decidePermission(params, state.policy);
|
|
141
|
+
this.onEvent?.("permission_request", {
|
|
142
|
+
...this.contextFor(params.sessionId, state),
|
|
143
|
+
request: params,
|
|
144
|
+
outcome,
|
|
145
|
+
});
|
|
146
|
+
return outcome;
|
|
119
147
|
}
|
|
120
148
|
sessionUpdate(params) {
|
|
121
|
-
this.sessions.get(params.sessionId)
|
|
149
|
+
const state = this.sessions.get(params.sessionId);
|
|
150
|
+
// Fold into the accumulator FIRST (the drain contract), THEN bubble the event up unchanged.
|
|
151
|
+
state?.applyUpdate(params.update);
|
|
152
|
+
if (this.onEvent) {
|
|
153
|
+
emitSessionUpdate(this.onEvent, params.update, this.contextFor(params.sessionId, state));
|
|
154
|
+
}
|
|
122
155
|
}
|
|
123
156
|
extNotification(method, params) {
|
|
124
157
|
if (method !== CLAUDE_RAW_MESSAGE_METHOD)
|
|
@@ -128,8 +161,14 @@ class MultiplexClient {
|
|
|
128
161
|
const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
|
|
129
162
|
if (!sessionId)
|
|
130
163
|
return;
|
|
131
|
-
const
|
|
132
|
-
this.sessions.get(sessionId)
|
|
164
|
+
const rawMessage = params.message;
|
|
165
|
+
const state = this.sessions.get(sessionId);
|
|
166
|
+
state?.applyRawMessage(rawMessage);
|
|
167
|
+
this.onEvent?.("raw_message", {
|
|
168
|
+
...this.contextFor(sessionId, state),
|
|
169
|
+
method,
|
|
170
|
+
message: rawMessage,
|
|
171
|
+
});
|
|
133
172
|
}
|
|
134
173
|
}
|
|
135
174
|
/** Merge the engine runId correlation stamp into a backend's session/new `_meta`. Returns the
|
|
@@ -162,8 +201,11 @@ export class PooledConnection {
|
|
|
162
201
|
rpc;
|
|
163
202
|
backend;
|
|
164
203
|
child;
|
|
165
|
-
client
|
|
204
|
+
client;
|
|
166
205
|
onDead;
|
|
206
|
+
onEvent;
|
|
207
|
+
/** Set true at the start of dispose() so the graceful-shutdown death is NOT reported as a crash. */
|
|
208
|
+
disposing = false;
|
|
167
209
|
/** Resolves once `initialize` completed (or rejects if the process died first). */
|
|
168
210
|
ready;
|
|
169
211
|
/** Resolves when the process dies; `race()` turns it into a thrown, descriptive error. */
|
|
@@ -178,6 +220,8 @@ export class PooledConnection {
|
|
|
178
220
|
this.backend = backend;
|
|
179
221
|
this.backendId = backend.id;
|
|
180
222
|
this.onDead = deps.onDead;
|
|
223
|
+
this.onEvent = deps.onEvent;
|
|
224
|
+
this.client = new MultiplexClient(this.backendId, this.onEvent);
|
|
181
225
|
const { command, args, env } = backend.spawnConfig();
|
|
182
226
|
// NOTE: deliberately NO `cwd` here. cwd is per-SESSION (session/new), so one pooled process
|
|
183
227
|
// serves runs in different worktrees without losing isolation.
|
|
@@ -232,6 +276,11 @@ export class PooledConnection {
|
|
|
232
276
|
this._alive = false;
|
|
233
277
|
this.deathError = error;
|
|
234
278
|
this.resolveDead();
|
|
279
|
+
// A crash (not a graceful dispose) is worth surfacing for observability; the engine still
|
|
280
|
+
// handles it by retrying the run on a fresh process. Best-effort, after death is recorded.
|
|
281
|
+
if (this.onEvent && !this.disposing) {
|
|
282
|
+
this.onEvent("backend_error", { backendId: this.backendId, error });
|
|
283
|
+
}
|
|
235
284
|
this.onDead(this);
|
|
236
285
|
}
|
|
237
286
|
stderrSuffix() {
|
|
@@ -266,7 +315,7 @@ export class PooledConnection {
|
|
|
266
315
|
this._activeSessions += 1;
|
|
267
316
|
try {
|
|
268
317
|
await this.ready;
|
|
269
|
-
const state = new SessionState(opts.policy);
|
|
318
|
+
const state = new SessionState(opts.policy, opts.label, opts.runId);
|
|
270
319
|
// The backend's vendor `_meta` (Claude schema channel; undefined for Codex) plus the
|
|
271
320
|
// optional engine runId correlation stamp. When neither is present, no `_meta` is sent.
|
|
272
321
|
const meta = stampRunId(this.backend.sessionMeta(opts.schema), opts.runId);
|
|
@@ -329,6 +378,8 @@ export class PooledConnection {
|
|
|
329
378
|
async dispose() {
|
|
330
379
|
if (!this._alive)
|
|
331
380
|
return;
|
|
381
|
+
// Mark graceful shutdown so the imminent process-exit `die()` does not emit `backend_error`.
|
|
382
|
+
this.disposing = true;
|
|
332
383
|
const exited = new Promise((resolve) => {
|
|
333
384
|
this.child.once("exit", () => resolve());
|
|
334
385
|
});
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { RequestPermissionRequest, RequestPermissionResponse, SessionNotification } from "@agentclientprotocol/sdk";
|
|
2
|
+
import type { BackendId } from "./backend.js";
|
|
3
|
+
/** The ACP session/update discriminated union (every real-time update an agent can stream). */
|
|
4
|
+
export type AcpSessionUpdate = SessionNotification["update"];
|
|
5
|
+
/** The `sessionUpdate` discriminant strings (agent_message_chunk | tool_call | usage_update | …). */
|
|
6
|
+
export type AcpUpdateKind = AcpSessionUpdate["sessionUpdate"];
|
|
7
|
+
/** Which run / session / backend an event belongs to. A pooled runner multiplexes many concurrent
|
|
8
|
+
* sessions over one process, so every event carries this envelope for disambiguation/filtering. */
|
|
9
|
+
export interface AcpEventContext {
|
|
10
|
+
/** ACP session id this event pertains to. */
|
|
11
|
+
sessionId: string;
|
|
12
|
+
/** Backend that produced it ("claude" | "codex"). */
|
|
13
|
+
backendId: BackendId;
|
|
14
|
+
/** `RunOptions.label` of the originating run(), if one was set. */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** `RunOptions.runId` correlation id, if one was set. */
|
|
17
|
+
runId?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Per-discriminant events: key = ACP `sessionUpdate` string, payload = that variant + context. */
|
|
20
|
+
type AcpSessionUpdateEvents = {
|
|
21
|
+
[K in AcpUpdateKind]: Extract<AcpSessionUpdate, {
|
|
22
|
+
sessionUpdate: K;
|
|
23
|
+
}> & AcpEventContext;
|
|
24
|
+
};
|
|
25
|
+
/** A tool-permission request the runner auto-answered, paired with the decision it returned. */
|
|
26
|
+
export interface AcpPermissionEvent extends AcpEventContext {
|
|
27
|
+
request: RequestPermissionRequest;
|
|
28
|
+
outcome: RequestPermissionResponse;
|
|
29
|
+
}
|
|
30
|
+
/** A vendor extension notification (e.g. Claude `_claude/sdkMessage`) routed to a session. */
|
|
31
|
+
export interface AcpRawMessageEvent extends AcpEventContext {
|
|
32
|
+
method: string;
|
|
33
|
+
message: unknown;
|
|
34
|
+
}
|
|
35
|
+
/** A pooled backend process crashed (not a graceful dispose). The engine retries the run on a
|
|
36
|
+
* fresh process; this surfaces the crash for observability. Carries no session context. */
|
|
37
|
+
export interface AcpBackendErrorEvent {
|
|
38
|
+
backendId: BackendId;
|
|
39
|
+
error: Error;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The full typed event map for AcpAgentRunner — every ACP `session/update` kind, plus the
|
|
43
|
+
* cross-cutting events. The keys are exactly the strings you pass to `runner.on(...)`, and the
|
|
44
|
+
* value is the payload your listener receives.
|
|
45
|
+
*/
|
|
46
|
+
export type AcpRunnerEventMap = AcpSessionUpdateEvents & {
|
|
47
|
+
/** Catch-all: fires for EVERY session/update regardless of kind (carries the raw update). */
|
|
48
|
+
session_update: {
|
|
49
|
+
update: AcpSessionUpdate;
|
|
50
|
+
} & AcpEventContext;
|
|
51
|
+
/** A permission request the runner auto-answered, with the decision returned. */
|
|
52
|
+
permission_request: AcpPermissionEvent;
|
|
53
|
+
/** A vendor extension notification arrived for a session. */
|
|
54
|
+
raw_message: AcpRawMessageEvent;
|
|
55
|
+
/** A new session was opened on a pooled connection. */
|
|
56
|
+
session_open: AcpEventContext;
|
|
57
|
+
/** A session was released / closed. */
|
|
58
|
+
session_close: AcpEventContext;
|
|
59
|
+
/** A pooled backend process crashed (not a graceful dispose). */
|
|
60
|
+
backend_error: AcpBackendErrorEvent;
|
|
61
|
+
};
|
|
62
|
+
export type AcpEventName = keyof AcpRunnerEventMap;
|
|
63
|
+
export type AcpEventListener<K extends AcpEventName> = (event: AcpRunnerEventMap[K]) => void;
|
|
64
|
+
/** Internal emit boundary handed from the runner down through the pool to each connection. */
|
|
65
|
+
export interface AcpEventSink {
|
|
66
|
+
<K extends AcpEventName>(name: K, event: AcpRunnerEventMap[K]): void;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A tiny strongly-typed event emitter (no node:events, zero deps). `on()`/`once()` return an
|
|
70
|
+
* unsubscribe thunk. `emit()` ISOLATES listener exceptions — one bad listener can never break the
|
|
71
|
+
* run, the synchronous drain, or sibling listeners — mirroring the best-effort contract of
|
|
72
|
+
* onUsage/onHistory. Generic over any event map `{ name: payload }`.
|
|
73
|
+
*/
|
|
74
|
+
export declare class TypedEventEmitter<EventMap> {
|
|
75
|
+
private readonly listeners;
|
|
76
|
+
/** Subscribe to `name`. Returns an unsubscribe thunk (calling it is equivalent to `off`). */
|
|
77
|
+
on<K extends keyof EventMap>(name: K, listener: (event: EventMap[K]) => void): () => void;
|
|
78
|
+
/** Subscribe once: the listener auto-unsubscribes after its first delivery. */
|
|
79
|
+
once<K extends keyof EventMap>(name: K, listener: (event: EventMap[K]) => void): () => void;
|
|
80
|
+
off<K extends keyof EventMap>(name: K, listener: (event: EventMap[K]) => void): void;
|
|
81
|
+
removeAllListeners(name?: keyof EventMap): void;
|
|
82
|
+
listenerCount(name: keyof EventMap): number;
|
|
83
|
+
emit<K extends keyof EventMap>(name: K, event: EventMap[K]): void;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fan ONE ACP session/update out to the typed bus: the `session_update` catch-all first, then the
|
|
87
|
+
* per-discriminant event. The payload IS the update variant merged with `ctx`, but TS cannot
|
|
88
|
+
* correlate the runtime discriminant `name` with the mapped payload type at the call site, so the
|
|
89
|
+
* (name, payload) pair is asserted once here against the precise indexed type — never `any`.
|
|
90
|
+
*/
|
|
91
|
+
export declare function emitSessionUpdate(emit: AcpEventSink, update: AcpSessionUpdate, ctx: AcpEventContext): void;
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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"}
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny strongly-typed event emitter (no node:events, zero deps). `on()`/`once()` return an
|
|
3
|
+
* unsubscribe thunk. `emit()` ISOLATES listener exceptions — one bad listener can never break the
|
|
4
|
+
* run, the synchronous drain, or sibling listeners — mirroring the best-effort contract of
|
|
5
|
+
* onUsage/onHistory. Generic over any event map `{ name: payload }`.
|
|
6
|
+
*/
|
|
7
|
+
export class TypedEventEmitter {
|
|
8
|
+
listeners = new Map();
|
|
9
|
+
/** Subscribe to `name`. Returns an unsubscribe thunk (calling it is equivalent to `off`). */
|
|
10
|
+
on(name, listener) {
|
|
11
|
+
let set = this.listeners.get(name);
|
|
12
|
+
if (!set) {
|
|
13
|
+
set = new Set();
|
|
14
|
+
this.listeners.set(name, set);
|
|
15
|
+
}
|
|
16
|
+
set.add(listener);
|
|
17
|
+
return () => this.off(name, listener);
|
|
18
|
+
}
|
|
19
|
+
/** Subscribe once: the listener auto-unsubscribes after its first delivery. */
|
|
20
|
+
once(name, listener) {
|
|
21
|
+
const off = this.on(name, (event) => {
|
|
22
|
+
off();
|
|
23
|
+
listener(event);
|
|
24
|
+
});
|
|
25
|
+
return off;
|
|
26
|
+
}
|
|
27
|
+
off(name, listener) {
|
|
28
|
+
const set = this.listeners.get(name);
|
|
29
|
+
if (!set)
|
|
30
|
+
return;
|
|
31
|
+
set.delete(listener);
|
|
32
|
+
if (set.size === 0)
|
|
33
|
+
this.listeners.delete(name);
|
|
34
|
+
}
|
|
35
|
+
removeAllListeners(name) {
|
|
36
|
+
if (name === undefined)
|
|
37
|
+
this.listeners.clear();
|
|
38
|
+
else
|
|
39
|
+
this.listeners.delete(name);
|
|
40
|
+
}
|
|
41
|
+
listenerCount(name) {
|
|
42
|
+
return this.listeners.get(name)?.size ?? 0;
|
|
43
|
+
}
|
|
44
|
+
emit(name, event) {
|
|
45
|
+
const set = this.listeners.get(name);
|
|
46
|
+
if (!set || set.size === 0)
|
|
47
|
+
return;
|
|
48
|
+
// Snapshot so a listener that (un)subscribes during dispatch can't perturb this emit.
|
|
49
|
+
for (const listener of [...set]) {
|
|
50
|
+
try {
|
|
51
|
+
listener(event);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Listeners are observers — never let one break the run or sibling listeners.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Fan ONE ACP session/update out to the typed bus: the `session_update` catch-all first, then the
|
|
61
|
+
* per-discriminant event. The payload IS the update variant merged with `ctx`, but TS cannot
|
|
62
|
+
* correlate the runtime discriminant `name` with the mapped payload type at the call site, so the
|
|
63
|
+
* (name, payload) pair is asserted once here against the precise indexed type — never `any`.
|
|
64
|
+
*/
|
|
65
|
+
export function emitSessionUpdate(emit, update, ctx) {
|
|
66
|
+
emit("session_update", { update, ...ctx });
|
|
67
|
+
const name = update.sessionUpdate;
|
|
68
|
+
emit(name, { ...update, ...ctx });
|
|
69
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ export { AcpAgentRunner, createAcpRunner, selectBackend } from "./runner.js";
|
|
|
2
2
|
export { PooledConnection, SessionHandle } from "./acp-client.js";
|
|
3
3
|
export type { AcpSessionOptions, PooledConnectionDeps } from "./acp-client.js";
|
|
4
4
|
export { AcpAgentPool, resolvePoolSize } from "./pool.js";
|
|
5
|
-
export type { AcpPoolOptions } from "./pool.js";
|
|
5
|
+
export type { AcpPoolOptions, AcpPoolDeps } from "./pool.js";
|
|
6
|
+
export { TypedEventEmitter, emitSessionUpdate } from "./events.js";
|
|
7
|
+
export type { AcpRunnerEventMap, AcpEventName, AcpEventListener, AcpEventContext, AcpEventSink, AcpSessionUpdate, AcpUpdateKind, AcpPermissionEvent, AcpRawMessageEvent, AcpBackendErrorEvent, } from "./events.js";
|
|
6
8
|
export type { Backend, BackendId, SpawnConfig, StructuredSource } from "./backend.js";
|
|
7
9
|
export { ClaudeBackend } from "./backends/claude.js";
|
|
8
10
|
export { CodexBackend } from "./backends/codex.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE7E,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC1D,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE7E,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC1D,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAG7D,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAEtE,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,uBAAuB,EACvB,aAAa,GACd,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
export { AcpAgentRunner, createAcpRunner, selectBackend } from "./runner.js";
|
|
8
8
|
export { PooledConnection, SessionHandle } from "./acp-client.js";
|
|
9
9
|
export { AcpAgentPool, resolvePoolSize } from "./pool.js";
|
|
10
|
+
// The typed ACP event bus surfaced on AcpAgentRunner (`runner.on(name, evt => …)`).
|
|
11
|
+
export { TypedEventEmitter, emitSessionUpdate } from "./events.js";
|
|
10
12
|
export { ClaudeBackend } from "./backends/claude.js";
|
|
11
13
|
export { CodexBackend } from "./backends/codex.js";
|
|
12
14
|
export { decidePermission } from "./permissions.js";
|
package/dist/pool.d.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import type { Backend } from "./backend.js";
|
|
2
2
|
import { SessionHandle, type AcpSessionOptions } from "./acp-client.js";
|
|
3
|
+
import type { AcpEventSink } from "./events.js";
|
|
3
4
|
export interface AcpPoolOptions {
|
|
4
5
|
/** Long-lived processes to keep PER backend. Default 1; falls back to AGENTPRISM_ACP_POOL_SIZE. */
|
|
5
6
|
size?: number;
|
|
6
7
|
}
|
|
8
|
+
/** Internal wiring the runner injects (NOT part of the public AcpPoolOptions surface): the typed
|
|
9
|
+
* event sink forwarded to every PooledConnection so ACP events bubble up to `runner.on(...)`. */
|
|
10
|
+
export interface AcpPoolDeps {
|
|
11
|
+
onEvent?: AcpEventSink;
|
|
12
|
+
}
|
|
7
13
|
/** Resolve the per-backend pool size: explicit option wins, else env, else 1. Clamped to >= 1. */
|
|
8
14
|
export declare function resolvePoolSize(option?: number): number;
|
|
9
15
|
export declare class AcpAgentPool {
|
|
16
|
+
private readonly deps;
|
|
10
17
|
private readonly size;
|
|
11
18
|
private readonly byBackend;
|
|
12
19
|
private readonly onProcessExit;
|
|
13
20
|
private exitHookInstalled;
|
|
14
21
|
private disposed;
|
|
15
|
-
constructor(options?: AcpPoolOptions);
|
|
22
|
+
constructor(options?: AcpPoolOptions, deps?: AcpPoolDeps);
|
|
16
23
|
/** Acquire a session for one agent() run: get/grow a pooled connection and open a session. */
|
|
17
24
|
acquire(backend: Backend, opts: AcpSessionOptions): Promise<SessionHandle>;
|
|
18
25
|
/**
|
package/dist/pool.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,cAAc,CAAC;AACvD,OAAO,EAAoB,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,cAAc,CAAC;AACvD,OAAO,EAAoB,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,MAAM,WAAW,cAAc;IAC7B,mGAAmG;IACnG,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;kGACkG;AAClG,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AAED,kGAAkG;AAClG,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAUvD;AAED,qBAAa,YAAY;IASrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IARvB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4C;IACtE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAC1D,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAS;gBAGvB,OAAO,GAAE,cAAmB,EACX,IAAI,GAAE,WAAgB;IAKzC,8FAA8F;IACxF,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC;IAMhF;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,cAAc;IAStB,+DAA+D;IAC/D,OAAO,CAAC,IAAI;IAOZ,iEAAiE;IAC3D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ9B,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,cAAc;IAMtB,gGAAgG;IAChG,OAAO,CAAC,WAAW;CAGpB"}
|
package/dist/pool.js
CHANGED
|
@@ -15,12 +15,14 @@ export function resolvePoolSize(option) {
|
|
|
15
15
|
return DEFAULT_POOL_SIZE;
|
|
16
16
|
}
|
|
17
17
|
export class AcpAgentPool {
|
|
18
|
+
deps;
|
|
18
19
|
size;
|
|
19
20
|
byBackend = new Map();
|
|
20
21
|
onProcessExit = () => this.killAllSync();
|
|
21
22
|
exitHookInstalled = false;
|
|
22
23
|
disposed = false;
|
|
23
|
-
constructor(options = {}) {
|
|
24
|
+
constructor(options = {}, deps = {}) {
|
|
25
|
+
this.deps = deps;
|
|
24
26
|
this.size = resolvePoolSize(options.size);
|
|
25
27
|
}
|
|
26
28
|
/** Acquire a session for one agent() run: get/grow a pooled connection and open a session. */
|
|
@@ -45,6 +47,7 @@ export class AcpAgentPool {
|
|
|
45
47
|
this.installExitHook();
|
|
46
48
|
const connection = PooledConnection.create(backend, {
|
|
47
49
|
onDead: (dead) => this.drop(backend.id, dead),
|
|
50
|
+
onEvent: this.deps.onEvent,
|
|
48
51
|
});
|
|
49
52
|
connections.push(connection);
|
|
50
53
|
return connection;
|
package/dist/runner.d.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import { type AgentResult, type AgentRunner, type RunOptions } from "@automatalabs/shared-types";
|
|
2
2
|
import type { TSchema } from "typebox";
|
|
3
3
|
import { type AcpPoolOptions } from "./pool.js";
|
|
4
|
+
import { type AcpEventListener, type AcpEventName } from "./events.js";
|
|
4
5
|
import type { Backend } from "./backend.js";
|
|
5
6
|
export declare class AcpAgentRunner implements AgentRunner {
|
|
6
7
|
private readonly pool;
|
|
8
|
+
/** Typed bus carrying every ACP event from every pooled session. Beyond the AgentRunner seam
|
|
9
|
+
* (additive observability) — subscribing never affects a run and never enters the resume hash. */
|
|
10
|
+
private readonly events;
|
|
11
|
+
private readonly emitEvent;
|
|
7
12
|
constructor(options?: AcpPoolOptions);
|
|
13
|
+
/**
|
|
14
|
+
* Listen in on the live ACP stream. `name` is an ACP `sessionUpdate` discriminant
|
|
15
|
+
* ("agent_message_chunk", "tool_call", "usage_update", …) or one of the cross-cutting events
|
|
16
|
+
* ("session_update" catch-all, "permission_request", "raw_message", "session_open",
|
|
17
|
+
* "session_close", "backend_error"). The listener is typed to the event. Returns an unsubscribe
|
|
18
|
+
* thunk. A pooled runner multiplexes many concurrent runs, so each event carries
|
|
19
|
+
* `{ sessionId, backendId, label?, runId? }` for filtering. Listeners are best-effort observers:
|
|
20
|
+
* a throwing listener is isolated and never affects the run.
|
|
21
|
+
*/
|
|
22
|
+
on<K extends AcpEventName>(name: K, listener: AcpEventListener<K>): () => void;
|
|
23
|
+
/** Subscribe once; the listener auto-unsubscribes after its first delivery. */
|
|
24
|
+
once<K extends AcpEventName>(name: K, listener: AcpEventListener<K>): () => void;
|
|
25
|
+
off<K extends AcpEventName>(name: K, listener: AcpEventListener<K>): void;
|
|
26
|
+
removeAllListeners(name?: AcpEventName): void;
|
|
27
|
+
listenerCount(name: AcpEventName): number;
|
|
8
28
|
run<S extends TSchema | undefined = undefined>(prompt: string, options?: RunOptions<S>): Promise<AgentResult<S>>;
|
|
9
29
|
/** Tear down the whole pool (close every long-lived process). Call when the run ends / the
|
|
10
30
|
* runner is disposed. Beyond the AgentRunner seam (additive) — never enters the resume hash. */
|
package/dist/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAkBA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAS5C,qBAAa,cAAe,YAAW,WAAW;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAkBA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAGlB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAS5C,qBAAa,cAAe,YAAW,WAAW;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC;uGACmG;IACnG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8C;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgE;gBAE9E,OAAO,GAAE,cAAmB;IAIxC;;;;;;;;OAQG;IACH,EAAE,CAAC,CAAC,SAAS,YAAY,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAI9E,+EAA+E;IAC/E,IAAI,CAAC,CAAC,SAAS,YAAY,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAIhF,GAAG,CAAC,CAAC,SAAS,YAAY,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI;IAIzE,kBAAkB,CAAC,IAAI,CAAC,EAAE,YAAY,GAAG,IAAI;IAI7C,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM;IAInC,GAAG,CAAC,CAAC,SAAS,OAAO,GAAG,SAAS,GAAG,SAAS,EACjD,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,UAAU,CAAC,CAAC,CAAM,GAC1B,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAmF1B;qGACiG;IAC3F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAI/B;AAED;;qEAEqE;AACrE,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,cAAc,CAExE;AAoED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAG9E"}
|
package/dist/runner.js
CHANGED
|
@@ -18,14 +18,44 @@
|
|
|
18
18
|
// and re-throw on abort, but never implement our own timeout.
|
|
19
19
|
import { WorkflowError, WorkflowErrorCode, } from "@automatalabs/shared-types";
|
|
20
20
|
import { AcpAgentPool } from "./pool.js";
|
|
21
|
+
import { TypedEventEmitter, } from "./events.js";
|
|
21
22
|
import { ClaudeBackend } from "./backends/claude.js";
|
|
22
23
|
import { CodexBackend } from "./backends/codex.js";
|
|
23
24
|
import { mapThrownError } from "./errors-map.js";
|
|
24
25
|
import { resolveStructuredOutput } from "./structured-output.js";
|
|
25
26
|
export class AcpAgentRunner {
|
|
26
27
|
pool;
|
|
28
|
+
/** Typed bus carrying every ACP event from every pooled session. Beyond the AgentRunner seam
|
|
29
|
+
* (additive observability) — subscribing never affects a run and never enters the resume hash. */
|
|
30
|
+
events = new TypedEventEmitter();
|
|
31
|
+
emitEvent = (name, event) => this.events.emit(name, event);
|
|
27
32
|
constructor(options = {}) {
|
|
28
|
-
this.pool = new AcpAgentPool(options);
|
|
33
|
+
this.pool = new AcpAgentPool(options, { onEvent: this.emitEvent });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Listen in on the live ACP stream. `name` is an ACP `sessionUpdate` discriminant
|
|
37
|
+
* ("agent_message_chunk", "tool_call", "usage_update", …) or one of the cross-cutting events
|
|
38
|
+
* ("session_update" catch-all, "permission_request", "raw_message", "session_open",
|
|
39
|
+
* "session_close", "backend_error"). The listener is typed to the event. Returns an unsubscribe
|
|
40
|
+
* thunk. A pooled runner multiplexes many concurrent runs, so each event carries
|
|
41
|
+
* `{ sessionId, backendId, label?, runId? }` for filtering. Listeners are best-effort observers:
|
|
42
|
+
* a throwing listener is isolated and never affects the run.
|
|
43
|
+
*/
|
|
44
|
+
on(name, listener) {
|
|
45
|
+
return this.events.on(name, listener);
|
|
46
|
+
}
|
|
47
|
+
/** Subscribe once; the listener auto-unsubscribes after its first delivery. */
|
|
48
|
+
once(name, listener) {
|
|
49
|
+
return this.events.once(name, listener);
|
|
50
|
+
}
|
|
51
|
+
off(name, listener) {
|
|
52
|
+
this.events.off(name, listener);
|
|
53
|
+
}
|
|
54
|
+
removeAllListeners(name) {
|
|
55
|
+
this.events.removeAllListeners(name);
|
|
56
|
+
}
|
|
57
|
+
listenerCount(name) {
|
|
58
|
+
return this.events.listenerCount(name);
|
|
29
59
|
}
|
|
30
60
|
async run(prompt, options = {}) {
|
|
31
61
|
const opts = options;
|
|
@@ -41,6 +71,8 @@ export class AcpAgentRunner {
|
|
|
41
71
|
mcpServers: opts.mcpServers,
|
|
42
72
|
// Engine correlation id -> session/new _meta (META_KEYS.runId). Additive; never hashed.
|
|
43
73
|
runId: opts.runId,
|
|
74
|
+
// Stamped onto emitted ACP events as context (never sent on the wire).
|
|
75
|
+
label: opts.label,
|
|
44
76
|
});
|
|
45
77
|
try {
|
|
46
78
|
opts.signal?.throwIfAborted();
|
|
@@ -113,6 +145,7 @@ export class AcpAgentRunner {
|
|
|
113
145
|
* runner is disposed. Beyond the AgentRunner seam (additive) — never enters the resume hash. */
|
|
114
146
|
async dispose() {
|
|
115
147
|
await this.pool.dispose();
|
|
148
|
+
this.events.removeAllListeners();
|
|
116
149
|
}
|
|
117
150
|
}
|
|
118
151
|
/** Factory the mcp-server composition root calls to inject the runner into the engine. The pool
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automatalabs/acp-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@agentclientprotocol/claude-agent-acp": "0.53.0",
|
|
29
29
|
"@automatalabs/codex-acp": "1.0.2",
|
|
30
30
|
"typebox": "1.3.2",
|
|
31
|
-
"@automatalabs/shared-types": "0.1.
|
|
31
|
+
"@automatalabs/shared-types": "0.1.2"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsc -b",
|