@automatalabs/acp-agents 0.4.0 → 0.5.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,7 @@ 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`, `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`, `runId`, `baseInstructions` / `developerInstructions` (Codex-only, see below), `onUsage`, `onModelResolved`, `onModelFallback`, and `onHistory`. See `@automatalabs/shared-types` for the field-by-field contract.
49
49
 
50
50
  ### Codex session instructions (`baseInstructions` / `developerInstructions`)
51
51
 
@@ -86,8 +86,9 @@ From [`src/index.ts`](./src/index.ts):
86
86
 
87
87
  - **`createAcpRunner(options?)`** — factory returning an `AcpAgentRunner` (this is what `@automatalabs/workflows` injects into the engine).
88
88
  - **`AcpAgentRunner`** — the `AgentRunner` implementation; `run(prompt, options)` and `dispose()`.
89
- - **`selectBackend({ model, tier })`** — the cross-provider routing rule: which backend a spec maps to.
90
- - **`ClaudeBackend` / `CodexBackend`** — the two backend strategies (spawn config + per-backend schema wiring).
89
+ - **`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
+ - **`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.
91
92
  - **`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`.
92
93
 
93
94
  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.
@@ -96,7 +97,8 @@ Also exported: `AcpAgentPool` / `resolvePoolSize`, `PooledConnection` / `Session
96
97
 
97
98
  | Variable | Effect |
98
99
  | --- | --- |
99
- | `AGENTPRISM_DEFAULT_BACKEND` | Backend when `model`/`tier` don't pick one (`codex` selects Codex; anything else is Claude). |
100
+ | `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": {…}}}`. |
100
102
  | `AGENTPRISM_ACP_POOL_SIZE` | Long-lived processes to keep per backend (default `1`). |
101
103
  | `AGENTPRISM_CLAUDE_ACP_CMD` / `AGENTPRISM_CLAUDE_ACP_ARGS` | Override the command (and args) used to spawn the Claude ACP server. |
102
104
  | `AGENTPRISM_CODEX_ACP_CMD` / `AGENTPRISM_CODEX_ACP_ARGS` | Override the command (and args) used to spawn the Codex ACP server. |
@@ -39,6 +39,10 @@ export interface AcpSessionOptions {
39
39
  signal?: AbortSignal;
40
40
  /** Client-provided MCP servers to attach at session/new. Omitted => `[]` (the default). */
41
41
  mcpServers?: McpServerConfig[];
42
+ /** Generic session-scoped `_meta` passthrough (RunOptions.meta). Merged FIRST, under the
43
+ * backend-computed `_meta` and the runId stamp, so user keys never clobber the schema /
44
+ * correlation channels. Omitted => the request `_meta` is whatever the backend set. */
45
+ meta?: Record<string, unknown>;
42
46
  /** Engine run id, stamped onto session/new `_meta` (META_KEYS.runId) as a correlation id.
43
47
  * Omitted => no runId `_meta` is stamped (the request `_meta` is whatever the backend set). */
44
48
  runId?: string;
@@ -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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"}
@@ -171,6 +171,14 @@ class MultiplexClient {
171
171
  });
172
172
  }
173
173
  }
174
+ /** Shallow-merge `_meta` layers lowest-to-highest precedence, treating empty layers as absent
175
+ * so an unconfigured session keeps sending NO `_meta` at all. */
176
+ function layerMeta(...layers) {
177
+ const present = layers.filter((l) => Boolean(l && Object.keys(l).length > 0));
178
+ if (present.length === 0)
179
+ return undefined;
180
+ return Object.assign({}, ...present);
181
+ }
174
182
  /** Merge the engine runId correlation stamp into a backend's session/new `_meta`. Returns the
175
183
  * meta unchanged when no runId is given (so a backend that sends no `_meta` keeps sending none). */
