@blade-hq/agent-client 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +351 -0
- package/dist/auth-login.d.ts +29 -0
- package/dist/auth.d.ts +8 -0
- package/dist/blade-client.d.ts +113 -0
- package/dist/commands/embedded.d.ts +32 -0
- package/dist/commands/protocol.d.ts +38 -0
- package/dist/commands/registry.d.ts +21 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +4182 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/auth.d.ts +34 -0
- package/dist/resources/headless.d.ts +33 -0
- package/dist/resources/sessions.d.ts +263 -0
- package/dist/rest.d.ts +21 -0
- package/dist/schemas/background.d.ts +16 -0
- package/dist/schemas/event.d.ts +51 -0
- package/dist/schemas/message-utils.d.ts +48 -0
- package/dist/schemas/message.d.ts +83 -0
- package/dist/schemas/projection.d.ts +87 -0
- package/dist/schemas/session.d.ts +126 -0
- package/dist/schemas/solution.d.ts +101 -0
- package/dist/schemas/task.d.ts +12 -0
- package/dist/session/agent-session.d.ts +179 -0
- package/dist/session/events.d.ts +143 -0
- package/dist/session/hub.d.ts +44 -0
- package/dist/session/state.d.ts +57 -0
- package/dist/shared/projection/builder.d.ts +59 -0
- package/dist/shared/projection/helpers.d.ts +34 -0
- package/dist/shared/projection/history.d.ts +12 -0
- package/dist/shared/projection/index.d.ts +3 -0
- package/dist/shared/projection/state.d.ts +22 -0
- package/dist/socket.d.ts +9 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/rest.d.ts +17315 -0
- package/dist/types/sdk-profile.d.ts +47 -0
- package/dist/types/socket-events.d.ts +401 -0
- package/dist/version.d.ts +45 -0
- package/package.json +29 -0
- package/public-api.md +1103 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4182 @@
|
|
|
1
|
+
// src/auth-login.ts
|
|
2
|
+
function isAuthCallbackMessage(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && value.type === "blade-agent:sdk-auth";
|
|
4
|
+
}
|
|
5
|
+
function randomState() {
|
|
6
|
+
const bytes = new Uint8Array(24);
|
|
7
|
+
crypto.getRandomValues(bytes);
|
|
8
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
9
|
+
}
|
|
10
|
+
var storageKeyFor = (baseUrl) => `blade-sdk-token:${baseUrl}`;
|
|
11
|
+
function readStoredToken(baseUrl) {
|
|
12
|
+
if (typeof localStorage === "undefined") return null;
|
|
13
|
+
try {
|
|
14
|
+
return localStorage.getItem(storageKeyFor(baseUrl));
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function writeStoredToken(baseUrl, token, mode) {
|
|
20
|
+
if (mode !== "local" || typeof localStorage === "undefined") return;
|
|
21
|
+
try {
|
|
22
|
+
if (token) {
|
|
23
|
+
localStorage.setItem(storageKeyFor(baseUrl), token);
|
|
24
|
+
} else {
|
|
25
|
+
localStorage.removeItem(storageKeyFor(baseUrl));
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function loginWithPopup(client, options = {}) {
|
|
31
|
+
if (typeof window === "undefined") {
|
|
32
|
+
throw new Error("client.auth.login() \u53EA\u80FD\u5728\u6D4F\u89C8\u5668\u4E2D\u8C03\u7528\uFF1BNode.js \u573A\u666F\u8BF7\u76F4\u63A5\u4F20\u5165 token\u3002");
|
|
33
|
+
}
|
|
34
|
+
const rawOrigin = window.location.origin;
|
|
35
|
+
const clientOrigin = /^https?:\/\//.test(rawOrigin) ? rawOrigin : "null";
|
|
36
|
+
const width = options.width ?? 480;
|
|
37
|
+
const height = options.height ?? 640;
|
|
38
|
+
const left = Math.max(0, (window.screen.width - width) / 2);
|
|
39
|
+
const top = Math.max(0, (window.screen.height - height) / 2);
|
|
40
|
+
const state = randomState();
|
|
41
|
+
const authorizeUrl = new URL(
|
|
42
|
+
"/api/auth/sdk/authorize",
|
|
43
|
+
client.options.baseUrl || window.location.origin
|
|
44
|
+
);
|
|
45
|
+
authorizeUrl.searchParams.set("client_origin", clientOrigin);
|
|
46
|
+
authorizeUrl.searchParams.set("state", state);
|
|
47
|
+
const popup = window.open(
|
|
48
|
+
authorizeUrl.toString(),
|
|
49
|
+
"blade-sdk-login",
|
|
50
|
+
`popup=yes,width=${width},height=${height},left=${left},top=${top}`
|
|
51
|
+
);
|
|
52
|
+
if (!popup) {
|
|
53
|
+
throw new Error("\u767B\u5F55\u5F39\u7A97\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\u4E86\u3002\u8BF7\u5141\u8BB8\u672C\u7AD9\u6253\u5F00\u5F39\u7A97\u540E\u91CD\u8BD5\u3002");
|
|
54
|
+
}
|
|
55
|
+
const expectedOrigin = new URL(client.options.baseUrl || window.location.origin).origin;
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
|
|
58
|
+
let settled = false;
|
|
59
|
+
const cleanup = () => {
|
|
60
|
+
window.removeEventListener("message", onMessage);
|
|
61
|
+
globalThis.clearInterval(closeWatcher);
|
|
62
|
+
globalThis.clearTimeout(timeoutHandle);
|
|
63
|
+
};
|
|
64
|
+
const settle = (fn) => {
|
|
65
|
+
if (settled) return;
|
|
66
|
+
settled = true;
|
|
67
|
+
cleanup();
|
|
68
|
+
fn();
|
|
69
|
+
};
|
|
70
|
+
const onMessage = (event) => {
|
|
71
|
+
if (event.origin !== expectedOrigin) return;
|
|
72
|
+
if (!isAuthCallbackMessage(event.data)) return;
|
|
73
|
+
if (event.data.state !== state) return;
|
|
74
|
+
const message = event.data;
|
|
75
|
+
try {
|
|
76
|
+
popup.close();
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
if (message.status === "denied") {
|
|
80
|
+
settle(() => reject(new Error("\u4F60\u53D6\u6D88\u4E86\u6388\u6743\uFF0C\u672A\u5B8C\u6210\u767B\u5F55\u3002")));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (message.status !== "success" || !message.code) {
|
|
84
|
+
settle(() => reject(new Error(message.error || "\u767B\u5F55\u5931\u8D25\uFF0C\u672A\u53D6\u5F97\u6388\u6743\u7801\u3002")));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
client.json("POST", "/api/auth/sdk/token", {
|
|
88
|
+
code: message.code,
|
|
89
|
+
state,
|
|
90
|
+
client_origin: clientOrigin
|
|
91
|
+
}).then((response) => {
|
|
92
|
+
settle(() => resolve({ token: response.access_token, user: response.user }));
|
|
93
|
+
}).catch((error) => {
|
|
94
|
+
settle(
|
|
95
|
+
() => reject(error instanceof Error ? error : new Error("\u6388\u6743\u7801\u6362\u53D6\u4EE4\u724C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002"))
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
const closeWatcher = globalThis.setInterval(() => {
|
|
100
|
+
if (popup.closed && !settled) {
|
|
101
|
+
globalThis.setTimeout(() => {
|
|
102
|
+
settle(() => reject(new Error("\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED\uFF0C\u672A\u5B8C\u6210\u6388\u6743\u3002")));
|
|
103
|
+
}, 400);
|
|
104
|
+
}
|
|
105
|
+
}, 500);
|
|
106
|
+
const timeoutHandle = globalThis.setTimeout(() => {
|
|
107
|
+
settle(() => reject(new Error("\u767B\u5F55\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5\u3002")));
|
|
108
|
+
}, timeoutMs);
|
|
109
|
+
window.addEventListener("message", onMessage);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/rest.ts
|
|
114
|
+
var DOCS_URL = "https://blade-hq.github.io/public-skills/";
|
|
115
|
+
var BladeApiError = class extends Error {
|
|
116
|
+
response;
|
|
117
|
+
status;
|
|
118
|
+
statusText;
|
|
119
|
+
constructor(response, message) {
|
|
120
|
+
super(message ?? `\u8BF7\u6C42\u5931\u8D25\uFF08${response.status} ${response.statusText}\uFF09`);
|
|
121
|
+
this.name = "BladeApiError";
|
|
122
|
+
this.response = response;
|
|
123
|
+
this.status = response.status;
|
|
124
|
+
this.statusText = response.statusText;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
async function extractErrorDetail(response) {
|
|
128
|
+
try {
|
|
129
|
+
const cloned = response.clone();
|
|
130
|
+
const contentType = cloned.headers.get("content-type") ?? "";
|
|
131
|
+
if (contentType.includes("application/json")) {
|
|
132
|
+
const data = await cloned.json();
|
|
133
|
+
const detail = data?.detail;
|
|
134
|
+
if (typeof detail === "string" && detail.trim()) return detail;
|
|
135
|
+
if (Array.isArray(detail) && detail.length > 0) {
|
|
136
|
+
const first = detail[0];
|
|
137
|
+
if (first && typeof first.msg === "string") return first.msg;
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
const text = (await cloned.text()).trim();
|
|
141
|
+
if (text) return text;
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
return void 0;
|
|
146
|
+
}
|
|
147
|
+
function decorateAuthError(error) {
|
|
148
|
+
if (!(error instanceof BladeApiError)) return error;
|
|
149
|
+
if (error.status === 401) {
|
|
150
|
+
error.message = `${error.message}\uFF08\u672A\u767B\u5F55\u6216\u51ED\u8BC1\u5DF2\u5931\u6548\u3002\u6D4F\u89C8\u5668\u573A\u666F\u8C03\u7528 client.auth.login() \u5F39\u7A97\u767B\u5F55\uFF0C\u81EA\u52A8\u5316\u573A\u666F\u5728 new BladeClient({ token }) \u4F20\u5165 PAT\u3002\u63A5\u5165\u6307\u5357\uFF1A${DOCS_URL}\uFF09`;
|
|
151
|
+
} else if (error.status === 403) {
|
|
152
|
+
error.message = `${error.message}\uFF08\u5F53\u524D\u8D26\u53F7\u65E0\u6743\u8BBF\u95EE\u8BE5\u8D44\u6E90\u3002\u63A5\u5165\u6307\u5357\uFF1A${DOCS_URL}\uFF09`;
|
|
153
|
+
}
|
|
154
|
+
return error;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/resources/auth.ts
|
|
158
|
+
var AuthResource = class {
|
|
159
|
+
constructor(client) {
|
|
160
|
+
this.client = client;
|
|
161
|
+
}
|
|
162
|
+
client;
|
|
163
|
+
/**
|
|
164
|
+
* 弹窗授权登录(浏览器专用):弹出后端授权页,用户点击"许可授权"后
|
|
165
|
+
* 自动取得 PAT 并热挂载到当前 client。等价于 client.login()。
|
|
166
|
+
*/
|
|
167
|
+
login(options) {
|
|
168
|
+
return this.client.login(options);
|
|
169
|
+
}
|
|
170
|
+
/** 清除 login() 存储的令牌。 */
|
|
171
|
+
logoutToken() {
|
|
172
|
+
this.client.logoutToken();
|
|
173
|
+
}
|
|
174
|
+
getProviders() {
|
|
175
|
+
return this.client.json("GET", "/api/auth/providers");
|
|
176
|
+
}
|
|
177
|
+
getMe() {
|
|
178
|
+
return this.client.json("GET", "/api/auth/me");
|
|
179
|
+
}
|
|
180
|
+
logout() {
|
|
181
|
+
return this.client.json("POST", "/api/auth/logout");
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// src/resources/headless.ts
|
|
186
|
+
var HeadlessError = class extends Error {
|
|
187
|
+
constructor(message, detail) {
|
|
188
|
+
super(message);
|
|
189
|
+
this.detail = detail;
|
|
190
|
+
this.name = "HeadlessError";
|
|
191
|
+
}
|
|
192
|
+
detail;
|
|
193
|
+
};
|
|
194
|
+
var SYSTEM_ERROR_CHAT_END_GRACE_MS = 1e3;
|
|
195
|
+
var HeadlessResource = class {
|
|
196
|
+
constructor(client) {
|
|
197
|
+
this.client = client;
|
|
198
|
+
}
|
|
199
|
+
client;
|
|
200
|
+
async run(prompt, options = {}) {
|
|
201
|
+
const { session_id } = await this.client.sessions.createSessionWithRequest({
|
|
202
|
+
intent: prompt,
|
|
203
|
+
model: options.model ?? null
|
|
204
|
+
});
|
|
205
|
+
return this.runTracked(session_id, prompt, options, false);
|
|
206
|
+
}
|
|
207
|
+
async runInSession(sessionId, prompt, options = {}) {
|
|
208
|
+
return this.runTracked(sessionId, prompt, options, false);
|
|
209
|
+
}
|
|
210
|
+
async runWithSession(prompt, options = {}) {
|
|
211
|
+
const { session_id } = await this.client.sessions.createSessionWithRequest({
|
|
212
|
+
intent: prompt,
|
|
213
|
+
model: options.model ?? null
|
|
214
|
+
});
|
|
215
|
+
return this.runTracked(session_id, prompt, options, true);
|
|
216
|
+
}
|
|
217
|
+
async runTracked(sessionId, prompt, options, returnEnvelope) {
|
|
218
|
+
const timeoutMs = options.timeoutMs ?? 3e5;
|
|
219
|
+
const session_id = sessionId;
|
|
220
|
+
const socket = this.client.socket();
|
|
221
|
+
await ensureSocketConnected(socket, timeoutMs, { session_id });
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
let settled = false;
|
|
224
|
+
let stopSent = false;
|
|
225
|
+
let started = false;
|
|
226
|
+
let chatRunId = null;
|
|
227
|
+
let systemErrorTimeout = null;
|
|
228
|
+
const timeout = setTimeout(() => {
|
|
229
|
+
fail(new HeadlessError(`headless run timed out after ${timeoutMs}ms`, { session_id, chat_run_id: chatRunId }));
|
|
230
|
+
}, timeoutMs);
|
|
231
|
+
const cleanup = (sendStop) => {
|
|
232
|
+
clearTimeout(timeout);
|
|
233
|
+
if (systemErrorTimeout) {
|
|
234
|
+
clearTimeout(systemErrorTimeout);
|
|
235
|
+
systemErrorTimeout = null;
|
|
236
|
+
}
|
|
237
|
+
socket.off("chat:start", onChatStart);
|
|
238
|
+
socket.off("chat:end", onChatEnd);
|
|
239
|
+
socket.off("system:error", onSystemError);
|
|
240
|
+
if (sendStop && !stopSent) {
|
|
241
|
+
stopSent = true;
|
|
242
|
+
socket.emit("chat:stop", { session_id }, () => void 0);
|
|
243
|
+
}
|
|
244
|
+
socket.emit("session:unsubscribe", { session_id });
|
|
245
|
+
};
|
|
246
|
+
const finish = (value) => {
|
|
247
|
+
if (settled) return;
|
|
248
|
+
settled = true;
|
|
249
|
+
cleanup(false);
|
|
250
|
+
if (returnEnvelope) {
|
|
251
|
+
resolve({ session_id, chat_run_id: chatRunId, result: value });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
resolve(value);
|
|
255
|
+
};
|
|
256
|
+
const fail = (error) => {
|
|
257
|
+
if (settled) return;
|
|
258
|
+
settled = true;
|
|
259
|
+
cleanup(true);
|
|
260
|
+
reject(error);
|
|
261
|
+
};
|
|
262
|
+
const failWithoutStop = (error) => {
|
|
263
|
+
if (settled) return;
|
|
264
|
+
settled = true;
|
|
265
|
+
cleanup(false);
|
|
266
|
+
reject(error);
|
|
267
|
+
};
|
|
268
|
+
const rejectAfterChatEnd = (error) => {
|
|
269
|
+
if (settled) return;
|
|
270
|
+
settled = true;
|
|
271
|
+
cleanup(false);
|
|
272
|
+
reject(error);
|
|
273
|
+
};
|
|
274
|
+
function onChatStart(payload) {
|
|
275
|
+
if (payload.session_id && payload.session_id !== session_id) return;
|
|
276
|
+
started = true;
|
|
277
|
+
}
|
|
278
|
+
function onSystemError(payload) {
|
|
279
|
+
if (payload.session_id !== session_id) return;
|
|
280
|
+
if (!started) {
|
|
281
|
+
failWithoutStop(new HeadlessError(payload.message || "headless run failed", {
|
|
282
|
+
session_id,
|
|
283
|
+
chat_run_id: chatRunId,
|
|
284
|
+
event: payload
|
|
285
|
+
}));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (systemErrorTimeout) {
|
|
289
|
+
clearTimeout(systemErrorTimeout);
|
|
290
|
+
}
|
|
291
|
+
systemErrorTimeout = setTimeout(() => {
|
|
292
|
+
systemErrorTimeout = null;
|
|
293
|
+
fail(new HeadlessError(payload.message || "headless run failed", {
|
|
294
|
+
session_id,
|
|
295
|
+
chat_run_id: chatRunId,
|
|
296
|
+
event: payload
|
|
297
|
+
}));
|
|
298
|
+
}, SYSTEM_ERROR_CHAT_END_GRACE_MS);
|
|
299
|
+
}
|
|
300
|
+
function onChatEnd(payload) {
|
|
301
|
+
if (payload.session_id && payload.session_id !== session_id) return;
|
|
302
|
+
chatRunId = typeof payload.chat_run_id === "string" ? payload.chat_run_id : chatRunId;
|
|
303
|
+
const result = payload.result;
|
|
304
|
+
if (payload.status === "failed") {
|
|
305
|
+
rejectAfterChatEnd(new HeadlessError(errorMessageFromResult(result) ?? "headless run failed", {
|
|
306
|
+
session_id,
|
|
307
|
+
chat_run_id: chatRunId,
|
|
308
|
+
event: payload
|
|
309
|
+
}));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
finish(normalizeResult(result, Boolean(options.schema)));
|
|
314
|
+
} catch (error) {
|
|
315
|
+
rejectAfterChatEnd(error);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
socket.on("chat:start", onChatStart);
|
|
319
|
+
socket.on("chat:end", onChatEnd);
|
|
320
|
+
socket.on("system:error", onSystemError);
|
|
321
|
+
socket.emit("session:subscribe", { session_id, event_format: "raw" });
|
|
322
|
+
socket.emit("chat:send", {
|
|
323
|
+
session_id,
|
|
324
|
+
message: prompt,
|
|
325
|
+
headless: true,
|
|
326
|
+
output_schema: options.schema ?? null,
|
|
327
|
+
model: options.model ?? null
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
function normalizeResult(result, hasSchema) {
|
|
333
|
+
if (!isRecord(result)) {
|
|
334
|
+
throw new HeadlessError("headless run finished without a result", result);
|
|
335
|
+
}
|
|
336
|
+
const errMsg = typeof result.err_msg === "string" ? result.err_msg : "";
|
|
337
|
+
if (errMsg) {
|
|
338
|
+
throw new HeadlessError(errMsg, result);
|
|
339
|
+
}
|
|
340
|
+
if (!hasSchema) {
|
|
341
|
+
const text = result.result;
|
|
342
|
+
return typeof text === "string" ? text : String(text ?? "");
|
|
343
|
+
}
|
|
344
|
+
const { err_msg: _errMsg, ...businessResult } = result;
|
|
345
|
+
return businessResult;
|
|
346
|
+
}
|
|
347
|
+
function errorMessageFromResult(result) {
|
|
348
|
+
if (isRecord(result) && typeof result.err_msg === "string" && result.err_msg) {
|
|
349
|
+
return result.err_msg;
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
function isRecord(value) {
|
|
354
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
355
|
+
}
|
|
356
|
+
function ensureSocketConnected(socket, timeoutMs, detail) {
|
|
357
|
+
if (socket.connected) {
|
|
358
|
+
return Promise.resolve();
|
|
359
|
+
}
|
|
360
|
+
return new Promise((resolve, reject) => {
|
|
361
|
+
let settled = false;
|
|
362
|
+
const timeout = setTimeout(() => {
|
|
363
|
+
finish(() => reject(new HeadlessError(`socket connection timed out after ${timeoutMs}ms`, detail)));
|
|
364
|
+
}, timeoutMs);
|
|
365
|
+
const onConnect = () => finish(resolve);
|
|
366
|
+
const onConnectError = (error) => {
|
|
367
|
+
finish(() => reject(new HeadlessError("socket connection failed", { ...detail, event: error })));
|
|
368
|
+
};
|
|
369
|
+
const finish = (callback) => {
|
|
370
|
+
if (settled) return;
|
|
371
|
+
settled = true;
|
|
372
|
+
clearTimeout(timeout);
|
|
373
|
+
socket.off("connect", onConnect);
|
|
374
|
+
socket.off("connect_error", onConnectError);
|
|
375
|
+
callback();
|
|
376
|
+
};
|
|
377
|
+
socket.on("connect", onConnect);
|
|
378
|
+
socket.on("connect_error", onConnectError);
|
|
379
|
+
socket.connect();
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/shared/projection/helpers.ts
|
|
384
|
+
function upsertToolCall(state, toolCallId, toolName, displayName, args) {
|
|
385
|
+
if (!toolCallId) return;
|
|
386
|
+
let found = false;
|
|
387
|
+
for (let i = 0; i < state.toolCalls.length; i++) {
|
|
388
|
+
if (state.toolCalls[i].id !== toolCallId) continue;
|
|
389
|
+
const tc = state.toolCalls[i];
|
|
390
|
+
state.toolCalls[i] = {
|
|
391
|
+
...tc,
|
|
392
|
+
tool_name: toolName || tc.tool_name,
|
|
393
|
+
display_name: displayName || tc.display_name,
|
|
394
|
+
arguments: args || tc.arguments
|
|
395
|
+
};
|
|
396
|
+
found = true;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
if (!found) {
|
|
400
|
+
state.toolCalls.push({
|
|
401
|
+
id: toolCallId,
|
|
402
|
+
tool_name: toolName,
|
|
403
|
+
display_name: displayName,
|
|
404
|
+
arguments: args,
|
|
405
|
+
status: "pending"
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
let blockFound = false;
|
|
409
|
+
for (let i = 0; i < state.blocks.length; i++) {
|
|
410
|
+
const b = state.blocks[i];
|
|
411
|
+
if (b.type !== "tool_use" || b.tool_call_id !== toolCallId) continue;
|
|
412
|
+
state.blocks[i] = {
|
|
413
|
+
...b,
|
|
414
|
+
content: args || b.content,
|
|
415
|
+
tool_name: toolName || b.tool_name,
|
|
416
|
+
display_name: displayName || b.display_name
|
|
417
|
+
};
|
|
418
|
+
blockFound = true;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
if (!blockFound) {
|
|
422
|
+
state.blocks.push({
|
|
423
|
+
type: "tool_use",
|
|
424
|
+
content: args,
|
|
425
|
+
tool_call_id: toolCallId,
|
|
426
|
+
tool_name: toolName || null,
|
|
427
|
+
display_name: displayName || null
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function applyToolResult(state, toolCallId, result, status, durationMs, sourceLoop) {
|
|
432
|
+
let toolName = "";
|
|
433
|
+
let displayName = "";
|
|
434
|
+
let finalResult = result;
|
|
435
|
+
for (let i = 0; i < state.toolCalls.length; i++) {
|
|
436
|
+
const tc = state.toolCalls[i];
|
|
437
|
+
if (tc.id !== toolCallId) continue;
|
|
438
|
+
if (typeof result === "string") {
|
|
439
|
+
finalResult = maybeEnrichAskUserResult(tc.tool_name, result, sourceLoop);
|
|
440
|
+
}
|
|
441
|
+
const update = { status, result: finalResult };
|
|
442
|
+
if (durationMs != null) update.duration_ms = durationMs;
|
|
443
|
+
state.toolCalls[i] = { ...tc, ...update };
|
|
444
|
+
toolName = tc.tool_name;
|
|
445
|
+
displayName = tc.display_name;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
upsertToolResultBlock(state, toolCallId, finalResult, toolName, displayName);
|
|
449
|
+
}
|
|
450
|
+
function appendToolResultDelta(state, toolCallId, delta) {
|
|
451
|
+
if (!toolCallId || !delta) return;
|
|
452
|
+
let toolName = "";
|
|
453
|
+
let displayName = "";
|
|
454
|
+
let currentResult = "";
|
|
455
|
+
for (let i = 0; i < state.toolCalls.length; i++) {
|
|
456
|
+
const tc = state.toolCalls[i];
|
|
457
|
+
if (tc.id !== toolCallId) continue;
|
|
458
|
+
toolName = tc.tool_name;
|
|
459
|
+
displayName = tc.display_name;
|
|
460
|
+
if (typeof tc.result === "string") currentResult = tc.result;
|
|
461
|
+
state.toolCalls[i] = { ...tc, result: currentResult + delta };
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
for (const block of state.blocks) {
|
|
465
|
+
if (block.type !== "tool_result" || block.tool_call_id !== toolCallId) continue;
|
|
466
|
+
if (typeof block.content === "string") currentResult = block.content;
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
upsertToolResultBlock(state, toolCallId, currentResult + delta, toolName, displayName);
|
|
470
|
+
}
|
|
471
|
+
function upsertToolResultBlock(state, toolCallId, content, toolName, displayName) {
|
|
472
|
+
for (let i = 0; i < state.blocks.length; i++) {
|
|
473
|
+
const block = state.blocks[i];
|
|
474
|
+
if (block.type !== "tool_result" || block.tool_call_id !== toolCallId) continue;
|
|
475
|
+
state.blocks[i] = {
|
|
476
|
+
...block,
|
|
477
|
+
content,
|
|
478
|
+
tool_name: toolName || block.tool_name,
|
|
479
|
+
display_name: displayName || block.display_name
|
|
480
|
+
};
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
state.blocks.push({
|
|
484
|
+
type: "tool_result",
|
|
485
|
+
content,
|
|
486
|
+
tool_call_id: toolCallId,
|
|
487
|
+
tool_name: toolName || null,
|
|
488
|
+
display_name: displayName || null
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
function appendTextBlock(state, blockType, content) {
|
|
492
|
+
if (content == null || content === "") return;
|
|
493
|
+
const text = String(content);
|
|
494
|
+
const lastBlock = state.blocks[state.blocks.length - 1];
|
|
495
|
+
if (lastBlock && lastBlock.type === blockType && typeof lastBlock.content === "string") {
|
|
496
|
+
state.blocks[state.blocks.length - 1] = {
|
|
497
|
+
...lastBlock,
|
|
498
|
+
content: lastBlock.content + text
|
|
499
|
+
};
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
state.blocks.push({ type: blockType, content: text });
|
|
503
|
+
}
|
|
504
|
+
function appendToolCallArguments(state, toolCallId, delta) {
|
|
505
|
+
let newArgs = "";
|
|
506
|
+
for (let i = 0; i < state.toolCalls.length; i++) {
|
|
507
|
+
const tc = state.toolCalls[i];
|
|
508
|
+
if (tc.id === toolCallId) {
|
|
509
|
+
newArgs = tc.arguments + delta;
|
|
510
|
+
state.toolCalls[i] = { ...tc, arguments: newArgs };
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (!newArgs) return;
|
|
515
|
+
for (let i = 0; i < state.blocks.length; i++) {
|
|
516
|
+
const b = state.blocks[i];
|
|
517
|
+
if (b.type === "tool_use" && b.tool_call_id === toolCallId) {
|
|
518
|
+
state.blocks[i] = { ...b, content: newArgs };
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function maybeEnrichAskUserResult(toolName, result, sourceLoop) {
|
|
524
|
+
if (!sourceLoop) return result;
|
|
525
|
+
const normalized = toolName.includes(":") ? toolName.split(":").pop() : toolName;
|
|
526
|
+
if (normalized !== "AskUserQuestion") return result;
|
|
527
|
+
try {
|
|
528
|
+
const payload = JSON.parse(result);
|
|
529
|
+
if (typeof payload !== "object" || payload === null) return result;
|
|
530
|
+
payload.source_loop = sourceLoop;
|
|
531
|
+
return JSON.stringify(payload);
|
|
532
|
+
} catch {
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function buildSystemNotificationTurn(sequence, turnId, loopId, notificationType, status, title, detail, metadata) {
|
|
537
|
+
return {
|
|
538
|
+
id: turnId,
|
|
539
|
+
sequence,
|
|
540
|
+
turn_id: turnId,
|
|
541
|
+
loop_id: loopId,
|
|
542
|
+
kind: "message",
|
|
543
|
+
role: "system",
|
|
544
|
+
status: "completed",
|
|
545
|
+
blocks: [
|
|
546
|
+
{
|
|
547
|
+
type: "system_notification",
|
|
548
|
+
content: {
|
|
549
|
+
notification_type: notificationType,
|
|
550
|
+
status,
|
|
551
|
+
title,
|
|
552
|
+
detail,
|
|
553
|
+
metadata
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
],
|
|
557
|
+
tool_calls: [],
|
|
558
|
+
model: null,
|
|
559
|
+
usage: null,
|
|
560
|
+
duration_ms: 0
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function buildSystemNotificationBlock(notificationType, status, title, detail, metadata) {
|
|
564
|
+
return {
|
|
565
|
+
type: "system_notification",
|
|
566
|
+
content: {
|
|
567
|
+
notification_type: notificationType,
|
|
568
|
+
status,
|
|
569
|
+
title,
|
|
570
|
+
detail,
|
|
571
|
+
metadata
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function buildMarkerTurn(sequence, turnId, loopId, blockType, content) {
|
|
576
|
+
return {
|
|
577
|
+
id: turnId,
|
|
578
|
+
sequence,
|
|
579
|
+
turn_id: turnId,
|
|
580
|
+
loop_id: loopId,
|
|
581
|
+
kind: "message",
|
|
582
|
+
role: "assistant",
|
|
583
|
+
status: "completed",
|
|
584
|
+
blocks: [{ type: blockType, content }],
|
|
585
|
+
tool_calls: [],
|
|
586
|
+
model: null,
|
|
587
|
+
usage: null,
|
|
588
|
+
duration_ms: 0
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function buildCompactionTurn(sequence, compactionId, loopId, status, data) {
|
|
592
|
+
const tokensBefore = asInt(data.tokens_before);
|
|
593
|
+
const tokensAfter = asInt(data.tokens_after);
|
|
594
|
+
let savedRatio = null;
|
|
595
|
+
if (tokensBefore && tokensBefore > 0 && tokensAfter != null) {
|
|
596
|
+
savedRatio = 1 - tokensAfter / tokensBefore;
|
|
597
|
+
} else {
|
|
598
|
+
savedRatio = asFloat(data.saved_ratio);
|
|
599
|
+
}
|
|
600
|
+
let archivedCount = asInt(data.archived_count);
|
|
601
|
+
if (archivedCount == null && Array.isArray(data.archived_files)) {
|
|
602
|
+
archivedCount = data.archived_files.length;
|
|
603
|
+
}
|
|
604
|
+
const content = data.content;
|
|
605
|
+
const summaryFull = compactionSummaryFull(content);
|
|
606
|
+
const summaryPreview = summaryFull ? summaryFull.split("\n").map((l) => l.trim()).filter(Boolean).join(" ").slice(0, 200) : null;
|
|
607
|
+
return {
|
|
608
|
+
id: `compaction:${compactionId}`,
|
|
609
|
+
sequence,
|
|
610
|
+
turn_id: `compaction:${compactionId}`,
|
|
611
|
+
loop_id: loopId,
|
|
612
|
+
kind: "compaction",
|
|
613
|
+
role: "system",
|
|
614
|
+
status,
|
|
615
|
+
blocks: [],
|
|
616
|
+
tool_calls: [],
|
|
617
|
+
model: null,
|
|
618
|
+
usage: null,
|
|
619
|
+
duration_ms: 0,
|
|
620
|
+
compaction_id: compactionId,
|
|
621
|
+
summary_preview: summaryPreview,
|
|
622
|
+
summary_full: summaryFull,
|
|
623
|
+
archived_count: archivedCount,
|
|
624
|
+
tokens_before: tokensBefore,
|
|
625
|
+
tokens_after: tokensAfter,
|
|
626
|
+
saved_ratio: savedRatio,
|
|
627
|
+
trigger: asTrigger(data.trigger),
|
|
628
|
+
failure_reason: typeof data.reason === "string" && data.reason ? data.reason : null,
|
|
629
|
+
fallback_applied: typeof data.fallback_applied === "boolean" ? data.fallback_applied : null
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function compactionSummaryFull(content) {
|
|
633
|
+
const text = String(content ?? "");
|
|
634
|
+
if (!text) return null;
|
|
635
|
+
const cleaned = text.replace("<compaction-summary>", "").replace("</compaction-summary>", "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
636
|
+
return cleaned || null;
|
|
637
|
+
}
|
|
638
|
+
function asInt(value) {
|
|
639
|
+
if (typeof value === "boolean") return null;
|
|
640
|
+
if (typeof value === "number") return Math.trunc(value);
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
function asFloat(value) {
|
|
644
|
+
if (typeof value === "boolean") return null;
|
|
645
|
+
if (typeof value === "number") return value;
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
function asTrigger(value) {
|
|
649
|
+
if (value === "auto" || value === "manual" || value === "forced_retry") return value;
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
function setToolCallStatus(turn, toolCallId, status, result, pendingQuestionRef) {
|
|
653
|
+
if (!turn || !toolCallId) return;
|
|
654
|
+
const toolCalls = "toolCalls" in turn ? turn.toolCalls : turn.tool_calls;
|
|
655
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
656
|
+
if (toolCalls[i].id !== toolCallId) continue;
|
|
657
|
+
toolCalls[i] = {
|
|
658
|
+
...toolCalls[i],
|
|
659
|
+
status,
|
|
660
|
+
result,
|
|
661
|
+
pending_question_ref: pendingQuestionRef
|
|
662
|
+
};
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function applyAskUserPauseToTurn(turn, pauseToolData, toolCallId, childLoopName, description, displayNameResolver) {
|
|
667
|
+
if (!turn || !toolCallId) return;
|
|
668
|
+
const sourceLoop = childLoopName ? { loop_name: childLoopName, description } : null;
|
|
669
|
+
const toolName = String(pauseToolData.name ?? "AskUserQuestion");
|
|
670
|
+
const args = JSON.stringify(pauseToolData.arguments ?? {});
|
|
671
|
+
const result = askUserResultFromPausePayload(pauseToolData, sourceLoop);
|
|
672
|
+
const isTurnState = "toolCalls" in turn;
|
|
673
|
+
const toolCalls = isTurnState ? turn.toolCalls : turn.tool_calls;
|
|
674
|
+
const blocks = turn.blocks;
|
|
675
|
+
const displayName = displayNameResolver(toolName);
|
|
676
|
+
let found = false;
|
|
677
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
678
|
+
if (toolCalls[i].id !== toolCallId) continue;
|
|
679
|
+
toolCalls[i] = {
|
|
680
|
+
...toolCalls[i],
|
|
681
|
+
tool_name: toolName || toolCalls[i].tool_name,
|
|
682
|
+
display_name: displayName || toolCalls[i].display_name,
|
|
683
|
+
arguments: args || toolCalls[i].arguments
|
|
684
|
+
};
|
|
685
|
+
found = true;
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
if (!found) {
|
|
689
|
+
toolCalls.push({
|
|
690
|
+
id: toolCallId,
|
|
691
|
+
tool_name: toolName,
|
|
692
|
+
display_name: displayName,
|
|
693
|
+
arguments: args,
|
|
694
|
+
status: "pending"
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
let blockFound = false;
|
|
698
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
699
|
+
if (blocks[i].type !== "tool_use" || blocks[i].tool_call_id !== toolCallId) continue;
|
|
700
|
+
blocks[i] = { ...blocks[i], content: args, tool_name: toolName, display_name: displayName };
|
|
701
|
+
blockFound = true;
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
if (!blockFound) {
|
|
705
|
+
blocks.push({
|
|
706
|
+
type: "tool_use",
|
|
707
|
+
content: args,
|
|
708
|
+
tool_call_id: toolCallId,
|
|
709
|
+
tool_name: toolName,
|
|
710
|
+
display_name: displayName
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
setToolCallStatus(turn, toolCallId, "awaiting_answer", result, null);
|
|
714
|
+
let resultBlockFound = false;
|
|
715
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
716
|
+
if (blocks[i].type !== "tool_result" || blocks[i].tool_call_id !== toolCallId) continue;
|
|
717
|
+
blocks[i] = { ...blocks[i], content: result };
|
|
718
|
+
resultBlockFound = true;
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
if (!resultBlockFound) {
|
|
722
|
+
blocks.push({
|
|
723
|
+
type: "tool_result",
|
|
724
|
+
content: result,
|
|
725
|
+
tool_call_id: toolCallId,
|
|
726
|
+
tool_name: toolName,
|
|
727
|
+
display_name: displayName
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function applyChildPauseToParent(parentTurn, parentToolCallId, childLoopName, childToolCallId, description, displayNameResolver) {
|
|
732
|
+
if (!parentTurn || !parentToolCallId) return;
|
|
733
|
+
const toolCalls = "toolCalls" in parentTurn ? parentTurn.toolCalls : parentTurn.tool_calls;
|
|
734
|
+
const blocks = parentTurn.blocks;
|
|
735
|
+
const displayName = displayNameResolver("Agent");
|
|
736
|
+
let found = false;
|
|
737
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
738
|
+
if (toolCalls[i].id !== parentToolCallId) continue;
|
|
739
|
+
toolCalls[i] = {
|
|
740
|
+
...toolCalls[i],
|
|
741
|
+
tool_name: "Agent",
|
|
742
|
+
display_name: displayName || toolCalls[i].display_name
|
|
743
|
+
};
|
|
744
|
+
found = true;
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
if (!found) {
|
|
748
|
+
toolCalls.push({
|
|
749
|
+
id: parentToolCallId,
|
|
750
|
+
tool_name: "Agent",
|
|
751
|
+
display_name: displayName,
|
|
752
|
+
arguments: "",
|
|
753
|
+
status: "pending"
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
let blockFound = false;
|
|
757
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
758
|
+
if (blocks[i].type !== "tool_use" || blocks[i].tool_call_id !== parentToolCallId) continue;
|
|
759
|
+
blockFound = true;
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
if (!blockFound) {
|
|
763
|
+
blocks.push({
|
|
764
|
+
type: "tool_use",
|
|
765
|
+
content: "",
|
|
766
|
+
tool_call_id: parentToolCallId,
|
|
767
|
+
tool_name: "Agent",
|
|
768
|
+
display_name: displayName
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
setToolCallStatus(parentTurn, parentToolCallId, "awaiting_answer", null, {
|
|
772
|
+
child_loop_name: childLoopName,
|
|
773
|
+
child_tool_call_id: childToolCallId,
|
|
774
|
+
description: description || null
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
function askUserResultFromPausePayload(pauseToolData, sourceLoop) {
|
|
778
|
+
const args = typeof pauseToolData.arguments === "object" && pauseToolData.arguments !== null ? pauseToolData.arguments : {};
|
|
779
|
+
const payload = { questions: args.questions ?? [] };
|
|
780
|
+
if (sourceLoop) payload.source_loop = sourceLoop;
|
|
781
|
+
return JSON.stringify(payload);
|
|
782
|
+
}
|
|
783
|
+
function sourceLoopFor(loopId, loopDescriptions) {
|
|
784
|
+
if (loopId === "root") return null;
|
|
785
|
+
const description = loopDescriptions.get(loopId);
|
|
786
|
+
if (!description) return null;
|
|
787
|
+
return { loop_name: loopId, description };
|
|
788
|
+
}
|
|
789
|
+
function normalizeChildPause(pausePayload) {
|
|
790
|
+
const rawPtd = pausePayload.pause_tool_data;
|
|
791
|
+
const pauseToolData = typeof rawPtd === "object" && rawPtd !== null ? { ...rawPtd } : { ...pausePayload };
|
|
792
|
+
let sourceLoop = pauseToolData.source_loop;
|
|
793
|
+
if (typeof sourceLoop !== "object" || sourceLoop === null) {
|
|
794
|
+
sourceLoop = pausePayload.source_loop;
|
|
795
|
+
}
|
|
796
|
+
const childLoopName = String(pausePayload.child_loop_name ?? "") || (typeof sourceLoop === "object" && sourceLoop ? String(sourceLoop.name ?? "") : "");
|
|
797
|
+
const childToolCallId = String(
|
|
798
|
+
pausePayload.child_pause_tool_call_id ?? pauseToolData.tool_call_id ?? ""
|
|
799
|
+
);
|
|
800
|
+
const description = typeof sourceLoop === "object" && sourceLoop ? String(sourceLoop.description ?? "") : "";
|
|
801
|
+
if (childLoopName && description) {
|
|
802
|
+
pauseToolData.source_loop = { name: childLoopName, description };
|
|
803
|
+
}
|
|
804
|
+
return { pauseToolData, childLoopName, childToolCallId, description };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/schemas/message.ts
|
|
808
|
+
function inferToolStatus(resultStr) {
|
|
809
|
+
if (resultStr.includes('"Cancelled by user"')) return "cancelled";
|
|
810
|
+
try {
|
|
811
|
+
const parsed = JSON.parse(resultStr);
|
|
812
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
813
|
+
if ("error" in parsed && parsed.error) return "error";
|
|
814
|
+
const nested = parsed.result;
|
|
815
|
+
if (typeof nested === "object" && nested !== null && "error" in nested && nested.error) {
|
|
816
|
+
return "error";
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
return "done";
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/shared/projection/state.ts
|
|
825
|
+
function createTurnState(turnId, loopId, opts = {}) {
|
|
826
|
+
return {
|
|
827
|
+
turnId,
|
|
828
|
+
loopId,
|
|
829
|
+
role: "assistant",
|
|
830
|
+
model: opts.model ?? null,
|
|
831
|
+
startedAt: Date.now(),
|
|
832
|
+
contextWindow: opts.contextWindow ?? 0,
|
|
833
|
+
blocks: [],
|
|
834
|
+
toolCalls: [],
|
|
835
|
+
usage: null,
|
|
836
|
+
memoryRefs: opts.memoryRefs ?? null,
|
|
837
|
+
parentForkToolCallId: null
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function snapshot(state, sequence, status) {
|
|
841
|
+
return {
|
|
842
|
+
id: state.turnId,
|
|
843
|
+
sequence,
|
|
844
|
+
turn_id: state.turnId,
|
|
845
|
+
loop_id: state.loopId,
|
|
846
|
+
kind: "message",
|
|
847
|
+
role: state.role,
|
|
848
|
+
status,
|
|
849
|
+
blocks: [...state.blocks],
|
|
850
|
+
tool_calls: [...state.toolCalls],
|
|
851
|
+
model: state.model,
|
|
852
|
+
usage: state.usage ? { ...state.usage } : null,
|
|
853
|
+
duration_ms: Math.max(0, Date.now() - state.startedAt),
|
|
854
|
+
started_at: new Date(state.startedAt).toISOString(),
|
|
855
|
+
context_window: state.contextWindow,
|
|
856
|
+
memory_refs: state.memoryRefs,
|
|
857
|
+
parent_fork_tool_call_id: state.parentForkToolCallId
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
var PAUSE_TOOL_NAMES = /* @__PURE__ */ new Set(["AskUserQuestion", "ExitPlanMode"]);
|
|
861
|
+
function toolStatusFromResult(result) {
|
|
862
|
+
const text = typeof result === "string" ? result : Array.isArray(result) ? result.filter(
|
|
863
|
+
(p) => typeof p === "object" && p !== null && p.type === "text"
|
|
864
|
+
).map((p) => p.text ?? "").join("\n") : String(result ?? "");
|
|
865
|
+
return inferToolStatus(text);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/shared/projection/builder.ts
|
|
869
|
+
var ClientProjectionBuilder = class {
|
|
870
|
+
activeTurns = /* @__PURE__ */ new Map();
|
|
871
|
+
lastTurnIds = /* @__PURE__ */ new Map();
|
|
872
|
+
latestTurns = /* @__PURE__ */ new Map();
|
|
873
|
+
loopDescriptions = /* @__PURE__ */ new Map();
|
|
874
|
+
pendingChildPauses = /* @__PURE__ */ new Map();
|
|
875
|
+
pendingMemoryRefs = /* @__PURE__ */ new Map();
|
|
876
|
+
activeCompactions = /* @__PURE__ */ new Map();
|
|
877
|
+
seq = 0;
|
|
878
|
+
syntheticCounter = 0;
|
|
879
|
+
displayNameResolver;
|
|
880
|
+
constructor(displayNameResolver) {
|
|
881
|
+
this.displayNameResolver = displayNameResolver ?? ((n) => n);
|
|
882
|
+
}
|
|
883
|
+
processBatch(events) {
|
|
884
|
+
const lastUpsertByTurn = /* @__PURE__ */ new Map();
|
|
885
|
+
const nonUpserts = [];
|
|
886
|
+
for (const event of events) {
|
|
887
|
+
const updates = this.processEvent(event);
|
|
888
|
+
if (!updates) continue;
|
|
889
|
+
for (const u of updates) {
|
|
890
|
+
if (u.kind === "upsert") {
|
|
891
|
+
lastUpsertByTurn.set(u.turn.turn_id, u);
|
|
892
|
+
} else {
|
|
893
|
+
nonUpserts.push(u);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return [...nonUpserts, ...lastUpsertByTurn.values()];
|
|
898
|
+
}
|
|
899
|
+
processEvent(event) {
|
|
900
|
+
const payload = event.payload;
|
|
901
|
+
const loopId = String(payload.loop_name ?? "root");
|
|
902
|
+
switch (event.type) {
|
|
903
|
+
case "memory:inject:done":
|
|
904
|
+
return this.onMemoryInject(loopId, payload);
|
|
905
|
+
case "memory:inject:none":
|
|
906
|
+
return null;
|
|
907
|
+
case "loop:turn":
|
|
908
|
+
return this.onLoopTurn(loopId, payload);
|
|
909
|
+
case "mode:change":
|
|
910
|
+
return this.onModeChange(loopId, payload);
|
|
911
|
+
case "plan:status:update":
|
|
912
|
+
return this.onPlanStatusUpdate(loopId, payload);
|
|
913
|
+
case "workspace:changed":
|
|
914
|
+
return this.onWorkspaceChanged(loopId, payload);
|
|
915
|
+
case "agent:start":
|
|
916
|
+
return this.onAgentStart(loopId, payload);
|
|
917
|
+
case "bg:started":
|
|
918
|
+
return this.onBgStarted(loopId, payload);
|
|
919
|
+
case "bg:tasks_completed":
|
|
920
|
+
return this.onBgTasksCompleted(loopId, payload);
|
|
921
|
+
case "loop:ask_user_answer":
|
|
922
|
+
return this.onAskUserAnswer(loopId, payload);
|
|
923
|
+
case "user:message":
|
|
924
|
+
return this.onUserMessage(loopId, payload);
|
|
925
|
+
}
|
|
926
|
+
if (event.type.startsWith("compaction:"))
|
|
927
|
+
return this.onCompaction(event.type, loopId, payload);
|
|
928
|
+
const state = this.activeTurns.get(loopId);
|
|
929
|
+
if (!state) {
|
|
930
|
+
if (event.type === "chat:end") {
|
|
931
|
+
this.pendingMemoryRefs.delete(loopId);
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
if (event.type === "agent:end") return this.onAgentEnd(null, loopId, payload);
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
switch (event.type) {
|
|
938
|
+
case "llm:thinking:delta":
|
|
939
|
+
return this.onDelta(state, "thinking", "", String(payload.content ?? ""));
|
|
940
|
+
case "llm:content:delta":
|
|
941
|
+
return this.onDelta(state, "text", "", String(payload.content ?? ""));
|
|
942
|
+
case "llm:tool_call:created":
|
|
943
|
+
return this.onToolCallCreated(state, payload);
|
|
944
|
+
case "llm:response:done":
|
|
945
|
+
return this.onResponseDone(state, loopId, payload);
|
|
946
|
+
case "tool:result:delta":
|
|
947
|
+
return this.onToolResultDelta(state, payload);
|
|
948
|
+
case "tool:result:done":
|
|
949
|
+
return this.onToolResultDone(state, loopId, payload);
|
|
950
|
+
case "loop:tool_ui":
|
|
951
|
+
return this.onToolUi(state, payload);
|
|
952
|
+
case "loop:tool_bridge":
|
|
953
|
+
return this.onToolBridge(state, payload);
|
|
954
|
+
case "tool:approval:required":
|
|
955
|
+
return this.onToolApprovalRequired(state, payload);
|
|
956
|
+
case "loop:child_pause":
|
|
957
|
+
return this.onChildPause(state, loopId, payload);
|
|
958
|
+
case "chat:end":
|
|
959
|
+
return this.onChatEnd(state, loopId, payload);
|
|
960
|
+
case "agent:end":
|
|
961
|
+
return this.onAgentEnd(state, loopId, payload);
|
|
962
|
+
default:
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
reset() {
|
|
967
|
+
this.activeTurns.clear();
|
|
968
|
+
this.lastTurnIds.clear();
|
|
969
|
+
this.latestTurns.clear();
|
|
970
|
+
this.loopDescriptions.clear();
|
|
971
|
+
this.pendingChildPauses.clear();
|
|
972
|
+
this.pendingMemoryRefs.clear();
|
|
973
|
+
this.activeCompactions.clear();
|
|
974
|
+
this.seq = 0;
|
|
975
|
+
this.syntheticCounter = 0;
|
|
976
|
+
}
|
|
977
|
+
seedSequence(maxSeq) {
|
|
978
|
+
if (maxSeq > this.seq) {
|
|
979
|
+
this.seq = maxSeq;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
// --- Event handlers ---
|
|
983
|
+
onMemoryInject(loopId, payload) {
|
|
984
|
+
const refs = payload.memory_refs;
|
|
985
|
+
const turnId = String(payload.turn_id ?? "");
|
|
986
|
+
if (Array.isArray(refs) && refs.length > 0) {
|
|
987
|
+
this.pendingMemoryRefs.set(loopId, {
|
|
988
|
+
turnId,
|
|
989
|
+
refs: refs.map((r) => ({
|
|
990
|
+
id: Number(r.id),
|
|
991
|
+
content_preview: String(r.content_preview ?? ""),
|
|
992
|
+
skill_name: r.skill_name != null ? String(r.skill_name) : null
|
|
993
|
+
}))
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
onLoopTurn(loopId, payload) {
|
|
999
|
+
let turnId = String(payload.turn_id ?? this.nextTurnId(`turn:${loopId}`));
|
|
1000
|
+
let memoryRefs = null;
|
|
1001
|
+
const pending = this.pendingMemoryRefs.get(loopId);
|
|
1002
|
+
if (pending) {
|
|
1003
|
+
this.pendingMemoryRefs.delete(loopId);
|
|
1004
|
+
if (pending.turnId) turnId = pending.turnId;
|
|
1005
|
+
memoryRefs = pending.refs;
|
|
1006
|
+
}
|
|
1007
|
+
const rawCw = payload.context_window;
|
|
1008
|
+
const contextWindow = typeof rawCw === "number" && rawCw > 0 ? Math.trunc(rawCw) : 0;
|
|
1009
|
+
const state = createTurnState(turnId, loopId, {
|
|
1010
|
+
model: optStr(payload.model),
|
|
1011
|
+
contextWindow,
|
|
1012
|
+
memoryRefs
|
|
1013
|
+
});
|
|
1014
|
+
this.activeTurns.set(loopId, state);
|
|
1015
|
+
this.lastTurnIds.set(loopId, turnId);
|
|
1016
|
+
const turn = snapshot(state, this.nextSeq(), "streaming");
|
|
1017
|
+
this.latestTurns.set(loopId, turn);
|
|
1018
|
+
return [{ kind: "upsert", turn }];
|
|
1019
|
+
}
|
|
1020
|
+
onModeChange(loopId, payload) {
|
|
1021
|
+
const content = {};
|
|
1022
|
+
if (payload.from != null && payload.from !== "") content.from = String(payload.from);
|
|
1023
|
+
if (payload.to != null && payload.to !== "") content.to = String(payload.to);
|
|
1024
|
+
const turn = buildMarkerTurn(
|
|
1025
|
+
this.nextSeq(),
|
|
1026
|
+
this.nextTurnId("mode_change"),
|
|
1027
|
+
loopId,
|
|
1028
|
+
"mode_change",
|
|
1029
|
+
content
|
|
1030
|
+
);
|
|
1031
|
+
return [{ kind: "upsert", turn }];
|
|
1032
|
+
}
|
|
1033
|
+
onPlanStatusUpdate(loopId, payload) {
|
|
1034
|
+
let statuses = payload.statuses;
|
|
1035
|
+
if (typeof statuses !== "object" || statuses === null) {
|
|
1036
|
+
statuses = payload._statuses ?? {};
|
|
1037
|
+
}
|
|
1038
|
+
const turn = buildMarkerTurn(
|
|
1039
|
+
this.nextSeq(),
|
|
1040
|
+
this.nextTurnId("plan_status"),
|
|
1041
|
+
loopId,
|
|
1042
|
+
"plan_status",
|
|
1043
|
+
{ plan_yaml: payload.plan_yaml ?? "", statuses }
|
|
1044
|
+
);
|
|
1045
|
+
return [{ kind: "upsert", turn }];
|
|
1046
|
+
}
|
|
1047
|
+
onWorkspaceChanged(loopId, payload) {
|
|
1048
|
+
const updates = [
|
|
1049
|
+
{
|
|
1050
|
+
kind: "workspace_changed",
|
|
1051
|
+
loopId,
|
|
1052
|
+
toolCallId: optStr(payload.tool_call_id) ?? void 0,
|
|
1053
|
+
toolName: toolNameFromPayload(payload) ?? void 0
|
|
1054
|
+
}
|
|
1055
|
+
];
|
|
1056
|
+
const state = this.activeTurns.get(loopId);
|
|
1057
|
+
if (state) {
|
|
1058
|
+
const turn = snapshot(state, this.nextSeq(), "streaming");
|
|
1059
|
+
this.latestTurns.set(loopId, turn);
|
|
1060
|
+
this.lastTurnIds.set(loopId, turn.turn_id);
|
|
1061
|
+
updates.push({ kind: "upsert", turn });
|
|
1062
|
+
}
|
|
1063
|
+
return updates;
|
|
1064
|
+
}
|
|
1065
|
+
onAgentStart(loopId, payload) {
|
|
1066
|
+
const description = optStr(payload.description);
|
|
1067
|
+
if (description) this.loopDescriptions.set(loopId, description);
|
|
1068
|
+
const turn = buildSystemNotificationTurn(
|
|
1069
|
+
this.nextSeq(),
|
|
1070
|
+
this.nextTurnId("agent:start"),
|
|
1071
|
+
loopId,
|
|
1072
|
+
"agent:start",
|
|
1073
|
+
"running",
|
|
1074
|
+
`Agent: ${payload.description ?? payload.skill_id ?? "agent"}`,
|
|
1075
|
+
optStr(payload.prompt),
|
|
1076
|
+
{
|
|
1077
|
+
skill_id: optStr(payload.skill_id),
|
|
1078
|
+
description: optStr(payload.description),
|
|
1079
|
+
parent_fork_tool_call_id: optStr(payload.parent_fork_tool_call_id)
|
|
1080
|
+
}
|
|
1081
|
+
);
|
|
1082
|
+
return [{ kind: "upsert", turn }];
|
|
1083
|
+
}
|
|
1084
|
+
onBgStarted(loopId, payload) {
|
|
1085
|
+
const taskId = optStr(payload.task_id) ?? "";
|
|
1086
|
+
const command = optStr(payload.command);
|
|
1087
|
+
const description = optStr(payload.description);
|
|
1088
|
+
const turn = buildSystemNotificationTurn(
|
|
1089
|
+
this.nextSeq(),
|
|
1090
|
+
this.nextTurnId("bg:started"),
|
|
1091
|
+
loopId,
|
|
1092
|
+
"bg:started",
|
|
1093
|
+
"running",
|
|
1094
|
+
description ?? `Background: ${taskId || "task"}`,
|
|
1095
|
+
command,
|
|
1096
|
+
{ task_id: taskId, command, description }
|
|
1097
|
+
);
|
|
1098
|
+
return [{ kind: "upsert", turn }];
|
|
1099
|
+
}
|
|
1100
|
+
onBgTasksCompleted(loopId, payload) {
|
|
1101
|
+
const taskIds = Array.isArray(payload.task_ids) ? payload.task_ids : [];
|
|
1102
|
+
const count = typeof payload.count === "number" ? payload.count : taskIds.length;
|
|
1103
|
+
const detail = taskIds.map(String).join(", ");
|
|
1104
|
+
const turn = buildSystemNotificationTurn(
|
|
1105
|
+
this.nextSeq(),
|
|
1106
|
+
this.nextTurnId("bg:tasks_completed"),
|
|
1107
|
+
loopId,
|
|
1108
|
+
"bg:tasks_completed",
|
|
1109
|
+
"done",
|
|
1110
|
+
`Background tasks completed (${count})`,
|
|
1111
|
+
detail || null,
|
|
1112
|
+
{ task_ids: taskIds, count }
|
|
1113
|
+
);
|
|
1114
|
+
return [{ kind: "upsert", turn }];
|
|
1115
|
+
}
|
|
1116
|
+
onAskUserAnswer(loopId, payload) {
|
|
1117
|
+
const toolCallId = String(payload.tool_call_id ?? "");
|
|
1118
|
+
const pending = this.pendingChildPauses.get(toolCallId);
|
|
1119
|
+
const parentLoopId = pending?.parentLoopName;
|
|
1120
|
+
if (pending) {
|
|
1121
|
+
this.pendingChildPauses.delete(toolCallId);
|
|
1122
|
+
const childTurn = this.activeTurns.get(pending.childLoopName) ?? this.latestTurns.get(pending.childLoopName);
|
|
1123
|
+
setToolCallStatus(childTurn, pending.childToolCallId, "done", null, null);
|
|
1124
|
+
const parentTurn = this.activeTurns.get(pending.parentLoopName) ?? this.latestTurns.get(pending.parentLoopName);
|
|
1125
|
+
setToolCallStatus(parentTurn, pending.parentToolCallId, "done", null, null);
|
|
1126
|
+
}
|
|
1127
|
+
const directTurn = this.activeTurns.get(loopId) ?? this.latestTurns.get(loopId);
|
|
1128
|
+
setToolCallStatus(directTurn, toolCallId, "done", null, null);
|
|
1129
|
+
return this.collectLoopUpdates(loopId, parentLoopId);
|
|
1130
|
+
}
|
|
1131
|
+
onUserMessage(_loopId, payload) {
|
|
1132
|
+
const entryId = String(payload.entry_id ?? this.nextTurnId("user"));
|
|
1133
|
+
const displayContent = payload.display_content ?? payload.content ?? "";
|
|
1134
|
+
const turn = {
|
|
1135
|
+
id: entryId,
|
|
1136
|
+
sequence: this.nextSeq(),
|
|
1137
|
+
turn_id: entryId,
|
|
1138
|
+
loop_id: "root",
|
|
1139
|
+
kind: "message",
|
|
1140
|
+
role: "user",
|
|
1141
|
+
status: "completed",
|
|
1142
|
+
blocks: [{ type: "text", content: displayContent }],
|
|
1143
|
+
tool_calls: [],
|
|
1144
|
+
model: null,
|
|
1145
|
+
usage: null,
|
|
1146
|
+
duration_ms: 0
|
|
1147
|
+
};
|
|
1148
|
+
return [{ kind: "upsert", turn }];
|
|
1149
|
+
}
|
|
1150
|
+
// --- Delta / streaming events ---
|
|
1151
|
+
onDelta(state, blockType, _toolCallId, content) {
|
|
1152
|
+
if (!content) return null;
|
|
1153
|
+
appendTextBlock(state, blockType, content);
|
|
1154
|
+
return this.syncPatch(state);
|
|
1155
|
+
}
|
|
1156
|
+
onToolCallCreated(state, payload) {
|
|
1157
|
+
if ("arguments_delta" in payload) {
|
|
1158
|
+
const delta = String(payload.arguments_delta ?? "");
|
|
1159
|
+
const toolCallId2 = String(payload.id ?? "");
|
|
1160
|
+
if (delta && toolCallId2) {
|
|
1161
|
+
appendToolCallArguments(state, toolCallId2, delta);
|
|
1162
|
+
return this.syncPatch(state);
|
|
1163
|
+
}
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
const toolCallId = String(payload.id ?? "");
|
|
1167
|
+
const toolName = toolNameFromPayload(payload);
|
|
1168
|
+
const displayName = String(payload.display_name ?? "") || this.displayNameResolver(toolName);
|
|
1169
|
+
const args = toolArgsFromPayload(payload);
|
|
1170
|
+
upsertToolCall(state, toolCallId, toolName, displayName, args);
|
|
1171
|
+
return this.syncPatch(state);
|
|
1172
|
+
}
|
|
1173
|
+
onResponseDone(state, loopId, payload) {
|
|
1174
|
+
const model = optStr(payload.model);
|
|
1175
|
+
if (model) state.model = model;
|
|
1176
|
+
if (typeof payload.usage === "object" && payload.usage !== null) {
|
|
1177
|
+
state.usage = { ...payload.usage };
|
|
1178
|
+
}
|
|
1179
|
+
const toolCalls = payload.tool_calls;
|
|
1180
|
+
if (Array.isArray(toolCalls)) {
|
|
1181
|
+
for (const raw of toolCalls) {
|
|
1182
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
1183
|
+
const tc = raw;
|
|
1184
|
+
const fn = tc.function ?? {};
|
|
1185
|
+
const toolName = String(fn.name ?? "");
|
|
1186
|
+
const displayName = String(tc.display_name ?? "") || this.displayNameResolver(toolName);
|
|
1187
|
+
upsertToolCall(
|
|
1188
|
+
state,
|
|
1189
|
+
String(tc.id ?? ""),
|
|
1190
|
+
toolName,
|
|
1191
|
+
displayName,
|
|
1192
|
+
String(fn.arguments ?? "")
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (state.toolCalls.length === 0) {
|
|
1197
|
+
return this.finalize(loopId, "completed");
|
|
1198
|
+
}
|
|
1199
|
+
return this.syncPatch(state);
|
|
1200
|
+
}
|
|
1201
|
+
onToolResultDone(state, loopId, payload) {
|
|
1202
|
+
const toolCallId = String(payload.tool_call_id ?? "");
|
|
1203
|
+
const content = payload.content ?? "";
|
|
1204
|
+
const status = toolStatusFromResult(content);
|
|
1205
|
+
const rawDuration = payload.duration_ms;
|
|
1206
|
+
const durationMs = typeof rawDuration === "number" && rawDuration >= 0 ? Math.round(rawDuration) : null;
|
|
1207
|
+
const sl = sourceLoopFor(state.loopId, this.loopDescriptions);
|
|
1208
|
+
applyToolResult(state, toolCallId, content, status, durationMs, sl);
|
|
1209
|
+
if (this.shouldFinalizeAfterToolResults(state)) {
|
|
1210
|
+
return this.finalize(loopId, "completed");
|
|
1211
|
+
}
|
|
1212
|
+
return this.syncPatch(state);
|
|
1213
|
+
}
|
|
1214
|
+
onToolResultDelta(state, payload) {
|
|
1215
|
+
appendToolResultDelta(
|
|
1216
|
+
state,
|
|
1217
|
+
String(payload.tool_call_id ?? ""),
|
|
1218
|
+
String(payload.content ?? "")
|
|
1219
|
+
);
|
|
1220
|
+
return this.syncPatch(state);
|
|
1221
|
+
}
|
|
1222
|
+
onToolUi(state, payload) {
|
|
1223
|
+
const toolCallId = String(payload.tool_call_id ?? "");
|
|
1224
|
+
const ui = payload.ui;
|
|
1225
|
+
if (toolCallId && typeof ui === "object" && ui !== null) {
|
|
1226
|
+
state.blocks.push({
|
|
1227
|
+
type: "tool_ui",
|
|
1228
|
+
content: ui,
|
|
1229
|
+
tool_call_id: toolCallId
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
return this.syncPatch(state);
|
|
1233
|
+
}
|
|
1234
|
+
onToolBridge(state, payload) {
|
|
1235
|
+
const bridge = payload.bridge;
|
|
1236
|
+
if (typeof bridge !== "object" || bridge === null) return null;
|
|
1237
|
+
state.blocks.push({
|
|
1238
|
+
type: "tool_bridge",
|
|
1239
|
+
content: bridge,
|
|
1240
|
+
tool_call_id: String(payload.tool_call_id ?? "") || null
|
|
1241
|
+
});
|
|
1242
|
+
return this.syncPatch(state);
|
|
1243
|
+
}
|
|
1244
|
+
onToolApprovalRequired(state, payload) {
|
|
1245
|
+
const toolCallId = String(payload.tool_call_id ?? "");
|
|
1246
|
+
const toolName = String(payload.tool_name ?? "");
|
|
1247
|
+
const displayName = this.displayNameResolver(toolName);
|
|
1248
|
+
upsertToolCall(state, toolCallId, toolName, displayName, "");
|
|
1249
|
+
return this.syncPatch(state);
|
|
1250
|
+
}
|
|
1251
|
+
onChildPause(state, loopId, payload) {
|
|
1252
|
+
const pauseToolData = payload.pause_tool_data;
|
|
1253
|
+
if (typeof pauseToolData !== "object" || pauseToolData === null)
|
|
1254
|
+
return this.syncPatch(state);
|
|
1255
|
+
const ptd = pauseToolData;
|
|
1256
|
+
const sourceLoop = ptd.source_loop;
|
|
1257
|
+
if (typeof sourceLoop === "object" && sourceLoop) {
|
|
1258
|
+
const slName = String(sourceLoop.name ?? "");
|
|
1259
|
+
const slDesc = optStr(sourceLoop.description);
|
|
1260
|
+
if (slName && slDesc) this.loopDescriptions.set(slName, slDesc);
|
|
1261
|
+
}
|
|
1262
|
+
const { childLoopName, childToolCallId, description } = normalizeChildPause(payload);
|
|
1263
|
+
if (!childToolCallId) return this.syncPatch(state);
|
|
1264
|
+
const existing = this.pendingChildPauses.get(childToolCallId);
|
|
1265
|
+
const parentLoopName = String(
|
|
1266
|
+
payload.parent_loop_name ?? existing?.parentLoopName ?? "root"
|
|
1267
|
+
);
|
|
1268
|
+
const parentToolCallId = String(
|
|
1269
|
+
payload.parent_fork_tool_call_id ?? existing?.parentToolCallId ?? ""
|
|
1270
|
+
);
|
|
1271
|
+
const resolvedChildLoopName = String(payload.child_loop_name ?? "") || existing?.childLoopName || childLoopName || loopId;
|
|
1272
|
+
this.pendingChildPauses.set(childToolCallId, {
|
|
1273
|
+
parentLoopName,
|
|
1274
|
+
parentToolCallId,
|
|
1275
|
+
childLoopName: resolvedChildLoopName,
|
|
1276
|
+
childToolCallId,
|
|
1277
|
+
description: description || existing?.description || ""
|
|
1278
|
+
});
|
|
1279
|
+
applyAskUserPauseToTurn(
|
|
1280
|
+
state,
|
|
1281
|
+
ptd,
|
|
1282
|
+
childToolCallId,
|
|
1283
|
+
resolvedChildLoopName,
|
|
1284
|
+
this.pendingChildPauses.get(childToolCallId).description,
|
|
1285
|
+
this.displayNameResolver
|
|
1286
|
+
);
|
|
1287
|
+
const parentTurn = this.activeTurns.get(parentLoopName) ?? this.latestTurns.get(parentLoopName);
|
|
1288
|
+
if (parentTurn) {
|
|
1289
|
+
applyChildPauseToParent(
|
|
1290
|
+
parentTurn,
|
|
1291
|
+
parentToolCallId,
|
|
1292
|
+
resolvedChildLoopName,
|
|
1293
|
+
childToolCallId,
|
|
1294
|
+
this.pendingChildPauses.get(childToolCallId).description,
|
|
1295
|
+
this.displayNameResolver
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
return this.collectLoopUpdates(loopId, parentLoopName);
|
|
1299
|
+
}
|
|
1300
|
+
onChatEnd(state, loopId, payload) {
|
|
1301
|
+
const status = String(payload.status ?? "completed");
|
|
1302
|
+
const pauseToolData = payload.pause_tool_data;
|
|
1303
|
+
if (typeof pauseToolData === "object" && pauseToolData !== null) {
|
|
1304
|
+
const ptd = pauseToolData;
|
|
1305
|
+
const pauseToolName = String(ptd.name ?? "");
|
|
1306
|
+
const normalized = pauseToolName.includes(":") ? pauseToolName.split(":").pop() : pauseToolName;
|
|
1307
|
+
const sourceLoop = ptd.source_loop;
|
|
1308
|
+
if (typeof sourceLoop === "object" && sourceLoop) {
|
|
1309
|
+
const slName = String(sourceLoop.name ?? "");
|
|
1310
|
+
const slDesc = optStr(sourceLoop.description);
|
|
1311
|
+
if (slName && slDesc) this.loopDescriptions.set(slName, slDesc);
|
|
1312
|
+
if (status === "paused" && slName) {
|
|
1313
|
+
applyChildPauseToParent(
|
|
1314
|
+
state,
|
|
1315
|
+
String(ptd.parent_fork_tool_call_id ?? ""),
|
|
1316
|
+
slName,
|
|
1317
|
+
String(ptd.tool_call_id ?? ""),
|
|
1318
|
+
slDesc ?? "",
|
|
1319
|
+
this.displayNameResolver
|
|
1320
|
+
);
|
|
1321
|
+
const childTurn = this.activeTurns.get(slName);
|
|
1322
|
+
if (childTurn) {
|
|
1323
|
+
applyAskUserPauseToTurn(
|
|
1324
|
+
childTurn,
|
|
1325
|
+
ptd,
|
|
1326
|
+
String(ptd.tool_call_id ?? ""),
|
|
1327
|
+
slName,
|
|
1328
|
+
slDesc ?? "",
|
|
1329
|
+
this.displayNameResolver
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
} else if (status === "paused" && normalized === "AskUserQuestion") {
|
|
1334
|
+
applyAskUserPauseToTurn(
|
|
1335
|
+
state,
|
|
1336
|
+
ptd,
|
|
1337
|
+
String(ptd.tool_call_id ?? ""),
|
|
1338
|
+
"",
|
|
1339
|
+
"",
|
|
1340
|
+
this.displayNameResolver
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
const finalStatus = { completed: "completed", paused: "paused", failed: "failed", interrupted: "interrupted" }[status];
|
|
1345
|
+
if (!finalStatus) return null;
|
|
1346
|
+
if (!this.activeTurns.has(loopId)) {
|
|
1347
|
+
this.pendingMemoryRefs.delete(loopId);
|
|
1348
|
+
}
|
|
1349
|
+
return this.finalize(loopId, finalStatus);
|
|
1350
|
+
}
|
|
1351
|
+
onAgentEnd(state, loopId, payload) {
|
|
1352
|
+
const ok = payload.ok !== false;
|
|
1353
|
+
const status = ok ? "done" : "error";
|
|
1354
|
+
const title = `Agent: ${payload.description ?? payload.skill_id ?? "agent"}`;
|
|
1355
|
+
const detail = optStr(payload.error);
|
|
1356
|
+
const metadata = {
|
|
1357
|
+
skill_id: optStr(payload.skill_id),
|
|
1358
|
+
description: optStr(payload.description),
|
|
1359
|
+
parent_fork_tool_call_id: optStr(payload.parent_fork_tool_call_id),
|
|
1360
|
+
ok
|
|
1361
|
+
};
|
|
1362
|
+
if (state) {
|
|
1363
|
+
state.blocks.push(buildSystemNotificationBlock("agent:end", status, title, detail, metadata));
|
|
1364
|
+
return this.finalize(loopId, ok ? "completed" : "failed");
|
|
1365
|
+
}
|
|
1366
|
+
const turn = buildSystemNotificationTurn(
|
|
1367
|
+
this.nextSeq(),
|
|
1368
|
+
this.nextTurnId("agent:end"),
|
|
1369
|
+
loopId,
|
|
1370
|
+
"agent:end",
|
|
1371
|
+
status,
|
|
1372
|
+
title,
|
|
1373
|
+
detail,
|
|
1374
|
+
metadata
|
|
1375
|
+
);
|
|
1376
|
+
return [{ kind: "upsert", turn }];
|
|
1377
|
+
}
|
|
1378
|
+
onCompaction(eventType, loopId, payload) {
|
|
1379
|
+
const compactionId = String(payload.compaction_id ?? "");
|
|
1380
|
+
if (!compactionId) return null;
|
|
1381
|
+
const current = {
|
|
1382
|
+
...this.activeCompactions.get(compactionId) ?? {},
|
|
1383
|
+
...payload,
|
|
1384
|
+
loop_id: loopId
|
|
1385
|
+
};
|
|
1386
|
+
let status;
|
|
1387
|
+
switch (eventType) {
|
|
1388
|
+
case "compaction:start":
|
|
1389
|
+
status = "streaming";
|
|
1390
|
+
this.activeCompactions.set(compactionId, current);
|
|
1391
|
+
break;
|
|
1392
|
+
case "compaction:end":
|
|
1393
|
+
status = "completed";
|
|
1394
|
+
this.activeCompactions.delete(compactionId);
|
|
1395
|
+
break;
|
|
1396
|
+
case "compaction:abort":
|
|
1397
|
+
status = "interrupted";
|
|
1398
|
+
this.activeCompactions.delete(compactionId);
|
|
1399
|
+
break;
|
|
1400
|
+
case "compaction:error":
|
|
1401
|
+
status = "failed";
|
|
1402
|
+
this.activeCompactions.delete(compactionId);
|
|
1403
|
+
break;
|
|
1404
|
+
default:
|
|
1405
|
+
status = "streaming";
|
|
1406
|
+
this.activeCompactions.set(compactionId, current);
|
|
1407
|
+
}
|
|
1408
|
+
const turn = buildCompactionTurn(this.nextSeq(), compactionId, loopId, status, current);
|
|
1409
|
+
return [{ kind: "upsert", turn }];
|
|
1410
|
+
}
|
|
1411
|
+
// --- Internal helpers ---
|
|
1412
|
+
syncPatch(state) {
|
|
1413
|
+
const turn = snapshot(state, this.nextSeq(), "streaming");
|
|
1414
|
+
this.latestTurns.set(state.loopId, turn);
|
|
1415
|
+
this.lastTurnIds.set(state.loopId, turn.turn_id);
|
|
1416
|
+
return [{ kind: "upsert", turn }];
|
|
1417
|
+
}
|
|
1418
|
+
finalize(loopId, status) {
|
|
1419
|
+
const state = this.activeTurns.get(loopId);
|
|
1420
|
+
if (!state) return null;
|
|
1421
|
+
this.activeTurns.delete(loopId);
|
|
1422
|
+
const turn = snapshot(state, this.nextSeq(), status);
|
|
1423
|
+
this.lastTurnIds.set(loopId, turn.turn_id);
|
|
1424
|
+
this.latestTurns.set(loopId, turn);
|
|
1425
|
+
return [{ kind: "upsert", turn }];
|
|
1426
|
+
}
|
|
1427
|
+
shouldFinalizeAfterToolResults(state) {
|
|
1428
|
+
if (state.toolCalls.length === 0) return true;
|
|
1429
|
+
if (state.toolCalls.some((tc) => tc.status === "pending")) return false;
|
|
1430
|
+
return !state.toolCalls.some((tc) => {
|
|
1431
|
+
const normalized = tc.tool_name.includes(":") ? tc.tool_name.split(":").pop() : tc.tool_name;
|
|
1432
|
+
return PAUSE_TOOL_NAMES.has(normalized);
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
collectLoopUpdates(...loopIds) {
|
|
1436
|
+
const updates = [];
|
|
1437
|
+
for (const loopId of loopIds) {
|
|
1438
|
+
if (!loopId) continue;
|
|
1439
|
+
const state = this.activeTurns.get(loopId);
|
|
1440
|
+
if (state) {
|
|
1441
|
+
updates.push(...this.syncPatch(state));
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
const proj = this.latestTurns.get(loopId);
|
|
1445
|
+
if (proj) {
|
|
1446
|
+
const updated = { ...proj, sequence: this.nextSeq() };
|
|
1447
|
+
this.latestTurns.set(loopId, updated);
|
|
1448
|
+
updates.push({ kind: "upsert", turn: updated });
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return updates;
|
|
1452
|
+
}
|
|
1453
|
+
nextSeq() {
|
|
1454
|
+
return ++this.seq;
|
|
1455
|
+
}
|
|
1456
|
+
nextTurnId(prefix) {
|
|
1457
|
+
this.syntheticCounter++;
|
|
1458
|
+
const hex = Math.random().toString(16).slice(2, 10);
|
|
1459
|
+
return `${prefix}:${this.syntheticCounter}:${hex}`;
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
function optStr(value) {
|
|
1463
|
+
if (value == null || value === "") return null;
|
|
1464
|
+
return String(value);
|
|
1465
|
+
}
|
|
1466
|
+
function toolNameFromPayload(payload) {
|
|
1467
|
+
const fn = payload.function;
|
|
1468
|
+
if (typeof fn === "object" && fn !== null) {
|
|
1469
|
+
return String(fn.name ?? "");
|
|
1470
|
+
}
|
|
1471
|
+
return String(payload.tool_name ?? "");
|
|
1472
|
+
}
|
|
1473
|
+
function toolArgsFromPayload(payload) {
|
|
1474
|
+
const fn = payload.function;
|
|
1475
|
+
if (typeof fn === "object" && fn !== null) {
|
|
1476
|
+
return String(fn.arguments ?? "");
|
|
1477
|
+
}
|
|
1478
|
+
return "";
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// src/shared/projection/history.ts
|
|
1482
|
+
function projectHistory(entries) {
|
|
1483
|
+
const turns = [];
|
|
1484
|
+
const toolOwners = /* @__PURE__ */ new Map();
|
|
1485
|
+
const latestAssistantByLoop = /* @__PURE__ */ new Map();
|
|
1486
|
+
const pendingChildPauses = /* @__PURE__ */ new Map();
|
|
1487
|
+
let sequence = 0;
|
|
1488
|
+
for (const entry of entries) {
|
|
1489
|
+
const kind = String(entry.kind ?? "");
|
|
1490
|
+
const loopId = String(entry.loop_name ?? "root");
|
|
1491
|
+
const id = String(entry.id ?? `history-${sequence + 1}`);
|
|
1492
|
+
if (kind === "message" && isRecord2(entry.message)) {
|
|
1493
|
+
const message = entry.message;
|
|
1494
|
+
const role = String(message.role ?? "");
|
|
1495
|
+
if (role === "tool") {
|
|
1496
|
+
const toolCallId = String(message.tool_call_id ?? "");
|
|
1497
|
+
const owner = toolOwners.get(toolCallId);
|
|
1498
|
+
const toolCall = owner?.tool_calls.find((item) => item.id === toolCallId);
|
|
1499
|
+
if (owner && toolCall) {
|
|
1500
|
+
toolCall.status = message._error ? "error" : inferToolStatus(String(message.content ?? ""));
|
|
1501
|
+
toolCall.result = message.content ?? "";
|
|
1502
|
+
owner.blocks.push({
|
|
1503
|
+
type: "tool_result",
|
|
1504
|
+
content: message.content ?? "",
|
|
1505
|
+
tool_call_id: toolCallId
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (role !== "user" && role !== "assistant" && role !== "system") continue;
|
|
1511
|
+
const toolCalls = role === "assistant" ? buildToolCalls(message.tool_calls) : [];
|
|
1512
|
+
const blocks = buildBlocks(message, toolCalls);
|
|
1513
|
+
const turn = {
|
|
1514
|
+
id,
|
|
1515
|
+
sequence: ++sequence,
|
|
1516
|
+
turn_id: id,
|
|
1517
|
+
loop_id: loopId,
|
|
1518
|
+
kind: "message",
|
|
1519
|
+
role,
|
|
1520
|
+
status: message._error ? "failed" : "completed",
|
|
1521
|
+
blocks,
|
|
1522
|
+
tool_calls: toolCalls,
|
|
1523
|
+
model: stringOrNull(message.model),
|
|
1524
|
+
usage: isRecord2(message._usage) ? { ...message._usage } : null,
|
|
1525
|
+
duration_ms: numberOrZero(message._duration_ms),
|
|
1526
|
+
started_at: typeof entry.timestamp === "string" ? entry.timestamp : void 0
|
|
1527
|
+
};
|
|
1528
|
+
turns.push(turn);
|
|
1529
|
+
if (role === "assistant") latestAssistantByLoop.set(loopId, turn);
|
|
1530
|
+
for (const toolCall of toolCalls) toolOwners.set(toolCall.id, turn);
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
if (kind === "mode_change") {
|
|
1534
|
+
turns.push(markerTurn(id, ++sequence, loopId, "mode_change", entry.data ?? {}));
|
|
1535
|
+
} else if (kind === "plan_status") {
|
|
1536
|
+
const data = entry.data ?? {};
|
|
1537
|
+
turns.push(
|
|
1538
|
+
markerTurn(id, ++sequence, loopId, "plan_status", {
|
|
1539
|
+
plan_yaml: data.plan_yaml ?? "",
|
|
1540
|
+
statuses: data.statuses ?? data._statuses ?? {}
|
|
1541
|
+
})
|
|
1542
|
+
);
|
|
1543
|
+
} else if (kind === "tool_ui" || kind === "tool_bridge") {
|
|
1544
|
+
const data = entry.data ?? {};
|
|
1545
|
+
const toolCallId = String(data.tool_call_id ?? "");
|
|
1546
|
+
const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
|
|
1547
|
+
const content = kind === "tool_ui" ? data.ui : data.bridge;
|
|
1548
|
+
if (owner && content !== void 0) {
|
|
1549
|
+
owner.blocks.push({
|
|
1550
|
+
type: kind,
|
|
1551
|
+
content,
|
|
1552
|
+
tool_call_id: toolCallId || null
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
} else if (kind === "compaction" || kind === "tool_result_archive") {
|
|
1556
|
+
const data = entry.data ?? {};
|
|
1557
|
+
const compactionId = String(data.compaction_id ?? entry.id ?? `history-${sequence + 1}`);
|
|
1558
|
+
let content = isRecord2(entry.message) ? entry.message.content : void 0;
|
|
1559
|
+
if (kind === "tool_result_archive") {
|
|
1560
|
+
const archivedCount = Array.isArray(data.archived_files) ? data.archived_files.length : 0;
|
|
1561
|
+
content = `<compaction-summary>
|
|
1562
|
+
\u5DF2\u5F52\u6863 ${archivedCount} \u4E2A\u5927\u578B\u5DE5\u5177\u7ED3\u679C\u3002
|
|
1563
|
+
</compaction-summary>`;
|
|
1564
|
+
}
|
|
1565
|
+
turns.push(
|
|
1566
|
+
buildCompactionTurn(++sequence, compactionId, loopId, "completed", {
|
|
1567
|
+
...data,
|
|
1568
|
+
content
|
|
1569
|
+
})
|
|
1570
|
+
);
|
|
1571
|
+
} else if (kind === "child_pause") {
|
|
1572
|
+
const data = entry.data ?? {};
|
|
1573
|
+
const sourceLoop = isRecord2(data.source_loop) ? data.source_loop : {};
|
|
1574
|
+
const childLoopId = String(data.child_loop_name ?? sourceLoop.name ?? "");
|
|
1575
|
+
const childToolCallId = String(data.child_pause_tool_call_id ?? "");
|
|
1576
|
+
const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
|
|
1577
|
+
const pauseToolData = isRecord2(data.pause_tool_data) ? data.pause_tool_data : {};
|
|
1578
|
+
const description = String(sourceLoop.description ?? "");
|
|
1579
|
+
applyAskUserPauseToTurn(
|
|
1580
|
+
latestAssistantByLoop.get(childLoopId),
|
|
1581
|
+
pauseToolData,
|
|
1582
|
+
childToolCallId,
|
|
1583
|
+
childLoopId,
|
|
1584
|
+
description,
|
|
1585
|
+
(name) => name
|
|
1586
|
+
);
|
|
1587
|
+
applyChildPauseToParent(
|
|
1588
|
+
latestAssistantByLoop.get(loopId),
|
|
1589
|
+
parentToolCallId,
|
|
1590
|
+
childLoopId,
|
|
1591
|
+
childToolCallId,
|
|
1592
|
+
description,
|
|
1593
|
+
(name) => name
|
|
1594
|
+
);
|
|
1595
|
+
if (childToolCallId) {
|
|
1596
|
+
pendingChildPauses.set(childToolCallId, {
|
|
1597
|
+
childLoopId,
|
|
1598
|
+
parentLoopId: loopId,
|
|
1599
|
+
parentToolCallId
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
} else if (kind === "ask_user_answer") {
|
|
1603
|
+
const data = entry.data ?? {};
|
|
1604
|
+
const toolCallId = String(data.tool_call_id ?? "");
|
|
1605
|
+
const pending = pendingChildPauses.get(toolCallId);
|
|
1606
|
+
if (pending) {
|
|
1607
|
+
setToolCallStatus(latestAssistantByLoop.get(pending.childLoopId), toolCallId, "done", null, null);
|
|
1608
|
+
setToolCallStatus(
|
|
1609
|
+
latestAssistantByLoop.get(pending.parentLoopId),
|
|
1610
|
+
pending.parentToolCallId,
|
|
1611
|
+
"done",
|
|
1612
|
+
null,
|
|
1613
|
+
null
|
|
1614
|
+
);
|
|
1615
|
+
pendingChildPauses.delete(toolCallId);
|
|
1616
|
+
} else {
|
|
1617
|
+
setToolCallStatus(latestAssistantByLoop.get(loopId), toolCallId, "done", null, null);
|
|
1618
|
+
}
|
|
1619
|
+
turns.push(
|
|
1620
|
+
markerTurn(
|
|
1621
|
+
id,
|
|
1622
|
+
++sequence,
|
|
1623
|
+
loopId,
|
|
1624
|
+
"ask_user_answer",
|
|
1625
|
+
data,
|
|
1626
|
+
toolCallId || null
|
|
1627
|
+
)
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return turns;
|
|
1632
|
+
}
|
|
1633
|
+
function buildToolCalls(value) {
|
|
1634
|
+
if (!Array.isArray(value)) return [];
|
|
1635
|
+
return value.filter(isRecord2).map((raw) => {
|
|
1636
|
+
const fn = isRecord2(raw.function) ? raw.function : {};
|
|
1637
|
+
const name = String(fn.name ?? "");
|
|
1638
|
+
return {
|
|
1639
|
+
id: String(raw.id ?? ""),
|
|
1640
|
+
tool_name: name,
|
|
1641
|
+
display_name: String(raw.display_name ?? name),
|
|
1642
|
+
arguments: typeof fn.arguments === "string" ? fn.arguments : JSON.stringify(fn.arguments ?? {}),
|
|
1643
|
+
status: "running",
|
|
1644
|
+
result: null
|
|
1645
|
+
};
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
function buildBlocks(message, toolCalls) {
|
|
1649
|
+
const displayContent = message.role === "user" ? message._display_content : void 0;
|
|
1650
|
+
const stored = message._blocks;
|
|
1651
|
+
const blocks = [];
|
|
1652
|
+
if (displayContent !== void 0) {
|
|
1653
|
+
blocks.push({ type: "text", content: displayContent });
|
|
1654
|
+
} else if (Array.isArray(stored)) {
|
|
1655
|
+
for (const raw of stored) {
|
|
1656
|
+
if (!isRecord2(raw)) continue;
|
|
1657
|
+
const type3 = String(raw.type ?? "");
|
|
1658
|
+
if (type3 === "thinking") blocks.push({ type: type3, content: raw.thinking ?? raw.content ?? "" });
|
|
1659
|
+
if (type3 === "text") blocks.push({ type: type3, content: raw.text ?? raw.content ?? "" });
|
|
1660
|
+
}
|
|
1661
|
+
} else if (message.content !== void 0 && message.content !== "") {
|
|
1662
|
+
blocks.push({ type: "text", content: message.content });
|
|
1663
|
+
}
|
|
1664
|
+
for (const toolCall of toolCalls) {
|
|
1665
|
+
blocks.push({
|
|
1666
|
+
type: "tool_use",
|
|
1667
|
+
content: { name: toolCall.tool_name, arguments: toolCall.arguments },
|
|
1668
|
+
tool_call_id: toolCall.id
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
return blocks;
|
|
1672
|
+
}
|
|
1673
|
+
function markerTurn(id, sequence, loopId, type3, content, toolCallId = null) {
|
|
1674
|
+
return {
|
|
1675
|
+
id,
|
|
1676
|
+
sequence,
|
|
1677
|
+
turn_id: id,
|
|
1678
|
+
loop_id: loopId,
|
|
1679
|
+
kind: type3,
|
|
1680
|
+
role: "system",
|
|
1681
|
+
status: "completed",
|
|
1682
|
+
blocks: [{ type: type3, content, tool_call_id: toolCallId }],
|
|
1683
|
+
tool_calls: [],
|
|
1684
|
+
model: null,
|
|
1685
|
+
usage: null,
|
|
1686
|
+
duration_ms: 0
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
function isRecord2(value) {
|
|
1690
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1691
|
+
}
|
|
1692
|
+
function stringOrNull(value) {
|
|
1693
|
+
return typeof value === "string" ? value : null;
|
|
1694
|
+
}
|
|
1695
|
+
function numberOrZero(value) {
|
|
1696
|
+
return typeof value === "number" ? value : 0;
|
|
1697
|
+
}
|
|
1698
|
+
function findLatestAssistant(turns, loopId) {
|
|
1699
|
+
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
1700
|
+
const turn = turns[index];
|
|
1701
|
+
if (turn?.role === "assistant" && turn.loop_id === loopId) return turn;
|
|
1702
|
+
}
|
|
1703
|
+
return void 0;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// src/resources/sessions.ts
|
|
1707
|
+
function toCreateSessionPayload(request) {
|
|
1708
|
+
return {
|
|
1709
|
+
intent: request.intent ?? "",
|
|
1710
|
+
template_id: request.template_id,
|
|
1711
|
+
solution_id: request.solution_id,
|
|
1712
|
+
biz_role_id: request.biz_role_id ?? null,
|
|
1713
|
+
model: request.model ?? null,
|
|
1714
|
+
enable_thinking: request.enable_thinking ?? null,
|
|
1715
|
+
memory_enabled: request.memory_enabled ?? null,
|
|
1716
|
+
primary_skill_id: request.primary_skill_id ?? null,
|
|
1717
|
+
env: request.env,
|
|
1718
|
+
disable_tools: request.disable_tools ?? null,
|
|
1719
|
+
session_solution_asset_id: request.session_solution_asset_id ?? null,
|
|
1720
|
+
compaction_ratio: request.compaction_ratio ?? null,
|
|
1721
|
+
...request.runtime_type ? { runtime_type: request.runtime_type } : {},
|
|
1722
|
+
...request.daemon_id ? { daemon_id: request.daemon_id } : {}
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
var SessionsResource = class {
|
|
1726
|
+
constructor(client) {
|
|
1727
|
+
this.client = client;
|
|
1728
|
+
}
|
|
1729
|
+
client;
|
|
1730
|
+
/**
|
|
1731
|
+
* 连接一个已存在的会话,返回实时会话对象 AgentSession
|
|
1732
|
+
* (自动完成历史加载、Socket.IO 订阅、断线重连补数)。
|
|
1733
|
+
*/
|
|
1734
|
+
connect(sessionId) {
|
|
1735
|
+
return this.client.hub.connect(sessionId);
|
|
1736
|
+
}
|
|
1737
|
+
/** 创建新会话并直接连接,返回可收发消息的 AgentSession。 */
|
|
1738
|
+
async create(request = {}) {
|
|
1739
|
+
await this.client.ensureVersionCompatible();
|
|
1740
|
+
const { session_id } = await this.createSessionWithRequest(request);
|
|
1741
|
+
return this.connect(session_id);
|
|
1742
|
+
}
|
|
1743
|
+
fetchSessionsPage({
|
|
1744
|
+
limit,
|
|
1745
|
+
offset,
|
|
1746
|
+
template_id_prefix,
|
|
1747
|
+
solution_id,
|
|
1748
|
+
q
|
|
1749
|
+
}) {
|
|
1750
|
+
const searchParams = new URLSearchParams();
|
|
1751
|
+
searchParams.set("limit", String(limit));
|
|
1752
|
+
searchParams.set("offset", String(offset));
|
|
1753
|
+
if (template_id_prefix) searchParams.set("template_id_prefix", template_id_prefix);
|
|
1754
|
+
if (solution_id) searchParams.set("solution_id", solution_id);
|
|
1755
|
+
const trimmedQuery = q?.trim();
|
|
1756
|
+
if (trimmedQuery) searchParams.set("q", trimmedQuery);
|
|
1757
|
+
return this.client.json("GET", `/api/sessions?${searchParams.toString()}`);
|
|
1758
|
+
}
|
|
1759
|
+
async listSessions(template_id_prefix, solution_id) {
|
|
1760
|
+
const pageSize = 100;
|
|
1761
|
+
const sessions = [];
|
|
1762
|
+
let offset = 0;
|
|
1763
|
+
while (true) {
|
|
1764
|
+
const page = await this.fetchSessionsPage({ limit: pageSize, offset, template_id_prefix, solution_id });
|
|
1765
|
+
sessions.push(...page.items);
|
|
1766
|
+
if (sessions.length >= page.total || page.items.length === 0) return sessions;
|
|
1767
|
+
offset += page.items.length;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
listSessionsPaginated(params) {
|
|
1771
|
+
return this.fetchSessionsPage(params);
|
|
1772
|
+
}
|
|
1773
|
+
listSessionsWithSkillData() {
|
|
1774
|
+
return this.client.json("GET", "/api/sessions/with-skill-data");
|
|
1775
|
+
}
|
|
1776
|
+
createSession(intent, template_id, primary_skill_id) {
|
|
1777
|
+
return this.createSessionWithRequest({ intent, template_id, primary_skill_id });
|
|
1778
|
+
}
|
|
1779
|
+
createSessionWithRequest(request) {
|
|
1780
|
+
return this.client.json("POST", "/api/sessions", toCreateSessionPayload(request));
|
|
1781
|
+
}
|
|
1782
|
+
createWithProfile(params) {
|
|
1783
|
+
const { intent, profile } = params;
|
|
1784
|
+
return this.createSessionWithRequest({
|
|
1785
|
+
intent,
|
|
1786
|
+
...profileToCreateSessionRequest(profile)
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
getSession(sessionId, init) {
|
|
1790
|
+
return this.client.jsonFromInit(`/api/sessions/${sessionId}`, init);
|
|
1791
|
+
}
|
|
1792
|
+
updateSession(sessionId, payload) {
|
|
1793
|
+
return this.client.json("PATCH", `/api/sessions/${sessionId}`, payload);
|
|
1794
|
+
}
|
|
1795
|
+
setSessionEnv(sessionId, env) {
|
|
1796
|
+
return this.client.json("PUT", `/api/sessions/${sessionId}/env`, { env });
|
|
1797
|
+
}
|
|
1798
|
+
pinSession(sessionId, pinned) {
|
|
1799
|
+
return this.client.json("PATCH", `/api/sessions/${sessionId}/pin`, { pinned });
|
|
1800
|
+
}
|
|
1801
|
+
startReplaySession(sourceSessionId, speed = 5) {
|
|
1802
|
+
return this.client.json("POST", `/api/sessions/${sourceSessionId}/replay`, { speed });
|
|
1803
|
+
}
|
|
1804
|
+
updateReplaySession(sessionId, payload) {
|
|
1805
|
+
return this.client.json("PATCH", `/api/sessions/${sessionId}/replay`, payload);
|
|
1806
|
+
}
|
|
1807
|
+
updateSharing(sessionId, shared) {
|
|
1808
|
+
return this.client.json("PATCH", `/api/sessions/${sessionId}/sharing`, { shared });
|
|
1809
|
+
}
|
|
1810
|
+
updateSessionMemory(sessionId, memoryEnabled) {
|
|
1811
|
+
return this.client.json("PATCH", `/api/sessions/${sessionId}/memory`, {
|
|
1812
|
+
memory_enabled: memoryEnabled
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
createShare(sessionId) {
|
|
1816
|
+
return this.client.json("POST", `/api/sessions/${sessionId}/share`, {});
|
|
1817
|
+
}
|
|
1818
|
+
revokeShare(sessionId, token) {
|
|
1819
|
+
return this.client.json("DELETE", `/api/sessions/${sessionId}/share/${encodeURIComponent(token)}`);
|
|
1820
|
+
}
|
|
1821
|
+
async getSharedSession(token) {
|
|
1822
|
+
const entries = await this.client.jsonFromInit(`/api/share/${encodeURIComponent(token)}`, {
|
|
1823
|
+
credentials: "omit",
|
|
1824
|
+
headers: { Accept: "application/json" }
|
|
1825
|
+
});
|
|
1826
|
+
return projectHistory(entries);
|
|
1827
|
+
}
|
|
1828
|
+
getSessionTasks(sessionId) {
|
|
1829
|
+
return this.client.json("GET", `/api/sessions/${sessionId}/tasks`);
|
|
1830
|
+
}
|
|
1831
|
+
async getSessionTurns(sessionId) {
|
|
1832
|
+
const entries = await this.client.json(
|
|
1833
|
+
"GET",
|
|
1834
|
+
`/api/sessions/${sessionId}/messages`
|
|
1835
|
+
);
|
|
1836
|
+
return projectHistory(entries);
|
|
1837
|
+
}
|
|
1838
|
+
getSessionContextStats(sessionId, init) {
|
|
1839
|
+
return this.client.jsonFromInit(`/api/sessions/${sessionId}/context-stats`, init);
|
|
1840
|
+
}
|
|
1841
|
+
getSessionHistory(sessionId, init) {
|
|
1842
|
+
return this.client.jsonFromInit(`/api/sessions/${sessionId}/history`, init);
|
|
1843
|
+
}
|
|
1844
|
+
tokenizePrompt(prompt, model) {
|
|
1845
|
+
return this.client.json("POST", "/api/sessions/tokenize/prompt", { prompt, model });
|
|
1846
|
+
}
|
|
1847
|
+
tokenizeMessages(messages, options) {
|
|
1848
|
+
return this.client.json("POST", "/api/sessions/tokenize/messages", {
|
|
1849
|
+
messages,
|
|
1850
|
+
model: options?.model,
|
|
1851
|
+
add_generation_prompt: options?.add_generation_prompt ?? true,
|
|
1852
|
+
enable_thinking: options?.enable_thinking ?? null,
|
|
1853
|
+
tools: options?.tools ?? null
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
getSessionCheckpoints(sessionId, init) {
|
|
1857
|
+
return this.client.jsonFromInit(`/api/sessions/${sessionId}/checkpoints`, init);
|
|
1858
|
+
}
|
|
1859
|
+
checkoutSession(sessionId, checkpointId, position, mode, linear) {
|
|
1860
|
+
return this.client.json("POST", `/api/sessions/${sessionId}/checkout`, {
|
|
1861
|
+
checkpoint_id: checkpointId,
|
|
1862
|
+
position,
|
|
1863
|
+
...mode ? { mode } : {},
|
|
1864
|
+
...linear ? { linear } : {}
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
async rewindSession(sessionId, checkpointId) {
|
|
1868
|
+
const result = await this.checkoutSession(sessionId, checkpointId, "before");
|
|
1869
|
+
return { id: result.id, content: result.content };
|
|
1870
|
+
}
|
|
1871
|
+
switchBranch(sessionId, checkpointId) {
|
|
1872
|
+
return this.client.json("POST", `/api/sessions/${sessionId}/switch-branch`, {
|
|
1873
|
+
checkpoint_id: checkpointId
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
deleteSession(sessionId) {
|
|
1877
|
+
return this.client.json("DELETE", `/api/sessions/${sessionId}`);
|
|
1878
|
+
}
|
|
1879
|
+
listBackgroundTasks(sessionId, init) {
|
|
1880
|
+
return this.client.jsonFromInit(`/api/sessions/${sessionId}/background-tasks`, init);
|
|
1881
|
+
}
|
|
1882
|
+
getBackgroundTask(sessionId, taskId, tail = 100, init) {
|
|
1883
|
+
return this.client.jsonFromInit(`/api/sessions/${sessionId}/background-tasks/${taskId}?tail=${tail}`, init);
|
|
1884
|
+
}
|
|
1885
|
+
stopBackgroundTask(sessionId, taskId) {
|
|
1886
|
+
return this.client.json("POST", `/api/sessions/${sessionId}/background-tasks/${taskId}/stop`);
|
|
1887
|
+
}
|
|
1888
|
+
probeSessionUrl(sessionId, url) {
|
|
1889
|
+
return this.client.json(
|
|
1890
|
+
"GET",
|
|
1891
|
+
`/api/sessions/${sessionId}/probe-url?url=${encodeURIComponent(url)}`
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
listDir(sessionId, dirPath) {
|
|
1895
|
+
return this.client.json("GET", `/api/sessions/${sessionId}/ls/${encodeURIComponent(dirPath)}`);
|
|
1896
|
+
}
|
|
1897
|
+
async uploadFiles(sessionId, dirPath, files, options = {}) {
|
|
1898
|
+
const formData = new FormData();
|
|
1899
|
+
const entries = Array.from(files).map(
|
|
1900
|
+
(f) => f instanceof File ? { file: f, name: f.name } : f
|
|
1901
|
+
);
|
|
1902
|
+
for (const entry of entries) {
|
|
1903
|
+
formData.append("files", entry.file, entry.name);
|
|
1904
|
+
}
|
|
1905
|
+
const relativePaths = entries.map((e) => e.file.webkitRelativePath || e.name);
|
|
1906
|
+
if (relativePaths.some((p) => p.includes("/"))) {
|
|
1907
|
+
formData.append("paths", JSON.stringify(relativePaths));
|
|
1908
|
+
}
|
|
1909
|
+
const response = await this.client.formData(
|
|
1910
|
+
"POST",
|
|
1911
|
+
`/api/sessions/${sessionId}/upload/${encodeURIComponent(dirPath)}`,
|
|
1912
|
+
formData,
|
|
1913
|
+
{ expectOk: false, onUploadProgress: options.onProgress }
|
|
1914
|
+
);
|
|
1915
|
+
return response.json();
|
|
1916
|
+
}
|
|
1917
|
+
async deleteFile(sessionId, filePath) {
|
|
1918
|
+
await this.client.json("DELETE", `/api/sessions/${sessionId}/files/${encodeURIComponent(filePath)}`);
|
|
1919
|
+
}
|
|
1920
|
+
writeFile(sessionId, filePath, content) {
|
|
1921
|
+
return this.client.json("PUT", `/api/sessions/${sessionId}/files/${encodeURIComponent(filePath)}`, {
|
|
1922
|
+
content
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
renameFile(sessionId, filePath, newName) {
|
|
1926
|
+
return this.client.json("POST", `/api/sessions/${sessionId}/files/${encodeURIComponent(filePath)}/rename`, {
|
|
1927
|
+
new_name: newName
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
copyFile(sessionId, filePath) {
|
|
1931
|
+
return this.client.json("POST", `/api/sessions/${sessionId}/files/${encodeURIComponent(filePath)}/copy`);
|
|
1932
|
+
}
|
|
1933
|
+
shareFile(sessionId, sourcePath, linkName, shareFolder) {
|
|
1934
|
+
return this.client.json("POST", `/api/sessions/${sessionId}/share-file`, {
|
|
1935
|
+
source_path: sourcePath,
|
|
1936
|
+
link_name: linkName ?? "",
|
|
1937
|
+
share_folder: shareFolder ?? ""
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
getDownloadDirUrl(sessionId, dirPath) {
|
|
1941
|
+
return this.client.buildAuthedUrl(`/api/sessions/${sessionId}/download-dir/${encodeURIComponent(dirPath)}`);
|
|
1942
|
+
}
|
|
1943
|
+
async exportSession(sessionId) {
|
|
1944
|
+
await this.getSession(sessionId);
|
|
1945
|
+
const a = document.createElement("a");
|
|
1946
|
+
a.href = this.client.buildAuthedUrl(`/api/sessions/${sessionId}/export`);
|
|
1947
|
+
a.download = `${sessionId}.zip`;
|
|
1948
|
+
document.body.appendChild(a);
|
|
1949
|
+
a.click();
|
|
1950
|
+
a.remove();
|
|
1951
|
+
}
|
|
1952
|
+
async previewImport(file) {
|
|
1953
|
+
const form = new FormData();
|
|
1954
|
+
form.append("file", file);
|
|
1955
|
+
const res = await this.client.formData("POST", "/api/sessions/preview-import", form);
|
|
1956
|
+
return res.json();
|
|
1957
|
+
}
|
|
1958
|
+
async importSession(file, name, solutionId) {
|
|
1959
|
+
const form = new FormData();
|
|
1960
|
+
form.append("file", file);
|
|
1961
|
+
if (name) form.append("name", name);
|
|
1962
|
+
if (solutionId) form.append("solution_id", solutionId);
|
|
1963
|
+
const res = await this.client.formData("POST", "/api/sessions/import", form);
|
|
1964
|
+
return res.json();
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
function profileToCreateSessionRequest(profile) {
|
|
1968
|
+
const solution = profile.solution;
|
|
1969
|
+
return {
|
|
1970
|
+
solution_id: solution?.kind === "existing" ? solution.solutionId : void 0,
|
|
1971
|
+
session_solution_asset_id: solution?.kind === "prepared" ? solution.assetId : void 0,
|
|
1972
|
+
biz_role_id: solution?.bizRoleId ?? null,
|
|
1973
|
+
model: profile.model ?? null,
|
|
1974
|
+
compaction_ratio: profile.compactionRatio ?? null,
|
|
1975
|
+
enable_thinking: profile.enableThinking ?? null,
|
|
1976
|
+
memory_enabled: profile.memoryEnabled ?? null,
|
|
1977
|
+
disable_tools: profile.disableTools ?? null,
|
|
1978
|
+
env: profile.env
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// src/commands/registry.ts
|
|
1983
|
+
var BUFFER_LIMIT_PER_ACTION = 20;
|
|
1984
|
+
var CommandRegistry = class {
|
|
1985
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1986
|
+
buffered = /* @__PURE__ */ new Map();
|
|
1987
|
+
onCommand(action, handler) {
|
|
1988
|
+
let set = this.handlers.get(action);
|
|
1989
|
+
if (!set) {
|
|
1990
|
+
set = /* @__PURE__ */ new Set();
|
|
1991
|
+
this.handlers.set(action, set);
|
|
1992
|
+
}
|
|
1993
|
+
set.add(handler);
|
|
1994
|
+
const pending = this.buffered.get(action);
|
|
1995
|
+
if (pending?.length) {
|
|
1996
|
+
this.buffered.delete(action);
|
|
1997
|
+
for (const command of pending) {
|
|
1998
|
+
this.invoke(handler, action, command);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
return () => {
|
|
2002
|
+
this.handlers.get(action)?.delete(handler);
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
dispatch(action, payload, meta = {}) {
|
|
2006
|
+
const set = this.handlers.get(action);
|
|
2007
|
+
if (set && set.size > 0) {
|
|
2008
|
+
for (const handler of [...set]) {
|
|
2009
|
+
this.invoke(handler, action, { payload, meta });
|
|
2010
|
+
}
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
let pending = this.buffered.get(action);
|
|
2014
|
+
if (!pending) {
|
|
2015
|
+
pending = [];
|
|
2016
|
+
this.buffered.set(action, pending);
|
|
2017
|
+
}
|
|
2018
|
+
pending.push({ payload, meta });
|
|
2019
|
+
if (pending.length > BUFFER_LIMIT_PER_ACTION) {
|
|
2020
|
+
pending.shift();
|
|
2021
|
+
console.warn(
|
|
2022
|
+
`[agent-client] \u6307\u4EE4 "${action}" \u65E0\u4EBA\u5904\u7406\u4E14\u7F13\u51B2\u5DF2\u6EE1\uFF0C\u6700\u65E7\u7684\u4E00\u6761\u5DF2\u4E22\u5F03\u3002\u8BF7\u8C03\u7528 onCommand("${action}", handler) \u6CE8\u518C\u5904\u7406\u51FD\u6570\u3002`
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
invoke(handler, action, command) {
|
|
2027
|
+
try {
|
|
2028
|
+
handler(command.payload, command.meta);
|
|
2029
|
+
} catch (error) {
|
|
2030
|
+
console.warn(`[agent-client] \u6307\u4EE4 "${action}" \u7684\u5904\u7406\u51FD\u6570\u629B\u51FA\u5F02\u5E38\uFF1A`, error);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
clear() {
|
|
2034
|
+
this.handlers.clear();
|
|
2035
|
+
this.buffered.clear();
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
|
|
2039
|
+
// src/session/events.ts
|
|
2040
|
+
var Emitter = class {
|
|
2041
|
+
handlers = /* @__PURE__ */ new Map();
|
|
2042
|
+
on(name, handler) {
|
|
2043
|
+
let set = this.handlers.get(name);
|
|
2044
|
+
if (!set) {
|
|
2045
|
+
set = /* @__PURE__ */ new Set();
|
|
2046
|
+
this.handlers.set(name, set);
|
|
2047
|
+
}
|
|
2048
|
+
set.add(handler);
|
|
2049
|
+
return () => this.off(name, handler);
|
|
2050
|
+
}
|
|
2051
|
+
off(name, handler) {
|
|
2052
|
+
this.handlers.get(name)?.delete(handler);
|
|
2053
|
+
}
|
|
2054
|
+
emit(name, event) {
|
|
2055
|
+
const set = this.handlers.get(name);
|
|
2056
|
+
if (!set) return;
|
|
2057
|
+
for (const handler of [...set]) {
|
|
2058
|
+
try {
|
|
2059
|
+
;
|
|
2060
|
+
handler(event);
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
console.warn(`[agent-client] "${String(name)}" \u4E8B\u4EF6\u5904\u7406\u5668\u629B\u51FA\u5F02\u5E38\uFF1A`, error);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* 只把事件交给最先注册的处理器。用于必须由单一交互入口作答的事件,
|
|
2068
|
+
* 避免多个 UI 同时弹窗并提交相互冲突的决定。
|
|
2069
|
+
*/
|
|
2070
|
+
emitFirst(name, event) {
|
|
2071
|
+
const handler = this.handlers.get(name)?.values().next().value;
|
|
2072
|
+
if (!handler) return false;
|
|
2073
|
+
try {
|
|
2074
|
+
;
|
|
2075
|
+
handler(event);
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
console.warn(`[agent-client] "${String(name)}" \u4E8B\u4EF6\u5904\u7406\u5668\u629B\u51FA\u5F02\u5E38\uFF1A`, error);
|
|
2078
|
+
return false;
|
|
2079
|
+
}
|
|
2080
|
+
return true;
|
|
2081
|
+
}
|
|
2082
|
+
listenerCount(name) {
|
|
2083
|
+
return this.handlers.get(name)?.size ?? 0;
|
|
2084
|
+
}
|
|
2085
|
+
clear() {
|
|
2086
|
+
this.handlers.clear();
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
// src/schemas/message-utils.ts
|
|
2091
|
+
var SYSTEM_REMINDER_PATTERN = /^<system-reminder>\s*[\s\S]*?\s*<\/system-reminder>$/i;
|
|
2092
|
+
var LEGACY_FORK_CONTEXT_MARKERS = [
|
|
2093
|
+
"\u7236\u667A\u80FD\u4F53\u5728\u6D3E\u751F\u4F60\u4E4B\u524D\u5DF2\u7ECF\u5B8C\u6210\u4E86\u4E00\u4E9B\u64CD\u4F5C\u3002",
|
|
2094
|
+
"\u53EF\u901A\u8FC7 `read_file` \u8BFB\u53D6\u3002",
|
|
2095
|
+
"# \u4F60\u7684\u4EFB\u52A1"
|
|
2096
|
+
];
|
|
2097
|
+
function normalizeMessageContent(content) {
|
|
2098
|
+
if (typeof content === "string") return content;
|
|
2099
|
+
if (Array.isArray(content)) return content;
|
|
2100
|
+
return String(content ?? "");
|
|
2101
|
+
}
|
|
2102
|
+
function getTextContent(content) {
|
|
2103
|
+
if (typeof content === "string") return content;
|
|
2104
|
+
return content.filter((part) => part.type === "text").map((part) => part.text).join("");
|
|
2105
|
+
}
|
|
2106
|
+
function isSystemReminderText(text) {
|
|
2107
|
+
return SYSTEM_REMINDER_PATTERN.test(text.trim());
|
|
2108
|
+
}
|
|
2109
|
+
function isLegacyForkContextText(text) {
|
|
2110
|
+
const normalized = text.trim();
|
|
2111
|
+
return LEGACY_FORK_CONTEXT_MARKERS.every((marker) => normalized.includes(marker));
|
|
2112
|
+
}
|
|
2113
|
+
function isHiddenInternalMessage(message) {
|
|
2114
|
+
if (message.role !== "user") return false;
|
|
2115
|
+
const text = getTextContent(normalizeMessageContent(message.content));
|
|
2116
|
+
return isSystemReminderText(text) || isLegacyForkContextText(text);
|
|
2117
|
+
}
|
|
2118
|
+
function buildMessageContent(text, attachments) {
|
|
2119
|
+
if (attachments.length === 0) return text;
|
|
2120
|
+
const textSegments = [];
|
|
2121
|
+
if (text.trim()) {
|
|
2122
|
+
textSegments.push(text);
|
|
2123
|
+
}
|
|
2124
|
+
for (const attachment of attachments) {
|
|
2125
|
+
let fileText;
|
|
2126
|
+
if (attachment.uploadedPath) {
|
|
2127
|
+
const displayName = attachment.uploadedPath.split("/").pop() || attachment.name;
|
|
2128
|
+
fileText = `[\u9644\u4EF6: ${displayName}] \u5DF2\u4E0A\u4F20\u5230\u5DE5\u4F5C\u533A\u8DEF\u5F84: ${attachment.uploadedPath}`;
|
|
2129
|
+
} else if (attachment.textContent) {
|
|
2130
|
+
const MAX_TEXT_CHARS = 1e5;
|
|
2131
|
+
const content = attachment.textContent.length > MAX_TEXT_CHARS ? `${attachment.textContent.slice(0, MAX_TEXT_CHARS)}
|
|
2132
|
+
|
|
2133
|
+
[\u5185\u5BB9\u5DF2\u622A\u65AD\uFF0C\u539F\u59CB\u5171 ${attachment.textContent.length} \u5B57\u7B26]` : attachment.textContent;
|
|
2134
|
+
fileText = `[\u9644\u4EF6: ${attachment.name}]
|
|
2135
|
+
${content}`;
|
|
2136
|
+
} else {
|
|
2137
|
+
fileText = `[\u9644\u4EF6: ${attachment.name}]`;
|
|
2138
|
+
}
|
|
2139
|
+
textSegments.push(fileText);
|
|
2140
|
+
}
|
|
2141
|
+
return textSegments.join("\n\n");
|
|
2142
|
+
}
|
|
2143
|
+
function contentPreview(content, maxLen = 80) {
|
|
2144
|
+
const text = getTextContent(content).trim();
|
|
2145
|
+
return text.length > maxLen ? `${text.slice(0, maxLen)}\u2026` : text;
|
|
2146
|
+
}
|
|
2147
|
+
function transformSlashCommand(skillName, rawInput) {
|
|
2148
|
+
const prompt = rawInput.replace(`<skill>${skillName}</skill>`, "").trim();
|
|
2149
|
+
return prompt ? `\u8BF7\u4F7F\u7528 ${skillName} skill \u5B8C\u6210\u4EFB\u52A1
|
|
2150
|
+
${prompt}` : `\u8BF7\u4F7F\u7528 ${skillName} skill \u5B8C\u6210\u4EFB\u52A1`;
|
|
2151
|
+
}
|
|
2152
|
+
var ATTACHMENT_TAG_RE = /\[附件:\s*([^\]]+)\](?:[ \t]*已上传到工作区路径:[ \t]*([^\r\n]+))?/g;
|
|
2153
|
+
var CONTEXT_PART_RE = /^\[上下文:\s*([^\]]+)\]\n([\s\S]*)$/;
|
|
2154
|
+
function extractTextAttachments(content) {
|
|
2155
|
+
if (typeof content === "string") {
|
|
2156
|
+
const attachments2 = [];
|
|
2157
|
+
const contexts2 = [];
|
|
2158
|
+
const ctxMatch = CONTEXT_PART_RE.exec(content);
|
|
2159
|
+
let working = content;
|
|
2160
|
+
if (ctxMatch) {
|
|
2161
|
+
contexts2.push({ label: ctxMatch[1].trim(), content: ctxMatch[2].trim() });
|
|
2162
|
+
working = "";
|
|
2163
|
+
}
|
|
2164
|
+
for (const match of working.matchAll(ATTACHMENT_TAG_RE)) {
|
|
2165
|
+
attachments2.push({
|
|
2166
|
+
name: match[1].trim(),
|
|
2167
|
+
uploadedPath: match[2]?.trim() || void 0
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
return {
|
|
2171
|
+
cleanText: working.replace(ATTACHMENT_TAG_RE, ""),
|
|
2172
|
+
attachments: attachments2,
|
|
2173
|
+
contexts: contexts2
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
const attachments = [];
|
|
2177
|
+
const contexts = [];
|
|
2178
|
+
const cleanParts = [];
|
|
2179
|
+
for (const part of content) {
|
|
2180
|
+
if (part.type !== "text") continue;
|
|
2181
|
+
const ctxMatch = CONTEXT_PART_RE.exec(part.text);
|
|
2182
|
+
if (ctxMatch) {
|
|
2183
|
+
contexts.push({ label: ctxMatch[1].trim(), content: ctxMatch[2].trim() });
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
let hasAttachment = false;
|
|
2187
|
+
for (const match of part.text.matchAll(ATTACHMENT_TAG_RE)) {
|
|
2188
|
+
attachments.push({
|
|
2189
|
+
name: match[1].trim(),
|
|
2190
|
+
uploadedPath: match[2]?.trim() || void 0
|
|
2191
|
+
});
|
|
2192
|
+
hasAttachment = true;
|
|
2193
|
+
}
|
|
2194
|
+
if (hasAttachment) {
|
|
2195
|
+
const remaining = part.text.replace(ATTACHMENT_TAG_RE, "");
|
|
2196
|
+
if (remaining.trim()) cleanParts.push(remaining);
|
|
2197
|
+
} else {
|
|
2198
|
+
cleanParts.push(part.text);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
return { cleanText: cleanParts.join("\n"), attachments, contexts };
|
|
2202
|
+
}
|
|
2203
|
+
function getImageParts(content) {
|
|
2204
|
+
if (typeof content === "string") return [];
|
|
2205
|
+
return content.filter((part) => part.type === "image_url");
|
|
2206
|
+
}
|
|
2207
|
+
function getFileParts(content) {
|
|
2208
|
+
if (typeof content === "string") return [];
|
|
2209
|
+
return content.filter((part) => part.type === "file");
|
|
2210
|
+
}
|
|
2211
|
+
function groupMessagesByLoop(messages) {
|
|
2212
|
+
const groups = [];
|
|
2213
|
+
let currentGroup = null;
|
|
2214
|
+
for (const msg of messages) {
|
|
2215
|
+
const loop = msg.loop_name ?? "root";
|
|
2216
|
+
if (loop === "root") {
|
|
2217
|
+
currentGroup = { root: msg, childLoops: [] };
|
|
2218
|
+
groups.push(currentGroup);
|
|
2219
|
+
} else {
|
|
2220
|
+
if (!currentGroup) {
|
|
2221
|
+
currentGroup = { root: null, childLoops: [] };
|
|
2222
|
+
groups.push(currentGroup);
|
|
2223
|
+
}
|
|
2224
|
+
const existingLoop = currentGroup.childLoops.find((l) => l.loopName === loop);
|
|
2225
|
+
if (existingLoop) {
|
|
2226
|
+
existingLoop.messages.push(msg);
|
|
2227
|
+
} else {
|
|
2228
|
+
currentGroup.childLoops.push({ loopName: loop, messages: [msg] });
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
return groups;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// src/session/state.ts
|
|
2236
|
+
function createInitialSessionState(sessionId) {
|
|
2237
|
+
return {
|
|
2238
|
+
sessionId,
|
|
2239
|
+
turns: [],
|
|
2240
|
+
messages: [],
|
|
2241
|
+
isStreaming: false,
|
|
2242
|
+
status: null,
|
|
2243
|
+
mode: null,
|
|
2244
|
+
connection: "disconnected",
|
|
2245
|
+
errorMessage: null,
|
|
2246
|
+
askAnswers: {},
|
|
2247
|
+
agentLoops: {},
|
|
2248
|
+
activeCompaction: null
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
function parseAgentDescription(argumentsJson) {
|
|
2252
|
+
try {
|
|
2253
|
+
return JSON.parse(argumentsJson)?.description ?? "\u5B50\u667A\u80FD\u4F53";
|
|
2254
|
+
} catch {
|
|
2255
|
+
return "\u5B50\u667A\u80FD\u4F53";
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
function normalizedToolName(name) {
|
|
2259
|
+
const separatorIndex = name.lastIndexOf(":");
|
|
2260
|
+
return separatorIndex >= 0 ? name.slice(separatorIndex + 1) : name;
|
|
2261
|
+
}
|
|
2262
|
+
function inferLoopStatusFromMessages(messages) {
|
|
2263
|
+
const toolCalls = messages.flatMap((message) => message.tool_calls ?? []);
|
|
2264
|
+
if (messages.some((message) => message.status === "streaming")) return "running";
|
|
2265
|
+
if (toolCalls.some((toolCall) => toolCall.status === "awaiting_answer")) return "awaiting_answer";
|
|
2266
|
+
if (toolCalls.some((toolCall) => toolCall.status === "error")) return "error";
|
|
2267
|
+
if (toolCalls.some((toolCall) => toolCall.status === "cancelled")) return "cancelled";
|
|
2268
|
+
if (toolCalls.some((toolCall) => toolCall.status === "pending")) return "running";
|
|
2269
|
+
return "done";
|
|
2270
|
+
}
|
|
2271
|
+
function inferLoopStatusFromTurns(turns, messages) {
|
|
2272
|
+
const latestAgentNotification = [...turns].reverse().flatMap((turn) => turn.blocks).find((block) => {
|
|
2273
|
+
if (block.type !== "system_notification" || !isRecord3(block.content)) return false;
|
|
2274
|
+
return block.content.notification_type === "agent:start" || block.content.notification_type === "agent:end";
|
|
2275
|
+
});
|
|
2276
|
+
if (latestAgentNotification?.type === "system_notification" && isRecord3(latestAgentNotification.content)) {
|
|
2277
|
+
const notificationType = latestAgentNotification.content.notification_type;
|
|
2278
|
+
const status = latestAgentNotification.content.status;
|
|
2279
|
+
if (notificationType === "agent:start" || status === "running") return "running";
|
|
2280
|
+
if (status === "error") return "error";
|
|
2281
|
+
if (status === "cancelled") return "cancelled";
|
|
2282
|
+
}
|
|
2283
|
+
return inferLoopStatusFromMessages(messages);
|
|
2284
|
+
}
|
|
2285
|
+
function isRecord3(value) {
|
|
2286
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2287
|
+
}
|
|
2288
|
+
function parentForkToolCallIdFromTurn(turn) {
|
|
2289
|
+
if (typeof turn.parent_fork_tool_call_id === "string" && turn.parent_fork_tool_call_id.length > 0) {
|
|
2290
|
+
return turn.parent_fork_tool_call_id;
|
|
2291
|
+
}
|
|
2292
|
+
for (const block of turn.blocks) {
|
|
2293
|
+
if (block.type !== "system_notification" || !isRecord3(block.content)) continue;
|
|
2294
|
+
const metadata = block.content.metadata;
|
|
2295
|
+
if (!isRecord3(metadata)) continue;
|
|
2296
|
+
const parentId = metadata.parent_fork_tool_call_id;
|
|
2297
|
+
if (typeof parentId === "string" && parentId.length > 0) return parentId;
|
|
2298
|
+
}
|
|
2299
|
+
return null;
|
|
2300
|
+
}
|
|
2301
|
+
function buildMessageContent2(turn) {
|
|
2302
|
+
const textBlocks = turn.blocks.filter((block) => block.type === "text");
|
|
2303
|
+
if (textBlocks.length === 0) return "";
|
|
2304
|
+
if (textBlocks.length === 1) return normalizeMessageContent(textBlocks[0].content);
|
|
2305
|
+
return textBlocks.map((block) => {
|
|
2306
|
+
if (typeof block.content === "string") return block.content;
|
|
2307
|
+
return JSON.stringify(block.content);
|
|
2308
|
+
}).join("");
|
|
2309
|
+
}
|
|
2310
|
+
function askUserAnswerContent(turn) {
|
|
2311
|
+
const answerBlock = turn.blocks.find((block) => block.type === "ask_user_answer");
|
|
2312
|
+
if (!answerBlock || !isRecord3(answerBlock.content)) return null;
|
|
2313
|
+
const answer = answerBlock.content.answer;
|
|
2314
|
+
return typeof answer === "string" && answer.trim().length > 0 ? answer : null;
|
|
2315
|
+
}
|
|
2316
|
+
function buildReasoning(turn) {
|
|
2317
|
+
const thinking = turn.blocks.filter((block) => block.type === "thinking").map((block) => typeof block.content === "string" ? block.content : JSON.stringify(block.content)).filter(Boolean);
|
|
2318
|
+
if (thinking.length === 0) return void 0;
|
|
2319
|
+
return thinking.join("\n\n");
|
|
2320
|
+
}
|
|
2321
|
+
function isModeChangeContent(value) {
|
|
2322
|
+
return typeof value === "object" && value !== null && typeof value.from === "string" && typeof value.to === "string";
|
|
2323
|
+
}
|
|
2324
|
+
function extractModeFromBlocks(blocks) {
|
|
2325
|
+
const modeBlock = blocks.find((block) => block.type === "mode_change");
|
|
2326
|
+
if (modeBlock && isModeChangeContent(modeBlock.content)) {
|
|
2327
|
+
return modeBlock.content.to === "planning" || modeBlock.content.to === "executing" ? modeBlock.content.to : null;
|
|
2328
|
+
}
|
|
2329
|
+
return null;
|
|
2330
|
+
}
|
|
2331
|
+
function projectionToMessage(turn) {
|
|
2332
|
+
if (turn.kind === "compaction" && turn.compaction_id) {
|
|
2333
|
+
return {
|
|
2334
|
+
role: "assistant",
|
|
2335
|
+
content: turn.summary_preview ?? "",
|
|
2336
|
+
kind: "compaction",
|
|
2337
|
+
loop_name: turn.loop_id,
|
|
2338
|
+
entry_id: turn.turn_id,
|
|
2339
|
+
status: turn.status,
|
|
2340
|
+
compaction: {
|
|
2341
|
+
compaction_id: turn.compaction_id,
|
|
2342
|
+
summary_preview: turn.summary_preview,
|
|
2343
|
+
summary_full: turn.summary_full,
|
|
2344
|
+
archived_count: turn.archived_count,
|
|
2345
|
+
archived_files: turn.archived_files,
|
|
2346
|
+
archived_tool_calls: turn.archived_tool_calls,
|
|
2347
|
+
tokens_before: turn.tokens_before,
|
|
2348
|
+
tokens_after: turn.tokens_after,
|
|
2349
|
+
saved_ratio: turn.saved_ratio,
|
|
2350
|
+
trigger: turn.trigger,
|
|
2351
|
+
failure_reason: turn.failure_reason,
|
|
2352
|
+
fallback_applied: turn.fallback_applied
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
const planningBlock = turn.blocks.find(
|
|
2357
|
+
(block) => block.type === "mode_change" || block.type === "plan_status"
|
|
2358
|
+
);
|
|
2359
|
+
if (planningBlock) {
|
|
2360
|
+
if (planningBlock.type === "plan_status") {
|
|
2361
|
+
return {
|
|
2362
|
+
role: "tool",
|
|
2363
|
+
content: typeof planningBlock.content === "string" ? planningBlock.content : JSON.stringify(planningBlock.content ?? {}, null, 2),
|
|
2364
|
+
kind: "plan_status",
|
|
2365
|
+
loop_name: turn.loop_id,
|
|
2366
|
+
entry_id: turn.turn_id,
|
|
2367
|
+
status: turn.status
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
return {
|
|
2371
|
+
role: "assistant",
|
|
2372
|
+
content: typeof planningBlock.content === "string" ? planningBlock.content : JSON.stringify(planningBlock.content ?? {}),
|
|
2373
|
+
kind: planningBlock.type,
|
|
2374
|
+
loop_name: turn.loop_id,
|
|
2375
|
+
entry_id: turn.turn_id,
|
|
2376
|
+
status: turn.status
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
const askAnswer = askUserAnswerContent(turn);
|
|
2380
|
+
if (askAnswer !== null) {
|
|
2381
|
+
return {
|
|
2382
|
+
role: "user",
|
|
2383
|
+
content: askAnswer,
|
|
2384
|
+
blocks: turn.blocks,
|
|
2385
|
+
loop_name: turn.loop_id,
|
|
2386
|
+
entry_id: turn.turn_id,
|
|
2387
|
+
status: turn.status,
|
|
2388
|
+
...typeof turn.duration_ms === "number" ? { duration_ms: turn.duration_ms } : {},
|
|
2389
|
+
...turn.started_at ? { timestamp: turn.started_at } : {}
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
const content = buildMessageContent2(turn);
|
|
2393
|
+
const reasoning = buildReasoning(turn);
|
|
2394
|
+
const toolCalls = turn.tool_calls.length > 0 ? turn.tool_calls.map((toolCall) => ({
|
|
2395
|
+
id: toolCall.id,
|
|
2396
|
+
name: toolCall.tool_name,
|
|
2397
|
+
display_name: toolCall.display_name,
|
|
2398
|
+
arguments: toolCall.arguments,
|
|
2399
|
+
result: toolCall.result ?? void 0,
|
|
2400
|
+
pending_question_ref: toolCall.pending_question_ref ?? void 0,
|
|
2401
|
+
status: toolCall.status === "pending" || toolCall.status === "awaiting_answer" || toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled" ? toolCall.status : "pending",
|
|
2402
|
+
...typeof toolCall.duration_ms === "number" ? { duration_ms: toolCall.duration_ms } : {}
|
|
2403
|
+
})) : void 0;
|
|
2404
|
+
if (turn.role === "system" && !content && !toolCalls?.length) {
|
|
2405
|
+
return null;
|
|
2406
|
+
}
|
|
2407
|
+
return {
|
|
2408
|
+
role: turn.role === "system" ? "assistant" : turn.role,
|
|
2409
|
+
content,
|
|
2410
|
+
blocks: turn.blocks,
|
|
2411
|
+
...reasoning ? { reasoning } : {},
|
|
2412
|
+
...toolCalls ? { tool_calls: toolCalls } : {},
|
|
2413
|
+
loop_name: turn.loop_id,
|
|
2414
|
+
entry_id: turn.turn_id,
|
|
2415
|
+
status: turn.status,
|
|
2416
|
+
...typeof turn.duration_ms === "number" ? { duration_ms: turn.duration_ms } : {},
|
|
2417
|
+
...turn.started_at ? { timestamp: turn.started_at } : {},
|
|
2418
|
+
...turn.memory_refs?.length ? { memory_refs: turn.memory_refs } : {}
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
var messageProjectionCache = /* @__PURE__ */ new WeakMap();
|
|
2422
|
+
function cachedProjectionToMessage(turn) {
|
|
2423
|
+
if (messageProjectionCache.has(turn)) {
|
|
2424
|
+
return messageProjectionCache.get(turn) ?? null;
|
|
2425
|
+
}
|
|
2426
|
+
const message = projectionToMessage(turn);
|
|
2427
|
+
messageProjectionCache.set(turn, message);
|
|
2428
|
+
return message;
|
|
2429
|
+
}
|
|
2430
|
+
function rebuildAgentLoops(turns, messages) {
|
|
2431
|
+
const childLoopNames = [
|
|
2432
|
+
...new Set(turns.map((turn) => turn.loop_id).filter((name) => name !== "root"))
|
|
2433
|
+
];
|
|
2434
|
+
if (childLoopNames.length === 0) return {};
|
|
2435
|
+
const agentToolCalls = messages.filter((message) => message.role === "assistant" && (message.loop_name ?? "root") === "root").flatMap((message) => message.tool_calls ?? []).filter((toolCall) => normalizedToolName(toolCall.name) === "Agent");
|
|
2436
|
+
const loops = {};
|
|
2437
|
+
const agentToolCallsById = new Map(agentToolCalls.map((toolCall) => [toolCall.id, toolCall]));
|
|
2438
|
+
const explicitParentToolCallIds = new Set(
|
|
2439
|
+
turns.map(parentForkToolCallIdFromTurn).filter((id) => id !== null)
|
|
2440
|
+
);
|
|
2441
|
+
const usedToolCallIds = /* @__PURE__ */ new Set();
|
|
2442
|
+
for (const loopName of childLoopNames) {
|
|
2443
|
+
const loopMessages = messages.filter((message) => (message.loop_name ?? "root") === loopName);
|
|
2444
|
+
const loopTurns = turns.filter((turn) => turn.loop_id === loopName);
|
|
2445
|
+
const parentToolCallId = loopTurns.map(parentForkToolCallIdFromTurn).find(Boolean);
|
|
2446
|
+
const explicitToolCall = parentToolCallId && !usedToolCallIds.has(parentToolCallId) ? agentToolCallsById.get(parentToolCallId) ?? null : null;
|
|
2447
|
+
const fallbackToolCall = explicitToolCall === null ? agentToolCalls.find(
|
|
2448
|
+
(toolCall2) => !usedToolCallIds.has(toolCall2.id) && !explicitParentToolCallIds.has(toolCall2.id)
|
|
2449
|
+
) : null;
|
|
2450
|
+
const toolCall = explicitToolCall ?? fallbackToolCall;
|
|
2451
|
+
if (!toolCall) continue;
|
|
2452
|
+
usedToolCallIds.add(toolCall.id);
|
|
2453
|
+
loops[loopName] = {
|
|
2454
|
+
toolCallId: toolCall.id,
|
|
2455
|
+
description: parseAgentDescription(toolCall.arguments),
|
|
2456
|
+
status: inferLoopStatusFromTurns(loopTurns, loopMessages)
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
return loops;
|
|
2460
|
+
}
|
|
2461
|
+
function materialize(turns) {
|
|
2462
|
+
const projectedMessages = turns.map(cachedProjectionToMessage).filter((message) => message !== null);
|
|
2463
|
+
const messages = projectedMessages.filter((message, index, allMessages) => {
|
|
2464
|
+
if (!message.blocks?.some((block) => block.type === "ask_user_answer")) return true;
|
|
2465
|
+
const next = allMessages[index + 1];
|
|
2466
|
+
return !(next?.role === "user" && next.content === message.content);
|
|
2467
|
+
});
|
|
2468
|
+
const activeCompaction = [...turns].reverse().find(
|
|
2469
|
+
(turn) => turn.kind === "compaction" && turn.status === "streaming" && typeof turn.compaction_id === "string"
|
|
2470
|
+
);
|
|
2471
|
+
return {
|
|
2472
|
+
messages,
|
|
2473
|
+
agentLoops: rebuildAgentLoops(turns, projectedMessages),
|
|
2474
|
+
activeCompaction: activeCompaction ? {
|
|
2475
|
+
turn_id: activeCompaction.turn_id,
|
|
2476
|
+
status: activeCompaction.status,
|
|
2477
|
+
compaction_id: activeCompaction.compaction_id,
|
|
2478
|
+
summary_preview: activeCompaction.summary_preview,
|
|
2479
|
+
summary_full: activeCompaction.summary_full,
|
|
2480
|
+
archived_count: activeCompaction.archived_count,
|
|
2481
|
+
archived_files: activeCompaction.archived_files,
|
|
2482
|
+
archived_tool_calls: activeCompaction.archived_tool_calls,
|
|
2483
|
+
tokens_before: activeCompaction.tokens_before,
|
|
2484
|
+
tokens_after: activeCompaction.tokens_after,
|
|
2485
|
+
saved_ratio: activeCompaction.saved_ratio,
|
|
2486
|
+
trigger: activeCompaction.trigger,
|
|
2487
|
+
failure_reason: activeCompaction.failure_reason,
|
|
2488
|
+
fallback_applied: activeCompaction.fallback_applied
|
|
2489
|
+
} : null
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
var ERROR_ANCHOR_PREFIX = "error-anchor:";
|
|
2493
|
+
function orderTurns(turns) {
|
|
2494
|
+
for (let index = 1; index < turns.length; index += 1) {
|
|
2495
|
+
if (turns[index - 1].sequence > turns[index].sequence) {
|
|
2496
|
+
return [...turns].sort((left, right) => left.sequence - right.sequence);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
return turns;
|
|
2500
|
+
}
|
|
2501
|
+
function areAgentLoopsEqual(left, right) {
|
|
2502
|
+
if (!left) return Object.keys(right).length === 0;
|
|
2503
|
+
const leftEntries = Object.entries(left);
|
|
2504
|
+
const rightKeys = Object.keys(right);
|
|
2505
|
+
if (leftEntries.length !== rightKeys.length) return false;
|
|
2506
|
+
return leftEntries.every(([key, value]) => {
|
|
2507
|
+
const next = right[key];
|
|
2508
|
+
return next !== void 0 && value.toolCallId === next.toolCallId && value.description === next.description && value.status === next.status;
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
function withTurns(state, turns) {
|
|
2512
|
+
const orderedTurns = orderTurns(turns);
|
|
2513
|
+
const { messages, agentLoops, activeCompaction } = materialize(orderedTurns);
|
|
2514
|
+
const isWaitingForInput = orderedTurns.some(
|
|
2515
|
+
(turn) => turn.tool_calls.some((toolCall) => toolCall.status === "awaiting_answer")
|
|
2516
|
+
);
|
|
2517
|
+
const stableAgentLoops = areAgentLoopsEqual(state.agentLoops, agentLoops) ? state.agentLoops : agentLoops;
|
|
2518
|
+
const lastTurnId = orderedTurns[orderedTurns.length - 1]?.turn_id ?? null;
|
|
2519
|
+
const preservedErrors = lastTurnId ? state.messages.filter(
|
|
2520
|
+
(m) => m.role === "error" && typeof m.entry_id === "string" && m.entry_id.startsWith(`${ERROR_ANCHOR_PREFIX}${lastTurnId}:`)
|
|
2521
|
+
) : [];
|
|
2522
|
+
const mergedMessages = preservedErrors.length > 0 ? [...messages, ...preservedErrors] : messages;
|
|
2523
|
+
const latestMode = [...orderedTurns].reverse().map((turn) => extractModeFromBlocks(turn.blocks)).find((mode) => mode !== null);
|
|
2524
|
+
return {
|
|
2525
|
+
...state,
|
|
2526
|
+
turns: orderedTurns,
|
|
2527
|
+
messages: mergedMessages,
|
|
2528
|
+
agentLoops: stableAgentLoops,
|
|
2529
|
+
activeCompaction,
|
|
2530
|
+
mode: latestMode ?? state.mode,
|
|
2531
|
+
// 历史恢复时 chat:end(paused) 可能已不在 socket replay 缓存中;
|
|
2532
|
+
// awaiting_answer 是投影里的持久权威状态,必须据此恢复问答交互。
|
|
2533
|
+
status: isWaitingForInput ? "waiting_for_input" : state.status
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
var localTurnCounter = 0;
|
|
2537
|
+
function addUserMessage(state, content) {
|
|
2538
|
+
localTurnCounter += 1;
|
|
2539
|
+
const turnId = `local-user-${Date.now()}-${localTurnCounter}`;
|
|
2540
|
+
const optimisticTurn = {
|
|
2541
|
+
id: turnId,
|
|
2542
|
+
sequence: Math.max(0, ...state.turns.map((turn) => turn.sequence)) + 1,
|
|
2543
|
+
turn_id: turnId,
|
|
2544
|
+
loop_id: "root",
|
|
2545
|
+
role: "user",
|
|
2546
|
+
status: "streaming",
|
|
2547
|
+
blocks: [{ type: "text", content }],
|
|
2548
|
+
tool_calls: [],
|
|
2549
|
+
model: null,
|
|
2550
|
+
usage: null,
|
|
2551
|
+
duration_ms: 0
|
|
2552
|
+
};
|
|
2553
|
+
return withTurns(state, [...state.turns, optimisticTurn]);
|
|
2554
|
+
}
|
|
2555
|
+
function upsertTurn(state, turn) {
|
|
2556
|
+
let existing = [...state.turns];
|
|
2557
|
+
if (turn.role === "user" && !turn.turn_id.startsWith("local-user-")) {
|
|
2558
|
+
existing = existing.filter((t) => !t.turn_id.startsWith("local-user-"));
|
|
2559
|
+
}
|
|
2560
|
+
const index = existing.findIndex((item) => item.turn_id === turn.turn_id);
|
|
2561
|
+
if (index >= 0) {
|
|
2562
|
+
existing[index] = turn;
|
|
2563
|
+
} else {
|
|
2564
|
+
existing.push(turn);
|
|
2565
|
+
}
|
|
2566
|
+
return withTurns(state, existing);
|
|
2567
|
+
}
|
|
2568
|
+
function applyTurnPatch(state, patch) {
|
|
2569
|
+
const turn = patch.data.turn;
|
|
2570
|
+
if (!turn) return state;
|
|
2571
|
+
const existing = [...state.turns];
|
|
2572
|
+
const index = existing.findIndex((item) => item.turn_id === turn.turn_id);
|
|
2573
|
+
const lastSequence = index >= 0 ? existing[index].sequence : null;
|
|
2574
|
+
if (lastSequence !== null && patch.sequence <= lastSequence) {
|
|
2575
|
+
return state;
|
|
2576
|
+
}
|
|
2577
|
+
const nextTurn = { ...turn, sequence: patch.sequence };
|
|
2578
|
+
if (index >= 0) {
|
|
2579
|
+
existing[index] = nextTurn;
|
|
2580
|
+
} else {
|
|
2581
|
+
existing.push(nextTurn);
|
|
2582
|
+
}
|
|
2583
|
+
return withTurns(state, existing);
|
|
2584
|
+
}
|
|
2585
|
+
function addErrorMessage(state, content) {
|
|
2586
|
+
const anchorTurnId = state.turns[state.turns.length - 1]?.turn_id ?? null;
|
|
2587
|
+
const entry_id = anchorTurnId ? `${ERROR_ANCHOR_PREFIX}${anchorTurnId}:${Date.now()}` : void 0;
|
|
2588
|
+
return {
|
|
2589
|
+
...state,
|
|
2590
|
+
messages: [...state.messages, { role: "error", content, loop_name: "root", entry_id }]
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
function markInterrupted(state) {
|
|
2594
|
+
return markStreamingTurns(state, "interrupted", "cancelled");
|
|
2595
|
+
}
|
|
2596
|
+
function markFailed(state) {
|
|
2597
|
+
return markStreamingTurns(state, "failed", "error");
|
|
2598
|
+
}
|
|
2599
|
+
function markStreamingTurns(state, turnStatus, toolCallStatus) {
|
|
2600
|
+
const turns = state.turns.map((turn) => {
|
|
2601
|
+
if (turn.status !== "streaming") return turn;
|
|
2602
|
+
return {
|
|
2603
|
+
...turn,
|
|
2604
|
+
status: turnStatus,
|
|
2605
|
+
tool_calls: turn.tool_calls.map(
|
|
2606
|
+
(toolCall) => toolCall.status === "pending" || toolCall.status === "awaiting_answer" ? { ...toolCall, status: toolCallStatus } : toolCall
|
|
2607
|
+
)
|
|
2608
|
+
};
|
|
2609
|
+
});
|
|
2610
|
+
return withTurns(state, turns);
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// src/session/agent-session.ts
|
|
2614
|
+
var OOM_MESSAGE = "\u6C99\u76D2\u5185\u5B58\u4F7F\u7528\u8D85\u51FA\u9650\u5236\uFF0C\u5DF2\u81EA\u52A8\u91CD\u542F\u3002\u5982\u679C\u7ECF\u5E38\u89E6\u53D1\uFF0C\u53EF\u4EE5\u8054\u7CFB\u7BA1\u7406\u5458\u8C03\u6574\u989D\u5EA6\u3002";
|
|
2615
|
+
var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
|
|
2616
|
+
function isRecord4(value) {
|
|
2617
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2618
|
+
}
|
|
2619
|
+
function isOomText(value) {
|
|
2620
|
+
return typeof value === "string" && OOM_KEYWORDS_RE.test(value);
|
|
2621
|
+
}
|
|
2622
|
+
function parseJsonRecord(value) {
|
|
2623
|
+
if (isRecord4(value)) return value;
|
|
2624
|
+
if (typeof value !== "string") return null;
|
|
2625
|
+
try {
|
|
2626
|
+
const parsed = JSON.parse(value);
|
|
2627
|
+
return isRecord4(parsed) ? parsed : null;
|
|
2628
|
+
} catch {
|
|
2629
|
+
return null;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
function isOomResult(value) {
|
|
2633
|
+
if (Array.isArray(value)) {
|
|
2634
|
+
return value.some((part) => isOomResult(part));
|
|
2635
|
+
}
|
|
2636
|
+
if (typeof value === "object" && value !== null) {
|
|
2637
|
+
const record = value;
|
|
2638
|
+
if (record.exit_code === 137) return true;
|
|
2639
|
+
if (typeof record.text === "string" && isOomText(record.text)) return true;
|
|
2640
|
+
if (typeof record.content === "string" && isOomText(record.content)) return true;
|
|
2641
|
+
return false;
|
|
2642
|
+
}
|
|
2643
|
+
if (typeof value === "string") {
|
|
2644
|
+
const parsed = parseJsonRecord(value);
|
|
2645
|
+
if (parsed?.exit_code === 137) return true;
|
|
2646
|
+
return isOomText(value);
|
|
2647
|
+
}
|
|
2648
|
+
return false;
|
|
2649
|
+
}
|
|
2650
|
+
function isToolBridgeContent(value) {
|
|
2651
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.action === "string";
|
|
2652
|
+
}
|
|
2653
|
+
function sessionStatusFromChatEnd(status) {
|
|
2654
|
+
if (status === "paused") return "waiting_for_input";
|
|
2655
|
+
if (status === "interrupted") return "interrupted";
|
|
2656
|
+
if (status === "failed") return "failed";
|
|
2657
|
+
return "completed";
|
|
2658
|
+
}
|
|
2659
|
+
function extractTerminalChatEndStatus(events) {
|
|
2660
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
2661
|
+
const event = events[i];
|
|
2662
|
+
if (event?.type !== "chat:end") continue;
|
|
2663
|
+
const status = event.payload?.status;
|
|
2664
|
+
return typeof status === "string" ? status : "completed";
|
|
2665
|
+
}
|
|
2666
|
+
return null;
|
|
2667
|
+
}
|
|
2668
|
+
function isUiMetaLike(value) {
|
|
2669
|
+
return isRecord4(value) && ("resourceHTML" in value || "resourceUri" in value || "resourceURI" in value);
|
|
2670
|
+
}
|
|
2671
|
+
var AgentSession = class _AgentSession {
|
|
2672
|
+
sessionId;
|
|
2673
|
+
state;
|
|
2674
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2675
|
+
emitter = new Emitter();
|
|
2676
|
+
commands = new CommandRegistry();
|
|
2677
|
+
runtime;
|
|
2678
|
+
projectionBuilder = new ClientProjectionBuilder();
|
|
2679
|
+
pendingTurnPatches = /* @__PURE__ */ new Map();
|
|
2680
|
+
patchFlushHandle = null;
|
|
2681
|
+
patchFlushCancel = null;
|
|
2682
|
+
joined = false;
|
|
2683
|
+
joinPending = null;
|
|
2684
|
+
pendingReplayMessage = null;
|
|
2685
|
+
pendingReplayMode;
|
|
2686
|
+
recentChatEndAt = 0;
|
|
2687
|
+
disposed = false;
|
|
2688
|
+
deliveredCommandKeys = /* @__PURE__ */ new Set();
|
|
2689
|
+
deliveredToolPreviews = /* @__PURE__ */ new Map();
|
|
2690
|
+
oomNotifiedKeys = /* @__PURE__ */ new Set();
|
|
2691
|
+
seenToolCallIds = /* @__PURE__ */ new Set();
|
|
2692
|
+
finishedToolCallIds = /* @__PURE__ */ new Set();
|
|
2693
|
+
emittedMessageTurnIds = /* @__PURE__ */ new Set();
|
|
2694
|
+
constructor(sessionId, runtime, connection) {
|
|
2695
|
+
this.sessionId = sessionId;
|
|
2696
|
+
this.runtime = runtime;
|
|
2697
|
+
this.state = { ...createInitialSessionState(sessionId), connection };
|
|
2698
|
+
}
|
|
2699
|
+
/** 从一份静态 turn 快照构造只展示历史的会话(例如公开分享页)。 */
|
|
2700
|
+
static fromTurns(sessionId, turns) {
|
|
2701
|
+
const readonlyError = () => {
|
|
2702
|
+
throw new Error("\u9759\u6001\u4F1A\u8BDD\u53EA\u7528\u4E8E\u5C55\u793A\u5386\u53F2\uFF0C\u4E0D\u80FD\u6267\u884C\u4F1A\u8BDD\u64CD\u4F5C");
|
|
2703
|
+
};
|
|
2704
|
+
const session = new _AgentSession(
|
|
2705
|
+
sessionId,
|
|
2706
|
+
{
|
|
2707
|
+
emitEvent: readonlyError,
|
|
2708
|
+
emitWithAck: async () => readonlyError(),
|
|
2709
|
+
ensureConnected: readonlyError,
|
|
2710
|
+
fetchTurns: async () => turns,
|
|
2711
|
+
onDispose: () => void 0
|
|
2712
|
+
},
|
|
2713
|
+
"connected"
|
|
2714
|
+
);
|
|
2715
|
+
session.state = {
|
|
2716
|
+
...withTurns(session.state, turns),
|
|
2717
|
+
status: "completed"
|
|
2718
|
+
};
|
|
2719
|
+
return session;
|
|
2720
|
+
}
|
|
2721
|
+
// ---------------------------------------------------------------- 状态订阅
|
|
2722
|
+
/** 返回当前会话快照。引用稳定:状态未变化时返回同一对象。 */
|
|
2723
|
+
getState() {
|
|
2724
|
+
return this.state;
|
|
2725
|
+
}
|
|
2726
|
+
/** 订阅状态变化。返回取消订阅函数。 */
|
|
2727
|
+
subscribe(listener) {
|
|
2728
|
+
this.listeners.add(listener);
|
|
2729
|
+
return () => {
|
|
2730
|
+
this.listeners.delete(listener);
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
setState(next) {
|
|
2734
|
+
if (next === this.state) return;
|
|
2735
|
+
this.state = next;
|
|
2736
|
+
for (const listener of [...this.listeners]) {
|
|
2737
|
+
try {
|
|
2738
|
+
listener();
|
|
2739
|
+
} catch (error) {
|
|
2740
|
+
console.warn("[agent-client] \u72B6\u6001\u8BA2\u9605\u56DE\u8C03\u629B\u51FA\u5F02\u5E38\uFF1A", error);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
update(fn) {
|
|
2745
|
+
this.setState(fn(this.state));
|
|
2746
|
+
}
|
|
2747
|
+
// ---------------------------------------------------------------- 语义事件
|
|
2748
|
+
/** 监听语义事件(底层逃生舱,见 events.ts)。返回取消函数。 */
|
|
2749
|
+
on(name, handler) {
|
|
2750
|
+
return this.emitter.on(name, handler);
|
|
2751
|
+
}
|
|
2752
|
+
off(name, handler) {
|
|
2753
|
+
this.emitter.off(name, handler);
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* 监听智能体下发给宿主页面的指令。
|
|
2757
|
+
*
|
|
2758
|
+
* action 是技能作者在工具返回值 `_meta.bridge` 里写的契约字符串(如
|
|
2759
|
+
* "map.highlight"),由你和技能作者约定;注册前到达的指令会被缓冲补投。
|
|
2760
|
+
*/
|
|
2761
|
+
onCommand(action, handler) {
|
|
2762
|
+
return this.commands.onCommand(action, handler);
|
|
2763
|
+
}
|
|
2764
|
+
// ---------------------------------------------------------------- 动作
|
|
2765
|
+
/** 发送一条消息。字符串或多模态内容均可。 */
|
|
2766
|
+
async send(content, options = {}) {
|
|
2767
|
+
this.runtime.ensureConnected();
|
|
2768
|
+
if (this.state.isStreaming) {
|
|
2769
|
+
this.update((s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002"));
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
await this.ensureIdleBeforeSend();
|
|
2773
|
+
if (this.state.isStreaming) {
|
|
2774
|
+
this.update((s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002"));
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
const askUserAnswer = options.askUserAnswer;
|
|
2778
|
+
if (!(askUserAnswer && typeof askUserAnswer.tool_call_id === "string")) {
|
|
2779
|
+
this.update((s) => addUserMessage(s, content));
|
|
2780
|
+
}
|
|
2781
|
+
this.update((s) => ({ ...s, isStreaming: true }));
|
|
2782
|
+
if (askUserAnswer && typeof askUserAnswer.tool_call_id === "string") {
|
|
2783
|
+
const { tool_call_id, ...rest } = askUserAnswer;
|
|
2784
|
+
this.update((s) => ({
|
|
2785
|
+
...s,
|
|
2786
|
+
askAnswers: { ...s.askAnswers, [tool_call_id]: rest }
|
|
2787
|
+
}));
|
|
2788
|
+
}
|
|
2789
|
+
if (!options.replayDecision) {
|
|
2790
|
+
this.pendingReplayMessage = content;
|
|
2791
|
+
this.pendingReplayMode = options.mode;
|
|
2792
|
+
}
|
|
2793
|
+
const payload = {
|
|
2794
|
+
session_id: this.sessionId,
|
|
2795
|
+
message: content,
|
|
2796
|
+
mode: options.mode,
|
|
2797
|
+
askuser_answer: askUserAnswer,
|
|
2798
|
+
model: options.model,
|
|
2799
|
+
thinking_override: options.thinkingOverride,
|
|
2800
|
+
whatif: options.whatif,
|
|
2801
|
+
replay_decision: options.replayDecision
|
|
2802
|
+
};
|
|
2803
|
+
try {
|
|
2804
|
+
await this.ensureJoined();
|
|
2805
|
+
} catch (error) {
|
|
2806
|
+
this.update(
|
|
2807
|
+
(s) => addErrorMessage(
|
|
2808
|
+
{ ...s, isStreaming: false, status: "failed" },
|
|
2809
|
+
error instanceof Error && error.message ? error.message : "\u4F1A\u8BDD\u8BA2\u9605\u5931\u8D25\uFF0C\u6D88\u606F\u672A\u53D1\u9001"
|
|
2810
|
+
)
|
|
2811
|
+
);
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
this.runtime.emitEvent("chat:send", payload);
|
|
2815
|
+
}
|
|
2816
|
+
/** 智能体运行期间追加补充说明(不打断当前回复)。 */
|
|
2817
|
+
append(text) {
|
|
2818
|
+
this.runtime.ensureConnected();
|
|
2819
|
+
this.runtime.emitEvent("chat:append", { session_id: this.sessionId, message: text });
|
|
2820
|
+
this.update((s) => addUserMessage(s, text));
|
|
2821
|
+
}
|
|
2822
|
+
/** 停止当前回复。 */
|
|
2823
|
+
stop() {
|
|
2824
|
+
return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) => response ?? {});
|
|
2825
|
+
}
|
|
2826
|
+
/** 手动触发上下文压缩。 */
|
|
2827
|
+
compact() {
|
|
2828
|
+
return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {});
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* 把一段业务数据作为附件放进聊天输入框(比如地图上选中的点位)。
|
|
2832
|
+
* 附件随下一条消息一起发给智能体。需要 ChatView / useChat 渲染层配合展示;
|
|
2833
|
+
* 无 UI 场景可直接用 send()。
|
|
2834
|
+
*/
|
|
2835
|
+
attach(label, data) {
|
|
2836
|
+
const content = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
2837
|
+
this.emitter.emit("attachRequested", { label, content });
|
|
2838
|
+
}
|
|
2839
|
+
/** 往聊天输入框追加一段文字(不发送)。 */
|
|
2840
|
+
insertText(text) {
|
|
2841
|
+
this.emitter.emit("insertTextRequested", { text });
|
|
2842
|
+
}
|
|
2843
|
+
/** 释放订阅与监听器。之后该实例不再接收任何事件。 */
|
|
2844
|
+
dispose() {
|
|
2845
|
+
if (this.disposed) return;
|
|
2846
|
+
this.disposed = true;
|
|
2847
|
+
this.cancelPatchFlush();
|
|
2848
|
+
this.listeners.clear();
|
|
2849
|
+
this.emitter.clear();
|
|
2850
|
+
this.commands.clear();
|
|
2851
|
+
this.runtime.onDispose(this);
|
|
2852
|
+
}
|
|
2853
|
+
get isDisposed() {
|
|
2854
|
+
return this.disposed;
|
|
2855
|
+
}
|
|
2856
|
+
// ------------------------------------------------------- Hub 回调(内部)
|
|
2857
|
+
/** @internal 连接状态变化(同 client 下所有会话共享一条连接)。 */
|
|
2858
|
+
_setConnection(connection) {
|
|
2859
|
+
if (this.state.connection === connection) return;
|
|
2860
|
+
this.update((s) => ({ ...s, connection }));
|
|
2861
|
+
}
|
|
2862
|
+
/** @internal socket 重连后由 Hub 调用:重新加入房间并补数。 */
|
|
2863
|
+
async _rejoinAfterReconnect() {
|
|
2864
|
+
const turnsBeforeRejoin = this.state.turns;
|
|
2865
|
+
this.joined = false;
|
|
2866
|
+
this.joinPending = null;
|
|
2867
|
+
try {
|
|
2868
|
+
await this.ensureJoined();
|
|
2869
|
+
await this.syncTurnsFromHistory({
|
|
2870
|
+
clearOnEmpty: true,
|
|
2871
|
+
preserveChangesSince: turnsBeforeRejoin
|
|
2872
|
+
});
|
|
2873
|
+
} catch {
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
/** @internal 首次连接:加载历史 + 加入房间。 */
|
|
2877
|
+
async _bootstrap() {
|
|
2878
|
+
await this.syncTurnsFromHistory({ suppressErrors: false });
|
|
2879
|
+
await this.ensureJoined();
|
|
2880
|
+
}
|
|
2881
|
+
/** @internal */
|
|
2882
|
+
_handleTurnStart(turn) {
|
|
2883
|
+
this.markRunningIfIdle();
|
|
2884
|
+
this.applyTurn(turn);
|
|
2885
|
+
}
|
|
2886
|
+
/** @internal */
|
|
2887
|
+
_handleTurnPatch(patch) {
|
|
2888
|
+
this.markRunningIfIdle();
|
|
2889
|
+
if (patch.data.workspace_changed) {
|
|
2890
|
+
this.emitter.emit("workspaceChanged", {});
|
|
2891
|
+
}
|
|
2892
|
+
const current = this.pendingTurnPatches.get(patch.turn_id);
|
|
2893
|
+
if (!current || patch.sequence >= current.sequence) {
|
|
2894
|
+
this.pendingTurnPatches.set(patch.turn_id, patch);
|
|
2895
|
+
}
|
|
2896
|
+
this.schedulePatchFlush();
|
|
2897
|
+
}
|
|
2898
|
+
/** @internal */
|
|
2899
|
+
_handleTurnEnd(turn) {
|
|
2900
|
+
this.applyTurn(turn);
|
|
2901
|
+
}
|
|
2902
|
+
/** @internal 历史回放通道(event_format: raw)。 */
|
|
2903
|
+
_handleTurnEvents(events) {
|
|
2904
|
+
if (!events.length) return;
|
|
2905
|
+
const terminalChatEndStatus = extractTerminalChatEndStatus(events);
|
|
2906
|
+
if (!terminalChatEndStatus) {
|
|
2907
|
+
this.markRunningIfIdle();
|
|
2908
|
+
}
|
|
2909
|
+
if (this.state.turns.length) {
|
|
2910
|
+
this.projectionBuilder.seedSequence(Math.max(...this.state.turns.map((t) => t.sequence)));
|
|
2911
|
+
}
|
|
2912
|
+
const updates = this.projectionBuilder.processBatch(events);
|
|
2913
|
+
for (const update of updates) {
|
|
2914
|
+
if (update.kind === "upsert") {
|
|
2915
|
+
this.applyTurn(update.turn);
|
|
2916
|
+
} else if (update.kind === "workspace_changed") {
|
|
2917
|
+
this.emitter.emit("workspaceChanged", {});
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
if (terminalChatEndStatus) {
|
|
2921
|
+
this.finishStreaming(terminalChatEndStatus);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
/** @internal */
|
|
2925
|
+
_handleChatStart() {
|
|
2926
|
+
this.update((s) => ({ ...s, status: "running", errorMessage: null, isStreaming: true }));
|
|
2927
|
+
}
|
|
2928
|
+
/** @internal */
|
|
2929
|
+
_handleChatEnd(status) {
|
|
2930
|
+
this.flushPatches();
|
|
2931
|
+
this.finishStreaming(status);
|
|
2932
|
+
this.recentChatEndAt = Date.now();
|
|
2933
|
+
void this.syncTurnsFromHistory();
|
|
2934
|
+
this.emitter.emit("chatEnd", { status });
|
|
2935
|
+
}
|
|
2936
|
+
/** @internal */
|
|
2937
|
+
_handleSystemError(message) {
|
|
2938
|
+
this.flushPatches();
|
|
2939
|
+
const finalMessage = isOomText(message) ? OOM_MESSAGE : message;
|
|
2940
|
+
this.update(
|
|
2941
|
+
(s) => addErrorMessage(markFailed({ ...s, status: "failed", errorMessage: finalMessage, isStreaming: false }), finalMessage)
|
|
2942
|
+
);
|
|
2943
|
+
this.recentChatEndAt = Date.now();
|
|
2944
|
+
this.emitter.emit("error", { message: finalMessage });
|
|
2945
|
+
}
|
|
2946
|
+
/** @internal */
|
|
2947
|
+
_handleTaskUpdated(tasks) {
|
|
2948
|
+
this.emitter.emit("taskListUpdated", { tasks });
|
|
2949
|
+
}
|
|
2950
|
+
/** @internal */
|
|
2951
|
+
_handleWorkspaceFilesChanged(filePath) {
|
|
2952
|
+
this.emitter.emit("workspaceChanged", { filePath });
|
|
2953
|
+
}
|
|
2954
|
+
/** @internal */
|
|
2955
|
+
_handleSessionUpdated(data) {
|
|
2956
|
+
if (data.status) {
|
|
2957
|
+
this.update((s) => ({ ...s, status: data.status ?? s.status }));
|
|
2958
|
+
}
|
|
2959
|
+
this.emitter.emit("sessionUpdated", {
|
|
2960
|
+
intent: data.intent,
|
|
2961
|
+
status: data.status,
|
|
2962
|
+
model: data.model,
|
|
2963
|
+
boundSkillId: data.bound_skill_id,
|
|
2964
|
+
replayState: data.replay_state
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
/** @internal */
|
|
2968
|
+
_handleRewind(checkpointId) {
|
|
2969
|
+
this.update((s) => ({ ...s, mode: null, errorMessage: null }));
|
|
2970
|
+
void this.syncTurnsFromHistory({ clearOnEmpty: true });
|
|
2971
|
+
this.emitter.emit("rewind", { checkpointId });
|
|
2972
|
+
}
|
|
2973
|
+
/** @internal */
|
|
2974
|
+
_handleReplayMismatch(data) {
|
|
2975
|
+
const message = this.pendingReplayMessage ?? data.actual_message ?? "";
|
|
2976
|
+
const mode = this.pendingReplayMode;
|
|
2977
|
+
this.pendingReplayMessage = null;
|
|
2978
|
+
this.pendingReplayMode = void 0;
|
|
2979
|
+
const respond = (decision) => {
|
|
2980
|
+
this.runtime.emitEvent("chat:send", {
|
|
2981
|
+
session_id: this.sessionId,
|
|
2982
|
+
message,
|
|
2983
|
+
mode,
|
|
2984
|
+
replay_decision: decision
|
|
2985
|
+
});
|
|
2986
|
+
};
|
|
2987
|
+
const handled = this.emitter.emitFirst("replayMismatch", {
|
|
2988
|
+
actualMessage: data.actual_message,
|
|
2989
|
+
expectedMessage: data.expected_message,
|
|
2990
|
+
respond
|
|
2991
|
+
});
|
|
2992
|
+
if (!handled) {
|
|
2993
|
+
respond("continue_replay");
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
/** @internal */
|
|
2997
|
+
_handleSystemNotification(loopId, notification) {
|
|
2998
|
+
const notificationType = typeof notification.notification_type === "string" ? notification.notification_type : "system_notification";
|
|
2999
|
+
const title = typeof notification.title === "string" ? notification.title : notificationType;
|
|
3000
|
+
const detail = typeof notification.detail === "string" ? notification.detail : notification.detail == null ? void 0 : JSON.stringify(notification.detail);
|
|
3001
|
+
const status = notification.status === "running" || notification.status === "done" || notification.status === "error" ? notification.status : "info";
|
|
3002
|
+
this.emitter.emit("notification", {
|
|
3003
|
+
type: notificationType,
|
|
3004
|
+
title,
|
|
3005
|
+
detail,
|
|
3006
|
+
status,
|
|
3007
|
+
loopId,
|
|
3008
|
+
metadata: isRecord4(notification.metadata) ? notification.metadata : void 0
|
|
3009
|
+
});
|
|
3010
|
+
if (notificationType === "bg:started" && isRecord4(notification.metadata)) {
|
|
3011
|
+
const taskId = typeof notification.metadata.task_id === "string" ? notification.metadata.task_id : "";
|
|
3012
|
+
if (taskId) {
|
|
3013
|
+
this.emitter.emit("backgroundTask", {
|
|
3014
|
+
id: taskId,
|
|
3015
|
+
command: typeof notification.metadata.command === "string" ? notification.metadata.command : "",
|
|
3016
|
+
description: typeof notification.metadata.description === "string" ? notification.metadata.description : void 0,
|
|
3017
|
+
status: "running",
|
|
3018
|
+
elapsedSeconds: 0
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
/** @internal */
|
|
3024
|
+
_handleBackgroundTask(task) {
|
|
3025
|
+
this.emitter.emit("backgroundTask", task);
|
|
3026
|
+
}
|
|
3027
|
+
/** @internal */
|
|
3028
|
+
_handleArtifact(event) {
|
|
3029
|
+
this.emitter.emit("artifact", event);
|
|
3030
|
+
}
|
|
3031
|
+
/** @internal 加入房间的应答里带 is_running,据此校准流式状态。 */
|
|
3032
|
+
_applyRunningState(isRunning) {
|
|
3033
|
+
if (isRunning) {
|
|
3034
|
+
this.update((s) => ({ ...s, status: "running", isStreaming: true }));
|
|
3035
|
+
} else if (this.state.isStreaming) {
|
|
3036
|
+
this.update((s) => ({ ...s, isStreaming: false }));
|
|
3037
|
+
void this.syncTurnsFromHistory();
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
// ---------------------------------------------------------------- 内部实现
|
|
3041
|
+
markRunningIfIdle() {
|
|
3042
|
+
if (!this.state.isStreaming) {
|
|
3043
|
+
this.update((s) => ({ ...s, status: "running", isStreaming: true }));
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
finishStreaming(status) {
|
|
3047
|
+
const sessionStatus = sessionStatusFromChatEnd(status);
|
|
3048
|
+
this.update((s) => {
|
|
3049
|
+
let next = { ...s, status: sessionStatus, isStreaming: false };
|
|
3050
|
+
if (status === "interrupted") next = markInterrupted(next);
|
|
3051
|
+
else if (status === "failed") next = markFailed(next);
|
|
3052
|
+
return next;
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
applyTurn(turn) {
|
|
3056
|
+
this.flushPatches();
|
|
3057
|
+
const previous = this.state;
|
|
3058
|
+
this.update((s) => upsertTurn(s, turn));
|
|
3059
|
+
this.emitTurnDerivedEvents(previous, turn);
|
|
3060
|
+
}
|
|
3061
|
+
schedulePatchFlush() {
|
|
3062
|
+
if (this.patchFlushHandle != null) return;
|
|
3063
|
+
const flush = () => {
|
|
3064
|
+
this.patchFlushHandle = null;
|
|
3065
|
+
this.patchFlushCancel = null;
|
|
3066
|
+
this.flushPatches();
|
|
3067
|
+
};
|
|
3068
|
+
if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
|
|
3069
|
+
const handle2 = window.requestAnimationFrame(flush);
|
|
3070
|
+
this.patchFlushHandle = handle2;
|
|
3071
|
+
this.patchFlushCancel = () => window.cancelAnimationFrame(handle2);
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
const handle = globalThis.setTimeout(flush, 33);
|
|
3075
|
+
this.patchFlushHandle = handle;
|
|
3076
|
+
this.patchFlushCancel = () => globalThis.clearTimeout(handle);
|
|
3077
|
+
}
|
|
3078
|
+
cancelPatchFlush() {
|
|
3079
|
+
this.patchFlushCancel?.();
|
|
3080
|
+
this.patchFlushHandle = null;
|
|
3081
|
+
this.patchFlushCancel = null;
|
|
3082
|
+
this.pendingTurnPatches.clear();
|
|
3083
|
+
}
|
|
3084
|
+
flushPatches() {
|
|
3085
|
+
if (this.pendingTurnPatches.size === 0) return;
|
|
3086
|
+
const patches = [...this.pendingTurnPatches.values()].sort(
|
|
3087
|
+
(left, right) => left.sequence - right.sequence
|
|
3088
|
+
);
|
|
3089
|
+
this.pendingTurnPatches.clear();
|
|
3090
|
+
for (const patch of patches) {
|
|
3091
|
+
const previous = this.state;
|
|
3092
|
+
this.update((s) => applyTurnPatch(s, patch));
|
|
3093
|
+
const turn = patch.data.turn;
|
|
3094
|
+
if (turn) {
|
|
3095
|
+
this.emitTurnDerivedEvents(previous, turn);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
if (this.patchFlushCancel) {
|
|
3099
|
+
this.patchFlushCancel();
|
|
3100
|
+
this.patchFlushHandle = null;
|
|
3101
|
+
this.patchFlushCancel = null;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
/** turn 数据落地后统一派生语义事件(指令 / 工具调用 / 消息 / 模式 / 预览 / OOM)。 */
|
|
3105
|
+
emitTurnDerivedEvents(previous, turn) {
|
|
3106
|
+
const mode = extractModeFromBlocks(turn.blocks);
|
|
3107
|
+
if (mode && previous.mode !== mode) {
|
|
3108
|
+
this.emitter.emit("modeChange", { mode });
|
|
3109
|
+
}
|
|
3110
|
+
for (const block of turn.blocks) {
|
|
3111
|
+
if (block.type === "tool_result" && isOomResult(block.content)) {
|
|
3112
|
+
this.notifySandboxOom(block.tool_call_id ?? turn.id);
|
|
3113
|
+
}
|
|
3114
|
+
if (block.type === "tool_bridge" && isToolBridgeContent(block.content)) {
|
|
3115
|
+
this.dispatchCommand(turn, block);
|
|
3116
|
+
continue;
|
|
3117
|
+
}
|
|
3118
|
+
if (block.type === "tool_ui" && block.tool_call_id && isUiMetaLike(block.content)) {
|
|
3119
|
+
const ui = block.content;
|
|
3120
|
+
if (ui.target !== "preview") continue;
|
|
3121
|
+
const previewType = ui.resourceHTML ? "resource-html" : "resource-uri";
|
|
3122
|
+
const previewContent = ui.resourceHTML ?? ui.resourceUri ?? ui.resourceURI;
|
|
3123
|
+
if (!previewContent) continue;
|
|
3124
|
+
const key = `${block.tool_call_id}:${previewType}`;
|
|
3125
|
+
if (this.deliveredToolPreviews.get(key) === previewContent) continue;
|
|
3126
|
+
this.deliveredToolPreviews.set(key, previewContent);
|
|
3127
|
+
this.emitter.emit("toolPreview", {
|
|
3128
|
+
toolCallId: block.tool_call_id,
|
|
3129
|
+
type: previewType,
|
|
3130
|
+
content: previewContent,
|
|
3131
|
+
title: ui.title ?? "\u5DE5\u5177\u9884\u89C8"
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
if (block.type === "system_notification" && isRecord4(block.content)) {
|
|
3135
|
+
this._handleSystemNotification(turn.loop_id, block.content);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
for (const toolCall of turn.tool_calls) {
|
|
3139
|
+
const info = {
|
|
3140
|
+
id: toolCall.id,
|
|
3141
|
+
name: toolCall.tool_name,
|
|
3142
|
+
display_name: toolCall.display_name,
|
|
3143
|
+
arguments: toolCall.arguments,
|
|
3144
|
+
result: toolCall.result ?? void 0,
|
|
3145
|
+
status: toolCall.status === "pending" || toolCall.status === "awaiting_answer" || toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled" ? toolCall.status : "pending"
|
|
3146
|
+
};
|
|
3147
|
+
if (!this.seenToolCallIds.has(toolCall.id)) {
|
|
3148
|
+
this.seenToolCallIds.add(toolCall.id);
|
|
3149
|
+
this.emitter.emit("toolCall", { toolCall: info, turn });
|
|
3150
|
+
}
|
|
3151
|
+
const finished = toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled";
|
|
3152
|
+
if (finished && !this.finishedToolCallIds.has(toolCall.id)) {
|
|
3153
|
+
this.finishedToolCallIds.add(toolCall.id);
|
|
3154
|
+
this.emitter.emit("toolResult", { toolCall: info, turn });
|
|
3155
|
+
if (isOomResult(toolCall.result)) {
|
|
3156
|
+
this.notifySandboxOom(toolCall.id);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
if (turn.status !== "streaming" && !this.emittedMessageTurnIds.has(turn.turn_id)) {
|
|
3161
|
+
const message = this.state.messages.find((m) => m.entry_id === turn.turn_id);
|
|
3162
|
+
if (message) {
|
|
3163
|
+
this.emittedMessageTurnIds.add(turn.turn_id);
|
|
3164
|
+
this.emitter.emit("message", { message, turn });
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
dispatchCommand(turn, block) {
|
|
3169
|
+
if (!isToolBridgeContent(block.content)) return;
|
|
3170
|
+
const { action, payload } = block.content;
|
|
3171
|
+
let payloadKey = "";
|
|
3172
|
+
try {
|
|
3173
|
+
payloadKey = JSON.stringify(payload ?? null);
|
|
3174
|
+
} catch {
|
|
3175
|
+
payloadKey = String(payload ?? "");
|
|
3176
|
+
}
|
|
3177
|
+
const deliveryKey = [block.tool_call_id ?? turn.id, action, payloadKey].join("\0");
|
|
3178
|
+
if (this.deliveredCommandKeys.has(deliveryKey)) return;
|
|
3179
|
+
this.deliveredCommandKeys.add(deliveryKey);
|
|
3180
|
+
const meta = { toolCallId: block.tool_call_id ?? void 0 };
|
|
3181
|
+
this.emitter.emit("command", { action, payload, toolCallId: meta.toolCallId });
|
|
3182
|
+
this.commands.dispatch(action, payload, meta);
|
|
3183
|
+
}
|
|
3184
|
+
notifySandboxOom(key) {
|
|
3185
|
+
if (this.oomNotifiedKeys.has(key)) return;
|
|
3186
|
+
this.oomNotifiedKeys.add(key);
|
|
3187
|
+
this.update((s) => addErrorMessage(s, OOM_MESSAGE));
|
|
3188
|
+
this.emitter.emit("sandboxOom", { toolCallId: key });
|
|
3189
|
+
}
|
|
3190
|
+
async syncTurnsFromHistory(options = {}) {
|
|
3191
|
+
const { clearOnEmpty = false, suppressErrors = true, preserveChangesSince } = options;
|
|
3192
|
+
const turnsAtRequestStart = this.state.turns;
|
|
3193
|
+
try {
|
|
3194
|
+
const turns = await this.runtime.fetchTurns(this.sessionId);
|
|
3195
|
+
if (turns.length === 0 && !clearOnEmpty) return;
|
|
3196
|
+
this.update((state) => {
|
|
3197
|
+
const mergeBaseline = preserveChangesSince ?? turnsAtRequestStart;
|
|
3198
|
+
if (state.turns === mergeBaseline) return withTurns(state, turns);
|
|
3199
|
+
const historyById = new Map(turns.map((turn) => [turn.turn_id, turn]));
|
|
3200
|
+
let merged = withTurns(state, turns);
|
|
3201
|
+
for (const current of state.turns) {
|
|
3202
|
+
const fromHistory = historyById.get(current.turn_id);
|
|
3203
|
+
if (!fromHistory || current.sequence > fromHistory.sequence) {
|
|
3204
|
+
merged = upsertTurn(merged, current);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
return merged;
|
|
3208
|
+
});
|
|
3209
|
+
this.replayCommandsFromTurns(turns);
|
|
3210
|
+
} catch (error) {
|
|
3211
|
+
if (!suppressErrors) throw error;
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
/**
|
|
3215
|
+
* 把历史里的指令补投给宿主页面。
|
|
3216
|
+
*
|
|
3217
|
+
* 重开会话、断线补数时,地图高亮这类"页面当前状态"必须能恢复——
|
|
3218
|
+
* 否则刷新一次页面,之前智能体让页面做的事就全丢了。
|
|
3219
|
+
* 只回放指令:toolCall / message 这些事件如果按历史全量重放,
|
|
3220
|
+
* 监听器会在挂载瞬间收到几百条陈年事件。
|
|
3221
|
+
* dispatch 内部按 deliveredCommandKeys 去重,重复调用安全。
|
|
3222
|
+
*/
|
|
3223
|
+
replayCommandsFromTurns(turns) {
|
|
3224
|
+
for (const turn of turns) {
|
|
3225
|
+
for (const block of turn.blocks) {
|
|
3226
|
+
if (block.type === "tool_bridge" && isToolBridgeContent(block.content)) {
|
|
3227
|
+
this.dispatchCommand(turn, block);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
ensureJoined() {
|
|
3233
|
+
if (this.joined) return Promise.resolve();
|
|
3234
|
+
if (this.joinPending) return this.joinPending;
|
|
3235
|
+
const join = this.runtime.emitWithAck(
|
|
3236
|
+
"session:subscribe",
|
|
3237
|
+
{ session_id: this.sessionId, event_format: "raw", resume: true },
|
|
3238
|
+
5e3
|
|
3239
|
+
).then((response) => {
|
|
3240
|
+
if (response && response.ok === false) {
|
|
3241
|
+
throw new Error(
|
|
3242
|
+
typeof response.message === "string" && response.message ? response.message : "Session subscription failed"
|
|
3243
|
+
);
|
|
3244
|
+
}
|
|
3245
|
+
this.joined = true;
|
|
3246
|
+
this._applyRunningState(Boolean(response?.is_running));
|
|
3247
|
+
}).finally(() => {
|
|
3248
|
+
this.joinPending = null;
|
|
3249
|
+
});
|
|
3250
|
+
this.joinPending = join;
|
|
3251
|
+
return join;
|
|
3252
|
+
}
|
|
3253
|
+
async ensureIdleBeforeSend() {
|
|
3254
|
+
if (Date.now() - this.recentChatEndAt > 3e3) return;
|
|
3255
|
+
const deadline = Date.now() + 3e3;
|
|
3256
|
+
while (Date.now() < deadline) {
|
|
3257
|
+
const response = await this.stop();
|
|
3258
|
+
const status = String(response.status ?? "");
|
|
3259
|
+
if (status !== "stopping") return;
|
|
3260
|
+
await new Promise((resolve) => globalThis.setTimeout(resolve, 150));
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
};
|
|
3264
|
+
|
|
3265
|
+
// src/session/hub.ts
|
|
3266
|
+
var SessionHub = class {
|
|
3267
|
+
client;
|
|
3268
|
+
sessions = /* @__PURE__ */ new Map();
|
|
3269
|
+
bootstrapPending = /* @__PURE__ */ new Map();
|
|
3270
|
+
connection = "disconnected";
|
|
3271
|
+
connectionListeners = /* @__PURE__ */ new Set();
|
|
3272
|
+
bound = false;
|
|
3273
|
+
hasConnected = false;
|
|
3274
|
+
constructor(client) {
|
|
3275
|
+
this.client = client;
|
|
3276
|
+
}
|
|
3277
|
+
/** 当前 Socket.IO 连接状态。 */
|
|
3278
|
+
getConnection() {
|
|
3279
|
+
return this.connection;
|
|
3280
|
+
}
|
|
3281
|
+
/** 订阅连接状态变化。 */
|
|
3282
|
+
onConnectionChange(listener) {
|
|
3283
|
+
this.connectionListeners.add(listener);
|
|
3284
|
+
return () => {
|
|
3285
|
+
this.connectionListeners.delete(listener);
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
/** 取得(或复用)一个会话的 AgentSession,并完成历史加载 + 房间订阅。 */
|
|
3289
|
+
async connect(sessionId) {
|
|
3290
|
+
await this.client.ensureVersionCompatible();
|
|
3291
|
+
const existing = this.sessions.get(sessionId);
|
|
3292
|
+
if (existing && !existing.isDisposed) {
|
|
3293
|
+
return this.bootstrapPending.get(sessionId) ?? existing;
|
|
3294
|
+
}
|
|
3295
|
+
this.ensureBound();
|
|
3296
|
+
const session = new AgentSession(sessionId, this.runtimeFor(), this.connection);
|
|
3297
|
+
this.sessions.set(sessionId, session);
|
|
3298
|
+
this.ensureConnected();
|
|
3299
|
+
const bootstrap = session._bootstrap().then(() => session);
|
|
3300
|
+
this.bootstrapPending.set(sessionId, bootstrap);
|
|
3301
|
+
try {
|
|
3302
|
+
return await bootstrap;
|
|
3303
|
+
} catch (error) {
|
|
3304
|
+
session.dispose();
|
|
3305
|
+
throw error;
|
|
3306
|
+
} finally {
|
|
3307
|
+
if (this.bootstrapPending.get(sessionId) === bootstrap) {
|
|
3308
|
+
this.bootstrapPending.delete(sessionId);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
/** 已有实例则返回,不触发网络请求。 */
|
|
3313
|
+
peek(sessionId) {
|
|
3314
|
+
if (this.bootstrapPending.has(sessionId)) return void 0;
|
|
3315
|
+
const session = this.sessions.get(sessionId);
|
|
3316
|
+
return session && !session.isDisposed ? session : void 0;
|
|
3317
|
+
}
|
|
3318
|
+
/** 手动重连(例如登录态变化后)。 */
|
|
3319
|
+
reconnect() {
|
|
3320
|
+
this.ensureBound();
|
|
3321
|
+
const socket = this.client.socket();
|
|
3322
|
+
socket.disconnect();
|
|
3323
|
+
this.setConnection("connecting");
|
|
3324
|
+
socket.connect();
|
|
3325
|
+
}
|
|
3326
|
+
/** 断开连接(不清理 session 实例)。 */
|
|
3327
|
+
disconnect() {
|
|
3328
|
+
this.client.socket().disconnect();
|
|
3329
|
+
this.setConnection("disconnected");
|
|
3330
|
+
}
|
|
3331
|
+
/** 清理当前身份下缓存的全部会话,避免切换登录身份后复用旧快照。 */
|
|
3332
|
+
disposeAll() {
|
|
3333
|
+
this.bootstrapPending.clear();
|
|
3334
|
+
for (const session of [...this.sessions.values()]) {
|
|
3335
|
+
session.dispose();
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
/**
|
|
3339
|
+
* @internal 底层 socket 被换掉时由 BladeClient 调用(如 setBaseUrl)。
|
|
3340
|
+
* 事件监听器绑在旧 socket 实例上,换新实例后必须重绑,否则所有
|
|
3341
|
+
* turn:* / chat:* 事件都会静默丢弃。
|
|
3342
|
+
*/
|
|
3343
|
+
_handleSocketReplaced() {
|
|
3344
|
+
this.bound = false;
|
|
3345
|
+
this.setConnection("disconnected");
|
|
3346
|
+
if (this.sessions.size === 0) return;
|
|
3347
|
+
this.ensureBound();
|
|
3348
|
+
this.ensureConnected();
|
|
3349
|
+
}
|
|
3350
|
+
runtimeFor() {
|
|
3351
|
+
return {
|
|
3352
|
+
emitEvent: (event, data) => {
|
|
3353
|
+
;
|
|
3354
|
+
this.client.socket().emit(event, data);
|
|
3355
|
+
},
|
|
3356
|
+
emitWithAck: (event, data, timeoutMs) => new Promise((resolve, reject) => {
|
|
3357
|
+
;
|
|
3358
|
+
this.client.socket().timeout(timeoutMs).emit(event, data, (err, response) => {
|
|
3359
|
+
if (err) {
|
|
3360
|
+
reject(err);
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
resolve(response);
|
|
3364
|
+
});
|
|
3365
|
+
}),
|
|
3366
|
+
ensureConnected: () => this.ensureConnected(),
|
|
3367
|
+
fetchTurns: (sessionId) => this.client.sessions.getSessionTurns(sessionId),
|
|
3368
|
+
onDispose: (session) => {
|
|
3369
|
+
if (this.sessions.get(session.sessionId) === session) {
|
|
3370
|
+
this.sessions.delete(session.sessionId);
|
|
3371
|
+
}
|
|
3372
|
+
;
|
|
3373
|
+
this.client.socket().emit("session:unsubscribe", { session_id: session.sessionId });
|
|
3374
|
+
}
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
ensureConnected() {
|
|
3378
|
+
const socket = this.client.socket();
|
|
3379
|
+
if (socket.connected || socket.active) return;
|
|
3380
|
+
this.setConnection("connecting");
|
|
3381
|
+
socket.connect();
|
|
3382
|
+
}
|
|
3383
|
+
setConnection(status) {
|
|
3384
|
+
if (this.connection === status) return;
|
|
3385
|
+
this.connection = status;
|
|
3386
|
+
for (const session of this.sessions.values()) {
|
|
3387
|
+
session._setConnection(status);
|
|
3388
|
+
}
|
|
3389
|
+
for (const listener of [...this.connectionListeners]) {
|
|
3390
|
+
try {
|
|
3391
|
+
listener(status);
|
|
3392
|
+
} catch (error) {
|
|
3393
|
+
console.warn("[agent-client] \u8FDE\u63A5\u72B6\u6001\u56DE\u8C03\u629B\u51FA\u5F02\u5E38\uFF1A", error);
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
/** 事件带 session_id 时精确路由;缺省时按当前唯一活跃会话兜底。 */
|
|
3398
|
+
route(sessionId) {
|
|
3399
|
+
if (sessionId) {
|
|
3400
|
+
const session = this.sessions.get(sessionId);
|
|
3401
|
+
return session && !session.isDisposed ? [session] : [];
|
|
3402
|
+
}
|
|
3403
|
+
const active = [...this.sessions.values()].filter((session) => !session.isDisposed);
|
|
3404
|
+
return active.length === 1 ? active : [];
|
|
3405
|
+
}
|
|
3406
|
+
ensureBound() {
|
|
3407
|
+
if (this.bound) return;
|
|
3408
|
+
this.bound = true;
|
|
3409
|
+
const socket = this.client.socket();
|
|
3410
|
+
socket.on("connect", () => {
|
|
3411
|
+
const isReconnect = this.hasConnected;
|
|
3412
|
+
this.hasConnected = true;
|
|
3413
|
+
this.setConnection("connected");
|
|
3414
|
+
if (isReconnect) {
|
|
3415
|
+
for (const session of this.sessions.values()) {
|
|
3416
|
+
void session._rejoinAfterReconnect();
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
});
|
|
3420
|
+
socket.on("connect_error", () => this.setConnection("disconnected"));
|
|
3421
|
+
socket.on("disconnect", () => this.setConnection("disconnected"));
|
|
3422
|
+
socket.io.on("reconnect_attempt", () => this.setConnection("reconnecting"));
|
|
3423
|
+
socket.on("turn:start", (data) => {
|
|
3424
|
+
for (const session of this.route(data.session_id)) session._handleTurnStart(data);
|
|
3425
|
+
});
|
|
3426
|
+
socket.on("turn:patch", (data) => {
|
|
3427
|
+
for (const session of this.route(data.session_id)) session._handleTurnPatch(data);
|
|
3428
|
+
});
|
|
3429
|
+
socket.on("turn:end", (data) => {
|
|
3430
|
+
for (const session of this.route(data.session_id)) session._handleTurnEnd(data);
|
|
3431
|
+
});
|
|
3432
|
+
socket.on("turn:events", (data) => {
|
|
3433
|
+
for (const session of this.route(data.session_id)) session._handleTurnEvents(data.events ?? []);
|
|
3434
|
+
});
|
|
3435
|
+
socket.on("chat:start", (data) => {
|
|
3436
|
+
for (const session of this.route(data?.session_id)) session._handleChatStart();
|
|
3437
|
+
});
|
|
3438
|
+
socket.on("chat:end", (data) => {
|
|
3439
|
+
for (const session of this.route(data?.session_id)) {
|
|
3440
|
+
session._handleChatEnd(data?.status ?? "completed");
|
|
3441
|
+
}
|
|
3442
|
+
});
|
|
3443
|
+
socket.on("system:error", (data) => {
|
|
3444
|
+
for (const session of this.route(data.session_id)) session._handleSystemError(data.message);
|
|
3445
|
+
});
|
|
3446
|
+
socket.on(
|
|
3447
|
+
"system:notification",
|
|
3448
|
+
(data) => {
|
|
3449
|
+
for (const session of this.route(data.session_id)) {
|
|
3450
|
+
session._handleSystemNotification("root", data);
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
);
|
|
3454
|
+
socket.on("task:updated", (data) => {
|
|
3455
|
+
for (const session of this.route(data.session_id)) session._handleTaskUpdated(data.tasks);
|
|
3456
|
+
});
|
|
3457
|
+
socket.on("task:synced", (data) => {
|
|
3458
|
+
for (const session of this.route(data.session_id)) session._handleTaskUpdated(data.tasks);
|
|
3459
|
+
});
|
|
3460
|
+
socket.on(
|
|
3461
|
+
"workspace:files_changed",
|
|
3462
|
+
(data) => {
|
|
3463
|
+
for (const session of this.route(data.session_id)) {
|
|
3464
|
+
session._handleWorkspaceFilesChanged(data.file_path);
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
);
|
|
3468
|
+
socket.on(
|
|
3469
|
+
"session:updated",
|
|
3470
|
+
(data) => {
|
|
3471
|
+
for (const session of this.route(data.session_id)) {
|
|
3472
|
+
session._handleSessionUpdated(data);
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
);
|
|
3476
|
+
socket.on("session:rewind", (data) => {
|
|
3477
|
+
for (const session of this.route(data.session_id)) {
|
|
3478
|
+
session._handleRewind(data.checkpoint_id);
|
|
3479
|
+
}
|
|
3480
|
+
});
|
|
3481
|
+
socket.on(
|
|
3482
|
+
"replay:input_mismatch",
|
|
3483
|
+
(data) => {
|
|
3484
|
+
for (const session of this.route(data.session_id)) session._handleReplayMismatch(data);
|
|
3485
|
+
}
|
|
3486
|
+
);
|
|
3487
|
+
socket.on("artifact:created", (data) => {
|
|
3488
|
+
this.routeArtifact(data, "created");
|
|
3489
|
+
});
|
|
3490
|
+
socket.on("artifact:updated", (data) => {
|
|
3491
|
+
this.routeArtifact(data, "updated");
|
|
3492
|
+
});
|
|
3493
|
+
socket.on("bg:started", (data) => {
|
|
3494
|
+
const taskId = typeof data.task_id === "string" ? data.task_id : typeof data.id === "string" ? data.id : "";
|
|
3495
|
+
if (!taskId) return;
|
|
3496
|
+
for (const session of this.route(data.session_id)) {
|
|
3497
|
+
session._handleBackgroundTask({
|
|
3498
|
+
id: taskId,
|
|
3499
|
+
command: typeof data.command === "string" ? data.command : "",
|
|
3500
|
+
description: typeof data.description === "string" ? data.description : void 0,
|
|
3501
|
+
status: "running",
|
|
3502
|
+
elapsedSeconds: typeof data.elapsed_seconds === "number" ? data.elapsed_seconds : 0
|
|
3503
|
+
});
|
|
3504
|
+
}
|
|
3505
|
+
});
|
|
3506
|
+
socket.on(
|
|
3507
|
+
"bg:tasks_completed",
|
|
3508
|
+
(data) => {
|
|
3509
|
+
for (const session of this.route(data.session_id)) {
|
|
3510
|
+
for (const task of data.tasks ?? []) {
|
|
3511
|
+
const taskId = typeof task.id === "string" ? task.id : typeof task.task_id === "string" ? task.task_id : "";
|
|
3512
|
+
if (!taskId) continue;
|
|
3513
|
+
session._handleBackgroundTask({
|
|
3514
|
+
id: taskId,
|
|
3515
|
+
command: typeof task.command === "string" ? task.command : "",
|
|
3516
|
+
description: typeof task.description === "string" ? task.description : void 0,
|
|
3517
|
+
status: task.status === "running" || task.status === "failed" ? task.status : "done",
|
|
3518
|
+
exitCode: typeof task.exit_code === "number" || task.exit_code === null ? task.exit_code : void 0,
|
|
3519
|
+
elapsedSeconds: typeof task.elapsed_seconds === "number" ? task.elapsed_seconds : 0,
|
|
3520
|
+
output: Array.isArray(task.output_preview) && task.output_preview.length > 0 ? task.output_preview.join("\n") : void 0
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
);
|
|
3526
|
+
}
|
|
3527
|
+
routeArtifact(data, kind) {
|
|
3528
|
+
const filePath = typeof data.file_path === "string" ? data.file_path : "";
|
|
3529
|
+
if (!filePath) return;
|
|
3530
|
+
for (const session of this.route(data.session_id)) {
|
|
3531
|
+
session._handleWorkspaceFilesChanged(filePath);
|
|
3532
|
+
session._handleArtifact({
|
|
3533
|
+
filePath,
|
|
3534
|
+
contentType: typeof data.content_type === "string" ? data.content_type : "",
|
|
3535
|
+
title: typeof data.title === "string" ? data.title : filePath,
|
|
3536
|
+
content: typeof data.content === "string" ? data.content : "",
|
|
3537
|
+
oldString: typeof data.old_string === "string" ? data.old_string : void 0,
|
|
3538
|
+
newString: typeof data.new_string === "string" ? data.new_string : void 0,
|
|
3539
|
+
kind
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
};
|
|
3544
|
+
|
|
3545
|
+
// src/socket.ts
|
|
3546
|
+
import { io } from "socket.io-client";
|
|
3547
|
+
|
|
3548
|
+
// src/auth.ts
|
|
3549
|
+
function buildSocketAuth(options) {
|
|
3550
|
+
const token = resolveAuthToken(options);
|
|
3551
|
+
return token ? { token } : void 0;
|
|
3552
|
+
}
|
|
3553
|
+
function resolveAuthToken(options) {
|
|
3554
|
+
const token = typeof options.token === "function" ? options.token() : options.token;
|
|
3555
|
+
return token ? token : null;
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
// src/version.ts
|
|
3559
|
+
var SDK_NAME = "agent-client";
|
|
3560
|
+
var SDK_VERSION = "2.0.0";
|
|
3561
|
+
var MIN_SERVER_VERSION = "1.1.1";
|
|
3562
|
+
var LEGACY_SDK = "@blade-hq/agent-kit@1.1.0";
|
|
3563
|
+
var VersionMismatchError = class extends Error {
|
|
3564
|
+
serverVersion;
|
|
3565
|
+
sdkVersion;
|
|
3566
|
+
hint;
|
|
3567
|
+
constructor(options) {
|
|
3568
|
+
super(
|
|
3569
|
+
`SDK \u4E0E Server \u7248\u672C\u4E0D\u5339\u914D\uFF08SDK ${SDK_NAME}@${SDK_VERSION}\uFF0CServer ${options.serverVersion ?? "\u7248\u672C\u672A\u77E5\uFF08\u4E0D\u652F\u6301 /api/version\uFF09"}\uFF09\uFF1A${options.hint}`
|
|
3570
|
+
);
|
|
3571
|
+
this.name = "VersionMismatchError";
|
|
3572
|
+
this.serverVersion = options.serverVersion;
|
|
3573
|
+
this.sdkVersion = SDK_VERSION;
|
|
3574
|
+
this.hint = options.hint;
|
|
3575
|
+
}
|
|
3576
|
+
};
|
|
3577
|
+
function parseCoreVersion(version) {
|
|
3578
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
|
|
3579
|
+
if (!match) return null;
|
|
3580
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
3581
|
+
}
|
|
3582
|
+
function isPrerelease(version) {
|
|
3583
|
+
const trimmed = version.trim();
|
|
3584
|
+
if (!parseCoreVersion(trimmed)) return true;
|
|
3585
|
+
return /[-+]/.test(trimmed);
|
|
3586
|
+
}
|
|
3587
|
+
function compareVersions(a, b) {
|
|
3588
|
+
const left = parseCoreVersion(a);
|
|
3589
|
+
const right = parseCoreVersion(b);
|
|
3590
|
+
if (!left || !right) return 0;
|
|
3591
|
+
for (let i = 0; i < 3; i++) {
|
|
3592
|
+
if (left[i] !== right[i]) return left[i] - right[i];
|
|
3593
|
+
}
|
|
3594
|
+
return 0;
|
|
3595
|
+
}
|
|
3596
|
+
function assertVersionCompatible(info) {
|
|
3597
|
+
if (info === null) {
|
|
3598
|
+
throw new VersionMismatchError({
|
|
3599
|
+
serverVersion: null,
|
|
3600
|
+
hint: `\u8BE5 Server \u4E0D\u63D0\u4F9B /api/version\uFF0C\u8BF4\u660E\u7248\u672C\u4F4E\u4E8E ${MIN_SERVER_VERSION}\u3002\u8BF7\u6539\u7528 ${LEGACY_SDK} \u63A5\u5165\uFF0C\u6216\u628A Server \u5347\u7EA7\u5230 ${MIN_SERVER_VERSION} \u53CA\u4EE5\u4E0A\u3002`
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
if (!isPrerelease(info.version) && compareVersions(info.version, MIN_SERVER_VERSION) < 0) {
|
|
3604
|
+
throw new VersionMismatchError({
|
|
3605
|
+
serverVersion: info.version,
|
|
3606
|
+
hint: `\u672C SDK \u9700\u8981 Server ${MIN_SERVER_VERSION} \u53CA\u4EE5\u4E0A\u3002\u8BF7\u6539\u7528 ${LEGACY_SDK} \u63A5\u5165\u8BE5 Server\uFF0C\u6216\u628A Server \u5347\u7EA7\u5230 ${MIN_SERVER_VERSION} \u53CA\u4EE5\u4E0A\u3002`
|
|
3607
|
+
});
|
|
3608
|
+
}
|
|
3609
|
+
const minSdk = info.min_sdk;
|
|
3610
|
+
if (minSdk && !isPrerelease(SDK_VERSION) && compareVersions(SDK_VERSION, minSdk) < 0) {
|
|
3611
|
+
throw new VersionMismatchError({
|
|
3612
|
+
serverVersion: info.version,
|
|
3613
|
+
hint: `\u8BE5 Server \u8981\u6C42 SDK ${minSdk} \u53CA\u4EE5\u4E0A\uFF0C\u8BF7\u628A ${SDK_NAME} \u5347\u7EA7\u5230 ${minSdk} \u53CA\u4EE5\u4E0A\u3002`
|
|
3614
|
+
});
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
// src/socket.ts
|
|
3619
|
+
function withSdkIdentity(auth) {
|
|
3620
|
+
return { ...auth ?? {}, sdk: SDK_NAME, sdk_version: SDK_VERSION };
|
|
3621
|
+
}
|
|
3622
|
+
function createSocket(options) {
|
|
3623
|
+
const token = resolveAuthToken(options);
|
|
3624
|
+
return io(options.baseUrl, {
|
|
3625
|
+
path: options.path ?? "/socket.io",
|
|
3626
|
+
withCredentials: true,
|
|
3627
|
+
query: token ? { token } : void 0,
|
|
3628
|
+
auth: typeof options.token === "function" ? (cb) => cb(withSdkIdentity(buildSocketAuth(options))) : withSdkIdentity(buildSocketAuth(options)),
|
|
3629
|
+
autoConnect: false
|
|
3630
|
+
});
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
// src/blade-client.ts
|
|
3634
|
+
var REFRESH_PATH = "/api/auth/refresh";
|
|
3635
|
+
var BladeClient = class {
|
|
3636
|
+
refreshPromise = null;
|
|
3637
|
+
socketInstance = null;
|
|
3638
|
+
socketReplacementListeners = /* @__PURE__ */ new Set();
|
|
3639
|
+
/** login() / setToken() 热挂载的令牌,优先级高于构造参数。 */
|
|
3640
|
+
runtimeToken = null;
|
|
3641
|
+
options;
|
|
3642
|
+
auth;
|
|
3643
|
+
headless;
|
|
3644
|
+
sessions;
|
|
3645
|
+
/** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
|
|
3646
|
+
hub;
|
|
3647
|
+
/** 版本握手结果缓存:每个 client 实例只探测一次,不逐请求重复。 */
|
|
3648
|
+
versionCheck = null;
|
|
3649
|
+
constructor(options) {
|
|
3650
|
+
this.options = {
|
|
3651
|
+
...options,
|
|
3652
|
+
baseUrl: normalizeBaseUrl(options.baseUrl)
|
|
3653
|
+
};
|
|
3654
|
+
this.auth = new AuthResource(this);
|
|
3655
|
+
this.headless = new HeadlessResource(this);
|
|
3656
|
+
this.sessions = new SessionsResource(this);
|
|
3657
|
+
this.hub = new SessionHub(this);
|
|
3658
|
+
if (this.options.token === void 0) {
|
|
3659
|
+
this.runtimeToken = readStoredToken(this.options.baseUrl);
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
/**
|
|
3663
|
+
* 弹窗授权登录:用户在弹出的授权页点击"许可授权"后自动取得 PAT,
|
|
3664
|
+
* REST 与 Socket.IO 立即生效。只在浏览器可用。
|
|
3665
|
+
*/
|
|
3666
|
+
async login(options) {
|
|
3667
|
+
const result = await loginWithPopup(this, options);
|
|
3668
|
+
this.setToken(result.token);
|
|
3669
|
+
return result;
|
|
3670
|
+
}
|
|
3671
|
+
/** 退出登录:清除 login() 存储的令牌并断开实时连接。 */
|
|
3672
|
+
logoutToken() {
|
|
3673
|
+
this.setToken(null);
|
|
3674
|
+
this.hub.disconnect();
|
|
3675
|
+
}
|
|
3676
|
+
/** 热挂载访问令牌(null 表示清除)。会重建 Socket.IO 连接以携带新凭证。 */
|
|
3677
|
+
setToken(token) {
|
|
3678
|
+
const previousToken = this.resolveToken();
|
|
3679
|
+
this.runtimeToken = token;
|
|
3680
|
+
const nextToken = this.resolveToken();
|
|
3681
|
+
if (previousToken !== nextToken) {
|
|
3682
|
+
this.hub.disposeAll();
|
|
3683
|
+
}
|
|
3684
|
+
writeStoredToken(this.options.baseUrl, token, this.options.tokenStorage ?? "local");
|
|
3685
|
+
if (this.socketInstance) {
|
|
3686
|
+
const wasConnected = this.socketInstance.connected || this.socketInstance.active;
|
|
3687
|
+
this.socketInstance.disconnect();
|
|
3688
|
+
if (wasConnected || token) {
|
|
3689
|
+
this.socketInstance.connect();
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
/** 当前是否持有访问令牌(不代表令牌一定有效)。 */
|
|
3694
|
+
hasToken() {
|
|
3695
|
+
return Boolean(this.resolveRestToken());
|
|
3696
|
+
}
|
|
3697
|
+
/**
|
|
3698
|
+
* 当前访问令牌(PAT),未登录时为 null。
|
|
3699
|
+
*
|
|
3700
|
+
* SDK 只封装会话相关接口;其余长尾接口对照后端 Swagger(`<baseUrl>/docs`)
|
|
3701
|
+
* 用原生 fetch 自行调用,把本令牌填入 Authorization 头即可:
|
|
3702
|
+
*
|
|
3703
|
+
* ```ts
|
|
3704
|
+
* fetch(`${baseUrl}/api/memories/search?q=xxx`, {
|
|
3705
|
+
* headers: { Authorization: `Bearer ${client.token}` },
|
|
3706
|
+
* })
|
|
3707
|
+
* ```
|
|
3708
|
+
*
|
|
3709
|
+
* 安全约束:该令牌只应发往 baseUrl 同源接口,不要附加到第三方域名的请求上。
|
|
3710
|
+
*/
|
|
3711
|
+
get token() {
|
|
3712
|
+
return this.resolveRestToken();
|
|
3713
|
+
}
|
|
3714
|
+
/**
|
|
3715
|
+
* 版本握手:首次连接前查询 Server 版本,不匹配则抛 VersionMismatchError。
|
|
3716
|
+
* 结果缓存在实例上,失败的探测不缓存以便重试。
|
|
3717
|
+
*/
|
|
3718
|
+
async ensureVersionCompatible() {
|
|
3719
|
+
if (!this.versionCheck) {
|
|
3720
|
+
this.versionCheck = this.fetchServerVersion().then(assertVersionCompatible).catch((error) => {
|
|
3721
|
+
this.versionCheck = null;
|
|
3722
|
+
throw error;
|
|
3723
|
+
});
|
|
3724
|
+
}
|
|
3725
|
+
return this.versionCheck;
|
|
3726
|
+
}
|
|
3727
|
+
/**
|
|
3728
|
+
* 读取 /api/version。接口存在但 404/响应不合法 → null(按旧 Server 处理);
|
|
3729
|
+
* 网络层不可达(后端没启动、端口不通、代理配错)→ 抛连接错误。
|
|
3730
|
+
* 两者必须区分:把「后端没起」误报成「版本不匹配,请改用 agent-kit」
|
|
3731
|
+
* 会把开发者引向完全错误的排查方向。
|
|
3732
|
+
*/
|
|
3733
|
+
async fetchServerVersion() {
|
|
3734
|
+
let response;
|
|
3735
|
+
try {
|
|
3736
|
+
response = await this.fetch("GET", "/api/version", { expectOk: false });
|
|
3737
|
+
} catch (error) {
|
|
3738
|
+
throw new Error(
|
|
3739
|
+
`\u65E0\u6CD5\u8FDE\u63A5\u5230 Server\uFF08${this.options.baseUrl || "\u5F53\u524D\u57DF\u540D"}\uFF09\u3002\u8BF7\u786E\u8BA4\u540E\u7AEF\u5DF2\u542F\u52A8\u3001\u5730\u5740\u4E0E\u7AEF\u53E3\u6B63\u786E\u3002`,
|
|
3740
|
+
{ cause: error }
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3743
|
+
if (!response.ok) return null;
|
|
3744
|
+
try {
|
|
3745
|
+
const info = await response.json();
|
|
3746
|
+
return typeof info?.version === "string" ? info : null;
|
|
3747
|
+
} catch {
|
|
3748
|
+
return null;
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
setBaseUrl(baseUrl) {
|
|
3752
|
+
const nextBaseUrl = normalizeBaseUrl(baseUrl);
|
|
3753
|
+
if (nextBaseUrl === this.options.baseUrl) {
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
this.hub.disposeAll();
|
|
3757
|
+
this.options.baseUrl = nextBaseUrl;
|
|
3758
|
+
this.versionCheck = null;
|
|
3759
|
+
if (this.socketInstance) {
|
|
3760
|
+
this.socketInstance.removeAllListeners();
|
|
3761
|
+
this.socketInstance.disconnect();
|
|
3762
|
+
this.socketInstance = null;
|
|
3763
|
+
this.hub._handleSocketReplaced();
|
|
3764
|
+
for (const listener of [...this.socketReplacementListeners]) {
|
|
3765
|
+
try {
|
|
3766
|
+
listener();
|
|
3767
|
+
} catch (error) {
|
|
3768
|
+
console.warn("[agent-client] socket \u66FF\u6362\u56DE\u8C03\u629B\u51FA\u5F02\u5E38\uFF1A", error);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
/** 底层 Socket.IO 实例因后端地址变化而被替换时通知高级集成层。 */
|
|
3774
|
+
onSocketReplaced(listener) {
|
|
3775
|
+
this.socketReplacementListeners.add(listener);
|
|
3776
|
+
return () => {
|
|
3777
|
+
this.socketReplacementListeners.delete(listener);
|
|
3778
|
+
};
|
|
3779
|
+
}
|
|
3780
|
+
socket() {
|
|
3781
|
+
if (!this.socketInstance) {
|
|
3782
|
+
this.socketInstance = createSocket({
|
|
3783
|
+
baseUrl: this.options.baseUrl,
|
|
3784
|
+
token: () => this.resolveSocketToken()
|
|
3785
|
+
});
|
|
3786
|
+
}
|
|
3787
|
+
return this.socketInstance;
|
|
3788
|
+
}
|
|
3789
|
+
async json(method, path, body) {
|
|
3790
|
+
const response = await this.fetch(method, path, {
|
|
3791
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
3792
|
+
headers: body === void 0 ? void 0 : { "content-type": "application/json" }
|
|
3793
|
+
});
|
|
3794
|
+
return readJsonResponse(response);
|
|
3795
|
+
}
|
|
3796
|
+
async jsonFromInit(path, init = {}) {
|
|
3797
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
3798
|
+
const headers = new Headers(init.headers);
|
|
3799
|
+
if (init.body != null && !isFormData(init.body) && !headers.has("content-type")) {
|
|
3800
|
+
headers.set("content-type", "application/json");
|
|
3801
|
+
}
|
|
3802
|
+
const response = await this.fetch(method, path, {
|
|
3803
|
+
body: init.body,
|
|
3804
|
+
credentials: init.credentials,
|
|
3805
|
+
headers,
|
|
3806
|
+
signal: init.signal
|
|
3807
|
+
});
|
|
3808
|
+
return readJsonResponse(response);
|
|
3809
|
+
}
|
|
3810
|
+
async text(method, path) {
|
|
3811
|
+
const response = await this.fetch(method, path);
|
|
3812
|
+
return response.text();
|
|
3813
|
+
}
|
|
3814
|
+
async textFromInit(path, init = {}) {
|
|
3815
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
3816
|
+
const response = await this.fetch(method, path, {
|
|
3817
|
+
body: init.body,
|
|
3818
|
+
credentials: init.credentials,
|
|
3819
|
+
headers: init.headers,
|
|
3820
|
+
signal: init.signal
|
|
3821
|
+
});
|
|
3822
|
+
return response.text();
|
|
3823
|
+
}
|
|
3824
|
+
async responseFromInit(path, init = {}) {
|
|
3825
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
3826
|
+
return this.fetch(method, path, {
|
|
3827
|
+
body: init.body,
|
|
3828
|
+
credentials: init.credentials,
|
|
3829
|
+
headers: init.headers,
|
|
3830
|
+
signal: init.signal
|
|
3831
|
+
});
|
|
3832
|
+
}
|
|
3833
|
+
async blob(method, path) {
|
|
3834
|
+
const response = await this.fetch(method, path);
|
|
3835
|
+
return response.blob();
|
|
3836
|
+
}
|
|
3837
|
+
async formData(method, path, form, options = {}) {
|
|
3838
|
+
if (options.onUploadProgress && !this.options.fetchImpl && typeof XMLHttpRequest !== "undefined") {
|
|
3839
|
+
return this.formDataWithUploadProgress(method, path, form, options);
|
|
3840
|
+
}
|
|
3841
|
+
return this.fetch(method, path, { body: form, expectOk: options.expectOk });
|
|
3842
|
+
}
|
|
3843
|
+
async fetch(method, path, init = {}, isRetry = false) {
|
|
3844
|
+
const url = this.buildUrl(path);
|
|
3845
|
+
const response = await (this.options.fetchImpl ?? fetch)(url.toString(), {
|
|
3846
|
+
method,
|
|
3847
|
+
body: init.body,
|
|
3848
|
+
credentials: init.credentials ?? "include",
|
|
3849
|
+
headers: this.buildHeaders(url, init.headers),
|
|
3850
|
+
signal: init.signal
|
|
3851
|
+
});
|
|
3852
|
+
if (response.status === 401 && !isRetry && this.shouldRefreshFor401(url)) {
|
|
3853
|
+
const refreshed = await this.tryRefresh();
|
|
3854
|
+
if (refreshed) {
|
|
3855
|
+
return this.fetch(method, path, init, true);
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
if (init.expectOk !== false && !response.ok) {
|
|
3859
|
+
const detail = await extractErrorDetail(response);
|
|
3860
|
+
throw decorateAuthError(new BladeApiError(response, detail));
|
|
3861
|
+
}
|
|
3862
|
+
return response;
|
|
3863
|
+
}
|
|
3864
|
+
buildAuthedUrl(path) {
|
|
3865
|
+
const url = new URL(this.toBaseRelativePath(path), this.baseUrlWithTrailingSlash());
|
|
3866
|
+
const token = this.resolveTokenForUrl(url);
|
|
3867
|
+
if (token) {
|
|
3868
|
+
url.searchParams.set("token", token);
|
|
3869
|
+
}
|
|
3870
|
+
return url.toString();
|
|
3871
|
+
}
|
|
3872
|
+
buildHeaders(url, headers) {
|
|
3873
|
+
const result = new Headers(headers);
|
|
3874
|
+
if (!this.isSameBackendUrl(url)) {
|
|
3875
|
+
return result;
|
|
3876
|
+
}
|
|
3877
|
+
const token = this.resolveRestToken();
|
|
3878
|
+
if (token) {
|
|
3879
|
+
result.set("Authorization", `Bearer ${token}`);
|
|
3880
|
+
}
|
|
3881
|
+
return result;
|
|
3882
|
+
}
|
|
3883
|
+
buildUrl(path) {
|
|
3884
|
+
return new URL(this.toBaseRelativePath(path), this.baseUrlWithTrailingSlash());
|
|
3885
|
+
}
|
|
3886
|
+
baseUrlWithTrailingSlash() {
|
|
3887
|
+
return this.options.baseUrl.endsWith("/") ? this.options.baseUrl : `${this.options.baseUrl}/`;
|
|
3888
|
+
}
|
|
3889
|
+
toBaseRelativePath(path) {
|
|
3890
|
+
return path.startsWith("/") ? path.slice(1) : path;
|
|
3891
|
+
}
|
|
3892
|
+
isSameBackendUrl(url) {
|
|
3893
|
+
const base = new URL(this.baseUrlWithTrailingSlash());
|
|
3894
|
+
if (url.origin !== base.origin) {
|
|
3895
|
+
return false;
|
|
3896
|
+
}
|
|
3897
|
+
const basePath = base.pathname.replace(/\/+$/, "");
|
|
3898
|
+
if (basePath === "") {
|
|
3899
|
+
return true;
|
|
3900
|
+
}
|
|
3901
|
+
return url.pathname === basePath || url.pathname.startsWith(`${basePath}/`);
|
|
3902
|
+
}
|
|
3903
|
+
shouldRefreshFor401(url) {
|
|
3904
|
+
const refreshUrl = this.buildUrl(REFRESH_PATH);
|
|
3905
|
+
return this.isSameBackendUrl(url) && url.pathname !== refreshUrl.pathname && !this.hasExplicitBearerToken();
|
|
3906
|
+
}
|
|
3907
|
+
hasExplicitBearerToken() {
|
|
3908
|
+
return Boolean(this.resolveRestToken());
|
|
3909
|
+
}
|
|
3910
|
+
async tryRefresh() {
|
|
3911
|
+
if (this.refreshPromise) {
|
|
3912
|
+
return this.refreshPromise;
|
|
3913
|
+
}
|
|
3914
|
+
this.refreshPromise = (async () => {
|
|
3915
|
+
try {
|
|
3916
|
+
const response = await (this.options.fetchImpl ?? fetch)(
|
|
3917
|
+
this.buildUrl(REFRESH_PATH).toString(),
|
|
3918
|
+
{
|
|
3919
|
+
method: "POST",
|
|
3920
|
+
credentials: "include"
|
|
3921
|
+
}
|
|
3922
|
+
);
|
|
3923
|
+
if (!response.ok) {
|
|
3924
|
+
return false;
|
|
3925
|
+
}
|
|
3926
|
+
await this.options.onRefreshSuccess?.();
|
|
3927
|
+
return true;
|
|
3928
|
+
} catch {
|
|
3929
|
+
return false;
|
|
3930
|
+
} finally {
|
|
3931
|
+
this.refreshPromise = null;
|
|
3932
|
+
}
|
|
3933
|
+
})();
|
|
3934
|
+
return this.refreshPromise;
|
|
3935
|
+
}
|
|
3936
|
+
resolveTokenForUrl(url) {
|
|
3937
|
+
if (!this.isSameBackendUrl(url)) {
|
|
3938
|
+
return null;
|
|
3939
|
+
}
|
|
3940
|
+
return this.resolveRestToken();
|
|
3941
|
+
}
|
|
3942
|
+
resolveRestToken() {
|
|
3943
|
+
return this.resolveToken();
|
|
3944
|
+
}
|
|
3945
|
+
resolveSocketToken() {
|
|
3946
|
+
if (this.runtimeToken) return this.runtimeToken;
|
|
3947
|
+
return resolveTokenOption(this.options.socketToken ?? this.options.token);
|
|
3948
|
+
}
|
|
3949
|
+
resolveToken() {
|
|
3950
|
+
if (this.runtimeToken) return this.runtimeToken;
|
|
3951
|
+
return resolveTokenOption(this.options.token);
|
|
3952
|
+
}
|
|
3953
|
+
formDataWithUploadProgress(method, path, form, options, isRetry = false) {
|
|
3954
|
+
const url = this.buildUrl(path);
|
|
3955
|
+
const headers = this.buildHeaders(url);
|
|
3956
|
+
return new Promise((resolve, reject) => {
|
|
3957
|
+
const xhr = new XMLHttpRequest();
|
|
3958
|
+
xhr.open(method, url.toString(), true);
|
|
3959
|
+
xhr.withCredentials = true;
|
|
3960
|
+
headers.forEach((value, key) => {
|
|
3961
|
+
xhr.setRequestHeader(key, value);
|
|
3962
|
+
});
|
|
3963
|
+
xhr.upload.onprogress = (event) => {
|
|
3964
|
+
options.onUploadProgress?.({
|
|
3965
|
+
loaded: event.loaded,
|
|
3966
|
+
total: event.lengthComputable ? event.total : void 0,
|
|
3967
|
+
percent: event.lengthComputable && event.total > 0 ? event.loaded / event.total : void 0
|
|
3968
|
+
});
|
|
3969
|
+
};
|
|
3970
|
+
xhr.onerror = () => reject(new TypeError("Network request failed"));
|
|
3971
|
+
xhr.ontimeout = () => reject(new TypeError("Network request timed out"));
|
|
3972
|
+
xhr.onabort = () => reject(new DOMException("Request aborted", "AbortError"));
|
|
3973
|
+
xhr.onload = async () => {
|
|
3974
|
+
if (xhr.status === 401 && !isRetry && this.shouldRefreshFor401(url)) {
|
|
3975
|
+
const refreshed = await this.tryRefresh();
|
|
3976
|
+
if (refreshed) {
|
|
3977
|
+
resolve(this.formDataWithUploadProgress(method, path, form, options, true));
|
|
3978
|
+
return;
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
const response = new Response(xhr.responseText, {
|
|
3982
|
+
status: xhr.status,
|
|
3983
|
+
statusText: xhr.statusText,
|
|
3984
|
+
headers: parseXhrHeaders(xhr.getAllResponseHeaders())
|
|
3985
|
+
});
|
|
3986
|
+
if (options.expectOk !== false && !response.ok) {
|
|
3987
|
+
const detail = await extractErrorDetail(response.clone());
|
|
3988
|
+
reject(new BladeApiError(response, detail));
|
|
3989
|
+
return;
|
|
3990
|
+
}
|
|
3991
|
+
options.onUploadProgress?.({ loaded: 1, total: 1, percent: 1 });
|
|
3992
|
+
resolve(response);
|
|
3993
|
+
};
|
|
3994
|
+
xhr.send(form);
|
|
3995
|
+
});
|
|
3996
|
+
}
|
|
3997
|
+
};
|
|
3998
|
+
function resolveTokenOption(tokenOption) {
|
|
3999
|
+
const token = typeof tokenOption === "function" ? tokenOption() : tokenOption;
|
|
4000
|
+
return token ? token : null;
|
|
4001
|
+
}
|
|
4002
|
+
function normalizeBaseUrl(baseUrl) {
|
|
4003
|
+
const normalized = baseUrl.trim().replace(/\/+$/, "");
|
|
4004
|
+
if (normalized || typeof window === "undefined") {
|
|
4005
|
+
return normalized;
|
|
4006
|
+
}
|
|
4007
|
+
return window.location.origin;
|
|
4008
|
+
}
|
|
4009
|
+
function isFormData(value) {
|
|
4010
|
+
return typeof FormData !== "undefined" && value instanceof FormData;
|
|
4011
|
+
}
|
|
4012
|
+
function readJsonResponse(response) {
|
|
4013
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
4014
|
+
return Promise.resolve(void 0);
|
|
4015
|
+
}
|
|
4016
|
+
return response.json();
|
|
4017
|
+
}
|
|
4018
|
+
function parseXhrHeaders(rawHeaders) {
|
|
4019
|
+
const headers = new Headers();
|
|
4020
|
+
for (const line of rawHeaders.trim().split(/[\r\n]+/)) {
|
|
4021
|
+
const separatorIndex = line.indexOf(":");
|
|
4022
|
+
if (separatorIndex <= 0) continue;
|
|
4023
|
+
headers.append(line.slice(0, separatorIndex).trim(), line.slice(separatorIndex + 1).trim());
|
|
4024
|
+
}
|
|
4025
|
+
return headers;
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
// src/commands/protocol.ts
|
|
4029
|
+
function isCommandEnvelope(value) {
|
|
4030
|
+
if (typeof value !== "object" || value === null) return false;
|
|
4031
|
+
const record = value;
|
|
4032
|
+
return record.__bladeBridge === true && record.direction === "agent-to-host" && typeof record.action === "string";
|
|
4033
|
+
}
|
|
4034
|
+
function isInboundEnvelope(value) {
|
|
4035
|
+
if (typeof value !== "object" || value === null) return false;
|
|
4036
|
+
const record = value;
|
|
4037
|
+
return record.__bladeBridge === true && record.direction === "host-to-agent" && (record.action === "addContext" || record.action === "appendInput" || record.action === "sendMessage");
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
// src/commands/embedded.ts
|
|
4041
|
+
function connectEmbedded(options) {
|
|
4042
|
+
if (typeof window === "undefined") {
|
|
4043
|
+
throw new Error("connectEmbedded() \u53EA\u80FD\u5728\u6D4F\u89C8\u5668\u4E2D\u4F7F\u7528\u3002");
|
|
4044
|
+
}
|
|
4045
|
+
if (!options.allowedOrigins?.length) {
|
|
4046
|
+
throw new Error(
|
|
4047
|
+
"connectEmbedded() \u9700\u8981 allowedOrigins\uFF08Blade \u9875\u9762\u7684 origin\uFF09\uFF0C\u5426\u5219\u65E0\u6CD5\u5B89\u5168\u63A5\u6536\u6D88\u606F\u3002"
|
|
4048
|
+
);
|
|
4049
|
+
}
|
|
4050
|
+
const registry = new CommandRegistry();
|
|
4051
|
+
const allowed = new Set(options.allowedOrigins);
|
|
4052
|
+
const onMessage = (event) => {
|
|
4053
|
+
if (!allowed.has(event.origin)) return;
|
|
4054
|
+
if (options.iframe && event.source !== options.iframe.contentWindow) return;
|
|
4055
|
+
if (!isCommandEnvelope(event.data)) return;
|
|
4056
|
+
registry.dispatch(event.data.action, event.data.payload, {
|
|
4057
|
+
toolCallId: event.data.meta?.toolCallId
|
|
4058
|
+
});
|
|
4059
|
+
};
|
|
4060
|
+
window.addEventListener("message", onMessage);
|
|
4061
|
+
const post = (action, payload) => {
|
|
4062
|
+
const target = options.iframe?.contentWindow;
|
|
4063
|
+
if (!target) {
|
|
4064
|
+
console.warn("[agent-client] connectEmbedded \u672A\u7ED1\u5B9A iframe\uFF0C\u65E0\u6CD5\u5411\u667A\u80FD\u4F53\u53D1\u9001\u6D88\u606F\u3002");
|
|
4065
|
+
return;
|
|
4066
|
+
}
|
|
4067
|
+
target.postMessage(
|
|
4068
|
+
{ __bladeBridge: true, direction: "host-to-agent", action, payload },
|
|
4069
|
+
options.allowedOrigins[0]
|
|
4070
|
+
);
|
|
4071
|
+
};
|
|
4072
|
+
return {
|
|
4073
|
+
onCommand: (action, handler) => registry.onCommand(action, handler),
|
|
4074
|
+
attach: (label, data) => {
|
|
4075
|
+
const content = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
4076
|
+
post("addContext", { label, content });
|
|
4077
|
+
},
|
|
4078
|
+
insertText: (text) => post("appendInput", { text }),
|
|
4079
|
+
send: (text) => post("sendMessage", { text }),
|
|
4080
|
+
dispose: () => {
|
|
4081
|
+
window.removeEventListener("message", onMessage);
|
|
4082
|
+
registry.clear();
|
|
4083
|
+
}
|
|
4084
|
+
};
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
// src/schemas/session.ts
|
|
4088
|
+
import { type } from "arktype";
|
|
4089
|
+
var SessionStatus = type(
|
|
4090
|
+
"'created' | 'running' | 'completed' | 'failed' | 'interrupted' | 'waiting_for_input'"
|
|
4091
|
+
);
|
|
4092
|
+
var SessionInfo = type({
|
|
4093
|
+
id: "string",
|
|
4094
|
+
intent: "string",
|
|
4095
|
+
status: SessionStatus,
|
|
4096
|
+
created_at: "string",
|
|
4097
|
+
updated_at: "string",
|
|
4098
|
+
"shared?": "boolean",
|
|
4099
|
+
"memory_enabled?": "boolean",
|
|
4100
|
+
"is_persistent?": "boolean",
|
|
4101
|
+
"is_headless?": "boolean",
|
|
4102
|
+
"viewer_role?": "'owner' | 'viewer'",
|
|
4103
|
+
"template_id?": "string | null",
|
|
4104
|
+
"model?": "string | null",
|
|
4105
|
+
"enable_thinking?": "boolean | null",
|
|
4106
|
+
"solution_id?": "string | null",
|
|
4107
|
+
"biz_role_id?": "string | null",
|
|
4108
|
+
"solution?": "unknown",
|
|
4109
|
+
"plan_summary?": "string | null",
|
|
4110
|
+
"primary_skill_id?": "string | null",
|
|
4111
|
+
// ship-attack v2:后端 engine.set_bound_skill 写入的"绑定 skill id"。
|
|
4112
|
+
// 未绑定 / 非 ship-attack session 为 null。
|
|
4113
|
+
"bound_skill_id?": "string | null",
|
|
4114
|
+
"replay_state?": "unknown",
|
|
4115
|
+
"is_pinned?": "boolean",
|
|
4116
|
+
"pinned_at?": "string | null",
|
|
4117
|
+
"is_example?": "boolean",
|
|
4118
|
+
"ports?": "unknown",
|
|
4119
|
+
"disable_tools?": "string[]",
|
|
4120
|
+
"runtime_type?": "string | null",
|
|
4121
|
+
"daemon_id?": "string | null",
|
|
4122
|
+
"match?": "unknown"
|
|
4123
|
+
});
|
|
4124
|
+
|
|
4125
|
+
// src/schemas/solution.ts
|
|
4126
|
+
var LayoutType = /* @__PURE__ */ ((LayoutType2) => {
|
|
4127
|
+
LayoutType2["Default"] = "default";
|
|
4128
|
+
LayoutType2["SkillEditor"] = "skill-editor";
|
|
4129
|
+
LayoutType2["BladeCoa"] = "blade-coa";
|
|
4130
|
+
LayoutType2["SolutionEditor"] = "solution-editor";
|
|
4131
|
+
LayoutType2["SolutionEditor2"] = "solution-editor2";
|
|
4132
|
+
LayoutType2["ChatPreview"] = "chat-preview";
|
|
4133
|
+
LayoutType2["ChatOnly"] = "chat-only";
|
|
4134
|
+
LayoutType2["SolutionApp"] = "solution-app";
|
|
4135
|
+
LayoutType2["Custom"] = "custom";
|
|
4136
|
+
return LayoutType2;
|
|
4137
|
+
})(LayoutType || {});
|
|
4138
|
+
|
|
4139
|
+
// src/schemas/task.ts
|
|
4140
|
+
import { type as type2 } from "arktype";
|
|
4141
|
+
var TaskStatus = type2("'pending' | 'in_progress' | 'done' | 'failed' | 'skipped'");
|
|
4142
|
+
var Task = type2({
|
|
4143
|
+
id: "string",
|
|
4144
|
+
goal: "string",
|
|
4145
|
+
skills: "string[]",
|
|
4146
|
+
depends_on: "string[]",
|
|
4147
|
+
status: TaskStatus,
|
|
4148
|
+
result_summary: "string",
|
|
4149
|
+
number: "string"
|
|
4150
|
+
});
|
|
4151
|
+
export {
|
|
4152
|
+
AgentSession,
|
|
4153
|
+
BladeApiError,
|
|
4154
|
+
BladeClient,
|
|
4155
|
+
ClientProjectionBuilder,
|
|
4156
|
+
LayoutType,
|
|
4157
|
+
MIN_SERVER_VERSION,
|
|
4158
|
+
SDK_NAME,
|
|
4159
|
+
SDK_VERSION,
|
|
4160
|
+
SessionHub,
|
|
4161
|
+
SessionInfo,
|
|
4162
|
+
SessionStatus,
|
|
4163
|
+
Task,
|
|
4164
|
+
TaskStatus,
|
|
4165
|
+
VersionMismatchError,
|
|
4166
|
+
buildMessageContent,
|
|
4167
|
+
connectEmbedded,
|
|
4168
|
+
contentPreview,
|
|
4169
|
+
createInitialSessionState,
|
|
4170
|
+
createSocket,
|
|
4171
|
+
extractTextAttachments,
|
|
4172
|
+
getFileParts,
|
|
4173
|
+
getImageParts,
|
|
4174
|
+
getTextContent,
|
|
4175
|
+
groupMessagesByLoop,
|
|
4176
|
+
isCommandEnvelope,
|
|
4177
|
+
isHiddenInternalMessage,
|
|
4178
|
+
isInboundEnvelope,
|
|
4179
|
+
normalizeMessageContent,
|
|
4180
|
+
transformSlashCommand
|
|
4181
|
+
};
|
|
4182
|
+
//# sourceMappingURL=index.js.map
|