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