@agen-ai/acp-transport 0.4.1 → 0.4.3

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
@@ -5,12 +5,17 @@ This optional AgenAI Agent SDK package connects a host to streams supplied by th
5
5
  host. It validates messages against packaged, hash-pinned ACP schemas and owns
6
6
  framing, correlation, reverse requests, deadlines and connection cleanup.
7
7
 
8
- The coordinated SDK source version is `0.4.1` and remains unpublished; the published
9
- release is `0.4.0`. The package is ESM, MIT
8
+ The coordinated release is `0.4.3`; published `0.4.1` keeps its original
9
+ archives and qualification. The package is ESM, MIT
10
10
  licensed and side-effect-free on import, with exact Ajv `8.17.1` as its sole runtime
11
11
  dependency. It is independent of the provider-neutral `@agen-ai/agent-protocol`
12
12
  and `@agen-ai/agent-runtime` packages.
13
13
 
14
+ Keep installed SDK packages at one coordinated version. Exact reverse-request cancellation and
15
+ actual response-write settlement are transport facts; the provider maps them to V12 request
16
+ availability and independent delivery receipts. Controlled transport FIFO proof does not qualify
17
+ simultaneous vendor callbacks. See the [0.4.2 migration guide](../../MIGRATING-TO-0.4.2.md).
18
+
14
19
  ## Connect existing streams
15
20
 
16
21
  The host supplies the agent's stdin, stdout and optional stderr streams. Process
@@ -57,7 +62,7 @@ bounded protocol limits, typed errors and explicit extension registration.
57
62
  No internal file path is a supported import.
58
63
 
59
64
  Transport errors and validated remote JSON-RPC errors retain their ACP meanings. Provider adapters
60
- own translation into Agent Protocol V11 failures and any provider-owned retry observation; the
65
+ own translation into Agent Protocol V12 failures and any provider-owned retry observation; the
61
66
  transport has no dependency on that protocol and grants no turn replay or quota authority.
62
67
 
63
68
  An adapter migrating from a private transport import should use these public
@@ -88,6 +93,14 @@ A reverse-request handler fulfills before its validated JSON-RPC response is ser
88
93
  Successful local writing does not establish peer receipt: the transport tracks no acknowledgment
89
94
  of a reverse response. A provider's canonical answer receipt must state its own acceptance boundary.
90
95
 
96
+ Each reverse handler receives `context.settled`, a nonrejecting promise that fulfills after that
97
+ handler and its actual response-write obligation settle. Register reservation release against it
98
+ when callback material must remain charged through writing. Do not await it inside the handler:
99
+ the transport must first receive the handler's result before it can write the response. The promise
100
+ also remains pending when disposal rejects an entered write early; an actual write callback or
101
+ stream termination must settle the underlying obligation. Fulfillment observes completion, not
102
+ successful delivery or peer acceptance, and does not authorize answer replay.
103
+
91
104
  Requests can set `onResponseReceived` to admit a validated result or remote error
92
105
  synchronously before the request promise settles and before the next frame is
93
106
  delivered. This lets a session order its terminal marker behind preceding updates.
package/dist/client.js CHANGED
@@ -101,18 +101,18 @@ function createAcpClient(options) {
101
101
  if (state !== "open") return;
102
102
  void close({ kind: "fatal", error }).catch(() => void 0);
103
103
  };
104
- const writeOrFail = async (message) => {
104
+ const writeOrFail = async (message, observeSettlement) => {
105
105
  try {
106
- await writer.write(message);
106
+ await writer.write(message, observeSettlement);
107
107
  } catch (error) {
108
108
  const normalized = error instanceof Error ? error : new AcpClientError("acp_stdin_write_failed", "The ACP input stream write failed.");
109
109
  fatal(normalized);
110
110
  throw normalized;
111
111
  }
112
112
  };
113
- const sendErrorResponse = async (id, error) => {
113
+ const sendErrorResponse = async (id, error, observeSettlement) => {
114
114
  if (state !== "open") return;
115
- await writeOrFail({ jsonrpc: "2.0", id, error });
115
+ await writeOrFail({ jsonrpc: "2.0", id, error }, observeSettlement);
116
116
  };
117
117
  const sendReservedErrorResponse = (key, id, error) => {
118
118
  reservedInboundRequestIds.add(key);
@@ -267,18 +267,23 @@ function createAcpClient(options) {
267
267
  }
268
268
  })();
269
269
  };
270
- const runReverseRequest = (message, extension, handler, controller) => (async () => {
270
+ const runReverseRequest = (message, extension, handler, controller, settled) => (async () => {
271
+ let actualWrite;
272
+ const observeWrite = (settlement) => {
273
+ actualWrite = settlement;
274
+ };
271
275
  try {
272
276
  const result = await handler({
273
277
  id: message.id,
274
278
  method: message.method,
275
279
  params: message.params,
276
- signal: controller.signal
280
+ signal: controller.signal,
281
+ settled
277
282
  });
278
283
  assertBoundedAcpJson(result, limits);
279
284
  if (extension) extensions.validateResult("inbound_request", message.method, result);
280
285
  else schema.validate("client", message.method, "response", result);
281
- if (state === "open") await writeOrFail({ jsonrpc: "2.0", id: message.id, result });
286
+ if (state === "open") await writeOrFail({ jsonrpc: "2.0", id: message.id, result }, observeWrite);
282
287
  } catch (error) {
283
288
  if (state !== "open") return;
284
289
  const rpcError = error instanceof AcpRpcHandlerError ? error : controller.signal.aborted ? new AcpRpcHandlerError(-32800, "Request cancelled") : new AcpRpcHandlerError(-32603, "Internal error");
@@ -287,7 +292,9 @@ function createAcpClient(options) {
287
292
  code: rpcError.code,
288
293
  message: rpcError.message,
289
294
  ...rpcError.data !== void 0 ? { data: rpcError.data } : {}
290
- });
295
+ }, observeWrite);
296
+ } finally {
297
+ await actualWrite;
291
298
  }
292
299
  })();
