@automatalabs/acp-agents 0.1.2 → 0.3.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
@@ -45,7 +45,40 @@ try {
45
45
  }
46
46
  ```
47
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.
48
+ `run()` accepts the full `RunOptions` seam: `schema`, `model`, `tier`, `cwd`, `instructions`, `label`, `signal` (cancellation), `toolNames` / `disallowedToolNames`, `mcpServers`, `baseInstructions` / `developerInstructions` (Codex-only, see below), `onUsage`, `onModelResolved`, `onModelFallback`, and `onHistory`. See `@automatalabs/shared-types` for the field-by-field contract.
49
+
50
+ ### Codex session instructions (`baseInstructions` / `developerInstructions`)
51
+
52
+ When the run routes to the Codex backend, two optional fields let you override Codex's thread-level instructions for the session:
53
+
54
+ - **`baseInstructions`** — replaces Codex's built-in base system prompt.
55
+ - **`developerInstructions`** — injects developer-role instructions (added on top of the base prompt).
56
+
57
+ They ride ACP `session/new` `_meta` as bare keys and are threaded into the Codex `thread/start.{baseInstructions,developerInstructions}` params by the [`@automatalabs/codex-acp`](https://www.npmjs.com/package/@automatalabs/codex-acp) adapter. Both are additive (never part of the resume identity) and are **ignored by the Claude backend** — Claude has no analog. Note this is distinct from `instructions`, which is folded into the prompt text for either backend.
58
+
59
+ ```ts
60
+ await runner.run("Cut the release.", {
61
+ model: "openai/gpt-5-codex",
62
+ cwd: "/abs/path/to/worktree",
63
+ baseInstructions: "You are a release bot. Only touch CHANGELOG.md.",
64
+ developerInstructions: "Prefer conventional-commit summaries.",
65
+ });
66
+ ```
67
+
68
+ ## Listening in: live ACP events
69
+
70
+ `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.
71
+
72
+ ```ts
73
+ const off = runner.on("agent_message_chunk", (e) => {
74
+ if (e.content.type === "text") process.stdout.write(e.content.text);
75
+ });
76
+ runner.on("tool_call", (e) => console.error(`[${e.label}] ${e.title}`));
77
+ // … run() …
78
+ off();
79
+ ```
80
+
81
+ The full event map (`AcpRunnerEventMap`) and helpers (`TypedEventEmitter`) are exported here and re-exported from `@automatalabs/workflows`.
49
82
 
50
83
  ## Key exports
51
84
 
@@ -57,7 +90,7 @@ From [`src/index.ts`](./src/index.ts):
57
90
  - **`ClaudeBackend` / `CodexBackend`** — the two backend strategies (spawn config + per-backend schema wiring).
