@automatalabs/acp-agents 0.13.0 → 0.15.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/dist/acp-client.d.ts +33 -3
- package/dist/acp-client.d.ts.map +1 -1
- package/dist/acp-client.js +242 -18
- package/dist/capabilities.d.ts +11 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +28 -1
- package/dist/client-handlers.d.ts +7 -1
- package/dist/client-handlers.d.ts.map +1 -1
- package/dist/client-handlers.js +3 -1
- package/dist/events.d.ts +24 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +3 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/interactive.d.ts +14 -6
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +10 -2
- package/dist/permissions.d.ts +5 -1
- 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 +2 -0
- package/dist/protocol-coverage.d.ts +3 -1
- package/dist/protocol-coverage.d.ts.map +1 -1
- package/dist/protocol-coverage.js +7 -7
- package/dist/runner.d.ts +52 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +142 -56
- package/package.json +1 -1
package/dist/runner.js
CHANGED
|
@@ -42,6 +42,7 @@ export class AcpAgentRunner {
|
|
|
42
42
|
* so dedicated interactive connections must receive the SAME deps the pool receives. */
|
|
43
43
|
clientHandlers;
|
|
44
44
|
permissionResolver;
|
|
45
|
+
elicitationResolver;
|
|
45
46
|
/** Held-open interactive sessions own dedicated ACP processes outside the pool. The runner
|
|
46
47
|
* tracks their connections so dispose() can release them and the process-exit hook can
|
|
47
48
|
* synchronously kill any dedicated children if the host exits without release(). */
|
|
@@ -52,16 +53,20 @@ export class AcpAgentRunner {
|
|
|
52
53
|
constructor(options = {}) {
|
|
53
54
|
this.clientHandlers = options.clientHandlers;
|
|
54
55
|
this.permissionResolver = options.onPermissionRequest;
|
|
56
|
+
this.elicitationResolver = options.onElicitation;
|
|
55
57
|
this.pool = new AcpAgentPool(options, {
|
|
56
58
|
onEvent: this.emitEvent,
|
|
57
59
|
permissionResolver: options.onPermissionRequest,
|
|
60
|
+
elicitationResolver: options.onElicitation,
|
|
61
|
+
advertiseElicitation: Boolean(options.onElicitation),
|
|
58
62
|
});
|
|
59
63
|
this.backends = resolveBackendRegistry(options.backends);
|
|
60
64
|
}
|
|
61
65
|
/**
|
|
62
66
|
* Listen in on the live ACP stream. `name` is an ACP `sessionUpdate` discriminant
|
|
63
67
|
* ("agent_message_chunk", "tool_call", "usage_update", …) or one of the cross-cutting events
|
|
64
|
-
* ("session_update" catch-all, "permission_pending", "permission_request",
|
|
68
|
+
* ("session_update" catch-all, "permission_pending", "permission_request",
|
|
69
|
+
* "elicitation_pending", "elicitation_request", "elicitation_complete", "raw_message",
|
|
65
70
|
* "session_open", "session_close", "backend_error"). The listener is typed to the event.
|
|
66
71
|
* Returns an unsubscribe thunk. A pooled runner multiplexes many concurrent runs, so each
|
|
67
72
|
* event carries `{ sessionId, backendId, label?, runId? }` for filtering. Listeners are
|
|
@@ -91,70 +96,66 @@ export class AcpAgentRunner {
|
|
|
91
96
|
* is one).
|
|
92
97
|
*/
|
|
93
98
|
async openSession(opts) {
|
|
99
|
+
return this.createInteractiveSession(opts, "openSession", (connection, prepared) => connection.openSession(prepared.sessionOptions));
|
|
100
|
+
}
|
|
101
|
+
/** List persisted ACP sessions from the selected backend. */
|
|
102
|
+
async listSessions(opts = {}) {
|
|
94
103
|
if (this.disposed)
|
|
95
104
|
throw new Error("ACP agent runner is disposed");
|
|
96
|
-
|
|
105
|
+
validateOptionalLifecycleCwd(opts.cwd, opts.label, "listSessions");
|
|
97
106
|
opts.signal?.throwIfAborted();
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
schema: undefined,
|
|
101
|
-
registry: this.backends,
|
|
102
|
-
permissionResolver: opts.onPermissionRequest,
|
|
103
|
-
retainSessionLog: false,
|
|
104
|
-
});
|
|
105
|
-
this.installExitHook();
|
|
106
|
-
let interactive;
|
|
107
|
-
const connection = PooledConnection.create(prepared.backend, {
|
|
108
|
-
onDead: () => {
|
|
109
|
-
// Dedicated connections are not stored in pool arrays, so there is nothing to evict.
|
|
110
|
-
// Once the public wrapper exists, process death releases it through the normal path:
|
|
111
|
-
// subscriptions are removed, session_close is emitted, and future prompt() calls fail
|
|
112
|
-
// with the clean released-session error. Before then, openSession's catch tears down.
|
|
113
|
-
void interactive?.release();
|
|
114
|
-
},
|
|
115
|
-
onEvent: this.emitEvent,
|
|
116
|
-
permissionResolver: this.permissionResolver,
|
|
117
|
-
clientHandlers: this.clientHandlers,
|
|
118
|
-
});
|
|
119
|
-
let session;
|
|
107
|
+
const backend = selectBackend(opts, this.backends);
|
|
108
|
+
const connection = this.createDedicatedConnection(backend, () => undefined);
|
|
120
109
|
try {
|
|
121
|
-
|
|
110
|
+
const request = {
|
|
111
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
112
|
+
...(opts.cursor ? { cursor: opts.cursor } : {}),
|
|
113
|
+
...(opts.meta ? { _meta: opts.meta } : {}),
|
|
114
|
+
};
|
|
115
|
+
const response = await connection.listSessions(request, opts.label);
|
|
122
116
|
opts.signal?.throwIfAborted();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
117
|
+
if (this.disposed)
|
|
118
|
+
throw new Error("ACP agent runner is disposed");
|
|
119
|
+
return response;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await disposeBestEffort(connection);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Delete a persisted ACP session from the selected backend. */
|
|
126
|
+
async deleteSession(opts) {
|
|
127
|
+
if (this.disposed)
|
|
128
|
+
throw new Error("ACP agent runner is disposed");
|
|
129
|
+
if (typeof opts.sessionId !== "string" || opts.sessionId.trim() === "") {
|
|
130
|
+
throw new WorkflowError("deleteSession requires sessionId to be a non-empty string", WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: opts.label });
|
|
131
|
+
}
|
|
132
|
+
opts.signal?.throwIfAborted();
|
|
133
|
+
const backend = selectBackend(opts, this.backends);
|
|
134
|
+
const connection = this.createDedicatedConnection(backend, () => undefined);
|
|
135
|
+
try {
|
|
136
|
+
const request = {
|
|
137
|
+
sessionId: opts.sessionId,
|
|
138
|
+
...(opts.meta ? { _meta: opts.meta } : {}),
|
|
139
|
+
};
|
|
140
|
+
await connection.deleteSession(request, opts.label);
|
|
127
141
|
opts.signal?.throwIfAborted();
|
|
128
142
|
if (this.disposed)
|
|
129
143
|
throw new Error("ACP agent runner is disposed");
|
|
130
|
-
interactive = new InteractiveSession({
|
|
131
|
-
session,
|
|
132
|
-
connection,
|
|
133
|
-
backend: prepared.backend,
|
|
134
|
-
subscribe: (name, listener) => this.events.on(name, listener),
|
|
135
|
-
onRelease: (self) => this.interactiveSessions.delete(self),
|
|
136
|
-
signal: opts.signal,
|
|
137
|
-
label: opts.label,
|
|
138
|
-
});
|
|
139
|
-
this.interactiveSessions.set(interactive, connection);
|
|
140
|
-
return interactive;
|
|
141
144
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await session?.release();
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
// best-effort: openSession failed, so teardown must never mask the real error.
|
|
148
|
-
}
|
|
149
|
-
try {
|
|
150
|
-
await connection.dispose();
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
// best-effort: same as pool teardown.
|
|
154
|
-
}
|
|
155
|
-
throw error;
|
|
145
|
+
finally {
|
|
146
|
+
await disposeBestEffort(connection);
|
|
156
147
|
}
|
|
157
148
|
}
|
|
149
|
+
/** Load an existing ACP session and return a live, routed InteractiveSession. */
|
|
150
|
+
async loadSession(opts) {
|
|
151
|
+
validateLifecycleSessionId(opts.sessionId, opts.label, "loadSession");
|
|
152
|
+
return this.createInteractiveSession(opts, "loadSession", (connection, prepared) => connection.loadSession(opts.sessionId, prepared.sessionOptions));
|
|
153
|
+
}
|
|
154
|
+
/** Resume an existing ACP session without replay and return a live, routed InteractiveSession. */
|
|
155
|
+
async resumeSession(opts) {
|
|
156
|
+
validateLifecycleSessionId(opts.sessionId, opts.label, "resumeSession");
|
|
157
|
+
return this.createInteractiveSession(opts, "resumeSession", (connection, prepared) => connection.resumeSession(opts.sessionId, prepared.sessionOptions));
|
|
158
|
+
}
|
|
158
159
|
async run(prompt, options = {}) {
|
|
159
160
|
const opts = options;
|
|
160
161
|
const schema = opts.schema;
|
|
@@ -266,6 +267,72 @@ export class AcpAgentRunner {
|
|
|
266
267
|
await this.pool.dispose();
|
|
267
268
|
this.events.removeAllListeners();
|
|
268
269
|
}
|
|
270
|
+
async createInteractiveSession(opts, methodName, open) {
|
|
271
|
+
if (this.disposed)
|
|
272
|
+
throw new Error("ACP agent runner is disposed");
|
|
273
|
+
validateInteractiveCwd(opts.cwd, opts.label, methodName);
|
|
274
|
+
opts.signal?.throwIfAborted();
|
|
275
|
+
const prepared = this.prepareSession(opts, {
|
|
276
|
+
cwd: opts.cwd,
|
|
277
|
+
schema: undefined,
|
|
278
|
+
registry: this.backends,
|
|
279
|
+
permissionResolver: opts.permissionResolver ?? opts.onPermissionRequest,
|
|
280
|
+
elicitationResolver: opts.onElicitation,
|
|
281
|
+
retainSessionLog: opts.retainSessionLog ?? false,
|
|
282
|
+
});
|
|
283
|
+
this.installExitHook();
|
|
284
|
+
let interactive;
|
|
285
|
+
const connection = this.createDedicatedConnection(prepared.backend, () => {
|
|
286
|
+
// Dedicated connections are not stored in pool arrays, so there is nothing to evict.
|
|
287
|
+
// Once the public wrapper exists, process death releases it through the normal path:
|
|
288
|
+
// subscriptions are removed, session_close is emitted, and future prompt() calls fail
|
|
289
|
+
// with the clean released-session error. Before then, this method's catch tears down.
|
|
290
|
+
void interactive?.release();
|
|
291
|
+
});
|
|
292
|
+
let session;
|
|
293
|
+
try {
|
|
294
|
+
session = await open(connection, prepared);
|
|
295
|
+
opts.signal?.throwIfAborted();
|
|
296
|
+
await applyModelSelection(session, prepared.modelSpec, opts);
|
|
297
|
+
opts.signal?.throwIfAborted();
|
|
298
|
+
if (opts.mode)
|
|
299
|
+
await session.setMode(opts.mode);
|
|
300
|
+
opts.signal?.throwIfAborted();
|
|
301
|
+
if (this.disposed)
|
|
302
|
+
throw new Error("ACP agent runner is disposed");
|
|
303
|
+
interactive = new InteractiveSession({
|
|
304
|
+
session,
|
|
305
|
+
connection,
|
|
306
|
+
backend: prepared.backend,
|
|
307
|
+
subscribe: (name, listener) => this.events.on(name, listener),
|
|
308
|
+
onRelease: (self) => this.interactiveSessions.delete(self),
|
|
309
|
+
signal: opts.signal,
|
|
310
|
+
label: opts.label,
|
|
311
|
+
});
|
|
312
|
+
this.interactiveSessions.set(interactive, connection);
|
|
313
|
+
return interactive;
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
try {
|
|
317
|
+
await session?.release();
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// best-effort: lifecycle setup failed, so teardown must never mask the real error.
|
|
321
|
+
}
|
|
322
|
+
await disposeBestEffort(connection);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
createDedicatedConnection(backend, onDead) {
|
|
327
|
+
return PooledConnection.create(backend, {
|
|
328
|
+
onDead,
|
|
329
|
+
onEvent: this.emitEvent,
|
|
330
|
+
permissionResolver: this.permissionResolver,
|
|
331
|
+
elicitationResolver: this.elicitationResolver,
|
|
332
|
+
advertiseElicitation: Boolean(this.elicitationResolver),
|
|
333
|
+
clientHandlers: this.clientHandlers,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
269
336
|
/** Build the backend choice, model-selection spec, tool policy, and session/new options in one
|
|
270
337
|
* place for run() and openSession() so new AcpSessionOptions fields cannot drift by path. */
|
|
271
338
|
prepareSession(opts, config) {
|
|
@@ -284,6 +351,7 @@ export class AcpAgentRunner {
|
|
|
284
351
|
schema: config.schema,
|
|
285
352
|
policy,
|
|
286
353
|
permissionResolver: config.permissionResolver,
|
|
354
|
+
elicitationResolver: config.elicitationResolver,
|
|
287
355
|
signal: config.signal,
|
|
288
356
|
mcpServers: opts.mcpServers,
|
|
289
357
|
// Generic session-scoped _meta passthrough (RunOptions.meta) — merged UNDER the
|
|
@@ -412,9 +480,27 @@ function innerModelSpec(spec, backend) {
|
|
|
412
480
|
}
|
|
413
481
|
/** Interactive sessions are public and long-lived, so fail before spawning a dedicated process
|
|
414
482
|
* when the required worktree root is absent or not absolute. */
|
|
415
|
-
function validateInteractiveCwd(cwd, label) {
|
|
483
|
+
function validateInteractiveCwd(cwd, label, methodName) {
|
|
416
484
|
if (typeof cwd !== "string" || cwd.trim() === "" || !isAbsolute(cwd)) {
|
|
417
|
-
throw new WorkflowError(
|
|
485
|
+
throw new WorkflowError(`${methodName} requires cwd to be a non-empty absolute path`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: label });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function validateOptionalLifecycleCwd(cwd, label, methodName) {
|
|
489
|
+
if (cwd === undefined || cwd === null)
|
|
490
|
+
return;
|
|
491
|
+
validateInteractiveCwd(cwd, label, methodName);
|
|
492
|
+
}
|
|
493
|
+
function validateLifecycleSessionId(sessionId, label, methodName) {
|
|
494
|
+
if (typeof sessionId !== "string" || sessionId.trim() === "") {
|
|
495
|
+
throw new WorkflowError(`${methodName} requires sessionId to be a non-empty string`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: label });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function disposeBestEffort(connection) {
|
|
499
|
+
try {
|
|
500
|
+
await connection.dispose();
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
// Dedicated lifecycle processes are already no longer useful; never mask the request result.
|
|
418
504
|
}
|
|
419
505
|
}
|
|
420
506
|
function backendIdForSpec(spec) {
|