@blade-hq/agent-client 2610.0.0-beta.9 → 2610.0.0-beta.90
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 +266 -11
- package/dist/auth.d.ts +0 -1
- package/dist/blade-client.d.ts +24 -3
- package/dist/index.d.ts +37 -7
- package/dist/index.js +2395 -295
- package/dist/index.js.map +1 -1
- package/dist/platform-endpoints.d.ts +14 -0
- package/dist/polling-backoff.d.ts +11 -0
- package/dist/resources/chat-groups.d.ts +31 -0
- package/dist/resources/computers.d.ts +115 -0
- package/dist/resources/models.d.ts +2 -0
- package/dist/resources/plugins.d.ts +43 -0
- package/dist/resources/sessions.d.ts +193 -2
- package/dist/resources/site-visits.d.ts +32 -0
- package/dist/rest.d.ts +8 -0
- package/dist/schemas/context.d.ts +49 -0
- package/dist/schemas/event.d.ts +9 -0
- package/dist/schemas/mcp-app.d.ts +30 -0
- package/dist/schemas/message-utils.d.ts +15 -1
- package/dist/schemas/message.d.ts +6 -0
- package/dist/schemas/projection.d.ts +37 -2
- package/dist/schemas/session.d.ts +10 -1
- package/dist/session/agent-session.d.ts +159 -17
- package/dist/session/definition.d.ts +6 -1
- package/dist/session/errors.d.ts +11 -0
- package/dist/session/events.d.ts +16 -2
- package/dist/session/history-page.d.ts +9 -0
- package/dist/session/hub.d.ts +9 -1
- package/dist/session/queue.d.ts +104 -0
- package/dist/session/state.d.ts +18 -2
- package/dist/shared/auth-busy.d.ts +9 -0
- package/dist/shared/projection/builder.d.ts +12 -0
- package/dist/shared/projection/context.d.ts +3 -0
- package/dist/shared/projection/helpers.d.ts +2 -2
- package/dist/shared/projection/history.d.ts +1 -0
- package/dist/shared/projection/index.d.ts +2 -1
- package/dist/shared/projection/state.d.ts +7 -0
- package/dist/shared/tool-ui-card.d.ts +60 -0
- package/dist/shared/type-guards.d.ts +2 -0
- package/dist/socket.d.ts +2 -0
- package/dist/types/socket-events.d.ts +199 -6
- package/package.json +1 -1
- package/public-api.md +960 -17
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// src/version.ts
|
|
2
|
+
var SDK_NAME = "agent-client";
|
|
3
|
+
var SDK_VERSION = true ? "2610.0.0-beta.90" : "1.1.1";
|
|
4
|
+
|
|
1
5
|
// src/auth-login.ts
|
|
2
6
|
function isAuthCallbackMessage(value) {
|
|
3
7
|
return typeof value === "object" && value !== null && value.type === "blade-agent:sdk-auth";
|
|
@@ -167,6 +171,10 @@ function decorateAuthError(error) {
|
|
|
167
171
|
}
|
|
168
172
|
return error;
|
|
169
173
|
}
|
|
174
|
+
function buildBackendUrl(baseUrl, path) {
|
|
175
|
+
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
176
|
+
return new URL(path.startsWith("/") ? path.slice(1) : path, base);
|
|
177
|
+
}
|
|
170
178
|
|
|
171
179
|
// src/resources/auth.ts
|
|
172
180
|
var AuthResource = class {
|
|
@@ -214,6 +222,11 @@ var AuthResource = class {
|
|
|
214
222
|
}
|
|
215
223
|
};
|
|
216
224
|
|
|
225
|
+
// src/shared/type-guards.ts
|
|
226
|
+
function isRecord(value) {
|
|
227
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
228
|
+
}
|
|
229
|
+
|
|
217
230
|
// src/resources/headless.ts
|
|
218
231
|
var HeadlessError = class extends Error {
|
|
219
232
|
constructor(message, detail) {
|
|
@@ -352,8 +365,7 @@ var HeadlessResource = class {
|
|
|
352
365
|
socket.on("system:error", onSystemError);
|
|
353
366
|
socket.emit("session:subscribe", {
|
|
354
367
|
session_id,
|
|
355
|
-
event_format: "raw"
|
|
356
|
-
stream_tokens: this.client.options.streamTokens !== false
|
|
368
|
+
event_format: "raw"
|
|
357
369
|
});
|
|
358
370
|
socket.emit(
|
|
359
371
|
"chat:send",
|
|
@@ -395,9 +407,6 @@ function errorMessageFromResult(result) {
|
|
|
395
407
|
}
|
|
396
408
|
return null;
|
|
397
409
|
}
|
|
398
|
-
function isRecord(value) {
|
|
399
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
400
|
-
}
|
|
401
410
|
function ensureSocketConnected(socket, timeoutMs, detail) {
|
|
402
411
|
if (socket.connected) {
|
|
403
412
|
return Promise.resolve();
|
|
@@ -425,6 +434,81 @@ function ensureSocketConnected(socket, timeoutMs, detail) {
|
|
|
425
434
|
});
|
|
426
435
|
}
|
|
427
436
|
|
|
437
|
+
// src/resources/computers.ts
|
|
438
|
+
function computerState(computer) {
|
|
439
|
+
if (computer.is_primary) return "primary";
|
|
440
|
+
if (computer.enabled) return "enabled";
|
|
441
|
+
return computer.online ? "idle" : "offline";
|
|
442
|
+
}
|
|
443
|
+
function computerOS(computer) {
|
|
444
|
+
const os = (computer.os ?? "").toLowerCase();
|
|
445
|
+
if (os.startsWith("darwin") || os.startsWith("mac")) return "macos";
|
|
446
|
+
if (os.startsWith("windows")) return "windows";
|
|
447
|
+
if (os.startsWith("linux")) return "linux";
|
|
448
|
+
return "unknown";
|
|
449
|
+
}
|
|
450
|
+
function computerPlatformLabel(computer) {
|
|
451
|
+
const os = (computer.os ?? "").trim();
|
|
452
|
+
const arch = (computer.arch ?? "").trim();
|
|
453
|
+
if (os && arch) return `${os}/${arch}`;
|
|
454
|
+
return os || arch || "";
|
|
455
|
+
}
|
|
456
|
+
function computerDaemonVersion(computer) {
|
|
457
|
+
return (computer.daemon_version ?? "").trim();
|
|
458
|
+
}
|
|
459
|
+
function canToggleComputer(computer) {
|
|
460
|
+
return !computer.is_primary;
|
|
461
|
+
}
|
|
462
|
+
function sortComputers(computers) {
|
|
463
|
+
return [...computers].sort((a, b) => {
|
|
464
|
+
const byCreated = (a.created_at ?? "").localeCompare(b.created_at ?? "");
|
|
465
|
+
if (byCreated !== 0) return byCreated;
|
|
466
|
+
return a.label.localeCompare(b.label);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
var ComputersResource = class {
|
|
470
|
+
constructor(client) {
|
|
471
|
+
this.client = client;
|
|
472
|
+
}
|
|
473
|
+
client;
|
|
474
|
+
/**
|
|
475
|
+
* 列出当前账号登记过的全部电脑及其在线状态。
|
|
476
|
+
*
|
|
477
|
+
* 首页还没有会话,也要能看到同一份清单——判定各写一份必然分叉,一边的兜底会把
|
|
478
|
+
* 另一边的错误盖住(#1597)。所以账号事实只从这一个接口读。
|
|
479
|
+
*/
|
|
480
|
+
async listAccount() {
|
|
481
|
+
const data = await this.client.json("GET", "/api/daemon/instances");
|
|
482
|
+
return {
|
|
483
|
+
computers: (data.instances ?? []).map((instance) => ({
|
|
484
|
+
...instance,
|
|
485
|
+
online: Boolean(instance.online)
|
|
486
|
+
}))
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* 列出当前用户的全部电脑,带上"在不在线"和"这次会话能不能用"。
|
|
491
|
+
* 服务端只查库,电脑离线时同样返回,所以拿得到"它离线了"这个结论。
|
|
492
|
+
*/
|
|
493
|
+
list(sessionId) {
|
|
494
|
+
return this.client.json(
|
|
495
|
+
"GET",
|
|
496
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/computers`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* 启动或停止一台电脑。只改这次会话的可用范围,不会碰远程机器,
|
|
501
|
+
* 电脑离线时同样能登记,等它上线就直接可用。
|
|
502
|
+
*/
|
|
503
|
+
setEnabled(sessionId, computer, enabled) {
|
|
504
|
+
return this.client.json(
|
|
505
|
+
"PUT",
|
|
506
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/computers/${encodeURIComponent(computer)}/enabled`,
|
|
507
|
+
{ enabled }
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
428
512
|
// src/resources/models.ts
|
|
429
513
|
var ModelsResource = class {
|
|
430
514
|
constructor(client) {
|
|
@@ -451,6 +535,7 @@ var SessionInfo = type({
|
|
|
451
535
|
"shared?": "boolean",
|
|
452
536
|
"memory_enabled?": "boolean",
|
|
453
537
|
"is_persistent?": "boolean",
|
|
538
|
+
"initiator?": "'ui' | 'sdk' | 'unknown' | null",
|
|
454
539
|
"is_headless?": "boolean",
|
|
455
540
|
"viewer_role?": "'owner' | 'viewer'",
|
|
456
541
|
"template_id?": "string | null",
|
|
@@ -473,7 +558,11 @@ var SessionInfo = type({
|
|
|
473
558
|
"disable_tools?": "string[]",
|
|
474
559
|
"runtime_type?": "string | null",
|
|
475
560
|
"daemon_id?": "string | null",
|
|
561
|
+
"agent_runtime_id?": "string | null",
|
|
476
562
|
"workspace_path?": "string | null",
|
|
563
|
+
"ba_version?": "string | null",
|
|
564
|
+
"sandbox_version?": "string | null",
|
|
565
|
+
"chat_group_id?": "string | null",
|
|
477
566
|
"match?": "unknown"
|
|
478
567
|
});
|
|
479
568
|
|
|
@@ -539,7 +628,7 @@ function upsertToolCall(state, toolCallId, toolName, displayName, args) {
|
|
|
539
628
|
});
|
|
540
629
|
}
|
|
541
630
|
}
|
|
542
|
-
function applyToolResult(state, toolCallId, result, status, durationMs, sourceLoop) {
|
|
631
|
+
function applyToolResult(state, toolCallId, result, status, durationMs, ranInParallel, sourceLoop, startedAt = null) {
|
|
543
632
|
let toolName = "";
|
|
544
633
|
let displayName = "";
|
|
545
634
|
let finalResult = result;
|
|
@@ -551,6 +640,8 @@ function applyToolResult(state, toolCallId, result, status, durationMs, sourceLo
|
|
|
551
640
|
}
|
|
552
641
|
const update = { status, result: finalResult };
|
|
553
642
|
if (durationMs != null) update.duration_ms = durationMs;
|
|
643
|
+
if (ranInParallel != null) update.ran_in_parallel = ranInParallel;
|
|
644
|
+
if (startedAt != null) update.started_at = startedAt;
|
|
554
645
|
state.toolCalls[i] = { ...tc, ...update };
|
|
555
646
|
toolName = tc.tool_name;
|
|
556
647
|
displayName = tc.display_name;
|
|
@@ -732,11 +823,17 @@ function buildCompactionTurn(sequence, compactionId, loopId, status, data) {
|
|
|
732
823
|
summary_preview: summaryPreview,
|
|
733
824
|
summary_full: summaryFull,
|
|
734
825
|
archived_count: archivedCount,
|
|
826
|
+
archived_files: Array.isArray(data.archived_files) ? data.archived_files.filter(
|
|
827
|
+
(item) => typeof item === "object" && item !== null && !Array.isArray(item)
|
|
828
|
+
) : null,
|
|
829
|
+
archived_tool_calls: Array.isArray(data.archived_tool_calls) ? data.archived_tool_calls.filter(
|
|
830
|
+
(item) => typeof item === "object" && item !== null && !Array.isArray(item)
|
|
831
|
+
) : null,
|
|
735
832
|
tokens_before: tokensBefore,
|
|
736
833
|
tokens_after: tokensAfter,
|
|
737
834
|
saved_ratio: savedRatio,
|
|
738
835
|
trigger: asTrigger(data.trigger),
|
|
739
|
-
failure_reason:
|
|
836
|
+
failure_reason: [data.failure_reason, data.fallback_reason, data.reason].find((value) => typeof value === "string" && value.length > 0) ?? null,
|
|
740
837
|
fallback_applied: typeof data.fallback_applied === "boolean" ? data.fallback_applied : null
|
|
741
838
|
};
|
|
742
839
|
}
|
|
@@ -991,6 +1088,12 @@ function createTurnState(turnId, loopId, opts = {}) {
|
|
|
991
1088
|
toolCalls: [],
|
|
992
1089
|
usage: null,
|
|
993
1090
|
responseDone: false,
|
|
1091
|
+
firstTokenMs: null,
|
|
1092
|
+
streamDurationMs: null,
|
|
1093
|
+
firstTokenDeliveryMs: null,
|
|
1094
|
+
modelStartedAt: null,
|
|
1095
|
+
modelDurationMs: null,
|
|
1096
|
+
modelStartedAtReported: false,
|
|
994
1097
|
memoryRefs: opts.memoryRefs ?? null,
|
|
995
1098
|
parentForkToolCallId: null
|
|
996
1099
|
};
|
|
@@ -1008,8 +1111,13 @@ function snapshot(state, sequence, status) {
|
|
|
1008
1111
|
tool_calls: [...state.toolCalls],
|
|
1009
1112
|
model: state.model,
|
|
1010
1113
|
usage: state.usage ? { ...state.usage } : null,
|
|
1011
|
-
duration_ms:
|
|
1012
|
-
|
|
1114
|
+
duration_ms: state.modelDurationMs,
|
|
1115
|
+
first_token_ms: state.firstTokenMs,
|
|
1116
|
+
stream_duration_ms: state.streamDurationMs,
|
|
1117
|
+
first_token_delivery_ms: state.firstTokenDeliveryMs,
|
|
1118
|
+
started_at: state.modelStartedAt ?? new Date(state.startedAt).toISOString(),
|
|
1119
|
+
// 没有 payload 上报时用的是 loop:turn 的浏览器事件时间,标记出来供瀑布区分。
|
|
1120
|
+
has_reported_timing: state.modelStartedAtReported,
|
|
1013
1121
|
context_window: state.contextWindow,
|
|
1014
1122
|
memory_refs: state.memoryRefs,
|
|
1015
1123
|
parent_fork_tool_call_id: state.parentForkToolCallId
|
|
@@ -1023,10 +1131,134 @@ function toolStatusFromResult(result) {
|
|
|
1023
1131
|
return inferToolStatus(text);
|
|
1024
1132
|
}
|
|
1025
1133
|
|
|
1134
|
+
// src/schemas/context.ts
|
|
1135
|
+
var BUILTIN_CONTEXT_TITLES = {
|
|
1136
|
+
"agent-instructions": "\u9879\u76EE\u8BF4\u660E",
|
|
1137
|
+
"skill-catalog": "\u53EF\u7528\u80FD\u529B",
|
|
1138
|
+
"runtime-context": "\u5F53\u524D\u5DE5\u4F5C\u73AF\u5883",
|
|
1139
|
+
"workspace-context": "\u5F53\u524D\u5DE5\u4F5C\u533A\u73AF\u5883"
|
|
1140
|
+
};
|
|
1141
|
+
var BUILTIN_CONTEXT_DETAILS = {
|
|
1142
|
+
"agent-instructions": "\u8FD9\u4E9B\u8BF4\u660E\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u7406\u89E3\u5F53\u524D\u9879\u76EE\u548C\u5DE5\u4F5C\u8981\u6C42\u3002",
|
|
1143
|
+
"skill-catalog": "\u8FD9\u4E9B\u80FD\u529B\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u9009\u62E9\u5408\u9002\u7684\u65B9\u5F0F\u5B8C\u6210\u4EFB\u52A1\u3002",
|
|
1144
|
+
"runtime-context": "\u8FD9\u4E9B\u4FE1\u606F\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u4E86\u89E3\u5F53\u524D\u6587\u4EF6\u5939\u548C\u8FD0\u884C\u6761\u4EF6\u3002",
|
|
1145
|
+
"workspace-context": "\u8FD9\u4E9B\u4FE1\u606F\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u4E86\u89E3\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\u3001\u6587\u4EF6\u548C\u8FD0\u884C\u73AF\u5883\u3002"
|
|
1146
|
+
};
|
|
1147
|
+
function nonEmptyString(value) {
|
|
1148
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1149
|
+
}
|
|
1150
|
+
function contextProjectionData(fields) {
|
|
1151
|
+
const contextKind = nonEmptyString(fields.context_kind);
|
|
1152
|
+
const contextKey = nonEmptyString(fields.context_key);
|
|
1153
|
+
const contextName = nonEmptyString(fields.context_name);
|
|
1154
|
+
const contextRevision = nonEmptyString(fields.context_revision);
|
|
1155
|
+
if (!contextKind || !contextKey || !contextName || !contextRevision || typeof fields.context_order !== "number" || !Number.isFinite(fields.context_order) || !fields.context_action) {
|
|
1156
|
+
return null;
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
context_kind: contextKind,
|
|
1160
|
+
context_key: contextKey,
|
|
1161
|
+
context_name: contextName,
|
|
1162
|
+
context_order: fields.context_order,
|
|
1163
|
+
context_action: fields.context_action,
|
|
1164
|
+
context_revision: contextRevision,
|
|
1165
|
+
display_title: nonEmptyString(fields.display_title),
|
|
1166
|
+
display_summary: nonEmptyString(fields.display_summary),
|
|
1167
|
+
sources: Array.isArray(fields.sources) ? fields.sources : []
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function getContextDisplayState(context) {
|
|
1171
|
+
const title = nonEmptyString(context.display_title) ?? BUILTIN_CONTEXT_TITLES[context.context_kind] ?? "\u5BF9\u8BDD\u80CC\u666F";
|
|
1172
|
+
const isRemoved = context.context_action === "removed";
|
|
1173
|
+
const summary = nonEmptyString(context.display_summary) ?? (isRemoved ? "\u5DF2\u505C\u6B62\u7528\u4E8E\u540E\u7EED\u5BF9\u8BDD" : context.context_action === "retained" ? "\u7EE7\u7EED\u7528\u4E8E\u540E\u7EED\u5BF9\u8BDD" : context.context_kind === "agent-instructions" ? "\u5DF2\u66F4\u65B0\u672C\u6B21\u5BF9\u8BDD\u4F7F\u7528\u7684\u9879\u76EE\u8BF4\u660E" : context.context_kind === "skill-catalog" ? "\u5DF2\u66F4\u65B0\u672C\u6B21\u53EF\u7528\u80FD\u529B" : context.context_kind === "runtime-context" ? "\u5DF2\u66F4\u65B0\u672C\u6B21\u5DE5\u4F5C\u73AF\u5883" : context.context_kind === "workspace-context" ? "\u5DF2\u66F4\u65B0\u672C\u6B21\u5DE5\u4F5C\u533A\u73AF\u5883" : "\u5DF2\u66F4\u65B0\u672C\u6B21\u5BF9\u8BDD\u80CC\u666F");
|
|
1174
|
+
const detail = isRemoved ? `${title}\u5DF2\u4E0D\u518D\u7528\u4E8E\u540E\u7EED\u5BF9\u8BDD\u3002` : BUILTIN_CONTEXT_DETAILS[context.context_kind] ?? "\u8FD9\u4E9B\u4FE1\u606F\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u7406\u89E3\u5F53\u524D\u4EFB\u52A1\u3002";
|
|
1175
|
+
return { title, summary, detail, isRemoved };
|
|
1176
|
+
}
|
|
1177
|
+
function groupAdjacentContextRuns(kinds) {
|
|
1178
|
+
const runs = [];
|
|
1179
|
+
let current = null;
|
|
1180
|
+
kinds.forEach((kind, index) => {
|
|
1181
|
+
if (kind === null) {
|
|
1182
|
+
current = null;
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (current === null) {
|
|
1186
|
+
current = [];
|
|
1187
|
+
runs.push(current);
|
|
1188
|
+
}
|
|
1189
|
+
current.push(index);
|
|
1190
|
+
});
|
|
1191
|
+
return runs;
|
|
1192
|
+
}
|
|
1193
|
+
function getContextGroupDisplayState(contexts) {
|
|
1194
|
+
const count = contexts.length;
|
|
1195
|
+
if (count === 1) {
|
|
1196
|
+
const single = getContextDisplayState(contexts[0]);
|
|
1197
|
+
return { title: single.title, summary: single.summary, count };
|
|
1198
|
+
}
|
|
1199
|
+
const removed = contexts.filter(
|
|
1200
|
+
(context) => context.context_action === "removed"
|
|
1201
|
+
).length;
|
|
1202
|
+
const updated = count - removed;
|
|
1203
|
+
const parts = [];
|
|
1204
|
+
if (updated > 0) parts.push(`${updated} \u9879\u5DF2\u66F4\u65B0`);
|
|
1205
|
+
if (removed > 0) parts.push(`${removed} \u9879\u5DF2\u505C\u7528`);
|
|
1206
|
+
return {
|
|
1207
|
+
title: "\u5BF9\u8BDD\u80CC\u666F",
|
|
1208
|
+
summary: parts.join(" \xB7 ") || `${count} \u9879`,
|
|
1209
|
+
count
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/shared/projection/context.ts
|
|
1214
|
+
function buildContextTurn(entry, sequence) {
|
|
1215
|
+
if (entry.kind !== "context" || !isRecord(entry.source) || !isRecord(entry.data)) {
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
const display = isRecord(entry.data.display) ? entry.data.display : {};
|
|
1219
|
+
const message = isRecord(entry.message) ? entry.message : {};
|
|
1220
|
+
const context = contextProjectionData({
|
|
1221
|
+
context_kind: typeof entry.source.kind === "string" ? entry.source.kind : null,
|
|
1222
|
+
context_key: typeof entry.source.key === "string" ? entry.source.key : null,
|
|
1223
|
+
context_name: typeof entry.data.contribution_name === "string" ? entry.data.contribution_name : null,
|
|
1224
|
+
context_order: typeof entry.data.order === "number" ? entry.data.order : null,
|
|
1225
|
+
context_action: entry.data.action === "published" || entry.data.action === "retained" || entry.data.action === "removed" ? entry.data.action : null,
|
|
1226
|
+
context_revision: typeof entry.data.revision === "string" ? entry.data.revision : null,
|
|
1227
|
+
display_title: typeof display.title === "string" ? display.title : null,
|
|
1228
|
+
display_summary: typeof display.summary === "string" ? display.summary : null,
|
|
1229
|
+
sources: Array.isArray(display.sources) ? display.sources.filter(isRecord) : []
|
|
1230
|
+
});
|
|
1231
|
+
if (!context) return null;
|
|
1232
|
+
const turnId = typeof entry.id === "string" && entry.id ? entry.id : `context-${sequence}`;
|
|
1233
|
+
return {
|
|
1234
|
+
id: turnId,
|
|
1235
|
+
sequence,
|
|
1236
|
+
turn_id: turnId,
|
|
1237
|
+
loop_id: typeof entry.loop_name === "string" && entry.loop_name ? entry.loop_name : "root",
|
|
1238
|
+
kind: "context",
|
|
1239
|
+
role: "system",
|
|
1240
|
+
status: "completed",
|
|
1241
|
+
blocks: [],
|
|
1242
|
+
tool_calls: [],
|
|
1243
|
+
model: null,
|
|
1244
|
+
usage: null,
|
|
1245
|
+
duration_ms: 0,
|
|
1246
|
+
context_content: Object.hasOwn(message, "content") ? message.content : null,
|
|
1247
|
+
...context
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1026
1251
|
// src/shared/projection/builder.ts
|
|
1252
|
+
function backgroundTaskStatus(payload) {
|
|
1253
|
+
const tasks = Array.isArray(payload.tasks) ? payload.tasks : [];
|
|
1254
|
+
return tasks.some((task) => isRecord(task) && (["failed", "cancelled", "lost"].includes(String(task.status)) || typeof task.exit_code === "number" && task.exit_code !== 0)) ? "error" : "done";
|
|
1255
|
+
}
|
|
1256
|
+
function acknowledgeTurnEvents(acknowledge) {
|
|
1257
|
+
acknowledge?.();
|
|
1258
|
+
}
|
|
1027
1259
|
function hasChatRunEvent(events) {
|
|
1028
1260
|
return events.some(
|
|
1029
|
-
(event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
|
|
1261
|
+
(event) => event.type !== "llm:first_token:delivered" && event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
|
|
1030
1262
|
);
|
|
1031
1263
|
}
|
|
1032
1264
|
function acceptedPostChatFollowupCompletesLatestRun(events, updates) {
|
|
@@ -1058,7 +1290,8 @@ function completedResponseText(content) {
|
|
|
1058
1290
|
).filter((part) => part.type === "text").map((part) => String(part.text ?? "")).join("");
|
|
1059
1291
|
}
|
|
1060
1292
|
function replaceTextContent(state, content) {
|
|
1061
|
-
|
|
1293
|
+
const currentContent = state.blocks.filter((block) => block.type === "text").map((block) => String(block.content ?? "")).join("");
|
|
1294
|
+
if (currentContent === content) return;
|
|
1062
1295
|
const replacement = { type: "text", content };
|
|
1063
1296
|
const blocks = [];
|
|
1064
1297
|
let inserted = false;
|
|
@@ -1078,11 +1311,39 @@ function replaceTextContent(state, content) {
|
|
|
1078
1311
|
}
|
|
1079
1312
|
state.blocks = blocks;
|
|
1080
1313
|
}
|
|
1314
|
+
function applyResponseBlockOrder(state, value) {
|
|
1315
|
+
if (!Array.isArray(value)) return false;
|
|
1316
|
+
const ordered = [];
|
|
1317
|
+
for (const item of value) {
|
|
1318
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
|
|
1319
|
+
const block = item;
|
|
1320
|
+
if (block.type === "text" || block.type === "thinking") {
|
|
1321
|
+
const content = String(block[block.type] ?? "");
|
|
1322
|
+
if (content) ordered.push({ type: block.type, content });
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
if (block.type !== "tool_call") continue;
|
|
1326
|
+
const toolCallId = String(block.id ?? "");
|
|
1327
|
+
const toolBlock = state.blocks.find(
|
|
1328
|
+
(candidate) => candidate.type === "tool_use" && candidate.tool_call_id === toolCallId
|
|
1329
|
+
);
|
|
1330
|
+
if (toolBlock) ordered.push(toolBlock);
|
|
1331
|
+
}
|
|
1332
|
+
if (ordered.length === 0 && value.length > 0) return false;
|
|
1333
|
+
const unrelatedBlocks = state.blocks.filter(
|
|
1334
|
+
(block) => block.type !== "text" && block.type !== "thinking" && block.type !== "tool_use"
|
|
1335
|
+
);
|
|
1336
|
+
state.blocks = [...ordered, ...unrelatedBlocks];
|
|
1337
|
+
return true;
|
|
1338
|
+
}
|
|
1081
1339
|
var ClientProjectionBuilder = class {
|
|
1082
1340
|
activeTurns = /* @__PURE__ */ new Map();
|
|
1083
1341
|
lastTurnIds = /* @__PURE__ */ new Map();
|
|
1084
1342
|
latestTurns = /* @__PURE__ */ new Map();
|
|
1343
|
+
turnsById = /* @__PURE__ */ new Map();
|
|
1085
1344
|
loopDescriptions = /* @__PURE__ */ new Map();
|
|
1345
|
+
/** loop_name → 正在运行的子智能体起点,agent:end 结算并挂到父工具调用上。 */
|
|
1346
|
+
activeSubagentRuns = /* @__PURE__ */ new Map();
|
|
1086
1347
|
pendingChildPauses = /* @__PURE__ */ new Map();
|
|
1087
1348
|
pendingMemoryRefs = /* @__PURE__ */ new Map();
|
|
1088
1349
|
activeCompactions = /* @__PURE__ */ new Map();
|
|
@@ -1117,6 +1378,8 @@ var ClientProjectionBuilder = class {
|
|
|
1117
1378
|
return this.onMemoryInject(loopId, payload);
|
|
1118
1379
|
case "memory:inject:none":
|
|
1119
1380
|
return null;
|
|
1381
|
+
case "context:entry":
|
|
1382
|
+
return this.onContextEntry(payload);
|
|
1120
1383
|
case "loop:turn":
|
|
1121
1384
|
return this.onLoopTurn(loopId, payload);
|
|
1122
1385
|
case "mode:change":
|
|
@@ -1140,6 +1403,25 @@ var ClientProjectionBuilder = class {
|
|
|
1140
1403
|
}
|
|
1141
1404
|
if (event.type.startsWith("compaction:"))
|
|
1142
1405
|
return this.onCompaction(event.type, loopId, payload);
|
|
1406
|
+
if (event.type === "llm:first_token:delivered") {
|
|
1407
|
+
const targetTurnId = String(payload.turn_id ?? "");
|
|
1408
|
+
const active = this.activeTurns.get(loopId);
|
|
1409
|
+
if (active && (!targetTurnId || active.turnId === targetTurnId)) {
|
|
1410
|
+
this.applyFirstTokenDelivery(active, payload);
|
|
1411
|
+
return this.syncPatch(active);
|
|
1412
|
+
}
|
|
1413
|
+
const latest = targetTurnId ? this.turnsById.get(targetTurnId) : this.latestTurns.get(loopId);
|
|
1414
|
+
const duration = payload.first_token_delivery_ms;
|
|
1415
|
+
if (!latest || latest.role !== "assistant" || typeof duration !== "number" || duration < 0) {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
const turn = { ...latest, first_token_delivery_ms: duration };
|
|
1419
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1420
|
+
if (this.latestTurns.get(loopId)?.turn_id === turn.turn_id) {
|
|
1421
|
+
this.latestTurns.set(loopId, turn);
|
|
1422
|
+
}
|
|
1423
|
+
return [{ kind: "upsert", turn }];
|
|
1424
|
+
}
|
|
1143
1425
|
const state = this.activeTurns.get(loopId);
|
|
1144
1426
|
if (!state) {
|
|
1145
1427
|
if (event.type === "chat:end") {
|
|
@@ -1155,16 +1437,28 @@ var ClientProjectionBuilder = class {
|
|
|
1155
1437
|
state.toolCalls = [];
|
|
1156
1438
|
state.usage = null;
|
|
1157
1439
|
state.responseDone = false;
|
|
1440
|
+
state.firstTokenMs = null;
|
|
1441
|
+
state.streamDurationMs = null;
|
|
1442
|
+
state.firstTokenDeliveryMs = null;
|
|
1158
1443
|
return this.syncPatch(state);
|
|
1159
1444
|
case "llm:thinking:delta":
|
|
1445
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1160
1446
|
return this.onDelta(state, "thinking", "", String(payload.content ?? ""));
|
|
1161
1447
|
case "llm:content:delta":
|
|
1448
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1162
1449
|
return this.onDelta(state, "text", "", String(payload.content ?? ""));
|
|
1163
1450
|
case "llm:content:complete":
|
|
1164
1451
|
replaceTextContent(state, String(payload.content ?? ""));
|
|
1165
1452
|
return this.syncPatch(state);
|
|
1453
|
+
case "llm:content:done":
|
|
1454
|
+
this.applyStreamTiming(state, payload);
|
|
1455
|
+
return this.syncPatch(state);
|
|
1166
1456
|
case "llm:tool_call:created":
|
|
1457
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1167
1458
|
return this.onToolCallCreated(state, payload);
|
|
1459
|
+
case "llm:tool_call:arguments:delta":
|
|
1460
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1461
|
+
return this.onToolCallArgumentsDelta(state, payload);
|
|
1168
1462
|
case "llm:response:done":
|
|
1169
1463
|
return this.onResponseDone(state, loopId, payload);
|
|
1170
1464
|
case "tool:result:delta":
|
|
@@ -1191,7 +1485,9 @@ var ClientProjectionBuilder = class {
|
|
|
1191
1485
|
this.activeTurns.clear();
|
|
1192
1486
|
this.lastTurnIds.clear();
|
|
1193
1487
|
this.latestTurns.clear();
|
|
1488
|
+
this.turnsById.clear();
|
|
1194
1489
|
this.loopDescriptions.clear();
|
|
1490
|
+
this.activeSubagentRuns.clear();
|
|
1195
1491
|
this.pendingChildPauses.clear();
|
|
1196
1492
|
this.pendingMemoryRefs.clear();
|
|
1197
1493
|
this.activeCompactions.clear();
|
|
@@ -1206,15 +1502,18 @@ var ClientProjectionBuilder = class {
|
|
|
1206
1502
|
}
|
|
1207
1503
|
_seedTurns(turns) {
|
|
1208
1504
|
const latestTurns = /* @__PURE__ */ new Map();
|
|
1505
|
+
const turnsById = /* @__PURE__ */ new Map();
|
|
1209
1506
|
for (const turn of turns) {
|
|
1210
1507
|
this.seedSequence(turn.sequence);
|
|
1211
1508
|
if (!isConversationTurn(turn)) continue;
|
|
1509
|
+
turnsById.set(turn.turn_id, turn);
|
|
1212
1510
|
const current = latestTurns.get(turn.loop_id);
|
|
1213
1511
|
if (!current || turn.sequence >= current.sequence) {
|
|
1214
1512
|
latestTurns.set(turn.loop_id, turn);
|
|
1215
1513
|
}
|
|
1216
1514
|
}
|
|
1217
1515
|
this.latestTurns = latestTurns;
|
|
1516
|
+
this.turnsById = turnsById;
|
|
1218
1517
|
}
|
|
1219
1518
|
// --- Event handlers ---
|
|
1220
1519
|
onMemoryInject(loopId, payload) {
|
|
@@ -1232,11 +1531,16 @@ var ClientProjectionBuilder = class {
|
|
|
1232
1531
|
}
|
|
1233
1532
|
return null;
|
|
1234
1533
|
}
|
|
1534
|
+
onContextEntry(payload) {
|
|
1535
|
+
if (!isRecord(payload.entry)) return null;
|
|
1536
|
+
const turn = buildContextTurn(payload.entry, this.nextSeq());
|
|
1537
|
+
return turn ? [{ kind: "upsert", turn }] : null;
|
|
1538
|
+
}
|
|
1235
1539
|
onLoopTurn(loopId, payload) {
|
|
1236
1540
|
let turnId = String(payload.turn_id ?? this.nextTurnId(`turn:${loopId}`));
|
|
1237
1541
|
let memoryRefs = null;
|
|
1238
1542
|
const pending = this.pendingMemoryRefs.get(loopId);
|
|
1239
|
-
if (pending) {
|
|
1543
|
+
if (pending && (!pending.turnId || pending.turnId === turnId)) {
|
|
1240
1544
|
this.pendingMemoryRefs.delete(loopId);
|
|
1241
1545
|
if (pending.turnId) turnId = pending.turnId;
|
|
1242
1546
|
memoryRefs = pending.refs;
|
|
@@ -1252,6 +1556,7 @@ var ClientProjectionBuilder = class {
|
|
|
1252
1556
|
this.lastTurnIds.set(loopId, turnId);
|
|
1253
1557
|
const turn = snapshot(state, this.nextSeq(), "streaming");
|
|
1254
1558
|
this.latestTurns.set(loopId, turn);
|
|
1559
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1255
1560
|
return [{ kind: "upsert", turn }];
|
|
1256
1561
|
}
|
|
1257
1562
|
onModeChange(loopId, payload) {
|
|
@@ -1288,11 +1593,11 @@ var ClientProjectionBuilder = class {
|
|
|
1288
1593
|
loopId,
|
|
1289
1594
|
toolCallId: optStr(payload.tool_call_id) ?? void 0,
|
|
1290
1595
|
toolName: toolNameFromPayload(payload) ?? void 0,
|
|
1291
|
-
workspacePath:
|
|
1596
|
+
workspacePath: isRecord(payload.workspace_change) ? optStr(payload.workspace_change.to) ?? void 0 : void 0
|
|
1292
1597
|
}
|
|
1293
1598
|
];
|
|
1294
1599
|
const change = payload.workspace_change;
|
|
1295
|
-
if (
|
|
1600
|
+
if (isRecord(change)) {
|
|
1296
1601
|
const source = optStr(change.source) ?? "";
|
|
1297
1602
|
const projectName = optStr(change.project_name) ?? "";
|
|
1298
1603
|
const fromPath = optStr(change.from) ?? "";
|
|
@@ -1324,6 +1629,13 @@ var ClientProjectionBuilder = class {
|
|
|
1324
1629
|
onAgentStart(loopId, payload) {
|
|
1325
1630
|
const description = optStr(payload.description);
|
|
1326
1631
|
if (description) this.loopDescriptions.set(loopId, description);
|
|
1632
|
+
const subagentStartedAt = optStr(payload.started_at);
|
|
1633
|
+
if (subagentStartedAt) {
|
|
1634
|
+
this.activeSubagentRuns.set(loopId, {
|
|
1635
|
+
runId: optStr(payload.run_id) ?? loopId,
|
|
1636
|
+
startedAt: subagentStartedAt
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1327
1639
|
const turn = buildSystemNotificationTurn(
|
|
1328
1640
|
this.nextSeq(),
|
|
1329
1641
|
this.nextTurnId("agent:start"),
|
|
@@ -1365,7 +1677,7 @@ var ClientProjectionBuilder = class {
|
|
|
1365
1677
|
this.nextTurnId("bg:tasks_completed"),
|
|
1366
1678
|
loopId,
|
|
1367
1679
|
"bg:tasks_completed",
|
|
1368
|
-
|
|
1680
|
+
backgroundTaskStatus(payload),
|
|
1369
1681
|
`Background tasks completed (${count})`,
|
|
1370
1682
|
detail || null,
|
|
1371
1683
|
{ task_ids: taskIds, count }
|
|
@@ -1385,7 +1697,32 @@ var ClientProjectionBuilder = class {
|
|
|
1385
1697
|
}
|
|
1386
1698
|
const directTurn = this.activeTurns.get(loopId) ?? this.latestTurns.get(loopId);
|
|
1387
1699
|
setToolCallStatus(directTurn, toolCallId, "done", null, null);
|
|
1388
|
-
|
|
1700
|
+
const updates = this.collectLoopUpdates(loopId, parentLoopId);
|
|
1701
|
+
const entryId = typeof payload.entry_id === "string" ? payload.entry_id : "";
|
|
1702
|
+
if (!entryId) return updates;
|
|
1703
|
+
const answerData = isRecord(payload.answer_data) ? payload.answer_data : {};
|
|
1704
|
+
const answerTurn = {
|
|
1705
|
+
id: entryId,
|
|
1706
|
+
sequence: this.nextSeq(),
|
|
1707
|
+
turn_id: entryId,
|
|
1708
|
+
loop_id: loopId,
|
|
1709
|
+
kind: "message",
|
|
1710
|
+
role: "assistant",
|
|
1711
|
+
status: "completed",
|
|
1712
|
+
blocks: [
|
|
1713
|
+
{
|
|
1714
|
+
type: "ask_user_answer",
|
|
1715
|
+
content: answerData,
|
|
1716
|
+
tool_call_id: toolCallId || null
|
|
1717
|
+
}
|
|
1718
|
+
],
|
|
1719
|
+
tool_calls: [],
|
|
1720
|
+
model: null,
|
|
1721
|
+
usage: null,
|
|
1722
|
+
duration_ms: 0,
|
|
1723
|
+
client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
|
|
1724
|
+
};
|
|
1725
|
+
return [...updates, { kind: "upsert", turn: answerTurn }];
|
|
1389
1726
|
}
|
|
1390
1727
|
onUserMessage(_loopId, payload) {
|
|
1391
1728
|
const entryId = String(payload.entry_id ?? this.nextTurnId("user"));
|
|
@@ -1402,8 +1739,12 @@ var ClientProjectionBuilder = class {
|
|
|
1402
1739
|
tool_calls: [],
|
|
1403
1740
|
model: null,
|
|
1404
1741
|
usage: null,
|
|
1405
|
-
duration_ms: 0
|
|
1742
|
+
duration_ms: 0,
|
|
1743
|
+
client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
|
|
1406
1744
|
};
|
|
1745
|
+
if (payload.start_reason === "background-completion") {
|
|
1746
|
+
turn.blocks.push({ type: "system_notification", content: { notification_type: "bg:wakeup" } });
|
|
1747
|
+
}
|
|
1407
1748
|
this.latestTurns.set("root", turn);
|
|
1408
1749
|
return [{ kind: "upsert", turn }];
|
|
1409
1750
|
}
|
|
@@ -1430,13 +1771,7 @@ var ClientProjectionBuilder = class {
|
|
|
1430
1771
|
}
|
|
1431
1772
|
onToolCallCreated(state, payload) {
|
|
1432
1773
|
if ("arguments_delta" in payload) {
|
|
1433
|
-
|
|
1434
|
-
const toolCallId2 = String(payload.id ?? "");
|
|
1435
|
-
if (delta && toolCallId2) {
|
|
1436
|
-
appendToolCallArguments(state, toolCallId2, delta);
|
|
1437
|
-
return this.syncPatch(state);
|
|
1438
|
-
}
|
|
1439
|
-
return null;
|
|
1774
|
+
return this.onToolCallArgumentsDelta(state, payload);
|
|
1440
1775
|
}
|
|
1441
1776
|
const toolCallId = String(payload.id ?? "");
|
|
1442
1777
|
const toolName = toolNameFromPayload(payload);
|
|
@@ -1445,16 +1780,21 @@ var ClientProjectionBuilder = class {
|
|
|
1445
1780
|
upsertToolCall(state, toolCallId, toolName, displayName, args);
|
|
1446
1781
|
return this.syncPatch(state);
|
|
1447
1782
|
}
|
|
1783
|
+
onToolCallArgumentsDelta(state, payload) {
|
|
1784
|
+
const delta = String(payload.arguments_delta ?? "");
|
|
1785
|
+
const toolCallId = String(payload.id ?? "");
|
|
1786
|
+
if (!delta || !toolCallId) return null;
|
|
1787
|
+
appendToolCallArguments(state, toolCallId, delta);
|
|
1788
|
+
return this.syncPatch(state);
|
|
1789
|
+
}
|
|
1448
1790
|
onResponseDone(state, loopId, payload) {
|
|
1449
1791
|
state.responseDone = true;
|
|
1792
|
+
this.applyStreamTiming(state, payload);
|
|
1450
1793
|
const model = optStr(payload.model);
|
|
1451
1794
|
if (model) state.model = model;
|
|
1452
1795
|
if (typeof payload.usage === "object" && payload.usage !== null) {
|
|
1453
1796
|
state.usage = { ...payload.usage };
|
|
1454
1797
|
}
|
|
1455
|
-
if ("content" in payload) {
|
|
1456
|
-
replaceTextContent(state, completedResponseText(payload.content));
|
|
1457
|
-
}
|
|
1458
1798
|
const toolCalls = payload.tool_calls;
|
|
1459
1799
|
if (Array.isArray(toolCalls)) {
|
|
1460
1800
|
for (const raw of toolCalls) {
|
|
@@ -1472,11 +1812,42 @@ var ClientProjectionBuilder = class {
|
|
|
1472
1812
|
);
|
|
1473
1813
|
}
|
|
1474
1814
|
}
|
|
1475
|
-
if (state.
|
|
1815
|
+
if (!applyResponseBlockOrder(state, payload.response_blocks) && "content" in payload) {
|
|
1816
|
+
replaceTextContent(state, completedResponseText(payload.content));
|
|
1817
|
+
}
|
|
1818
|
+
const isNativeProviderResponse = typeof payload.provider === "string" && payload.provider !== "";
|
|
1819
|
+
if (state.toolCalls.length === 0 || isNativeProviderResponse && state.toolCalls.every(
|
|
1820
|
+
(toolCall) => toolCall.status !== "pending" && toolCall.status !== "awaiting_answer"
|
|
1821
|
+
)) {
|
|
1476
1822
|
return this.finalize(loopId, "completed");
|
|
1477
1823
|
}
|
|
1478
1824
|
return this.syncPatch(state);
|
|
1479
1825
|
}
|
|
1826
|
+
applyFirstTokenDelivery(state, payload) {
|
|
1827
|
+
const firstTokenMs = payload.first_token_ms;
|
|
1828
|
+
if (state.firstTokenMs === null && typeof firstTokenMs === "number" && firstTokenMs >= 0) {
|
|
1829
|
+
state.firstTokenMs = Math.round(firstTokenMs);
|
|
1830
|
+
}
|
|
1831
|
+
const duration = payload.first_token_delivery_ms;
|
|
1832
|
+
if (state.firstTokenDeliveryMs === null && typeof duration === "number" && duration >= 0) {
|
|
1833
|
+
state.firstTokenDeliveryMs = duration;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
applyStreamTiming(state, payload) {
|
|
1837
|
+
if (typeof payload.first_token_ms === "number" && payload.first_token_ms >= 0) {
|
|
1838
|
+
state.firstTokenMs = Math.round(payload.first_token_ms);
|
|
1839
|
+
}
|
|
1840
|
+
if (typeof payload.stream_duration_ms === "number" && payload.stream_duration_ms >= 0) {
|
|
1841
|
+
state.streamDurationMs = Math.round(payload.stream_duration_ms);
|
|
1842
|
+
}
|
|
1843
|
+
if (typeof payload.started_at === "string" && payload.started_at) {
|
|
1844
|
+
state.modelStartedAt = payload.started_at;
|
|
1845
|
+
state.modelStartedAtReported = true;
|
|
1846
|
+
}
|
|
1847
|
+
if (typeof payload.duration_ms === "number" && payload.duration_ms >= 0) {
|
|
1848
|
+
state.modelDurationMs = Math.round(payload.duration_ms);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1480
1851
|
onToolResultDone(state, loopId, payload) {
|
|
1481
1852
|
const toolCallId = String(payload.tool_call_id ?? "");
|
|
1482
1853
|
const streamedResult = state.toolCalls.find((toolCall) => toolCall.id === toolCallId)?.result;
|
|
@@ -1484,9 +1855,12 @@ var ClientProjectionBuilder = class {
|
|
|
1484
1855
|
const status = toolStatusFromResult(content);
|
|
1485
1856
|
const rawDuration = payload.duration_ms;
|
|
1486
1857
|
const durationMs = typeof rawDuration === "number" && rawDuration >= 0 ? Math.round(rawDuration) : null;
|
|
1858
|
+
const ranInParallel = typeof payload.ran_in_parallel === "boolean" ? payload.ran_in_parallel : null;
|
|
1487
1859
|
const sl = sourceLoopFor(state.loopId, this.loopDescriptions);
|
|
1488
|
-
|
|
1489
|
-
|
|
1860
|
+
const startedAt = typeof payload.started_at === "string" && payload.started_at ? payload.started_at : null;
|
|
1861
|
+
applyToolResult(state, toolCallId, content, status, durationMs, ranInParallel, sl, startedAt);
|
|
1862
|
+
const isNativeProviderResult = typeof payload.provider === "string" && payload.provider !== "";
|
|
1863
|
+
if (!isNativeProviderResult && this.shouldFinalizeAfterToolResults(state)) {
|
|
1490
1864
|
return this.finalize(loopId, "completed");
|
|
1491
1865
|
}
|
|
1492
1866
|
return this.syncPatch(state);
|
|
@@ -1515,10 +1889,13 @@ var ClientProjectionBuilder = class {
|
|
|
1515
1889
|
onToolBridge(state, payload) {
|
|
1516
1890
|
const bridge = payload.bridge;
|
|
1517
1891
|
if (typeof bridge !== "object" || bridge === null) return null;
|
|
1892
|
+
const toolCallId = String(payload.tool_call_id ?? "");
|
|
1893
|
+
if (!toolCallId || !state.toolCalls.some((toolCall) => toolCall.id === toolCallId)) return null;
|
|
1518
1894
|
state.blocks.push({
|
|
1519
1895
|
type: "tool_bridge",
|
|
1520
1896
|
content: bridge,
|
|
1521
|
-
|
|
1897
|
+
delivery_id: String(payload.delivery_id ?? "") || null,
|
|
1898
|
+
tool_call_id: toolCallId
|
|
1522
1899
|
});
|
|
1523
1900
|
return this.syncPatch(state);
|
|
1524
1901
|
}
|
|
@@ -1527,6 +1904,11 @@ var ClientProjectionBuilder = class {
|
|
|
1527
1904
|
const toolName = String(payload.tool_name ?? "");
|
|
1528
1905
|
const displayName = this.displayNameResolver(toolName);
|
|
1529
1906
|
upsertToolCall(state, toolCallId, toolName, displayName, "");
|
|
1907
|
+
const relatedToolCallId = String(payload.related_tool_call_id ?? "");
|
|
1908
|
+
if (relatedToolCallId && relatedToolCallId !== toolCallId) {
|
|
1909
|
+
const related = state.toolCalls.find((toolCall) => toolCall.id === relatedToolCallId);
|
|
1910
|
+
if (related) related.status = "awaiting_answer";
|
|
1911
|
+
}
|
|
1530
1912
|
return this.syncPatch(state);
|
|
1531
1913
|
}
|
|
1532
1914
|
onChildPause(state, loopId, payload) {
|
|
@@ -1632,6 +2014,7 @@ var ClientProjectionBuilder = class {
|
|
|
1632
2014
|
}
|
|
1633
2015
|
onAgentEnd(state, loopId, payload) {
|
|
1634
2016
|
const ok = payload.ok !== false;
|
|
2017
|
+
this.settleSubagentRun(loopId, payload);
|
|
1635
2018
|
const status = ok ? "done" : "error";
|
|
1636
2019
|
const title = `Agent: ${payload.description ?? payload.skill_id ?? "agent"}`;
|
|
1637
2020
|
const detail = optStr(payload.error);
|
|
@@ -1639,6 +2022,7 @@ var ClientProjectionBuilder = class {
|
|
|
1639
2022
|
skill_id: optStr(payload.skill_id),
|
|
1640
2023
|
description: optStr(payload.description),
|
|
1641
2024
|
parent_fork_tool_call_id: optStr(payload.parent_fork_tool_call_id),
|
|
2025
|
+
...typeof payload.run_status === "string" ? { run_status: payload.run_status } : {},
|
|
1642
2026
|
ok
|
|
1643
2027
|
};
|
|
1644
2028
|
if (state) {
|
|
@@ -1658,6 +2042,32 @@ var ClientProjectionBuilder = class {
|
|
|
1658
2042
|
);
|
|
1659
2043
|
return [{ kind: "upsert", turn }];
|
|
1660
2044
|
}
|
|
2045
|
+
settleSubagentRun(loopId, payload) {
|
|
2046
|
+
const active = this.activeSubagentRuns.get(loopId);
|
|
2047
|
+
this.activeSubagentRuns.delete(loopId);
|
|
2048
|
+
const parentToolCallId = optStr(payload.parent_fork_tool_call_id);
|
|
2049
|
+
if (!parentToolCallId) return;
|
|
2050
|
+
const rawDuration = payload.run_duration_ms;
|
|
2051
|
+
const durationMs = typeof rawDuration === "number" && rawDuration >= 0 ? Math.round(rawDuration) : null;
|
|
2052
|
+
const rawStatus = optStr(payload.run_status);
|
|
2053
|
+
const status = rawStatus === "failed" || rawStatus === "cancelled" || rawStatus === "paused" || rawStatus === "incomplete" ? rawStatus : payload.ok === false ? "failed" : "completed";
|
|
2054
|
+
const run = {
|
|
2055
|
+
run_id: active?.runId ?? optStr(payload.run_id) ?? loopId,
|
|
2056
|
+
scope: "subagent",
|
|
2057
|
+
loop_id: loopId,
|
|
2058
|
+
started_at: active?.startedAt ?? optStr(payload.run_started_at),
|
|
2059
|
+
duration_ms: durationMs,
|
|
2060
|
+
status
|
|
2061
|
+
};
|
|
2062
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2063
|
+
for (const turn of [...this.latestTurns.values(), ...this.turnsById.values()]) {
|
|
2064
|
+
if (seen.has(turn)) continue;
|
|
2065
|
+
seen.add(turn);
|
|
2066
|
+
const toolCall = turn.tool_calls.find((item) => item.id === parentToolCallId);
|
|
2067
|
+
if (!toolCall) continue;
|
|
2068
|
+
toolCall.subagent_runs = [...toolCall.subagent_runs ?? [], run];
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
1661
2071
|
dropIncompleteToolCalls(state) {
|
|
1662
2072
|
if (state.responseDone) return;
|
|
1663
2073
|
state.toolCalls = [];
|
|
@@ -1707,6 +2117,7 @@ var ClientProjectionBuilder = class {
|
|
|
1707
2117
|
syncPatch(state) {
|
|
1708
2118
|
const turn = snapshot(state, this.nextSeq(), "streaming");
|
|
1709
2119
|
this.latestTurns.set(state.loopId, turn);
|
|
2120
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1710
2121
|
this.lastTurnIds.set(state.loopId, turn.turn_id);
|
|
1711
2122
|
return [{ kind: "upsert", turn }];
|
|
1712
2123
|
}
|
|
@@ -1717,6 +2128,7 @@ var ClientProjectionBuilder = class {
|
|
|
1717
2128
|
const turn = snapshot(state, this.nextSeq(), status);
|
|
1718
2129
|
this.lastTurnIds.set(loopId, turn.turn_id);
|
|
1719
2130
|
this.latestTurns.set(loopId, turn);
|
|
2131
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1720
2132
|
return [{ kind: "upsert", turn }];
|
|
1721
2133
|
}
|
|
1722
2134
|
shouldFinalizeAfterToolResults(state) {
|
|
@@ -1754,9 +2166,6 @@ var ClientProjectionBuilder = class {
|
|
|
1754
2166
|
return `${prefix}:${this.syntheticCounter}:${hex}`;
|
|
1755
2167
|
}
|
|
1756
2168
|
};
|
|
1757
|
-
function isRecord2(value) {
|
|
1758
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1759
|
-
}
|
|
1760
2169
|
function optStr(value) {
|
|
1761
2170
|
if (value == null || value === "") return null;
|
|
1762
2171
|
return String(value);
|
|
@@ -1776,6 +2185,101 @@ function toolArgsFromPayload(payload) {
|
|
|
1776
2185
|
return "";
|
|
1777
2186
|
}
|
|
1778
2187
|
|
|
2188
|
+
// src/schemas/mcp-app.ts
|
|
2189
|
+
function isMcpAppData(value) {
|
|
2190
|
+
return isRecord(value) && typeof value.sourceId === "string" && value.sourceId.length > 0 && typeof value.resourceUri === "string" && value.resourceUri.startsWith("ui://") && typeof value.mimeType === "string" && isRecord(value.resourceMeta) && isRecord(value.toolInput) && isRecord(value.toolResult);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// src/shared/tool-ui-card.ts
|
|
2194
|
+
function isNonEmptyString(value) {
|
|
2195
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
2196
|
+
}
|
|
2197
|
+
function isPositiveNumber(value) {
|
|
2198
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
2199
|
+
}
|
|
2200
|
+
function isToolUiCard(value) {
|
|
2201
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2202
|
+
return false;
|
|
2203
|
+
}
|
|
2204
|
+
const raw = value;
|
|
2205
|
+
if (raw.target !== "inline" && raw.target !== "preview") {
|
|
2206
|
+
return false;
|
|
2207
|
+
}
|
|
2208
|
+
if (!isPositiveNumber(raw.height)) {
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
if (raw.archived === true && !isNonEmptyString(raw.resourceHTML)) {
|
|
2212
|
+
return false;
|
|
2213
|
+
}
|
|
2214
|
+
if (!isNonEmptyString(raw.resourceHTML) && !isNonEmptyString(raw.resourceUri) && !isNonEmptyString(raw.resourceURI) && !(raw.target === "preview" && isNonEmptyString(raw.sourcePath))) {
|
|
2215
|
+
return false;
|
|
2216
|
+
}
|
|
2217
|
+
if (raw.title != null && !isNonEmptyString(raw.title)) {
|
|
2218
|
+
return false;
|
|
2219
|
+
}
|
|
2220
|
+
if (raw.sourcePath != null && !isNonEmptyString(raw.sourcePath)) return false;
|
|
2221
|
+
if (raw.mcpApp !== void 0 && !isMcpAppData(raw.mcpApp)) {
|
|
2222
|
+
return false;
|
|
2223
|
+
}
|
|
2224
|
+
return true;
|
|
2225
|
+
}
|
|
2226
|
+
function resolveToolUiCardContent(card) {
|
|
2227
|
+
if (isNonEmptyString(card.resourceHTML)) {
|
|
2228
|
+
return { type: "resource-html", content: card.resourceHTML };
|
|
2229
|
+
}
|
|
2230
|
+
if (card.archived) return null;
|
|
2231
|
+
const uri = isNonEmptyString(card.resourceUri) ? card.resourceUri : card.resourceURI;
|
|
2232
|
+
if (isNonEmptyString(uri)) return { type: "resource-uri", content: uri };
|
|
2233
|
+
return card.target === "preview" && isNonEmptyString(card.sourcePath) ? { type: "resource-file", content: card.sourcePath } : null;
|
|
2234
|
+
}
|
|
2235
|
+
function buildToolUiCardKey(toolCallId, type3, content, sourcePath) {
|
|
2236
|
+
if (type3 === "resource-uri") {
|
|
2237
|
+
return `tool-preview-uri:${content}`;
|
|
2238
|
+
}
|
|
2239
|
+
if (sourcePath?.trim()) return sourcePath.trim();
|
|
2240
|
+
if (type3 === "resource-file") return content;
|
|
2241
|
+
return `tool-preview:${toolCallId}`;
|
|
2242
|
+
}
|
|
2243
|
+
function isInternalStatusToolUiCard(card) {
|
|
2244
|
+
return card?.title?.trim() === "\u9636\u6BB5\u8FDB\u5EA6";
|
|
2245
|
+
}
|
|
2246
|
+
function isAppDevToolUiCard(card, isAppDev) {
|
|
2247
|
+
return isAppDev && card.target === "preview" && !card.resourceHTML && Boolean(card.resourceUri || card.resourceURI);
|
|
2248
|
+
}
|
|
2249
|
+
function collectInlineToolUiCards(messages) {
|
|
2250
|
+
return messages.flatMap(
|
|
2251
|
+
(message) => (message.blocks ?? []).flatMap(
|
|
2252
|
+
(block, index) => block.type === "tool_ui" && block.tool_call_id && isToolUiCard(block.content) && block.content.target === "inline" ? [
|
|
2253
|
+
{
|
|
2254
|
+
key: `${message.entry_id ?? message.timestamp ?? "message"}-${block.tool_call_id}-${index}`,
|
|
2255
|
+
toolCallId: block.tool_call_id,
|
|
2256
|
+
card: block.content
|
|
2257
|
+
}
|
|
2258
|
+
] : []
|
|
2259
|
+
)
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
function collectPreviewToolUiCards(messages) {
|
|
2263
|
+
return messages.flatMap(
|
|
2264
|
+
(message) => (message.blocks ?? []).flatMap((block, index) => {
|
|
2265
|
+
if (block.type !== "tool_ui" || !block.tool_call_id || !isToolUiCard(block.content) || block.content.target !== "preview") {
|
|
2266
|
+
return [];
|
|
2267
|
+
}
|
|
2268
|
+
const toolCall = (message.tool_calls ?? []).find(
|
|
2269
|
+
(candidate) => candidate.id === block.tool_call_id
|
|
2270
|
+
);
|
|
2271
|
+
return toolCall ? [
|
|
2272
|
+
{
|
|
2273
|
+
key: `${message.entry_id ?? message.timestamp ?? "message"}-${block.tool_call_id}-${index}`,
|
|
2274
|
+
toolCall,
|
|
2275
|
+
card: block.content,
|
|
2276
|
+
blocks: message.blocks ?? []
|
|
2277
|
+
}
|
|
2278
|
+
] : [];
|
|
2279
|
+
})
|
|
2280
|
+
);
|
|
2281
|
+
}
|
|
2282
|
+
|
|
1779
2283
|
// src/shared/projection/history.ts
|
|
1780
2284
|
function projectHistory(entries) {
|
|
1781
2285
|
const turns = [];
|
|
@@ -1783,6 +2287,8 @@ function projectHistory(entries) {
|
|
|
1783
2287
|
const latestAssistantByLoop = /* @__PURE__ */ new Map();
|
|
1784
2288
|
const pendingChildPauses = /* @__PURE__ */ new Map();
|
|
1785
2289
|
const answeredToolCallIds = /* @__PURE__ */ new Set();
|
|
2290
|
+
const nativeToolTurns = /* @__PURE__ */ new Set();
|
|
2291
|
+
const turnEntryAliases = /* @__PURE__ */ new Map();
|
|
1786
2292
|
let latestRootMessage = null;
|
|
1787
2293
|
let latestRootAssistant = null;
|
|
1788
2294
|
let sequence = 0;
|
|
@@ -1790,7 +2296,40 @@ function projectHistory(entries) {
|
|
|
1790
2296
|
const kind = String(entry.kind ?? "");
|
|
1791
2297
|
const loopId = String(entry.loop_name ?? "root");
|
|
1792
2298
|
const id = String(entry.id ?? `history-${sequence + 1}`);
|
|
1793
|
-
if (kind === "
|
|
2299
|
+
if (kind === "turn_timing" && isRecord(entry.data)) {
|
|
2300
|
+
const turnId = typeof entry.data.turn_id === "string" ? entry.data.turn_id : "";
|
|
2301
|
+
const duration = numberOrNull(entry.data.first_token_delivery_ms);
|
|
2302
|
+
const target = turnEntryAliases.get(turnId);
|
|
2303
|
+
if (target && duration !== null) target.first_token_delivery_ms = duration;
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
if (kind === "run_timing" && isRecord(entry.data)) {
|
|
2307
|
+
const data = entry.data;
|
|
2308
|
+
if (data.scope === "subagent") {
|
|
2309
|
+
const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
|
|
2310
|
+
const duration = numberOrNull(data.duration_ms);
|
|
2311
|
+
const rawStatus = String(data.status ?? "completed");
|
|
2312
|
+
const status = rawStatus === "failed" || rawStatus === "cancelled" || rawStatus === "paused" || rawStatus === "incomplete" ? rawStatus : "completed";
|
|
2313
|
+
for (const turn of turns) {
|
|
2314
|
+
const toolCall = turn.tool_calls.find((item) => item.id === parentToolCallId);
|
|
2315
|
+
if (!toolCall) continue;
|
|
2316
|
+
toolCall.subagent_runs = [
|
|
2317
|
+
...toolCall.subagent_runs ?? [],
|
|
2318
|
+
{
|
|
2319
|
+
run_id: String(data.run_id ?? ""),
|
|
2320
|
+
scope: "subagent",
|
|
2321
|
+
loop_id: loopId,
|
|
2322
|
+
started_at: typeof data.started_at === "string" ? data.started_at : null,
|
|
2323
|
+
duration_ms: duration,
|
|
2324
|
+
status
|
|
2325
|
+
}
|
|
2326
|
+
];
|
|
2327
|
+
break;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
if (kind === "post_chat_followup" && isRecord(entry.data)) {
|
|
1794
2333
|
const assistantEntryId = String(entry.data.assistant_entry_id ?? "");
|
|
1795
2334
|
if (latestRootMessage?.role === "assistant" && latestRootMessage.turn_id === assistantEntryId) {
|
|
1796
2335
|
const followup = normalizePostChatFollowup(entry.data, assistantEntryId);
|
|
@@ -1798,7 +2337,15 @@ function projectHistory(entries) {
|
|
|
1798
2337
|
}
|
|
1799
2338
|
continue;
|
|
1800
2339
|
}
|
|
1801
|
-
if (kind === "
|
|
2340
|
+
if (kind === "context") {
|
|
2341
|
+
const turn = buildContextTurn(entry, sequence + 1);
|
|
2342
|
+
if (turn) {
|
|
2343
|
+
sequence += 1;
|
|
2344
|
+
turns.push(turn);
|
|
2345
|
+
}
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
if (kind === "message" && isRecord(entry.message)) {
|
|
1802
2349
|
const message = entry.message;
|
|
1803
2350
|
const role = String(message.role ?? "");
|
|
1804
2351
|
if (role === "tool") {
|
|
@@ -1808,6 +2355,12 @@ function projectHistory(entries) {
|
|
|
1808
2355
|
if (owner && toolCall) {
|
|
1809
2356
|
toolCall.status = message._error ? "error" : inferToolStatus(String(message.content ?? ""));
|
|
1810
2357
|
toolCall.result = message.content ?? "";
|
|
2358
|
+
const duration = numberOrNull(message._duration_ms);
|
|
2359
|
+
if (duration !== null) toolCall.duration_ms = duration;
|
|
2360
|
+
if (typeof message._started_at === "string") {
|
|
2361
|
+
toolCall.started_at = message._started_at;
|
|
2362
|
+
}
|
|
2363
|
+
if (message._ran_in_parallel === true) toolCall.ran_in_parallel = true;
|
|
1811
2364
|
owner.blocks.push({
|
|
1812
2365
|
type: "tool_result",
|
|
1813
2366
|
content: message.content ?? "",
|
|
@@ -1817,8 +2370,11 @@ function projectHistory(entries) {
|
|
|
1817
2370
|
continue;
|
|
1818
2371
|
}
|
|
1819
2372
|
if (role !== "user" && role !== "assistant" && role !== "system") continue;
|
|
1820
|
-
const toolCalls = role === "assistant" ? buildToolCalls(message.tool_calls) : [];
|
|
2373
|
+
const toolCalls = role === "assistant" && !message._error ? buildToolCalls(message.tool_calls) : [];
|
|
1821
2374
|
const blocks = buildBlocks(message, toolCalls);
|
|
2375
|
+
if (role === "user" && message._start_reason === "background-completion") {
|
|
2376
|
+
blocks.push({ type: "system_notification", content: { notification_type: "bg:wakeup" } });
|
|
2377
|
+
}
|
|
1822
2378
|
const turn = {
|
|
1823
2379
|
id,
|
|
1824
2380
|
sequence: ++sequence,
|
|
@@ -1830,14 +2386,52 @@ function projectHistory(entries) {
|
|
|
1830
2386
|
blocks,
|
|
1831
2387
|
tool_calls: toolCalls,
|
|
1832
2388
|
model: stringOrNull(message.model),
|
|
1833
|
-
usage:
|
|
1834
|
-
|
|
1835
|
-
|
|
2389
|
+
usage: isRecord(message._usage) ? { ...message._usage } : null,
|
|
2390
|
+
// 模型请求区间来自持久化字段;旧会话缺失时为 null,不用落盘时刻冒充(#2581 A1)。
|
|
2391
|
+
duration_ms: numberOrNull(message._duration_ms),
|
|
2392
|
+
first_token_ms: numberOrNull(message._first_token_ms),
|
|
2393
|
+
stream_duration_ms: numberOrNull(message._stream_duration_ms),
|
|
2394
|
+
started_at: typeof message._started_at === "string" ? message._started_at : typeof entry.timestamp === "string" ? entry.timestamp : void 0,
|
|
2395
|
+
has_reported_timing: typeof message._started_at === "string",
|
|
2396
|
+
// fork 子会话的父锚点:历史里落在消息级 _parent_fork_tool_call_id。丢掉它,
|
|
2397
|
+
// 刷新后工作流步骤 fork 就配不回 workflow:run 工具调用(普通 Agent fork 有
|
|
2398
|
+
// root Agent 工具调用兜底配对,工作流 fork 没有,子智能体卡会整体消失)。
|
|
2399
|
+
parent_fork_tool_call_id: stringOrNull(message._parent_fork_tool_call_id)
|
|
1836
2400
|
};
|
|
2401
|
+
const isNativeToolEntry = role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.some(
|
|
2402
|
+
(toolCall) => isRecord(toolCall) && typeof toolCall.provider === "string"
|
|
2403
|
+
);
|
|
2404
|
+
const previousTurn = turns[turns.length - 1];
|
|
2405
|
+
if (role === "assistant" && previousTurn?.role === "assistant" && previousTurn.loop_id === loopId && nativeToolTurns.has(previousTurn)) {
|
|
2406
|
+
nativeToolTurns.delete(previousTurn);
|
|
2407
|
+
previousTurn.blocks.push(...turn.blocks);
|
|
2408
|
+
previousTurn.tool_calls.push(...turn.tool_calls);
|
|
2409
|
+
previousTurn.status = turn.status;
|
|
2410
|
+
previousTurn.model = turn.model ?? previousTurn.model;
|
|
2411
|
+
previousTurn.usage = turn.usage ?? previousTurn.usage;
|
|
2412
|
+
if (turn.duration_ms) previousTurn.duration_ms = turn.duration_ms;
|
|
2413
|
+
previousTurn.first_token_ms = turn.first_token_ms ?? previousTurn.first_token_ms;
|
|
2414
|
+
previousTurn.stream_duration_ms = turn.stream_duration_ms ?? previousTurn.stream_duration_ms;
|
|
2415
|
+
if (turn.has_reported_timing) {
|
|
2416
|
+
previousTurn.started_at = turn.started_at;
|
|
2417
|
+
previousTurn.has_reported_timing = true;
|
|
2418
|
+
} else if (!previousTurn.has_reported_timing) {
|
|
2419
|
+
previousTurn.started_at = previousTurn.started_at ?? turn.started_at;
|
|
2420
|
+
}
|
|
2421
|
+
previousTurn.id = turn.id;
|
|
2422
|
+
previousTurn.turn_id = turn.turn_id;
|
|
2423
|
+
if (isNativeToolEntry) nativeToolTurns.add(previousTurn);
|
|
2424
|
+
turnEntryAliases.set(id, previousTurn);
|
|
2425
|
+
if (loopId === "root") latestRootMessage = previousTurn;
|
|
2426
|
+
latestAssistantByLoop.set(loopId, previousTurn);
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
1837
2429
|
if (loopId === "root" && (role === "user" || role === "assistant")) {
|
|
1838
2430
|
latestRootMessage = turn;
|
|
1839
2431
|
}
|
|
1840
2432
|
turns.push(turn);
|
|
2433
|
+
turnEntryAliases.set(id, turn);
|
|
2434
|
+
if (isNativeToolEntry) nativeToolTurns.add(turn);
|
|
1841
2435
|
if (role === "assistant") latestAssistantByLoop.set(loopId, turn);
|
|
1842
2436
|
if (loopId === "root") {
|
|
1843
2437
|
if (role === "assistant") latestRootAssistant = turn;
|
|
@@ -1846,10 +2440,25 @@ function projectHistory(entries) {
|
|
|
1846
2440
|
for (const toolCall of toolCalls) toolOwners.set(toolCall.id, turn);
|
|
1847
2441
|
continue;
|
|
1848
2442
|
}
|
|
2443
|
+
if (kind === "tool_result") {
|
|
2444
|
+
const data = entry.data ?? {};
|
|
2445
|
+
const toolCallId = String(data.tool_call_id ?? "");
|
|
2446
|
+
const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
|
|
2447
|
+
if (owner && toolCallId) {
|
|
2448
|
+
const rawContent = data.content;
|
|
2449
|
+
const content = typeof rawContent === "string" ? rawContent : Array.isArray(rawContent) ? rawContent : String(rawContent ?? "");
|
|
2450
|
+
const status = data.is_error === true ? "error" : inferToolStatus(typeof content === "string" ? content : JSON.stringify(content));
|
|
2451
|
+
setToolCallStatus(owner, toolCallId, status, content, null);
|
|
2452
|
+
if (!owner.blocks.some((block) => block.type === "tool_result" && block.tool_call_id === toolCallId)) {
|
|
2453
|
+
owner.blocks.push({ type: "tool_result", content, tool_call_id: toolCallId });
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
1849
2458
|
if (kind === "mode_change") {
|
|
1850
2459
|
turns.push(markerTurn(id, ++sequence, loopId, "mode_change", entry.data ?? {}));
|
|
1851
2460
|
} else if (kind === "workspace_change") {
|
|
1852
|
-
const change =
|
|
2461
|
+
const change = isRecord(entry.data) ? entry.data : {};
|
|
1853
2462
|
const source = stringOrNull(change.source) ?? "";
|
|
1854
2463
|
const projectName = stringOrNull(change.project_name) ?? "";
|
|
1855
2464
|
const fromPath = stringOrNull(change.from) ?? "";
|
|
@@ -1878,19 +2487,22 @@ function projectHistory(entries) {
|
|
|
1878
2487
|
} else if (kind === "tool_ui" || kind === "tool_bridge") {
|
|
1879
2488
|
const data = entry.data ?? {};
|
|
1880
2489
|
const toolCallId = String(data.tool_call_id ?? "");
|
|
1881
|
-
const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
|
|
1882
|
-
const
|
|
2490
|
+
const owner = kind === "tool_bridge" ? toolCallId ? toolOwners.get(toolCallId) : void 0 : toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
|
|
2491
|
+
const rawContent = kind === "tool_ui" ? data.ui : data.bridge;
|
|
2492
|
+
const fileReference = isToolUiCard(rawContent) && resolveToolUiCardContent(rawContent)?.type === "resource-file";
|
|
2493
|
+
const content = kind === "tool_ui" && isRecord(rawContent) ? { ...rawContent, ...!fileReference ? { archived: true } : {} } : rawContent;
|
|
1883
2494
|
if (owner && content !== void 0) {
|
|
1884
2495
|
owner.blocks.push({
|
|
1885
2496
|
type: kind,
|
|
1886
2497
|
content,
|
|
2498
|
+
...kind === "tool_bridge" ? { delivery_id: id } : {},
|
|
1887
2499
|
tool_call_id: toolCallId || null
|
|
1888
2500
|
});
|
|
1889
2501
|
}
|
|
1890
2502
|
} else if (kind === "compaction" || kind === "tool_result_archive") {
|
|
1891
2503
|
const data = entry.data ?? {};
|
|
1892
2504
|
const compactionId = String(data.compaction_id ?? entry.id ?? `history-${sequence + 1}`);
|
|
1893
|
-
let content =
|
|
2505
|
+
let content = isRecord(entry.message) ? entry.message.content : void 0;
|
|
1894
2506
|
if (kind === "tool_result_archive") {
|
|
1895
2507
|
const archivedCount = Array.isArray(data.archived_files) ? data.archived_files.length : 0;
|
|
1896
2508
|
content = `<compaction-summary>
|
|
@@ -1898,18 +2510,24 @@ function projectHistory(entries) {
|
|
|
1898
2510
|
</compaction-summary>`;
|
|
1899
2511
|
}
|
|
1900
2512
|
turns.push(
|
|
1901
|
-
buildCompactionTurn(
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2513
|
+
buildCompactionTurn(
|
|
2514
|
+
++sequence,
|
|
2515
|
+
compactionId,
|
|
2516
|
+
loopId,
|
|
2517
|
+
data.status === "failed" ? "failed" : "completed",
|
|
2518
|
+
{
|
|
2519
|
+
...data,
|
|
2520
|
+
content
|
|
2521
|
+
}
|
|
2522
|
+
)
|
|
1905
2523
|
);
|
|
1906
2524
|
} else if (kind === "child_pause") {
|
|
1907
2525
|
const data = entry.data ?? {};
|
|
1908
|
-
const sourceLoop =
|
|
2526
|
+
const sourceLoop = isRecord(data.source_loop) ? data.source_loop : {};
|
|
1909
2527
|
const childLoopId = String(data.child_loop_name ?? sourceLoop.name ?? "");
|
|
1910
2528
|
const childToolCallId = String(data.child_pause_tool_call_id ?? "");
|
|
1911
2529
|
const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
|
|
1912
|
-
const pauseToolData =
|
|
2530
|
+
const pauseToolData = isRecord(data.pause_tool_data) ? data.pause_tool_data : {};
|
|
1913
2531
|
const description = String(sourceLoop.description ?? "");
|
|
1914
2532
|
applyAskUserPauseToTurn(
|
|
1915
2533
|
latestAssistantByLoop.get(childLoopId),
|
|
@@ -1997,15 +2615,15 @@ function parseToolArguments(value) {
|
|
|
1997
2615
|
if (!value) return {};
|
|
1998
2616
|
try {
|
|
1999
2617
|
const parsed = JSON.parse(value);
|
|
2000
|
-
return
|
|
2618
|
+
return isRecord(parsed) ? parsed : {};
|
|
2001
2619
|
} catch {
|
|
2002
2620
|
return {};
|
|
2003
2621
|
}
|
|
2004
2622
|
}
|
|
2005
2623
|
function buildToolCalls(value) {
|
|
2006
2624
|
if (!Array.isArray(value)) return [];
|
|
2007
|
-
return value.filter(
|
|
2008
|
-
const fn =
|
|
2625
|
+
return value.filter(isRecord).map((raw) => {
|
|
2626
|
+
const fn = isRecord(raw.function) ? raw.function : {};
|
|
2009
2627
|
const name = String(fn.name ?? "");
|
|
2010
2628
|
return {
|
|
2011
2629
|
id: String(raw.id ?? ""),
|
|
@@ -2021,23 +2639,40 @@ function buildBlocks(message, toolCalls) {
|
|
|
2021
2639
|
const displayContent = message.role === "user" ? message._display_content : void 0;
|
|
2022
2640
|
const stored = message._blocks;
|
|
2023
2641
|
const blocks = [];
|
|
2642
|
+
const storedToolCallIds = /* @__PURE__ */ new Set();
|
|
2024
2643
|
if (displayContent !== void 0) {
|
|
2025
2644
|
blocks.push({ type: "text", content: displayContent });
|
|
2026
2645
|
} else if (Array.isArray(stored)) {
|
|
2027
2646
|
for (const raw of stored) {
|
|
2028
|
-
if (!
|
|
2647
|
+
if (!isRecord(raw)) continue;
|
|
2029
2648
|
const type3 = String(raw.type ?? "");
|
|
2030
2649
|
if (type3 === "thinking") blocks.push({ type: type3, content: raw.thinking ?? raw.content ?? "" });
|
|
2031
2650
|
if (type3 === "text") blocks.push({ type: type3, content: raw.text ?? raw.content ?? "" });
|
|
2651
|
+
if (type3 === "tool_call") {
|
|
2652
|
+
const toolCallId = String(raw.id ?? "");
|
|
2653
|
+
const toolCall = toolCalls.find((candidate) => candidate.id === toolCallId);
|
|
2654
|
+
if (!toolCall) continue;
|
|
2655
|
+
storedToolCallIds.add(toolCallId);
|
|
2656
|
+
blocks.push({
|
|
2657
|
+
type: "tool_use",
|
|
2658
|
+
content: toolCall.arguments,
|
|
2659
|
+
tool_call_id: toolCall.id,
|
|
2660
|
+
tool_name: toolCall.tool_name,
|
|
2661
|
+
display_name: toolCall.display_name
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2032
2664
|
}
|
|
2033
2665
|
} else if (message.content !== void 0 && message.content !== "") {
|
|
2034
2666
|
blocks.push({ type: "text", content: message.content });
|
|
2035
2667
|
}
|
|
2036
2668
|
for (const toolCall of toolCalls) {
|
|
2669
|
+
if (storedToolCallIds.has(toolCall.id)) continue;
|
|
2037
2670
|
blocks.push({
|
|
2038
2671
|
type: "tool_use",
|
|
2039
|
-
content:
|
|
2040
|
-
tool_call_id: toolCall.id
|
|
2672
|
+
content: toolCall.arguments,
|
|
2673
|
+
tool_call_id: toolCall.id,
|
|
2674
|
+
tool_name: toolCall.tool_name,
|
|
2675
|
+
display_name: toolCall.display_name
|
|
2041
2676
|
});
|
|
2042
2677
|
}
|
|
2043
2678
|
return blocks;
|
|
@@ -2058,14 +2693,11 @@ function markerTurn(id, sequence, loopId, type3, content, toolCallId = null) {
|
|
|
2058
2693
|
duration_ms: 0
|
|
2059
2694
|
};
|
|
2060
2695
|
}
|
|
2061
|
-
function isRecord3(value) {
|
|
2062
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2063
|
-
}
|
|
2064
2696
|
function stringOrNull(value) {
|
|
2065
2697
|
return typeof value === "string" ? value : null;
|
|
2066
2698
|
}
|
|
2067
|
-
function
|
|
2068
|
-
return typeof value === "number" ? value :
|
|
2699
|
+
function numberOrNull(value) {
|
|
2700
|
+
return typeof value === "number" && value >= 0 ? value : null;
|
|
2069
2701
|
}
|
|
2070
2702
|
function findLatestAssistant(turns, loopId) {
|
|
2071
2703
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
@@ -2076,6 +2708,10 @@ function findLatestAssistant(turns, loopId) {
|
|
|
2076
2708
|
}
|
|
2077
2709
|
|
|
2078
2710
|
// src/resources/sessions.ts
|
|
2711
|
+
function isEntryGoneError(error) {
|
|
2712
|
+
const status = error?.status;
|
|
2713
|
+
return status === 404 || status === 409;
|
|
2714
|
+
}
|
|
2079
2715
|
function toCreateSessionPayload(request) {
|
|
2080
2716
|
const solutionRef = request.solution_ref ?? externalSolutionRef(request.solution_id);
|
|
2081
2717
|
return {
|
|
@@ -2094,13 +2730,14 @@ function toCreateSessionPayload(request) {
|
|
|
2094
2730
|
solution_snapshot_session_id: request.solution_snapshot_session_id ?? null,
|
|
2095
2731
|
compaction_ratio: request.compaction_ratio ?? null,
|
|
2096
2732
|
...request.runtime_type ? { runtime_type: request.runtime_type } : {},
|
|
2097
|
-
...request.daemon_id ? { daemon_id: request.daemon_id } : {}
|
|
2733
|
+
...request.daemon_id ? { daemon_id: request.daemon_id } : {},
|
|
2734
|
+
...request.agent_runtime_id ? { agent_runtime_id: request.agent_runtime_id } : {},
|
|
2735
|
+
...request.chat_group_id ? { chat_group_id: request.chat_group_id } : {}
|
|
2098
2736
|
};
|
|
2099
2737
|
}
|
|
2100
2738
|
var builtinSolutionIds = /* @__PURE__ */ new Set([
|
|
2101
2739
|
"app-dev",
|
|
2102
2740
|
"general_chat",
|
|
2103
|
-
"multica",
|
|
2104
2741
|
"night_build",
|
|
2105
2742
|
"skill_editor",
|
|
2106
2743
|
"smart_bid",
|
|
@@ -2123,6 +2760,54 @@ var SessionsResource = class {
|
|
|
2123
2760
|
this.client = client;
|
|
2124
2761
|
}
|
|
2125
2762
|
client;
|
|
2763
|
+
pendingImages = /* @__PURE__ */ new Map();
|
|
2764
|
+
/** Fetch an immutable message image using this client's authenticated identity. */
|
|
2765
|
+
fetchImage(sessionId, assetId, preview = true) {
|
|
2766
|
+
return this.fetchImagePath(`/api/sessions/${encodeURIComponent(sessionId)}/media/${encodeURIComponent(assetId)}`, preview);
|
|
2767
|
+
}
|
|
2768
|
+
/** Fetch a public-share image; the server validates the share token on every request. */
|
|
2769
|
+
fetchSharedImage(token, assetId, preview = true) {
|
|
2770
|
+
return this.fetchImagePath(`/api/share/${encodeURIComponent(token)}/media/${encodeURIComponent(assetId)}`, preview);
|
|
2771
|
+
}
|
|
2772
|
+
fetchImagePath(resource, preview) {
|
|
2773
|
+
const path = `${resource}${preview ? "?variant=preview" : ""}`;
|
|
2774
|
+
const identity = this.client.buildAuthedUrl(path);
|
|
2775
|
+
const existing = this.pendingImages.get(identity);
|
|
2776
|
+
if (existing) return existing;
|
|
2777
|
+
const pending = this.client.blob("GET", path).finally(() => this.pendingImages.delete(identity));
|
|
2778
|
+
this.pendingImages.set(identity, pending);
|
|
2779
|
+
return pending;
|
|
2780
|
+
}
|
|
2781
|
+
getMcpApp(sessionId, sourceId, init) {
|
|
2782
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}`, init);
|
|
2783
|
+
}
|
|
2784
|
+
onMcpAppChanged(sessionId, sourceId, listener) {
|
|
2785
|
+
let socket = this.client.socket();
|
|
2786
|
+
const changed = (payload) => {
|
|
2787
|
+
if (payload.session_id === sessionId && payload.source_id === sourceId) listener();
|
|
2788
|
+
};
|
|
2789
|
+
socket.on("mcp-app:changed", changed);
|
|
2790
|
+
socket.on("connect", listener);
|
|
2791
|
+
const off = this.client.onSocketReplaced(() => {
|
|
2792
|
+
socket.off("mcp-app:changed", changed);
|
|
2793
|
+
socket.off("connect", listener);
|
|
2794
|
+
socket = this.client.socket();
|
|
2795
|
+
socket.on("mcp-app:changed", changed);
|
|
2796
|
+
socket.on("connect", listener);
|
|
2797
|
+
});
|
|
2798
|
+
return () => {
|
|
2799
|
+
off();
|
|
2800
|
+
socket.off("mcp-app:changed", changed);
|
|
2801
|
+
socket.off("connect", listener);
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
callMcpApp(sessionId, sourceId, request, init) {
|
|
2805
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/calls`, {
|
|
2806
|
+
...init,
|
|
2807
|
+
method: "POST",
|
|
2808
|
+
body: JSON.stringify(request)
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2126
2811
|
/**
|
|
2127
2812
|
* 连接一个已存在的会话,返回实时会话对象 AgentSession
|
|
2128
2813
|
* (自动完成历史加载、Socket.IO 订阅、断线重连补数)。
|
|
@@ -2130,6 +2815,56 @@ var SessionsResource = class {
|
|
|
2130
2815
|
connect(sessionId) {
|
|
2131
2816
|
return this.client.hub.connect(sessionId);
|
|
2132
2817
|
}
|
|
2818
|
+
listSessionPlugins(sessionId, init) {
|
|
2819
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins`, init);
|
|
2820
|
+
}
|
|
2821
|
+
setSessionPluginActivation(sessionId, name, active, init) {
|
|
2822
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}:activation`, { method: "POST", body: JSON.stringify({ active }), ...init });
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Fetch a plugin's package-local icon as bytes.
|
|
2826
|
+
*
|
|
2827
|
+
* The request carries the client's own bearer credentials, so callers turn
|
|
2828
|
+
* the blob into an object URL instead of pointing `<img>` at an API path and
|
|
2829
|
+
* hoping the request is authenticated.
|
|
2830
|
+
*/
|
|
2831
|
+
fetchSessionPluginIcon(sessionId, name) {
|
|
2832
|
+
return this.client.blob("GET", `/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/icon`);
|
|
2833
|
+
}
|
|
2834
|
+
/** Authenticated URL for the icon; useful where a blob URL cannot be used. */
|
|
2835
|
+
sessionPluginIconUrl(sessionId, name) {
|
|
2836
|
+
return this.client.buildAuthedUrl(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/icon`);
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* 准备一个插件的配置:把包准备好并读出用户的配置契约,**不启用**它。
|
|
2840
|
+
*
|
|
2841
|
+
* 开启插件前必须先走这一步:缺配置时用户只会看到配置表单,服务端不会先把
|
|
2842
|
+
* active 持久化成 true 再靠补偿关掉。它不修改会话的插件选择、不连接 MCP。
|
|
2843
|
+
*/
|
|
2844
|
+
prepareSessionPluginConfig(sessionId, name, init) {
|
|
2845
|
+
return this.client.jsonFromInit(
|
|
2846
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}:prepare-config`,
|
|
2847
|
+
{ method: "POST", ...init }
|
|
2848
|
+
);
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Read a plugin's configuration contract and the user's stored values.
|
|
2852
|
+
*
|
|
2853
|
+
* Secrets are never returned: `write_only` lists the pointers that hold one.
|
|
2854
|
+
*/
|
|
2855
|
+
getSessionPluginConfig(sessionId, name, init) {
|
|
2856
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/config`, init);
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Publish explicit set/remove operations to the user's keychain file.
|
|
2860
|
+
*
|
|
2861
|
+
* Fields absent from both lists keep their stored value, so a form never has
|
|
2862
|
+
* to round-trip a secret. Rejections leave the previous file untouched; the
|
|
2863
|
+
* call never restarts MCP servers.
|
|
2864
|
+
*/
|
|
2865
|
+
updateSessionPluginConfig(sessionId, name, update, init) {
|
|
2866
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/config`, { method: "PUT", body: JSON.stringify(update), ...init });
|
|
2867
|
+
}
|
|
2133
2868
|
/** 创建新会话并直接连接,返回可收发消息的 AgentSession。 */
|
|
2134
2869
|
async create(request = {}) {
|
|
2135
2870
|
if (isSessionDefinition(request)) {
|
|
@@ -2226,7 +2961,8 @@ var SessionsResource = class {
|
|
|
2226
2961
|
template_id_prefix,
|
|
2227
2962
|
solution_id,
|
|
2228
2963
|
solution_registry_id,
|
|
2229
|
-
q
|
|
2964
|
+
q,
|
|
2965
|
+
chat_group_id
|
|
2230
2966
|
}) {
|
|
2231
2967
|
const searchParams = new URLSearchParams();
|
|
2232
2968
|
searchParams.set("limit", String(limit));
|
|
@@ -2239,6 +2975,7 @@ var SessionsResource = class {
|
|
|
2239
2975
|
}
|
|
2240
2976
|
const trimmedQuery = q?.trim();
|
|
2241
2977
|
if (trimmedQuery) searchParams.set("q", trimmedQuery);
|
|
2978
|
+
if (chat_group_id) searchParams.set("chat_group_id", chat_group_id);
|
|
2242
2979
|
return this.client.json("GET", `/api/sessions?${searchParams.toString()}`);
|
|
2243
2980
|
}
|
|
2244
2981
|
async listSessions(template_id_prefix, solution_id) {
|
|
@@ -2329,6 +3066,55 @@ var SessionsResource = class {
|
|
|
2329
3066
|
);
|
|
2330
3067
|
return projectHistory(entries);
|
|
2331
3068
|
}
|
|
3069
|
+
async getSessionTurnsPage(sessionId, options = {}, init = {}) {
|
|
3070
|
+
const query = new URLSearchParams();
|
|
3071
|
+
if (options.limit !== void 0) query.set("limit", String(options.limit));
|
|
3072
|
+
if (options.before !== void 0) query.set("before", options.before);
|
|
3073
|
+
if (options.fromEntryId !== void 0) query.set("from_entry_id", options.fromEntryId);
|
|
3074
|
+
const suffix = query.size ? `?${query}` : "";
|
|
3075
|
+
const page = await this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/turns${suffix}`, init);
|
|
3076
|
+
return {
|
|
3077
|
+
turns: page.turns,
|
|
3078
|
+
nextBefore: page.next_before,
|
|
3079
|
+
currentMode: page.current_mode
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* 当前分支上可导航的用户发言目录(消息导航侧栏)。
|
|
3084
|
+
*
|
|
3085
|
+
* 只回 id、标题和时间:侧栏要列的是**整条会话**的发言,而窗口外的发言 id 只能
|
|
3086
|
+
* 从服务端拿;消息体仍然走 `/turns`。
|
|
3087
|
+
*/
|
|
3088
|
+
async getSessionTurnIndex(sessionId) {
|
|
3089
|
+
const index = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turn-index`);
|
|
3090
|
+
return {
|
|
3091
|
+
sessionId: index.session_id,
|
|
3092
|
+
items: index.items.map((item) => ({
|
|
3093
|
+
entryId: item.entry_id,
|
|
3094
|
+
preview: item.preview,
|
|
3095
|
+
createdAt: item.created_at
|
|
3096
|
+
}))
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
getMcpAppContext(sessionId, sourceId, init) {
|
|
3100
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/context`, init);
|
|
3101
|
+
}
|
|
3102
|
+
updateMcpAppContext(sessionId, sourceId, body, init) {
|
|
3103
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/context`, {
|
|
3104
|
+
...init,
|
|
3105
|
+
method: "POST",
|
|
3106
|
+
body: JSON.stringify(body)
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
async getSessionTurnCommands(sessionId) {
|
|
3110
|
+
const commands = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turn-commands`);
|
|
3111
|
+
return commands.map((command) => ({
|
|
3112
|
+
deliveryId: command.delivery_id,
|
|
3113
|
+
toolCallId: command.tool_call_id ?? void 0,
|
|
3114
|
+
action: command.action,
|
|
3115
|
+
payload: command.payload
|
|
3116
|
+
}));
|
|
3117
|
+
}
|
|
2332
3118
|
listResultFeedback(sessionId) {
|
|
2333
3119
|
return this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/feedback`);
|
|
2334
3120
|
}
|
|
@@ -2396,8 +3182,9 @@ var SessionsResource = class {
|
|
|
2396
3182
|
`/api/sessions/${sessionId}/probe-url?url=${encodeURIComponent(url)}`
|
|
2397
3183
|
);
|
|
2398
3184
|
}
|
|
2399
|
-
listDir(sessionId, dirPath) {
|
|
2400
|
-
|
|
3185
|
+
listDir(sessionId, dirPath, showHidden = false) {
|
|
3186
|
+
const query = showHidden ? "?show_hidden=true" : "";
|
|
3187
|
+
return this.client.json("GET", `/api/sessions/${sessionId}/ls/${encodeURIComponent(dirPath)}${query}`);
|
|
2401
3188
|
}
|
|
2402
3189
|
async countFiles(sessionId, dirPath = "") {
|
|
2403
3190
|
const entries = await this.listDir(sessionId, dirPath);
|
|
@@ -2592,16 +3379,140 @@ function profileToCreateSessionRequest(profile) {
|
|
|
2592
3379
|
};
|
|
2593
3380
|
}
|
|
2594
3381
|
|
|
2595
|
-
// src/
|
|
2596
|
-
var
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
3382
|
+
// src/resources/plugins.ts
|
|
3383
|
+
var PluginsResource = class {
|
|
3384
|
+
constructor(client) {
|
|
3385
|
+
this.client = client;
|
|
3386
|
+
}
|
|
3387
|
+
client;
|
|
3388
|
+
/**
|
|
3389
|
+
* 列出当前用户安装的插件及其展示信息。
|
|
3390
|
+
*
|
|
3391
|
+
* 服务端可能为了补齐名称/图标惰性获取包内容,但绝不激活插件、不连 MCP;
|
|
3392
|
+
* 单个插件的展示信息读不出来只会降级成占位,不会让整份目录失败。
|
|
3393
|
+
*/
|
|
3394
|
+
catalog(init) {
|
|
3395
|
+
return this.client.jsonFromInit("/api/plugins/catalog", init);
|
|
3396
|
+
}
|
|
3397
|
+
/** 取一个插件的包内图标字节(用户级,不需要会话)。 */
|
|
3398
|
+
fetchIcon(name) {
|
|
3399
|
+
return this.client.blob("GET", `/api/plugins/${encodeURIComponent(name)}/icon`);
|
|
3400
|
+
}
|
|
3401
|
+
/** 带凭据的图标地址;能直接用 blob URL 的地方优先用 `fetchIcon`。 */
|
|
3402
|
+
iconUrl(name) {
|
|
3403
|
+
return this.client.buildAuthedUrl(`/api/plugins/${encodeURIComponent(name)}/icon`);
|
|
3404
|
+
}
|
|
3405
|
+
};
|
|
3406
|
+
|
|
3407
|
+
// src/resources/chat-groups.ts
|
|
3408
|
+
var ChatGroupsResource = class {
|
|
3409
|
+
constructor(client) {
|
|
3410
|
+
this.client = client;
|
|
3411
|
+
}
|
|
3412
|
+
client;
|
|
3413
|
+
list() {
|
|
3414
|
+
return this.client.json("GET", "/api/chat-groups");
|
|
3415
|
+
}
|
|
3416
|
+
create(payload) {
|
|
3417
|
+
return this.client.json("POST", "/api/chat-groups", payload);
|
|
3418
|
+
}
|
|
3419
|
+
update(groupId, payload) {
|
|
3420
|
+
return this.client.json("PATCH", `/api/chat-groups/${encodeURIComponent(groupId)}`, payload);
|
|
3421
|
+
}
|
|
3422
|
+
delete(groupId, options) {
|
|
3423
|
+
const query = options?.deleteWorkspace ? "?delete_workspace=true" : "";
|
|
3424
|
+
return this.client.json("DELETE", `/api/chat-groups/${encodeURIComponent(groupId)}${query}`);
|
|
3425
|
+
}
|
|
3426
|
+
};
|
|
3427
|
+
|
|
3428
|
+
// src/resources/site-visits.ts
|
|
3429
|
+
var SITE_VISITS_PATH = "/api/site-visits";
|
|
3430
|
+
var REQUEST_TIMEOUT_MS = 5e3;
|
|
3431
|
+
var SiteVisitsResource = class {
|
|
3432
|
+
constructor(client) {
|
|
3433
|
+
this.client = client;
|
|
3434
|
+
}
|
|
3435
|
+
client;
|
|
3436
|
+
/** 上报一次页面访问,返回当前快照;不承诺本次已计入或已落盘。 */
|
|
3437
|
+
record() {
|
|
3438
|
+
return this.request("POST");
|
|
3439
|
+
}
|
|
3440
|
+
/** 只读快照:不设置访客 Cookie,也不增加任何计数。 */
|
|
3441
|
+
summary() {
|
|
3442
|
+
return this.request("GET");
|
|
3443
|
+
}
|
|
3444
|
+
async request(method) {
|
|
3445
|
+
const url = buildBackendUrl(this.client.options.baseUrl, SITE_VISITS_PATH);
|
|
3446
|
+
const controller = new AbortController();
|
|
3447
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
3448
|
+
try {
|
|
3449
|
+
const response = await (this.client.options.fetchImpl ?? fetch)(url.toString(), {
|
|
3450
|
+
method,
|
|
3451
|
+
// 访客 Cookie 是唯一的去重依据,必须随请求往返。
|
|
3452
|
+
credentials: "include",
|
|
3453
|
+
signal: controller.signal
|
|
3454
|
+
});
|
|
3455
|
+
if (!response.ok) {
|
|
3456
|
+
throw new BladeApiError(response, await extractErrorDetail(response));
|
|
3457
|
+
}
|
|
3458
|
+
return await response.json();
|
|
3459
|
+
} finally {
|
|
3460
|
+
clearTimeout(timeout);
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
};
|
|
3464
|
+
|
|
3465
|
+
// src/shared/auth-busy.ts
|
|
3466
|
+
var DEFAULT_RETRY_AFTER_SECONDS = 1;
|
|
3467
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
3468
|
+
function errorData(error) {
|
|
3469
|
+
if (typeof error !== "object" || error === null || !("data" in error)) return null;
|
|
3470
|
+
const data = error.data;
|
|
3471
|
+
return typeof data === "object" && data !== null ? data : null;
|
|
3472
|
+
}
|
|
3473
|
+
function authBusyRetryDelayMs(error) {
|
|
3474
|
+
const data = errorData(error);
|
|
3475
|
+
if (data?.code !== "auth_busy" || Number(data.status) !== 429) return null;
|
|
3476
|
+
const rawRetryAfter = data.retry_after;
|
|
3477
|
+
const seconds = rawRetryAfter == null || String(rawRetryAfter).trim() === "" ? Number.NaN : Number(rawRetryAfter);
|
|
3478
|
+
const delaySeconds = Number.isFinite(seconds) && seconds >= 0 ? seconds : DEFAULT_RETRY_AFTER_SECONDS;
|
|
3479
|
+
return Math.min(delaySeconds * 1e3, MAX_TIMER_DELAY_MS);
|
|
3480
|
+
}
|
|
3481
|
+
var AuthBusyReconnect = class {
|
|
3482
|
+
timer = null;
|
|
3483
|
+
get pending() {
|
|
3484
|
+
return this.timer !== null;
|
|
3485
|
+
}
|
|
3486
|
+
schedule(error, reconnect) {
|
|
3487
|
+
const delay = authBusyRetryDelayMs(error);
|
|
3488
|
+
if (delay === null) {
|
|
3489
|
+
this.cancel();
|
|
3490
|
+
return false;
|
|
3491
|
+
}
|
|
3492
|
+
this.cancel();
|
|
3493
|
+
this.timer = globalThis.setTimeout(() => {
|
|
3494
|
+
this.timer = null;
|
|
3495
|
+
reconnect();
|
|
3496
|
+
}, delay);
|
|
3497
|
+
return true;
|
|
3498
|
+
}
|
|
3499
|
+
cancel() {
|
|
3500
|
+
if (this.timer === null) return;
|
|
3501
|
+
globalThis.clearTimeout(this.timer);
|
|
3502
|
+
this.timer = null;
|
|
3503
|
+
}
|
|
3504
|
+
};
|
|
3505
|
+
|
|
3506
|
+
// src/commands/registry.ts
|
|
3507
|
+
var BUFFER_LIMIT_PER_ACTION = 20;
|
|
3508
|
+
var CommandRegistry = class {
|
|
3509
|
+
handlers = /* @__PURE__ */ new Map();
|
|
3510
|
+
buffered = /* @__PURE__ */ new Map();
|
|
3511
|
+
onCommand(action, handler) {
|
|
3512
|
+
let set = this.handlers.get(action);
|
|
3513
|
+
if (!set) {
|
|
3514
|
+
set = /* @__PURE__ */ new Set();
|
|
3515
|
+
this.handlers.set(action, set);
|
|
2605
3516
|
}
|
|
2606
3517
|
set.add(handler);
|
|
2607
3518
|
const pending = this.buffered.get(action);
|
|
@@ -2700,6 +3611,172 @@ var Emitter = class {
|
|
|
2700
3611
|
}
|
|
2701
3612
|
};
|
|
2702
3613
|
|
|
3614
|
+
// src/session/history-page.ts
|
|
3615
|
+
function sortedUnique(turns) {
|
|
3616
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3617
|
+
for (const turn of turns) byId.set(turn.turn_id, turn);
|
|
3618
|
+
return [...byId.values()].sort((left, right) => left.sequence - right.sequence);
|
|
3619
|
+
}
|
|
3620
|
+
function prependOlder(current, page) {
|
|
3621
|
+
const currentById = new Map(current.map((turn) => [turn.turn_id, turn]));
|
|
3622
|
+
return sortedUnique([
|
|
3623
|
+
...current,
|
|
3624
|
+
...page.map((turn) => {
|
|
3625
|
+
const live = currentById.get(turn.turn_id);
|
|
3626
|
+
return live ? { ...live, sequence: turn.sequence } : turn;
|
|
3627
|
+
})
|
|
3628
|
+
]);
|
|
3629
|
+
}
|
|
3630
|
+
function replaceWindow(current, page, liveRevisionAtStart, liveRevisions) {
|
|
3631
|
+
const pageById = new Map(page.map((turn) => [turn.turn_id, turn]));
|
|
3632
|
+
const newerLive = current.filter(
|
|
3633
|
+
(turn) => turn.turn_id.startsWith("local-user-") || (liveRevisions.byTurnId.get(turn.turn_id) ?? 0) > liveRevisionAtStart
|
|
3634
|
+
);
|
|
3635
|
+
return sortedUnique([
|
|
3636
|
+
...page,
|
|
3637
|
+
...newerLive.map((turn) => {
|
|
3638
|
+
const snapshot2 = pageById.get(turn.turn_id);
|
|
3639
|
+
return snapshot2 ? { ...turn, sequence: snapshot2.sequence } : turn;
|
|
3640
|
+
})
|
|
3641
|
+
]);
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
// src/session/queue.ts
|
|
3645
|
+
var EMPTY_SESSION_QUEUE = {
|
|
3646
|
+
session_id: "",
|
|
3647
|
+
revision: 0,
|
|
3648
|
+
paused: false,
|
|
3649
|
+
pause_reason: null,
|
|
3650
|
+
items: []
|
|
3651
|
+
};
|
|
3652
|
+
var QUEUE_FAILURE_MESSAGE = "\u961F\u5217\u64CD\u4F5C\u5931\u8D25";
|
|
3653
|
+
var QUEUED_MESSAGE_STATUSES = [
|
|
3654
|
+
"pending",
|
|
3655
|
+
"delivering",
|
|
3656
|
+
"claimed",
|
|
3657
|
+
"consumed",
|
|
3658
|
+
"cancelled"
|
|
3659
|
+
];
|
|
3660
|
+
function toQueuedMessage(value) {
|
|
3661
|
+
if (!isRecord(value)) return null;
|
|
3662
|
+
const status = typeof value.status === "string" && QUEUED_MESSAGE_STATUSES.includes(value.status) ? value.status : null;
|
|
3663
|
+
if (typeof value.id !== "string" || typeof value.content !== "string" || typeof value.position !== "number" || typeof value.version !== "number" || typeof value.created_at !== "string" || typeof value.updated_at !== "string" || status === null) {
|
|
3664
|
+
return null;
|
|
3665
|
+
}
|
|
3666
|
+
const item = {
|
|
3667
|
+
id: value.id,
|
|
3668
|
+
content: value.content,
|
|
3669
|
+
position: value.position,
|
|
3670
|
+
version: value.version,
|
|
3671
|
+
status,
|
|
3672
|
+
created_at: value.created_at,
|
|
3673
|
+
updated_at: value.updated_at
|
|
3674
|
+
};
|
|
3675
|
+
if (typeof value.target_run_id === "string") item.target_run_id = value.target_run_id;
|
|
3676
|
+
if (typeof value.error === "string") item.error = value.error;
|
|
3677
|
+
return item;
|
|
3678
|
+
}
|
|
3679
|
+
function toQueueSnapshot(value, fallbackSessionId) {
|
|
3680
|
+
if (!isRecord(value)) return null;
|
|
3681
|
+
if (typeof value.revision !== "number" || !Number.isInteger(value.revision) || value.revision < 0) {
|
|
3682
|
+
return null;
|
|
3683
|
+
}
|
|
3684
|
+
if (typeof value.paused !== "boolean") return null;
|
|
3685
|
+
if (!Array.isArray(value.items)) return null;
|
|
3686
|
+
const pauseReason = value.pause_reason;
|
|
3687
|
+
if (pauseReason !== null && pauseReason !== void 0 && typeof pauseReason !== "string") {
|
|
3688
|
+
return null;
|
|
3689
|
+
}
|
|
3690
|
+
const items = [];
|
|
3691
|
+
for (const raw of value.items) {
|
|
3692
|
+
const item = toQueuedMessage(raw);
|
|
3693
|
+
if (!item) return null;
|
|
3694
|
+
items.push(item);
|
|
3695
|
+
}
|
|
3696
|
+
return {
|
|
3697
|
+
session_id: typeof value.session_id === "string" && value.session_id ? value.session_id : fallbackSessionId,
|
|
3698
|
+
revision: value.revision,
|
|
3699
|
+
paused: value.paused,
|
|
3700
|
+
pause_reason: typeof pauseReason === "string" ? pauseReason : null,
|
|
3701
|
+
items
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
function isSessionQueueSnapshot(value) {
|
|
3705
|
+
if (!isRecord(value) || typeof value.session_id !== "string") return false;
|
|
3706
|
+
return toQueueSnapshot(value, value.session_id) !== null;
|
|
3707
|
+
}
|
|
3708
|
+
function shouldApplySnapshot(current, next) {
|
|
3709
|
+
if (!isSessionQueueSnapshot(next)) return false;
|
|
3710
|
+
if (current?.session_id && next.session_id !== current.session_id) return false;
|
|
3711
|
+
return next.revision > (current?.revision ?? -1);
|
|
3712
|
+
}
|
|
3713
|
+
function parseQueueAck(response, fallbackSessionId) {
|
|
3714
|
+
if (!isRecord(response)) {
|
|
3715
|
+
return { ok: false, conflict: false, snapshot: null, message: QUEUE_FAILURE_MESSAGE };
|
|
3716
|
+
}
|
|
3717
|
+
const message = typeof response.message === "string" && response.message ? response.message : null;
|
|
3718
|
+
const code = typeof response.code === "string" && response.code ? response.code : void 0;
|
|
3719
|
+
const status = response.status;
|
|
3720
|
+
if (status !== "ok" && status !== "conflict" && status !== "queued") {
|
|
3721
|
+
return {
|
|
3722
|
+
ok: false,
|
|
3723
|
+
conflict: false,
|
|
3724
|
+
snapshot: null,
|
|
3725
|
+
message: message ?? QUEUE_FAILURE_MESSAGE,
|
|
3726
|
+
...code ? { code } : {}
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
const snapshot2 = toQueueSnapshot(response, fallbackSessionId);
|
|
3730
|
+
if (status === "conflict") {
|
|
3731
|
+
return {
|
|
3732
|
+
ok: false,
|
|
3733
|
+
conflict: true,
|
|
3734
|
+
snapshot: snapshot2,
|
|
3735
|
+
message: message ?? QUEUE_FAILURE_MESSAGE,
|
|
3736
|
+
...code ? { code } : {}
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
if (!snapshot2) {
|
|
3740
|
+
return {
|
|
3741
|
+
ok: false,
|
|
3742
|
+
conflict: false,
|
|
3743
|
+
snapshot: null,
|
|
3744
|
+
message: message ?? QUEUE_FAILURE_MESSAGE,
|
|
3745
|
+
...code ? { code } : {}
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
return { ok: true, conflict: false, snapshot: snapshot2, message, ...code ? { code } : {} };
|
|
3749
|
+
}
|
|
3750
|
+
function canEditQueuedMessage(item) {
|
|
3751
|
+
return item.status === "pending";
|
|
3752
|
+
}
|
|
3753
|
+
function canCancelQueuedMessage(item) {
|
|
3754
|
+
return item.status === "pending";
|
|
3755
|
+
}
|
|
3756
|
+
function canDeliverQueuedMessage(item) {
|
|
3757
|
+
return item.status === "pending";
|
|
3758
|
+
}
|
|
3759
|
+
var QUEUED_MESSAGE_STATUS_LABELS = {
|
|
3760
|
+
pending: "\u5F85\u6267\u884C",
|
|
3761
|
+
delivering: "\u7B49\u5F85\u63A5\u6536",
|
|
3762
|
+
claimed: "",
|
|
3763
|
+
consumed: "",
|
|
3764
|
+
cancelled: ""
|
|
3765
|
+
};
|
|
3766
|
+
function queuedMessageStatusLabel(status) {
|
|
3767
|
+
return QUEUED_MESSAGE_STATUS_LABELS[status] ?? "";
|
|
3768
|
+
}
|
|
3769
|
+
var QUEUE_PAUSE_REASON_LABELS = {
|
|
3770
|
+
user_stop: "\u5DF2\u6682\u505C",
|
|
3771
|
+
run_failed: "\u4EFB\u52A1\u5F02\u5E38\u5DF2\u6682\u505C",
|
|
3772
|
+
run_unknown: "\u4EFB\u52A1\u7ED3\u675F\u72B6\u6001\u672A\u77E5\uFF0C\u5DF2\u6682\u505C",
|
|
3773
|
+
server_restart: "\u670D\u52A1\u91CD\u542F\u5DF2\u6682\u505C"
|
|
3774
|
+
};
|
|
3775
|
+
function queuePauseReasonLabel(reason) {
|
|
3776
|
+
if (!reason) return null;
|
|
3777
|
+
return QUEUE_PAUSE_REASON_LABELS[reason] ?? reason;
|
|
3778
|
+
}
|
|
3779
|
+
|
|
2703
3780
|
// src/schemas/message-utils.ts
|
|
2704
3781
|
var SYSTEM_REMINDER_PATTERN = /^<system-reminder>\s*[\s\S]*?\s*<\/system-reminder>$/i;
|
|
2705
3782
|
var SYSTEM_NOTIFICATION_PATTERN = /^<system-notification>\s*[\s\S]*?\s*<\/system-notification>$/i;
|
|
@@ -2708,6 +3785,36 @@ var LEGACY_FORK_CONTEXT_MARKERS = [
|
|
|
2708
3785
|
"\u53EF\u901A\u8FC7 `read_file` \u8BFB\u53D6\u3002",
|
|
2709
3786
|
"# \u4F60\u7684\u4EFB\u52A1"
|
|
2710
3787
|
];
|
|
3788
|
+
var INTERNAL_CHAT_ERROR_PATTERNS = [
|
|
3789
|
+
/\b(?:ENOENT|EACCES|EPERM)\b/i,
|
|
3790
|
+
/\[(?:Errno -?\d+|WinError \d+)\]/i,
|
|
3791
|
+
/\b(?:ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|ENOTFOUND|EAI_AGAIN)\b/i,
|
|
3792
|
+
/\b(?:Connection refused|Connection reset by peer|All connection attempts failed|getaddrinfo failed)\b/i,
|
|
3793
|
+
/(?:^|[\s("'`])\/(?:root|home|Users|tmp|var|opt|etc)\//,
|
|
3794
|
+
/[A-Za-z]:\\(?:Users|Windows|Program Files|AppData)\\/i,
|
|
3795
|
+
/(?:^|\n)Traceback \(most recent call last\):/,
|
|
3796
|
+
/(?:^|\n)\s*at\s+\S+(?:\s+\(|:\d+:\d+)/,
|
|
3797
|
+
/(?:^|\n)File "[^"]+", line \d+/
|
|
3798
|
+
];
|
|
3799
|
+
var SAFE_CHAT_ERROR_PREFIXES = [
|
|
3800
|
+
"\u6C99\u76D2\u8FD0\u884C\u73AF\u5883\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
|
|
3801
|
+
"\u6A21\u578B\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u68C0\u67E5\u6A21\u578B\u914D\u7F6E\u540E\u91CD\u8BD5\u3002"
|
|
3802
|
+
];
|
|
3803
|
+
var GENERIC_CHAT_ERROR = "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002";
|
|
3804
|
+
function chatErrorForDisplay(message, developer = false) {
|
|
3805
|
+
const normalized = message.trim();
|
|
3806
|
+
if (!developer) {
|
|
3807
|
+
const safePrefix = SAFE_CHAT_ERROR_PREFIXES.find((prefix) => normalized.startsWith(prefix));
|
|
3808
|
+
if (safePrefix) return safePrefix;
|
|
3809
|
+
}
|
|
3810
|
+
if (!developer && /no source matches this model/i.test(normalized)) {
|
|
3811
|
+
return "\u5F53\u524D\u6A21\u578B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u6216\u91CD\u65B0\u9009\u62E9\u6A21\u578B\u3002";
|
|
3812
|
+
}
|
|
3813
|
+
if (!developer && INTERNAL_CHAT_ERROR_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
3814
|
+
return GENERIC_CHAT_ERROR;
|
|
3815
|
+
}
|
|
3816
|
+
return normalized || GENERIC_CHAT_ERROR;
|
|
3817
|
+
}
|
|
2711
3818
|
function normalizeMessageContent(content) {
|
|
2712
3819
|
if (typeof content === "string") return content;
|
|
2713
3820
|
if (Array.isArray(content)) return content;
|
|
@@ -2727,10 +3834,10 @@ function isLegacyForkContextText(text) {
|
|
|
2727
3834
|
const normalized = text.trim();
|
|
2728
3835
|
return LEGACY_FORK_CONTEXT_MARKERS.every((marker) => normalized.includes(marker));
|
|
2729
3836
|
}
|
|
2730
|
-
function isHiddenInternalMessage(message) {
|
|
3837
|
+
function isHiddenInternalMessage(message, options = {}) {
|
|
2731
3838
|
if (message.role !== "user") return false;
|
|
2732
3839
|
const text = getTextContent(normalizeMessageContent(message.content));
|
|
2733
|
-
return isSystemReminderText(text) || isSystemNotificationText(text) || isLegacyForkContextText(text);
|
|
3840
|
+
return isSystemReminderText(text) || isSystemNotificationText(text) || (options.includeLegacyForkContext ?? true) && isLegacyForkContextText(text);
|
|
2734
3841
|
}
|
|
2735
3842
|
function buildMessageContent(text, attachments) {
|
|
2736
3843
|
if (attachments.length === 0) return text;
|
|
@@ -2853,6 +3960,12 @@ function groupMessagesByLoop(messages) {
|
|
|
2853
3960
|
}
|
|
2854
3961
|
return groups;
|
|
2855
3962
|
}
|
|
3963
|
+
function isBackgroundWakeMessage(message) {
|
|
3964
|
+
return message.blocks?.some((block) => {
|
|
3965
|
+
const content = block.content;
|
|
3966
|
+
return block.type === "system_notification" && content !== null && typeof content === "object" && "notification_type" in content && content.notification_type === "bg:wakeup";
|
|
3967
|
+
}) ?? false;
|
|
3968
|
+
}
|
|
2856
3969
|
|
|
2857
3970
|
// src/session/state.ts
|
|
2858
3971
|
function toReplaySnapshot(raw) {
|
|
@@ -2877,7 +3990,10 @@ function createInitialSessionState(sessionId) {
|
|
|
2877
3990
|
agentLoops: {},
|
|
2878
3991
|
activeCompaction: null,
|
|
2879
3992
|
replay: null,
|
|
2880
|
-
|
|
3993
|
+
queue: EMPTY_SESSION_QUEUE,
|
|
3994
|
+
viewerRole: null,
|
|
3995
|
+
nextBefore: null,
|
|
3996
|
+
loadingOlder: false
|
|
2881
3997
|
};
|
|
2882
3998
|
}
|
|
2883
3999
|
function parseAgentDescription(argumentsJson) {
|
|
@@ -2903,10 +4019,10 @@ function inferLoopStatusFromMessages(messages) {
|
|
|
2903
4019
|
}
|
|
2904
4020
|
function inferLoopStatusFromTurns(turns, messages) {
|
|
2905
4021
|
const latestAgentNotification = [...turns].reverse().flatMap((turn) => turn.blocks).find((block) => {
|
|
2906
|
-
if (block.type !== "system_notification" || !
|
|
4022
|
+
if (block.type !== "system_notification" || !isRecord(block.content)) return false;
|
|
2907
4023
|
return block.content.notification_type === "agent:start" || block.content.notification_type === "agent:end";
|
|
2908
4024
|
});
|
|
2909
|
-
if (latestAgentNotification?.type === "system_notification" &&
|
|
4025
|
+
if (latestAgentNotification?.type === "system_notification" && isRecord(latestAgentNotification.content)) {
|
|
2910
4026
|
const notificationType = latestAgentNotification.content.notification_type;
|
|
2911
4027
|
const status = latestAgentNotification.content.status;
|
|
2912
4028
|
if (notificationType === "agent:start" || status === "running") return "running";
|
|
@@ -2915,17 +4031,44 @@ function inferLoopStatusFromTurns(turns, messages) {
|
|
|
2915
4031
|
}
|
|
2916
4032
|
return inferLoopStatusFromMessages(messages);
|
|
2917
4033
|
}
|
|
2918
|
-
function
|
|
2919
|
-
|
|
4034
|
+
function toSelectionMap(value) {
|
|
4035
|
+
if (!isRecord(value)) return {};
|
|
4036
|
+
const entries = Object.entries(value).map(([questionKey, optionIndexes]) => {
|
|
4037
|
+
if (!Array.isArray(optionIndexes)) return null;
|
|
4038
|
+
const parsedIndexes = optionIndexes.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item));
|
|
4039
|
+
return [Number(questionKey), parsedIndexes];
|
|
4040
|
+
}).filter((entry) => entry !== null);
|
|
4041
|
+
return Object.fromEntries(entries);
|
|
4042
|
+
}
|
|
4043
|
+
function toCustomMap(value) {
|
|
4044
|
+
if (!isRecord(value)) return {};
|
|
4045
|
+
const entries = Object.entries(value).filter(([, text]) => typeof text === "string").map(([questionKey, text]) => [Number(questionKey), text]);
|
|
4046
|
+
return Object.fromEntries(entries);
|
|
4047
|
+
}
|
|
4048
|
+
function extractAskAnswers(turns) {
|
|
4049
|
+
const answers = {};
|
|
4050
|
+
for (const turn of turns) {
|
|
4051
|
+
for (const block of turn.blocks) {
|
|
4052
|
+
if (block.type !== "ask_user_answer" || typeof block.tool_call_id !== "string") continue;
|
|
4053
|
+
if (!isRecord(block.content)) continue;
|
|
4054
|
+
const note = typeof block.content.note === "string" ? block.content.note.trim() : "";
|
|
4055
|
+
answers[block.tool_call_id] = {
|
|
4056
|
+
selections: toSelectionMap(block.content.selections),
|
|
4057
|
+
custom: toCustomMap(block.content.custom),
|
|
4058
|
+
...note ? { note } : {}
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
return answers;
|
|
2920
4063
|
}
|
|
2921
4064
|
function parentForkToolCallIdFromTurn(turn) {
|
|
2922
4065
|
if (typeof turn.parent_fork_tool_call_id === "string" && turn.parent_fork_tool_call_id.length > 0) {
|
|
2923
4066
|
return turn.parent_fork_tool_call_id;
|
|
2924
4067
|
}
|
|
2925
4068
|
for (const block of turn.blocks) {
|
|
2926
|
-
if (block.type !== "system_notification" || !
|
|
4069
|
+
if (block.type !== "system_notification" || !isRecord(block.content)) continue;
|
|
2927
4070
|
const metadata = block.content.metadata;
|
|
2928
|
-
if (!
|
|
4071
|
+
if (!isRecord(metadata)) continue;
|
|
2929
4072
|
const parentId = metadata.parent_fork_tool_call_id;
|
|
2930
4073
|
if (typeof parentId === "string" && parentId.length > 0) return parentId;
|
|
2931
4074
|
}
|
|
@@ -2942,16 +4085,16 @@ function buildMessageContent2(turn) {
|
|
|
2942
4085
|
}
|
|
2943
4086
|
function workspaceNotificationContent(turn) {
|
|
2944
4087
|
const block = turn.blocks.find(
|
|
2945
|
-
(candidate) => candidate.type === "system_notification" &&
|
|
4088
|
+
(candidate) => candidate.type === "system_notification" && isRecord(candidate.content) && candidate.content.notification_type === "workspace_change"
|
|
2946
4089
|
);
|
|
2947
|
-
if (!block || !
|
|
4090
|
+
if (!block || !isRecord(block.content)) return "";
|
|
2948
4091
|
const title = typeof block.content.title === "string" ? block.content.title.trim() : "";
|
|
2949
4092
|
const detail = typeof block.content.detail === "string" ? block.content.detail.trim() : "";
|
|
2950
4093
|
return [title ? `**${title}**` : "", detail].filter(Boolean).join("\n\n");
|
|
2951
4094
|
}
|
|
2952
4095
|
function askUserAnswerContent(turn) {
|
|
2953
4096
|
const answerBlock = turn.blocks.find((block) => block.type === "ask_user_answer");
|
|
2954
|
-
if (!answerBlock || !
|
|
4097
|
+
if (!answerBlock || !isRecord(answerBlock.content)) return null;
|
|
2955
4098
|
const answer = answerBlock.content.answer;
|
|
2956
4099
|
return typeof answer === "string" && answer.trim().length > 0 ? answer : null;
|
|
2957
4100
|
}
|
|
@@ -2971,6 +4114,19 @@ function extractModeFromBlocks(blocks) {
|
|
|
2971
4114
|
return null;
|
|
2972
4115
|
}
|
|
2973
4116
|
function projectionToMessage(turn) {
|
|
4117
|
+
if (turn.kind === "context") {
|
|
4118
|
+
const context = contextProjectionData(turn);
|
|
4119
|
+
if (!context) return null;
|
|
4120
|
+
return {
|
|
4121
|
+
role: "assistant",
|
|
4122
|
+
content: "",
|
|
4123
|
+
kind: "context",
|
|
4124
|
+
context,
|
|
4125
|
+
loop_name: turn.loop_id,
|
|
4126
|
+
entry_id: turn.turn_id,
|
|
4127
|
+
status: turn.status
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
2974
4130
|
if (turn.kind === "compaction" && turn.compaction_id) {
|
|
2975
4131
|
return {
|
|
2976
4132
|
role: "assistant",
|
|
@@ -3141,21 +4297,58 @@ function orderTurns(turns) {
|
|
|
3141
4297
|
return turns;
|
|
3142
4298
|
}
|
|
3143
4299
|
function reconcileOptimisticUserTurns(currentTurns, incomingTurns) {
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
4300
|
+
return reconcileUserTurns(currentTurns, incomingTurns, true, false);
|
|
4301
|
+
}
|
|
4302
|
+
function reconcileHistoricalUserTurns(currentTurns, incomingTurns) {
|
|
4303
|
+
return reconcileUserTurns(currentTurns, incomingTurns, false, true);
|
|
4304
|
+
}
|
|
4305
|
+
function reconcileUserTurns(currentTurns, incomingTurns, allowLiveFallback, keepUnclaimedOptimistic) {
|
|
4306
|
+
const preservedTurns = preserveClientRenderIds(currentTurns, incomingTurns);
|
|
4307
|
+
const optimisticUserTurns = currentTurns.filter(
|
|
4308
|
+
(turn) => turn.role === "user" && (turn.kind ?? "message") === "message" && turn.turn_id.startsWith("local-user-")
|
|
4309
|
+
);
|
|
4310
|
+
const optimisticByRequestId = new Map(
|
|
4311
|
+
optimisticUserTurns.map((turn) => [
|
|
4312
|
+
turn.turn_id.slice("local-user-".length),
|
|
4313
|
+
turn
|
|
4314
|
+
])
|
|
4315
|
+
);
|
|
4316
|
+
const currentTurnIds = new Set(currentTurns.map((turn) => turn.turn_id));
|
|
4317
|
+
const claimedOptimisticIds = /* @__PURE__ */ new Set();
|
|
4318
|
+
const reconciled = preservedTurns.map((turn) => {
|
|
4319
|
+
const exactOptimistic = typeof turn.client_request_id === "string" ? optimisticByRequestId.get(turn.client_request_id) : void 0;
|
|
4320
|
+
if (exactOptimistic && !claimedOptimisticIds.has(exactOptimistic.turn_id) && isUserConfirmation(turn)) {
|
|
4321
|
+
claimedOptimisticIds.add(exactOptimistic.turn_id);
|
|
4322
|
+
return { ...turn, client_render_id: exactOptimistic.turn_id };
|
|
4323
|
+
}
|
|
4324
|
+
const fallbackOptimistic = optimisticUserTurns.find(
|
|
4325
|
+
(candidate) => !claimedOptimisticIds.has(candidate.turn_id)
|
|
4326
|
+
);
|
|
4327
|
+
if (allowLiveFallback && typeof turn.client_request_id !== "string" && fallbackOptimistic && isUserConfirmation(turn) && !turn.turn_id.startsWith("local-user-") && !currentTurnIds.has(turn.turn_id)) {
|
|
4328
|
+
claimedOptimisticIds.add(fallbackOptimistic.turn_id);
|
|
4329
|
+
return { ...turn, client_render_id: fallbackOptimistic.turn_id };
|
|
4330
|
+
}
|
|
4331
|
+
return turn;
|
|
4332
|
+
});
|
|
4333
|
+
const result = claimedOptimisticIds.size === 0 ? reconciled : reconciled.filter((turn) => !claimedOptimisticIds.has(turn.turn_id));
|
|
4334
|
+
if (!keepUnclaimedOptimistic) return result;
|
|
4335
|
+
const resultIds = new Set(result.map((turn) => turn.turn_id));
|
|
4336
|
+
const unclaimedOptimisticTurns = optimisticUserTurns.filter(
|
|
4337
|
+
(turn) => !claimedOptimisticIds.has(turn.turn_id) && !resultIds.has(turn.turn_id)
|
|
3147
4338
|
);
|
|
3148
|
-
|
|
4339
|
+
return unclaimedOptimisticTurns.length === 0 ? result : orderTurns([...result, ...unclaimedOptimisticTurns]);
|
|
4340
|
+
}
|
|
4341
|
+
function isUserConfirmation(turn) {
|
|
4342
|
+
return turn.role === "user" && (turn.kind ?? "message") === "message" || turn.blocks.some((block) => block.type === "ask_user_answer");
|
|
4343
|
+
}
|
|
4344
|
+
function preserveClientRenderIds(currentTurns, incomingTurns) {
|
|
4345
|
+
const currentById = new Map(currentTurns.map((turn) => [turn.turn_id, turn]));
|
|
3149
4346
|
return incomingTurns.map((turn) => {
|
|
3150
4347
|
const current = currentById.get(turn.turn_id);
|
|
3151
4348
|
if (current) {
|
|
3152
4349
|
const currentRenderId = current.client_render_id;
|
|
3153
4350
|
return turn.client_render_id || !currentRenderId ? turn : { ...turn, client_render_id: currentRenderId };
|
|
3154
4351
|
}
|
|
3155
|
-
if (!optimisticUserMatched && optimisticUserTurn && turn.role === "user" && !turn.turn_id.startsWith("local-user-")) {
|
|
3156
|
-
optimisticUserMatched = true;
|
|
3157
|
-
return { ...turn, client_render_id: optimisticUserTurn.turn_id };
|
|
3158
|
-
}
|
|
3159
4352
|
return turn;
|
|
3160
4353
|
});
|
|
3161
4354
|
}
|
|
@@ -3170,7 +4363,7 @@ function areAgentLoopsEqual(left, right) {
|
|
|
3170
4363
|
});
|
|
3171
4364
|
}
|
|
3172
4365
|
function withTurns(state, turns) {
|
|
3173
|
-
const orderedTurns = orderTurns(
|
|
4366
|
+
const orderedTurns = orderTurns(reconcileHistoricalUserTurns(state.turns, turns));
|
|
3174
4367
|
const { messages, agentLoops, activeCompaction } = materialize(orderedTurns);
|
|
3175
4368
|
const isWaitingForInput = orderedTurns.some(
|
|
3176
4369
|
(turn) => turn.tool_calls.some((toolCall) => toolCall.status === "awaiting_answer")
|
|
@@ -3188,26 +4381,45 @@ function withTurns(state, turns) {
|
|
|
3188
4381
|
});
|
|
3189
4382
|
const mergedMessages = preservedErrors.length > 0 ? [...messagesWithRenderIds, ...preservedErrors] : messagesWithRenderIds;
|
|
3190
4383
|
const latestMode = [...orderedTurns].reverse().map((turn) => extractModeFromBlocks(turn.blocks)).find((mode) => mode !== null);
|
|
4384
|
+
const askAnswersFromHistory = extractAskAnswers(orderedTurns);
|
|
4385
|
+
const presentToolCallIds = new Set(
|
|
4386
|
+
orderedTurns.flatMap((turn) => turn.tool_calls.map((toolCall) => toolCall.id))
|
|
4387
|
+
);
|
|
4388
|
+
const askAnswers = { ...askAnswersFromHistory };
|
|
4389
|
+
for (const [toolCallId, answer] of Object.entries(state.askAnswers)) {
|
|
4390
|
+
if (!(toolCallId in askAnswers) && presentToolCallIds.has(toolCallId)) {
|
|
4391
|
+
askAnswers[toolCallId] = answer;
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
3191
4394
|
return {
|
|
3192
4395
|
...state,
|
|
3193
4396
|
turns: orderedTurns,
|
|
3194
4397
|
messages: mergedMessages,
|
|
3195
4398
|
agentLoops: stableAgentLoops,
|
|
3196
4399
|
activeCompaction,
|
|
4400
|
+
askAnswers,
|
|
3197
4401
|
mode: latestMode ?? state.mode,
|
|
3198
4402
|
// 历史恢复时 chat:end(paused) 可能已不在 socket replay 缓存中;
|
|
3199
4403
|
// awaiting_answer 是投影里的持久权威状态,必须据此恢复问答交互。
|
|
3200
4404
|
status: isWaitingForInput ? "waiting_for_input" : state.status
|
|
3201
4405
|
};
|
|
3202
4406
|
}
|
|
4407
|
+
function withQueue(state, snapshot2) {
|
|
4408
|
+
if (!shouldApplySnapshot(state.queue, snapshot2)) return state;
|
|
4409
|
+
return { ...state, queue: snapshot2 };
|
|
4410
|
+
}
|
|
3203
4411
|
var localTurnCounter = 0;
|
|
3204
|
-
function addUserMessage(state, content) {
|
|
4412
|
+
function addUserMessage(state, content, clientRequestId) {
|
|
3205
4413
|
localTurnCounter += 1;
|
|
3206
|
-
const turnId = `local-user-${Date.now()}-${localTurnCounter}`;
|
|
4414
|
+
const turnId = `local-user-${clientRequestId ?? `${Date.now()}-${localTurnCounter}`}`;
|
|
3207
4415
|
const optimisticTurn = {
|
|
3208
4416
|
id: turnId,
|
|
3209
4417
|
sequence: Math.max(0, ...state.turns.map((turn) => turn.sequence)) + 1,
|
|
3210
4418
|
turn_id: turnId,
|
|
4419
|
+
// 乐观态就自赋,跟服务端确认后 reconcileUserTurns 补上的
|
|
4420
|
+
// client_render_id(= 乐观 turn 自己的 turn_id)保持同一个值,
|
|
4421
|
+
// 消费方才能拿它当"确认前后不变"的稳定标识使用。
|
|
4422
|
+
client_render_id: turnId,
|
|
3211
4423
|
loop_id: "root",
|
|
3212
4424
|
role: "user",
|
|
3213
4425
|
status: "streaming",
|
|
@@ -3222,10 +4434,9 @@ function addUserMessage(state, content) {
|
|
|
3222
4434
|
function upsertTurn(state, turn) {
|
|
3223
4435
|
let existing = [...state.turns];
|
|
3224
4436
|
const [nextTurn] = reconcileOptimisticUserTurns(existing, [turn]);
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
);
|
|
4437
|
+
const claimedOptimisticId = nextTurn.client_render_id;
|
|
4438
|
+
if (claimedOptimisticId?.startsWith("local-user-")) {
|
|
4439
|
+
existing = existing.filter((item) => item.turn_id !== claimedOptimisticId);
|
|
3229
4440
|
}
|
|
3230
4441
|
const index = existing.findIndex((item) => item.turn_id === nextTurn.turn_id);
|
|
3231
4442
|
if (index >= 0) {
|
|
@@ -3233,7 +4444,7 @@ function upsertTurn(state, turn) {
|
|
|
3233
4444
|
} else {
|
|
3234
4445
|
existing.push(nextTurn);
|
|
3235
4446
|
}
|
|
3236
|
-
return withTurns(state, existing);
|
|
4447
|
+
return withTurns({ ...state, turns: existing }, existing);
|
|
3237
4448
|
}
|
|
3238
4449
|
function applyTurnPatch(state, patch) {
|
|
3239
4450
|
const turn = patch.data.turn;
|
|
@@ -3244,13 +4455,7 @@ function applyTurnPatch(state, patch) {
|
|
|
3244
4455
|
if (lastSequence !== null && patch.sequence <= lastSequence) {
|
|
3245
4456
|
return state;
|
|
3246
4457
|
}
|
|
3247
|
-
|
|
3248
|
-
if (index >= 0) {
|
|
3249
|
-
existing[index] = nextTurn;
|
|
3250
|
-
} else {
|
|
3251
|
-
existing.push(nextTurn);
|
|
3252
|
-
}
|
|
3253
|
-
return withTurns(state, existing);
|
|
4458
|
+
return upsertTurn(state, { ...turn, sequence: patch.sequence });
|
|
3254
4459
|
}
|
|
3255
4460
|
function addErrorMessage(state, content) {
|
|
3256
4461
|
const anchorTurnId = state.turns[state.turns.length - 1]?.turn_id ?? null;
|
|
@@ -3280,21 +4485,49 @@ function markStreamingTurns(state, turnStatus, toolCallStatus) {
|
|
|
3280
4485
|
return withTurns(state, turns);
|
|
3281
4486
|
}
|
|
3282
4487
|
|
|
4488
|
+
// src/session/errors.ts
|
|
4489
|
+
var AgentSessionError = class extends Error {
|
|
4490
|
+
name = "AgentSessionError";
|
|
4491
|
+
code;
|
|
4492
|
+
constructor(message, code) {
|
|
4493
|
+
super(message);
|
|
4494
|
+
this.code = code;
|
|
4495
|
+
}
|
|
4496
|
+
};
|
|
4497
|
+
|
|
3283
4498
|
// src/session/agent-session.ts
|
|
3284
4499
|
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";
|
|
3285
|
-
var
|
|
3286
|
-
|
|
3287
|
-
|
|
4500
|
+
var COMMAND_RESTORE_RETRY_BASE_MS = 1e3;
|
|
4501
|
+
var COMMAND_RESTORE_RETRY_MAX_MS = 3e4;
|
|
4502
|
+
var QUEUE_ACK_TIMEOUT_MS = 1e4;
|
|
4503
|
+
var fallbackRequestCounter = 0;
|
|
4504
|
+
function createClientRequestId() {
|
|
4505
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
4506
|
+
return globalThis.crypto.randomUUID();
|
|
4507
|
+
}
|
|
4508
|
+
fallbackRequestCounter += 1;
|
|
4509
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}-${fallbackRequestCounter}`;
|
|
3288
4510
|
}
|
|
4511
|
+
var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
|
|
3289
4512
|
function isOomText(value) {
|
|
3290
4513
|
return typeof value === "string" && OOM_KEYWORDS_RE.test(value);
|
|
3291
4514
|
}
|
|
4515
|
+
function describeSessionError(error, fallback) {
|
|
4516
|
+
if (error instanceof AgentSessionError) {
|
|
4517
|
+
return {
|
|
4518
|
+
message: error.message || fallback,
|
|
4519
|
+
...error.code ? { code: error.code } : {}
|
|
4520
|
+
};
|
|
4521
|
+
}
|
|
4522
|
+
if (error instanceof Error && error.message) return { message: error.message };
|
|
4523
|
+
return { message: fallback };
|
|
4524
|
+
}
|
|
3292
4525
|
function parseJsonRecord(value) {
|
|
3293
|
-
if (
|
|
4526
|
+
if (isRecord(value)) return value;
|
|
3294
4527
|
if (typeof value !== "string") return null;
|
|
3295
4528
|
try {
|
|
3296
4529
|
const parsed = JSON.parse(value);
|
|
3297
|
-
return
|
|
4530
|
+
return isRecord(parsed) ? parsed : null;
|
|
3298
4531
|
} catch {
|
|
3299
4532
|
return null;
|
|
3300
4533
|
}
|
|
@@ -3320,6 +4553,21 @@ function isOomResult(value) {
|
|
|
3320
4553
|
function isToolBridgeContent(value) {
|
|
3321
4554
|
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.action === "string";
|
|
3322
4555
|
}
|
|
4556
|
+
function isCursorInvalidError(error) {
|
|
4557
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 409;
|
|
4558
|
+
}
|
|
4559
|
+
function queueRequestFailure(error) {
|
|
4560
|
+
const failure = error instanceof AgentSessionError ? { message: error.message, ...error.code ? { code: error.code } : {} } : { message: error instanceof Error && error.message ? error.message : "\u961F\u5217\u64CD\u4F5C\u5931\u8D25" };
|
|
4561
|
+
return {
|
|
4562
|
+
ok: false,
|
|
4563
|
+
conflict: false,
|
|
4564
|
+
snapshot: null,
|
|
4565
|
+
...failure
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
function commandDeliveryKey(deliveryId) {
|
|
4569
|
+
return `tool-command:${deliveryId}`;
|
|
4570
|
+
}
|
|
3323
4571
|
function sessionStatusFromChatEnd(status) {
|
|
3324
4572
|
if (status === "paused") return "waiting_for_input";
|
|
3325
4573
|
if (status === "interrupted") return "interrupted";
|
|
@@ -3335,14 +4583,6 @@ function extractTerminalChatEndStatus(events) {
|
|
|
3335
4583
|
}
|
|
3336
4584
|
return null;
|
|
3337
4585
|
}
|
|
3338
|
-
function hasChatRunEvent2(events) {
|
|
3339
|
-
return events.some(
|
|
3340
|
-
(event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
|
|
3341
|
-
);
|
|
3342
|
-
}
|
|
3343
|
-
function isUiMetaLike(value) {
|
|
3344
|
-
return isRecord5(value) && ("resourceHTML" in value || "resourceUri" in value || "resourceURI" in value);
|
|
3345
|
-
}
|
|
3346
4586
|
var AgentSession = class _AgentSession {
|
|
3347
4587
|
sessionId;
|
|
3348
4588
|
state;
|
|
@@ -3356,14 +4596,55 @@ var AgentSession = class _AgentSession {
|
|
|
3356
4596
|
patchFlushCancel = null;
|
|
3357
4597
|
joined = false;
|
|
3358
4598
|
joinPending = null;
|
|
4599
|
+
// bootstrap 与每次 reconnect 共用一条订阅生命周期队列。它们都会重置 join
|
|
4600
|
+
// 状态并临时设置 replay source,禁止并发,否则较早操作的 finally 会清掉
|
|
4601
|
+
// 较晚操作仍在使用的来源上下文。
|
|
4602
|
+
subscriptionQueue = Promise.resolve();
|
|
4603
|
+
replaceQueue = Promise.resolve();
|
|
4604
|
+
timelineEpoch = 0;
|
|
4605
|
+
/** 定位请求的流水号:被后一次选择取代的那次定位不再提交窗口(见 locateEntry)。 */
|
|
4606
|
+
locateToken = 0;
|
|
4607
|
+
/** 在途那次定位的请求句柄。**只**服务定位:权威刷新不共用它(见 cancelPendingLocate)。 */
|
|
4608
|
+
locateAbort = null;
|
|
4609
|
+
pagingGeneration = 0;
|
|
4610
|
+
liveRevision = 0;
|
|
4611
|
+
modeRevision = 0;
|
|
4612
|
+
liveRevisions = /* @__PURE__ */ new Map();
|
|
4613
|
+
pendingOptimisticTurnIds = /* @__PURE__ */ new Set();
|
|
4614
|
+
olderPending = null;
|
|
4615
|
+
replaceInFlight = 0;
|
|
3359
4616
|
// 回放控制请求排成一条队列。连着点"5x"再点"退出回放"时,并发发出去的两个
|
|
3360
4617
|
// 请求响应可能乱序回来,晚到的旧值会把已经退出的会话又显示成回放中;用
|
|
3361
4618
|
// 序号丢弃过期响应又会在失败时留下中间态。串行执行就没有这些情况:每个
|
|
3362
4619
|
// 响应都是当时最新的,失败也只影响自己。
|
|
3363
4620
|
replayQueue = Promise.resolve();
|
|
4621
|
+
// Historical snapshots and live commands share one delivery barrier. A
|
|
4622
|
+
// repair closes the barrier before joining, snapshots the raw command log
|
|
4623
|
+
// after the join, then releases buffered commands in socket arrival order.
|
|
4624
|
+
commandRestoreQueue = Promise.resolve();
|
|
4625
|
+
commandRecoveryGeneration = 0;
|
|
4626
|
+
commandRestoreRetryAttempt = 0;
|
|
4627
|
+
commandRestoreRetryHandle = null;
|
|
4628
|
+
bufferedCommandDeliveries = null;
|
|
3364
4629
|
pendingReplayMessage = null;
|
|
3365
4630
|
pendingReplayMode;
|
|
4631
|
+
pendingReplayKbIds;
|
|
3366
4632
|
recentChatEndAt = 0;
|
|
4633
|
+
/**
|
|
4634
|
+
* 最近一次已经通过 `error` 事件投递过的服务端失败(`message` + `code`)。
|
|
4635
|
+
*
|
|
4636
|
+
* `chat:send` 被代理时服务端会先发 `system:error`、再用同一条失败回 ack,两条
|
|
4637
|
+
* 通道都会到达这里:前者走 `_handleSystemError`,后者走发送失败的回滚。不去重
|
|
4638
|
+
* 的话调用方会收到两次同一个 `error`(重复提示、重试逻辑跑两遍)。
|
|
4639
|
+
*
|
|
4640
|
+
* 带代际边界,只认「本次 `emitWithAck` 等待期间」投递的那一条:否则队列/停止
|
|
4641
|
+
* 失败留下的标记会一直挂着,之后一次无关的订阅失败只是文案码相同就被当成
|
|
4642
|
+
* 重复而静音。用单调递增的代际号而不是时间戳,同一毫秒内的两条也不会混淆。
|
|
4643
|
+
*/
|
|
4644
|
+
lastDeliveredSystemError = null;
|
|
4645
|
+
/** 每次投递 `system:error` 递增,用于判定某条失败是否属于本次等待。 */
|
|
4646
|
+
systemErrorGeneration = 0;
|
|
4647
|
+
subscriptionReplaySource = null;
|
|
3367
4648
|
disposed = false;
|
|
3368
4649
|
getAppContext = null;
|
|
3369
4650
|
sendPending = false;
|
|
@@ -3374,11 +4655,19 @@ var AgentSession = class _AgentSession {
|
|
|
3374
4655
|
seenToolCallIds = /* @__PURE__ */ new Set();
|
|
3375
4656
|
finishedToolCallIds = /* @__PURE__ */ new Set();
|
|
3376
4657
|
emittedMessageTurnIds = /* @__PURE__ */ new Set();
|
|
3377
|
-
constructor(sessionId, runtime, connection) {
|
|
4658
|
+
constructor(sessionId, runtime, connection, options = {}) {
|
|
3378
4659
|
this.sessionId = sessionId;
|
|
3379
4660
|
this.runtime = runtime;
|
|
3380
4661
|
this.state = { ...createInitialSessionState(sessionId), connection };
|
|
4662
|
+
this.isStatic = options.staticHistory === true;
|
|
3381
4663
|
}
|
|
4664
|
+
/**
|
|
4665
|
+
* 静态历史(分享页 / `fromTurns`):没有目录接口,也**没有**"目录还没到位"这个中间态。
|
|
4666
|
+
*
|
|
4667
|
+
* 宿主据此决定要不要拿已加载窗口当兜底条目——真实会话在 timeline 被重写后,窗口里还是
|
|
4668
|
+
* 旧分支的发言,拿它兜底会把已经失效的 entry id 重新摆到轨道上,点了必然找不到。
|
|
4669
|
+
*/
|
|
4670
|
+
isStatic;
|
|
3382
4671
|
/** 从一份静态 turn 快照构造只展示历史的会话(例如公开分享页)。 */
|
|
3383
4672
|
static fromTurns(sessionId, turns) {
|
|
3384
4673
|
const readonlyError = () => {
|
|
@@ -3387,11 +4676,13 @@ var AgentSession = class _AgentSession {
|
|
|
3387
4676
|
const session = new _AgentSession(
|
|
3388
4677
|
sessionId,
|
|
3389
4678
|
{
|
|
3390
|
-
streamTokens: false,
|
|
3391
4679
|
emitEvent: readonlyError,
|
|
3392
4680
|
emitWithAck: async () => readonlyError(),
|
|
3393
4681
|
ensureConnected: readonlyError,
|
|
3394
|
-
|
|
4682
|
+
fetchTurnsPage: async () => ({ turns, nextBefore: null, currentMode: "executing" }),
|
|
4683
|
+
// 静态会话(分享页)没有会话目录接口,宿主用已加载的历史兜底条目。
|
|
4684
|
+
fetchTurnIndex: async () => ({ sessionId, items: [] }),
|
|
4685
|
+
fetchCommands: async () => [],
|
|
3395
4686
|
// 静态历史没有回放身份可言,给个空详情即可,不要抛错——_bootstrap
|
|
3396
4687
|
// 不会跑到这里,但保持只读语义一致。
|
|
3397
4688
|
fetchSessionInfo: async () => ({ replay_state: null }),
|
|
@@ -3402,7 +4693,8 @@ var AgentSession = class _AgentSession {
|
|
|
3402
4693
|
attachAppCli: async () => readonlyError(),
|
|
3403
4694
|
onDispose: () => true
|
|
3404
4695
|
},
|
|
3405
|
-
"connected"
|
|
4696
|
+
"connected",
|
|
4697
|
+
{ staticHistory: true }
|
|
3406
4698
|
);
|
|
3407
4699
|
session.state = {
|
|
3408
4700
|
...withTurns(session.state, turns),
|
|
@@ -3415,6 +4707,101 @@ var AgentSession = class _AgentSession {
|
|
|
3415
4707
|
getState() {
|
|
3416
4708
|
return this.state;
|
|
3417
4709
|
}
|
|
4710
|
+
get hasOlderHistory() {
|
|
4711
|
+
return this.state.nextBefore !== null;
|
|
4712
|
+
}
|
|
4713
|
+
/** Load the page immediately preceding the current display window. */
|
|
4714
|
+
loadOlderHistory(options = {}) {
|
|
4715
|
+
if (this.disposed) return Promise.resolve();
|
|
4716
|
+
if (this.olderPending) return this.olderPending;
|
|
4717
|
+
if (this.replaceInFlight > 0) {
|
|
4718
|
+
return this.replaceQueue.then(() => this.loadOlderHistory(options));
|
|
4719
|
+
}
|
|
4720
|
+
const before = this.state.nextBefore;
|
|
4721
|
+
if (!before) return Promise.resolve();
|
|
4722
|
+
const epoch = this.timelineEpoch;
|
|
4723
|
+
const generation = this.pagingGeneration;
|
|
4724
|
+
this.update((state) => ({ ...state, loadingOlder: true }));
|
|
4725
|
+
const request = this.runtime.fetchTurnsPage(this.sessionId, { before }).then((page) => {
|
|
4726
|
+
if (epoch !== this.timelineEpoch || generation !== this.pagingGeneration || this.state.nextBefore !== before) return;
|
|
4727
|
+
options.beforeCommit?.();
|
|
4728
|
+
this.update((state) => ({
|
|
4729
|
+
...withTurns(state, prependOlder(state.turns, page.turns)),
|
|
4730
|
+
nextBefore: page.nextBefore,
|
|
4731
|
+
loadingOlder: false
|
|
4732
|
+
}));
|
|
4733
|
+
}).catch(async (error) => {
|
|
4734
|
+
if (isCursorInvalidError(error) && epoch === this.timelineEpoch && generation === this.pagingGeneration && this.state.nextBefore === before) {
|
|
4735
|
+
this.invalidateTimeline();
|
|
4736
|
+
await this.replaceFromHistory();
|
|
4737
|
+
return;
|
|
4738
|
+
}
|
|
4739
|
+
}).finally(() => {
|
|
4740
|
+
if (this.olderPending !== request) return;
|
|
4741
|
+
this.olderPending = null;
|
|
4742
|
+
this.update((state) => ({ ...state, loadingOlder: false }));
|
|
4743
|
+
});
|
|
4744
|
+
this.olderPending = request;
|
|
4745
|
+
return request;
|
|
4746
|
+
}
|
|
4747
|
+
/**
|
|
4748
|
+
* 定位到一条窗口外的历史发言:请服务端返回"从这条发言到最新"的窗口并整窗替换。
|
|
4749
|
+
*
|
|
4750
|
+
* 返回 false 表示这次定位没有落地(请求失败,或期间 timeline 被重写、会话已释放),
|
|
4751
|
+
* 当前窗口保持不变,调用方可以原样重试。已加载的发言不要走这里——宿主直接滚动,
|
|
4752
|
+
* 一次网络请求都不该发。
|
|
4753
|
+
*/
|
|
4754
|
+
async locateEntry(entryId) {
|
|
4755
|
+
const token = ++this.locateToken;
|
|
4756
|
+
const controller = new AbortController();
|
|
4757
|
+
this.locateAbort?.abort();
|
|
4758
|
+
this.locateAbort = controller;
|
|
4759
|
+
try {
|
|
4760
|
+
return await this.replaceFromHistory({
|
|
4761
|
+
suppressErrors: false,
|
|
4762
|
+
fromEntryId: entryId,
|
|
4763
|
+
signal: controller.signal,
|
|
4764
|
+
isStale: () => this.locateToken !== token
|
|
4765
|
+
});
|
|
4766
|
+
} catch {
|
|
4767
|
+
return false;
|
|
4768
|
+
} finally {
|
|
4769
|
+
if (this.locateAbort === controller) this.locateAbort = null;
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
/**
|
|
4773
|
+
* 取消在途定位:用户改选了另一条发言(哪怕那条已经加载、不需要请求),
|
|
4774
|
+
* 先前那次定位既不该再落地,也不该继续占着替换队列——排队中的下一次定位
|
|
4775
|
+
* 不然要等它跑完(用户看到的是"点了新目标一直转圈")。
|
|
4776
|
+
*
|
|
4777
|
+
* **只**中止定位自己的请求:权威刷新(运行结束 / 重连补数)不共用这个 controller,
|
|
4778
|
+
* 否则"运行刚结束时点一下侧栏"就会把终态对账一起中止,流式与历史对不上。
|
|
4779
|
+
*/
|
|
4780
|
+
cancelPendingLocate() {
|
|
4781
|
+
this.locateToken += 1;
|
|
4782
|
+
this.locateAbort?.abort();
|
|
4783
|
+
this.locateAbort = null;
|
|
4784
|
+
}
|
|
4785
|
+
/**
|
|
4786
|
+
* timeline 被整体改写(rewind / checkout / 释放):推进纪元,并把在途定位请求一并中止
|
|
4787
|
+
* ——它已经属于旧 timeline,却还占着替换队列,会让改写后的权威窗口排在后面等它跑完。
|
|
4788
|
+
* 作废号仍然保留:中止只是让它更早结束。
|
|
4789
|
+
*/
|
|
4790
|
+
invalidateTimeline() {
|
|
4791
|
+
this.timelineEpoch += 1;
|
|
4792
|
+
this.locateAbort?.abort();
|
|
4793
|
+
this.locateAbort = null;
|
|
4794
|
+
}
|
|
4795
|
+
/**
|
|
4796
|
+
* 拉取当前分支的用户发言目录(消息导航侧栏用)。
|
|
4797
|
+
*
|
|
4798
|
+
* 不在这里缓存:什么时候该刷新只有调用方知道(会话切换、历史被替换、新发言),
|
|
4799
|
+
* 缓存只会让旧条目在 rewind 之后继续可点。
|
|
4800
|
+
*/
|
|
4801
|
+
async loadTurnIndex() {
|
|
4802
|
+
const index = await this.runtime.fetchTurnIndex(this.sessionId);
|
|
4803
|
+
return index.items;
|
|
4804
|
+
}
|
|
3418
4805
|
/** 订阅状态变化。返回取消订阅函数。 */
|
|
3419
4806
|
subscribe(listener) {
|
|
3420
4807
|
this.listeners.add(listener);
|
|
@@ -3467,7 +4854,7 @@ var AgentSession = class _AgentSession {
|
|
|
3467
4854
|
/** 发送一条消息。字符串或多模态内容均可。 */
|
|
3468
4855
|
async send(content, options = {}) {
|
|
3469
4856
|
this.runtime.ensureConnected();
|
|
3470
|
-
if (this.state.isStreaming || this.state.activeCompaction || this.sendPending || this.manualCompactionPending) {
|
|
4857
|
+
if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.sendPending || this.manualCompactionPending) {
|
|
3471
4858
|
this.update(
|
|
3472
4859
|
(s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\u6216\u5BF9\u8BDD\u6B63\u5728\u6574\u7406\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002")
|
|
3473
4860
|
);
|
|
@@ -3480,9 +4867,125 @@ var AgentSession = class _AgentSession {
|
|
|
3480
4867
|
this.sendPending = false;
|
|
3481
4868
|
}
|
|
3482
4869
|
}
|
|
4870
|
+
/**
|
|
4871
|
+
* 入队一条等待消息(运行中排队 / 暂停期间照常入队)。
|
|
4872
|
+
*
|
|
4873
|
+
* ack 超时或传输失败时结果未知:用**同一个** `client_request_id` 重试一次,
|
|
4874
|
+
* 服务端按 `(session_id, client_request_id)` 幂等,不会制造第二条。
|
|
4875
|
+
*/
|
|
4876
|
+
async queueMessage(content) {
|
|
4877
|
+
const text = content.trim();
|
|
4878
|
+
if (!text) {
|
|
4879
|
+
return { ok: false, conflict: false, snapshot: null, message: "\u6D88\u606F\u4E0D\u80FD\u4E3A\u7A7A" };
|
|
4880
|
+
}
|
|
4881
|
+
const payload = {
|
|
4882
|
+
session_id: this.sessionId,
|
|
4883
|
+
message: text,
|
|
4884
|
+
client_request_id: createClientRequestId()
|
|
4885
|
+
};
|
|
4886
|
+
try {
|
|
4887
|
+
return await this.requestQueueAck("chat:append", payload);
|
|
4888
|
+
} catch {
|
|
4889
|
+
try {
|
|
4890
|
+
return await this.requestQueueAck("chat:append", payload);
|
|
4891
|
+
} catch (error) {
|
|
4892
|
+
return queueRequestFailure(error);
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
/** 修改一条等待消息的正文;`expectedVersion` 是客户端看到的条目版本。 */
|
|
4897
|
+
async updateQueuedMessage(itemId, expectedVersion, content) {
|
|
4898
|
+
return this.runQueueCommand("chat:queue:update", {
|
|
4899
|
+
session_id: this.sessionId,
|
|
4900
|
+
item_id: itemId,
|
|
4901
|
+
expected_version: expectedVersion,
|
|
4902
|
+
message: content
|
|
4903
|
+
});
|
|
4904
|
+
}
|
|
4905
|
+
/**
|
|
4906
|
+
* 重排等待消息。`orderedIds` 必须是当前 pending 条目 id 的**完整集合**(顺序任意),
|
|
4907
|
+
* 服务端按 `expectedRevision` 校验后整体替换顺序。
|
|
4908
|
+
*
|
|
4909
|
+
* 拖动排序就该像本地操作一样落地:版本冲突(运行中的任务刚领走一条 / 另一个端也在
|
|
4910
|
+
* 动队列)不甩给用户「刷新后重试」,而是拿冲突响应里那份**最新快照**重算一次再提交
|
|
4911
|
+
* ——掉队的条目(已经被领取执行的)从顺序里剔除,剩下的保持用户拖出来的相对次序。
|
|
4912
|
+
* 最多补两次;仍旧冲突才把结果交回调用方。
|
|
4913
|
+
*/
|
|
4914
|
+
async reorderQueuedMessages(expectedRevision, orderedIds) {
|
|
4915
|
+
let revision = expectedRevision;
|
|
4916
|
+
let ids = orderedIds;
|
|
4917
|
+
let result = await this.runQueueCommand("chat:queue:reorder", {
|
|
4918
|
+
session_id: this.sessionId,
|
|
4919
|
+
expected_revision: revision,
|
|
4920
|
+
ordered_ids: ids
|
|
4921
|
+
});
|
|
4922
|
+
for (let attempt = 0; attempt < 2 && !result.ok && result.conflict; attempt++) {
|
|
4923
|
+
const snapshot2 = result.snapshot;
|
|
4924
|
+
if (!snapshot2) break;
|
|
4925
|
+
const pending = snapshot2.items.filter(canEditQueuedMessage);
|
|
4926
|
+
const live = new Set(pending.map((item) => item.id));
|
|
4927
|
+
const next = ids.filter((id) => live.has(id));
|
|
4928
|
+
for (const item of pending) if (!next.includes(item.id)) next.push(item.id);
|
|
4929
|
+
if (next.length === 0) {
|
|
4930
|
+
return { ok: true, conflict: false, snapshot: snapshot2, message: null };
|
|
4931
|
+
}
|
|
4932
|
+
revision = snapshot2.revision;
|
|
4933
|
+
ids = next;
|
|
4934
|
+
result = await this.runQueueCommand("chat:queue:reorder", {
|
|
4935
|
+
session_id: this.sessionId,
|
|
4936
|
+
expected_revision: revision,
|
|
4937
|
+
ordered_ids: ids
|
|
4938
|
+
});
|
|
4939
|
+
}
|
|
4940
|
+
return result;
|
|
4941
|
+
}
|
|
4942
|
+
/** 删除一条等待消息;不传版本时由服务端自行判定。 */
|
|
4943
|
+
async cancelQueuedMessage(itemId, expectedVersion) {
|
|
4944
|
+
return this.runQueueCommand("chat:queue:cancel", {
|
|
4945
|
+
session_id: this.sessionId,
|
|
4946
|
+
item_id: itemId,
|
|
4947
|
+
expected_version: expectedVersion
|
|
4948
|
+
});
|
|
4949
|
+
}
|
|
4950
|
+
/** 立即补充:把等待消息交给当前运行,在下一个接收点消费,不停止也不另起一轮。 */
|
|
4951
|
+
async deliverQueuedMessage(itemId, expectedVersion) {
|
|
4952
|
+
return this.runQueueCommand("chat:queue:deliver", {
|
|
4953
|
+
session_id: this.sessionId,
|
|
4954
|
+
item_id: itemId,
|
|
4955
|
+
expected_version: expectedVersion
|
|
4956
|
+
});
|
|
4957
|
+
}
|
|
4958
|
+
/** 继续执行:清除暂停,并按当前顺序恢复逐条接续。 */
|
|
4959
|
+
async resumeQueue() {
|
|
4960
|
+
return this.runQueueCommand("chat:queue:resume", { session_id: this.sessionId });
|
|
4961
|
+
}
|
|
4962
|
+
/** 拉取一份权威队列快照。首次连接与每次重连后由实例自动调用,也可以手动刷新。 */
|
|
4963
|
+
async refreshQueue() {
|
|
4964
|
+
return this.runQueueCommand("chat:queue:list", { session_id: this.sessionId });
|
|
4965
|
+
}
|
|
4966
|
+
/**
|
|
4967
|
+
* 发出一条队列命令并安装 ack 里的快照。
|
|
4968
|
+
* 传输层失败(超时 / 断线)会 reject,由调用方决定是否重试。
|
|
4969
|
+
*/
|
|
4970
|
+
async requestQueueAck(event, payload) {
|
|
4971
|
+
this.runtime.ensureConnected();
|
|
4972
|
+
const response = await this.runtime.emitWithAck(event, payload, QUEUE_ACK_TIMEOUT_MS);
|
|
4973
|
+
const result = parseQueueAck(response, this.sessionId);
|
|
4974
|
+
const snapshot2 = result.snapshot;
|
|
4975
|
+
if (snapshot2) this.update((state) => withQueue(state, snapshot2));
|
|
4976
|
+
return result;
|
|
4977
|
+
}
|
|
4978
|
+
/** 队列命令的对外入口:传输失败只体现为失败结果,不向调用方抛异常。 */
|
|
4979
|
+
async runQueueCommand(event, payload) {
|
|
4980
|
+
try {
|
|
4981
|
+
return await this.requestQueueAck(event, payload);
|
|
4982
|
+
} catch (error) {
|
|
4983
|
+
return queueRequestFailure(error);
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
3483
4986
|
async sendUnlocked(content, options) {
|
|
3484
4987
|
await this.ensureIdleBeforeSend();
|
|
3485
|
-
if (this.state.isStreaming || this.state.activeCompaction || this.manualCompactionPending) {
|
|
4988
|
+
if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.manualCompactionPending) {
|
|
3486
4989
|
this.update(
|
|
3487
4990
|
(s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\u6216\u5BF9\u8BDD\u6B63\u5728\u6574\u7406\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002")
|
|
3488
4991
|
);
|
|
@@ -3492,7 +4995,7 @@ var AgentSession = class _AgentSession {
|
|
|
3492
4995
|
if (this.getAppContext) {
|
|
3493
4996
|
try {
|
|
3494
4997
|
const rawContext = await this.getAppContext();
|
|
3495
|
-
if (!
|
|
4998
|
+
if (!isRecord(rawContext)) {
|
|
3496
4999
|
throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
|
|
3497
5000
|
}
|
|
3498
5001
|
const serialized = JSON.stringify(rawContext);
|
|
@@ -3500,7 +5003,7 @@ var AgentSession = class _AgentSession {
|
|
|
3500
5003
|
throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE\u53EF\u5E8F\u5217\u5316\u7684 JSON \u5BF9\u8C61");
|
|
3501
5004
|
}
|
|
3502
5005
|
const normalized = JSON.parse(serialized);
|
|
3503
|
-
if (!
|
|
5006
|
+
if (!isRecord(normalized)) {
|
|
3504
5007
|
throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
|
|
3505
5008
|
}
|
|
3506
5009
|
appContext = normalized;
|
|
@@ -3510,17 +5013,18 @@ var AgentSession = class _AgentSession {
|
|
|
3510
5013
|
return false;
|
|
3511
5014
|
}
|
|
3512
5015
|
}
|
|
5016
|
+
const wasStreaming = this.state.isStreaming;
|
|
3513
5017
|
const askUserAnswer = options.askUserAnswer;
|
|
3514
5018
|
let optimisticTurnId = null;
|
|
5019
|
+
const clientRequestId = createClientRequestId();
|
|
3515
5020
|
let optimisticAskAnswer;
|
|
3516
5021
|
let previousAskAnswer;
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
}
|
|
5022
|
+
this.update((state) => {
|
|
5023
|
+
const next = addUserMessage(state, content, clientRequestId);
|
|
5024
|
+
optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
|
|
5025
|
+
return next;
|
|
5026
|
+
});
|
|
5027
|
+
if (optimisticTurnId) this.markOptimisticTurnPending(optimisticTurnId);
|
|
3524
5028
|
this.update((s) => ({ ...s, isStreaming: true }));
|
|
3525
5029
|
if (askUserAnswer && typeof askUserAnswer.tool_call_id === "string") {
|
|
3526
5030
|
const { tool_call_id, ...rest } = askUserAnswer;
|
|
@@ -3534,57 +5038,103 @@ var AgentSession = class _AgentSession {
|
|
|
3534
5038
|
if (!options.replayDecision) {
|
|
3535
5039
|
this.pendingReplayMessage = content;
|
|
3536
5040
|
this.pendingReplayMode = options.mode;
|
|
5041
|
+
this.pendingReplayKbIds = options.kbIds;
|
|
3537
5042
|
}
|
|
3538
5043
|
const payload = {
|
|
3539
5044
|
session_id: this.sessionId,
|
|
3540
5045
|
message: content,
|
|
5046
|
+
client_request_id: clientRequestId,
|
|
3541
5047
|
mode: options.mode,
|
|
3542
5048
|
askuser_answer: askUserAnswer,
|
|
3543
5049
|
model: options.model,
|
|
3544
5050
|
thinking_override: options.thinkingOverride,
|
|
3545
5051
|
whatif: options.whatif,
|
|
3546
5052
|
replay_decision: options.replayDecision,
|
|
3547
|
-
app_context: appContext
|
|
5053
|
+
app_context: appContext,
|
|
5054
|
+
kb_ids: options.kbIds,
|
|
5055
|
+
// 运行中提交不必由客户端挑命令:服务端按会话运行状态决定直接执行还是入队。
|
|
5056
|
+
queue_when_running: options.queueWhenRunning === true
|
|
3548
5057
|
};
|
|
3549
5058
|
try {
|
|
3550
5059
|
await this.ensureJoined();
|
|
3551
5060
|
} catch (error) {
|
|
5061
|
+
const failure = describeSessionError(error, "\u4F1A\u8BDD\u8BA2\u9605\u5931\u8D25\uFF0C\u6D88\u606F\u672A\u53D1\u9001");
|
|
3552
5062
|
this.rollbackOptimisticSend(
|
|
3553
5063
|
optimisticTurnId,
|
|
3554
5064
|
askUserAnswer,
|
|
3555
5065
|
optimisticAskAnswer,
|
|
3556
5066
|
previousAskAnswer,
|
|
3557
|
-
|
|
5067
|
+
wasStreaming,
|
|
5068
|
+
failure.message,
|
|
5069
|
+
failure.code
|
|
3558
5070
|
);
|
|
3559
5071
|
return false;
|
|
3560
5072
|
}
|
|
5073
|
+
const waitStartedAt = this.systemErrorGeneration;
|
|
3561
5074
|
try {
|
|
3562
5075
|
const response = await this.runtime.emitWithAck("chat:send", payload, 3e4);
|
|
5076
|
+
if (response?.status === "queued") {
|
|
5077
|
+
this.discardOptimisticSend(
|
|
5078
|
+
optimisticTurnId,
|
|
5079
|
+
askUserAnswer,
|
|
5080
|
+
optimisticAskAnswer,
|
|
5081
|
+
previousAskAnswer
|
|
5082
|
+
);
|
|
5083
|
+
const snapshot2 = parseQueueAck(response, this.sessionId).snapshot;
|
|
5084
|
+
const running = response.running === true;
|
|
5085
|
+
this.update((current) => {
|
|
5086
|
+
const next = running ? current : { ...current, isStreaming: false };
|
|
5087
|
+
return snapshot2 ? withQueue(next, snapshot2) : next;
|
|
5088
|
+
});
|
|
5089
|
+
return true;
|
|
5090
|
+
}
|
|
3563
5091
|
if (response?.status !== "accepted") {
|
|
3564
|
-
throw new
|
|
3565
|
-
typeof response?.message === "string" ? response.message : "\u6D88\u606F\u672A\u88AB\u670D\u52A1\u7AEF\u63A5\u53D7"
|
|
5092
|
+
throw new AgentSessionError(
|
|
5093
|
+
typeof response?.message === "string" ? response.message : "\u6D88\u606F\u672A\u88AB\u670D\u52A1\u7AEF\u63A5\u53D7",
|
|
5094
|
+
typeof response?.code === "string" ? response.code : void 0
|
|
3566
5095
|
);
|
|
3567
5096
|
}
|
|
3568
5097
|
return true;
|
|
3569
5098
|
} catch (error) {
|
|
5099
|
+
const failure = describeSessionError(error, "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
|
|
3570
5100
|
this.rollbackOptimisticSend(
|
|
3571
5101
|
optimisticTurnId,
|
|
3572
5102
|
askUserAnswer,
|
|
3573
5103
|
optimisticAskAnswer,
|
|
3574
5104
|
previousAskAnswer,
|
|
3575
|
-
|
|
5105
|
+
wasStreaming,
|
|
5106
|
+
failure.message,
|
|
5107
|
+
failure.code,
|
|
5108
|
+
waitStartedAt
|
|
3576
5109
|
);
|
|
3577
5110
|
return false;
|
|
3578
5111
|
}
|
|
3579
5112
|
}
|
|
3580
|
-
rollbackOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer, message) {
|
|
5113
|
+
rollbackOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer, wasStreaming, message, code, waitStartedAt) {
|
|
5114
|
+
this.discardOptimisticSend(
|
|
5115
|
+
optimisticTurnId,
|
|
5116
|
+
askUserAnswer,
|
|
5117
|
+
optimisticAskAnswer,
|
|
5118
|
+
previousAskAnswer
|
|
5119
|
+
);
|
|
5120
|
+
this.update(
|
|
5121
|
+
(current) => addErrorMessage(wasStreaming ? current : { ...current, isStreaming: false }, message)
|
|
5122
|
+
);
|
|
5123
|
+
const delivered = this.lastDeliveredSystemError;
|
|
5124
|
+
if (waitStartedAt !== void 0 && delivered && delivered.generation > waitStartedAt && delivered.message === message && delivered.code === code) {
|
|
5125
|
+
this.lastDeliveredSystemError = null;
|
|
5126
|
+
return;
|
|
5127
|
+
}
|
|
5128
|
+
this.emitter.emit("error", { message, ...code ? { code } : {} });
|
|
5129
|
+
}
|
|
5130
|
+
/** 撤回一次乐观提交:移除乐观消息、恢复提问作答、清掉回放草稿。 */
|
|
5131
|
+
discardOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer) {
|
|
3581
5132
|
this.pendingReplayMessage = null;
|
|
3582
5133
|
this.pendingReplayMode = void 0;
|
|
5134
|
+
this.pendingReplayKbIds = void 0;
|
|
3583
5135
|
this.update((current) => {
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
current.turns.filter((turn) => turn.turn_id !== optimisticTurnId)
|
|
3587
|
-
) : current;
|
|
5136
|
+
const filtered = optimisticTurnId ? current.turns.filter((turn) => turn.turn_id !== optimisticTurnId) : current.turns;
|
|
5137
|
+
let next = optimisticTurnId ? withTurns({ ...current, turns: filtered }, filtered) : current;
|
|
3588
5138
|
const toolCallId = askUserAnswer && typeof askUserAnswer.tool_call_id === "string" ? askUserAnswer.tool_call_id : null;
|
|
3589
5139
|
if (toolCallId && next.askAnswers[toolCallId] === optimisticAskAnswer) {
|
|
3590
5140
|
const askAnswers = { ...next.askAnswers };
|
|
@@ -3595,18 +5145,20 @@ var AgentSession = class _AgentSession {
|
|
|
3595
5145
|
}
|
|
3596
5146
|
next = { ...next, askAnswers };
|
|
3597
5147
|
}
|
|
3598
|
-
return
|
|
5148
|
+
return next;
|
|
3599
5149
|
});
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
this.runtime.emitEvent("chat:append", { session_id: this.sessionId, message: text });
|
|
3605
|
-
this.update((s) => addUserMessage(s, text));
|
|
5150
|
+
if (optimisticTurnId) {
|
|
5151
|
+
this.pendingOptimisticTurnIds.delete(optimisticTurnId);
|
|
5152
|
+
this.liveRevisions.delete(optimisticTurnId);
|
|
5153
|
+
}
|
|
3606
5154
|
}
|
|
3607
5155
|
/** 停止当前回复。 */
|
|
3608
5156
|
stop() {
|
|
3609
|
-
return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) =>
|
|
5157
|
+
return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) => {
|
|
5158
|
+
const result = response ?? {};
|
|
5159
|
+
if (result.status !== "accepted") this.manualCompactionPending = false;
|
|
5160
|
+
return result;
|
|
5161
|
+
});
|
|
3610
5162
|
}
|
|
3611
5163
|
/** 手动触发上下文压缩。 */
|
|
3612
5164
|
compact() {
|
|
@@ -3626,8 +5178,10 @@ var AgentSession = class _AgentSession {
|
|
|
3626
5178
|
trigger: "manual"
|
|
3627
5179
|
}
|
|
3628
5180
|
}));
|
|
3629
|
-
return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {}).
|
|
5181
|
+
return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {}).catch((error) => {
|
|
3630
5182
|
this.manualCompactionPending = false;
|
|
5183
|
+
throw error;
|
|
5184
|
+
}).finally(() => {
|
|
3631
5185
|
if (this.state.activeCompaction?.compaction_id === "pending-manual-compaction") {
|
|
3632
5186
|
this.update((state) => ({ ...state, activeCompaction: null }));
|
|
3633
5187
|
}
|
|
@@ -3666,10 +5220,20 @@ var AgentSession = class _AgentSession {
|
|
|
3666
5220
|
if (this.disposed) return;
|
|
3667
5221
|
if (!this.runtime.onDispose(this)) return;
|
|
3668
5222
|
this.disposed = true;
|
|
5223
|
+
this.invalidateTimeline();
|
|
5224
|
+
this.invalidateOlderPaging();
|
|
5225
|
+
this.state = { ...this.state, nextBefore: null, loadingOlder: false };
|
|
3669
5226
|
this.cancelPatchFlush();
|
|
3670
5227
|
this.listeners.clear();
|
|
3671
5228
|
this.emitter.clear();
|
|
3672
5229
|
this.commands.clear();
|
|
5230
|
+
this.commandRecoveryGeneration += 1;
|
|
5231
|
+
this.commandRestoreRetryAttempt = 0;
|
|
5232
|
+
this.bufferedCommandDeliveries = null;
|
|
5233
|
+
if (this.commandRestoreRetryHandle !== null) {
|
|
5234
|
+
globalThis.clearTimeout(this.commandRestoreRetryHandle);
|
|
5235
|
+
this.commandRestoreRetryHandle = null;
|
|
5236
|
+
}
|
|
3673
5237
|
}
|
|
3674
5238
|
get isDisposed() {
|
|
3675
5239
|
return this.disposed;
|
|
@@ -3680,28 +5244,64 @@ var AgentSession = class _AgentSession {
|
|
|
3680
5244
|
if (this.state.connection === connection) return;
|
|
3681
5245
|
this.update((s) => ({ ...s, connection }));
|
|
3682
5246
|
}
|
|
5247
|
+
/**
|
|
5248
|
+
* @internal 服务端广播的权威队列快照。
|
|
5249
|
+
* 结构不合法或 revision 不新的一律丢弃,绝不用旧快照覆盖新状态。
|
|
5250
|
+
*/
|
|
5251
|
+
_handleQueueUpdated(payload) {
|
|
5252
|
+
if (!isSessionQueueSnapshot(payload)) return;
|
|
5253
|
+
const snapshot2 = payload;
|
|
5254
|
+
this.update((state) => withQueue(state, snapshot2));
|
|
5255
|
+
}
|
|
3683
5256
|
/** @internal socket 重连后由 Hub 调用:重新加入房间并补数。 */
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
this.
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
5257
|
+
_rejoinAfterReconnect() {
|
|
5258
|
+
this.invalidateOlderPaging();
|
|
5259
|
+
this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
|
|
5260
|
+
const reconnectLiveRevision = this.liveRevision;
|
|
5261
|
+
const reconnectModeRevision = this.modeRevision;
|
|
5262
|
+
const commandRecoveryGeneration = this.beginCommandRecovery();
|
|
5263
|
+
return this.enqueueSubscription(async () => {
|
|
5264
|
+
this.joined = false;
|
|
5265
|
+
this.joinPending = null;
|
|
5266
|
+
try {
|
|
5267
|
+
this.subscriptionReplaySource = "reconnect_replay";
|
|
5268
|
+
try {
|
|
5269
|
+
await this.ensureJoined(reconnectLiveRevision);
|
|
5270
|
+
} finally {
|
|
5271
|
+
this.subscriptionReplaySource = null;
|
|
5272
|
+
this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
|
|
5273
|
+
}
|
|
5274
|
+
await Promise.all([
|
|
5275
|
+
this.refreshAuthoritativeHistory({
|
|
5276
|
+
liveRevisionAtStart: reconnectLiveRevision,
|
|
5277
|
+
modeRevisionAtStart: reconnectModeRevision
|
|
5278
|
+
}),
|
|
5279
|
+
this.syncReplayFromSession()
|
|
5280
|
+
]);
|
|
5281
|
+
} catch {
|
|
5282
|
+
}
|
|
5283
|
+
});
|
|
3699
5284
|
}
|
|
3700
5285
|
/** @internal 首次连接:加载历史 + 加入房间。 */
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
5286
|
+
_bootstrap() {
|
|
5287
|
+
const commandRecoveryGeneration = this.beginCommandRecovery();
|
|
5288
|
+
return this.enqueueSubscription(async () => {
|
|
5289
|
+
await this.replaceFromHistory({ suppressErrors: false });
|
|
5290
|
+
this.subscriptionReplaySource = "bootstrap_replay";
|
|
5291
|
+
try {
|
|
5292
|
+
await this.ensureJoined();
|
|
5293
|
+
} finally {
|
|
5294
|
+
this.subscriptionReplaySource = null;
|
|
5295
|
+
}
|
|
5296
|
+
this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
|
|
5297
|
+
await Promise.all([this.syncReplayFromSession(), this.refreshQueue()]);
|
|
5298
|
+
});
|
|
5299
|
+
}
|
|
5300
|
+
/** 订阅生命周期串行执行;一次失败只由自己的调用方观察,不截断后续重连。 */
|
|
5301
|
+
enqueueSubscription(operation) {
|
|
5302
|
+
const run = this.subscriptionQueue.then(operation);
|
|
5303
|
+
this.subscriptionQueue = run.catch(() => void 0);
|
|
5304
|
+
return run;
|
|
3705
5305
|
}
|
|
3706
5306
|
/** 改回放是 owner 专属(服务端 PATCH 走 _get_owned_session)。 */
|
|
3707
5307
|
get canControlReplay() {
|
|
@@ -3758,10 +5358,16 @@ var AgentSession = class _AgentSession {
|
|
|
3758
5358
|
this.applyTurn(turn);
|
|
3759
5359
|
}
|
|
3760
5360
|
/** @internal 历史回放通道(event_format: raw)。 */
|
|
3761
|
-
_handleTurnEvents(events) {
|
|
5361
|
+
_handleTurnEvents(events, replayed = false) {
|
|
3762
5362
|
if (!events.length) return;
|
|
5363
|
+
if (events.some(
|
|
5364
|
+
(event) => event.type === "compaction:end" || event.type === "compaction:error" || event.type === "compaction:abort"
|
|
5365
|
+
)) {
|
|
5366
|
+
this.manualCompactionPending = false;
|
|
5367
|
+
}
|
|
5368
|
+
this.dispatchRawCommands(events);
|
|
3763
5369
|
const terminalChatEndStatus = extractTerminalChatEndStatus(events);
|
|
3764
|
-
if (!terminalChatEndStatus &&
|
|
5370
|
+
if (!terminalChatEndStatus && hasChatRunEvent(events) && this.state.status !== "waiting_for_input") {
|
|
3765
5371
|
this.markRunningIfIdle();
|
|
3766
5372
|
}
|
|
3767
5373
|
if (this.state.turns.length) {
|
|
@@ -3770,7 +5376,10 @@ var AgentSession = class _AgentSession {
|
|
|
3770
5376
|
const updates = this.projectionBuilder.processBatch(events);
|
|
3771
5377
|
for (const update of updates) {
|
|
3772
5378
|
if (update.kind === "upsert") {
|
|
3773
|
-
this.applyTurn(
|
|
5379
|
+
this.applyTurn(
|
|
5380
|
+
update.turn,
|
|
5381
|
+
replayed ? this.subscriptionReplaySource ?? "bootstrap_replay" : "live"
|
|
5382
|
+
);
|
|
3774
5383
|
} else if (update.kind === "workspace_changed") {
|
|
3775
5384
|
this.emitter.emit("workspaceChanged", {});
|
|
3776
5385
|
}
|
|
@@ -3781,7 +5390,12 @@ var AgentSession = class _AgentSession {
|
|
|
3781
5390
|
}
|
|
3782
5391
|
/** @internal */
|
|
3783
5392
|
_handleChatStart() {
|
|
3784
|
-
this.update((s) => ({
|
|
5393
|
+
this.update((s) => ({
|
|
5394
|
+
...s,
|
|
5395
|
+
status: "running",
|
|
5396
|
+
errorMessage: null,
|
|
5397
|
+
isStreaming: true
|
|
5398
|
+
}));
|
|
3785
5399
|
}
|
|
3786
5400
|
/** @internal */
|
|
3787
5401
|
_handleChatEnd(status) {
|
|
@@ -3789,14 +5403,31 @@ var AgentSession = class _AgentSession {
|
|
|
3789
5403
|
this.completeChat(status);
|
|
3790
5404
|
}
|
|
3791
5405
|
/** @internal */
|
|
3792
|
-
_handleSystemError(message) {
|
|
5406
|
+
_handleSystemError(message, code) {
|
|
3793
5407
|
this.flushPatches();
|
|
3794
5408
|
const finalMessage = isOomText(message) ? OOM_MESSAGE : message;
|
|
3795
5409
|
this.update(
|
|
3796
|
-
(s) => addErrorMessage(
|
|
5410
|
+
(s) => addErrorMessage(
|
|
5411
|
+
markFailed({
|
|
5412
|
+
...s,
|
|
5413
|
+
status: "failed",
|
|
5414
|
+
errorMessage: finalMessage,
|
|
5415
|
+
isStreaming: false
|
|
5416
|
+
}),
|
|
5417
|
+
finalMessage
|
|
5418
|
+
)
|
|
3797
5419
|
);
|
|
3798
5420
|
this.recentChatEndAt = Date.now();
|
|
3799
|
-
this.
|
|
5421
|
+
this.systemErrorGeneration += 1;
|
|
5422
|
+
this.lastDeliveredSystemError = {
|
|
5423
|
+
message: finalMessage,
|
|
5424
|
+
...code ? { code } : {},
|
|
5425
|
+
generation: this.systemErrorGeneration
|
|
5426
|
+
};
|
|
5427
|
+
this.emitter.emit("error", {
|
|
5428
|
+
message: finalMessage,
|
|
5429
|
+
...code ? { code } : {}
|
|
5430
|
+
});
|
|
3800
5431
|
}
|
|
3801
5432
|
/** @internal */
|
|
3802
5433
|
_handleTaskUpdated(tasks) {
|
|
@@ -3816,6 +5447,8 @@ var AgentSession = class _AgentSession {
|
|
|
3816
5447
|
this.update((s) => ({ ...s, replay }));
|
|
3817
5448
|
}
|
|
3818
5449
|
this.emitter.emit("sessionUpdated", {
|
|
5450
|
+
workspacePath: data.workspace_path,
|
|
5451
|
+
chatGroupId: data.chat_group_id,
|
|
3819
5452
|
intent: data.intent,
|
|
3820
5453
|
status: data.status,
|
|
3821
5454
|
model: data.model,
|
|
@@ -3825,16 +5458,26 @@ var AgentSession = class _AgentSession {
|
|
|
3825
5458
|
}
|
|
3826
5459
|
/** @internal */
|
|
3827
5460
|
_handleRewind(checkpointId) {
|
|
3828
|
-
this.
|
|
3829
|
-
|
|
5461
|
+
this.invalidateTimeline();
|
|
5462
|
+
this.invalidateOlderPaging();
|
|
5463
|
+
this.update((s) => ({
|
|
5464
|
+
...s,
|
|
5465
|
+
mode: null,
|
|
5466
|
+
errorMessage: null,
|
|
5467
|
+
nextBefore: null,
|
|
5468
|
+
loadingOlder: false
|
|
5469
|
+
}));
|
|
5470
|
+
void this.replaceFromHistory();
|
|
3830
5471
|
this.emitter.emit("rewind", { checkpointId });
|
|
3831
5472
|
}
|
|
3832
5473
|
/** @internal */
|
|
3833
5474
|
_handleReplayMismatch(data) {
|
|
3834
5475
|
const message = this.pendingReplayMessage ?? data.actual_message ?? "";
|
|
3835
5476
|
const mode = this.pendingReplayMode;
|
|
5477
|
+
const kbIds = this.pendingReplayKbIds;
|
|
3836
5478
|
this.pendingReplayMessage = null;
|
|
3837
5479
|
this.pendingReplayMode = void 0;
|
|
5480
|
+
this.pendingReplayKbIds = void 0;
|
|
3838
5481
|
const respond = (decision) => {
|
|
3839
5482
|
void this.runtime.emitWithAck(
|
|
3840
5483
|
"chat:send",
|
|
@@ -3842,7 +5485,8 @@ var AgentSession = class _AgentSession {
|
|
|
3842
5485
|
session_id: this.sessionId,
|
|
3843
5486
|
message,
|
|
3844
5487
|
mode,
|
|
3845
|
-
replay_decision: decision
|
|
5488
|
+
replay_decision: decision,
|
|
5489
|
+
kb_ids: kbIds
|
|
3846
5490
|
},
|
|
3847
5491
|
3e4
|
|
3848
5492
|
).then((response) => {
|
|
@@ -3883,9 +5527,9 @@ var AgentSession = class _AgentSession {
|
|
|
3883
5527
|
detail,
|
|
3884
5528
|
status,
|
|
3885
5529
|
loopId,
|
|
3886
|
-
metadata:
|
|
5530
|
+
metadata: isRecord(notification.metadata) ? notification.metadata : void 0
|
|
3887
5531
|
});
|
|
3888
|
-
if (notificationType === "bg:started" &&
|
|
5532
|
+
if (notificationType === "bg:started" && isRecord(notification.metadata)) {
|
|
3889
5533
|
const taskId = typeof notification.metadata.task_id === "string" ? notification.metadata.task_id : "";
|
|
3890
5534
|
if (taskId) {
|
|
3891
5535
|
this.emitter.emit("backgroundTask", {
|
|
@@ -3907,12 +5551,12 @@ var AgentSession = class _AgentSession {
|
|
|
3907
5551
|
this.emitter.emit("artifact", event);
|
|
3908
5552
|
}
|
|
3909
5553
|
/** @internal 加入房间的应答里带 is_running,据此校准流式状态。 */
|
|
3910
|
-
_applyRunningState(isRunning) {
|
|
5554
|
+
_applyRunningState(isRunning, liveRevisionAtJoin = this.liveRevision) {
|
|
3911
5555
|
if (isRunning) {
|
|
3912
5556
|
this.update((s) => ({ ...s, status: "running", isStreaming: true }));
|
|
3913
5557
|
} else if (this.state.isStreaming) {
|
|
3914
5558
|
this.update((s) => ({ ...s, isStreaming: false }));
|
|
3915
|
-
void this.
|
|
5559
|
+
void this.refreshAuthoritativeHistory({ liveRevisionAtStart: liveRevisionAtJoin });
|
|
3916
5560
|
}
|
|
3917
5561
|
}
|
|
3918
5562
|
// ---------------------------------------------------------------- 内部实现
|
|
@@ -3933,14 +5577,36 @@ var AgentSession = class _AgentSession {
|
|
|
3933
5577
|
completeChat(status) {
|
|
3934
5578
|
this.finishStreaming(status);
|
|
3935
5579
|
this.recentChatEndAt = Date.now();
|
|
3936
|
-
void this.
|
|
5580
|
+
void this.refreshAuthoritativeHistory();
|
|
3937
5581
|
this.emitter.emit("chatEnd", { status });
|
|
3938
5582
|
}
|
|
3939
|
-
|
|
5583
|
+
markTurnFresh(turnId) {
|
|
5584
|
+
this.liveRevision += 1;
|
|
5585
|
+
this.liveRevisions.set(turnId, this.liveRevision);
|
|
5586
|
+
}
|
|
5587
|
+
markOptimisticTurnPending(turnId) {
|
|
5588
|
+
this.pendingOptimisticTurnIds.add(turnId);
|
|
5589
|
+
this.markTurnFresh(turnId);
|
|
5590
|
+
}
|
|
5591
|
+
pruneOptimisticLiveRevisions() {
|
|
5592
|
+
const retained = new Set(
|
|
5593
|
+
this.state.turns.map((turn) => turn.turn_id).filter((turnId) => turnId.startsWith("local-user-"))
|
|
5594
|
+
);
|
|
5595
|
+
for (const turnId of this.liveRevisions.keys()) {
|
|
5596
|
+
if (turnId.startsWith("local-user-") && !retained.has(turnId)) {
|
|
5597
|
+
this.pendingOptimisticTurnIds.delete(turnId);
|
|
5598
|
+
this.liveRevisions.delete(turnId);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
}
|
|
5602
|
+
applyTurn(turn, source = "live") {
|
|
3940
5603
|
this.flushPatches();
|
|
5604
|
+
this.markTurnFresh(turn.turn_id);
|
|
5605
|
+
if (extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
|
|
3941
5606
|
const previous = this.state;
|
|
3942
5607
|
this.update((s) => upsertTurn(s, turn));
|
|
3943
|
-
this.
|
|
5608
|
+
this.pruneOptimisticLiveRevisions();
|
|
5609
|
+
this.emitTurnDerivedEvents(previous, turn, source);
|
|
3944
5610
|
}
|
|
3945
5611
|
schedulePatchFlush() {
|
|
3946
5612
|
if (this.patchFlushHandle != null) return;
|
|
@@ -3973,11 +5639,15 @@ var AgentSession = class _AgentSession {
|
|
|
3973
5639
|
this.pendingTurnPatches.clear();
|
|
3974
5640
|
for (const patch of patches) {
|
|
3975
5641
|
const previous = this.state;
|
|
3976
|
-
|
|
5642
|
+
const next = applyTurnPatch(previous, patch);
|
|
5643
|
+
if (next === previous) continue;
|
|
5644
|
+
this.liveRevision += 1;
|
|
5645
|
+
this.liveRevisions.set(patch.turn_id, this.liveRevision);
|
|
3977
5646
|
const turn = patch.data.turn;
|
|
3978
|
-
if (turn)
|
|
3979
|
-
|
|
3980
|
-
|
|
5647
|
+
if (turn && extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
|
|
5648
|
+
this.setState(next);
|
|
5649
|
+
this.pruneOptimisticLiveRevisions();
|
|
5650
|
+
if (turn) this.emitTurnDerivedEvents(previous, turn);
|
|
3981
5651
|
}
|
|
3982
5652
|
if (this.patchFlushCancel) {
|
|
3983
5653
|
this.patchFlushCancel();
|
|
@@ -3986,7 +5656,7 @@ var AgentSession = class _AgentSession {
|
|
|
3986
5656
|
}
|
|
3987
5657
|
}
|
|
3988
5658
|
/** turn 数据落地后统一派生语义事件(指令 / 工具调用 / 消息 / 模式 / 预览 / OOM)。 */
|
|
3989
|
-
emitTurnDerivedEvents(previous, turn) {
|
|
5659
|
+
emitTurnDerivedEvents(previous, turn, source = "live") {
|
|
3990
5660
|
const mode = extractModeFromBlocks(turn.blocks);
|
|
3991
5661
|
if (mode && previous.mode !== mode) {
|
|
3992
5662
|
this.emitter.emit("modeChange", { mode });
|
|
@@ -3999,23 +5669,29 @@ var AgentSession = class _AgentSession {
|
|
|
3999
5669
|
this.dispatchCommand(turn, block);
|
|
4000
5670
|
continue;
|
|
4001
5671
|
}
|
|
4002
|
-
if (block.type === "tool_ui" && block.tool_call_id &&
|
|
5672
|
+
if (block.type === "tool_ui" && block.tool_call_id && isToolUiCard(block.content)) {
|
|
4003
5673
|
const ui = block.content;
|
|
4004
5674
|
if (ui.target !== "preview") continue;
|
|
4005
|
-
const
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
const
|
|
4009
|
-
|
|
4010
|
-
|
|
5675
|
+
const resolved = resolveToolUiCardContent(ui);
|
|
5676
|
+
if (!resolved) continue;
|
|
5677
|
+
const key = buildToolUiCardKey(block.tool_call_id, resolved.type, resolved.content, ui.sourcePath);
|
|
5678
|
+
const fingerprint = JSON.stringify([
|
|
5679
|
+
resolved.content,
|
|
5680
|
+
ui.mcpApp?.sourceId,
|
|
5681
|
+
resolved.type === "resource-file" ? block.tool_call_id : void 0
|
|
5682
|
+
]);
|
|
5683
|
+
if (this.deliveredToolPreviews.get(key) === fingerprint) continue;
|
|
5684
|
+
this.deliveredToolPreviews.set(key, fingerprint);
|
|
4011
5685
|
this.emitter.emit("toolPreview", {
|
|
4012
5686
|
toolCallId: block.tool_call_id,
|
|
4013
|
-
type:
|
|
4014
|
-
content:
|
|
4015
|
-
title: ui.title ?? "\u5DE5\u5177\u9884\u89C8"
|
|
5687
|
+
type: resolved.type,
|
|
5688
|
+
content: resolved.content,
|
|
5689
|
+
title: ui.title ?? "\u5DE5\u5177\u9884\u89C8",
|
|
5690
|
+
...ui.sourcePath ? { sourcePath: ui.sourcePath } : {},
|
|
5691
|
+
...ui.mcpApp ? { mcpApp: ui.mcpApp } : {}
|
|
4016
5692
|
});
|
|
4017
5693
|
}
|
|
4018
|
-
if (block.type === "system_notification" &&
|
|
5694
|
+
if (block.type === "system_notification" && isRecord(block.content)) {
|
|
4019
5695
|
this._handleSystemNotification(turn.loop_id, block.content);
|
|
4020
5696
|
}
|
|
4021
5697
|
}
|
|
@@ -4035,7 +5711,7 @@ var AgentSession = class _AgentSession {
|
|
|
4035
5711
|
const finished = toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled";
|
|
4036
5712
|
if (finished && !this.finishedToolCallIds.has(toolCall.id)) {
|
|
4037
5713
|
this.finishedToolCallIds.add(toolCall.id);
|
|
4038
|
-
this.emitter.emit("toolResult", { toolCall: info, turn });
|
|
5714
|
+
this.emitter.emit("toolResult", { toolCall: info, turn, source });
|
|
4039
5715
|
if (isOomResult(toolCall.result)) {
|
|
4040
5716
|
this.notifySandboxOom(toolCall.id);
|
|
4041
5717
|
}
|
|
@@ -4052,17 +5728,36 @@ var AgentSession = class _AgentSession {
|
|
|
4052
5728
|
dispatchCommand(turn, block) {
|
|
4053
5729
|
if (!isToolBridgeContent(block.content)) return;
|
|
4054
5730
|
const { action, payload } = block.content;
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
5731
|
+
const deliveryKey = block.delivery_id ? commandDeliveryKey(block.delivery_id) : `legacy-tool-command:${turn.turn_id}:${block.tool_call_id ?? ""}:${action}:${JSON.stringify(payload)}`;
|
|
5732
|
+
this.deliverCommand(deliveryKey, action, payload, block.tool_call_id ?? void 0);
|
|
5733
|
+
}
|
|
5734
|
+
dispatchRawCommands(events) {
|
|
5735
|
+
for (const event of events) {
|
|
5736
|
+
if (event.type !== "loop:tool_bridge") continue;
|
|
5737
|
+
const deliveryId = String(event.payload.delivery_id ?? "");
|
|
5738
|
+
const bridge = event.payload.bridge;
|
|
5739
|
+
if (!deliveryId || !isToolBridgeContent(bridge)) continue;
|
|
5740
|
+
this.deliverCommand(
|
|
5741
|
+
commandDeliveryKey(deliveryId),
|
|
5742
|
+
bridge.action,
|
|
5743
|
+
bridge.payload,
|
|
5744
|
+
String(event.payload.tool_call_id ?? "") || void 0
|
|
5745
|
+
);
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
deliverCommand(deliveryKey, action, payload, toolCallId) {
|
|
5749
|
+
if (this.bufferedCommandDeliveries) {
|
|
5750
|
+
this.bufferedCommandDeliveries.push({ deliveryKey, action, payload, toolCallId });
|
|
5751
|
+
return;
|
|
4060
5752
|
}
|
|
4061
|
-
|
|
5753
|
+
this.deliverCommandNow({ deliveryKey, action, payload, toolCallId });
|
|
5754
|
+
}
|
|
5755
|
+
deliverCommandNow(command) {
|
|
5756
|
+
const { deliveryKey, action, payload, toolCallId } = command;
|
|
4062
5757
|
if (this.deliveredCommandKeys.has(deliveryKey)) return;
|
|
4063
5758
|
this.deliveredCommandKeys.add(deliveryKey);
|
|
4064
|
-
const meta = { toolCallId
|
|
4065
|
-
this.emitter.emit("command", { action, payload, toolCallId
|
|
5759
|
+
const meta = { toolCallId };
|
|
5760
|
+
this.emitter.emit("command", { action, payload, toolCallId });
|
|
4066
5761
|
this.commands.dispatch(action, payload, meta);
|
|
4067
5762
|
}
|
|
4068
5763
|
notifySandboxOom(key) {
|
|
@@ -4071,49 +5766,195 @@ var AgentSession = class _AgentSession {
|
|
|
4071
5766
|
this.update((s) => addErrorMessage(s, OOM_MESSAGE));
|
|
4072
5767
|
this.emitter.emit("sandboxOom", { toolCallId: key });
|
|
4073
5768
|
}
|
|
4074
|
-
async syncTurnsFromHistory(options = {}) {
|
|
4075
|
-
const { clearOnEmpty = false, suppressErrors = true, preserveChangesSince } = options;
|
|
4076
|
-
const turnsAtRequestStart = this.state.turns;
|
|
4077
|
-
try {
|
|
4078
|
-
const turns = await this.runtime.fetchTurns(this.sessionId);
|
|
4079
|
-
if (turns.length === 0 && !clearOnEmpty) return;
|
|
4080
|
-
this.update((state) => {
|
|
4081
|
-
const mergeBaseline = preserveChangesSince ?? turnsAtRequestStart;
|
|
4082
|
-
if (state.turns === mergeBaseline) return withTurns(state, turns);
|
|
4083
|
-
const historyById = new Map(turns.map((turn) => [turn.turn_id, turn]));
|
|
4084
|
-
let merged = withTurns(state, turns);
|
|
4085
|
-
for (const current of state.turns) {
|
|
4086
|
-
const fromHistory = historyById.get(current.turn_id);
|
|
4087
|
-
if (!fromHistory || current.sequence > fromHistory.sequence) {
|
|
4088
|
-
merged = upsertTurn(merged, current);
|
|
4089
|
-
}
|
|
4090
|
-
}
|
|
4091
|
-
return merged;
|
|
4092
|
-
});
|
|
4093
|
-
this.replayCommandsFromTurns(turns);
|
|
4094
|
-
} catch (error) {
|
|
4095
|
-
if (!suppressErrors) throw error;
|
|
4096
|
-
}
|
|
4097
|
-
}
|
|
4098
5769
|
/**
|
|
4099
|
-
*
|
|
5770
|
+
* 权威刷新(运行结束 / 重连补数):**按当前窗口第一条发言取窗口**,不再另记"用户想看哪段"。
|
|
4100
5771
|
*
|
|
4101
|
-
*
|
|
4102
|
-
*
|
|
4103
|
-
*
|
|
4104
|
-
*
|
|
4105
|
-
* dispatch 内部按 deliveredCommandKeys 去重,重复调用安全。
|
|
5772
|
+
* 窗口本身就是"用户在看哪段":首屏就是最新一页,向上翻页 / 定位之后首条就是用户滚到的位置。
|
|
5773
|
+
* 直接拿 `from_entry_id = 当前窗口第一条 turn_id` 去取,运行结束、重连补数、翻页之后的对账
|
|
5774
|
+
* 是同一条规则,不需要意图、锚点、已落地锚点这些状态;窗口首条已经不在分支上(404 / 409)时
|
|
5775
|
+
* 回落到最新一页。
|
|
4106
5776
|
*/
|
|
4107
|
-
|
|
5777
|
+
refreshAuthoritativeHistory(options = {}) {
|
|
5778
|
+
let entryGone = false;
|
|
5779
|
+
const locateTokenAtStart = this.locateToken;
|
|
5780
|
+
return this.replaceFromHistory({
|
|
5781
|
+
...options,
|
|
5782
|
+
// 锚点在**这次操作真正开始**时采样:排队期间用户可能刚定位过,那时窗口首条才是他要的。
|
|
5783
|
+
fromEntryId: () => this.state.turns[0]?.turn_id,
|
|
5784
|
+
onError: (error) => {
|
|
5785
|
+
entryGone = isEntryGoneError(error);
|
|
5786
|
+
}
|
|
5787
|
+
}).then((located) => {
|
|
5788
|
+
if (located || !entryGone) return located;
|
|
5789
|
+
if (this.locateToken !== locateTokenAtStart) return false;
|
|
5790
|
+
return this.replaceFromHistory(options);
|
|
5791
|
+
});
|
|
5792
|
+
}
|
|
5793
|
+
replaceFromHistory(options = {}) {
|
|
5794
|
+
if (this.disposed) return Promise.resolve(false);
|
|
5795
|
+
const operationEpoch = this.timelineEpoch;
|
|
5796
|
+
const liveRevisionAtStart = options.liveRevisionAtStart ?? this.liveRevision;
|
|
5797
|
+
const modeRevisionAtStart = options.modeRevisionAtStart ?? this.modeRevision;
|
|
5798
|
+
this.invalidateOlderPaging();
|
|
5799
|
+
this.replaceInFlight += 1;
|
|
5800
|
+
const operation = async () => {
|
|
5801
|
+
let previousNextBefore = null;
|
|
5802
|
+
const restoreCursor = () => {
|
|
5803
|
+
if (this.disposed || previousNextBefore === null) return;
|
|
5804
|
+
this.update((state) => ({
|
|
5805
|
+
...state,
|
|
5806
|
+
nextBefore: previousNextBefore,
|
|
5807
|
+
loadingOlder: false
|
|
5808
|
+
}));
|
|
5809
|
+
};
|
|
5810
|
+
try {
|
|
5811
|
+
if (this.disposed || operationEpoch !== this.timelineEpoch) return false;
|
|
5812
|
+
if (options.isStale?.()) return false;
|
|
5813
|
+
this.invalidateOlderPaging();
|
|
5814
|
+
previousNextBefore = this.state.nextBefore;
|
|
5815
|
+
this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
|
|
5816
|
+
const signal = options.signal;
|
|
5817
|
+
const fromEntryId = typeof options.fromEntryId === "function" ? options.fromEntryId() : options.fromEntryId;
|
|
5818
|
+
const page = await this.runtime.fetchTurnsPage(
|
|
5819
|
+
this.sessionId,
|
|
5820
|
+
fromEntryId ? { fromEntryId } : void 0,
|
|
5821
|
+
signal ? { signal } : void 0
|
|
5822
|
+
);
|
|
5823
|
+
if (this.disposed || operationEpoch !== this.timelineEpoch) {
|
|
5824
|
+
restoreCursor();
|
|
5825
|
+
return false;
|
|
5826
|
+
}
|
|
5827
|
+
if (options.isStale?.()) {
|
|
5828
|
+
restoreCursor();
|
|
5829
|
+
return false;
|
|
5830
|
+
}
|
|
5831
|
+
if (!this.joined) this.seedToolCallDeliveryBaseline(page.turns);
|
|
5832
|
+
this.update((state) => {
|
|
5833
|
+
const next = withTurns(
|
|
5834
|
+
state,
|
|
5835
|
+
replaceWindow(state.turns, page.turns, liveRevisionAtStart, {
|
|
5836
|
+
current: this.liveRevision,
|
|
5837
|
+
byTurnId: this.liveRevisions
|
|
5838
|
+
})
|
|
5839
|
+
);
|
|
5840
|
+
return {
|
|
5841
|
+
...next,
|
|
5842
|
+
mode: this.modeRevision === modeRevisionAtStart ? page.currentMode : state.mode,
|
|
5843
|
+
nextBefore: page.nextBefore,
|
|
5844
|
+
loadingOlder: false
|
|
5845
|
+
};
|
|
5846
|
+
});
|
|
5847
|
+
this.pruneOptimisticLiveRevisions();
|
|
5848
|
+
this.projectionBuilder._seedTurns(this.state.turns);
|
|
5849
|
+
return true;
|
|
5850
|
+
} catch (error) {
|
|
5851
|
+
options.onError?.(error);
|
|
5852
|
+
if (!this.disposed && operationEpoch === this.timelineEpoch) {
|
|
5853
|
+
this.update((state) => ({
|
|
5854
|
+
...state,
|
|
5855
|
+
nextBefore: previousNextBefore,
|
|
5856
|
+
loadingOlder: false
|
|
5857
|
+
}));
|
|
5858
|
+
}
|
|
5859
|
+
if (options.suppressErrors === false) throw error;
|
|
5860
|
+
return false;
|
|
5861
|
+
} finally {
|
|
5862
|
+
this.replaceInFlight -= 1;
|
|
5863
|
+
}
|
|
5864
|
+
};
|
|
5865
|
+
const run = this.replaceQueue.then(operation);
|
|
5866
|
+
this.replaceQueue = run.catch(() => void 0);
|
|
5867
|
+
return run;
|
|
5868
|
+
}
|
|
5869
|
+
invalidateOlderPaging() {
|
|
5870
|
+
this.pagingGeneration += 1;
|
|
5871
|
+
this.olderPending = null;
|
|
5872
|
+
}
|
|
5873
|
+
beginCommandRecovery() {
|
|
5874
|
+
this.commandRecoveryGeneration += 1;
|
|
5875
|
+
this.commandRestoreRetryAttempt = 0;
|
|
5876
|
+
this.bufferedCommandDeliveries ??= [];
|
|
5877
|
+
if (this.commandRestoreRetryHandle !== null) {
|
|
5878
|
+
globalThis.clearTimeout(this.commandRestoreRetryHandle);
|
|
5879
|
+
this.commandRestoreRetryHandle = null;
|
|
5880
|
+
}
|
|
5881
|
+
return this.commandRecoveryGeneration;
|
|
5882
|
+
}
|
|
5883
|
+
scheduleHistoricalCommandRestore(generation) {
|
|
5884
|
+
const restore = async () => {
|
|
5885
|
+
if (this.disposed || generation !== this.commandRecoveryGeneration) return;
|
|
5886
|
+
try {
|
|
5887
|
+
const commands = await this.runtime.fetchCommands(this.sessionId);
|
|
5888
|
+
if (this.disposed) return;
|
|
5889
|
+
for (const command of commands) {
|
|
5890
|
+
if (this.disposed) return;
|
|
5891
|
+
this.deliverCommandNow({
|
|
5892
|
+
deliveryKey: commandDeliveryKey(command.deliveryId),
|
|
5893
|
+
action: command.action,
|
|
5894
|
+
payload: command.payload,
|
|
5895
|
+
toolCallId: command.toolCallId
|
|
5896
|
+
});
|
|
5897
|
+
}
|
|
5898
|
+
if (generation !== this.commandRecoveryGeneration) return;
|
|
5899
|
+
this.commandRestoreRetryAttempt = 0;
|
|
5900
|
+
this.flushBufferedCommands(generation);
|
|
5901
|
+
} catch {
|
|
5902
|
+
if (!this.disposed && generation === this.commandRecoveryGeneration && this.commandRestoreRetryHandle === null) {
|
|
5903
|
+
const retryDelay = Math.min(
|
|
5904
|
+
COMMAND_RESTORE_RETRY_BASE_MS * 2 ** this.commandRestoreRetryAttempt,
|
|
5905
|
+
COMMAND_RESTORE_RETRY_MAX_MS
|
|
5906
|
+
);
|
|
5907
|
+
this.commandRestoreRetryAttempt += 1;
|
|
5908
|
+
this.commandRestoreRetryHandle = globalThis.setTimeout(() => {
|
|
5909
|
+
this.commandRestoreRetryHandle = null;
|
|
5910
|
+
if (!this.disposed && generation === this.commandRecoveryGeneration) {
|
|
5911
|
+
this.scheduleHistoricalCommandRestore(generation);
|
|
5912
|
+
}
|
|
5913
|
+
}, retryDelay);
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
};
|
|
5917
|
+
const run = this.commandRestoreQueue.then(restore);
|
|
5918
|
+
this.commandRestoreQueue = run.catch(() => void 0);
|
|
5919
|
+
}
|
|
5920
|
+
flushBufferedCommands(generation) {
|
|
5921
|
+
const pending = [...this.bufferedCommandDeliveries ?? []];
|
|
5922
|
+
this.bufferedCommandDeliveries = [];
|
|
5923
|
+
let index = 0;
|
|
5924
|
+
while (index < pending.length) {
|
|
5925
|
+
if (this.disposed) return;
|
|
5926
|
+
if (generation !== this.commandRecoveryGeneration) {
|
|
5927
|
+
const currentGenerationBuffer = this.bufferedCommandDeliveries ?? [];
|
|
5928
|
+
this.bufferedCommandDeliveries = [
|
|
5929
|
+
...pending.slice(index),
|
|
5930
|
+
...currentGenerationBuffer
|
|
5931
|
+
];
|
|
5932
|
+
return;
|
|
5933
|
+
}
|
|
5934
|
+
this.deliverCommandNow(pending[index]);
|
|
5935
|
+
index += 1;
|
|
5936
|
+
const reentrantBuffer = this.bufferedCommandDeliveries;
|
|
5937
|
+
if (index === pending.length && reentrantBuffer && reentrantBuffer.length > 0) {
|
|
5938
|
+
pending.push(...reentrantBuffer);
|
|
5939
|
+
this.bufferedCommandDeliveries = [];
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5942
|
+
if (!this.disposed && generation === this.commandRecoveryGeneration) {
|
|
5943
|
+
this.bufferedCommandDeliveries = null;
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
/** 首次订阅前的 REST 历史建立语义事件基线,随后同一工具的订阅回放不再投递。 */
|
|
5947
|
+
seedToolCallDeliveryBaseline(turns) {
|
|
4108
5948
|
for (const turn of turns) {
|
|
4109
|
-
for (const
|
|
4110
|
-
|
|
4111
|
-
|
|
5949
|
+
for (const toolCall of turn.tool_calls) {
|
|
5950
|
+
this.seenToolCallIds.add(toolCall.id);
|
|
5951
|
+
if (toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled") {
|
|
5952
|
+
this.finishedToolCallIds.add(toolCall.id);
|
|
4112
5953
|
}
|
|
4113
5954
|
}
|
|
4114
5955
|
}
|
|
4115
5956
|
}
|
|
4116
|
-
ensureJoined() {
|
|
5957
|
+
ensureJoined(liveRevisionBoundary = this.liveRevision) {
|
|
4117
5958
|
if (this.joined) return Promise.resolve();
|
|
4118
5959
|
if (this.joinPending) return this.joinPending;
|
|
4119
5960
|
const join = this.runtime.emitWithAck(
|
|
@@ -4121,18 +5962,18 @@ var AgentSession = class _AgentSession {
|
|
|
4121
5962
|
{
|
|
4122
5963
|
session_id: this.sessionId,
|
|
4123
5964
|
event_format: "raw",
|
|
4124
|
-
resume: true
|
|
4125
|
-
stream_tokens: this.runtime.streamTokens
|
|
5965
|
+
resume: true
|
|
4126
5966
|
},
|
|
4127
5967
|
5e3
|
|
4128
5968
|
).then((response) => {
|
|
4129
5969
|
if (response && response.ok === false) {
|
|
4130
|
-
throw new
|
|
4131
|
-
typeof response.message === "string" && response.message ? response.message : "Session subscription failed"
|
|
5970
|
+
throw new AgentSessionError(
|
|
5971
|
+
typeof response.message === "string" && response.message ? response.message : "Session subscription failed",
|
|
5972
|
+
typeof response.code === "string" ? response.code : void 0
|
|
4132
5973
|
);
|
|
4133
5974
|
}
|
|
4134
5975
|
this.joined = true;
|
|
4135
|
-
this._applyRunningState(Boolean(response?.is_running));
|
|
5976
|
+
this._applyRunningState(Boolean(response?.is_running), liveRevisionBoundary);
|
|
4136
5977
|
}).finally(() => {
|
|
4137
5978
|
this.joinPending = null;
|
|
4138
5979
|
});
|
|
@@ -4140,6 +5981,7 @@ var AgentSession = class _AgentSession {
|
|
|
4140
5981
|
return join;
|
|
4141
5982
|
}
|
|
4142
5983
|
async ensureIdleBeforeSend() {
|
|
5984
|
+
if (this.state.isStreaming) return;
|
|
4143
5985
|
if (Date.now() - this.recentChatEndAt > 3e3) return;
|
|
4144
5986
|
const deadline = Date.now() + 3e3;
|
|
4145
5987
|
while (Date.now() < deadline) {
|
|
@@ -4174,6 +6016,7 @@ var SessionHub = class {
|
|
|
4174
6016
|
connectionListeners = /* @__PURE__ */ new Set();
|
|
4175
6017
|
bound = false;
|
|
4176
6018
|
hasConnected = false;
|
|
6019
|
+
authBusyReconnect = new AuthBusyReconnect();
|
|
4177
6020
|
constructor(client) {
|
|
4178
6021
|
this.client = client;
|
|
4179
6022
|
}
|
|
@@ -4195,16 +6038,28 @@ var SessionHub = class {
|
|
|
4195
6038
|
* 只有最后一份归还才退订房间——否则 StrictMode 双跑、路由抖动这类"连两次
|
|
4196
6039
|
* 只释放一次"的场景会把仍在用的会话退订掉,之后所有 turn/chat 事件静默丢失。
|
|
4197
6040
|
*/
|
|
4198
|
-
async connect(sessionId) {
|
|
6041
|
+
async connect(sessionId, options = {}) {
|
|
4199
6042
|
const existing = this.sessions.get(sessionId);
|
|
4200
6043
|
if (existing && !existing.isDisposed) {
|
|
4201
6044
|
this.hold(sessionId);
|
|
6045
|
+
try {
|
|
6046
|
+
options.setup?.(existing);
|
|
6047
|
+
} catch (error) {
|
|
6048
|
+
existing.dispose();
|
|
6049
|
+
throw error;
|
|
6050
|
+
}
|
|
4202
6051
|
return this.bootstrapPending.get(sessionId) ?? existing;
|
|
4203
6052
|
}
|
|
4204
6053
|
this.ensureBound();
|
|
4205
6054
|
const session = new AgentSession(sessionId, this.runtimeFor(), this.connection);
|
|
4206
6055
|
this.sessions.set(sessionId, session);
|
|
4207
6056
|
this.hold(sessionId);
|
|
6057
|
+
try {
|
|
6058
|
+
options.setup?.(session);
|
|
6059
|
+
} catch (error) {
|
|
6060
|
+
session.dispose();
|
|
6061
|
+
throw error;
|
|
6062
|
+
}
|
|
4208
6063
|
this.ensureConnected();
|
|
4209
6064
|
const bootstrap = session._bootstrap().then(() => session);
|
|
4210
6065
|
this.bootstrapPending.set(sessionId, bootstrap);
|
|
@@ -4228,6 +6083,7 @@ var SessionHub = class {
|
|
|
4228
6083
|
/** 手动重连(例如登录态变化后)。 */
|
|
4229
6084
|
reconnect() {
|
|
4230
6085
|
this.ensureBound();
|
|
6086
|
+
this.authBusyReconnect.cancel();
|
|
4231
6087
|
const socket = this.client.socket();
|
|
4232
6088
|
socket.disconnect();
|
|
4233
6089
|
this.setConnection("connecting");
|
|
@@ -4235,11 +6091,13 @@ var SessionHub = class {
|
|
|
4235
6091
|
}
|
|
4236
6092
|
/** 断开连接(不清理 session 实例)。 */
|
|
4237
6093
|
disconnect() {
|
|
6094
|
+
this.authBusyReconnect.cancel();
|
|
4238
6095
|
this.client.socket().disconnect();
|
|
4239
6096
|
this.setConnection("disconnected");
|
|
4240
6097
|
}
|
|
4241
6098
|
/** 清理当前身份下缓存的全部会话,避免切换登录身份后复用旧快照。 */
|
|
4242
6099
|
disposeAll() {
|
|
6100
|
+
this.authBusyReconnect.cancel();
|
|
4243
6101
|
this.bootstrapPending.clear();
|
|
4244
6102
|
for (const session of [...this.sessions.values()]) {
|
|
4245
6103
|
this.forceRelease(session);
|
|
@@ -4251,6 +6109,7 @@ var SessionHub = class {
|
|
|
4251
6109
|
* turn:* / chat:* 事件都会静默丢弃。
|
|
4252
6110
|
*/
|
|
4253
6111
|
_handleSocketReplaced() {
|
|
6112
|
+
this.authBusyReconnect.cancel();
|
|
4254
6113
|
this.bound = false;
|
|
4255
6114
|
this.setConnection("disconnected");
|
|
4256
6115
|
if (this.sessions.size === 0) return;
|
|
@@ -4259,23 +6118,15 @@ var SessionHub = class {
|
|
|
4259
6118
|
}
|
|
4260
6119
|
runtimeFor() {
|
|
4261
6120
|
return {
|
|
4262
|
-
streamTokens: this.client.options?.streamTokens !== false,
|
|
4263
6121
|
emitEvent: (event, data) => {
|
|
4264
6122
|
;
|
|
4265
6123
|
this.client.socket().emit(event, data);
|
|
4266
6124
|
},
|
|
4267
|
-
emitWithAck: (event, data, timeoutMs) =>
|
|
4268
|
-
;
|
|
4269
|
-
this.client.socket().timeout(timeoutMs).emit(event, data, (err, response) => {
|
|
4270
|
-
if (err) {
|
|
4271
|
-
reject(err);
|
|
4272
|
-
return;
|
|
4273
|
-
}
|
|
4274
|
-
resolve(response);
|
|
4275
|
-
});
|
|
4276
|
-
}),
|
|
6125
|
+
emitWithAck: (event, data, timeoutMs) => this.client.requestCommand(event, data, timeoutMs),
|
|
4277
6126
|
ensureConnected: () => this.ensureConnected(),
|
|
4278
|
-
|
|
6127
|
+
fetchTurnsPage: (sessionId, query, init) => this.client.sessions.getSessionTurnsPage(sessionId, query, init),
|
|
6128
|
+
fetchTurnIndex: (sessionId) => this.client.sessions.getSessionTurnIndex(sessionId),
|
|
6129
|
+
fetchCommands: (sessionId) => this.client.sessions.getSessionTurnCommands(sessionId),
|
|
4279
6130
|
fetchSessionInfo: (sessionId) => this.client.sessions.getSession(sessionId),
|
|
4280
6131
|
updateReplay: (sessionId, payload) => this.client.sessions.updateReplaySession(sessionId, payload).then((result) => result.replay_state ?? null),
|
|
4281
6132
|
listDir: (sessionId, dirPath) => this.client.sessions.listDir(sessionId, dirPath),
|
|
@@ -4303,6 +6154,9 @@ var SessionHub = class {
|
|
|
4303
6154
|
if (this.sessions.get(session.sessionId) === session) {
|
|
4304
6155
|
this.sessions.delete(session.sessionId);
|
|
4305
6156
|
}
|
|
6157
|
+
if (this.sessions.size === 0) {
|
|
6158
|
+
this.authBusyReconnect.cancel();
|
|
6159
|
+
}
|
|
4306
6160
|
;
|
|
4307
6161
|
this.client.socket().emit("session:unsubscribe", { session_id: session.sessionId });
|
|
4308
6162
|
return true;
|
|
@@ -4311,10 +6165,13 @@ var SessionHub = class {
|
|
|
4311
6165
|
forceRelease(session) {
|
|
4312
6166
|
this.holdCounts.delete(session.sessionId);
|
|
4313
6167
|
session.dispose();
|
|
6168
|
+
if (this.sessions.size === 0) {
|
|
6169
|
+
this.authBusyReconnect.cancel();
|
|
6170
|
+
}
|
|
4314
6171
|
}
|
|
4315
6172
|
ensureConnected() {
|
|
4316
6173
|
const socket = this.client.socket();
|
|
4317
|
-
if (socket.connected || socket.active) return;
|
|
6174
|
+
if (socket.connected || socket.active || this.authBusyReconnect.pending) return;
|
|
4318
6175
|
this.setConnection("connecting");
|
|
4319
6176
|
socket.connect();
|
|
4320
6177
|
}
|
|
@@ -4346,17 +6203,35 @@ var SessionHub = class {
|
|
|
4346
6203
|
this.bound = true;
|
|
4347
6204
|
const socket = this.client.socket();
|
|
4348
6205
|
socket.on("connect", () => {
|
|
6206
|
+
this.authBusyReconnect.cancel();
|
|
4349
6207
|
const isReconnect = this.hasConnected;
|
|
4350
6208
|
this.hasConnected = true;
|
|
4351
6209
|
this.setConnection("connected");
|
|
4352
6210
|
if (isReconnect) {
|
|
4353
6211
|
for (const session of this.sessions.values()) {
|
|
4354
6212
|
void session._rejoinAfterReconnect();
|
|
6213
|
+
void session.refreshQueue();
|
|
4355
6214
|
}
|
|
4356
6215
|
}
|
|
4357
6216
|
});
|
|
4358
|
-
socket.on("connect_error", () =>
|
|
4359
|
-
|
|
6217
|
+
socket.on("connect_error", (error) => {
|
|
6218
|
+
this.setConnection("disconnected");
|
|
6219
|
+
if (this.sessions.size === 0) {
|
|
6220
|
+
this.authBusyReconnect.cancel();
|
|
6221
|
+
return;
|
|
6222
|
+
}
|
|
6223
|
+
if (this.authBusyReconnect.schedule(error, () => {
|
|
6224
|
+
const current = this.client.socket();
|
|
6225
|
+
if (current !== socket || current.connected || current.active) return;
|
|
6226
|
+
this.setConnection("connecting");
|
|
6227
|
+
current.connect();
|
|
6228
|
+
})) return;
|
|
6229
|
+
this.authBusyReconnect.cancel();
|
|
6230
|
+
});
|
|
6231
|
+
socket.on("disconnect", () => {
|
|
6232
|
+
this.authBusyReconnect.cancel();
|
|
6233
|
+
this.setConnection("disconnected");
|
|
6234
|
+
});
|
|
4360
6235
|
socket.io.on("reconnect_attempt", () => this.setConnection("reconnecting"));
|
|
4361
6236
|
socket.on("turn:start", (data) => {
|
|
4362
6237
|
for (const session of this.route(data.session_id)) session._handleTurnStart(data);
|
|
@@ -4367,9 +6242,25 @@ var SessionHub = class {
|
|
|
4367
6242
|
socket.on("turn:end", (data) => {
|
|
4368
6243
|
for (const session of this.route(data.session_id)) session._handleTurnEnd(data);
|
|
4369
6244
|
});
|
|
4370
|
-
socket.on(
|
|
4371
|
-
|
|
4372
|
-
|
|
6245
|
+
socket.on(
|
|
6246
|
+
"turn:timing",
|
|
6247
|
+
(data) => {
|
|
6248
|
+
for (const session of this.route(data.session_id)) {
|
|
6249
|
+
session._handleTurnEvents([
|
|
6250
|
+
{ type: "llm:first_token:delivered", payload: data }
|
|
6251
|
+
]);
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
);
|
|
6255
|
+
socket.on(
|
|
6256
|
+
"turn:events",
|
|
6257
|
+
(data, acknowledge) => {
|
|
6258
|
+
acknowledgeTurnEvents(acknowledge);
|
|
6259
|
+
for (const session of this.route(data.session_id)) {
|
|
6260
|
+
session._handleTurnEvents(data.events ?? [], data.replayed === true);
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
);
|
|
4373
6264
|
socket.on("chat:start", (data) => {
|
|
4374
6265
|
for (const session of this.route(data?.session_id)) session._handleChatStart();
|
|
4375
6266
|
});
|
|
@@ -4378,8 +6269,17 @@ var SessionHub = class {
|
|
|
4378
6269
|
session._handleChatEnd(data?.status ?? "completed");
|
|
4379
6270
|
}
|
|
4380
6271
|
});
|
|
4381
|
-
socket.on(
|
|
4382
|
-
|
|
6272
|
+
socket.on(
|
|
6273
|
+
"system:error",
|
|
6274
|
+
(data) => {
|
|
6275
|
+
const code = data.code == null ? void 0 : String(data.code);
|
|
6276
|
+
for (const session of this.route(data.session_id)) {
|
|
6277
|
+
session._handleSystemError(data.message, code);
|
|
6278
|
+
}
|
|
6279
|
+
}
|
|
6280
|
+
);
|
|
6281
|
+
socket.on("chat:queue:updated", (data) => {
|
|
6282
|
+
for (const session of this.route(data?.session_id)) session._handleQueueUpdated(data);
|
|
4383
6283
|
});
|
|
4384
6284
|
socket.on(
|
|
4385
6285
|
"system:notification",
|
|
@@ -4497,21 +6397,15 @@ function resolveAuthToken(options) {
|
|
|
4497
6397
|
return token ? token : null;
|
|
4498
6398
|
}
|
|
4499
6399
|
|
|
4500
|
-
// src/version.ts
|
|
4501
|
-
var SDK_NAME = "agent-client";
|
|
4502
|
-
var SDK_VERSION = true ? "2610.0.0-beta.9" : "1.1.1";
|
|
4503
|
-
|
|
4504
6400
|
// src/socket.ts
|
|
4505
|
-
function withSdkIdentity(auth) {
|
|
4506
|
-
return { ...auth ?? {}, sdk: SDK_NAME, sdk_version: SDK_VERSION };
|
|
4507
|
-
}
|
|
4508
6401
|
function createSocket(options) {
|
|
4509
6402
|
const token = resolveAuthToken(options);
|
|
4510
6403
|
return io(options.baseUrl, {
|
|
4511
6404
|
path: options.path ?? "/socket.io",
|
|
4512
6405
|
withCredentials: true,
|
|
6406
|
+
...options.transports ? { transports: options.transports } : {},
|
|
4513
6407
|
query: token ? { token } : void 0,
|
|
4514
|
-
auth: typeof options.token === "function" ? (cb) => cb(
|
|
6408
|
+
auth: typeof options.token === "function" ? (cb) => cb(buildSocketAuth(options) ?? {}) : buildSocketAuth(options) ?? {},
|
|
4515
6409
|
autoConnect: false
|
|
4516
6410
|
});
|
|
4517
6411
|
}
|
|
@@ -4527,8 +6421,17 @@ var BladeClient = class {
|
|
|
4527
6421
|
options;
|
|
4528
6422
|
auth;
|
|
4529
6423
|
headless;
|
|
6424
|
+
computers;
|
|
4530
6425
|
models;
|
|
4531
6426
|
sessions;
|
|
6427
|
+
/** 按用户读取的插件目录与图标;没有会话时也能用。 */
|
|
6428
|
+
plugins;
|
|
6429
|
+
chatGroups;
|
|
6430
|
+
/**
|
|
6431
|
+
* 全站访问统计:匿名读写,不带 bearer 也不触发登录刷新。
|
|
6432
|
+
* 只有内置 Web 的同源页面使用,第三方宿主页面不会被自动统计。
|
|
6433
|
+
*/
|
|
6434
|
+
siteVisits;
|
|
4532
6435
|
/** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
|
|
4533
6436
|
hub;
|
|
4534
6437
|
constructor(options) {
|
|
@@ -4538,8 +6441,12 @@ var BladeClient = class {
|
|
|
4538
6441
|
};
|
|
4539
6442
|
this.auth = new AuthResource(this);
|
|
4540
6443
|
this.headless = new HeadlessResource(this);
|
|
6444
|
+
this.computers = new ComputersResource(this);
|
|
4541
6445
|
this.models = new ModelsResource(this);
|
|
4542
6446
|
this.sessions = new SessionsResource(this);
|
|
6447
|
+
this.plugins = new PluginsResource(this);
|
|
6448
|
+
this.chatGroups = new ChatGroupsResource(this);
|
|
6449
|
+
this.siteVisits = new SiteVisitsResource(this);
|
|
4543
6450
|
this.hub = new SessionHub(this);
|
|
4544
6451
|
if (this.options.token === void 0) {
|
|
4545
6452
|
this.runtimeToken = readStoredToken(this.options.baseUrl);
|
|
@@ -4634,6 +6541,16 @@ var BladeClient = class {
|
|
|
4634
6541
|
}
|
|
4635
6542
|
return this.socketInstance;
|
|
4636
6543
|
}
|
|
6544
|
+
requestCommand(event, payload, timeoutMs) {
|
|
6545
|
+
const send = () => new Promise((resolve, reject) => {
|
|
6546
|
+
;
|
|
6547
|
+
this.socket().timeout(timeoutMs).emit(event, payload, (error, ack) => {
|
|
6548
|
+
if (error) reject(error);
|
|
6549
|
+
else resolve(ack ?? {});
|
|
6550
|
+
});
|
|
6551
|
+
});
|
|
6552
|
+
return this.options.commandTransport?.(event, payload, send) ?? send();
|
|
6553
|
+
}
|
|
4637
6554
|
async json(method, path, body) {
|
|
4638
6555
|
const response = await this.fetch(method, path, {
|
|
4639
6556
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
@@ -4698,7 +6615,7 @@ var BladeClient = class {
|
|
|
4698
6615
|
signal: init.signal
|
|
4699
6616
|
});
|
|
4700
6617
|
if (response.status === 401 && !isRetry && this.shouldRefreshFor401(url)) {
|
|
4701
|
-
const refreshed = await this.
|
|
6618
|
+
const refreshed = await this.tryRefreshUnlessAborted(init.signal);
|
|
4702
6619
|
if (refreshed) {
|
|
4703
6620
|
return this.fetch(method, path, init, true);
|
|
4704
6621
|
}
|
|
@@ -4710,7 +6627,7 @@ var BladeClient = class {
|
|
|
4710
6627
|
return response;
|
|
4711
6628
|
}
|
|
4712
6629
|
buildAuthedUrl(path) {
|
|
4713
|
-
const url =
|
|
6630
|
+
const url = buildBackendUrl(this.options.baseUrl, path);
|
|
4714
6631
|
const token = this.resolveTokenForUrl(url);
|
|
4715
6632
|
if (token) {
|
|
4716
6633
|
url.searchParams.set("token", token);
|
|
@@ -4722,6 +6639,7 @@ var BladeClient = class {
|
|
|
4722
6639
|
if (!this.isSameBackendUrl(url)) {
|
|
4723
6640
|
return result;
|
|
4724
6641
|
}
|
|
6642
|
+
result.set("X-Blade-Client", `${SDK_NAME}/${SDK_VERSION}`);
|
|
4725
6643
|
const token = this.resolveRestToken();
|
|
4726
6644
|
if (token) {
|
|
4727
6645
|
result.set("Authorization", `Bearer ${token}`);
|
|
@@ -4729,14 +6647,11 @@ var BladeClient = class {
|
|
|
4729
6647
|
return result;
|
|
4730
6648
|
}
|
|
4731
6649
|
buildUrl(path) {
|
|
4732
|
-
return
|
|
6650
|
+
return buildBackendUrl(this.options.baseUrl, path);
|
|
4733
6651
|
}
|
|
4734
6652
|
baseUrlWithTrailingSlash() {
|
|
4735
6653
|
return this.options.baseUrl.endsWith("/") ? this.options.baseUrl : `${this.options.baseUrl}/`;
|
|
4736
6654
|
}
|
|
4737
|
-
toBaseRelativePath(path) {
|
|
4738
|
-
return path.startsWith("/") ? path.slice(1) : path;
|
|
4739
|
-
}
|
|
4740
6655
|
isSameBackendUrl(url) {
|
|
4741
6656
|
const base = new URL(this.baseUrlWithTrailingSlash());
|
|
4742
6657
|
if (url.origin !== base.origin) {
|
|
@@ -4755,6 +6670,27 @@ var BladeClient = class {
|
|
|
4755
6670
|
hasExplicitBearerToken() {
|
|
4756
6671
|
return Boolean(this.resolveRestToken());
|
|
4757
6672
|
}
|
|
6673
|
+
/**
|
|
6674
|
+
* 等一次令牌刷新,但调用方中止请求时不再等下去。
|
|
6675
|
+
*
|
|
6676
|
+
* 刷新本身是共享的(`refreshPromise`,别的请求也在等它),这里只是让这次已被取消的
|
|
6677
|
+
* 请求尽早结束——否则它会一直挂在调用方(定位)的串行队列里,用户改选的新目标要等到
|
|
6678
|
+
* 刷新结束才轮得到。
|
|
6679
|
+
*/
|
|
6680
|
+
async tryRefreshUnlessAborted(signal) {
|
|
6681
|
+
if (!signal) return this.tryRefresh();
|
|
6682
|
+
if (signal.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
6683
|
+
let onAbort;
|
|
6684
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
6685
|
+
onAbort = () => reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
6686
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
6687
|
+
});
|
|
6688
|
+
try {
|
|
6689
|
+
return await Promise.race([this.tryRefresh(), aborted]);
|
|
6690
|
+
} finally {
|
|
6691
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
4758
6694
|
async tryRefresh() {
|
|
4759
6695
|
if (this.refreshPromise) {
|
|
4760
6696
|
return this.refreshPromise;
|
|
@@ -4765,7 +6701,9 @@ var BladeClient = class {
|
|
|
4765
6701
|
this.buildUrl(REFRESH_PATH).toString(),
|
|
4766
6702
|
{
|
|
4767
6703
|
method: "POST",
|
|
4768
|
-
credentials: "include"
|
|
6704
|
+
credentials: "include",
|
|
6705
|
+
// Refresh remains cookie-only; do not attach the REST bearer.
|
|
6706
|
+
headers: { "X-Blade-Client": `${SDK_NAME}/${SDK_VERSION}` }
|
|
4769
6707
|
}
|
|
4770
6708
|
);
|
|
4771
6709
|
if (!response.ok) {
|
|
@@ -4892,6 +6830,122 @@ function parseXhrHeaders(rawHeaders) {
|
|
|
4892
6830
|
return headers;
|
|
4893
6831
|
}
|
|
4894
6832
|
|
|
6833
|
+
// src/polling-backoff.ts
|
|
6834
|
+
var MAX_BACKOFF_MS = 6e4;
|
|
6835
|
+
var MAX_TIMER_MS = 2147483647;
|
|
6836
|
+
var PollingBackoff = class {
|
|
6837
|
+
constructor(intervalMs) {
|
|
6838
|
+
this.intervalMs = intervalMs;
|
|
6839
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0 || intervalMs > MAX_BACKOFF_MS) {
|
|
6840
|
+
throw new RangeError("Polling interval must be between 0 and 60000 ms");
|
|
6841
|
+
}
|
|
6842
|
+
}
|
|
6843
|
+
intervalMs;
|
|
6844
|
+
failures = 0;
|
|
6845
|
+
nextAt = 0;
|
|
6846
|
+
reset() {
|
|
6847
|
+
this.failures = 0;
|
|
6848
|
+
this.nextAt = 0;
|
|
6849
|
+
}
|
|
6850
|
+
failed(error) {
|
|
6851
|
+
if (error instanceof Error && error.name === "AbortError" || error instanceof BladeApiError && error.status < 500 && error.status !== 408 && error.status !== 429) {
|
|
6852
|
+
this.nextAt = false;
|
|
6853
|
+
return;
|
|
6854
|
+
}
|
|
6855
|
+
this.failures = Math.min(this.failures + 1, 10);
|
|
6856
|
+
const ceiling = Math.min(MAX_BACKOFF_MS, this.intervalMs * 2 ** this.failures);
|
|
6857
|
+
const backoff = Math.max(this.intervalMs, ceiling * (0.5 + Math.random() * 0.5));
|
|
6858
|
+
const header = error instanceof BladeApiError ? error.response.headers.get("Retry-After")?.trim() : void 0;
|
|
6859
|
+
const retryAfter = header ? /^\d+$/.test(header) ? Number(header) * 1e3 : Date.parse(header) - Date.now() : 0;
|
|
6860
|
+
const delay = Math.max(backoff, Number.isNaN(retryAfter) ? 0 : retryAfter);
|
|
6861
|
+
this.nextAt = delay > MAX_TIMER_MS ? false : Date.now() + delay;
|
|
6862
|
+
}
|
|
6863
|
+
/** Zero means ready, false means stop, otherwise wait at least this many ms. */
|
|
6864
|
+
waitMs() {
|
|
6865
|
+
return this.nextAt === false ? false : Math.max(0, this.nextAt - Date.now());
|
|
6866
|
+
}
|
|
6867
|
+
};
|
|
6868
|
+
|
|
6869
|
+
// src/platform-endpoints.ts
|
|
6870
|
+
var PLATFORM_SERVICE_NAMES = [
|
|
6871
|
+
"os",
|
|
6872
|
+
"hub",
|
|
6873
|
+
"agent",
|
|
6874
|
+
"oauth",
|
|
6875
|
+
"llmGateway",
|
|
6876
|
+
"gitea",
|
|
6877
|
+
"tile"
|
|
6878
|
+
];
|
|
6879
|
+
var EMPTY_PLATFORM_ENDPOINTS = Object.freeze({
|
|
6880
|
+
services: Object.freeze({})
|
|
6881
|
+
});
|
|
6882
|
+
function defaultDocumentBaseUrl() {
|
|
6883
|
+
if (typeof document === "undefined") return void 0;
|
|
6884
|
+
if (document.querySelector("base[href]")) return document.baseURI;
|
|
6885
|
+
if (typeof location !== "undefined" && location.origin) return `${location.origin}/`;
|
|
6886
|
+
return document.baseURI;
|
|
6887
|
+
}
|
|
6888
|
+
function normalizeServiceUrl(value) {
|
|
6889
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
6890
|
+
if (value.includes("?") || value.includes("#")) return null;
|
|
6891
|
+
try {
|
|
6892
|
+
const url = new URL(value.trim());
|
|
6893
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
6894
|
+
if (url.username || url.password || url.search || url.hash) return null;
|
|
6895
|
+
return url.toString().replace(/\/$/, "");
|
|
6896
|
+
} catch {
|
|
6897
|
+
return null;
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
6900
|
+
function parsePlatformEndpoints(value) {
|
|
6901
|
+
if (!isRecord(value) || !isRecord(value.services)) return null;
|
|
6902
|
+
const services = {};
|
|
6903
|
+
for (const name of PLATFORM_SERVICE_NAMES) {
|
|
6904
|
+
if (!(name in value.services)) continue;
|
|
6905
|
+
const url = normalizeServiceUrl(value.services[name]);
|
|
6906
|
+
if (!url) return null;
|
|
6907
|
+
if (name === "agent" && new URL(url).pathname !== "/") return null;
|
|
6908
|
+
services[name] = url;
|
|
6909
|
+
}
|
|
6910
|
+
return { services };
|
|
6911
|
+
}
|
|
6912
|
+
async function loadPlatformEndpoints(options = {}) {
|
|
6913
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
6914
|
+
const baseUrl = options.baseUrl ?? defaultDocumentBaseUrl();
|
|
6915
|
+
if (!fetchImpl || !baseUrl) return EMPTY_PLATFORM_ENDPOINTS;
|
|
6916
|
+
const controller = new AbortController();
|
|
6917
|
+
const timeout = globalThis.setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
|
|
6918
|
+
try {
|
|
6919
|
+
const response = await fetchImpl(new URL("config.json", baseUrl), {
|
|
6920
|
+
cache: "no-store",
|
|
6921
|
+
credentials: "same-origin",
|
|
6922
|
+
headers: { Accept: "application/json" },
|
|
6923
|
+
signal: controller.signal
|
|
6924
|
+
});
|
|
6925
|
+
if (!response.ok) return EMPTY_PLATFORM_ENDPOINTS;
|
|
6926
|
+
return parsePlatformEndpoints(await response.json()) ?? EMPTY_PLATFORM_ENDPOINTS;
|
|
6927
|
+
} catch {
|
|
6928
|
+
return EMPTY_PLATFORM_ENDPOINTS;
|
|
6929
|
+
} finally {
|
|
6930
|
+
globalThis.clearTimeout(timeout);
|
|
6931
|
+
}
|
|
6932
|
+
}
|
|
6933
|
+
function resolveServiceUrl(endpoints, name, path = "") {
|
|
6934
|
+
const baseUrl = endpoints.services[name];
|
|
6935
|
+
if (!baseUrl) return null;
|
|
6936
|
+
if (!path) return baseUrl;
|
|
6937
|
+
const pathReference = path.trimStart();
|
|
6938
|
+
if (path.includes("\\") || pathReference.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(pathReference)) {
|
|
6939
|
+
return null;
|
|
6940
|
+
}
|
|
6941
|
+
const base = new URL(`${baseUrl}/`);
|
|
6942
|
+
const target = new URL(path.replace(/^\/+/, ""), base);
|
|
6943
|
+
if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) {
|
|
6944
|
+
return null;
|
|
6945
|
+
}
|
|
6946
|
+
return target.toString();
|
|
6947
|
+
}
|
|
6948
|
+
|
|
4895
6949
|
// src/commands/protocol.ts
|
|
4896
6950
|
function isCommandEnvelope(value) {
|
|
4897
6951
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -4992,12 +7046,20 @@ var Task = type2({
|
|
|
4992
7046
|
});
|
|
4993
7047
|
export {
|
|
4994
7048
|
AgentSession,
|
|
7049
|
+
AgentSessionError,
|
|
7050
|
+
AuthBusyReconnect,
|
|
4995
7051
|
BladeApiError,
|
|
4996
7052
|
BladeClient,
|
|
7053
|
+
ChatGroupsResource,
|
|
4997
7054
|
ClientProjectionBuilder,
|
|
7055
|
+
ComputersResource,
|
|
4998
7056
|
DEFAULT_REPLAY_SPEED,
|
|
7057
|
+
EMPTY_PLATFORM_ENDPOINTS,
|
|
7058
|
+
EMPTY_SESSION_QUEUE,
|
|
4999
7059
|
LayoutType,
|
|
5000
7060
|
ModelsResource,
|
|
7061
|
+
PluginsResource,
|
|
7062
|
+
PollingBackoff,
|
|
5001
7063
|
SDK_NAME,
|
|
5002
7064
|
SDK_VERSION,
|
|
5003
7065
|
SessionHub,
|
|
@@ -5007,22 +7069,60 @@ export {
|
|
|
5007
7069
|
Task,
|
|
5008
7070
|
TaskStatus,
|
|
5009
7071
|
acceptedPostChatFollowupCompletesLatestRun,
|
|
7072
|
+
acknowledgeTurnEvents,
|
|
7073
|
+
authBusyRetryDelayMs,
|
|
5010
7074
|
buildMessageContent,
|
|
7075
|
+
buildToolUiCardKey,
|
|
7076
|
+
canCancelQueuedMessage,
|
|
7077
|
+
canDeliverQueuedMessage,
|
|
7078
|
+
canEditQueuedMessage,
|
|
7079
|
+
canToggleComputer,
|
|
7080
|
+
chatErrorForDisplay,
|
|
7081
|
+
collectInlineToolUiCards,
|
|
7082
|
+
collectPreviewToolUiCards,
|
|
7083
|
+
computerDaemonVersion,
|
|
7084
|
+
computerOS,
|
|
7085
|
+
computerPlatformLabel,
|
|
7086
|
+
computerState,
|
|
5011
7087
|
connectEmbedded,
|
|
5012
7088
|
contentPreview,
|
|
7089
|
+
contextProjectionData,
|
|
5013
7090
|
createInitialSessionState,
|
|
5014
7091
|
createSocket,
|
|
5015
7092
|
extractTextAttachments,
|
|
7093
|
+
getContextDisplayState,
|
|
7094
|
+
getContextGroupDisplayState,
|
|
5016
7095
|
getFileParts,
|
|
5017
7096
|
getImageParts,
|
|
5018
7097
|
getTextContent,
|
|
7098
|
+
groupAdjacentContextRuns,
|
|
5019
7099
|
groupMessagesByLoop,
|
|
7100
|
+
hasChatRunEvent,
|
|
7101
|
+
inferToolStatus,
|
|
7102
|
+
isAppDevToolUiCard,
|
|
7103
|
+
isBackgroundWakeMessage,
|
|
5020
7104
|
isCommandEnvelope,
|
|
7105
|
+
isEntryGoneError,
|
|
5021
7106
|
isHiddenInternalMessage,
|
|
5022
7107
|
isInboundEnvelope,
|
|
7108
|
+
isInternalStatusToolUiCard,
|
|
7109
|
+
isMcpAppData,
|
|
7110
|
+
isSessionQueueSnapshot,
|
|
7111
|
+
isToolUiCard,
|
|
5023
7112
|
latestPostChatFollowup,
|
|
7113
|
+
loadPlatformEndpoints,
|
|
5024
7114
|
normalizeMessageContent,
|
|
7115
|
+
parseQueueAck,
|
|
7116
|
+
prependOlder,
|
|
7117
|
+
queuePauseReasonLabel,
|
|
7118
|
+
queuedMessageStatusLabel,
|
|
7119
|
+
reconcileHistoricalUserTurns,
|
|
5025
7120
|
reconcileOptimisticUserTurns,
|
|
7121
|
+
replaceWindow,
|
|
7122
|
+
resolveServiceUrl,
|
|
7123
|
+
resolveToolUiCardContent,
|
|
7124
|
+
shouldApplySnapshot,
|
|
7125
|
+
sortComputers,
|
|
5026
7126
|
toReplaySnapshot,
|
|
5027
7127
|
transformSlashCommand
|
|
5028
7128
|
};
|