@automatalabs/acp-agents 0.15.0 → 0.17.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/runner.js CHANGED
@@ -98,6 +98,137 @@ export class AcpAgentRunner {
98
98
  async openSession(opts) {
99
99
  return this.createInteractiveSession(opts, "openSession", (connection, prepared) => connection.openSession(prepared.sessionOptions));
100
100
  }
101
+ /** Return the selected backend's initialize-advertised authentication methods. */
102
+ async authMethods(opts = {}) {
103
+ if (this.disposed)
104
+ throw new Error("ACP agent runner is disposed");
105
+ const backend = selectBackend(opts, this.backends);
106
+ const connection = this.createDedicatedConnection(backend, () => undefined);
107
+ try {
108
+ if (this.disposed)
109
+ throw new Error("ACP agent runner is disposed");
110
+ return await connection.authMethods();
111
+ }
112
+ finally {
113
+ await disposeBestEffort(connection);
114
+ }
115
+ }
116
+ /** Drive ACP authenticate on the selected backend. */
117
+ async authenticate(opts) {
118
+ if (this.disposed)
119
+ throw new Error("ACP agent runner is disposed");
120
+ validateRequiredString(opts.methodId, opts.label, "authenticate", "methodId");
121
+ opts.signal?.throwIfAborted();
122
+ const backend = selectBackend(opts, this.backends);
123
+ const connection = this.createDedicatedConnection(backend, () => undefined);
124
+ try {
125
+ const request = {
126
+ methodId: opts.methodId,
127
+ ...(opts.meta ? { _meta: opts.meta } : {}),
128
+ };
129
+ const response = await connection.authenticate(request, opts.label);
130
+ opts.signal?.throwIfAborted();
131
+ if (this.disposed)
132
+ throw new Error("ACP agent runner is disposed");
133
+ return response;
134
+ }
135
+ finally {
136
+ await disposeBestEffort(connection);
137
+ }
138
+ }
139
+ /** List configurable providers from the selected backend. */
140
+ async listProviders(opts = {}) {
141
+ if (this.disposed)
142
+ throw new Error("ACP agent runner is disposed");
143
+ opts.signal?.throwIfAborted();
144
+ const backend = selectBackend(opts, this.backends);
145
+ const connection = this.createDedicatedConnection(backend, () => undefined);
146
+ try {
147
+ const request = {
148
+ ...(opts.meta ? { _meta: opts.meta } : {}),
149
+ };
150
+ const response = await connection.listProviders(request, opts.label);
151
+ opts.signal?.throwIfAborted();
152
+ if (this.disposed)
153
+ throw new Error("ACP agent runner is disposed");
154
+ return response;
155
+ }
156
+ finally {
157
+ await disposeBestEffort(connection);
158
+ }
159
+ }
160
+ /** Configure one provider on the selected backend. */
161
+ async setProvider(opts) {
162
+ if (this.disposed)
163
+ throw new Error("ACP agent runner is disposed");
164
+ validateRequiredString(opts.providerId, opts.label, "setProvider", "providerId");
165
+ validateRequiredString(opts.apiType, opts.label, "setProvider", "apiType");
166
+ validateRequiredString(opts.baseUrl, opts.label, "setProvider", "baseUrl");
167
+ opts.signal?.throwIfAborted();
168
+ const backend = selectBackend(opts, this.backends);
169
+ const connection = this.createDedicatedConnection(backend, () => undefined);
170
+ try {
171
+ const request = {
172
+ providerId: opts.providerId,
173
+ apiType: opts.apiType,
174
+ baseUrl: opts.baseUrl,
175
+ ...(opts.headers ? { headers: opts.headers } : {}),
176
+ ...(opts.meta ? { _meta: opts.meta } : {}),
177
+ };
178
+ const response = await connection.setProvider(request, opts.label);
179
+ opts.signal?.throwIfAborted();
180
+ if (this.disposed)
181
+ throw new Error("ACP agent runner is disposed");
182
+ return response;
183
+ }
184
+ finally {
185
+ await disposeBestEffort(connection);
186
+ }
187
+ }
188
+ /** Disable one provider on the selected backend. */
189
+ async disableProvider(opts) {
190
+ if (this.disposed)
191
+ throw new Error("ACP agent runner is disposed");
192
+ validateRequiredString(opts.providerId, opts.label, "disableProvider", "providerId");
193
+ opts.signal?.throwIfAborted();
194
+ const backend = selectBackend(opts, this.backends);
195
+ const connection = this.createDedicatedConnection(backend, () => undefined);
196
+ try {
197
+ const request = {
198
+ providerId: opts.providerId,
199
+ ...(opts.meta ? { _meta: opts.meta } : {}),
200
+ };
201
+ const response = await connection.disableProvider(request, opts.label);
202
+ opts.signal?.throwIfAborted();
203
+ if (this.disposed)
204
+ throw new Error("ACP agent runner is disposed");
205
+ return response;
206
+ }
207
+ finally {
208
+ await disposeBestEffort(connection);
209
+ }
210
+ }
211
+ /** Logout through the selected backend. */
212
+ async logout(opts = {}) {
213
+ if (this.disposed)
214
+ throw new Error("ACP agent runner is disposed");
215
+ opts.signal?.throwIfAborted();
216
+ const backend = selectBackend(opts, this.backends);
217
+ const connection = this.createDedicatedConnection(backend, () => undefined);
218
+ try {
219
+ const request = {
220
+ ...(opts.meta ? { _meta: opts.meta } : {}),
221
+ };
222
+ const response = await connection.logout(request, opts.label);
223
+ opts.signal?.throwIfAborted();
224
+ if (this.disposed)
225
+ throw new Error("ACP agent runner is disposed");
226
+ return response;
227
+ }
228
+ finally {
229
+ await disposeBestEffort(connection);
230
+ }
231
+ }
101
232
  /** List persisted ACP sessions from the selected backend. */
102
233
  async listSessions(opts = {}) {
103
234
  if (this.disposed)
@@ -180,22 +311,24 @@ export class AcpAgentRunner {
180
311
  registry,
181
312
  signal: opts.signal,
182
313
  });
183
- const session = await this.pool.acquire(prepared.backend, prepared.sessionOptions);
314
+ let session;
184
315
  try {
316
+ session = await this.pool.acquire(prepared.backend, prepared.sessionOptions);
317
+ const activeSession = session;
185
318
  opts.signal?.throwIfAborted();
186
319
  // For a CUSTOM backend chosen by its registered name, the name itself is routing, not a
187
320
  // model id: "browser" selects nothing; "browser/foo" selects "foo". Built-ins get the
188
321
  // full spec unchanged (their catalogs match provider-prefixed and bare ids).
189
- await applyModelSelection(session, prepared.modelSpec, opts);
322
+ await applyModelSelection(activeSession, prepared.modelSpec, opts);
190
323
  opts.signal?.throwIfAborted();
191
324
  if (opts.mode)
192
- await session.setMode(opts.mode);
325
+ await activeSession.setMode(opts.mode);
193
326
  const text = buildRunPrompt(prompt, opts, schema, prepared.backend);
194
327
  const initialPrompt = opts.images && opts.images.length > 0 ? promptWithImages(text, opts.images) : text;
195
328
  // Generic turn-scoped _meta passthrough merged UNDER the backend-computed keys (e.g. the
196
329
  // outputSchema forward when a schema is set) — user meta never clobbers the schema channel.
197
330
  const promptMeta = mergeTurnMeta(opts.promptMeta, prepared.backend.promptMeta(schema));
198
- const response = await session.prompt(initialPrompt, promptMeta);
331
+ const response = await activeSession.prompt(initialPrompt, promptMeta);
199
332
  opts.signal?.throwIfAborted();
200
333
  // Inspect the turn's stop reason BEFORE the text/schema path: a refusal or truncation
201
334
  // must surface distinctly here, never be misread as empty output or burned through the
@@ -204,13 +337,13 @@ export class AcpAgentRunner {
204
337
  if (schema) {
205
338
  const structuredSession = {
206
339
  prompt: async (repromptText) => {
207
- const repromptResponse = await session.prompt(repromptText, promptMeta);
340
+ const repromptResponse = await activeSession.prompt(repromptText, promptMeta);
208
341
  // A repair turn that refuses / truncates / cancels must also surface distinctly
209
342
  // instead of silently continuing the ladder.
210
343
  assertNormalStopReason(repromptResponse.stopReason, opts.label);
211
344
  },
212
- lastText: () => session.currentTurnText(),
213
- tryNative: () => prepared.backend.nativeStructured(session),
345
+ lastText: () => activeSession.currentTurnText(),
346
+ tryNative: () => prepared.backend.nativeStructured(activeSession),
214
347
  };
215
348
  const result = await resolveStructuredOutput(structuredSession, schema, {
216
349
  maxSchemaRetries: opts.maxSchemaRetries,
@@ -219,7 +352,7 @@ export class AcpAgentRunner {
219
352
  });
220
353
  return result;
221
354
  }
222
- const finalText = session.currentTurnText().trim();
355
+ const finalText = activeSession.currentTurnText().trim();
223
356
  if (!finalText) {
224
357
  throw new WorkflowError("Subagent produced no assistant output", WorkflowErrorCode.AGENT_EMPTY_OUTPUT, {
225
358
  recoverable: true,
@@ -232,28 +365,34 @@ export class AcpAgentRunner {
232
365
  // Abort is the engine's concern (throwIfAborted before/after the call) — re-throw it raw.
233
366
  if (opts.signal?.aborted)
234
367
  throw error;
235
- throw mapThrownError(error, opts.label);
368
+ throw mapThrownError(error, {
369
+ label: opts.label,
370
+ backendId: prepared.backend.id,
371
+ authMethods: session?.capabilities?.authMethods,
372
+ });
236
373
  }
237
374
  finally {
238
- // Read real usage on BOTH success and error so partial usage is never lost.
239
- try {
240
- opts.onUsage?.(session.usage.toAgentUsage());
241
- }
242
- catch {
243
- // usage is best-effort; never let it mask the real result/error.
244
- }
245
- try {
246
- opts.onHistory?.(session.history);
247
- }
248
- catch {
249
- // history is diagnostic only.
250
- }
251
- // Release the SESSION (best-effort session/close) WITHOUT killing the pooled process.
252
- try {
253
- await session.release();
254
- }
255
- catch {
256
- // release is best-effort (session already untracked); never mask the real result/error.
375
+ if (session) {
376
+ // Read real usage on BOTH success and error so partial usage is never lost.
377
+ try {
378
+ opts.onUsage?.(session.usage.toAgentUsage());
379
+ }
380
+ catch {
381
+ // usage is best-effort; never let it mask the real result/error.
382
+ }
383
+ try {
384
+ opts.onHistory?.(session.history);
385
+ }
386
+ catch {
387
+ // history is diagnostic only.
388
+ }
389
+ // Release the SESSION (best-effort session/close) WITHOUT killing the pooled process.
390
+ try {
391
+ await session.release();
392
+ }
393
+ catch {
394
+ // release is best-effort (session already untracked); never mask the real result/error.
395
+ }
257
396
  }
258
397
  }
259
398
  }
@@ -320,7 +459,13 @@ export class AcpAgentRunner {
320
459
  // best-effort: lifecycle setup failed, so teardown must never mask the real error.
321
460
  }
322
461
  await disposeBestEffort(connection);
323
- throw error;
462
+ if (opts.signal?.aborted)
463
+ throw error;
464
+ throw mapThrownError(error, {
465
+ label: opts.label,
466
+ backendId: prepared.backend.id,
467
+ authMethods: connection.capabilities?.authMethods,
468
+ });
324
469
  }
325
470
  }
326
471
  createDedicatedConnection(backend, onDead) {
@@ -491,8 +636,11 @@ function validateOptionalLifecycleCwd(cwd, label, methodName) {
491
636
  validateInteractiveCwd(cwd, label, methodName);
492
637
  }
493
638
  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 });
639
+ validateRequiredString(sessionId, label, methodName, "sessionId");
640
+ }
641
+ function validateRequiredString(value, label, methodName, fieldName) {
642
+ if (typeof value !== "string" || value.trim() === "") {
643
+ throw new WorkflowError(`${methodName} requires ${fieldName} to be a non-empty string`, WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, { recoverable: false, agentLabel: label });
496
644
  }
497
645
  }
498
646
  async function disposeBestEffort(connection) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automatalabs/acp-agents",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "license": "Apache-2.0",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -31,7 +31,7 @@
31
31
  "@agentclientprotocol/claude-agent-acp": "0.56.0",
32
32
  "@automatalabs/codex-acp": "1.4.0",
33
33
  "typebox": "1.3.2",
34
- "@automatalabs/shared-types": "0.9.0"
34
+ "@automatalabs/shared-types": "0.11.0"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc -b",