@blade-hq/agent-client 2610.0.0-beta.8 → 2610.0.0-beta.80
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 +250 -9
- package/dist/auth.d.ts +0 -1
- package/dist/blade-client.d.ts +21 -3
- package/dist/index.d.ts +35 -6
- package/dist/index.js +2227 -270
- 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 +29 -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 +187 -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 +13 -1
- package/dist/schemas/message.d.ts +3 -0
- package/dist/schemas/projection.d.ts +37 -2
- package/dist/schemas/session.d.ts +10 -1
- package/dist/session/agent-session.d.ts +144 -16
- package/dist/session/events.d.ts +6 -0
- package/dist/session/history-page.d.ts +9 -0
- package/dist/session/hub.d.ts +9 -1
- package/dist/session/queue.d.ts +99 -0
- package/dist/session/state.d.ts +18 -2
- package/dist/shared/auth-busy.d.ts +9 -0
- package/dist/shared/projection/builder.d.ts +12 -0
- package/dist/shared/projection/context.d.ts +3 -0
- package/dist/shared/projection/helpers.d.ts +2 -2
- package/dist/shared/projection/history.d.ts +1 -0
- package/dist/shared/projection/index.d.ts +2 -1
- package/dist/shared/projection/state.d.ts +7 -0
- package/dist/shared/tool-ui-card.d.ts +58 -0
- package/dist/shared/type-guards.d.ts +2 -0
- package/dist/types/socket-events.d.ts +199 -6
- package/package.json +1 -1
- package/public-api.md +923 -14
package/dist/index.js
CHANGED
|
@@ -167,6 +167,10 @@ function decorateAuthError(error) {
|
|
|
167
167
|
}
|
|
168
168
|
return error;
|
|
169
169
|
}
|
|
170
|
+
function buildBackendUrl(baseUrl, path) {
|
|
171
|
+
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
172
|
+
return new URL(path.startsWith("/") ? path.slice(1) : path, base);
|
|
173
|
+
}
|
|
170
174
|
|
|
171
175
|
// src/resources/auth.ts
|
|
172
176
|
var AuthResource = class {
|
|
@@ -214,6 +218,11 @@ var AuthResource = class {
|
|
|
214
218
|
}
|
|
215
219
|
};
|
|
216
220
|
|
|
221
|
+
// src/shared/type-guards.ts
|
|
222
|
+
function isRecord(value) {
|
|
223
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
224
|
+
}
|
|
225
|
+
|
|
217
226
|
// src/resources/headless.ts
|
|
218
227
|
var HeadlessError = class extends Error {
|
|
219
228
|
constructor(message, detail) {
|
|
@@ -352,8 +361,7 @@ var HeadlessResource = class {
|
|
|
352
361
|
socket.on("system:error", onSystemError);
|
|
353
362
|
socket.emit("session:subscribe", {
|
|
354
363
|
session_id,
|
|
355
|
-
event_format: "raw"
|
|
356
|
-
stream_tokens: this.client.options.streamTokens !== false
|
|
364
|
+
event_format: "raw"
|
|
357
365
|
});
|
|
358
366
|
socket.emit(
|
|
359
367
|
"chat:send",
|
|
@@ -395,9 +403,6 @@ function errorMessageFromResult(result) {
|
|
|
395
403
|
}
|
|
396
404
|
return null;
|
|
397
405
|
}
|
|
398
|
-
function isRecord(value) {
|
|
399
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
400
|
-
}
|
|
401
406
|
function ensureSocketConnected(socket, timeoutMs, detail) {
|
|
402
407
|
if (socket.connected) {
|
|
403
408
|
return Promise.resolve();
|
|
@@ -425,6 +430,81 @@ function ensureSocketConnected(socket, timeoutMs, detail) {
|
|
|
425
430
|
});
|
|
426
431
|
}
|
|
427
432
|
|
|
433
|
+
// src/resources/computers.ts
|
|
434
|
+
function computerState(computer) {
|
|
435
|
+
if (computer.is_primary) return "primary";
|
|
436
|
+
if (computer.enabled) return "enabled";
|
|
437
|
+
return computer.online ? "idle" : "offline";
|
|
438
|
+
}
|
|
439
|
+
function computerOS(computer) {
|
|
440
|
+
const os = (computer.os ?? "").toLowerCase();
|
|
441
|
+
if (os.startsWith("darwin") || os.startsWith("mac")) return "macos";
|
|
442
|
+
if (os.startsWith("windows")) return "windows";
|
|
443
|
+
if (os.startsWith("linux")) return "linux";
|
|
444
|
+
return "unknown";
|
|
445
|
+
}
|
|
446
|
+
function computerPlatformLabel(computer) {
|
|
447
|
+
const os = (computer.os ?? "").trim();
|
|
448
|
+
const arch = (computer.arch ?? "").trim();
|
|
449
|
+
if (os && arch) return `${os}/${arch}`;
|
|
450
|
+
return os || arch || "";
|
|
451
|
+
}
|
|
452
|
+
function computerDaemonVersion(computer) {
|
|
453
|
+
return (computer.daemon_version ?? "").trim();
|
|
454
|
+
}
|
|
455
|
+
function canToggleComputer(computer) {
|
|
456
|
+
return !computer.is_primary;
|
|
457
|
+
}
|
|
458
|
+
function sortComputers(computers) {
|
|
459
|
+
return [...computers].sort((a, b) => {
|
|
460
|
+
const byCreated = (a.created_at ?? "").localeCompare(b.created_at ?? "");
|
|
461
|
+
if (byCreated !== 0) return byCreated;
|
|
462
|
+
return a.label.localeCompare(b.label);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
var ComputersResource = class {
|
|
466
|
+
constructor(client) {
|
|
467
|
+
this.client = client;
|
|
468
|
+
}
|
|
469
|
+
client;
|
|
470
|
+
/**
|
|
471
|
+
* 列出当前账号登记过的全部电脑及其在线状态。
|
|
472
|
+
*
|
|
473
|
+
* 首页还没有会话,也要能看到同一份清单——判定各写一份必然分叉,一边的兜底会把
|
|
474
|
+
* 另一边的错误盖住(#1597)。所以账号事实只从这一个接口读。
|
|
475
|
+
*/
|
|
476
|
+
async listAccount() {
|
|
477
|
+
const data = await this.client.json("GET", "/api/daemon/instances");
|
|
478
|
+
return {
|
|
479
|
+
computers: (data.instances ?? []).map((instance) => ({
|
|
480
|
+
...instance,
|
|
481
|
+
online: Boolean(instance.online)
|
|
482
|
+
}))
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* 列出当前用户的全部电脑,带上"在不在线"和"这次会话能不能用"。
|
|
487
|
+
* 服务端只查库,电脑离线时同样返回,所以拿得到"它离线了"这个结论。
|
|
488
|
+
*/
|
|
489
|
+
list(sessionId) {
|
|
490
|
+
return this.client.json(
|
|
491
|
+
"GET",
|
|
492
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/computers`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* 启动或停止一台电脑。只改这次会话的可用范围,不会碰远程机器,
|
|
497
|
+
* 电脑离线时同样能登记,等它上线就直接可用。
|
|
498
|
+
*/
|
|
499
|
+
setEnabled(sessionId, computer, enabled) {
|
|
500
|
+
return this.client.json(
|
|
501
|
+
"PUT",
|
|
502
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/computers/${encodeURIComponent(computer)}/enabled`,
|
|
503
|
+
{ enabled }
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
428
508
|
// src/resources/models.ts
|
|
429
509
|
var ModelsResource = class {
|
|
430
510
|
constructor(client) {
|
|
@@ -451,6 +531,7 @@ var SessionInfo = type({
|
|
|
451
531
|
"shared?": "boolean",
|
|
452
532
|
"memory_enabled?": "boolean",
|
|
453
533
|
"is_persistent?": "boolean",
|
|
534
|
+
"initiator?": "'ui' | 'sdk' | 'unknown' | null",
|
|
454
535
|
"is_headless?": "boolean",
|
|
455
536
|
"viewer_role?": "'owner' | 'viewer'",
|
|
456
537
|
"template_id?": "string | null",
|
|
@@ -473,7 +554,11 @@ var SessionInfo = type({
|
|
|
473
554
|
"disable_tools?": "string[]",
|
|
474
555
|
"runtime_type?": "string | null",
|
|
475
556
|
"daemon_id?": "string | null",
|
|
557
|
+
"agent_runtime_id?": "string | null",
|
|
476
558
|
"workspace_path?": "string | null",
|
|
559
|
+
"ba_version?": "string | null",
|
|
560
|
+
"sandbox_version?": "string | null",
|
|
561
|
+
"chat_group_id?": "string | null",
|
|
477
562
|
"match?": "unknown"
|
|
478
563
|
});
|
|
479
564
|
|
|
@@ -539,7 +624,7 @@ function upsertToolCall(state, toolCallId, toolName, displayName, args) {
|
|
|
539
624
|
});
|
|
540
625
|
}
|
|
541
626
|
}
|
|
542
|
-
function applyToolResult(state, toolCallId, result, status, durationMs, sourceLoop) {
|
|
627
|
+
function applyToolResult(state, toolCallId, result, status, durationMs, ranInParallel, sourceLoop, startedAt = null) {
|
|
543
628
|
let toolName = "";
|
|
544
629
|
let displayName = "";
|
|
545
630
|
let finalResult = result;
|
|
@@ -551,6 +636,8 @@ function applyToolResult(state, toolCallId, result, status, durationMs, sourceLo
|
|
|
551
636
|
}
|
|
552
637
|
const update = { status, result: finalResult };
|
|
553
638
|
if (durationMs != null) update.duration_ms = durationMs;
|
|
639
|
+
if (ranInParallel != null) update.ran_in_parallel = ranInParallel;
|
|
640
|
+
if (startedAt != null) update.started_at = startedAt;
|
|
554
641
|
state.toolCalls[i] = { ...tc, ...update };
|
|
555
642
|
toolName = tc.tool_name;
|
|
556
643
|
displayName = tc.display_name;
|
|
@@ -732,11 +819,17 @@ function buildCompactionTurn(sequence, compactionId, loopId, status, data) {
|
|
|
732
819
|
summary_preview: summaryPreview,
|
|
733
820
|
summary_full: summaryFull,
|
|
734
821
|
archived_count: archivedCount,
|
|
822
|
+
archived_files: Array.isArray(data.archived_files) ? data.archived_files.filter(
|
|
823
|
+
(item) => typeof item === "object" && item !== null && !Array.isArray(item)
|
|
824
|
+
) : null,
|
|
825
|
+
archived_tool_calls: Array.isArray(data.archived_tool_calls) ? data.archived_tool_calls.filter(
|
|
826
|
+
(item) => typeof item === "object" && item !== null && !Array.isArray(item)
|
|
827
|
+
) : null,
|
|
735
828
|
tokens_before: tokensBefore,
|
|
736
829
|
tokens_after: tokensAfter,
|
|
737
830
|
saved_ratio: savedRatio,
|
|
738
831
|
trigger: asTrigger(data.trigger),
|
|
739
|
-
failure_reason:
|
|
832
|
+
failure_reason: [data.failure_reason, data.fallback_reason, data.reason].find((value) => typeof value === "string" && value.length > 0) ?? null,
|
|
740
833
|
fallback_applied: typeof data.fallback_applied === "boolean" ? data.fallback_applied : null
|
|
741
834
|
};
|
|
742
835
|
}
|
|
@@ -991,6 +1084,12 @@ function createTurnState(turnId, loopId, opts = {}) {
|
|
|
991
1084
|
toolCalls: [],
|
|
992
1085
|
usage: null,
|
|
993
1086
|
responseDone: false,
|
|
1087
|
+
firstTokenMs: null,
|
|
1088
|
+
streamDurationMs: null,
|
|
1089
|
+
firstTokenDeliveryMs: null,
|
|
1090
|
+
modelStartedAt: null,
|
|
1091
|
+
modelDurationMs: null,
|
|
1092
|
+
modelStartedAtReported: false,
|
|
994
1093
|
memoryRefs: opts.memoryRefs ?? null,
|
|
995
1094
|
parentForkToolCallId: null
|
|
996
1095
|
};
|
|
@@ -1008,8 +1107,13 @@ function snapshot(state, sequence, status) {
|
|
|
1008
1107
|
tool_calls: [...state.toolCalls],
|
|
1009
1108
|
model: state.model,
|
|
1010
1109
|
usage: state.usage ? { ...state.usage } : null,
|
|
1011
|
-
duration_ms:
|
|
1012
|
-
|
|
1110
|
+
duration_ms: state.modelDurationMs,
|
|
1111
|
+
first_token_ms: state.firstTokenMs,
|
|
1112
|
+
stream_duration_ms: state.streamDurationMs,
|
|
1113
|
+
first_token_delivery_ms: state.firstTokenDeliveryMs,
|
|
1114
|
+
started_at: state.modelStartedAt ?? new Date(state.startedAt).toISOString(),
|
|
1115
|
+
// 没有 payload 上报时用的是 loop:turn 的浏览器事件时间,标记出来供瀑布区分。
|
|
1116
|
+
has_reported_timing: state.modelStartedAtReported,
|
|
1013
1117
|
context_window: state.contextWindow,
|
|
1014
1118
|
memory_refs: state.memoryRefs,
|
|
1015
1119
|
parent_fork_tool_call_id: state.parentForkToolCallId
|
|
@@ -1023,10 +1127,134 @@ function toolStatusFromResult(result) {
|
|
|
1023
1127
|
return inferToolStatus(text);
|
|
1024
1128
|
}
|
|
1025
1129
|
|
|
1130
|
+
// src/schemas/context.ts
|
|
1131
|
+
var BUILTIN_CONTEXT_TITLES = {
|
|
1132
|
+
"agent-instructions": "\u9879\u76EE\u8BF4\u660E",
|
|
1133
|
+
"skill-catalog": "\u53EF\u7528\u80FD\u529B",
|
|
1134
|
+
"runtime-context": "\u5F53\u524D\u5DE5\u4F5C\u73AF\u5883",
|
|
1135
|
+
"workspace-context": "\u5F53\u524D\u5DE5\u4F5C\u533A\u73AF\u5883"
|
|
1136
|
+
};
|
|
1137
|
+
var BUILTIN_CONTEXT_DETAILS = {
|
|
1138
|
+
"agent-instructions": "\u8FD9\u4E9B\u8BF4\u660E\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u7406\u89E3\u5F53\u524D\u9879\u76EE\u548C\u5DE5\u4F5C\u8981\u6C42\u3002",
|
|
1139
|
+
"skill-catalog": "\u8FD9\u4E9B\u80FD\u529B\u4F1A\u5E2E\u52A9\u667A\u80FD\u4F53\u9009\u62E9\u5408\u9002\u7684\u65B9\u5F0F\u5B8C\u6210\u4EFB\u52A1\u3002",
|
|
1140
|
+
"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",
|
|
1141
|
+
"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"
|
|
1142
|
+
};
|
|
1143
|
+
function nonEmptyString(value) {
|
|
1144
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
1145
|
+
}
|
|
1146
|
+
function contextProjectionData(fields) {
|
|
1147
|
+
const contextKind = nonEmptyString(fields.context_kind);
|
|
1148
|
+
const contextKey = nonEmptyString(fields.context_key);
|
|
1149
|
+
const contextName = nonEmptyString(fields.context_name);
|
|
1150
|
+
const contextRevision = nonEmptyString(fields.context_revision);
|
|
1151
|
+
if (!contextKind || !contextKey || !contextName || !contextRevision || typeof fields.context_order !== "number" || !Number.isFinite(fields.context_order) || !fields.context_action) {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
context_kind: contextKind,
|
|
1156
|
+
context_key: contextKey,
|
|
1157
|
+
context_name: contextName,
|
|
1158
|
+
context_order: fields.context_order,
|
|
1159
|
+
context_action: fields.context_action,
|
|
1160
|
+
context_revision: contextRevision,
|
|
1161
|
+
display_title: nonEmptyString(fields.display_title),
|
|
1162
|
+
display_summary: nonEmptyString(fields.display_summary),
|
|
1163
|
+
sources: Array.isArray(fields.sources) ? fields.sources : []
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function getContextDisplayState(context) {
|
|
1167
|
+
const title = nonEmptyString(context.display_title) ?? BUILTIN_CONTEXT_TITLES[context.context_kind] ?? "\u5BF9\u8BDD\u80CC\u666F";
|
|
1168
|
+
const isRemoved = context.context_action === "removed";
|
|
1169
|
+
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");
|
|
1170
|
+
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";
|
|
1171
|
+
return { title, summary, detail, isRemoved };
|
|
1172
|
+
}
|
|
1173
|
+
function groupAdjacentContextRuns(kinds) {
|
|
1174
|
+
const runs = [];
|
|
1175
|
+
let current = null;
|
|
1176
|
+
kinds.forEach((kind, index) => {
|
|
1177
|
+
if (kind === null) {
|
|
1178
|
+
current = null;
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (current === null) {
|
|
1182
|
+
current = [];
|
|
1183
|
+
runs.push(current);
|
|
1184
|
+
}
|
|
1185
|
+
current.push(index);
|
|
1186
|
+
});
|
|
1187
|
+
return runs;
|
|
1188
|
+
}
|
|
1189
|
+
function getContextGroupDisplayState(contexts) {
|
|
1190
|
+
const count = contexts.length;
|
|
1191
|
+
if (count === 1) {
|
|
1192
|
+
const single = getContextDisplayState(contexts[0]);
|
|
1193
|
+
return { title: single.title, summary: single.summary, count };
|
|
1194
|
+
}
|
|
1195
|
+
const removed = contexts.filter(
|
|
1196
|
+
(context) => context.context_action === "removed"
|
|
1197
|
+
).length;
|
|
1198
|
+
const updated = count - removed;
|
|
1199
|
+
const parts = [];
|
|
1200
|
+
if (updated > 0) parts.push(`${updated} \u9879\u5DF2\u66F4\u65B0`);
|
|
1201
|
+
if (removed > 0) parts.push(`${removed} \u9879\u5DF2\u505C\u7528`);
|
|
1202
|
+
return {
|
|
1203
|
+
title: "\u5BF9\u8BDD\u80CC\u666F",
|
|
1204
|
+
summary: parts.join(" \xB7 ") || `${count} \u9879`,
|
|
1205
|
+
count
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/shared/projection/context.ts
|
|
1210
|
+
function buildContextTurn(entry, sequence) {
|
|
1211
|
+
if (entry.kind !== "context" || !isRecord(entry.source) || !isRecord(entry.data)) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
const display = isRecord(entry.data.display) ? entry.data.display : {};
|
|
1215
|
+
const message = isRecord(entry.message) ? entry.message : {};
|
|
1216
|
+
const context = contextProjectionData({
|
|
1217
|
+
context_kind: typeof entry.source.kind === "string" ? entry.source.kind : null,
|
|
1218
|
+
context_key: typeof entry.source.key === "string" ? entry.source.key : null,
|
|
1219
|
+
context_name: typeof entry.data.contribution_name === "string" ? entry.data.contribution_name : null,
|
|
1220
|
+
context_order: typeof entry.data.order === "number" ? entry.data.order : null,
|
|
1221
|
+
context_action: entry.data.action === "published" || entry.data.action === "retained" || entry.data.action === "removed" ? entry.data.action : null,
|
|
1222
|
+
context_revision: typeof entry.data.revision === "string" ? entry.data.revision : null,
|
|
1223
|
+
display_title: typeof display.title === "string" ? display.title : null,
|
|
1224
|
+
display_summary: typeof display.summary === "string" ? display.summary : null,
|
|
1225
|
+
sources: Array.isArray(display.sources) ? display.sources.filter(isRecord) : []
|
|
1226
|
+
});
|
|
1227
|
+
if (!context) return null;
|
|
1228
|
+
const turnId = typeof entry.id === "string" && entry.id ? entry.id : `context-${sequence}`;
|
|
1229
|
+
return {
|
|
1230
|
+
id: turnId,
|
|
1231
|
+
sequence,
|
|
1232
|
+
turn_id: turnId,
|
|
1233
|
+
loop_id: typeof entry.loop_name === "string" && entry.loop_name ? entry.loop_name : "root",
|
|
1234
|
+
kind: "context",
|
|
1235
|
+
role: "system",
|
|
1236
|
+
status: "completed",
|
|
1237
|
+
blocks: [],
|
|
1238
|
+
tool_calls: [],
|
|
1239
|
+
model: null,
|
|
1240
|
+
usage: null,
|
|
1241
|
+
duration_ms: 0,
|
|
1242
|
+
context_content: Object.hasOwn(message, "content") ? message.content : null,
|
|
1243
|
+
...context
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1026
1247
|
// src/shared/projection/builder.ts
|
|
1248
|
+
function backgroundTaskStatus(payload) {
|
|
1249
|
+
const tasks = Array.isArray(payload.tasks) ? payload.tasks : [];
|
|
1250
|
+
return tasks.some((task) => isRecord(task) && (["failed", "cancelled", "lost"].includes(String(task.status)) || typeof task.exit_code === "number" && task.exit_code !== 0)) ? "error" : "done";
|
|
1251
|
+
}
|
|
1252
|
+
function acknowledgeTurnEvents(acknowledge) {
|
|
1253
|
+
acknowledge?.();
|
|
1254
|
+
}
|
|
1027
1255
|
function hasChatRunEvent(events) {
|
|
1028
1256
|
return events.some(
|
|
1029
|
-
(event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
|
|
1257
|
+
(event) => event.type !== "llm:first_token:delivered" && event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
|
|
1030
1258
|
);
|
|
1031
1259
|
}
|
|
1032
1260
|
function acceptedPostChatFollowupCompletesLatestRun(events, updates) {
|
|
@@ -1058,7 +1286,8 @@ function completedResponseText(content) {
|
|
|
1058
1286
|
).filter((part) => part.type === "text").map((part) => String(part.text ?? "")).join("");
|
|
1059
1287
|
}
|
|
1060
1288
|
function replaceTextContent(state, content) {
|
|
1061
|
-
|
|
1289
|
+
const currentContent = state.blocks.filter((block) => block.type === "text").map((block) => String(block.content ?? "")).join("");
|
|
1290
|
+
if (currentContent === content) return;
|
|
1062
1291
|
const replacement = { type: "text", content };
|
|
1063
1292
|
const blocks = [];
|
|
1064
1293
|
let inserted = false;
|
|
@@ -1078,11 +1307,39 @@ function replaceTextContent(state, content) {
|
|
|
1078
1307
|
}
|
|
1079
1308
|
state.blocks = blocks;
|
|
1080
1309
|
}
|
|
1310
|
+
function applyResponseBlockOrder(state, value) {
|
|
1311
|
+
if (!Array.isArray(value)) return false;
|
|
1312
|
+
const ordered = [];
|
|
1313
|
+
for (const item of value) {
|
|
1314
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
|
|
1315
|
+
const block = item;
|
|
1316
|
+
if (block.type === "text" || block.type === "thinking") {
|
|
1317
|
+
const content = String(block[block.type] ?? "");
|
|
1318
|
+
if (content) ordered.push({ type: block.type, content });
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
if (block.type !== "tool_call") continue;
|
|
1322
|
+
const toolCallId = String(block.id ?? "");
|
|
1323
|
+
const toolBlock = state.blocks.find(
|
|
1324
|
+
(candidate) => candidate.type === "tool_use" && candidate.tool_call_id === toolCallId
|
|
1325
|
+
);
|
|
1326
|
+
if (toolBlock) ordered.push(toolBlock);
|
|
1327
|
+
}
|
|
1328
|
+
if (ordered.length === 0 && value.length > 0) return false;
|
|
1329
|
+
const unrelatedBlocks = state.blocks.filter(
|
|
1330
|
+
(block) => block.type !== "text" && block.type !== "thinking" && block.type !== "tool_use"
|
|
1331
|
+
);
|
|
1332
|
+
state.blocks = [...ordered, ...unrelatedBlocks];
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1081
1335
|
var ClientProjectionBuilder = class {
|
|
1082
1336
|
activeTurns = /* @__PURE__ */ new Map();
|
|
1083
1337
|
lastTurnIds = /* @__PURE__ */ new Map();
|
|
1084
1338
|
latestTurns = /* @__PURE__ */ new Map();
|
|
1339
|
+
turnsById = /* @__PURE__ */ new Map();
|
|
1085
1340
|
loopDescriptions = /* @__PURE__ */ new Map();
|
|
1341
|
+
/** loop_name → 正在运行的子智能体起点,agent:end 结算并挂到父工具调用上。 */
|
|
1342
|
+
activeSubagentRuns = /* @__PURE__ */ new Map();
|
|
1086
1343
|
pendingChildPauses = /* @__PURE__ */ new Map();
|
|
1087
1344
|
pendingMemoryRefs = /* @__PURE__ */ new Map();
|
|
1088
1345
|
activeCompactions = /* @__PURE__ */ new Map();
|
|
@@ -1117,6 +1374,8 @@ var ClientProjectionBuilder = class {
|
|
|
1117
1374
|
return this.onMemoryInject(loopId, payload);
|
|
1118
1375
|
case "memory:inject:none":
|
|
1119
1376
|
return null;
|
|
1377
|
+
case "context:entry":
|
|
1378
|
+
return this.onContextEntry(payload);
|
|
1120
1379
|
case "loop:turn":
|
|
1121
1380
|
return this.onLoopTurn(loopId, payload);
|
|
1122
1381
|
case "mode:change":
|
|
@@ -1140,6 +1399,25 @@ var ClientProjectionBuilder = class {
|
|
|
1140
1399
|
}
|
|
1141
1400
|
if (event.type.startsWith("compaction:"))
|
|
1142
1401
|
return this.onCompaction(event.type, loopId, payload);
|
|
1402
|
+
if (event.type === "llm:first_token:delivered") {
|
|
1403
|
+
const targetTurnId = String(payload.turn_id ?? "");
|
|
1404
|
+
const active = this.activeTurns.get(loopId);
|
|
1405
|
+
if (active && (!targetTurnId || active.turnId === targetTurnId)) {
|
|
1406
|
+
this.applyFirstTokenDelivery(active, payload);
|
|
1407
|
+
return this.syncPatch(active);
|
|
1408
|
+
}
|
|
1409
|
+
const latest = targetTurnId ? this.turnsById.get(targetTurnId) : this.latestTurns.get(loopId);
|
|
1410
|
+
const duration = payload.first_token_delivery_ms;
|
|
1411
|
+
if (!latest || latest.role !== "assistant" || typeof duration !== "number" || duration < 0) {
|
|
1412
|
+
return null;
|
|
1413
|
+
}
|
|
1414
|
+
const turn = { ...latest, first_token_delivery_ms: duration };
|
|
1415
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1416
|
+
if (this.latestTurns.get(loopId)?.turn_id === turn.turn_id) {
|
|
1417
|
+
this.latestTurns.set(loopId, turn);
|
|
1418
|
+
}
|
|
1419
|
+
return [{ kind: "upsert", turn }];
|
|
1420
|
+
}
|
|
1143
1421
|
const state = this.activeTurns.get(loopId);
|
|
1144
1422
|
if (!state) {
|
|
1145
1423
|
if (event.type === "chat:end") {
|
|
@@ -1155,16 +1433,28 @@ var ClientProjectionBuilder = class {
|
|
|
1155
1433
|
state.toolCalls = [];
|
|
1156
1434
|
state.usage = null;
|
|
1157
1435
|
state.responseDone = false;
|
|
1436
|
+
state.firstTokenMs = null;
|
|
1437
|
+
state.streamDurationMs = null;
|
|
1438
|
+
state.firstTokenDeliveryMs = null;
|
|
1158
1439
|
return this.syncPatch(state);
|
|
1159
1440
|
case "llm:thinking:delta":
|
|
1441
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1160
1442
|
return this.onDelta(state, "thinking", "", String(payload.content ?? ""));
|
|
1161
1443
|
case "llm:content:delta":
|
|
1444
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1162
1445
|
return this.onDelta(state, "text", "", String(payload.content ?? ""));
|
|
1163
1446
|
case "llm:content:complete":
|
|
1164
1447
|
replaceTextContent(state, String(payload.content ?? ""));
|
|
1165
1448
|
return this.syncPatch(state);
|
|
1449
|
+
case "llm:content:done":
|
|
1450
|
+
this.applyStreamTiming(state, payload);
|
|
1451
|
+
return this.syncPatch(state);
|
|
1166
1452
|
case "llm:tool_call:created":
|
|
1453
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1167
1454
|
return this.onToolCallCreated(state, payload);
|
|
1455
|
+
case "llm:tool_call:arguments:delta":
|
|
1456
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1457
|
+
return this.onToolCallArgumentsDelta(state, payload);
|
|
1168
1458
|
case "llm:response:done":
|
|
1169
1459
|
return this.onResponseDone(state, loopId, payload);
|
|
1170
1460
|
case "tool:result:delta":
|
|
@@ -1191,7 +1481,9 @@ var ClientProjectionBuilder = class {
|
|
|
1191
1481
|
this.activeTurns.clear();
|
|
1192
1482
|
this.lastTurnIds.clear();
|
|
1193
1483
|
this.latestTurns.clear();
|
|
1484
|
+
this.turnsById.clear();
|
|
1194
1485
|
this.loopDescriptions.clear();
|
|
1486
|
+
this.activeSubagentRuns.clear();
|
|
1195
1487
|
this.pendingChildPauses.clear();
|
|
1196
1488
|
this.pendingMemoryRefs.clear();
|
|
1197
1489
|
this.activeCompactions.clear();
|
|
@@ -1206,15 +1498,18 @@ var ClientProjectionBuilder = class {
|
|
|
1206
1498
|
}
|
|
1207
1499
|
_seedTurns(turns) {
|
|
1208
1500
|
const latestTurns = /* @__PURE__ */ new Map();
|
|
1501
|
+
const turnsById = /* @__PURE__ */ new Map();
|
|
1209
1502
|
for (const turn of turns) {
|
|
1210
1503
|
this.seedSequence(turn.sequence);
|
|
1211
1504
|
if (!isConversationTurn(turn)) continue;
|
|
1505
|
+
turnsById.set(turn.turn_id, turn);
|
|
1212
1506
|
const current = latestTurns.get(turn.loop_id);
|
|
1213
1507
|
if (!current || turn.sequence >= current.sequence) {
|
|
1214
1508
|
latestTurns.set(turn.loop_id, turn);
|
|
1215
1509
|
}
|
|
1216
1510
|
}
|
|
1217
1511
|
this.latestTurns = latestTurns;
|
|
1512
|
+
this.turnsById = turnsById;
|
|
1218
1513
|
}
|
|
1219
1514
|
// --- Event handlers ---
|
|
1220
1515
|
onMemoryInject(loopId, payload) {
|
|
@@ -1232,11 +1527,16 @@ var ClientProjectionBuilder = class {
|
|
|
1232
1527
|
}
|
|
1233
1528
|
return null;
|
|
1234
1529
|
}
|
|
1530
|
+
onContextEntry(payload) {
|
|
1531
|
+
if (!isRecord(payload.entry)) return null;
|
|
1532
|
+
const turn = buildContextTurn(payload.entry, this.nextSeq());
|
|
1533
|
+
return turn ? [{ kind: "upsert", turn }] : null;
|
|
1534
|
+
}
|
|
1235
1535
|
onLoopTurn(loopId, payload) {
|
|
1236
1536
|
let turnId = String(payload.turn_id ?? this.nextTurnId(`turn:${loopId}`));
|
|
1237
1537
|
let memoryRefs = null;
|
|
1238
1538
|
const pending = this.pendingMemoryRefs.get(loopId);
|
|
1239
|
-
if (pending) {
|
|
1539
|
+
if (pending && (!pending.turnId || pending.turnId === turnId)) {
|
|
1240
1540
|
this.pendingMemoryRefs.delete(loopId);
|
|
1241
1541
|
if (pending.turnId) turnId = pending.turnId;
|
|
1242
1542
|
memoryRefs = pending.refs;
|
|
@@ -1252,6 +1552,7 @@ var ClientProjectionBuilder = class {
|
|
|
1252
1552
|
this.lastTurnIds.set(loopId, turnId);
|
|
1253
1553
|
const turn = snapshot(state, this.nextSeq(), "streaming");
|
|
1254
1554
|
this.latestTurns.set(loopId, turn);
|
|
1555
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1255
1556
|
return [{ kind: "upsert", turn }];
|
|
1256
1557
|
}
|
|
1257
1558
|
onModeChange(loopId, payload) {
|
|
@@ -1288,11 +1589,11 @@ var ClientProjectionBuilder = class {
|
|
|
1288
1589
|
loopId,
|
|
1289
1590
|
toolCallId: optStr(payload.tool_call_id) ?? void 0,
|
|
1290
1591
|
toolName: toolNameFromPayload(payload) ?? void 0,
|
|
1291
|
-
workspacePath:
|
|
1592
|
+
workspacePath: isRecord(payload.workspace_change) ? optStr(payload.workspace_change.to) ?? void 0 : void 0
|
|
1292
1593
|
}
|
|
1293
1594
|
];
|
|
1294
1595
|
const change = payload.workspace_change;
|
|
1295
|
-
if (
|
|
1596
|
+
if (isRecord(change)) {
|
|
1296
1597
|
const source = optStr(change.source) ?? "";
|
|
1297
1598
|
const projectName = optStr(change.project_name) ?? "";
|
|
1298
1599
|
const fromPath = optStr(change.from) ?? "";
|
|
@@ -1324,6 +1625,13 @@ var ClientProjectionBuilder = class {
|
|
|
1324
1625
|
onAgentStart(loopId, payload) {
|
|
1325
1626
|
const description = optStr(payload.description);
|
|
1326
1627
|
if (description) this.loopDescriptions.set(loopId, description);
|
|
1628
|
+
const subagentStartedAt = optStr(payload.started_at);
|
|
1629
|
+
if (subagentStartedAt) {
|
|
1630
|
+
this.activeSubagentRuns.set(loopId, {
|
|
1631
|
+
runId: optStr(payload.run_id) ?? loopId,
|
|
1632
|
+
startedAt: subagentStartedAt
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1327
1635
|
const turn = buildSystemNotificationTurn(
|
|
1328
1636
|
this.nextSeq(),
|
|
1329
1637
|
this.nextTurnId("agent:start"),
|
|
@@ -1365,7 +1673,7 @@ var ClientProjectionBuilder = class {
|
|
|
1365
1673
|
this.nextTurnId("bg:tasks_completed"),
|
|
1366
1674
|
loopId,
|
|
1367
1675
|
"bg:tasks_completed",
|
|
1368
|
-
|
|
1676
|
+
backgroundTaskStatus(payload),
|
|
1369
1677
|
`Background tasks completed (${count})`,
|
|
1370
1678
|
detail || null,
|
|
1371
1679
|
{ task_ids: taskIds, count }
|
|
@@ -1385,7 +1693,32 @@ var ClientProjectionBuilder = class {
|
|
|
1385
1693
|
}
|
|
1386
1694
|
const directTurn = this.activeTurns.get(loopId) ?? this.latestTurns.get(loopId);
|
|
1387
1695
|
setToolCallStatus(directTurn, toolCallId, "done", null, null);
|
|
1388
|
-
|
|
1696
|
+
const updates = this.collectLoopUpdates(loopId, parentLoopId);
|
|
1697
|
+
const entryId = typeof payload.entry_id === "string" ? payload.entry_id : "";
|
|
1698
|
+
if (!entryId) return updates;
|
|
1699
|
+
const answerData = isRecord(payload.answer_data) ? payload.answer_data : {};
|
|
1700
|
+
const answerTurn = {
|
|
1701
|
+
id: entryId,
|
|
1702
|
+
sequence: this.nextSeq(),
|
|
1703
|
+
turn_id: entryId,
|
|
1704
|
+
loop_id: loopId,
|
|
1705
|
+
kind: "message",
|
|
1706
|
+
role: "assistant",
|
|
1707
|
+
status: "completed",
|
|
1708
|
+
blocks: [
|
|
1709
|
+
{
|
|
1710
|
+
type: "ask_user_answer",
|
|
1711
|
+
content: answerData,
|
|
1712
|
+
tool_call_id: toolCallId || null
|
|
1713
|
+
}
|
|
1714
|
+
],
|
|
1715
|
+
tool_calls: [],
|
|
1716
|
+
model: null,
|
|
1717
|
+
usage: null,
|
|
1718
|
+
duration_ms: 0,
|
|
1719
|
+
client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
|
|
1720
|
+
};
|
|
1721
|
+
return [...updates, { kind: "upsert", turn: answerTurn }];
|
|
1389
1722
|
}
|
|
1390
1723
|
onUserMessage(_loopId, payload) {
|
|
1391
1724
|
const entryId = String(payload.entry_id ?? this.nextTurnId("user"));
|
|
@@ -1402,7 +1735,8 @@ var ClientProjectionBuilder = class {
|
|
|
1402
1735
|
tool_calls: [],
|
|
1403
1736
|
model: null,
|
|
1404
1737
|
usage: null,
|
|
1405
|
-
duration_ms: 0
|
|
1738
|
+
duration_ms: 0,
|
|
1739
|
+
client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
|
|
1406
1740
|
};
|
|
1407
1741
|
this.latestTurns.set("root", turn);
|
|
1408
1742
|
return [{ kind: "upsert", turn }];
|
|
@@ -1430,13 +1764,7 @@ var ClientProjectionBuilder = class {
|
|
|
1430
1764
|
}
|
|
1431
1765
|
onToolCallCreated(state, payload) {
|
|
1432
1766
|
if ("arguments_delta" in payload) {
|
|
1433
|
-
|
|
1434
|
-
const toolCallId2 = String(payload.id ?? "");
|
|
1435
|
-
if (delta && toolCallId2) {
|
|
1436
|
-
appendToolCallArguments(state, toolCallId2, delta);
|
|
1437
|
-
return this.syncPatch(state);
|
|
1438
|
-
}
|
|
1439
|
-
return null;
|
|
1767
|
+
return this.onToolCallArgumentsDelta(state, payload);
|
|
1440
1768
|
}
|
|
1441
1769
|
const toolCallId = String(payload.id ?? "");
|
|
1442
1770
|
const toolName = toolNameFromPayload(payload);
|
|
@@ -1445,16 +1773,21 @@ var ClientProjectionBuilder = class {
|
|
|
1445
1773
|
upsertToolCall(state, toolCallId, toolName, displayName, args);
|
|
1446
1774
|
return this.syncPatch(state);
|
|
1447
1775
|
}
|
|
1776
|
+
onToolCallArgumentsDelta(state, payload) {
|
|
1777
|
+
const delta = String(payload.arguments_delta ?? "");
|
|
1778
|
+
const toolCallId = String(payload.id ?? "");
|
|
1779
|
+
if (!delta || !toolCallId) return null;
|
|
1780
|
+
appendToolCallArguments(state, toolCallId, delta);
|
|
1781
|
+
return this.syncPatch(state);
|
|
1782
|
+
}
|
|
1448
1783
|
onResponseDone(state, loopId, payload) {
|
|
1449
1784
|
state.responseDone = true;
|
|
1785
|
+
this.applyStreamTiming(state, payload);
|
|
1450
1786
|
const model = optStr(payload.model);
|
|
1451
1787
|
if (model) state.model = model;
|
|
1452
1788
|
if (typeof payload.usage === "object" && payload.usage !== null) {
|
|
1453
1789
|
state.usage = { ...payload.usage };
|
|
1454
1790
|
}
|
|
1455
|
-
if ("content" in payload) {
|
|
1456
|
-
replaceTextContent(state, completedResponseText(payload.content));
|
|
1457
|
-
}
|
|
1458
1791
|
const toolCalls = payload.tool_calls;
|
|
1459
1792
|
if (Array.isArray(toolCalls)) {
|
|
1460
1793
|
for (const raw of toolCalls) {
|
|
@@ -1472,11 +1805,42 @@ var ClientProjectionBuilder = class {
|
|
|
1472
1805
|
);
|
|
1473
1806
|
}
|
|
1474
1807
|
}
|
|
1475
|
-
if (state.
|
|
1808
|
+
if (!applyResponseBlockOrder(state, payload.response_blocks) && "content" in payload) {
|
|
1809
|
+
replaceTextContent(state, completedResponseText(payload.content));
|
|
1810
|
+
}
|
|
1811
|
+
const isNativeProviderResponse = typeof payload.provider === "string" && payload.provider !== "";
|
|
1812
|
+
if (state.toolCalls.length === 0 || isNativeProviderResponse && state.toolCalls.every(
|
|
1813
|
+
(toolCall) => toolCall.status !== "pending" && toolCall.status !== "awaiting_answer"
|
|
1814
|
+
)) {
|
|
1476
1815
|
return this.finalize(loopId, "completed");
|
|
1477
1816
|
}
|
|
1478
1817
|
return this.syncPatch(state);
|
|
1479
1818
|
}
|
|
1819
|
+
applyFirstTokenDelivery(state, payload) {
|
|
1820
|
+
const firstTokenMs = payload.first_token_ms;
|
|
1821
|
+
if (state.firstTokenMs === null && typeof firstTokenMs === "number" && firstTokenMs >= 0) {
|
|
1822
|
+
state.firstTokenMs = Math.round(firstTokenMs);
|
|
1823
|
+
}
|
|
1824
|
+
const duration = payload.first_token_delivery_ms;
|
|
1825
|
+
if (state.firstTokenDeliveryMs === null && typeof duration === "number" && duration >= 0) {
|
|
1826
|
+
state.firstTokenDeliveryMs = duration;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
applyStreamTiming(state, payload) {
|
|
1830
|
+
if (typeof payload.first_token_ms === "number" && payload.first_token_ms >= 0) {
|
|
1831
|
+
state.firstTokenMs = Math.round(payload.first_token_ms);
|
|
1832
|
+
}
|
|
1833
|
+
if (typeof payload.stream_duration_ms === "number" && payload.stream_duration_ms >= 0) {
|
|
1834
|
+
state.streamDurationMs = Math.round(payload.stream_duration_ms);
|
|
1835
|
+
}
|
|
1836
|
+
if (typeof payload.started_at === "string" && payload.started_at) {
|
|
1837
|
+
state.modelStartedAt = payload.started_at;
|
|
1838
|
+
state.modelStartedAtReported = true;
|
|
1839
|
+
}
|
|
1840
|
+
if (typeof payload.duration_ms === "number" && payload.duration_ms >= 0) {
|
|
1841
|
+
state.modelDurationMs = Math.round(payload.duration_ms);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1480
1844
|
onToolResultDone(state, loopId, payload) {
|
|
1481
1845
|
const toolCallId = String(payload.tool_call_id ?? "");
|
|
1482
1846
|
const streamedResult = state.toolCalls.find((toolCall) => toolCall.id === toolCallId)?.result;
|
|
@@ -1484,9 +1848,12 @@ var ClientProjectionBuilder = class {
|
|
|
1484
1848
|
const status = toolStatusFromResult(content);
|
|
1485
1849
|
const rawDuration = payload.duration_ms;
|
|
1486
1850
|
const durationMs = typeof rawDuration === "number" && rawDuration >= 0 ? Math.round(rawDuration) : null;
|
|
1851
|
+
const ranInParallel = typeof payload.ran_in_parallel === "boolean" ? payload.ran_in_parallel : null;
|
|
1487
1852
|
const sl = sourceLoopFor(state.loopId, this.loopDescriptions);
|
|
1488
|
-
|
|
1489
|
-
|
|
1853
|
+
const startedAt = typeof payload.started_at === "string" && payload.started_at ? payload.started_at : null;
|
|
1854
|
+
applyToolResult(state, toolCallId, content, status, durationMs, ranInParallel, sl, startedAt);
|
|
1855
|
+
const isNativeProviderResult = typeof payload.provider === "string" && payload.provider !== "";
|
|
1856
|
+
if (!isNativeProviderResult && this.shouldFinalizeAfterToolResults(state)) {
|
|
1490
1857
|
return this.finalize(loopId, "completed");
|
|
1491
1858
|
}
|
|
1492
1859
|
return this.syncPatch(state);
|
|
@@ -1515,10 +1882,13 @@ var ClientProjectionBuilder = class {
|
|
|
1515
1882
|
onToolBridge(state, payload) {
|
|
1516
1883
|
const bridge = payload.bridge;
|
|
1517
1884
|
if (typeof bridge !== "object" || bridge === null) return null;
|
|
1885
|
+
const toolCallId = String(payload.tool_call_id ?? "");
|
|
1886
|
+
if (!toolCallId || !state.toolCalls.some((toolCall) => toolCall.id === toolCallId)) return null;
|
|
1518
1887
|
state.blocks.push({
|
|
1519
1888
|
type: "tool_bridge",
|
|
1520
1889
|
content: bridge,
|
|
1521
|
-
|
|
1890
|
+
delivery_id: String(payload.delivery_id ?? "") || null,
|
|
1891
|
+
tool_call_id: toolCallId
|
|
1522
1892
|
});
|
|
1523
1893
|
return this.syncPatch(state);
|
|
1524
1894
|
}
|
|
@@ -1527,6 +1897,11 @@ var ClientProjectionBuilder = class {
|
|
|
1527
1897
|
const toolName = String(payload.tool_name ?? "");
|
|
1528
1898
|
const displayName = this.displayNameResolver(toolName);
|
|
1529
1899
|
upsertToolCall(state, toolCallId, toolName, displayName, "");
|
|
1900
|
+
const relatedToolCallId = String(payload.related_tool_call_id ?? "");
|
|
1901
|
+
if (relatedToolCallId && relatedToolCallId !== toolCallId) {
|
|
1902
|
+
const related = state.toolCalls.find((toolCall) => toolCall.id === relatedToolCallId);
|
|
1903
|
+
if (related) related.status = "awaiting_answer";
|
|
1904
|
+
}
|
|
1530
1905
|
return this.syncPatch(state);
|
|
1531
1906
|
}
|
|
1532
1907
|
onChildPause(state, loopId, payload) {
|
|
@@ -1632,6 +2007,7 @@ var ClientProjectionBuilder = class {
|
|
|
1632
2007
|
}
|
|
1633
2008
|
onAgentEnd(state, loopId, payload) {
|
|
1634
2009
|
const ok = payload.ok !== false;
|
|
2010
|
+
this.settleSubagentRun(loopId, payload);
|
|
1635
2011
|
const status = ok ? "done" : "error";
|
|
1636
2012
|
const title = `Agent: ${payload.description ?? payload.skill_id ?? "agent"}`;
|
|
1637
2013
|
const detail = optStr(payload.error);
|
|
@@ -1639,6 +2015,7 @@ var ClientProjectionBuilder = class {
|
|
|
1639
2015
|
skill_id: optStr(payload.skill_id),
|
|
1640
2016
|
description: optStr(payload.description),
|
|
1641
2017
|
parent_fork_tool_call_id: optStr(payload.parent_fork_tool_call_id),
|
|
2018
|
+
...typeof payload.run_status === "string" ? { run_status: payload.run_status } : {},
|
|
1642
2019
|
ok
|
|
1643
2020
|
};
|
|
1644
2021
|
if (state) {
|
|
@@ -1658,6 +2035,32 @@ var ClientProjectionBuilder = class {
|
|
|
1658
2035
|
);
|
|
1659
2036
|
return [{ kind: "upsert", turn }];
|
|
1660
2037
|
}
|
|
2038
|
+
settleSubagentRun(loopId, payload) {
|
|
2039
|
+
const active = this.activeSubagentRuns.get(loopId);
|
|
2040
|
+
this.activeSubagentRuns.delete(loopId);
|
|
2041
|
+
const parentToolCallId = optStr(payload.parent_fork_tool_call_id);
|
|
2042
|
+
if (!parentToolCallId) return;
|
|
2043
|
+
const rawDuration = payload.run_duration_ms;
|
|
2044
|
+
const durationMs = typeof rawDuration === "number" && rawDuration >= 0 ? Math.round(rawDuration) : null;
|
|
2045
|
+
const rawStatus = optStr(payload.run_status);
|
|
2046
|
+
const status = rawStatus === "failed" || rawStatus === "cancelled" || rawStatus === "paused" || rawStatus === "incomplete" ? rawStatus : payload.ok === false ? "failed" : "completed";
|
|
2047
|
+
const run = {
|
|
2048
|
+
run_id: active?.runId ?? optStr(payload.run_id) ?? loopId,
|
|
2049
|
+
scope: "subagent",
|
|
2050
|
+
loop_id: loopId,
|
|
2051
|
+
started_at: active?.startedAt ?? optStr(payload.run_started_at),
|
|
2052
|
+
duration_ms: durationMs,
|
|
2053
|
+
status
|
|
2054
|
+
};
|
|
2055
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2056
|
+
for (const turn of [...this.latestTurns.values(), ...this.turnsById.values()]) {
|
|
2057
|
+
if (seen.has(turn)) continue;
|
|
2058
|
+
seen.add(turn);
|
|
2059
|
+
const toolCall = turn.tool_calls.find((item) => item.id === parentToolCallId);
|
|
2060
|
+
if (!toolCall) continue;
|
|
2061
|
+
toolCall.subagent_runs = [...toolCall.subagent_runs ?? [], run];
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
1661
2064
|
dropIncompleteToolCalls(state) {
|
|
1662
2065
|
if (state.responseDone) return;
|
|
1663
2066
|
state.toolCalls = [];
|
|
@@ -1707,6 +2110,7 @@ var ClientProjectionBuilder = class {
|
|
|
1707
2110
|
syncPatch(state) {
|
|
1708
2111
|
const turn = snapshot(state, this.nextSeq(), "streaming");
|
|
1709
2112
|
this.latestTurns.set(state.loopId, turn);
|
|
2113
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1710
2114
|
this.lastTurnIds.set(state.loopId, turn.turn_id);
|
|
1711
2115
|
return [{ kind: "upsert", turn }];
|
|
1712
2116
|
}
|
|
@@ -1717,6 +2121,7 @@ var ClientProjectionBuilder = class {
|
|
|
1717
2121
|
const turn = snapshot(state, this.nextSeq(), status);
|
|
1718
2122
|
this.lastTurnIds.set(loopId, turn.turn_id);
|
|
1719
2123
|
this.latestTurns.set(loopId, turn);
|
|
2124
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1720
2125
|
return [{ kind: "upsert", turn }];
|
|
1721
2126
|
}
|
|
1722
2127
|
shouldFinalizeAfterToolResults(state) {
|
|
@@ -1754,9 +2159,6 @@ var ClientProjectionBuilder = class {
|
|
|
1754
2159
|
return `${prefix}:${this.syntheticCounter}:${hex}`;
|
|
1755
2160
|
}
|
|
1756
2161
|
};
|
|
1757
|
-
function isRecord2(value) {
|
|
1758
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1759
|
-
}
|
|
1760
2162
|
function optStr(value) {
|
|
1761
2163
|
if (value == null || value === "") return null;
|
|
1762
2164
|
return String(value);
|
|
@@ -1783,6 +2185,8 @@ function projectHistory(entries) {
|
|
|
1783
2185
|
const latestAssistantByLoop = /* @__PURE__ */ new Map();
|
|
1784
2186
|
const pendingChildPauses = /* @__PURE__ */ new Map();
|
|
1785
2187
|
const answeredToolCallIds = /* @__PURE__ */ new Set();
|
|
2188
|
+
const nativeToolTurns = /* @__PURE__ */ new Set();
|
|
2189
|
+
const turnEntryAliases = /* @__PURE__ */ new Map();
|
|
1786
2190
|
let latestRootMessage = null;
|
|
1787
2191
|
let latestRootAssistant = null;
|
|
1788
2192
|
let sequence = 0;
|
|
@@ -1790,7 +2194,40 @@ function projectHistory(entries) {
|
|
|
1790
2194
|
const kind = String(entry.kind ?? "");
|
|
1791
2195
|
const loopId = String(entry.loop_name ?? "root");
|
|
1792
2196
|
const id = String(entry.id ?? `history-${sequence + 1}`);
|
|
1793
|
-
if (kind === "
|
|
2197
|
+
if (kind === "turn_timing" && isRecord(entry.data)) {
|
|
2198
|
+
const turnId = typeof entry.data.turn_id === "string" ? entry.data.turn_id : "";
|
|
2199
|
+
const duration = numberOrNull(entry.data.first_token_delivery_ms);
|
|
2200
|
+
const target = turnEntryAliases.get(turnId);
|
|
2201
|
+
if (target && duration !== null) target.first_token_delivery_ms = duration;
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
if (kind === "run_timing" && isRecord(entry.data)) {
|
|
2205
|
+
const data = entry.data;
|
|
2206
|
+
if (data.scope === "subagent") {
|
|
2207
|
+
const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
|
|
2208
|
+
const duration = numberOrNull(data.duration_ms);
|
|
2209
|
+
const rawStatus = String(data.status ?? "completed");
|
|
2210
|
+
const status = rawStatus === "failed" || rawStatus === "cancelled" || rawStatus === "paused" || rawStatus === "incomplete" ? rawStatus : "completed";
|
|
2211
|
+
for (const turn of turns) {
|
|
2212
|
+
const toolCall = turn.tool_calls.find((item) => item.id === parentToolCallId);
|
|
2213
|
+
if (!toolCall) continue;
|
|
2214
|
+
toolCall.subagent_runs = [
|
|
2215
|
+
...toolCall.subagent_runs ?? [],
|
|
2216
|
+
{
|
|
2217
|
+
run_id: String(data.run_id ?? ""),
|
|
2218
|
+
scope: "subagent",
|
|
2219
|
+
loop_id: loopId,
|
|
2220
|
+
started_at: typeof data.started_at === "string" ? data.started_at : null,
|
|
2221
|
+
duration_ms: duration,
|
|
2222
|
+
status
|
|
2223
|
+
}
|
|
2224
|
+
];
|
|
2225
|
+
break;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
if (kind === "post_chat_followup" && isRecord(entry.data)) {
|
|
1794
2231
|
const assistantEntryId = String(entry.data.assistant_entry_id ?? "");
|
|
1795
2232
|
if (latestRootMessage?.role === "assistant" && latestRootMessage.turn_id === assistantEntryId) {
|
|
1796
2233
|
const followup = normalizePostChatFollowup(entry.data, assistantEntryId);
|
|
@@ -1798,7 +2235,15 @@ function projectHistory(entries) {
|
|
|
1798
2235
|
}
|
|
1799
2236
|
continue;
|
|
1800
2237
|
}
|
|
1801
|
-
if (kind === "
|
|
2238
|
+
if (kind === "context") {
|
|
2239
|
+
const turn = buildContextTurn(entry, sequence + 1);
|
|
2240
|
+
if (turn) {
|
|
2241
|
+
sequence += 1;
|
|
2242
|
+
turns.push(turn);
|
|
2243
|
+
}
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
if (kind === "message" && isRecord(entry.message)) {
|
|
1802
2247
|
const message = entry.message;
|
|
1803
2248
|
const role = String(message.role ?? "");
|
|
1804
2249
|
if (role === "tool") {
|
|
@@ -1808,6 +2253,12 @@ function projectHistory(entries) {
|
|
|
1808
2253
|
if (owner && toolCall) {
|
|
1809
2254
|
toolCall.status = message._error ? "error" : inferToolStatus(String(message.content ?? ""));
|
|
1810
2255
|
toolCall.result = message.content ?? "";
|
|
2256
|
+
const duration = numberOrNull(message._duration_ms);
|
|
2257
|
+
if (duration !== null) toolCall.duration_ms = duration;
|
|
2258
|
+
if (typeof message._started_at === "string") {
|
|
2259
|
+
toolCall.started_at = message._started_at;
|
|
2260
|
+
}
|
|
2261
|
+
if (message._ran_in_parallel === true) toolCall.ran_in_parallel = true;
|
|
1811
2262
|
owner.blocks.push({
|
|
1812
2263
|
type: "tool_result",
|
|
1813
2264
|
content: message.content ?? "",
|
|
@@ -1817,7 +2268,7 @@ function projectHistory(entries) {
|
|
|
1817
2268
|
continue;
|
|
1818
2269
|
}
|
|
1819
2270
|
if (role !== "user" && role !== "assistant" && role !== "system") continue;
|
|
1820
|
-
const toolCalls = role === "assistant" ? buildToolCalls(message.tool_calls) : [];
|
|
2271
|
+
const toolCalls = role === "assistant" && !message._error ? buildToolCalls(message.tool_calls) : [];
|
|
1821
2272
|
const blocks = buildBlocks(message, toolCalls);
|
|
1822
2273
|
const turn = {
|
|
1823
2274
|
id,
|
|
@@ -1830,14 +2281,52 @@ function projectHistory(entries) {
|
|
|
1830
2281
|
blocks,
|
|
1831
2282
|
tool_calls: toolCalls,
|
|
1832
2283
|
model: stringOrNull(message.model),
|
|
1833
|
-
usage:
|
|
1834
|
-
|
|
1835
|
-
|
|
2284
|
+
usage: isRecord(message._usage) ? { ...message._usage } : null,
|
|
2285
|
+
// 模型请求区间来自持久化字段;旧会话缺失时为 null,不用落盘时刻冒充(#2581 A1)。
|
|
2286
|
+
duration_ms: numberOrNull(message._duration_ms),
|
|
2287
|
+
first_token_ms: numberOrNull(message._first_token_ms),
|
|
2288
|
+
stream_duration_ms: numberOrNull(message._stream_duration_ms),
|
|
2289
|
+
started_at: typeof message._started_at === "string" ? message._started_at : typeof entry.timestamp === "string" ? entry.timestamp : void 0,
|
|
2290
|
+
has_reported_timing: typeof message._started_at === "string",
|
|
2291
|
+
// fork 子会话的父锚点:历史里落在消息级 _parent_fork_tool_call_id。丢掉它,
|
|
2292
|
+
// 刷新后工作流步骤 fork 就配不回 workflow:run 工具调用(普通 Agent fork 有
|
|
2293
|
+
// root Agent 工具调用兜底配对,工作流 fork 没有,子智能体卡会整体消失)。
|
|
2294
|
+
parent_fork_tool_call_id: stringOrNull(message._parent_fork_tool_call_id)
|
|
1836
2295
|
};
|
|
2296
|
+
const isNativeToolEntry = role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.some(
|
|
2297
|
+
(toolCall) => isRecord(toolCall) && typeof toolCall.provider === "string"
|
|
2298
|
+
);
|
|
2299
|
+
const previousTurn = turns[turns.length - 1];
|
|
2300
|
+
if (role === "assistant" && previousTurn?.role === "assistant" && previousTurn.loop_id === loopId && nativeToolTurns.has(previousTurn)) {
|
|
2301
|
+
nativeToolTurns.delete(previousTurn);
|
|
2302
|
+
previousTurn.blocks.push(...turn.blocks);
|
|
2303
|
+
previousTurn.tool_calls.push(...turn.tool_calls);
|
|
2304
|
+
previousTurn.status = turn.status;
|
|
2305
|
+
previousTurn.model = turn.model ?? previousTurn.model;
|
|
2306
|
+
previousTurn.usage = turn.usage ?? previousTurn.usage;
|
|
2307
|
+
if (turn.duration_ms) previousTurn.duration_ms = turn.duration_ms;
|
|
2308
|
+
previousTurn.first_token_ms = turn.first_token_ms ?? previousTurn.first_token_ms;
|
|
2309
|
+
previousTurn.stream_duration_ms = turn.stream_duration_ms ?? previousTurn.stream_duration_ms;
|
|
2310
|
+
if (turn.has_reported_timing) {
|
|
2311
|
+
previousTurn.started_at = turn.started_at;
|
|
2312
|
+
previousTurn.has_reported_timing = true;
|
|
2313
|
+
} else if (!previousTurn.has_reported_timing) {
|
|
2314
|
+
previousTurn.started_at = previousTurn.started_at ?? turn.started_at;
|
|
2315
|
+
}
|
|
2316
|
+
previousTurn.id = turn.id;
|
|
2317
|
+
previousTurn.turn_id = turn.turn_id;
|
|
2318
|
+
if (isNativeToolEntry) nativeToolTurns.add(previousTurn);
|
|
2319
|
+
turnEntryAliases.set(id, previousTurn);
|
|
2320
|
+
if (loopId === "root") latestRootMessage = previousTurn;
|
|
2321
|
+
latestAssistantByLoop.set(loopId, previousTurn);
|
|
2322
|
+
continue;
|
|
2323
|
+
}
|
|
1837
2324
|
if (loopId === "root" && (role === "user" || role === "assistant")) {
|
|
1838
2325
|
latestRootMessage = turn;
|
|
1839
2326
|
}
|
|
1840
2327
|
turns.push(turn);
|
|
2328
|
+
turnEntryAliases.set(id, turn);
|
|
2329
|
+
if (isNativeToolEntry) nativeToolTurns.add(turn);
|
|
1841
2330
|
if (role === "assistant") latestAssistantByLoop.set(loopId, turn);
|
|
1842
2331
|
if (loopId === "root") {
|
|
1843
2332
|
if (role === "assistant") latestRootAssistant = turn;
|
|
@@ -1846,10 +2335,25 @@ function projectHistory(entries) {
|
|
|
1846
2335
|
for (const toolCall of toolCalls) toolOwners.set(toolCall.id, turn);
|
|
1847
2336
|
continue;
|
|
1848
2337
|
}
|
|
2338
|
+
if (kind === "tool_result") {
|
|
2339
|
+
const data = entry.data ?? {};
|
|
2340
|
+
const toolCallId = String(data.tool_call_id ?? "");
|
|
2341
|
+
const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
|
|
2342
|
+
if (owner && toolCallId) {
|
|
2343
|
+
const rawContent = data.content;
|
|
2344
|
+
const content = typeof rawContent === "string" ? rawContent : Array.isArray(rawContent) ? rawContent : String(rawContent ?? "");
|
|
2345
|
+
const status = data.is_error === true ? "error" : inferToolStatus(typeof content === "string" ? content : JSON.stringify(content));
|
|
2346
|
+
setToolCallStatus(owner, toolCallId, status, content, null);
|
|
2347
|
+
if (!owner.blocks.some((block) => block.type === "tool_result" && block.tool_call_id === toolCallId)) {
|
|
2348
|
+
owner.blocks.push({ type: "tool_result", content, tool_call_id: toolCallId });
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
1849
2353
|
if (kind === "mode_change") {
|
|
1850
2354
|
turns.push(markerTurn(id, ++sequence, loopId, "mode_change", entry.data ?? {}));
|
|
1851
2355
|
} else if (kind === "workspace_change") {
|
|
1852
|
-
const change =
|
|
2356
|
+
const change = isRecord(entry.data) ? entry.data : {};
|
|
1853
2357
|
const source = stringOrNull(change.source) ?? "";
|
|
1854
2358
|
const projectName = stringOrNull(change.project_name) ?? "";
|
|
1855
2359
|
const fromPath = stringOrNull(change.from) ?? "";
|
|
@@ -1878,19 +2382,21 @@ function projectHistory(entries) {
|
|
|
1878
2382
|
} else if (kind === "tool_ui" || kind === "tool_bridge") {
|
|
1879
2383
|
const data = entry.data ?? {};
|
|
1880
2384
|
const toolCallId = String(data.tool_call_id ?? "");
|
|
1881
|
-
const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
|
|
1882
|
-
const
|
|
2385
|
+
const owner = kind === "tool_bridge" ? toolCallId ? toolOwners.get(toolCallId) : void 0 : toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
|
|
2386
|
+
const rawContent = kind === "tool_ui" ? data.ui : data.bridge;
|
|
2387
|
+
const content = kind === "tool_ui" && isRecord(rawContent) ? { ...rawContent, archived: true } : rawContent;
|
|
1883
2388
|
if (owner && content !== void 0) {
|
|
1884
2389
|
owner.blocks.push({
|
|
1885
2390
|
type: kind,
|
|
1886
2391
|
content,
|
|
2392
|
+
...kind === "tool_bridge" ? { delivery_id: id } : {},
|
|
1887
2393
|
tool_call_id: toolCallId || null
|
|
1888
2394
|
});
|
|
1889
2395
|
}
|
|
1890
2396
|
} else if (kind === "compaction" || kind === "tool_result_archive") {
|
|
1891
2397
|
const data = entry.data ?? {};
|
|
1892
2398
|
const compactionId = String(data.compaction_id ?? entry.id ?? `history-${sequence + 1}`);
|
|
1893
|
-
let content =
|
|
2399
|
+
let content = isRecord(entry.message) ? entry.message.content : void 0;
|
|
1894
2400
|
if (kind === "tool_result_archive") {
|
|
1895
2401
|
const archivedCount = Array.isArray(data.archived_files) ? data.archived_files.length : 0;
|
|
1896
2402
|
content = `<compaction-summary>
|
|
@@ -1898,18 +2404,24 @@ function projectHistory(entries) {
|
|
|
1898
2404
|
</compaction-summary>`;
|
|
1899
2405
|
}
|
|
1900
2406
|
turns.push(
|
|
1901
|
-
buildCompactionTurn(
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2407
|
+
buildCompactionTurn(
|
|
2408
|
+
++sequence,
|
|
2409
|
+
compactionId,
|
|
2410
|
+
loopId,
|
|
2411
|
+
data.status === "failed" ? "failed" : "completed",
|
|
2412
|
+
{
|
|
2413
|
+
...data,
|
|
2414
|
+
content
|
|
2415
|
+
}
|
|
2416
|
+
)
|
|
1905
2417
|
);
|
|
1906
2418
|
} else if (kind === "child_pause") {
|
|
1907
2419
|
const data = entry.data ?? {};
|
|
1908
|
-
const sourceLoop =
|
|
2420
|
+
const sourceLoop = isRecord(data.source_loop) ? data.source_loop : {};
|
|
1909
2421
|
const childLoopId = String(data.child_loop_name ?? sourceLoop.name ?? "");
|
|
1910
2422
|
const childToolCallId = String(data.child_pause_tool_call_id ?? "");
|
|
1911
2423
|
const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
|
|
1912
|
-
const pauseToolData =
|
|
2424
|
+
const pauseToolData = isRecord(data.pause_tool_data) ? data.pause_tool_data : {};
|
|
1913
2425
|
const description = String(sourceLoop.description ?? "");
|
|
1914
2426
|
applyAskUserPauseToTurn(
|
|
1915
2427
|
latestAssistantByLoop.get(childLoopId),
|
|
@@ -1997,15 +2509,15 @@ function parseToolArguments(value) {
|
|
|
1997
2509
|
if (!value) return {};
|
|
1998
2510
|
try {
|
|
1999
2511
|
const parsed = JSON.parse(value);
|
|
2000
|
-
return
|
|
2512
|
+
return isRecord(parsed) ? parsed : {};
|
|
2001
2513
|
} catch {
|
|
2002
2514
|
return {};
|
|
2003
2515
|
}
|
|
2004
2516
|
}
|
|
2005
2517
|
function buildToolCalls(value) {
|
|
2006
2518
|
if (!Array.isArray(value)) return [];
|
|
2007
|
-
return value.filter(
|
|
2008
|
-
const fn =
|
|
2519
|
+
return value.filter(isRecord).map((raw) => {
|
|
2520
|
+
const fn = isRecord(raw.function) ? raw.function : {};
|
|
2009
2521
|
const name = String(fn.name ?? "");
|
|
2010
2522
|
return {
|
|
2011
2523
|
id: String(raw.id ?? ""),
|
|
@@ -2021,23 +2533,40 @@ function buildBlocks(message, toolCalls) {
|
|
|
2021
2533
|
const displayContent = message.role === "user" ? message._display_content : void 0;
|
|
2022
2534
|
const stored = message._blocks;
|
|
2023
2535
|
const blocks = [];
|
|
2536
|
+
const storedToolCallIds = /* @__PURE__ */ new Set();
|
|
2024
2537
|
if (displayContent !== void 0) {
|
|
2025
2538
|
blocks.push({ type: "text", content: displayContent });
|
|
2026
2539
|
} else if (Array.isArray(stored)) {
|
|
2027
2540
|
for (const raw of stored) {
|
|
2028
|
-
if (!
|
|
2541
|
+
if (!isRecord(raw)) continue;
|
|
2029
2542
|
const type3 = String(raw.type ?? "");
|
|
2030
2543
|
if (type3 === "thinking") blocks.push({ type: type3, content: raw.thinking ?? raw.content ?? "" });
|
|
2031
2544
|
if (type3 === "text") blocks.push({ type: type3, content: raw.text ?? raw.content ?? "" });
|
|
2545
|
+
if (type3 === "tool_call") {
|
|
2546
|
+
const toolCallId = String(raw.id ?? "");
|
|
2547
|
+
const toolCall = toolCalls.find((candidate) => candidate.id === toolCallId);
|
|
2548
|
+
if (!toolCall) continue;
|
|
2549
|
+
storedToolCallIds.add(toolCallId);
|
|
2550
|
+
blocks.push({
|
|
2551
|
+
type: "tool_use",
|
|
2552
|
+
content: toolCall.arguments,
|
|
2553
|
+
tool_call_id: toolCall.id,
|
|
2554
|
+
tool_name: toolCall.tool_name,
|
|
2555
|
+
display_name: toolCall.display_name
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2032
2558
|
}
|
|
2033
2559
|
} else if (message.content !== void 0 && message.content !== "") {
|
|
2034
2560
|
blocks.push({ type: "text", content: message.content });
|
|
2035
2561
|
}
|
|
2036
2562
|
for (const toolCall of toolCalls) {
|
|
2563
|
+
if (storedToolCallIds.has(toolCall.id)) continue;
|
|
2037
2564
|
blocks.push({
|
|
2038
2565
|
type: "tool_use",
|
|
2039
|
-
content:
|
|
2040
|
-
tool_call_id: toolCall.id
|
|
2566
|
+
content: toolCall.arguments,
|
|
2567
|
+
tool_call_id: toolCall.id,
|
|
2568
|
+
tool_name: toolCall.tool_name,
|
|
2569
|
+
display_name: toolCall.display_name
|
|
2041
2570
|
});
|
|
2042
2571
|
}
|
|
2043
2572
|
return blocks;
|
|
@@ -2058,14 +2587,11 @@ function markerTurn(id, sequence, loopId, type3, content, toolCallId = null) {
|
|
|
2058
2587
|
duration_ms: 0
|
|
2059
2588
|
};
|
|
2060
2589
|
}
|
|
2061
|
-
function isRecord3(value) {
|
|
2062
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2063
|
-
}
|
|
2064
2590
|
function stringOrNull(value) {
|
|
2065
2591
|
return typeof value === "string" ? value : null;
|
|
2066
2592
|
}
|
|
2067
|
-
function
|
|
2068
|
-
return typeof value === "number" ? value :
|
|
2593
|
+
function numberOrNull(value) {
|
|
2594
|
+
return typeof value === "number" && value >= 0 ? value : null;
|
|
2069
2595
|
}
|
|
2070
2596
|
function findLatestAssistant(turns, loopId) {
|
|
2071
2597
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
@@ -2076,6 +2602,10 @@ function findLatestAssistant(turns, loopId) {
|
|
|
2076
2602
|
}
|
|
2077
2603
|
|
|
2078
2604
|
// src/resources/sessions.ts
|
|
2605
|
+
function isEntryGoneError(error) {
|
|
2606
|
+
const status = error?.status;
|
|
2607
|
+
return status === 404 || status === 409;
|
|
2608
|
+
}
|
|
2079
2609
|
function toCreateSessionPayload(request) {
|
|
2080
2610
|
const solutionRef = request.solution_ref ?? externalSolutionRef(request.solution_id);
|
|
2081
2611
|
return {
|
|
@@ -2094,13 +2624,14 @@ function toCreateSessionPayload(request) {
|
|
|
2094
2624
|
solution_snapshot_session_id: request.solution_snapshot_session_id ?? null,
|
|
2095
2625
|
compaction_ratio: request.compaction_ratio ?? null,
|
|
2096
2626
|
...request.runtime_type ? { runtime_type: request.runtime_type } : {},
|
|
2097
|
-
...request.daemon_id ? { daemon_id: request.daemon_id } : {}
|
|
2627
|
+
...request.daemon_id ? { daemon_id: request.daemon_id } : {},
|
|
2628
|
+
...request.agent_runtime_id ? { agent_runtime_id: request.agent_runtime_id } : {},
|
|
2629
|
+
...request.chat_group_id ? { chat_group_id: request.chat_group_id } : {}
|
|
2098
2630
|
};
|
|
2099
2631
|
}
|
|
2100
2632
|
var builtinSolutionIds = /* @__PURE__ */ new Set([
|
|
2101
2633
|
"app-dev",
|
|
2102
2634
|
"general_chat",
|
|
2103
|
-
"multica",
|
|
2104
2635
|
"night_build",
|
|
2105
2636
|
"skill_editor",
|
|
2106
2637
|
"smart_bid",
|
|
@@ -2123,6 +2654,36 @@ var SessionsResource = class {
|
|
|
2123
2654
|
this.client = client;
|
|
2124
2655
|
}
|
|
2125
2656
|
client;
|
|
2657
|
+
getMcpApp(sessionId, sourceId, init) {
|
|
2658
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}`, init);
|
|
2659
|
+
}
|
|
2660
|
+
onMcpAppChanged(sessionId, sourceId, listener) {
|
|
2661
|
+
let socket = this.client.socket();
|
|
2662
|
+
const changed = (payload) => {
|
|
2663
|
+
if (payload.session_id === sessionId && payload.source_id === sourceId) listener();
|
|
2664
|
+
};
|
|
2665
|
+
socket.on("mcp-app:changed", changed);
|
|
2666
|
+
socket.on("connect", listener);
|
|
2667
|
+
const off = this.client.onSocketReplaced(() => {
|
|
2668
|
+
socket.off("mcp-app:changed", changed);
|
|
2669
|
+
socket.off("connect", listener);
|
|
2670
|
+
socket = this.client.socket();
|
|
2671
|
+
socket.on("mcp-app:changed", changed);
|
|
2672
|
+
socket.on("connect", listener);
|
|
2673
|
+
});
|
|
2674
|
+
return () => {
|
|
2675
|
+
off();
|
|
2676
|
+
socket.off("mcp-app:changed", changed);
|
|
2677
|
+
socket.off("connect", listener);
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
callMcpApp(sessionId, sourceId, request, init) {
|
|
2681
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/calls`, {
|
|
2682
|
+
...init,
|
|
2683
|
+
method: "POST",
|
|
2684
|
+
body: JSON.stringify(request)
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2126
2687
|
/**
|
|
2127
2688
|
* 连接一个已存在的会话,返回实时会话对象 AgentSession
|
|
2128
2689
|
* (自动完成历史加载、Socket.IO 订阅、断线重连补数)。
|
|
@@ -2130,6 +2691,56 @@ var SessionsResource = class {
|
|
|
2130
2691
|
connect(sessionId) {
|
|
2131
2692
|
return this.client.hub.connect(sessionId);
|
|
2132
2693
|
}
|
|
2694
|
+
listSessionPlugins(sessionId, init) {
|
|
2695
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins`, init);
|
|
2696
|
+
}
|
|
2697
|
+
setSessionPluginActivation(sessionId, name, active, init) {
|
|
2698
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}:activation`, { method: "POST", body: JSON.stringify({ active }), ...init });
|
|
2699
|
+
}
|
|
2700
|
+
/**
|
|
2701
|
+
* Fetch a plugin's package-local icon as bytes.
|
|
2702
|
+
*
|
|
2703
|
+
* The request carries the client's own bearer credentials, so callers turn
|
|
2704
|
+
* the blob into an object URL instead of pointing `<img>` at an API path and
|
|
2705
|
+
* hoping the request is authenticated.
|
|
2706
|
+
*/
|
|
2707
|
+
fetchSessionPluginIcon(sessionId, name) {
|
|
2708
|
+
return this.client.blob("GET", `/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/icon`);
|
|
2709
|
+
}
|
|
2710
|
+
/** Authenticated URL for the icon; useful where a blob URL cannot be used. */
|
|
2711
|
+
sessionPluginIconUrl(sessionId, name) {
|
|
2712
|
+
return this.client.buildAuthedUrl(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/icon`);
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* 准备一个插件的配置:把包准备好并读出用户的配置契约,**不启用**它。
|
|
2716
|
+
*
|
|
2717
|
+
* 开启插件前必须先走这一步:缺配置时用户只会看到配置表单,服务端不会先把
|
|
2718
|
+
* active 持久化成 true 再靠补偿关掉。它不修改会话的插件选择、不连接 MCP。
|
|
2719
|
+
*/
|
|
2720
|
+
prepareSessionPluginConfig(sessionId, name, init) {
|
|
2721
|
+
return this.client.jsonFromInit(
|
|
2722
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}:prepare-config`,
|
|
2723
|
+
{ method: "POST", ...init }
|
|
2724
|
+
);
|
|
2725
|
+
}
|
|
2726
|
+
/**
|
|
2727
|
+
* Read a plugin's configuration contract and the user's stored values.
|
|
2728
|
+
*
|
|
2729
|
+
* Secrets are never returned: `write_only` lists the pointers that hold one.
|
|
2730
|
+
*/
|
|
2731
|
+
getSessionPluginConfig(sessionId, name, init) {
|
|
2732
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/config`, init);
|
|
2733
|
+
}
|
|
2734
|
+
/**
|
|
2735
|
+
* Publish explicit set/remove operations to the user's keychain file.
|
|
2736
|
+
*
|
|
2737
|
+
* Fields absent from both lists keep their stored value, so a form never has
|
|
2738
|
+
* to round-trip a secret. Rejections leave the previous file untouched; the
|
|
2739
|
+
* call never restarts MCP servers.
|
|
2740
|
+
*/
|
|
2741
|
+
updateSessionPluginConfig(sessionId, name, update, init) {
|
|
2742
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/config`, { method: "PUT", body: JSON.stringify(update), ...init });
|
|
2743
|
+
}
|
|
2133
2744
|
/** 创建新会话并直接连接,返回可收发消息的 AgentSession。 */
|
|
2134
2745
|
async create(request = {}) {
|
|
2135
2746
|
if (isSessionDefinition(request)) {
|
|
@@ -2226,7 +2837,8 @@ var SessionsResource = class {
|
|
|
2226
2837
|
template_id_prefix,
|
|
2227
2838
|
solution_id,
|
|
2228
2839
|
solution_registry_id,
|
|
2229
|
-
q
|
|
2840
|
+
q,
|
|
2841
|
+
chat_group_id
|
|
2230
2842
|
}) {
|
|
2231
2843
|
const searchParams = new URLSearchParams();
|
|
2232
2844
|
searchParams.set("limit", String(limit));
|
|
@@ -2239,6 +2851,7 @@ var SessionsResource = class {
|
|
|
2239
2851
|
}
|
|
2240
2852
|
const trimmedQuery = q?.trim();
|
|
2241
2853
|
if (trimmedQuery) searchParams.set("q", trimmedQuery);
|
|
2854
|
+
if (chat_group_id) searchParams.set("chat_group_id", chat_group_id);
|
|
2242
2855
|
return this.client.json("GET", `/api/sessions?${searchParams.toString()}`);
|
|
2243
2856
|
}
|
|
2244
2857
|
async listSessions(template_id_prefix, solution_id) {
|
|
@@ -2329,6 +2942,55 @@ var SessionsResource = class {
|
|
|
2329
2942
|
);
|
|
2330
2943
|
return projectHistory(entries);
|
|
2331
2944
|
}
|
|
2945
|
+
async getSessionTurnsPage(sessionId, options = {}, init = {}) {
|
|
2946
|
+
const query = new URLSearchParams();
|
|
2947
|
+
if (options.limit !== void 0) query.set("limit", String(options.limit));
|
|
2948
|
+
if (options.before !== void 0) query.set("before", options.before);
|
|
2949
|
+
if (options.fromEntryId !== void 0) query.set("from_entry_id", options.fromEntryId);
|
|
2950
|
+
const suffix = query.size ? `?${query}` : "";
|
|
2951
|
+
const page = await this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/turns${suffix}`, init);
|
|
2952
|
+
return {
|
|
2953
|
+
turns: page.turns,
|
|
2954
|
+
nextBefore: page.next_before,
|
|
2955
|
+
currentMode: page.current_mode
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2959
|
+
* 当前分支上可导航的用户发言目录(消息导航侧栏)。
|
|
2960
|
+
*
|
|
2961
|
+
* 只回 id、标题和时间:侧栏要列的是**整条会话**的发言,而窗口外的发言 id 只能
|
|
2962
|
+
* 从服务端拿;消息体仍然走 `/turns`。
|
|
2963
|
+
*/
|
|
2964
|
+
async getSessionTurnIndex(sessionId) {
|
|
2965
|
+
const index = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turn-index`);
|
|
2966
|
+
return {
|
|
2967
|
+
sessionId: index.session_id,
|
|
2968
|
+
items: index.items.map((item) => ({
|
|
2969
|
+
entryId: item.entry_id,
|
|
2970
|
+
preview: item.preview,
|
|
2971
|
+
createdAt: item.created_at
|
|
2972
|
+
}))
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
getMcpAppContext(sessionId, sourceId, init) {
|
|
2976
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/context`, init);
|
|
2977
|
+
}
|
|
2978
|
+
updateMcpAppContext(sessionId, sourceId, body, init) {
|
|
2979
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/context`, {
|
|
2980
|
+
...init,
|
|
2981
|
+
method: "POST",
|
|
2982
|
+
body: JSON.stringify(body)
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
async getSessionTurnCommands(sessionId) {
|
|
2986
|
+
const commands = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turn-commands`);
|
|
2987
|
+
return commands.map((command) => ({
|
|
2988
|
+
deliveryId: command.delivery_id,
|
|
2989
|
+
toolCallId: command.tool_call_id ?? void 0,
|
|
2990
|
+
action: command.action,
|
|
2991
|
+
payload: command.payload
|
|
2992
|
+
}));
|
|
2993
|
+
}
|
|
2332
2994
|
listResultFeedback(sessionId) {
|
|
2333
2995
|
return this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/feedback`);
|
|
2334
2996
|
}
|
|
@@ -2396,8 +3058,9 @@ var SessionsResource = class {
|
|
|
2396
3058
|
`/api/sessions/${sessionId}/probe-url?url=${encodeURIComponent(url)}`
|
|
2397
3059
|
);
|
|
2398
3060
|
}
|
|
2399
|
-
listDir(sessionId, dirPath) {
|
|
2400
|
-
|
|
3061
|
+
listDir(sessionId, dirPath, showHidden = false) {
|
|
3062
|
+
const query = showHidden ? "?show_hidden=true" : "";
|
|
3063
|
+
return this.client.json("GET", `/api/sessions/${sessionId}/ls/${encodeURIComponent(dirPath)}${query}`);
|
|
2401
3064
|
}
|
|
2402
3065
|
async countFiles(sessionId, dirPath = "") {
|
|
2403
3066
|
const entries = await this.listDir(sessionId, dirPath);
|
|
@@ -2592,6 +3255,130 @@ function profileToCreateSessionRequest(profile) {
|
|
|
2592
3255
|
};
|
|
2593
3256
|
}
|
|
2594
3257
|
|
|
3258
|
+
// src/resources/plugins.ts
|
|
3259
|
+
var PluginsResource = class {
|
|
3260
|
+
constructor(client) {
|
|
3261
|
+
this.client = client;
|
|
3262
|
+
}
|
|
3263
|
+
client;
|
|
3264
|
+
/**
|
|
3265
|
+
* 列出当前用户安装的插件及其展示信息。
|
|
3266
|
+
*
|
|
3267
|
+
* 服务端可能为了补齐名称/图标惰性获取包内容,但绝不激活插件、不连 MCP;
|
|
3268
|
+
* 单个插件的展示信息读不出来只会降级成占位,不会让整份目录失败。
|
|
3269
|
+
*/
|
|
3270
|
+
catalog(init) {
|
|
3271
|
+
return this.client.jsonFromInit("/api/plugins/catalog", init);
|
|
3272
|
+
}
|
|
3273
|
+
/** 取一个插件的包内图标字节(用户级,不需要会话)。 */
|
|
3274
|
+
fetchIcon(name) {
|
|
3275
|
+
return this.client.blob("GET", `/api/plugins/${encodeURIComponent(name)}/icon`);
|
|
3276
|
+
}
|
|
3277
|
+
/** 带凭据的图标地址;能直接用 blob URL 的地方优先用 `fetchIcon`。 */
|
|
3278
|
+
iconUrl(name) {
|
|
3279
|
+
return this.client.buildAuthedUrl(`/api/plugins/${encodeURIComponent(name)}/icon`);
|
|
3280
|
+
}
|
|
3281
|
+
};
|
|
3282
|
+
|
|
3283
|
+
// src/resources/chat-groups.ts
|
|
3284
|
+
var ChatGroupsResource = class {
|
|
3285
|
+
constructor(client) {
|
|
3286
|
+
this.client = client;
|
|
3287
|
+
}
|
|
3288
|
+
client;
|
|
3289
|
+
list() {
|
|
3290
|
+
return this.client.json("GET", "/api/chat-groups");
|
|
3291
|
+
}
|
|
3292
|
+
create(payload) {
|
|
3293
|
+
return this.client.json("POST", "/api/chat-groups", payload);
|
|
3294
|
+
}
|
|
3295
|
+
update(groupId, payload) {
|
|
3296
|
+
return this.client.json("PATCH", `/api/chat-groups/${encodeURIComponent(groupId)}`, payload);
|
|
3297
|
+
}
|
|
3298
|
+
delete(groupId, options) {
|
|
3299
|
+
const query = options?.deleteWorkspace ? "?delete_workspace=true" : "";
|
|
3300
|
+
return this.client.json("DELETE", `/api/chat-groups/${encodeURIComponent(groupId)}${query}`);
|
|
3301
|
+
}
|
|
3302
|
+
};
|
|
3303
|
+
|
|
3304
|
+
// src/resources/site-visits.ts
|
|
3305
|
+
var SITE_VISITS_PATH = "/api/site-visits";
|
|
3306
|
+
var REQUEST_TIMEOUT_MS = 5e3;
|
|
3307
|
+
var SiteVisitsResource = class {
|
|
3308
|
+
constructor(client) {
|
|
3309
|
+
this.client = client;
|
|
3310
|
+
}
|
|
3311
|
+
client;
|
|
3312
|
+
/** 上报一次页面访问,返回当前快照;不承诺本次已计入或已落盘。 */
|
|
3313
|
+
record() {
|
|
3314
|
+
return this.request("POST");
|
|
3315
|
+
}
|
|
3316
|
+
/** 只读快照:不设置访客 Cookie,也不增加任何计数。 */
|
|
3317
|
+
summary() {
|
|
3318
|
+
return this.request("GET");
|
|
3319
|
+
}
|
|
3320
|
+
async request(method) {
|
|
3321
|
+
const url = buildBackendUrl(this.client.options.baseUrl, SITE_VISITS_PATH);
|
|
3322
|
+
const controller = new AbortController();
|
|
3323
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
3324
|
+
try {
|
|
3325
|
+
const response = await (this.client.options.fetchImpl ?? fetch)(url.toString(), {
|
|
3326
|
+
method,
|
|
3327
|
+
// 访客 Cookie 是唯一的去重依据,必须随请求往返。
|
|
3328
|
+
credentials: "include",
|
|
3329
|
+
signal: controller.signal
|
|
3330
|
+
});
|
|
3331
|
+
if (!response.ok) {
|
|
3332
|
+
throw new BladeApiError(response, await extractErrorDetail(response));
|
|
3333
|
+
}
|
|
3334
|
+
return await response.json();
|
|
3335
|
+
} finally {
|
|
3336
|
+
clearTimeout(timeout);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
};
|
|
3340
|
+
|
|
3341
|
+
// src/shared/auth-busy.ts
|
|
3342
|
+
var DEFAULT_RETRY_AFTER_SECONDS = 1;
|
|
3343
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
3344
|
+
function errorData(error) {
|
|
3345
|
+
if (typeof error !== "object" || error === null || !("data" in error)) return null;
|
|
3346
|
+
const data = error.data;
|
|
3347
|
+
return typeof data === "object" && data !== null ? data : null;
|
|
3348
|
+
}
|
|
3349
|
+
function authBusyRetryDelayMs(error) {
|
|
3350
|
+
const data = errorData(error);
|
|
3351
|
+
if (data?.code !== "auth_busy" || Number(data.status) !== 429) return null;
|
|
3352
|
+
const rawRetryAfter = data.retry_after;
|
|
3353
|
+
const seconds = rawRetryAfter == null || String(rawRetryAfter).trim() === "" ? Number.NaN : Number(rawRetryAfter);
|
|
3354
|
+
const delaySeconds = Number.isFinite(seconds) && seconds >= 0 ? seconds : DEFAULT_RETRY_AFTER_SECONDS;
|
|
3355
|
+
return Math.min(delaySeconds * 1e3, MAX_TIMER_DELAY_MS);
|
|
3356
|
+
}
|
|
3357
|
+
var AuthBusyReconnect = class {
|
|
3358
|
+
timer = null;
|
|
3359
|
+
get pending() {
|
|
3360
|
+
return this.timer !== null;
|
|
3361
|
+
}
|
|
3362
|
+
schedule(error, reconnect) {
|
|
3363
|
+
const delay = authBusyRetryDelayMs(error);
|
|
3364
|
+
if (delay === null) {
|
|
3365
|
+
this.cancel();
|
|
3366
|
+
return false;
|
|
3367
|
+
}
|
|
3368
|
+
this.cancel();
|
|
3369
|
+
this.timer = globalThis.setTimeout(() => {
|
|
3370
|
+
this.timer = null;
|
|
3371
|
+
reconnect();
|
|
3372
|
+
}, delay);
|
|
3373
|
+
return true;
|
|
3374
|
+
}
|
|
3375
|
+
cancel() {
|
|
3376
|
+
if (this.timer === null) return;
|
|
3377
|
+
globalThis.clearTimeout(this.timer);
|
|
3378
|
+
this.timer = null;
|
|
3379
|
+
}
|
|
3380
|
+
};
|
|
3381
|
+
|
|
2595
3382
|
// src/commands/registry.ts
|
|
2596
3383
|
var BUFFER_LIMIT_PER_ACTION = 20;
|
|
2597
3384
|
var CommandRegistry = class {
|
|
@@ -2700,14 +3487,282 @@ var Emitter = class {
|
|
|
2700
3487
|
}
|
|
2701
3488
|
};
|
|
2702
3489
|
|
|
2703
|
-
// src/
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
];
|
|
3490
|
+
// src/session/history-page.ts
|
|
3491
|
+
function sortedUnique(turns) {
|
|
3492
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3493
|
+
for (const turn of turns) byId.set(turn.turn_id, turn);
|
|
3494
|
+
return [...byId.values()].sort((left, right) => left.sequence - right.sequence);
|
|
3495
|
+
}
|
|
3496
|
+
function prependOlder(current, page) {
|
|
3497
|
+
const currentById = new Map(current.map((turn) => [turn.turn_id, turn]));
|
|
3498
|
+
return sortedUnique([
|
|
3499
|
+
...current,
|
|
3500
|
+
...page.map((turn) => {
|
|
3501
|
+
const live = currentById.get(turn.turn_id);
|
|
3502
|
+
return live ? { ...live, sequence: turn.sequence } : turn;
|
|
3503
|
+
})
|
|
3504
|
+
]);
|
|
3505
|
+
}
|
|
3506
|
+
function replaceWindow(current, page, liveRevisionAtStart, liveRevisions) {
|
|
3507
|
+
const pageById = new Map(page.map((turn) => [turn.turn_id, turn]));
|
|
3508
|
+
const newerLive = current.filter(
|
|
3509
|
+
(turn) => turn.turn_id.startsWith("local-user-") || (liveRevisions.byTurnId.get(turn.turn_id) ?? 0) > liveRevisionAtStart
|
|
3510
|
+
);
|
|
3511
|
+
return sortedUnique([
|
|
3512
|
+
...page,
|
|
3513
|
+
...newerLive.map((turn) => {
|
|
3514
|
+
const snapshot2 = pageById.get(turn.turn_id);
|
|
3515
|
+
return snapshot2 ? { ...turn, sequence: snapshot2.sequence } : turn;
|
|
3516
|
+
})
|
|
3517
|
+
]);
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
// src/session/queue.ts
|
|
3521
|
+
var EMPTY_SESSION_QUEUE = {
|
|
3522
|
+
session_id: "",
|
|
3523
|
+
revision: 0,
|
|
3524
|
+
paused: false,
|
|
3525
|
+
pause_reason: null,
|
|
3526
|
+
items: []
|
|
3527
|
+
};
|
|
3528
|
+
var QUEUE_FAILURE_MESSAGE = "\u961F\u5217\u64CD\u4F5C\u5931\u8D25";
|
|
3529
|
+
var QUEUED_MESSAGE_STATUSES = [
|
|
3530
|
+
"pending",
|
|
3531
|
+
"delivering",
|
|
3532
|
+
"claimed",
|
|
3533
|
+
"consumed",
|
|
3534
|
+
"cancelled"
|
|
3535
|
+
];
|
|
3536
|
+
function toQueuedMessage(value) {
|
|
3537
|
+
if (!isRecord(value)) return null;
|
|
3538
|
+
const status = typeof value.status === "string" && QUEUED_MESSAGE_STATUSES.includes(value.status) ? value.status : null;
|
|
3539
|
+
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) {
|
|
3540
|
+
return null;
|
|
3541
|
+
}
|
|
3542
|
+
const item = {
|
|
3543
|
+
id: value.id,
|
|
3544
|
+
content: value.content,
|
|
3545
|
+
position: value.position,
|
|
3546
|
+
version: value.version,
|
|
3547
|
+
status,
|
|
3548
|
+
created_at: value.created_at,
|
|
3549
|
+
updated_at: value.updated_at
|
|
3550
|
+
};
|
|
3551
|
+
if (typeof value.target_run_id === "string") item.target_run_id = value.target_run_id;
|
|
3552
|
+
if (typeof value.error === "string") item.error = value.error;
|
|
3553
|
+
return item;
|
|
3554
|
+
}
|
|
3555
|
+
function toQueueSnapshot(value, fallbackSessionId) {
|
|
3556
|
+
if (!isRecord(value)) return null;
|
|
3557
|
+
if (typeof value.revision !== "number" || !Number.isInteger(value.revision) || value.revision < 0) {
|
|
3558
|
+
return null;
|
|
3559
|
+
}
|
|
3560
|
+
if (typeof value.paused !== "boolean") return null;
|
|
3561
|
+
if (!Array.isArray(value.items)) return null;
|
|
3562
|
+
const pauseReason = value.pause_reason;
|
|
3563
|
+
if (pauseReason !== null && pauseReason !== void 0 && typeof pauseReason !== "string") {
|
|
3564
|
+
return null;
|
|
3565
|
+
}
|
|
3566
|
+
const items = [];
|
|
3567
|
+
for (const raw of value.items) {
|
|
3568
|
+
const item = toQueuedMessage(raw);
|
|
3569
|
+
if (!item) return null;
|
|
3570
|
+
items.push(item);
|
|
3571
|
+
}
|
|
3572
|
+
return {
|
|
3573
|
+
session_id: typeof value.session_id === "string" && value.session_id ? value.session_id : fallbackSessionId,
|
|
3574
|
+
revision: value.revision,
|
|
3575
|
+
paused: value.paused,
|
|
3576
|
+
pause_reason: typeof pauseReason === "string" ? pauseReason : null,
|
|
3577
|
+
items
|
|
3578
|
+
};
|
|
3579
|
+
}
|
|
3580
|
+
function isSessionQueueSnapshot(value) {
|
|
3581
|
+
if (!isRecord(value) || typeof value.session_id !== "string") return false;
|
|
3582
|
+
return toQueueSnapshot(value, value.session_id) !== null;
|
|
3583
|
+
}
|
|
3584
|
+
function shouldApplySnapshot(current, next) {
|
|
3585
|
+
if (!isSessionQueueSnapshot(next)) return false;
|
|
3586
|
+
if (current?.session_id && next.session_id !== current.session_id) return false;
|
|
3587
|
+
return next.revision > (current?.revision ?? -1);
|
|
3588
|
+
}
|
|
3589
|
+
function parseQueueAck(response, fallbackSessionId) {
|
|
3590
|
+
if (!isRecord(response)) {
|
|
3591
|
+
return { ok: false, conflict: false, snapshot: null, message: QUEUE_FAILURE_MESSAGE };
|
|
3592
|
+
}
|
|
3593
|
+
const message = typeof response.message === "string" && response.message ? response.message : null;
|
|
3594
|
+
const status = response.status;
|
|
3595
|
+
if (status !== "ok" && status !== "conflict" && status !== "queued") {
|
|
3596
|
+
return { ok: false, conflict: false, snapshot: null, message: message ?? QUEUE_FAILURE_MESSAGE };
|
|
3597
|
+
}
|
|
3598
|
+
const snapshot2 = toQueueSnapshot(response, fallbackSessionId);
|
|
3599
|
+
if (status === "conflict") {
|
|
3600
|
+
return { ok: false, conflict: true, snapshot: snapshot2, message: message ?? QUEUE_FAILURE_MESSAGE };
|
|
3601
|
+
}
|
|
3602
|
+
if (!snapshot2) {
|
|
3603
|
+
return { ok: false, conflict: false, snapshot: null, message: message ?? QUEUE_FAILURE_MESSAGE };
|
|
3604
|
+
}
|
|
3605
|
+
return { ok: true, conflict: false, snapshot: snapshot2, message };
|
|
3606
|
+
}
|
|
3607
|
+
function canEditQueuedMessage(item) {
|
|
3608
|
+
return item.status === "pending";
|
|
3609
|
+
}
|
|
3610
|
+
function canCancelQueuedMessage(item) {
|
|
3611
|
+
return item.status === "pending";
|
|
3612
|
+
}
|
|
3613
|
+
function canDeliverQueuedMessage(item) {
|
|
3614
|
+
return item.status === "pending";
|
|
3615
|
+
}
|
|
3616
|
+
var QUEUED_MESSAGE_STATUS_LABELS = {
|
|
3617
|
+
pending: "\u5F85\u6267\u884C",
|
|
3618
|
+
delivering: "\u7B49\u5F85\u63A5\u6536",
|
|
3619
|
+
claimed: "",
|
|
3620
|
+
consumed: "",
|
|
3621
|
+
cancelled: ""
|
|
3622
|
+
};
|
|
3623
|
+
function queuedMessageStatusLabel(status) {
|
|
3624
|
+
return QUEUED_MESSAGE_STATUS_LABELS[status] ?? "";
|
|
3625
|
+
}
|
|
3626
|
+
var QUEUE_PAUSE_REASON_LABELS = {
|
|
3627
|
+
user_stop: "\u5DF2\u6682\u505C",
|
|
3628
|
+
run_failed: "\u4EFB\u52A1\u5F02\u5E38\u5DF2\u6682\u505C",
|
|
3629
|
+
run_unknown: "\u4EFB\u52A1\u7ED3\u675F\u72B6\u6001\u672A\u77E5\uFF0C\u5DF2\u6682\u505C",
|
|
3630
|
+
server_restart: "\u670D\u52A1\u91CD\u542F\u5DF2\u6682\u505C"
|
|
3631
|
+
};
|
|
3632
|
+
function queuePauseReasonLabel(reason) {
|
|
3633
|
+
if (!reason) return null;
|
|
3634
|
+
return QUEUE_PAUSE_REASON_LABELS[reason] ?? reason;
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
// src/schemas/mcp-app.ts
|
|
3638
|
+
function isMcpAppData(value) {
|
|
3639
|
+
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);
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
// src/shared/tool-ui-card.ts
|
|
3643
|
+
function isNonEmptyString(value) {
|
|
3644
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
3645
|
+
}
|
|
3646
|
+
function isPositiveNumber(value) {
|
|
3647
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
3648
|
+
}
|
|
3649
|
+
function isToolUiCard(value) {
|
|
3650
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
3651
|
+
return false;
|
|
3652
|
+
}
|
|
3653
|
+
const raw = value;
|
|
3654
|
+
if (raw.target !== "inline" && raw.target !== "preview") {
|
|
3655
|
+
return false;
|
|
3656
|
+
}
|
|
3657
|
+
if (!isPositiveNumber(raw.height)) {
|
|
3658
|
+
return false;
|
|
3659
|
+
}
|
|
3660
|
+
if (raw.archived === true && !isNonEmptyString(raw.resourceHTML)) {
|
|
3661
|
+
return false;
|
|
3662
|
+
}
|
|
3663
|
+
if (!isNonEmptyString(raw.resourceHTML) && !isNonEmptyString(raw.resourceUri) && !isNonEmptyString(raw.resourceURI)) {
|
|
3664
|
+
return false;
|
|
3665
|
+
}
|
|
3666
|
+
if (raw.title != null && !isNonEmptyString(raw.title)) {
|
|
3667
|
+
return false;
|
|
3668
|
+
}
|
|
3669
|
+
if (raw.mcpApp !== void 0 && !isMcpAppData(raw.mcpApp)) {
|
|
3670
|
+
return false;
|
|
3671
|
+
}
|
|
3672
|
+
return true;
|
|
3673
|
+
}
|
|
3674
|
+
function resolveToolUiCardContent(card) {
|
|
3675
|
+
if (isNonEmptyString(card.resourceHTML)) {
|
|
3676
|
+
return { type: "resource-html", content: card.resourceHTML };
|
|
3677
|
+
}
|
|
3678
|
+
if (card.archived) return null;
|
|
3679
|
+
const uri = isNonEmptyString(card.resourceUri) ? card.resourceUri : card.resourceURI;
|
|
3680
|
+
return isNonEmptyString(uri) ? { type: "resource-uri", content: uri } : null;
|
|
3681
|
+
}
|
|
3682
|
+
function buildToolUiCardKey(toolCallId, type3, content) {
|
|
3683
|
+
if (type3 === "resource-uri") {
|
|
3684
|
+
return `tool-preview-uri:${content}`;
|
|
3685
|
+
}
|
|
3686
|
+
return `tool-preview:${toolCallId}`;
|
|
3687
|
+
}
|
|
3688
|
+
function isInternalStatusToolUiCard(card) {
|
|
3689
|
+
return card?.title?.trim() === "\u9636\u6BB5\u8FDB\u5EA6";
|
|
3690
|
+
}
|
|
3691
|
+
function isAppDevToolUiCard(card, isAppDev) {
|
|
3692
|
+
return isAppDev && card.target === "preview" && !card.resourceHTML && Boolean(card.resourceUri || card.resourceURI);
|
|
3693
|
+
}
|
|
3694
|
+
function collectInlineToolUiCards(messages) {
|
|
3695
|
+
return messages.flatMap(
|
|
3696
|
+
(message) => (message.blocks ?? []).flatMap(
|
|
3697
|
+
(block, index) => block.type === "tool_ui" && block.tool_call_id && isToolUiCard(block.content) && block.content.target === "inline" ? [
|
|
3698
|
+
{
|
|
3699
|
+
key: `${message.entry_id ?? message.timestamp ?? "message"}-${block.tool_call_id}-${index}`,
|
|
3700
|
+
toolCallId: block.tool_call_id,
|
|
3701
|
+
card: block.content
|
|
3702
|
+
}
|
|
3703
|
+
] : []
|
|
3704
|
+
)
|
|
3705
|
+
);
|
|
3706
|
+
}
|
|
3707
|
+
function collectPreviewToolUiCards(messages) {
|
|
3708
|
+
return messages.flatMap(
|
|
3709
|
+
(message) => (message.blocks ?? []).flatMap((block, index) => {
|
|
3710
|
+
if (block.type !== "tool_ui" || !block.tool_call_id || !isToolUiCard(block.content) || block.content.target !== "preview") {
|
|
3711
|
+
return [];
|
|
3712
|
+
}
|
|
3713
|
+
const toolCall = (message.tool_calls ?? []).find(
|
|
3714
|
+
(candidate) => candidate.id === block.tool_call_id
|
|
3715
|
+
);
|
|
3716
|
+
return toolCall ? [
|
|
3717
|
+
{
|
|
3718
|
+
key: `${message.entry_id ?? message.timestamp ?? "message"}-${block.tool_call_id}-${index}`,
|
|
3719
|
+
toolCall,
|
|
3720
|
+
card: block.content,
|
|
3721
|
+
blocks: message.blocks ?? []
|
|
3722
|
+
}
|
|
3723
|
+
] : [];
|
|
3724
|
+
})
|
|
3725
|
+
);
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
// src/schemas/message-utils.ts
|
|
3729
|
+
var SYSTEM_REMINDER_PATTERN = /^<system-reminder>\s*[\s\S]*?\s*<\/system-reminder>$/i;
|
|
3730
|
+
var SYSTEM_NOTIFICATION_PATTERN = /^<system-notification>\s*[\s\S]*?\s*<\/system-notification>$/i;
|
|
3731
|
+
var LEGACY_FORK_CONTEXT_MARKERS = [
|
|
3732
|
+
"\u7236\u667A\u80FD\u4F53\u5728\u6D3E\u751F\u4F60\u4E4B\u524D\u5DF2\u7ECF\u5B8C\u6210\u4E86\u4E00\u4E9B\u64CD\u4F5C\u3002",
|
|
3733
|
+
"\u53EF\u901A\u8FC7 `read_file` \u8BFB\u53D6\u3002",
|
|
3734
|
+
"# \u4F60\u7684\u4EFB\u52A1"
|
|
3735
|
+
];
|
|
3736
|
+
var INTERNAL_CHAT_ERROR_PATTERNS = [
|
|
3737
|
+
/\b(?:ENOENT|EACCES|EPERM)\b/i,
|
|
3738
|
+
/\[(?:Errno -?\d+|WinError \d+)\]/i,
|
|
3739
|
+
/\b(?:ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|ENOTFOUND|EAI_AGAIN)\b/i,
|
|
3740
|
+
/\b(?:Connection refused|Connection reset by peer|All connection attempts failed|getaddrinfo failed)\b/i,
|
|
3741
|
+
/(?:^|[\s("'`])\/(?:root|home|Users|tmp|var|opt|etc)\//,
|
|
3742
|
+
/[A-Za-z]:\\(?:Users|Windows|Program Files|AppData)\\/i,
|
|
3743
|
+
/(?:^|\n)Traceback \(most recent call last\):/,
|
|
3744
|
+
/(?:^|\n)\s*at\s+\S+(?:\s+\(|:\d+:\d+)/,
|
|
3745
|
+
/(?:^|\n)File "[^"]+", line \d+/
|
|
3746
|
+
];
|
|
3747
|
+
var SAFE_CHAT_ERROR_PREFIXES = [
|
|
3748
|
+
"\u6C99\u76D2\u8FD0\u884C\u73AF\u5883\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
|
|
3749
|
+
"\u6A21\u578B\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u68C0\u67E5\u6A21\u578B\u914D\u7F6E\u540E\u91CD\u8BD5\u3002"
|
|
3750
|
+
];
|
|
3751
|
+
var GENERIC_CHAT_ERROR = "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002";
|
|
3752
|
+
function chatErrorForDisplay(message, developer = false) {
|
|
3753
|
+
const normalized = message.trim();
|
|
3754
|
+
if (!developer) {
|
|
3755
|
+
const safePrefix = SAFE_CHAT_ERROR_PREFIXES.find((prefix) => normalized.startsWith(prefix));
|
|
3756
|
+
if (safePrefix) return safePrefix;
|
|
3757
|
+
}
|
|
3758
|
+
if (!developer && /no source matches this model/i.test(normalized)) {
|
|
3759
|
+
return "\u5F53\u524D\u6A21\u578B\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u6216\u91CD\u65B0\u9009\u62E9\u6A21\u578B\u3002";
|
|
3760
|
+
}
|
|
3761
|
+
if (!developer && INTERNAL_CHAT_ERROR_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
3762
|
+
return GENERIC_CHAT_ERROR;
|
|
3763
|
+
}
|
|
3764
|
+
return normalized || GENERIC_CHAT_ERROR;
|
|
3765
|
+
}
|
|
2711
3766
|
function normalizeMessageContent(content) {
|
|
2712
3767
|
if (typeof content === "string") return content;
|
|
2713
3768
|
if (Array.isArray(content)) return content;
|
|
@@ -2727,10 +3782,10 @@ function isLegacyForkContextText(text) {
|
|
|
2727
3782
|
const normalized = text.trim();
|
|
2728
3783
|
return LEGACY_FORK_CONTEXT_MARKERS.every((marker) => normalized.includes(marker));
|
|
2729
3784
|
}
|
|
2730
|
-
function isHiddenInternalMessage(message) {
|
|
3785
|
+
function isHiddenInternalMessage(message, options = {}) {
|
|
2731
3786
|
if (message.role !== "user") return false;
|
|
2732
3787
|
const text = getTextContent(normalizeMessageContent(message.content));
|
|
2733
|
-
return isSystemReminderText(text) || isSystemNotificationText(text) || isLegacyForkContextText(text);
|
|
3788
|
+
return isSystemReminderText(text) || isSystemNotificationText(text) || (options.includeLegacyForkContext ?? true) && isLegacyForkContextText(text);
|
|
2734
3789
|
}
|
|
2735
3790
|
function buildMessageContent(text, attachments) {
|
|
2736
3791
|
if (attachments.length === 0) return text;
|
|
@@ -2877,7 +3932,10 @@ function createInitialSessionState(sessionId) {
|
|
|
2877
3932
|
agentLoops: {},
|
|
2878
3933
|
activeCompaction: null,
|
|
2879
3934
|
replay: null,
|
|
2880
|
-
|
|
3935
|
+
queue: EMPTY_SESSION_QUEUE,
|
|
3936
|
+
viewerRole: null,
|
|
3937
|
+
nextBefore: null,
|
|
3938
|
+
loadingOlder: false
|
|
2881
3939
|
};
|
|
2882
3940
|
}
|
|
2883
3941
|
function parseAgentDescription(argumentsJson) {
|
|
@@ -2903,10 +3961,10 @@ function inferLoopStatusFromMessages(messages) {
|
|
|
2903
3961
|
}
|
|
2904
3962
|
function inferLoopStatusFromTurns(turns, messages) {
|
|
2905
3963
|
const latestAgentNotification = [...turns].reverse().flatMap((turn) => turn.blocks).find((block) => {
|
|
2906
|
-
if (block.type !== "system_notification" || !
|
|
3964
|
+
if (block.type !== "system_notification" || !isRecord(block.content)) return false;
|
|
2907
3965
|
return block.content.notification_type === "agent:start" || block.content.notification_type === "agent:end";
|
|
2908
3966
|
});
|
|
2909
|
-
if (latestAgentNotification?.type === "system_notification" &&
|
|
3967
|
+
if (latestAgentNotification?.type === "system_notification" && isRecord(latestAgentNotification.content)) {
|
|
2910
3968
|
const notificationType = latestAgentNotification.content.notification_type;
|
|
2911
3969
|
const status = latestAgentNotification.content.status;
|
|
2912
3970
|
if (notificationType === "agent:start" || status === "running") return "running";
|
|
@@ -2915,17 +3973,44 @@ function inferLoopStatusFromTurns(turns, messages) {
|
|
|
2915
3973
|
}
|
|
2916
3974
|
return inferLoopStatusFromMessages(messages);
|
|
2917
3975
|
}
|
|
2918
|
-
function
|
|
2919
|
-
|
|
3976
|
+
function toSelectionMap(value) {
|
|
3977
|
+
if (!isRecord(value)) return {};
|
|
3978
|
+
const entries = Object.entries(value).map(([questionKey, optionIndexes]) => {
|
|
3979
|
+
if (!Array.isArray(optionIndexes)) return null;
|
|
3980
|
+
const parsedIndexes = optionIndexes.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item));
|
|
3981
|
+
return [Number(questionKey), parsedIndexes];
|
|
3982
|
+
}).filter((entry) => entry !== null);
|
|
3983
|
+
return Object.fromEntries(entries);
|
|
3984
|
+
}
|
|
3985
|
+
function toCustomMap(value) {
|
|
3986
|
+
if (!isRecord(value)) return {};
|
|
3987
|
+
const entries = Object.entries(value).filter(([, text]) => typeof text === "string").map(([questionKey, text]) => [Number(questionKey), text]);
|
|
3988
|
+
return Object.fromEntries(entries);
|
|
3989
|
+
}
|
|
3990
|
+
function extractAskAnswers(turns) {
|
|
3991
|
+
const answers = {};
|
|
3992
|
+
for (const turn of turns) {
|
|
3993
|
+
for (const block of turn.blocks) {
|
|
3994
|
+
if (block.type !== "ask_user_answer" || typeof block.tool_call_id !== "string") continue;
|
|
3995
|
+
if (!isRecord(block.content)) continue;
|
|
3996
|
+
const note = typeof block.content.note === "string" ? block.content.note.trim() : "";
|
|
3997
|
+
answers[block.tool_call_id] = {
|
|
3998
|
+
selections: toSelectionMap(block.content.selections),
|
|
3999
|
+
custom: toCustomMap(block.content.custom),
|
|
4000
|
+
...note ? { note } : {}
|
|
4001
|
+
};
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
return answers;
|
|
2920
4005
|
}
|
|
2921
4006
|
function parentForkToolCallIdFromTurn(turn) {
|
|
2922
4007
|
if (typeof turn.parent_fork_tool_call_id === "string" && turn.parent_fork_tool_call_id.length > 0) {
|
|
2923
4008
|
return turn.parent_fork_tool_call_id;
|
|
2924
4009
|
}
|
|
2925
4010
|
for (const block of turn.blocks) {
|
|
2926
|
-
if (block.type !== "system_notification" || !
|
|
4011
|
+
if (block.type !== "system_notification" || !isRecord(block.content)) continue;
|
|
2927
4012
|
const metadata = block.content.metadata;
|
|
2928
|
-
if (!
|
|
4013
|
+
if (!isRecord(metadata)) continue;
|
|
2929
4014
|
const parentId = metadata.parent_fork_tool_call_id;
|
|
2930
4015
|
if (typeof parentId === "string" && parentId.length > 0) return parentId;
|
|
2931
4016
|
}
|
|
@@ -2942,16 +4027,16 @@ function buildMessageContent2(turn) {
|
|
|
2942
4027
|
}
|
|
2943
4028
|
function workspaceNotificationContent(turn) {
|
|
2944
4029
|
const block = turn.blocks.find(
|
|
2945
|
-
(candidate) => candidate.type === "system_notification" &&
|
|
4030
|
+
(candidate) => candidate.type === "system_notification" && isRecord(candidate.content) && candidate.content.notification_type === "workspace_change"
|
|
2946
4031
|
);
|
|
2947
|
-
if (!block || !
|
|
4032
|
+
if (!block || !isRecord(block.content)) return "";
|
|
2948
4033
|
const title = typeof block.content.title === "string" ? block.content.title.trim() : "";
|
|
2949
4034
|
const detail = typeof block.content.detail === "string" ? block.content.detail.trim() : "";
|
|
2950
4035
|
return [title ? `**${title}**` : "", detail].filter(Boolean).join("\n\n");
|
|
2951
4036
|
}
|
|
2952
4037
|
function askUserAnswerContent(turn) {
|
|
2953
4038
|
const answerBlock = turn.blocks.find((block) => block.type === "ask_user_answer");
|
|
2954
|
-
if (!answerBlock || !
|
|
4039
|
+
if (!answerBlock || !isRecord(answerBlock.content)) return null;
|
|
2955
4040
|
const answer = answerBlock.content.answer;
|
|
2956
4041
|
return typeof answer === "string" && answer.trim().length > 0 ? answer : null;
|
|
2957
4042
|
}
|
|
@@ -2971,6 +4056,19 @@ function extractModeFromBlocks(blocks) {
|
|
|
2971
4056
|
return null;
|
|
2972
4057
|
}
|
|
2973
4058
|
function projectionToMessage(turn) {
|
|
4059
|
+
if (turn.kind === "context") {
|
|
4060
|
+
const context = contextProjectionData(turn);
|
|
4061
|
+
if (!context) return null;
|
|
4062
|
+
return {
|
|
4063
|
+
role: "assistant",
|
|
4064
|
+
content: "",
|
|
4065
|
+
kind: "context",
|
|
4066
|
+
context,
|
|
4067
|
+
loop_name: turn.loop_id,
|
|
4068
|
+
entry_id: turn.turn_id,
|
|
4069
|
+
status: turn.status
|
|
4070
|
+
};
|
|
4071
|
+
}
|
|
2974
4072
|
if (turn.kind === "compaction" && turn.compaction_id) {
|
|
2975
4073
|
return {
|
|
2976
4074
|
role: "assistant",
|
|
@@ -3141,21 +4239,58 @@ function orderTurns(turns) {
|
|
|
3141
4239
|
return turns;
|
|
3142
4240
|
}
|
|
3143
4241
|
function reconcileOptimisticUserTurns(currentTurns, incomingTurns) {
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
4242
|
+
return reconcileUserTurns(currentTurns, incomingTurns, true, false);
|
|
4243
|
+
}
|
|
4244
|
+
function reconcileHistoricalUserTurns(currentTurns, incomingTurns) {
|
|
4245
|
+
return reconcileUserTurns(currentTurns, incomingTurns, false, true);
|
|
4246
|
+
}
|
|
4247
|
+
function reconcileUserTurns(currentTurns, incomingTurns, allowLiveFallback, keepUnclaimedOptimistic) {
|
|
4248
|
+
const preservedTurns = preserveClientRenderIds(currentTurns, incomingTurns);
|
|
4249
|
+
const optimisticUserTurns = currentTurns.filter(
|
|
4250
|
+
(turn) => turn.role === "user" && (turn.kind ?? "message") === "message" && turn.turn_id.startsWith("local-user-")
|
|
3147
4251
|
);
|
|
3148
|
-
|
|
4252
|
+
const optimisticByRequestId = new Map(
|
|
4253
|
+
optimisticUserTurns.map((turn) => [
|
|
4254
|
+
turn.turn_id.slice("local-user-".length),
|
|
4255
|
+
turn
|
|
4256
|
+
])
|
|
4257
|
+
);
|
|
4258
|
+
const currentTurnIds = new Set(currentTurns.map((turn) => turn.turn_id));
|
|
4259
|
+
const claimedOptimisticIds = /* @__PURE__ */ new Set();
|
|
4260
|
+
const reconciled = preservedTurns.map((turn) => {
|
|
4261
|
+
const exactOptimistic = typeof turn.client_request_id === "string" ? optimisticByRequestId.get(turn.client_request_id) : void 0;
|
|
4262
|
+
if (exactOptimistic && !claimedOptimisticIds.has(exactOptimistic.turn_id) && isUserConfirmation(turn)) {
|
|
4263
|
+
claimedOptimisticIds.add(exactOptimistic.turn_id);
|
|
4264
|
+
return { ...turn, client_render_id: exactOptimistic.turn_id };
|
|
4265
|
+
}
|
|
4266
|
+
const fallbackOptimistic = optimisticUserTurns.find(
|
|
4267
|
+
(candidate) => !claimedOptimisticIds.has(candidate.turn_id)
|
|
4268
|
+
);
|
|
4269
|
+
if (allowLiveFallback && typeof turn.client_request_id !== "string" && fallbackOptimistic && isUserConfirmation(turn) && !turn.turn_id.startsWith("local-user-") && !currentTurnIds.has(turn.turn_id)) {
|
|
4270
|
+
claimedOptimisticIds.add(fallbackOptimistic.turn_id);
|
|
4271
|
+
return { ...turn, client_render_id: fallbackOptimistic.turn_id };
|
|
4272
|
+
}
|
|
4273
|
+
return turn;
|
|
4274
|
+
});
|
|
4275
|
+
const result = claimedOptimisticIds.size === 0 ? reconciled : reconciled.filter((turn) => !claimedOptimisticIds.has(turn.turn_id));
|
|
4276
|
+
if (!keepUnclaimedOptimistic) return result;
|
|
4277
|
+
const resultIds = new Set(result.map((turn) => turn.turn_id));
|
|
4278
|
+
const unclaimedOptimisticTurns = optimisticUserTurns.filter(
|
|
4279
|
+
(turn) => !claimedOptimisticIds.has(turn.turn_id) && !resultIds.has(turn.turn_id)
|
|
4280
|
+
);
|
|
4281
|
+
return unclaimedOptimisticTurns.length === 0 ? result : orderTurns([...result, ...unclaimedOptimisticTurns]);
|
|
4282
|
+
}
|
|
4283
|
+
function isUserConfirmation(turn) {
|
|
4284
|
+
return turn.role === "user" && (turn.kind ?? "message") === "message" || turn.blocks.some((block) => block.type === "ask_user_answer");
|
|
4285
|
+
}
|
|
4286
|
+
function preserveClientRenderIds(currentTurns, incomingTurns) {
|
|
4287
|
+
const currentById = new Map(currentTurns.map((turn) => [turn.turn_id, turn]));
|
|
3149
4288
|
return incomingTurns.map((turn) => {
|
|
3150
4289
|
const current = currentById.get(turn.turn_id);
|
|
3151
4290
|
if (current) {
|
|
3152
4291
|
const currentRenderId = current.client_render_id;
|
|
3153
4292
|
return turn.client_render_id || !currentRenderId ? turn : { ...turn, client_render_id: currentRenderId };
|
|
3154
4293
|
}
|
|
3155
|
-
if (!optimisticUserMatched && optimisticUserTurn && turn.role === "user" && !turn.turn_id.startsWith("local-user-")) {
|
|
3156
|
-
optimisticUserMatched = true;
|
|
3157
|
-
return { ...turn, client_render_id: optimisticUserTurn.turn_id };
|
|
3158
|
-
}
|
|
3159
4294
|
return turn;
|
|
3160
4295
|
});
|
|
3161
4296
|
}
|
|
@@ -3170,7 +4305,7 @@ function areAgentLoopsEqual(left, right) {
|
|
|
3170
4305
|
});
|
|
3171
4306
|
}
|
|
3172
4307
|
function withTurns(state, turns) {
|
|
3173
|
-
const orderedTurns = orderTurns(
|
|
4308
|
+
const orderedTurns = orderTurns(reconcileHistoricalUserTurns(state.turns, turns));
|
|
3174
4309
|
const { messages, agentLoops, activeCompaction } = materialize(orderedTurns);
|
|
3175
4310
|
const isWaitingForInput = orderedTurns.some(
|
|
3176
4311
|
(turn) => turn.tool_calls.some((toolCall) => toolCall.status === "awaiting_answer")
|
|
@@ -3188,26 +4323,45 @@ function withTurns(state, turns) {
|
|
|
3188
4323
|
});
|
|
3189
4324
|
const mergedMessages = preservedErrors.length > 0 ? [...messagesWithRenderIds, ...preservedErrors] : messagesWithRenderIds;
|
|
3190
4325
|
const latestMode = [...orderedTurns].reverse().map((turn) => extractModeFromBlocks(turn.blocks)).find((mode) => mode !== null);
|
|
4326
|
+
const askAnswersFromHistory = extractAskAnswers(orderedTurns);
|
|
4327
|
+
const presentToolCallIds = new Set(
|
|
4328
|
+
orderedTurns.flatMap((turn) => turn.tool_calls.map((toolCall) => toolCall.id))
|
|
4329
|
+
);
|
|
4330
|
+
const askAnswers = { ...askAnswersFromHistory };
|
|
4331
|
+
for (const [toolCallId, answer] of Object.entries(state.askAnswers)) {
|
|
4332
|
+
if (!(toolCallId in askAnswers) && presentToolCallIds.has(toolCallId)) {
|
|
4333
|
+
askAnswers[toolCallId] = answer;
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
3191
4336
|
return {
|
|
3192
4337
|
...state,
|
|
3193
4338
|
turns: orderedTurns,
|
|
3194
4339
|
messages: mergedMessages,
|
|
3195
4340
|
agentLoops: stableAgentLoops,
|
|
3196
4341
|
activeCompaction,
|
|
4342
|
+
askAnswers,
|
|
3197
4343
|
mode: latestMode ?? state.mode,
|
|
3198
4344
|
// 历史恢复时 chat:end(paused) 可能已不在 socket replay 缓存中;
|
|
3199
4345
|
// awaiting_answer 是投影里的持久权威状态,必须据此恢复问答交互。
|
|
3200
4346
|
status: isWaitingForInput ? "waiting_for_input" : state.status
|
|
3201
4347
|
};
|
|
3202
4348
|
}
|
|
4349
|
+
function withQueue(state, snapshot2) {
|
|
4350
|
+
if (!shouldApplySnapshot(state.queue, snapshot2)) return state;
|
|
4351
|
+
return { ...state, queue: snapshot2 };
|
|
4352
|
+
}
|
|
3203
4353
|
var localTurnCounter = 0;
|
|
3204
|
-
function addUserMessage(state, content) {
|
|
4354
|
+
function addUserMessage(state, content, clientRequestId) {
|
|
3205
4355
|
localTurnCounter += 1;
|
|
3206
|
-
const turnId = `local-user-${Date.now()}-${localTurnCounter}`;
|
|
4356
|
+
const turnId = `local-user-${clientRequestId ?? `${Date.now()}-${localTurnCounter}`}`;
|
|
3207
4357
|
const optimisticTurn = {
|
|
3208
4358
|
id: turnId,
|
|
3209
4359
|
sequence: Math.max(0, ...state.turns.map((turn) => turn.sequence)) + 1,
|
|
3210
4360
|
turn_id: turnId,
|
|
4361
|
+
// 乐观态就自赋,跟服务端确认后 reconcileUserTurns 补上的
|
|
4362
|
+
// client_render_id(= 乐观 turn 自己的 turn_id)保持同一个值,
|
|
4363
|
+
// 消费方才能拿它当"确认前后不变"的稳定标识使用。
|
|
4364
|
+
client_render_id: turnId,
|
|
3211
4365
|
loop_id: "root",
|
|
3212
4366
|
role: "user",
|
|
3213
4367
|
status: "streaming",
|
|
@@ -3222,10 +4376,9 @@ function addUserMessage(state, content) {
|
|
|
3222
4376
|
function upsertTurn(state, turn) {
|
|
3223
4377
|
let existing = [...state.turns];
|
|
3224
4378
|
const [nextTurn] = reconcileOptimisticUserTurns(existing, [turn]);
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
);
|
|
4379
|
+
const claimedOptimisticId = nextTurn.client_render_id;
|
|
4380
|
+
if (claimedOptimisticId?.startsWith("local-user-")) {
|
|
4381
|
+
existing = existing.filter((item) => item.turn_id !== claimedOptimisticId);
|
|
3229
4382
|
}
|
|
3230
4383
|
const index = existing.findIndex((item) => item.turn_id === nextTurn.turn_id);
|
|
3231
4384
|
if (index >= 0) {
|
|
@@ -3233,7 +4386,7 @@ function upsertTurn(state, turn) {
|
|
|
3233
4386
|
} else {
|
|
3234
4387
|
existing.push(nextTurn);
|
|
3235
4388
|
}
|
|
3236
|
-
return withTurns(state, existing);
|
|
4389
|
+
return withTurns({ ...state, turns: existing }, existing);
|
|
3237
4390
|
}
|
|
3238
4391
|
function applyTurnPatch(state, patch) {
|
|
3239
4392
|
const turn = patch.data.turn;
|
|
@@ -3244,13 +4397,7 @@ function applyTurnPatch(state, patch) {
|
|
|
3244
4397
|
if (lastSequence !== null && patch.sequence <= lastSequence) {
|
|
3245
4398
|
return state;
|
|
3246
4399
|
}
|
|
3247
|
-
|
|
3248
|
-
if (index >= 0) {
|
|
3249
|
-
existing[index] = nextTurn;
|
|
3250
|
-
} else {
|
|
3251
|
-
existing.push(nextTurn);
|
|
3252
|
-
}
|
|
3253
|
-
return withTurns(state, existing);
|
|
4400
|
+
return upsertTurn(state, { ...turn, sequence: patch.sequence });
|
|
3254
4401
|
}
|
|
3255
4402
|
function addErrorMessage(state, content) {
|
|
3256
4403
|
const anchorTurnId = state.turns[state.turns.length - 1]?.turn_id ?? null;
|
|
@@ -3282,19 +4429,27 @@ function markStreamingTurns(state, turnStatus, toolCallStatus) {
|
|
|
3282
4429
|
|
|
3283
4430
|
// src/session/agent-session.ts
|
|
3284
4431
|
var OOM_MESSAGE = "\u6C99\u76D2\u5185\u5B58\u4F7F\u7528\u8D85\u51FA\u9650\u5236\uFF0C\u5DF2\u81EA\u52A8\u91CD\u542F\u3002\u5982\u679C\u7ECF\u5E38\u89E6\u53D1\uFF0C\u53EF\u4EE5\u8054\u7CFB\u7BA1\u7406\u5458\u8C03\u6574\u989D\u5EA6\u3002";
|
|
3285
|
-
var
|
|
3286
|
-
|
|
3287
|
-
|
|
4432
|
+
var COMMAND_RESTORE_RETRY_BASE_MS = 1e3;
|
|
4433
|
+
var COMMAND_RESTORE_RETRY_MAX_MS = 3e4;
|
|
4434
|
+
var QUEUE_ACK_TIMEOUT_MS = 1e4;
|
|
4435
|
+
var fallbackRequestCounter = 0;
|
|
4436
|
+
function createClientRequestId() {
|
|
4437
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
4438
|
+
return globalThis.crypto.randomUUID();
|
|
4439
|
+
}
|
|
4440
|
+
fallbackRequestCounter += 1;
|
|
4441
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}-${fallbackRequestCounter}`;
|
|
3288
4442
|
}
|
|
4443
|
+
var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
|
|
3289
4444
|
function isOomText(value) {
|
|
3290
4445
|
return typeof value === "string" && OOM_KEYWORDS_RE.test(value);
|
|
3291
4446
|
}
|
|
3292
4447
|
function parseJsonRecord(value) {
|
|
3293
|
-
if (
|
|
4448
|
+
if (isRecord(value)) return value;
|
|
3294
4449
|
if (typeof value !== "string") return null;
|
|
3295
4450
|
try {
|
|
3296
4451
|
const parsed = JSON.parse(value);
|
|
3297
|
-
return
|
|
4452
|
+
return isRecord(parsed) ? parsed : null;
|
|
3298
4453
|
} catch {
|
|
3299
4454
|
return null;
|
|
3300
4455
|
}
|
|
@@ -3320,6 +4475,20 @@ function isOomResult(value) {
|
|
|
3320
4475
|
function isToolBridgeContent(value) {
|
|
3321
4476
|
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.action === "string";
|
|
3322
4477
|
}
|
|
4478
|
+
function isCursorInvalidError(error) {
|
|
4479
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 409;
|
|
4480
|
+
}
|
|
4481
|
+
function queueRequestFailure(error) {
|
|
4482
|
+
return {
|
|
4483
|
+
ok: false,
|
|
4484
|
+
conflict: false,
|
|
4485
|
+
snapshot: null,
|
|
4486
|
+
message: error instanceof Error && error.message ? error.message : "\u961F\u5217\u64CD\u4F5C\u5931\u8D25"
|
|
4487
|
+
};
|
|
4488
|
+
}
|
|
4489
|
+
function commandDeliveryKey(deliveryId) {
|
|
4490
|
+
return `tool-command:${deliveryId}`;
|
|
4491
|
+
}
|
|
3323
4492
|
function sessionStatusFromChatEnd(status) {
|
|
3324
4493
|
if (status === "paused") return "waiting_for_input";
|
|
3325
4494
|
if (status === "interrupted") return "interrupted";
|
|
@@ -3335,14 +4504,6 @@ function extractTerminalChatEndStatus(events) {
|
|
|
3335
4504
|
}
|
|
3336
4505
|
return null;
|
|
3337
4506
|
}
|
|
3338
|
-
function hasChatRunEvent2(events) {
|
|
3339
|
-
return events.some(
|
|
3340
|
-
(event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
|
|
3341
|
-
);
|
|
3342
|
-
}
|
|
3343
|
-
function isUiMetaLike(value) {
|
|
3344
|
-
return isRecord5(value) && ("resourceHTML" in value || "resourceUri" in value || "resourceURI" in value);
|
|
3345
|
-
}
|
|
3346
4507
|
var AgentSession = class _AgentSession {
|
|
3347
4508
|
sessionId;
|
|
3348
4509
|
state;
|
|
@@ -3356,14 +4517,41 @@ var AgentSession = class _AgentSession {
|
|
|
3356
4517
|
patchFlushCancel = null;
|
|
3357
4518
|
joined = false;
|
|
3358
4519
|
joinPending = null;
|
|
4520
|
+
// bootstrap 与每次 reconnect 共用一条订阅生命周期队列。它们都会重置 join
|
|
4521
|
+
// 状态并临时设置 replay source,禁止并发,否则较早操作的 finally 会清掉
|
|
4522
|
+
// 较晚操作仍在使用的来源上下文。
|
|
4523
|
+
subscriptionQueue = Promise.resolve();
|
|
4524
|
+
replaceQueue = Promise.resolve();
|
|
4525
|
+
timelineEpoch = 0;
|
|
4526
|
+
/** 定位请求的流水号:被后一次选择取代的那次定位不再提交窗口(见 locateEntry)。 */
|
|
4527
|
+
locateToken = 0;
|
|
4528
|
+
/** 在途那次定位的请求句柄。**只**服务定位:权威刷新不共用它(见 cancelPendingLocate)。 */
|
|
4529
|
+
locateAbort = null;
|
|
4530
|
+
pagingGeneration = 0;
|
|
4531
|
+
liveRevision = 0;
|
|
4532
|
+
modeRevision = 0;
|
|
4533
|
+
liveRevisions = /* @__PURE__ */ new Map();
|
|
4534
|
+
pendingOptimisticTurnIds = /* @__PURE__ */ new Set();
|
|
4535
|
+
olderPending = null;
|
|
4536
|
+
replaceInFlight = 0;
|
|
3359
4537
|
// 回放控制请求排成一条队列。连着点"5x"再点"退出回放"时,并发发出去的两个
|
|
3360
4538
|
// 请求响应可能乱序回来,晚到的旧值会把已经退出的会话又显示成回放中;用
|
|
3361
4539
|
// 序号丢弃过期响应又会在失败时留下中间态。串行执行就没有这些情况:每个
|
|
3362
4540
|
// 响应都是当时最新的,失败也只影响自己。
|
|
3363
4541
|
replayQueue = Promise.resolve();
|
|
4542
|
+
// Historical snapshots and live commands share one delivery barrier. A
|
|
4543
|
+
// repair closes the barrier before joining, snapshots the raw command log
|
|
4544
|
+
// after the join, then releases buffered commands in socket arrival order.
|
|
4545
|
+
commandRestoreQueue = Promise.resolve();
|
|
4546
|
+
commandRecoveryGeneration = 0;
|
|
4547
|
+
commandRestoreRetryAttempt = 0;
|
|
4548
|
+
commandRestoreRetryHandle = null;
|
|
4549
|
+
bufferedCommandDeliveries = null;
|
|
3364
4550
|
pendingReplayMessage = null;
|
|
3365
4551
|
pendingReplayMode;
|
|
4552
|
+
pendingReplayKbIds;
|
|
3366
4553
|
recentChatEndAt = 0;
|
|
4554
|
+
subscriptionReplaySource = null;
|
|
3367
4555
|
disposed = false;
|
|
3368
4556
|
getAppContext = null;
|
|
3369
4557
|
sendPending = false;
|
|
@@ -3374,11 +4562,19 @@ var AgentSession = class _AgentSession {
|
|
|
3374
4562
|
seenToolCallIds = /* @__PURE__ */ new Set();
|
|
3375
4563
|
finishedToolCallIds = /* @__PURE__ */ new Set();
|
|
3376
4564
|
emittedMessageTurnIds = /* @__PURE__ */ new Set();
|
|
3377
|
-
constructor(sessionId, runtime, connection) {
|
|
4565
|
+
constructor(sessionId, runtime, connection, options = {}) {
|
|
3378
4566
|
this.sessionId = sessionId;
|
|
3379
4567
|
this.runtime = runtime;
|
|
3380
4568
|
this.state = { ...createInitialSessionState(sessionId), connection };
|
|
4569
|
+
this.isStatic = options.staticHistory === true;
|
|
3381
4570
|
}
|
|
4571
|
+
/**
|
|
4572
|
+
* 静态历史(分享页 / `fromTurns`):没有目录接口,也**没有**"目录还没到位"这个中间态。
|
|
4573
|
+
*
|
|
4574
|
+
* 宿主据此决定要不要拿已加载窗口当兜底条目——真实会话在 timeline 被重写后,窗口里还是
|
|
4575
|
+
* 旧分支的发言,拿它兜底会把已经失效的 entry id 重新摆到轨道上,点了必然找不到。
|
|
4576
|
+
*/
|
|
4577
|
+
isStatic;
|
|
3382
4578
|
/** 从一份静态 turn 快照构造只展示历史的会话(例如公开分享页)。 */
|
|
3383
4579
|
static fromTurns(sessionId, turns) {
|
|
3384
4580
|
const readonlyError = () => {
|
|
@@ -3387,11 +4583,13 @@ var AgentSession = class _AgentSession {
|
|
|
3387
4583
|
const session = new _AgentSession(
|
|
3388
4584
|
sessionId,
|
|
3389
4585
|
{
|
|
3390
|
-
streamTokens: false,
|
|
3391
4586
|
emitEvent: readonlyError,
|
|
3392
4587
|
emitWithAck: async () => readonlyError(),
|
|
3393
4588
|
ensureConnected: readonlyError,
|
|
3394
|
-
|
|
4589
|
+
fetchTurnsPage: async () => ({ turns, nextBefore: null, currentMode: "executing" }),
|
|
4590
|
+
// 静态会话(分享页)没有会话目录接口,宿主用已加载的历史兜底条目。
|
|
4591
|
+
fetchTurnIndex: async () => ({ sessionId, items: [] }),
|
|
4592
|
+
fetchCommands: async () => [],
|
|
3395
4593
|
// 静态历史没有回放身份可言,给个空详情即可,不要抛错——_bootstrap
|
|
3396
4594
|
// 不会跑到这里,但保持只读语义一致。
|
|
3397
4595
|
fetchSessionInfo: async () => ({ replay_state: null }),
|
|
@@ -3402,7 +4600,8 @@ var AgentSession = class _AgentSession {
|
|
|
3402
4600
|
attachAppCli: async () => readonlyError(),
|
|
3403
4601
|
onDispose: () => true
|
|
3404
4602
|
},
|
|
3405
|
-
"connected"
|
|
4603
|
+
"connected",
|
|
4604
|
+
{ staticHistory: true }
|
|
3406
4605
|
);
|
|
3407
4606
|
session.state = {
|
|
3408
4607
|
...withTurns(session.state, turns),
|
|
@@ -3415,6 +4614,101 @@ var AgentSession = class _AgentSession {
|
|
|
3415
4614
|
getState() {
|
|
3416
4615
|
return this.state;
|
|
3417
4616
|
}
|
|
4617
|
+
get hasOlderHistory() {
|
|
4618
|
+
return this.state.nextBefore !== null;
|
|
4619
|
+
}
|
|
4620
|
+
/** Load the page immediately preceding the current display window. */
|
|
4621
|
+
loadOlderHistory(options = {}) {
|
|
4622
|
+
if (this.disposed) return Promise.resolve();
|
|
4623
|
+
if (this.olderPending) return this.olderPending;
|
|
4624
|
+
if (this.replaceInFlight > 0) {
|
|
4625
|
+
return this.replaceQueue.then(() => this.loadOlderHistory(options));
|
|
4626
|
+
}
|
|
4627
|
+
const before = this.state.nextBefore;
|
|
4628
|
+
if (!before) return Promise.resolve();
|
|
4629
|
+
const epoch = this.timelineEpoch;
|
|
4630
|
+
const generation = this.pagingGeneration;
|
|
4631
|
+
this.update((state) => ({ ...state, loadingOlder: true }));
|
|
4632
|
+
const request = this.runtime.fetchTurnsPage(this.sessionId, { before }).then((page) => {
|
|
4633
|
+
if (epoch !== this.timelineEpoch || generation !== this.pagingGeneration || this.state.nextBefore !== before) return;
|
|
4634
|
+
options.beforeCommit?.();
|
|
4635
|
+
this.update((state) => ({
|
|
4636
|
+
...withTurns(state, prependOlder(state.turns, page.turns)),
|
|
4637
|
+
nextBefore: page.nextBefore,
|
|
4638
|
+
loadingOlder: false
|
|
4639
|
+
}));
|
|
4640
|
+
}).catch(async (error) => {
|
|
4641
|
+
if (isCursorInvalidError(error) && epoch === this.timelineEpoch && generation === this.pagingGeneration && this.state.nextBefore === before) {
|
|
4642
|
+
this.invalidateTimeline();
|
|
4643
|
+
await this.replaceFromHistory();
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
4646
|
+
}).finally(() => {
|
|
4647
|
+
if (this.olderPending !== request) return;
|
|
4648
|
+
this.olderPending = null;
|
|
4649
|
+
this.update((state) => ({ ...state, loadingOlder: false }));
|
|
4650
|
+
});
|
|
4651
|
+
this.olderPending = request;
|
|
4652
|
+
return request;
|
|
4653
|
+
}
|
|
4654
|
+
/**
|
|
4655
|
+
* 定位到一条窗口外的历史发言:请服务端返回"从这条发言到最新"的窗口并整窗替换。
|
|
4656
|
+
*
|
|
4657
|
+
* 返回 false 表示这次定位没有落地(请求失败,或期间 timeline 被重写、会话已释放),
|
|
4658
|
+
* 当前窗口保持不变,调用方可以原样重试。已加载的发言不要走这里——宿主直接滚动,
|
|
4659
|
+
* 一次网络请求都不该发。
|
|
4660
|
+
*/
|
|
4661
|
+
async locateEntry(entryId) {
|
|
4662
|
+
const token = ++this.locateToken;
|
|
4663
|
+
const controller = new AbortController();
|
|
4664
|
+
this.locateAbort?.abort();
|
|
4665
|
+
this.locateAbort = controller;
|
|
4666
|
+
try {
|
|
4667
|
+
return await this.replaceFromHistory({
|
|
4668
|
+
suppressErrors: false,
|
|
4669
|
+
fromEntryId: entryId,
|
|
4670
|
+
signal: controller.signal,
|
|
4671
|
+
isStale: () => this.locateToken !== token
|
|
4672
|
+
});
|
|
4673
|
+
} catch {
|
|
4674
|
+
return false;
|
|
4675
|
+
} finally {
|
|
4676
|
+
if (this.locateAbort === controller) this.locateAbort = null;
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
/**
|
|
4680
|
+
* 取消在途定位:用户改选了另一条发言(哪怕那条已经加载、不需要请求),
|
|
4681
|
+
* 先前那次定位既不该再落地,也不该继续占着替换队列——排队中的下一次定位
|
|
4682
|
+
* 不然要等它跑完(用户看到的是"点了新目标一直转圈")。
|
|
4683
|
+
*
|
|
4684
|
+
* **只**中止定位自己的请求:权威刷新(运行结束 / 重连补数)不共用这个 controller,
|
|
4685
|
+
* 否则"运行刚结束时点一下侧栏"就会把终态对账一起中止,流式与历史对不上。
|
|
4686
|
+
*/
|
|
4687
|
+
cancelPendingLocate() {
|
|
4688
|
+
this.locateToken += 1;
|
|
4689
|
+
this.locateAbort?.abort();
|
|
4690
|
+
this.locateAbort = null;
|
|
4691
|
+
}
|
|
4692
|
+
/**
|
|
4693
|
+
* timeline 被整体改写(rewind / checkout / 释放):推进纪元,并把在途定位请求一并中止
|
|
4694
|
+
* ——它已经属于旧 timeline,却还占着替换队列,会让改写后的权威窗口排在后面等它跑完。
|
|
4695
|
+
* 作废号仍然保留:中止只是让它更早结束。
|
|
4696
|
+
*/
|
|
4697
|
+
invalidateTimeline() {
|
|
4698
|
+
this.timelineEpoch += 1;
|
|
4699
|
+
this.locateAbort?.abort();
|
|
4700
|
+
this.locateAbort = null;
|
|
4701
|
+
}
|
|
4702
|
+
/**
|
|
4703
|
+
* 拉取当前分支的用户发言目录(消息导航侧栏用)。
|
|
4704
|
+
*
|
|
4705
|
+
* 不在这里缓存:什么时候该刷新只有调用方知道(会话切换、历史被替换、新发言),
|
|
4706
|
+
* 缓存只会让旧条目在 rewind 之后继续可点。
|
|
4707
|
+
*/
|
|
4708
|
+
async loadTurnIndex() {
|
|
4709
|
+
const index = await this.runtime.fetchTurnIndex(this.sessionId);
|
|
4710
|
+
return index.items;
|
|
4711
|
+
}
|
|
3418
4712
|
/** 订阅状态变化。返回取消订阅函数。 */
|
|
3419
4713
|
subscribe(listener) {
|
|
3420
4714
|
this.listeners.add(listener);
|
|
@@ -3467,7 +4761,7 @@ var AgentSession = class _AgentSession {
|
|
|
3467
4761
|
/** 发送一条消息。字符串或多模态内容均可。 */
|
|
3468
4762
|
async send(content, options = {}) {
|
|
3469
4763
|
this.runtime.ensureConnected();
|
|
3470
|
-
if (this.state.isStreaming || this.state.activeCompaction || this.sendPending || this.manualCompactionPending) {
|
|
4764
|
+
if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.sendPending || this.manualCompactionPending) {
|
|
3471
4765
|
this.update(
|
|
3472
4766
|
(s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\u6216\u5BF9\u8BDD\u6B63\u5728\u6574\u7406\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002")
|
|
3473
4767
|
);
|
|
@@ -3480,9 +4774,125 @@ var AgentSession = class _AgentSession {
|
|
|
3480
4774
|
this.sendPending = false;
|
|
3481
4775
|
}
|
|
3482
4776
|
}
|
|
4777
|
+
/**
|
|
4778
|
+
* 入队一条等待消息(运行中排队 / 暂停期间照常入队)。
|
|
4779
|
+
*
|
|
4780
|
+
* ack 超时或传输失败时结果未知:用**同一个** `client_request_id` 重试一次,
|
|
4781
|
+
* 服务端按 `(session_id, client_request_id)` 幂等,不会制造第二条。
|
|
4782
|
+
*/
|
|
4783
|
+
async queueMessage(content) {
|
|
4784
|
+
const text = content.trim();
|
|
4785
|
+
if (!text) {
|
|
4786
|
+
return { ok: false, conflict: false, snapshot: null, message: "\u6D88\u606F\u4E0D\u80FD\u4E3A\u7A7A" };
|
|
4787
|
+
}
|
|
4788
|
+
const payload = {
|
|
4789
|
+
session_id: this.sessionId,
|
|
4790
|
+
message: text,
|
|
4791
|
+
client_request_id: createClientRequestId()
|
|
4792
|
+
};
|
|
4793
|
+
try {
|
|
4794
|
+
return await this.requestQueueAck("chat:append", payload);
|
|
4795
|
+
} catch {
|
|
4796
|
+
try {
|
|
4797
|
+
return await this.requestQueueAck("chat:append", payload);
|
|
4798
|
+
} catch (error) {
|
|
4799
|
+
return queueRequestFailure(error);
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
/** 修改一条等待消息的正文;`expectedVersion` 是客户端看到的条目版本。 */
|
|
4804
|
+
async updateQueuedMessage(itemId, expectedVersion, content) {
|
|
4805
|
+
return this.runQueueCommand("chat:queue:update", {
|
|
4806
|
+
session_id: this.sessionId,
|
|
4807
|
+
item_id: itemId,
|
|
4808
|
+
expected_version: expectedVersion,
|
|
4809
|
+
message: content
|
|
4810
|
+
});
|
|
4811
|
+
}
|
|
4812
|
+
/**
|
|
4813
|
+
* 重排等待消息。`orderedIds` 必须是当前 pending 条目 id 的**完整集合**(顺序任意),
|
|
4814
|
+
* 服务端按 `expectedRevision` 校验后整体替换顺序。
|
|
4815
|
+
*
|
|
4816
|
+
* 拖动排序就该像本地操作一样落地:版本冲突(运行中的任务刚领走一条 / 另一个端也在
|
|
4817
|
+
* 动队列)不甩给用户「刷新后重试」,而是拿冲突响应里那份**最新快照**重算一次再提交
|
|
4818
|
+
* ——掉队的条目(已经被领取执行的)从顺序里剔除,剩下的保持用户拖出来的相对次序。
|
|
4819
|
+
* 最多补两次;仍旧冲突才把结果交回调用方。
|
|
4820
|
+
*/
|
|
4821
|
+
async reorderQueuedMessages(expectedRevision, orderedIds) {
|
|
4822
|
+
let revision = expectedRevision;
|
|
4823
|
+
let ids = orderedIds;
|
|
4824
|
+
let result = await this.runQueueCommand("chat:queue:reorder", {
|
|
4825
|
+
session_id: this.sessionId,
|
|
4826
|
+
expected_revision: revision,
|
|
4827
|
+
ordered_ids: ids
|
|
4828
|
+
});
|
|
4829
|
+
for (let attempt = 0; attempt < 2 && !result.ok && result.conflict; attempt++) {
|
|
4830
|
+
const snapshot2 = result.snapshot;
|
|
4831
|
+
if (!snapshot2) break;
|
|
4832
|
+
const pending = snapshot2.items.filter(canEditQueuedMessage);
|
|
4833
|
+
const live = new Set(pending.map((item) => item.id));
|
|
4834
|
+
const next = ids.filter((id) => live.has(id));
|
|
4835
|
+
for (const item of pending) if (!next.includes(item.id)) next.push(item.id);
|
|
4836
|
+
if (next.length === 0) {
|
|
4837
|
+
return { ok: true, conflict: false, snapshot: snapshot2, message: null };
|
|
4838
|
+
}
|
|
4839
|
+
revision = snapshot2.revision;
|
|
4840
|
+
ids = next;
|
|
4841
|
+
result = await this.runQueueCommand("chat:queue:reorder", {
|
|
4842
|
+
session_id: this.sessionId,
|
|
4843
|
+
expected_revision: revision,
|
|
4844
|
+
ordered_ids: ids
|
|
4845
|
+
});
|
|
4846
|
+
}
|
|
4847
|
+
return result;
|
|
4848
|
+
}
|
|
4849
|
+
/** 删除一条等待消息;不传版本时由服务端自行判定。 */
|
|
4850
|
+
async cancelQueuedMessage(itemId, expectedVersion) {
|
|
4851
|
+
return this.runQueueCommand("chat:queue:cancel", {
|
|
4852
|
+
session_id: this.sessionId,
|
|
4853
|
+
item_id: itemId,
|
|
4854
|
+
expected_version: expectedVersion
|
|
4855
|
+
});
|
|
4856
|
+
}
|
|
4857
|
+
/** 立即补充:把等待消息交给当前运行,在下一个接收点消费,不停止也不另起一轮。 */
|
|
4858
|
+
async deliverQueuedMessage(itemId, expectedVersion) {
|
|
4859
|
+
return this.runQueueCommand("chat:queue:deliver", {
|
|
4860
|
+
session_id: this.sessionId,
|
|
4861
|
+
item_id: itemId,
|
|
4862
|
+
expected_version: expectedVersion
|
|
4863
|
+
});
|
|
4864
|
+
}
|
|
4865
|
+
/** 继续执行:清除暂停,并按当前顺序恢复逐条接续。 */
|
|
4866
|
+
async resumeQueue() {
|
|
4867
|
+
return this.runQueueCommand("chat:queue:resume", { session_id: this.sessionId });
|
|
4868
|
+
}
|
|
4869
|
+
/** 拉取一份权威队列快照。首次连接与每次重连后由实例自动调用,也可以手动刷新。 */
|
|
4870
|
+
async refreshQueue() {
|
|
4871
|
+
return this.runQueueCommand("chat:queue:list", { session_id: this.sessionId });
|
|
4872
|
+
}
|
|
4873
|
+
/**
|
|
4874
|
+
* 发出一条队列命令并安装 ack 里的快照。
|
|
4875
|
+
* 传输层失败(超时 / 断线)会 reject,由调用方决定是否重试。
|
|
4876
|
+
*/
|
|
4877
|
+
async requestQueueAck(event, payload) {
|
|
4878
|
+
this.runtime.ensureConnected();
|
|
4879
|
+
const response = await this.runtime.emitWithAck(event, payload, QUEUE_ACK_TIMEOUT_MS);
|
|
4880
|
+
const result = parseQueueAck(response, this.sessionId);
|
|
4881
|
+
const snapshot2 = result.snapshot;
|
|
4882
|
+
if (snapshot2) this.update((state) => withQueue(state, snapshot2));
|
|
4883
|
+
return result;
|
|
4884
|
+
}
|
|
4885
|
+
/** 队列命令的对外入口:传输失败只体现为失败结果,不向调用方抛异常。 */
|
|
4886
|
+
async runQueueCommand(event, payload) {
|
|
4887
|
+
try {
|
|
4888
|
+
return await this.requestQueueAck(event, payload);
|
|
4889
|
+
} catch (error) {
|
|
4890
|
+
return queueRequestFailure(error);
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
3483
4893
|
async sendUnlocked(content, options) {
|
|
3484
4894
|
await this.ensureIdleBeforeSend();
|
|
3485
|
-
if (this.state.isStreaming || this.state.activeCompaction || this.manualCompactionPending) {
|
|
4895
|
+
if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.manualCompactionPending) {
|
|
3486
4896
|
this.update(
|
|
3487
4897
|
(s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\u6216\u5BF9\u8BDD\u6B63\u5728\u6574\u7406\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002")
|
|
3488
4898
|
);
|
|
@@ -3492,7 +4902,7 @@ var AgentSession = class _AgentSession {
|
|
|
3492
4902
|
if (this.getAppContext) {
|
|
3493
4903
|
try {
|
|
3494
4904
|
const rawContext = await this.getAppContext();
|
|
3495
|
-
if (!
|
|
4905
|
+
if (!isRecord(rawContext)) {
|
|
3496
4906
|
throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
|
|
3497
4907
|
}
|
|
3498
4908
|
const serialized = JSON.stringify(rawContext);
|
|
@@ -3500,7 +4910,7 @@ var AgentSession = class _AgentSession {
|
|
|
3500
4910
|
throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE\u53EF\u5E8F\u5217\u5316\u7684 JSON \u5BF9\u8C61");
|
|
3501
4911
|
}
|
|
3502
4912
|
const normalized = JSON.parse(serialized);
|
|
3503
|
-
if (!
|
|
4913
|
+
if (!isRecord(normalized)) {
|
|
3504
4914
|
throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
|
|
3505
4915
|
}
|
|
3506
4916
|
appContext = normalized;
|
|
@@ -3510,17 +4920,18 @@ var AgentSession = class _AgentSession {
|
|
|
3510
4920
|
return false;
|
|
3511
4921
|
}
|
|
3512
4922
|
}
|
|
4923
|
+
const wasStreaming = this.state.isStreaming;
|
|
3513
4924
|
const askUserAnswer = options.askUserAnswer;
|
|
3514
4925
|
let optimisticTurnId = null;
|
|
4926
|
+
const clientRequestId = createClientRequestId();
|
|
3515
4927
|
let optimisticAskAnswer;
|
|
3516
4928
|
let previousAskAnswer;
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
}
|
|
4929
|
+
this.update((state) => {
|
|
4930
|
+
const next = addUserMessage(state, content, clientRequestId);
|
|
4931
|
+
optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
|
|
4932
|
+
return next;
|
|
4933
|
+
});
|
|
4934
|
+
if (optimisticTurnId) this.markOptimisticTurnPending(optimisticTurnId);
|
|
3524
4935
|
this.update((s) => ({ ...s, isStreaming: true }));
|
|
3525
4936
|
if (askUserAnswer && typeof askUserAnswer.tool_call_id === "string") {
|
|
3526
4937
|
const { tool_call_id, ...rest } = askUserAnswer;
|
|
@@ -3534,17 +4945,22 @@ var AgentSession = class _AgentSession {
|
|
|
3534
4945
|
if (!options.replayDecision) {
|
|
3535
4946
|
this.pendingReplayMessage = content;
|
|
3536
4947
|
this.pendingReplayMode = options.mode;
|
|
4948
|
+
this.pendingReplayKbIds = options.kbIds;
|
|
3537
4949
|
}
|
|
3538
4950
|
const payload = {
|
|
3539
4951
|
session_id: this.sessionId,
|
|
3540
4952
|
message: content,
|
|
4953
|
+
client_request_id: clientRequestId,
|
|
3541
4954
|
mode: options.mode,
|
|
3542
4955
|
askuser_answer: askUserAnswer,
|
|
3543
4956
|
model: options.model,
|
|
3544
4957
|
thinking_override: options.thinkingOverride,
|
|
3545
4958
|
whatif: options.whatif,
|
|
3546
4959
|
replay_decision: options.replayDecision,
|
|
3547
|
-
app_context: appContext
|
|
4960
|
+
app_context: appContext,
|
|
4961
|
+
kb_ids: options.kbIds,
|
|
4962
|
+
// 运行中提交不必由客户端挑命令:服务端按会话运行状态决定直接执行还是入队。
|
|
4963
|
+
queue_when_running: options.queueWhenRunning === true
|
|
3548
4964
|
};
|
|
3549
4965
|
try {
|
|
3550
4966
|
await this.ensureJoined();
|
|
@@ -3554,12 +4970,28 @@ var AgentSession = class _AgentSession {
|
|
|
3554
4970
|
askUserAnswer,
|
|
3555
4971
|
optimisticAskAnswer,
|
|
3556
4972
|
previousAskAnswer,
|
|
4973
|
+
wasStreaming,
|
|
3557
4974
|
error instanceof Error && error.message ? error.message : "\u4F1A\u8BDD\u8BA2\u9605\u5931\u8D25\uFF0C\u6D88\u606F\u672A\u53D1\u9001"
|
|
3558
4975
|
);
|
|
3559
4976
|
return false;
|
|
3560
4977
|
}
|
|
3561
4978
|
try {
|
|
3562
4979
|
const response = await this.runtime.emitWithAck("chat:send", payload, 3e4);
|
|
4980
|
+
if (response?.status === "queued") {
|
|
4981
|
+
this.discardOptimisticSend(
|
|
4982
|
+
optimisticTurnId,
|
|
4983
|
+
askUserAnswer,
|
|
4984
|
+
optimisticAskAnswer,
|
|
4985
|
+
previousAskAnswer
|
|
4986
|
+
);
|
|
4987
|
+
const snapshot2 = parseQueueAck(response, this.sessionId).snapshot;
|
|
4988
|
+
const running = response.running === true;
|
|
4989
|
+
this.update((current) => {
|
|
4990
|
+
const next = running ? current : { ...current, isStreaming: false };
|
|
4991
|
+
return snapshot2 ? withQueue(next, snapshot2) : next;
|
|
4992
|
+
});
|
|
4993
|
+
return true;
|
|
4994
|
+
}
|
|
3563
4995
|
if (response?.status !== "accepted") {
|
|
3564
4996
|
throw new Error(
|
|
3565
4997
|
typeof response?.message === "string" ? response.message : "\u6D88\u606F\u672A\u88AB\u670D\u52A1\u7AEF\u63A5\u53D7"
|
|
@@ -3572,19 +5004,31 @@ var AgentSession = class _AgentSession {
|
|
|
3572
5004
|
askUserAnswer,
|
|
3573
5005
|
optimisticAskAnswer,
|
|
3574
5006
|
previousAskAnswer,
|
|
5007
|
+
wasStreaming,
|
|
3575
5008
|
error instanceof Error && error.message ? error.message : "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5"
|
|
3576
5009
|
);
|
|
3577
5010
|
return false;
|
|
3578
5011
|
}
|
|
3579
5012
|
}
|
|
3580
|
-
rollbackOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer, message) {
|
|
5013
|
+
rollbackOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer, wasStreaming, message) {
|
|
5014
|
+
this.discardOptimisticSend(
|
|
5015
|
+
optimisticTurnId,
|
|
5016
|
+
askUserAnswer,
|
|
5017
|
+
optimisticAskAnswer,
|
|
5018
|
+
previousAskAnswer
|
|
5019
|
+
);
|
|
5020
|
+
this.update(
|
|
5021
|
+
(current) => addErrorMessage(wasStreaming ? current : { ...current, isStreaming: false }, message)
|
|
5022
|
+
);
|
|
5023
|
+
}
|
|
5024
|
+
/** 撤回一次乐观提交:移除乐观消息、恢复提问作答、清掉回放草稿。 */
|
|
5025
|
+
discardOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer) {
|
|
3581
5026
|
this.pendingReplayMessage = null;
|
|
3582
5027
|
this.pendingReplayMode = void 0;
|
|
5028
|
+
this.pendingReplayKbIds = void 0;
|
|
3583
5029
|
this.update((current) => {
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
current.turns.filter((turn) => turn.turn_id !== optimisticTurnId)
|
|
3587
|
-
) : current;
|
|
5030
|
+
const filtered = optimisticTurnId ? current.turns.filter((turn) => turn.turn_id !== optimisticTurnId) : current.turns;
|
|
5031
|
+
let next = optimisticTurnId ? withTurns({ ...current, turns: filtered }, filtered) : current;
|
|
3588
5032
|
const toolCallId = askUserAnswer && typeof askUserAnswer.tool_call_id === "string" ? askUserAnswer.tool_call_id : null;
|
|
3589
5033
|
if (toolCallId && next.askAnswers[toolCallId] === optimisticAskAnswer) {
|
|
3590
5034
|
const askAnswers = { ...next.askAnswers };
|
|
@@ -3595,18 +5039,20 @@ var AgentSession = class _AgentSession {
|
|
|
3595
5039
|
}
|
|
3596
5040
|
next = { ...next, askAnswers };
|
|
3597
5041
|
}
|
|
3598
|
-
return
|
|
5042
|
+
return next;
|
|
3599
5043
|
});
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
this.runtime.emitEvent("chat:append", { session_id: this.sessionId, message: text });
|
|
3605
|
-
this.update((s) => addUserMessage(s, text));
|
|
5044
|
+
if (optimisticTurnId) {
|
|
5045
|
+
this.pendingOptimisticTurnIds.delete(optimisticTurnId);
|
|
5046
|
+
this.liveRevisions.delete(optimisticTurnId);
|
|
5047
|
+
}
|
|
3606
5048
|
}
|
|
3607
5049
|
/** 停止当前回复。 */
|
|
3608
5050
|
stop() {
|
|
3609
|
-
return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) =>
|
|
5051
|
+
return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) => {
|
|
5052
|
+
const result = response ?? {};
|
|
5053
|
+
if (result.status !== "accepted") this.manualCompactionPending = false;
|
|
5054
|
+
return result;
|
|
5055
|
+
});
|
|
3610
5056
|
}
|
|
3611
5057
|
/** 手动触发上下文压缩。 */
|
|
3612
5058
|
compact() {
|
|
@@ -3626,8 +5072,10 @@ var AgentSession = class _AgentSession {
|
|
|
3626
5072
|
trigger: "manual"
|
|
3627
5073
|
}
|
|
3628
5074
|
}));
|
|
3629
|
-
return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {}).
|
|
5075
|
+
return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {}).catch((error) => {
|
|
3630
5076
|
this.manualCompactionPending = false;
|
|
5077
|
+
throw error;
|
|
5078
|
+
}).finally(() => {
|
|
3631
5079
|
if (this.state.activeCompaction?.compaction_id === "pending-manual-compaction") {
|
|
3632
5080
|
this.update((state) => ({ ...state, activeCompaction: null }));
|
|
3633
5081
|
}
|
|
@@ -3666,10 +5114,20 @@ var AgentSession = class _AgentSession {
|
|
|
3666
5114
|
if (this.disposed) return;
|
|
3667
5115
|
if (!this.runtime.onDispose(this)) return;
|
|
3668
5116
|
this.disposed = true;
|
|
5117
|
+
this.invalidateTimeline();
|
|
5118
|
+
this.invalidateOlderPaging();
|
|
5119
|
+
this.state = { ...this.state, nextBefore: null, loadingOlder: false };
|
|
3669
5120
|
this.cancelPatchFlush();
|
|
3670
5121
|
this.listeners.clear();
|
|
3671
5122
|
this.emitter.clear();
|
|
3672
5123
|
this.commands.clear();
|
|
5124
|
+
this.commandRecoveryGeneration += 1;
|
|
5125
|
+
this.commandRestoreRetryAttempt = 0;
|
|
5126
|
+
this.bufferedCommandDeliveries = null;
|
|
5127
|
+
if (this.commandRestoreRetryHandle !== null) {
|
|
5128
|
+
globalThis.clearTimeout(this.commandRestoreRetryHandle);
|
|
5129
|
+
this.commandRestoreRetryHandle = null;
|
|
5130
|
+
}
|
|
3673
5131
|
}
|
|
3674
5132
|
get isDisposed() {
|
|
3675
5133
|
return this.disposed;
|
|
@@ -3680,28 +5138,64 @@ var AgentSession = class _AgentSession {
|
|
|
3680
5138
|
if (this.state.connection === connection) return;
|
|
3681
5139
|
this.update((s) => ({ ...s, connection }));
|
|
3682
5140
|
}
|
|
5141
|
+
/**
|
|
5142
|
+
* @internal 服务端广播的权威队列快照。
|
|
5143
|
+
* 结构不合法或 revision 不新的一律丢弃,绝不用旧快照覆盖新状态。
|
|
5144
|
+
*/
|
|
5145
|
+
_handleQueueUpdated(payload) {
|
|
5146
|
+
if (!isSessionQueueSnapshot(payload)) return;
|
|
5147
|
+
const snapshot2 = payload;
|
|
5148
|
+
this.update((state) => withQueue(state, snapshot2));
|
|
5149
|
+
}
|
|
3683
5150
|
/** @internal socket 重连后由 Hub 调用:重新加入房间并补数。 */
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
this.
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
5151
|
+
_rejoinAfterReconnect() {
|
|
5152
|
+
this.invalidateOlderPaging();
|
|
5153
|
+
this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
|
|
5154
|
+
const reconnectLiveRevision = this.liveRevision;
|
|
5155
|
+
const reconnectModeRevision = this.modeRevision;
|
|
5156
|
+
const commandRecoveryGeneration = this.beginCommandRecovery();
|
|
5157
|
+
return this.enqueueSubscription(async () => {
|
|
5158
|
+
this.joined = false;
|
|
5159
|
+
this.joinPending = null;
|
|
5160
|
+
try {
|
|
5161
|
+
this.subscriptionReplaySource = "reconnect_replay";
|
|
5162
|
+
try {
|
|
5163
|
+
await this.ensureJoined(reconnectLiveRevision);
|
|
5164
|
+
} finally {
|
|
5165
|
+
this.subscriptionReplaySource = null;
|
|
5166
|
+
this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
|
|
5167
|
+
}
|
|
5168
|
+
await Promise.all([
|
|
5169
|
+
this.refreshAuthoritativeHistory({
|
|
5170
|
+
liveRevisionAtStart: reconnectLiveRevision,
|
|
5171
|
+
modeRevisionAtStart: reconnectModeRevision
|
|
5172
|
+
}),
|
|
5173
|
+
this.syncReplayFromSession()
|
|
5174
|
+
]);
|
|
5175
|
+
} catch {
|
|
5176
|
+
}
|
|
5177
|
+
});
|
|
3699
5178
|
}
|
|
3700
5179
|
/** @internal 首次连接:加载历史 + 加入房间。 */
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
5180
|
+
_bootstrap() {
|
|
5181
|
+
const commandRecoveryGeneration = this.beginCommandRecovery();
|
|
5182
|
+
return this.enqueueSubscription(async () => {
|
|
5183
|
+
await this.replaceFromHistory({ suppressErrors: false });
|
|
5184
|
+
this.subscriptionReplaySource = "bootstrap_replay";
|
|
5185
|
+
try {
|
|
5186
|
+
await this.ensureJoined();
|
|
5187
|
+
} finally {
|
|
5188
|
+
this.subscriptionReplaySource = null;
|
|
5189
|
+
}
|
|
5190
|
+
this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
|
|
5191
|
+
await Promise.all([this.syncReplayFromSession(), this.refreshQueue()]);
|
|
5192
|
+
});
|
|
5193
|
+
}
|
|
5194
|
+
/** 订阅生命周期串行执行;一次失败只由自己的调用方观察,不截断后续重连。 */
|
|
5195
|
+
enqueueSubscription(operation) {
|
|
5196
|
+
const run = this.subscriptionQueue.then(operation);
|
|
5197
|
+
this.subscriptionQueue = run.catch(() => void 0);
|
|
5198
|
+
return run;
|
|
3705
5199
|
}
|
|
3706
5200
|
/** 改回放是 owner 专属(服务端 PATCH 走 _get_owned_session)。 */
|
|
3707
5201
|
get canControlReplay() {
|
|
@@ -3758,10 +5252,16 @@ var AgentSession = class _AgentSession {
|
|
|
3758
5252
|
this.applyTurn(turn);
|
|
3759
5253
|
}
|
|
3760
5254
|
/** @internal 历史回放通道(event_format: raw)。 */
|
|
3761
|
-
_handleTurnEvents(events) {
|
|
5255
|
+
_handleTurnEvents(events, replayed = false) {
|
|
3762
5256
|
if (!events.length) return;
|
|
5257
|
+
if (events.some(
|
|
5258
|
+
(event) => event.type === "compaction:end" || event.type === "compaction:error" || event.type === "compaction:abort"
|
|
5259
|
+
)) {
|
|
5260
|
+
this.manualCompactionPending = false;
|
|
5261
|
+
}
|
|
5262
|
+
this.dispatchRawCommands(events);
|
|
3763
5263
|
const terminalChatEndStatus = extractTerminalChatEndStatus(events);
|
|
3764
|
-
if (!terminalChatEndStatus &&
|
|
5264
|
+
if (!terminalChatEndStatus && hasChatRunEvent(events) && this.state.status !== "waiting_for_input") {
|
|
3765
5265
|
this.markRunningIfIdle();
|
|
3766
5266
|
}
|
|
3767
5267
|
if (this.state.turns.length) {
|
|
@@ -3770,7 +5270,10 @@ var AgentSession = class _AgentSession {
|
|
|
3770
5270
|
const updates = this.projectionBuilder.processBatch(events);
|
|
3771
5271
|
for (const update of updates) {
|
|
3772
5272
|
if (update.kind === "upsert") {
|
|
3773
|
-
this.applyTurn(
|
|
5273
|
+
this.applyTurn(
|
|
5274
|
+
update.turn,
|
|
5275
|
+
replayed ? this.subscriptionReplaySource ?? "bootstrap_replay" : "live"
|
|
5276
|
+
);
|
|
3774
5277
|
} else if (update.kind === "workspace_changed") {
|
|
3775
5278
|
this.emitter.emit("workspaceChanged", {});
|
|
3776
5279
|
}
|
|
@@ -3816,6 +5319,8 @@ var AgentSession = class _AgentSession {
|
|
|
3816
5319
|
this.update((s) => ({ ...s, replay }));
|
|
3817
5320
|
}
|
|
3818
5321
|
this.emitter.emit("sessionUpdated", {
|
|
5322
|
+
workspacePath: data.workspace_path,
|
|
5323
|
+
chatGroupId: data.chat_group_id,
|
|
3819
5324
|
intent: data.intent,
|
|
3820
5325
|
status: data.status,
|
|
3821
5326
|
model: data.model,
|
|
@@ -3825,16 +5330,26 @@ var AgentSession = class _AgentSession {
|
|
|
3825
5330
|
}
|
|
3826
5331
|
/** @internal */
|
|
3827
5332
|
_handleRewind(checkpointId) {
|
|
3828
|
-
this.
|
|
3829
|
-
|
|
5333
|
+
this.invalidateTimeline();
|
|
5334
|
+
this.invalidateOlderPaging();
|
|
5335
|
+
this.update((s) => ({
|
|
5336
|
+
...s,
|
|
5337
|
+
mode: null,
|
|
5338
|
+
errorMessage: null,
|
|
5339
|
+
nextBefore: null,
|
|
5340
|
+
loadingOlder: false
|
|
5341
|
+
}));
|
|
5342
|
+
void this.replaceFromHistory();
|
|
3830
5343
|
this.emitter.emit("rewind", { checkpointId });
|
|
3831
5344
|
}
|
|
3832
5345
|
/** @internal */
|
|
3833
5346
|
_handleReplayMismatch(data) {
|
|
3834
5347
|
const message = this.pendingReplayMessage ?? data.actual_message ?? "";
|
|
3835
5348
|
const mode = this.pendingReplayMode;
|
|
5349
|
+
const kbIds = this.pendingReplayKbIds;
|
|
3836
5350
|
this.pendingReplayMessage = null;
|
|
3837
5351
|
this.pendingReplayMode = void 0;
|
|
5352
|
+
this.pendingReplayKbIds = void 0;
|
|
3838
5353
|
const respond = (decision) => {
|
|
3839
5354
|
void this.runtime.emitWithAck(
|
|
3840
5355
|
"chat:send",
|
|
@@ -3842,7 +5357,8 @@ var AgentSession = class _AgentSession {
|
|
|
3842
5357
|
session_id: this.sessionId,
|
|
3843
5358
|
message,
|
|
3844
5359
|
mode,
|
|
3845
|
-
replay_decision: decision
|
|
5360
|
+
replay_decision: decision,
|
|
5361
|
+
kb_ids: kbIds
|
|
3846
5362
|
},
|
|
3847
5363
|
3e4
|
|
3848
5364
|
).then((response) => {
|
|
@@ -3883,9 +5399,9 @@ var AgentSession = class _AgentSession {
|
|
|
3883
5399
|
detail,
|
|
3884
5400
|
status,
|
|
3885
5401
|
loopId,
|
|
3886
|
-
metadata:
|
|
5402
|
+
metadata: isRecord(notification.metadata) ? notification.metadata : void 0
|
|
3887
5403
|
});
|
|
3888
|
-
if (notificationType === "bg:started" &&
|
|
5404
|
+
if (notificationType === "bg:started" && isRecord(notification.metadata)) {
|
|
3889
5405
|
const taskId = typeof notification.metadata.task_id === "string" ? notification.metadata.task_id : "";
|
|
3890
5406
|
if (taskId) {
|
|
3891
5407
|
this.emitter.emit("backgroundTask", {
|
|
@@ -3907,12 +5423,12 @@ var AgentSession = class _AgentSession {
|
|
|
3907
5423
|
this.emitter.emit("artifact", event);
|
|
3908
5424
|
}
|
|
3909
5425
|
/** @internal 加入房间的应答里带 is_running,据此校准流式状态。 */
|
|
3910
|
-
_applyRunningState(isRunning) {
|
|
5426
|
+
_applyRunningState(isRunning, liveRevisionAtJoin = this.liveRevision) {
|
|
3911
5427
|
if (isRunning) {
|
|
3912
5428
|
this.update((s) => ({ ...s, status: "running", isStreaming: true }));
|
|
3913
5429
|
} else if (this.state.isStreaming) {
|
|
3914
5430
|
this.update((s) => ({ ...s, isStreaming: false }));
|
|
3915
|
-
void this.
|
|
5431
|
+
void this.refreshAuthoritativeHistory({ liveRevisionAtStart: liveRevisionAtJoin });
|
|
3916
5432
|
}
|
|
3917
5433
|
}
|
|
3918
5434
|
// ---------------------------------------------------------------- 内部实现
|
|
@@ -3933,14 +5449,36 @@ var AgentSession = class _AgentSession {
|
|
|
3933
5449
|
completeChat(status) {
|
|
3934
5450
|
this.finishStreaming(status);
|
|
3935
5451
|
this.recentChatEndAt = Date.now();
|
|
3936
|
-
void this.
|
|
5452
|
+
void this.refreshAuthoritativeHistory();
|
|
3937
5453
|
this.emitter.emit("chatEnd", { status });
|
|
3938
5454
|
}
|
|
3939
|
-
|
|
5455
|
+
markTurnFresh(turnId) {
|
|
5456
|
+
this.liveRevision += 1;
|
|
5457
|
+
this.liveRevisions.set(turnId, this.liveRevision);
|
|
5458
|
+
}
|
|
5459
|
+
markOptimisticTurnPending(turnId) {
|
|
5460
|
+
this.pendingOptimisticTurnIds.add(turnId);
|
|
5461
|
+
this.markTurnFresh(turnId);
|
|
5462
|
+
}
|
|
5463
|
+
pruneOptimisticLiveRevisions() {
|
|
5464
|
+
const retained = new Set(
|
|
5465
|
+
this.state.turns.map((turn) => turn.turn_id).filter((turnId) => turnId.startsWith("local-user-"))
|
|
5466
|
+
);
|
|
5467
|
+
for (const turnId of this.liveRevisions.keys()) {
|
|
5468
|
+
if (turnId.startsWith("local-user-") && !retained.has(turnId)) {
|
|
5469
|
+
this.pendingOptimisticTurnIds.delete(turnId);
|
|
5470
|
+
this.liveRevisions.delete(turnId);
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
applyTurn(turn, source = "live") {
|
|
3940
5475
|
this.flushPatches();
|
|
5476
|
+
this.markTurnFresh(turn.turn_id);
|
|
5477
|
+
if (extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
|
|
3941
5478
|
const previous = this.state;
|
|
3942
5479
|
this.update((s) => upsertTurn(s, turn));
|
|
3943
|
-
this.
|
|
5480
|
+
this.pruneOptimisticLiveRevisions();
|
|
5481
|
+
this.emitTurnDerivedEvents(previous, turn, source);
|
|
3944
5482
|
}
|
|
3945
5483
|
schedulePatchFlush() {
|
|
3946
5484
|
if (this.patchFlushHandle != null) return;
|
|
@@ -3973,11 +5511,15 @@ var AgentSession = class _AgentSession {
|
|
|
3973
5511
|
this.pendingTurnPatches.clear();
|
|
3974
5512
|
for (const patch of patches) {
|
|
3975
5513
|
const previous = this.state;
|
|
3976
|
-
|
|
5514
|
+
const next = applyTurnPatch(previous, patch);
|
|
5515
|
+
if (next === previous) continue;
|
|
5516
|
+
this.liveRevision += 1;
|
|
5517
|
+
this.liveRevisions.set(patch.turn_id, this.liveRevision);
|
|
3977
5518
|
const turn = patch.data.turn;
|
|
3978
|
-
if (turn)
|
|
3979
|
-
|
|
3980
|
-
|
|
5519
|
+
if (turn && extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
|
|
5520
|
+
this.setState(next);
|
|
5521
|
+
this.pruneOptimisticLiveRevisions();
|
|
5522
|
+
if (turn) this.emitTurnDerivedEvents(previous, turn);
|
|
3981
5523
|
}
|
|
3982
5524
|
if (this.patchFlushCancel) {
|
|
3983
5525
|
this.patchFlushCancel();
|
|
@@ -3986,7 +5528,7 @@ var AgentSession = class _AgentSession {
|
|
|
3986
5528
|
}
|
|
3987
5529
|
}
|
|
3988
5530
|
/** turn 数据落地后统一派生语义事件(指令 / 工具调用 / 消息 / 模式 / 预览 / OOM)。 */
|
|
3989
|
-
emitTurnDerivedEvents(previous, turn) {
|
|
5531
|
+
emitTurnDerivedEvents(previous, turn, source = "live") {
|
|
3990
5532
|
const mode = extractModeFromBlocks(turn.blocks);
|
|
3991
5533
|
if (mode && previous.mode !== mode) {
|
|
3992
5534
|
this.emitter.emit("modeChange", { mode });
|
|
@@ -3999,23 +5541,24 @@ var AgentSession = class _AgentSession {
|
|
|
3999
5541
|
this.dispatchCommand(turn, block);
|
|
4000
5542
|
continue;
|
|
4001
5543
|
}
|
|
4002
|
-
if (block.type === "tool_ui" && block.tool_call_id &&
|
|
5544
|
+
if (block.type === "tool_ui" && block.tool_call_id && isToolUiCard(block.content)) {
|
|
4003
5545
|
const ui = block.content;
|
|
4004
5546
|
if (ui.target !== "preview") continue;
|
|
4005
|
-
const
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
const
|
|
4009
|
-
if (this.deliveredToolPreviews.get(key) ===
|
|
4010
|
-
this.deliveredToolPreviews.set(key,
|
|
5547
|
+
const resolved = resolveToolUiCardContent(ui);
|
|
5548
|
+
if (!resolved) continue;
|
|
5549
|
+
const key = buildToolUiCardKey(block.tool_call_id, resolved.type, resolved.content);
|
|
5550
|
+
const fingerprint = JSON.stringify([resolved.content, ui.mcpApp?.sourceId]);
|
|
5551
|
+
if (this.deliveredToolPreviews.get(key) === fingerprint) continue;
|
|
5552
|
+
this.deliveredToolPreviews.set(key, fingerprint);
|
|
4011
5553
|
this.emitter.emit("toolPreview", {
|
|
4012
5554
|
toolCallId: block.tool_call_id,
|
|
4013
|
-
type:
|
|
4014
|
-
content:
|
|
4015
|
-
title: ui.title ?? "\u5DE5\u5177\u9884\u89C8"
|
|
5555
|
+
type: resolved.type,
|
|
5556
|
+
content: resolved.content,
|
|
5557
|
+
title: ui.title ?? "\u5DE5\u5177\u9884\u89C8",
|
|
5558
|
+
...ui.mcpApp ? { mcpApp: ui.mcpApp } : {}
|
|
4016
5559
|
});
|
|
4017
5560
|
}
|
|
4018
|
-
if (block.type === "system_notification" &&
|
|
5561
|
+
if (block.type === "system_notification" && isRecord(block.content)) {
|
|
4019
5562
|
this._handleSystemNotification(turn.loop_id, block.content);
|
|
4020
5563
|
}
|
|
4021
5564
|
}
|
|
@@ -4035,7 +5578,7 @@ var AgentSession = class _AgentSession {
|
|
|
4035
5578
|
const finished = toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled";
|
|
4036
5579
|
if (finished && !this.finishedToolCallIds.has(toolCall.id)) {
|
|
4037
5580
|
this.finishedToolCallIds.add(toolCall.id);
|
|
4038
|
-
this.emitter.emit("toolResult", { toolCall: info, turn });
|
|
5581
|
+
this.emitter.emit("toolResult", { toolCall: info, turn, source });
|
|
4039
5582
|
if (isOomResult(toolCall.result)) {
|
|
4040
5583
|
this.notifySandboxOom(toolCall.id);
|
|
4041
5584
|
}
|
|
@@ -4052,17 +5595,36 @@ var AgentSession = class _AgentSession {
|
|
|
4052
5595
|
dispatchCommand(turn, block) {
|
|
4053
5596
|
if (!isToolBridgeContent(block.content)) return;
|
|
4054
5597
|
const { action, payload } = block.content;
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
5598
|
+
const deliveryKey = block.delivery_id ? commandDeliveryKey(block.delivery_id) : `legacy-tool-command:${turn.turn_id}:${block.tool_call_id ?? ""}:${action}:${JSON.stringify(payload)}`;
|
|
5599
|
+
this.deliverCommand(deliveryKey, action, payload, block.tool_call_id ?? void 0);
|
|
5600
|
+
}
|
|
5601
|
+
dispatchRawCommands(events) {
|
|
5602
|
+
for (const event of events) {
|
|
5603
|
+
if (event.type !== "loop:tool_bridge") continue;
|
|
5604
|
+
const deliveryId = String(event.payload.delivery_id ?? "");
|
|
5605
|
+
const bridge = event.payload.bridge;
|
|
5606
|
+
if (!deliveryId || !isToolBridgeContent(bridge)) continue;
|
|
5607
|
+
this.deliverCommand(
|
|
5608
|
+
commandDeliveryKey(deliveryId),
|
|
5609
|
+
bridge.action,
|
|
5610
|
+
bridge.payload,
|
|
5611
|
+
String(event.payload.tool_call_id ?? "") || void 0
|
|
5612
|
+
);
|
|
4060
5613
|
}
|
|
4061
|
-
|
|
5614
|
+
}
|
|
5615
|
+
deliverCommand(deliveryKey, action, payload, toolCallId) {
|
|
5616
|
+
if (this.bufferedCommandDeliveries) {
|
|
5617
|
+
this.bufferedCommandDeliveries.push({ deliveryKey, action, payload, toolCallId });
|
|
5618
|
+
return;
|
|
5619
|
+
}
|
|
5620
|
+
this.deliverCommandNow({ deliveryKey, action, payload, toolCallId });
|
|
5621
|
+
}
|
|
5622
|
+
deliverCommandNow(command) {
|
|
5623
|
+
const { deliveryKey, action, payload, toolCallId } = command;
|
|
4062
5624
|
if (this.deliveredCommandKeys.has(deliveryKey)) return;
|
|
4063
5625
|
this.deliveredCommandKeys.add(deliveryKey);
|
|
4064
|
-
const meta = { toolCallId
|
|
4065
|
-
this.emitter.emit("command", { action, payload, toolCallId
|
|
5626
|
+
const meta = { toolCallId };
|
|
5627
|
+
this.emitter.emit("command", { action, payload, toolCallId });
|
|
4066
5628
|
this.commands.dispatch(action, payload, meta);
|
|
4067
5629
|
}
|
|
4068
5630
|
notifySandboxOom(key) {
|
|
@@ -4071,49 +5633,195 @@ var AgentSession = class _AgentSession {
|
|
|
4071
5633
|
this.update((s) => addErrorMessage(s, OOM_MESSAGE));
|
|
4072
5634
|
this.emitter.emit("sandboxOom", { toolCallId: key });
|
|
4073
5635
|
}
|
|
4074
|
-
async syncTurnsFromHistory(options = {}) {
|
|
4075
|
-
const { clearOnEmpty = false, suppressErrors = true, preserveChangesSince } = options;
|
|
4076
|
-
const turnsAtRequestStart = this.state.turns;
|
|
4077
|
-
try {
|
|
4078
|
-
const turns = await this.runtime.fetchTurns(this.sessionId);
|
|
4079
|
-
if (turns.length === 0 && !clearOnEmpty) return;
|
|
4080
|
-
this.update((state) => {
|
|
4081
|
-
const mergeBaseline = preserveChangesSince ?? turnsAtRequestStart;
|
|
4082
|
-
if (state.turns === mergeBaseline) return withTurns(state, turns);
|
|
4083
|
-
const historyById = new Map(turns.map((turn) => [turn.turn_id, turn]));
|
|
4084
|
-
let merged = withTurns(state, turns);
|
|
4085
|
-
for (const current of state.turns) {
|
|
4086
|
-
const fromHistory = historyById.get(current.turn_id);
|
|
4087
|
-
if (!fromHistory || current.sequence > fromHistory.sequence) {
|
|
4088
|
-
merged = upsertTurn(merged, current);
|
|
4089
|
-
}
|
|
4090
|
-
}
|
|
4091
|
-
return merged;
|
|
4092
|
-
});
|
|
4093
|
-
this.replayCommandsFromTurns(turns);
|
|
4094
|
-
} catch (error) {
|
|
4095
|
-
if (!suppressErrors) throw error;
|
|
4096
|
-
}
|
|
4097
|
-
}
|
|
4098
5636
|
/**
|
|
4099
|
-
*
|
|
5637
|
+
* 权威刷新(运行结束 / 重连补数):**按当前窗口第一条发言取窗口**,不再另记"用户想看哪段"。
|
|
4100
5638
|
*
|
|
4101
|
-
*
|
|
4102
|
-
*
|
|
4103
|
-
*
|
|
4104
|
-
*
|
|
4105
|
-
* dispatch 内部按 deliveredCommandKeys 去重,重复调用安全。
|
|
5639
|
+
* 窗口本身就是"用户在看哪段":首屏就是最新一页,向上翻页 / 定位之后首条就是用户滚到的位置。
|
|
5640
|
+
* 直接拿 `from_entry_id = 当前窗口第一条 turn_id` 去取,运行结束、重连补数、翻页之后的对账
|
|
5641
|
+
* 是同一条规则,不需要意图、锚点、已落地锚点这些状态;窗口首条已经不在分支上(404 / 409)时
|
|
5642
|
+
* 回落到最新一页。
|
|
4106
5643
|
*/
|
|
4107
|
-
|
|
5644
|
+
refreshAuthoritativeHistory(options = {}) {
|
|
5645
|
+
let entryGone = false;
|
|
5646
|
+
const locateTokenAtStart = this.locateToken;
|
|
5647
|
+
return this.replaceFromHistory({
|
|
5648
|
+
...options,
|
|
5649
|
+
// 锚点在**这次操作真正开始**时采样:排队期间用户可能刚定位过,那时窗口首条才是他要的。
|
|
5650
|
+
fromEntryId: () => this.state.turns[0]?.turn_id,
|
|
5651
|
+
onError: (error) => {
|
|
5652
|
+
entryGone = isEntryGoneError(error);
|
|
5653
|
+
}
|
|
5654
|
+
}).then((located) => {
|
|
5655
|
+
if (located || !entryGone) return located;
|
|
5656
|
+
if (this.locateToken !== locateTokenAtStart) return false;
|
|
5657
|
+
return this.replaceFromHistory(options);
|
|
5658
|
+
});
|
|
5659
|
+
}
|
|
5660
|
+
replaceFromHistory(options = {}) {
|
|
5661
|
+
if (this.disposed) return Promise.resolve(false);
|
|
5662
|
+
const operationEpoch = this.timelineEpoch;
|
|
5663
|
+
const liveRevisionAtStart = options.liveRevisionAtStart ?? this.liveRevision;
|
|
5664
|
+
const modeRevisionAtStart = options.modeRevisionAtStart ?? this.modeRevision;
|
|
5665
|
+
this.invalidateOlderPaging();
|
|
5666
|
+
this.replaceInFlight += 1;
|
|
5667
|
+
const operation = async () => {
|
|
5668
|
+
let previousNextBefore = null;
|
|
5669
|
+
const restoreCursor = () => {
|
|
5670
|
+
if (this.disposed || previousNextBefore === null) return;
|
|
5671
|
+
this.update((state) => ({
|
|
5672
|
+
...state,
|
|
5673
|
+
nextBefore: previousNextBefore,
|
|
5674
|
+
loadingOlder: false
|
|
5675
|
+
}));
|
|
5676
|
+
};
|
|
5677
|
+
try {
|
|
5678
|
+
if (this.disposed || operationEpoch !== this.timelineEpoch) return false;
|
|
5679
|
+
if (options.isStale?.()) return false;
|
|
5680
|
+
this.invalidateOlderPaging();
|
|
5681
|
+
previousNextBefore = this.state.nextBefore;
|
|
5682
|
+
this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
|
|
5683
|
+
const signal = options.signal;
|
|
5684
|
+
const fromEntryId = typeof options.fromEntryId === "function" ? options.fromEntryId() : options.fromEntryId;
|
|
5685
|
+
const page = await this.runtime.fetchTurnsPage(
|
|
5686
|
+
this.sessionId,
|
|
5687
|
+
fromEntryId ? { fromEntryId } : void 0,
|
|
5688
|
+
signal ? { signal } : void 0
|
|
5689
|
+
);
|
|
5690
|
+
if (this.disposed || operationEpoch !== this.timelineEpoch) {
|
|
5691
|
+
restoreCursor();
|
|
5692
|
+
return false;
|
|
5693
|
+
}
|
|
5694
|
+
if (options.isStale?.()) {
|
|
5695
|
+
restoreCursor();
|
|
5696
|
+
return false;
|
|
5697
|
+
}
|
|
5698
|
+
if (!this.joined) this.seedToolCallDeliveryBaseline(page.turns);
|
|
5699
|
+
this.update((state) => {
|
|
5700
|
+
const next = withTurns(
|
|
5701
|
+
state,
|
|
5702
|
+
replaceWindow(state.turns, page.turns, liveRevisionAtStart, {
|
|
5703
|
+
current: this.liveRevision,
|
|
5704
|
+
byTurnId: this.liveRevisions
|
|
5705
|
+
})
|
|
5706
|
+
);
|
|
5707
|
+
return {
|
|
5708
|
+
...next,
|
|
5709
|
+
mode: this.modeRevision === modeRevisionAtStart ? page.currentMode : state.mode,
|
|
5710
|
+
nextBefore: page.nextBefore,
|
|
5711
|
+
loadingOlder: false
|
|
5712
|
+
};
|
|
5713
|
+
});
|
|
5714
|
+
this.pruneOptimisticLiveRevisions();
|
|
5715
|
+
this.projectionBuilder._seedTurns(this.state.turns);
|
|
5716
|
+
return true;
|
|
5717
|
+
} catch (error) {
|
|
5718
|
+
options.onError?.(error);
|
|
5719
|
+
if (!this.disposed && operationEpoch === this.timelineEpoch) {
|
|
5720
|
+
this.update((state) => ({
|
|
5721
|
+
...state,
|
|
5722
|
+
nextBefore: previousNextBefore,
|
|
5723
|
+
loadingOlder: false
|
|
5724
|
+
}));
|
|
5725
|
+
}
|
|
5726
|
+
if (options.suppressErrors === false) throw error;
|
|
5727
|
+
return false;
|
|
5728
|
+
} finally {
|
|
5729
|
+
this.replaceInFlight -= 1;
|
|
5730
|
+
}
|
|
5731
|
+
};
|
|
5732
|
+
const run = this.replaceQueue.then(operation);
|
|
5733
|
+
this.replaceQueue = run.catch(() => void 0);
|
|
5734
|
+
return run;
|
|
5735
|
+
}
|
|
5736
|
+
invalidateOlderPaging() {
|
|
5737
|
+
this.pagingGeneration += 1;
|
|
5738
|
+
this.olderPending = null;
|
|
5739
|
+
}
|
|
5740
|
+
beginCommandRecovery() {
|
|
5741
|
+
this.commandRecoveryGeneration += 1;
|
|
5742
|
+
this.commandRestoreRetryAttempt = 0;
|
|
5743
|
+
this.bufferedCommandDeliveries ??= [];
|
|
5744
|
+
if (this.commandRestoreRetryHandle !== null) {
|
|
5745
|
+
globalThis.clearTimeout(this.commandRestoreRetryHandle);
|
|
5746
|
+
this.commandRestoreRetryHandle = null;
|
|
5747
|
+
}
|
|
5748
|
+
return this.commandRecoveryGeneration;
|
|
5749
|
+
}
|
|
5750
|
+
scheduleHistoricalCommandRestore(generation) {
|
|
5751
|
+
const restore = async () => {
|
|
5752
|
+
if (this.disposed || generation !== this.commandRecoveryGeneration) return;
|
|
5753
|
+
try {
|
|
5754
|
+
const commands = await this.runtime.fetchCommands(this.sessionId);
|
|
5755
|
+
if (this.disposed) return;
|
|
5756
|
+
for (const command of commands) {
|
|
5757
|
+
if (this.disposed) return;
|
|
5758
|
+
this.deliverCommandNow({
|
|
5759
|
+
deliveryKey: commandDeliveryKey(command.deliveryId),
|
|
5760
|
+
action: command.action,
|
|
5761
|
+
payload: command.payload,
|
|
5762
|
+
toolCallId: command.toolCallId
|
|
5763
|
+
});
|
|
5764
|
+
}
|
|
5765
|
+
if (generation !== this.commandRecoveryGeneration) return;
|
|
5766
|
+
this.commandRestoreRetryAttempt = 0;
|
|
5767
|
+
this.flushBufferedCommands(generation);
|
|
5768
|
+
} catch {
|
|
5769
|
+
if (!this.disposed && generation === this.commandRecoveryGeneration && this.commandRestoreRetryHandle === null) {
|
|
5770
|
+
const retryDelay = Math.min(
|
|
5771
|
+
COMMAND_RESTORE_RETRY_BASE_MS * 2 ** this.commandRestoreRetryAttempt,
|
|
5772
|
+
COMMAND_RESTORE_RETRY_MAX_MS
|
|
5773
|
+
);
|
|
5774
|
+
this.commandRestoreRetryAttempt += 1;
|
|
5775
|
+
this.commandRestoreRetryHandle = globalThis.setTimeout(() => {
|
|
5776
|
+
this.commandRestoreRetryHandle = null;
|
|
5777
|
+
if (!this.disposed && generation === this.commandRecoveryGeneration) {
|
|
5778
|
+
this.scheduleHistoricalCommandRestore(generation);
|
|
5779
|
+
}
|
|
5780
|
+
}, retryDelay);
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
};
|
|
5784
|
+
const run = this.commandRestoreQueue.then(restore);
|
|
5785
|
+
this.commandRestoreQueue = run.catch(() => void 0);
|
|
5786
|
+
}
|
|
5787
|
+
flushBufferedCommands(generation) {
|
|
5788
|
+
const pending = [...this.bufferedCommandDeliveries ?? []];
|
|
5789
|
+
this.bufferedCommandDeliveries = [];
|
|
5790
|
+
let index = 0;
|
|
5791
|
+
while (index < pending.length) {
|
|
5792
|
+
if (this.disposed) return;
|
|
5793
|
+
if (generation !== this.commandRecoveryGeneration) {
|
|
5794
|
+
const currentGenerationBuffer = this.bufferedCommandDeliveries ?? [];
|
|
5795
|
+
this.bufferedCommandDeliveries = [
|
|
5796
|
+
...pending.slice(index),
|
|
5797
|
+
...currentGenerationBuffer
|
|
5798
|
+
];
|
|
5799
|
+
return;
|
|
5800
|
+
}
|
|
5801
|
+
this.deliverCommandNow(pending[index]);
|
|
5802
|
+
index += 1;
|
|
5803
|
+
const reentrantBuffer = this.bufferedCommandDeliveries;
|
|
5804
|
+
if (index === pending.length && reentrantBuffer && reentrantBuffer.length > 0) {
|
|
5805
|
+
pending.push(...reentrantBuffer);
|
|
5806
|
+
this.bufferedCommandDeliveries = [];
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
if (!this.disposed && generation === this.commandRecoveryGeneration) {
|
|
5810
|
+
this.bufferedCommandDeliveries = null;
|
|
5811
|
+
}
|
|
5812
|
+
}
|
|
5813
|
+
/** 首次订阅前的 REST 历史建立语义事件基线,随后同一工具的订阅回放不再投递。 */
|
|
5814
|
+
seedToolCallDeliveryBaseline(turns) {
|
|
4108
5815
|
for (const turn of turns) {
|
|
4109
|
-
for (const
|
|
4110
|
-
|
|
4111
|
-
|
|
5816
|
+
for (const toolCall of turn.tool_calls) {
|
|
5817
|
+
this.seenToolCallIds.add(toolCall.id);
|
|
5818
|
+
if (toolCall.status === "done" || toolCall.status === "error" || toolCall.status === "cancelled") {
|
|
5819
|
+
this.finishedToolCallIds.add(toolCall.id);
|
|
4112
5820
|
}
|
|
4113
5821
|
}
|
|
4114
5822
|
}
|
|
4115
5823
|
}
|
|
4116
|
-
ensureJoined() {
|
|
5824
|
+
ensureJoined(liveRevisionBoundary = this.liveRevision) {
|
|
4117
5825
|
if (this.joined) return Promise.resolve();
|
|
4118
5826
|
if (this.joinPending) return this.joinPending;
|
|
4119
5827
|
const join = this.runtime.emitWithAck(
|
|
@@ -4121,8 +5829,7 @@ var AgentSession = class _AgentSession {
|
|
|
4121
5829
|
{
|
|
4122
5830
|
session_id: this.sessionId,
|
|
4123
5831
|
event_format: "raw",
|
|
4124
|
-
resume: true
|
|
4125
|
-
stream_tokens: this.runtime.streamTokens
|
|
5832
|
+
resume: true
|
|
4126
5833
|
},
|
|
4127
5834
|
5e3
|
|
4128
5835
|
).then((response) => {
|
|
@@ -4132,7 +5839,7 @@ var AgentSession = class _AgentSession {
|
|
|
4132
5839
|
);
|
|
4133
5840
|
}
|
|
4134
5841
|
this.joined = true;
|
|
4135
|
-
this._applyRunningState(Boolean(response?.is_running));
|
|
5842
|
+
this._applyRunningState(Boolean(response?.is_running), liveRevisionBoundary);
|
|
4136
5843
|
}).finally(() => {
|
|
4137
5844
|
this.joinPending = null;
|
|
4138
5845
|
});
|
|
@@ -4140,6 +5847,7 @@ var AgentSession = class _AgentSession {
|
|
|
4140
5847
|
return join;
|
|
4141
5848
|
}
|
|
4142
5849
|
async ensureIdleBeforeSend() {
|
|
5850
|
+
if (this.state.isStreaming) return;
|
|
4143
5851
|
if (Date.now() - this.recentChatEndAt > 3e3) return;
|
|
4144
5852
|
const deadline = Date.now() + 3e3;
|
|
4145
5853
|
while (Date.now() < deadline) {
|
|
@@ -4174,6 +5882,7 @@ var SessionHub = class {
|
|
|
4174
5882
|
connectionListeners = /* @__PURE__ */ new Set();
|
|
4175
5883
|
bound = false;
|
|
4176
5884
|
hasConnected = false;
|
|
5885
|
+
authBusyReconnect = new AuthBusyReconnect();
|
|
4177
5886
|
constructor(client) {
|
|
4178
5887
|
this.client = client;
|
|
4179
5888
|
}
|
|
@@ -4195,16 +5904,28 @@ var SessionHub = class {
|
|
|
4195
5904
|
* 只有最后一份归还才退订房间——否则 StrictMode 双跑、路由抖动这类"连两次
|
|
4196
5905
|
* 只释放一次"的场景会把仍在用的会话退订掉,之后所有 turn/chat 事件静默丢失。
|
|
4197
5906
|
*/
|
|
4198
|
-
async connect(sessionId) {
|
|
5907
|
+
async connect(sessionId, options = {}) {
|
|
4199
5908
|
const existing = this.sessions.get(sessionId);
|
|
4200
5909
|
if (existing && !existing.isDisposed) {
|
|
4201
5910
|
this.hold(sessionId);
|
|
5911
|
+
try {
|
|
5912
|
+
options.setup?.(existing);
|
|
5913
|
+
} catch (error) {
|
|
5914
|
+
existing.dispose();
|
|
5915
|
+
throw error;
|
|
5916
|
+
}
|
|
4202
5917
|
return this.bootstrapPending.get(sessionId) ?? existing;
|
|
4203
5918
|
}
|
|
4204
5919
|
this.ensureBound();
|
|
4205
5920
|
const session = new AgentSession(sessionId, this.runtimeFor(), this.connection);
|
|
4206
5921
|
this.sessions.set(sessionId, session);
|
|
4207
5922
|
this.hold(sessionId);
|
|
5923
|
+
try {
|
|
5924
|
+
options.setup?.(session);
|
|
5925
|
+
} catch (error) {
|
|
5926
|
+
session.dispose();
|
|
5927
|
+
throw error;
|
|
5928
|
+
}
|
|
4208
5929
|
this.ensureConnected();
|
|
4209
5930
|
const bootstrap = session._bootstrap().then(() => session);
|
|
4210
5931
|
this.bootstrapPending.set(sessionId, bootstrap);
|
|
@@ -4228,6 +5949,7 @@ var SessionHub = class {
|
|
|
4228
5949
|
/** 手动重连(例如登录态变化后)。 */
|
|
4229
5950
|
reconnect() {
|
|
4230
5951
|
this.ensureBound();
|
|
5952
|
+
this.authBusyReconnect.cancel();
|
|
4231
5953
|
const socket = this.client.socket();
|
|
4232
5954
|
socket.disconnect();
|
|
4233
5955
|
this.setConnection("connecting");
|
|
@@ -4235,11 +5957,13 @@ var SessionHub = class {
|
|
|
4235
5957
|
}
|
|
4236
5958
|
/** 断开连接(不清理 session 实例)。 */
|
|
4237
5959
|
disconnect() {
|
|
5960
|
+
this.authBusyReconnect.cancel();
|
|
4238
5961
|
this.client.socket().disconnect();
|
|
4239
5962
|
this.setConnection("disconnected");
|
|
4240
5963
|
}
|
|
4241
5964
|
/** 清理当前身份下缓存的全部会话,避免切换登录身份后复用旧快照。 */
|
|
4242
5965
|
disposeAll() {
|
|
5966
|
+
this.authBusyReconnect.cancel();
|
|
4243
5967
|
this.bootstrapPending.clear();
|
|
4244
5968
|
for (const session of [...this.sessions.values()]) {
|
|
4245
5969
|
this.forceRelease(session);
|
|
@@ -4251,6 +5975,7 @@ var SessionHub = class {
|
|
|
4251
5975
|
* turn:* / chat:* 事件都会静默丢弃。
|
|
4252
5976
|
*/
|
|
4253
5977
|
_handleSocketReplaced() {
|
|
5978
|
+
this.authBusyReconnect.cancel();
|
|
4254
5979
|
this.bound = false;
|
|
4255
5980
|
this.setConnection("disconnected");
|
|
4256
5981
|
if (this.sessions.size === 0) return;
|
|
@@ -4259,7 +5984,6 @@ var SessionHub = class {
|
|
|
4259
5984
|
}
|
|
4260
5985
|
runtimeFor() {
|
|
4261
5986
|
return {
|
|
4262
|
-
streamTokens: this.client.options?.streamTokens !== false,
|
|
4263
5987
|
emitEvent: (event, data) => {
|
|
4264
5988
|
;
|
|
4265
5989
|
this.client.socket().emit(event, data);
|
|
@@ -4275,7 +5999,9 @@ var SessionHub = class {
|
|
|
4275
5999
|
});
|
|
4276
6000
|
}),
|
|
4277
6001
|
ensureConnected: () => this.ensureConnected(),
|
|
4278
|
-
|
|
6002
|
+
fetchTurnsPage: (sessionId, query, init) => this.client.sessions.getSessionTurnsPage(sessionId, query, init),
|
|
6003
|
+
fetchTurnIndex: (sessionId) => this.client.sessions.getSessionTurnIndex(sessionId),
|
|
6004
|
+
fetchCommands: (sessionId) => this.client.sessions.getSessionTurnCommands(sessionId),
|
|
4279
6005
|
fetchSessionInfo: (sessionId) => this.client.sessions.getSession(sessionId),
|
|
4280
6006
|
updateReplay: (sessionId, payload) => this.client.sessions.updateReplaySession(sessionId, payload).then((result) => result.replay_state ?? null),
|
|
4281
6007
|
listDir: (sessionId, dirPath) => this.client.sessions.listDir(sessionId, dirPath),
|
|
@@ -4303,6 +6029,9 @@ var SessionHub = class {
|
|
|
4303
6029
|
if (this.sessions.get(session.sessionId) === session) {
|
|
4304
6030
|
this.sessions.delete(session.sessionId);
|
|
4305
6031
|
}
|
|
6032
|
+
if (this.sessions.size === 0) {
|
|
6033
|
+
this.authBusyReconnect.cancel();
|
|
6034
|
+
}
|
|
4306
6035
|
;
|
|
4307
6036
|
this.client.socket().emit("session:unsubscribe", { session_id: session.sessionId });
|
|
4308
6037
|
return true;
|
|
@@ -4311,10 +6040,13 @@ var SessionHub = class {
|
|
|
4311
6040
|
forceRelease(session) {
|
|
4312
6041
|
this.holdCounts.delete(session.sessionId);
|
|
4313
6042
|
session.dispose();
|
|
6043
|
+
if (this.sessions.size === 0) {
|
|
6044
|
+
this.authBusyReconnect.cancel();
|
|
6045
|
+
}
|
|
4314
6046
|
}
|
|
4315
6047
|
ensureConnected() {
|
|
4316
6048
|
const socket = this.client.socket();
|
|
4317
|
-
if (socket.connected || socket.active) return;
|
|
6049
|
+
if (socket.connected || socket.active || this.authBusyReconnect.pending) return;
|
|
4318
6050
|
this.setConnection("connecting");
|
|
4319
6051
|
socket.connect();
|
|
4320
6052
|
}
|
|
@@ -4346,17 +6078,35 @@ var SessionHub = class {
|
|
|
4346
6078
|
this.bound = true;
|
|
4347
6079
|
const socket = this.client.socket();
|
|
4348
6080
|
socket.on("connect", () => {
|
|
6081
|
+
this.authBusyReconnect.cancel();
|
|
4349
6082
|
const isReconnect = this.hasConnected;
|
|
4350
6083
|
this.hasConnected = true;
|
|
4351
6084
|
this.setConnection("connected");
|
|
4352
6085
|
if (isReconnect) {
|
|
4353
6086
|
for (const session of this.sessions.values()) {
|
|
4354
6087
|
void session._rejoinAfterReconnect();
|
|
6088
|
+
void session.refreshQueue();
|
|
4355
6089
|
}
|
|
4356
6090
|
}
|
|
4357
6091
|
});
|
|
4358
|
-
socket.on("connect_error", () =>
|
|
4359
|
-
|
|
6092
|
+
socket.on("connect_error", (error) => {
|
|
6093
|
+
this.setConnection("disconnected");
|
|
6094
|
+
if (this.sessions.size === 0) {
|
|
6095
|
+
this.authBusyReconnect.cancel();
|
|
6096
|
+
return;
|
|
6097
|
+
}
|
|
6098
|
+
if (this.authBusyReconnect.schedule(error, () => {
|
|
6099
|
+
const current = this.client.socket();
|
|
6100
|
+
if (current !== socket || current.connected || current.active) return;
|
|
6101
|
+
this.setConnection("connecting");
|
|
6102
|
+
current.connect();
|
|
6103
|
+
})) return;
|
|
6104
|
+
this.authBusyReconnect.cancel();
|
|
6105
|
+
});
|
|
6106
|
+
socket.on("disconnect", () => {
|
|
6107
|
+
this.authBusyReconnect.cancel();
|
|
6108
|
+
this.setConnection("disconnected");
|
|
6109
|
+
});
|
|
4360
6110
|
socket.io.on("reconnect_attempt", () => this.setConnection("reconnecting"));
|
|
4361
6111
|
socket.on("turn:start", (data) => {
|
|
4362
6112
|
for (const session of this.route(data.session_id)) session._handleTurnStart(data);
|
|
@@ -4367,9 +6117,25 @@ var SessionHub = class {
|
|
|
4367
6117
|
socket.on("turn:end", (data) => {
|
|
4368
6118
|
for (const session of this.route(data.session_id)) session._handleTurnEnd(data);
|
|
4369
6119
|
});
|
|
4370
|
-
socket.on(
|
|
4371
|
-
|
|
4372
|
-
|
|
6120
|
+
socket.on(
|
|
6121
|
+
"turn:timing",
|
|
6122
|
+
(data) => {
|
|
6123
|
+
for (const session of this.route(data.session_id)) {
|
|
6124
|
+
session._handleTurnEvents([
|
|
6125
|
+
{ type: "llm:first_token:delivered", payload: data }
|
|
6126
|
+
]);
|
|
6127
|
+
}
|
|
6128
|
+
}
|
|
6129
|
+
);
|
|
6130
|
+
socket.on(
|
|
6131
|
+
"turn:events",
|
|
6132
|
+
(data, acknowledge) => {
|
|
6133
|
+
acknowledgeTurnEvents(acknowledge);
|
|
6134
|
+
for (const session of this.route(data.session_id)) {
|
|
6135
|
+
session._handleTurnEvents(data.events ?? [], data.replayed === true);
|
|
6136
|
+
}
|
|
6137
|
+
}
|
|
6138
|
+
);
|
|
4373
6139
|
socket.on("chat:start", (data) => {
|
|
4374
6140
|
for (const session of this.route(data?.session_id)) session._handleChatStart();
|
|
4375
6141
|
});
|
|
@@ -4381,6 +6147,9 @@ var SessionHub = class {
|
|
|
4381
6147
|
socket.on("system:error", (data) => {
|
|
4382
6148
|
for (const session of this.route(data.session_id)) session._handleSystemError(data.message);
|
|
4383
6149
|
});
|
|
6150
|
+
socket.on("chat:queue:updated", (data) => {
|
|
6151
|
+
for (const session of this.route(data?.session_id)) session._handleQueueUpdated(data);
|
|
6152
|
+
});
|
|
4384
6153
|
socket.on(
|
|
4385
6154
|
"system:notification",
|
|
4386
6155
|
(data) => {
|
|
@@ -4497,21 +6266,14 @@ function resolveAuthToken(options) {
|
|
|
4497
6266
|
return token ? token : null;
|
|
4498
6267
|
}
|
|
4499
6268
|
|
|
4500
|
-
// src/version.ts
|
|
4501
|
-
var SDK_NAME = "agent-client";
|
|
4502
|
-
var SDK_VERSION = true ? "2610.0.0-beta.8" : "1.1.1";
|
|
4503
|
-
|
|
4504
6269
|
// src/socket.ts
|
|
4505
|
-
function withSdkIdentity(auth) {
|
|
4506
|
-
return { ...auth ?? {}, sdk: SDK_NAME, sdk_version: SDK_VERSION };
|
|
4507
|
-
}
|
|
4508
6270
|
function createSocket(options) {
|
|
4509
6271
|
const token = resolveAuthToken(options);
|
|
4510
6272
|
return io(options.baseUrl, {
|
|
4511
6273
|
path: options.path ?? "/socket.io",
|
|
4512
6274
|
withCredentials: true,
|
|
4513
6275
|
query: token ? { token } : void 0,
|
|
4514
|
-
auth: typeof options.token === "function" ? (cb) => cb(
|
|
6276
|
+
auth: typeof options.token === "function" ? (cb) => cb(buildSocketAuth(options) ?? {}) : buildSocketAuth(options) ?? {},
|
|
4515
6277
|
autoConnect: false
|
|
4516
6278
|
});
|
|
4517
6279
|
}
|
|
@@ -4527,8 +6289,17 @@ var BladeClient = class {
|
|
|
4527
6289
|
options;
|
|
4528
6290
|
auth;
|
|
4529
6291
|
headless;
|
|
6292
|
+
computers;
|
|
4530
6293
|
models;
|
|
4531
6294
|
sessions;
|
|
6295
|
+
/** 按用户读取的插件目录与图标;没有会话时也能用。 */
|
|
6296
|
+
plugins;
|
|
6297
|
+
chatGroups;
|
|
6298
|
+
/**
|
|
6299
|
+
* 全站访问统计:匿名读写,不带 bearer 也不触发登录刷新。
|
|
6300
|
+
* 只有内置 Web 的同源页面使用,第三方宿主页面不会被自动统计。
|
|
6301
|
+
*/
|
|
6302
|
+
siteVisits;
|
|
4532
6303
|
/** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
|
|
4533
6304
|
hub;
|
|
4534
6305
|
constructor(options) {
|
|
@@ -4538,8 +6309,12 @@ var BladeClient = class {
|
|
|
4538
6309
|
};
|
|
4539
6310
|
this.auth = new AuthResource(this);
|
|
4540
6311
|
this.headless = new HeadlessResource(this);
|
|
6312
|
+
this.computers = new ComputersResource(this);
|
|
4541
6313
|
this.models = new ModelsResource(this);
|
|
4542
6314
|
this.sessions = new SessionsResource(this);
|
|
6315
|
+
this.plugins = new PluginsResource(this);
|
|
6316
|
+
this.chatGroups = new ChatGroupsResource(this);
|
|
6317
|
+
this.siteVisits = new SiteVisitsResource(this);
|
|
4543
6318
|
this.hub = new SessionHub(this);
|
|
4544
6319
|
if (this.options.token === void 0) {
|
|
4545
6320
|
this.runtimeToken = readStoredToken(this.options.baseUrl);
|
|
@@ -4698,7 +6473,7 @@ var BladeClient = class {
|
|
|
4698
6473
|
signal: init.signal
|
|
4699
6474
|
});
|
|
4700
6475
|
if (response.status === 401 && !isRetry && this.shouldRefreshFor401(url)) {
|
|
4701
|
-
const refreshed = await this.
|
|
6476
|
+
const refreshed = await this.tryRefreshUnlessAborted(init.signal);
|
|
4702
6477
|
if (refreshed) {
|
|
4703
6478
|
return this.fetch(method, path, init, true);
|
|
4704
6479
|
}
|
|
@@ -4710,7 +6485,7 @@ var BladeClient = class {
|
|
|
4710
6485
|
return response;
|
|
4711
6486
|
}
|
|
4712
6487
|
buildAuthedUrl(path) {
|
|
4713
|
-
const url =
|
|
6488
|
+
const url = buildBackendUrl(this.options.baseUrl, path);
|
|
4714
6489
|
const token = this.resolveTokenForUrl(url);
|
|
4715
6490
|
if (token) {
|
|
4716
6491
|
url.searchParams.set("token", token);
|
|
@@ -4729,14 +6504,11 @@ var BladeClient = class {
|
|
|
4729
6504
|
return result;
|
|
4730
6505
|
}
|
|
4731
6506
|
buildUrl(path) {
|
|
4732
|
-
return
|
|
6507
|
+
return buildBackendUrl(this.options.baseUrl, path);
|
|
4733
6508
|
}
|
|
4734
6509
|
baseUrlWithTrailingSlash() {
|
|
4735
6510
|
return this.options.baseUrl.endsWith("/") ? this.options.baseUrl : `${this.options.baseUrl}/`;
|
|
4736
6511
|
}
|
|
4737
|
-
toBaseRelativePath(path) {
|
|
4738
|
-
return path.startsWith("/") ? path.slice(1) : path;
|
|
4739
|
-
}
|
|
4740
6512
|
isSameBackendUrl(url) {
|
|
4741
6513
|
const base = new URL(this.baseUrlWithTrailingSlash());
|
|
4742
6514
|
if (url.origin !== base.origin) {
|
|
@@ -4755,6 +6527,27 @@ var BladeClient = class {
|
|
|
4755
6527
|
hasExplicitBearerToken() {
|
|
4756
6528
|
return Boolean(this.resolveRestToken());
|
|
4757
6529
|
}
|
|
6530
|
+
/**
|
|
6531
|
+
* 等一次令牌刷新,但调用方中止请求时不再等下去。
|
|
6532
|
+
*
|
|
6533
|
+
* 刷新本身是共享的(`refreshPromise`,别的请求也在等它),这里只是让这次已被取消的
|
|
6534
|
+
* 请求尽早结束——否则它会一直挂在调用方(定位)的串行队列里,用户改选的新目标要等到
|
|
6535
|
+
* 刷新结束才轮得到。
|
|
6536
|
+
*/
|
|
6537
|
+
async tryRefreshUnlessAborted(signal) {
|
|
6538
|
+
if (!signal) return this.tryRefresh();
|
|
6539
|
+
if (signal.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
6540
|
+
let onAbort;
|
|
6541
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
6542
|
+
onAbort = () => reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
6543
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
6544
|
+
});
|
|
6545
|
+
try {
|
|
6546
|
+
return await Promise.race([this.tryRefresh(), aborted]);
|
|
6547
|
+
} finally {
|
|
6548
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
6549
|
+
}
|
|
6550
|
+
}
|
|
4758
6551
|
async tryRefresh() {
|
|
4759
6552
|
if (this.refreshPromise) {
|
|
4760
6553
|
return this.refreshPromise;
|
|
@@ -4892,6 +6685,126 @@ function parseXhrHeaders(rawHeaders) {
|
|
|
4892
6685
|
return headers;
|
|
4893
6686
|
}
|
|
4894
6687
|
|
|
6688
|
+
// src/polling-backoff.ts
|
|
6689
|
+
var MAX_BACKOFF_MS = 6e4;
|
|
6690
|
+
var MAX_TIMER_MS = 2147483647;
|
|
6691
|
+
var PollingBackoff = class {
|
|
6692
|
+
constructor(intervalMs) {
|
|
6693
|
+
this.intervalMs = intervalMs;
|
|
6694
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0 || intervalMs > MAX_BACKOFF_MS) {
|
|
6695
|
+
throw new RangeError("Polling interval must be between 0 and 60000 ms");
|
|
6696
|
+
}
|
|
6697
|
+
}
|
|
6698
|
+
intervalMs;
|
|
6699
|
+
failures = 0;
|
|
6700
|
+
nextAt = 0;
|
|
6701
|
+
reset() {
|
|
6702
|
+
this.failures = 0;
|
|
6703
|
+
this.nextAt = 0;
|
|
6704
|
+
}
|
|
6705
|
+
failed(error) {
|
|
6706
|
+
if (error instanceof Error && error.name === "AbortError" || error instanceof BladeApiError && error.status < 500 && error.status !== 408 && error.status !== 429) {
|
|
6707
|
+
this.nextAt = false;
|
|
6708
|
+
return;
|
|
6709
|
+
}
|
|
6710
|
+
this.failures = Math.min(this.failures + 1, 10);
|
|
6711
|
+
const ceiling = Math.min(MAX_BACKOFF_MS, this.intervalMs * 2 ** this.failures);
|
|
6712
|
+
const backoff = Math.max(this.intervalMs, ceiling * (0.5 + Math.random() * 0.5));
|
|
6713
|
+
const header = error instanceof BladeApiError ? error.response.headers.get("Retry-After")?.trim() : void 0;
|
|
6714
|
+
const retryAfter = header ? /^\d+$/.test(header) ? Number(header) * 1e3 : Date.parse(header) - Date.now() : 0;
|
|
6715
|
+
const delay = Math.max(backoff, Number.isNaN(retryAfter) ? 0 : retryAfter);
|
|
6716
|
+
this.nextAt = delay > MAX_TIMER_MS ? false : Date.now() + delay;
|
|
6717
|
+
}
|
|
6718
|
+
/** Zero means ready, false means stop, otherwise wait at least this many ms. */
|
|
6719
|
+
waitMs() {
|
|
6720
|
+
return this.nextAt === false ? false : Math.max(0, this.nextAt - Date.now());
|
|
6721
|
+
}
|
|
6722
|
+
};
|
|
6723
|
+
|
|
6724
|
+
// src/platform-endpoints.ts
|
|
6725
|
+
var PLATFORM_SERVICE_NAMES = [
|
|
6726
|
+
"os",
|
|
6727
|
+
"hub",
|
|
6728
|
+
"agent",
|
|
6729
|
+
"oauth",
|
|
6730
|
+
"llmGateway",
|
|
6731
|
+
"gitea",
|
|
6732
|
+
"tile"
|
|
6733
|
+
];
|
|
6734
|
+
var EMPTY_PLATFORM_ENDPOINTS = Object.freeze({
|
|
6735
|
+
services: Object.freeze({})
|
|
6736
|
+
});
|
|
6737
|
+
function defaultDocumentBaseUrl() {
|
|
6738
|
+
if (typeof document === "undefined") return void 0;
|
|
6739
|
+
if (document.querySelector("base[href]")) return document.baseURI;
|
|
6740
|
+
if (typeof location !== "undefined" && location.origin) return `${location.origin}/`;
|
|
6741
|
+
return document.baseURI;
|
|
6742
|
+
}
|
|
6743
|
+
function normalizeServiceUrl(value) {
|
|
6744
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
6745
|
+
if (value.includes("?") || value.includes("#")) return null;
|
|
6746
|
+
try {
|
|
6747
|
+
const url = new URL(value.trim());
|
|
6748
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
6749
|
+
if (url.username || url.password || url.search || url.hash) return null;
|
|
6750
|
+
return url.toString().replace(/\/$/, "");
|
|
6751
|
+
} catch {
|
|
6752
|
+
return null;
|
|
6753
|
+
}
|
|
6754
|
+
}
|
|
6755
|
+
function parsePlatformEndpoints(value) {
|
|
6756
|
+
if (!isRecord(value) || !isRecord(value.services)) return null;
|
|
6757
|
+
const services = {};
|
|
6758
|
+
for (const name of PLATFORM_SERVICE_NAMES) {
|
|
6759
|
+
if (!(name in value.services)) continue;
|
|
6760
|
+
const url = normalizeServiceUrl(value.services[name]);
|
|
6761
|
+
if (!url) return null;
|
|
6762
|
+
if (name === "agent" && new URL(url).pathname !== "/") return null;
|
|
6763
|
+
services[name] = url;
|
|
6764
|
+
}
|
|
6765
|
+
return { services };
|
|
6766
|
+
}
|
|
6767
|
+
async function loadPlatformEndpoints(options = {}) {
|
|
6768
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
6769
|
+
const baseUrl = options.baseUrl ?? defaultDocumentBaseUrl();
|
|
6770
|
+
if (!fetchImpl || !baseUrl) return EMPTY_PLATFORM_ENDPOINTS;
|
|
6771
|
+
const controller = new AbortController();
|
|
6772
|
+
const timeout = globalThis.setTimeout(() => controller.abort(), options.timeoutMs ?? 3e3);
|
|
6773
|
+
try {
|
|
6774
|
+
const response = await fetchImpl(new URL("config.json", baseUrl), {
|
|
6775
|
+
cache: "no-store",
|
|
6776
|
+
credentials: "same-origin",
|
|
6777
|
+
headers: { Accept: "application/json" },
|
|
6778
|
+
signal: controller.signal
|
|
6779
|
+
});
|
|
6780
|
+
if (!response.ok) return EMPTY_PLATFORM_ENDPOINTS;
|
|
6781
|
+
return parsePlatformEndpoints(await response.json()) ?? EMPTY_PLATFORM_ENDPOINTS;
|
|
6782
|
+
} catch {
|
|
6783
|
+
return EMPTY_PLATFORM_ENDPOINTS;
|
|
6784
|
+
} finally {
|
|
6785
|
+
globalThis.clearTimeout(timeout);
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
function resolveServiceUrl(endpoints, name, path = "") {
|
|
6789
|
+
const baseUrl = endpoints.services[name];
|
|
6790
|
+
if (!baseUrl) return null;
|
|
6791
|
+
if (!path) return baseUrl;
|
|
6792
|
+
const pathReference = path.trimStart();
|
|
6793
|
+
if (path.includes("\\") || pathReference.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(pathReference)) {
|
|
6794
|
+
return null;
|
|
6795
|
+
}
|
|
6796
|
+
const base = new URL(`${baseUrl}/`);
|
|
6797
|
+
const target = new URL(path.replace(/^\/+/, ""), base);
|
|
6798
|
+
if (target.origin !== base.origin || !target.pathname.startsWith(base.pathname)) {
|
|
6799
|
+
return null;
|
|
6800
|
+
}
|
|
6801
|
+
return target.toString();
|
|
6802
|
+
}
|
|
6803
|
+
|
|
6804
|
+
// src/version.ts
|
|
6805
|
+
var SDK_NAME = "agent-client";
|
|
6806
|
+
var SDK_VERSION = true ? "2610.0.0-beta.80" : "1.1.1";
|
|
6807
|
+
|
|
4895
6808
|
// src/commands/protocol.ts
|
|
4896
6809
|
function isCommandEnvelope(value) {
|
|
4897
6810
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -4992,12 +6905,19 @@ var Task = type2({
|
|
|
4992
6905
|
});
|
|
4993
6906
|
export {
|
|
4994
6907
|
AgentSession,
|
|
6908
|
+
AuthBusyReconnect,
|
|
4995
6909
|
BladeApiError,
|
|
4996
6910
|
BladeClient,
|
|
6911
|
+
ChatGroupsResource,
|
|
4997
6912
|
ClientProjectionBuilder,
|
|
6913
|
+
ComputersResource,
|
|
4998
6914
|
DEFAULT_REPLAY_SPEED,
|
|
6915
|
+
EMPTY_PLATFORM_ENDPOINTS,
|
|
6916
|
+
EMPTY_SESSION_QUEUE,
|
|
4999
6917
|
LayoutType,
|
|
5000
6918
|
ModelsResource,
|
|
6919
|
+
PluginsResource,
|
|
6920
|
+
PollingBackoff,
|
|
5001
6921
|
SDK_NAME,
|
|
5002
6922
|
SDK_VERSION,
|
|
5003
6923
|
SessionHub,
|
|
@@ -5007,22 +6927,59 @@ export {
|
|
|
5007
6927
|
Task,
|
|
5008
6928
|
TaskStatus,
|
|
5009
6929
|
acceptedPostChatFollowupCompletesLatestRun,
|
|
6930
|
+
acknowledgeTurnEvents,
|
|
6931
|
+
authBusyRetryDelayMs,
|
|
5010
6932
|
buildMessageContent,
|
|
6933
|
+
buildToolUiCardKey,
|
|
6934
|
+
canCancelQueuedMessage,
|
|
6935
|
+
canDeliverQueuedMessage,
|
|
6936
|
+
canEditQueuedMessage,
|
|
6937
|
+
canToggleComputer,
|
|
6938
|
+
chatErrorForDisplay,
|
|
6939
|
+
collectInlineToolUiCards,
|
|
6940
|
+
collectPreviewToolUiCards,
|
|
6941
|
+
computerDaemonVersion,
|
|
6942
|
+
computerOS,
|
|
6943
|
+
computerPlatformLabel,
|
|
6944
|
+
computerState,
|
|
5011
6945
|
connectEmbedded,
|
|
5012
6946
|
contentPreview,
|
|
6947
|
+
contextProjectionData,
|
|
5013
6948
|
createInitialSessionState,
|
|
5014
6949
|
createSocket,
|
|
5015
6950
|
extractTextAttachments,
|
|
6951
|
+
getContextDisplayState,
|
|
6952
|
+
getContextGroupDisplayState,
|
|
5016
6953
|
getFileParts,
|
|
5017
6954
|
getImageParts,
|
|
5018
6955
|
getTextContent,
|
|
6956
|
+
groupAdjacentContextRuns,
|
|
5019
6957
|
groupMessagesByLoop,
|
|
6958
|
+
hasChatRunEvent,
|
|
6959
|
+
inferToolStatus,
|
|
6960
|
+
isAppDevToolUiCard,
|
|
5020
6961
|
isCommandEnvelope,
|
|
6962
|
+
isEntryGoneError,
|
|
5021
6963
|
isHiddenInternalMessage,
|
|
5022
6964
|
isInboundEnvelope,
|
|
6965
|
+
isInternalStatusToolUiCard,
|
|
6966
|
+
isMcpAppData,
|
|
6967
|
+
isSessionQueueSnapshot,
|
|
6968
|
+
isToolUiCard,
|
|
5023
6969
|
latestPostChatFollowup,
|
|
6970
|
+
loadPlatformEndpoints,
|
|
5024
6971
|
normalizeMessageContent,
|
|
6972
|
+
parseQueueAck,
|
|
6973
|
+
prependOlder,
|
|
6974
|
+
queuePauseReasonLabel,
|
|
6975
|
+
queuedMessageStatusLabel,
|
|
6976
|
+
reconcileHistoricalUserTurns,
|
|
5025
6977
|
reconcileOptimisticUserTurns,
|
|
6978
|
+
replaceWindow,
|
|
6979
|
+
resolveServiceUrl,
|
|
6980
|
+
resolveToolUiCardContent,
|
|
6981
|
+
shouldApplySnapshot,
|
|
6982
|
+
sortComputers,
|
|
5026
6983
|
toReplaySnapshot,
|
|
5027
6984
|
transformSlashCommand
|
|
5028
6985
|
};
|