@mono-agent/agent-runtime 0.18.0 → 0.18.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/MIGRATION.md +49 -1
- package/README.md +73 -10
- package/package.json +1 -1
- package/src/agent/tools/agent-tool.js +16 -4
- package/src/agent/tools/web-controller.js +97 -7
- package/src/agent/tools/web-search.js +296 -60
- package/src/ai/providers/acp-client.js +38 -13
- package/src/ai/providers/acp-session-tokens.js +198 -45
- package/src/ai/providers/acp-transport.js +97 -0
- package/src/ai/providers/acp.js +22 -6
- package/src/ai/providers/claude-cli.js +77 -22
- package/src/ai/providers/claude-sdk.js +54 -11
- package/src/ai/providers/claude-subagent-activity.js +719 -0
- package/src/ai/providers/codex-app.js +1039 -105
- package/src/ai/runtime/router.js +42 -0
- package/src/ai/types.js +101 -11
- package/src/runtime.js +1 -0
- package/types/agent/tools/web-controller.d.ts +5 -1
- package/types/agent/tools/web-search.d.ts +13 -0
- package/types/ai/providers/acp-client.d.ts +4 -0
- package/types/ai/providers/acp-session-tokens.d.ts +16 -9
- package/types/ai/providers/acp-transport.d.ts +13 -0
- package/types/ai/providers/claude-subagent-activity.d.ts +53 -0
- package/types/ai/runtime/router.d.ts +9 -0
- package/types/ai/types.d.ts +243 -22
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
createCipheriv,
|
|
5
|
+
createDecipheriv,
|
|
6
|
+
hkdfSync,
|
|
7
|
+
randomBytes,
|
|
8
|
+
} from "node:crypto";
|
|
9
|
+
|
|
3
10
|
const PROFILE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
4
11
|
const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
5
12
|
const MAX_PROFILE_ID_LENGTH = 128;
|
|
6
13
|
const MAX_RAW_TOKEN_BYTES = 4_096;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
14
|
+
const TOKEN_KEY_BYTES = 32;
|
|
15
|
+
const NONCE_BYTES = 12;
|
|
16
|
+
const AUTH_TAG_BYTES = 16;
|
|
17
|
+
const MAX_SEALED_TOKEN_BYTES = NONCE_BYTES + MAX_RAW_TOKEN_BYTES + AUTH_TAG_BYTES;
|
|
18
|
+
const MAX_ENCODED_TOKEN_LENGTH = Math.ceil(MAX_SEALED_TOKEN_BYTES * 4 / 3);
|
|
19
|
+
const PROVIDER_SESSION_PREFIX = "acp:v2:";
|
|
20
|
+
const SESSION_CURSOR_PREFIX = "acp-cursor:v2:";
|
|
10
21
|
const MAX_PROVIDER_SESSION_ID_LENGTH = PROVIDER_SESSION_PREFIX.length
|
|
11
22
|
+ MAX_PROFILE_ID_LENGTH + 1 + MAX_ENCODED_TOKEN_LENGTH;
|
|
12
23
|
const MAX_SESSION_CURSOR_LENGTH = SESSION_CURSOR_PREFIX.length
|
|
13
24
|
+ MAX_PROFILE_ID_LENGTH + 1 + MAX_ENCODED_TOKEN_LENGTH;
|
|
25
|
+
const TOKEN_DOMAIN = Buffer.from("mono-agent/acp-session-token", "utf8");
|
|
26
|
+
const TOKEN_VERSION = Buffer.from("v2", "utf8");
|
|
27
|
+
const HKDF_SALT = Buffer.from("mono-agent/acp-session-token/v2/hkdf", "utf8");
|
|
28
|
+
const ENCRYPTION_INFO = Buffer.from("mono-agent/acp-session-token/v2/encryption", "utf8");
|
|
14
29
|
|
|
15
30
|
export class AcpClientError extends Error {
|
|
16
31
|
/**
|
|
@@ -45,46 +60,187 @@ function requiredTokenString(value, code, label) {
|
|
|
45
60
|
return value;
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The token key is binary on purpose: accepting textual secrets here would
|
|
65
|
+
* make encoding, truncation, and cross-host persistence ambiguous.
|
|
66
|
+
* @param {unknown} value
|
|
67
|
+
*/
|
|
68
|
+
function sessionTokenKey(value) {
|
|
69
|
+
if (!(value instanceof Uint8Array) || value.byteLength !== TOKEN_KEY_BYTES) {
|
|
70
|
+
throw new AcpClientError(
|
|
71
|
+
"invalid_token_key",
|
|
72
|
+
`ACP session token key must be exactly ${TOKEN_KEY_BYTES} bytes.`,
|
|
73
|
+
);
|
|
54
74
|
}
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @param {unknown} key @returns {Buffer} */
|
|
79
|
+
export function validateAcpSessionTokenKey(key) {
|
|
80
|
+
return Buffer.from(sessionTokenKey(key));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @param {ReadonlyArray<Uint8Array>} parts */
|
|
84
|
+
function frame(parts) {
|
|
85
|
+
const size = parts.reduce((total, part) => total + 4 + part.byteLength, 0);
|
|
86
|
+
const result = Buffer.allocUnsafe(size);
|
|
87
|
+
let offset = 0;
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
result.writeUInt32BE(part.byteLength, offset);
|
|
90
|
+
offset += 4;
|
|
91
|
+
Buffer.from(part.buffer, part.byteOffset, part.byteLength).copy(result, offset);
|
|
92
|
+
offset += part.byteLength;
|
|
57
93
|
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @param {"session"|"cursor"} kind @param {string} profileId */
|
|
98
|
+
function tokenAad(kind, profileId) {
|
|
99
|
+
return frame([
|
|
100
|
+
TOKEN_DOMAIN,
|
|
101
|
+
TOKEN_VERSION,
|
|
102
|
+
Buffer.from(kind, "utf8"),
|
|
103
|
+
Buffer.from(profileId, "utf8"),
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @param {Uint8Array} key */
|
|
108
|
+
function encryptionKey(key) {
|
|
109
|
+
const rawKey = Buffer.from(sessionTokenKey(key));
|
|
58
110
|
try {
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
throw new AcpClientError(code, `${label} is not valid UTF-8.`);
|
|
111
|
+
return Buffer.from(hkdfSync("sha256", rawKey, HKDF_SALT, ENCRYPTION_INFO, TOKEN_KEY_BYTES));
|
|
112
|
+
} finally {
|
|
113
|
+
rawKey.fill(0);
|
|
63
114
|
}
|
|
64
115
|
}
|
|
65
116
|
|
|
66
|
-
/** @param {string} profileId @param {string}
|
|
67
|
-
|
|
117
|
+
/** @param {string} profileId @param {string} raw @param {string} code @param {string} label */
|
|
118
|
+
function rawTokenBytes(profileId, raw, code, label) {
|
|
68
119
|
validateAcpProfileId(profileId);
|
|
69
|
-
requiredTokenString(
|
|
70
|
-
|
|
71
|
-
|
|
120
|
+
requiredTokenString(raw, code, label);
|
|
121
|
+
const plaintext = Buffer.from(raw, "utf8");
|
|
122
|
+
if (plaintext.byteLength > MAX_RAW_TOKEN_BYTES) {
|
|
123
|
+
throw new AcpClientError(code, `${label} exceeds ${MAX_RAW_TOKEN_BYTES} bytes.`);
|
|
72
124
|
}
|
|
73
|
-
return
|
|
125
|
+
return plaintext;
|
|
74
126
|
}
|
|
75
127
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
128
|
+
/**
|
|
129
|
+
* @param {"session"|"cursor"} kind
|
|
130
|
+
* @param {string} profileId
|
|
131
|
+
* @param {string} raw
|
|
132
|
+
* @param {Uint8Array} key
|
|
133
|
+
* @param {string} code
|
|
134
|
+
* @param {string} label
|
|
135
|
+
*/
|
|
136
|
+
function sealToken(kind, profileId, raw, key, code, label) {
|
|
137
|
+
const plaintext = rawTokenBytes(profileId, raw, code, label);
|
|
138
|
+
const aad = tokenAad(kind, profileId);
|
|
139
|
+
const nonce = randomBytes(NONCE_BYTES);
|
|
140
|
+
const derivedKey = encryptionKey(key);
|
|
141
|
+
let cipher;
|
|
142
|
+
try {
|
|
143
|
+
cipher = createCipheriv("aes-256-gcm", derivedKey, nonce);
|
|
144
|
+
} finally {
|
|
145
|
+
derivedKey.fill(0);
|
|
80
146
|
}
|
|
81
|
-
|
|
82
|
-
|
|
147
|
+
cipher.setAAD(aad);
|
|
148
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
149
|
+
return Buffer.concat([nonce, ciphertext, cipher.getAuthTag()]).toString("base64url");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @param {string} profileId @param {string} code @param {string} label */
|
|
153
|
+
function tokenProfileId(profileId, code, label) {
|
|
154
|
+
try {
|
|
155
|
+
return validateAcpProfileId(profileId);
|
|
156
|
+
} catch {
|
|
157
|
+
throw new AcpClientError(code, `Invalid ${label}.`);
|
|
83
158
|
}
|
|
84
|
-
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} encoded
|
|
163
|
+
* @param {string} code
|
|
164
|
+
* @param {string} label
|
|
165
|
+
*/
|
|
166
|
+
function sealedTokenBytes(encoded, code, label) {
|
|
167
|
+
if (!BASE64URL_RE.test(encoded)) throw new AcpClientError(code, `Invalid ${label}.`);
|
|
168
|
+
const sealed = Buffer.from(encoded, "base64url");
|
|
169
|
+
if (sealed.toString("base64url") !== encoded
|
|
170
|
+
|| sealed.byteLength <= NONCE_BYTES + AUTH_TAG_BYTES
|
|
171
|
+
|| sealed.byteLength > MAX_SEALED_TOKEN_BYTES) {
|
|
172
|
+
throw new AcpClientError(code, `Invalid ${label}.`);
|
|
173
|
+
}
|
|
174
|
+
return sealed;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {"session"|"cursor"} kind
|
|
179
|
+
* @param {string} profileId
|
|
180
|
+
* @param {string} encoded
|
|
181
|
+
* @param {Uint8Array} key
|
|
182
|
+
* @param {string} code
|
|
183
|
+
* @param {string} label
|
|
184
|
+
*/
|
|
185
|
+
function openToken(kind, profileId, encoded, key, code, label) {
|
|
186
|
+
const sealed = sealedTokenBytes(encoded, code, label);
|
|
187
|
+
const aad = tokenAad(kind, profileId);
|
|
188
|
+
const nonce = sealed.subarray(0, NONCE_BYTES);
|
|
189
|
+
const ciphertext = sealed.subarray(NONCE_BYTES, -AUTH_TAG_BYTES);
|
|
190
|
+
const authTag = sealed.subarray(-AUTH_TAG_BYTES);
|
|
191
|
+
const derivedKey = encryptionKey(key);
|
|
192
|
+
let decipher;
|
|
193
|
+
try {
|
|
194
|
+
decipher = createDecipheriv("aes-256-gcm", derivedKey, nonce);
|
|
195
|
+
} finally {
|
|
196
|
+
derivedKey.fill(0);
|
|
197
|
+
}
|
|
198
|
+
let plaintext;
|
|
199
|
+
try {
|
|
200
|
+
decipher.setAAD(aad);
|
|
201
|
+
decipher.setAuthTag(authTag);
|
|
202
|
+
plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
203
|
+
} catch {
|
|
204
|
+
throw new AcpClientError(code, `Invalid ${label}.`);
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
return requiredTokenString(
|
|
208
|
+
new TextDecoder("utf-8", { fatal: true }).decode(plaintext),
|
|
209
|
+
code,
|
|
210
|
+
label,
|
|
211
|
+
);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error instanceof AcpClientError) throw error;
|
|
214
|
+
throw new AcpClientError(code, `Invalid ${label}.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @param {string} profileId @param {string} sessionId @param {Uint8Array} key */
|
|
219
|
+
export function encodeAcpProviderSessionId(profileId, sessionId, key) {
|
|
220
|
+
const encoded = sealToken("session", profileId, sessionId, key, "invalid_session_id", "ACP session id");
|
|
221
|
+
return `${PROVIDER_SESSION_PREFIX}${profileId}:${encoded}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Internal protocol-state decoder. This module is not a package export.
|
|
226
|
+
* @param {string} providerSessionId
|
|
227
|
+
* @param {Uint8Array} key
|
|
228
|
+
*/
|
|
229
|
+
export function decodeAcpProviderSessionId(providerSessionId, key) {
|
|
230
|
+
if (typeof providerSessionId !== "string" || providerSessionId.length > MAX_PROVIDER_SESSION_ID_LENGTH) {
|
|
231
|
+
throw new AcpClientError("invalid_session_id", "Invalid ACP provider session id.");
|
|
232
|
+
}
|
|
233
|
+
const match = /^acp:v2:([^:]+):([^:]+)$/.exec(providerSessionId);
|
|
85
234
|
if (!match) throw new AcpClientError("invalid_session_id", "Invalid ACP provider session id.");
|
|
86
|
-
const profileId =
|
|
87
|
-
const sessionId =
|
|
235
|
+
const profileId = tokenProfileId(match[1], "invalid_session_id", "ACP provider session id");
|
|
236
|
+
const sessionId = openToken(
|
|
237
|
+
"session",
|
|
238
|
+
profileId,
|
|
239
|
+
match[2],
|
|
240
|
+
key,
|
|
241
|
+
"invalid_session_id",
|
|
242
|
+
"ACP provider session id",
|
|
243
|
+
);
|
|
88
244
|
return { profileId, sessionId };
|
|
89
245
|
}
|
|
90
246
|
|
|
@@ -94,36 +250,33 @@ export function decodeAcpProviderSessionId(providerSessionId) {
|
|
|
94
250
|
*
|
|
95
251
|
* @param {string} providerSessionId
|
|
96
252
|
* @param {string} expectedProfileId
|
|
253
|
+
* @param {Uint8Array} key Host-owned 32-byte ACP session-token key.
|
|
97
254
|
* @returns {string}
|
|
98
255
|
*/
|
|
99
|
-
export function validateAcpProviderSessionId(providerSessionId, expectedProfileId) {
|
|
256
|
+
export function validateAcpProviderSessionId(providerSessionId, expectedProfileId, key) {
|
|
100
257
|
const profileId = validateAcpProfileId(expectedProfileId);
|
|
101
|
-
const decoded = decodeAcpProviderSessionId(providerSessionId);
|
|
258
|
+
const decoded = decodeAcpProviderSessionId(providerSessionId, key);
|
|
102
259
|
if (decoded.profileId !== profileId) {
|
|
103
260
|
throw new AcpClientError("invalid_session_id", "ACP provider session belongs to a different profile.");
|
|
104
261
|
}
|
|
105
262
|
return providerSessionId;
|
|
106
263
|
}
|
|
107
264
|
|
|
108
|
-
/** @param {string} profileId @param {string} cursor */
|
|
109
|
-
export function encodeAcpSessionCursor(profileId, cursor) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (Buffer.byteLength(cursor, "utf8") > MAX_RAW_TOKEN_BYTES) {
|
|
113
|
-
throw new AcpClientError("invalid_cursor", `ACP session cursor exceeds ${MAX_RAW_TOKEN_BYTES} bytes.`);
|
|
114
|
-
}
|
|
115
|
-
return `${SESSION_CURSOR_PREFIX}${profileId}:${Buffer.from(cursor, "utf8").toString("base64url")}`;
|
|
265
|
+
/** @param {string} profileId @param {string} cursor @param {Uint8Array} key */
|
|
266
|
+
export function encodeAcpSessionCursor(profileId, cursor, key) {
|
|
267
|
+
const encoded = sealToken("cursor", profileId, cursor, key, "invalid_cursor", "ACP session cursor");
|
|
268
|
+
return `${SESSION_CURSOR_PREFIX}${profileId}:${encoded}`;
|
|
116
269
|
}
|
|
117
270
|
|
|
118
|
-
/** @param {string} profileId @param {unknown} cursor */
|
|
119
|
-
export function decodeAcpSessionCursor(profileId, cursor) {
|
|
271
|
+
/** @param {string} profileId @param {unknown} cursor @param {Uint8Array} key */
|
|
272
|
+
export function decodeAcpSessionCursor(profileId, cursor, key) {
|
|
120
273
|
validateAcpProfileId(profileId);
|
|
121
274
|
if (typeof cursor !== "string" || cursor.length > MAX_SESSION_CURSOR_LENGTH) {
|
|
122
275
|
throw new AcpClientError("invalid_cursor", "ACP session cursor must be an opaque cursor returned by listAcpSessions.");
|
|
123
276
|
}
|
|
124
|
-
const match = /^acp-cursor:
|
|
125
|
-
if (!match || match[1] !== profileId) {
|
|
277
|
+
const match = /^acp-cursor:v2:([^:]+):([^:]+)$/.exec(cursor);
|
|
278
|
+
if (!match || tokenProfileId(match[1], "invalid_cursor", "ACP session cursor") !== profileId) {
|
|
126
279
|
throw new AcpClientError("invalid_cursor", "ACP session cursor is invalid for this profile.");
|
|
127
280
|
}
|
|
128
|
-
return
|
|
281
|
+
return openToken("cursor", profileId, match[2], key, "invalid_cursor", "ACP session cursor");
|
|
129
282
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
+
|
|
3
5
|
// Strict, bounded ACP v1 newline transport for an owned stdio child process.
|
|
4
6
|
// The SDK's stock ndJsonStream intentionally tolerates malformed input and its
|
|
5
7
|
// line buffer is unbounded, which is a poor fit for a long-lived host boundary.
|
|
@@ -7,6 +9,101 @@
|
|
|
7
9
|
const DEFAULT_MAX_LINE_BYTES = 1024 * 1024;
|
|
8
10
|
const MAX_MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
9
11
|
|
|
12
|
+
// @agentclientprotocol/sdk 1.3.0 sends the trailing arguments of these
|
|
13
|
+
// diagnostics directly to console. Those values include raw JSON-RPC payloads
|
|
14
|
+
// (and, for malformed notifications, Zod error details derived from them).
|
|
15
|
+
// Preserve the useful classification while dropping payload-bearing details.
|
|
16
|
+
const ACP_SDK_PAYLOAD_DIAGNOSTICS = Object.freeze({
|
|
17
|
+
error: new Set([
|
|
18
|
+
"Invalid message",
|
|
19
|
+
"Error handling notification",
|
|
20
|
+
"Got response to unknown request",
|
|
21
|
+
"Failed to parse JSON message:",
|
|
22
|
+
"ACP connection router stopped unexpectedly:",
|
|
23
|
+
]),
|
|
24
|
+
warn: new Set([
|
|
25
|
+
"Skipping JSON line that is not an object:",
|
|
26
|
+
]),
|
|
27
|
+
});
|
|
28
|
+
const acpSdkDiagnosticScope = new AsyncLocalStorage();
|
|
29
|
+
let activeAcpSdkDiagnosticGuards = 0;
|
|
30
|
+
/** @type {null|{
|
|
31
|
+
* originalError: typeof console.error,
|
|
32
|
+
* originalWarn: typeof console.warn,
|
|
33
|
+
* guardedError: typeof console.error,
|
|
34
|
+
* guardedWarn: typeof console.warn,
|
|
35
|
+
* }} */
|
|
36
|
+
let acpSdkConsoleGuard = null;
|
|
37
|
+
|
|
38
|
+
function installAcpSdkConsoleGuard() {
|
|
39
|
+
activeAcpSdkDiagnosticGuards += 1;
|
|
40
|
+
if (acpSdkConsoleGuard) return;
|
|
41
|
+
|
|
42
|
+
const originalError = console.error;
|
|
43
|
+
const originalWarn = console.warn;
|
|
44
|
+
/** @type {typeof console.error} */
|
|
45
|
+
const guardedError = (...args) => {
|
|
46
|
+
const contentFree = args[0];
|
|
47
|
+
if (acpSdkDiagnosticScope.getStore() === true
|
|
48
|
+
&& typeof contentFree === "string"
|
|
49
|
+
&& ACP_SDK_PAYLOAD_DIAGNOSTICS.error.has(contentFree)) {
|
|
50
|
+
Reflect.apply(originalError, console, [contentFree]);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
Reflect.apply(originalError, console, args);
|
|
54
|
+
};
|
|
55
|
+
/** @type {typeof console.warn} */
|
|
56
|
+
const guardedWarn = (...args) => {
|
|
57
|
+
const contentFree = args[0];
|
|
58
|
+
if (acpSdkDiagnosticScope.getStore() === true
|
|
59
|
+
&& typeof contentFree === "string"
|
|
60
|
+
&& ACP_SDK_PAYLOAD_DIAGNOSTICS.warn.has(contentFree)) {
|
|
61
|
+
Reflect.apply(originalWarn, console, [contentFree]);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
Reflect.apply(originalWarn, console, args);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
acpSdkConsoleGuard = { originalError, originalWarn, guardedError, guardedWarn };
|
|
68
|
+
console.error = guardedError;
|
|
69
|
+
console.warn = guardedWarn;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function releaseAcpSdkConsoleGuard() {
|
|
73
|
+
activeAcpSdkDiagnosticGuards = Math.max(0, activeAcpSdkDiagnosticGuards - 1);
|
|
74
|
+
if (activeAcpSdkDiagnosticGuards !== 0 || !acpSdkConsoleGuard) return;
|
|
75
|
+
const guard = acpSdkConsoleGuard;
|
|
76
|
+
acpSdkConsoleGuard = null;
|
|
77
|
+
if (console.error === guard.guardedError) console.error = guard.originalError;
|
|
78
|
+
if (console.warn === guard.guardedWarn) console.warn = guard.originalWarn;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Open one SDK connection inside an async context that strips payload-bearing
|
|
83
|
+
* SDK console arguments. The SDK starts its detached receive loop during
|
|
84
|
+
* `connect`, so descendants retain this scope without muting concurrent host
|
|
85
|
+
* work or other ACP connections.
|
|
86
|
+
*
|
|
87
|
+
* @template {{closed: Promise<unknown>}} T
|
|
88
|
+
* @param {() => T} connect
|
|
89
|
+
* @returns {T}
|
|
90
|
+
*/
|
|
91
|
+
export function connectWithSafeAcpSdkDiagnostics(connect) {
|
|
92
|
+
installAcpSdkConsoleGuard();
|
|
93
|
+
let connection;
|
|
94
|
+
try {
|
|
95
|
+
connection = acpSdkDiagnosticScope.run(true, connect);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
releaseAcpSdkConsoleGuard();
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
void Promise.resolve(connection.closed).then(
|
|
101
|
+
releaseAcpSdkConsoleGuard,
|
|
102
|
+
releaseAcpSdkConsoleGuard,
|
|
103
|
+
);
|
|
104
|
+
return connection;
|
|
105
|
+
}
|
|
106
|
+
|
|
10
107
|
export class AcpTransportError extends Error {
|
|
11
108
|
/**
|
|
12
109
|
* @param {string} code
|
package/src/ai/providers/acp.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
decodeAcpProviderSessionId,
|
|
12
12
|
encodeAcpProviderSessionId,
|
|
13
13
|
validateAcpProviderSessionId,
|
|
14
|
+
validateAcpSessionTokenKey,
|
|
14
15
|
} from "./acp-session-tokens.js";
|
|
15
16
|
import {
|
|
16
17
|
ownAcpSessionUpdateKind,
|
|
@@ -288,8 +289,8 @@ function selectValues(options) {
|
|
|
288
289
|
return values;
|
|
289
290
|
}
|
|
290
291
|
|
|
291
|
-
/** @param {any} connection @param {any} descriptor @param {any} req @param {string} profileId @param {(notification:any)=>void} onUpdate */
|
|
292
|
-
async function openSession(connection, descriptor, req, profileId, onUpdate) {
|
|
292
|
+
/** @param {any} connection @param {any} descriptor @param {any} req @param {string} profileId @param {Uint8Array} key @param {(notification:any)=>void} onUpdate */
|
|
293
|
+
async function openSession(connection, descriptor, req, profileId, key, onUpdate) {
|
|
293
294
|
const cwd = workspaceFor(descriptor, req);
|
|
294
295
|
const config = sessionConfig(descriptor);
|
|
295
296
|
const baseRequest = { cwd, ...config };
|
|
@@ -297,7 +298,7 @@ async function openSession(connection, descriptor, req, profileId, onUpdate) {
|
|
|
297
298
|
const response = await connection.newSession(baseRequest);
|
|
298
299
|
return { sessionId: response.sessionId, response, resumed: false, resumeMethod: null };
|
|
299
300
|
}
|
|
300
|
-
const decoded = decodeAcpProviderSessionId(req.providerSessionId);
|
|
301
|
+
const decoded = decodeAcpProviderSessionId(req.providerSessionId, key);
|
|
301
302
|
if (decoded.profileId !== profileId) {
|
|
302
303
|
throw new AcpClientError("invalid_session_id", "ACP provider session belongs to a different profile.");
|
|
303
304
|
}
|
|
@@ -369,8 +370,13 @@ export async function generateAcpResponse(systemPrompt, req) {
|
|
|
369
370
|
let setup = null;
|
|
370
371
|
let configuration = { modeApplied: false, configOptionsApplied: [] };
|
|
371
372
|
try {
|
|
373
|
+
const sessionTokenKey = validateAcpSessionTokenKey(req?.acpSessionTokenKey);
|
|
372
374
|
if (req?.providerSessionId != null) {
|
|
373
|
-
providerSessionId = validateAcpProviderSessionId(
|
|
375
|
+
providerSessionId = validateAcpProviderSessionId(
|
|
376
|
+
req.providerSessionId,
|
|
377
|
+
profileId,
|
|
378
|
+
sessionTokenKey,
|
|
379
|
+
);
|
|
374
380
|
}
|
|
375
381
|
connection = await connectAcpProfile(profileId, {
|
|
376
382
|
resolveAcpProfile: req.resolveAcpProfile,
|
|
@@ -382,6 +388,7 @@ export async function generateAcpResponse(systemPrompt, req) {
|
|
|
382
388
|
signal: req.abortSignal,
|
|
383
389
|
context: { operation: "run", model: reference },
|
|
384
390
|
operation: "run",
|
|
391
|
+
acpSessionTokenKey: sessionTokenKey,
|
|
385
392
|
});
|
|
386
393
|
capture({
|
|
387
394
|
type: "capabilities_resolved",
|
|
@@ -395,9 +402,18 @@ export async function generateAcpResponse(systemPrompt, req) {
|
|
|
395
402
|
const onUpdate = (notification) => {
|
|
396
403
|
for (const event of normalizeUpdate(notification, state)) capture(event);
|
|
397
404
|
};
|
|
398
|
-
setup = await openSession(
|
|
405
|
+
setup = await openSession(
|
|
406
|
+
connection,
|
|
407
|
+
connection.descriptor,
|
|
408
|
+
req,
|
|
409
|
+
profileId,
|
|
410
|
+
sessionTokenKey,
|
|
411
|
+
onUpdate,
|
|
412
|
+
);
|
|
399
413
|
sessionId = setup.sessionId;
|
|
400
|
-
providerSessionId =
|
|
414
|
+
providerSessionId = setup.resumed && providerSessionId
|
|
415
|
+
? providerSessionId
|
|
416
|
+
: encodeAcpProviderSessionId(profileId, sessionId, sessionTokenKey);
|
|
401
417
|
configuration = await applyClientConfiguration(
|
|
402
418
|
connection,
|
|
403
419
|
sessionId,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
claudeSandboxPolicyProblem,
|
|
26
26
|
} from "./claude-sandbox.js";
|
|
27
27
|
import { resolveSandboxPolicy } from "../../agent/tools/shared/tool-context.js";
|
|
28
|
+
import { createClaudeSubagentActivityNormalizer } from "./claude-subagent-activity.js";
|
|
28
29
|
|
|
29
30
|
const CODEX_CLI_SANDBOX_POLICY_UNSUPPORTED =
|
|
30
31
|
"Direct Codex CLI cannot enforce mono-agent's native srt sandbox scopes. Remove the mono-agent sandbox policy or use a Pi runtime for exact readableRoots, writableRoots, denyWrite, and network rules.";
|
|
@@ -449,6 +450,7 @@ export function buildCliCommand({
|
|
|
449
450
|
"-p",
|
|
450
451
|
"--output-format", "stream-json",
|
|
451
452
|
"--include-partial-messages",
|
|
453
|
+
"--forward-subagent-text",
|
|
452
454
|
"--verbose",
|
|
453
455
|
...(outputSchema ? ["--json-schema", JSON.stringify(outputSchema)] : []),
|
|
454
456
|
"--model", modelWithContextWindow(model, contextWindow),
|
|
@@ -615,8 +617,29 @@ export async function generateCliResponse(systemPrompt, options = {}) {
|
|
|
615
617
|
fileChangeSnapshots: new Map(),
|
|
616
618
|
thinkingBuffer: createThinkingBuffer(),
|
|
617
619
|
};
|
|
620
|
+
const subagentNormalizer = resolved.sdk === "claude-code"
|
|
621
|
+
? createClaudeSubagentActivityNormalizer()
|
|
622
|
+
: null;
|
|
623
|
+
function emitSubagentEvents(activityEvents) {
|
|
624
|
+
for (const activity of activityEvents) {
|
|
625
|
+
events.push(activity);
|
|
626
|
+
options.onEvent?.(activity);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function drainSubagents(reason) {
|
|
630
|
+
if (!subagentNormalizer) return;
|
|
631
|
+
emitSubagentEvents(subagentNormalizer.drain(reason));
|
|
632
|
+
}
|
|
633
|
+
function observedSubagentCapabilities() {
|
|
634
|
+
if (!subagentNormalizer) return { invoked: null, names: [] };
|
|
635
|
+
return {
|
|
636
|
+
invoked: subagentNormalizer.subagentInvoked(),
|
|
637
|
+
names: subagentNormalizer.nativeSubagentsUsed(),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
618
640
|
|
|
619
641
|
const stderrTail = createStderrTail({ limit: 8 * 1024 });
|
|
642
|
+
let abortHandler = null;
|
|
620
643
|
try {
|
|
621
644
|
const child = spawn(commandSpec.command, commandSpec.args, {
|
|
622
645
|
cwd: commandSpec.cwd,
|
|
@@ -638,43 +661,67 @@ export async function generateCliResponse(systemPrompt, options = {}) {
|
|
|
638
661
|
options.onEvent?.(ev);
|
|
639
662
|
return;
|
|
640
663
|
}
|
|
641
|
-
|
|
664
|
+
// Capture the provider session before a child event is consumed. Claude
|
|
665
|
+
// uses the same session id on parent and child records.
|
|
666
|
+
const candidateSessionId = raw.session_id ?? raw.sessionId ?? raw.thread_id ?? null;
|
|
667
|
+
if (typeof candidateSessionId === "string" && candidateSessionId.trim().length > 0) {
|
|
668
|
+
providerSessionId = candidateSessionId.trim();
|
|
669
|
+
}
|
|
670
|
+
const observation = subagentNormalizer?.observe(raw);
|
|
671
|
+
if (observation) emitSubagentEvents(observation.events);
|
|
672
|
+
// Child messages must not be normalized, added to parent text, counted as
|
|
673
|
+
// parent usage, or considered for the parent's StructuredOutput result.
|
|
674
|
+
if (observation?.consumed) return;
|
|
675
|
+
// A root user message may batch a background Agent launch acknowledgement
|
|
676
|
+
// with unrelated tool results. The normalizer removes only that launch
|
|
677
|
+
// block so the remaining parent activity still flows normally.
|
|
678
|
+
const parentRaw = observation?.forwarded ?? raw;
|
|
679
|
+
const ev = normalizeCliEvent(parentRaw, cliEventContext);
|
|
642
680
|
if (ev) {
|
|
643
681
|
events.push(ev);
|
|
644
682
|
options.onEvent?.(ev);
|
|
645
683
|
}
|
|
646
|
-
if (!isCodexReasoningEvent(
|
|
647
|
-
const text = textFromEvent(
|
|
684
|
+
if (!isCodexReasoningEvent(parentRaw)) {
|
|
685
|
+
const text = textFromEvent(parentRaw);
|
|
648
686
|
pushUniqueText(texts, text);
|
|
649
687
|
}
|
|
650
|
-
captureStructuredOutputFromRaw(
|
|
651
|
-
if (
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
// session_id on the init system message and again on the result event.
|
|
655
|
-
const candidateSessionId = raw.session_id ?? raw.sessionId ?? raw.thread_id ?? null;
|
|
656
|
-
if (typeof candidateSessionId === "string" && candidateSessionId.trim().length > 0) {
|
|
657
|
-
providerSessionId = candidateSessionId.trim();
|
|
658
|
-
}
|
|
659
|
-
if (raw.type === "error") {
|
|
660
|
-
const rawError = raw.message || raw.error || "cli error";
|
|
688
|
+
captureStructuredOutputFromRaw(parentRaw);
|
|
689
|
+
if (parentRaw.usage) usage = parentRaw.usage;
|
|
690
|
+
if (parentRaw.type === "error") {
|
|
691
|
+
const rawError = parentRaw.message || parentRaw.error || "cli error";
|
|
661
692
|
errorMessage = typeof rawError === "string" ? rawError : JSON.stringify(rawError);
|
|
662
693
|
failureKind = "provider_unavailable";
|
|
694
|
+
drainSubagents("subagent stopped because the Claude CLI stream failed");
|
|
663
695
|
}
|
|
664
|
-
const resultError = resultEventError(
|
|
696
|
+
const resultError = resultEventError(parentRaw, commandSpec.command);
|
|
665
697
|
if (resultError) {
|
|
666
698
|
errorMessage = resultError.message;
|
|
667
699
|
failureKind = resultError.failureKind;
|
|
700
|
+
drainSubagents("subagent stopped because the Claude CLI stream failed");
|
|
668
701
|
}
|
|
669
702
|
});
|
|
670
703
|
|
|
704
|
+
child.on("error", (error) => {
|
|
705
|
+
if (!errorMessage) errorMessage = error?.message || String(error);
|
|
706
|
+
failureKind ||= "provider_unavailable";
|
|
707
|
+
drainSubagents("subagent stopped because the Claude CLI process failed");
|
|
708
|
+
});
|
|
709
|
+
|
|
671
710
|
if (options.abortSignal) {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
711
|
+
abortHandler = () => {
|
|
712
|
+
drainSubagents("subagent cancelled with the parent run");
|
|
713
|
+
child.kill("SIGTERM");
|
|
714
|
+
};
|
|
715
|
+
if (options.abortSignal.aborted) abortHandler();
|
|
716
|
+
else options.abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
675
717
|
}
|
|
676
718
|
|
|
677
719
|
const exitCode = await new Promise((resolve) => child.on("close", resolve));
|
|
720
|
+
drainSubagents(options.abortSignal?.aborted
|
|
721
|
+
? "subagent cancelled with the parent run"
|
|
722
|
+
: exitCode === 0
|
|
723
|
+
? "subagent stream closed before completion"
|
|
724
|
+
: "subagent stopped because the Claude CLI process failed");
|
|
678
725
|
const stderrText = stderrTail.toString().trim();
|
|
679
726
|
let cliErrorCode = null;
|
|
680
727
|
if (exitCode !== 0 && !errorMessage) errorMessage = stderrText || `${commandSpec.command} exited ${exitCode}`;
|
|
@@ -706,6 +753,7 @@ export async function generateCliResponse(systemPrompt, options = {}) {
|
|
|
706
753
|
cache_creation_tokens: cacheCreationTokens || null,
|
|
707
754
|
cost_usd: costUsd,
|
|
708
755
|
};
|
|
756
|
+
const subagentCapabilities = observedSubagentCapabilities();
|
|
709
757
|
return {
|
|
710
758
|
text,
|
|
711
759
|
structuredResult,
|
|
@@ -733,14 +781,18 @@ export async function generateCliResponse(systemPrompt, options = {}) {
|
|
|
733
781
|
promptCacheActive: (cachedTokens || 0) > 0 || (cacheCreationTokens || 0) > 0,
|
|
734
782
|
thinkingEnabled: null,
|
|
735
783
|
structuredOutputEnforced: !!options.outputSchema,
|
|
736
|
-
subagentInvoked:
|
|
784
|
+
subagentInvoked: subagentCapabilities.invoked,
|
|
737
785
|
mcpServersUsed: Object.keys(options.mcpServers || {}),
|
|
738
|
-
nativeSubagentsUsed:
|
|
786
|
+
nativeSubagentsUsed: subagentCapabilities.names,
|
|
739
787
|
toolCompactionApplied: false,
|
|
740
788
|
contextCompactionApplied: null,
|
|
741
789
|
}),
|
|
742
790
|
};
|
|
743
791
|
} catch (err) {
|
|
792
|
+
drainSubagents(options.abortSignal?.aborted
|
|
793
|
+
? "subagent cancelled with the parent run"
|
|
794
|
+
: "subagent stopped because the Claude CLI process failed");
|
|
795
|
+
const subagentCapabilities = observedSubagentCapabilities();
|
|
744
796
|
return {
|
|
745
797
|
text: texts[texts.length - 1] || null,
|
|
746
798
|
structuredResult,
|
|
@@ -766,14 +818,17 @@ export async function generateCliResponse(systemPrompt, options = {}) {
|
|
|
766
818
|
promptCacheActive: null,
|
|
767
819
|
thinkingEnabled: null,
|
|
768
820
|
structuredOutputEnforced: !!options.outputSchema,
|
|
769
|
-
subagentInvoked:
|
|
821
|
+
subagentInvoked: subagentCapabilities.invoked,
|
|
770
822
|
mcpServersUsed: Object.keys(options.mcpServers || {}),
|
|
771
|
-
nativeSubagentsUsed:
|
|
823
|
+
nativeSubagentsUsed: subagentCapabilities.names,
|
|
772
824
|
toolCompactionApplied: false,
|
|
773
825
|
contextCompactionApplied: null,
|
|
774
826
|
}),
|
|
775
827
|
};
|
|
776
828
|
} finally {
|
|
829
|
+
if (abortHandler && options.abortSignal) {
|
|
830
|
+
options.abortSignal.removeEventListener?.("abort", abortHandler);
|
|
831
|
+
}
|
|
777
832
|
try { rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
778
833
|
}
|
|
779
834
|
}
|