@automatalabs/acp-agents 0.7.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,4 +1,4 @@
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";
@@ -6,6 +6,7 @@ import { type NegotiatedCapabilities } from "./capabilities.js";
6
6
  import { type AcpEventSink } from "./events.js";
7
7
  import { type ToolPolicy } from "./permissions.js";
8
8
  import { UsageAccumulator } from "./usage.js";
9
+ import { type ClientHandlers } from "./client-handlers.js";
9
10
  interface RawResultSuccess {
10
11
  type: string;
11
12
  subtype: string;
@@ -14,6 +15,7 @@ interface RawResultSuccess {
14
15
  /** Per-session accumulator: assistant text, tool history, usage, the Claude raw structured_output,
15
16
  * and the tool policy used to auto-answer permission requests for THIS session. */
16
17
  declare class SessionState {
18
+ readonly cwd: string;
17
19
  readonly policy: ToolPolicy;
18
20
  readonly label?: string | undefined;
19
21
  readonly runId?: string | undefined;
@@ -24,7 +26,7 @@ declare class SessionState {
24
26
  private turnStartIndex;
25
27
  /** `label`/`runId` are carried here ONLY so the MultiplexClient can stamp them onto emitted
26
28
  * events as context — they never affect routing or the wire request. */
27
- constructor(policy: ToolPolicy, label?: string | undefined, runId?: string | undefined);
29
+ constructor(cwd: string, policy: ToolPolicy, label?: string | undefined, runId?: string | undefined);
28
30
  /** Mark the start of a new turn so currentTurnText()/structured_output read only this turn. */
29
31
  beginTurn(): void;
30
32
  currentTurnText(): string;
@@ -61,6 +63,8 @@ export interface PooledConnectionDeps {
61
63
  * session lifecycle change on this connection is bubbled up through it (additive observability;
62
64
  * it is invoked AFTER the drain accumulation and never affects the run). */
63
65
  onEvent?: AcpEventSink;
66
+ /** Client-side ACP fs/terminal handlers advertised once and routed by sessionId. */
67
+ clientHandlers?: ClientHandlers;
64
68
  }
65
69
  /**
66
70
  * One long-lived ACP server subprocess + its held ACP client connection. Initialized ONCE and
@@ -76,6 +80,7 @@ export declare class PooledConnection {
76
80
  private readonly client;
77
81
  private readonly onDead;
78
82
  private readonly onEvent;
83
+ private readonly clientHandlers;
79
84
  /** Set true at the start of dispose() so the graceful-shutdown death is NOT reported as a crash. */
80
85
  private disposing;
81
86
  /** Resolves once `initialize` completed (or rejects if the process died first). */
@@ -98,8 +103,8 @@ export declare class PooledConnection {
98
103
  /** The capabilities negotiated on this connection's one-time initialize handshake, or undefined
99
104
  * until it completes — derived-state-behind-a-getter, like `alive`/`activeSessions`. */
100
105
  get capabilities(): NegotiatedCapabilities | undefined;
101
- /** Drop the fork's custom bare `_meta` keys the connected agent did not advertise support for
102
- * (see gateCustomMeta). Applied to BOTH session/new and session/prompt `_meta`. */
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`. */
103
108
  gateCustomMeta(meta: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
104
109
  /** Mark this connection dead exactly once, then ask the pool to evict it. Idempotent. */
105
110
  private die;
@@ -117,9 +122,9 @@ export declare class PooledConnection {
117
122
  /** Best-effort ACP cancel for one session (wired to opts.signal). The PROCESS stays pooled. */
118
123
  cancelSession(sessionId: string): Promise<void>;
119
124
  /**
120
- * Release a session: stop routing it, free the load slot, and best-effort session/close on the
121
- * wire (capability-gated, bounded, never fatal). The PROCESS is NOT killed — it returns to the
122
- * 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.
123
128
  */
124
129
  releaseSession(sessionId: string): Promise<void>;
125
130
  /** Synchronous best-effort kill for a process-exit hook (no time to await a graceful close). */
@@ -180,7 +185,7 @@ export declare class SessionHandle implements StructuredSource {
180
185
  /** Set one session config option via the wire method and adopt the echoed catalog. */
181
186
  private applyConfigOption;
182
187
  /** Send a prompt turn and drain it; returns the final PromptResponse. */
183
- prompt(text: string, promptMeta?: Record<string, unknown>): Promise<PromptResponse>;
188
+ prompt(content: string | ContentBlock[], promptMeta?: Record<string, unknown>): Promise<PromptResponse>;
184
189
  /** StructuredSource — the latest turn's assistant text. */
185
190
  currentTurnText(): string;
186
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,12 +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";
25
+ import { ClientSideConnection, CLIENT_METHODS, ndJsonStream, PROTOCOL_VERSION, RequestError, } from "@agentclientprotocol/sdk";
26
26
  import { META_KEYS, WorkflowError, WorkflowErrorCode, } from "@automatalabs/shared-types";
27
- import { gateCustomMeta, isSupportedProtocolVersion, negotiateCapabilities, unsupportedMcpServer, } from "./capabilities.js";
27
+ import { adaptPromptContent, gateCustomMeta, isSupportedProtocolVersion, negotiateCapabilities, unsupportedMcpServer, } from "./capabilities.js";
28
28
  import { emitSessionUpdate } from "./events.js";
29
29
  import { decidePermission } from "./permissions.js";
30
30
  import { UsageAccumulator } from "./usage.js";
31
+ import { clientCapabilitiesFor, } from "./client-handlers.js";
31
32
  /** A benign client identity. NOT JetBrains/IntelliJ 2026.1 — that exact identity makes
32
33
  * codex-acp disable session config options (our model/effort routing channel). */
33
34
  const CLIENT_INFO = {
@@ -40,9 +41,11 @@ const CLAUDE_RAW_MESSAGE_METHOD = "_claude/sdkMessage";
40
41
  const CLOSE_SESSION_TIMEOUT_MS = 5_000;
41
42
  /** Bound the graceful SIGTERM shutdown before escalating to SIGKILL. */
42
43
  const DISPOSE_SIGKILL_GRACE_MS = 2_000;
44
+ const TOMBSTONE_SESSION_CAP = 64;
43
45
  /** Per-session accumulator: assistant text, tool history, usage, the Claude raw structured_output,
44
46
  * and the tool policy used to auto-answer permission requests for THIS session. */
45
47
  class SessionState {
48
+ cwd;
46
49
  policy;
47
50
  label;
48
51
  runId;
@@ -53,7 +56,8 @@ class SessionState {
53
56
  turnStartIndex = 0;
54
57
  /** `label`/`runId` are carried here ONLY so the MultiplexClient can stamp them onto emitted
55
58
  * events as context — they never affect routing or the wire request. */
56
- constructor(policy, label, runId) {
59
+ constructor(cwd, policy, label, runId) {
60
+ this.cwd = cwd;
57
61
  this.policy = policy;
58
62
  this.label = label;
59
63
  this.runId = runId;
@@ -113,25 +117,60 @@ class SessionState {
113
117
  class MultiplexClient {
114
118
  backendId;
115
119
  onEvent;
120
+ handlers;
116
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();
117
127
  /** `backendId` stamps event context; `onEvent` (optional) bubbles every notification, permission
118
128
  * request and session lifecycle change up to the runner's typed bus. */
119
- constructor(backendId, onEvent) {
129
+ constructor(backendId, onEvent, handlers) {
120
130
  this.backendId = backendId;
121
131
  this.onEvent = onEvent;
132
+ this.handlers = handlers;
122
133
  }
123
134
  contextFor(sessionId, state) {
124
135
  return { sessionId, backendId: this.backendId, label: state?.label, runId: state?.runId };
125
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
+ }
126
152
  register(sessionId, state) {
153
+ this.tombstones.delete(sessionId);
127
154
  this.sessions.set(sessionId, state);
128
155
  this.onEvent?.("session_open", this.contextFor(sessionId, state));
129
156
  }
130
157
  unregister(sessionId) {
131
158
  const state = this.sessions.get(sessionId);
132
159
  this.sessions.delete(sessionId);
133
- 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
+ }
134
172
  this.onEvent?.("session_close", this.contextFor(sessionId, state));
173
+ }
135
174
  }
136
175
  requestPermission(params) {
137
176
  const state = this.sessions.get(params.sessionId);
@@ -146,6 +185,27 @@ class MultiplexClient {
146
185
  });
147
186
  return outcome;
148
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
+ }
149
209
  sessionUpdate(params) {
150
210
  const state = this.sessions.get(params.sessionId);
151
211
  // Fold into the accumulator FIRST (the drain contract), THEN bubble the event up unchanged.
@@ -172,6 +232,12 @@ class MultiplexClient {
172
232
  });
173
233
  }
174
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
+ }
175
241
  const DEFAULT_INIT_TIMEOUT_MS = 60_000;
176
242
  /** Deadline for the one-time ACP `initialize` handshake per pooled process. Overridable via
177
243
  * AGENTPRISM_ACP_INIT_TIMEOUT_MS (e.g. for slow cold-start backends). Clamped to >= 1s. */
@@ -225,6 +291,7 @@ export class PooledConnection {
225
291
  client;
226
292
  onDead;
227
293
  onEvent;
294
+ clientHandlers;
228
295
  /** Set true at the start of dispose() so the graceful-shutdown death is NOT reported as a crash. */
229
296
  disposing = false;
230
297
  /** Resolves once `initialize` completed (or rejects if the process died first). */
@@ -243,7 +310,8 @@ export class PooledConnection {
243
310
  this.backendId = backend.id;
244
311
  this.onDead = deps.onDead;
245
312
  this.onEvent = deps.onEvent;
246
- this.client = new MultiplexClient(this.backendId, this.onEvent);
313
+ this.clientHandlers = deps.clientHandlers;
314
+ this.client = new MultiplexClient(this.backendId, this.onEvent, deps.clientHandlers);
247
315
  const { command, args, env } = backend.spawnConfig();
248
316
  // NOTE: deliberately NO `cwd` here. cwd is per-SESSION (session/new), so one pooled process
249
317
  // serves runs in different worktrees without losing isolation.
@@ -296,10 +364,10 @@ export class PooledConnection {
296
364
  get capabilities() {
297
365
  return this.negotiated;
298
366
  }
299
- /** Drop the fork's custom bare `_meta` keys the connected agent did not advertise support for
300
- * (see gateCustomMeta). Applied to BOTH session/new and session/prompt `_meta`. */
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`. */
301
369
  gateCustomMeta(meta) {
302
- return gateCustomMeta(meta, this.negotiated?.customMetaSupport);
370
+ return gateCustomMeta(meta, this.negotiated?.customMetaSupport, this.negotiated?.gatedKeys);
303
371
  }
304
372
  /** Mark this connection dead exactly once, then ask the pool to evict it. Idempotent. */
305
373
  die(error) {
@@ -349,15 +417,14 @@ export class PooledConnection {
349
417
  const response = await Promise.race([
350
418
  this.race(this.rpc.initialize({
351
419
  protocolVersion: PROTOCOL_VERSION,
352
- // Truthful advertisement: MultiplexClient implements NONE of the client-side
353
- // fs/terminal methods, so we advertise none (the ACP rule is "advertise only what you
354
- // implement"). Everything omitted here is treated as unsupported by the agent.
355
- 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),
356
423
  clientInfo: { ...CLIENT_INFO },
357
424
  })),
358
425
  deadline,
359
426
  ]);
360
- const negotiated = negotiateCapabilities(response);
427
+ const negotiated = negotiateCapabilities(response, this.backend.customCapabilities);
361
428
  // Version negotiation: the agent replies with the version it chose (our requested version if
362
429
  // it supports it, else its own latest). If this client cannot speak it, the ACP spec says
363
430
  // CLOSE the connection and inform the user — kill the process (so the pool evicts it) and
@@ -395,13 +462,13 @@ export class PooledConnection {
395
462
  throw new WorkflowError(`MCP server "${unsupported.name}" uses the "${unsupported.transport}" transport, which the ` +
396
463
  `${this.backendId} agent does not support`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: opts.label });
397
464
  }
398
- const state = new SessionState(opts.policy, opts.label, opts.runId);
465
+ const state = new SessionState(opts.cwd, opts.policy, opts.label, opts.runId);
399
466
  // session/new `_meta`, layered lowest-to-highest precedence: the backend's static
400
467
  // defaults (a custom registry entry's `sessionMeta`), then the generic user passthrough
401
468
  // (opts.meta), then the backend's protocol-critical `_meta` (Claude schema channel;
402
469
  // Codex base/developer instructions), then the engine runId correlation stamp. The result
403
- // is gated against the agent's advertised custom capabilities (a Codex instruction key the
404
- // agent said it does not honor is dropped). When no layer survives, no `_meta` is sent.
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.
405
472
  const meta = this.gateCustomMeta(stampRunId(layerMeta(this.backend.sessionMetaDefaults?.(), opts.meta, this.backend.sessionMeta(opts.schema, {
406
473
  baseInstructions: opts.baseInstructions,
407
474
  developerInstructions: opts.developerInstructions,
@@ -433,9 +500,9 @@ export class PooledConnection {
433
500
  }
434
501
  }
435
502
  /**
436
- * Release a session: stop routing it, free the load slot, and best-effort session/close on the
437
- * wire (capability-gated, bounded, never fatal). The PROCESS is NOT killed — it returns to the
438
- * 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.
439
506
  */
440
507
  async releaseSession(sessionId) {
441
508
  this.client.unregister(sessionId);
@@ -623,16 +690,18 @@ export class SessionHandle {
623
690
  this.configOptions = response.configOptions;
624
691
  }
625
692
  /** Send a prompt turn and drain it; returns the final PromptResponse. */
626
- async prompt(text, promptMeta) {
693
+ async prompt(content, promptMeta) {
627
694
  this.opts.signal?.throwIfAborted();
628
695
  this.state.beginTurn();
629
- const prompt = [{ type: "text", text }];
630
- // Gate the turn `_meta` against the agent's advertised custom capabilities: the Codex
631
- // outputSchema forward is dropped when the connected agent said it does not honor it.
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.
632
701
  const gatedMeta = this.pooled.gateCustomMeta(promptMeta);
633
702
  const request = {
634
703
  sessionId: this.sessionId,
635
- prompt,
704
+ prompt: typeof prompt === "string" ? [{ type: "text", text: prompt }] : prompt,
636
705
  ...(gatedMeta ? { _meta: gatedMeta } : {}),
637
706
  };
638
707
  const response = await this.pooled.race(this.pooled.rpc.prompt(request));
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 ?? [],
@@ -1,6 +1,7 @@
1
- import { type AgentCapabilities, type Implementation, type InitializeResponse } from "@agentclientprotocol/sdk";
1
+ import { type AgentCapabilities, type ContentBlock, type Implementation, type InitializeResponse } from "@agentclientprotocol/sdk";
2
2
  import { type McpServerConfig } from "@automatalabs/shared-types";
3
- /** The bare `_meta` keys whose emission is gated by the fork's custom-capability advertisement.
3
+ import type { Backend } from "./backend.js";
4
+ /** The bare `_meta` keys whose emission is gated by Codex's custom-capability advertisement.
4
5
  * Each is named EXACTLY like the advertised flag that gates it, so `support[key] === true` is the
5
6
  * whole test. session/new carries baseInstructions/developerInstructions and session/prompt carries
6
7
  * outputSchema — one list covers both (a key absent from a given `_meta` is simply skipped). */
@@ -17,23 +18,33 @@ export interface NegotiatedCapabilities {
17
18
  agentInfo: Implementation | undefined;
18
19
  /** Whether session/close is advertised (gates the best-effort release-time close). */
19
20
  supportsClose: boolean;
20
- /** The parsed @automatalabs/codex-acp custom-capability block (the namespaced `_meta` object), or
21
- * undefined when the agent did not advertise the namespace at all the legacy passthrough. */
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. */
22
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;
23
27
  }
24
28
  /** Parse an initialize response into the connection's derived capability state. */
25
- export declare function negotiateCapabilities(response: InitializeResponse): NegotiatedCapabilities;
29
+ export declare function negotiateCapabilities(response: InitializeResponse, customCapabilities?: Backend["customCapabilities"]): NegotiatedCapabilities;
26
30
  /** True only when the agent selected EXACTLY PROTOCOL_VERSION. This client implements that one wire
27
31
  * version and adapts its behavior to no other, so any other selected version — older or newer —
28
32
  * means close the connection per the ACP spec's SHOULD-close rule. Per the spec the agent echoes
29
33
  * the requested version when it supports it, else its own latest; we do NOT accept older versions
30
34
  * (we cannot speak them) — the equality is the whole test. */
31
35
  export declare function isSupportedProtocolVersion(version: number): boolean;
32
- /** Remove the fork's custom bare `_meta` keys the connected agent did NOT advertise support for.
36
+ /** Remove the declared custom bare `_meta` keys the connected agent did NOT advertise support for.
33
37
  * A no-op when the agent advertised no namespace (`support` undefined => legacy => every key
34
38
  * passes) or the meta is empty/undefined. Never mutates its input; collapses to undefined if
35
39
  * gating empties the object (so no `_meta` is sent at all). */
36
- export declare function gateCustomMeta(meta: Record<string, unknown> | undefined, support: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
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[];
37
48
  /** The first client-provided MCP server whose transport the agent did NOT advertise, or undefined
38
49
  * when every server is serviceable. stdio is ALWAYS serviceable (the baseline transport); http/sse
39
50
  * are gated on mcpCapabilities.{http,sse}. Lenient for legacy agents: when the agent advertised no
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAeA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,4BAA4B,CAAC;AAEpC;;;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;CACxD;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,sBAAsB,CAS1F;AAUD;;;;+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,GAC3C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAWrC;AAED;;;;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"}
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"}
@@ -1,21 +1,29 @@
1
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 into: the agent's chosen
3
- // protocolVersion (validated against what this client speaks), its full advertised agentCapabilities
4
- // + agentInfo, whether session/close is supported, and the @automatalabs/codex-acp fork's custom
5
- // capability advertisement (the namespaced `_meta` block gating which bare `_meta` inputs the client
6
- // may send).
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.
7
9
  //
8
- // GATING PHILOSOPHY — lenient for legacy agents. An agent that advertises NOTHING is treated as
9
- // today's behavior (every gated key still sent), because the currently-published fork
10
- // (@automatalabs/codex-acp 1.2.0) and arbitrary custom ACP servers honor these inputs WITHOUT
11
- // advertising them; a strict "omitted => unsupported" reading would silently regress them. Only once
12
- // an agent DOES advertise the fork namespace (opts into negotiation) is each bare key gated on its
13
- // same-named flag and, symmetrically, only once an agent advertises mcpCapabilities is an
14
- // unsupported MCP transport rejected. Truthfully-advertised absence still gates (per spec) WITHIN an
15
- // advertised capability; total silence is the legacy passthrough.
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.
16
19
  import { PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
17
- import { CODEX_CUSTOM_CAPABILITY_NAMESPACE, CODEX_META_KEYS, META_KEYS, } from "@automatalabs/shared-types";
18
- /** The bare `_meta` keys whose emission is gated by the fork's custom-capability advertisement.
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.
19
27
  * Each is named EXACTLY like the advertised flag that gates it, so `support[key] === true` is the
20
28
  * whole test. session/new carries baseInstructions/developerInstructions and session/prompt carries
21
29
  * outputSchema — one list covers both (a key absent from a given `_meta` is simply skipped). */
@@ -25,20 +33,23 @@ export const GATED_CUSTOM_META_KEYS = [
25
33
  CODEX_META_KEYS.developerInstructions,
26
34
  ];
27
35
  /** Parse an initialize response into the connection's derived capability state. */
28
- export function negotiateCapabilities(response) {
36
+ export function negotiateCapabilities(response, customCapabilities) {
29
37
  const agent = response.agentCapabilities ?? {};
30
38
  return {
31
39
  protocolVersion: response.protocolVersion,
32
40
  agent,
33
41
  agentInfo: response.agentInfo ?? undefined,
34
42
  supportsClose: Boolean(agent.sessionCapabilities?.close),
35
- customMetaSupport: readCustomNamespace(agent._meta),
43
+ customMetaSupport: customCapabilities
44
+ ? readCustomNamespace(agent._meta, customCapabilities.namespace)
45
+ : undefined,
46
+ gatedKeys: customCapabilities ? [...customCapabilities.gatedKeys] : undefined,
36
47
  };
37
48
  }
38
- function readCustomNamespace(meta) {
49
+ function readCustomNamespace(meta, namespace) {
39
50
  if (!meta || typeof meta !== "object")
40
51
  return undefined;
41
- const block = meta[CODEX_CUSTOM_CAPABILITY_NAMESPACE];
52
+ const block = meta[namespace];
42
53
  return block && typeof block === "object" && !Array.isArray(block)
43
54
  ? block
44
55
  : undefined;
@@ -51,15 +62,15 @@ function readCustomNamespace(meta) {
51
62
  export function isSupportedProtocolVersion(version) {
52
63
  return version === PROTOCOL_VERSION;
53
64
  }
54
- /** Remove the fork's custom bare `_meta` keys the connected agent did NOT advertise support for.
65
+ /** Remove the declared custom bare `_meta` keys the connected agent did NOT advertise support for.
55
66
  * A no-op when the agent advertised no namespace (`support` undefined => legacy => every key
56
67
  * passes) or the meta is empty/undefined. Never mutates its input; collapses to undefined if
57
68
  * gating empties the object (so no `_meta` is sent at all). */
58
- export function gateCustomMeta(meta, support) {
69
+ export function gateCustomMeta(meta, support, gatedKeys = GATED_CUSTOM_META_KEYS) {
59
70
  if (!meta || !support)
60
71
  return meta;
61
72
  let gated;
62
- for (const key of GATED_CUSTOM_META_KEYS) {
73
+ for (const key of gatedKeys) {
63
74
  if (key in meta && support[key] !== true) {
64
75
  gated ??= { ...meta };
65
76
  delete gated[key];
@@ -68,6 +79,53 @@ export function gateCustomMeta(meta, support) {
68
79
  const result = gated ?? meta;
69
80
  return Object.keys(result).length > 0 ? result : undefined;
70
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
+ }
71
129
  /** The first client-provided MCP server whose transport the agent did NOT advertise, or undefined
72
130
  * when every server is serviceable. stdio is ALWAYS serviceable (the baseline transport); http/sse
73
131
  * are gated on mcpCapabilities.{http,sse}. Lenient for legacy agents: when the agent advertised no
@@ -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,10 +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, gateCustomMeta, isSupportedProtocolVersion, negotiateCapabilities, unsupportedMcpServer, } from "./capabilities.js";
7
+ export { GATED_CUSTOM_META_KEYS, adaptPromptContent, gateCustomMeta, isSupportedProtocolVersion, negotiateCapabilities, unsupportedMcpServer, } from "./capabilities.js";
8
8
  export type { NegotiatedCapabilities } from "./capabilities.js";
9
9
  export { AcpAgentPool, resolvePoolSize } from "./pool.js";
10
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";
11
13
  export { TypedEventEmitter, emitSessionUpdate } from "./events.js";
12
14
  export type { AcpRunnerEventMap, AcpEventName, AcpEventListener, AcpEventContext, AcpEventSink, AcpSessionUpdate, AcpUpdateKind, AcpPermissionEvent, AcpRawMessageEvent, AcpBackendErrorEvent, } from "./events.js";
13
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;AAI/E,OAAO,EACL,sBAAsB,EACtB,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;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
@@ -10,8 +10,11 @@ export { BACKENDS_ENV, registryWithRunBackends, resolveBackendRegistry } from ".
10
10
  export { PooledConnection, SessionHandle } from "./acp-client.js";
11
11
  // ACP capability negotiation: parse/validate the initialize response and gate what the client
12
12
  // sends (custom `_meta` keys, MCP transports) on what the connected agent advertised.
13
- export { GATED_CUSTOM_META_KEYS, gateCustomMeta, isSupportedProtocolVersion, negotiateCapabilities, unsupportedMcpServer, } from "./capabilities.js";
13
+ export { GATED_CUSTOM_META_KEYS, adaptPromptContent, gateCustomMeta, isSupportedProtocolVersion, negotiateCapabilities, unsupportedMcpServer, } from "./capabilities.js";
14
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";
15
18
  // The typed ACP event bus surfaced on AcpAgentRunner (`runner.on(name, evt => …)`).
16
19
  export { TypedEventEmitter, emitSessionUpdate } from "./events.js";
17
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.7.0",
3
+ "version": "0.8.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,7 +28,7 @@
28
28
  "@agentclientprotocol/claude-agent-acp": "0.55.0",
29
29
  "@automatalabs/codex-acp": "1.3.0",
30
30
  "typebox": "1.3.2",
31
- "@automatalabs/shared-types": "0.6.0"
31
+ "@automatalabs/shared-types": "0.7.0"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsc -b",