@automatalabs/acp-agents 0.1.2 → 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 CHANGED
@@ -47,6 +47,21 @@ try {
47
47
 
48
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
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
+
50
65
  ## Key exports
51
66
 
52
67
  From [`src/index.ts`](./src/index.ts):
@@ -57,7 +72,7 @@ From [`src/index.ts`](./src/index.ts):
57
72
  - **`ClaudeBackend` / `CodexBackend`** — the two backend strategies (spawn config + per-backend schema wiring).
58
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`.
59
74
 
60
- Also exported: `AcpAgentPool` / `resolvePoolSize`, `PooledConnection` / `SessionHandle`, `decidePermission`, `UsageAccumulator`, `resolveStructuredOutput` / `extractValidated` / `findJsonBlock` / `validateValue`, and `errorText` / `mapThrownError`, plus their associated types.
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.
61
76
 
62
77
  ## Environment overrides
63
78
 
@@ -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
- constructor(policy: ToolPolicy);
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. */
@@ -1 +1 @@
1
- {"version":3,"file":"acp-client.d.ts","sourceRoot":"","sources":["../src/acp-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"acp-client.d.ts","sourceRoot":"","sources":["../src/acp-client.ts"],"names":[],"mappings":"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"}
@@ -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
- constructor(policy) {
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
- return decidePermission(params, state.policy);
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)?.applyUpdate(params.update);
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 message = params.message;
132
- this.sessions.get(sessionId)?.applyRawMessage(message);
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 = new MultiplexClient();
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
  });
@@ -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";
@@ -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;AAEhD,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"}
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
  /**
@@ -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;AAK1F,MAAM,WAAW,cAAc;IAC7B,mGAAmG;IACnG,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,kGAAkG;AAClG,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAUvD;AAED,qBAAa,YAAY;IACvB,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;gBAEb,OAAO,GAAE,cAAmB;IAIxC,8FAA8F;IACxF,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC;IAMhF;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAoBxB,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"}
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. */
@@ -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;gBAExB,OAAO,GAAE,cAAmB;IAIlC,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;IAiF1B;qGACiG;IAC3F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/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"}
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.1.2",
3
+ "version": "0.2.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",