@automatalabs/acp-agents 0.6.0 → 0.8.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,18 @@ 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`, `runId`, `baseInstructions` / `developerInstructions` (Codex-only, see below), `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`, `images` (see below), `runId`, `baseInstructions` / `developerInstructions` (Codex-only, see below), `onUsage`, `onModelResolved`, `onModelFallback`, and `onHistory`. See `@automatalabs/shared-types` for the field-by-field contract.
49
+
50
+ ### Image attachments (`images`)
51
+
52
+ `images` appends base64 image `ContentBlock`s to the first prompt turn. The client adapts content to what the connected agent advertised at `initialize`: when the agent does not advertise `promptCapabilities.image`, each attachment degrades to a bracketed text note naming the mime type (never an error, never silently dropped). Repair/re-prompt turns stay text-only.
53
+
54
+ ```ts
55
+ await runner.run("What's in this screenshot?", {
56
+ cwd: "/abs/path/to/worktree",
57
+ images: [{ data: base64Png, mimeType: "image/png" }],
58
+ });
59
+ ```
49
60
 
50
61
  ### Codex session instructions (`baseInstructions` / `developerInstructions`)
51
62
 
@@ -65,6 +76,24 @@ await runner.run("Cut the release.", {
65
76
  });
66
77
  ```
67
78
 
79
+ ## Client-side fs / terminal handlers (`clientHandlers`)
80
+
81
+ By default the agent uses its **own** built-in file and exec tools — the client never sees those operations. Register `clientHandlers` to interpose: the client then advertises exactly what you registered at `initialize` (`fs.readTextFile` / `fs.writeTextFile` per-method; `terminal` only when **all five** terminal methods are provided) and routes the agent's `fs/*` and `terminal/*` requests to your handlers. Every handler receives the request params plus an `AcpSessionContext` — `sessionId`, the session's **own** `cwd`, `label`, `runId` — so a pooled process serving many sessions still gets per-session isolation.
82
+
83
+ **Confinement is your job.** The library routes requests and supplies the session context; enforcing worktree roots, resolving symlinks, scoping environment variables, bounding output, and applying timeouts belongs in your handler implementation. Requests for methods you did not register are rejected with a JSON-RPC method-not-found error (agents that respect the advertisement never send them).
84
+
85
+ ```ts
86
+ const runner = createAcpRunner({
87
+ clientHandlers: {
88
+ fs: {
89
+ readTextFile: async ({ path }, { cwd }) => ({ content: await confinedRead(cwd, path) }),
90
+ writeTextFile: async ({ path, content }, { cwd }) => { await confinedWrite(cwd, path, content); },
91
+ },
92
+ // terminal: { createTerminal, terminalOutput, waitForTerminalExit, killTerminal, releaseTerminal },
93
+ },
94
+ });
95
+ ```
96
+
68
97
  ## Listening in: live ACP events
69
98
 
70
99
  `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.
@@ -88,7 +117,9 @@ From [`src/index.ts`](./src/index.ts):
88
117
  - **`AcpAgentRunner`** — the `AgentRunner` implementation; `run(prompt, options)` and `dispose()`.
89
118
  - **`selectBackend({ model, tier }, registry?)`** — the cross-provider routing rule: which backend a spec maps to (registered custom names match first, exact or `name/<inner-model>`).
90
119
  - **`ClaudeBackend` / `CodexBackend`** — the two built-in backend strategies (spawn config + per-backend schema wiring).
91
- - **`CustomAcpBackend` / `resolveBackendRegistry` / `BACKENDS_ENV`** — the custom-backend registry: run **any** ACP agent as a named backend via `createAcpRunner({ backends: { name: { command, args?, env?, sessionMeta? } } })` or the `AGENTPRISM_BACKENDS` env var (JSON, same shape; the option wins per name; `claude`/`codex` reserved). Custom backends carry a `schema` as turn-level `_meta.outputSchema` and read the result off the final message as JSON.
120
+ - **`CustomAcpBackend` / `resolveBackendRegistry` / `BACKENDS_ENV`** — the custom-backend registry: run **any** ACP agent as a named backend via `createAcpRunner({ backends: { name: { command, args?, env?, sessionMeta?, customCapabilities? } } })` or the `AGENTPRISM_BACKENDS` env var (JSON, same shape; the option wins per name; `claude`/`codex` reserved). Custom backends carry a `schema` as turn-level `_meta.outputSchema` and read the result off the final message as JSON. `customCapabilities: { namespace, gatedKeys }` declares the agent's `agentCapabilities._meta` negotiation contract: once the agent advertises that namespace, each declared bare `_meta` key is sent only when its same-named flag is `true` (no declaration = never gated).
121
+ - **`clientCapabilitiesFor` + the `ClientHandlers` / `FsHandlers` / `TerminalHandlers` / `AcpSessionContext` types** — the client-side fs/terminal interposition surface (see above).
122
+ - **`negotiateCapabilities` / `adaptPromptContent` / `gateCustomMeta` / `unsupportedMcpServer` + `NegotiatedCapabilities`** — the `initialize` capability-negotiation primitives; the negotiated record for a live connection is exposed on `PooledConnection.capabilities`.
92
123
  - **`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`.
93
124
 
94
125
  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.
@@ -98,7 +129,7 @@ Also exported: `AcpAgentPool` / `resolvePoolSize`, `PooledConnection` / `Session
98
129
  | Variable | Effect |
99
130
  | --- | --- |
100
131
  | `AGENTPRISM_DEFAULT_BACKEND` | Backend when `model`/`tier` don't pick one (`codex` selects Codex; a registered custom name selects that backend; anything else is Claude). |
101
- | `AGENTPRISM_BACKENDS` | Custom ACP backends as JSON: `{"<name>": {"command": "…", "args": […], "env": {…}, "sessionMeta": {…}}}`. |
132
+ | `AGENTPRISM_BACKENDS` | Custom ACP backends as JSON: `{"<name>": {"command": "…", "args": […], "env": {…}, "sessionMeta": {…}, "customCapabilities": {"namespace": "…", "gatedKeys": […]}}}`. |
102
133
  | `AGENTPRISM_ACP_INIT_TIMEOUT_MS` | Deadline (default `60000`) for a backend's one-time ACP `initialize` handshake — a non-ACP command fails fast instead of hanging. |
103
134
  | `AGENTPRISM_ACP_POOL_SIZE` | Long-lived processes to keep per backend (default `1`). |
104
135
  | `AGENTPRISM_CLAUDE_ACP_CMD` / `AGENTPRISM_CLAUDE_ACP_ARGS` | Override the command (and args) used to spawn the Claude ACP server. |
@@ -1,10 +1,12 @@
1
- import { ClientSideConnection, type PromptResponse, type SessionConfigOption, type SessionNotification } from "@agentclientprotocol/sdk";
1
+ import { ClientSideConnection, type ContentBlock, type PromptResponse, type SessionConfigOption, type SessionNotification } from "@agentclientprotocol/sdk";
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 NegotiatedCapabilities } from "./capabilities.js";
5
6
  import { type AcpEventSink } from "./events.js";
6
7
  import { type ToolPolicy } from "./permissions.js";
7
8
  import { UsageAccumulator } from "./usage.js";