293
300
  const handleRequest = (message) => {
@@ -339,7 +346,7 @@ function createAcpClient(options) {
339
346
  reservedInboundRequestIds.add(key);
340
347
  const settlement = deferred();
341
348
  activeReverse.set(key, { controller, settlement: settlement.promise });
342
- void runReverseRequest(message, extension, handler, controller).catch((error) => fatal(error instanceof Error ? error : new AcpProtocolError(
349
+ void runReverseRequest(message, extension, handler, controller, settlement.promise).catch((error) => fatal(error instanceof Error ? error : new AcpProtocolError(
343
350
  "acp_reverse_request_failed"
344
351
  ))).finally(() => {
345
352
  activeReverse.delete(key);
@@ -10,6 +10,8 @@ export interface AcpReverseRequestContext {
10
10
  readonly method: string;
11
11
  readonly params: AcpJsonValue | undefined;
12
12
  readonly signal: AbortSignal;
13
+ /** Fulfills after this handler and its actual response write settle. Observe without awaiting inside the handler. */
14
+ readonly settled: Promise<void>;
13
15
  }
14
16
  export type AcpReverseRequestHandler = (context: AcpReverseRequestContext) => AcpJsonValue | Promise<AcpJsonValue>;
15
17
  export interface AcpNotificationContext {
@@ -1,7 +1,7 @@
1
1
  import type { Writable } from 'node:stream';
2
2
  import type { AcpMessage } from './protocol.js';
3
3
  export interface AcpMessageWriter {
4
- readonly write: (message: AcpMessage) => Promise<void>;
4
+ readonly write: (message: AcpMessage, observeSettlement?: (settled: Promise<void>) => void) => Promise<void>;
5
5
  readonly dispose: (drainMs: number) => Promise<void>;
6
6
  }
7
7
  export declare function createAcpMessageWriter(input: {
@@ -4,8 +4,9 @@ function createAcpMessageWriter(input) {
4
4
  let queuedBytes = 0;
5
5
  let queue = Promise.resolve();
6
6
  const activeRejectors = /* @__PURE__ */ new Set();
7
- const writeNow = (frame) => new Promise((resolve, reject) => {
7
+ const writeNow = (frame, settled) => new Promise((resolve, reject) => {
8
8
  if (disposed) {
9
+ settled?.();
9
10
  reject(new AcpClientError("acp_client_disposed", "The ACP client is disposed."));
10
11
  return;
11
12
  }
@@ -14,7 +15,7 @@ function createAcpMessageWriter(input) {
14
15
  let writeCompleted = false;
15
16
  let drainRequired = false;
16
17
  let drainObserved = false;
17
- let settled = false;
18
+ let observed = false;
18
19
  const cleanupListeners = () => {
19
20
  if (!listenersAttached) return;
20
21
  listenersAttached = false;
@@ -23,8 +24,8 @@ function createAcpMessageWriter(input) {
23
24
  input.writable.off("close", onClose);
24
25
  };
25
26
  const settle = (error) => {
26
- if (settled) return;
27
- settled = true;
27
+ if (observed) return;
28
+ observed = true;
28
29
  activeRejectors.delete(rejectActive);
29
30
  if (error) reject(error);
30
31
  else resolve();
@@ -32,6 +33,7 @@ function createAcpMessageWriter(input) {
32
33
  const complete = (error) => {
33
34
  cleanupListeners();
34
35
  settle(error);
36
+ settled?.();
35
37
  };
36
38
  const completeSuccessfulWrite = () => {
37
39
  if (!returnObserved || !writeCompleted || drainRequired && !drainObserved) return;
@@ -40,7 +42,10 @@ function createAcpMessageWriter(input) {
40
42
  const rejectActive = (error) => {
41
43
  settle(error);
42
44
  input.writable.off("drain", onDrain);
43
- if (writeCompleted) cleanupListeners();
45
+ if (writeCompleted) {
46
+ cleanupListeners();
47
+ settled?.();
48
+ }
44
49
  };
45
50
  const onDrain = () => {
46
51
  drainObserved = true;
@@ -62,8 +67,10 @@ function createAcpMessageWriter(input) {
62
67
  const admitted = input.writable.write(frame, (error) => {
63
68
  if (error) return;
64
69
  writeCompleted = true;
65
- if (disposed) cleanupListeners();
66
- else completeSuccessfulWrite();
70
+ if (disposed) {
71
+ cleanupListeners();
72
+ settled?.();
73
+ } else completeSuccessfulWrite();
67
74
  });
68
75
  drainRequired = !admitted;
69
76
  drainObserved = admitted;
@@ -77,7 +84,7 @@ function createAcpMessageWriter(input) {
77
84
  }
78
85
  });
79
86
  const writer = {
80
- write: (message) => {
87
+ write: (message, observeSettlement) => {
81
88
  if (disposed) {
82
89
  return Promise.reject(new AcpClientError(
83
90
  "acp_client_disposed",
@@ -99,7 +106,9 @@ function createAcpMessageWriter(input) {
99
106
  );
100
107
  }
101
108
  queuedBytes += frame.byteLength;
102
- const operation = queue.then(() => writeNow(frame));
109
+ const actual = observeSettlement ? Promise.withResolvers() : void 0;
110
+ if (actual) observeSettlement?.(actual.promise);
111
+ const operation = queue.then(() => writeNow(frame, actual?.resolve));
103
112
  queue = operation.then(
104
113
  () => {
105
114
  queuedBytes -= frame.byteLength;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agen-ai/acp-transport",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,