@automatalabs/acp-agents 0.8.0 → 0.9.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.
@@ -0,0 +1,139 @@
1
+ import { appendPromptImages, mergeTurnMeta, validatePromptImages, } from "./prompt.js";
2
+ /** A held-open multi-turn ACP session backed by a dedicated agent process. Only one prompt may
3
+ * be in flight at a time; hosts that want queued turns should serialize calls themselves so
4
+ * cancellation, permissions, and turn text stay attributable to a single active turn. If the
5
+ * dedicated process dies, the runner observes that per session by auto-releasing this wrapper:
6
+ * session-scoped listeners are removed, later prompt() calls fail with the released-session
7
+ * error, and `session_close` is emitted on this session's event stream. The connection-scoped
8
+ * `backend_error` event is emitted on the runner bus only; it is not delivered through
9
+ * session.on(). */
10
+ export class InteractiveSession {
11
+ sessionId;
12
+ backendId;
13
+ session;
14
+ connection;
15
+ backend;
16
+ subscribe;
17
+ onReleaseCallback;
18
+ signal;
19
+ label;
20
+ subscriptions = new Set();
21
+ removeAbort;
22
+ promptInFlight = false;
23
+ releasePromise;
24
+ /** Construct the public wrapper around an already-open ACP session. Hosts normally receive
25
+ * instances from AcpAgentRunner.openSession(), which supplies the internal session/connection
26
+ * dependencies and owns lifecycle tracking. */
27
+ constructor(deps) {
28
+ this.session = deps.session;
29
+ this.connection = deps.connection;
30
+ this.backend = deps.backend;
31
+ this.subscribe = deps.subscribe;
32
+ this.onReleaseCallback = deps.onRelease;
33
+ this.signal = deps.signal;
34
+ this.label = deps.label;
35
+ this.sessionId = deps.session.sessionId;
36
+ this.backendId = deps.connection.backendId;
37
+ if (deps.signal) {
38
+ const signal = deps.signal;
39
+ const onAbort = () => {
40
+ void this.release();
41
+ };
42
+ signal.addEventListener("abort", onAbort, { once: true });
43
+ this.removeAbort = () => signal.removeEventListener("abort", onAbort);
44
+ if (signal.aborted)
45
+ void this.release();
46
+ }
47
+ }
48
+ /** Negotiated initialize capabilities for this session's dedicated connection. */
49
+ get capabilities() {
50
+ return this.connection.capabilities;
51
+ }
52
+ /** Send one prompt turn. A concurrent prompt on the same InteractiveSession is rejected with a
53
+ * clear host-side error; queueing is deliberately left to the host so turn boundaries remain
54
+ * explicit. Per-turn images are appended only to this prompt, and SessionHandle.prompt()
55
+ * performs capability adaptation before sending. */
56
+ async prompt(content, opts = {}) {
57
+ if (this.releasePromise)
58
+ throw new Error("InteractiveSession has been released");
59
+ this.signal?.throwIfAborted();
60
+ if (this.promptInFlight) {
61
+ throw new Error("InteractiveSession.prompt() already has a prompt in flight; await it before sending another turn");
62
+ }
63
+ validatePromptImages(opts.images, this.label);
64
+ this.promptInFlight = true;
65
+ try {
66
+ const turnContent = appendPromptImages(content, opts.images);
67
+ const promptMeta = mergeTurnMeta(opts.promptMeta, this.backend.promptMeta(undefined));
68
+ const response = await this.session.prompt(turnContent, promptMeta);
69
+ return {
70
+ stopReason: response.stopReason,
71
+ text: this.session.currentTurnText(),
72
+ };
73
+ }
74
+ finally {
75
+ this.promptInFlight = false;
76
+ }
77
+ }
78
+ /** Best-effort ACP session/cancel for the active turn. Pending permission resolvers are
79
+ * settled as cancelled by the SessionHandle/PooledConnection cancel path. */
80
+ async cancel() {
81
+ if (this.releasePromise)
82
+ return;
83
+ await this.session.cancel();
84
+ }
85
+ /** Subscribe to runner events for THIS ACP session only. Events from other one-shot or
86
+ * interactive sessions on the same runner are filtered out by sessionId. The returned
87
+ * unsubscribe thunk and every still-live subscription are removed automatically on release. */
88
+ on(name, listener) {
89
+ if (this.releasePromise)
90
+ return () => { };
91
+ const wrapped = (event) => {
92
+ if (event.sessionId === this.sessionId)
93
+ listener(event);
94
+ };
95
+ const removeRunnerListener = this.subscribe(name, wrapped);
96
+ let active = true;
97
+ const off = () => {
98
+ if (!active)
99
+ return;
100
+ active = false;
101
+ removeRunnerListener();
102
+ this.subscriptions.delete(off);
103
+ };
104
+ this.subscriptions.add(off);
105
+ return off;
106
+ }
107
+ /** Release the ACP session and close the dedicated process. Idempotent. Session close is
108
+ * best-effort and bounded by SessionHandle; process disposal mirrors pool teardown. */
109
+ release() {
110
+ this.releasePromise ??= this.doRelease();
111
+ return this.releasePromise;
112
+ }
113
+ async doRelease() {
114
+ this.removeAbort?.();
115
+ this.removeAbort = undefined;
116
+ try {
117
+ await this.session.release();
118
+ }
119
+ catch {
120
+ // best-effort: release must still dispose the dedicated process and unregister.
121
+ }
122
+ try {
123
+ await this.connection.dispose();
124
+ }
125
+ catch {
126
+ // best-effort: mirrors pool disposal semantics.
127
+ }
128
+ finally {
129
+ this.removeSubscriptions();
130
+ this.onReleaseCallback(this);
131
+ }
132
+ }
133
+ removeSubscriptions() {
134
+ const subscriptions = [...this.subscriptions];
135
+ for (const off of subscriptions)
136
+ off();
137
+ this.subscriptions.clear();
138
+ }
139
+ }
@@ -1,4 +1,13 @@
1
1
  import type { RequestPermissionRequest, RequestPermissionResponse } from "@agentclientprotocol/sdk";
