@automatalabs/acp-agents 0.8.0 → 0.9.1
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 +42 -3
- package/dist/acp-client.d.ts +25 -6
- package/dist/acp-client.d.ts.map +1 -1
- package/dist/acp-client.js +82 -10
- package/dist/events.d.ts +16 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +10 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/interactive.d.ts +114 -0
- package/dist/interactive.d.ts.map +1 -0
- package/dist/interactive.js +139 -0
- package/dist/permissions.d.ts +9 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/pool.d.ts +3 -1
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +1 -0
- package/dist/prompt.d.ts +21 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +75 -0
- package/dist/runner.d.ts +39 -7
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +162 -84
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/permissions.d.ts
CHANGED
|
@@ -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;
|
|
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
|
|
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;
|
package/dist/pool.d.ts.map
CHANGED
|
@@ -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;
|
|
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);
|
package/dist/prompt.d.ts
ADDED
|
@@ -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
|
|
19
|
-
* (additive observability) — subscribing never affects a run and never enters
|
|
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, "
|
|
27
|
-
* "session_close", "backend_error"). The listener is typed to the event.
|
|
28
|
-
* thunk. A pooled runner multiplexes many concurrent runs, so each
|
|
29
|
-
* `{ sessionId, backendId, label?, runId? }` for filtering. Listeners are
|
|
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
|
package/dist/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
|