@tencent-ai/cloud-agent-sdk 0.3.0 → 0.3.1-next.4f1aebf.20260506

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/index.cjs CHANGED
@@ -1,4 +1,95 @@
1
1
 
2
+ //#region src/v1/internal/log.ts
3
+ /** 级别数值:越大越详细;方法级别 > 当前级别时,该方法变成 noop。 */
4
+ const LEVEL_NUMBER = {
5
+ off: 0,
6
+ error: 200,
7
+ warn: 300,
8
+ info: 400,
9
+ debug: 500
10
+ };
11
+ /** 默认级别(不配置时)。 */
12
+ const DEFAULT_LEVEL = "warn";
13
+ /** 真正的空函数。替换被关闭的级别,避免字符串拼接开销。 */
14
+ const noop = () => {};
15
+ /** 全 noop 的 logger(内部单例)。 */
16
+ const noopLogger = {
17
+ error: noop,
18
+ warn: noop,
19
+ info: noop,
20
+ debug: noop
21
+ };
22
+ /**
23
+ * 缓存 `[baseLogger, logLevel] -> wrappedLogger`。
24
+ *
25
+ * 用 WeakMap<Logger, ...>:只要 user 传的 logger 引用不变、level 不变,就复用。
26
+ */
27
+ const cache = /* @__PURE__ */ new WeakMap();
28
+ /**
29
+ * 解析级别字符串;非法值返回 undefined(调用方自行回退)。
30
+ */
31
+ function parseLogLevel(value) {
32
+ if (typeof value !== "string") return void 0;
33
+ const lower = value.toLowerCase();
34
+ if (lower in LEVEL_NUMBER) return lower;
35
+ }
36
+ /**
37
+ * 读取环境变量 `CLOUD_AGENT_SDK_LOG`(仅 Node.js 环境,浏览器返回 undefined)。
38
+ *
39
+ * 用 `globalThis` 方式访问避免对 `@types/node` 的强依赖。
40
+ */
41
+ function readEnvLogLevel() {
42
+ const env = globalThis.process?.env;
43
+ if (!env) return void 0;
44
+ return parseLogLevel(env.CLOUD_AGENT_SDK_LOG);
45
+ }
46
+ /**
47
+ * 按 ConnectionOpts 决定最终的 LogLevel。
48
+ *
49
+ * 优先级:`opts.logLevel` > env `CLOUD_AGENT_SDK_LOG` > `'warn'`。
50
+ */
51
+ function resolveLogLevel(opts) {
52
+ return parseLogLevel(opts.logLevel) ?? readEnvLogLevel() ?? DEFAULT_LEVEL;
53
+ }
54
+ /**
55
+ * 返回按 `opts` 过滤后的 Logger。
56
+ *
57
+ * - `opts.logger` 为空时使用 `globalThis.console`
58
+ * - 级别关闭时返回 noop,开启时返回 `baseLogger[fn].bind(baseLogger)`
59
+ * - 相同入参多次调用复用同一对象(WeakMap 缓存)
60
+ */
61
+ function loggerFor(opts) {
62
+ const base = opts.logger ?? console;
63
+ const level = resolveLogLevel(opts);
64
+ if (level === "off") return noopLogger;
65
+ let byLevel = cache.get(base);
66
+ if (!byLevel) {
67
+ byLevel = /* @__PURE__ */ new Map();
68
+ cache.set(base, byLevel);
69
+ }
70
+ const cached = byLevel.get(level);
71
+ if (cached) return cached;
72
+ const wrapped = makeLeveledLogger(base, level);
73
+ byLevel.set(level, wrapped);
74
+ return wrapped;
75
+ }
76
+ /**
77
+ * 按级别构造 Logger:开放的级别直接 bind,关闭的级别替换为 noop。
78
+ *
79
+ * bind 而非 wrap 是为了保留调用点的 stack trace(开发者看日志能点回 SDK 行号)。
80
+ */
81
+ function makeLeveledLogger(base, level) {
82
+ const threshold = LEVEL_NUMBER[level];
83
+ const enable = (fn) => LEVEL_NUMBER[fn] <= threshold && typeof base[fn] === "function" ? base[fn].bind(base) : noop;
84
+ return {
85
+ error: enable("error"),
86
+ warn: enable("warn"),
87
+ info: enable("info"),
88
+ debug: enable("debug")
89
+ };
90
+ }
91
+
92
+ //#endregion
2
93
  //#region src/v1/errors.ts
