@agen-ai/acp-transport 0.4.1 → 0.4.2
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 +16 -3
- package/dist/client.js +16 -9
- package/dist/clientContracts.d.ts +2 -0
- package/dist/messageWriter.d.ts +1 -1
- package/dist/messageWriter.js +18 -9
- package/package.json +1 -1
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
|
|
9
|
-
|
|
8
|
+
The coordinated source candidate is unpublished `0.4.2`; 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
|
|
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 {
|
package/dist/messageWriter.d.ts
CHANGED
|
@@ -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: {
|
package/dist/messageWriter.js
CHANGED
|
@@ -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
|
|
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 (
|
|
27
|
-
|
|
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)
|
|
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)
|
|
66
|
-
|
|
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
|
|
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;
|