2
+ import type { AcpEventContext } from "./events.js";
3
+ /** Async human-in-the-loop decider for ACP permission requests. When present, it REPLACES the
4
+ * synchronous ToolPolicy path for the sessions it applies to: the agent turn parks until this
5
+ * resolver selects an ACP permission outcome. It may resolve arbitrarily later (seconds or
6
+ * minutes); the library guarantees every parked request is settled with the ACP
7
+ * `{ outcome: { outcome: "cancelled" } }` response when its session is released, its turn is
8
+ * cancelled via session/cancel, or its connection dies, so teardown can never leave an agent
9
+ * turn hung behind an unanswered permission prompt. */
10
+ export type PermissionResolver = (params: RequestPermissionRequest, ctx: AcpEventContext) => Promise<RequestPermissionResponse> | RequestPermissionResponse;
2
11
  export interface ToolPolicy {
3
12
  /** Allow-list (agentType `tools`). When non-empty, a tool that matches NOTHING is denied. */
4
13
  allow?: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAGV,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,UAAU;IACzB,6FAA6F;IAC7F,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAKD,iFAAiF;AACjF,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,wBAAwB,EACjC,MAAM,EAAE,UAAU,GACjB,yBAAyB,CAqC3B"}
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAGV,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;;wDAMwD;AACxD,MAAM,MAAM,kBAAkB,GAAG,CAC/B,MAAM,EAAE,wBAAwB,EAChC,GAAG,EAAE,eAAe,KACjB,OAAO,CAAC,yBAAyB,CAAC,GAAG,yBAAyB,CAAC;AAEpE,MAAM,WAAW,UAAU;IACzB,6FAA6F;IAC7F,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAKD,iFAAiF;AACjF,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,wBAAwB,EACjC,MAAM,EAAE,UAAU,GACjB,yBAAyB,CAqC3B"}
package/dist/pool.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { Backend } from "./backend.js";
2
2
  import { SessionHandle, type AcpSessionOptions } from "./acp-client.js";
3
3
  import { type ClientHandlers } from "./client-handlers.js";
4
4
  import type { AcpEventSink } from "./events.js";
5
+ import type { PermissionResolver } from "./permissions.js";
5
6
  export interface AcpPoolOptions {
6
7
  /** Long-lived processes to keep PER backend. Default 1; falls back to AGENTPRISM_ACP_POOL_SIZE. */
7
8
  size?: number;
@@ -9,9 +10,10 @@ export interface AcpPoolOptions {
9
10
  clientHandlers?: ClientHandlers;
10
11
  }
11
12
  /** Internal wiring the runner injects (NOT part of the public AcpPoolOptions surface): the typed
12
- * event sink forwarded to every PooledConnection so ACP events bubble up to `runner.on(...)`. */
13
+ * event sink and runner-default permission resolver forwarded to every PooledConnection. */
13
14
  export interface AcpPoolDeps {
14
15
  onEvent?: AcpEventSink;
16
+ permissionResolver?: PermissionResolver;
15
17
  }
16
18
  /** Resolve the per-backend pool size: explicit option wins, else env, else 1. Clamped to >= 1. */
17
19
  export declare function resolvePoolSize(option?: number): number;
@@ -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,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"}
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;AAChD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAK3D,MAAM,WAAW,cAAc;IAC7B,mGAAmG;IACnG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,4FAA4F;IAC5F,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED;6FAC6F;AAC7F,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;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;IA0BxB,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
@@ -55,6 +55,7 @@ export class AcpAgentPool {
55
55
  const connection = PooledConnection.create(backend, {
56
56
  onDead: (dead) => this.drop(key, dead),
57
57
  onEvent: this.deps.onEvent,
58
+ permissionResolver: this.deps.permissionResolver,
58
59
  clientHandlers: this.clientHandlers,
59
60
  });
60
61
  connections.push(connection);
@@ -0,0 +1,21 @@
1
+ import type { ContentBlock } from "@agentclientprotocol/sdk";
2
+ import { type PromptImage } from "@automatalabs/shared-types";
3
+ import type { TSchema } from "typebox";
4
+ import type { Backend } from "./backend.js";
5
+ /** Build the exact one-shot run() prompt text: optional engine instructions, a diagnostic label,
6
+ * the user prompt, and (for schema runs) the backend-appropriate final-output contract. */
7
+ export declare function buildRunPrompt(prompt: string, opts: {
8
+ instructions?: string;
9
+ label?: string;
10
+ }, schema: TSchema | undefined, backend: Backend): string;
11
+ /** Validate prompt images before spawning/sending. Invalid images are deterministic script
12
+ * errors, never provider failures, and the bad index is part of the contract for callers. */
13
+ export declare function validatePromptImages(images: readonly PromptImage[] | undefined, label?: string): void;
14
+ /** One-shot run() image shape: the text prompt first, then each image attachment. */
15
+ export declare function promptWithImages(text: string, images: readonly PromptImage[]): ContentBlock[];
16
+ /** Interactive prompt image shape: string prompts match run(); ContentBlock prompts keep the
17
+ * caller's blocks and append this turn's image attachments. */
18
+ export declare function appendPromptImages(content: string | ContentBlock[], images: readonly PromptImage[] | undefined): string | ContentBlock[];
19
+ /** Merge generic turn-scoped `_meta` UNDER the backend-computed turn meta. */
20
+ export declare function mergeTurnMeta(user: Record<string, unknown> | undefined, backend: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
21
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5C;4FAC4F;AAC5F,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAC/C,MAAM,EAAE,OAAO,GAAG,SAAS,EAC3B,OAAO,EAAE,OAAO,GACf,MAAM,CAoBR;AAED;8FAC8F;AAC9F,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,GAAG,SAAS,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CA0BrG;AAgBD,qFAAqF;AACrF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,WAAW,EAAE,GAAG,YAAY,EAAE,CAE7F;AAED;gEACgE;AAChE,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,EAChC,MAAM,EAAE,SAAS,WAAW,EAAE,GAAG,SAAS,GACzC,MAAM,GAAG,YAAY,EAAE,CAIzB;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAC3B,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,CAGrC"}
package/dist/prompt.js ADDED
@@ -0,0 +1,75 @@
1
+ import { WorkflowError, WorkflowErrorCode, } from "@automatalabs/shared-types";
2
+ import { toJsonSchema } from "./schema-strict.js";
3
+ /** Build the exact one-shot run() prompt text: optional engine instructions, a diagnostic label,
4
+ * the user prompt, and (for schema runs) the backend-appropriate final-output contract. */
5
+ export function buildRunPrompt(prompt, opts, schema, backend) {
6
+ const parts = [];
7
+ if (opts.instructions)
8
+ parts.push(opts.instructions);
9
+ if (opts.label)
10
+ parts.push(`Task label: ${opts.label}`);
11
+ parts.push(prompt);
12
+ if (schema) {
13
+ const contract = [
14
+ "Final output contract:",
15
+ "- Your FINAL message MUST be a single JSON object that conforms to the required output schema.",
16
+ "- Output ONLY that JSON object — no prose, no explanation, and no markdown code fences.",
17
+ "- If you need to inspect files or run commands first, do so, then emit the JSON object as your final message.",
18
+ ];
19
+ if (backend.embedSchemaInPrompt) {
20
+ // Custom ACP agents may ignore `_meta.outputSchema`; state the schema in-band so the
21
+ // repair ladder is correcting against a visible contract, not an unseen extension key.
22
+ contract.push(`- The required output schema (JSON Schema):\n${JSON.stringify(toJsonSchema(schema))}`);
23
+ }
24
+ parts.push(contract.join("\n"));
25
+ }
26
+ return parts.join("\n\n");
27
+ }
28
+ /** Validate prompt images before spawning/sending. Invalid images are deterministic script
29
+ * errors, never provider failures, and the bad index is part of the contract for callers. */
30
+ export function validatePromptImages(images, label) {
31
+ if (!images || images.length === 0)
32
+ return;
33
+ for (let i = 0; i < images.length; i += 1) {
34
+ const image = images[i];
35
+ if (typeof image?.data !== "string" || image.data.trim() === "") {
36
+ throw new WorkflowError(`images[${i}].data must be a non-empty string`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: label });
37
+ }
38
+ if (typeof image.mimeType !== "string" || image.mimeType.trim() === "") {
39
+ throw new WorkflowError(`images[${i}].mimeType must be a non-empty string`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: label });
40
+ }
41
+ if (image.uri !== undefined && (typeof image.uri !== "string" || image.uri.trim() === "")) {
42
+ throw new WorkflowError(`images[${i}].uri must be a non-empty string when present`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: label });
43
+ }
44
+ }
45
+ }
46
+ /** Convert base64 image attachments to ACP image ContentBlocks without capability adaptation.
47
+ * SessionHandle.prompt() performs the advertised-capability downgrade at send time. */
48
+ function imageContentBlocks(images) {
49
+ const blocks = [];
50
+ for (const image of images) {
51
+ blocks.push(image.uri === undefined
52
+ ? { type: "image", data: image.data, mimeType: image.mimeType }
53
+ : { type: "image", data: image.data, mimeType: image.mimeType, uri: image.uri });
54
+ }
55
+ return blocks;
56
+ }
57
+ /** One-shot run() image shape: the text prompt first, then each image attachment. */
58
+ export function promptWithImages(text, images) {
59
+ return [{ type: "text", text }, ...imageContentBlocks(images)];
60
+ }
61
+ /** Interactive prompt image shape: string prompts match run(); ContentBlock prompts keep the
62
+ * caller's blocks and append this turn's image attachments. */
63
+ export function appendPromptImages(content, images) {
64
+ if (!images || images.length === 0)
65
+ return content;
66
+ if (typeof content === "string")
67
+ return promptWithImages(content, images);
68
+ return [...content, ...imageContentBlocks(images)];
69
+ }
70
+ /** Merge generic turn-scoped `_meta` UNDER the backend-computed turn meta. */
71
+ export function mergeTurnMeta(user, backend) {
72
+ if (!user)
73
+ return backend;
74
+ return { ...user, ...(backend ?? {}) };
75
+ }
package/dist/runner.d.ts CHANGED
@@ -3,31 +3,48 @@ 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 { InteractiveSession, type InteractiveSessionOptions } from "./interactive.js";
6
7
  import { type BackendRegistry, type CustomBackendConfig } from "./registry.js";
8
+ import type { PermissionResolver } from "./permissions.js";
7
9
  /** Constructor options for the runner: pool sizing, client-side handlers, and the custom-backend
8
10
  * registry. `backends` merges over (and wins against) env-declared AGENTPRISM_BACKENDS entries. */
9
11
  export interface AcpRunnerOptions extends AcpPoolOptions {
10
12
  /** Custom ACP backends, keyed by registered name (see registry.ts for the config shape
11
13
  * and the routing rules). Names are case-insensitive; "claude"/"codex" are reserved. */
12
14
  backends?: Record<string, CustomBackendConfig>;
15
+ /** Runner-wide human-in-the-loop permission resolver. When set, it replaces ToolPolicy
16
+ * auto-decisions for every session that does not provide its own resolver. */
17
+ onPermissionRequest?: PermissionResolver;
13
18
  }
14
19
  export declare class AcpAgentRunner implements AgentRunner {
15
20
  private readonly pool;
16
21
  /** The resolved custom-backend registry (env + option, validated at construction). */
17
22
  private readonly backends;
18
- /** Typed bus carrying every ACP event from every pooled session. Beyond the AgentRunner seam
19
- * (additive observability) — subscribing never affects a run and never enters the resume hash. */
23
+ /** Typed bus carrying every ACP event from every pooled or interactive session. Beyond the
24
+ * AgentRunner seam (additive observability) — subscribing never affects a run and never enters
25
+ * the resume hash. */
20
26
  private readonly events;
21
27
  private readonly emitEvent;
28
+ /** Client-side handlers and the runner-wide permission resolver are initialize/session wiring,
29
+ * so dedicated interactive connections must receive the SAME deps the pool receives. */
30
+ private readonly clientHandlers;
31
+ private readonly permissionResolver;
32
+ /** Held-open interactive sessions own dedicated ACP processes outside the pool. The runner
33
+ * tracks their connections so dispose() can release them and the process-exit hook can
34
+ * synchronously kill any dedicated children if the host exits without release(). */
35
+ private readonly interactiveSessions;
36
+ private readonly onProcessExit;
37
+ private exitHookInstalled;
38
+ private disposed;
22
39
  constructor(options?: AcpRunnerOptions);
23
40
  /**
24
41
  * Listen in on the live ACP stream. `name` is an ACP `sessionUpdate` discriminant
25
42
  * ("agent_message_chunk", "tool_call", "usage_update", …) or one of the cross-cutting events
26
- * ("session_update" catch-all, "permission_request", "raw_message", "session_open",
27
- * "session_close", "backend_error"). The listener is typed to the event. Returns an unsubscribe
28
- * thunk. A pooled runner multiplexes many concurrent runs, so each event carries
29
- * `{ sessionId, backendId, label?, runId? }` for filtering. Listeners are best-effort observers:
30
- * a throwing listener is isolated and never affects the run.
43
+ * ("session_update" catch-all, "permission_pending", "permission_request", "raw_message",
44
+ * "session_open", "session_close", "backend_error"). The listener is typed to the event.
45
+ * Returns an unsubscribe thunk. A pooled runner multiplexes many concurrent runs, so each
46
+ * event carries `{ sessionId, backendId, label?, runId? }` for filtering. Listeners are
47
+ * best-effort observers: a throwing listener is isolated and never affects the run.
31
48
  */
32
49
  on<K extends AcpEventName>(name: K, listener: AcpEventListener<K>): () => void;
33
50
  /** Subscribe once; the listener auto-unsubscribes after its first delivery. */
@@ -35,10 +52,25 @@ export declare class AcpAgentRunner implements AgentRunner {
35
52
  off<K extends AcpEventName>(name: K, listener: AcpEventListener<K>): void;
36
53
  removeAllListeners(name?: AcpEventName): void;
37
54
  listenerCount(name: AcpEventName): number;
55
+ /**
56
+ * Open a held ACP session for multi-turn callers. Unlike run(), this does NOT acquire a pool
57
+ * slot: it spawns one dedicated backend process, opens one ACP session on it, and hands the
58
+ * caller an InteractiveSession that must be released. The dedicated process means a long-lived
59
+ * chat/debug loop never starves one-shot run() calls on the same backend (the default pool size
60
+ * is one).
61
+ */
62
+ openSession(opts: InteractiveSessionOptions): Promise<InteractiveSession>;
38
63
  run<S extends TSchema | undefined = undefined>(prompt: string, options?: RunOptions<S>): Promise<AgentResult<S>>;
39
64
  /** Tear down the whole pool (close every long-lived process). Call when the run ends / the
40
65
  * runner is disposed. Beyond the AgentRunner seam (additive) — never enters the resume hash. */
41
66
  dispose(): Promise<void>;
67
+ /** Build the backend choice, model-selection spec, tool policy, and session/new options in one
68
+ * place for run() and openSession() so new AcpSessionOptions fields cannot drift by path. */
69
+ private prepareSession;
70
+ private installExitHook;
71
+ private removeExitHook;
72
+ /** Synchronous best-effort child kill for the process-exit hook (no async work is possible). */
73
+ private killAllSync;
42
74
  }
43
75
  /** Factory the mcp-server composition root calls to inject the runner into the engine. The pool
44
76
  * size and the custom-backend registry are runner-level options — NOT RunOptions fields, so they
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}