@blade-hq/agent-client 2610.0.1-alpha.1 → 2610.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -11
- package/dist/auth.d.ts +0 -1
- package/dist/blade-client.d.ts +24 -1
- package/dist/index.d.ts +38 -7
- package/dist/index.js +2520 -283
- 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 +238 -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 +8 -0
- package/dist/schemas/projection-utils.d.ts +3 -0
- package/dist/schemas/projection.d.ts +53 -2
- package/dist/schemas/session.d.ts +10 -1
- package/dist/session/agent-session.d.ts +160 -16
- 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 +19 -2
- package/dist/shared/auth-busy.d.ts +9 -0
- package/dist/shared/projection/builder.d.ts +17 -0
- package/dist/shared/projection/context.d.ts +3 -0
- package/dist/shared/projection/helpers.d.ts +4 -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 +201 -2
- package/package.json +4 -2
- package/public-api.md +1071 -23
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.1" : "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) {
|
|
@@ -350,7 +363,10 @@ var HeadlessResource = class {
|
|
|
350
363
|
socket.on("chat:start", onChatStart);
|
|
351
364
|
socket.on("chat:end", onChatEnd);
|
|
352
365
|
socket.on("system:error", onSystemError);
|
|
353
|
-
socket.emit("session:subscribe", {
|
|
366
|
+
socket.emit("session:subscribe", {
|
|
367
|
+
session_id,
|
|
368
|
+
event_format: "raw"
|
|
369
|
+
});
|
|
354
370
|
socket.emit(
|
|
355
371
|
"chat:send",
|
|
356
372
|
{
|
|
@@ -391,9 +407,6 @@ function errorMessageFromResult(result) {
|
|
|
391
407
|
}
|
|
392
408
|
return null;
|
|
393
409
|
}
|
|
394
|
-
function isRecord(value) {
|
|
395
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
396
|
-
}
|
|
397
410
|
function ensureSocketConnected(socket, timeoutMs, detail) {
|
|
398
411
|
if (socket.connected) {
|
|
399
412
|
return Promise.resolve();
|
|
@@ -421,6 +434,81 @@ function ensureSocketConnected(socket, timeoutMs, detail) {
|
|
|
421
434
|
});
|
|
422
435
|
}
|
|
423
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
|
+
|
|
424
512
|
// src/resources/models.ts
|
|
425
513
|
var ModelsResource = class {
|
|
426
514
|
constructor(client) {
|
|
@@ -447,6 +535,7 @@ var SessionInfo = type({
|
|
|
447
535
|
"shared?": "boolean",
|
|
448
536
|
"memory_enabled?": "boolean",
|
|
449
537
|
"is_persistent?": "boolean",
|
|
538
|
+
"initiator?": "'ui' | 'sdk' | 'unknown' | null",
|
|
450
539
|
"is_headless?": "boolean",
|
|
451
540
|
"viewer_role?": "'owner' | 'viewer'",
|
|
452
541
|
"template_id?": "string | null",
|
|
@@ -469,7 +558,11 @@ var SessionInfo = type({
|
|
|
469
558
|
"disable_tools?": "string[]",
|
|
470
559
|
"runtime_type?": "string | null",
|
|
471
560
|
"daemon_id?": "string | null",
|
|
561
|
+
"agent_runtime_id?": "string | null",
|
|
472
562
|
"workspace_path?": "string | null",
|
|
563
|
+
"ba_version?": "string | null",
|
|
564
|
+
"sandbox_version?": "string | null",
|
|
565
|
+
"chat_group_id?": "string | null",
|
|
473
566
|
"match?": "unknown"
|
|
474
567
|
});
|
|
475
568
|
|
|
@@ -535,7 +628,7 @@ function upsertToolCall(state, toolCallId, toolName, displayName, args) {
|
|
|
535
628
|
});
|
|
536
629
|
}
|
|
537
630
|
}
|
|
538
|
-
function applyToolResult(state, toolCallId, result, status, durationMs, sourceLoop) {
|
|
631
|
+
function applyToolResult(state, toolCallId, result, status, durationMs, ranInParallel, sourceLoop, startedAt = null) {
|
|
539
632
|
let toolName = "";
|
|
540
633
|
let displayName = "";
|
|
541
634
|
let finalResult = result;
|
|
@@ -547,6 +640,8 @@ function applyToolResult(state, toolCallId, result, status, durationMs, sourceLo
|
|
|
547
640
|
}
|
|
548
641
|
const update = { status, result: finalResult };
|
|
549
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;
|
|
550
645
|
state.toolCalls[i] = { ...tc, ...update };
|
|
551
646
|
toolName = tc.tool_name;
|
|
552
647
|
displayName = tc.display_name;
|
|
@@ -728,11 +823,17 @@ function buildCompactionTurn(sequence, compactionId, loopId, status, data) {
|
|
|
728
823
|
summary_preview: summaryPreview,
|
|
729
824
|
summary_full: summaryFull,
|
|
730
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,
|
|
731
832
|
tokens_before: tokensBefore,
|
|
732
833
|
tokens_after: tokensAfter,
|
|
733
834
|
saved_ratio: savedRatio,
|
|
734
835
|
trigger: asTrigger(data.trigger),
|
|
735
|
-
failure_reason:
|
|
836
|
+
failure_reason: [data.failure_reason, data.fallback_reason, data.reason].find((value) => typeof value === "string" && value.length > 0) ?? null,
|
|
736
837
|
fallback_applied: typeof data.fallback_applied === "boolean" ? data.fallback_applied : null
|
|
737
838
|
};
|
|
738
839
|
}
|
|
@@ -893,7 +994,7 @@ function sourceLoopFor(loopId, loopDescriptions) {
|
|
|
893
994
|
if (!description) return null;
|
|
894
995
|
return { loop_name: loopId, description };
|
|
895
996
|
}
|
|
896
|
-
var MAX_FINAL_ARTIFACTS =
|
|
997
|
+
var MAX_FINAL_ARTIFACTS = 20;
|
|
897
998
|
function normalizeFinalArtifacts(raw) {
|
|
898
999
|
if (!Array.isArray(raw)) return [];
|
|
899
1000
|
const artifacts = [];
|
|
@@ -927,12 +1028,16 @@ function normalizePostChatFollowup(data, assistantEntryId) {
|
|
|
927
1028
|
}
|
|
928
1029
|
const recaption = String(data.recaption ?? "");
|
|
929
1030
|
const finalArtifacts = normalizeFinalArtifacts(data.final_artifacts);
|
|
930
|
-
|
|
1031
|
+
const feedbackEligible = data.feedback_eligible === true;
|
|
1032
|
+
if (suggestions.length === 0 && !recaption && finalArtifacts.length === 0 && !feedbackEligible)
|
|
1033
|
+
return null;
|
|
931
1034
|
return {
|
|
932
1035
|
assistant_entry_id: assistantEntryId,
|
|
933
1036
|
recaption,
|
|
934
1037
|
suggestions: suggestions.slice(0, 3),
|
|
935
|
-
final_artifacts: finalArtifacts
|
|
1038
|
+
final_artifacts: finalArtifacts,
|
|
1039
|
+
feedback_eligible: feedbackEligible,
|
|
1040
|
+
feedback_scope: "latest_result"
|
|
936
1041
|
};
|
|
937
1042
|
}
|
|
938
1043
|
function normalizeChildPause(pausePayload) {
|
|
@@ -983,6 +1088,12 @@ function createTurnState(turnId, loopId, opts = {}) {
|
|
|
983
1088
|
toolCalls: [],
|
|
984
1089
|
usage: null,
|
|
985
1090
|
responseDone: false,
|
|
1091
|
+
firstTokenMs: null,
|
|
1092
|
+
streamDurationMs: null,
|
|
1093
|
+
firstTokenDeliveryMs: null,
|
|
1094
|
+
modelStartedAt: null,
|
|
1095
|
+
modelDurationMs: null,
|
|
1096
|
+
modelStartedAtReported: false,
|
|
986
1097
|
memoryRefs: opts.memoryRefs ?? null,
|
|
987
1098
|
parentForkToolCallId: null
|
|
988
1099
|
};
|
|
@@ -1000,8 +1111,13 @@ function snapshot(state, sequence, status) {
|
|
|
1000
1111
|
tool_calls: [...state.toolCalls],
|
|
1001
1112
|
model: state.model,
|
|
1002
1113
|
usage: state.usage ? { ...state.usage } : null,
|
|
1003
|
-
duration_ms:
|
|
1004
|
-
|
|
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,
|
|
1005
1121
|
context_window: state.contextWindow,
|
|
1006
1122
|
memory_refs: state.memoryRefs,
|
|
1007
1123
|
parent_fork_tool_call_id: state.parentForkToolCallId
|
|
@@ -1015,17 +1131,219 @@ function toolStatusFromResult(result) {
|
|
|
1015
1131
|
return inferToolStatus(text);
|
|
1016
1132
|
}
|
|
1017
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
|
+
|
|
1018
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
|
+
}
|
|
1259
|
+
function hasChatRunEvent(events) {
|
|
1260
|
+
return events.some(
|
|
1261
|
+
(event) => event.type !== "llm:first_token:delivered" && event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
function acceptedPostChatFollowupCompletesLatestRun(events, updates) {
|
|
1265
|
+
const acceptedAssistantEntryIds = new Set(
|
|
1266
|
+
updates.flatMap((update) => {
|
|
1267
|
+
if (update.kind !== "upsert") return [];
|
|
1268
|
+
const followup = update.turn.post_chat_followup;
|
|
1269
|
+
return followup?.assistant_entry_id ? [followup.assistant_entry_id] : [];
|
|
1270
|
+
})
|
|
1271
|
+
);
|
|
1272
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
1273
|
+
const event = events[index];
|
|
1274
|
+
if (event?.type !== "session:post_chat_followup") continue;
|
|
1275
|
+
const assistantEntryId = String(event.payload.assistant_entry_id ?? "");
|
|
1276
|
+
return acceptedAssistantEntryIds.has(assistantEntryId) && !hasChatRunEvent(events.slice(index + 1));
|
|
1277
|
+
}
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1019
1280
|
function isConversationTurn(turn) {
|
|
1020
1281
|
return (turn.role === "user" || turn.role === "assistant") && (turn.kind ?? "message") === "message" && !turn.blocks.some(
|
|
1021
1282
|
(block) => block.type === "mode_change" || block.type === "plan_status" || block.type === "ask_user_answer"
|
|
1022
1283
|
);
|
|
1023
1284
|
}
|
|
1285
|
+
function completedResponseText(content) {
|
|
1286
|
+
if (typeof content === "string") return content;
|
|
1287
|
+
if (!Array.isArray(content)) return content == null ? "" : String(content);
|
|
1288
|
+
return content.filter(
|
|
1289
|
+
(part) => typeof part === "object" && part !== null && !Array.isArray(part)
|
|
1290
|
+
).filter((part) => part.type === "text").map((part) => String(part.text ?? "")).join("");
|
|
1291
|
+
}
|
|
1292
|
+
function replaceTextContent(state, content) {
|
|
1293
|
+
const currentContent = state.blocks.filter((block) => block.type === "text").map((block) => String(block.content ?? "")).join("");
|
|
1294
|
+
if (currentContent === content) return;
|
|
1295
|
+
const replacement = { type: "text", content };
|
|
1296
|
+
const blocks = [];
|
|
1297
|
+
let inserted = false;
|
|
1298
|
+
for (const block of state.blocks) {
|
|
1299
|
+
if (block.type === "text") {
|
|
1300
|
+
if (content && !inserted) {
|
|
1301
|
+
blocks.push(replacement);
|
|
1302
|
+
inserted = true;
|
|
1303
|
+
}
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
blocks.push(block);
|
|
1307
|
+
}
|
|
1308
|
+
if (content && !inserted) {
|
|
1309
|
+
const toolIndex = blocks.findIndex((block) => block.type === "tool_use");
|
|
1310
|
+
blocks.splice(toolIndex < 0 ? blocks.length : toolIndex, 0, replacement);
|
|
1311
|
+
}
|
|
1312
|
+
state.blocks = blocks;
|
|
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
|
+
}
|
|
1024
1339
|
var ClientProjectionBuilder = class {
|
|
1025
1340
|
activeTurns = /* @__PURE__ */ new Map();
|
|
1026
1341
|
lastTurnIds = /* @__PURE__ */ new Map();
|
|
1027
1342
|
latestTurns = /* @__PURE__ */ new Map();
|
|
1343
|
+
turnsById = /* @__PURE__ */ new Map();
|
|
1028
1344
|
loopDescriptions = /* @__PURE__ */ new Map();
|
|
1345
|
+
/** loop_name → 正在运行的子智能体起点,agent:end 结算并挂到父工具调用上。 */
|
|
1346
|
+
activeSubagentRuns = /* @__PURE__ */ new Map();
|
|
1029
1347
|
pendingChildPauses = /* @__PURE__ */ new Map();
|
|
1030
1348
|
pendingMemoryRefs = /* @__PURE__ */ new Map();
|
|
1031
1349
|
activeCompactions = /* @__PURE__ */ new Map();
|
|
@@ -1060,6 +1378,8 @@ var ClientProjectionBuilder = class {
|
|
|
1060
1378
|
return this.onMemoryInject(loopId, payload);
|
|
1061
1379
|
case "memory:inject:none":
|
|
1062
1380
|
return null;
|
|
1381
|
+
case "context:entry":
|
|
1382
|
+
return this.onContextEntry(payload);
|
|
1063
1383
|
case "loop:turn":
|
|
1064
1384
|
return this.onLoopTurn(loopId, payload);
|
|
1065
1385
|
case "mode:change":
|
|
@@ -1083,6 +1403,25 @@ var ClientProjectionBuilder = class {
|
|
|
1083
1403
|
}
|
|
1084
1404
|
if (event.type.startsWith("compaction:"))
|
|
1085
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
|
+
}
|
|
1086
1425
|
const state = this.activeTurns.get(loopId);
|
|
1087
1426
|
if (!state) {
|
|
1088
1427
|
if (event.type === "chat:end") {
|
|
@@ -1098,13 +1437,28 @@ var ClientProjectionBuilder = class {
|
|
|
1098
1437
|
state.toolCalls = [];
|
|
1099
1438
|
state.usage = null;
|
|
1100
1439
|
state.responseDone = false;
|
|
1440
|
+
state.firstTokenMs = null;
|
|
1441
|
+
state.streamDurationMs = null;
|
|
1442
|
+
state.firstTokenDeliveryMs = null;
|
|
1101
1443
|
return this.syncPatch(state);
|
|
1102
1444
|
case "llm:thinking:delta":
|
|
1445
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1103
1446
|
return this.onDelta(state, "thinking", "", String(payload.content ?? ""));
|
|
1104
1447
|
case "llm:content:delta":
|
|
1448
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1105
1449
|
return this.onDelta(state, "text", "", String(payload.content ?? ""));
|
|
1450
|
+
case "llm:content:complete":
|
|
1451
|
+
replaceTextContent(state, String(payload.content ?? ""));
|
|
1452
|
+
return this.syncPatch(state);
|
|
1453
|
+
case "llm:content:done":
|
|
1454
|
+
this.applyStreamTiming(state, payload);
|
|
1455
|
+
return this.syncPatch(state);
|
|
1106
1456
|
case "llm:tool_call:created":
|
|
1457
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1107
1458
|
return this.onToolCallCreated(state, payload);
|
|
1459
|
+
case "llm:tool_call:arguments:delta":
|
|
1460
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1461
|
+
return this.onToolCallArgumentsDelta(state, payload);
|
|
1108
1462
|
case "llm:response:done":
|
|
1109
1463
|
return this.onResponseDone(state, loopId, payload);
|
|
1110
1464
|
case "tool:result:delta":
|
|
@@ -1131,7 +1485,9 @@ var ClientProjectionBuilder = class {
|
|
|
1131
1485
|
this.activeTurns.clear();
|
|
1132
1486
|
this.lastTurnIds.clear();
|
|
1133
1487
|
this.latestTurns.clear();
|
|
1488
|
+
this.turnsById.clear();
|
|
1134
1489
|
this.loopDescriptions.clear();
|
|
1490
|
+
this.activeSubagentRuns.clear();
|
|
1135
1491
|
this.pendingChildPauses.clear();
|
|
1136
1492
|
this.pendingMemoryRefs.clear();
|
|
1137
1493
|
this.activeCompactions.clear();
|
|
@@ -1146,15 +1502,18 @@ var ClientProjectionBuilder = class {
|
|
|
1146
1502
|
}
|
|
1147
1503
|
_seedTurns(turns) {
|
|
1148
1504
|
const latestTurns = /* @__PURE__ */ new Map();
|
|
1505
|
+
const turnsById = /* @__PURE__ */ new Map();
|
|
1149
1506
|
for (const turn of turns) {
|
|
1150
1507
|
this.seedSequence(turn.sequence);
|
|
1151
1508
|
if (!isConversationTurn(turn)) continue;
|
|
1509
|
+
turnsById.set(turn.turn_id, turn);
|
|
1152
1510
|
const current = latestTurns.get(turn.loop_id);
|
|
1153
1511
|
if (!current || turn.sequence >= current.sequence) {
|
|
1154
1512
|
latestTurns.set(turn.loop_id, turn);
|
|
1155
1513
|
}
|
|
1156
1514
|
}
|
|
1157
1515
|
this.latestTurns = latestTurns;
|
|
1516
|
+
this.turnsById = turnsById;
|
|
1158
1517
|
}
|
|
1159
1518
|
// --- Event handlers ---
|
|
1160
1519
|
onMemoryInject(loopId, payload) {
|
|
@@ -1172,11 +1531,16 @@ var ClientProjectionBuilder = class {
|
|
|
1172
1531
|
}
|
|
1173
1532
|
return null;
|
|
1174
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
|
+
}
|
|
1175
1539
|
onLoopTurn(loopId, payload) {
|
|
1176
1540
|
let turnId = String(payload.turn_id ?? this.nextTurnId(`turn:${loopId}`));
|
|
1177
1541
|
let memoryRefs = null;
|
|
1178
1542
|
const pending = this.pendingMemoryRefs.get(loopId);
|
|
1179
|
-
if (pending) {
|
|
1543
|
+
if (pending && (!pending.turnId || pending.turnId === turnId)) {
|
|
1180
1544
|
this.pendingMemoryRefs.delete(loopId);
|
|
1181
1545
|
if (pending.turnId) turnId = pending.turnId;
|
|
1182
1546
|
memoryRefs = pending.refs;
|
|
@@ -1192,6 +1556,7 @@ var ClientProjectionBuilder = class {
|
|
|
1192
1556
|
this.lastTurnIds.set(loopId, turnId);
|
|
1193
1557
|
const turn = snapshot(state, this.nextSeq(), "streaming");
|
|
1194
1558
|
this.latestTurns.set(loopId, turn);
|
|
1559
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1195
1560
|
return [{ kind: "upsert", turn }];
|
|
1196
1561
|
}
|
|
1197
1562
|
onModeChange(loopId, payload) {
|
|
@@ -1228,11 +1593,11 @@ var ClientProjectionBuilder = class {
|
|
|
1228
1593
|
loopId,
|
|
1229
1594
|
toolCallId: optStr(payload.tool_call_id) ?? void 0,
|
|
1230
1595
|
toolName: toolNameFromPayload(payload) ?? void 0,
|
|
1231
|
-
workspacePath:
|
|
1596
|
+
workspacePath: isRecord(payload.workspace_change) ? optStr(payload.workspace_change.to) ?? void 0 : void 0
|
|
1232
1597
|
}
|
|
1233
1598
|
];
|
|
1234
1599
|
const change = payload.workspace_change;
|
|
1235
|
-
if (
|
|
1600
|
+
if (isRecord(change)) {
|
|
1236
1601
|
const source = optStr(change.source) ?? "";
|
|
1237
1602
|
const projectName = optStr(change.project_name) ?? "";
|
|
1238
1603
|
const fromPath = optStr(change.from) ?? "";
|
|
@@ -1264,6 +1629,13 @@ var ClientProjectionBuilder = class {
|
|
|
1264
1629
|
onAgentStart(loopId, payload) {
|
|
1265
1630
|
const description = optStr(payload.description);
|
|
1266
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
|
+
}
|
|
1267
1639
|
const turn = buildSystemNotificationTurn(
|
|
1268
1640
|
this.nextSeq(),
|
|
1269
1641
|
this.nextTurnId("agent:start"),
|
|
@@ -1305,7 +1677,7 @@ var ClientProjectionBuilder = class {
|
|
|
1305
1677
|
this.nextTurnId("bg:tasks_completed"),
|
|
1306
1678
|
loopId,
|
|
1307
1679
|
"bg:tasks_completed",
|
|
1308
|
-
|
|
1680
|
+
backgroundTaskStatus(payload),
|
|
1309
1681
|
`Background tasks completed (${count})`,
|
|
1310
1682
|
detail || null,
|
|
1311
1683
|
{ task_ids: taskIds, count }
|
|
@@ -1325,7 +1697,32 @@ var ClientProjectionBuilder = class {
|
|
|
1325
1697
|
}
|
|
1326
1698
|
const directTurn = this.activeTurns.get(loopId) ?? this.latestTurns.get(loopId);
|
|
1327
1699
|
setToolCallStatus(directTurn, toolCallId, "done", null, null);
|
|
1328
|
-
|
|
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 }];
|
|
1329
1726
|
}
|
|
1330
1727
|
onUserMessage(_loopId, payload) {
|
|
1331
1728
|
const entryId = String(payload.entry_id ?? this.nextTurnId("user"));
|
|
@@ -1342,8 +1739,12 @@ var ClientProjectionBuilder = class {
|
|
|
1342
1739
|
tool_calls: [],
|
|
1343
1740
|
model: null,
|
|
1344
1741
|
usage: null,
|
|
1345
|
-
duration_ms: 0
|
|
1742
|
+
duration_ms: 0,
|
|
1743
|
+
client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
|
|
1346
1744
|
};
|
|
1745
|
+
if (payload.start_reason === "background-completion") {
|
|
1746
|
+
turn.blocks.push({ type: "system_notification", content: { notification_type: "bg:wakeup" } });
|
|
1747
|
+
}
|
|
1347
1748
|
this.latestTurns.set("root", turn);
|
|
1348
1749
|
return [{ kind: "upsert", turn }];
|
|
1349
1750
|
}
|
|
@@ -1370,13 +1771,7 @@ var ClientProjectionBuilder = class {
|
|
|
1370
1771
|
}
|
|
1371
1772
|
onToolCallCreated(state, payload) {
|
|
1372
1773
|
if ("arguments_delta" in payload) {
|
|
1373
|
-
|
|
1374
|
-
const toolCallId2 = String(payload.id ?? "");
|
|
1375
|
-
if (delta && toolCallId2) {
|
|
1376
|
-
appendToolCallArguments(state, toolCallId2, delta);
|
|
1377
|
-
return this.syncPatch(state);
|
|
1378
|
-
}
|
|
1379
|
-
return null;
|
|
1774
|
+
return this.onToolCallArgumentsDelta(state, payload);
|
|
1380
1775
|
}
|
|
1381
1776
|
const toolCallId = String(payload.id ?? "");
|
|
1382
1777
|
const toolName = toolNameFromPayload(payload);
|
|
@@ -1385,8 +1780,16 @@ var ClientProjectionBuilder = class {
|
|
|
1385
1780
|
upsertToolCall(state, toolCallId, toolName, displayName, args);
|
|
1386
1781
|
return this.syncPatch(state);
|
|
1387
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
|
+
}
|
|
1388
1790
|
onResponseDone(state, loopId, payload) {
|
|
1389
1791
|
state.responseDone = true;
|
|
1792
|
+
this.applyStreamTiming(state, payload);
|
|
1390
1793
|
const model = optStr(payload.model);
|
|
1391
1794
|
if (model) state.model = model;
|
|
1392
1795
|
if (typeof payload.usage === "object" && payload.usage !== null) {
|
|
@@ -1409,11 +1812,42 @@ var ClientProjectionBuilder = class {
|
|
|
1409
1812
|
);
|
|
1410
1813
|
}
|
|
1411
1814
|
}
|
|
1412
|
-
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
|
+
)) {
|
|
1413
1822
|
return this.finalize(loopId, "completed");
|
|
1414
1823
|
}
|
|
1415
1824
|
return this.syncPatch(state);
|
|
1416
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
|
+
}
|
|
1417
1851
|
onToolResultDone(state, loopId, payload) {
|
|
1418
1852
|
const toolCallId = String(payload.tool_call_id ?? "");
|
|
1419
1853
|
const streamedResult = state.toolCalls.find((toolCall) => toolCall.id === toolCallId)?.result;
|
|
@@ -1421,9 +1855,12 @@ var ClientProjectionBuilder = class {
|
|
|
1421
1855
|
const status = toolStatusFromResult(content);
|
|
1422
1856
|
const rawDuration = payload.duration_ms;
|
|
1423
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;
|
|
1424
1859
|
const sl = sourceLoopFor(state.loopId, this.loopDescriptions);
|
|
1425
|
-
|
|
1426
|
-
|
|
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)) {
|
|
1427
1864
|
return this.finalize(loopId, "completed");
|
|
1428
1865
|
}
|
|
1429
1866
|
return this.syncPatch(state);
|
|
@@ -1452,10 +1889,13 @@ var ClientProjectionBuilder = class {
|
|
|
1452
1889
|
onToolBridge(state, payload) {
|
|
1453
1890
|
const bridge = payload.bridge;
|
|
1454
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;
|
|
1455
1894
|
state.blocks.push({
|
|
1456
1895
|
type: "tool_bridge",
|
|
1457
1896
|
content: bridge,
|
|
1458
|
-
|
|
1897
|
+
delivery_id: String(payload.delivery_id ?? "") || null,
|
|
1898
|
+
tool_call_id: toolCallId
|
|
1459
1899
|
});
|
|
1460
1900
|
return this.syncPatch(state);
|
|
1461
1901
|
}
|
|
@@ -1464,6 +1904,11 @@ var ClientProjectionBuilder = class {
|
|
|
1464
1904
|
const toolName = String(payload.tool_name ?? "");
|
|
1465
1905
|
const displayName = this.displayNameResolver(toolName);
|
|
1466
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
|
+
}
|
|
1467
1912
|
return this.syncPatch(state);
|
|
1468
1913
|
}
|
|
1469
1914
|
onChildPause(state, loopId, payload) {
|
|
@@ -1569,6 +2014,7 @@ var ClientProjectionBuilder = class {
|
|
|
1569
2014
|
}
|
|
1570
2015
|
onAgentEnd(state, loopId, payload) {
|
|
1571
2016
|
const ok = payload.ok !== false;
|
|
2017
|
+
this.settleSubagentRun(loopId, payload);
|
|
1572
2018
|
const status = ok ? "done" : "error";
|
|
1573
2019
|
const title = `Agent: ${payload.description ?? payload.skill_id ?? "agent"}`;
|
|
1574
2020
|
const detail = optStr(payload.error);
|
|
@@ -1576,6 +2022,7 @@ var ClientProjectionBuilder = class {
|
|
|
1576
2022
|
skill_id: optStr(payload.skill_id),
|
|
1577
2023
|
description: optStr(payload.description),
|
|
1578
2024
|
parent_fork_tool_call_id: optStr(payload.parent_fork_tool_call_id),
|
|
2025
|
+
...typeof payload.run_status === "string" ? { run_status: payload.run_status } : {},
|
|
1579
2026
|
ok
|
|
1580
2027
|
};
|
|
1581
2028
|
if (state) {
|
|
@@ -1595,6 +2042,32 @@ var ClientProjectionBuilder = class {
|
|
|
1595
2042
|
);
|
|
1596
2043
|
return [{ kind: "upsert", turn }];
|
|
1597
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
|
+
}
|
|
1598
2071
|
dropIncompleteToolCalls(state) {
|
|
1599
2072
|
if (state.responseDone) return;
|
|
1600
2073
|
state.toolCalls = [];
|
|
@@ -1644,6 +2117,7 @@ var ClientProjectionBuilder = class {
|
|
|
1644
2117
|
syncPatch(state) {
|
|
1645
2118
|
const turn = snapshot(state, this.nextSeq(), "streaming");
|
|
1646
2119
|
this.latestTurns.set(state.loopId, turn);
|
|
2120
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1647
2121
|
this.lastTurnIds.set(state.loopId, turn.turn_id);
|
|
1648
2122
|
return [{ kind: "upsert", turn }];
|
|
1649
2123
|
}
|
|
@@ -1654,6 +2128,7 @@ var ClientProjectionBuilder = class {
|
|
|
1654
2128
|
const turn = snapshot(state, this.nextSeq(), status);
|
|
1655
2129
|
this.lastTurnIds.set(loopId, turn.turn_id);
|
|
1656
2130
|
this.latestTurns.set(loopId, turn);
|
|
2131
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1657
2132
|
return [{ kind: "upsert", turn }];
|
|
1658
2133
|
}
|
|
1659
2134
|
shouldFinalizeAfterToolResults(state) {
|
|
@@ -1691,9 +2166,6 @@ var ClientProjectionBuilder = class {
|
|
|
1691
2166
|
return `${prefix}:${this.syntheticCounter}:${hex}`;
|
|
1692
2167
|
}
|
|
1693
2168
|
};
|
|
1694
|
-
function isRecord2(value) {
|
|
1695
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1696
|
-
}
|
|
1697
2169
|
function optStr(value) {
|
|
1698
2170
|
if (value == null || value === "") return null;
|
|
1699
2171
|
return String(value);
|
|
@@ -1713,6 +2185,101 @@ function toolArgsFromPayload(payload) {
|
|
|
1713
2185
|
return "";
|
|
1714
2186
|
}
|
|
1715
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
|
+
|
|
1716
2283
|
// src/shared/projection/history.ts
|
|
1717
2284
|
function projectHistory(entries) {
|
|
1718
2285
|
const turns = [];
|
|
@@ -1720,6 +2287,8 @@ function projectHistory(entries) {
|
|
|
1720
2287
|
const latestAssistantByLoop = /* @__PURE__ */ new Map();
|
|
1721
2288
|
const pendingChildPauses = /* @__PURE__ */ new Map();
|
|
1722
2289
|
const answeredToolCallIds = /* @__PURE__ */ new Set();
|
|
2290
|
+
const nativeToolTurns = /* @__PURE__ */ new Set();
|
|
2291
|
+
const turnEntryAliases = /* @__PURE__ */ new Map();
|
|
1723
2292
|
let latestRootMessage = null;
|
|
1724
2293
|
let latestRootAssistant = null;
|
|
1725
2294
|
let sequence = 0;
|
|
@@ -1727,7 +2296,40 @@ function projectHistory(entries) {
|
|
|
1727
2296
|
const kind = String(entry.kind ?? "");
|
|
1728
2297
|
const loopId = String(entry.loop_name ?? "root");
|
|
1729
2298
|
const id = String(entry.id ?? `history-${sequence + 1}`);
|
|
1730
|
-
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)) {
|
|
1731
2333
|
const assistantEntryId = String(entry.data.assistant_entry_id ?? "");
|
|
1732
2334
|
if (latestRootMessage?.role === "assistant" && latestRootMessage.turn_id === assistantEntryId) {
|
|
1733
2335
|
const followup = normalizePostChatFollowup(entry.data, assistantEntryId);
|
|
@@ -1735,7 +2337,15 @@ function projectHistory(entries) {
|
|
|
1735
2337
|
}
|
|
1736
2338
|
continue;
|
|
1737
2339
|
}
|
|
1738
|
-
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)) {
|
|
1739
2349
|
const message = entry.message;
|
|
1740
2350
|
const role = String(message.role ?? "");
|
|
1741
2351
|
if (role === "tool") {
|
|
@@ -1745,6 +2355,12 @@ function projectHistory(entries) {
|
|
|
1745
2355
|
if (owner && toolCall) {
|
|
1746
2356
|
toolCall.status = message._error ? "error" : inferToolStatus(String(message.content ?? ""));
|
|
1747
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;
|
|
1748
2364
|
owner.blocks.push({
|
|
1749
2365
|
type: "tool_result",
|
|
1750
2366
|
content: message.content ?? "",
|
|
@@ -1754,8 +2370,11 @@ function projectHistory(entries) {
|
|
|
1754
2370
|
continue;
|
|
1755
2371
|
}
|
|
1756
2372
|
if (role !== "user" && role !== "assistant" && role !== "system") continue;
|
|
1757
|
-
const toolCalls = role === "assistant" ? buildToolCalls(message.tool_calls) : [];
|
|
2373
|
+
const toolCalls = role === "assistant" && !message._error ? buildToolCalls(message.tool_calls) : [];
|
|
1758
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
|
+
}
|
|
1759
2378
|
const turn = {
|
|
1760
2379
|
id,
|
|
1761
2380
|
sequence: ++sequence,
|
|
@@ -1767,14 +2386,52 @@ function projectHistory(entries) {
|
|
|
1767
2386
|
blocks,
|
|
1768
2387
|
tool_calls: toolCalls,
|
|
1769
2388
|
model: stringOrNull(message.model),
|
|
1770
|
-
usage:
|
|
1771
|
-
|
|
1772
|
-
|
|
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)
|
|
1773
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
|
+
}
|
|
1774
2429
|
if (loopId === "root" && (role === "user" || role === "assistant")) {
|
|
1775
2430
|
latestRootMessage = turn;
|
|
1776
2431
|
}
|
|
1777
2432
|
turns.push(turn);
|
|
2433
|
+
turnEntryAliases.set(id, turn);
|
|
2434
|
+
if (isNativeToolEntry) nativeToolTurns.add(turn);
|
|
1778
2435
|
if (role === "assistant") latestAssistantByLoop.set(loopId, turn);
|
|
1779
2436
|
if (loopId === "root") {
|
|
1780
2437
|
if (role === "assistant") latestRootAssistant = turn;
|
|
@@ -1783,10 +2440,25 @@ function projectHistory(entries) {
|
|
|
1783
2440
|
for (const toolCall of toolCalls) toolOwners.set(toolCall.id, turn);
|
|
1784
2441
|
continue;
|
|
1785
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
|
+
}
|
|
1786
2458
|
if (kind === "mode_change") {
|
|
1787
2459
|
turns.push(markerTurn(id, ++sequence, loopId, "mode_change", entry.data ?? {}));
|
|
1788
2460
|
} else if (kind === "workspace_change") {
|
|
1789
|
-
const change =
|
|
2461
|
+
const change = isRecord(entry.data) ? entry.data : {};
|
|
1790
2462
|
const source = stringOrNull(change.source) ?? "";
|
|
1791
2463
|
const projectName = stringOrNull(change.project_name) ?? "";
|
|
1792
2464
|
const fromPath = stringOrNull(change.from) ?? "";
|
|
@@ -1815,19 +2487,22 @@ function projectHistory(entries) {
|
|
|
1815
2487
|
} else if (kind === "tool_ui" || kind === "tool_bridge") {
|
|
1816
2488
|
const data = entry.data ?? {};
|
|
1817
2489
|
const toolCallId = String(data.tool_call_id ?? "");
|
|
1818
|
-
const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
|
|
1819
|
-
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;
|
|
1820
2494
|
if (owner && content !== void 0) {
|
|
1821
2495
|
owner.blocks.push({
|
|
1822
2496
|
type: kind,
|
|
1823
2497
|
content,
|
|
2498
|
+
...kind === "tool_bridge" ? { delivery_id: id } : {},
|
|
1824
2499
|
tool_call_id: toolCallId || null
|
|
1825
2500
|
});
|
|
1826
2501
|
}
|
|
1827
2502
|
} else if (kind === "compaction" || kind === "tool_result_archive") {
|
|
1828
2503
|
const data = entry.data ?? {};
|
|
1829
2504
|
const compactionId = String(data.compaction_id ?? entry.id ?? `history-${sequence + 1}`);
|
|
1830
|
-
let content =
|
|
2505
|
+
let content = isRecord(entry.message) ? entry.message.content : void 0;
|
|
1831
2506
|
if (kind === "tool_result_archive") {
|
|
1832
2507
|
const archivedCount = Array.isArray(data.archived_files) ? data.archived_files.length : 0;
|
|
1833
2508
|
content = `<compaction-summary>
|
|
@@ -1835,18 +2510,24 @@ function projectHistory(entries) {
|
|
|
1835
2510
|
</compaction-summary>`;
|
|
1836
2511
|
}
|
|
1837
2512
|
turns.push(
|
|
1838
|
-
buildCompactionTurn(
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2513
|
+
buildCompactionTurn(
|
|
2514
|
+
++sequence,
|
|
2515
|
+
compactionId,
|
|
2516
|
+
loopId,
|
|
2517
|
+
data.status === "interrupted" ? "interrupted" : data.status === "failed" ? "failed" : "completed",
|
|
2518
|
+
{
|
|
2519
|
+
...data,
|
|
2520
|
+
content
|
|
2521
|
+
}
|
|
2522
|
+
)
|
|
1842
2523
|
);
|
|
1843
2524
|
} else if (kind === "child_pause") {
|
|
1844
2525
|
const data = entry.data ?? {};
|
|
1845
|
-
const sourceLoop =
|
|
2526
|
+
const sourceLoop = isRecord(data.source_loop) ? data.source_loop : {};
|
|
1846
2527
|
const childLoopId = String(data.child_loop_name ?? sourceLoop.name ?? "");
|
|
1847
2528
|
const childToolCallId = String(data.child_pause_tool_call_id ?? "");
|
|
1848
2529
|
const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
|
|
1849
|
-
const pauseToolData =
|
|
2530
|
+
const pauseToolData = isRecord(data.pause_tool_data) ? data.pause_tool_data : {};
|
|
1850
2531
|
const description = String(sourceLoop.description ?? "");
|
|
1851
2532
|
applyAskUserPauseToTurn(
|
|
1852
2533
|
latestAssistantByLoop.get(childLoopId),
|
|
@@ -1934,15 +2615,15 @@ function parseToolArguments(value) {
|
|
|
1934
2615
|
if (!value) return {};
|
|
1935
2616
|
try {
|
|
1936
2617
|
const parsed = JSON.parse(value);
|
|
1937
|
-
return
|
|
2618
|
+
return isRecord(parsed) ? parsed : {};
|
|
1938
2619
|
} catch {
|
|
1939
2620
|
return {};
|
|
1940
2621
|
}
|
|
1941
2622
|
}
|
|
1942
2623
|
function buildToolCalls(value) {
|
|
1943
2624
|
if (!Array.isArray(value)) return [];
|
|
1944
|
-
return value.filter(
|
|
1945
|
-
const fn =
|
|
2625
|
+
return value.filter(isRecord).map((raw) => {
|
|
2626
|
+
const fn = isRecord(raw.function) ? raw.function : {};
|
|
1946
2627
|
const name = String(fn.name ?? "");
|
|
1947
2628
|
return {
|
|
1948
2629
|
id: String(raw.id ?? ""),
|
|
@@ -1958,23 +2639,40 @@ function buildBlocks(message, toolCalls) {
|
|
|
1958
2639
|
const displayContent = message.role === "user" ? message._display_content : void 0;
|
|
1959
2640
|
const stored = message._blocks;
|
|
1960
2641
|
const blocks = [];
|
|
2642
|
+
const storedToolCallIds = /* @__PURE__ */ new Set();
|
|
1961
2643
|
if (displayContent !== void 0) {
|
|
1962
2644
|
blocks.push({ type: "text", content: displayContent });
|
|
1963
2645
|
} else if (Array.isArray(stored)) {
|
|
1964
2646
|
for (const raw of stored) {
|
|
1965
|
-
if (!
|
|
2647
|
+
if (!isRecord(raw)) continue;
|
|
1966
2648
|
const type3 = String(raw.type ?? "");
|
|
1967
2649
|
if (type3 === "thinking") blocks.push({ type: type3, content: raw.thinking ?? raw.content ?? "" });
|
|
1968
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
|
+
}
|
|
1969
2664
|
}
|
|
1970
2665
|
} else if (message.content !== void 0 && message.content !== "") {
|
|
1971
2666
|
blocks.push({ type: "text", content: message.content });
|
|
1972
2667
|
}
|
|
1973
2668
|
for (const toolCall of toolCalls) {
|
|
2669
|
+
if (storedToolCallIds.has(toolCall.id)) continue;
|
|
1974
2670
|
blocks.push({
|
|
1975
2671
|
type: "tool_use",
|
|
1976
|
-
content:
|
|
1977
|
-
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
|
|
1978
2676
|
});
|
|
1979
2677
|
}
|
|
1980
2678
|
return blocks;
|
|
@@ -1995,14 +2693,11 @@ function markerTurn(id, sequence, loopId, type3, content, toolCallId = null) {
|
|
|
1995
2693
|
duration_ms: 0
|
|
1996
2694
|
};
|
|
1997
2695
|
}
|
|
1998
|
-
function isRecord3(value) {
|
|
1999
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2000
|
-
}
|
|
2001
2696
|
function stringOrNull(value) {
|
|
2002
2697
|
return typeof value === "string" ? value : null;
|
|
2003
2698
|
}
|
|
2004
|
-
function
|
|
2005
|
-
return typeof value === "number" ? value :
|
|
2699
|
+
function numberOrNull(value) {
|
|
2700
|
+
return typeof value === "number" && value >= 0 ? value : null;
|
|
2006
2701
|
}
|
|
2007
2702
|
function findLatestAssistant(turns, loopId) {
|
|
2008
2703
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
@@ -2013,6 +2708,10 @@ function findLatestAssistant(turns, loopId) {
|
|
|
2013
2708
|
}
|
|
2014
2709
|
|
|
2015
2710
|
// src/resources/sessions.ts
|
|
2711
|
+
function isEntryGoneError(error) {
|
|
2712
|
+
const status = error?.status;
|
|
2713
|
+
return status === 404 || status === 409;
|
|
2714
|
+
}
|
|
2016
2715
|
function toCreateSessionPayload(request) {
|
|
2017
2716
|
const solutionRef = request.solution_ref ?? externalSolutionRef(request.solution_id);
|
|
2018
2717
|
return {
|
|
@@ -2031,13 +2730,14 @@ function toCreateSessionPayload(request) {
|
|
|
2031
2730
|
solution_snapshot_session_id: request.solution_snapshot_session_id ?? null,
|
|
2032
2731
|
compaction_ratio: request.compaction_ratio ?? null,
|
|
2033
2732
|
...request.runtime_type ? { runtime_type: request.runtime_type } : {},
|
|
2034
|
-
...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 } : {}
|
|
2035
2736
|
};
|
|
2036
2737
|
}
|
|
2037
2738
|
var builtinSolutionIds = /* @__PURE__ */ new Set([
|
|
2038
2739
|
"app-dev",
|
|
2039
2740
|
"general_chat",
|
|
2040
|
-
"multica",
|
|
2041
2741
|
"night_build",
|
|
2042
2742
|
"skill_editor",
|
|
2043
2743
|
"smart_bid",
|
|
@@ -2060,6 +2760,54 @@ var SessionsResource = class {
|
|
|
2060
2760
|
this.client = client;
|
|
2061
2761
|
}
|
|
2062
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
|
+
}
|
|
2063
2811
|
/**
|
|
2064
2812
|
* 连接一个已存在的会话,返回实时会话对象 AgentSession
|
|
2065
2813
|
* (自动完成历史加载、Socket.IO 订阅、断线重连补数)。
|
|
@@ -2067,6 +2815,56 @@ var SessionsResource = class {
|
|
|
2067
2815
|
connect(sessionId) {
|
|
2068
2816
|
return this.client.hub.connect(sessionId);
|
|
2069
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
|
+
}
|
|
2070
2868
|
/** 创建新会话并直接连接,返回可收发消息的 AgentSession。 */
|
|
2071
2869
|
async create(request = {}) {
|
|
2072
2870
|
if (isSessionDefinition(request)) {
|
|
@@ -2163,7 +2961,8 @@ var SessionsResource = class {
|
|
|
2163
2961
|
template_id_prefix,
|
|
2164
2962
|
solution_id,
|
|
2165
2963
|
solution_registry_id,
|
|
2166
|
-
q
|
|
2964
|
+
q,
|
|
2965
|
+
chat_group_id
|
|
2167
2966
|
}) {
|
|
2168
2967
|
const searchParams = new URLSearchParams();
|
|
2169
2968
|
searchParams.set("limit", String(limit));
|
|
@@ -2176,6 +2975,7 @@ var SessionsResource = class {
|
|
|
2176
2975
|
}
|
|
2177
2976
|
const trimmedQuery = q?.trim();
|
|
2178
2977
|
if (trimmedQuery) searchParams.set("q", trimmedQuery);
|
|
2978
|
+
if (chat_group_id) searchParams.set("chat_group_id", chat_group_id);
|
|
2179
2979
|
return this.client.json("GET", `/api/sessions?${searchParams.toString()}`);
|
|
2180
2980
|
}
|
|
2181
2981
|
async listSessions(template_id_prefix, solution_id) {
|
|
@@ -2195,6 +2995,11 @@ var SessionsResource = class {
|
|
|
2195
2995
|
listSessionsWithSkillData() {
|
|
2196
2996
|
return this.client.json("GET", "/api/sessions/with-skill-data");
|
|
2197
2997
|
}
|
|
2998
|
+
searchOwnedContent(params) {
|
|
2999
|
+
const searchParams = new URLSearchParams({ q: params.q.trim() });
|
|
3000
|
+
if (params.limit !== void 0) searchParams.set("limit", String(params.limit));
|
|
3001
|
+
return this.client.json("GET", `/api/search?${searchParams.toString()}`);
|
|
3002
|
+
}
|
|
2198
3003
|
createSession(intent, template_id, primary_skill_id) {
|
|
2199
3004
|
return this.createSessionWithRequest({ intent, template_id, primary_skill_id });
|
|
2200
3005
|
}
|
|
@@ -2261,9 +3066,68 @@ var SessionsResource = class {
|
|
|
2261
3066
|
);
|
|
2262
3067
|
return projectHistory(entries);
|
|
2263
3068
|
}
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
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
|
+
}
|
|
3118
|
+
listResultFeedback(sessionId) {
|
|
3119
|
+
return this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/feedback`);
|
|
3120
|
+
}
|
|
3121
|
+
putResultFeedback(sessionId, assistantEntryId, feedback) {
|
|
3122
|
+
return this.client.json(
|
|
3123
|
+
"PUT",
|
|
3124
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/turns/${encodeURIComponent(assistantEntryId)}/feedback`,
|
|
3125
|
+
feedback
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
3128
|
+
getSessionContextStats(sessionId, init) {
|
|
3129
|
+
return this.client.jsonFromInit(`/api/sessions/${sessionId}/context-stats`, init);
|
|
3130
|
+
}
|
|
2267
3131
|
getSessionHistory(sessionId, init) {
|
|
2268
3132
|
return this.client.jsonFromInit(`/api/sessions/${sessionId}/history`, init);
|
|
2269
3133
|
}
|
|
@@ -2318,8 +3182,9 @@ var SessionsResource = class {
|
|
|
2318
3182
|
`/api/sessions/${sessionId}/probe-url?url=${encodeURIComponent(url)}`
|
|
2319
3183
|
);
|
|
2320
3184
|
}
|
|
2321
|
-
listDir(sessionId, dirPath) {
|
|
2322
|
-
|
|
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}`);
|
|
2323
3188
|
}
|
|
2324
3189
|
async countFiles(sessionId, dirPath = "") {
|
|
2325
3190
|
const entries = await this.listDir(sessionId, dirPath);
|
|
@@ -2514,6 +3379,130 @@ function profileToCreateSessionRequest(profile) {
|
|
|
2514
3379
|
};
|
|
2515
3380
|
}
|
|
2516
3381
|
|
|
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
|
+
|
|
2517
3506
|
// src/commands/registry.ts
|
|
2518
3507
|
var BUFFER_LIMIT_PER_ACTION = 20;
|
|
2519
3508
|
var CommandRegistry = class {
|
|
@@ -2622,6 +3611,172 @@ var Emitter = class {
|
|
|
2622
3611
|
}
|
|
2623
3612
|
};
|
|
2624
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
|
+
|
|
2625
3780
|
// src/schemas/message-utils.ts
|
|
2626
3781
|
var SYSTEM_REMINDER_PATTERN = /^<system-reminder>\s*[\s\S]*?\s*<\/system-reminder>$/i;
|
|
2627
3782
|
var SYSTEM_NOTIFICATION_PATTERN = /^<system-notification>\s*[\s\S]*?\s*<\/system-notification>$/i;
|
|
@@ -2630,6 +3785,36 @@ var LEGACY_FORK_CONTEXT_MARKERS = [
|
|
|
2630
3785
|
"\u53EF\u901A\u8FC7 `read_file` \u8BFB\u53D6\u3002",
|
|
2631
3786
|
"# \u4F60\u7684\u4EFB\u52A1"
|
|
2632
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
|
+
}
|
|
2633
3818
|
function normalizeMessageContent(content) {
|
|
2634
3819
|
if (typeof content === "string") return content;
|
|
2635
3820
|
if (Array.isArray(content)) return content;
|
|
@@ -2649,10 +3834,10 @@ function isLegacyForkContextText(text) {
|
|
|
2649
3834
|
const normalized = text.trim();
|
|
2650
3835
|
return LEGACY_FORK_CONTEXT_MARKERS.every((marker) => normalized.includes(marker));
|
|
2651
3836
|
}
|
|
2652
|
-
function isHiddenInternalMessage(message) {
|
|
3837
|
+
function isHiddenInternalMessage(message, options = {}) {
|
|
2653
3838
|
if (message.role !== "user") return false;
|
|
2654
3839
|
const text = getTextContent(normalizeMessageContent(message.content));
|
|
2655
|
-
return isSystemReminderText(text) || isSystemNotificationText(text) || isLegacyForkContextText(text);
|
|
3840
|
+
return isSystemReminderText(text) || isSystemNotificationText(text) || (options.includeLegacyForkContext ?? true) && isLegacyForkContextText(text);
|
|
2656
3841
|
}
|
|
2657
3842
|
function buildMessageContent(text, attachments) {
|
|
2658
3843
|
if (attachments.length === 0) return text;
|
|
@@ -2775,6 +3960,12 @@ function groupMessagesByLoop(messages) {
|
|
|
2775
3960
|
}
|
|
2776
3961
|
return groups;
|
|
2777
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
|
+
}
|
|
2778
3969
|
|
|
2779
3970
|
// src/session/state.ts
|
|
2780
3971
|
function toReplaySnapshot(raw) {
|
|
@@ -2799,7 +3990,10 @@ function createInitialSessionState(sessionId) {
|
|
|
2799
3990
|
agentLoops: {},
|
|
2800
3991
|
activeCompaction: null,
|
|
2801
3992
|
replay: null,
|
|
2802
|
-
|
|
3993
|
+
queue: EMPTY_SESSION_QUEUE,
|
|
3994
|
+
viewerRole: null,
|
|
3995
|
+
nextBefore: null,
|
|
3996
|
+
loadingOlder: false
|
|
2803
3997
|
};
|
|
2804
3998
|
}
|
|
2805
3999
|
function parseAgentDescription(argumentsJson) {
|
|
@@ -2825,10 +4019,10 @@ function inferLoopStatusFromMessages(messages) {
|
|
|
2825
4019
|
}
|
|
2826
4020
|
function inferLoopStatusFromTurns(turns, messages) {
|
|
2827
4021
|
const latestAgentNotification = [...turns].reverse().flatMap((turn) => turn.blocks).find((block) => {
|
|
2828
|
-
if (block.type !== "system_notification" || !
|
|
4022
|
+
if (block.type !== "system_notification" || !isRecord(block.content)) return false;
|
|
2829
4023
|
return block.content.notification_type === "agent:start" || block.content.notification_type === "agent:end";
|
|
2830
4024
|
});
|
|
2831
|
-
if (latestAgentNotification?.type === "system_notification" &&
|
|
4025
|
+
if (latestAgentNotification?.type === "system_notification" && isRecord(latestAgentNotification.content)) {
|
|
2832
4026
|
const notificationType = latestAgentNotification.content.notification_type;
|
|
2833
4027
|
const status = latestAgentNotification.content.status;
|
|
2834
4028
|
if (notificationType === "agent:start" || status === "running") return "running";
|
|
@@ -2837,17 +4031,44 @@ function inferLoopStatusFromTurns(turns, messages) {
|
|
|
2837
4031
|
}
|
|
2838
4032
|
return inferLoopStatusFromMessages(messages);
|
|
2839
4033
|
}
|
|
2840
|
-
function
|
|
2841
|
-
|
|
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;
|
|
2842
4063
|
}
|
|
2843
4064
|
function parentForkToolCallIdFromTurn(turn) {
|
|
2844
4065
|
if (typeof turn.parent_fork_tool_call_id === "string" && turn.parent_fork_tool_call_id.length > 0) {
|
|
2845
4066
|
return turn.parent_fork_tool_call_id;
|
|
2846
4067
|
}
|
|
2847
4068
|
for (const block of turn.blocks) {
|
|
2848
|
-
if (block.type !== "system_notification" || !
|
|
4069
|
+
if (block.type !== "system_notification" || !isRecord(block.content)) continue;
|
|
2849
4070
|
const metadata = block.content.metadata;
|
|
2850
|
-
if (!
|
|
4071
|
+
if (!isRecord(metadata)) continue;
|
|
2851
4072
|
const parentId = metadata.parent_fork_tool_call_id;
|
|
2852
4073
|
if (typeof parentId === "string" && parentId.length > 0) return parentId;
|
|
2853
4074
|
}
|
|
@@ -2864,16 +4085,16 @@ function buildMessageContent2(turn) {
|
|
|
2864
4085
|
}
|
|
2865
4086
|
function workspaceNotificationContent(turn) {
|
|
2866
4087
|
const block = turn.blocks.find(
|
|
2867
|
-
(candidate) => candidate.type === "system_notification" &&
|
|
4088
|
+
(candidate) => candidate.type === "system_notification" && isRecord(candidate.content) && candidate.content.notification_type === "workspace_change"
|
|
2868
4089
|
);
|
|
2869
|
-
if (!block || !
|
|
4090
|
+
if (!block || !isRecord(block.content)) return "";
|
|
2870
4091
|
const title = typeof block.content.title === "string" ? block.content.title.trim() : "";
|
|
2871
4092
|
const detail = typeof block.content.detail === "string" ? block.content.detail.trim() : "";
|
|
2872
4093
|
return [title ? `**${title}**` : "", detail].filter(Boolean).join("\n\n");
|
|
2873
4094
|
}
|
|
2874
4095
|
function askUserAnswerContent(turn) {
|
|
2875
4096
|
const answerBlock = turn.blocks.find((block) => block.type === "ask_user_answer");
|
|
2876
|
-
if (!answerBlock || !
|
|
4097
|
+
if (!answerBlock || !isRecord(answerBlock.content)) return null;
|
|
2877
4098
|
const answer = answerBlock.content.answer;
|
|
2878
4099
|
return typeof answer === "string" && answer.trim().length > 0 ? answer : null;
|
|
2879
4100
|
}
|
|
@@ -2893,6 +4114,19 @@ function extractModeFromBlocks(blocks) {
|
|
|
2893
4114
|
return null;
|
|
2894
4115
|
}
|
|
2895
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
|
+
}
|
|
2896
4130
|
if (turn.kind === "compaction" && turn.compaction_id) {
|
|
2897
4131
|
return {
|
|
2898
4132
|
role: "assistant",
|
|
@@ -3062,6 +4296,62 @@ function orderTurns(turns) {
|
|
|
3062
4296
|
}
|
|
3063
4297
|
return turns;
|
|
3064
4298
|
}
|
|
4299
|
+
function reconcileOptimisticUserTurns(currentTurns, incomingTurns) {
|
|
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)
|
|
4338
|
+
);
|
|
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]));
|
|
4346
|
+
return incomingTurns.map((turn) => {
|
|
4347
|
+
const current = currentById.get(turn.turn_id);
|
|
4348
|
+
if (current) {
|
|
4349
|
+
const currentRenderId = current.client_render_id;
|
|
4350
|
+
return turn.client_render_id || !currentRenderId ? turn : { ...turn, client_render_id: currentRenderId };
|
|
4351
|
+
}
|
|
4352
|
+
return turn;
|
|
4353
|
+
});
|
|
4354
|
+
}
|
|
3065
4355
|
function areAgentLoopsEqual(left, right) {
|
|
3066
4356
|
if (!left) return Object.keys(right).length === 0;
|
|
3067
4357
|
const leftEntries = Object.entries(left);
|
|
@@ -3073,7 +4363,7 @@ function areAgentLoopsEqual(left, right) {
|
|
|
3073
4363
|
});
|
|
3074
4364
|
}
|
|
3075
4365
|
function withTurns(state, turns) {
|
|
3076
|
-
const orderedTurns = orderTurns(turns);
|
|
4366
|
+
const orderedTurns = orderTurns(reconcileHistoricalUserTurns(state.turns, turns));
|
|
3077
4367
|
const { messages, agentLoops, activeCompaction } = materialize(orderedTurns);
|
|
3078
4368
|
const isWaitingForInput = orderedTurns.some(
|
|
3079
4369
|
(turn) => turn.tool_calls.some((toolCall) => toolCall.status === "awaiting_answer")
|
|
@@ -3083,28 +4373,53 @@ function withTurns(state, turns) {
|
|
|
3083
4373
|
const preservedErrors = lastTurnId ? state.messages.filter(
|
|
3084
4374
|
(m) => m.role === "error" && typeof m.entry_id === "string" && m.entry_id.startsWith(`${ERROR_ANCHOR_PREFIX}${lastTurnId}:`)
|
|
3085
4375
|
) : [];
|
|
3086
|
-
const
|
|
4376
|
+
const turnById = new Map(orderedTurns.map((turn) => [turn.turn_id, turn]));
|
|
4377
|
+
const messagesWithRenderIds = messages.map((message) => {
|
|
4378
|
+
if (!message.entry_id) return message;
|
|
4379
|
+
const renderId = turnById.get(message.entry_id)?.client_render_id;
|
|
4380
|
+
return renderId ? { ...message, render_id: renderId } : message;
|
|
4381
|
+
});
|
|
4382
|
+
const mergedMessages = preservedErrors.length > 0 ? [...messagesWithRenderIds, ...preservedErrors] : messagesWithRenderIds;
|
|
3087
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
|
+
}
|
|
3088
4394
|
return {
|
|
3089
4395
|
...state,
|
|
3090
4396
|
turns: orderedTurns,
|
|
3091
4397
|
messages: mergedMessages,
|
|
3092
4398
|
agentLoops: stableAgentLoops,
|
|
3093
4399
|
activeCompaction,
|
|
4400
|
+
askAnswers,
|
|
3094
4401
|
mode: latestMode ?? state.mode,
|
|
3095
4402
|
// 历史恢复时 chat:end(paused) 可能已不在 socket replay 缓存中;
|
|
3096
4403
|
// awaiting_answer 是投影里的持久权威状态,必须据此恢复问答交互。
|
|
3097
4404
|
status: isWaitingForInput ? "waiting_for_input" : state.status
|
|
3098
4405
|
};
|
|
3099
4406
|
}
|
|
4407
|
+
function withQueue(state, snapshot2) {
|
|
4408
|
+
if (!shouldApplySnapshot(state.queue, snapshot2)) return state;
|
|
4409
|
+
return { ...state, queue: snapshot2 };
|
|
4410
|
+
}
|
|
3100
4411
|
var localTurnCounter = 0;
|
|
3101
|
-
function addUserMessage(state, content) {
|
|
4412
|
+
function addUserMessage(state, content, clientRequestId) {
|
|
3102
4413
|
localTurnCounter += 1;
|
|
3103
|
-
const turnId = `local-user-${Date.now()}-${localTurnCounter}`;
|
|
4414
|
+
const turnId = `local-user-${clientRequestId ?? `${Date.now()}-${localTurnCounter}`}`;
|
|
3104
4415
|
const optimisticTurn = {
|
|
3105
4416
|
id: turnId,
|
|
3106
4417
|
sequence: Math.max(0, ...state.turns.map((turn) => turn.sequence)) + 1,
|
|
3107
4418
|
turn_id: turnId,
|
|
4419
|
+
// 乐观态就自赋,跟服务端确认后 reconcileUserTurns 补上的
|
|
4420
|
+
// client_render_id(= 乐观 turn 自己的 turn_id)保持同一个值,
|
|
4421
|
+
// 消费方才能拿它当"确认前后不变"的稳定标识使用。
|
|
4422
|
+
client_render_id: turnId,
|
|
3108
4423
|
loop_id: "root",
|
|
3109
4424
|
role: "user",
|
|
3110
4425
|
status: "streaming",
|
|
@@ -3118,16 +4433,18 @@ function addUserMessage(state, content) {
|
|
|
3118
4433
|
}
|
|
3119
4434
|
function upsertTurn(state, turn) {
|
|
3120
4435
|
let existing = [...state.turns];
|
|
3121
|
-
|
|
3122
|
-
|
|
4436
|
+
const [nextTurn] = reconcileOptimisticUserTurns(existing, [turn]);
|
|
4437
|
+
const claimedOptimisticId = nextTurn.client_render_id;
|
|
4438
|
+
if (claimedOptimisticId?.startsWith("local-user-")) {
|
|
4439
|
+
existing = existing.filter((item) => item.turn_id !== claimedOptimisticId);
|
|
3123
4440
|
}
|
|
3124
|
-
const index = existing.findIndex((item) => item.turn_id ===
|
|
4441
|
+
const index = existing.findIndex((item) => item.turn_id === nextTurn.turn_id);
|
|
3125
4442
|
if (index >= 0) {
|
|
3126
|
-
existing[index] =
|
|
4443
|
+
existing[index] = nextTurn;
|
|
3127
4444
|
} else {
|
|
3128
|
-
existing.push(
|
|
4445
|
+
existing.push(nextTurn);
|
|
3129
4446
|
}
|
|
3130
|
-
return withTurns(state, existing);
|
|
4447
|
+
return withTurns({ ...state, turns: existing }, existing);
|
|
3131
4448
|
}
|
|
3132
4449
|
function applyTurnPatch(state, patch) {
|
|
3133
4450
|
const turn = patch.data.turn;
|
|
@@ -3138,13 +4455,7 @@ function applyTurnPatch(state, patch) {
|
|
|
3138
4455
|
if (lastSequence !== null && patch.sequence <= lastSequence) {
|
|
3139
4456
|
return state;
|
|
3140
4457
|
}
|
|
3141
|
-
|
|
3142
|
-
if (index >= 0) {
|
|
3143
|
-
existing[index] = nextTurn;
|
|
3144
|
-
} else {
|
|
3145
|
-
existing.push(nextTurn);
|
|
3146
|
-
}
|
|
3147
|
-
return withTurns(state, existing);
|
|
4458
|
+
return upsertTurn(state, { ...turn, sequence: patch.sequence });
|
|
3148
4459
|
}
|
|
3149
4460
|
function addErrorMessage(state, content) {
|
|
3150
4461
|
const anchorTurnId = state.turns[state.turns.length - 1]?.turn_id ?? null;
|
|
@@ -3174,21 +4485,49 @@ function markStreamingTurns(state, turnStatus, toolCallStatus) {
|
|
|
3174
4485
|
return withTurns(state, turns);
|
|
3175
4486
|
}
|
|
3176
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
|
+
|
|
3177
4498
|
// src/session/agent-session.ts
|
|
3178
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";
|
|
3179
|
-
var
|
|
3180
|
-
|
|
3181
|
-
|
|
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}`;
|
|
3182
4510
|
}
|
|
4511
|
+
var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
|
|
3183
4512
|
function isOomText(value) {
|
|
3184
4513
|
return typeof value === "string" && OOM_KEYWORDS_RE.test(value);
|
|
3185
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
|
+
}
|
|
3186
4525
|
function parseJsonRecord(value) {
|
|
3187
|
-
if (
|
|
4526
|
+
if (isRecord(value)) return value;
|
|
3188
4527
|
if (typeof value !== "string") return null;
|
|
3189
4528
|
try {
|
|
3190
4529
|
const parsed = JSON.parse(value);
|
|
3191
|
-
return
|
|
4530
|
+
return isRecord(parsed) ? parsed : null;
|
|
3192
4531
|
} catch {
|
|
3193
4532
|
return null;
|
|
3194
4533
|
}
|
|
@@ -3214,6 +4553,21 @@ function isOomResult(value) {
|
|
|
3214
4553
|
function isToolBridgeContent(value) {
|
|
3215
4554
|
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.action === "string";
|
|
3216
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
|
+
}
|
|
3217
4571
|
function sessionStatusFromChatEnd(status) {
|
|
3218
4572
|
if (status === "paused") return "waiting_for_input";
|
|
3219
4573
|
if (status === "interrupted") return "interrupted";
|
|
@@ -3229,14 +4583,6 @@ function extractTerminalChatEndStatus(events) {
|
|
|
3229
4583
|
}
|
|
3230
4584
|
return null;
|
|
3231
4585
|
}
|
|
3232
|
-
function hasChatRunEvent(events) {
|
|
3233
|
-
return events.some(
|
|
3234
|
-
(event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
|
|
3235
|
-
);
|
|
3236
|
-
}
|
|
3237
|
-
function isUiMetaLike(value) {
|
|
3238
|
-
return isRecord5(value) && ("resourceHTML" in value || "resourceUri" in value || "resourceURI" in value);
|
|
3239
|
-
}
|
|
3240
4586
|
var AgentSession = class _AgentSession {
|
|
3241
4587
|
sessionId;
|
|
3242
4588
|
state;
|
|
@@ -3250,14 +4596,55 @@ var AgentSession = class _AgentSession {
|
|
|
3250
4596
|
patchFlushCancel = null;
|
|
3251
4597
|
joined = false;
|
|
3252
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;
|
|
3253
4616
|
// 回放控制请求排成一条队列。连着点"5x"再点"退出回放"时,并发发出去的两个
|
|
3254
4617
|
// 请求响应可能乱序回来,晚到的旧值会把已经退出的会话又显示成回放中;用
|
|
3255
4618
|
// 序号丢弃过期响应又会在失败时留下中间态。串行执行就没有这些情况:每个
|
|
3256
4619
|
// 响应都是当时最新的,失败也只影响自己。
|
|
3257
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;
|
|
3258
4629
|
pendingReplayMessage = null;
|
|
3259
4630
|
pendingReplayMode;
|
|
4631
|
+
pendingReplayKbIds;
|
|
3260
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;
|
|
3261
4648
|
disposed = false;
|
|
3262
4649
|
getAppContext = null;
|
|
3263
4650
|
sendPending = false;
|
|
@@ -3268,11 +4655,19 @@ var AgentSession = class _AgentSession {
|
|
|
3268
4655
|
seenToolCallIds = /* @__PURE__ */ new Set();
|
|
3269
4656
|
finishedToolCallIds = /* @__PURE__ */ new Set();
|
|
3270
4657
|
emittedMessageTurnIds = /* @__PURE__ */ new Set();
|
|
3271
|
-
constructor(sessionId, runtime, connection) {
|
|
4658
|
+
constructor(sessionId, runtime, connection, options = {}) {
|
|
3272
4659
|
this.sessionId = sessionId;
|
|
3273
4660
|
this.runtime = runtime;
|
|
3274
4661
|
this.state = { ...createInitialSessionState(sessionId), connection };
|
|
4662
|
+
this.isStatic = options.staticHistory === true;
|
|
3275
4663
|
}
|
|
4664
|
+
/**
|
|
4665
|
+
* 静态历史(分享页 / `fromTurns`):没有目录接口,也**没有**"目录还没到位"这个中间态。
|
|
4666
|
+
*
|
|
4667
|
+
* 宿主据此决定要不要拿已加载窗口当兜底条目——真实会话在 timeline 被重写后,窗口里还是
|
|
4668
|
+
* 旧分支的发言,拿它兜底会把已经失效的 entry id 重新摆到轨道上,点了必然找不到。
|
|
4669
|
+
*/
|
|
4670
|
+
isStatic;
|
|
3276
4671
|
/** 从一份静态 turn 快照构造只展示历史的会话(例如公开分享页)。 */
|
|
3277
4672
|
static fromTurns(sessionId, turns) {
|
|
3278
4673
|
const readonlyError = () => {
|
|
@@ -3284,7 +4679,10 @@ var AgentSession = class _AgentSession {
|
|
|
3284
4679
|
emitEvent: readonlyError,
|
|
3285
4680
|
emitWithAck: async () => readonlyError(),
|
|
3286
4681
|
ensureConnected: readonlyError,
|
|
3287
|
-
|
|
4682
|
+
fetchTurnsPage: async () => ({ turns, nextBefore: null, currentMode: "executing" }),
|
|
4683
|
+
// 静态会话(分享页)没有会话目录接口,宿主用已加载的历史兜底条目。
|
|
4684
|
+
fetchTurnIndex: async () => ({ sessionId, items: [] }),
|
|
4685
|
+
fetchCommands: async () => [],
|
|
3288
4686
|
// 静态历史没有回放身份可言,给个空详情即可,不要抛错——_bootstrap
|
|
3289
4687
|
// 不会跑到这里,但保持只读语义一致。
|
|
3290
4688
|
fetchSessionInfo: async () => ({ replay_state: null }),
|
|
@@ -3295,7 +4693,8 @@ var AgentSession = class _AgentSession {
|
|
|
3295
4693
|
attachAppCli: async () => readonlyError(),
|
|
3296
4694
|
onDispose: () => true
|
|
3297
4695
|
},
|
|
3298
|
-
"connected"
|
|
4696
|
+
"connected",
|
|
4697
|
+
{ staticHistory: true }
|
|
3299
4698
|
);
|
|
3300
4699
|
session.state = {
|
|
3301
4700
|
...withTurns(session.state, turns),
|
|
@@ -3308,6 +4707,101 @@ var AgentSession = class _AgentSession {
|
|
|
3308
4707
|
getState() {
|
|
3309
4708
|
return this.state;
|
|
3310
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
|
+
}
|
|
3311
4805
|
/** 订阅状态变化。返回取消订阅函数。 */
|
|
3312
4806
|
subscribe(listener) {
|
|
3313
4807
|
this.listeners.add(listener);
|
|
@@ -3360,7 +4854,7 @@ var AgentSession = class _AgentSession {
|
|
|
3360
4854
|
/** 发送一条消息。字符串或多模态内容均可。 */
|
|
3361
4855
|
async send(content, options = {}) {
|
|
3362
4856
|
this.runtime.ensureConnected();
|
|
3363
|
-
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) {
|
|
3364
4858
|
this.update(
|
|
3365
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")
|
|
3366
4860
|
);
|
|
@@ -3373,9 +4867,125 @@ var AgentSession = class _AgentSession {
|
|
|
3373
4867
|
this.sendPending = false;
|
|
3374
4868
|
}
|
|
3375
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
|
+
}
|
|
3376
4986
|
async sendUnlocked(content, options) {
|
|
3377
4987
|
await this.ensureIdleBeforeSend();
|
|
3378
|
-
if (this.state.isStreaming || this.state.activeCompaction || this.manualCompactionPending) {
|
|
4988
|
+
if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.manualCompactionPending) {
|
|
3379
4989
|
this.update(
|
|
3380
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")
|
|
3381
4991
|
);
|
|
@@ -3385,7 +4995,7 @@ var AgentSession = class _AgentSession {
|
|
|
3385
4995
|
if (this.getAppContext) {
|
|
3386
4996
|
try {
|
|
3387
4997
|
const rawContext = await this.getAppContext();
|
|
3388
|
-
if (!
|
|
4998
|
+
if (!isRecord(rawContext)) {
|
|
3389
4999
|
throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
|
|
3390
5000
|
}
|
|
3391
5001
|
const serialized = JSON.stringify(rawContext);
|
|
@@ -3393,7 +5003,7 @@ var AgentSession = class _AgentSession {
|
|
|
3393
5003
|
throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE\u53EF\u5E8F\u5217\u5316\u7684 JSON \u5BF9\u8C61");
|
|
3394
5004
|
}
|
|
3395
5005
|
const normalized = JSON.parse(serialized);
|
|
3396
|
-
if (!
|
|
5006
|
+
if (!isRecord(normalized)) {
|
|
3397
5007
|
throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
|
|
3398
5008
|
}
|
|
3399
5009
|
appContext = normalized;
|
|
@@ -3403,17 +5013,18 @@ var AgentSession = class _AgentSession {
|
|
|
3403
5013
|
return false;
|
|
3404
5014
|
}
|
|
3405
5015
|
}
|
|
5016
|
+
const wasStreaming = this.state.isStreaming;
|
|
3406
5017
|
const askUserAnswer = options.askUserAnswer;
|
|
3407
5018
|
let optimisticTurnId = null;
|
|
5019
|
+
const clientRequestId = createClientRequestId();
|
|
3408
5020
|
let optimisticAskAnswer;
|
|
3409
5021
|
let previousAskAnswer;
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
}
|
|
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);
|
|
3417
5028
|
this.update((s) => ({ ...s, isStreaming: true }));
|
|
3418
5029
|
if (askUserAnswer && typeof askUserAnswer.tool_call_id === "string") {
|
|
3419
5030
|
const { tool_call_id, ...rest } = askUserAnswer;
|
|
@@ -3427,57 +5038,103 @@ var AgentSession = class _AgentSession {
|
|
|
3427
5038
|
if (!options.replayDecision) {
|
|
3428
5039
|
this.pendingReplayMessage = content;
|
|
3429
5040
|
this.pendingReplayMode = options.mode;
|
|
5041
|
+
this.pendingReplayKbIds = options.kbIds;
|
|
3430
5042
|
}
|
|
3431
5043
|
const payload = {
|
|
3432
5044
|
session_id: this.sessionId,
|
|
3433
5045
|
message: content,
|
|
5046
|
+
client_request_id: clientRequestId,
|
|
3434
5047
|
mode: options.mode,
|
|
3435
5048
|
askuser_answer: askUserAnswer,
|
|
3436
5049
|
model: options.model,
|
|
3437
5050
|
thinking_override: options.thinkingOverride,
|
|
3438
5051
|
whatif: options.whatif,
|
|
3439
5052
|
replay_decision: options.replayDecision,
|
|
3440
|
-
app_context: appContext
|
|
5053
|
+
app_context: appContext,
|
|
5054
|
+
kb_ids: options.kbIds,
|
|
5055
|
+
// 运行中提交不必由客户端挑命令:服务端按会话运行状态决定直接执行还是入队。
|
|
5056
|
+
queue_when_running: options.queueWhenRunning === true
|
|
3441
5057
|
};
|
|
3442
5058
|
try {
|
|
3443
5059
|
await this.ensureJoined();
|
|
3444
5060
|
} catch (error) {
|
|
5061
|
+
const failure = describeSessionError(error, "\u4F1A\u8BDD\u8BA2\u9605\u5931\u8D25\uFF0C\u6D88\u606F\u672A\u53D1\u9001");
|
|
3445
5062
|
this.rollbackOptimisticSend(
|
|
3446
5063
|
optimisticTurnId,
|
|
3447
5064
|
askUserAnswer,
|
|
3448
5065
|
optimisticAskAnswer,
|
|
3449
5066
|
previousAskAnswer,
|
|
3450
|
-
|
|
5067
|
+
wasStreaming,
|
|
5068
|
+
failure.message,
|
|
5069
|
+
failure.code
|
|
3451
5070
|
);
|
|
3452
5071
|
return false;
|
|
3453
5072
|
}
|
|
5073
|
+
const waitStartedAt = this.systemErrorGeneration;
|
|
3454
5074
|
try {
|
|
3455
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
|
+
}
|
|
3456
5091
|
if (response?.status !== "accepted") {
|
|
3457
|
-
throw new
|
|
3458
|
-
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
|
|
3459
5095
|
);
|
|
3460
5096
|
}
|
|
3461
5097
|
return true;
|
|
3462
5098
|
} catch (error) {
|
|
5099
|
+
const failure = describeSessionError(error, "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
|
|
3463
5100
|
this.rollbackOptimisticSend(
|
|
3464
5101
|
optimisticTurnId,
|
|
3465
5102
|
askUserAnswer,
|
|
3466
5103
|
optimisticAskAnswer,
|
|
3467
5104
|
previousAskAnswer,
|
|
3468
|
-
|
|
5105
|
+
wasStreaming,
|
|
5106
|
+
failure.message,
|
|
5107
|
+
failure.code,
|
|
5108
|
+
waitStartedAt
|
|
3469
5109
|
);
|
|
3470
5110
|
return false;
|
|
3471
5111
|
}
|
|
3472
5112
|
}
|
|
3473
|
-
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) {
|
|
3474
5132
|
this.pendingReplayMessage = null;
|
|
3475
5133
|
this.pendingReplayMode = void 0;
|
|
5134
|
+
this.pendingReplayKbIds = void 0;
|
|
3476
5135
|
this.update((current) => {
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
current.turns.filter((turn) => turn.turn_id !== optimisticTurnId)
|
|
3480
|
-
) : 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;
|
|
3481
5138
|
const toolCallId = askUserAnswer && typeof askUserAnswer.tool_call_id === "string" ? askUserAnswer.tool_call_id : null;
|
|
3482
5139
|
if (toolCallId && next.askAnswers[toolCallId] === optimisticAskAnswer) {
|
|
3483
5140
|
const askAnswers = { ...next.askAnswers };
|
|
@@ -3488,18 +5145,20 @@ var AgentSession = class _AgentSession {
|
|
|
3488
5145
|
}
|
|
3489
5146
|
next = { ...next, askAnswers };
|
|
3490
5147
|
}
|
|
3491
|
-
return
|
|
5148
|
+
return next;
|
|
3492
5149
|
});
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
this.runtime.emitEvent("chat:append", { session_id: this.sessionId, message: text });
|
|
3498
|
-
this.update((s) => addUserMessage(s, text));
|
|
5150
|
+
if (optimisticTurnId) {
|
|
5151
|
+
this.pendingOptimisticTurnIds.delete(optimisticTurnId);
|
|
5152
|
+
this.liveRevisions.delete(optimisticTurnId);
|
|
5153
|
+
}
|
|
3499
5154
|
}
|
|
3500
5155
|
/** 停止当前回复。 */
|
|
3501
5156
|
stop() {
|
|
3502
|
-
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
|
+
});
|
|
3503
5162
|
}
|
|
3504
5163
|
/** 手动触发上下文压缩。 */
|
|
3505
5164
|
compact() {
|
|
@@ -3519,8 +5178,14 @@ var AgentSession = class _AgentSession {
|
|
|
3519
5178
|
trigger: "manual"
|
|
3520
5179
|
}
|
|
3521
5180
|
}));
|
|
3522
|
-
return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) =>
|
|
5181
|
+
return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => {
|
|
5182
|
+
const result = response ?? {};
|
|
5183
|
+
if (result.status !== "accepted") this.manualCompactionPending = false;
|
|
5184
|
+
return result;
|
|
5185
|
+
}).catch((error) => {
|
|
3523
5186
|
this.manualCompactionPending = false;
|
|
5187
|
+
throw error;
|
|
5188
|
+
}).finally(() => {
|
|
3524
5189
|
if (this.state.activeCompaction?.compaction_id === "pending-manual-compaction") {
|
|
3525
5190
|
this.update((state) => ({ ...state, activeCompaction: null }));
|
|
3526
5191
|
}
|
|
@@ -3559,10 +5224,20 @@ var AgentSession = class _AgentSession {
|
|
|
3559
5224
|
if (this.disposed) return;
|
|
3560
5225
|
if (!this.runtime.onDispose(this)) return;
|
|
3561
5226
|
this.disposed = true;
|
|
5227
|
+
this.invalidateTimeline();
|
|
5228
|
+
this.invalidateOlderPaging();
|
|
5229
|
+
this.state = { ...this.state, nextBefore: null, loadingOlder: false };
|
|
3562
5230
|
this.cancelPatchFlush();
|
|
3563
5231
|
this.listeners.clear();
|
|
3564
5232
|
this.emitter.clear();
|
|
3565
5233
|
this.commands.clear();
|
|
5234
|
+
this.commandRecoveryGeneration += 1;
|
|
5235
|
+
this.commandRestoreRetryAttempt = 0;
|
|
5236
|
+
this.bufferedCommandDeliveries = null;
|
|
5237
|
+
if (this.commandRestoreRetryHandle !== null) {
|
|
5238
|
+
globalThis.clearTimeout(this.commandRestoreRetryHandle);
|
|
5239
|
+
this.commandRestoreRetryHandle = null;
|
|
5240
|
+
}
|
|
3566
5241
|
}
|
|
3567
5242
|
get isDisposed() {
|
|
3568
5243
|
return this.disposed;
|
|
@@ -3573,28 +5248,64 @@ var AgentSession = class _AgentSession {
|
|
|
3573
5248
|
if (this.state.connection === connection) return;
|
|
3574
5249
|
this.update((s) => ({ ...s, connection }));
|
|
3575
5250
|
}
|
|
5251
|
+
/**
|
|
5252
|
+
* @internal 服务端广播的权威队列快照。
|
|
5253
|
+
* 结构不合法或 revision 不新的一律丢弃,绝不用旧快照覆盖新状态。
|
|
5254
|
+
*/
|
|
5255
|
+
_handleQueueUpdated(payload) {
|
|
5256
|
+
if (!isSessionQueueSnapshot(payload)) return;
|
|
5257
|
+
const snapshot2 = payload;
|
|
5258
|
+
this.update((state) => withQueue(state, snapshot2));
|
|
5259
|
+
}
|
|
3576
5260
|
/** @internal socket 重连后由 Hub 调用:重新加入房间并补数。 */
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
this.
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
5261
|
+
_rejoinAfterReconnect() {
|
|
5262
|
+
this.invalidateOlderPaging();
|
|
5263
|
+
this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
|
|
5264
|
+
const reconnectLiveRevision = this.liveRevision;
|
|
5265
|
+
const reconnectModeRevision = this.modeRevision;
|
|
5266
|
+
const commandRecoveryGeneration = this.beginCommandRecovery();
|
|
5267
|
+
return this.enqueueSubscription(async () => {
|
|
5268
|
+
this.joined = false;
|
|
5269
|
+
this.joinPending = null;
|
|
5270
|
+
try {
|
|
5271
|
+
this.subscriptionReplaySource = "reconnect_replay";
|
|
5272
|
+
try {
|
|
5273
|
+
await this.ensureJoined(reconnectLiveRevision);
|
|
5274
|
+
} finally {
|
|
5275
|
+
this.subscriptionReplaySource = null;
|
|
5276
|
+
this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
|
|
5277
|
+
}
|
|
5278
|
+
await Promise.all([
|
|
5279
|
+
this.refreshAuthoritativeHistory({
|
|
5280
|
+
liveRevisionAtStart: reconnectLiveRevision,
|
|
5281
|
+
modeRevisionAtStart: reconnectModeRevision
|
|
5282
|
+
}),
|
|
5283
|
+
this.syncReplayFromSession()
|
|
5284
|
+
]);
|
|
5285
|
+
} catch {
|
|
5286
|
+
}
|
|
5287
|
+
});
|
|
3592
5288
|
}
|
|
3593
5289
|
/** @internal 首次连接:加载历史 + 加入房间。 */
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
5290
|
+
_bootstrap() {
|
|
5291
|
+
const commandRecoveryGeneration = this.beginCommandRecovery();
|
|
5292
|
+
return this.enqueueSubscription(async () => {
|
|
5293
|
+
await this.replaceFromHistory({ suppressErrors: false });
|
|
5294
|
+
this.subscriptionReplaySource = "bootstrap_replay";
|
|
5295
|
+
try {
|
|
5296
|
+
await this.ensureJoined();
|
|
5297
|
+
} finally {
|
|
5298
|
+
this.subscriptionReplaySource = null;
|
|
5299
|
+
}
|
|
5300
|
+
this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
|
|
5301
|
+
await Promise.all([this.syncReplayFromSession(), this.refreshQueue()]);
|
|
5302
|
+
});
|
|
5303
|
+
}
|
|
5304
|
+
/** 订阅生命周期串行执行;一次失败只由自己的调用方观察,不截断后续重连。 */
|
|
5305
|
+
enqueueSubscription(operation) {
|
|
5306
|
+
const run = this.subscriptionQueue.then(operation);
|
|
5307
|
+
this.subscriptionQueue = run.catch(() => void 0);
|
|
5308
|
+
return run;
|
|
3598
5309
|
}
|
|
3599
5310
|
/** 改回放是 owner 专属(服务端 PATCH 走 _get_owned_session)。 */
|
|
3600
5311
|
get canControlReplay() {
|
|
@@ -3651,10 +5362,16 @@ var AgentSession = class _AgentSession {
|
|
|
3651
5362
|
this.applyTurn(turn);
|
|
3652
5363
|
}
|
|
3653
5364
|
/** @internal 历史回放通道(event_format: raw)。 */
|
|
3654
|
-
_handleTurnEvents(events) {
|
|
5365
|
+
_handleTurnEvents(events, replayed = false) {
|
|
3655
5366
|
if (!events.length) return;
|
|
5367
|
+
if (events.some(
|
|
5368
|
+
(event) => event.type === "compaction:end" || event.type === "compaction:error" || event.type === "compaction:abort"
|
|
5369
|
+
)) {
|
|
5370
|
+
this.manualCompactionPending = false;
|
|
5371
|
+
}
|
|
5372
|
+
this.dispatchRawCommands(events);
|
|
3656
5373
|
const terminalChatEndStatus = extractTerminalChatEndStatus(events);
|
|
3657
|
-
if (!terminalChatEndStatus && hasChatRunEvent(events)) {
|
|
5374
|
+
if (!terminalChatEndStatus && hasChatRunEvent(events) && this.state.status !== "waiting_for_input") {
|
|
3658
5375
|
this.markRunningIfIdle();
|
|
3659
5376
|
}
|
|
3660
5377
|
if (this.state.turns.length) {
|
|
@@ -3663,36 +5380,58 @@ var AgentSession = class _AgentSession {
|
|
|
3663
5380
|
const updates = this.projectionBuilder.processBatch(events);
|
|
3664
5381
|
for (const update of updates) {
|
|
3665
5382
|
if (update.kind === "upsert") {
|
|
3666
|
-
this.applyTurn(
|
|
5383
|
+
this.applyTurn(
|
|
5384
|
+
update.turn,
|
|
5385
|
+
replayed ? this.subscriptionReplaySource ?? "bootstrap_replay" : "live"
|
|
5386
|
+
);
|
|
3667
5387
|
} else if (update.kind === "workspace_changed") {
|
|
3668
5388
|
this.emitter.emit("workspaceChanged", {});
|
|
3669
5389
|
}
|
|
3670
5390
|
}
|
|
3671
|
-
if (terminalChatEndStatus) {
|
|
3672
|
-
this.
|
|
5391
|
+
if (terminalChatEndStatus || acceptedPostChatFollowupCompletesLatestRun(events, updates)) {
|
|
5392
|
+
this.completeChat(terminalChatEndStatus ?? "completed");
|
|
3673
5393
|
}
|
|
3674
5394
|
}
|
|
3675
5395
|
/** @internal */
|
|
3676
5396
|
_handleChatStart() {
|
|
3677
|
-
this.update((s) => ({
|
|
5397
|
+
this.update((s) => ({
|
|
5398
|
+
...s,
|
|
5399
|
+
status: "running",
|
|
5400
|
+
errorMessage: null,
|
|
5401
|
+
isStreaming: true
|
|
5402
|
+
}));
|
|
3678
5403
|
}
|
|
3679
5404
|
/** @internal */
|
|
3680
5405
|
_handleChatEnd(status) {
|
|
3681
5406
|
this.flushPatches();
|
|
3682
|
-
this.
|
|
3683
|
-
this.recentChatEndAt = Date.now();
|
|
3684
|
-
void this.syncTurnsFromHistory();
|
|
3685
|
-
this.emitter.emit("chatEnd", { status });
|
|
5407
|
+
this.completeChat(status);
|
|
3686
5408
|
}
|
|
3687
5409
|
/** @internal */
|
|
3688
|
-
_handleSystemError(message) {
|
|
5410
|
+
_handleSystemError(message, code) {
|
|
3689
5411
|
this.flushPatches();
|
|
3690
5412
|
const finalMessage = isOomText(message) ? OOM_MESSAGE : message;
|
|
3691
5413
|
this.update(
|
|
3692
|
-
(s) => addErrorMessage(
|
|
5414
|
+
(s) => addErrorMessage(
|
|
5415
|
+
markFailed({
|
|
5416
|
+
...s,
|
|
5417
|
+
status: "failed",
|
|
5418
|
+
errorMessage: finalMessage,
|
|
5419
|
+
isStreaming: false
|
|
5420
|
+
}),
|
|
5421
|
+
finalMessage
|
|
5422
|
+
)
|
|
3693
5423
|
);
|
|
3694
5424
|
this.recentChatEndAt = Date.now();
|
|
3695
|
-
this.
|
|
5425
|
+
this.systemErrorGeneration += 1;
|
|
5426
|
+
this.lastDeliveredSystemError = {
|
|
5427
|
+
message: finalMessage,
|
|
5428
|
+
...code ? { code } : {},
|
|
5429
|
+
generation: this.systemErrorGeneration
|
|
5430
|
+
};
|
|
5431
|
+
this.emitter.emit("error", {
|
|
5432
|
+
message: finalMessage,
|
|
5433
|
+
...code ? { code } : {}
|
|
5434
|
+
});
|
|
3696
5435
|
}
|
|
3697
5436
|
/** @internal */
|
|
3698
5437
|
_handleTaskUpdated(tasks) {
|
|
@@ -3712,6 +5451,8 @@ var AgentSession = class _AgentSession {
|
|
|
3712
5451
|
this.update((s) => ({ ...s, replay }));
|
|
3713
5452
|
}
|
|
3714
5453
|
this.emitter.emit("sessionUpdated", {
|
|
5454
|
+
workspacePath: data.workspace_path,
|
|
5455
|
+
chatGroupId: data.chat_group_id,
|
|
3715
5456
|
intent: data.intent,
|
|
3716
5457
|
status: data.status,
|
|
3717
5458
|
model: data.model,
|
|
@@ -3721,16 +5462,26 @@ var AgentSession = class _AgentSession {
|
|
|
3721
5462
|
}
|
|
3722
5463
|
/** @internal */
|
|
3723
5464
|
_handleRewind(checkpointId) {
|
|
3724
|
-
this.
|
|
3725
|
-
|
|
5465
|
+
this.invalidateTimeline();
|
|
5466
|
+
this.invalidateOlderPaging();
|
|
5467
|
+
this.update((s) => ({
|
|
5468
|
+
...s,
|
|
5469
|
+
mode: null,
|
|
5470
|
+
errorMessage: null,
|
|
5471
|
+
nextBefore: null,
|
|
5472
|
+
loadingOlder: false
|
|
5473
|
+
}));
|
|
5474
|
+
void this.replaceFromHistory();
|
|
3726
5475
|
this.emitter.emit("rewind", { checkpointId });
|
|
3727
5476
|
}
|
|
3728
5477
|
/** @internal */
|
|
3729
5478
|
_handleReplayMismatch(data) {
|
|
3730
5479
|
const message = this.pendingReplayMessage ?? data.actual_message ?? "";
|
|
3731
5480
|
const mode = this.pendingReplayMode;
|
|
5481
|
+
const kbIds = this.pendingReplayKbIds;
|
|
3732
5482
|
this.pendingReplayMessage = null;
|
|
3733
5483
|
this.pendingReplayMode = void 0;
|
|
5484
|
+
this.pendingReplayKbIds = void 0;
|
|
3734
5485
|
const respond = (decision) => {
|
|
3735
5486
|
void this.runtime.emitWithAck(
|
|
3736
5487
|
"chat:send",
|
|
@@ -3738,7 +5489,8 @@ var AgentSession = class _AgentSession {
|
|
|
3738
5489
|
session_id: this.sessionId,
|
|
3739
5490
|
message,
|
|
3740
5491
|
mode,
|
|
3741
|
-
replay_decision: decision
|
|
5492
|
+
replay_decision: decision,
|
|
5493
|
+
kb_ids: kbIds
|
|
3742
5494
|
},
|
|
3743
5495
|
3e4
|
|
3744
5496
|
).then((response) => {
|
|
@@ -3779,9 +5531,9 @@ var AgentSession = class _AgentSession {
|
|
|
3779
5531
|
detail,
|
|
3780
5532
|
status,
|
|
3781
5533
|
loopId,
|
|
3782
|
-
metadata:
|
|
5534
|
+
metadata: isRecord(notification.metadata) ? notification.metadata : void 0
|
|
3783
5535
|
});
|
|
3784
|
-
if (notificationType === "bg:started" &&
|
|
5536
|
+
if (notificationType === "bg:started" && isRecord(notification.metadata)) {
|
|
3785
5537
|
const taskId = typeof notification.metadata.task_id === "string" ? notification.metadata.task_id : "";
|
|
3786
5538
|
if (taskId) {
|
|
3787
5539
|
this.emitter.emit("backgroundTask", {
|
|
@@ -3803,12 +5555,12 @@ var AgentSession = class _AgentSession {
|
|
|
3803
5555
|
this.emitter.emit("artifact", event);
|
|
3804
5556
|
}
|
|
3805
5557
|
/** @internal 加入房间的应答里带 is_running,据此校准流式状态。 */
|
|
3806
|
-
_applyRunningState(isRunning) {
|
|
5558
|
+
_applyRunningState(isRunning, liveRevisionAtJoin = this.liveRevision) {
|
|
3807
5559
|
if (isRunning) {
|
|
3808
5560
|
this.update((s) => ({ ...s, status: "running", isStreaming: true }));
|
|
3809
5561
|
} else if (this.state.isStreaming) {
|
|
3810
5562
|
this.update((s) => ({ ...s, isStreaming: false }));
|
|
3811
|
-
void this.
|
|
5563
|
+
void this.refreshAuthoritativeHistory({ liveRevisionAtStart: liveRevisionAtJoin });
|
|
3812
5564
|
}
|
|
3813
5565
|
}
|
|
3814
5566
|
// ---------------------------------------------------------------- 内部实现
|
|
@@ -3826,11 +5578,39 @@ var AgentSession = class _AgentSession {
|
|
|
3826
5578
|
return next;
|
|
3827
5579
|
});
|
|
3828
5580
|
}
|
|
3829
|
-
|
|
5581
|
+
completeChat(status) {
|
|
5582
|
+
this.finishStreaming(status);
|
|
5583
|
+
this.recentChatEndAt = Date.now();
|
|
5584
|
+
void this.refreshAuthoritativeHistory();
|
|
5585
|
+
this.emitter.emit("chatEnd", { status });
|
|
5586
|
+
}
|
|
5587
|
+
markTurnFresh(turnId) {
|
|
5588
|
+
this.liveRevision += 1;
|
|
5589
|
+
this.liveRevisions.set(turnId, this.liveRevision);
|
|
5590
|
+
}
|
|
5591
|
+
markOptimisticTurnPending(turnId) {
|
|
5592
|
+
this.pendingOptimisticTurnIds.add(turnId);
|
|
5593
|
+
this.markTurnFresh(turnId);
|
|
5594
|
+
}
|
|
5595
|
+
pruneOptimisticLiveRevisions() {
|
|
5596
|
+
const retained = new Set(
|
|
5597
|
+
this.state.turns.map((turn) => turn.turn_id).filter((turnId) => turnId.startsWith("local-user-"))
|
|
5598
|
+
);
|
|
5599
|
+
for (const turnId of this.liveRevisions.keys()) {
|
|
5600
|
+
if (turnId.startsWith("local-user-") && !retained.has(turnId)) {
|
|
5601
|
+
this.pendingOptimisticTurnIds.delete(turnId);
|
|
5602
|
+
this.liveRevisions.delete(turnId);
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
applyTurn(turn, source = "live") {
|
|
3830
5607
|
this.flushPatches();
|
|
5608
|
+
this.markTurnFresh(turn.turn_id);
|
|
5609
|
+
if (extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
|
|
3831
5610
|
const previous = this.state;
|
|
3832
5611
|
this.update((s) => upsertTurn(s, turn));
|
|
3833
|
-
this.
|
|
5612
|
+
this.pruneOptimisticLiveRevisions();
|
|
5613
|
+
this.emitTurnDerivedEvents(previous, turn, source);
|
|
3834
5614
|
}
|
|
3835
5615
|
schedulePatchFlush() {
|
|
3836
5616
|
if (this.patchFlushHandle != null) return;
|
|
@@ -3863,11 +5643,15 @@ var AgentSession = class _AgentSession {
|
|
|
3863
5643
|
this.pendingTurnPatches.clear();
|
|
3864
5644
|
for (const patch of patches) {
|
|
3865
5645
|
const previous = this.state;
|
|
3866
|
-
|
|
5646
|
+
const next = applyTurnPatch(previous, patch);
|
|
5647
|
+
if (next === previous) continue;
|
|
5648
|
+
this.liveRevision += 1;
|
|
5649
|
+
this.liveRevisions.set(patch.turn_id, this.liveRevision);
|
|
3867
5650
|
const turn = patch.data.turn;
|
|
3868
|
-
if (turn)
|
|
3869
|
-
|
|
3870
|
-
|
|
5651
|
+
if (turn && extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
|
|
5652
|
+
this.setState(next);
|
|
5653
|
+
this.pruneOptimisticLiveRevisions();
|
|
5654
|
+
if (turn) this.emitTurnDerivedEvents(previous, turn);
|
|
3871
5655
|
}
|
|
3872
5656
|
if (this.patchFlushCancel) {
|
|
3873
5657
|
this.patchFlushCancel();
|
|
@@ -3876,7 +5660,7 @@ var AgentSession = class _AgentSession {
|
|
|
3876
5660
|
}
|
|
3877
5661
|
}
|
|
3878
5662
|
/** turn 数据落地后统一派生语义事件(指令 / 工具调用 / 消息 / 模式 / 预览 / OOM)。 */
|
|
3879
|
-
emitTurnDerivedEvents(previous, turn) {
|
|
5663
|
+
emitTurnDerivedEvents(previous, turn, source = "live") {
|
|
3880
5664
|
const mode = extractModeFromBlocks(turn.blocks);
|
|
3881
5665
|
if (mode && previous.mode !== mode) {
|
|
3882
5666
|
this.emitter.emit("modeChange", { mode });
|
|
@@ -3889,23 +5673,29 @@ var AgentSession = class _AgentSession {
|
|
|
3889
5673
|
this.dispatchCommand(turn, block);
|
|
3890
5674
|
continue;
|
|
3891
5675
|
}
|
|
3892
|
-
if (block.type === "tool_ui" && block.tool_call_id &&
|
|
5676
|
+
if (block.type === "tool_ui" && block.tool_call_id && isToolUiCard(block.content)) {
|
|
3893
5677
|
const ui = block.content;
|
|
3894
5678
|
if (ui.target !== "preview") continue;
|
|
3895
|
-
const
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
const
|
|
3899
|
-
|
|
3900
|
-
|
|
5679
|
+
const resolved = resolveToolUiCardContent(ui);
|
|
5680
|
+
if (!resolved) continue;
|
|
5681
|
+
const key = buildToolUiCardKey(block.tool_call_id, resolved.type, resolved.content, ui.sourcePath);
|
|
5682
|
+
const fingerprint = JSON.stringify([
|
|
5683
|
+
resolved.content,
|
|
5684
|
+
ui.mcpApp?.sourceId,
|
|
5685
|
+
resolved.type === "resource-file" ? block.tool_call_id : void 0
|
|
5686
|
+
]);
|
|
5687
|
+
if (this.deliveredToolPreviews.get(key) === fingerprint) continue;
|
|
5688
|
+
this.deliveredToolPreviews.set(key, fingerprint);
|
|
3901
5689
|
this.emitter.emit("toolPreview", {
|
|
3902
5690
|
toolCallId: block.tool_call_id,
|
|
3903
|
-
type:
|
|
3904
|
-
content:
|
|
3905
|
-
title: ui.title ?? "\u5DE5\u5177\u9884\u89C8"
|
|
5691
|
+
type: resolved.type,
|
|
5692
|
+
content: resolved.content,
|
|
5693
|
+
title: ui.title ?? "\u5DE5\u5177\u9884\u89C8",
|
|
5694
|
+
...ui.sourcePath ? { sourcePath: ui.sourcePath } : {},
|
|
5695
|
+
...ui.mcpApp ? { mcpApp: ui.mcpApp } : {}
|
|
3906
5696
|
});
|
|
3907
5697
|
}
|
|
3908
|
-
if (block.type === "system_notification" &&
|
|
5698
|
+
if (block.type === "system_notification" && isRecord(block.content)) {
|
|
3909
5699
|
this._handleSystemNotification(turn.loop_id, block.content);
|
|
3910
5700
|
}
|
|
3911
5701
|
}
|
|
@@ -3925,7 +5715,7 @@ var AgentSession = class _AgentSession {
|
|
|
3925
5715
|
const finished = toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled";
|
|
3926
5716
|
if (finished && !this.finishedToolCallIds.has(toolCall.id)) {
|
|
3927
5717
|
this.finishedToolCallIds.add(toolCall.id);
|
|
3928
|
-
this.emitter.emit("toolResult", { toolCall: info, turn });
|
|
5718
|
+
this.emitter.emit("toolResult", { toolCall: info, turn, source });
|
|
3929
5719
|
if (isOomResult(toolCall.result)) {
|
|
3930
5720
|
this.notifySandboxOom(toolCall.id);
|
|
3931
5721
|
}
|
|
@@ -3942,17 +5732,36 @@ var AgentSession = class _AgentSession {
|
|
|
3942
5732
|
dispatchCommand(turn, block) {
|
|
3943
5733
|
if (!isToolBridgeContent(block.content)) return;
|
|
3944
5734
|
const { action, payload } = block.content;
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
5735
|
+
const deliveryKey = block.delivery_id ? commandDeliveryKey(block.delivery_id) : `legacy-tool-command:${turn.turn_id}:${block.tool_call_id ?? ""}:${action}:${JSON.stringify(payload)}`;
|
|
5736
|
+
this.deliverCommand(deliveryKey, action, payload, block.tool_call_id ?? void 0);
|
|
5737
|
+
}
|
|
5738
|
+
dispatchRawCommands(events) {
|
|
5739
|
+
for (const event of events) {
|
|
5740
|
+
if (event.type !== "loop:tool_bridge") continue;
|
|
5741
|
+
const deliveryId = String(event.payload.delivery_id ?? "");
|
|
5742
|
+
const bridge = event.payload.bridge;
|
|
5743
|
+
if (!deliveryId || !isToolBridgeContent(bridge)) continue;
|
|
5744
|
+
this.deliverCommand(
|
|
5745
|
+
commandDeliveryKey(deliveryId),
|
|
5746
|
+
bridge.action,
|
|
5747
|
+
bridge.payload,
|
|
5748
|
+
String(event.payload.tool_call_id ?? "") || void 0
|
|
5749
|
+
);
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
deliverCommand(deliveryKey, action, payload, toolCallId) {
|
|
5753
|
+
if (this.bufferedCommandDeliveries) {
|
|
5754
|
+
this.bufferedCommandDeliveries.push({ deliveryKey, action, payload, toolCallId });
|
|
5755
|
+
return;
|
|
3950
5756
|
}
|
|
3951
|
-
|
|
5757
|
+
this.deliverCommandNow({ deliveryKey, action, payload, toolCallId });
|
|
5758
|
+
}
|
|
5759
|
+
deliverCommandNow(command) {
|
|
5760
|
+
const { deliveryKey, action, payload, toolCallId } = command;
|
|
3952
5761
|
if (this.deliveredCommandKeys.has(deliveryKey)) return;
|
|
3953
5762
|
this.deliveredCommandKeys.add(deliveryKey);
|
|
3954
|
-
const meta = { toolCallId
|
|
3955
|
-
this.emitter.emit("command", { action, payload, toolCallId
|
|
5763
|
+
const meta = { toolCallId };
|
|
5764
|
+
this.emitter.emit("command", { action, payload, toolCallId });
|
|
3956
5765
|
this.commands.dispatch(action, payload, meta);
|
|
3957
5766
|
}
|
|
3958
5767
|
notifySandboxOom(key) {
|
|
@@ -3961,63 +5770,217 @@ var AgentSession = class _AgentSession {
|
|
|
3961
5770
|
this.update((s) => addErrorMessage(s, OOM_MESSAGE));
|
|
3962
5771
|
this.emitter.emit("sandboxOom", { toolCallId: key });
|
|
3963
5772
|
}
|
|
3964
|
-
async syncTurnsFromHistory(options = {}) {
|
|
3965
|
-
const { clearOnEmpty = false, suppressErrors = true, preserveChangesSince } = options;
|
|
3966
|
-
const turnsAtRequestStart = this.state.turns;
|
|
3967
|
-
try {
|
|
3968
|
-
const turns = await this.runtime.fetchTurns(this.sessionId);
|
|
3969
|
-
if (turns.length === 0 && !clearOnEmpty) return;
|
|
3970
|
-
this.update((state) => {
|
|
3971
|
-
const mergeBaseline = preserveChangesSince ?? turnsAtRequestStart;
|
|
3972
|
-
if (state.turns === mergeBaseline) return withTurns(state, turns);
|
|
3973
|
-
const historyById = new Map(turns.map((turn) => [turn.turn_id, turn]));
|
|
3974
|
-
let merged = withTurns(state, turns);
|
|
3975
|
-
for (const current of state.turns) {
|
|
3976
|
-
const fromHistory = historyById.get(current.turn_id);
|
|
3977
|
-
if (!fromHistory || current.sequence > fromHistory.sequence) {
|
|
3978
|
-
merged = upsertTurn(merged, current);
|
|
3979
|
-
}
|
|
3980
|
-
}
|
|
3981
|
-
return merged;
|
|
3982
|
-
});
|
|
3983
|
-
this.replayCommandsFromTurns(turns);
|
|
3984
|
-
} catch (error) {
|
|
3985
|
-
if (!suppressErrors) throw error;
|
|
3986
|
-
}
|
|
3987
|
-
}
|
|
3988
5773
|
/**
|
|
3989
|
-
*
|
|
5774
|
+
* 权威刷新(运行结束 / 重连补数):**按当前窗口第一条发言取窗口**,不再另记"用户想看哪段"。
|
|
3990
5775
|
*
|
|
3991
|
-
*
|
|
3992
|
-
*
|
|
3993
|
-
*
|
|
3994
|
-
*
|
|
3995
|
-
* dispatch 内部按 deliveredCommandKeys 去重,重复调用安全。
|
|
5776
|
+
* 窗口本身就是"用户在看哪段":首屏就是最新一页,向上翻页 / 定位之后首条就是用户滚到的位置。
|
|
5777
|
+
* 直接拿 `from_entry_id = 当前窗口第一条 turn_id` 去取,运行结束、重连补数、翻页之后的对账
|
|
5778
|
+
* 是同一条规则,不需要意图、锚点、已落地锚点这些状态;窗口首条已经不在分支上(404 / 409)时
|
|
5779
|
+
* 回落到最新一页。
|
|
3996
5780
|
*/
|
|
3997
|
-
|
|
5781
|
+
refreshAuthoritativeHistory(options = {}) {
|
|
5782
|
+
let entryGone = false;
|
|
5783
|
+
const locateTokenAtStart = this.locateToken;
|
|
5784
|
+
return this.replaceFromHistory({
|
|
5785
|
+
...options,
|
|
5786
|
+
// 锚点在**这次操作真正开始**时采样:排队期间用户可能刚定位过,那时窗口首条才是他要的。
|
|
5787
|
+
fromEntryId: () => {
|
|
5788
|
+
const anchor = this.state.turns[0]?.turn_id;
|
|
5789
|
+
return anchor?.startsWith("local-user-") ? void 0 : anchor;
|
|
5790
|
+
},
|
|
5791
|
+
onError: (error) => {
|
|
5792
|
+
entryGone = isEntryGoneError(error);
|
|
5793
|
+
}
|
|
5794
|
+
}).then((located) => {
|
|
5795
|
+
if (located || !entryGone) return located;
|
|
5796
|
+
if (this.locateToken !== locateTokenAtStart) return false;
|
|
5797
|
+
return this.replaceFromHistory(options);
|
|
5798
|
+
});
|
|
5799
|
+
}
|
|
5800
|
+
replaceFromHistory(options = {}) {
|
|
5801
|
+
if (this.disposed) return Promise.resolve(false);
|
|
5802
|
+
const operationEpoch = this.timelineEpoch;
|
|
5803
|
+
const liveRevisionAtStart = options.liveRevisionAtStart ?? this.liveRevision;
|
|
5804
|
+
const modeRevisionAtStart = options.modeRevisionAtStart ?? this.modeRevision;
|
|
5805
|
+
this.invalidateOlderPaging();
|
|
5806
|
+
this.replaceInFlight += 1;
|
|
5807
|
+
const operation = async () => {
|
|
5808
|
+
let previousNextBefore = null;
|
|
5809
|
+
const restoreCursor = () => {
|
|
5810
|
+
if (this.disposed || previousNextBefore === null) return;
|
|
5811
|
+
this.update((state) => ({
|
|
5812
|
+
...state,
|
|
5813
|
+
nextBefore: previousNextBefore,
|
|
5814
|
+
loadingOlder: false
|
|
5815
|
+
}));
|
|
5816
|
+
};
|
|
5817
|
+
try {
|
|
5818
|
+
if (this.disposed || operationEpoch !== this.timelineEpoch) return false;
|
|
5819
|
+
if (options.isStale?.()) return false;
|
|
5820
|
+
this.invalidateOlderPaging();
|
|
5821
|
+
previousNextBefore = this.state.nextBefore;
|
|
5822
|
+
this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
|
|
5823
|
+
const signal = options.signal;
|
|
5824
|
+
const fromEntryId = typeof options.fromEntryId === "function" ? options.fromEntryId() : options.fromEntryId;
|
|
5825
|
+
const page = await this.runtime.fetchTurnsPage(
|
|
5826
|
+
this.sessionId,
|
|
5827
|
+
fromEntryId ? { fromEntryId } : void 0,
|
|
5828
|
+
signal ? { signal } : void 0
|
|
5829
|
+
);
|
|
5830
|
+
if (this.disposed || operationEpoch !== this.timelineEpoch) {
|
|
5831
|
+
restoreCursor();
|
|
5832
|
+
return false;
|
|
5833
|
+
}
|
|
5834
|
+
if (options.isStale?.()) {
|
|
5835
|
+
restoreCursor();
|
|
5836
|
+
return false;
|
|
5837
|
+
}
|
|
5838
|
+
if (!this.joined) this.seedToolCallDeliveryBaseline(page.turns);
|
|
5839
|
+
this.update((state) => {
|
|
5840
|
+
const next = withTurns(
|
|
5841
|
+
state,
|
|
5842
|
+
replaceWindow(state.turns, page.turns, liveRevisionAtStart, {
|
|
5843
|
+
current: this.liveRevision,
|
|
5844
|
+
byTurnId: this.liveRevisions
|
|
5845
|
+
})
|
|
5846
|
+
);
|
|
5847
|
+
return {
|
|
5848
|
+
...next,
|
|
5849
|
+
mode: this.modeRevision === modeRevisionAtStart ? page.currentMode : state.mode,
|
|
5850
|
+
nextBefore: page.nextBefore,
|
|
5851
|
+
loadingOlder: false
|
|
5852
|
+
};
|
|
5853
|
+
});
|
|
5854
|
+
this.pruneOptimisticLiveRevisions();
|
|
5855
|
+
this.projectionBuilder._seedTurns(this.state.turns);
|
|
5856
|
+
return true;
|
|
5857
|
+
} catch (error) {
|
|
5858
|
+
options.onError?.(error);
|
|
5859
|
+
if (!this.disposed && operationEpoch === this.timelineEpoch) {
|
|
5860
|
+
this.update((state) => ({
|
|
5861
|
+
...state,
|
|
5862
|
+
nextBefore: previousNextBefore,
|
|
5863
|
+
loadingOlder: false
|
|
5864
|
+
}));
|
|
5865
|
+
}
|
|
5866
|
+
if (options.suppressErrors === false) throw error;
|
|
5867
|
+
return false;
|
|
5868
|
+
} finally {
|
|
5869
|
+
this.replaceInFlight -= 1;
|
|
5870
|
+
}
|
|
5871
|
+
};
|
|
5872
|
+
const run = this.replaceQueue.then(operation);
|
|
5873
|
+
this.replaceQueue = run.catch(() => void 0);
|
|
5874
|
+
return run;
|
|
5875
|
+
}
|
|
5876
|
+
invalidateOlderPaging() {
|
|
5877
|
+
this.pagingGeneration += 1;
|
|
5878
|
+
this.olderPending = null;
|
|
5879
|
+
}
|
|
5880
|
+
beginCommandRecovery() {
|
|
5881
|
+
this.commandRecoveryGeneration += 1;
|
|
5882
|
+
this.commandRestoreRetryAttempt = 0;
|
|
5883
|
+
this.bufferedCommandDeliveries ??= [];
|
|
5884
|
+
if (this.commandRestoreRetryHandle !== null) {
|
|
5885
|
+
globalThis.clearTimeout(this.commandRestoreRetryHandle);
|
|
5886
|
+
this.commandRestoreRetryHandle = null;
|
|
5887
|
+
}
|
|
5888
|
+
return this.commandRecoveryGeneration;
|
|
5889
|
+
}
|
|
5890
|
+
scheduleHistoricalCommandRestore(generation) {
|
|
5891
|
+
const restore = async () => {
|
|
5892
|
+
if (this.disposed || generation !== this.commandRecoveryGeneration) return;
|
|
5893
|
+
try {
|
|
5894
|
+
const commands = await this.runtime.fetchCommands(this.sessionId);
|
|
5895
|
+
if (this.disposed) return;
|
|
5896
|
+
for (const command of commands) {
|
|
5897
|
+
if (this.disposed) return;
|
|
5898
|
+
this.deliverCommandNow({
|
|
5899
|
+
deliveryKey: commandDeliveryKey(command.deliveryId),
|
|
5900
|
+
action: command.action,
|
|
5901
|
+
payload: command.payload,
|
|
5902
|
+
toolCallId: command.toolCallId
|
|
5903
|
+
});
|
|
5904
|
+
}
|
|
5905
|
+
if (generation !== this.commandRecoveryGeneration) return;
|
|
5906
|
+
this.commandRestoreRetryAttempt = 0;
|
|
5907
|
+
this.flushBufferedCommands(generation);
|
|
5908
|
+
} catch {
|
|
5909
|
+
if (!this.disposed && generation === this.commandRecoveryGeneration && this.commandRestoreRetryHandle === null) {
|
|
5910
|
+
const retryDelay = Math.min(
|
|
5911
|
+
COMMAND_RESTORE_RETRY_BASE_MS * 2 ** this.commandRestoreRetryAttempt,
|
|
5912
|
+
COMMAND_RESTORE_RETRY_MAX_MS
|
|
5913
|
+
);
|
|
5914
|
+
this.commandRestoreRetryAttempt += 1;
|
|
5915
|
+
this.commandRestoreRetryHandle = globalThis.setTimeout(() => {
|
|
5916
|
+
this.commandRestoreRetryHandle = null;
|
|
5917
|
+
if (!this.disposed && generation === this.commandRecoveryGeneration) {
|
|
5918
|
+
this.scheduleHistoricalCommandRestore(generation);
|
|
5919
|
+
}
|
|
5920
|
+
}, retryDelay);
|
|
5921
|
+
}
|
|
5922
|
+
}
|
|
5923
|
+
};
|
|
5924
|
+
const run = this.commandRestoreQueue.then(restore);
|
|
5925
|
+
this.commandRestoreQueue = run.catch(() => void 0);
|
|
5926
|
+
}
|
|
5927
|
+
flushBufferedCommands(generation) {
|
|
5928
|
+
const pending = [...this.bufferedCommandDeliveries ?? []];
|
|
5929
|
+
this.bufferedCommandDeliveries = [];
|
|
5930
|
+
let index = 0;
|
|
5931
|
+
while (index < pending.length) {
|
|
5932
|
+
if (this.disposed) return;
|
|
5933
|
+
if (generation !== this.commandRecoveryGeneration) {
|
|
5934
|
+
const currentGenerationBuffer = this.bufferedCommandDeliveries ?? [];
|
|
5935
|
+
this.bufferedCommandDeliveries = [
|
|
5936
|
+
...pending.slice(index),
|
|
5937
|
+
...currentGenerationBuffer
|
|
5938
|
+
];
|
|
5939
|
+
return;
|
|
5940
|
+
}
|
|
5941
|
+
this.deliverCommandNow(pending[index]);
|
|
5942
|
+
index += 1;
|
|
5943
|
+
const reentrantBuffer = this.bufferedCommandDeliveries;
|
|
5944
|
+
if (index === pending.length && reentrantBuffer && reentrantBuffer.length > 0) {
|
|
5945
|
+
pending.push(...reentrantBuffer);
|
|
5946
|
+
this.bufferedCommandDeliveries = [];
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5949
|
+
if (!this.disposed && generation === this.commandRecoveryGeneration) {
|
|
5950
|
+
this.bufferedCommandDeliveries = null;
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
5953
|
+
/** 首次订阅前的 REST 历史建立语义事件基线,随后同一工具的订阅回放不再投递。 */
|
|
5954
|
+
seedToolCallDeliveryBaseline(turns) {
|
|
3998
5955
|
for (const turn of turns) {
|
|
3999
|
-
for (const
|
|
4000
|
-
|
|
4001
|
-
|
|
5956
|
+
for (const toolCall of turn.tool_calls) {
|
|
5957
|
+
this.seenToolCallIds.add(toolCall.id);
|
|
5958
|
+
if (toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled") {
|
|
5959
|
+
this.finishedToolCallIds.add(toolCall.id);
|
|
4002
5960
|
}
|
|
4003
5961
|
}
|
|
4004
5962
|
}
|
|
4005
5963
|
}
|
|
4006
|
-
ensureJoined() {
|
|
5964
|
+
ensureJoined(liveRevisionBoundary = this.liveRevision) {
|
|
4007
5965
|
if (this.joined) return Promise.resolve();
|
|
4008
5966
|
if (this.joinPending) return this.joinPending;
|
|
4009
5967
|
const join = this.runtime.emitWithAck(
|
|
4010
5968
|
"session:subscribe",
|
|
4011
|
-
{
|
|
5969
|
+
{
|
|
5970
|
+
session_id: this.sessionId,
|
|
5971
|
+
event_format: "raw",
|
|
5972
|
+
resume: true
|
|
5973
|
+
},
|
|
4012
5974
|
5e3
|
|
4013
5975
|
).then((response) => {
|
|
4014
5976
|
if (response && response.ok === false) {
|
|
4015
|
-
throw new
|
|
4016
|
-
typeof response.message === "string" && response.message ? response.message : "Session subscription failed"
|
|
5977
|
+
throw new AgentSessionError(
|
|
5978
|
+
typeof response.message === "string" && response.message ? response.message : "Session subscription failed",
|
|
5979
|
+
typeof response.code === "string" ? response.code : void 0
|
|
4017
5980
|
);
|
|
4018
5981
|
}
|
|
4019
5982
|
this.joined = true;
|
|
4020
|
-
this._applyRunningState(Boolean(response?.is_running));
|
|
5983
|
+
this._applyRunningState(Boolean(response?.is_running), liveRevisionBoundary);
|
|
4021
5984
|
}).finally(() => {
|
|
4022
5985
|
this.joinPending = null;
|
|
4023
5986
|
});
|
|
@@ -4025,6 +5988,7 @@ var AgentSession = class _AgentSession {
|
|
|
4025
5988
|
return join;
|
|
4026
5989
|
}
|
|
4027
5990
|
async ensureIdleBeforeSend() {
|
|
5991
|
+
if (this.state.isStreaming) return;
|
|
4028
5992
|
if (Date.now() - this.recentChatEndAt > 3e3) return;
|
|
4029
5993
|
const deadline = Date.now() + 3e3;
|
|
4030
5994
|
while (Date.now() < deadline) {
|
|
@@ -4059,6 +6023,7 @@ var SessionHub = class {
|
|
|
4059
6023
|
connectionListeners = /* @__PURE__ */ new Set();
|
|
4060
6024
|
bound = false;
|
|
4061
6025
|
hasConnected = false;
|
|
6026
|
+
authBusyReconnect = new AuthBusyReconnect();
|
|
4062
6027
|
constructor(client) {
|
|
4063
6028
|
this.client = client;
|
|
4064
6029
|
}
|
|
@@ -4080,16 +6045,28 @@ var SessionHub = class {
|
|
|
4080
6045
|
* 只有最后一份归还才退订房间——否则 StrictMode 双跑、路由抖动这类"连两次
|
|
4081
6046
|
* 只释放一次"的场景会把仍在用的会话退订掉,之后所有 turn/chat 事件静默丢失。
|
|
4082
6047
|
*/
|
|
4083
|
-
async connect(sessionId) {
|
|
6048
|
+
async connect(sessionId, options = {}) {
|
|
4084
6049
|
const existing = this.sessions.get(sessionId);
|
|
4085
6050
|
if (existing && !existing.isDisposed) {
|
|
4086
6051
|
this.hold(sessionId);
|
|
6052
|
+
try {
|
|
6053
|
+
options.setup?.(existing);
|
|
6054
|
+
} catch (error) {
|
|
6055
|
+
existing.dispose();
|
|
6056
|
+
throw error;
|
|
6057
|
+
}
|
|
4087
6058
|
return this.bootstrapPending.get(sessionId) ?? existing;
|
|
4088
6059
|
}
|
|
4089
6060
|
this.ensureBound();
|
|
4090
6061
|
const session = new AgentSession(sessionId, this.runtimeFor(), this.connection);
|
|
4091
6062
|
this.sessions.set(sessionId, session);
|
|
4092
6063
|
this.hold(sessionId);
|
|
6064
|
+
try {
|
|
6065
|
+
options.setup?.(session);
|
|
6066
|
+
} catch (error) {
|
|
6067
|
+
session.dispose();
|
|
6068
|
+
throw error;
|
|
6069
|
+
}
|
|
4093
6070
|
this.ensureConnected();
|
|
4094
6071
|
const bootstrap = session._bootstrap().then(() => session);
|
|
4095
6072
|
this.bootstrapPending.set(sessionId, bootstrap);
|
|
@@ -4113,6 +6090,7 @@ var SessionHub = class {
|
|
|
4113
6090
|
/** 手动重连(例如登录态变化后)。 */
|
|
4114
6091
|
reconnect() {
|
|
4115
6092
|
this.ensureBound();
|
|
6093
|
+
this.authBusyReconnect.cancel();
|
|
4116
6094
|
const socket = this.client.socket();
|
|
4117
6095
|
socket.disconnect();
|
|
4118
6096
|
this.setConnection("connecting");
|
|
@@ -4120,11 +6098,13 @@ var SessionHub = class {
|
|
|
4120
6098
|
}
|
|
4121
6099
|
/** 断开连接(不清理 session 实例)。 */
|
|
4122
6100
|
disconnect() {
|
|
6101
|
+
this.authBusyReconnect.cancel();
|
|
4123
6102
|
this.client.socket().disconnect();
|
|
4124
6103
|
this.setConnection("disconnected");
|
|
4125
6104
|
}
|
|
4126
6105
|
/** 清理当前身份下缓存的全部会话,避免切换登录身份后复用旧快照。 */
|
|
4127
6106
|
disposeAll() {
|
|
6107
|
+
this.authBusyReconnect.cancel();
|
|
4128
6108
|
this.bootstrapPending.clear();
|
|
4129
6109
|
for (const session of [...this.sessions.values()]) {
|
|
4130
6110
|
this.forceRelease(session);
|
|
@@ -4136,6 +6116,7 @@ var SessionHub = class {
|
|
|
4136
6116
|
* turn:* / chat:* 事件都会静默丢弃。
|
|
4137
6117
|
*/
|
|
4138
6118
|
_handleSocketReplaced() {
|
|
6119
|
+
this.authBusyReconnect.cancel();
|
|
4139
6120
|
this.bound = false;
|
|
4140
6121
|
this.setConnection("disconnected");
|
|
4141
6122
|
if (this.sessions.size === 0) return;
|
|
@@ -4148,18 +6129,11 @@ var SessionHub = class {
|
|
|
4148
6129
|
;
|
|
4149
6130
|
this.client.socket().emit(event, data);
|
|
4150
6131
|
},
|
|
4151
|
-
emitWithAck: (event, data, timeoutMs) =>
|
|
4152
|
-
;
|
|
4153
|
-
this.client.socket().timeout(timeoutMs).emit(event, data, (err, response) => {
|
|
4154
|
-
if (err) {
|
|
4155
|
-
reject(err);
|
|
4156
|
-
return;
|
|
4157
|
-
}
|
|
4158
|
-
resolve(response);
|
|
4159
|
-
});
|
|
4160
|
-
}),
|
|
6132
|
+
emitWithAck: (event, data, timeoutMs) => this.client.requestCommand(event, data, timeoutMs),
|
|
4161
6133
|
ensureConnected: () => this.ensureConnected(),
|
|
4162
|
-
|
|
6134
|
+
fetchTurnsPage: (sessionId, query, init) => this.client.sessions.getSessionTurnsPage(sessionId, query, init),
|
|
6135
|
+
fetchTurnIndex: (sessionId) => this.client.sessions.getSessionTurnIndex(sessionId),
|
|
6136
|
+
fetchCommands: (sessionId) => this.client.sessions.getSessionTurnCommands(sessionId),
|
|
4163
6137
|
fetchSessionInfo: (sessionId) => this.client.sessions.getSession(sessionId),
|
|
4164
6138
|
updateReplay: (sessionId, payload) => this.client.sessions.updateReplaySession(sessionId, payload).then((result) => result.replay_state ?? null),
|
|
4165
6139
|
listDir: (sessionId, dirPath) => this.client.sessions.listDir(sessionId, dirPath),
|
|
@@ -4187,6 +6161,9 @@ var SessionHub = class {
|
|
|
4187
6161
|
if (this.sessions.get(session.sessionId) === session) {
|
|
4188
6162
|
this.sessions.delete(session.sessionId);
|
|
4189
6163
|
}
|
|
6164
|
+
if (this.sessions.size === 0) {
|
|
6165
|
+
this.authBusyReconnect.cancel();
|
|
6166
|
+
}
|
|
4190
6167
|
;
|
|
4191
6168
|
this.client.socket().emit("session:unsubscribe", { session_id: session.sessionId });
|
|
4192
6169
|
return true;
|
|
@@ -4195,10 +6172,13 @@ var SessionHub = class {
|
|
|
4195
6172
|
forceRelease(session) {
|
|
4196
6173
|
this.holdCounts.delete(session.sessionId);
|
|
4197
6174
|
session.dispose();
|
|
6175
|
+
if (this.sessions.size === 0) {
|
|
6176
|
+
this.authBusyReconnect.cancel();
|
|
6177
|
+
}
|
|
4198
6178
|
}
|
|
4199
6179
|
ensureConnected() {
|
|
4200
6180
|
const socket = this.client.socket();
|
|
4201
|
-
if (socket.connected || socket.active) return;
|
|
6181
|
+
if (socket.connected || socket.active || this.authBusyReconnect.pending) return;
|
|
4202
6182
|
this.setConnection("connecting");
|
|
4203
6183
|
socket.connect();
|
|
4204
6184
|
}
|
|
@@ -4230,17 +6210,35 @@ var SessionHub = class {
|
|
|
4230
6210
|
this.bound = true;
|
|
4231
6211
|
const socket = this.client.socket();
|
|
4232
6212
|
socket.on("connect", () => {
|
|
6213
|
+
this.authBusyReconnect.cancel();
|
|
4233
6214
|
const isReconnect = this.hasConnected;
|
|
4234
6215
|
this.hasConnected = true;
|
|
4235
6216
|
this.setConnection("connected");
|
|
4236
6217
|
if (isReconnect) {
|
|
4237
6218
|
for (const session of this.sessions.values()) {
|
|
4238
6219
|
void session._rejoinAfterReconnect();
|
|
6220
|
+
void session.refreshQueue();
|
|
4239
6221
|
}
|
|
4240
6222
|
}
|
|
4241
6223
|
});
|
|
4242
|
-
socket.on("connect_error", () =>
|
|
4243
|
-
|
|
6224
|
+
socket.on("connect_error", (error) => {
|
|
6225
|
+
this.setConnection("disconnected");
|
|
6226
|
+
if (this.sessions.size === 0) {
|
|
6227
|
+
this.authBusyReconnect.cancel();
|
|
6228
|
+
return;
|
|
6229
|
+
}
|
|
6230
|
+
if (this.authBusyReconnect.schedule(error, () => {
|
|
6231
|
+
const current = this.client.socket();
|
|
6232
|
+
if (current !== socket || current.connected || current.active) return;
|
|
6233
|
+
this.setConnection("connecting");
|
|
6234
|
+
current.connect();
|
|
6235
|
+
})) return;
|
|
6236
|
+
this.authBusyReconnect.cancel();
|
|
6237
|
+
});
|
|
6238
|
+
socket.on("disconnect", () => {
|
|
6239
|
+
this.authBusyReconnect.cancel();
|
|
6240
|
+
this.setConnection("disconnected");
|
|
6241
|
+
});
|
|
4244
6242
|
socket.io.on("reconnect_attempt", () => this.setConnection("reconnecting"));
|
|
4245
6243
|
socket.on("turn:start", (data) => {
|
|
4246
6244
|
for (const session of this.route(data.session_id)) session._handleTurnStart(data);
|
|
@@ -4251,9 +6249,25 @@ var SessionHub = class {
|
|
|
4251
6249
|
socket.on("turn:end", (data) => {
|
|
4252
6250
|
for (const session of this.route(data.session_id)) session._handleTurnEnd(data);
|
|
4253
6251
|
});
|
|
4254
|
-
socket.on(
|
|
4255
|
-
|
|
4256
|
-
|
|
6252
|
+
socket.on(
|
|
6253
|
+
"turn:timing",
|
|
6254
|
+
(data) => {
|
|
6255
|
+
for (const session of this.route(data.session_id)) {
|
|
6256
|
+
session._handleTurnEvents([
|
|
6257
|
+
{ type: "llm:first_token:delivered", payload: data }
|
|
6258
|
+
]);
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
);
|
|
6262
|
+
socket.on(
|
|
6263
|
+
"turn:events",
|
|
6264
|
+
(data, acknowledge) => {
|
|
6265
|
+
acknowledgeTurnEvents(acknowledge);
|
|
6266
|
+
for (const session of this.route(data.session_id)) {
|
|
6267
|
+
session._handleTurnEvents(data.events ?? [], data.replayed === true);
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
);
|
|
4257
6271
|
socket.on("chat:start", (data) => {
|
|
4258
6272
|
for (const session of this.route(data?.session_id)) session._handleChatStart();
|
|
4259
6273
|
});
|
|
@@ -4262,8 +6276,17 @@ var SessionHub = class {
|
|
|
4262
6276
|
session._handleChatEnd(data?.status ?? "completed");
|
|
4263
6277
|
}
|
|
4264
6278
|
});
|
|
4265
|
-
socket.on(
|
|
4266
|
-
|
|
6279
|
+
socket.on(
|
|
6280
|
+
"system:error",
|
|
6281
|
+
(data) => {
|
|
6282
|
+
const code = data.code == null ? void 0 : String(data.code);
|
|
6283
|
+
for (const session of this.route(data.session_id)) {
|
|
6284
|
+
session._handleSystemError(data.message, code);
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
6287
|
+
);
|
|
6288
|
+
socket.on("chat:queue:updated", (data) => {
|
|
6289
|
+
for (const session of this.route(data?.session_id)) session._handleQueueUpdated(data);
|
|
4267
6290
|
});
|
|
4268
6291
|
socket.on(
|
|
4269
6292
|
"system:notification",
|
|
@@ -4381,21 +6404,13 @@ function resolveAuthToken(options) {
|
|
|
4381
6404
|
return token ? token : null;
|
|
4382
6405
|
}
|
|
4383
6406
|
|
|
4384
|
-
// src/version.ts
|
|
4385
|
-
var SDK_NAME = "agent-client";
|
|
4386
|
-
var SDK_VERSION = true ? "2610.0.1-alpha.1" : "1.1.1";
|
|
4387
|
-
|
|
4388
6407
|
// src/socket.ts
|
|
4389
|
-
function withSdkIdentity(auth) {
|
|
4390
|
-
return { ...auth ?? {}, sdk: SDK_NAME, sdk_version: SDK_VERSION };
|
|
4391
|
-
}
|
|
4392
6408
|
function createSocket(options) {
|
|
4393
|
-
const token = resolveAuthToken(options);
|
|
4394
6409
|
return io(options.baseUrl, {
|
|
4395
6410
|
path: options.path ?? "/socket.io",
|
|
4396
6411
|
withCredentials: true,
|
|
4397
|
-
|
|
4398
|
-
auth: typeof options.token === "function" ? (cb) => cb(
|
|
6412
|
+
...options.transports ? { transports: options.transports } : {},
|
|
6413
|
+
auth: typeof options.token === "function" ? (cb) => cb(buildSocketAuth(options) ?? {}) : buildSocketAuth(options) ?? {},
|
|
4399
6414
|
autoConnect: false
|
|
4400
6415
|
});
|
|
4401
6416
|
}
|
|
@@ -4411,8 +6426,17 @@ var BladeClient = class {
|
|
|
4411
6426
|
options;
|
|
4412
6427
|
auth;
|
|
4413
6428
|
headless;
|
|
6429
|
+
computers;
|
|
4414
6430
|
models;
|
|
4415
6431
|
sessions;
|
|
6432
|
+
/** 按用户读取的插件目录与图标;没有会话时也能用。 */
|
|
6433
|
+
plugins;
|
|
6434
|
+
chatGroups;
|
|
6435
|
+
/**
|
|
6436
|
+
* 全站访问统计:匿名读写,不带 bearer 也不触发登录刷新。
|
|
6437
|
+
* 只有内置 Web 的同源页面使用,第三方宿主页面不会被自动统计。
|
|
6438
|
+
*/
|
|
6439
|
+
siteVisits;
|
|
4416
6440
|
/** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
|
|
4417
6441
|
hub;
|
|
4418
6442
|
constructor(options) {
|
|
@@ -4422,8 +6446,12 @@ var BladeClient = class {
|
|
|
4422
6446
|
};
|
|
4423
6447
|
this.auth = new AuthResource(this);
|
|
4424
6448
|
this.headless = new HeadlessResource(this);
|
|
6449
|
+
this.computers = new ComputersResource(this);
|
|
4425
6450
|
this.models = new ModelsResource(this);
|
|
4426
6451
|
this.sessions = new SessionsResource(this);
|
|
6452
|
+
this.plugins = new PluginsResource(this);
|
|
6453
|
+
this.chatGroups = new ChatGroupsResource(this);
|
|
6454
|
+
this.siteVisits = new SiteVisitsResource(this);
|
|
4427
6455
|
this.hub = new SessionHub(this);
|
|
4428
6456
|
if (this.options.token === void 0) {
|
|
4429
6457
|
this.runtimeToken = readStoredToken(this.options.baseUrl);
|
|
@@ -4518,6 +6546,16 @@ var BladeClient = class {
|
|
|
4518
6546
|
}
|
|
4519
6547
|
return this.socketInstance;
|
|
4520
6548
|
}
|
|
6549
|
+
requestCommand(event, payload, timeoutMs) {
|
|
6550
|
+
const send = () => new Promise((resolve, reject) => {
|
|
6551
|
+
;
|
|
6552
|
+
this.socket().timeout(timeoutMs).emit(event, payload, (error, ack) => {
|
|
6553
|
+
if (error) reject(error);
|
|
6554
|
+
else resolve(ack ?? {});
|
|
6555
|
+
});
|
|
6556
|
+
});
|
|
6557
|
+
return this.options.commandTransport?.(event, payload, send) ?? send();
|
|
6558
|
+
}
|
|
4521
6559
|
async json(method, path, body) {
|
|
4522
6560
|
const response = await this.fetch(method, path, {
|
|
4523
6561
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
@@ -4582,7 +6620,7 @@ var BladeClient = class {
|
|
|
4582
6620
|
signal: init.signal
|
|
4583
6621
|
});
|
|
4584
6622
|
if (response.status === 401 && !isRetry && this.shouldRefreshFor401(url)) {
|
|
4585
|
-
const refreshed = await this.
|
|
6623
|
+
const refreshed = await this.tryRefreshUnlessAborted(init.signal);
|
|
4586
6624
|
if (refreshed) {
|
|
4587
6625
|
return this.fetch(method, path, init, true);
|
|
4588
6626
|
}
|
|
@@ -4594,7 +6632,7 @@ var BladeClient = class {
|
|
|
4594
6632
|
return response;
|
|
4595
6633
|
}
|
|
4596
6634
|
buildAuthedUrl(path) {
|
|
4597
|
-
const url =
|
|
6635
|
+
const url = buildBackendUrl(this.options.baseUrl, path);
|
|
4598
6636
|
const token = this.resolveTokenForUrl(url);
|
|
4599
6637
|
if (token) {
|
|
4600
6638
|
url.searchParams.set("token", token);
|
|
@@ -4606,6 +6644,7 @@ var BladeClient = class {
|
|
|
4606
6644
|
if (!this.isSameBackendUrl(url)) {
|
|
4607
6645
|
return result;
|
|
4608
6646
|
}
|
|
6647
|
+
result.set("X-Blade-Client", `${SDK_NAME}/${SDK_VERSION}`);
|
|
4609
6648
|
const token = this.resolveRestToken();
|
|
4610
6649
|
if (token) {
|
|
4611
6650
|
result.set("Authorization", `Bearer ${token}`);
|
|
@@ -4613,14 +6652,11 @@ var BladeClient = class {
|
|
|
4613
6652
|
return result;
|
|
4614
6653
|
}
|
|
4615
6654
|
buildUrl(path) {
|
|
4616
|
-
return
|
|
6655
|
+
return buildBackendUrl(this.options.baseUrl, path);
|
|
4617
6656
|
}
|
|
4618
6657
|
baseUrlWithTrailingSlash() {
|
|
4619
6658
|
return this.options.baseUrl.endsWith("/") ? this.options.baseUrl : `${this.options.baseUrl}/`;
|
|
4620
6659
|
}
|
|
4621
|
-
toBaseRelativePath(path) {
|
|
4622
|
-
return path.startsWith("/") ? path.slice(1) : path;
|
|
4623
|
-
}
|
|
4624
6660
|
isSameBackendUrl(url) {
|
|
4625
6661
|
const base = new URL(this.baseUrlWithTrailingSlash());
|
|
4626
6662
|
if (url.origin !== base.origin) {
|
|
@@ -4639,6 +6675,27 @@ var BladeClient = class {
|
|
|
4639
6675
|
hasExplicitBearerToken() {
|
|
4640
6676
|
return Boolean(this.resolveRestToken());
|
|
4641
6677
|
}
|
|
6678
|
+
/**
|
|
6679
|
+
* 等一次令牌刷新,但调用方中止请求时不再等下去。
|
|
6680
|
+
*
|
|
6681
|
+
* 刷新本身是共享的(`refreshPromise`,别的请求也在等它),这里只是让这次已被取消的
|
|
6682
|
+
* 请求尽早结束——否则它会一直挂在调用方(定位)的串行队列里,用户改选的新目标要等到
|
|
6683
|
+
* 刷新结束才轮得到。
|
|
6684
|
+
*/
|
|
6685
|
+
async tryRefreshUnlessAborted(signal) {
|
|
6686
|
+
if (!signal) return this.tryRefresh();
|
|
6687
|
+
if (signal.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
6688
|
+
let onAbort;
|
|
6689
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
6690
|
+
onAbort = () => reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
6691
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
6692
|
+
});
|
|
6693
|
+
try {
|
|
6694
|
+
return await Promise.race([this.tryRefresh(), aborted]);
|
|
6695
|
+
} finally {
|
|
6696
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
4642
6699
|
async tryRefresh() {
|
|
4643
6700
|
if (this.refreshPromise) {
|
|
4644
6701
|
return this.refreshPromise;
|
|
@@ -4649,7 +6706,9 @@ var BladeClient = class {
|
|
|
4649
6706
|
this.buildUrl(REFRESH_PATH).toString(),
|
|
4650
6707
|
{
|
|
4651
6708
|
method: "POST",
|
|
4652
|
-
credentials: "include"
|
|
6709
|
+
credentials: "include",
|
|
6710
|
+
// Refresh remains cookie-only; do not attach the REST bearer.
|
|
6711
|
+
headers: { "X-Blade-Client": `${SDK_NAME}/${SDK_VERSION}` }
|
|
4653
6712
|
}
|
|
4654
6713
|
);
|
|
4655
6714
|
if (!response.ok) {
|
|
@@ -4776,6 +6835,122 @@ function parseXhrHeaders(rawHeaders) {
|
|
|
4776
6835
|
return headers;
|
|
4777
6836
|
}
|
|
4778
6837
|
|
|
6838
|
+
// src/polling-backoff.ts
|
|
6839
|
+
var MAX_BACKOFF_MS = 6e4;
|
|
6840
|
+
var MAX_TIMER_MS = 2147483647;
|
|
6841
|
+
var PollingBackoff = class {
|
|
6842
|
+
constructor(intervalMs) {
|
|
6843
|
+
this.intervalMs = intervalMs;
|
|
6844
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0 || intervalMs > MAX_BACKOFF_MS) {
|
|
6845
|
+
throw new RangeError("Polling interval must be between 0 and 60000 ms");
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
intervalMs;
|
|
6849
|
+
failures = 0;
|
|
6850
|
+
nextAt = 0;
|
|
6851
|
+
reset() {
|
|
6852
|
+
this.failures = 0;
|
|
6853
|
+
this.nextAt = 0;
|
|
6854
|
+
}
|
|
6855
|
+
failed(error) {
|
|
6856
|
+
if (error instanceof Error && error.name === "AbortError" || error instanceof BladeApiError && error.status < 500 && error.status !== 408 && error.status !== 429) {
|
|
6857
|
+
this.nextAt = false;
|
|
6858
|
+
return;
|
|
6859
|
+
}
|
|
6860
|
+
this.failures = Math.min(this.failures + 1, 10);
|
|
6861
|
+
const ceiling = Math.min(MAX_BACKOFF_MS, this.intervalMs * 2 ** this.failures);
|
|
6862
|
+
const backoff = Math.max(this.intervalMs, ceiling * (0.5 + Math.random() * 0.5));
|
|
6863
|
+
const header = error instanceof BladeApiError ? error.response.headers.get("Retry-After")?.trim() : void 0;
|
|
6864
|
+
const retryAfter = header ? /^\d+$/.test(header) ? Number(header) * 1e3 : Date.parse(header) - Date.now() : 0;
|
|
6865
|
+
const delay = Math.max(backoff, Number.isNaN(retryAfter) ? 0 : retryAfter);
|
|
6866
|
+
this.nextAt = delay > MAX_TIMER_MS ? false : Date.now() + delay;
|
|
6867
|
+
}
|
|
6868
|
+
/** Zero means ready, false means stop, otherwise wait at least this many ms. */
|
|
6869
|
+
waitMs() {
|
|
6870
|
+
return this.nextAt === false ? false : Math.max(0, this.nextAt - Date.now());
|
|
6871
|
+
}
|
|
6872
|
+
};
|
|
6873
|
+
|
|
6874
|
+
// src/platform-endpoints.ts
|
|
6875
|
+
var PLATFORM_SERVICE_NAMES = [
|
|
6876
|
+
"os",
|
|
6877
|
+
"hub",
|
|
6878
|
+
"agent",
|
|
6879
|
+
"oauth",
|
|
6880
|
+
"llmGateway",
|
|
6881
|
+
"gitea",
|
|
6882
|
+
"tile"
|
|
6883
|
+
];
|
|
6884
|
+
var EMPTY_PLATFORM_ENDPOINTS = Object.freeze({
|
|
6885
|
+
services: Object.freeze({})
|
|
6886
|
+
});
|
|
6887
|
+
function defaultDocumentBaseUrl() {
|
|
6888
|
+
if (typeof document === "undefined") return void 0;
|
|
6889
|
+
if (document.querySelector("base[href]")) return document.baseURI;
|
|
6890
|
+
if (typeof location !== "undefined" && location.origin) return `${location.origin}/`;
|
|
6891
|
+
return document.baseURI;
|
|
6892
|
+
}
|
|
6893
|
+
function normalizeServiceUrl(value) {
|
|
6894
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
6895
|
+
if (value.includes("?") || value.includes("#")) return null;
|
|
6896
|
+
try {
|
|
6897
|
+
const url = new URL(value.trim());
|
|
6898
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
6899
|
+
if (url.username || url.password || url.search || url.hash) return null;
|
|
6900
|
+
return url.toString().replace(/\/$/, "");
|
|
6901
|
+
} catch {
|
|
6902
|
+
return null;
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
function parsePlatformEndpoints(value) {
|
|
6906
|
+
if (!isRecord(value) || !isRecord(value.services)) return null;
|
|
6907
|
+
const services = {};
|
|
6908
|
+
for (const name of PLATFORM_SERVICE_NAMES) {
|
|
6909
|
+
if (!(name in value.services)) continue;
|
|
6910
|
+
const url = normalizeServiceUrl(value.services[name]);
|
|
6911
|
+
if (!url) return null;
|
|
6912
|
+
if (name === "agent" && new URL(url).pathname !== "/") return null;
|
|
6913
|
+
services[name] = url;
|
|
6914
|
+
}
|
|
6915
|
+
return { services };
|
|
6916
|
+
}
|
|
6917
|
+
async function loadPlatformEndpoints(options = {}) {
|
|
6918
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
6919
|
+
const baseUrl = options.baseUrl ?? defaultDocumentBaseUrl();
|
|
6920
|
+
if (!fetchImpl || !baseUrl) return EMPTY_PLATFORM_ENDPOINTS;
|
|
6921
|
+
const controller = new AbortController();
|
|
6922
|
+
const timeout = globalThis.setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
|
|
6923
|
+
try {
|
|
6924
|
+
const response = await fetchImpl(new URL("config.json", baseUrl), {
|
|
6925
|
+
cache: "no-store",
|
|
6926
|
+
credentials: "same-origin",
|
|
6927
|
+
headers: { Accept: "application/json" },
|
|
6928
|
+
signal: controller.signal
|
|
6929
|
+
});
|
|
6930
|
+
if (!response.ok) return EMPTY_PLATFORM_ENDPOINTS;
|
|
6931
|
+
return parsePlatformEndpoints(await response.json()) ?? EMPTY_PLATFORM_ENDPOINTS;
|
|
6932
|
+
} catch {
|
|
6933
|
+
return EMPTY_PLATFORM_ENDPOINTS;
|
|
6934
|
+
} finally {
|
|
6935
|
+
globalThis.clearTimeout(timeout);
|
|
6936
|
+
}
|
|
6937
|
+
}
|
|
6938
|
+
function resolveServiceUrl(endpoints, name, path = "") {
|
|
6939
|
+
const baseUrl = endpoints.services[name];
|
|
6940
|
+
if (!baseUrl) return null;
|
|
6941
|
+
if (!path) return baseUrl;
|
|
6942
|
+
const pathReference = path.trimStart();
|
|
6943
|
+
if (path.includes("\\") || pathReference.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(pathReference)) {
|
|
6944
|
+
return null;
|
|
6945
|
+
}
|
|
6946
|
+
const base = new URL(`${baseUrl}/`);
|
|
6947
|
+
const target = new URL(path.replace(/^\/+/, ""), base);
|
|
6948
|
+
if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) {
|
|
6949
|
+
return null;
|
|
6950
|
+
}
|
|
6951
|
+
return target.toString();
|
|
6952
|
+
}
|
|
6953
|
+
|
|
4779
6954
|
// src/commands/protocol.ts
|
|
4780
6955
|
function isCommandEnvelope(value) {
|
|
4781
6956
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -4835,6 +7010,19 @@ function connectEmbedded(options) {
|
|
|
4835
7010
|
};
|
|
4836
7011
|
}
|
|
4837
7012
|
|
|
7013
|
+
// src/schemas/projection-utils.ts
|
|
7014
|
+
function latestPostChatFollowup(turns) {
|
|
7015
|
+
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
7016
|
+
const turn = turns[index];
|
|
7017
|
+
if (turn.loop_id === "root" && (turn.role === "user" || turn.role === "assistant") && (turn.kind ?? "message") === "message" && !turn.blocks.some(
|
|
7018
|
+
(block) => ["mode_change", "plan_status", "ask_user_answer"].includes(block.type)
|
|
7019
|
+
)) {
|
|
7020
|
+
return turn.role === "assistant" ? turn.post_chat_followup ?? null : null;
|
|
7021
|
+
}
|
|
7022
|
+
}
|
|
7023
|
+
return null;
|
|
7024
|
+
}
|
|
7025
|
+
|
|
4838
7026
|
// src/schemas/solution.ts
|
|
4839
7027
|
var LayoutType = /* @__PURE__ */ ((LayoutType2) => {
|
|
4840
7028
|
LayoutType2["Default"] = "default";
|
|
@@ -4863,12 +7051,20 @@ var Task = type2({
|
|
|
4863
7051
|
});
|
|
4864
7052
|
export {
|
|
4865
7053
|
AgentSession,
|
|
7054
|
+
AgentSessionError,
|
|
7055
|
+
AuthBusyReconnect,
|
|
4866
7056
|
BladeApiError,
|
|
4867
7057
|
BladeClient,
|
|
7058
|
+
ChatGroupsResource,
|
|
4868
7059
|
ClientProjectionBuilder,
|
|
7060
|
+
ComputersResource,
|
|
4869
7061
|
DEFAULT_REPLAY_SPEED,
|
|
7062
|
+
EMPTY_PLATFORM_ENDPOINTS,
|
|
7063
|
+
EMPTY_SESSION_QUEUE,
|
|
4870
7064
|
LayoutType,
|
|
4871
7065
|
ModelsResource,
|
|
7066
|
+
PluginsResource,
|
|
7067
|
+
PollingBackoff,
|
|
4872
7068
|
SDK_NAME,
|
|
4873
7069
|
SDK_VERSION,
|
|
4874
7070
|
SessionHub,
|
|
@@ -4877,20 +7073,61 @@ export {
|
|
|
4877
7073
|
SessionStatus,
|
|
4878
7074
|
Task,
|
|
4879
7075
|
TaskStatus,
|
|
7076
|
+
acceptedPostChatFollowupCompletesLatestRun,
|
|
7077
|
+
acknowledgeTurnEvents,
|
|
7078
|
+
authBusyRetryDelayMs,
|
|
4880
7079
|
buildMessageContent,
|
|
7080
|
+
buildToolUiCardKey,
|
|
7081
|
+
canCancelQueuedMessage,
|
|
7082
|
+
canDeliverQueuedMessage,
|
|
7083
|
+
canEditQueuedMessage,
|
|
7084
|
+
canToggleComputer,
|
|
7085
|
+
chatErrorForDisplay,
|
|
7086
|
+
collectInlineToolUiCards,
|
|
7087
|
+
collectPreviewToolUiCards,
|
|
7088
|
+
computerDaemonVersion,
|
|
7089
|
+
computerOS,
|
|
7090
|
+
computerPlatformLabel,
|
|
7091
|
+
computerState,
|
|
4881
7092
|
connectEmbedded,
|
|
4882
7093
|
contentPreview,
|
|
7094
|
+
contextProjectionData,
|
|
4883
7095
|
createInitialSessionState,
|
|
4884
7096
|
createSocket,
|
|
4885
7097
|
extractTextAttachments,
|
|
7098
|
+
getContextDisplayState,
|
|
7099
|
+
getContextGroupDisplayState,
|
|
4886
7100
|
getFileParts,
|
|
4887
7101
|
getImageParts,
|
|
4888
7102
|
getTextContent,
|
|
7103
|
+
groupAdjacentContextRuns,
|
|
4889
7104
|
groupMessagesByLoop,
|
|
7105
|
+
hasChatRunEvent,
|
|
7106
|
+
inferToolStatus,
|
|
7107
|
+
isAppDevToolUiCard,
|
|
7108
|
+
isBackgroundWakeMessage,
|
|
4890
7109
|
isCommandEnvelope,
|
|
7110
|
+
isEntryGoneError,
|
|
4891
7111
|
isHiddenInternalMessage,
|
|
4892
7112
|
isInboundEnvelope,
|
|
7113
|
+
isInternalStatusToolUiCard,
|
|
7114
|
+
isMcpAppData,
|
|
7115
|
+
isSessionQueueSnapshot,
|
|
7116
|
+
isToolUiCard,
|
|
7117
|
+
latestPostChatFollowup,
|
|
7118
|
+
loadPlatformEndpoints,
|
|
4893
7119
|
normalizeMessageContent,
|
|
7120
|
+
parseQueueAck,
|
|
7121
|
+
prependOlder,
|
|
7122
|
+
queuePauseReasonLabel,
|
|
7123
|
+
queuedMessageStatusLabel,
|
|
7124
|
+
reconcileHistoricalUserTurns,
|
|
7125
|
+
reconcileOptimisticUserTurns,
|
|
7126
|
+
replaceWindow,
|
|
7127
|
+
resolveServiceUrl,
|
|
7128
|
+
resolveToolUiCardContent,
|
|
7129
|
+
shouldApplySnapshot,
|
|
7130
|
+
sortComputers,
|
|
4894
7131
|
toReplaySnapshot,
|
|
4895
7132
|
transformSlashCommand
|
|
4896
7133
|
};
|