@tencent-ai/cloud-agent-sdk 0.3.0 → 0.3.1

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