176
184
  function stampRunId(meta, runId) {
@@ -316,12 +324,15 @@ export class PooledConnection {
316
324
  try {
317
325
  await this.ready;
318
326
  const state = new SessionState(opts.policy, opts.label, opts.runId);
319
- // The backend's vendor `_meta` (Claude schema channel; Codex base/developer instructions)
320
- // plus the optional engine runId correlation stamp. When none is present, no `_meta` is sent.
321
- const meta = stampRunId(this.backend.sessionMeta(opts.schema, {
327
+ // session/new `_meta`, layered lowest-to-highest precedence: the backend's static
328
+ // defaults (a custom registry entry's `sessionMeta`), then the generic user passthrough
329
+ // (opts.meta), then the backend's protocol-critical `_meta` (Claude schema channel;
330
+ // Codex base/developer instructions), then the engine runId correlation stamp. When no
331
+ // layer is present, no `_meta` is sent.
332
+ const meta = stampRunId(layerMeta(this.backend.sessionMetaDefaults?.(), opts.meta, this.backend.sessionMeta(opts.schema, {
322
333
  baseInstructions: opts.baseInstructions,
323
334
  developerInstructions: opts.developerInstructions,
324
- }), opts.runId);
335
+ })), opts.runId);
325
336
  const request = {
326
337
  cwd: opts.cwd,
327
338
  // Client-provided MCP servers (additive run input), else the default empty list.
package/dist/backend.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { TSchema } from "typebox";
2
- export type BackendId = "claude" | "codex";
2
+ /** The two built-in backends. Custom registry backends extend the id space beyond these. */
3
+ export type BuiltinBackendId = "claude" | "codex";
4
+ /** A backend id: one of the built-ins, or the registered name of a custom ACP backend
5
+ * (see registry.ts). The pool keys connections by this id, so ids must be stable. */
6
+ export type BackendId = string;
3
7
  export interface SpawnConfig {
4
8
  command: string;
5
9
  args: string[];
@@ -24,9 +28,14 @@ export interface Backend {
24
28
  readonly id: BackendId;
25
29
  /** How to launch this backend's ACP server over stdio. */
26
30
  spawnConfig(): SpawnConfig;
27
- /** `_meta` for session/new (undefined when this backend carries nothing there). `inputs`
28
- * carries optional per-session extras (e.g. Codex base/developer instructions); a backend
29
- * that has no use for them ignores it. */
31
+ /** OPTIONAL backend-level `_meta` DEFAULTS for session/new (e.g. a custom registry entry's
32
+ * static `sessionMeta`). Lowest precedence: per-call RunOptions.meta merges OVER these,
33
+ * and sessionMeta()'s protocol-critical keys merge over both. */
34
+ sessionMetaDefaults?(): Record<string, unknown> | undefined;
35
+ /** PROTOCOL-CRITICAL `_meta` for session/new (undefined when this backend carries nothing
36
+ * there) — e.g. the Claude schema channel. Highest precedence below the runId stamp: these
37
+ * keys win over the generic user passthrough. `inputs` carries optional per-session extras
38
+ * (e.g. Codex base/developer instructions); a backend that has no use for them ignores it. */
30
39
  sessionMeta(schema: TSchema | undefined, inputs?: SessionMetaInputs): Record<string, unknown> | undefined;
31
40
  /** `_meta` for session/prompt (undefined when this backend carries the schema at session/new). */
32
41
  promptMeta(schema: TSchema | undefined): Record<string, unknown> | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3C,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED,8FAA8F;AAC9F,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,eAAe,IAAI,MAAM,CAAC;IAC1B,uGAAuG;IACvG,mBAAmB,IAAI,OAAO,CAAC;CAChC;AAED;+EAC+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,yFAAyF;IACzF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sFAAsF;IACtF,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,0DAA0D;IAC1D,WAAW,IAAI,WAAW,CAAC;IAC3B;;+CAE2C;IAC3C,WAAW,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC1G,kGAAkG;IAClG,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC7E,oGAAoG;IACpG,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC;CACrD;AAED,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAE7D"}
1
+ {"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,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 +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;CAoBpD"}
1
+ {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/backends/codex.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAO/F,qBAAa,YAAa,YAAW,OAAO;IAC1C,QAAQ,CAAC,EAAE,EAAG,OAAO,CAAU;IAE/B,WAAW,IAAI,WAAW;IAe1B,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAY1G,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAK5E,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO;CAKpD"}
@@ -10,7 +10,7 @@ import { createRequire } from "node:module";
10
10
  import { CODEX_META_KEYS, META_KEYS } from "@automatalabs/shared-types";
11
11
  import { splitArgs } from "../backend.js";
12
12
  import { toStrictJsonSchema } from "../schema-strict.js";
13
- import { findJsonBlock } from "../structured-output.js";
13
+ import { parseFinalJson } from "../structured-output.js";
14
14
  const require = createRequire(import.meta.url);
15
15
  export class CodexBackend {
16
16
  id = "codex";
@@ -46,26 +46,8 @@ export class CodexBackend {
46
46
  return { [META_KEYS.outputSchema]: toStrictJsonSchema(schema) };
47
47
  }
48
48
  nativeStructured(source) {
49
- const text = source.currentTurnText().trim();
50
- if (!text)
51
- return undefined;
52
- // The constrained final message is pure JSON; parse it directly, then fall back to a
53
- // balanced-block extraction if the turn also emitted leading prose.
54
- try {
55
- return JSON.parse(text);
56
- }
57
- catch {
58
- // fall through to block extraction
59
- }
60
- const block = findJsonBlock(text);
61
- if (block !== undefined) {
62
- try {
63
- return JSON.parse(block);
64
- }
65
- catch {
66
- // give up; the runner's ladder will re-prompt / extract.
67
- }
68
- }
69
- return undefined;
49
+ // The constrained final message is pure JSON; parse it directly, with a balanced-block
50
+ // fallback if the turn also emitted leading prose.
51
+ return parseFinalJson(source.currentTurnText());
70
52
  }
71
53
  }
@@ -0,0 +1,14 @@
1
+ import type { TSchema } from "typebox";
2
+ import type { Backend, SpawnConfig, StructuredSource } from "../backend.js";
3
+ import type { RegisteredBackend } from "../registry.js";
4
+ export declare class CustomAcpBackend implements Backend {
5
+ private readonly config;
6
+ readonly id: string;
7
+ constructor(config: RegisteredBackend);
8
+ spawnConfig(): SpawnConfig;
9
+ sessionMetaDefaults(): Record<string, unknown> | undefined;
10
+ sessionMeta(_schema: TSchema | undefined): Record<string, unknown> | undefined;
11
+ promptMeta(schema: TSchema | undefined): Record<string, unknown> | undefined;
12
+ nativeStructured(source: StructuredSource): unknown;
13
+ }
14
+ //# sourceMappingURL=custom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom.d.ts","sourceRoot":"","sources":["../../src/backends/custom.ts"],"names":[],"mappings":"AAaA,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;IAGlC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;gBAES,MAAM,EAAE,iBAAiB;IAItD,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"}
@@ -0,0 +1,43 @@
1
+ import { META_KEYS } from "@automatalabs/shared-types";
2
+ import { toJsonSchema } from "../schema-strict.js";
3
+ import { parseFinalJson } from "../structured-output.js";
4
+ export class CustomAcpBackend {
5
+ config;
6
+ id;
7
+ constructor(config) {
8
+ this.config = config;
9
+ this.id = config.name;
10
+ }
11
+ spawnConfig() {
12
+ return {
13
+ command: this.config.command,
14
+ args: [...(this.config.args ?? [])],
15
+ // Registry-declared env merges OVER the inherited environment.
16
+ env: { ...process.env, ...(this.config.env ?? {}) },
17
+ };
18
+ }
19
+ sessionMetaDefaults() {
20
+ // The registry entry's static `_meta` — DEFAULTS, so per-call RunOptions.meta overrides
21
+ // them. Return a copy so callers can merge without mutating config.
22
+ const staticMeta = this.config.sessionMeta;
23
+ if (!staticMeta || Object.keys(staticMeta).length === 0)
24
+ return undefined;
25
+ return { ...staticMeta };
26
+ }
27
+ sessionMeta(_schema) {
28
+ // The schema rides the turn (see promptMeta); a custom backend has no protocol-critical
29
+ // session/new `_meta` of its own.
30
+ return undefined;
31
+ }
32
+ promptMeta(schema) {
33
+ if (!schema)
34
+ return undefined;
35
+ // Plain JSON Schema (NOT OpenAI-strict-normalized — strictness is a Codex/Responses-API
36
+ // constraint, not part of the generic dialect). Agents that ignore it are repaired by the
37
+ // runner's ladder.
38
+ return { [META_KEYS.outputSchema]: toJsonSchema(schema) };
39
+ }
40
+ nativeStructured(source) {
41
+ return parseFinalJson(source.currentTurnText());
42
+ }
43
+ }
package/dist/index.d.ts CHANGED
@@ -1,18 +1,22 @@
1
1
  export { AcpAgentRunner, createAcpRunner, selectBackend } from "./runner.js";
2
+ export type { AcpRunnerOptions } from "./runner.js";
3
+ export { BACKENDS_ENV, resolveBackendRegistry } from "./registry.js";
4
+ export type { BackendRegistry, CustomBackendConfig, RegisteredBackend } from "./registry.js";
2
5
  export { PooledConnection, SessionHandle } from "./acp-client.js";
3
6
  export type { AcpSessionOptions, PooledConnectionDeps } from "./acp-client.js";
4
7
  export { AcpAgentPool, resolvePoolSize } from "./pool.js";
5
8
  export type { AcpPoolOptions, AcpPoolDeps } from "./pool.js";
6
9
  export { TypedEventEmitter, emitSessionUpdate } from "./events.js";
7
10
  export type { AcpRunnerEventMap, AcpEventName, AcpEventListener, AcpEventContext, AcpEventSink, AcpSessionUpdate, AcpUpdateKind, AcpPermissionEvent, AcpRawMessageEvent, AcpBackendErrorEvent, } from "./events.js";
8
- export type { Backend, BackendId, SessionMetaInputs, SpawnConfig, StructuredSource } from "./backend.js";
11
+ export type { Backend, BackendId, BuiltinBackendId, SessionMetaInputs, SpawnConfig, StructuredSource, } from "./backend.js";
9
12
  export { ClaudeBackend } from "./backends/claude.js";
10
13
  export { CodexBackend } from "./backends/codex.js";
14
+ export { CustomAcpBackend } from "./backends/custom.js";
11
15
  export { decidePermission } from "./permissions.js";
12
16
  export type { ToolPolicy } from "./permissions.js";
13
17
  export { UsageAccumulator } from "./usage.js";
14
18
  export { toJsonSchema, toStrictJsonSchema } from "./schema-strict.js";
15
- export { extractValidated, findJsonBlock, resolveStructuredOutput, validateValue, } from "./structured-output.js";
19
+ export { extractValidated, findJsonBlock, parseFinalJson, resolveStructuredOutput, validateValue, } from "./structured-output.js";
16
20
  export type { ResolveOptions, StructuredSession } from "./structured-output.js";
17
21
  export { errorText, mapThrownError } from "./errors-map.js";
18
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE7E,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC1D,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAG7D,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACzG,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAEtE,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,uBAAuB,EACvB,aAAa,GACd,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
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,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACrE,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"}
package/dist/index.js CHANGED
@@ -1,18 +1,21 @@
1
1
  // @automatalabs/acp-agents — implements the AgentRunner seam from @automatalabs/shared-types over
2
- // the Agent Client Protocol. It spawns claude-agent-acp / the installed npm dep codex-acp
3
- // (patched via pnpm patchedDependencies) as child processes and drives one subagent run to
4
- // completion. It NEVER imports
5
- // @automatalabs/workflow-engine; the two siblings meet ONLY at AgentRunner, injected by
6
- // mcp-server (the composition root) via createAcpRunner().
2
+ // the Agent Client Protocol. It spawns claude-agent-acp / the installed npm dep @automatalabs/codex-acp
3
+ // (a published fork of @agentclientprotocol/codex-acp with the outputSchema forward baked into its
4
+ // dist) as child processes and drives one subagent run to completion. It NEVER imports
5
+ // @automatalabs/workflow-engine; the two siblings meet ONLY at AgentRunner, injected by the
6
+ // @automatalabs/workflows facade (which mcp-server builds on) via createAcpRunner().
7
7
  export { AcpAgentRunner, createAcpRunner, selectBackend } from "./runner.js";
8
+ // The custom-backend registry: run ANY ACP agent as an agent() target.
9
+ export { BACKENDS_ENV, resolveBackendRegistry } from "./registry.js";
8
10
  export { PooledConnection, SessionHandle } from "./acp-client.js";
9
11
  export { AcpAgentPool, resolvePoolSize } from "./pool.js";
10
12
  // The typed ACP event bus surfaced on AcpAgentRunner (`runner.on(name, evt => …)`).
11
13
  export { TypedEventEmitter, emitSessionUpdate } from "./events.js";
12
14
  export { ClaudeBackend } from "./backends/claude.js";
13
15
  export { CodexBackend } from "./backends/codex.js";
16
+ export { CustomAcpBackend } from "./backends/custom.js";
14
17
  export { decidePermission } from "./permissions.js";
15
18
  export { UsageAccumulator } from "./usage.js";
16
19
  export { toJsonSchema, toStrictJsonSchema } from "./schema-strict.js";
17
- export { extractValidated, findJsonBlock, resolveStructuredOutput, validateValue, } from "./structured-output.js";
20
+ export { extractValidated, findJsonBlock, parseFinalJson, resolveStructuredOutput, validateValue, } from "./structured-output.js";
18
21
  export { errorText, mapThrownError } from "./errors-map.js";
@@ -0,0 +1,26 @@
1
+ export declare const BACKENDS_ENV = "AGENTPRISM_BACKENDS";
2
+ /** How to spawn (and optionally pre-configure) one custom ACP backend. */
3
+ export interface CustomBackendConfig {
4
+ /** The ACP server executable (absolute path or on PATH). */
5
+ command: string;
6
+ /** Arguments for the command. Default []. */
7
+ args?: string[];
8
+ /** Extra environment for the subprocess, merged OVER the inherited process.env. */
9
+ env?: Record<string, string>;
10
+ /** Static `_meta` sent on every session/new for this backend (backend-level defaults).
11
+ * Per-call RunOptions.meta merges over these; backend-computed keys win over both. */
12
+ sessionMeta?: Record<string, unknown>;
13
+ }
14
+ /** A validated registry entry: the (lowercased) name plus its config. */
15
+ export interface RegisteredBackend extends CustomBackendConfig {
16
+ name: string;
17
+ }
18
+ export type BackendRegistry = ReadonlyMap<string, RegisteredBackend>;
19
+ /**
20
+ * Resolve the custom-backend registry: env-declared backends first, programmatic `backends`
21
+ * merged over them (option wins per name). Throws on malformed JSON, invalid names/configs, or
22
+ * reserved names — a misconfigured registry should fail LOUDLY at construction, not silently
23
+ * misroute at run time.
24
+ */
25
+ export declare function resolveBackendRegistry(option?: Record<string, CustomBackendConfig>, env?: NodeJS.ProcessEnv): BackendRegistry;
26
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,82 @@
1
+ // The custom-backend REGISTRY — lets any ACP agent plug in as an agent() target, not just the
2
+ // built-in Claude/Codex pair. A registered backend is (name -> how to spawn it + optional static
3
+ // session `_meta`), resolved once at runner construction from:
4
+ // 1. the programmatic option: createAcpRunner({ backends: { name: config } }) (wins per name)
5
+ // 2. the env var AGENTPRISM_BACKENDS: a JSON object of the same shape
6
+ // Routing then matches `model`/`tier` specs against registered names FIRST (exact name, or
7
+ // `name/<inner-model>` prefix), before the built-in claude/codex heuristics — see runner.ts.
8
+ // Names are case-insensitive (stored lowercased); "claude" and "codex" are reserved because the
9
+ // built-ins own their ids (and their own AGENTPRISM_*_ACP_CMD override channel).
10
+ export const BACKENDS_ENV = "AGENTPRISM_BACKENDS";
11
+ const RESERVED_NAMES = new Set(["claude", "codex"]);
12
+ const NAME_PATTERN = /^[a-z][a-z0-9._-]*$/;
13
+ /**
14
+ * Resolve the custom-backend registry: env-declared backends first, programmatic `backends`
15
+ * merged over them (option wins per name). Throws on malformed JSON, invalid names/configs, or
16
+ * reserved names — a misconfigured registry should fail LOUDLY at construction, not silently
17
+ * misroute at run time.
18
+ */
19
+ export function resolveBackendRegistry(option, env = process.env) {
20
+ const registry = new Map();
21
+ const raw = env[BACKENDS_ENV];
22
+ if (raw !== undefined && raw.trim() !== "") {
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(raw);
26
+ }
27
+ catch (error) {
28
+ throw new Error(`${BACKENDS_ENV} is not valid JSON: ${error.message}`);
29
+ }
30
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
31
+ throw new Error(`${BACKENDS_ENV} must be a JSON object of { "<name>": { "command": … } }`);
32
+ }
33
+ for (const [name, config] of Object.entries(parsed)) {
34
+ registry.set(...validateEntry(name, config, BACKENDS_ENV));
35
+ }
36
+ }
37
+ for (const [name, config] of Object.entries(option ?? {})) {
38
+ registry.set(...validateEntry(name, config, "backends option"));
39
+ }
40
+ return registry;
41
+ }
42
+ function validateEntry(rawName, config, source) {
43
+ const name = rawName.toLowerCase();
44
+ if (!NAME_PATTERN.test(name)) {
45
+ throw new Error(`${source}: invalid backend name "${rawName}" (must match ${NAME_PATTERN}, matched case-insensitively)`);
46
+ }
47
+ if (RESERVED_NAMES.has(name)) {
48
+ throw new Error(`${source}: backend name "${rawName}" is reserved for the built-in backend (use AGENTPRISM_${name.toUpperCase()}_ACP_CMD to override how it spawns)`);
49
+ }
50
+ if (config === null || typeof config !== "object" || Array.isArray(config)) {
51
+ throw new Error(`${source}: backend "${rawName}" must be an object with at least { command }`);
52
+ }
53
+ const c = config;
54
+ if (typeof c.command !== "string" || c.command.trim() === "") {
55
+ throw new Error(`${source}: backend "${rawName}" needs a non-empty string "command"`);
56
+ }
57
+ if (c.args !== undefined && !(Array.isArray(c.args) && c.args.every((a) => typeof a === "string"))) {
58
+ throw new Error(`${source}: backend "${rawName}" "args" must be an array of strings`);
59
+ }
60
+ if (c.env !== undefined && !isStringRecord(c.env)) {
61
+ throw new Error(`${source}: backend "${rawName}" "env" must be an object of string values`);
62
+ }
63
+ if (c.sessionMeta !== undefined && (c.sessionMeta === null || typeof c.sessionMeta !== "object" || Array.isArray(c.sessionMeta))) {
64
+ throw new Error(`${source}: backend "${rawName}" "sessionMeta" must be an object`);
65
+ }
66
+ return [
67
+ name,
68
+ {
69
+ name,
70
+ command: c.command,
71
+ ...(c.args !== undefined ? { args: c.args } : {}),
72
+ ...(c.env !== undefined ? { env: c.env } : {}),
73
+ ...(c.sessionMeta !== undefined ? { sessionMeta: c.sessionMeta } : {}),
74
+ },
75
+ ];
76
+ }
77
+ function isStringRecord(value) {
78
+ return (value !== null &&
79
+ typeof value === "object" &&
80
+ !Array.isArray(value) &&
81
+ Object.values(value).every((v) => typeof v === "string"));
82
+ }
package/dist/runner.d.ts CHANGED
@@ -3,13 +3,23 @@ import type { TSchema } from "typebox";
3
3
  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
+ 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. */
9
+ export interface AcpRunnerOptions extends AcpPoolOptions {
10
+ /** Custom ACP backends, keyed by registered name (see registry.ts for the config shape
11
+ * and the routing rules). Names are case-insensitive; "claude"/"codex" are reserved. */
12
+ backends?: Record<string, CustomBackendConfig>;
13
+ }
6
14
  export declare class AcpAgentRunner implements AgentRunner {
7
15
  private readonly pool;
16
+ /** The resolved custom-backend registry (env + option, validated at construction). */
17
+ private readonly backends;
8
18
  /** Typed bus carrying every ACP event from every pooled session. Beyond the AgentRunner seam
9
19
  * (additive observability) — subscribing never affects a run and never enters the resume hash. */
10
20
  private readonly events;
11
21
  private readonly emitEvent;
12
- constructor(options?: AcpPoolOptions);
22
+ constructor(options?: AcpRunnerOptions);
13
23
  /**
14
24
  * Listen in on the live ACP stream. `name` is an ACP `sessionUpdate` discriminant
15
25
  * ("agent_message_chunk", "tool_call", "usage_update", …) or one of the cross-cutting events
@@ -31,12 +41,17 @@ export declare class AcpAgentRunner implements AgentRunner {
31
41
  dispose(): Promise<void>;
32
42
  }
33
43
  /** Factory the mcp-server composition root calls to inject the runner into the engine. The pool
34
- * size is a runner-level option (default 1, else AGENTPRISM_ACP_POOL_SIZE) — NOT a RunOptions
35
- * field, so it never enters hashAgentCall / the resume identity. */
36
- export declare function createAcpRunner(options?: AcpPoolOptions): AcpAgentRunner;
37
- /** Pick the backend by model/tier. Cross-provider routing = which ACP server to spawn. */
44
+ * size and the custom-backend registry are runner-level options — NOT RunOptions fields, so they
45
+ * never enter hashAgentCall / the resume identity. Env fallbacks: AGENTPRISM_ACP_POOL_SIZE for
46
+ * size, AGENTPRISM_BACKENDS (JSON) for backends. */
47
+ export declare function createAcpRunner(options?: AcpRunnerOptions): AcpAgentRunner;
48
+ /** Pick the backend by model/tier. Cross-provider routing = which ACP server to spawn.
49
+ * Registered CUSTOM names resolve FIRST (exact name, or `name/<inner-model>` prefix) so a
50
+ * registry entry is never shadowed by the built-in heuristics; then the claude/codex
51
+ * heuristics; then the default backend (AGENTPRISM_DEFAULT_BACKEND — which may itself name
52
+ * a registered custom backend). */
38
53
  export declare function selectBackend(opts: {
39
54
  model?: string;
40
55
  tier?: string;
41
- }): Backend;
56
+ }, registry?: BackendRegistry): Backend;
42
57
  //# sourceMappingURL=runner.d.ts.map
@@ -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;AAS5C,qBAAa,cAAe,YAAW,WAAW;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC;uGACmG;IACnG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8C;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgE;gBAE9E,OAAO,GAAE,cAAmB;IAIxC;;;;;;;;OAQG;IACH,EAAE,CAAC,CAAC,SAAS,YAAY,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAI9E,+EAA+E;IAC/E,IAAI,CAAC,CAAC,SAAS,YAAY,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAIhF,GAAG,CAAC,CAAC,SAAS,YAAY,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI;IAIzE,kBAAkB,CAAC,IAAI,CAAC,EAAE,YAAY,GAAG,IAAI;IAI7C,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM;IAInC,GAAG,CAAC,CAAC,SAAS,OAAO,GAAG,SAAS,GAAG,SAAS,EACjD,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,UAAU,CAAC,CAAC,CAAM,GAC1B,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAuF1B;qGACiG;IAC3F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAI/B;AAED;;qEAEqE;AACrE,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,cAAc,CAExE;AAoED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAG9E"}
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,EAA0B,KAAK,eAAe,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAOvG;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;IA+F1B;qGACiG;IAC3F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAI/B;AAED;;;qDAGqD;AACrD,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,cAAc,CAE1E;AAwED;;;;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
@@ -21,16 +21,21 @@ import { AcpAgentPool } from "./pool.js";
21
21
  import { TypedEventEmitter, } from "./events.js";
22
22
  import { ClaudeBackend } from "./backends/claude.js";
23
23
  import { CodexBackend } from "./backends/codex.js";
24
+ import { CustomAcpBackend } from "./backends/custom.js";
25
+ import { resolveBackendRegistry } from "./registry.js";
24
26
  import { mapThrownError } from "./errors-map.js";
25
27
  import { resolveStructuredOutput } from "./structured-output.js";
26
28
  export class AcpAgentRunner {
27
29
  pool;
30
+ /** The resolved custom-backend registry (env + option, validated at construction). */
31
+ backends;
28
32
  /** Typed bus carrying every ACP event from every pooled session. Beyond the AgentRunner seam
29
33
  * (additive observability) — subscribing never affects a run and never enters the resume hash. */
30
34
  events = new TypedEventEmitter();
31
35
  emitEvent = (name, event) => this.events.emit(name, event);
32
36
  constructor(options = {}) {
33
37
  this.pool = new AcpAgentPool(options, { onEvent: this.emitEvent });
38
+ this.backends = resolveBackendRegistry(options.backends);
34
39
  }
35
40
  /**
36
41
  * Listen in on the live ACP stream. `name` is an ACP `sessionUpdate` discriminant
@@ -60,7 +65,7 @@ export class AcpAgentRunner {
60
65
  async run(prompt, options = {}) {
61
66
  const opts = options;
62
67
  const schema = opts.schema;
63
- const backend = selectBackend(opts);
68
+ const backend = selectBackend(opts, this.backends);
64
69
  const policy = { allow: opts.toolNames, deny: opts.disallowedToolNames };
65
70
  const cwd = opts.cwd ?? process.cwd();
66
71
  const session = await this.pool.acquire(backend, {
@@ -69,6 +74,9 @@ export class AcpAgentRunner {
69
74
  policy,
70
75
  signal: opts.signal,
71
76
  mcpServers: opts.mcpServers,
77
+ // Generic session-scoped _meta passthrough (RunOptions.meta) — merged UNDER the
78
+ // backend-computed keys and the runId stamp in openSession. Additive; never hashed.
79
+ meta: opts.meta,
72
80
  // Engine correlation id -> session/new _meta (META_KEYS.runId). Additive; never hashed.
73
81
  runId: opts.runId,
74
82
  // Stamped onto emitted ACP events as context (never sent on the wire).
@@ -80,9 +88,14 @@ export class AcpAgentRunner {
80
88
  });
81
89
  try {
82
90
  opts.signal?.throwIfAborted();
83
- await applyModelSelection(session, opts);
91
+ // For a CUSTOM backend chosen by its registered name, the name itself is routing, not a
92
+ // model id: "browser" selects nothing; "browser/foo" selects "foo". Built-ins get the
93
+ // full spec unchanged (their catalogs match provider-prefixed and bare ids).
94
+ await applyModelSelection(session, innerModelSpec(opts.model ?? opts.tier, backend), opts);
84
95
  const text = buildPrompt(prompt, opts, Boolean(schema));
85
- const promptMeta = backend.promptMeta(schema);
96
+ // Generic turn-scoped _meta passthrough merged UNDER the backend-computed keys (e.g. the
97
+ // outputSchema forward when a schema is set) — user meta never clobbers the schema channel.
98
+ const promptMeta = mergeTurnMeta(opts.promptMeta, backend.promptMeta(schema));
86
99
  const response = await session.prompt(text, promptMeta);
87
100
  opts.signal?.throwIfAborted();
88
101
  // Inspect the turn's stop reason BEFORE the text/schema path: a refusal or truncation
@@ -153,8 +166,9 @@ export class AcpAgentRunner {
153
166
  }
154
167
  }
155
168
  /** Factory the mcp-server composition root calls to inject the runner into the engine. The pool
156
- * size is a runner-level option (default 1, else AGENTPRISM_ACP_POOL_SIZE) — NOT a RunOptions
157
- * field, so it never enters hashAgentCall / the resume identity. */
169
+ * size and the custom-backend registry are runner-level options — NOT RunOptions fields, so they
170
+ * never enter hashAgentCall / the resume identity. Env fallbacks: AGENTPRISM_ACP_POOL_SIZE for
171
+ * size, AGENTPRISM_BACKENDS (JSON) for backends. */
158
172
  export function createAcpRunner(options) {
159
173
  return new AcpAgentRunner(options);
160
174
  }
@@ -188,9 +202,9 @@ function assertNormalStopReason(stopReason, label) {
188
202
  return;
189
203
  }
190
204
  }
191
- async function applyModelSelection(session, opts) {
192
- // `model` wins; `tier` is consulted only when `model` is unset (frozen contract).
193
- const spec = opts.model ?? opts.tier;
205
+ async function applyModelSelection(session, spec, opts) {
206
+ // `spec` is opts.model ?? opts.tier (`model` wins frozen contract), with a custom
207
+ // backend's routing name already stripped by innerModelSpec.
194
208
  if (!spec)
195
209
  return;
196
210
  const { matched, resolved, modifierFallbacks } = await session.selectModel(spec);
@@ -221,11 +235,50 @@ function buildPrompt(prompt, opts, structured) {
221
235
  }
222
236
  return parts.join("\n\n");
223
237
  }
224
- /** Pick the backend by model/tier. Cross-provider routing = which ACP server to spawn. */
225
- export function selectBackend(opts) {
226
- const id = backendIdForSpec(opts.model) ?? backendIdForSpec(opts.tier) ?? defaultBackendId();
238
+ /** Pick the backend by model/tier. Cross-provider routing = which ACP server to spawn.
239
+ * Registered CUSTOM names resolve FIRST (exact name, or `name/<inner-model>` prefix) so a
240
+ * registry entry is never shadowed by the built-in heuristics; then the claude/codex
241
+ * heuristics; then the default backend (AGENTPRISM_DEFAULT_BACKEND — which may itself name
242
+ * a registered custom backend). */
243
+ export function selectBackend(opts, registry) {
244
+ const custom = customBackendForSpec(opts.model, registry) ?? customBackendForSpec(opts.tier, registry);
245
+ if (custom)
246
+ return custom;
247
+ const id = backendIdForSpec(opts.model) ?? backendIdForSpec(opts.tier) ?? defaultBackendId(registry);
248
+ if (typeof id !== "string")
249
+ return id; // the default resolved to a registered custom backend
227
250
  return id === "codex" ? new CodexBackend() : new ClaudeBackend();
228
251
  }
252
+ /** Match a model/tier spec against the registry: the whole spec, or its `<name>/` prefix. */
253
+ function customBackendForSpec(spec, registry) {
254
+ if (!spec || !registry || registry.size === 0)
255
+ return undefined;
256
+ const lower = spec.toLowerCase();
257
+ const slash = lower.indexOf("/");
258
+ const name = slash > 0 ? lower.slice(0, slash) : lower;
259
+ const config = registry.get(name);
260
+ return config ? new CustomAcpBackend(config) : undefined;
261
+ }
262
+ /** Strip a custom backend's routing name off the model/tier spec: the spec `"name"` selects no
263
+ * inner model; `"name/foo"` selects `"foo"`. Built-in backends receive the spec unchanged, and
264
+ * a spec that reached a custom DEFAULT backend without naming it also passes through (the
265
+ * agent's own catalog may know it). */
266
+ function innerModelSpec(spec, backend) {
267
+ if (!spec || !(backend instanceof CustomAcpBackend))
268
+ return spec;
269
+ const lower = spec.toLowerCase();
270
+ if (lower === backend.id)
271
+ return undefined;
272
+ if (lower.startsWith(`${backend.id}/`))
273
+ return spec.slice(backend.id.length + 1) || undefined;
274
+ return spec;
275
+ }
276
+ /** Merge the generic turn-scoped meta passthrough UNDER the backend-computed turn meta. */
277
+ function mergeTurnMeta(user, backend) {
278
+ if (!user)
279
+ return backend;
280
+ return { ...user, ...(backend ?? {}) };
281
+ }
229
282
  function backendIdForSpec(spec) {
230
283
  if (!spec)
231
284
  return undefined;
@@ -243,6 +296,14 @@ function backendIdForSpec(spec) {
243
296
  return "claude";
244
297
  return undefined;
245
298
  }
246
- function defaultBackendId() {
247
- return process.env.AGENTPRISM_DEFAULT_BACKEND?.toLowerCase() === "codex" ? "codex" : "claude";
299
+ /** Resolve the default backend: a registered custom name wins (returned as a Backend), else
300
+ * the built-in id. An unknown/unset value falls back to "claude" (the historical default). */
301
+ function defaultBackendId(registry) {
302
+ const name = process.env.AGENTPRISM_DEFAULT_BACKEND?.toLowerCase();
303
+ if (name && registry) {
304
+ const config = registry.get(name);
305
+ if (config)
306
+ return new CustomAcpBackend(config);
307
+ }
308
+ return name === "codex" ? "codex" : "claude";
248
309
  }
@@ -5,6 +5,14 @@ import type { TSchema } from "typebox";
5
5
  * raw JSON string, or undefined when none is found. (Ported VERBATIM from pi agent.ts.)
6
6
  */
7
7
  export declare function findJsonBlock(text: string): string | undefined;
8
+ /**
9
+ * Parse a turn's final assistant text as JSON: direct parse first (a natively-constrained
10
+ * final message is pure JSON), then a balanced-block extraction when the turn also emitted
11
+ * leading prose. Returns undefined when no parseable JSON is present (the caller's ladder
12
+ * re-prompts). Shared by the Codex backend and every custom registry backend — the "read the
13
+ * structured result off the final message" convention is the generic ACP dialect.
14
+ */
15
+ export declare function parseFinalJson(text: string): unknown;
8
16
  /** Coerce an already-parsed value toward the schema and accept it only if it then validates. */
9
17
  export declare function validateValue(value: unknown, schema: TSchema): unknown;
10
18
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"structured-output.d.ts","sourceRoot":"","sources":["../src/structured-output.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIvC;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAa9D;AAED,gGAAgG;AAChG,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAQtE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAUvE;AAED,sGAAsG;AACtG,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,+DAA+D;IAC/D,QAAQ,IAAI,MAAM,CAAC;IACnB,8FAA8F;IAC9F,SAAS,IAAI,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAQD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,OAAO,CAAC,CA0BlB"}
1
+ {"version":3,"file":"structured-output.d.ts","sourceRoot":"","sources":["../src/structured-output.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIvC;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAa9D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAiBpD;AAED,gGAAgG;AAChG,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAQtE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAUvE;AAED,sGAAsG;AACtG,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,+DAA+D;IAC/D,QAAQ,IAAI,MAAM,CAAC;IACnB,8FAA8F;IAC9F,SAAS,IAAI,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAQD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,OAAO,CAAC,CA0BlB"}
@@ -23,6 +23,34 @@ export function findJsonBlock(text) {
23
23
  }
24
24
  return undefined;
25
25
  }
26
+ /**
27
+ * Parse a turn's final assistant text as JSON: direct parse first (a natively-constrained
28
+ * final message is pure JSON), then a balanced-block extraction when the turn also emitted
29
+ * leading prose. Returns undefined when no parseable JSON is present (the caller's ladder
30
+ * re-prompts). Shared by the Codex backend and every custom registry backend — the "read the
31
+ * structured result off the final message" convention is the generic ACP dialect.
32
+ */
33
+ export function parseFinalJson(text) {
34
+ const trimmed = text.trim();
35
+ if (!trimmed)
36
+ return undefined;
37
+ try {
38
+ return JSON.parse(trimmed);
39
+ }
40
+ catch {
41
+ // fall through to block extraction
42
+ }
43
+ const block = findJsonBlock(trimmed);
44
+ if (block !== undefined) {
45
+ try {
46
+ return JSON.parse(block);
47
+ }
48
+ catch {
49
+ // give up; the runner's ladder will re-prompt / extract.
50
+ }
51
+ }
52
+ return undefined;
53
+ }
26
54
  /** Coerce an already-parsed value toward the schema and accept it only if it then validates. */
27
55
  export function validateValue(value, schema) {
28
56
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automatalabs/acp-agents",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,7 +28,7 @@
28
28
  "@agentclientprotocol/claude-agent-acp": "0.53.0",
29
29
  "@automatalabs/codex-acp": "1.2.0",
30
30
  "typebox": "1.3.2",
31
- "@automatalabs/shared-types": "0.3.0"
31
+ "@automatalabs/shared-types": "0.4.0"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsc -b",