@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.cjs +1870 -1811
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -18
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +24 -18
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1870 -1811
- package/dist/index.mjs.map +1 -1
- package/dist/tencent-ai-cloud-agent-sdk-0.3.1.tgz +0 -0
- package/package.json +4 -4
- package/dist/tencent-ai-cloud-agent-sdk-0.3.0.tgz +0 -0
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/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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/
|
|
208
|
+
//#region src/v1/rest/client.ts
|
|
209
|
+
/** 默认 baseUrl */
|
|
210
|
+
const DEFAULT_BASE_URL = "https://www.codebuddy.cn/v2/agentos";
|
|
63
211
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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/
|
|
1057
|
+
//#region src/v1/acp/vendor/events.ts
|
|
773
1058
|
/**
|
|
774
|
-
*
|
|
1059
|
+
* Type-safe event emitter implementation
|
|
775
1060
|
*/
|
|
776
|
-
var
|
|
777
|
-
constructor(
|
|
778
|
-
this.
|
|
779
|
-
this.
|
|
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
|
-
*
|
|
1067
|
+
* Add an event listener
|
|
795
1068
|
*/
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
this.
|
|
799
|
-
|
|
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
|
-
*
|
|
1075
|
+
* Remove an event listener
|
|
824
1076
|
*/
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
if (
|
|
828
|
-
this.
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
*
|
|
836
|
-
* Returns true if the permission was found and resolved
|
|
1085
|
+
* Add a one-time event listener
|
|
837
1086
|
*/
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
*
|
|
856
|
-
* Returns true if
|
|
1093
|
+
* Emit an event to all registered listeners
|
|
1094
|
+
* Returns true if any listeners were invoked
|
|
857
1095
|
*/
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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 (
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1127
|
+
* Remove all listeners for an event, or all listeners if no event specified
|
|
973
1128
|
*/
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
*
|
|
988
|
-
* Returns true if the request was found and answered
|
|
1140
|
+
* Get the number of listeners for an event
|
|
989
1141
|
*/
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
*
|
|
1008
|
-
* Returns true if the request was found and cancelled
|
|
1146
|
+
* Get all event names that have listeners
|
|
1009
1147
|
*/
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
-
*
|
|
1171
|
+
* Register a handler for a specific extension method
|
|
1028
1172
|
*/
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
*
|
|
1180
|
+
* Set a fallback handler for unknown extensions
|
|
1039
1181
|
*/
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
-
*
|
|
1186
|
+
* Handle an extension notification
|
|
1050
1187
|
*/
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
-
*
|
|
1202
|
+
* Check if a method is a known extension
|
|
1056
1203
|
*/
|
|
1057
|
-
|
|
1058
|
-
return
|
|
1204
|
+
isKnownExtension(method) {
|
|
1205
|
+
return KNOWN_EXTENSIONS.includes(method);
|
|
1059
1206
|
}
|
|
1060
1207
|
/**
|
|
1061
|
-
*
|
|
1208
|
+
* Check if method is the artifact extension
|
|
1062
1209
|
*/
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
*
|
|
1214
|
+
* Clear all handlers
|
|
1077
1215
|
*/
|
|
1078
|
-
|
|
1079
|
-
this.
|
|
1080
|
-
|
|
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/
|
|
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
|
-
*
|
|
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
|
|
1104
|
-
constructor(
|
|
1105
|
-
this.
|
|
1106
|
-
this.
|
|
1107
|
-
this.
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
-
*
|
|
1274
|
+
* Handle timeout for a permission request
|
|
1172
1275
|
*/
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
-
*
|
|
1286
|
+
* Resolve a permission request with a selected option
|
|
1287
|
+
* Returns true if the permission was found and resolved
|
|
1178
1288
|
*/
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
-
*
|
|
1306
|
+
* Reject (cancel) a permission request
|
|
1307
|
+
* Returns true if the permission was found and rejected
|
|
1184
1308
|
*/
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
|
1323
|
+
* Get all pending permissions
|
|
1190
1324
|
*/
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
|
1334
|
+
* Get a specific pending permission
|
|
1196
1335
|
*/
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
-
*
|
|
1345
|
+
* Check if there are any pending permissions
|
|
1202
1346
|
*/
|
|
1203
|
-
|
|
1204
|
-
return this.
|
|
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
|
-
*
|
|
1351
|
+
* Get the count of pending permissions
|
|
1229
1352
|
*/
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
*
|
|
1292
|
-
* Sends DELETE request to server before closing local resources
|
|
1357
|
+
* Clear all pending permissions (reject all)
|
|
1293
1358
|
*/
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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.
|
|
1306
|
-
this.
|
|
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
|
-
*
|
|
1369
|
+
* Update configuration
|
|
1313
1370
|
*/
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
*
|
|
1372
|
-
* Requires agent to support loadSession capability
|
|
1395
|
+
* Set event callbacks
|
|
1373
1396
|
*/
|
|
1374
|
-
|
|
1375
|
-
this.
|
|
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
|
-
*
|
|
1401
|
+
* Handle a question request from the agent
|
|
1402
|
+
* Called when receiving _codebuddy.ai/question extMethod
|
|
1391
1403
|
*/
|
|
1392
|
-
async
|
|
1393
|
-
|
|
1394
|
-
this.
|
|
1395
|
-
return
|
|
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
|
-
*
|
|
1399
|
-
* @experimental This API is unstable and may change
|
|
1423
|
+
* Handle timeout for a question request
|
|
1400
1424
|
*/
|
|
1401
|
-
|
|
1402
|
-
this.
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
-
*
|
|
1438
|
+
* Answer a question request with user's selections
|
|
1439
|
+
* Returns true if the request was found and answered
|
|
1408
1440
|
*/
|
|
1409
|
-
|
|
1410
|
-
this.
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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
|
|
1458
|
+
* Cancel a question request
|
|
1459
|
+
* Returns true if the request was found and cancelled
|
|
1423
1460
|
*/
|
|
1424
|
-
|
|
1425
|
-
this.
|
|
1426
|
-
|
|
1427
|
-
|
|
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
|
-
*
|
|
1478
|
+
* Get all pending question requests
|
|
1431
1479
|
*/
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
-
*
|
|
1489
|
+
* Get a specific pending question request
|
|
1437
1490
|
*/
|
|
1438
|
-
|
|
1439
|
-
|
|
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
|
-
*
|
|
1500
|
+
* Check if there are any pending question requests
|
|
1443
1501
|
*/
|
|
1444
|
-
|
|
1445
|
-
return this.
|
|
1502
|
+
hasPending() {
|
|
1503
|
+
return this.pending.size > 0;
|
|
1446
1504
|
}
|
|
1447
1505
|
/**
|
|
1448
|
-
*
|
|
1506
|
+
* Get the count of pending question requests
|
|
1449
1507
|
*/
|
|
1450
|
-
|
|
1451
|
-
return this.
|
|
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
|
-
*
|
|
1622
|
+
* Get current client state
|
|
1455
1623
|
*/
|
|
1456
|
-
|
|
1457
|
-
return this.
|
|
1624
|
+
get currentState() {
|
|
1625
|
+
return this.state;
|
|
1458
1626
|
}
|
|
1459
1627
|
/**
|
|
1460
|
-
*
|
|
1628
|
+
* Check if client is initialized
|
|
1461
1629
|
*/
|
|
1462
|
-
|
|
1463
|
-
return this.
|
|
1630
|
+
get isInitialized() {
|
|
1631
|
+
return this.state === "initialized";
|
|
1464
1632
|
}
|
|
1465
1633
|
/**
|
|
1466
|
-
*
|
|
1634
|
+
* Check if client is connected (but maybe not initialized)
|
|
1467
1635
|
*/
|
|
1468
|
-
|
|
1469
|
-
return this.
|
|
1636
|
+
get isConnected() {
|
|
1637
|
+
return this.state === "connected" || this.state === "initialized";
|
|
1470
1638
|
}
|
|
1471
1639
|
/**
|
|
1472
|
-
*
|
|
1640
|
+
* Get agent capabilities from initialization response
|
|
1473
1641
|
*/
|
|
1474
|
-
|
|
1475
|
-
return this.
|
|
1642
|
+
get agentCapabilities() {
|
|
1643
|
+
return this.initializeResponse?.agentCapabilities;
|
|
1476
1644
|
}
|
|
1477
1645
|
/**
|
|
1478
|
-
*
|
|
1646
|
+
* Get full initialization response
|
|
1479
1647
|
*/
|
|
1480
|
-
|
|
1481
|
-
this.
|
|
1482
|
-
return this.connection.extMethod(method, params);
|
|
1648
|
+
get initializeResult() {
|
|
1649
|
+
return this.initializeResponse;
|
|
1483
1650
|
}
|
|
1484
1651
|
/**
|
|
1485
|
-
*
|
|
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.
|
|
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
|
-
|
|
1644
|
-
|
|
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
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
this.
|
|
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.
|
|
1653
|
-
endpoint:
|
|
1654
|
-
authToken:
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
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
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
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
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1723
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1724
|
+
setTimeout(() => {
|
|
1725
|
+
reject(new InitializationError(`Initialize timed out after ${timeout}ms`));
|
|
1726
|
+
}, timeout);
|
|
1675
1727
|
});
|
|
1676
|
-
await
|
|
1677
|
-
this.
|
|
1678
|
-
this.
|
|
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.
|
|
1681
|
-
|
|
1682
|
-
|
|
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.
|
|
1687
|
-
this.
|
|
1688
|
-
if (this.
|
|
1746
|
+
if (this.state === "disconnected") return;
|
|
1747
|
+
this.options.logger?.info("Disconnecting client");
|
|
1748
|
+
if (this.transport) {
|
|
1689
1749
|
try {
|
|
1690
|
-
await this.
|
|
1691
|
-
} catch {
|
|
1692
|
-
|
|
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.
|
|
1695
|
-
this.
|
|
1696
|
-
this.
|
|
1697
|
-
this.
|
|
1698
|
-
this.
|
|
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
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
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
|
-
*
|
|
1778
|
+
* Create a new session
|
|
1720
1779
|
*
|
|
1721
|
-
*
|
|
1722
|
-
*
|
|
1723
|
-
*
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
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
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
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
|
-
/**
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1773
|
-
|
|
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
|
-
|
|
1780
|
-
|
|
1922
|
+
/**
|
|
1923
|
+
* Check if there are pending question requests
|
|
1924
|
+
*/
|
|
1925
|
+
hasPendingQuestions() {
|
|
1926
|
+
return this.questionManager.hasPending();
|
|
1781
1927
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
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
|
-
*
|
|
1787
|
-
*
|
|
1788
|
-
* 上游保证 `SessionNotification` 的 `sessionId` / `update` 字段都有值
|
|
1789
|
-
* (vendor 的 `SessionUpdateCallback` 签名即如此),无需再做类型守卫。
|
|
1936
|
+
* Send an extension notification
|
|
1790
1937
|
*/
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
} catch {}
|
|
1942
|
+
on(event, listener) {
|
|
1943
|
+
this.emitter.on(event, listener);
|
|
1944
|
+
return this;
|
|
1802
1945
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
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
|
-
|
|
1813
|
-
this.
|
|
1814
|
-
|
|
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
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
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
|
-
*
|
|
1896
|
-
*
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
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
|
-
*
|
|
2053
|
+
* ACP 客户端 — 订阅模型
|
|
2054
|
+
*
|
|
2055
|
+
* 设计:直接映射到 ACP 协议的两条独立通道:
|
|
1911
2056
|
*
|
|
1912
|
-
*
|
|
2057
|
+
* - **RPC 通道(POST)**:`prompt` / `sessionCancel` 等是纯 `Promise`,转发上游。
|
|
2058
|
+
* - **Notification 通道(SSE)**:`subscribe(sessionId, listener)` 给用户注册
|
|
2059
|
+
* 监听器,每条上游 `SessionNotification` 多路分发给所有订阅者。
|
|
1913
2060
|
*
|
|
1914
|
-
*
|
|
2061
|
+
* 不做聚合、不建队列。上游 `SessionNotification` / `PromptResponse` 等类型原封
|
|
2062
|
+
* 不动透给上层(`Session` 类)。
|
|
2063
|
+
*
|
|
2064
|
+
* 具体实现基于 vendor 的 `StreamableHttpClient`,它内部管理一条 SSE + POST 复合
|
|
2065
|
+
* 通道。用户侧看到的只是"一个 session 对应一个 AcpClient"。
|
|
1915
2066
|
*/
|
|
1916
|
-
|
|
1917
|
-
|
|
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
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
this.
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
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
|
|
1931
|
-
return this.
|
|
2097
|
+
get state() {
|
|
2098
|
+
return this._state;
|
|
1932
2099
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
return this._acpClient?.state === "OPEN";
|
|
2100
|
+
get connectionId() {
|
|
2101
|
+
return this._client?.connectionId;
|
|
1936
2102
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
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
|
-
|
|
1944
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
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.
|
|
1995
|
-
|
|
1996
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
2031
|
-
*
|
|
2032
|
-
*
|
|
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.
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
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
|
-
*
|
|
2232
|
+
* 订阅指定 sessionId 的上游 notifications。
|
|
2065
2233
|
*
|
|
2066
2234
|
* Listener 收到的是上游 `SessionNotification` 原样(`{ sessionId, update, _meta? }`)。
|
|
2067
|
-
*
|
|
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.
|
|
2092
|
-
|
|
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
|
-
*
|
|
2096
|
-
*
|
|
2097
|
-
* 向服务端发 `session/cancel` notification,服务端会以 `stopReason: 'cancelled'`
|
|
2098
|
-
* 结束当前 prompt,对应的 `prompt()` Promise 正常 resolve(不 reject)。
|
|
2254
|
+
* 把上游 notification 按 sessionId 分发给订阅者。不做任何转换。
|
|
2099
2255
|
*
|
|
2100
|
-
*
|
|
2101
|
-
*
|
|
2256
|
+
* 上游保证 `SessionNotification` 的 `sessionId` / `update` 字段都有值
|
|
2257
|
+
* (vendor 的 `SessionUpdateCallback` 签名即如此),无需再做类型守卫。
|
|
2102
2258
|
*/
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
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.
|
|
2164
|
-
|
|
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
|
-
|
|
2193
|
-
|
|
2194
|
-
const
|
|
2195
|
-
|
|
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/
|
|
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
|
-
*
|
|
2290
|
+
* Session — 对话上下文
|
|
2273
2291
|
*
|
|
2274
|
-
*
|
|
2275
|
-
*
|
|
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
|
-
*
|
|
2279
|
-
*
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
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
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
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
|
-
|
|
2311
|
-
|
|
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
|
-
/**
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
return this.request("POST", url, body, opts);
|
|
2335
|
+
/** ACP 连接是否已建立 */
|
|
2336
|
+
get connected() {
|
|
2337
|
+
return this._acpClient?.state === "OPEN";
|
|
2319
2338
|
}
|
|
2320
|
-
/**
|
|
2321
|
-
async
|
|
2322
|
-
const
|
|
2323
|
-
|
|
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
|
-
/**
|
|
2326
|
-
async
|
|
2327
|
-
const
|
|
2328
|
-
|
|
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
|
-
/**
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
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
|
|
2357
|
+
return this._restClient.post(`/runtimes/${this.runtimeId}/sessions/${this.id}/delete`, void 0, opts);
|
|
2339
2358
|
}
|
|
2340
|
-
/**
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
if (this.
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
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
|
-
/**
|
|
2356
|
-
async
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
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
|
-
/**
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
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
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
}
|
|
2471
|
-
|
|
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
|
-
/**
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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
|
-
*
|
|
2524
|
-
*
|
|
2499
|
+
* 向服务端发 `session/cancel` notification,服务端会以 `stopReason: 'cancelled'`
|
|
2500
|
+
* 结束当前 prompt,对应的 `prompt()` Promise 正常 resolve(不 reject)。
|
|
2501
|
+
*
|
|
2502
|
+
* **尽力而为**:服务端 RPC 失败时打 warn 日志吞掉,不抛给调用方——
|
|
2503
|
+
* 因为"用户点了取消按钮"的 UX 契约就是"本地能返回"。
|
|
2525
2504
|
*/
|
|
2526
|
-
async
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
2515
|
+
* 合并两个 AbortSignal:任一 abort 时返回的 signal 也 abort。
|
|
2575
2516
|
*
|
|
2576
|
-
*
|
|
2517
|
+
* 用原生 `AbortSignal.any()` 不行——Node 18 没有。这里用最小 polyfill。
|
|
2577
2518
|
*/
|
|
2578
|
-
function
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
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
|
|
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
|