3
94
  var CloudAgentError = class extends Error {
4
95
  constructor(message, opts = {}) {
@@ -6,7 +97,6 @@ var CloudAgentError = class extends Error {
6
97
  this.name = "CloudAgentError";
7
98
  this.code = opts.code ?? -1;
8
99
  this.httpStatus = opts.httpStatus;
9
- this.logId = opts.logId;
10
100
  this.requestId = opts.requestId;
11
101
  this.originalCause = opts.cause;
12
102
  if (opts.cause && !("cause" in this)) Object.defineProperty(this, "cause", {
@@ -53,72 +143,433 @@ var AcpProtocolError = class extends CloudAgentError {
53
143
  };
54
144
 
55
145
  //#endregion
56
- //#region src/v1/acp/vendor/sdk.ts
57
- const PROTOCOL_VERSION = 1;
58
- async function loadAcpSdk() {
59
- return import("@agentclientprotocol/sdk");
146
+ //#region src/v1/internal/redact.ts
147
+ /**
148
+ * 敏感信息脱敏
149
+ *
150
+ * 范围:headers + 特定已知的 body 字段(白名单 path,非启发式猜测)。
151
+ * 脱敏仅作用于日志打印和钩子回调,不影响实际发送的请求。
152
+ *
153
+ * 详见 docs/agentos/sdk/07-error-retry.md § 4.5。
154
+ */
155
+ /** 需要脱敏的 header 名(小写)。 */
156
+ const SENSITIVE_HEADER_NAMES = new Set([
157
+ "authorization",
158
+ "cookie",
159
+ "set-cookie",
160
+ "x-api-key"
161
+ ]);
162
+ /** 脱敏替换值。 */
163
+ const REDACTED = "***";
164
+ /**
165
+ * 返回脱敏后的 headers 副本。
166
+ *
167
+ * - 键按原样保留(大小写不变)
168
+ * - 值按键名(小写后)匹配敏感列表时替换为 `'***'`
169
+ * - **纯函数**,不修改入参
170
+ */
171
+ function redactHeaders(headers) {
172
+ if (!headers) return {};
173
+ const out = {};
174
+ for (const [name, value] of Object.entries(headers)) out[name] = SENSITIVE_HEADER_NAMES.has(name.toLowerCase()) ? REDACTED : value;
175
+ return out;
176
+ }
177
+ /** 判断 value 是否是对象数组(`Array<Record<string, unknown>>`)。 */
178
+ function isObjectArray(v) {
179
+ return Array.isArray(v) && v.every((x) => x !== null && typeof x === "object" && !Array.isArray(x));
180
+ }
181
+ /**
182
+ * Body 脱敏器:对给定对象做**浅复制 + 定向脱敏**,返回安全的日志副本。
183
+ *
184
+ * 不修改入参。若入参不是对象(string / number / ...),原样返回。
185
+ *
186
+ * 已处理字段:
187
+ * - `agentManifest.secrets[].value` → `'***'`(保留 name,只脱 value)
188
+ */
189
+ function redactBody(body) {
190
+ if (body === null || body === void 0) return body;
191
+ if (typeof body !== "object" || Array.isArray(body)) return body;
192
+ const input = body;
193
+ const out = { ...input };
194
+ const manifest = input.agentManifest;
195
+ if (manifest && typeof manifest === "object" && !Array.isArray(manifest)) {
196
+ const secrets = manifest.secrets;
197
+ if (isObjectArray(secrets)) out.agentManifest = {
198
+ ...manifest,
199
+ secrets: secrets.map((s) => "value" in s ? {
200
+ ...s,
201
+ value: REDACTED
202
+ } : s)
203
+ };
204
+ }
205
+ return out;
60
206
  }
61
207
 
62
208
  //#endregion
63
- //#region src/v1/acp/transport.ts
209
+ //#region src/v1/rest/client.ts
210
+ /** 默认 baseUrl */
211
+ const DEFAULT_BASE_URL = "https://www.codebuddy.cn/v2/agentos";
64
212
  /**
65
- * 创建 Streamable HTTP transport。
66
- * 返回的对象实现 Stream 接口(readable + writable),可直接传给 ClientSideConnection。
213
+ * SDK 内置的默认重试判定。
214
+ *
215
+ * 传 `retry: { retryOn: ... }` 会**完全替换**这里的规则(不叠加)。
216
+ * 公开这个函数是为了让用户在自定义 `retryOn` 里可以手动调用回落到默认:
217
+ *
218
+ * ```ts
219
+ * retry: {
220
+ * retryOn: (err, ctx) => {
221
+ * if (err instanceof TimeoutError && ctx.url.includes('/upload')) return false;
222
+ * return DEFAULT_RETRY_ON(err, ctx);
223
+ * },
224
+ * }
225
+ * ```
67
226
  */
68
- function createStreamableHttpTransport(options) {
69
- const { endpoint, authToken, headers: customHeaders = {}, reconnect = {}, signal: externalSignal, fetch: customFetch = globalThis.fetch, onConnect, onDisconnect, onError, heartbeatTimeout = 6e4, connectionTimeout = 3e4, postTimeout = 3e4, backpressure = {} } = options;
70
- const { enabled: reconnectEnabled = true, initialDelay = 1e3, maxDelay = 3e4, maxRetries = Infinity, jitter: jitterEnabled = true } = reconnect;
71
- const { highWaterMark = 100, lowWaterMark = 50, pauseTimeout = 5e3 } = backpressure;
72
- let connectionId;
73
- let lastEventId;
74
- let reconnectAttempts = 0;
75
- let closed = false;
76
- let isClosing = false;
77
- let connectionVersion = 0;
78
- let connectionReady;
79
- let resolveConnection;
80
- let rejectConnection;
81
- connectionReady = new Promise((resolve, reject) => {
82
- resolveConnection = resolve;
83
- rejectConnection = reject;
84
- });
85
- const abortController = new AbortController();
86
- function isAborted() {
87
- return abortController.signal.aborted || (externalSignal?.aborted ?? false);
88
- }
89
- const messageQueue = [];
90
- const messageResolvers = [];
91
- let streamError = null;
92
- let isPaused = false;
93
- let resumeReading = null;
94
- let lastActivity = Date.now();
95
- let heartbeatCheckTimer;
96
- function enqueueMessage(message) {
97
- if (messageResolvers.length > 0) messageResolvers.shift()(message);
98
- else {
99
- messageQueue.push(message);
100
- if (messageQueue.length >= highWaterMark) isPaused = true;
101
- }
227
+ const DEFAULT_RETRY_ON = (err, ctx) => {
228
+ if (err.name === "AbortError") return false;
229
+ if (err instanceof AuthError) return false;
230
+ if (err instanceof NotFoundError) return false;
231
+ if (err instanceof ValidationError) return false;
232
+ if (err instanceof AcpProtocolError) return false;
233
+ if (err instanceof TimeoutError) return ctx.method === "GET";
234
+ if (err instanceof NetworkError) return true;
235
+ return true;
236
+ };
237
+ /** 默认重试配置。 */
238
+ const DEFAULT_RETRY = {
239
+ maxAttempts: 3,
240
+ backoffMs: 300,
241
+ backoffFactor: 2,
242
+ jitter: true,
243
+ retryOn: DEFAULT_RETRY_ON
244
+ };
245
+ var RestClient = class {
246
+ constructor(opts) {
247
+ this.opts = opts;
248
+ this.logger = loggerFor(opts);
249
+ this.fetchImpl = opts.fetch ?? fetch.bind(globalThis);
102
250
  }
103
- function dequeueMessage() {
104
- if (closed) return Promise.resolve(null);
105
- if (streamError) return Promise.reject(streamError);
106
- if (messageQueue.length > 0) {
107
- const message = messageQueue.shift();
108
- if (isPaused && messageQueue.length <= lowWaterMark) {
109
- isPaused = false;
110
- if (resumeReading) queueMicrotask(() => resumeReading());
111
- }
112
- return Promise.resolve(message);
113
- }
114
- return new Promise((resolve) => {
115
- messageResolvers.push(resolve);
116
- });
251
+ /** GET 请求。 */
252
+ async get(path, query, opts) {
253
+ const url = this.buildUrl(path, query);
254
+ return this.request("GET", url, void 0, opts);
117
255
  }
118
- function updateLastActivity() {
119
- lastActivity = Date.now();
256
+ /** POST 请求。 */
257
+ async post(path, body, opts) {
258
+ const url = this.buildUrl(path);
259
+ return this.request("POST", url, body, opts);
120
260
  }
121
- function startHeartbeatCheck(triggerReconnect) {
261
+ /** PATCH 请求。 */
262
+ async patch(path, body, opts) {
263
+ const url = this.buildUrl(path);
264
+ return this.request("PATCH", url, body, opts);
265
+ }
266
+ /** DELETE 请求。 */
267
+ async delete(path, opts) {
268
+ const url = this.buildUrl(path);
269
+ return this.request("DELETE", url, void 0, opts);
270
+ }
271
+ /** 构建完整 URL。 */
272
+ buildUrl(path, query) {
273
+ const base = (this.opts.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
274
+ const fullPath = path.startsWith("/") ? path : `/${path}`;
275
+ const url = new URL(`${base}${fullPath}`);
276
+ if (query) {
277
+ for (const [k, v] of Object.entries(query)) if (v !== void 0 && v !== "") url.searchParams.set(k, v);
278
+ }
279
+ return url.toString();
280
+ }
281
+ /** 构建请求 headers(含认证、身份、trace、自定义)。 */
282
+ buildHeaders(opts) {
283
+ const headers = {
284
+ "Content-Type": "application/json",
285
+ "Accept": "application/json"
286
+ };
287
+ if (this.opts.apiKey) headers["x-api-key"] = this.opts.apiKey;
288
+ if (this.opts.sourceApp) headers["X-Source-App"] = this.opts.sourceApp;
289
+ if (this.opts.sourceTenantId) headers["X-Source-Tenant-Id"] = this.opts.sourceTenantId;
290
+ if (this.opts.userId) headers["X-User-Id"] = this.opts.userId;
291
+ if (this.opts.headers) Object.assign(headers, this.opts.headers);
292
+ if (opts?.headers) Object.assign(headers, opts.headers);
293
+ headers["X-Request-Id"] = opts?.requestId ?? generateRequestId();
294
+ return headers;
295
+ }
296
+ /** 发起请求(含超时 + 重试 + 日志 + 脱敏 + hooks)。 */
297
+ async request(method, url, body, opts) {
298
+ const shouldRetry = opts?.retry !== void 0 ? opts.retry : this.opts.retry ?? DEFAULT_RETRY;
299
+ const ctx = {
300
+ attempt: 1,
301
+ startTimeMs: 0
302
+ };
303
+ const doRequest = async () => {
304
+ ctx.startTimeMs = Date.now();
305
+ const timeoutMs = opts?.timeoutMs ?? this.opts.timeoutMs ?? 3e4;
306
+ const headers = this.buildHeaders(opts);
307
+ const requestId = headers["X-Request-Id"];
308
+ const controller = new AbortController();
309
+ let timeoutId;
310
+ if (timeoutMs > 0) timeoutId = setTimeout(() => controller.abort(), timeoutMs);
311
+ if (opts?.signal) if (opts.signal.aborted) controller.abort();
312
+ else opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
313
+ const redactedReqHeaders = redactHeaders(headers);
314
+ this.opts.onRequest?.({
315
+ method,
316
+ url,
317
+ headers: { ...redactedReqHeaders }
318
+ });
319
+ this.logger.debug(`Sending HTTP Request: ${method} ${url}`, {
320
+ headers: redactedReqHeaders,
321
+ body: body !== void 0 ? truncate(redactBody(body)) : void 0
322
+ });
323
+ try {
324
+ const response = await this.fetchImpl(url, {
325
+ method,
326
+ headers,
327
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
328
+ signal: controller.signal
329
+ });
330
+ const durationMs = Date.now() - ctx.startTimeMs;
331
+ const responseHeaders = {};
332
+ response.headers.forEach((v, k) => {
333
+ responseHeaders[k] = v;
334
+ });
335
+ const redactedResHeaders = redactHeaders(responseHeaders);
336
+ this.opts.onResponse?.({
337
+ status: response.status,
338
+ headers: { ...redactedResHeaders },
339
+ url
340
+ });
341
+ const serverReqId = response.headers.get("x-request-id") ?? requestId;
342
+ this.logger.debug(`HTTP Response: ${method} ${url} "${response.status} ${response.statusText}" durationMs=${durationMs}`, { headers: redactedResHeaders });
343
+ if (serverReqId) this.logger.debug(`request_id: ${serverReqId}`);
344
+ return await this.unwrap(response, serverReqId);
345
+ } catch (err) {
346
+ if (timeoutId) clearTimeout(timeoutId);
347
+ const durationMs = Date.now() - ctx.startTimeMs;
348
+ if (err instanceof Error && err.name === "AbortError") {
349
+ if (opts?.signal?.aborted) {
350
+ this.logger.debug(`Request cancelled by caller after ${durationMs}ms: ${method} ${url}`);
351
+ throw err;
352
+ }
353
+ const msg = `Request timed out after ${timeoutMs}ms: ${method} ${url}`;
354
+ this.logger.debug(msg);
355
+ throw new TimeoutError(msg, { cause: err });
356
+ }
357
+ if (err instanceof TypeError || err instanceof Error && isNetworkError(err)) {
358
+ const msg = `Network error: ${method} ${url} - ${err.message}`;
359
+ this.logger.debug(msg);
360
+ throw new NetworkError(msg, { cause: err });
361
+ }
362
+ if (err instanceof CloudAgentError) throw err;
363
+ const msg = `Unexpected error: ${method} ${url} - ${err.message}`;
364
+ this.logger.error(msg);
365
+ throw new NetworkError(msg, { cause: err });
366
+ } finally {
367
+ if (timeoutId) clearTimeout(timeoutId);
368
+ }
369
+ };
370
+ if (shouldRetry === false) return doRequest();
371
+ return this.withRetry(doRequest, shouldRetry, ctx, method, url);
372
+ }
373
+ /** 解包响应 `{ code, msg, data }` → `data` 或抛错。 */
374
+ async unwrap(response, serverReqId) {
375
+ if (response.status === 204) return;
376
+ let body;
377
+ let rawText;
378
+ try {
379
+ rawText = await response.text();
380
+ const safeText = rawText.replace(/:\s*(\d{17,})/g, (match, num) => match.replace(num, `"${num}"`));
381
+ body = JSON.parse(safeText);
382
+ } catch {
383
+ if (!response.ok) throw this.httpStatusToError(response.status, `HTTP ${response.status}`, { requestId: serverReqId });
384
+ return rawText;
385
+ }
386
+ const effectiveRequestId = body.requestId || serverReqId;
387
+ if (!response.ok) throw this.httpStatusToError(response.status, body.msg || `HTTP ${response.status}`, {
388
+ requestId: effectiveRequestId,
389
+ code: body.code
390
+ });
391
+ if (body.code !== 0) throw this.codeToError(body.code, body.msg, {
392
+ requestId: effectiveRequestId,
393
+ httpStatus: response.status
394
+ });
395
+ return body.data;
396
+ }
397
+ /** HTTP 状态码映射为错误。 */
398
+ httpStatusToError(status, message, ctx) {
399
+ const opts = {
400
+ httpStatus: status,
401
+ requestId: ctx.requestId,
402
+ code: ctx.code
403
+ };
404
+ switch (status) {
405
+ case 400: return new ValidationError(message, opts);
406
+ case 401:
407
+ case 403: return new AuthError(message, opts);
408
+ case 404: return new NotFoundError(message, opts);
409
+ case 408:
410
+ case 504: return new TimeoutError(message, opts);
411
+ case 429: return new NetworkError(message, opts);
412
+ default:
413
+ if (status >= 500) return new NetworkError(message, opts);
414
+ return new CloudAgentError(message, opts);
415
+ }
416
+ }
417
+ /** 业务 code 映射为错误。 */
418
+ codeToError(code, message, ctx) {
419
+ const opts = {
420
+ code,
421
+ httpStatus: ctx.httpStatus,
422
+ requestId: ctx.requestId
423
+ };
424
+ switch (code) {
425
+ case 10001: return new ValidationError(message, opts);
426
+ case 10034:
427
+ case 10085: return new AuthError(message, opts);
428
+ case 10064:
429
+ case 10065:
430
+ case 10067: return new NetworkError(message, opts);
431
+ case 10084: return new NotFoundError(message, opts);
432
+ case 10094: return new TimeoutError(message, opts);
433
+ case 1e4: return new NetworkError(message, opts);
434
+ default: return new CloudAgentError(message, opts);
435
+ }
436
+ }
437
+ /**
438
+ * 重试逻辑。
439
+ *
440
+ * 是否重试只看 `opts.retryOn(err, ctx)`。用户传的 retryOn **完全替换** SDK
441
+ * 默认判定(不叠加、不 fallback 到 error 类上的任何字段)。
442
+ */
443
+ async withRetry(fn, retryOpts, reqCtx, method, url) {
444
+ const opts = {
445
+ ...DEFAULT_RETRY,
446
+ ...retryOpts
447
+ };
448
+ let lastError;
449
+ for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
450
+ reqCtx.attempt = attempt;
451
+ try {
452
+ return await fn();
453
+ } catch (err) {
454
+ lastError = err;
455
+ const retryCtx = {
456
+ attempt,
457
+ method,
458
+ url
459
+ };
460
+ if (!opts.retryOn(err, retryCtx)) throw err;
461
+ if (attempt >= opts.maxAttempts) break;
462
+ const delayMs = this.getBackoffMs(attempt, opts);
463
+ this.logger.warn(`Retrying request to ${method} ${url} in ${delayMs}ms (attempt ${attempt}/${opts.maxAttempts}): ${err.name}: ${err.message}`);
464
+ await sleep(delayMs);
465
+ }
466
+ }
467
+ throw lastError;
468
+ }
469
+ /** 计算退避时间。 */
470
+ getBackoffMs(attempt, opts) {
471
+ const baseDelay = Math.min(opts.backoffMs * Math.pow(opts.backoffFactor, attempt - 1), 3e4);
472
+ if (!opts.jitter) return Math.round(baseDelay);
473
+ const jitterFactor = .2 * (Math.random() * 2 - 1);
474
+ return Math.round(baseDelay * (1 + jitterFactor));
475
+ }
476
+ };
477
+ function sleep(ms) {
478
+ return new Promise((resolve) => setTimeout(resolve, ms));
479
+ }
480
+ function isNetworkError(err) {
481
+ const msg = err.message.toLowerCase();
482
+ return msg.includes("failed to fetch") || msg.includes("fetch failed") || msg.includes("network") || msg.includes("econnrefused") || msg.includes("econnreset") || msg.includes("enotfound") || msg.includes("socket hang up");
483
+ }
484
+ function generateRequestId() {
485
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
486
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
487
+ const r = Math.random() * 16 | 0;
488
+ return (c === "x" ? r : r & 3 | 8).toString(16);
489
+ });
490
+ }
491
+ /**
492
+ * 截断超长 body 用于 debug 日志,避免刷屏。
493
+ *
494
+ * 调用方应先通过 `redactBody()` 做敏感字段脱敏,此函数只负责序列化 + 截断。
495
+ */
496
+ function truncate(body, maxChars = 500) {
497
+ try {
498
+ const str = typeof body === "string" ? body : JSON.stringify(body);
499
+ if (str.length <= maxChars) return str;
500
+ return str.slice(0, maxChars) + `…(truncated, total ${str.length} chars)`;
501
+ } catch {
502
+ return "[unserializable]";
503
+ }
504
+ }
505
+
506
+ //#endregion
507
+ //#region src/v1/acp/vendor/sdk.ts
508
+ const PROTOCOL_VERSION = 1;
509
+ async function loadAcpSdk() {
510
+ return import("@agentclientprotocol/sdk");
511
+ }
512
+
513
+ //#endregion
514
+ //#region src/v1/acp/transport.ts
515
+ /**
516
+ * 创建 Streamable HTTP transport。
517
+ * 返回的对象实现 Stream 接口(readable + writable),可直接传给 ClientSideConnection。
518
+ */
519
+ function createStreamableHttpTransport(options) {
520
+ const { endpoint, authToken, headers: customHeaders = {}, reconnect = {}, signal: externalSignal, fetch: customFetch = globalThis.fetch, onConnect, onDisconnect, onError, heartbeatTimeout = 6e4, connectionTimeout = 3e4, postTimeout = 3e4, backpressure = {} } = options;
521
+ const { enabled: reconnectEnabled = true, initialDelay = 1e3, maxDelay = 3e4, maxRetries = Infinity, jitter: jitterEnabled = true } = reconnect;
522
+ const { highWaterMark = 100, lowWaterMark = 50, pauseTimeout = 5e3 } = backpressure;
523
+ let connectionId;
524
+ let lastEventId;
525
+ let reconnectAttempts = 0;
526
+ let closed = false;
527
+ let isClosing = false;
528
+ let connectionVersion = 0;
529
+ let connectionReady;
530
+ let resolveConnection;
531
+ let rejectConnection;
532
+ connectionReady = new Promise((resolve, reject) => {
533
+ resolveConnection = resolve;
534
+ rejectConnection = reject;
535
+ });
536
+ const abortController = new AbortController();
537
+ function isAborted() {
538
+ return abortController.signal.aborted || (externalSignal?.aborted ?? false);
539
+ }
540
+ const messageQueue = [];
541
+ const messageResolvers = [];
542
+ let streamError = null;
543
+ let isPaused = false;
544
+ let resumeReading = null;
545
+ let lastActivity = Date.now();
546
+ let heartbeatCheckTimer;
547
+ function enqueueMessage(message) {
548
+ if (messageResolvers.length > 0) messageResolvers.shift()(message);
549
+ else {
550
+ messageQueue.push(message);
551
+ if (messageQueue.length >= highWaterMark) isPaused = true;
552
+ }
553
+ }
554
+ function dequeueMessage() {
555
+ if (closed) return Promise.resolve(null);
556
+ if (streamError) return Promise.reject(streamError);
557
+ if (messageQueue.length > 0) {
558
+ const message = messageQueue.shift();
559
+ if (isPaused && messageQueue.length <= lowWaterMark) {
560
+ isPaused = false;
561
+ if (resumeReading) queueMicrotask(() => resumeReading());
562
+ }
563
+ return Promise.resolve(message);
564
+ }
565
+ return new Promise((resolve) => {
566
+ messageResolvers.push(resolve);
567
+ });
568
+ }
569
+ function updateLastActivity() {
570
+ lastActivity = Date.now();
571
+ }
572
+ function startHeartbeatCheck(triggerReconnect) {
122
573
  if (heartbeatTimeout <= 0) return;
123
574
  heartbeatCheckTimer = setInterval(() => {
124
575
  if (Date.now() - lastActivity > heartbeatTimeout) triggerReconnect();
@@ -598,2004 +1049,1580 @@ var InvalidStateError = class extends ACPClientError {
598
1049
  constructor(operation, currentState, expectedStates) {
599
1050
  super(`Cannot perform '${operation}' in state '${currentState}'. Expected: ${expectedStates.join(" or ")}`, "INVALID_STATE_ERROR");
600
1051
  this.name = "InvalidStateError";
601
- this.currentState = currentState;
602
- this.expectedStates = expectedStates;
603
- }
604
- };
605
-
606
- //#endregion
607
- //#region src/v1/acp/vendor/events.ts
608
- /**
609
- * Type-safe event emitter implementation
610
- */
611
- var EventEmitter = class {
612
- constructor() {
613
- this.listeners = /* @__PURE__ */ new Map();
614
- this.onceListeners = /* @__PURE__ */ new Map();
615
- }
616
- /**
617
- * Add an event listener
618
- */
619
- on(event, listener) {
620
- if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
621
- this.listeners.get(event).add(listener);
622
- return this;
623
- }
624
- /**
625
- * Remove an event listener
626
- */
627
- off(event, listener) {
628
- const eventListeners = this.listeners.get(event);
629
- if (eventListeners) eventListeners.delete(listener);
630
- const onceEventListeners = this.onceListeners.get(event);
631
- if (onceEventListeners) onceEventListeners.delete(listener);
632
- return this;
633
- }
634
- /**
635
- * Add a one-time event listener
636
- */
637
- once(event, listener) {
638
- if (!this.onceListeners.has(event)) this.onceListeners.set(event, /* @__PURE__ */ new Set());
639
- this.onceListeners.get(event).add(listener);
640
- return this;
641
- }
642
- /**
643
- * Emit an event to all registered listeners
644
- * Returns true if any listeners were invoked
645
- */
646
- emit(event, data) {
647
- const regularListeners = this.listeners.get(event);
648
- const onceEventListeners = this.onceListeners.get(event);
649
- let hasListeners = false;
650
- if (regularListeners && regularListeners.size > 0) {
651
- hasListeners = true;
652
- for (const listener of regularListeners) try {
653
- const result = listener(data);
654
- if (result instanceof Promise) result.catch((err) => {
655
- console.error(`Error in async event listener for '${String(event)}':`, err);
656
- });
657
- } catch (err) {
658
- console.error(`Error in event listener for '${String(event)}':`, err);
659
- }
660
- }
661
- if (onceEventListeners && onceEventListeners.size > 0) {
662
- hasListeners = true;
663
- const listenersToCall = Array.from(onceEventListeners);
664
- this.onceListeners.delete(event);
665
- for (const listener of listenersToCall) try {
666
- const result = listener(data);
667
- if (result instanceof Promise) result.catch((err) => {
668
- console.error(`Error in async once event listener for '${String(event)}':`, err);
669
- });
670
- } catch (err) {
671
- console.error(`Error in once event listener for '${String(event)}':`, err);
672
- }
673
- }
674
- return hasListeners;
675
- }
676
- /**
677
- * Remove all listeners for an event, or all listeners if no event specified
678
- */
679
- removeAllListeners(event) {
680
- if (event !== void 0) {
681
- this.listeners.delete(event);
682
- this.onceListeners.delete(event);
683
- } else {
684
- this.listeners.clear();
685
- this.onceListeners.clear();
686
- }
687
- return this;
688
- }
689
- /**
690
- * Get the number of listeners for an event
691
- */
692
- listenerCount(event) {
693
- return (this.listeners.get(event)?.size ?? 0) + (this.onceListeners.get(event)?.size ?? 0);
694
- }
695
- /**
696
- * Get all event names that have listeners
697
- */
698
- eventNames() {
699
- const names = /* @__PURE__ */ new Set();
700
- for (const event of this.listeners.keys()) names.add(event);
701
- for (const event of this.onceListeners.keys()) names.add(event);
702
- return Array.from(names);
703
- }
704
- };
705
-
706
- //#endregion
707
- //#region src/v1/acp/vendor/extensions.ts
708
- /**
709
- * Extension Method Handler for Streamable HTTP ACP Client
710
- * Routes and handles custom extension notifications
711
- */
712
- /**
713
- * Manages extension methods and notifications
714
- */
715
- var ExtensionManager = class {
716
- constructor(config = {}) {
717
- this.handlers = /* @__PURE__ */ new Map();
718
- this.config = config;
719
- }
720
- /**
721
- * Register a handler for a specific extension method
722
- */
723
- registerHandler(method, handler) {
724
- this.handlers.set(method, handler);
725
- return () => {
726
- this.handlers.delete(method);
727
- };
728
- }
729
- /**
730
- * Set a fallback handler for unknown extensions
731
- */
732
- setFallbackHandler(handler) {
733
- this.fallbackHandler = handler;
734
- }
735
- /**
736
- * Handle an extension notification
737
- */
738
- async handleNotification(method, params) {
739
- this.config.logger?.debug(`Extension notification: ${method}`);
740
- const handler = this.handlers.get(method);
741
- if (handler) {
742
- await handler(method, params);
743
- return;
744
- }
745
- if (this.fallbackHandler) {
746
- await this.fallbackHandler(method, params);
747
- return;
748
- }
749
- if (!this.isKnownExtension(method)) this.config.logger?.warn(`Unknown extension notification: ${method}`);
750
- }
751
- /**
752
- * Check if a method is a known extension
753
- */
754
- isKnownExtension(method) {
755
- return KNOWN_EXTENSIONS.includes(method);
756
- }
757
- /**
758
- * Check if method is the artifact extension
759
- */
760
- isArtifactExtension(method) {
761
- return method === ExtensionMethod.ARTIFACT;
762
- }
763
- /**
764
- * Clear all handlers
765
- */
766
- clear() {
767
- this.handlers.clear();
768
- this.fallbackHandler = void 0;
1052
+ this.currentState = currentState;
1053
+ this.expectedStates = expectedStates;
769
1054
  }
770
1055
  };
771
1056
 
772
1057
  //#endregion
773
- //#region src/v1/acp/vendor/permissions.ts
1058
+ //#region src/v1/acp/vendor/events.ts
774
1059
  /**
775
- * Manages permission requests from the agent
1060
+ * Type-safe event emitter implementation
776
1061
  */
777
- var PermissionManager = class {
778
- constructor(config = {}) {
779
- this.pending = /* @__PURE__ */ new Map();
780
- this.callbacks = {};
781
- this.config = {
782
- timeout: DEFAULT_PERMISSION_TIMEOUT,
783
- autoRejectOnTimeout: true,
784
- autoApprove: false,
785
- ...config
786
- };
787
- }
788
- /**
789
- * Set event callbacks
790
- */
791
- setCallbacks(callbacks) {
792
- this.callbacks = callbacks;
1062
+ var EventEmitter = class {
1063
+ constructor() {
1064
+ this.listeners = /* @__PURE__ */ new Map();
1065
+ this.onceListeners = /* @__PURE__ */ new Map();
793
1066
  }
794
1067
  /**
795
- * Handle a permission request from the agent
1068
+ * Add an event listener
796
1069
  */
797
- async handleRequest(params) {
798
- const requestId = params.toolCall.toolCallId;
799
- this.config.logger?.debug(`Permission request received: ${requestId}`);
800
- if (this.config.autoApprove) {
801
- const firstOption = params.options[0];
802
- this.config.logger?.debug(`Auto-approving permission: ${requestId}`);
803
- return { outcome: {
804
- outcome: "selected",
805
- optionId: firstOption?.optionId ?? "approve"
806
- } };
807
- }
808
- if (this.config.handler) return this.config.handler(params);
809
- return new Promise((resolve, reject) => {
810
- const pending = {
811
- params,
812
- resolve,
813
- reject,
814
- createdAt: Date.now()
815
- };
816
- if (this.config.timeout && this.config.timeout > 0) pending.timeoutId = setTimeout(() => {
817
- this.handleTimeout(requestId);
818
- }, this.config.timeout);
819
- this.pending.set(requestId, pending);
820
- this.callbacks.onRequest?.(requestId, params);
821
- });
1070
+ on(event, listener) {
1071
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
1072
+ this.listeners.get(event).add(listener);
1073
+ return this;
822
1074
  }
823
1075
  /**
824
- * Handle timeout for a permission request
1076
+ * Remove an event listener
825
1077
  */
826
- handleTimeout(requestId) {
827
- const pending = this.pending.get(requestId);
828
- if (!pending) return;
829
- this.config.logger?.warn(`Permission request timed out: ${requestId}`);
830
- this.callbacks.onTimeout?.(requestId);
831
- if (this.config.autoRejectOnTimeout) pending.resolve({ outcome: { outcome: "cancelled" } });
832
- else pending.reject(new TimeoutError$1("permission", this.config.timeout ?? DEFAULT_PERMISSION_TIMEOUT));
833
- this.pending.delete(requestId);
1078
+ off(event, listener) {
1079
+ const eventListeners = this.listeners.get(event);
1080
+ if (eventListeners) eventListeners.delete(listener);
1081
+ const onceEventListeners = this.onceListeners.get(event);
1082
+ if (onceEventListeners) onceEventListeners.delete(listener);
1083
+ return this;
834
1084
  }
835
1085
  /**
836
- * Resolve a permission request with a selected option
837
- * Returns true if the permission was found and resolved
1086
+ * Add a one-time event listener
838
1087
  */
839
- resolve(requestId, optionId) {
840
- const pending = this.pending.get(requestId);
841
- if (!pending) {
842
- this.config.logger?.warn(`Permission request not found: ${requestId}`);
843
- return false;
844
- }
845
- if (pending.timeoutId) clearTimeout(pending.timeoutId);
846
- this.config.logger?.debug(`Permission resolved: ${requestId} -> ${optionId}`);
847
- pending.resolve({ outcome: {
848
- outcome: "selected",
849
- optionId
850
- } });
851
- this.pending.delete(requestId);
852
- this.callbacks.onResolved?.(requestId, optionId);
853
- return true;
1088
+ once(event, listener) {
1089
+ if (!this.onceListeners.has(event)) this.onceListeners.set(event, /* @__PURE__ */ new Set());
1090
+ this.onceListeners.get(event).add(listener);
1091
+ return this;
854
1092
  }
855
1093
  /**
856
- * Reject (cancel) a permission request
857
- * Returns true if the permission was found and rejected
1094
+ * Emit an event to all registered listeners
1095
+ * Returns true if any listeners were invoked
858
1096
  */
859
- reject(requestId, reason) {
860
- const pending = this.pending.get(requestId);
861
- if (!pending) {
862
- this.config.logger?.warn(`Permission request not found: ${requestId}`);
863
- return false;
1097
+ emit(event, data) {
1098
+ const regularListeners = this.listeners.get(event);
1099
+ const onceEventListeners = this.onceListeners.get(event);
1100
+ let hasListeners = false;
1101
+ if (regularListeners && regularListeners.size > 0) {
1102
+ hasListeners = true;
1103
+ for (const listener of regularListeners) try {
1104
+ const result = listener(data);
1105
+ if (result instanceof Promise) result.catch((err) => {
1106
+ console.error(`Error in async event listener for '${String(event)}':`, err);
1107
+ });
1108
+ } catch (err) {
1109
+ console.error(`Error in event listener for '${String(event)}':`, err);
1110
+ }
864
1111
  }
865
- if (pending.timeoutId) clearTimeout(pending.timeoutId);
866
- this.config.logger?.debug(`Permission rejected: ${requestId}${reason ? ` - ${reason}` : ""}`);
867
- pending.resolve({ outcome: { outcome: "cancelled" } });
868
- this.pending.delete(requestId);
869
- this.callbacks.onRejected?.(requestId, reason);
870
- return true;
871
- }
872
- /**
873
- * Get all pending permissions
874
- */
875
- getPending() {
876
- const result = /* @__PURE__ */ new Map();
877
- for (const [id, pending] of this.pending) result.set(id, {
878
- params: pending.params,
879
- createdAt: pending.createdAt
880
- });
881
- return result;
882
- }
883
- /**
884
- * Get a specific pending permission
885
- */
886
- getPendingById(requestId) {
887
- const pending = this.pending.get(requestId);
888
- if (!pending) return;
889
- return {
890
- params: pending.params,
891
- createdAt: pending.createdAt
892
- };
893
- }
894
- /**
895
- * Check if there are any pending permissions
896
- */
897
- hasPending() {
898
- return this.pending.size > 0;
899
- }
900
- /**
901
- * Get the count of pending permissions
902
- */
903
- get pendingCount() {
904
- return this.pending.size;
905
- }
906
- /**
907
- * Clear all pending permissions (reject all)
908
- */
909
- clear() {
910
- for (const [requestId, pending] of this.pending) {
911
- if (pending.timeoutId) clearTimeout(pending.timeoutId);
912
- pending.resolve({ outcome: { outcome: "cancelled" } });
913
- this.callbacks.onRejected?.(requestId, "cleared");
1112
+ if (onceEventListeners && onceEventListeners.size > 0) {
1113
+ hasListeners = true;
1114
+ const listenersToCall = Array.from(onceEventListeners);
1115
+ this.onceListeners.delete(event);
1116
+ for (const listener of listenersToCall) try {
1117
+ const result = listener(data);
1118
+ if (result instanceof Promise) result.catch((err) => {
1119
+ console.error(`Error in async once event listener for '${String(event)}':`, err);
1120
+ });
1121
+ } catch (err) {
1122
+ console.error(`Error in once event listener for '${String(event)}':`, err);
1123
+ }
914
1124
  }
915
- this.pending.clear();
916
- this.config.logger?.debug("Cleared all pending permissions");
917
- }
918
- /**
919
- * Update configuration
920
- */
921
- updateConfig(config) {
922
- this.config = {
923
- ...this.config,
924
- ...config
925
- };
926
- }
927
- };
928
-
929
- //#endregion
930
- //#region src/v1/acp/vendor/questions.ts
931
- /**
932
- * Manages question requests from the agent (ask_followup_question tool)
933
- */
934
- var QuestionManager = class {
935
- constructor(config = {}) {
936
- this.pending = /* @__PURE__ */ new Map();
937
- this.callbacks = {};
938
- this.config = {
939
- timeout: DEFAULT_QUESTION_TIMEOUT,
940
- autoCancelOnTimeout: true,
941
- ...config
942
- };
943
- }
944
- /**
945
- * Set event callbacks
946
- */
947
- setCallbacks(callbacks) {
948
- this.callbacks = callbacks;
949
- }
950
- /**
951
- * Handle a question request from the agent
952
- * Called when receiving _codebuddy.ai/question extMethod
953
- */
954
- async handleRequest(request) {
955
- const toolCallId = request.toolCallId;
956
- this.config.logger?.debug(`Question request received: ${toolCallId}`);
957
- return new Promise((resolve, reject) => {
958
- const pending = {
959
- request,
960
- resolve,
961
- reject,
962
- createdAt: Date.now()
963
- };
964
- const timeout = request.timeout ?? this.config.timeout;
965
- if (timeout && timeout > 0) pending.timeoutId = setTimeout(() => {
966
- this.handleTimeout(toolCallId);
967
- }, timeout);
968
- this.pending.set(toolCallId, pending);
969
- this.callbacks.onRequest?.(toolCallId, request);
970
- });
1125
+ return hasListeners;
971
1126
  }
972
1127
  /**
973
- * Handle timeout for a question request
1128
+ * Remove all listeners for an event, or all listeners if no event specified
974
1129
  */
975
- handleTimeout(toolCallId) {
976
- const pending = this.pending.get(toolCallId);
977
- if (!pending) return;
978
- this.config.logger?.warn(`Question request timed out: ${toolCallId}`);
979
- this.callbacks.onTimeout?.(toolCallId);
980
- if (this.config.autoCancelOnTimeout) pending.resolve({
981
- outcome: "cancelled",
982
- reason: "timeout"
983
- });
984
- else pending.reject(new TimeoutError$1("question", this.config.timeout ?? DEFAULT_QUESTION_TIMEOUT));
985
- this.pending.delete(toolCallId);
1130
+ removeAllListeners(event) {
1131
+ if (event !== void 0) {
1132
+ this.listeners.delete(event);
1133
+ this.onceListeners.delete(event);
1134
+ } else {
1135
+ this.listeners.clear();
1136
+ this.onceListeners.clear();
1137
+ }
1138
+ return this;
986
1139
  }
987
1140
  /**
988
- * Answer a question request with user's selections
989
- * Returns true if the request was found and answered
1141
+ * Get the number of listeners for an event
990
1142
  */
991
- answer(toolCallId, answers) {
992
- const pending = this.pending.get(toolCallId);
993
- if (!pending) {
994
- this.config.logger?.warn(`Question request not found: ${toolCallId}`);
995
- return false;
996
- }
997
- if (pending.timeoutId) clearTimeout(pending.timeoutId);
998
- this.config.logger?.debug(`Question answered: ${toolCallId}`);
999
- pending.resolve({
1000
- outcome: "submitted",
1001
- answers
1002
- });
1003
- this.pending.delete(toolCallId);
1004
- this.callbacks.onAnswered?.(toolCallId, answers);
1005
- return true;
1143
+ listenerCount(event) {
1144
+ return (this.listeners.get(event)?.size ?? 0) + (this.onceListeners.get(event)?.size ?? 0);
1006
1145
  }
1007
1146
  /**
1008
- * Cancel a question request
1009
- * Returns true if the request was found and cancelled
1147
+ * Get all event names that have listeners
1010
1148
  */
1011
- cancel(toolCallId, reason) {
1012
- const pending = this.pending.get(toolCallId);
1013
- if (!pending) {
1014
- this.config.logger?.warn(`Question request not found: ${toolCallId}`);
1015
- return false;
1016
- }
1017
- if (pending.timeoutId) clearTimeout(pending.timeoutId);
1018
- this.config.logger?.debug(`Question cancelled: ${toolCallId}${reason ? ` - ${reason}` : ""}`);
1019
- pending.resolve({
1020
- outcome: "cancelled",
1021
- reason
1022
- });
1023
- this.pending.delete(toolCallId);
1024
- this.callbacks.onCancelled?.(toolCallId, reason);
1025
- return true;
1149
+ eventNames() {
1150
+ const names = /* @__PURE__ */ new Set();
1151
+ for (const event of this.listeners.keys()) names.add(event);
1152
+ for (const event of this.onceListeners.keys()) names.add(event);
1153
+ return Array.from(names);
1154
+ }
1155
+ };
1156
+
1157
+ //#endregion
1158
+ //#region src/v1/acp/vendor/extensions.ts
1159
+ /**
1160
+ * Extension Method Handler for Streamable HTTP ACP Client
1161
+ * Routes and handles custom extension notifications
1162
+ */
1163
+ /**
1164
+ * Manages extension methods and notifications
1165
+ */
1166
+ var ExtensionManager = class {
1167
+ constructor(config = {}) {
1168
+ this.handlers = /* @__PURE__ */ new Map();
1169
+ this.config = config;
1026
1170
  }
1027
1171
  /**
1028
- * Get all pending question requests
1172
+ * Register a handler for a specific extension method
1029
1173
  */
1030
- getPending() {
1031
- const result = /* @__PURE__ */ new Map();
1032
- for (const [id, pending] of this.pending) result.set(id, {
1033
- request: pending.request,
1034
- createdAt: pending.createdAt
1035
- });
1036
- return result;
1174
+ registerHandler(method, handler) {
1175
+ this.handlers.set(method, handler);
1176
+ return () => {
1177
+ this.handlers.delete(method);
1178
+ };
1037
1179
  }
1038
1180
  /**
1039
- * Get a specific pending question request
1181
+ * Set a fallback handler for unknown extensions
1040
1182
  */
1041
- getPendingById(toolCallId) {
1042
- const pending = this.pending.get(toolCallId);
1043
- if (!pending) return;
1044
- return {
1045
- request: pending.request,
1046
- createdAt: pending.createdAt
1047
- };
1183
+ setFallbackHandler(handler) {
1184
+ this.fallbackHandler = handler;
1048
1185
  }
1049
1186
  /**
1050
- * Check if there are any pending question requests
1187
+ * Handle an extension notification
1051
1188
  */
1052
- hasPending() {
1053
- return this.pending.size > 0;
1189
+ async handleNotification(method, params) {
1190
+ this.config.logger?.debug(`Extension notification: ${method}`);
1191
+ const handler = this.handlers.get(method);
1192
+ if (handler) {
1193
+ await handler(method, params);
1194
+ return;
1195
+ }
1196
+ if (this.fallbackHandler) {
1197
+ await this.fallbackHandler(method, params);
1198
+ return;
1199
+ }
1200
+ if (!this.isKnownExtension(method)) this.config.logger?.warn(`Unknown extension notification: ${method}`);
1054
1201
  }
1055
1202
  /**
1056
- * Get the count of pending question requests
1203
+ * Check if a method is a known extension
1057
1204
  */
1058
- get pendingCount() {
1059
- return this.pending.size;
1205
+ isKnownExtension(method) {
1206
+ return KNOWN_EXTENSIONS.includes(method);
1060
1207
  }
1061
1208
  /**
1062
- * Clear all pending question requests (cancel all)
1209
+ * Check if method is the artifact extension
1063
1210
  */
1064
- clear() {
1065
- for (const [toolCallId, pending] of this.pending) {
1066
- if (pending.timeoutId) clearTimeout(pending.timeoutId);
1067
- pending.resolve({
1068
- outcome: "cancelled",
1069
- reason: "cleared"
1070
- });
1071
- this.callbacks.onCancelled?.(toolCallId, "cleared");
1072
- }
1073
- this.pending.clear();
1074
- this.config.logger?.debug("Cleared all pending question requests");
1211
+ isArtifactExtension(method) {
1212
+ return method === ExtensionMethod.ARTIFACT;
1075
1213
  }
1076
1214
  /**
1077
- * Update configuration
1215
+ * Clear all handlers
1078
1216
  */
1079
- updateConfig(config) {
1080
- this.config = {
1081
- ...this.config,
1082
- ...config
1083
- };
1217
+ clear() {
1218
+ this.handlers.clear();
1219
+ this.fallbackHandler = void 0;
1084
1220
  }
1085
1221
  };
1086
1222
 
1087
1223
  //#endregion
1088
- //#region src/v1/acp/vendor/client.ts
1089
- /**
1090
- * Streamable HTTP ACP Client
1091
- * Production-grade client for connecting to cloud-hosted ACP agents
1092
- */
1224
+ //#region src/v1/acp/vendor/permissions.ts
1093
1225
  /**
1094
- * Production-grade Streamable HTTP ACP Client
1095
- *
1096
- * Features:
1097
- * - Full ACP protocol support (initialize, session, prompt, cancel)
1098
- * - Artifact notification handling
1099
- * - Permission handling with timeout support
1100
- * - Extension method support
1101
- * - Type-safe event system
1102
- * - Configurable logging
1226
+ * Manages permission requests from the agent
1103
1227
  */
1104
- var StreamableHttpClient = class {
1105
- constructor(options) {
1106
- this.state = "disconnected";
1107
- this.emitter = new EventEmitter();
1108
- this.options = options;
1109
- this.artifactManager = new ArtifactManager({ logger: options.logger });
1110
- this.permissionManager = new PermissionManager({
1111
- timeout: options.permissionTimeout,
1112
- autoRejectOnTimeout: options.permissionAutoRejectOnTimeout ?? true,
1113
- autoApprove: options.autoApprove,
1114
- handler: options.requestPermissionHandler,
1115
- logger: options.logger
1116
- });
1117
- this.permissionManager.setCallbacks({
1118
- onRequest: (requestId, params) => {
1119
- this.emitter.emit("permissionRequest", {
1120
- requestId,
1121
- params
1122
- });
1123
- },
1124
- onResolved: (requestId, optionId) => {
1125
- this.emitter.emit("permissionResolved", {
1126
- requestId,
1127
- optionId
1128
- });
1129
- },
1130
- onRejected: (requestId, reason) => {
1131
- this.emitter.emit("permissionRejected", {
1132
- requestId,
1133
- reason
1134
- });
1135
- },
1136
- onTimeout: (requestId) => {
1137
- this.emitter.emit("permissionTimeout", { requestId });
1138
- }
1139
- });
1140
- this.questionManager = new QuestionManager({
1141
- timeout: options.questionTimeout,
1142
- autoCancelOnTimeout: options.questionAutoCancelOnTimeout ?? true,
1143
- logger: options.logger
1144
- });
1145
- this.questionManager.setCallbacks({
1146
- onRequest: (toolCallId, request) => {
1147
- this.emitter.emit("questionRequest", {
1148
- toolCallId,
1149
- request
1150
- });
1151
- options.onQuestionRequest?.(toolCallId, request);
1152
- },
1153
- onAnswered: (toolCallId, answers) => {
1154
- this.emitter.emit("questionAnswered", {
1155
- toolCallId,
1156
- answers
1157
- });
1158
- },
1159
- onCancelled: (toolCallId, reason) => {
1160
- this.emitter.emit("questionCancelled", {
1161
- toolCallId,
1162
- reason
1163
- });
1164
- },
1165
- onTimeout: (toolCallId) => {
1166
- this.emitter.emit("questionTimeout", { toolCallId });
1167
- }
1228
+ var PermissionManager = class {
1229
+ constructor(config = {}) {
1230
+ this.pending = /* @__PURE__ */ new Map();
1231
+ this.callbacks = {};
1232
+ this.config = {
1233
+ timeout: DEFAULT_PERMISSION_TIMEOUT,
1234
+ autoRejectOnTimeout: true,
1235
+ autoApprove: false,
1236
+ ...config
1237
+ };
1238
+ }
1239
+ /**
1240
+ * Set event callbacks
1241
+ */
1242
+ setCallbacks(callbacks) {
1243
+ this.callbacks = callbacks;
1244
+ }
1245
+ /**
1246
+ * Handle a permission request from the agent
1247
+ */
1248
+ async handleRequest(params) {
1249
+ const requestId = params.toolCall.toolCallId;
1250
+ this.config.logger?.debug(`Permission request received: ${requestId}`);
1251
+ if (this.config.autoApprove) {
1252
+ const firstOption = params.options[0];
1253
+ this.config.logger?.debug(`Auto-approving permission: ${requestId}`);
1254
+ return { outcome: {
1255
+ outcome: "selected",
1256
+ optionId: firstOption?.optionId ?? "approve"
1257
+ } };
1258
+ }
1259
+ if (this.config.handler) return this.config.handler(params);
1260
+ return new Promise((resolve, reject) => {
1261
+ const pending = {
1262
+ params,
1263
+ resolve,
1264
+ reject,
1265
+ createdAt: Date.now()
1266
+ };
1267
+ if (this.config.timeout && this.config.timeout > 0) pending.timeoutId = setTimeout(() => {
1268
+ this.handleTimeout(requestId);
1269
+ }, this.config.timeout);
1270
+ this.pending.set(requestId, pending);
1271
+ this.callbacks.onRequest?.(requestId, params);
1168
1272
  });
1169
- this.extensionManager = new ExtensionManager({ logger: options.logger });
1170
1273
  }
1171
1274
  /**
1172
- * Get current client state
1275
+ * Handle timeout for a permission request
1173
1276
  */
1174
- get currentState() {
1175
- return this.state;
1277
+ handleTimeout(requestId) {
1278
+ const pending = this.pending.get(requestId);
1279
+ if (!pending) return;
1280
+ this.config.logger?.warn(`Permission request timed out: ${requestId}`);
1281
+ this.callbacks.onTimeout?.(requestId);
1282
+ if (this.config.autoRejectOnTimeout) pending.resolve({ outcome: { outcome: "cancelled" } });
1283
+ else pending.reject(new TimeoutError$1("permission", this.config.timeout ?? DEFAULT_PERMISSION_TIMEOUT));
1284
+ this.pending.delete(requestId);
1176
1285
  }
1177
1286
  /**
1178
- * Check if client is initialized
1287
+ * Resolve a permission request with a selected option
1288
+ * Returns true if the permission was found and resolved
1179
1289
  */
1180
- get isInitialized() {
1181
- return this.state === "initialized";
1290
+ resolve(requestId, optionId) {
1291
+ const pending = this.pending.get(requestId);
1292
+ if (!pending) {
1293
+ this.config.logger?.warn(`Permission request not found: ${requestId}`);
1294
+ return false;
1295
+ }
1296
+ if (pending.timeoutId) clearTimeout(pending.timeoutId);
1297
+ this.config.logger?.debug(`Permission resolved: ${requestId} -> ${optionId}`);
1298
+ pending.resolve({ outcome: {
1299
+ outcome: "selected",
1300
+ optionId
1301
+ } });
1302
+ this.pending.delete(requestId);
1303
+ this.callbacks.onResolved?.(requestId, optionId);
1304
+ return true;
1182
1305
  }
1183
1306
  /**
1184
- * Check if client is connected (but maybe not initialized)
1307
+ * Reject (cancel) a permission request
1308
+ * Returns true if the permission was found and rejected
1185
1309
  */
1186
- get isConnected() {
1187
- return this.state === "connected" || this.state === "initialized";
1310
+ reject(requestId, reason) {
1311
+ const pending = this.pending.get(requestId);
1312
+ if (!pending) {
1313
+ this.config.logger?.warn(`Permission request not found: ${requestId}`);
1314
+ return false;
1315
+ }
1316
+ if (pending.timeoutId) clearTimeout(pending.timeoutId);
1317
+ this.config.logger?.debug(`Permission rejected: ${requestId}${reason ? ` - ${reason}` : ""}`);
1318
+ pending.resolve({ outcome: { outcome: "cancelled" } });
1319
+ this.pending.delete(requestId);
1320
+ this.callbacks.onRejected?.(requestId, reason);
1321
+ return true;
1188
1322
  }
1189
1323
  /**
1190
- * Get agent capabilities from initialization response
1324
+ * Get all pending permissions
1191
1325
  */
1192
- get agentCapabilities() {
1193
- return this.initializeResponse?.agentCapabilities;
1326
+ getPending() {
1327
+ const result = /* @__PURE__ */ new Map();
1328
+ for (const [id, pending] of this.pending) result.set(id, {
1329
+ params: pending.params,
1330
+ createdAt: pending.createdAt
1331
+ });
1332
+ return result;
1194
1333
  }
1195
1334
  /**
1196
- * Get full initialization response
1335
+ * Get a specific pending permission
1197
1336
  */
1198
- get initializeResult() {
1199
- return this.initializeResponse;
1337
+ getPendingById(requestId) {
1338
+ const pending = this.pending.get(requestId);
1339
+ if (!pending) return;
1340
+ return {
1341
+ params: pending.params,
1342
+ createdAt: pending.createdAt
1343
+ };
1200
1344
  }
1201
1345
  /**
1202
- * Get current transport connection ID
1346
+ * Check if there are any pending permissions
1203
1347
  */
1204
- get connectionId() {
1205
- return this.transport?.connectionId;
1206
- }
1207
- setState(newState) {
1208
- const previous = this.state;
1209
- this.state = newState;
1210
- this.options.logger?.debug(`State change: ${previous} -> ${newState}`);
1211
- this.emitter.emit("stateChange", {
1212
- previous,
1213
- current: newState
1214
- });
1215
- switch (newState) {
1216
- case "connecting":
1217
- this.emitter.emit("connecting", void 0);
1218
- break;
1219
- case "connected":
1220
- this.emitter.emit("connected", void 0);
1221
- break;
1222
- case "disconnected":
1223
- this.emitter.emit("disconnected", void 0);
1224
- break;
1225
- case "error": break;
1226
- }
1348
+ hasPending() {
1349
+ return this.pending.size > 0;
1227
1350
  }
1228
1351
  /**
1229
- * Connect and initialize the client
1352
+ * Get the count of pending permissions
1230
1353
  */
1231
- async connect() {
1232
- if (this.state !== "disconnected") await this.disconnect();
1233
- if (this.state === "initialized") return this.initializeResponse;
1234
- if (this.state === "connecting") throw new ConnectionError("Connection already in progress");
1235
- this.setState("connecting");
1236
- try {
1237
- this.transport = createStreamableHttpTransport({
1238
- endpoint: this.options.endpoint,
1239
- authToken: this.options.authToken,
1240
- headers: this.options.headers,
1241
- reconnect: this.options.reconnect,
1242
- fetch: this.options.fetch,
1243
- heartbeatTimeout: this.options.heartbeatTimeout,
1244
- postTimeout: this.options.postTimeout,
1245
- connectionTimeout: this.options.connectionTimeout,
1246
- onConnect: (connectionId) => {
1247
- this.options.logger?.debug(`Transport connected: ${connectionId}`);
1248
- },
1249
- onDisconnect: (connectionId) => {
1250
- this.options.logger?.debug(`Transport disconnected: ${connectionId}`);
1251
- },
1252
- onError: (error) => {
1253
- this.options.logger?.error("Transport error:", error);
1254
- this.emitter.emit("error", error);
1255
- }
1256
- });
1257
- const { ClientSideConnection } = await loadAcpSdk();
1258
- this.connection = new ClientSideConnection(() => this.createClientHandler(), this.transport);
1259
- this.setState("connected");
1260
- const timeout = this.options.initializeTimeout ?? DEFAULT_INITIALIZE_TIMEOUT;
1261
- const mergedCapabilities = {
1262
- ...this.options.clientCapabilities,
1263
- ...CLOUD_CLIENT_CAPABILITIES,
1264
- _meta: {
1265
- ...this.options.clientCapabilities?._meta,
1266
- ...CLOUD_CLIENT_CAPABILITIES._meta
1267
- }
1268
- };
1269
- const initPromise = this.connection.initialize({
1270
- protocolVersion: PROTOCOL_VERSION,
1271
- clientCapabilities: mergedCapabilities
1272
- });
1273
- const timeoutPromise = new Promise((_, reject) => {
1274
- setTimeout(() => {
1275
- reject(new InitializationError(`Initialize timed out after ${timeout}ms`));
1276
- }, timeout);
1277
- });
1278
- const initializeResponse = await Promise.race([initPromise, timeoutPromise]);
1279
- this.initializeResponse = initializeResponse;
1280
- this.setState("initialized");
1281
- this.options.logger?.info("Client initialized successfully");
1282
- return initializeResponse;
1283
- } catch (err) {
1284
- this.setState("error");
1285
- const error = err instanceof Error ? err : new Error(String(err));
1286
- this.emitter.emit("error", error);
1287
- if (err instanceof InitializationError || err instanceof ConnectionError) throw err;
1288
- throw new ConnectionError("Failed to connect", error);
1289
- }
1354
+ get pendingCount() {
1355
+ return this.pending.size;
1290
1356
  }
1291
1357
  /**
1292
- * Disconnect the client gracefully
1293
- * Sends DELETE request to server before closing local resources
1358
+ * Clear all pending permissions (reject all)
1294
1359
  */
1295
- async disconnect() {
1296
- if (this.state === "disconnected") return;
1297
- this.options.logger?.info("Disconnecting client");
1298
- if (this.transport) {
1299
- try {
1300
- await this.transport.close();
1301
- } catch (err) {
1302
- this.options.logger?.warn("Error closing transport:", err);
1303
- }
1304
- this.transport = void 0;
1360
+ clear() {
1361
+ for (const [requestId, pending] of this.pending) {
1362
+ if (pending.timeoutId) clearTimeout(pending.timeoutId);
1363
+ pending.resolve({ outcome: { outcome: "cancelled" } });
1364
+ this.callbacks.onRejected?.(requestId, "cleared");
1305
1365
  }
1306
- this.permissionManager.clear();
1307
- this.questionManager.clear();
1308
- this.artifactManager.clear();
1309
- this.initializeResponse = void 0;
1310
- this.setState("disconnected");
1366
+ this.pending.clear();
1367
+ this.config.logger?.debug("Cleared all pending permissions");
1311
1368
  }
1312
1369
  /**
1313
- * Create the client handler for the connection
1370
+ * Update configuration
1314
1371
  */
1315
- createClientHandler() {
1316
- return {
1317
- sessionUpdate: async (params) => {
1318
- await this.handleSessionUpdate(params);
1319
- },
1320
- requestPermission: async (params) => this.handleRequestPermission(params),
1321
- extNotification: async (method, params) => {
1322
- await this.handleExtNotification(method, params);
1323
- },
1324
- extMethod: async (method, params) => this.handleExtMethod(method, params)
1372
+ updateConfig(config) {
1373
+ this.config = {
1374
+ ...this.config,
1375
+ ...config
1325
1376
  };
1326
1377
  }
1327
- /**
1328
- * Create a new session
1329
- *
1330
- * Retries on transient network errors (e.g., proxy connection reset)
1331
- * since session/new is idempotent and safe to retry.
1332
- *
1333
- * A timeout (`options.sessionTimeout`, default 30 s) guards against the
1334
- * SSE response never arriving — the POST returns 202 immediately and the
1335
- * sessionId comes back via GET SSE, which can hang indefinitely.
1336
- * On timeout a `SessionError` is thrown.
1337
- */
1338
- async createSession(cwd) {
1339
- this.ensureInitialized("createSession");
1340
- const maxRetries = 2;
1341
- const timeout = this.options.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
1342
- let lastError;
1343
- for (let attempt = 0; attempt <= maxRetries; attempt++) try {
1344
- const sessionPromise = this.connection.newSession({
1345
- cwd,
1346
- mcpServers: []
1347
- });
1348
- let response;
1349
- if (timeout > 0) {
1350
- const timeoutPromise = new Promise((_, reject) => {
1351
- setTimeout(() => {
1352
- reject(new SessionError(`session/new timed out after ${timeout}ms`));
1353
- }, timeout);
1354
- });
1355
- response = await Promise.race([sessionPromise, timeoutPromise]);
1356
- } else response = await sessionPromise;
1357
- this.options.logger?.info(`Session created: ${response.sessionId}`);
1358
- return response;
1359
- } catch (err) {
1360
- lastError = err instanceof Error ? err : new Error(String(err));
1361
- if (attempt < maxRetries && isRetryableNetworkError(err)) {
1362
- const delay = 500 * Math.pow(2, attempt);
1363
- this.options.logger?.warn(`session/new network error, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries}): ${lastError.message}`);
1364
- await new Promise((resolve) => setTimeout(resolve, delay));
1365
- continue;
1366
- }
1367
- throw new SessionError(`Failed to create session: ${lastError.message}`, void 0, lastError);
1368
- }
1369
- throw new SessionError(`Failed to create session: ${lastError?.message}`, void 0, lastError);
1378
+ };
1379
+
1380
+ //#endregion
1381
+ //#region src/v1/acp/vendor/questions.ts
1382
+ /**
1383
+ * Manages question requests from the agent (ask_followup_question tool)
1384
+ */
1385
+ var QuestionManager = class {
1386
+ constructor(config = {}) {
1387
+ this.pending = /* @__PURE__ */ new Map();
1388
+ this.callbacks = {};
1389
+ this.config = {
1390
+ timeout: DEFAULT_QUESTION_TIMEOUT,
1391
+ autoCancelOnTimeout: true,
1392
+ ...config
1393
+ };
1370
1394
  }
1371
1395
  /**
1372
- * Load an existing session
1373
- * Requires agent to support loadSession capability
1396
+ * Set event callbacks
1374
1397
  */
1375
- async loadSession(sessionId, cwd) {
1376
- this.ensureInitialized("loadSession");
1377
- if (!this.agentCapabilities?.loadSession) throw new SessionError("Agent does not support session loading", sessionId);
1378
- try {
1379
- const response = await this.connection.loadSession({
1380
- sessionId,
1381
- cwd,
1382
- mcpServers: []
1383
- });
1384
- this.options.logger?.info(`Session loaded: ${sessionId}`);
1385
- return response;
1386
- } catch (err) {
1387
- throw new SessionError(`Failed to load session: ${err instanceof Error ? err.message : String(err)}`, sessionId, err instanceof Error ? err : void 0);
1388
- }
1398
+ setCallbacks(callbacks) {
1399
+ this.callbacks = callbacks;
1389
1400
  }
1390
1401
  /**
1391
- * Set the session mode
1402
+ * Handle a question request from the agent
1403
+ * Called when receiving _codebuddy.ai/question extMethod
1392
1404
  */
1393
- async setSessionMode(params) {
1394
- this.ensureInitialized("setSessionMode");
1395
- this.options.logger?.debug(`Setting session mode: ${params.sessionId} -> ${params.modeId}`);
1396
- return this.connection.setSessionMode(params);
1405
+ async handleRequest(request) {
1406
+ const toolCallId = request.toolCallId;
1407
+ this.config.logger?.debug(`Question request received: ${toolCallId}`);
1408
+ return new Promise((resolve, reject) => {
1409
+ const pending = {
1410
+ request,
1411
+ resolve,
1412
+ reject,
1413
+ createdAt: Date.now()
1414
+ };
1415
+ const timeout = request.timeout ?? this.config.timeout;
1416
+ if (timeout && timeout > 0) pending.timeoutId = setTimeout(() => {
1417
+ this.handleTimeout(toolCallId);
1418
+ }, timeout);
1419
+ this.pending.set(toolCallId, pending);
1420
+ this.callbacks.onRequest?.(toolCallId, request);
1421
+ });
1397
1422
  }
1398
1423
  /**
1399
- * Set the session model
1400
- * @experimental This API is unstable and may change
1424
+ * Handle timeout for a question request
1401
1425
  */
1402
- async setSessionModel(params) {
1403
- this.ensureInitialized("setSessionModel");
1404
- this.options.logger?.debug(`Setting session model: ${params.sessionId} -> ${params.modelId}`);
1405
- return this.connection.unstable_setSessionModel(params);
1426
+ handleTimeout(toolCallId) {
1427
+ const pending = this.pending.get(toolCallId);
1428
+ if (!pending) return;
1429
+ this.config.logger?.warn(`Question request timed out: ${toolCallId}`);
1430
+ this.callbacks.onTimeout?.(toolCallId);
1431
+ if (this.config.autoCancelOnTimeout) pending.resolve({
1432
+ outcome: "cancelled",
1433
+ reason: "timeout"
1434
+ });
1435
+ else pending.reject(new TimeoutError$1("question", this.config.timeout ?? DEFAULT_QUESTION_TIMEOUT));
1436
+ this.pending.delete(toolCallId);
1406
1437
  }
1407
1438
  /**
1408
- * Send a prompt to the agent
1439
+ * Answer a question request with user's selections
1440
+ * Returns true if the request was found and answered
1409
1441
  */
1410
- async prompt(sessionId, prompt, options) {
1411
- this.ensureInitialized("prompt");
1412
- this.options.logger?.debug(`Sending prompt to session: ${sessionId}`);
1413
- return this.connection.prompt({
1414
- sessionId,
1415
- prompt,
1416
- _meta: options?.planMode ? {
1417
- planMode: true,
1418
- ...options._meta
1419
- } : options?._meta
1442
+ answer(toolCallId, answers) {
1443
+ const pending = this.pending.get(toolCallId);
1444
+ if (!pending) {
1445
+ this.config.logger?.warn(`Question request not found: ${toolCallId}`);
1446
+ return false;
1447
+ }
1448
+ if (pending.timeoutId) clearTimeout(pending.timeoutId);
1449
+ this.config.logger?.debug(`Question answered: ${toolCallId}`);
1450
+ pending.resolve({
1451
+ outcome: "submitted",
1452
+ answers
1420
1453
  });
1454
+ this.pending.delete(toolCallId);
1455
+ this.callbacks.onAnswered?.(toolCallId, answers);
1456
+ return true;
1421
1457
  }
1422
1458
  /**
1423
- * Cancel ongoing operations for a session
1459
+ * Cancel a question request
1460
+ * Returns true if the request was found and cancelled
1424
1461
  */
1425
- async cancel(sessionId) {
1426
- this.ensureInitialized("cancel");
1427
- this.options.logger?.debug(`Cancelling session: ${sessionId}`);
1428
- await this.connection.cancel({ sessionId });
1462
+ cancel(toolCallId, reason) {
1463
+ const pending = this.pending.get(toolCallId);
1464
+ if (!pending) {
1465
+ this.config.logger?.warn(`Question request not found: ${toolCallId}`);
1466
+ return false;
1467
+ }
1468
+ if (pending.timeoutId) clearTimeout(pending.timeoutId);
1469
+ this.config.logger?.debug(`Question cancelled: ${toolCallId}${reason ? ` - ${reason}` : ""}`);
1470
+ pending.resolve({
1471
+ outcome: "cancelled",
1472
+ reason
1473
+ });
1474
+ this.pending.delete(toolCallId);
1475
+ this.callbacks.onCancelled?.(toolCallId, reason);
1476
+ return true;
1429
1477
  }
1430
1478
  /**
1431
- * Resolve a pending permission request
1479
+ * Get all pending question requests
1432
1480
  */
1433
- resolvePermission(requestId, optionId) {
1434
- return this.permissionManager.resolve(requestId, optionId);
1481
+ getPending() {
1482
+ const result = /* @__PURE__ */ new Map();
1483
+ for (const [id, pending] of this.pending) result.set(id, {
1484
+ request: pending.request,
1485
+ createdAt: pending.createdAt
1486
+ });
1487
+ return result;
1435
1488
  }
1436
1489
  /**
1437
- * Reject a pending permission request
1490
+ * Get a specific pending question request
1438
1491
  */
1439
- rejectPermission(requestId, reason) {
1440
- return this.permissionManager.reject(requestId, reason);
1492
+ getPendingById(toolCallId) {
1493
+ const pending = this.pending.get(toolCallId);
1494
+ if (!pending) return;
1495
+ return {
1496
+ request: pending.request,
1497
+ createdAt: pending.createdAt
1498
+ };
1441
1499
  }
1442
1500
  /**
1443
- * Get all pending permissions
1501
+ * Check if there are any pending question requests
1444
1502
  */
1445
- getPendingPermissions() {
1446
- return this.permissionManager.getPending();
1503
+ hasPending() {
1504
+ return this.pending.size > 0;
1447
1505
  }
1448
1506
  /**
1449
- * Check if there are pending permissions
1507
+ * Get the count of pending question requests
1450
1508
  */
1451
- hasPendingPermissions() {
1452
- return this.permissionManager.hasPending();
1509
+ get pendingCount() {
1510
+ return this.pending.size;
1511
+ }
1512
+ /**
1513
+ * Clear all pending question requests (cancel all)
1514
+ */
1515
+ clear() {
1516
+ for (const [toolCallId, pending] of this.pending) {
1517
+ if (pending.timeoutId) clearTimeout(pending.timeoutId);
1518
+ pending.resolve({
1519
+ outcome: "cancelled",
1520
+ reason: "cleared"
1521
+ });
1522
+ this.callbacks.onCancelled?.(toolCallId, "cleared");
1523
+ }
1524
+ this.pending.clear();
1525
+ this.config.logger?.debug("Cleared all pending question requests");
1526
+ }
1527
+ /**
1528
+ * Update configuration
1529
+ */
1530
+ updateConfig(config) {
1531
+ this.config = {
1532
+ ...this.config,
1533
+ ...config
1534
+ };
1535
+ }
1536
+ };
1537
+
1538
+ //#endregion
1539
+ //#region src/v1/acp/vendor/client.ts
1540
+ /**
1541
+ * Streamable HTTP ACP Client
1542
+ * Production-grade client for connecting to cloud-hosted ACP agents
1543
+ */
1544
+ /**
1545
+ * Production-grade Streamable HTTP ACP Client
1546
+ *
1547
+ * Features:
1548
+ * - Full ACP protocol support (initialize, session, prompt, cancel)
1549
+ * - Artifact notification handling
1550
+ * - Permission handling with timeout support
1551
+ * - Extension method support
1552
+ * - Type-safe event system
1553
+ * - Configurable logging
1554
+ */
1555
+ var StreamableHttpClient = class {
1556
+ constructor(options) {
1557
+ this.state = "disconnected";
1558
+ this.emitter = new EventEmitter();
1559
+ this.options = options;
1560
+ this.artifactManager = new ArtifactManager({ logger: options.logger });
1561
+ this.permissionManager = new PermissionManager({
1562
+ timeout: options.permissionTimeout,
1563
+ autoRejectOnTimeout: options.permissionAutoRejectOnTimeout ?? true,
1564
+ autoApprove: options.autoApprove,
1565
+ handler: options.requestPermissionHandler,
1566
+ logger: options.logger
1567
+ });
1568
+ this.permissionManager.setCallbacks({
1569
+ onRequest: (requestId, params) => {
1570
+ this.emitter.emit("permissionRequest", {
1571
+ requestId,
1572
+ params
1573
+ });
1574
+ },
1575
+ onResolved: (requestId, optionId) => {
1576
+ this.emitter.emit("permissionResolved", {
1577
+ requestId,
1578
+ optionId
1579
+ });
1580
+ },
1581
+ onRejected: (requestId, reason) => {
1582
+ this.emitter.emit("permissionRejected", {
1583
+ requestId,
1584
+ reason
1585
+ });
1586
+ },
1587
+ onTimeout: (requestId) => {
1588
+ this.emitter.emit("permissionTimeout", { requestId });
1589
+ }
1590
+ });
1591
+ this.questionManager = new QuestionManager({
1592
+ timeout: options.questionTimeout,
1593
+ autoCancelOnTimeout: options.questionAutoCancelOnTimeout ?? true,
1594
+ logger: options.logger
1595
+ });
1596
+ this.questionManager.setCallbacks({
1597
+ onRequest: (toolCallId, request) => {
1598
+ this.emitter.emit("questionRequest", {
1599
+ toolCallId,
1600
+ request
1601
+ });
1602
+ options.onQuestionRequest?.(toolCallId, request);
1603
+ },
1604
+ onAnswered: (toolCallId, answers) => {
1605
+ this.emitter.emit("questionAnswered", {
1606
+ toolCallId,
1607
+ answers
1608
+ });
1609
+ },
1610
+ onCancelled: (toolCallId, reason) => {
1611
+ this.emitter.emit("questionCancelled", {
1612
+ toolCallId,
1613
+ reason
1614
+ });
1615
+ },
1616
+ onTimeout: (toolCallId) => {
1617
+ this.emitter.emit("questionTimeout", { toolCallId });
1618
+ }
1619
+ });
1620
+ this.extensionManager = new ExtensionManager({ logger: options.logger });
1453
1621
  }
1454
1622
  /**
1455
- * Answer a pending question request with user's selections
1623
+ * Get current client state
1456
1624
  */
1457
- answerQuestion(toolCallId, answers) {
1458
- return this.questionManager.answer(toolCallId, answers);
1625
+ get currentState() {
1626
+ return this.state;
1459
1627
  }
1460
1628
  /**
1461
- * Cancel a pending question request
1629
+ * Check if client is initialized
1462
1630
  */
1463
- cancelQuestion(toolCallId, reason) {
1464
- return this.questionManager.cancel(toolCallId, reason);
1631
+ get isInitialized() {
1632
+ return this.state === "initialized";
1465
1633
  }
1466
1634
  /**
1467
- * Get all pending question requests
1635
+ * Check if client is connected (but maybe not initialized)
1468
1636
  */
1469
- getPendingQuestions() {
1470
- return this.questionManager.getPending();
1637
+ get isConnected() {
1638
+ return this.state === "connected" || this.state === "initialized";
1471
1639
  }
1472
1640
  /**
1473
- * Check if there are pending question requests
1641
+ * Get agent capabilities from initialization response
1474
1642
  */
1475
- hasPendingQuestions() {
1476
- return this.questionManager.hasPending();
1643
+ get agentCapabilities() {
1644
+ return this.initializeResponse?.agentCapabilities;
1477
1645
  }
1478
1646
  /**
1479
- * Send an extension method request
1647
+ * Get full initialization response
1480
1648
  */
1481
- async extMethod(method, params) {
1482
- this.ensureInitialized("extMethod");
1483
- return this.connection.extMethod(method, params);
1649
+ get initializeResult() {
1650
+ return this.initializeResponse;
1484
1651
  }
1485
1652
  /**
1486
- * Send an extension notification
1653
+ * Get current transport connection ID
1487
1654
  */
1488
- async extNotification(method, params) {
1489
- this.ensureInitialized("extNotification");
1490
- return this.connection.extNotification(method, params);
1491
- }
1492
- on(event, listener) {
1493
- this.emitter.on(event, listener);
1494
- return this;
1495
- }
1496
- off(event, listener) {
1497
- this.emitter.off(event, listener);
1498
- return this;
1499
- }
1500
- once(event, listener) {
1501
- this.emitter.once(event, listener);
1502
- return this;
1503
- }
1504
- emit(event, data) {
1505
- return this.emitter.emit(event, data);
1506
- }
1507
- removeAllListeners(event) {
1508
- this.emitter.removeAllListeners(event);
1509
- return this;
1510
- }
1511
- async handleSessionUpdate(params) {
1512
- await this.options.onSessionUpdate?.(params);
1513
- this.emitter.emit("sessionUpdate", params);
1514
- }
1515
- async handleRequestPermission(params) {
1516
- return this.permissionManager.handleRequest(params);
1517
- }
1518
- async handleExtNotification(method, params) {
1519
- if (method === ExtensionMethod.ARTIFACT) {
1520
- const notification = params;
1521
- const artifactData = notification.artifact;
1522
- this.options.logger?.debug("[ACP-Client] Received artifact notification:", {
1523
- event: notification.event,
1524
- artifactUri: artifactData?.uri,
1525
- artifactType: artifactData?.type
1526
- });
1527
- if (notification.event === "deleted") {
1528
- const existing = this.artifactManager.get(notification.artifact.uri);
1529
- this.artifactManager.handleNotification(notification);
1530
- if (existing) {
1531
- await this.options.onArtifact?.(existing, "deleted");
1532
- this.emitter.emit("artifactDeleted", existing);
1533
- }
1534
- } else {
1535
- const { artifact, event } = notification;
1536
- this.artifactManager.handleNotification(notification);
1537
- const storedArtifact = this.artifactManager.get(artifact.uri) || artifact;
1538
- this.options.logger?.debug("[ACP-Client] Stored artifact:", {
1539
- event,
1540
- artifactUri: storedArtifact.uri,
1541
- artifactType: storedArtifact.type,
1542
- hasText: storedArtifact.type === "plan" ? !!storedArtifact.text : void 0
1543
- });
1544
- await this.options.onArtifact?.(storedArtifact, event);
1545
- if (event === "created") {
1546
- this.options.logger?.debug("[ACP-Client] Emitting artifactCreated event");
1547
- this.emitter.emit("artifactCreated", storedArtifact);
1548
- if (storedArtifact.type === "plan") await this.options.onPlanReady?.(storedArtifact);
1549
- } else {
1550
- this.options.logger?.debug("[ACP-Client] Emitting artifactUpdated event");
1551
- this.emitter.emit("artifactUpdated", storedArtifact);
1552
- }
1553
- }
1554
- return;
1555
- }
1556
- if (method === ExtensionMethod.CHECKPOINT) {
1557
- const notification = params;
1558
- if (notification.event === "created") this.emitter.emit("checkpointCreated", notification.checkpoint);
1559
- else if (notification.event === "updated") this.emitter.emit("checkpointUpdated", notification.checkpoint);
1560
- return;
1561
- }
1562
- await this.options.onExtNotification?.(method, params);
1563
- await this.extensionManager.handleNotification(method, params);
1564
- }
1565
- async handleExtMethod(method, params) {
1566
- if (method === ExtensionMethod.QUESTION) {
1567
- const response = await this.questionManager.handleRequest(params);
1568
- if (response.outcome === "submitted" && response.answers) return { outcome: {
1569
- outcome: "submitted",
1570
- data: { answers: response.answers }
1571
- } };
1572
- else return { outcome: {
1573
- outcome: "cancelled",
1574
- reason: response.reason
1575
- } };
1576
- }
1577
- this.options.logger?.warn(`Unknown extension method: ${method}`);
1578
- return { outcome: {
1579
- outcome: "cancelled",
1580
- reason: "unknown method"
1581
- } };
1582
- }
1583
- ensureInitialized(operation) {
1584
- if (this.state !== "initialized") throw new InvalidStateError(operation, this.state, ["initialized"]);
1585
- }
1586
- };
1587
- /**
1588
- * Check if an error is a retryable network-level error.
1589
- * Only network failures (TypeError from fetch) are retried, NOT HTTP errors (4xx/5xx).
1590
- */
1591
- function isRetryableNetworkError(error) {
1592
- if (error instanceof TypeError) return true;
1593
- if (error instanceof Error) {
1594
- const msg = error.message.toLowerCase();
1595
- return msg.includes("failed to fetch") || msg.includes("fetch failed") || msg.includes("network request failed") || msg.includes("econnreset") || msg.includes("econnrefused") || msg.includes("socket hang up");
1596
- }
1597
- return false;
1598
- }
1599
-
1600
- //#endregion
1601
- //#region src/v1/acp/client.ts
1602
- let _consolePatched = false;
1603
- function patchAcpSdkNoiseLogsOnce() {
1604
- if (_consolePatched) return;
1605
- _consolePatched = true;
1606
- const origError = console.error.bind(console);
1607
- console.error = (...args) => {
1608
- if (args.length >= 3 && args[0] === "Error handling notification") {
1609
- const err = args[2];
1610
- if (err && typeof err === "object" && err.code === -32602) return;
1611
- }
1612
- origError(...args);
1613
- };
1614
- }
1615
- var AcpClient = class {
1616
- constructor(opts) {
1617
- this._state = "INITIAL";
1618
- this._initialized = false;
1619
- this._sessionIds = /* @__PURE__ */ new Set();
1620
- this._subscribers = /* @__PURE__ */ new Map();
1621
- this._connListeners = {
1622
- open: /* @__PURE__ */ new Set(),
1623
- error: /* @__PURE__ */ new Set(),
1624
- close: /* @__PURE__ */ new Set()
1625
- };
1626
- this._activePrompts = /* @__PURE__ */ new Set();
1627
- this._promptQueues = /* @__PURE__ */ new Map();
1628
- this._logger = opts?.logger;
1629
- this._fetch = opts?.fetch;
1630
- this._exposeVendorLogs = opts?.logLevel === "debug";
1631
- }
1632
- get state() {
1633
- return this._state;
1634
- }
1635
1655
  get connectionId() {
1636
- return this._client?.connectionId;
1637
- }
1638
- get sessionIds() {
1639
- return Array.from(this._sessionIds);
1640
- }
1641
- get initialized() {
1642
- return this._initialized;
1656
+ return this.transport?.connectionId;
1643
1657
  }
1644
- setTokenRefresher(fn) {
1645
- this.tokenRefresher = fn;
1658
+ setState(newState) {
1659
+ const previous = this.state;
1660
+ this.state = newState;
1661
+ this.options.logger?.debug(`State change: ${previous} -> ${newState}`);
1662
+ this.emitter.emit("stateChange", {
1663
+ previous,
1664
+ current: newState
1665
+ });
1666
+ switch (newState) {
1667
+ case "connecting":
1668
+ this.emitter.emit("connecting", void 0);
1669
+ break;
1670
+ case "connected":
1671
+ this.emitter.emit("connected", void 0);
1672
+ break;
1673
+ case "disconnected":
1674
+ this.emitter.emit("disconnected", void 0);
1675
+ break;
1676
+ case "error": break;
1677
+ }
1646
1678
  }
1647
- async connect(acpUrl, token, opts) {
1648
- if (this._state === "OPEN" || this._state === "CONNECTING") return;
1649
- this._state = "CONNECTING";
1650
- patchAcpSdkNoiseLogsOnce();
1651
- this._logger?.info(`[acp] connecting to ${acpUrl}`);
1679
+ /**
1680
+ * Connect and initialize the client
1681
+ */
1682
+ async connect() {
1683
+ if (this.state !== "disconnected") await this.disconnect();
1684
+ if (this.state === "initialized") return this.initializeResponse;
1685
+ if (this.state === "connecting") throw new ConnectionError("Connection already in progress");
1686
+ this.setState("connecting");
1652
1687
  try {
1653
- this._client = new StreamableHttpClient({
1654
- endpoint: acpUrl,
1655
- authToken: token,
1656
- reconnect: {
1657
- enabled: true,
1658
- initialDelay: opts?.reconnectBackoffMs ?? 1e3,
1659
- maxDelay: opts?.reconnectMaxBackoffMs ?? 3e4,
1660
- maxRetries: opts?.reconnectMaxAttempts ?? Infinity
1688
+ this.transport = createStreamableHttpTransport({
1689
+ endpoint: this.options.endpoint,
1690
+ authToken: this.options.authToken,
1691
+ headers: this.options.headers,
1692
+ reconnect: this.options.reconnect,
1693
+ fetch: this.options.fetch,
1694
+ heartbeatTimeout: this.options.heartbeatTimeout,
1695
+ postTimeout: this.options.postTimeout,
1696
+ connectionTimeout: this.options.connectionTimeout,
1697
+ onConnect: (connectionId) => {
1698
+ this.options.logger?.debug(`Transport connected: ${connectionId}`);
1661
1699
  },
1662
- heartbeatTimeout: opts?.heartbeatTimeoutMs ?? 6e4,
1663
- initializeTimeout: 3e4,
1664
- postTimeout: 3e4,
1665
- logger: this._exposeVendorLogs ? this._logger : void 0,
1666
- fetch: this._fetch,
1667
- onSessionUpdate: (notification) => this.dispatchNotification(notification)
1700
+ onDisconnect: (connectionId) => {
1701
+ this.options.logger?.debug(`Transport disconnected: ${connectionId}`);
1702
+ },
1703
+ onError: (error) => {
1704
+ this.options.logger?.error("Transport error:", error);
1705
+ this.emitter.emit("error", error);
1706
+ }
1668
1707
  });
1669
- this._client.on("connected", () => {
1670
- this._logger?.info(`[acp] connected (connectionId=${this._client?.connectionId ?? "n/a"})`);
1671
- this.emitConnEvent("open");
1708
+ const { ClientSideConnection } = await loadAcpSdk();
1709
+ this.connection = new ClientSideConnection(() => this.createClientHandler(), this.transport);
1710
+ this.setState("connected");
1711
+ const timeout = this.options.initializeTimeout ?? DEFAULT_INITIALIZE_TIMEOUT;
1712
+ const mergedCapabilities = {
1713
+ ...this.options.clientCapabilities,
1714
+ ...CLOUD_CLIENT_CAPABILITIES,
1715
+ _meta: {
1716
+ ...this.options.clientCapabilities?._meta,
1717
+ ...CLOUD_CLIENT_CAPABILITIES._meta
1718
+ }
1719
+ };
1720
+ const initPromise = this.connection.initialize({
1721
+ protocolVersion: PROTOCOL_VERSION,
1722
+ clientCapabilities: mergedCapabilities
1672
1723
  });
1673
- this._client.on("error", (err) => {
1674
- this._logger?.warn(`[acp] error: ${err.message}`);
1675
- this.emitConnEvent("error", err);
1724
+ const timeoutPromise = new Promise((_, reject) => {
1725
+ setTimeout(() => {
1726
+ reject(new InitializationError(`Initialize timed out after ${timeout}ms`));
1727
+ }, timeout);
1676
1728
  });
1677
- await this._client.connect();
1678
- this._state = "OPEN";
1679
- this._initialized = true;
1729
+ const initializeResponse = await Promise.race([initPromise, timeoutPromise]);
1730
+ this.initializeResponse = initializeResponse;
1731
+ this.setState("initialized");
1732
+ this.options.logger?.info("Client initialized successfully");
1733
+ return initializeResponse;
1680
1734
  } catch (err) {
1681
- this._state = "CLOSED";
1682
- this._logger?.debug(`[acp] connect failed: ${err.message}`);
1683
- throw new NetworkError(`ACP connect failed: ${err.message}`, { cause: err });
1735
+ this.setState("error");
1736
+ const error = err instanceof Error ? err : new Error(String(err));
1737
+ this.emitter.emit("error", error);
1738
+ if (err instanceof InitializationError || err instanceof ConnectionError) throw err;
1739
+ throw new ConnectionError("Failed to connect", error);
1684
1740
  }
1685
1741
  }
1742
+ /**
1743
+ * Disconnect the client gracefully
1744
+ * Sends DELETE request to server before closing local resources
1745
+ */
1686
1746
  async disconnect() {
1687
- if (this._state === "CLOSED" || this._state === "CLOSING") return;
1688
- this._state = "CLOSING";
1689
- if (this._client) {
1747
+ if (this.state === "disconnected") return;
1748
+ this.options.logger?.info("Disconnecting client");
1749
+ if (this.transport) {
1690
1750
  try {
1691
- await this._client.disconnect();
1692
- } catch {}
1693
- this._client = void 0;
1751
+ await this.transport.close();
1752
+ } catch (err) {
1753
+ this.options.logger?.warn("Error closing transport:", err);
1754
+ }
1755
+ this.transport = void 0;
1694
1756
  }
1695
- this._initialized = false;
1696
- this._state = "CLOSED";
1697
- this._subscribers.clear();
1698
- this._activePrompts.clear();
1699
- this._promptQueues.clear();
1700
- this._logger?.info("[acp] disconnected");
1701
- this.emitConnEvent("close");
1702
- }
1703
- /** initialize 由 connect() 内部完成,这里只取缓存结果。 */
1704
- async initialize() {
1705
- if (!this._client) throw new AcpProtocolError("Not connected");
1706
- return this._client.initializeResult;
1707
- }
1708
- async sessionNew(sessionId) {
1709
- if (!this._client) throw new AcpProtocolError("Not connected");
1710
- const newId = (await this._client.createSession("/workspace")).sessionId || sessionId || "";
1711
- this._sessionIds.add(newId);
1712
- return newId;
1757
+ this.permissionManager.clear();
1758
+ this.questionManager.clear();
1759
+ this.artifactManager.clear();
1760
+ this.initializeResponse = void 0;
1761
+ this.setState("disconnected");
1713
1762
  }
1714
- async sessionLoad(sessionId) {
1715
- if (!this._client) throw new AcpProtocolError("Not connected");
1716
- await this._client.loadSession(sessionId, "/workspace");
1717
- this._sessionIds.add(sessionId);
1763
+ /**
1764
+ * Create the client handler for the connection
1765
+ */
1766
+ createClientHandler() {
1767
+ return {
1768
+ sessionUpdate: async (params) => {
1769
+ await this.handleSessionUpdate(params);
1770
+ },
1771
+ requestPermission: async (params) => this.handleRequestPermission(params),
1772
+ extNotification: async (method, params) => {
1773
+ await this.handleExtNotification(method, params);
1774
+ },
1775
+ extMethod: async (method, params) => this.handleExtMethod(method, params)
1776
+ };
1718
1777
  }
1719
1778
  /**
1720
- * prompt。纯 Promise,直接转发上游 `session/prompt` RPC。
1779
+ * Create a new session
1721
1780
  *
1722
- * - **不聚合 notification**:本次 prompt 期间的 notifications
1723
- * `subscribe()` 订阅器独立接收,与此 Promise 并行。
1724
- * - **串行化**:同 sessionId 的多次 `prompt()` 会**排队**等前一个完成再发出
1725
- * (ACP 协议要求),保证顺序一致。
1726
- * - **signal 支持**:`opts.signal` abort 时向服务端发 `session/cancel`。
1727
- * Promise 会以 `AbortError` reject。
1781
+ * Retries on transient network errors (e.g., proxy connection reset)
1782
+ * since session/new is idempotent and safe to retry.
1783
+ *
1784
+ * A timeout (`options.sessionTimeout`, default 30 s) guards against the
1785
+ * SSE response never arriving the POST returns 202 immediately and the
1786
+ * sessionId comes back via GET SSE, which can hang indefinitely.
1787
+ * On timeout a `SessionError` is thrown.
1728
1788
  */
1729
- async prompt(sessionId, input, opts) {
1730
- if (!this._client) throw new AcpProtocolError("Not connected");
1731
- return new Promise((resolve, reject) => {
1732
- this.enqueuePrompt(sessionId, async () => {
1733
- let aborted = false;
1734
- const onAbort = () => {
1735
- aborted = true;
1736
- this._client?.cancel(sessionId).catch(() => {});
1737
- const err = /* @__PURE__ */ new Error("Prompt aborted");
1738
- err.name = "AbortError";
1739
- reject(err);
1740
- };
1741
- if (opts?.signal) {
1742
- if (opts.signal.aborted) {
1743
- onAbort();
1744
- return;
1745
- }
1746
- opts.signal.addEventListener("abort", onAbort, { once: true });
1747
- }
1748
- try {
1749
- const response = await this._client.prompt(sessionId, input);
1750
- if (!aborted) resolve(response);
1751
- } catch (err) {
1752
- if (!aborted) reject(err);
1753
- } finally {
1754
- opts?.signal?.removeEventListener("abort", onAbort);
1755
- }
1789
+ async createSession(cwd) {
1790
+ this.ensureInitialized("createSession");
1791
+ const maxRetries = 2;
1792
+ const timeout = this.options.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
1793
+ let lastError;
1794
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
1795
+ const sessionPromise = this.connection.newSession({
1796
+ cwd,
1797
+ mcpServers: []
1798
+ });
1799
+ let response;
1800
+ if (timeout > 0) {
1801
+ const timeoutPromise = new Promise((_, reject) => {
1802
+ setTimeout(() => {
1803
+ reject(new SessionError(`session/new timed out after ${timeout}ms`));
1804
+ }, timeout);
1805
+ });
1806
+ response = await Promise.race([sessionPromise, timeoutPromise]);
1807
+ } else response = await sessionPromise;
1808
+ this.options.logger?.info(`Session created: ${response.sessionId}`);
1809
+ return response;
1810
+ } catch (err) {
1811
+ lastError = err instanceof Error ? err : new Error(String(err));
1812
+ if (attempt < maxRetries && isRetryableNetworkError(err)) {
1813
+ const delay = 500 * Math.pow(2, attempt);
1814
+ this.options.logger?.warn(`session/new network error, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries}): ${lastError.message}`);
1815
+ await new Promise((resolve) => setTimeout(resolve, delay));
1816
+ continue;
1817
+ }
1818
+ throw new SessionError(`Failed to create session: ${lastError.message}`, void 0, lastError);
1819
+ }
1820
+ throw new SessionError(`Failed to create session: ${lastError?.message}`, void 0, lastError);
1821
+ }
1822
+ /**
1823
+ * Load an existing session
1824
+ * Requires agent to support loadSession capability
1825
+ */
1826
+ async loadSession(sessionId, cwd) {
1827
+ this.ensureInitialized("loadSession");
1828
+ if (!this.agentCapabilities?.loadSession) throw new SessionError("Agent does not support session loading", sessionId);
1829
+ try {
1830
+ const response = await this.connection.loadSession({
1831
+ sessionId,
1832
+ cwd,
1833
+ mcpServers: []
1756
1834
  });
1835
+ this.options.logger?.info(`Session loaded: ${sessionId}`);
1836
+ return response;
1837
+ } catch (err) {
1838
+ throw new SessionError(`Failed to load session: ${err instanceof Error ? err.message : String(err)}`, sessionId, err instanceof Error ? err : void 0);
1839
+ }
1840
+ }
1841
+ /**
1842
+ * Set the session mode
1843
+ */
1844
+ async setSessionMode(params) {
1845
+ this.ensureInitialized("setSessionMode");
1846
+ this.options.logger?.debug(`Setting session mode: ${params.sessionId} -> ${params.modeId}`);
1847
+ return this.connection.setSessionMode(params);
1848
+ }
1849
+ /**
1850
+ * Set the session model
1851
+ * @experimental This API is unstable and may change
1852
+ */
1853
+ async setSessionModel(params) {
1854
+ this.ensureInitialized("setSessionModel");
1855
+ this.options.logger?.debug(`Setting session model: ${params.sessionId} -> ${params.modelId}`);
1856
+ return this.connection.unstable_setSessionModel(params);
1857
+ }
1858
+ /**
1859
+ * Send a prompt to the agent
1860
+ */
1861
+ async prompt(sessionId, prompt, options) {
1862
+ this.ensureInitialized("prompt");
1863
+ this.options.logger?.debug(`Sending prompt to session: ${sessionId}`);
1864
+ return this.connection.prompt({
1865
+ sessionId,
1866
+ prompt,
1867
+ _meta: options?.planMode ? {
1868
+ planMode: true,
1869
+ ...options._meta
1870
+ } : options?._meta
1757
1871
  });
1758
1872
  }
1759
- /** 发 session/cancel 通知(幂等,尽力而为)。 */
1760
- async sessionCancel(sessionId) {
1761
- if (!this._client) throw new AcpProtocolError("Not connected");
1762
- await this._client.cancel(sessionId);
1873
+ /**
1874
+ * Cancel ongoing operations for a session
1875
+ */
1876
+ async cancel(sessionId) {
1877
+ this.ensureInitialized("cancel");
1878
+ this.options.logger?.debug(`Cancelling session: ${sessionId}`);
1879
+ await this.connection.cancel({ sessionId });
1880
+ }
1881
+ /**
1882
+ * Resolve a pending permission request
1883
+ */
1884
+ resolvePermission(requestId, optionId) {
1885
+ return this.permissionManager.resolve(requestId, optionId);
1886
+ }
1887
+ /**
1888
+ * Reject a pending permission request
1889
+ */
1890
+ rejectPermission(requestId, reason) {
1891
+ return this.permissionManager.reject(requestId, reason);
1892
+ }
1893
+ /**
1894
+ * Get all pending permissions
1895
+ */
1896
+ getPendingPermissions() {
1897
+ return this.permissionManager.getPending();
1898
+ }
1899
+ /**
1900
+ * Check if there are pending permissions
1901
+ */
1902
+ hasPendingPermissions() {
1903
+ return this.permissionManager.hasPending();
1904
+ }
1905
+ /**
1906
+ * Answer a pending question request with user's selections
1907
+ */
1908
+ answerQuestion(toolCallId, answers) {
1909
+ return this.questionManager.answer(toolCallId, answers);
1910
+ }
1911
+ /**
1912
+ * Cancel a pending question request
1913
+ */
1914
+ cancelQuestion(toolCallId, reason) {
1915
+ return this.questionManager.cancel(toolCallId, reason);
1763
1916
  }
1764
1917
  /**
1765
- * 订阅指定 sessionId 的上游 notifications。
1766
- *
1767
- * Listener 收到的是上游 `SessionNotification` 原样(`{ sessionId, update, _meta? }`)。
1768
- * 同一 session 可以有任意多个 listener,彼此独立;每条 notification 都会
1769
- * 按注册顺序调用所有 listener(同步调用,异常被吞)。
1770
- *
1771
- * @returns unsubscribe 函数,调用即退订。
1918
+ * Get all pending question requests
1772
1919
  */
1773
- subscribe(sessionId, listener) {
1774
- if (!this._subscribers.has(sessionId)) this._subscribers.set(sessionId, /* @__PURE__ */ new Set());
1775
- this._subscribers.get(sessionId).add(listener);
1776
- return () => {
1777
- this._subscribers.get(sessionId)?.delete(listener);
1778
- };
1920
+ getPendingQuestions() {
1921
+ return this.questionManager.getPending();
1779
1922
  }
1780
- on(event, handler) {
1781
- this._connListeners[event].add(handler);
1923
+ /**
1924
+ * Check if there are pending question requests
1925
+ */
1926
+ hasPendingQuestions() {
1927
+ return this.questionManager.hasPending();
1782
1928
  }
1783
- off(event, handler) {
1784
- this._connListeners[event].delete(handler);
1929
+ /**
1930
+ * Send an extension method request
1931
+ */
1932
+ async extMethod(method, params) {
1933
+ this.ensureInitialized("extMethod");
1934
+ return this.connection.extMethod(method, params);
1785
1935
  }
1786
1936
  /**
1787
- * 把上游 notification sessionId 分发给订阅者。不做任何转换。
1788
- *
1789
- * 上游保证 `SessionNotification` 的 `sessionId` / `update` 字段都有值
1790
- * (vendor 的 `SessionUpdateCallback` 签名即如此),无需再做类型守卫。
1937
+ * Send an extension notification
1791
1938
  */
1792
- dispatchNotification(notification) {
1793
- const subs = this._subscribers.get(notification.sessionId);
1794
- if (!subs || subs.size === 0) return;
1795
- for (const listener of subs) try {
1796
- listener(notification);
1797
- } catch {}
1939
+ async extNotification(method, params) {
1940
+ this.ensureInitialized("extNotification");
1941
+ return this.connection.extNotification(method, params);
1798
1942
  }
1799
- emitConnEvent(event, ...args) {
1800
- for (const handler of this._connListeners[event]) try {
1801
- handler(...args);
1802
- } catch {}
1943
+ on(event, listener) {
1944
+ this.emitter.on(event, listener);
1945
+ return this;
1803
1946
  }
1804
- enqueuePrompt(sid, exec) {
1805
- if (!this._promptQueues.has(sid)) this._promptQueues.set(sid, []);
1806
- const runThenDequeue = () => {
1807
- this._activePrompts.add(sid);
1808
- exec().finally(() => this.dequeuePrompt(sid));
1809
- };
1810
- if (this._activePrompts.has(sid)) this._promptQueues.get(sid).push(runThenDequeue);
1811
- else runThenDequeue();
1947
+ off(event, listener) {
1948
+ this.emitter.off(event, listener);
1949
+ return this;
1812
1950
  }
1813
- dequeuePrompt(sid) {
1814
- this._activePrompts.delete(sid);
1815
- const q = this._promptQueues.get(sid);
1816
- if (q && q.length > 0) q.shift()();
1951
+ once(event, listener) {
1952
+ this.emitter.once(event, listener);
1953
+ return this;
1817
1954
  }
1818
- };
1819
-
1820
- //#endregion
1821
- //#region src/v1/internal/log.ts
1822
- /** 级别数值:越大越详细;方法级别 > 当前级别时,该方法变成 noop。 */
1823
- const LEVEL_NUMBER = {
1824
- off: 0,
1825
- error: 200,
1826
- warn: 300,
1827
- info: 400,
1828
- debug: 500
1829
- };
1830
- /** 默认级别(不配置时)。 */
1831
- const DEFAULT_LEVEL = "warn";
1832
- /** 真正的空函数。替换被关闭的级别,避免字符串拼接开销。 */
1833
- const noop = () => {};
1834
- /** 全 noop 的 logger(内部单例)。 */
1835
- const noopLogger = {
1836
- error: noop,
1837
- warn: noop,
1838
- info: noop,
1839
- debug: noop
1840
- };
1841
- /**
1842
- * 缓存 `[baseLogger, logLevel] -> wrappedLogger`。
1843
- *
1844
- * 用 WeakMap<Logger, ...>:只要 user 传的 logger 引用不变、level 不变,就复用。
1845
- */
1846
- const cache = /* @__PURE__ */ new WeakMap();
1847
- /**
1848
- * 解析级别字符串;非法值返回 undefined(调用方自行回退)。
1849
- */
1850
- function parseLogLevel(value) {
1851
- if (typeof value !== "string") return void 0;
1852
- const lower = value.toLowerCase();
1853
- if (lower in LEVEL_NUMBER) return lower;
1854
- }
1855
- /**
1856
- * 读取环境变量 `CLOUD_AGENT_SDK_LOG`(仅 Node.js 环境,浏览器返回 undefined)。
1857
- *
1858
- * 用 `globalThis` 方式访问避免对 `@types/node` 的强依赖。
1859
- */
1860
- function readEnvLogLevel() {
1861
- const env = globalThis.process?.env;
1862
- if (!env) return void 0;
1863
- return parseLogLevel(env.CLOUD_AGENT_SDK_LOG);
1864
- }
1865
- /**
1866
- * 按 ConnectionOpts 决定最终的 LogLevel。
1867
- *
1868
- * 优先级:`opts.logLevel` > env `CLOUD_AGENT_SDK_LOG` > `'warn'`。
1869
- */
1870
- function resolveLogLevel(opts) {
1871
- return parseLogLevel(opts.logLevel) ?? readEnvLogLevel() ?? DEFAULT_LEVEL;
1872
- }
1873
- /**
1874
- * 返回按 `opts` 过滤后的 Logger。
1875
- *
1876
- * - `opts.logger` 为空时使用 `globalThis.console`
1877
- * - 级别关闭时返回 noop,开启时返回 `baseLogger[fn].bind(baseLogger)`
1878
- * - 相同入参多次调用复用同一对象(WeakMap 缓存)
1879
- */
1880
- function loggerFor(opts) {
1881
- const base = opts.logger ?? console;
1882
- const level = resolveLogLevel(opts);
1883
- if (level === "off") return noopLogger;
1884
- let byLevel = cache.get(base);
1885
- if (!byLevel) {
1886
- byLevel = /* @__PURE__ */ new Map();
1887
- cache.set(base, byLevel);
1955
+ emit(event, data) {
1956
+ return this.emitter.emit(event, data);
1888
1957
  }
1889
- const cached = byLevel.get(level);
1890
- if (cached) return cached;
1891
- const wrapped = makeLeveledLogger(base, level);
1892
- byLevel.set(level, wrapped);
1893
- return wrapped;
1894
- }
1958
+ removeAllListeners(event) {
1959
+ this.emitter.removeAllListeners(event);
1960
+ return this;
1961
+ }
1962
+ async handleSessionUpdate(params) {
1963
+ await this.options.onSessionUpdate?.(params);
1964
+ this.emitter.emit("sessionUpdate", params);
1965
+ }
1966
+ async handleRequestPermission(params) {
1967
+ return this.permissionManager.handleRequest(params);
1968
+ }
1969
+ async handleExtNotification(method, params) {
1970
+ if (method === ExtensionMethod.ARTIFACT) {
1971
+ const notification = params;
1972
+ const artifactData = notification.artifact;
1973
+ this.options.logger?.debug("[ACP-Client] Received artifact notification:", {
1974
+ event: notification.event,
1975
+ artifactUri: artifactData?.uri,
1976
+ artifactType: artifactData?.type
1977
+ });
1978
+ if (notification.event === "deleted") {
1979
+ const existing = this.artifactManager.get(notification.artifact.uri);
1980
+ this.artifactManager.handleNotification(notification);
1981
+ if (existing) {
1982
+ await this.options.onArtifact?.(existing, "deleted");
1983
+ this.emitter.emit("artifactDeleted", existing);
1984
+ }
1985
+ } else {
1986
+ const { artifact, event } = notification;
1987
+ this.artifactManager.handleNotification(notification);
1988
+ const storedArtifact = this.artifactManager.get(artifact.uri) || artifact;
1989
+ this.options.logger?.debug("[ACP-Client] Stored artifact:", {
1990
+ event,
1991
+ artifactUri: storedArtifact.uri,
1992
+ artifactType: storedArtifact.type,
1993
+ hasText: storedArtifact.type === "plan" ? !!storedArtifact.text : void 0
1994
+ });
1995
+ await this.options.onArtifact?.(storedArtifact, event);
1996
+ if (event === "created") {
1997
+ this.options.logger?.debug("[ACP-Client] Emitting artifactCreated event");
1998
+ this.emitter.emit("artifactCreated", storedArtifact);
1999
+ if (storedArtifact.type === "plan") await this.options.onPlanReady?.(storedArtifact);
2000
+ } else {
2001
+ this.options.logger?.debug("[ACP-Client] Emitting artifactUpdated event");
2002
+ this.emitter.emit("artifactUpdated", storedArtifact);
2003
+ }
2004
+ }
2005
+ return;
2006
+ }
2007
+ if (method === ExtensionMethod.CHECKPOINT) {
2008
+ const notification = params;
2009
+ if (notification.event === "created") this.emitter.emit("checkpointCreated", notification.checkpoint);
2010
+ else if (notification.event === "updated") this.emitter.emit("checkpointUpdated", notification.checkpoint);
2011
+ return;
2012
+ }
2013
+ await this.options.onExtNotification?.(method, params);
2014
+ await this.extensionManager.handleNotification(method, params);
2015
+ }
2016
+ async handleExtMethod(method, params) {
2017
+ if (method === ExtensionMethod.QUESTION) {
2018
+ const response = await this.questionManager.handleRequest(params);
2019
+ if (response.outcome === "submitted" && response.answers) return { outcome: {
2020
+ outcome: "submitted",
2021
+ data: { answers: response.answers }
2022
+ } };
2023
+ else return { outcome: {
2024
+ outcome: "cancelled",
2025
+ reason: response.reason
2026
+ } };
2027
+ }
2028
+ this.options.logger?.warn(`Unknown extension method: ${method}`);
2029
+ return { outcome: {
2030
+ outcome: "cancelled",
2031
+ reason: "unknown method"
2032
+ } };
2033
+ }
2034
+ ensureInitialized(operation) {
2035
+ if (this.state !== "initialized") throw new InvalidStateError(operation, this.state, ["initialized"]);
2036
+ }
2037
+ };
1895
2038
  /**
1896
- * 按级别构造 Logger:开放的级别直接 bind,关闭的级别替换为 noop。
1897
- *
1898
- * bind 而非 wrap 是为了保留调用点的 stack trace(开发者看日志能点回 SDK 行号)。
1899
- */
1900
- function makeLeveledLogger(base, level) {
1901
- const threshold = LEVEL_NUMBER[level];
1902
- const enable = (fn) => LEVEL_NUMBER[fn] <= threshold && typeof base[fn] === "function" ? base[fn].bind(base) : noop;
1903
- return {
1904
- error: enable("error"),
1905
- warn: enable("warn"),
1906
- info: enable("info"),
1907
- debug: enable("debug")
1908
- };
2039
+ * Check if an error is a retryable network-level error.
2040
+ * Only network failures (TypeError from fetch) are retried, NOT HTTP errors (4xx/5xx).
2041
+ */
2042
+ function isRetryableNetworkError(error) {
2043
+ if (error instanceof TypeError) return true;
2044
+ if (error instanceof Error) {
2045
+ const msg = error.message.toLowerCase();
2046
+ return msg.includes("failed to fetch") || msg.includes("fetch failed") || msg.includes("network request failed") || msg.includes("econnreset") || msg.includes("econnrefused") || msg.includes("socket hang up");
2047
+ }
2048
+ return false;
1909
2049
  }
2050
+
2051
+ //#endregion
2052
+ //#region src/v1/acp/client.ts
1910
2053
  /**
1911
- * 生成短本地日志 ID,用于在用户日志里串联一次 HTTP 请求(含重试链)。
2054
+ * ACP 客户端 订阅模型
2055
+ *
2056
+ * 设计:直接映射到 ACP 协议的两条独立通道:
1912
2057
  *
1913
- * 形态:`log_` + 6 位十六进制(24 bit 随机,单进程内冲突概率可忽略)。
2058
+ * - **RPC 通道(POST)**:`prompt` / `sessionCancel` 等是纯 `Promise`,转发上游。
2059
+ * - **Notification 通道(SSE)**:`subscribe(sessionId, listener)` 给用户注册
2060
+ * 监听器,每条上游 `SessionNotification` 多路分发给所有订阅者。
1914
2061
  *
1915
- * 对齐 Anthropic SDK 的做法(详见 docs/agentos/sdk/07-error-retry.md § 4.3)。
2062
+ * 不做聚合、不建队列。上游 `SessionNotification` / `PromptResponse` 等类型原封
2063
+ * 不动透给上层(`Session` 类)。
2064
+ *
2065
+ * 具体实现基于 vendor 的 `StreamableHttpClient`,它内部管理一条 SSE + POST 复合
2066
+ * 通道。用户侧看到的只是"一个 session 对应一个 AcpClient"。
1916
2067
  */
1917
- function makeLogId() {
1918
- return "log_" + (Math.random() * (1 << 24) | 0).toString(16).padStart(6, "0");
2068
+ let _consolePatched = false;
2069
+ function patchAcpSdkNoiseLogsOnce() {
2070
+ if (_consolePatched) return;
2071
+ _consolePatched = true;
2072
+ const origError = console.error.bind(console);
2073
+ console.error = (...args) => {
2074
+ if (args.length >= 3 && args[0] === "Error handling notification") {
2075
+ const err = args[2];
2076
+ if (err && typeof err === "object" && err.code === -32602) return;
2077
+ }
2078
+ origError(...args);
2079
+ };
1919
2080
  }
1920
-
1921
- //#endregion
1922
- //#region src/v1/session.ts
1923
- var Session = class {
1924
- /** @internal */
1925
- constructor(id, runtime, restClient) {
1926
- this.id = id;
1927
- this.runtimeId = runtime.id;
1928
- this._runtime = runtime;
1929
- this._restClient = restClient;
2081
+ var AcpClient = class {
2082
+ constructor(opts) {
2083
+ this._state = "INITIAL";
2084
+ this._initialized = false;
2085
+ this._sessionIds = /* @__PURE__ */ new Set();
2086
+ this._subscribers = /* @__PURE__ */ new Map();
2087
+ this._connListeners = {
2088
+ open: /* @__PURE__ */ new Set(),
2089
+ error: /* @__PURE__ */ new Set(),
2090
+ close: /* @__PURE__ */ new Set()
2091
+ };
2092
+ this._activePrompts = /* @__PURE__ */ new Set();
2093
+ this._promptQueues = /* @__PURE__ */ new Map();
2094
+ this._logger = opts?.logger;
2095
+ this._fetch = opts?.fetch;
2096
+ this._exposeVendorLogs = opts?.logLevel === "debug";
1930
2097
  }
1931
- get status() {
1932
- return this._status;
2098
+ get state() {
2099
+ return this._state;
1933
2100
  }
1934
- /** ACP 连接是否已建立 */
1935
- get connected() {
1936
- return this._acpClient?.state === "OPEN";
2101
+ get connectionId() {
2102
+ return this._client?.connectionId;
1937
2103
  }
1938
- /** 拉最新元数据 */
1939
- async info(opts) {
1940
- const info = await this._restClient.get(`/runtimes/${this.runtimeId}/sessions/${this.id}`, void 0, opts);
1941
- this._status = info.sessionStatus;
1942
- return info;
2104
+ get sessionIds() {
2105
+ return Array.from(this._sessionIds);
1943
2106
  }
1944
- /** 更新 name / manifest */
1945
- async update(req, opts) {
1946
- const info = await this._restClient.post(`/runtimes/${this.runtimeId}/sessions/${this.id}/update`, req, opts);
1947
- this._status = info.sessionStatus;
1948
- return info;
2107
+ get initialized() {
2108
+ return this._initialized;
1949
2109
  }
1950
- /** 软删除 */
1951
- async delete(opts) {
1952
- if (this._acpClient) {
1953
- await this._acpClient.disconnect();
1954
- this._acpClient = void 0;
1955
- }
1956
- return this._restClient.post(`/runtimes/${this.runtimeId}/sessions/${this.id}/delete`, void 0, opts);
2110
+ setTokenRefresher(fn) {
2111
+ this.tokenRefresher = fn;
1957
2112
  }
1958
- /**
1959
- * 建立到沙箱的 ACP 连接。
1960
- *
1961
- * 内部流程:GET SSE → 获取 connectionId → initialize → session/load。
1962
- * 调用后 `prompt` / `subscribe` / `cancel` 才可用。
1963
- *
1964
- * 幂等:已连接时直接返回;并发调用时后来的等前者完成。
1965
- */
1966
- async connect(opts) {
1967
- if (this._acpClient && this._acpClient.state === "OPEN") return;
1968
- const acpUrl = this._runtime.acpUrl;
1969
- let acpToken = this._runtime.acpToken;
1970
- if (!acpUrl || !acpToken) {
1971
- acpToken = await this._runtime.refreshToken();
1972
- if (!this._runtime.acpUrl || !acpToken) throw new ValidationError("Runtime does not have ACP link. Is it in RUNNING state?");
2113
+ async connect(acpUrl, token, opts) {
2114
+ if (this._state === "OPEN" || this._state === "CONNECTING") return;
2115
+ this._state = "CONNECTING";
2116
+ patchAcpSdkNoiseLogsOnce();
2117
+ this._logger?.debug(`[acp conn=-] connecting to ${acpUrl}`);
2118
+ try {
2119
+ this._client = new StreamableHttpClient({
2120
+ endpoint: acpUrl,
2121
+ authToken: token,
2122
+ headers: opts?.headers,
2123
+ reconnect: {
2124
+ enabled: true,
2125
+ initialDelay: opts?.reconnectBackoffMs ?? 1e3,
2126
+ maxDelay: opts?.reconnectMaxBackoffMs ?? 3e4,
2127
+ maxRetries: opts?.reconnectMaxAttempts ?? Infinity
2128
+ },
2129
+ heartbeatTimeout: opts?.heartbeatTimeoutMs ?? 6e4,
2130
+ initializeTimeout: 3e4,
2131
+ postTimeout: 3e4,
2132
+ logger: this._exposeVendorLogs ? this._logger : void 0,
2133
+ fetch: this._fetch,
2134
+ onSessionUpdate: (notification) => this.dispatchNotification(notification)
2135
+ });
2136
+ this._client.on("connected", () => {
2137
+ this._logger?.debug(`[acp conn=${this._client?.connectionId ?? "-"}] connected`);
2138
+ this.emitConnEvent("open");
2139
+ });
2140
+ this._client.on("error", (err) => {
2141
+ this._logger?.warn(`[acp conn=${this._client?.connectionId ?? "-"}] error: ${err.message}`);
2142
+ this.emitConnEvent("error", err);
2143
+ });
2144
+ await this._client.connect();
2145
+ this._state = "OPEN";
2146
+ this._initialized = true;
2147
+ } catch (err) {
2148
+ this._state = "CLOSED";
2149
+ this._logger?.debug(`[acp conn=-] connect failed: ${err.message}`);
2150
+ throw new NetworkError(`ACP connect failed: ${err.message}`, { cause: err });
1973
2151
  }
1974
- const connOpts = this._runtime._connectionOpts;
1975
- const client = new AcpClient({
1976
- logger: loggerFor(connOpts),
1977
- logLevel: resolveLogLevel(connOpts),
1978
- fetch: connOpts.fetch
1979
- });
1980
- client.setTokenRefresher(async () => this._runtime.refreshToken());
1981
- await client.connect(this._runtime.acpUrl, acpToken, {
1982
- ...opts,
1983
- heartbeatTimeoutMs: opts?.heartbeatTimeoutMs ?? 45e3,
1984
- reconnectMaxAttempts: opts?.reconnectMaxAttempts ?? Infinity,
1985
- reconnectBackoffMs: opts?.reconnectBackoffMs ?? 300,
1986
- reconnectMaxBackoffMs: opts?.reconnectMaxBackoffMs ?? 3e4,
1987
- lastEventId: opts?.lastEventId
1988
- });
1989
- if (opts?.initialize !== false) await client.initialize();
1990
- await client.sessionLoad(this.id);
1991
- this._acpClient = client;
1992
2152
  }
1993
- /** 断开 ACP 连接。所有订阅器被自动清除。 */
1994
2153
  async disconnect() {
1995
- if (this._acpClient) {
1996
- await this._acpClient.disconnect();
1997
- this._acpClient = void 0;
2154
+ if (this._state === "CLOSED" || this._state === "CLOSING") return;
2155
+ this._state = "CLOSING";
2156
+ const lastConnId = this._client?.connectionId ?? "-";
2157
+ if (this._client) {
2158
+ try {
2159
+ await this._client.disconnect();
2160
+ } catch {}
2161
+ this._client = void 0;
1998
2162
  }
2163
+ this._initialized = false;
2164
+ this._state = "CLOSED";
2165
+ this._subscribers.clear();
2166
+ this._activePrompts.clear();
2167
+ this._promptQueues.clear();
2168
+ this._logger?.debug(`[acp conn=${lastConnId}] disconnected`);
2169
+ this.emitConnEvent("close");
2170
+ }
2171
+ /** initialize 由 connect() 内部完成,这里只取缓存结果。 */
2172
+ async initialize() {
2173
+ if (!this._client) throw new AcpProtocolError("Not connected");
2174
+ return this._client.initializeResult;
2175
+ }
2176
+ async sessionNew(sessionId) {
2177
+ if (!this._client) throw new AcpProtocolError("Not connected");
2178
+ const newId = (await this._client.createSession("/workspace")).sessionId || sessionId || "";
2179
+ this._sessionIds.add(newId);
2180
+ return newId;
2181
+ }
2182
+ async sessionLoad(sessionId) {
2183
+ if (!this._client) throw new AcpProtocolError("Not connected");
2184
+ await this._client.loadSession(sessionId, "/workspace");
2185
+ this._sessionIds.add(sessionId);
1999
2186
  }
2000
2187
  /**
2001
- * 发送 prompt 并等待 `PromptResponse`。
2002
- *
2003
- * @param input 纯文本字符串(自动包装成 text block)或 `ContentBlock[]`(多模态)
2004
- * @param opts 见 `PromptOpts`,支持 `signal` 取消、`onChunk` 便利钩子等
2005
- *
2006
- * @returns `PromptResponse`(当前只含 `stopReason`)
2007
- *
2008
- * **时序**:
2009
- * - 如果未 connect,会自动 connect
2010
- * - RPC 发出期间,上游会通过 **SSE notification 通道**推送 `session/update`,
2011
- * 这些消息**不会进 Promise 的结果**,需要通过 `subscribe()` 或 `onChunk`
2012
- * 回调接收
2013
- * - 同 session 并发 prompt 会被内部**串行化**(ACP 协议要求)
2014
- *
2015
- * @example
2016
- * ```ts
2017
- * // 最简:只要结果
2018
- * const r = await session.prompt('2+2?');
2019
- *
2020
- * // 流式打印 + 结果
2021
- * const r = await session.prompt('write a poem', {
2022
- * onChunk: (n) => {
2023
- * if (n.update.sessionUpdate === 'agent_message_chunk' &&
2024
- * n.update.content.type === 'text') {
2025
- * process.stdout.write(n.update.content.text);
2026
- * }
2027
- * },
2028
- * });
2188
+ * prompt。纯 Promise,直接转发上游 `session/prompt` RPC。
2029
2189
  *
2030
- * // 取消
2031
- * const ctrl = new AbortController();
2032
- * setTimeout(() => ctrl.abort(), 5000);
2033
- * const r = await session.prompt('long task', { signal: ctrl.signal });
2034
- * ```
2190
+ * - **不聚合 notification**:本次 prompt 期间的 notifications 由
2191
+ * `subscribe()` 订阅器独立接收,与此 Promise 并行。
2192
+ * - **串行化**:同 sessionId 的多次 `prompt()` 会**排队**等前一个完成再发出
2193
+ * (ACP 协议要求),保证顺序一致。
2194
+ * - **signal 支持**:`opts.signal` abort 时向服务端发 `session/cancel`。
2195
+ * 本 Promise 会以 `AbortError` reject。
2035
2196
  */
2036
- async prompt(input, opts) {
2037
- if (!this._acpClient || this._acpClient.state !== "OPEN") await this.connect();
2038
- const content = typeof input === "string" ? [{
2039
- type: "text",
2040
- text: input
2041
- }] : input;
2042
- const unsubChunk = opts?.onChunk ? this._acpClient.subscribe(this.id, opts.onChunk) : void 0;
2043
- let timeoutCtrl;
2044
- let signal = opts?.signal;
2045
- if (opts?.timeoutMs !== void 0 && opts.timeoutMs > 0) {
2046
- timeoutCtrl = new AbortController();
2047
- const timer = setTimeout(() => timeoutCtrl.abort(), opts.timeoutMs);
2048
- signal?.addEventListener("abort", () => {
2049
- clearTimeout(timer);
2050
- timeoutCtrl.abort();
2051
- }, { once: true });
2052
- if (!signal) signal = timeoutCtrl.signal;
2053
- else signal = mergeAbortSignals(signal, timeoutCtrl.signal);
2054
- }
2055
- opts?.onTurnStart?.();
2056
- try {
2057
- const response = await this._acpClient.prompt(this.id, content, { signal });
2058
- opts?.onTurnEnd?.(response);
2059
- return response;
2060
- } finally {
2061
- unsubChunk?.();
2062
- }
2197
+ async prompt(sessionId, input, opts) {
2198
+ if (!this._client) throw new AcpProtocolError("Not connected");
2199
+ return new Promise((resolve, reject) => {
2200
+ this.enqueuePrompt(sessionId, async () => {
2201
+ let aborted = false;
2202
+ const onAbort = () => {
2203
+ aborted = true;
2204
+ this._client?.cancel(sessionId).catch(() => {});
2205
+ const err = /* @__PURE__ */ new Error("Prompt aborted");
2206
+ err.name = "AbortError";
2207
+ reject(err);
2208
+ };
2209
+ if (opts?.signal) {
2210
+ if (opts.signal.aborted) {
2211
+ onAbort();
2212
+ return;
2213
+ }
2214
+ opts.signal.addEventListener("abort", onAbort, { once: true });
2215
+ }
2216
+ try {
2217
+ const response = await this._client.prompt(sessionId, input);
2218
+ if (!aborted) resolve(response);
2219
+ } catch (err) {
2220
+ if (!aborted) reject(err);
2221
+ } finally {
2222
+ opts?.signal?.removeEventListener("abort", onAbort);
2223
+ }
2224
+ });
2225
+ });
2226
+ }
2227
+ /** 发 session/cancel 通知(幂等,尽力而为)。 */
2228
+ async sessionCancel(sessionId) {
2229
+ if (!this._client) throw new AcpProtocolError("Not connected");
2230
+ await this._client.cancel(sessionId);
2063
2231
  }
2064
2232
  /**
2065
- * 订阅该 session 所有上游 notifications。
2233
+ * 订阅指定 sessionId 的上游 notifications。
2066
2234
  *
2067
2235
  * Listener 收到的是上游 `SessionNotification` 原样(`{ sessionId, update, _meta? }`)。
2068
- * `update` 是上游 11-tag 判别联合,在 switch 分支里类型自动收窄。
2069
- *
2070
- * 订阅独立于 prompt 生命周期——connect 之后注册,直到 unsubscribe 或
2071
- * disconnect 为止。一个 session 支持任意多个订阅者。
2236
+ * 同一 session 可以有任意多个 listener,彼此独立;每条 notification 都会
2237
+ * 按注册顺序调用所有 listener(同步调用,异常被吞)。
2072
2238
  *
2073
2239
  * @returns unsubscribe 函数,调用即退订。
2074
- *
2075
- * @example
2076
- * ```ts
2077
- * const unsub = session.subscribe((n) => {
2078
- * switch (n.update.sessionUpdate) {
2079
- * case 'agent_message_chunk':
2080
- * if (n.update.content.type === 'text') print(n.update.content.text);
2081
- * break;
2082
- * case 'tool_call':
2083
- * console.log('tool:', n.update.title);
2084
- * break;
2085
- * }
2086
- * });
2087
- * // ...
2088
- * unsub();
2089
- * ```
2090
2240
  */
2091
- subscribe(listener) {
2092
- if (!this._acpClient || this._acpClient.state !== "OPEN") throw new ValidationError("Session not connected. Call connect() first.");
2093
- return this._acpClient.subscribe(this.id, listener);
2241
+ subscribe(sessionId, listener) {
2242
+ if (!this._subscribers.has(sessionId)) this._subscribers.set(sessionId, /* @__PURE__ */ new Set());
2243
+ this._subscribers.get(sessionId).add(listener);
2244
+ return () => {
2245
+ this._subscribers.get(sessionId)?.delete(listener);
2246
+ };
2247
+ }
2248
+ on(event, handler) {
2249
+ this._connListeners[event].add(handler);
2250
+ }
2251
+ off(event, handler) {
2252
+ this._connListeners[event].delete(handler);
2094
2253
  }
2095
2254
  /**
2096
- * 取消当前运行的 prompt(如果有)。
2097
- *
2098
- * 向服务端发 `session/cancel` notification,服务端会以 `stopReason: 'cancelled'`
2099
- * 结束当前 prompt,对应的 `prompt()` Promise 正常 resolve(不 reject)。
2255
+ * 把上游 notification 按 sessionId 分发给订阅者。不做任何转换。
2100
2256
  *
2101
- * **尽力而为**:服务端 RPC 失败时打 warn 日志吞掉,不抛给调用方——
2102
- * 因为"用户点了取消按钮"UX 契约就是"本地能返回"。
2257
+ * 上游保证 `SessionNotification` `sessionId` / `update` 字段都有值
2258
+ * (vendor `SessionUpdateCallback` 签名即如此),无需再做类型守卫。
2103
2259
  */
2104
- async cancel() {
2105
- if (!this._acpClient) return;
2106
- try {
2107
- await this._acpClient.sessionCancel(this.id);
2108
- } catch (err) {
2109
- loggerFor(this._runtime._connectionOpts).warn(`[session ${this.id}] cancel failed: ${err.message}`);
2110
- }
2260
+ dispatchNotification(notification) {
2261
+ const subs = this._subscribers.get(notification.sessionId);
2262
+ if (!subs || subs.size === 0) return;
2263
+ for (const listener of subs) try {
2264
+ listener(notification);
2265
+ } catch {}
2111
2266
  }
2112
- };
2113
- /**
2114
- * 合并两个 AbortSignal:任一 abort 时返回的 signal 也 abort。
2115
- *
2116
- * 用原生 `AbortSignal.any()` 不行——Node 18 没有。这里用最小 polyfill。
2117
- */
2118
- function mergeAbortSignals(a, b) {
2119
- if (typeof AbortSignal.any === "function") return AbortSignal.any([a, b]);
2120
- const ctrl = new AbortController();
2121
- const onAbort = () => ctrl.abort();
2122
- if (a.aborted || b.aborted) ctrl.abort();
2123
- else {
2124
- a.addEventListener("abort", onAbort, { once: true });
2125
- b.addEventListener("abort", onAbort, { once: true });
2267
+ emitConnEvent(event, ...args) {
2268
+ for (const handler of this._connListeners[event]) try {
2269
+ handler(...args);
2270
+ } catch {}
2126
2271
  }
2127
- return ctrl.signal;
2128
- }
2129
-
2130
- //#endregion
2131
- //#region src/v1/runtime.ts
2132
- var Runtime = class {
2133
- /** @internal */
2134
- constructor(restClient, connectionOpts, info) {
2135
- this.sessions = {
2136
- create: async (opts) => {
2137
- const body = {
2138
- sessionId: opts.sessionId,
2139
- sessionName: opts.sessionName,
2140
- agentManifest: opts.agentManifest
2141
- };
2142
- return new Session((await this._restClient.post(`/runtimes/${this.id}/sessions`, body, {
2143
- timeoutMs: opts.timeoutMs,
2144
- headers: opts.headers,
2145
- signal: opts.signal,
2146
- requestId: opts.requestId
2147
- })).sessionId, this, this._restClient);
2148
- },
2149
- get: async (sessionId, opts) => {
2150
- await this._restClient.get(`/runtimes/${this.id}/sessions/${sessionId}`, void 0, opts);
2151
- return new Session(sessionId, this, this._restClient);
2152
- },
2153
- list: async (opts) => {
2154
- const params = {};
2155
- if (opts?.page !== void 0) params.page = String(opts.page);
2156
- if (opts?.pageSize !== void 0) params.pageSize = String(opts.pageSize);
2157
- if (opts?.sessionStatus) params.sessionStatus = opts.sessionStatus;
2158
- return this._restClient.get(`/runtimes/${this.id}/sessions`, params, opts);
2159
- },
2160
- default: () => {
2161
- return new Session(this._defaultSessionId || this.id, this, this._restClient);
2162
- }
2272
+ enqueuePrompt(sid, exec) {
2273
+ if (!this._promptQueues.has(sid)) this._promptQueues.set(sid, []);
2274
+ const runThenDequeue = () => {
2275
+ this._activePrompts.add(sid);
2276
+ exec().finally(() => this.dequeuePrompt(sid));
2163
2277
  };
2164
- this.id = info.id;
2165
- this._info = info;
2166
- this._restClient = restClient;
2167
- this._connectionOpts = connectionOpts;
2168
- this.sandboxId = info.links?.sandboxLink?.sandboxId;
2169
- this.sandboxDomain = info.links?.sandboxLink?.endpoint;
2170
- this._acpUrl = info.links?.acpLink?.url;
2171
- this._acpToken = info.links?.acpLink?.token;
2172
- if (info.sessions && info.sessions.length > 0) this._defaultSessionId = info.sessions[0].sessionId;
2173
- }
2174
- get runtimeInfo() {
2175
- return this._info;
2176
- }
2177
- get acpUrl() {
2178
- return this._acpUrl;
2179
- }
2180
- get acpToken() {
2181
- return this._acpToken;
2182
- }
2183
- /** 更新 Runtime 元数据 */
2184
- async update(req, opts) {
2185
- const info = await this._restClient.post(`/runtimes/${this.id}/update`, req, opts);
2186
- this._info = info;
2187
- return info;
2188
- }
2189
- /** 软删除 */
2190
- async delete(opts) {
2191
- return this._restClient.post(`/runtimes/${this.id}/delete`, void 0, opts);
2278
+ if (this._activePrompts.has(sid)) this._promptQueues.get(sid).push(runThenDequeue);
2279
+ else runThenDequeue();
2192
2280
  }
2193
- /** 刷新 token(拉最新 RuntimeInfo,更新 acpLink) */
2194
- async refreshToken(opts) {
2195
- const info = await this._restClient.get(`/runtimes/${this.id}`, void 0, opts);
2196
- this._info = info;
2197
- if (info.links?.acpLink) {
2198
- this._acpUrl = info.links.acpLink.url;
2199
- this._acpToken = info.links.acpLink.token;
2200
- }
2201
- return this._acpToken || "";
2281
+ dequeuePrompt(sid) {
2282
+ this._activePrompts.delete(sid);
2283
+ const q = this._promptQueues.get(sid);
2284
+ if (q && q.length > 0) q.shift()();
2202
2285
  }
2203
2286
  };
2204
2287
 
2205
2288
  //#endregion
2206
- //#region src/v1/internal/redact.ts
2207
- /**
2208
- * 敏感信息脱敏
2209
- *
2210
- * 范围:headers + 特定已知的 body 字段(白名单 path,非启发式猜测)。
2211
- * 脱敏仅作用于日志打印和钩子回调,不影响实际发送的请求。
2212
- *
2213
- * 详见 docs/agentos/sdk/07-error-retry.md § 4.5。
2214
- */
2215
- /** 需要脱敏的 header 名(小写)。 */
2216
- const SENSITIVE_HEADER_NAMES = new Set([
2217
- "authorization",
2218
- "cookie",
2219
- "set-cookie",
2220
- "x-api-key"
2221
- ]);
2222
- /** 脱敏替换值。 */
2223
- const REDACTED = "***";
2224
- /**
2225
- * 返回脱敏后的 headers 副本。
2226
- *
2227
- * - 键按原样保留(大小写不变)
2228
- * - 值按键名(小写后)匹配敏感列表时替换为 `'***'`
2229
- * - **纯函数**,不修改入参
2230
- */
2231
- function redactHeaders(headers) {
2232
- if (!headers) return {};
2233
- const out = {};
2234
- for (const [name, value] of Object.entries(headers)) out[name] = SENSITIVE_HEADER_NAMES.has(name.toLowerCase()) ? REDACTED : value;
2235
- return out;
2236
- }
2237
- /** 判断 value 是否是对象数组(`Array<Record<string, unknown>>`)。 */
2238
- function isObjectArray(v) {
2239
- return Array.isArray(v) && v.every((x) => x !== null && typeof x === "object" && !Array.isArray(x));
2240
- }
2241
- /**
2242
- * Body 脱敏器:对给定对象做**浅复制 + 定向脱敏**,返回安全的日志副本。
2243
- *
2244
- * 不修改入参。若入参不是对象(string / number / ...),原样返回。
2245
- *
2246
- * 已处理字段:
2247
- * - `agentManifest.secrets[].value` → `'***'`(保留 name,只脱 value)
2248
- */
2249
- function redactBody(body) {
2250
- if (body === null || body === void 0) return body;
2251
- if (typeof body !== "object" || Array.isArray(body)) return body;
2252
- const input = body;
2253
- const out = { ...input };
2254
- const manifest = input.agentManifest;
2255
- if (manifest && typeof manifest === "object" && !Array.isArray(manifest)) {
2256
- const secrets = manifest.secrets;
2257
- if (isObjectArray(secrets)) out.agentManifest = {
2258
- ...manifest,
2259
- secrets: secrets.map((s) => "value" in s ? {
2260
- ...s,
2261
- value: REDACTED
2262
- } : s)
2263
- };
2264
- }
2265
- return out;
2266
- }
2267
-
2268
- //#endregion
2269
- //#region src/v1/rest/client.ts
2270
- /** 默认 baseUrl */
2271
- const DEFAULT_BASE_URL = "https://www.codebuddy.cn/v2/agentos";
2289
+ //#region src/v1/session.ts
2272
2290
  /**
2273
- * SDK 内置的默认重试判定。
2291
+ * Session — 对话上下文
2274
2292
  *
2275
- * `retry: { retryOn: ... }` 会**完全替换**这里的规则(不叠加)。
2276
- * 公开这个函数是为了让用户在自定义 `retryOn` 里可以手动调用回落到默认:
2293
+ * 控制面(REST):`info` / `update` / `delete`
2294
+ * 数据面(ACP):`connect` / `prompt` / `subscribe` / `disconnect`
2277
2295
  *
2296
+ * **订阅模型**(对齐 ACP 协议原生形态):
2297
+ * - `prompt(input)` 返回 `Promise<PromptResponse>` —— 一轮的终局
2298
+ * - `subscribe(listener)` —— 流式 notifications 由 listener 异步接收
2299
+ * - 两条通道独立、不聚合、无队列
2300
+ *
2301
+ * 典型用法:
2278
2302
  * ```ts
2279
- * retry: {
2280
- * retryOn: (err, ctx) => {
2281
- * if (err instanceof TimeoutError && ctx.url.includes('/upload')) return false;
2282
- * return DEFAULT_RETRY_ON(err, ctx);
2283
- * },
2284
- * }
2303
+ * await session.connect();
2304
+ *
2305
+ * // 场景 A:只要最终结果
2306
+ * const response = await session.prompt('hello');
2307
+ * console.log(response.stopReason);
2308
+ *
2309
+ * // 场景 B:流式打印 + 最终结果
2310
+ * const unsub = session.subscribe((n) => {
2311
+ * if (n.update.sessionUpdate === 'agent_message_chunk' &&
2312
+ * n.update.content.type === 'text') {
2313
+ * process.stdout.write(n.update.content.text);
2314
+ * }
2315
+ * });
2316
+ * const response = await session.prompt('hello');
2317
+ * unsub();
2318
+ *
2319
+ * // 场景 C:便利钩子(等价于 B 但不需手动 unsub)
2320
+ * const response = await session.prompt('hello', {
2321
+ * onChunk: (n) => { ... },
2322
+ * });
2285
2323
  * ```
2286
2324
  */
2287
- const DEFAULT_RETRY_ON = (err, ctx) => {
2288
- if (err.name === "AbortError") return false;
2289
- if (err instanceof AuthError) return false;
2290
- if (err instanceof NotFoundError) return false;
2291
- if (err instanceof ValidationError) return false;
2292
- if (err instanceof AcpProtocolError) return false;
2293
- if (err instanceof TimeoutError) return ctx.method === "GET";
2294
- if (err instanceof NetworkError) return true;
2295
- return true;
2296
- };
2297
- /** 默认重试配置。 */
2298
- const DEFAULT_RETRY = {
2299
- maxAttempts: 3,
2300
- backoffMs: 300,
2301
- backoffFactor: 2,
2302
- jitter: true,
2303
- retryOn: DEFAULT_RETRY_ON
2304
- };
2305
- var RestClient = class {
2306
- constructor(opts) {
2307
- this.opts = opts;
2308
- this.logger = loggerFor(opts);
2309
- this.fetchImpl = opts.fetch ?? fetch.bind(globalThis);
2325
+ var Session = class {
2326
+ /** @internal */
2327
+ constructor(id, runtime, restClient) {
2328
+ this.id = id;
2329
+ this.runtimeId = runtime.id;
2330
+ this._runtime = runtime;
2331
+ this._restClient = restClient;
2310
2332
  }
2311
- /** GET 请求。 */
2312
- async get(path, query, opts) {
2313
- const url = this.buildUrl(path, query);
2314
- return this.request("GET", url, void 0, opts);
2333
+ get status() {
2334
+ return this._status;
2315
2335
  }
2316
- /** POST 请求。 */
2317
- async post(path, body, opts) {
2318
- const url = this.buildUrl(path);
2319
- return this.request("POST", url, body, opts);
2336
+ /** ACP 连接是否已建立 */
2337
+ get connected() {
2338
+ return this._acpClient?.state === "OPEN";
2320
2339
  }
2321
- /** PATCH 请求。 */
2322
- async patch(path, body, opts) {
2323
- const url = this.buildUrl(path);
2324
- return this.request("PATCH", url, body, opts);
2340
+ /** 拉最新元数据 */
2341
+ async info(opts) {
2342
+ const info = await this._restClient.get(`/runtimes/${this.runtimeId}/sessions/${this.id}`, void 0, opts);
2343
+ this._status = info.sessionStatus;
2344
+ return info;
2325
2345
  }
2326
- /** DELETE 请求。 */
2327
- async delete(path, opts) {
2328
- const url = this.buildUrl(path);
2329
- return this.request("DELETE", url, void 0, opts);
2346
+ /** 更新 name / manifest */
2347
+ async update(req, opts) {
2348
+ const info = await this._restClient.post(`/runtimes/${this.runtimeId}/sessions/${this.id}/update`, req, opts);
2349
+ this._status = info.sessionStatus;
2350
+ return info;
2330
2351
  }
2331
- /** 构建完整 URL。 */
2332
- buildUrl(path, query) {
2333
- const base = (this.opts.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
2334
- const fullPath = path.startsWith("/") ? path : `/${path}`;
2335
- const url = new URL(`${base}${fullPath}`);
2336
- if (query) {
2337
- for (const [k, v] of Object.entries(query)) if (v !== void 0 && v !== "") url.searchParams.set(k, v);
2352
+ /** 软删除 */
2353
+ async delete(opts) {
2354
+ if (this._acpClient) {
2355
+ await this._acpClient.disconnect();
2356
+ this._acpClient = void 0;
2338
2357
  }
2339
- return url.toString();
2358
+ return this._restClient.post(`/runtimes/${this.runtimeId}/sessions/${this.id}/delete`, void 0, opts);
2340
2359
  }
2341
- /** 构建请求 headers(含认证、身份、trace、自定义)。 */
2342
- buildHeaders(opts) {
2343
- const headers = {
2344
- "Content-Type": "application/json",
2345
- "Accept": "application/json"
2346
- };
2347
- if (this.opts.apiKey) headers["x-api-key"] = this.opts.apiKey;
2348
- if (this.opts.sourceApp) headers["X-Source-App"] = this.opts.sourceApp;
2349
- if (this.opts.sourceTenantId) headers["X-Source-Tenant-Id"] = this.opts.sourceTenantId;
2350
- if (this.opts.userId) headers["X-User-Id"] = this.opts.userId;
2351
- if (this.opts.headers) Object.assign(headers, this.opts.headers);
2352
- if (opts?.headers) Object.assign(headers, opts.headers);
2353
- headers["X-Request-Id"] = opts?.requestId ?? generateRequestId();
2354
- return headers;
2360
+ /**
2361
+ * 建立到沙箱的 ACP 连接。
2362
+ *
2363
+ * 内部流程:GET SSE → 获取 connectionId → initialize → session/load。
2364
+ * 调用后 `prompt` / `subscribe` / `cancel` 才可用。
2365
+ *
2366
+ * 幂等:已连接时直接返回;并发调用时后来的等前者完成。
2367
+ */
2368
+ async connect(opts) {
2369
+ if (this._acpClient && this._acpClient.state === "OPEN") return;
2370
+ const acpUrl = this._runtime.acpUrl;
2371
+ let acpToken = this._runtime.acpToken;
2372
+ if (!acpUrl || !acpToken) {
2373
+ acpToken = await this._runtime.refreshToken();
2374
+ if (!this._runtime.acpUrl || !acpToken) throw new ValidationError("Runtime does not have ACP link. Is it in RUNNING state?");
2375
+ }
2376
+ const connOpts = this._runtime._connectionOpts;
2377
+ const client = new AcpClient({
2378
+ logger: loggerFor(connOpts),
2379
+ logLevel: resolveLogLevel(connOpts),
2380
+ fetch: connOpts.fetch
2381
+ });
2382
+ client.setTokenRefresher(async () => this._runtime.refreshToken());
2383
+ await client.connect(this._runtime.acpUrl, acpToken, {
2384
+ ...opts,
2385
+ heartbeatTimeoutMs: opts?.heartbeatTimeoutMs ?? 45e3,
2386
+ reconnectMaxAttempts: opts?.reconnectMaxAttempts ?? Infinity,
2387
+ reconnectBackoffMs: opts?.reconnectBackoffMs ?? 300,
2388
+ reconnectMaxBackoffMs: opts?.reconnectMaxBackoffMs ?? 3e4,
2389
+ lastEventId: opts?.lastEventId
2390
+ });
2391
+ if (opts?.initialize !== false) await client.initialize();
2392
+ await client.sessionLoad(this.id);
2393
+ this._acpClient = client;
2355
2394
  }
2356
- /** 发起请求(含超时 + 重试 + 日志 + 脱敏 + hooks)。 */
2357
- async request(method, url, body, opts) {
2358
- const shouldRetry = opts?.retry !== void 0 ? opts.retry : this.opts.retry ?? DEFAULT_RETRY;
2359
- const ctx = {
2360
- logId: makeLogId(),
2361
- retryOfLogId: void 0,
2362
- startTimeMs: 0
2363
- };
2364
- const doRequest = async () => {
2365
- ctx.startTimeMs = Date.now();
2366
- const timeoutMs = opts?.timeoutMs ?? this.opts.timeoutMs ?? 3e4;
2367
- const headers = this.buildHeaders(opts);
2368
- const requestId = headers["X-Request-Id"];
2369
- const controller = new AbortController();
2370
- let timeoutId;
2371
- if (timeoutMs > 0) timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2372
- if (opts?.signal) if (opts.signal.aborted) controller.abort();
2373
- else opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
2374
- const retryTag = ctx.retryOfLogId ? `, retryOf: ${ctx.retryOfLogId}` : "";
2375
- const redactedReqHeaders = redactHeaders(headers);
2376
- this.opts.onRequest?.({
2377
- method,
2378
- url,
2379
- headers: { ...redactedReqHeaders }
2380
- });
2381
- this.logger.debug(`[${ctx.logId}${retryTag}] sending request ${method} ${url}`, {
2382
- headers: redactedReqHeaders,
2383
- body: body !== void 0 ? truncate(redactBody(body)) : void 0
2384
- });
2385
- try {
2386
- const response = await this.fetchImpl(url, {
2387
- method,
2388
- headers,
2389
- body: body !== void 0 ? JSON.stringify(body) : void 0,
2390
- signal: controller.signal
2391
- });
2392
- const durationMs = Date.now() - ctx.startTimeMs;
2393
- const responseHeaders = {};
2394
- response.headers.forEach((v, k) => {
2395
- responseHeaders[k] = v;
2396
- });
2397
- const redactedResHeaders = redactHeaders(responseHeaders);
2398
- this.opts.onResponse?.({
2399
- status: response.status,
2400
- headers: { ...redactedResHeaders },
2401
- url
2402
- });
2403
- const serverReqId = response.headers.get("x-request-id") ?? requestId;
2404
- const reqIdTag = serverReqId ? `, request-id: "${serverReqId}"` : "";
2405
- const summary = `[${ctx.logId}${retryTag}${reqIdTag}] ${method} ${url} ${response.ok ? "succeeded" : "failed"} with status ${response.status} in ${durationMs}ms`;
2406
- this.logger.info(summary);
2407
- this.logger.debug(`[${ctx.logId}${retryTag}] response received`, {
2408
- status: response.status,
2409
- headers: redactedResHeaders,
2410
- durationMs
2411
- });
2412
- return await this.unwrap(response, ctx.logId, serverReqId);
2413
- } catch (err) {
2414
- if (timeoutId) clearTimeout(timeoutId);
2415
- const durationMs = Date.now() - ctx.startTimeMs;
2416
- if (err instanceof Error && err.name === "AbortError") {
2417
- if (opts?.signal?.aborted) {
2418
- this.logger.debug(`[${ctx.logId}${retryTag}] request cancelled by caller after ${durationMs}ms`);
2419
- throw err;
2420
- }
2421
- const msg = `Request timed out after ${timeoutMs}ms: ${method} ${url}`;
2422
- this.logger.debug(`[${ctx.logId}${retryTag}] ${msg}`);
2423
- throw new TimeoutError(msg, {
2424
- logId: ctx.logId,
2425
- cause: err
2426
- });
2427
- }
2428
- if (err instanceof TypeError || err instanceof Error && isNetworkError(err)) {
2429
- const msg = `Network error: ${method} ${url} - ${err.message}`;
2430
- this.logger.debug(`[${ctx.logId}${retryTag}] ${msg}`);
2431
- throw new NetworkError(msg, {
2432
- logId: ctx.logId,
2433
- cause: err
2434
- });
2435
- }
2436
- if (err instanceof CloudAgentError) throw err;
2437
- const msg = `Unexpected error: ${method} ${url} - ${err.message}`;
2438
- this.logger.error(`[${ctx.logId}${retryTag}] ${msg}`);
2439
- throw new NetworkError(msg, {
2440
- logId: ctx.logId,
2441
- cause: err
2442
- });
2443
- } finally {
2444
- if (timeoutId) clearTimeout(timeoutId);
2445
- }
2446
- };
2447
- if (shouldRetry === false) return doRequest();
2448
- return this.withRetry(doRequest, shouldRetry, ctx, method, url);
2395
+ /** 断开 ACP 连接。所有订阅器被自动清除。 */
2396
+ async disconnect() {
2397
+ if (this._acpClient) {
2398
+ await this._acpClient.disconnect();
2399
+ this._acpClient = void 0;
2400
+ }
2449
2401
  }
2450
- /** 解包响应 `{ code, msg, data }` → `data` 或抛错。 */
2451
- async unwrap(response, logId, serverReqId) {
2452
- if (response.status === 204) return;
2453
- let body;
2454
- let rawText;
2455
- try {
2456
- rawText = await response.text();
2457
- const safeText = rawText.replace(/:\s*(\d{17,})/g, (match, num) => match.replace(num, `"${num}"`));
2458
- body = JSON.parse(safeText);
2459
- } catch {
2460
- if (!response.ok) throw this.httpStatusToError(response.status, `HTTP ${response.status}`, {
2461
- logId,
2462
- requestId: serverReqId
2463
- });
2464
- return rawText;
2402
+ /**
2403
+ * 发送 prompt 并等待 `PromptResponse`。
2404
+ *
2405
+ * @param input 纯文本字符串(自动包装成 text block)或 `ContentBlock[]`(多模态)
2406
+ * @param opts 见 `PromptOpts`,支持 `signal` 取消、`onChunk` 便利钩子等
2407
+ *
2408
+ * @returns `PromptResponse`(当前只含 `stopReason`)
2409
+ *
2410
+ * **时序**:
2411
+ * - 如果未 connect,会自动 connect
2412
+ * - RPC 发出期间,上游会通过 **SSE notification 通道**推送 `session/update`,
2413
+ * 这些消息**不会进 Promise 的结果**,需要通过 `subscribe()` 或 `onChunk`
2414
+ * 回调接收
2415
+ * - 同 session 并发 prompt 会被内部**串行化**(ACP 协议要求)
2416
+ *
2417
+ * @example
2418
+ * ```ts
2419
+ * // 最简:只要结果
2420
+ * const r = await session.prompt('2+2?');
2421
+ *
2422
+ * // 流式打印 + 结果
2423
+ * const r = await session.prompt('write a poem', {
2424
+ * onChunk: (n) => {
2425
+ * if (n.update.sessionUpdate === 'agent_message_chunk' &&
2426
+ * n.update.content.type === 'text') {
2427
+ * process.stdout.write(n.update.content.text);
2428
+ * }
2429
+ * },
2430
+ * });
2431
+ *
2432
+ * // 取消
2433
+ * const ctrl = new AbortController();
2434
+ * setTimeout(() => ctrl.abort(), 5000);
2435
+ * const r = await session.prompt('long task', { signal: ctrl.signal });
2436
+ * ```
2437
+ */
2438
+ async prompt(input, opts) {
2439
+ if (!this._acpClient || this._acpClient.state !== "OPEN") await this.connect();
2440
+ const content = typeof input === "string" ? [{
2441
+ type: "text",
2442
+ text: input
2443
+ }] : input;
2444
+ const unsubChunk = opts?.onChunk ? this._acpClient.subscribe(this.id, opts.onChunk) : void 0;
2445
+ let timeoutCtrl;
2446
+ let signal = opts?.signal;
2447
+ if (opts?.timeoutMs !== void 0 && opts.timeoutMs > 0) {
2448
+ timeoutCtrl = new AbortController();
2449
+ const timer = setTimeout(() => timeoutCtrl.abort(), opts.timeoutMs);
2450
+ signal?.addEventListener("abort", () => {
2451
+ clearTimeout(timer);
2452
+ timeoutCtrl.abort();
2453
+ }, { once: true });
2454
+ if (!signal) signal = timeoutCtrl.signal;
2455
+ else signal = mergeAbortSignals(signal, timeoutCtrl.signal);
2465
2456
  }
2466
- const effectiveRequestId = body.requestId || serverReqId;
2467
- if (!response.ok) throw this.httpStatusToError(response.status, body.msg || `HTTP ${response.status}`, {
2468
- logId,
2469
- requestId: effectiveRequestId,
2470
- code: body.code
2471
- });
2472
- if (body.code !== 0) throw this.codeToError(body.code, body.msg, {
2473
- logId,
2474
- requestId: effectiveRequestId,
2475
- httpStatus: response.status
2476
- });
2477
- return body.data;
2478
- }
2479
- /** HTTP 状态码映射为错误。 */
2480
- httpStatusToError(status, message, ctx) {
2481
- const opts = {
2482
- httpStatus: status,
2483
- logId: ctx.logId,
2484
- requestId: ctx.requestId,
2485
- code: ctx.code
2486
- };
2487
- switch (status) {
2488
- case 400: return new ValidationError(message, opts);
2489
- case 401:
2490
- case 403: return new AuthError(message, opts);
2491
- case 404: return new NotFoundError(message, opts);
2492
- case 408:
2493
- case 504: return new TimeoutError(message, opts);
2494
- case 429: return new NetworkError(message, opts);
2495
- default:
2496
- if (status >= 500) return new NetworkError(message, opts);
2497
- return new CloudAgentError(message, opts);
2457
+ opts?.onTurnStart?.();
2458
+ try {
2459
+ const response = await this._acpClient.prompt(this.id, content, { signal });
2460
+ opts?.onTurnEnd?.(response);
2461
+ return response;
2462
+ } finally {
2463
+ unsubChunk?.();
2498
2464
  }
2499
2465
  }
2500
- /** 业务 code 映射为错误。 */
2501
- codeToError(code, message, ctx) {
2502
- const opts = {
2503
- code,
2504
- httpStatus: ctx.httpStatus,
2505
- logId: ctx.logId,
2506
- requestId: ctx.requestId
2507
- };
2508
- switch (code) {
2509
- case 10001: return new ValidationError(message, opts);
2510
- case 10034:
2511
- case 10085: return new AuthError(message, opts);
2512
- case 10064:
2513
- case 10065:
2514
- case 10067: return new NetworkError(message, opts);
2515
- case 10084: return new NotFoundError(message, opts);
2516
- case 10094: return new TimeoutError(message, opts);
2517
- case 1e4: return new NetworkError(message, opts);
2518
- default: return new CloudAgentError(message, opts);
2519
- }
2466
+ /**
2467
+ * 订阅该 session 所有上游 notifications。
2468
+ *
2469
+ * Listener 收到的是上游 `SessionNotification` 原样(`{ sessionId, update, _meta? }`)。
2470
+ * `update` 是上游 11-tag 判别联合,在 switch 分支里类型自动收窄。
2471
+ *
2472
+ * 订阅独立于 prompt 生命周期——connect 之后注册,直到 unsubscribe 或
2473
+ * disconnect 为止。一个 session 支持任意多个订阅者。
2474
+ *
2475
+ * @returns unsubscribe 函数,调用即退订。
2476
+ *
2477
+ * @example
2478
+ * ```ts
2479
+ * const unsub = session.subscribe((n) => {
2480
+ * switch (n.update.sessionUpdate) {
2481
+ * case 'agent_message_chunk':
2482
+ * if (n.update.content.type === 'text') print(n.update.content.text);
2483
+ * break;
2484
+ * case 'tool_call':
2485
+ * console.log('tool:', n.update.title);
2486
+ * break;
2487
+ * }
2488
+ * });
2489
+ * // ...
2490
+ * unsub();
2491
+ * ```
2492
+ */
2493
+ subscribe(listener) {
2494
+ if (!this._acpClient || this._acpClient.state !== "OPEN") throw new ValidationError("Session not connected. Call connect() first.");
2495
+ return this._acpClient.subscribe(this.id, listener);
2520
2496
  }
2521
2497
  /**
2522
- * 重试逻辑。
2498
+ * 取消当前运行的 prompt(如果有)。
2523
2499
  *
2524
- * 是否重试只看 `opts.retryOn(err, ctx)`。用户传的 retryOn **完全替换** SDK
2525
- * 默认判定(不叠加、不 fallback error 类上的任何字段)。
2500
+ * 向服务端发 `session/cancel` notification,服务端会以 `stopReason: 'cancelled'`
2501
+ * 结束当前 prompt,对应的 `prompt()` Promise 正常 resolve(不 reject)。
2502
+ *
2503
+ * **尽力而为**:服务端 RPC 失败时打 warn 日志吞掉,不抛给调用方——
2504
+ * 因为"用户点了取消按钮"的 UX 契约就是"本地能返回"。
2526
2505
  */
2527
- async withRetry(fn, retryOpts, reqCtx, method, url) {
2528
- const opts = {
2529
- ...DEFAULT_RETRY,
2530
- ...retryOpts
2531
- };
2532
- let lastError;
2533
- for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) try {
2534
- return await fn();
2506
+ async cancel() {
2507
+ if (!this._acpClient) return;
2508
+ try {
2509
+ await this._acpClient.sessionCancel(this.id);
2535
2510
  } catch (err) {
2536
- lastError = err;
2537
- const retryCtx = {
2538
- attempt,
2539
- method,
2540
- url
2541
- };
2542
- if (!opts.retryOn(err, retryCtx)) throw err;
2543
- if (attempt >= opts.maxAttempts) break;
2544
- const delayMs = this.getBackoffMs(attempt, opts);
2545
- this.logger.warn(`[${reqCtx.logId}] retrying (attempt ${attempt}/${opts.maxAttempts}) in ${delayMs}ms — ${err.name}: ${err.message}`);
2546
- await sleep(delayMs);
2547
- reqCtx.retryOfLogId = reqCtx.logId;
2548
- reqCtx.logId = makeLogId();
2511
+ loggerFor(this._runtime._connectionOpts).warn(`[session ${this.id}] cancel failed: ${err.message}`);
2549
2512
  }
2550
- throw lastError;
2551
- }
2552
- /** 计算退避时间。 */
2553
- getBackoffMs(attempt, opts) {
2554
- const baseDelay = Math.min(opts.backoffMs * Math.pow(opts.backoffFactor, attempt - 1), 3e4);
2555
- if (!opts.jitter) return Math.round(baseDelay);
2556
- const jitterFactor = .2 * (Math.random() * 2 - 1);
2557
- return Math.round(baseDelay * (1 + jitterFactor));
2558
2513
  }
2559
2514
  };
2560
- function sleep(ms) {
2561
- return new Promise((resolve) => setTimeout(resolve, ms));
2562
- }
2563
- function isNetworkError(err) {
2564
- const msg = err.message.toLowerCase();
2565
- return msg.includes("failed to fetch") || msg.includes("fetch failed") || msg.includes("network") || msg.includes("econnrefused") || msg.includes("econnreset") || msg.includes("enotfound") || msg.includes("socket hang up");
2566
- }
2567
- function generateRequestId() {
2568
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
2569
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
2570
- const r = Math.random() * 16 | 0;
2571
- return (c === "x" ? r : r & 3 | 8).toString(16);
2572
- });
2573
- }
2574
2515
  /**
2575
- * 截断超长 body 用于 debug 日志,避免刷屏。
2516
+ * 合并两个 AbortSignal:任一 abort 时返回的 signal 也 abort。
2576
2517
  *
2577
- * 调用方应先通过 `redactBody()` 做敏感字段脱敏,此函数只负责序列化 + 截断。
2518
+ * 用原生 `AbortSignal.any()` 不行——Node 18 没有。这里用最小 polyfill。
2578
2519
  */
2579
- function truncate(body, maxChars = 500) {
2580
- try {
2581
- const str = typeof body === "string" ? body : JSON.stringify(body);
2582
- if (str.length <= maxChars) return str;
2583
- return str.slice(0, maxChars) + `…(truncated, total ${str.length} chars)`;
2584
- } catch {
2585
- return "[unserializable]";
2520
+ function mergeAbortSignals(a, b) {
2521
+ if (typeof AbortSignal.any === "function") return AbortSignal.any([a, b]);
2522
+ const ctrl = new AbortController();
2523
+ const onAbort = () => ctrl.abort();
2524
+ if (a.aborted || b.aborted) ctrl.abort();
2525
+ else {
2526
+ a.addEventListener("abort", onAbort, { once: true });
2527
+ b.addEventListener("abort", onAbort, { once: true });
2586
2528
  }
2529
+ return ctrl.signal;
2587
2530
  }
2588
2531
 
2532
+ //#endregion
2533
+ //#region src/v1/runtime.ts
2534
+ var Runtime = class {
2535
+ /** @internal */
2536
+ constructor(restClient, connectionOpts, info) {
2537
+ this.sessions = {
2538
+ create: async (opts) => {
2539
+ const body = {
2540
+ sessionId: opts.sessionId,
2541
+ sessionName: opts.sessionName,
2542
+ agentManifest: opts.agentManifest
2543
+ };
2544
+ return new Session((await this._restClient.post(`/runtimes/${this.id}/sessions`, body, {
2545
+ timeoutMs: opts.timeoutMs,
2546
+ headers: opts.headers,
2547
+ signal: opts.signal,
2548
+ requestId: opts.requestId
2549
+ })).sessionId, this, this._restClient);
2550
+ },
2551
+ get: async (sessionId, opts) => {
2552
+ await this._restClient.get(`/runtimes/${this.id}/sessions/${sessionId}`, void 0, opts);
2553
+ return new Session(sessionId, this, this._restClient);
2554
+ },
2555
+ list: async (opts) => {
2556
+ const params = {};
2557
+ if (opts?.page !== void 0) params.page = String(opts.page);
2558
+ if (opts?.pageSize !== void 0) params.pageSize = String(opts.pageSize);
2559
+ if (opts?.sessionStatus) params.sessionStatus = opts.sessionStatus;
2560
+ return this._restClient.get(`/runtimes/${this.id}/sessions`, params, opts);
2561
+ },
2562
+ default: () => {
2563
+ return new Session(this._defaultSessionId || this.id, this, this._restClient);
2564
+ }
2565
+ };
2566
+ this.id = info.id;
2567
+ this._info = info;
2568
+ this._restClient = restClient;
2569
+ this._connectionOpts = connectionOpts;
2570
+ this.sandboxId = info.links?.sandboxLink?.sandboxId;
2571
+ this.sandboxDomain = info.links?.sandboxLink?.endpoint;
2572
+ this._acpUrl = info.links?.acpLink?.url;
2573
+ this._acpToken = info.links?.acpLink?.token;
2574
+ if (info.sessions && info.sessions.length > 0) this._defaultSessionId = info.sessions[0].sessionId;
2575
+ }
2576
+ get runtimeInfo() {
2577
+ return this._info;
2578
+ }
2579
+ get acpUrl() {
2580
+ return this._acpUrl;
2581
+ }
2582
+ get acpToken() {
2583
+ return this._acpToken;
2584
+ }
2585
+ /** 更新 Runtime 元数据 */
2586
+ async update(req, opts) {
2587
+ const info = await this._restClient.post(`/runtimes/${this.id}/update`, req, opts);
2588
+ this._info = info;
2589
+ return info;
2590
+ }
2591
+ /** 软删除 */
2592
+ async delete(opts) {
2593
+ return this._restClient.post(`/runtimes/${this.id}/delete`, void 0, opts);
2594
+ }
2595
+ /** 刷新 token(拉最新 RuntimeInfo,更新 acpLink) */
2596
+ async refreshToken(opts) {
2597
+ const info = await this._restClient.get(`/runtimes/${this.id}`, void 0, opts);
2598
+ this._info = info;
2599
+ if (info.links?.acpLink) {
2600
+ this._acpUrl = info.links.acpLink.url;
2601
+ this._acpToken = info.links.acpLink.token;
2602
+ }
2603
+ return this._acpToken || "";
2604
+ }
2605
+ };
2606
+
2589
2607
  //#endregion
2590
2608
  //#region src/v1/client.ts
2609
+ /**
2610
+ * CloudAgentClient — SDK 顶层入口
2611
+ *
2612
+ * 持有 ConnectionOpts,提供 runtimes 子命名空间(仅控制面集合操作)。
2613
+ * 本身不发起网络请求、不持有连接。
2614
+ */
2615
+ /** Secret key 名(服务端 `constants.CodebuddyAPIKey`)。 */
2616
+ const CODEBUDDY_API_KEY = "CODEBUDDY_API_KEY";
2591
2617
  var CloudAgentClient = class CloudAgentClient {
2592
2618
  /** 构造(同步,不发网络请求) */
2593
2619
  constructor(opts = {}) {
2594
2620
  this.runtimes = {
2595
2621
  create: async (opts) => {
2622
+ const agentManifest = withInjectedApiKeySecret(opts.agentManifest, this.opts.apiKey, this.opts);
2596
2623
  const body = {
2597
2624
  runtimeName: opts.runtimeName,
2598
- agentManifest: opts.agentManifest,
2625
+ agentManifest,
2599
2626
  sandboxTemplateId: opts.sandboxTemplateId,
2600
2627
  sandboxSpec: opts.sandboxSpec,
2601
2628
  sandboxType: opts.sandboxType,
@@ -2633,6 +2660,38 @@ var CloudAgentClient = class CloudAgentClient {
2633
2660
  });
2634
2661
  }
2635
2662
  };
2663
+ /**
2664
+ * 按需注入 `CODEBUDDY_API_KEY` secret 的兜底逻辑。
2665
+ *
2666
+ * SDK 只负责一件事:**把 `ConnectionOpts.apiKey` 透传到沙箱 agent** —— 这个
2667
+ * credential 本来就是调用方已经提供过的,用户不应该被迫在 manifest 里再写一遍。
2668
+ *
2669
+ * 规则(按优先级):
2670
+ * 1. `userManifest.secrets[CODEBUDDY_API_KEY]` 已显式声明 → 保留用户值,不覆盖
2671
+ * 2. `userManifest` 根本没传 → 原样返回 `undefined`,让服务端按"manifest 必填字段缺失"
2672
+ * 报明确错误(SDK 不编造 `id/name/manifestVersion` 等用户的业务语义)
2673
+ * 3. `ConnectionOpts.apiKey` 为空 → 无从兜底,原样返回
2674
+ * 4. 其他情况(用户传了 manifest 但没 `CODEBUDDY_API_KEY`,且 Client 有 apiKey)
2675
+ * → 追加 secrets,不 mutate 入参
2676
+ *
2677
+ * 设计取舍:SDK 不探查 `opts.metadata.CODEBUDDY_API_KEY` —— metadata 是
2678
+ * AgentOS 与上游网关之间的内部协议(见服务端 `sandbox_builder.go` 的优先级逻辑),
2679
+ * 不是 SDK 用户的常规使用通道。即便用户显式在 metadata 里塞了一个 key,服务端
2680
+ * 自身的优先级会用 metadata 值覆盖 manifest 里注入的那条,行为正确。
2681
+ */
2682
+ function withInjectedApiKeySecret(userManifest, apiKey, connectionOpts) {
2683
+ if (userManifest?.secrets?.some((s) => s.key === CODEBUDDY_API_KEY)) return userManifest;
2684
+ if (!userManifest) return;
2685
+ if (!apiKey) return userManifest;
2686
+ loggerFor(connectionOpts).debug(`[runtimes.create] auto-injected ${CODEBUDDY_API_KEY} secret from ConnectionOpts.apiKey (no explicit value in agentManifest.secrets)`);
2687
+ return {
2688
+ ...userManifest,
2689
+ secrets: [...userManifest.secrets ?? [], {
2690
+ key: CODEBUDDY_API_KEY,
2691
+ value: apiKey
2692
+ }]
2693
+ };
2694
+ }
2636
2695
 
2637
2696
  //#endregion
2638
2697
  //#region src/v1/manifest.ts