9
+ import { type ClientHandlers } from "./client-handlers.js";
8
10
  interface RawResultSuccess {
9
11
  type: string;
10
12
  subtype: string;
@@ -13,6 +15,7 @@ interface RawResultSuccess {
13
15
  /** Per-session accumulator: assistant text, tool history, usage, the Claude raw structured_output,
14
16
  * and the tool policy used to auto-answer permission requests for THIS session. */
15
17
  declare class SessionState {
18
+ readonly cwd: string;
16
19
  readonly policy: ToolPolicy;
17
20
  readonly label?: string | undefined;
18
21
  readonly runId?: string | undefined;
@@ -23,7 +26,7 @@ declare class SessionState {
23
26
  private turnStartIndex;
24
27
  /** `label`/`runId` are carried here ONLY so the MultiplexClient can stamp them onto emitted
25
28
  * events as context — they never affect routing or the wire request. */
26
- constructor(policy: ToolPolicy, label?: string | undefined, runId?: string | undefined);
29
+ constructor(cwd: string, policy: ToolPolicy, label?: string | undefined, runId?: string | undefined);
27
30
  /** Mark the start of a new turn so currentTurnText()/structured_output read only this turn. */
28
31
  beginTurn(): void;
29
32
  currentTurnText(): string;
@@ -60,6 +63,8 @@ export interface PooledConnectionDeps {
60
63
  * session lifecycle change on this connection is bubbled up through it (additive observability;
61
64
  * it is invoked AFTER the drain accumulation and never affects the run). */
62
65
  onEvent?: AcpEventSink;
66
+ /** Client-side ACP fs/terminal handlers advertised once and routed by sessionId. */
67
+ clientHandlers?: ClientHandlers;
63
68
  }
64
69
  /**
65
70
  * One long-lived ACP server subprocess + its held ACP client connection. Initialized ONCE and
@@ -75,6 +80,7 @@ export declare class PooledConnection {
75
80
  private readonly client;
76
81
  private readonly onDead;
77
82
  private readonly onEvent;
83
+ private readonly clientHandlers;
78
84
  /** Set true at the start of dispose() so the graceful-shutdown death is NOT reported as a crash. */
79
85
  private disposing;
80
86
  /** Resolves once `initialize` completed (or rejects if the process died first). */
@@ -83,7 +89,8 @@ export declare class PooledConnection {
83
89
  private readonly whenDead;
84
90
  private resolveDead;
85
91
  private deathError;
86
- private supportsClose;
92
+ /** Set from the one-time initialize handshake; undefined until it completes (or if it failed). */
93
+ private negotiated;
87
94
  private _alive;
88
95
  private _activeSessions;
89
96
  private stderrTail;
@@ -93,6 +100,12 @@ export declare class PooledConnection {
93
100
  static create(backend: Backend, deps: PooledConnectionDeps): PooledConnection;
94
101
  get alive(): boolean;
95
102
  get activeSessions(): number;
103
+ /** The capabilities negotiated on this connection's one-time initialize handshake, or undefined
104
+ * until it completes — derived-state-behind-a-getter, like `alive`/`activeSessions`. */
105
+ get capabilities(): NegotiatedCapabilities | undefined;
106
+ /** Drop the backend-declared bare `_meta` keys the connected agent did not advertise support
107
+ * for (see gateCustomMeta). Applied to BOTH session/new and session/prompt `_meta`. */
108
+ gateCustomMeta(meta: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
96
109
  /** Mark this connection dead exactly once, then ask the pool to evict it. Idempotent. */
97
110
  private die;
98
111
  private stderrSuffix;
@@ -109,9 +122,9 @@ export declare class PooledConnection {
109
122
  /** Best-effort ACP cancel for one session (wired to opts.signal). The PROCESS stays pooled. */
110
123
  cancelSession(sessionId: string): Promise<void>;
111
124
  /**
112
- * Release a session: stop routing it, free the load slot, and best-effort session/close on the
113
- * wire (capability-gated, bounded, never fatal). The PROCESS is NOT killed — it returns to the
114
- * pool for the next agent() call.
125
+ * Release a session: move it to teardown-only routing, free the load slot, and best-effort
126
+ * session/close on the wire (capability-gated, bounded, never fatal). The PROCESS is NOT
127
+ * killed — it returns to the pool for the next agent() call.
115
128
  */
116
129
  releaseSession(sessionId: string): Promise<void>;
117
130
  /** Synchronous best-effort kill for a process-exit hook (no time to await a graceful close). */
@@ -172,7 +185,7 @@ export declare class SessionHandle implements StructuredSource {
172
185
  /** Set one session config option via the wire method and adopt the echoed catalog. */
173
186
  private applyConfigOption;
174
187
  /** Send a prompt turn and drain it; returns the final PromptResponse. */
175
- prompt(text: string, promptMeta?: Record<string, unknown>): Promise<PromptResponse>;
188
+ prompt(content: string | ContentBlock[], promptMeta?: Record<string, unknown>): Promise<PromptResponse>;
176
189
  /** StructuredSource — the latest turn's assistant text. */
177
190
  currentTurnText(): string;
178
191
  /** StructuredSource — Claude's raw structured_output for the latest turn, if any. */
@@ -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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"}
@@ -22,11 +22,13 @@
22
22
  // other sessions' updates interleave on the same wire.
23
23
  import { spawn } from "node:child_process";
24
24
  import { Readable, Writable } from "node:stream";
25
- import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
26
- import { META_KEYS } from "@automatalabs/shared-types";
25
+ import { ClientSideConnection, CLIENT_METHODS, ndJsonStream, PROTOCOL_VERSION, RequestError, } from "@agentclientprotocol/sdk";
26
+ import { META_KEYS, WorkflowError, WorkflowErrorCode, } from "@automatalabs/shared-types";
27
+ import { adaptPromptContent, gateCustomMeta, isSupportedProtocolVersion, negotiateCapabilities, unsupportedMcpServer, } from "./capabilities.js";
27
28
  import { emitSessionUpdate } from "./events.js";
28
29
  import { decidePermission } from "./permissions.js";
29
30
  import { UsageAccumulator } from "./usage.js";
31
+ import { clientCapabilitiesFor, } from "./client-handlers.js";
30
32
  /** A benign client identity. NOT JetBrains/IntelliJ 2026.1 — that exact identity makes
31
33
  * codex-acp disable session config options (our model/effort routing channel). */
32
34
  const CLIENT_INFO = {
@@ -39,9 +41,11 @@ const CLAUDE_RAW_MESSAGE_METHOD = "_claude/sdkMessage";
39
41
  const CLOSE_SESSION_TIMEOUT_MS = 5_000;
40
42
  /** Bound the graceful SIGTERM shutdown before escalating to SIGKILL. */
41
43
  const DISPOSE_SIGKILL_GRACE_MS = 2_000;
44
+ const TOMBSTONE_SESSION_CAP = 64;
42
45
  /** Per-session accumulator: assistant text, tool history, usage, the Claude raw structured_output,
43
46
  * and the tool policy used to auto-answer permission requests for THIS session. */
44
47
  class SessionState {
48
+ cwd;
45
49
  policy;
46
50
  label;
47
51
  runId;
@@ -52,7 +56,8 @@ class SessionState {
52
56
  turnStartIndex = 0;
53
57
  /** `label`/`runId` are carried here ONLY so the MultiplexClient can stamp them onto emitted
54
58
  * events as context — they never affect routing or the wire request. */
55
- constructor(policy, label, runId) {
59
+ constructor(cwd, policy, label, runId) {
60
+ this.cwd = cwd;
56
61
  this.policy = policy;
57
62
  this.label = label;
58
63
  this.runId = runId;
@@ -112,25 +117,60 @@ class SessionState {
112
117
  class MultiplexClient {
113
118
  backendId;
114
119
  onEvent;
120
+ handlers;
115
121
  sessions = new Map();
122
+ /** Recently unregistered sessions kept ONLY for the teardown window: ACP agents may
123
+ * legitimately release terminals (or finish fs/terminal cleanup) after this client releases
124
+ * the session because session/close is cancel + free resources and the Agent owns terminal
125
+ * release. Store only the slim routing context and cap it FIFO so the memory cost is bounded. */
126
+ tombstones = new Map();
116
127
  /** `backendId` stamps event context; `onEvent` (optional) bubbles every notification, permission
117
128
  * request and session lifecycle change up to the runner's typed bus. */
118
- constructor(backendId, onEvent) {
129
+ constructor(backendId, onEvent, handlers) {
119
130
  this.backendId = backendId;
120
131
  this.onEvent = onEvent;
132
+ this.handlers = handlers;
121
133
  }
122
134
  contextFor(sessionId, state) {
123
135
  return { sessionId, backendId: this.backendId, label: state?.label, runId: state?.runId };
124
136
  }
137
+ handlerContext(params) {
138
+ const state = this.sessions.get(params.sessionId);
139
+ if (state)
140
+ return { sessionId: params.sessionId, cwd: state.cwd, label: state.label, runId: state.runId };
141
+ const tombstone = this.tombstones.get(params.sessionId);
142
+ if (tombstone)
143
+ return { sessionId: params.sessionId, ...tombstone };
144
+ throw unknownSession(params.sessionId);
145
+ }
146
+ dispatch(params, wireMethod, handler) {
147
+ if (typeof handler !== "function")
148
+ throw methodNotAdvertised(wireMethod);
149
+ const ctx = this.handlerContext(params);
150
+ return handler(params, ctx);
151
+ }
125
152
  register(sessionId, state) {
153
+ this.tombstones.delete(sessionId);
126
154
  this.sessions.set(sessionId, state);
127
155
  this.onEvent?.("session_open", this.contextFor(sessionId, state));
128
156
  }
129
157
  unregister(sessionId) {
130
158
  const state = this.sessions.get(sessionId);
131
159
  this.sessions.delete(sessionId);
132
- if (state)
160
+ if (state) {
161
+ this.tombstones.set(sessionId, {
162
+ cwd: state.cwd,
163
+ label: state.label,
164
+ runId: state.runId,
165
+ });
166
+ while (this.tombstones.size > TOMBSTONE_SESSION_CAP) {
167
+ const oldest = this.tombstones.keys().next().value;
168
+ if (oldest === undefined)
169
+ break;
170
+ this.tombstones.delete(oldest);
171
+ }
133
172
  this.onEvent?.("session_close", this.contextFor(sessionId, state));
173
+ }
134
174
  }
135
175
  requestPermission(params) {
136
176
  const state = this.sessions.get(params.sessionId);
@@ -145,6 +185,27 @@ class MultiplexClient {
145
185
  });
146
186
  return outcome;
147
187
  }
188
+ readTextFile(params) {
189
+ return this.dispatch(params, CLIENT_METHODS.fs_read_text_file, this.handlers?.fs?.readTextFile);
190
+ }
191
+ writeTextFile(params) {
192
+ return this.dispatch(params, CLIENT_METHODS.fs_write_text_file, this.handlers?.fs?.writeTextFile);
193
+ }
194
+ createTerminal(params) {
195
+ return this.dispatch(params, CLIENT_METHODS.terminal_create, this.handlers?.terminal?.createTerminal);
196
+ }
197
+ terminalOutput(params) {
198
+ return this.dispatch(params, CLIENT_METHODS.terminal_output, this.handlers?.terminal?.terminalOutput);
199
+ }
200
+ releaseTerminal(params) {
201
+ return this.dispatch(params, CLIENT_METHODS.terminal_release, this.handlers?.terminal?.releaseTerminal);
202
+ }
203
+ waitForTerminalExit(params) {
204
+ return this.dispatch(params, CLIENT_METHODS.terminal_wait_for_exit, this.handlers?.terminal?.waitForTerminalExit);
205
+ }
206
+ killTerminal(params) {
207
+ return this.dispatch(params, CLIENT_METHODS.terminal_kill, this.handlers?.terminal?.killTerminal);
208
+ }
148
209
  sessionUpdate(params) {
149
210
  const state = this.sessions.get(params.sessionId);
150
211
  // Fold into the accumulator FIRST (the drain contract), THEN bubble the event up unchanged.
@@ -171,6 +232,12 @@ class MultiplexClient {
171
232
  });
172
233
  }
173
234
  }
235
+ function unknownSession(sessionId) {
236
+ return RequestError.invalidParams({ sessionId }, `unknown session: ${sessionId}`);
237
+ }
238
+ function methodNotAdvertised(method) {
239
+ return new RequestError(-32601, `${method} was not advertised by this client`, { method });
240
+ }
174
241
  const DEFAULT_INIT_TIMEOUT_MS = 60_000;
175
242
  /** Deadline for the one-time ACP `initialize` handshake per pooled process. Overridable via
176
243
  * AGENTPRISM_ACP_INIT_TIMEOUT_MS (e.g. for slow cold-start backends). Clamped to >= 1s. */
@@ -224,6 +291,7 @@ export class PooledConnection {
224
291
  client;
225
292
  onDead;
226
293
  onEvent;
294
+ clientHandlers;
227
295
  /** Set true at the start of dispose() so the graceful-shutdown death is NOT reported as a crash. */
228
296
  disposing = false;
229
297
  /** Resolves once `initialize` completed (or rejects if the process died first). */
@@ -232,7 +300,8 @@ export class PooledConnection {
232
300
  whenDead;
233
301
  resolveDead;
234
302
  deathError;
235
- supportsClose = false;
303
+ /** Set from the one-time initialize handshake; undefined until it completes (or if it failed). */
304
+ negotiated;
236
305
  _alive = true;
237
306
  _activeSessions = 0;
238
307
  stderrTail = "";
@@ -241,7 +310,8 @@ export class PooledConnection {
241
310
  this.backendId = backend.id;
242
311
  this.onDead = deps.onDead;
243
312
  this.onEvent = deps.onEvent;
244
- this.client = new MultiplexClient(this.backendId, this.onEvent);
313
+ this.clientHandlers = deps.clientHandlers;
314
+ this.client = new MultiplexClient(this.backendId, this.onEvent, deps.clientHandlers);
245
315
  const { command, args, env } = backend.spawnConfig();
246
316
  // NOTE: deliberately NO `cwd` here. cwd is per-SESSION (session/new), so one pooled process
247
317
  // serves runs in different worktrees without losing isolation.
@@ -289,6 +359,16 @@ export class PooledConnection {
289
359
  get activeSessions() {
290
360
  return this._activeSessions;
291
361
  }
362
+ /** The capabilities negotiated on this connection's one-time initialize handshake, or undefined
363
+ * until it completes — derived-state-behind-a-getter, like `alive`/`activeSessions`. */
364
+ get capabilities() {
365
+ return this.negotiated;
366
+ }
367
+ /** Drop the backend-declared bare `_meta` keys the connected agent did not advertise support
368
+ * for (see gateCustomMeta). Applied to BOTH session/new and session/prompt `_meta`. */
369
+ gateCustomMeta(meta) {
370
+ return gateCustomMeta(meta, this.negotiated?.customMetaSupport, this.negotiated?.gatedKeys);
371
+ }
292
372
  /** Mark this connection dead exactly once, then ask the pool to evict it. Idempotent. */
293
373
  die(error) {
294
374
  if (!this._alive)
@@ -337,12 +417,26 @@ export class PooledConnection {
337
417
  const response = await Promise.race([
338
418
  this.race(this.rpc.initialize({
339
419
  protocolVersion: PROTOCOL_VERSION,
340
- clientCapabilities: {},
420
+ // Truthful advertisement: computed from the consumer-provided handlers registered on
421
+ // this runner. Omitted flags are unsupported; false flags are never sent deliberately.
422
+ clientCapabilities: clientCapabilitiesFor(this.clientHandlers),
341
423
  clientInfo: { ...CLIENT_INFO },
342
424
  })),
343
425
  deadline,
344
426
  ]);
345
- this.supportsClose = Boolean(response.agentCapabilities?.sessionCapabilities?.close);
427
+ const negotiated = negotiateCapabilities(response, this.backend.customCapabilities);
428
+ // Version negotiation: the agent replies with the version it chose (our requested version if
429
+ // it supports it, else its own latest). If this client cannot speak it, the ACP spec says
430
+ // CLOSE the connection and inform the user — kill the process (so the pool evicts it) and
431
+ // surface a legible error instead of proceeding on an unspoken protocol. Non-recoverable: a
432
+ // deterministic protocol incompatibility must fail fast, not be retried as a transient
433
+ // AGENT_EXECUTION_ERROR.
434
+ if (!isSupportedProtocolVersion(negotiated.protocolVersion)) {
435
+ this.killNow();
436
+ throw new WorkflowError(`ACP agent (${this.backendId}) selected protocol version ${negotiated.protocolVersion}, which ` +
437
+ `this client (protocol v${PROTOCOL_VERSION}) does not support — closing the connection.${this.stderrSuffix()}`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false });
438
+ }
439
+ this.negotiated = negotiated;
346
440
  }
347
441
  finally {
348
442
  clearTimeout(timer);
@@ -357,16 +451,28 @@ export class PooledConnection {
357
451
  this._activeSessions += 1;
358
452
  try {
359
453
  await this.ready;
360
- const state = new SessionState(opts.policy, opts.label, opts.runId);
454
+ // Capability gate: reject a client-provided MCP server whose transport the connected agent
455
+ // does not advertise (http/sse gated on mcpCapabilities; stdio is always serviceable).
456
+ // Fail-fast and non-recoverable — re-running the same incompatible transport can never
457
+ // succeed. Lenient for agents that advertise no mcpCapabilities (the legacy passthrough).
458
+ const unsupported = this.negotiated
459
+ ? unsupportedMcpServer(opts.mcpServers, this.negotiated.agent)
460
+ : undefined;
461
+ if (unsupported) {
462
+ throw new WorkflowError(`MCP server "${unsupported.name}" uses the "${unsupported.transport}" transport, which the ` +
463
+ `${this.backendId} agent does not support`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: opts.label });
464
+ }
465
+ const state = new SessionState(opts.cwd, opts.policy, opts.label, opts.runId);
361
466
  // session/new `_meta`, layered lowest-to-highest precedence: the backend's static
362
467
  // defaults (a custom registry entry's `sessionMeta`), then the generic user passthrough
363
468
  // (opts.meta), then the backend's protocol-critical `_meta` (Claude schema channel;
364
- // Codex base/developer instructions), then the engine runId correlation stamp. When no
365
- // layer is present, no `_meta` is sent.
366
- const meta = stampRunId(layerMeta(this.backend.sessionMetaDefaults?.(), opts.meta, this.backend.sessionMeta(opts.schema, {
469
+ // Codex base/developer instructions), then the engine runId correlation stamp. The result
470
+ // is gated against the agent's advertised custom capabilities (a declared key the agent
471
+ // said it does not honor is dropped). When no layer survives, no `_meta` is sent.
472
+ const meta = this.gateCustomMeta(stampRunId(layerMeta(this.backend.sessionMetaDefaults?.(), opts.meta, this.backend.sessionMeta(opts.schema, {
367
473
  baseInstructions: opts.baseInstructions,
368
474
  developerInstructions: opts.developerInstructions,
369
- })), opts.runId);
475
+ })), opts.runId));
370
476
  const request = {
371
477
  cwd: opts.cwd,
372
478
  // Client-provided MCP servers (additive run input), else the default empty list.
@@ -394,15 +500,15 @@ export class PooledConnection {
394
500
  }
395
501
  }
396
502
  /**
397
- * Release a session: stop routing it, free the load slot, and best-effort session/close on the
398
- * wire (capability-gated, bounded, never fatal). The PROCESS is NOT killed — it returns to the
399
- * pool for the next agent() call.
503
+ * Release a session: move it to teardown-only routing, free the load slot, and best-effort
504
+ * session/close on the wire (capability-gated, bounded, never fatal). The PROCESS is NOT
505
+ * killed — it returns to the pool for the next agent() call.
400
506
  */
401
507
  async releaseSession(sessionId) {
402
508
  this.client.unregister(sessionId);
403
509
  if (this._activeSessions > 0)
404
510
  this._activeSessions -= 1;
405
- if (!this.supportsClose || !this._alive)
511
+ if (!this.negotiated?.supportsClose || !this._alive)
406
512
  return;
407
513
  try {
408
514
  await this.race(withTimeout(this.rpc.closeSession({ sessionId }), CLOSE_SESSION_TIMEOUT_MS));
@@ -584,14 +690,19 @@ export class SessionHandle {
584
690
  this.configOptions = response.configOptions;
585
691
  }
586
692
  /** Send a prompt turn and drain it; returns the final PromptResponse. */
587
- async prompt(text, promptMeta) {
693
+ async prompt(content, promptMeta) {
588
694
  this.opts.signal?.throwIfAborted();
589
695
  this.state.beginTurn();
590
- const prompt = [{ type: "text", text }];
696
+ const prompt = typeof content === "string"
697
+ ? content
698
+ : adaptPromptContent(content, this.pooled.capabilities?.agent ?? {}, this.pooled.backendId);
699
+ // Gate the turn `_meta` against the agent's advertised custom capabilities: a declared
700
+ // turn-level key is dropped when the connected agent said it does not honor it.
701
+ const gatedMeta = this.pooled.gateCustomMeta(promptMeta);
591
702
  const request = {
592
703
  sessionId: this.sessionId,
593
- prompt,
594
- ...(promptMeta ? { _meta: promptMeta } : {}),
704
+ prompt: typeof prompt === "string" ? [{ type: "text", text: prompt }] : prompt,
705
+ ...(gatedMeta ? { _meta: gatedMeta } : {}),
595
706
  };
596
707
  const response = await this.pooled.race(this.pooled.rpc.prompt(request));
597
708
  this.state.usage.recordPromptUsage(response.usage);
package/dist/backend.d.ts CHANGED
@@ -37,6 +37,13 @@ export interface Backend {
37
37
  * without the schema in the prompt, such an agent returns well-formed JSON with the WRONG
38
38
  * KEYS and the repair ladder can never converge (it can fix prose, not unseen contracts). */
39
39
  readonly embedSchemaInPrompt?: boolean;
40
+ /** The agentCapabilities._meta namespace this backend's agent advertises under, and the bare
41
+ * `_meta` keys gated by same-named boolean flags in that block; undefined = this backend has
42
+ * no custom-capability contract (its custom `_meta`, if any, is never gated). */
43
+ readonly customCapabilities?: {
44
+ readonly namespace: string;
45
+ readonly gatedKeys: readonly string[];
46
+ };
40
47
  /** How to launch this backend's ACP server over stdio. */
41
48
  spawnConfig(): SpawnConfig;
42
49
  /** OPTIONAL backend-level `_meta` DEFAULTS for session/new (e.g. a custom registry entry's
@@ -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,4FAA4F;AAC5F,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAClD;sFACsF;AACtF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC;AAE/B,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;;;0EAGsE;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;kGAI8F;IAC9F,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC,0DAA0D;IAC1D,WAAW,IAAI,WAAW,CAAC;IAC3B;;sEAEkE;IAClE,mBAAmB,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC5D;;;mGAG+F;IAC/F,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
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,4FAA4F;AAC5F,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAClD;sFACsF;AACtF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC;AAE/B,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;;;0EAGsE;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;kGAI8F;IAC9F,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IACvC;;sFAEkF;IAClF,QAAQ,CAAC,kBAAkB,CAAC,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAC;IACpG,0DAA0D;IAC1D,WAAW,IAAI,WAAW,CAAC;IAC3B;;sEAEkE;IAClE,mBAAmB,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC5D;;;mGAG+F;IAC/F,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"}
@@ -2,6 +2,10 @@ import type { TSchema } from "typebox";
2
2
  import type { Backend, SessionMetaInputs, SpawnConfig, StructuredSource } from "../backend.js";
3
3
  export declare class CodexBackend implements Backend {
4
4
  readonly id: "codex";
5
+ readonly customCapabilities: {
6
+ readonly namespace: "@automatalabs/codex-acp";
7
+ readonly gatedKeys: readonly string[];
8
+ };
5
9
  spawnConfig(): SpawnConfig;
6
10
  sessionMeta(_schema: TSchema | undefined, inputs?: SessionMetaInputs): Record<string, unknown> | undefined;
7
11
  promptMeta(schema: TSchema | undefined): Record<string, unknown> | undefined;
@@ -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,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;CAKpD"}
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;AAQ/F,qBAAa,YAAa,YAAW,OAAO;IAC1C,QAAQ,CAAC,EAAE,EAAG,OAAO,CAAU;IAC/B,QAAQ,CAAC,kBAAkB;;;MAGhB;IAEX,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;CAKpD"}
@@ -7,13 +7,18 @@
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 { CODEX_META_KEYS, META_KEYS } from "@automatalabs/shared-types";
10
+ import { CODEX_CUSTOM_CAPABILITY_NAMESPACE, CODEX_META_KEYS, META_KEYS } from "@automatalabs/shared-types";
11
11
  import { splitArgs } from "../backend.js";
12
+ import { GATED_CUSTOM_META_KEYS } from "../capabilities.js";
12
13
  import { toStrictJsonSchema } from "../schema-strict.js";
13
14
  import { parseFinalJson } from "../structured-output.js";
14
15
  const require = createRequire(import.meta.url);
15
16
  export class CodexBackend {
16
17
  id = "codex";
18
+ customCapabilities = {
19
+ namespace: CODEX_CUSTOM_CAPABILITY_NAMESPACE,
20
+ gatedKeys: GATED_CUSTOM_META_KEYS,
21
+ };
17
22
  spawnConfig() {
18
23
  const env = process.env;
19
24
  const override = env.AGENTPRISM_CODEX_ACP_CMD;
@@ -11,6 +11,7 @@ export declare class CustomAcpBackend implements Backend {
11
11
  * schema in the prompt — otherwise the model returns JSON with keys it invented and the
12
12
  * repair ladder can never converge on a contract the model was never shown. */
13
13
  readonly embedSchemaInPrompt = true;
14
+ readonly customCapabilities?: NonNullable<Backend["customCapabilities"]>;
14
15
  constructor(config: RegisteredBackend);
15
16
  spawnConfig(): SpawnConfig;
16
17
  sessionMetaDefaults(): Record<string, unknown> | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"custom.d.ts","sourceRoot":"","sources":["../../src/backends/custom.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAIxD,qBAAa,gBAAiB,YAAW,OAAO;IAUlC,OAAO,CAAC,QAAQ,CAAC,MAAM;IATnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;mGAC+F;IAC/F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;oFAEgF;IAChF,QAAQ,CAAC,mBAAmB,QAAQ;gBAEP,MAAM,EAAE,iBAAiB;IAUtD,WAAW,IAAI,WAAW;IAS1B,mBAAmB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAQ1D,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAM9E,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAQ5E,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO;CAGpD"}
1
+ {"version":3,"file":"custom.d.ts","sourceRoot":"","sources":["../../src/backends/custom.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAIxD,qBAAa,gBAAiB,YAAW,OAAO;IAWlC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAVnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;mGAC+F;IAC/F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;oFAEgF;IAChF,QAAQ,CAAC,mBAAmB,QAAQ;IACpC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC;gBAE5C,MAAM,EAAE,iBAAiB;IAWtD,WAAW,IAAI,WAAW;IAS1B,mBAAmB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAQ1D,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAM9E,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAQ5E,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO;CAGpD"}
@@ -25,9 +25,12 @@ export class CustomAcpBackend {
25
25
  * schema in the prompt — otherwise the model returns JSON with keys it invented and the
26
26
  * repair ladder can never converge on a contract the model was never shown. */
27
27
  embedSchemaInPrompt = true;
28
+ customCapabilities;
28
29
  constructor(config) {
29
30
  this.config = config;
30
31
  this.id = config.name;
32
+ if (config.customCapabilities)
33
+ this.customCapabilities = config.customCapabilities;
31
34
  const spawnIdentity = JSON.stringify({
32
35
  command: config.command,
33
36
  args: config.args ?? [],
@@ -0,0 +1,57 @@
1
+ import { type AgentCapabilities, type ContentBlock, type Implementation, type InitializeResponse } from "@agentclientprotocol/sdk";
2
+ import { type McpServerConfig } from "@automatalabs/shared-types";
3
+ import type { Backend } from "./backend.js";
4
+ /** The bare `_meta` keys whose emission is gated by Codex's custom-capability advertisement.
5
+ * Each is named EXACTLY like the advertised flag that gates it, so `support[key] === true` is the
6
+ * whole test. session/new carries baseInstructions/developerInstructions and session/prompt carries
7
+ * outputSchema — one list covers both (a key absent from a given `_meta` is simply skipped). */
8
+ export declare const GATED_CUSTOM_META_KEYS: readonly string[];
9
+ /** The capability state a pooled connection derives from its initialize response. */
10
+ export interface NegotiatedCapabilities {
11
+ /** The protocol version the agent selected (echoes the client's when supported, else the agent's
12
+ * latest). Validated by isSupportedProtocolVersion before the connection is used. */
13
+ protocolVersion: number;
14
+ /** The agent's full advertised capabilities — an empty object when the agent sent none (every
15
+ * capability is then UNSUPPORTED per the ACP spec). */
16
+ agent: AgentCapabilities;
17
+ /** The agent's self-identification, when it sent agentInfo. */
18
+ agentInfo: Implementation | undefined;
19
+ /** Whether session/close is advertised (gates the best-effort release-time close). */
20
+ supportsClose: boolean;
21
+ /** The parsed backend-declared custom-capability block (the namespaced `_meta` object), or
22
+ * undefined when the backend declared none or the agent did not advertise it — passthrough. */
23
+ customMetaSupport: Record<string, unknown> | undefined;
24
+ /** The backend-declared bare `_meta` keys gated by customMetaSupport; undefined when this backend
25
+ * has no custom-capability contract, so custom `_meta` is never gated. */
26
+ gatedKeys: readonly string[] | undefined;
27
+ }
28
+ /** Parse an initialize response into the connection's derived capability state. */
29
+ export declare function negotiateCapabilities(response: InitializeResponse, customCapabilities?: Backend["customCapabilities"]): NegotiatedCapabilities;
30
+ /** True only when the agent selected EXACTLY PROTOCOL_VERSION. This client implements that one wire
31
+ * version and adapts its behavior to no other, so any other selected version — older or newer —
32
+ * means close the connection per the ACP spec's SHOULD-close rule. Per the spec the agent echoes
33
+ * the requested version when it supports it, else its own latest; we do NOT accept older versions
34
+ * (we cannot speak them) — the equality is the whole test. */
35
+ export declare function isSupportedProtocolVersion(version: number): boolean;
36
+ /** Remove the declared custom bare `_meta` keys the connected agent did NOT advertise support for.
37
+ * A no-op when the agent advertised no namespace (`support` undefined => legacy => every key
38
+ * passes) or the meta is empty/undefined. Never mutates its input; collapses to undefined if
39
+ * gating empties the object (so no `_meta` is sent at all). */
40
+ export declare function gateCustomMeta(meta: Record<string, unknown> | undefined, support: Record<string, unknown> | undefined, gatedKeys?: readonly string[]): Record<string, unknown> | undefined;
41
+ /** Adapt prompt content to the agent's PromptCapabilities. ACP baseline content (text and
42
+ * resource_link) is never gated; optional blocks follow the spec's capability table:
43
+ * image->promptCapabilities.image, audio->promptCapabilities.audio, and resource->
44
+ * promptCapabilities.embeddedContext. Unsupported optional blocks are represented as explicit
45
+ * bracketed text notes so context is never silently lost. Returns the SAME array reference when
46
+ * no block changes and never mutates the input or any surviving block. */
47
+ export declare function adaptPromptContent(blocks: ContentBlock[], agent: AgentCapabilities, backendId: string): ContentBlock[];
48
+ /** The first client-provided MCP server whose transport the agent did NOT advertise, or undefined
49
+ * when every server is serviceable. stdio is ALWAYS serviceable (the baseline transport); http/sse
50
+ * are gated on mcpCapabilities.{http,sse}. Lenient for legacy agents: when the agent advertised no
51
+ * mcpCapabilities at all we cannot know its transports, so we do not gate (preserving today's
52
+ * send-and-let-the-agent-decide behavior for minimal/custom servers). */
53
+ export declare function unsupportedMcpServer(servers: McpServerConfig[] | undefined, agent: AgentCapabilities): {
54
+ name: string;
55
+ transport: "http" | "sse";
56
+ } | undefined;
57
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAkBA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAQ5C;;;iGAGiG;AACjG,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAInD,CAAC;AAEF,qFAAqF;AACrF,MAAM,WAAW,sBAAsB;IACrC;0FACsF;IACtF,eAAe,EAAE,MAAM,CAAC;IACxB;4DACwD;IACxD,KAAK,EAAE,iBAAiB,CAAC;IACzB,+DAA+D;IAC/D,SAAS,EAAE,cAAc,GAAG,SAAS,CAAC;IACtC,sFAAsF;IACtF,aAAa,EAAE,OAAO,CAAC;IACvB;oGACgG;IAChG,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACvD;+EAC2E;IAC3E,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAC1C;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,kBAAkB,EAC5B,kBAAkB,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,GACjD,sBAAsB,CAYxB;AAaD;;;;+DAI+D;AAC/D,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED;;;gEAGgE;AAChE,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC5C,SAAS,GAAE,SAAS,MAAM,EAA2B,GACpD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAWrC;AAED;;;;;2EAK2E;AAC3E,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EAAE,EACtB,KAAK,EAAE,iBAAiB,EACxB,SAAS,EAAE,MAAM,GAChB,YAAY,EAAE,CAchB;AAkCD;;;;0EAI0E;AAC1E,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,eAAe,EAAE,GAAG,SAAS,EACtC,KAAK,EAAE,iBAAiB,GACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAAA;CAAE,GAAG,SAAS,CAYzD"}
@@ -0,0 +1,147 @@
1
+ // Negotiated ACP capabilities for one pooled connection — the derived state the connection acts on
2
+ // after its ONE-TIME `initialize` handshake. Parses the InitializeResponse + the backend's optional
3
+ // custom-capability declaration into: the agent's chosen protocolVersion (validated against what this
4
+ // client speaks), its full advertised agentCapabilities + agentInfo, whether session/close is
5
+ // supported, and the declared custom-capability advertisement (the namespaced `_meta` block gating
6
+ // which backend-declared bare `_meta` inputs the client may send). The negotiated record is a pure
7
+ // parse of (response + declaration): `gatedKeys` captures the declaration at handshake time, and
8
+ // PooledConnection gates against that captured list.
9
+ //
10
+ // GATING PHILOSOPHY — backend-declared and lenient for legacy agents. A backend with NO declaration
11
+ // has NO custom-capability contract: its custom `_meta`, if any, is never gated, even when the agent
12
+ // advertises some other backend's namespace. Once a backend declares a namespace + bare keys, an
13
+ // agent that advertises NO usable namespace block is legacy passthrough (every declared key still
14
+ // sent); malformed namespace values (non-object or arrays) are treated as not advertised. Only once
15
+ // an agent DOES advertise the declared namespace object is each declared bare key gated on its
16
+ // same-named flag === true. Symmetrically, only once an agent advertises mcpCapabilities is an
17
+ // unsupported MCP transport rejected. Truthfully-advertised absence still gates WITHIN an advertised
18
+ // capability; total silence is the legacy passthrough.
19
+ import { PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
20
+ import { CODEX_META_KEYS, META_KEYS, } from "@automatalabs/shared-types";
21
+ const PROMPT_CAPABILITY_BY_BLOCK_KIND = {
22
+ image: "image",
23
+ audio: "audio",
24
+ resource: "embeddedContext",
25
+ };
26
+ /** The bare `_meta` keys whose emission is gated by Codex's custom-capability advertisement.
27
+ * Each is named EXACTLY like the advertised flag that gates it, so `support[key] === true` is the
28
+ * whole test. session/new carries baseInstructions/developerInstructions and session/prompt carries
29
+ * outputSchema — one list covers both (a key absent from a given `_meta` is simply skipped). */
30
+ export const GATED_CUSTOM_META_KEYS = [
31
+ META_KEYS.outputSchema,
32
+ CODEX_META_KEYS.baseInstructions,
33
+ CODEX_META_KEYS.developerInstructions,
34
+ ];
35
+ /** Parse an initialize response into the connection's derived capability state. */
36
+ export function negotiateCapabilities(response, customCapabilities) {
37
+ const agent = response.agentCapabilities ?? {};
38
+ return {
39
+ protocolVersion: response.protocolVersion,
40
+ agent,
41
+ agentInfo: response.agentInfo ?? undefined,
42
+ supportsClose: Boolean(agent.sessionCapabilities?.close),
43
+ customMetaSupport: customCapabilities
44
+ ? readCustomNamespace(agent._meta, customCapabilities.namespace)
45
+ : undefined,
46
+ gatedKeys: customCapabilities ? [...customCapabilities.gatedKeys] : undefined,
47
+ };
48
+ }
49
+ function readCustomNamespace(meta, namespace) {
50
+ if (!meta || typeof meta !== "object")
51
+ return undefined;
52
+ const block = meta[namespace];
53
+ return block && typeof block === "object" && !Array.isArray(block)
54
+ ? block
55
+ : undefined;
56
+ }
57
+ /** True only when the agent selected EXACTLY PROTOCOL_VERSION. This client implements that one wire
58
+ * version and adapts its behavior to no other, so any other selected version — older or newer —
59
+ * means close the connection per the ACP spec's SHOULD-close rule. Per the spec the agent echoes
60
+ * the requested version when it supports it, else its own latest; we do NOT accept older versions
61
+ * (we cannot speak them) — the equality is the whole test. */
62
+ export function isSupportedProtocolVersion(version) {
63
+ return version === PROTOCOL_VERSION;
64
+ }
65
+ /** Remove the declared custom bare `_meta` keys the connected agent did NOT advertise support for.
66
+ * A no-op when the agent advertised no namespace (`support` undefined => legacy => every key
67
+ * passes) or the meta is empty/undefined. Never mutates its input; collapses to undefined if
68
+ * gating empties the object (so no `_meta` is sent at all). */
69
+ export function gateCustomMeta(meta, support, gatedKeys = GATED_CUSTOM_META_KEYS) {
70
+ if (!meta || !support)
71
+ return meta;
72
+ let gated;
73
+ for (const key of gatedKeys) {
74
+ if (key in meta && support[key] !== true) {
75
+ gated ??= { ...meta };
76
+ delete gated[key];
77
+ }
78
+ }
79
+ const result = gated ?? meta;
80
+ return Object.keys(result).length > 0 ? result : undefined;
81
+ }
82
+ /** Adapt prompt content to the agent's PromptCapabilities. ACP baseline content (text and
83
+ * resource_link) is never gated; optional blocks follow the spec's capability table:
84
+ * image->promptCapabilities.image, audio->promptCapabilities.audio, and resource->
85
+ * promptCapabilities.embeddedContext. Unsupported optional blocks are represented as explicit
86
+ * bracketed text notes so context is never silently lost. Returns the SAME array reference when
87
+ * no block changes and never mutates the input or any surviving block. */
88
+ export function adaptPromptContent(blocks, agent, backendId) {
89
+ let adapted;
90
+ for (let i = 0; i < blocks.length; i += 1) {
91
+ const block = blocks[i];
92
+ const replacement = unsupportedPromptBlockNote(block, agent, backendId);
93
+ if (replacement) {
94
+ adapted ??= blocks.slice(0, i);
95
+ adapted.push(replacement);
96
+ }
97
+ else if (adapted) {
98
+ adapted.push(block);
99
+ }
100
+ }
101
+ return adapted ?? blocks;
102
+ }
103
+ function unsupportedPromptBlockNote(block, agent, backendId) {
104
+ const capability = PROMPT_CAPABILITY_BY_BLOCK_KIND[block.type];
105
+ if (!capability || agent.promptCapabilities?.[capability] === true)
106
+ return undefined;
107
+ switch (block.type) {
108
+ case "image": {
109
+ const uriSuffix = typeof block.uri === "string" && block.uri.length > 0 ? `; uri=${block.uri}` : "";
110
+ return {
111
+ type: "text",
112
+ text: `[image omitted: ${block.mimeType}${uriSuffix} — the ${backendId} agent does not advertise promptCapabilities.image]`,
113
+ };
114
+ }
115
+ case "audio":
116
+ return {
117
+ type: "text",
118
+ text: `[audio omitted: ${block.mimeType} — the ${backendId} agent does not advertise promptCapabilities.audio]`,
119
+ };
120
+ case "resource":
121
+ return {
122
+ type: "text",
123
+ text: `[resource omitted: uri=${block.resource.uri} — the ${backendId} agent does not advertise promptCapabilities.embeddedContext]`,
124
+ };
125
+ default:
126
+ return undefined;
127
+ }
128
+ }
129
+ /** The first client-provided MCP server whose transport the agent did NOT advertise, or undefined
130
+ * when every server is serviceable. stdio is ALWAYS serviceable (the baseline transport); http/sse
131
+ * are gated on mcpCapabilities.{http,sse}. Lenient for legacy agents: when the agent advertised no
132
+ * mcpCapabilities at all we cannot know its transports, so we do not gate (preserving today's
133
+ * send-and-let-the-agent-decide behavior for minimal/custom servers). */
134
+ export function unsupportedMcpServer(servers, agent) {
135
+ const mcp = agent.mcpCapabilities;
136
+ if (!mcp || !servers)
137
+ return undefined;
138
+ for (const server of servers) {
139
+ if ("type" in server && server.type === "http" && mcp.http !== true) {
140
+ return { name: server.name, transport: "http" };
141
+ }
142
+ if ("type" in server && server.type === "sse" && mcp.sse !== true) {
143
+ return { name: server.name, transport: "sse" };
144
+ }
145
+ }
146
+ return undefined;
147
+ }
@@ -0,0 +1,27 @@
1
+ import type { ClientCapabilities, CreateTerminalRequest, CreateTerminalResponse, KillTerminalRequest, KillTerminalResponse, ReadTextFileRequest, ReadTextFileResponse, ReleaseTerminalRequest, ReleaseTerminalResponse, TerminalOutputRequest, TerminalOutputResponse, WaitForTerminalExitRequest, WaitForTerminalExitResponse, WriteTextFileRequest, WriteTextFileResponse } from "@agentclientprotocol/sdk";
2
+ export interface AcpSessionContext {
3
+ readonly sessionId: string;
4
+ readonly cwd: string;
5
+ readonly label?: string;
6
+ readonly runId?: string;
7
+ }
8
+ export interface FsHandlers {
9
+ readTextFile?(params: ReadTextFileRequest, ctx: AcpSessionContext): Promise<ReadTextFileResponse> | ReadTextFileResponse;
10
+ writeTextFile?(params: WriteTextFileRequest, ctx: AcpSessionContext): Promise<WriteTextFileResponse | void> | WriteTextFileResponse | void;
11
+ }
12
+ export interface TerminalHandlers {
13
+ createTerminal(params: CreateTerminalRequest, ctx: AcpSessionContext): Promise<CreateTerminalResponse> | CreateTerminalResponse;
14
+ terminalOutput(params: TerminalOutputRequest, ctx: AcpSessionContext): Promise<TerminalOutputResponse> | TerminalOutputResponse;
15
+ waitForTerminalExit(params: WaitForTerminalExitRequest, ctx: AcpSessionContext): Promise<WaitForTerminalExitResponse> | WaitForTerminalExitResponse;
16
+ killTerminal(params: KillTerminalRequest, ctx: AcpSessionContext): Promise<KillTerminalResponse | void> | KillTerminalResponse | void;
17
+ releaseTerminal(params: ReleaseTerminalRequest, ctx: AcpSessionContext): Promise<ReleaseTerminalResponse | void> | ReleaseTerminalResponse | void;
18
+ }
19
+ export interface ClientHandlers {
20
+ fs?: FsHandlers;
21
+ terminal?: TerminalHandlers;
22
+ }
23
+ /** The client capability advertisement derived solely from registered consumer handlers. */
24
+ export declare function clientCapabilitiesFor(handlers: ClientHandlers | undefined): ClientCapabilities;
25
+ /** Fail-fast validation for JavaScript consumers bypassing the TerminalHandlers type. */
26
+ export declare function validateClientHandlers(handlers: ClientHandlers | undefined): void;
27
+ //# sourceMappingURL=client-handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-handlers.d.ts","sourceRoot":"","sources":["../src/client-handlers.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,kBAAkB,EAClB,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,sBAAsB,EACtB,0BAA0B,EAC1B,2BAA2B,EAC3B,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,CAAC,CACX,MAAM,EAAE,mBAAmB,EAC3B,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,oBAAoB,CAAC,GAAG,oBAAoB,CAAC;IACxD,aAAa,CAAC,CACZ,MAAM,EAAE,oBAAoB,EAC5B,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,GAAG,qBAAqB,GAAG,IAAI,CAAC;CACzE;AAED,MAAM,WAAW,gBAAgB;IAC/B,cAAc,CACZ,MAAM,EAAE,qBAAqB,EAC7B,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,sBAAsB,CAAC,GAAG,sBAAsB,CAAC;IAC5D,cAAc,CACZ,MAAM,EAAE,qBAAqB,EAC7B,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,sBAAsB,CAAC,GAAG,sBAAsB,CAAC;IAC5D,mBAAmB,CACjB,MAAM,EAAE,0BAA0B,EAClC,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,2BAA2B,CAAC,GAAG,2BAA2B,CAAC;IACtE,YAAY,CACV,MAAM,EAAE,mBAAmB,EAC3B,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,GAAG,oBAAoB,GAAG,IAAI,CAAC;IACtE,eAAe,CACb,MAAM,EAAE,sBAAsB,EAC9B,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,GAAG,uBAAuB,GAAG,IAAI,CAAC;CAC7E;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,EAAE,UAAU,CAAC;IAChB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;CAC7B;AAUD,4FAA4F;AAC5F,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,cAAc,GAAG,SAAS,GAAG,kBAAkB,CAY9F;AAED,yFAAyF;AACzF,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,CAMjF"}
@@ -0,0 +1,39 @@
1
+ const TERMINAL_HANDLER_METHODS = [
2
+ "createTerminal",
3
+ "terminalOutput",
4
+ "waitForTerminalExit",
5
+ "killTerminal",
6
+ "releaseTerminal",
7
+ ];
8
+ /** The client capability advertisement derived solely from registered consumer handlers. */
9
+ export function clientCapabilitiesFor(handlers) {
10
+ if (!handlers)
11
+ return {};
12
+ const capabilities = {};
13
+ const fs = handlers.fs;
14
+ const fsCapabilities = {};
15
+ if (typeof fs?.readTextFile === "function")
16
+ fsCapabilities.readTextFile = true;
17
+ if (typeof fs?.writeTextFile === "function")
18
+ fsCapabilities.writeTextFile = true;
19
+ if (Object.keys(fsCapabilities).length > 0)
20
+ capabilities.fs = fsCapabilities;
21
+ if (hasFullTerminalHandlers(handlers.terminal))
22
+ capabilities.terminal = true;
23
+ return capabilities;
24
+ }
25
+ /** Fail-fast validation for JavaScript consumers bypassing the TerminalHandlers type. */
26
+ export function validateClientHandlers(handlers) {
27
+ if (!handlers?.terminal)
28
+ return;
29
+ const missing = missingTerminalMethods(handlers.terminal);
30
+ if (missing.length > 0) {
31
+ throw new Error(`clientHandlers.terminal missing required methods: ${missing.join(", ")}`);
32
+ }
33
+ }
34
+ function hasFullTerminalHandlers(terminal) {
35
+ return Boolean(terminal && missingTerminalMethods(terminal).length === 0);
36
+ }
37
+ function missingTerminalMethods(terminal) {
38
+ return TERMINAL_HANDLER_METHODS.filter((method) => typeof terminal[method] !== "function");
39
+ }
package/dist/index.d.ts CHANGED
@@ -4,8 +4,12 @@ export { BACKENDS_ENV, registryWithRunBackends, resolveBackendRegistry } from ".
4
4
  export type { BackendRegistry, CustomBackendConfig, RegisteredBackend } from "./registry.js";
5
5
  export { PooledConnection, SessionHandle } from "./acp-client.js";
6
6
  export type { AcpSessionOptions, PooledConnectionDeps } from "./acp-client.js";
7
+ export { GATED_CUSTOM_META_KEYS, adaptPromptContent, gateCustomMeta, isSupportedProtocolVersion, negotiateCapabilities, unsupportedMcpServer, } from "./capabilities.js";
8
+ export type { NegotiatedCapabilities } from "./capabilities.js";
7
9
  export { AcpAgentPool, resolvePoolSize } from "./pool.js";
8
10
  export type { AcpPoolOptions, AcpPoolDeps } from "./pool.js";
11
+ export { clientCapabilitiesFor } from "./client-handlers.js";
12
+ export type { AcpSessionContext, ClientHandlers, FsHandlers, TerminalHandlers } from "./client-handlers.js";
9
13
  export { TypedEventEmitter, emitSessionUpdate } from "./events.js";
10
14
  export type { AcpRunnerEventMap, AcpEventName, AcpEventListener, AcpEventContext, AcpEventSink, AcpSessionUpdate, AcpUpdateKind, AcpPermissionEvent, AcpRawMessageEvent, AcpBackendErrorEvent, } from "./events.js";
11
15
  export type { Backend, BackendId, BuiltinBackendId, SessionMetaInputs, SpawnConfig, StructuredSource, } from "./backend.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;AAC7E,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAC9F,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE7F,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,EACV,OAAO,EACP,SAAS,EACT,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD,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,cAAc,EACd,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;AAC7E,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAC9F,YAAY,EAAE,eAAe,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE7F,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAI/E,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,cAAc,EACd,0BAA0B,EAC1B,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC1D,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAI7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG5G,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,EACV,OAAO,EACP,SAAS,EACT,gBAAgB,EAChB,iBAAiB,EACjB,WAAW,EACX,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD,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,cAAc,EACd,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
@@ -8,7 +8,13 @@ export { AcpAgentRunner, createAcpRunner, selectBackend } from "./runner.js";
8
8
  // The custom-backend registry: run ANY ACP agent as an agent() target.
9
9
  export { BACKENDS_ENV, registryWithRunBackends, resolveBackendRegistry } from "./registry.js";
10
10
  export { PooledConnection, SessionHandle } from "./acp-client.js";
11
+ // ACP capability negotiation: parse/validate the initialize response and gate what the client
12
+ // sends (custom `_meta` keys, MCP transports) on what the connected agent advertised.
13
+ export { GATED_CUSTOM_META_KEYS, adaptPromptContent, gateCustomMeta, isSupportedProtocolVersion, negotiateCapabilities, unsupportedMcpServer, } from "./capabilities.js";
11
14
  export { AcpAgentPool, resolvePoolSize } from "./pool.js";
15
+ // Consumer-provided client-side ACP handlers: fs/terminal routing plus truthful initialize
16
+ // clientCapabilities derived from the registered handler set.
17
+ export { clientCapabilitiesFor } from "./client-handlers.js";
12
18
  // The typed ACP event bus surfaced on AcpAgentRunner (`runner.on(name, evt => …)`).
13
19
  export { TypedEventEmitter, emitSessionUpdate } from "./events.js";
14
20
  export { ClaudeBackend } from "./backends/claude.js";
package/dist/pool.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import type { Backend } from "./backend.js";
2
2
  import { SessionHandle, type AcpSessionOptions } from "./acp-client.js";
3
+ import { type ClientHandlers } from "./client-handlers.js";
3
4
  import type { AcpEventSink } from "./events.js";
4
5
  export interface AcpPoolOptions {
5
6
  /** Long-lived processes to keep PER backend. Default 1; falls back to AGENTPRISM_ACP_POOL_SIZE. */
6
7
  size?: number;
8
+ /** Client-side ACP fs/terminal handlers advertised at initialize and routed per session. */
9
+ clientHandlers?: ClientHandlers;
7
10
  }
8
11
  /** Internal wiring the runner injects (NOT part of the public AcpPoolOptions surface): the typed
9
12
  * event sink forwarded to every PooledConnection so ACP events bubble up to `runner.on(...)`. */
@@ -15,6 +18,7 @@ export declare function resolvePoolSize(option?: number): number;
15
18
  export declare class AcpAgentPool {
16
19
  private readonly deps;
17
20
  private readonly size;
21
+ private readonly clientHandlers;
18
22
  private readonly byBackend;
19
23
  private readonly onProcessExit;
20
24
  private exitHookInstalled;
@@ -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;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;IAwBxB,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,EAA0B,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,MAAM,WAAW,cAAc;IAC7B,mGAAmG;IACnG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4FAA4F;IAC5F,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;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;IAUrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IATvB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,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;IAOzC,8FAA8F;IACxF,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC;IAMhF;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAyBxB,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
@@ -1,4 +1,5 @@
1
1
  import { PooledConnection } from "./acp-client.js";
2
+ import { validateClientHandlers } from "./client-handlers.js";
2
3
  const DEFAULT_POOL_SIZE = 1;
3
4
  const POOL_SIZE_ENV = "AGENTPRISM_ACP_POOL_SIZE";
4
5
  /** Resolve the per-backend pool size: explicit option wins, else env, else 1. Clamped to >= 1. */
@@ -17,13 +18,16 @@ export function resolvePoolSize(option) {
17
18
  export class AcpAgentPool {
18
19
  deps;
19
20
  size;
21
+ clientHandlers;
20
22
  byBackend = new Map();
21
23
  onProcessExit = () => this.killAllSync();
22
24
  exitHookInstalled = false;
23
25
  disposed = false;
24
26
  constructor(options = {}, deps = {}) {
25
27
  this.deps = deps;
28
+ validateClientHandlers(options.clientHandlers);
26
29
  this.size = resolvePoolSize(options.size);
30
+ this.clientHandlers = options.clientHandlers;
27
31
  }
28
32
  /** Acquire a session for one agent() run: get/grow a pooled connection and open a session. */
29
33
  async acquire(backend, opts) {
@@ -51,6 +55,7 @@ export class AcpAgentPool {
51
55
  const connection = PooledConnection.create(backend, {
52
56
  onDead: (dead) => this.drop(key, dead),
53
57
  onEvent: this.deps.onEvent,
58
+ clientHandlers: this.clientHandlers,
54
59
  });
55
60
  connections.push(connection);
56
61
  return connection;
@@ -10,6 +10,12 @@ export interface CustomBackendConfig {
10
10
  /** Static `_meta` sent on every session/new for this backend (backend-level defaults).
11
11
  * Per-call RunOptions.meta merges over these; backend-computed keys win over both. */
12
12
  sessionMeta?: Record<string, unknown>;
13
+ /** agentCapabilities._meta namespace + bare `_meta` keys this custom agent negotiates.
14
+ * Undefined means the backend's custom `_meta`, if any, is never gated. */
15
+ customCapabilities?: {
16
+ readonly namespace: string;
17
+ readonly gatedKeys: readonly string[];
18
+ };
13
19
  }
14
20
  /** A validated registry entry: the (lowercased) name plus its config. */
15
21
  export interface RegisteredBackend extends CustomBackendConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,YAAY,wBAAwB,CAAC;AAKlD,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,mFAAmF;IACnF,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B;2FACuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED,yEAAyE;AACzE,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAErE;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC5C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,eAAe,CAwBjB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,eAAe,EACrB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,GACxC,eAAe,CAQjB"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,YAAY,wBAAwB,CAAC;AAKlD,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,mFAAmF;IACnF,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B;2FACuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC;gFAC4E;IAC5E,kBAAkB,CAAC,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAC;CAC5F;AAED,yEAAyE;AACzE,MAAM,WAAW,iBAAkB,SAAQ,mBAAmB;IAC5D,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAErE;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC5C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,eAAe,CAwBjB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,eAAe,EACrB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,GACxC,eAAe,CAQjB"}
package/dist/registry.js CHANGED
@@ -81,6 +81,7 @@ function validateEntry(rawName, config, source) {
81
81
  if (c.sessionMeta !== undefined && (c.sessionMeta === null || typeof c.sessionMeta !== "object" || Array.isArray(c.sessionMeta))) {
82
82
  throw new Error(`${source}: backend "${rawName}" "sessionMeta" must be an object`);
83
83
  }
84
+ const customCapabilities = validateCustomCapabilities(c.customCapabilities, source, rawName);
84
85
  return [
85
86
  name,
86
87
  {
@@ -89,9 +90,27 @@ function validateEntry(rawName, config, source) {
89
90
  ...(c.args !== undefined ? { args: c.args } : {}),
90
91
  ...(c.env !== undefined ? { env: c.env } : {}),
91
92
  ...(c.sessionMeta !== undefined ? { sessionMeta: c.sessionMeta } : {}),
93
+ ...(customCapabilities !== undefined ? { customCapabilities } : {}),
92
94
  },
93
95
  ];
94
96
  }
97
+ function validateCustomCapabilities(value, source, rawName) {
98
+ if (value === undefined)
99
+ return undefined;
100
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
101
+ throw new Error(`${source}: backend "${rawName}" "customCapabilities" must be an object`);
102
+ }
103
+ const c = value;
104
+ if (typeof c.namespace !== "string" || c.namespace.trim() === "") {
105
+ throw new Error(`${source}: backend "${rawName}" "customCapabilities.namespace" must be a non-empty string`);
106
+ }
107
+ if (!Array.isArray(c.gatedKeys) ||
108
+ c.gatedKeys.length === 0 ||
109
+ !c.gatedKeys.every((key) => typeof key === "string" && key.trim() !== "")) {
110
+ throw new Error(`${source}: backend "${rawName}" "customCapabilities.gatedKeys" must be a non-empty array of non-empty strings`);
111
+ }
112
+ return { namespace: c.namespace, gatedKeys: [...c.gatedKeys] };
113
+ }
95
114
  function isStringRecord(value) {
96
115
  return (value !== null &&
97
116
  typeof value === "object" &&
package/dist/runner.d.ts CHANGED
@@ -4,8 +4,8 @@ import { type AcpPoolOptions } from "./pool.js";
4
4
  import { type AcpEventListener, type AcpEventName } from "./events.js";
5
5
  import type { Backend } from "./backend.js";
6
6
  import { type BackendRegistry, type CustomBackendConfig } from "./registry.js";
7
- /** Constructor options for the runner: pool sizing PLUS the custom-backend registry.
8
- * `backends` merges over (and wins against) env-declared AGENTPRISM_BACKENDS entries. */
7
+ /** Constructor options for the runner: pool sizing, client-side handlers, and the custom-backend
8
+ * registry. `backends` merges over (and wins against) env-declared AGENTPRISM_BACKENDS entries. */
9
9
  export interface AcpRunnerOptions extends AcpPoolOptions {
10
10
  /** Custom ACP backends, keyed by registered name (see registry.ts for the config shape
11
11
  * and the routing rules). Names are case-insensitive; "claude"/"codex" are reserved. */
@@ -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,EAEL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAGlB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAI5C,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACzB,MAAM,eAAe,CAAC;AAQvB;0FAC0F;AAC1F,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD;6FACyF;IACzF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAChD;AAED,qBAAa,cAAe,YAAW,WAAW;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC,sFAAsF;IACtF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C;uGACmG;IACnG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8C;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgE;gBAE9E,OAAO,GAAE,gBAAqB;IAK1C;;;;;;;;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;IA2G1B;qGACiG;IAC3F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAI/B;AAED;;;qDAGqD;AACrD,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,cAAc,CAE1E;AAmFD;;;;oCAIoC;AACpC,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,QAAQ,CAAC,EAAE,eAAe,GAAG,OAAO,CAM1G"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAkBA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,WAAW,EAEhB,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;AAI5C,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACzB,MAAM,eAAe,CAAC;AAQvB;oGACoG;AACpG,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD;6FACyF;IACzF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAChD;AAED,qBAAa,cAAe,YAAW,WAAW;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC,sFAAsF;IACtF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C;uGACmG;IACnG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8C;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgE;gBAE9E,OAAO,GAAE,gBAAqB;IAK1C;;;;;;;;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;IA8G1B;qGACiG;IAC3F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAI/B;AAED;;;qDAGqD;AACrD,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,cAAc,CAE1E;AA2HD;;;;oCAIoC;AACpC,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,QAAQ,CAAC,EAAE,eAAe,GAAG,OAAO,CAM1G"}
package/dist/runner.js CHANGED
@@ -82,6 +82,7 @@ export class AcpAgentRunner {
82
82
  const backend = selectBackend(opts, registry);
83
83
  const policy = { allow: opts.toolNames, deny: opts.disallowedToolNames };
84
84
  const cwd = opts.cwd ?? process.cwd();
85
+ validatePromptImages(opts.images, opts.label);
85
86
  const session = await this.pool.acquire(backend, {
86
87
  cwd,
87
88
  schema,
@@ -107,10 +108,11 @@ export class AcpAgentRunner {
107
108
  // full spec unchanged (their catalogs match provider-prefixed and bare ids).
108
109
  await applyModelSelection(session, innerModelSpec(opts.model ?? opts.tier, backend), opts);
109
110
  const text = buildPrompt(prompt, opts, schema, backend);
111
+ const initialPrompt = opts.images && opts.images.length > 0 ? promptWithImages(text, opts.images) : text;
110
112
  // Generic turn-scoped _meta passthrough merged UNDER the backend-computed keys (e.g. the
111
113
  // outputSchema forward when a schema is set) — user meta never clobbers the schema channel.
112
114
  const promptMeta = mergeTurnMeta(opts.promptMeta, backend.promptMeta(schema));
113
- const response = await session.prompt(text, promptMeta);
115
+ const response = await session.prompt(initialPrompt, promptMeta);
114
116
  opts.signal?.throwIfAborted();
115
117
  // Inspect the turn's stop reason BEFORE the text/schema path: a refusal or truncation
116
118
  // must surface distinctly here, never be misread as empty output or burned through the
@@ -257,6 +259,31 @@ function buildPrompt(prompt, opts, schema, backend) {
257
259
  }
258
260
  return parts.join("\n\n");
259
261
  }
262
+ function validatePromptImages(images, label) {
263
+ if (!images || images.length === 0)
264
+ return;
265
+ for (let i = 0; i < images.length; i += 1) {
266
+ const image = images[i];
267
+ if (typeof image?.data !== "string" || image.data.trim() === "") {
268
+ throw new WorkflowError(`images[${i}].data must be a non-empty string`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: label });
269
+ }
270
+ if (typeof image.mimeType !== "string" || image.mimeType.trim() === "") {
271
+ throw new WorkflowError(`images[${i}].mimeType must be a non-empty string`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: label });
272
+ }
273
+ if (image.uri !== undefined && (typeof image.uri !== "string" || image.uri.trim() === "")) {
274
+ throw new WorkflowError(`images[${i}].uri must be a non-empty string when present`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: label });
275
+ }
276
+ }
277
+ }
278
+ function promptWithImages(text, images) {
279
+ const blocks = [{ type: "text", text }];
280
+ for (const image of images) {
281
+ blocks.push(image.uri === undefined
282
+ ? { type: "image", data: image.data, mimeType: image.mimeType }
283
+ : { type: "image", data: image.data, mimeType: image.mimeType, uri: image.uri });
284
+ }
285
+ return blocks;
286
+ }
260
287
  /** Pick the backend by model/tier. Cross-provider routing = which ACP server to spawn.
261
288
  * Registered CUSTOM names resolve FIRST (exact name, or `name/<inner-model>` prefix) so a
262
289
  * registry entry is never shadowed by the built-in heuristics; then the claude/codex
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automatalabs/acp-agents",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,11 +24,11 @@
24
24
  "access": "public"
25
25
  },
26
26
  "dependencies": {
27
- "@agentclientprotocol/sdk": "1.0.0",
28
- "@agentclientprotocol/claude-agent-acp": "0.53.0",
29
- "@automatalabs/codex-acp": "1.2.0",
27
+ "@agentclientprotocol/sdk": "1.1.0",
28
+ "@agentclientprotocol/claude-agent-acp": "0.55.0",
29
+ "@automatalabs/codex-acp": "1.3.0",
30
30
  "typebox": "1.3.2",
31
- "@automatalabs/shared-types": "0.5.0"
31
+ "@automatalabs/shared-types": "0.7.0"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsc -b",