58
91
  - **`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
92
 
60
- Also exported: `AcpAgentPool` / `resolvePoolSize`, `PooledConnection` / `SessionHandle`, `decidePermission`, `UsageAccumulator`, `resolveStructuredOutput` / `extractValidated` / `findJsonBlock` / `validateValue`, and `errorText` / `mapThrownError`, plus their associated types.
93
+ 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
94
 
62
95
  ## Environment overrides
63
96
 
@@ -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,20 @@ 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;
47
+ /** CODEX-ONLY session instruction overrides. The backend folds these into session/new `_meta`
48
+ * (bare keys) for the codex-acp adapter; the Claude backend ignores them. Omitted => unset. */
49
+ baseInstructions?: string;
50
+ developerInstructions?: string;
40
51
  }
41
52
  /** Notified by a PooledConnection when its process dies, so the pool can drop it. */
42
53
  export interface PooledConnectionDeps {
43
54
  onDead(connection: PooledConnection): void;
55
+ /** Optional typed event sink. When present, every ACP notification / permission request /
56
+ * session lifecycle change on this connection is bubbled up through it (additive observability;
57
+ * it is invoked AFTER the drain accumulation and never affects the run). */
58
+ onEvent?: AcpEventSink;
44
59
  }
45
60
  /**
46
61
  * One long-lived ACP server subprocess + its held ACP client connection. Initialized ONCE and
@@ -55,6 +70,9 @@ export declare class PooledConnection {
55
70
  private readonly child;
56
71
  private readonly client;
57
72
  private readonly onDead;
73
+ private readonly onEvent;
74
+ /** Set true at the start of dispose() so the graceful-shutdown death is NOT reported as a crash. */
75
+ private disposing;
58
76
  /** Resolves once `initialize` completed (or rejects if the process died first). */
59
77
  private readonly ready;
60
78
  /** 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,10 +315,13 @@ export class PooledConnection {
266
315
  this._activeSessions += 1;
267
316
  try {
268
317
  await this.ready;
269
- const state = new SessionState(opts.policy);
270
- // The backend's vendor `_meta` (Claude schema channel; undefined for Codex) plus the
271
- // optional engine runId correlation stamp. When neither is present, no `_meta` is sent.
272
- const meta = stampRunId(this.backend.sessionMeta(opts.schema), opts.runId);
318
+ const state = new SessionState(opts.policy, opts.label, opts.runId);
319
+ // The backend's vendor `_meta` (Claude schema channel; Codex base/developer instructions)
320
+ // plus the optional engine runId correlation stamp. When none is present, no `_meta` is sent.
321
+ const meta = stampRunId(this.backend.sessionMeta(opts.schema, {
322
+ baseInstructions: opts.baseInstructions,
323
+ developerInstructions: opts.developerInstructions,
324
+ }), opts.runId);
273
325
  const request = {
274
326
  cwd: opts.cwd,
275
327
  // Client-provided MCP servers (additive run input), else the default empty list.
@@ -329,6 +381,8 @@ export class PooledConnection {
329
381
  async dispose() {
330
382
  if (!this._alive)
331
383
  return;
384
+ // Mark graceful shutdown so the imminent process-exit `die()` does not emit `backend_error`.
385
+ this.disposing = true;
332
386
  const exited = new Promise((resolve) => {
333
387
  this.child.once("exit", () => resolve());
334
388
  });
package/dist/backend.d.ts CHANGED
@@ -12,12 +12,22 @@ export interface StructuredSource {
12
12
  /** Claude only: `structured_output` from the latest `type:"result", subtype:"success"` raw message. */
13
13
  rawStructuredOutput(): unknown;
14
14
  }
15
+ /** Per-session inputs a backend may fold into its `session/new` `_meta`, beyond the schema.
16
+ * Additive/optional; a backend that doesn't understand a field ignores it. */
17
+ export interface SessionMetaInputs {
18
+ /** CODEX-ONLY: replaces Codex's base system prompt (`thread/start.baseInstructions`). */
19
+ baseInstructions?: string;
20
+ /** CODEX-ONLY: developer-role instructions (`thread/start.developerInstructions`). */
21
+ developerInstructions?: string;
22
+ }
15
23
  export interface Backend {
16
24
  readonly id: BackendId;
17
25
  /** How to launch this backend's ACP server over stdio. */
18
26
  spawnConfig(): SpawnConfig;
19
- /** `_meta` for session/new (undefined when this backend carries the schema elsewhere). */
20
- sessionMeta(schema: TSchema | undefined): Record<string, unknown> | undefined;
27
+ /** `_meta` for session/new (undefined when this backend carries nothing there). `inputs`
28
+ * carries optional per-session extras (e.g. Codex base/developer instructions); a backend
29
+ * that has no use for them ignores it. */
30
+ sessionMeta(schema: TSchema | undefined, inputs?: SessionMetaInputs): Record<string, unknown> | undefined;
21
31
  /** `_meta` for session/prompt (undefined when this backend carries the schema at session/new). */
22
32
  promptMeta(schema: TSchema | undefined): Record<string, unknown> | undefined;
23
33
  /** Read this backend's native structured result for the latest turn (unvalidated), or undefined. */
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3C,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED,8FAA8F;AAC9F,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,eAAe,IAAI,MAAM,CAAC;IAC1B,uGAAuG;IACvG,mBAAmB,IAAI,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,0DAA0D;IAC1D,WAAW,IAAI,WAAW,CAAC;IAC3B,0FAA0F;IAC1F,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC9E,kGAAkG;IAClG,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC7E,oGAAoG;IACpG,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC;CACrD;AAED,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAE7D"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3C,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED,8FAA8F;AAC9F,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,eAAe,IAAI,MAAM,CAAC;IAC1B,uGAAuG;IACvG,mBAAmB,IAAI,OAAO,CAAC;CAChC;AAED;+EAC+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sFAAsF;IACtF,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,0DAA0D;IAC1D,WAAW,IAAI,WAAW,CAAC;IAC3B;;+CAE2C;IAC3C,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC1G,kGAAkG;IAClG,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC7E,oGAAoG;IACpG,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC;CACrD;AAED,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAE7D"}
@@ -1 +1 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/backends/claude.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAM5E,qBAAa,aAAc,YAAW,OAAO;IAC3C,QAAQ,CAAC,EAAE,EAAG,QAAQ,CAAU;IAEhC,WAAW,IAAI,WAAW;IAgB1B,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAY7E,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAKjD,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO;CAGpD"}
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/backends/claude.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAM5E,qBAAa,aAAc,YAAW,OAAO;IAC3C,QAAQ,CAAC,EAAE,EAAG,QAAQ,CAAU;IAEhC,WAAW,IAAI,WAAW;IAgB1B,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAc7E,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAKjD,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO;CAGpD"}
@@ -27,6 +27,8 @@ export class ClaudeBackend {
27
27
  }
28
28
  }
29
29
  sessionMeta(schema) {
30
+ // Claude has no analog to Codex's base/developer instruction overrides, so it ignores the
31
+ // optional SessionMetaInputs (the seam still accepts them via the Backend interface).
30
32
  if (!schema)
31
33
  return undefined;
32
34
  return {
@@ -1,9 +1,9 @@
1
1
  import type { TSchema } from "typebox";
2
- import type { Backend, SpawnConfig, StructuredSource } from "../backend.js";
2
+ import type { Backend, SessionMetaInputs, SpawnConfig, StructuredSource } from "../backend.js";
3
3
  export declare class CodexBackend implements Backend {
4
4
  readonly id: "codex";
5
5
  spawnConfig(): SpawnConfig;
6
- sessionMeta(): Record<string, unknown> | undefined;
6
+ sessionMeta(_schema: TSchema | undefined, inputs?: SessionMetaInputs): Record<string, unknown> | undefined;
7
7
  promptMeta(schema: TSchema | undefined): Record<string, unknown> | undefined;
8
8
  nativeStructured(source: StructuredSource): unknown;
9
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/backends/codex.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAO5E,qBAAa,YAAa,YAAW,OAAO;IAC1C,QAAQ,CAAC,EAAE,EAAG,OAAO,CAAU;IAE/B,WAAW,IAAI,WAAW;IAe1B,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAKlD,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAK5E,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO;CAoBpD"}
1
+ {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/backends/codex.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAO/F,qBAAa,YAAa,YAAW,OAAO;IAC1C,QAAQ,CAAC,EAAE,EAAG,OAAO,CAAU;IAE/B,WAAW,IAAI,WAAW;IAe1B,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAY1G,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAK5E,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO;CAoBpD"}
@@ -7,7 +7,7 @@
7
7
  // constrained final message flows back over the normal agent-message stream, so the backend
8
8
  // reads the final text and JSON.parses it.
9
9
  import { createRequire } from "node:module";
10
- import { META_KEYS } from "@automatalabs/shared-types";
10
+ import { CODEX_META_KEYS, META_KEYS } from "@automatalabs/shared-types";
11
11
  import { splitArgs } from "../backend.js";
12
12
  import { toStrictJsonSchema } from "../schema-strict.js";
13
13
  import { findJsonBlock } from "../structured-output.js";
@@ -27,9 +27,18 @@ export class CodexBackend {
27
27
  const bin = env.AGENTPRISM_CODEX_ACP_BIN ?? require.resolve("@automatalabs/codex-acp");
28
28
  return { command: process.execPath, args: [bin], env };
29
29
  }
30
- sessionMeta() {
31
- // Codex carries the schema on the turn, not session/new.
32
- return undefined;
30
+ sessionMeta(_schema, inputs) {
31
+ // Codex carries the SCHEMA on the turn (see promptMeta), so nothing schema-related rides
32
+ // session/new. But the optional base/developer instruction overrides ARE session-scoped: the
33
+ // @automatalabs/codex-acp fork reads these bare `_meta` keys and threads them into
34
+ // thread/start.{baseInstructions,developerInstructions}. Emit them only when set so an
35
+ // unconfigured run sends no `_meta` at all (preserving the "Codex default" path).
36
+ const meta = {};
37
+ if (inputs?.baseInstructions !== undefined)
38
+ meta[CODEX_META_KEYS.baseInstructions] = inputs.baseInstructions;
39
+ if (inputs?.developerInstructions !== undefined)
40
+ meta[CODEX_META_KEYS.developerInstructions] = inputs.developerInstructions;
41
+ return Object.keys(meta).length > 0 ? meta : undefined;
33
42
  }
34
43
  promptMeta(schema) {
35
44
  if (!schema)
@@ -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,8 +2,10 @@ 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";
6
- export type { Backend, BackendId, SpawnConfig, StructuredSource } from "./backend.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";
8
+ export type { Backend, BackendId, SessionMetaInputs, SpawnConfig, StructuredSource } from "./backend.js";
7
9
  export { ClaudeBackend } from "./backends/claude.js";
8
10
  export { CodexBackend } from "./backends/codex.js";
9
11
  export { decidePermission } from "./permissions.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,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACzG,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;IAuF1B;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,12 @@ 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,
76
+ // CODEX-ONLY session instruction overrides -> session/new _meta bare keys. Additive; never
77
+ // hashed. The Claude backend ignores them.
78
+ baseInstructions: opts.baseInstructions,
79
+ developerInstructions: opts.developerInstructions,
44
80
  });
45
81
  try {
46
82
  opts.signal?.throwIfAborted();
@@ -113,6 +149,7 @@ export class AcpAgentRunner {
113
149
  * runner is disposed. Beyond the AgentRunner seam (additive) — never enters the resume hash. */
114
150
  async dispose() {
115
151
  await this.pool.dispose();
152
+ this.events.removeAllListeners();
116
153
  }
117
154
  }
118
155
  /** 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.3.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,9 +26,9 @@
26
26
  "dependencies": {
27
27
  "@agentclientprotocol/sdk": "1.0.0",
28
28
  "@agentclientprotocol/claude-agent-acp": "0.53.0",
29
- "@automatalabs/codex-acp": "1.0.2",
29
+ "@automatalabs/codex-acp": "1.1.0",
30
30
  "typebox": "1.3.2",
31
- "@automatalabs/shared-types": "0.1.2"
31
+ "@automatalabs/shared-types": "0.2.0"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsc -b",