@blade-hq/agent-client 2610.0.0-rc.0 → 2610.0.0
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 +202 -12
- package/dist/auth.d.ts +0 -1
- package/dist/blade-client.d.ts +22 -1
- package/dist/index.d.ts +31 -9
- package/dist/index.js +1851 -263
- package/dist/index.js.map +1 -1
- package/dist/polling-backoff.d.ts +11 -0
- package/dist/resources/chat-groups.d.ts +31 -0
- package/dist/resources/computers.d.ts +54 -2
- package/dist/resources/plugins.d.ts +43 -0
- package/dist/resources/sessions.d.ts +190 -1
- package/dist/resources/site-visits.d.ts +32 -0
- package/dist/rest.d.ts +8 -0
- package/dist/schemas/mcp-app.d.ts +30 -0
- package/dist/schemas/message-utils.d.ts +10 -0
- package/dist/schemas/message.d.ts +3 -0
- package/dist/schemas/projection.d.ts +25 -1
- package/dist/schemas/session.d.ts +7 -0
- package/dist/session/agent-session.d.ts +152 -18
- package/dist/session/definition.d.ts +6 -1
- package/dist/session/errors.d.ts +11 -0
- package/dist/session/events.d.ts +14 -2
- package/dist/session/history-page.d.ts +9 -0
- package/dist/session/hub.d.ts +1 -0
- package/dist/session/queue.d.ts +104 -0
- package/dist/session/state.d.ts +15 -1
- package/dist/shared/auth-busy.d.ts +9 -0
- package/dist/shared/projection/builder.d.ts +10 -0
- package/dist/shared/projection/helpers.d.ts +2 -2
- package/dist/shared/projection/index.d.ts +1 -1
- package/dist/shared/projection/state.d.ts +7 -0
- package/dist/shared/tool-ui-card.d.ts +60 -0
- package/dist/shared/type-guards.d.ts +2 -0
- package/dist/socket.d.ts +2 -0
- package/dist/types/socket-events.d.ts +186 -2
- package/package.json +1 -1
- package/public-api.md +747 -11
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// src/version.ts
|
|
2
|
+
var SDK_NAME = "agent-client";
|
|
3
|
+
var SDK_VERSION = true ? "2610.0.0" : "1.1.1";
|
|
4
|
+
|
|
1
5
|
// src/auth-login.ts
|
|
2
6
|
function isAuthCallbackMessage(value) {
|
|
3
7
|
return typeof value === "object" && value !== null && value.type === "blade-agent:sdk-auth";
|
|
@@ -167,6 +171,10 @@ function decorateAuthError(error) {
|
|
|
167
171
|
}
|
|
168
172
|
return error;
|
|
169
173
|
}
|
|
174
|
+
function buildBackendUrl(baseUrl, path) {
|
|
175
|
+
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
176
|
+
return new URL(path.startsWith("/") ? path.slice(1) : path, base);
|
|
177
|
+
}
|
|
170
178
|
|
|
171
179
|
// src/resources/auth.ts
|
|
172
180
|
var AuthResource = class {
|
|
@@ -214,6 +222,11 @@ var AuthResource = class {
|
|
|
214
222
|
}
|
|
215
223
|
};
|
|
216
224
|
|
|
225
|
+
// src/shared/type-guards.ts
|
|
226
|
+
function isRecord(value) {
|
|
227
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
228
|
+
}
|
|
229
|
+
|
|
217
230
|
// src/resources/headless.ts
|
|
218
231
|
var HeadlessError = class extends Error {
|
|
219
232
|
constructor(message, detail) {
|
|
@@ -394,9 +407,6 @@ function errorMessageFromResult(result) {
|
|
|
394
407
|
}
|
|
395
408
|
return null;
|
|
396
409
|
}
|
|
397
|
-
function isRecord(value) {
|
|
398
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
399
|
-
}
|
|
400
410
|
function ensureSocketConnected(socket, timeoutMs, detail) {
|
|
401
411
|
if (socket.connected) {
|
|
402
412
|
return Promise.resolve();
|
|
@@ -430,13 +440,29 @@ function computerState(computer) {
|
|
|
430
440
|
if (computer.enabled) return "enabled";
|
|
431
441
|
return computer.online ? "idle" : "offline";
|
|
432
442
|
}
|
|
443
|
+
function computerOS(computer) {
|
|
444
|
+
const os = (computer.os ?? "").toLowerCase();
|
|
445
|
+
if (os.startsWith("darwin") || os.startsWith("mac")) return "macos";
|
|
446
|
+
if (os.startsWith("windows")) return "windows";
|
|
447
|
+
if (os.startsWith("linux")) return "linux";
|
|
448
|
+
return "unknown";
|
|
449
|
+
}
|
|
450
|
+
function computerPlatformLabel(computer) {
|
|
451
|
+
const os = (computer.os ?? "").trim();
|
|
452
|
+
const arch = (computer.arch ?? "").trim();
|
|
453
|
+
if (os && arch) return `${os}/${arch}`;
|
|
454
|
+
return os || arch || "";
|
|
455
|
+
}
|
|
456
|
+
function computerDaemonVersion(computer) {
|
|
457
|
+
return (computer.daemon_version ?? "").trim();
|
|
458
|
+
}
|
|
433
459
|
function canToggleComputer(computer) {
|
|
434
460
|
return !computer.is_primary;
|
|
435
461
|
}
|
|
436
462
|
function sortComputers(computers) {
|
|
437
463
|
return [...computers].sort((a, b) => {
|
|
438
|
-
|
|
439
|
-
if (
|
|
464
|
+
const byCreated = (a.created_at ?? "").localeCompare(b.created_at ?? "");
|
|
465
|
+
if (byCreated !== 0) return byCreated;
|
|
440
466
|
return a.label.localeCompare(b.label);
|
|
441
467
|
});
|
|
442
468
|
}
|
|
@@ -445,6 +471,21 @@ var ComputersResource = class {
|
|
|
445
471
|
this.client = client;
|
|
446
472
|
}
|
|
447
473
|
client;
|
|
474
|
+
/**
|
|
475
|
+
* 列出当前账号登记过的全部电脑及其在线状态。
|
|
476
|
+
*
|
|
477
|
+
* 首页还没有会话,也要能看到同一份清单——判定各写一份必然分叉,一边的兜底会把
|
|
478
|
+
* 另一边的错误盖住(#1597)。所以账号事实只从这一个接口读。
|
|
479
|
+
*/
|
|
480
|
+
async listAccount() {
|
|
481
|
+
const data = await this.client.json("GET", "/api/daemon/instances");
|
|
482
|
+
return {
|
|
483
|
+
computers: (data.instances ?? []).map((instance) => ({
|
|
484
|
+
...instance,
|
|
485
|
+
online: Boolean(instance.online)
|
|
486
|
+
}))
|
|
487
|
+
};
|
|
488
|
+
}
|
|
448
489
|
/**
|
|
449
490
|
* 列出当前用户的全部电脑,带上"在不在线"和"这次会话能不能用"。
|
|
450
491
|
* 服务端只查库,电脑离线时同样返回,所以拿得到"它离线了"这个结论。
|
|
@@ -494,6 +535,7 @@ var SessionInfo = type({
|
|
|
494
535
|
"shared?": "boolean",
|
|
495
536
|
"memory_enabled?": "boolean",
|
|
496
537
|
"is_persistent?": "boolean",
|
|
538
|
+
"initiator?": "'ui' | 'sdk' | 'unknown' | null",
|
|
497
539
|
"is_headless?": "boolean",
|
|
498
540
|
"viewer_role?": "'owner' | 'viewer'",
|
|
499
541
|
"template_id?": "string | null",
|
|
@@ -518,6 +560,9 @@ var SessionInfo = type({
|
|
|
518
560
|
"daemon_id?": "string | null",
|
|
519
561
|
"agent_runtime_id?": "string | null",
|
|
520
562
|
"workspace_path?": "string | null",
|
|
563
|
+
"ba_version?": "string | null",
|
|
564
|
+
"sandbox_version?": "string | null",
|
|
565
|
+
"chat_group_id?": "string | null",
|
|
521
566
|
"match?": "unknown"
|
|
522
567
|
});
|
|
523
568
|
|
|
@@ -583,7 +628,7 @@ function upsertToolCall(state, toolCallId, toolName, displayName, args) {
|
|
|
583
628
|
});
|
|
584
629
|
}
|
|
585
630
|
}
|
|
586
|
-
function applyToolResult(state, toolCallId, result, status, durationMs, sourceLoop) {
|
|
631
|
+
function applyToolResult(state, toolCallId, result, status, durationMs, ranInParallel, sourceLoop, startedAt = null) {
|
|
587
632
|
let toolName = "";
|
|
588
633
|
let displayName = "";
|
|
589
634
|
let finalResult = result;
|
|
@@ -595,6 +640,8 @@ function applyToolResult(state, toolCallId, result, status, durationMs, sourceLo
|
|
|
595
640
|
}
|
|
596
641
|
const update = { status, result: finalResult };
|
|
597
642
|
if (durationMs != null) update.duration_ms = durationMs;
|
|
643
|
+
if (ranInParallel != null) update.ran_in_parallel = ranInParallel;
|
|
644
|
+
if (startedAt != null) update.started_at = startedAt;
|
|
598
645
|
state.toolCalls[i] = { ...tc, ...update };
|
|
599
646
|
toolName = tc.tool_name;
|
|
600
647
|
displayName = tc.display_name;
|
|
@@ -776,11 +823,17 @@ function buildCompactionTurn(sequence, compactionId, loopId, status, data) {
|
|
|
776
823
|
summary_preview: summaryPreview,
|
|
777
824
|
summary_full: summaryFull,
|
|
778
825
|
archived_count: archivedCount,
|
|
826
|
+
archived_files: Array.isArray(data.archived_files) ? data.archived_files.filter(
|
|
827
|
+
(item) => typeof item === "object" && item !== null && !Array.isArray(item)
|
|
828
|
+
) : null,
|
|
829
|
+
archived_tool_calls: Array.isArray(data.archived_tool_calls) ? data.archived_tool_calls.filter(
|
|
830
|
+
(item) => typeof item === "object" && item !== null && !Array.isArray(item)
|
|
831
|
+
) : null,
|
|
779
832
|
tokens_before: tokensBefore,
|
|
780
833
|
tokens_after: tokensAfter,
|
|
781
834
|
saved_ratio: savedRatio,
|
|
782
835
|
trigger: asTrigger(data.trigger),
|
|
783
|
-
failure_reason:
|
|
836
|
+
failure_reason: [data.failure_reason, data.fallback_reason, data.reason].find((value) => typeof value === "string" && value.length > 0) ?? null,
|
|
784
837
|
fallback_applied: typeof data.fallback_applied === "boolean" ? data.fallback_applied : null
|
|
785
838
|
};
|
|
786
839
|
}
|
|
@@ -1035,6 +1088,12 @@ function createTurnState(turnId, loopId, opts = {}) {
|
|
|
1035
1088
|
toolCalls: [],
|
|
1036
1089
|
usage: null,
|
|
1037
1090
|
responseDone: false,
|
|
1091
|
+
firstTokenMs: null,
|
|
1092
|
+
streamDurationMs: null,
|
|
1093
|
+
firstTokenDeliveryMs: null,
|
|
1094
|
+
modelStartedAt: null,
|
|
1095
|
+
modelDurationMs: null,
|
|
1096
|
+
modelStartedAtReported: false,
|
|
1038
1097
|
memoryRefs: opts.memoryRefs ?? null,
|
|
1039
1098
|
parentForkToolCallId: null
|
|
1040
1099
|
};
|
|
@@ -1052,8 +1111,13 @@ function snapshot(state, sequence, status) {
|
|
|
1052
1111
|
tool_calls: [...state.toolCalls],
|
|
1053
1112
|
model: state.model,
|
|
1054
1113
|
usage: state.usage ? { ...state.usage } : null,
|
|
1055
|
-
duration_ms:
|
|
1056
|
-
|
|
1114
|
+
duration_ms: state.modelDurationMs,
|
|
1115
|
+
first_token_ms: state.firstTokenMs,
|
|
1116
|
+
stream_duration_ms: state.streamDurationMs,
|
|
1117
|
+
first_token_delivery_ms: state.firstTokenDeliveryMs,
|
|
1118
|
+
started_at: state.modelStartedAt ?? new Date(state.startedAt).toISOString(),
|
|
1119
|
+
// 没有 payload 上报时用的是 loop:turn 的浏览器事件时间,标记出来供瀑布区分。
|
|
1120
|
+
has_reported_timing: state.modelStartedAtReported,
|
|
1057
1121
|
context_window: state.contextWindow,
|
|
1058
1122
|
memory_refs: state.memoryRefs,
|
|
1059
1123
|
parent_fork_tool_call_id: state.parentForkToolCallId
|
|
@@ -1147,15 +1211,12 @@ function getContextGroupDisplayState(contexts) {
|
|
|
1147
1211
|
}
|
|
1148
1212
|
|
|
1149
1213
|
// src/shared/projection/context.ts
|
|
1150
|
-
function isRecord2(value) {
|
|
1151
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1152
|
-
}
|
|
1153
1214
|
function buildContextTurn(entry, sequence) {
|
|
1154
|
-
if (entry.kind !== "context" || !
|
|
1215
|
+
if (entry.kind !== "context" || !isRecord(entry.source) || !isRecord(entry.data)) {
|
|
1155
1216
|
return null;
|
|
1156
1217
|
}
|
|
1157
|
-
const display =
|
|
1158
|
-
const message =
|
|
1218
|
+
const display = isRecord(entry.data.display) ? entry.data.display : {};
|
|
1219
|
+
const message = isRecord(entry.message) ? entry.message : {};
|
|
1159
1220
|
const context = contextProjectionData({
|
|
1160
1221
|
context_kind: typeof entry.source.kind === "string" ? entry.source.kind : null,
|
|
1161
1222
|
context_key: typeof entry.source.key === "string" ? entry.source.key : null,
|
|
@@ -1165,7 +1226,7 @@ function buildContextTurn(entry, sequence) {
|
|
|
1165
1226
|
context_revision: typeof entry.data.revision === "string" ? entry.data.revision : null,
|
|
1166
1227
|
display_title: typeof display.title === "string" ? display.title : null,
|
|
1167
1228
|
display_summary: typeof display.summary === "string" ? display.summary : null,
|
|
1168
|
-
sources: Array.isArray(display.sources) ? display.sources.filter(
|
|
1229
|
+
sources: Array.isArray(display.sources) ? display.sources.filter(isRecord) : []
|
|
1169
1230
|
});
|
|
1170
1231
|
if (!context) return null;
|
|
1171
1232
|
const turnId = typeof entry.id === "string" && entry.id ? entry.id : `context-${sequence}`;
|
|
@@ -1188,9 +1249,16 @@ function buildContextTurn(entry, sequence) {
|
|
|
1188
1249
|
}
|
|
1189
1250
|
|
|
1190
1251
|
// src/shared/projection/builder.ts
|
|
1252
|
+
function backgroundTaskStatus(payload) {
|
|
1253
|
+
const tasks = Array.isArray(payload.tasks) ? payload.tasks : [];
|
|
1254
|
+
return tasks.some((task) => isRecord(task) && (["failed", "cancelled", "lost"].includes(String(task.status)) || typeof task.exit_code === "number" && task.exit_code !== 0)) ? "error" : "done";
|
|
1255
|
+
}
|
|
1256
|
+
function acknowledgeTurnEvents(acknowledge) {
|
|
1257
|
+
acknowledge?.();
|
|
1258
|
+
}
|
|
1191
1259
|
function hasChatRunEvent(events) {
|
|
1192
1260
|
return events.some(
|
|
1193
|
-
(event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
|
|
1261
|
+
(event) => event.type !== "llm:first_token:delivered" && event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
|
|
1194
1262
|
);
|
|
1195
1263
|
}
|
|
1196
1264
|
function acceptedPostChatFollowupCompletesLatestRun(events, updates) {
|
|
@@ -1272,7 +1340,10 @@ var ClientProjectionBuilder = class {
|
|
|
1272
1340
|
activeTurns = /* @__PURE__ */ new Map();
|
|
1273
1341
|
lastTurnIds = /* @__PURE__ */ new Map();
|
|
1274
1342
|
latestTurns = /* @__PURE__ */ new Map();
|
|
1343
|
+
turnsById = /* @__PURE__ */ new Map();
|
|
1275
1344
|
loopDescriptions = /* @__PURE__ */ new Map();
|
|
1345
|
+
/** loop_name → 正在运行的子智能体起点,agent:end 结算并挂到父工具调用上。 */
|
|
1346
|
+
activeSubagentRuns = /* @__PURE__ */ new Map();
|
|
1276
1347
|
pendingChildPauses = /* @__PURE__ */ new Map();
|
|
1277
1348
|
pendingMemoryRefs = /* @__PURE__ */ new Map();
|
|
1278
1349
|
activeCompactions = /* @__PURE__ */ new Map();
|
|
@@ -1332,6 +1403,25 @@ var ClientProjectionBuilder = class {
|
|
|
1332
1403
|
}
|
|
1333
1404
|
if (event.type.startsWith("compaction:"))
|
|
1334
1405
|
return this.onCompaction(event.type, loopId, payload);
|
|
1406
|
+
if (event.type === "llm:first_token:delivered") {
|
|
1407
|
+
const targetTurnId = String(payload.turn_id ?? "");
|
|
1408
|
+
const active = this.activeTurns.get(loopId);
|
|
1409
|
+
if (active && (!targetTurnId || active.turnId === targetTurnId)) {
|
|
1410
|
+
this.applyFirstTokenDelivery(active, payload);
|
|
1411
|
+
return this.syncPatch(active);
|
|
1412
|
+
}
|
|
1413
|
+
const latest = targetTurnId ? this.turnsById.get(targetTurnId) : this.latestTurns.get(loopId);
|
|
1414
|
+
const duration = payload.first_token_delivery_ms;
|
|
1415
|
+
if (!latest || latest.role !== "assistant" || typeof duration !== "number" || duration < 0) {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
const turn = { ...latest, first_token_delivery_ms: duration };
|
|
1419
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1420
|
+
if (this.latestTurns.get(loopId)?.turn_id === turn.turn_id) {
|
|
1421
|
+
this.latestTurns.set(loopId, turn);
|
|
1422
|
+
}
|
|
1423
|
+
return [{ kind: "upsert", turn }];
|
|
1424
|
+
}
|
|
1335
1425
|
const state = this.activeTurns.get(loopId);
|
|
1336
1426
|
if (!state) {
|
|
1337
1427
|
if (event.type === "chat:end") {
|
|
@@ -1347,17 +1437,27 @@ var ClientProjectionBuilder = class {
|
|
|
1347
1437
|
state.toolCalls = [];
|
|
1348
1438
|
state.usage = null;
|
|
1349
1439
|
state.responseDone = false;
|
|
1440
|
+
state.firstTokenMs = null;
|
|
1441
|
+
state.streamDurationMs = null;
|
|
1442
|
+
state.firstTokenDeliveryMs = null;
|
|
1350
1443
|
return this.syncPatch(state);
|
|
1351
1444
|
case "llm:thinking:delta":
|
|
1445
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1352
1446
|
return this.onDelta(state, "thinking", "", String(payload.content ?? ""));
|
|
1353
1447
|
case "llm:content:delta":
|
|
1448
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1354
1449
|
return this.onDelta(state, "text", "", String(payload.content ?? ""));
|
|
1355
1450
|
case "llm:content:complete":
|
|
1356
1451
|
replaceTextContent(state, String(payload.content ?? ""));
|
|
1357
1452
|
return this.syncPatch(state);
|
|
1453
|
+
case "llm:content:done":
|
|
1454
|
+
this.applyStreamTiming(state, payload);
|
|
1455
|
+
return this.syncPatch(state);
|
|
1358
1456
|
case "llm:tool_call:created":
|
|
1457
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1359
1458
|
return this.onToolCallCreated(state, payload);
|
|
1360
1459
|
case "llm:tool_call:arguments:delta":
|
|
1460
|
+
this.applyFirstTokenDelivery(state, payload);
|
|
1361
1461
|
return this.onToolCallArgumentsDelta(state, payload);
|
|
1362
1462
|
case "llm:response:done":
|
|
1363
1463
|
return this.onResponseDone(state, loopId, payload);
|
|
@@ -1385,7 +1485,9 @@ var ClientProjectionBuilder = class {
|
|
|
1385
1485
|
this.activeTurns.clear();
|
|
1386
1486
|
this.lastTurnIds.clear();
|
|
1387
1487
|
this.latestTurns.clear();
|
|
1488
|
+
this.turnsById.clear();
|
|
1388
1489
|
this.loopDescriptions.clear();
|
|
1490
|
+
this.activeSubagentRuns.clear();
|
|
1389
1491
|
this.pendingChildPauses.clear();
|
|
1390
1492
|
this.pendingMemoryRefs.clear();
|
|
1391
1493
|
this.activeCompactions.clear();
|
|
@@ -1400,15 +1502,18 @@ var ClientProjectionBuilder = class {
|
|
|
1400
1502
|
}
|
|
1401
1503
|
_seedTurns(turns) {
|
|
1402
1504
|
const latestTurns = /* @__PURE__ */ new Map();
|
|
1505
|
+
const turnsById = /* @__PURE__ */ new Map();
|
|
1403
1506
|
for (const turn of turns) {
|
|
1404
1507
|
this.seedSequence(turn.sequence);
|
|
1405
1508
|
if (!isConversationTurn(turn)) continue;
|
|
1509
|
+
turnsById.set(turn.turn_id, turn);
|
|
1406
1510
|
const current = latestTurns.get(turn.loop_id);
|
|
1407
1511
|
if (!current || turn.sequence >= current.sequence) {
|
|
1408
1512
|
latestTurns.set(turn.loop_id, turn);
|
|
1409
1513
|
}
|
|
1410
1514
|
}
|
|
1411
1515
|
this.latestTurns = latestTurns;
|
|
1516
|
+
this.turnsById = turnsById;
|
|
1412
1517
|
}
|
|
1413
1518
|
// --- Event handlers ---
|
|
1414
1519
|
onMemoryInject(loopId, payload) {
|
|
@@ -1427,7 +1532,7 @@ var ClientProjectionBuilder = class {
|
|
|
1427
1532
|
return null;
|
|
1428
1533
|
}
|
|
1429
1534
|
onContextEntry(payload) {
|
|
1430
|
-
if (!
|
|
1535
|
+
if (!isRecord(payload.entry)) return null;
|
|
1431
1536
|
const turn = buildContextTurn(payload.entry, this.nextSeq());
|
|
1432
1537
|
return turn ? [{ kind: "upsert", turn }] : null;
|
|
1433
1538
|
}
|
|
@@ -1435,7 +1540,7 @@ var ClientProjectionBuilder = class {
|
|
|
1435
1540
|
let turnId = String(payload.turn_id ?? this.nextTurnId(`turn:${loopId}`));
|
|
1436
1541
|
let memoryRefs = null;
|
|
1437
1542
|
const pending = this.pendingMemoryRefs.get(loopId);
|
|
1438
|
-
if (pending) {
|
|
1543
|
+
if (pending && (!pending.turnId || pending.turnId === turnId)) {
|
|
1439
1544
|
this.pendingMemoryRefs.delete(loopId);
|
|
1440
1545
|
if (pending.turnId) turnId = pending.turnId;
|
|
1441
1546
|
memoryRefs = pending.refs;
|
|
@@ -1451,6 +1556,7 @@ var ClientProjectionBuilder = class {
|
|
|
1451
1556
|
this.lastTurnIds.set(loopId, turnId);
|
|
1452
1557
|
const turn = snapshot(state, this.nextSeq(), "streaming");
|
|
1453
1558
|
this.latestTurns.set(loopId, turn);
|
|
1559
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1454
1560
|
return [{ kind: "upsert", turn }];
|
|
1455
1561
|
}
|
|
1456
1562
|
onModeChange(loopId, payload) {
|
|
@@ -1487,11 +1593,11 @@ var ClientProjectionBuilder = class {
|
|
|
1487
1593
|
loopId,
|
|
1488
1594
|
toolCallId: optStr(payload.tool_call_id) ?? void 0,
|
|
1489
1595
|
toolName: toolNameFromPayload(payload) ?? void 0,
|
|
1490
|
-
workspacePath:
|
|
1596
|
+
workspacePath: isRecord(payload.workspace_change) ? optStr(payload.workspace_change.to) ?? void 0 : void 0
|
|
1491
1597
|
}
|
|
1492
1598
|
];
|
|
1493
1599
|
const change = payload.workspace_change;
|
|
1494
|
-
if (
|
|
1600
|
+
if (isRecord(change)) {
|
|
1495
1601
|
const source = optStr(change.source) ?? "";
|
|
1496
1602
|
const projectName = optStr(change.project_name) ?? "";
|
|
1497
1603
|
const fromPath = optStr(change.from) ?? "";
|
|
@@ -1523,6 +1629,13 @@ var ClientProjectionBuilder = class {
|
|
|
1523
1629
|
onAgentStart(loopId, payload) {
|
|
1524
1630
|
const description = optStr(payload.description);
|
|
1525
1631
|
if (description) this.loopDescriptions.set(loopId, description);
|
|
1632
|
+
const subagentStartedAt = optStr(payload.started_at);
|
|
1633
|
+
if (subagentStartedAt) {
|
|
1634
|
+
this.activeSubagentRuns.set(loopId, {
|
|
1635
|
+
runId: optStr(payload.run_id) ?? loopId,
|
|
1636
|
+
startedAt: subagentStartedAt
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1526
1639
|
const turn = buildSystemNotificationTurn(
|
|
1527
1640
|
this.nextSeq(),
|
|
1528
1641
|
this.nextTurnId("agent:start"),
|
|
@@ -1564,7 +1677,7 @@ var ClientProjectionBuilder = class {
|
|
|
1564
1677
|
this.nextTurnId("bg:tasks_completed"),
|
|
1565
1678
|
loopId,
|
|
1566
1679
|
"bg:tasks_completed",
|
|
1567
|
-
|
|
1680
|
+
backgroundTaskStatus(payload),
|
|
1568
1681
|
`Background tasks completed (${count})`,
|
|
1569
1682
|
detail || null,
|
|
1570
1683
|
{ task_ids: taskIds, count }
|
|
@@ -1584,7 +1697,32 @@ var ClientProjectionBuilder = class {
|
|
|
1584
1697
|
}
|
|
1585
1698
|
const directTurn = this.activeTurns.get(loopId) ?? this.latestTurns.get(loopId);
|
|
1586
1699
|
setToolCallStatus(directTurn, toolCallId, "done", null, null);
|
|
1587
|
-
|
|
1700
|
+
const updates = this.collectLoopUpdates(loopId, parentLoopId);
|
|
1701
|
+
const entryId = typeof payload.entry_id === "string" ? payload.entry_id : "";
|
|
1702
|
+
if (!entryId) return updates;
|
|
1703
|
+
const answerData = isRecord(payload.answer_data) ? payload.answer_data : {};
|
|
1704
|
+
const answerTurn = {
|
|
1705
|
+
id: entryId,
|
|
1706
|
+
sequence: this.nextSeq(),
|
|
1707
|
+
turn_id: entryId,
|
|
1708
|
+
loop_id: loopId,
|
|
1709
|
+
kind: "message",
|
|
1710
|
+
role: "assistant",
|
|
1711
|
+
status: "completed",
|
|
1712
|
+
blocks: [
|
|
1713
|
+
{
|
|
1714
|
+
type: "ask_user_answer",
|
|
1715
|
+
content: answerData,
|
|
1716
|
+
tool_call_id: toolCallId || null
|
|
1717
|
+
}
|
|
1718
|
+
],
|
|
1719
|
+
tool_calls: [],
|
|
1720
|
+
model: null,
|
|
1721
|
+
usage: null,
|
|
1722
|
+
duration_ms: 0,
|
|
1723
|
+
client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
|
|
1724
|
+
};
|
|
1725
|
+
return [...updates, { kind: "upsert", turn: answerTurn }];
|
|
1588
1726
|
}
|
|
1589
1727
|
onUserMessage(_loopId, payload) {
|
|
1590
1728
|
const entryId = String(payload.entry_id ?? this.nextTurnId("user"));
|
|
@@ -1601,8 +1739,12 @@ var ClientProjectionBuilder = class {
|
|
|
1601
1739
|
tool_calls: [],
|
|
1602
1740
|
model: null,
|
|
1603
1741
|
usage: null,
|
|
1604
|
-
duration_ms: 0
|
|
1742
|
+
duration_ms: 0,
|
|
1743
|
+
client_request_id: typeof payload.client_request_id === "string" ? payload.client_request_id : null
|
|
1605
1744
|
};
|
|
1745
|
+
if (payload.start_reason === "background-completion") {
|
|
1746
|
+
turn.blocks.push({ type: "system_notification", content: { notification_type: "bg:wakeup" } });
|
|
1747
|
+
}
|
|
1606
1748
|
this.latestTurns.set("root", turn);
|
|
1607
1749
|
return [{ kind: "upsert", turn }];
|
|
1608
1750
|
}
|
|
@@ -1647,6 +1789,7 @@ var ClientProjectionBuilder = class {
|
|
|
1647
1789
|
}
|
|
1648
1790
|
onResponseDone(state, loopId, payload) {
|
|
1649
1791
|
state.responseDone = true;
|
|
1792
|
+
this.applyStreamTiming(state, payload);
|
|
1650
1793
|
const model = optStr(payload.model);
|
|
1651
1794
|
if (model) state.model = model;
|
|
1652
1795
|
if (typeof payload.usage === "object" && payload.usage !== null) {
|
|
@@ -1680,6 +1823,31 @@ var ClientProjectionBuilder = class {
|
|
|
1680
1823
|
}
|
|
1681
1824
|
return this.syncPatch(state);
|
|
1682
1825
|
}
|
|
1826
|
+
applyFirstTokenDelivery(state, payload) {
|
|
1827
|
+
const firstTokenMs = payload.first_token_ms;
|
|
1828
|
+
if (state.firstTokenMs === null && typeof firstTokenMs === "number" && firstTokenMs >= 0) {
|
|
1829
|
+
state.firstTokenMs = Math.round(firstTokenMs);
|
|
1830
|
+
}
|
|
1831
|
+
const duration = payload.first_token_delivery_ms;
|
|
1832
|
+
if (state.firstTokenDeliveryMs === null && typeof duration === "number" && duration >= 0) {
|
|
1833
|
+
state.firstTokenDeliveryMs = duration;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
applyStreamTiming(state, payload) {
|
|
1837
|
+
if (typeof payload.first_token_ms === "number" && payload.first_token_ms >= 0) {
|
|
1838
|
+
state.firstTokenMs = Math.round(payload.first_token_ms);
|
|
1839
|
+
}
|
|
1840
|
+
if (typeof payload.stream_duration_ms === "number" && payload.stream_duration_ms >= 0) {
|
|
1841
|
+
state.streamDurationMs = Math.round(payload.stream_duration_ms);
|
|
1842
|
+
}
|
|
1843
|
+
if (typeof payload.started_at === "string" && payload.started_at) {
|
|
1844
|
+
state.modelStartedAt = payload.started_at;
|
|
1845
|
+
state.modelStartedAtReported = true;
|
|
1846
|
+
}
|
|
1847
|
+
if (typeof payload.duration_ms === "number" && payload.duration_ms >= 0) {
|
|
1848
|
+
state.modelDurationMs = Math.round(payload.duration_ms);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1683
1851
|
onToolResultDone(state, loopId, payload) {
|
|
1684
1852
|
const toolCallId = String(payload.tool_call_id ?? "");
|
|
1685
1853
|
const streamedResult = state.toolCalls.find((toolCall) => toolCall.id === toolCallId)?.result;
|
|
@@ -1687,8 +1855,10 @@ var ClientProjectionBuilder = class {
|
|
|
1687
1855
|
const status = toolStatusFromResult(content);
|
|
1688
1856
|
const rawDuration = payload.duration_ms;
|
|
1689
1857
|
const durationMs = typeof rawDuration === "number" && rawDuration >= 0 ? Math.round(rawDuration) : null;
|
|
1858
|
+
const ranInParallel = typeof payload.ran_in_parallel === "boolean" ? payload.ran_in_parallel : null;
|
|
1690
1859
|
const sl = sourceLoopFor(state.loopId, this.loopDescriptions);
|
|
1691
|
-
|
|
1860
|
+
const startedAt = typeof payload.started_at === "string" && payload.started_at ? payload.started_at : null;
|
|
1861
|
+
applyToolResult(state, toolCallId, content, status, durationMs, ranInParallel, sl, startedAt);
|
|
1692
1862
|
const isNativeProviderResult = typeof payload.provider === "string" && payload.provider !== "";
|
|
1693
1863
|
if (!isNativeProviderResult && this.shouldFinalizeAfterToolResults(state)) {
|
|
1694
1864
|
return this.finalize(loopId, "completed");
|
|
@@ -1719,10 +1889,13 @@ var ClientProjectionBuilder = class {
|
|
|
1719
1889
|
onToolBridge(state, payload) {
|
|
1720
1890
|
const bridge = payload.bridge;
|
|
1721
1891
|
if (typeof bridge !== "object" || bridge === null) return null;
|
|
1892
|
+
const toolCallId = String(payload.tool_call_id ?? "");
|
|
1893
|
+
if (!toolCallId || !state.toolCalls.some((toolCall) => toolCall.id === toolCallId)) return null;
|
|
1722
1894
|
state.blocks.push({
|
|
1723
1895
|
type: "tool_bridge",
|
|
1724
1896
|
content: bridge,
|
|
1725
|
-
|
|
1897
|
+
delivery_id: String(payload.delivery_id ?? "") || null,
|
|
1898
|
+
tool_call_id: toolCallId
|
|
1726
1899
|
});
|
|
1727
1900
|
return this.syncPatch(state);
|
|
1728
1901
|
}
|
|
@@ -1841,6 +2014,7 @@ var ClientProjectionBuilder = class {
|
|
|
1841
2014
|
}
|
|
1842
2015
|
onAgentEnd(state, loopId, payload) {
|
|
1843
2016
|
const ok = payload.ok !== false;
|
|
2017
|
+
this.settleSubagentRun(loopId, payload);
|
|
1844
2018
|
const status = ok ? "done" : "error";
|
|
1845
2019
|
const title = `Agent: ${payload.description ?? payload.skill_id ?? "agent"}`;
|
|
1846
2020
|
const detail = optStr(payload.error);
|
|
@@ -1848,6 +2022,7 @@ var ClientProjectionBuilder = class {
|
|
|
1848
2022
|
skill_id: optStr(payload.skill_id),
|
|
1849
2023
|
description: optStr(payload.description),
|
|
1850
2024
|
parent_fork_tool_call_id: optStr(payload.parent_fork_tool_call_id),
|
|
2025
|
+
...typeof payload.run_status === "string" ? { run_status: payload.run_status } : {},
|
|
1851
2026
|
ok
|
|
1852
2027
|
};
|
|
1853
2028
|
if (state) {
|
|
@@ -1867,6 +2042,32 @@ var ClientProjectionBuilder = class {
|
|
|
1867
2042
|
);
|
|
1868
2043
|
return [{ kind: "upsert", turn }];
|
|
1869
2044
|
}
|
|
2045
|
+
settleSubagentRun(loopId, payload) {
|
|
2046
|
+
const active = this.activeSubagentRuns.get(loopId);
|
|
2047
|
+
this.activeSubagentRuns.delete(loopId);
|
|
2048
|
+
const parentToolCallId = optStr(payload.parent_fork_tool_call_id);
|
|
2049
|
+
if (!parentToolCallId) return;
|
|
2050
|
+
const rawDuration = payload.run_duration_ms;
|
|
2051
|
+
const durationMs = typeof rawDuration === "number" && rawDuration >= 0 ? Math.round(rawDuration) : null;
|
|
2052
|
+
const rawStatus = optStr(payload.run_status);
|
|
2053
|
+
const status = rawStatus === "failed" || rawStatus === "cancelled" || rawStatus === "paused" || rawStatus === "incomplete" ? rawStatus : payload.ok === false ? "failed" : "completed";
|
|
2054
|
+
const run = {
|
|
2055
|
+
run_id: active?.runId ?? optStr(payload.run_id) ?? loopId,
|
|
2056
|
+
scope: "subagent",
|
|
2057
|
+
loop_id: loopId,
|
|
2058
|
+
started_at: active?.startedAt ?? optStr(payload.run_started_at),
|
|
2059
|
+
duration_ms: durationMs,
|
|
2060
|
+
status
|
|
2061
|
+
};
|
|
2062
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2063
|
+
for (const turn of [...this.latestTurns.values(), ...this.turnsById.values()]) {
|
|
2064
|
+
if (seen.has(turn)) continue;
|
|
2065
|
+
seen.add(turn);
|
|
2066
|
+
const toolCall = turn.tool_calls.find((item) => item.id === parentToolCallId);
|
|
2067
|
+
if (!toolCall) continue;
|
|
2068
|
+
toolCall.subagent_runs = [...toolCall.subagent_runs ?? [], run];
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
1870
2071
|
dropIncompleteToolCalls(state) {
|
|
1871
2072
|
if (state.responseDone) return;
|
|
1872
2073
|
state.toolCalls = [];
|
|
@@ -1916,6 +2117,7 @@ var ClientProjectionBuilder = class {
|
|
|
1916
2117
|
syncPatch(state) {
|
|
1917
2118
|
const turn = snapshot(state, this.nextSeq(), "streaming");
|
|
1918
2119
|
this.latestTurns.set(state.loopId, turn);
|
|
2120
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1919
2121
|
this.lastTurnIds.set(state.loopId, turn.turn_id);
|
|
1920
2122
|
return [{ kind: "upsert", turn }];
|
|
1921
2123
|
}
|
|
@@ -1926,6 +2128,7 @@ var ClientProjectionBuilder = class {
|
|
|
1926
2128
|
const turn = snapshot(state, this.nextSeq(), status);
|
|
1927
2129
|
this.lastTurnIds.set(loopId, turn.turn_id);
|
|
1928
2130
|
this.latestTurns.set(loopId, turn);
|
|
2131
|
+
this.turnsById.set(turn.turn_id, turn);
|
|
1929
2132
|
return [{ kind: "upsert", turn }];
|
|
1930
2133
|
}
|
|
1931
2134
|
shouldFinalizeAfterToolResults(state) {
|
|
@@ -1963,9 +2166,6 @@ var ClientProjectionBuilder = class {
|
|
|
1963
2166
|
return `${prefix}:${this.syntheticCounter}:${hex}`;
|
|
1964
2167
|
}
|
|
1965
2168
|
};
|
|
1966
|
-
function isRecord3(value) {
|
|
1967
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1968
|
-
}
|
|
1969
2169
|
function optStr(value) {
|
|
1970
2170
|
if (value == null || value === "") return null;
|
|
1971
2171
|
return String(value);
|
|
@@ -1985,6 +2185,101 @@ function toolArgsFromPayload(payload) {
|
|
|
1985
2185
|
return "";
|
|
1986
2186
|
}
|
|
1987
2187
|
|
|
2188
|
+
// src/schemas/mcp-app.ts
|
|
2189
|
+
function isMcpAppData(value) {
|
|
2190
|
+
return isRecord(value) && typeof value.sourceId === "string" && value.sourceId.length > 0 && typeof value.resourceUri === "string" && value.resourceUri.startsWith("ui://") && typeof value.mimeType === "string" && isRecord(value.resourceMeta) && isRecord(value.toolInput) && isRecord(value.toolResult);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// src/shared/tool-ui-card.ts
|
|
2194
|
+
function isNonEmptyString(value) {
|
|
2195
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
2196
|
+
}
|
|
2197
|
+
function isPositiveNumber(value) {
|
|
2198
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
2199
|
+
}
|
|
2200
|
+
function isToolUiCard(value) {
|
|
2201
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2202
|
+
return false;
|
|
2203
|
+
}
|
|
2204
|
+
const raw = value;
|
|
2205
|
+
if (raw.target !== "inline" && raw.target !== "preview") {
|
|
2206
|
+
return false;
|
|
2207
|
+
}
|
|
2208
|
+
if (!isPositiveNumber(raw.height)) {
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
if (raw.archived === true && !isNonEmptyString(raw.resourceHTML)) {
|
|
2212
|
+
return false;
|
|
2213
|
+
}
|
|
2214
|
+
if (!isNonEmptyString(raw.resourceHTML) && !isNonEmptyString(raw.resourceUri) && !isNonEmptyString(raw.resourceURI) && !(raw.target === "preview" && isNonEmptyString(raw.sourcePath))) {
|
|
2215
|
+
return false;
|
|
2216
|
+
}
|
|
2217
|
+
if (raw.title != null && !isNonEmptyString(raw.title)) {
|
|
2218
|
+
return false;
|
|
2219
|
+
}
|
|
2220
|
+
if (raw.sourcePath != null && !isNonEmptyString(raw.sourcePath)) return false;
|
|
2221
|
+
if (raw.mcpApp !== void 0 && !isMcpAppData(raw.mcpApp)) {
|
|
2222
|
+
return false;
|
|
2223
|
+
}
|
|
2224
|
+
return true;
|
|
2225
|
+
}
|
|
2226
|
+
function resolveToolUiCardContent(card) {
|
|
2227
|
+
if (isNonEmptyString(card.resourceHTML)) {
|
|
2228
|
+
return { type: "resource-html", content: card.resourceHTML };
|
|
2229
|
+
}
|
|
2230
|
+
if (card.archived) return null;
|
|
2231
|
+
const uri = isNonEmptyString(card.resourceUri) ? card.resourceUri : card.resourceURI;
|
|
2232
|
+
if (isNonEmptyString(uri)) return { type: "resource-uri", content: uri };
|
|
2233
|
+
return card.target === "preview" && isNonEmptyString(card.sourcePath) ? { type: "resource-file", content: card.sourcePath } : null;
|
|
2234
|
+
}
|
|
2235
|
+
function buildToolUiCardKey(toolCallId, type3, content, sourcePath) {
|
|
2236
|
+
if (type3 === "resource-uri") {
|
|
2237
|
+
return `tool-preview-uri:${content}`;
|
|
2238
|
+
}
|
|
2239
|
+
if (sourcePath?.trim()) return sourcePath.trim();
|
|
2240
|
+
if (type3 === "resource-file") return content;
|
|
2241
|
+
return `tool-preview:${toolCallId}`;
|
|
2242
|
+
}
|
|
2243
|
+
function isInternalStatusToolUiCard(card) {
|
|
2244
|
+
return card?.title?.trim() === "\u9636\u6BB5\u8FDB\u5EA6";
|
|
2245
|
+
}
|
|
2246
|
+
function isAppDevToolUiCard(card, isAppDev) {
|
|
2247
|
+
return isAppDev && card.target === "preview" && !card.resourceHTML && Boolean(card.resourceUri || card.resourceURI);
|
|
2248
|
+
}
|
|
2249
|
+
function collectInlineToolUiCards(messages) {
|
|
2250
|
+
return messages.flatMap(
|
|
2251
|
+
(message) => (message.blocks ?? []).flatMap(
|
|
2252
|
+
(block, index) => block.type === "tool_ui" && block.tool_call_id && isToolUiCard(block.content) && block.content.target === "inline" ? [
|
|
2253
|
+
{
|
|
2254
|
+
key: `${message.entry_id ?? message.timestamp ?? "message"}-${block.tool_call_id}-${index}`,
|
|
2255
|
+
toolCallId: block.tool_call_id,
|
|
2256
|
+
card: block.content
|
|
2257
|
+
}
|
|
2258
|
+
] : []
|
|
2259
|
+
)
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
function collectPreviewToolUiCards(messages) {
|
|
2263
|
+
return messages.flatMap(
|
|
2264
|
+
(message) => (message.blocks ?? []).flatMap((block, index) => {
|
|
2265
|
+
if (block.type !== "tool_ui" || !block.tool_call_id || !isToolUiCard(block.content) || block.content.target !== "preview") {
|
|
2266
|
+
return [];
|
|
2267
|
+
}
|
|
2268
|
+
const toolCall = (message.tool_calls ?? []).find(
|
|
2269
|
+
(candidate) => candidate.id === block.tool_call_id
|
|
2270
|
+
);
|
|
2271
|
+
return toolCall ? [
|
|
2272
|
+
{
|
|
2273
|
+
key: `${message.entry_id ?? message.timestamp ?? "message"}-${block.tool_call_id}-${index}`,
|
|
2274
|
+
toolCall,
|
|
2275
|
+
card: block.content,
|
|
2276
|
+
blocks: message.blocks ?? []
|
|
2277
|
+
}
|
|
2278
|
+
] : [];
|
|
2279
|
+
})
|
|
2280
|
+
);
|
|
2281
|
+
}
|
|
2282
|
+
|
|
1988
2283
|
// src/shared/projection/history.ts
|
|
1989
2284
|
function projectHistory(entries) {
|
|
1990
2285
|
const turns = [];
|
|
@@ -1993,6 +2288,7 @@ function projectHistory(entries) {
|
|
|
1993
2288
|
const pendingChildPauses = /* @__PURE__ */ new Map();
|
|
1994
2289
|
const answeredToolCallIds = /* @__PURE__ */ new Set();
|
|
1995
2290
|
const nativeToolTurns = /* @__PURE__ */ new Set();
|
|
2291
|
+
const turnEntryAliases = /* @__PURE__ */ new Map();
|
|
1996
2292
|
let latestRootMessage = null;
|
|
1997
2293
|
let latestRootAssistant = null;
|
|
1998
2294
|
let sequence = 0;
|
|
@@ -2000,7 +2296,40 @@ function projectHistory(entries) {
|
|
|
2000
2296
|
const kind = String(entry.kind ?? "");
|
|
2001
2297
|
const loopId = String(entry.loop_name ?? "root");
|
|
2002
2298
|
const id = String(entry.id ?? `history-${sequence + 1}`);
|
|
2003
|
-
if (kind === "
|
|
2299
|
+
if (kind === "turn_timing" && isRecord(entry.data)) {
|
|
2300
|
+
const turnId = typeof entry.data.turn_id === "string" ? entry.data.turn_id : "";
|
|
2301
|
+
const duration = numberOrNull(entry.data.first_token_delivery_ms);
|
|
2302
|
+
const target = turnEntryAliases.get(turnId);
|
|
2303
|
+
if (target && duration !== null) target.first_token_delivery_ms = duration;
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
if (kind === "run_timing" && isRecord(entry.data)) {
|
|
2307
|
+
const data = entry.data;
|
|
2308
|
+
if (data.scope === "subagent") {
|
|
2309
|
+
const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
|
|
2310
|
+
const duration = numberOrNull(data.duration_ms);
|
|
2311
|
+
const rawStatus = String(data.status ?? "completed");
|
|
2312
|
+
const status = rawStatus === "failed" || rawStatus === "cancelled" || rawStatus === "paused" || rawStatus === "incomplete" ? rawStatus : "completed";
|
|
2313
|
+
for (const turn of turns) {
|
|
2314
|
+
const toolCall = turn.tool_calls.find((item) => item.id === parentToolCallId);
|
|
2315
|
+
if (!toolCall) continue;
|
|
2316
|
+
toolCall.subagent_runs = [
|
|
2317
|
+
...toolCall.subagent_runs ?? [],
|
|
2318
|
+
{
|
|
2319
|
+
run_id: String(data.run_id ?? ""),
|
|
2320
|
+
scope: "subagent",
|
|
2321
|
+
loop_id: loopId,
|
|
2322
|
+
started_at: typeof data.started_at === "string" ? data.started_at : null,
|
|
2323
|
+
duration_ms: duration,
|
|
2324
|
+
status
|
|
2325
|
+
}
|
|
2326
|
+
];
|
|
2327
|
+
break;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
if (kind === "post_chat_followup" && isRecord(entry.data)) {
|
|
2004
2333
|
const assistantEntryId = String(entry.data.assistant_entry_id ?? "");
|
|
2005
2334
|
if (latestRootMessage?.role === "assistant" && latestRootMessage.turn_id === assistantEntryId) {
|
|
2006
2335
|
const followup = normalizePostChatFollowup(entry.data, assistantEntryId);
|
|
@@ -2016,7 +2345,7 @@ function projectHistory(entries) {
|
|
|
2016
2345
|
}
|
|
2017
2346
|
continue;
|
|
2018
2347
|
}
|
|
2019
|
-
if (kind === "message" &&
|
|
2348
|
+
if (kind === "message" && isRecord(entry.message)) {
|
|
2020
2349
|
const message = entry.message;
|
|
2021
2350
|
const role = String(message.role ?? "");
|
|
2022
2351
|
if (role === "tool") {
|
|
@@ -2026,6 +2355,12 @@ function projectHistory(entries) {
|
|
|
2026
2355
|
if (owner && toolCall) {
|
|
2027
2356
|
toolCall.status = message._error ? "error" : inferToolStatus(String(message.content ?? ""));
|
|
2028
2357
|
toolCall.result = message.content ?? "";
|
|
2358
|
+
const duration = numberOrNull(message._duration_ms);
|
|
2359
|
+
if (duration !== null) toolCall.duration_ms = duration;
|
|
2360
|
+
if (typeof message._started_at === "string") {
|
|
2361
|
+
toolCall.started_at = message._started_at;
|
|
2362
|
+
}
|
|
2363
|
+
if (message._ran_in_parallel === true) toolCall.ran_in_parallel = true;
|
|
2029
2364
|
owner.blocks.push({
|
|
2030
2365
|
type: "tool_result",
|
|
2031
2366
|
content: message.content ?? "",
|
|
@@ -2035,8 +2370,11 @@ function projectHistory(entries) {
|
|
|
2035
2370
|
continue;
|
|
2036
2371
|
}
|
|
2037
2372
|
if (role !== "user" && role !== "assistant" && role !== "system") continue;
|
|
2038
|
-
const toolCalls = role === "assistant" ? buildToolCalls(message.tool_calls) : [];
|
|
2373
|
+
const toolCalls = role === "assistant" && !message._error ? buildToolCalls(message.tool_calls) : [];
|
|
2039
2374
|
const blocks = buildBlocks(message, toolCalls);
|
|
2375
|
+
if (role === "user" && message._start_reason === "background-completion") {
|
|
2376
|
+
blocks.push({ type: "system_notification", content: { notification_type: "bg:wakeup" } });
|
|
2377
|
+
}
|
|
2040
2378
|
const turn = {
|
|
2041
2379
|
id,
|
|
2042
2380
|
sequence: ++sequence,
|
|
@@ -2048,26 +2386,42 @@ function projectHistory(entries) {
|
|
|
2048
2386
|
blocks,
|
|
2049
2387
|
tool_calls: toolCalls,
|
|
2050
2388
|
model: stringOrNull(message.model),
|
|
2051
|
-
usage:
|
|
2052
|
-
|
|
2053
|
-
|
|
2389
|
+
usage: isRecord(message._usage) ? { ...message._usage } : null,
|
|
2390
|
+
// 模型请求区间来自持久化字段;旧会话缺失时为 null,不用落盘时刻冒充(#2581 A1)。
|
|
2391
|
+
duration_ms: numberOrNull(message._duration_ms),
|
|
2392
|
+
first_token_ms: numberOrNull(message._first_token_ms),
|
|
2393
|
+
stream_duration_ms: numberOrNull(message._stream_duration_ms),
|
|
2394
|
+
started_at: typeof message._started_at === "string" ? message._started_at : typeof entry.timestamp === "string" ? entry.timestamp : void 0,
|
|
2395
|
+
has_reported_timing: typeof message._started_at === "string",
|
|
2054
2396
|
// fork 子会话的父锚点:历史里落在消息级 _parent_fork_tool_call_id。丢掉它,
|
|
2055
2397
|
// 刷新后工作流步骤 fork 就配不回 workflow:run 工具调用(普通 Agent fork 有
|
|
2056
2398
|
// root Agent 工具调用兜底配对,工作流 fork 没有,子智能体卡会整体消失)。
|
|
2057
2399
|
parent_fork_tool_call_id: stringOrNull(message._parent_fork_tool_call_id)
|
|
2058
2400
|
};
|
|
2059
2401
|
const isNativeToolEntry = role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.some(
|
|
2060
|
-
(toolCall) =>
|
|
2402
|
+
(toolCall) => isRecord(toolCall) && typeof toolCall.provider === "string"
|
|
2061
2403
|
);
|
|
2062
2404
|
const previousTurn = turns[turns.length - 1];
|
|
2063
2405
|
if (role === "assistant" && previousTurn?.role === "assistant" && previousTurn.loop_id === loopId && nativeToolTurns.has(previousTurn)) {
|
|
2406
|
+
nativeToolTurns.delete(previousTurn);
|
|
2064
2407
|
previousTurn.blocks.push(...turn.blocks);
|
|
2065
2408
|
previousTurn.tool_calls.push(...turn.tool_calls);
|
|
2066
2409
|
previousTurn.status = turn.status;
|
|
2067
2410
|
previousTurn.model = turn.model ?? previousTurn.model;
|
|
2068
2411
|
previousTurn.usage = turn.usage ?? previousTurn.usage;
|
|
2069
|
-
previousTurn.duration_ms = turn.duration_ms
|
|
2070
|
-
|
|
2412
|
+
if (turn.duration_ms) previousTurn.duration_ms = turn.duration_ms;
|
|
2413
|
+
previousTurn.first_token_ms = turn.first_token_ms ?? previousTurn.first_token_ms;
|
|
2414
|
+
previousTurn.stream_duration_ms = turn.stream_duration_ms ?? previousTurn.stream_duration_ms;
|
|
2415
|
+
if (turn.has_reported_timing) {
|
|
2416
|
+
previousTurn.started_at = turn.started_at;
|
|
2417
|
+
previousTurn.has_reported_timing = true;
|
|
2418
|
+
} else if (!previousTurn.has_reported_timing) {
|
|
2419
|
+
previousTurn.started_at = previousTurn.started_at ?? turn.started_at;
|
|
2420
|
+
}
|
|
2421
|
+
previousTurn.id = turn.id;
|
|
2422
|
+
previousTurn.turn_id = turn.turn_id;
|
|
2423
|
+
if (isNativeToolEntry) nativeToolTurns.add(previousTurn);
|
|
2424
|
+
turnEntryAliases.set(id, previousTurn);
|
|
2071
2425
|
if (loopId === "root") latestRootMessage = previousTurn;
|
|
2072
2426
|
latestAssistantByLoop.set(loopId, previousTurn);
|
|
2073
2427
|
continue;
|
|
@@ -2076,6 +2430,7 @@ function projectHistory(entries) {
|
|
|
2076
2430
|
latestRootMessage = turn;
|
|
2077
2431
|
}
|
|
2078
2432
|
turns.push(turn);
|
|
2433
|
+
turnEntryAliases.set(id, turn);
|
|
2079
2434
|
if (isNativeToolEntry) nativeToolTurns.add(turn);
|
|
2080
2435
|
if (role === "assistant") latestAssistantByLoop.set(loopId, turn);
|
|
2081
2436
|
if (loopId === "root") {
|
|
@@ -2103,7 +2458,7 @@ function projectHistory(entries) {
|
|
|
2103
2458
|
if (kind === "mode_change") {
|
|
2104
2459
|
turns.push(markerTurn(id, ++sequence, loopId, "mode_change", entry.data ?? {}));
|
|
2105
2460
|
} else if (kind === "workspace_change") {
|
|
2106
|
-
const change =
|
|
2461
|
+
const change = isRecord(entry.data) ? entry.data : {};
|
|
2107
2462
|
const source = stringOrNull(change.source) ?? "";
|
|
2108
2463
|
const projectName = stringOrNull(change.project_name) ?? "";
|
|
2109
2464
|
const fromPath = stringOrNull(change.from) ?? "";
|
|
@@ -2132,19 +2487,22 @@ function projectHistory(entries) {
|
|
|
2132
2487
|
} else if (kind === "tool_ui" || kind === "tool_bridge") {
|
|
2133
2488
|
const data = entry.data ?? {};
|
|
2134
2489
|
const toolCallId = String(data.tool_call_id ?? "");
|
|
2135
|
-
const owner = toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
|
|
2136
|
-
const
|
|
2490
|
+
const owner = kind === "tool_bridge" ? toolCallId ? toolOwners.get(toolCallId) : void 0 : toolOwners.get(toolCallId) ?? findLatestAssistant(turns, loopId);
|
|
2491
|
+
const rawContent = kind === "tool_ui" ? data.ui : data.bridge;
|
|
2492
|
+
const fileReference = isToolUiCard(rawContent) && resolveToolUiCardContent(rawContent)?.type === "resource-file";
|
|
2493
|
+
const content = kind === "tool_ui" && isRecord(rawContent) ? { ...rawContent, ...!fileReference ? { archived: true } : {} } : rawContent;
|
|
2137
2494
|
if (owner && content !== void 0) {
|
|
2138
2495
|
owner.blocks.push({
|
|
2139
2496
|
type: kind,
|
|
2140
2497
|
content,
|
|
2498
|
+
...kind === "tool_bridge" ? { delivery_id: id } : {},
|
|
2141
2499
|
tool_call_id: toolCallId || null
|
|
2142
2500
|
});
|
|
2143
2501
|
}
|
|
2144
2502
|
} else if (kind === "compaction" || kind === "tool_result_archive") {
|
|
2145
2503
|
const data = entry.data ?? {};
|
|
2146
2504
|
const compactionId = String(data.compaction_id ?? entry.id ?? `history-${sequence + 1}`);
|
|
2147
|
-
let content =
|
|
2505
|
+
let content = isRecord(entry.message) ? entry.message.content : void 0;
|
|
2148
2506
|
if (kind === "tool_result_archive") {
|
|
2149
2507
|
const archivedCount = Array.isArray(data.archived_files) ? data.archived_files.length : 0;
|
|
2150
2508
|
content = `<compaction-summary>
|
|
@@ -2152,18 +2510,24 @@ function projectHistory(entries) {
|
|
|
2152
2510
|
</compaction-summary>`;
|
|
2153
2511
|
}
|
|
2154
2512
|
turns.push(
|
|
2155
|
-
buildCompactionTurn(
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2513
|
+
buildCompactionTurn(
|
|
2514
|
+
++sequence,
|
|
2515
|
+
compactionId,
|
|
2516
|
+
loopId,
|
|
2517
|
+
data.status === "failed" ? "failed" : "completed",
|
|
2518
|
+
{
|
|
2519
|
+
...data,
|
|
2520
|
+
content
|
|
2521
|
+
}
|
|
2522
|
+
)
|
|
2159
2523
|
);
|
|
2160
2524
|
} else if (kind === "child_pause") {
|
|
2161
2525
|
const data = entry.data ?? {};
|
|
2162
|
-
const sourceLoop =
|
|
2526
|
+
const sourceLoop = isRecord(data.source_loop) ? data.source_loop : {};
|
|
2163
2527
|
const childLoopId = String(data.child_loop_name ?? sourceLoop.name ?? "");
|
|
2164
2528
|
const childToolCallId = String(data.child_pause_tool_call_id ?? "");
|
|
2165
2529
|
const parentToolCallId = String(data.parent_fork_tool_call_id ?? "");
|
|
2166
|
-
const pauseToolData =
|
|
2530
|
+
const pauseToolData = isRecord(data.pause_tool_data) ? data.pause_tool_data : {};
|
|
2167
2531
|
const description = String(sourceLoop.description ?? "");
|
|
2168
2532
|
applyAskUserPauseToTurn(
|
|
2169
2533
|
latestAssistantByLoop.get(childLoopId),
|
|
@@ -2251,15 +2615,15 @@ function parseToolArguments(value) {
|
|
|
2251
2615
|
if (!value) return {};
|
|
2252
2616
|
try {
|
|
2253
2617
|
const parsed = JSON.parse(value);
|
|
2254
|
-
return
|
|
2618
|
+
return isRecord(parsed) ? parsed : {};
|
|
2255
2619
|
} catch {
|
|
2256
2620
|
return {};
|
|
2257
2621
|
}
|
|
2258
2622
|
}
|
|
2259
2623
|
function buildToolCalls(value) {
|
|
2260
2624
|
if (!Array.isArray(value)) return [];
|
|
2261
|
-
return value.filter(
|
|
2262
|
-
const fn =
|
|
2625
|
+
return value.filter(isRecord).map((raw) => {
|
|
2626
|
+
const fn = isRecord(raw.function) ? raw.function : {};
|
|
2263
2627
|
const name = String(fn.name ?? "");
|
|
2264
2628
|
return {
|
|
2265
2629
|
id: String(raw.id ?? ""),
|
|
@@ -2280,7 +2644,7 @@ function buildBlocks(message, toolCalls) {
|
|
|
2280
2644
|
blocks.push({ type: "text", content: displayContent });
|
|
2281
2645
|
} else if (Array.isArray(stored)) {
|
|
2282
2646
|
for (const raw of stored) {
|
|
2283
|
-
if (!
|
|
2647
|
+
if (!isRecord(raw)) continue;
|
|
2284
2648
|
const type3 = String(raw.type ?? "");
|
|
2285
2649
|
if (type3 === "thinking") blocks.push({ type: type3, content: raw.thinking ?? raw.content ?? "" });
|
|
2286
2650
|
if (type3 === "text") blocks.push({ type: type3, content: raw.text ?? raw.content ?? "" });
|
|
@@ -2329,14 +2693,11 @@ function markerTurn(id, sequence, loopId, type3, content, toolCallId = null) {
|
|
|
2329
2693
|
duration_ms: 0
|
|
2330
2694
|
};
|
|
2331
2695
|
}
|
|
2332
|
-
function isRecord4(value) {
|
|
2333
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2334
|
-
}
|
|
2335
2696
|
function stringOrNull(value) {
|
|
2336
2697
|
return typeof value === "string" ? value : null;
|
|
2337
2698
|
}
|
|
2338
|
-
function
|
|
2339
|
-
return typeof value === "number" ? value :
|
|
2699
|
+
function numberOrNull(value) {
|
|
2700
|
+
return typeof value === "number" && value >= 0 ? value : null;
|
|
2340
2701
|
}
|
|
2341
2702
|
function findLatestAssistant(turns, loopId) {
|
|
2342
2703
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
@@ -2347,6 +2708,10 @@ function findLatestAssistant(turns, loopId) {
|
|
|
2347
2708
|
}
|
|
2348
2709
|
|
|
2349
2710
|
// src/resources/sessions.ts
|
|
2711
|
+
function isEntryGoneError(error) {
|
|
2712
|
+
const status = error?.status;
|
|
2713
|
+
return status === 404 || status === 409;
|
|
2714
|
+
}
|
|
2350
2715
|
function toCreateSessionPayload(request) {
|
|
2351
2716
|
const solutionRef = request.solution_ref ?? externalSolutionRef(request.solution_id);
|
|
2352
2717
|
return {
|
|
@@ -2366,13 +2731,13 @@ function toCreateSessionPayload(request) {
|
|
|
2366
2731
|
compaction_ratio: request.compaction_ratio ?? null,
|
|
2367
2732
|
...request.runtime_type ? { runtime_type: request.runtime_type } : {},
|
|
2368
2733
|
...request.daemon_id ? { daemon_id: request.daemon_id } : {},
|
|
2369
|
-
...request.agent_runtime_id ? { agent_runtime_id: request.agent_runtime_id } : {}
|
|
2734
|
+
...request.agent_runtime_id ? { agent_runtime_id: request.agent_runtime_id } : {},
|
|
2735
|
+
...request.chat_group_id ? { chat_group_id: request.chat_group_id } : {}
|
|
2370
2736
|
};
|
|
2371
2737
|
}
|
|
2372
2738
|
var builtinSolutionIds = /* @__PURE__ */ new Set([
|
|
2373
2739
|
"app-dev",
|
|
2374
2740
|
"general_chat",
|
|
2375
|
-
"multica",
|
|
2376
2741
|
"night_build",
|
|
2377
2742
|
"skill_editor",
|
|
2378
2743
|
"smart_bid",
|
|
@@ -2395,6 +2760,54 @@ var SessionsResource = class {
|
|
|
2395
2760
|
this.client = client;
|
|
2396
2761
|
}
|
|
2397
2762
|
client;
|
|
2763
|
+
pendingImages = /* @__PURE__ */ new Map();
|
|
2764
|
+
/** Fetch an immutable message image using this client's authenticated identity. */
|
|
2765
|
+
fetchImage(sessionId, assetId, preview = true) {
|
|
2766
|
+
return this.fetchImagePath(`/api/sessions/${encodeURIComponent(sessionId)}/media/${encodeURIComponent(assetId)}`, preview);
|
|
2767
|
+
}
|
|
2768
|
+
/** Fetch a public-share image; the server validates the share token on every request. */
|
|
2769
|
+
fetchSharedImage(token, assetId, preview = true) {
|
|
2770
|
+
return this.fetchImagePath(`/api/share/${encodeURIComponent(token)}/media/${encodeURIComponent(assetId)}`, preview);
|
|
2771
|
+
}
|
|
2772
|
+
fetchImagePath(resource, preview) {
|
|
2773
|
+
const path = `${resource}${preview ? "?variant=preview" : ""}`;
|
|
2774
|
+
const identity = this.client.buildAuthedUrl(path);
|
|
2775
|
+
const existing = this.pendingImages.get(identity);
|
|
2776
|
+
if (existing) return existing;
|
|
2777
|
+
const pending = this.client.blob("GET", path).finally(() => this.pendingImages.delete(identity));
|
|
2778
|
+
this.pendingImages.set(identity, pending);
|
|
2779
|
+
return pending;
|
|
2780
|
+
}
|
|
2781
|
+
getMcpApp(sessionId, sourceId, init) {
|
|
2782
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}`, init);
|
|
2783
|
+
}
|
|
2784
|
+
onMcpAppChanged(sessionId, sourceId, listener) {
|
|
2785
|
+
let socket = this.client.socket();
|
|
2786
|
+
const changed = (payload) => {
|
|
2787
|
+
if (payload.session_id === sessionId && payload.source_id === sourceId) listener();
|
|
2788
|
+
};
|
|
2789
|
+
socket.on("mcp-app:changed", changed);
|
|
2790
|
+
socket.on("connect", listener);
|
|
2791
|
+
const off = this.client.onSocketReplaced(() => {
|
|
2792
|
+
socket.off("mcp-app:changed", changed);
|
|
2793
|
+
socket.off("connect", listener);
|
|
2794
|
+
socket = this.client.socket();
|
|
2795
|
+
socket.on("mcp-app:changed", changed);
|
|
2796
|
+
socket.on("connect", listener);
|
|
2797
|
+
});
|
|
2798
|
+
return () => {
|
|
2799
|
+
off();
|
|
2800
|
+
socket.off("mcp-app:changed", changed);
|
|
2801
|
+
socket.off("connect", listener);
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
callMcpApp(sessionId, sourceId, request, init) {
|
|
2805
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/calls`, {
|
|
2806
|
+
...init,
|
|
2807
|
+
method: "POST",
|
|
2808
|
+
body: JSON.stringify(request)
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2398
2811
|
/**
|
|
2399
2812
|
* 连接一个已存在的会话,返回实时会话对象 AgentSession
|
|
2400
2813
|
* (自动完成历史加载、Socket.IO 订阅、断线重连补数)。
|
|
@@ -2402,6 +2815,56 @@ var SessionsResource = class {
|
|
|
2402
2815
|
connect(sessionId) {
|
|
2403
2816
|
return this.client.hub.connect(sessionId);
|
|
2404
2817
|
}
|
|
2818
|
+
listSessionPlugins(sessionId, init) {
|
|
2819
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins`, init);
|
|
2820
|
+
}
|
|
2821
|
+
setSessionPluginActivation(sessionId, name, active, init) {
|
|
2822
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}:activation`, { method: "POST", body: JSON.stringify({ active }), ...init });
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Fetch a plugin's package-local icon as bytes.
|
|
2826
|
+
*
|
|
2827
|
+
* The request carries the client's own bearer credentials, so callers turn
|
|
2828
|
+
* the blob into an object URL instead of pointing `<img>` at an API path and
|
|
2829
|
+
* hoping the request is authenticated.
|
|
2830
|
+
*/
|
|
2831
|
+
fetchSessionPluginIcon(sessionId, name) {
|
|
2832
|
+
return this.client.blob("GET", `/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/icon`);
|
|
2833
|
+
}
|
|
2834
|
+
/** Authenticated URL for the icon; useful where a blob URL cannot be used. */
|
|
2835
|
+
sessionPluginIconUrl(sessionId, name) {
|
|
2836
|
+
return this.client.buildAuthedUrl(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/icon`);
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* 准备一个插件的配置:把包准备好并读出用户的配置契约,**不启用**它。
|
|
2840
|
+
*
|
|
2841
|
+
* 开启插件前必须先走这一步:缺配置时用户只会看到配置表单,服务端不会先把
|
|
2842
|
+
* active 持久化成 true 再靠补偿关掉。它不修改会话的插件选择、不连接 MCP。
|
|
2843
|
+
*/
|
|
2844
|
+
prepareSessionPluginConfig(sessionId, name, init) {
|
|
2845
|
+
return this.client.jsonFromInit(
|
|
2846
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}:prepare-config`,
|
|
2847
|
+
{ method: "POST", ...init }
|
|
2848
|
+
);
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Read a plugin's configuration contract and the user's stored values.
|
|
2852
|
+
*
|
|
2853
|
+
* Secrets are never returned: `write_only` lists the pointers that hold one.
|
|
2854
|
+
*/
|
|
2855
|
+
getSessionPluginConfig(sessionId, name, init) {
|
|
2856
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/config`, init);
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Publish explicit set/remove operations to the user's keychain file.
|
|
2860
|
+
*
|
|
2861
|
+
* Fields absent from both lists keep their stored value, so a form never has
|
|
2862
|
+
* to round-trip a secret. Rejections leave the previous file untouched; the
|
|
2863
|
+
* call never restarts MCP servers.
|
|
2864
|
+
*/
|
|
2865
|
+
updateSessionPluginConfig(sessionId, name, update, init) {
|
|
2866
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/plugins/${encodeURIComponent(name)}/config`, { method: "PUT", body: JSON.stringify(update), ...init });
|
|
2867
|
+
}
|
|
2405
2868
|
/** 创建新会话并直接连接,返回可收发消息的 AgentSession。 */
|
|
2406
2869
|
async create(request = {}) {
|
|
2407
2870
|
if (isSessionDefinition(request)) {
|
|
@@ -2498,7 +2961,8 @@ var SessionsResource = class {
|
|
|
2498
2961
|
template_id_prefix,
|
|
2499
2962
|
solution_id,
|
|
2500
2963
|
solution_registry_id,
|
|
2501
|
-
q
|
|
2964
|
+
q,
|
|
2965
|
+
chat_group_id
|
|
2502
2966
|
}) {
|
|
2503
2967
|
const searchParams = new URLSearchParams();
|
|
2504
2968
|
searchParams.set("limit", String(limit));
|
|
@@ -2511,6 +2975,7 @@ var SessionsResource = class {
|
|
|
2511
2975
|
}
|
|
2512
2976
|
const trimmedQuery = q?.trim();
|
|
2513
2977
|
if (trimmedQuery) searchParams.set("q", trimmedQuery);
|
|
2978
|
+
if (chat_group_id) searchParams.set("chat_group_id", chat_group_id);
|
|
2514
2979
|
return this.client.json("GET", `/api/sessions?${searchParams.toString()}`);
|
|
2515
2980
|
}
|
|
2516
2981
|
async listSessions(template_id_prefix, solution_id) {
|
|
@@ -2601,6 +3066,55 @@ var SessionsResource = class {
|
|
|
2601
3066
|
);
|
|
2602
3067
|
return projectHistory(entries);
|
|
2603
3068
|
}
|
|
3069
|
+
async getSessionTurnsPage(sessionId, options = {}, init = {}) {
|
|
3070
|
+
const query = new URLSearchParams();
|
|
3071
|
+
if (options.limit !== void 0) query.set("limit", String(options.limit));
|
|
3072
|
+
if (options.before !== void 0) query.set("before", options.before);
|
|
3073
|
+
if (options.fromEntryId !== void 0) query.set("from_entry_id", options.fromEntryId);
|
|
3074
|
+
const suffix = query.size ? `?${query}` : "";
|
|
3075
|
+
const page = await this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/turns${suffix}`, init);
|
|
3076
|
+
return {
|
|
3077
|
+
turns: page.turns,
|
|
3078
|
+
nextBefore: page.next_before,
|
|
3079
|
+
currentMode: page.current_mode
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* 当前分支上可导航的用户发言目录(消息导航侧栏)。
|
|
3084
|
+
*
|
|
3085
|
+
* 只回 id、标题和时间:侧栏要列的是**整条会话**的发言,而窗口外的发言 id 只能
|
|
3086
|
+
* 从服务端拿;消息体仍然走 `/turns`。
|
|
3087
|
+
*/
|
|
3088
|
+
async getSessionTurnIndex(sessionId) {
|
|
3089
|
+
const index = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turn-index`);
|
|
3090
|
+
return {
|
|
3091
|
+
sessionId: index.session_id,
|
|
3092
|
+
items: index.items.map((item) => ({
|
|
3093
|
+
entryId: item.entry_id,
|
|
3094
|
+
preview: item.preview,
|
|
3095
|
+
createdAt: item.created_at
|
|
3096
|
+
}))
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
getMcpAppContext(sessionId, sourceId, init) {
|
|
3100
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/context`, init);
|
|
3101
|
+
}
|
|
3102
|
+
updateMcpAppContext(sessionId, sourceId, body, init) {
|
|
3103
|
+
return this.client.jsonFromInit(`/api/sessions/${encodeURIComponent(sessionId)}/mcp-apps/${encodeURIComponent(sourceId)}/context`, {
|
|
3104
|
+
...init,
|
|
3105
|
+
method: "POST",
|
|
3106
|
+
body: JSON.stringify(body)
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
async getSessionTurnCommands(sessionId) {
|
|
3110
|
+
const commands = await this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/turn-commands`);
|
|
3111
|
+
return commands.map((command) => ({
|
|
3112
|
+
deliveryId: command.delivery_id,
|
|
3113
|
+
toolCallId: command.tool_call_id ?? void 0,
|
|
3114
|
+
action: command.action,
|
|
3115
|
+
payload: command.payload
|
|
3116
|
+
}));
|
|
3117
|
+
}
|
|
2604
3118
|
listResultFeedback(sessionId) {
|
|
2605
3119
|
return this.client.json("GET", `/api/sessions/${encodeURIComponent(sessionId)}/feedback`);
|
|
2606
3120
|
}
|
|
@@ -2668,8 +3182,9 @@ var SessionsResource = class {
|
|
|
2668
3182
|
`/api/sessions/${sessionId}/probe-url?url=${encodeURIComponent(url)}`
|
|
2669
3183
|
);
|
|
2670
3184
|
}
|
|
2671
|
-
listDir(sessionId, dirPath) {
|
|
2672
|
-
|
|
3185
|
+
listDir(sessionId, dirPath, showHidden = false) {
|
|
3186
|
+
const query = showHidden ? "?show_hidden=true" : "";
|
|
3187
|
+
return this.client.json("GET", `/api/sessions/${sessionId}/ls/${encodeURIComponent(dirPath)}${query}`);
|
|
2673
3188
|
}
|
|
2674
3189
|
async countFiles(sessionId, dirPath = "") {
|
|
2675
3190
|
const entries = await this.listDir(sessionId, dirPath);
|
|
@@ -2864,6 +3379,130 @@ function profileToCreateSessionRequest(profile) {
|
|
|
2864
3379
|
};
|
|
2865
3380
|
}
|
|
2866
3381
|
|
|
3382
|
+
// src/resources/plugins.ts
|
|
3383
|
+
var PluginsResource = class {
|
|
3384
|
+
constructor(client) {
|
|
3385
|
+
this.client = client;
|
|
3386
|
+
}
|
|
3387
|
+
client;
|
|
3388
|
+
/**
|
|
3389
|
+
* 列出当前用户安装的插件及其展示信息。
|
|
3390
|
+
*
|
|
3391
|
+
* 服务端可能为了补齐名称/图标惰性获取包内容,但绝不激活插件、不连 MCP;
|
|
3392
|
+
* 单个插件的展示信息读不出来只会降级成占位,不会让整份目录失败。
|
|
3393
|
+
*/
|
|
3394
|
+
catalog(init) {
|
|
3395
|
+
return this.client.jsonFromInit("/api/plugins/catalog", init);
|
|
3396
|
+
}
|
|
3397
|
+
/** 取一个插件的包内图标字节(用户级,不需要会话)。 */
|
|
3398
|
+
fetchIcon(name) {
|
|
3399
|
+
return this.client.blob("GET", `/api/plugins/${encodeURIComponent(name)}/icon`);
|
|
3400
|
+
}
|
|
3401
|
+
/** 带凭据的图标地址;能直接用 blob URL 的地方优先用 `fetchIcon`。 */
|
|
3402
|
+
iconUrl(name) {
|
|
3403
|
+
return this.client.buildAuthedUrl(`/api/plugins/${encodeURIComponent(name)}/icon`);
|
|
3404
|
+
}
|
|
3405
|
+
};
|
|
3406
|
+
|
|
3407
|
+
// src/resources/chat-groups.ts
|
|
3408
|
+
var ChatGroupsResource = class {
|
|
3409
|
+
constructor(client) {
|
|
3410
|
+
this.client = client;
|
|
3411
|
+
}
|
|
3412
|
+
client;
|
|
3413
|
+
list() {
|
|
3414
|
+
return this.client.json("GET", "/api/chat-groups");
|
|
3415
|
+
}
|
|
3416
|
+
create(payload) {
|
|
3417
|
+
return this.client.json("POST", "/api/chat-groups", payload);
|
|
3418
|
+
}
|
|
3419
|
+
update(groupId, payload) {
|
|
3420
|
+
return this.client.json("PATCH", `/api/chat-groups/${encodeURIComponent(groupId)}`, payload);
|
|
3421
|
+
}
|
|
3422
|
+
delete(groupId, options) {
|
|
3423
|
+
const query = options?.deleteWorkspace ? "?delete_workspace=true" : "";
|
|
3424
|
+
return this.client.json("DELETE", `/api/chat-groups/${encodeURIComponent(groupId)}${query}`);
|
|
3425
|
+
}
|
|
3426
|
+
};
|
|
3427
|
+
|
|
3428
|
+
// src/resources/site-visits.ts
|
|
3429
|
+
var SITE_VISITS_PATH = "/api/site-visits";
|
|
3430
|
+
var REQUEST_TIMEOUT_MS = 5e3;
|
|
3431
|
+
var SiteVisitsResource = class {
|
|
3432
|
+
constructor(client) {
|
|
3433
|
+
this.client = client;
|
|
3434
|
+
}
|
|
3435
|
+
client;
|
|
3436
|
+
/** 上报一次页面访问,返回当前快照;不承诺本次已计入或已落盘。 */
|
|
3437
|
+
record() {
|
|
3438
|
+
return this.request("POST");
|
|
3439
|
+
}
|
|
3440
|
+
/** 只读快照:不设置访客 Cookie,也不增加任何计数。 */
|
|
3441
|
+
summary() {
|
|
3442
|
+
return this.request("GET");
|
|
3443
|
+
}
|
|
3444
|
+
async request(method) {
|
|
3445
|
+
const url = buildBackendUrl(this.client.options.baseUrl, SITE_VISITS_PATH);
|
|
3446
|
+
const controller = new AbortController();
|
|
3447
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
3448
|
+
try {
|
|
3449
|
+
const response = await (this.client.options.fetchImpl ?? fetch)(url.toString(), {
|
|
3450
|
+
method,
|
|
3451
|
+
// 访客 Cookie 是唯一的去重依据,必须随请求往返。
|
|
3452
|
+
credentials: "include",
|
|
3453
|
+
signal: controller.signal
|
|
3454
|
+
});
|
|
3455
|
+
if (!response.ok) {
|
|
3456
|
+
throw new BladeApiError(response, await extractErrorDetail(response));
|
|
3457
|
+
}
|
|
3458
|
+
return await response.json();
|
|
3459
|
+
} finally {
|
|
3460
|
+
clearTimeout(timeout);
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
};
|
|
3464
|
+
|
|
3465
|
+
// src/shared/auth-busy.ts
|
|
3466
|
+
var DEFAULT_RETRY_AFTER_SECONDS = 1;
|
|
3467
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
3468
|
+
function errorData(error) {
|
|
3469
|
+
if (typeof error !== "object" || error === null || !("data" in error)) return null;
|
|
3470
|
+
const data = error.data;
|
|
3471
|
+
return typeof data === "object" && data !== null ? data : null;
|
|
3472
|
+
}
|
|
3473
|
+
function authBusyRetryDelayMs(error) {
|
|
3474
|
+
const data = errorData(error);
|
|
3475
|
+
if (data?.code !== "auth_busy" || Number(data.status) !== 429) return null;
|
|
3476
|
+
const rawRetryAfter = data.retry_after;
|
|
3477
|
+
const seconds = rawRetryAfter == null || String(rawRetryAfter).trim() === "" ? Number.NaN : Number(rawRetryAfter);
|
|
3478
|
+
const delaySeconds = Number.isFinite(seconds) && seconds >= 0 ? seconds : DEFAULT_RETRY_AFTER_SECONDS;
|
|
3479
|
+
return Math.min(delaySeconds * 1e3, MAX_TIMER_DELAY_MS);
|
|
3480
|
+
}
|
|
3481
|
+
var AuthBusyReconnect = class {
|
|
3482
|
+
timer = null;
|
|
3483
|
+
get pending() {
|
|
3484
|
+
return this.timer !== null;
|
|
3485
|
+
}
|
|
3486
|
+
schedule(error, reconnect) {
|
|
3487
|
+
const delay = authBusyRetryDelayMs(error);
|
|
3488
|
+
if (delay === null) {
|
|
3489
|
+
this.cancel();
|
|
3490
|
+
return false;
|
|
3491
|
+
}
|
|
3492
|
+
this.cancel();
|
|
3493
|
+
this.timer = globalThis.setTimeout(() => {
|
|
3494
|
+
this.timer = null;
|
|
3495
|
+
reconnect();
|
|
3496
|
+
}, delay);
|
|
3497
|
+
return true;
|
|
3498
|
+
}
|
|
3499
|
+
cancel() {
|
|
3500
|
+
if (this.timer === null) return;
|
|
3501
|
+
globalThis.clearTimeout(this.timer);
|
|
3502
|
+
this.timer = null;
|
|
3503
|
+
}
|
|
3504
|
+
};
|
|
3505
|
+
|
|
2867
3506
|
// src/commands/registry.ts
|
|
2868
3507
|
var BUFFER_LIMIT_PER_ACTION = 20;
|
|
2869
3508
|
var CommandRegistry = class {
|
|
@@ -2972,6 +3611,172 @@ var Emitter = class {
|
|
|
2972
3611
|
}
|
|
2973
3612
|
};
|
|
2974
3613
|
|
|
3614
|
+
// src/session/history-page.ts
|
|
3615
|
+
function sortedUnique(turns) {
|
|
3616
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3617
|
+
for (const turn of turns) byId.set(turn.turn_id, turn);
|
|
3618
|
+
return [...byId.values()].sort((left, right) => left.sequence - right.sequence);
|
|
3619
|
+
}
|
|
3620
|
+
function prependOlder(current, page) {
|
|
3621
|
+
const currentById = new Map(current.map((turn) => [turn.turn_id, turn]));
|
|
3622
|
+
return sortedUnique([
|
|
3623
|
+
...current,
|
|
3624
|
+
...page.map((turn) => {
|
|
3625
|
+
const live = currentById.get(turn.turn_id);
|
|
3626
|
+
return live ? { ...live, sequence: turn.sequence } : turn;
|
|
3627
|
+
})
|
|
3628
|
+
]);
|
|
3629
|
+
}
|
|
3630
|
+
function replaceWindow(current, page, liveRevisionAtStart, liveRevisions) {
|
|
3631
|
+
const pageById = new Map(page.map((turn) => [turn.turn_id, turn]));
|
|
3632
|
+
const newerLive = current.filter(
|
|
3633
|
+
(turn) => turn.turn_id.startsWith("local-user-") || (liveRevisions.byTurnId.get(turn.turn_id) ?? 0) > liveRevisionAtStart
|
|
3634
|
+
);
|
|
3635
|
+
return sortedUnique([
|
|
3636
|
+
...page,
|
|
3637
|
+
...newerLive.map((turn) => {
|
|
3638
|
+
const snapshot2 = pageById.get(turn.turn_id);
|
|
3639
|
+
return snapshot2 ? { ...turn, sequence: snapshot2.sequence } : turn;
|
|
3640
|
+
})
|
|
3641
|
+
]);
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
// src/session/queue.ts
|
|
3645
|
+
var EMPTY_SESSION_QUEUE = {
|
|
3646
|
+
session_id: "",
|
|
3647
|
+
revision: 0,
|
|
3648
|
+
paused: false,
|
|
3649
|
+
pause_reason: null,
|
|
3650
|
+
items: []
|
|
3651
|
+
};
|
|
3652
|
+
var QUEUE_FAILURE_MESSAGE = "\u961F\u5217\u64CD\u4F5C\u5931\u8D25";
|
|
3653
|
+
var QUEUED_MESSAGE_STATUSES = [
|
|
3654
|
+
"pending",
|
|
3655
|
+
"delivering",
|
|
3656
|
+
"claimed",
|
|
3657
|
+
"consumed",
|
|
3658
|
+
"cancelled"
|
|
3659
|
+
];
|
|
3660
|
+
function toQueuedMessage(value) {
|
|
3661
|
+
if (!isRecord(value)) return null;
|
|
3662
|
+
const status = typeof value.status === "string" && QUEUED_MESSAGE_STATUSES.includes(value.status) ? value.status : null;
|
|
3663
|
+
if (typeof value.id !== "string" || typeof value.content !== "string" || typeof value.position !== "number" || typeof value.version !== "number" || typeof value.created_at !== "string" || typeof value.updated_at !== "string" || status === null) {
|
|
3664
|
+
return null;
|
|
3665
|
+
}
|
|
3666
|
+
const item = {
|
|
3667
|
+
id: value.id,
|
|
3668
|
+
content: value.content,
|
|
3669
|
+
position: value.position,
|
|
3670
|
+
version: value.version,
|
|
3671
|
+
status,
|
|
3672
|
+
created_at: value.created_at,
|
|
3673
|
+
updated_at: value.updated_at
|
|
3674
|
+
};
|
|
3675
|
+
if (typeof value.target_run_id === "string") item.target_run_id = value.target_run_id;
|
|
3676
|
+
if (typeof value.error === "string") item.error = value.error;
|
|
3677
|
+
return item;
|
|
3678
|
+
}
|
|
3679
|
+
function toQueueSnapshot(value, fallbackSessionId) {
|
|
3680
|
+
if (!isRecord(value)) return null;
|
|
3681
|
+
if (typeof value.revision !== "number" || !Number.isInteger(value.revision) || value.revision < 0) {
|
|
3682
|
+
return null;
|
|
3683
|
+
}
|
|
3684
|
+
if (typeof value.paused !== "boolean") return null;
|
|
3685
|
+
if (!Array.isArray(value.items)) return null;
|
|
3686
|
+
const pauseReason = value.pause_reason;
|
|
3687
|
+
if (pauseReason !== null && pauseReason !== void 0 && typeof pauseReason !== "string") {
|
|
3688
|
+
return null;
|
|
3689
|
+
}
|
|
3690
|
+
const items = [];
|
|
3691
|
+
for (const raw of value.items) {
|
|
3692
|
+
const item = toQueuedMessage(raw);
|
|
3693
|
+
if (!item) return null;
|
|
3694
|
+
items.push(item);
|
|
3695
|
+
}
|
|
3696
|
+
return {
|
|
3697
|
+
session_id: typeof value.session_id === "string" && value.session_id ? value.session_id : fallbackSessionId,
|
|
3698
|
+
revision: value.revision,
|
|
3699
|
+
paused: value.paused,
|
|
3700
|
+
pause_reason: typeof pauseReason === "string" ? pauseReason : null,
|
|
3701
|
+
items
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
function isSessionQueueSnapshot(value) {
|
|
3705
|
+
if (!isRecord(value) || typeof value.session_id !== "string") return false;
|
|
3706
|
+
return toQueueSnapshot(value, value.session_id) !== null;
|
|
3707
|
+
}
|
|
3708
|
+
function shouldApplySnapshot(current, next) {
|
|
3709
|
+
if (!isSessionQueueSnapshot(next)) return false;
|
|
3710
|
+
if (current?.session_id && next.session_id !== current.session_id) return false;
|
|
3711
|
+
return next.revision > (current?.revision ?? -1);
|
|
3712
|
+
}
|
|
3713
|
+
function parseQueueAck(response, fallbackSessionId) {
|
|
3714
|
+
if (!isRecord(response)) {
|
|
3715
|
+
return { ok: false, conflict: false, snapshot: null, message: QUEUE_FAILURE_MESSAGE };
|
|
3716
|
+
}
|
|
3717
|
+
const message = typeof response.message === "string" && response.message ? response.message : null;
|
|
3718
|
+
const code = typeof response.code === "string" && response.code ? response.code : void 0;
|
|
3719
|
+
const status = response.status;
|
|
3720
|
+
if (status !== "ok" && status !== "conflict" && status !== "queued") {
|
|
3721
|
+
return {
|
|
3722
|
+
ok: false,
|
|
3723
|
+
conflict: false,
|
|
3724
|
+
snapshot: null,
|
|
3725
|
+
message: message ?? QUEUE_FAILURE_MESSAGE,
|
|
3726
|
+
...code ? { code } : {}
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
const snapshot2 = toQueueSnapshot(response, fallbackSessionId);
|
|
3730
|
+
if (status === "conflict") {
|
|
3731
|
+
return {
|
|
3732
|
+
ok: false,
|
|
3733
|
+
conflict: true,
|
|
3734
|
+
snapshot: snapshot2,
|
|
3735
|
+
message: message ?? QUEUE_FAILURE_MESSAGE,
|
|
3736
|
+
...code ? { code } : {}
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
if (!snapshot2) {
|
|
3740
|
+
return {
|
|
3741
|
+
ok: false,
|
|
3742
|
+
conflict: false,
|
|
3743
|
+
snapshot: null,
|
|
3744
|
+
message: message ?? QUEUE_FAILURE_MESSAGE,
|
|
3745
|
+
...code ? { code } : {}
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
return { ok: true, conflict: false, snapshot: snapshot2, message, ...code ? { code } : {} };
|
|
3749
|
+
}
|
|
3750
|
+
function canEditQueuedMessage(item) {
|
|
3751
|
+
return item.status === "pending";
|
|
3752
|
+
}
|
|
3753
|
+
function canCancelQueuedMessage(item) {
|
|
3754
|
+
return item.status === "pending";
|
|
3755
|
+
}
|
|
3756
|
+
function canDeliverQueuedMessage(item) {
|
|
3757
|
+
return item.status === "pending";
|
|
3758
|
+
}
|
|
3759
|
+
var QUEUED_MESSAGE_STATUS_LABELS = {
|
|
3760
|
+
pending: "\u5F85\u6267\u884C",
|
|
3761
|
+
delivering: "\u7B49\u5F85\u63A5\u6536",
|
|
3762
|
+
claimed: "",
|
|
3763
|
+
consumed: "",
|
|
3764
|
+
cancelled: ""
|
|
3765
|
+
};
|
|
3766
|
+
function queuedMessageStatusLabel(status) {
|
|
3767
|
+
return QUEUED_MESSAGE_STATUS_LABELS[status] ?? "";
|
|
3768
|
+
}
|
|
3769
|
+
var QUEUE_PAUSE_REASON_LABELS = {
|
|
3770
|
+
user_stop: "\u5DF2\u6682\u505C",
|
|
3771
|
+
run_failed: "\u4EFB\u52A1\u5F02\u5E38\u5DF2\u6682\u505C",
|
|
3772
|
+
run_unknown: "\u4EFB\u52A1\u7ED3\u675F\u72B6\u6001\u672A\u77E5\uFF0C\u5DF2\u6682\u505C",
|
|
3773
|
+
server_restart: "\u670D\u52A1\u91CD\u542F\u5DF2\u6682\u505C"
|
|
3774
|
+
};
|
|
3775
|
+
function queuePauseReasonLabel(reason) {
|
|
3776
|
+
if (!reason) return null;
|
|
3777
|
+
return QUEUE_PAUSE_REASON_LABELS[reason] ?? reason;
|
|
3778
|
+
}
|
|
3779
|
+
|
|
2975
3780
|
// src/schemas/message-utils.ts
|
|
2976
3781
|
var SYSTEM_REMINDER_PATTERN = /^<system-reminder>\s*[\s\S]*?\s*<\/system-reminder>$/i;
|
|
2977
3782
|
var SYSTEM_NOTIFICATION_PATTERN = /^<system-notification>\s*[\s\S]*?\s*<\/system-notification>$/i;
|
|
@@ -3155,6 +3960,12 @@ function groupMessagesByLoop(messages) {
|
|
|
3155
3960
|
}
|
|
3156
3961
|
return groups;
|
|
3157
3962
|
}
|
|
3963
|
+
function isBackgroundWakeMessage(message) {
|
|
3964
|
+
return message.blocks?.some((block) => {
|
|
3965
|
+
const content = block.content;
|
|
3966
|
+
return block.type === "system_notification" && content !== null && typeof content === "object" && "notification_type" in content && content.notification_type === "bg:wakeup";
|
|
3967
|
+
}) ?? false;
|
|
3968
|
+
}
|
|
3158
3969
|
|
|
3159
3970
|
// src/session/state.ts
|
|
3160
3971
|
function toReplaySnapshot(raw) {
|
|
@@ -3179,7 +3990,10 @@ function createInitialSessionState(sessionId) {
|
|
|
3179
3990
|
agentLoops: {},
|
|
3180
3991
|
activeCompaction: null,
|
|
3181
3992
|
replay: null,
|
|
3182
|
-
|
|
3993
|
+
queue: EMPTY_SESSION_QUEUE,
|
|
3994
|
+
viewerRole: null,
|
|
3995
|
+
nextBefore: null,
|
|
3996
|
+
loadingOlder: false
|
|
3183
3997
|
};
|
|
3184
3998
|
}
|
|
3185
3999
|
function parseAgentDescription(argumentsJson) {
|
|
@@ -3205,10 +4019,10 @@ function inferLoopStatusFromMessages(messages) {
|
|
|
3205
4019
|
}
|
|
3206
4020
|
function inferLoopStatusFromTurns(turns, messages) {
|
|
3207
4021
|
const latestAgentNotification = [...turns].reverse().flatMap((turn) => turn.blocks).find((block) => {
|
|
3208
|
-
if (block.type !== "system_notification" || !
|
|
4022
|
+
if (block.type !== "system_notification" || !isRecord(block.content)) return false;
|
|
3209
4023
|
return block.content.notification_type === "agent:start" || block.content.notification_type === "agent:end";
|
|
3210
4024
|
});
|
|
3211
|
-
if (latestAgentNotification?.type === "system_notification" &&
|
|
4025
|
+
if (latestAgentNotification?.type === "system_notification" && isRecord(latestAgentNotification.content)) {
|
|
3212
4026
|
const notificationType = latestAgentNotification.content.notification_type;
|
|
3213
4027
|
const status = latestAgentNotification.content.status;
|
|
3214
4028
|
if (notificationType === "agent:start" || status === "running") return "running";
|
|
@@ -3217,11 +4031,8 @@ function inferLoopStatusFromTurns(turns, messages) {
|
|
|
3217
4031
|
}
|
|
3218
4032
|
return inferLoopStatusFromMessages(messages);
|
|
3219
4033
|
}
|
|
3220
|
-
function isRecord5(value) {
|
|
3221
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3222
|
-
}
|
|
3223
4034
|
function toSelectionMap(value) {
|
|
3224
|
-
if (!
|
|
4035
|
+
if (!isRecord(value)) return {};
|
|
3225
4036
|
const entries = Object.entries(value).map(([questionKey, optionIndexes]) => {
|
|
3226
4037
|
if (!Array.isArray(optionIndexes)) return null;
|
|
3227
4038
|
const parsedIndexes = optionIndexes.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item));
|
|
@@ -3230,7 +4041,7 @@ function toSelectionMap(value) {
|
|
|
3230
4041
|
return Object.fromEntries(entries);
|
|
3231
4042
|
}
|
|
3232
4043
|
function toCustomMap(value) {
|
|
3233
|
-
if (!
|
|
4044
|
+
if (!isRecord(value)) return {};
|
|
3234
4045
|
const entries = Object.entries(value).filter(([, text]) => typeof text === "string").map(([questionKey, text]) => [Number(questionKey), text]);
|
|
3235
4046
|
return Object.fromEntries(entries);
|
|
3236
4047
|
}
|
|
@@ -3239,7 +4050,7 @@ function extractAskAnswers(turns) {
|
|
|
3239
4050
|
for (const turn of turns) {
|
|
3240
4051
|
for (const block of turn.blocks) {
|
|
3241
4052
|
if (block.type !== "ask_user_answer" || typeof block.tool_call_id !== "string") continue;
|
|
3242
|
-
if (!
|
|
4053
|
+
if (!isRecord(block.content)) continue;
|
|
3243
4054
|
const note = typeof block.content.note === "string" ? block.content.note.trim() : "";
|
|
3244
4055
|
answers[block.tool_call_id] = {
|
|
3245
4056
|
selections: toSelectionMap(block.content.selections),
|
|
@@ -3255,9 +4066,9 @@ function parentForkToolCallIdFromTurn(turn) {
|
|
|
3255
4066
|
return turn.parent_fork_tool_call_id;
|
|
3256
4067
|
}
|
|
3257
4068
|
for (const block of turn.blocks) {
|
|
3258
|
-
if (block.type !== "system_notification" || !
|
|
4069
|
+
if (block.type !== "system_notification" || !isRecord(block.content)) continue;
|
|
3259
4070
|
const metadata = block.content.metadata;
|
|
3260
|
-
if (!
|
|
4071
|
+
if (!isRecord(metadata)) continue;
|
|
3261
4072
|
const parentId = metadata.parent_fork_tool_call_id;
|
|
3262
4073
|
if (typeof parentId === "string" && parentId.length > 0) return parentId;
|
|
3263
4074
|
}
|
|
@@ -3274,16 +4085,16 @@ function buildMessageContent2(turn) {
|
|
|
3274
4085
|
}
|
|
3275
4086
|
function workspaceNotificationContent(turn) {
|
|
3276
4087
|
const block = turn.blocks.find(
|
|
3277
|
-
(candidate) => candidate.type === "system_notification" &&
|
|
4088
|
+
(candidate) => candidate.type === "system_notification" && isRecord(candidate.content) && candidate.content.notification_type === "workspace_change"
|
|
3278
4089
|
);
|
|
3279
|
-
if (!block || !
|
|
4090
|
+
if (!block || !isRecord(block.content)) return "";
|
|
3280
4091
|
const title = typeof block.content.title === "string" ? block.content.title.trim() : "";
|
|
3281
4092
|
const detail = typeof block.content.detail === "string" ? block.content.detail.trim() : "";
|
|
3282
4093
|
return [title ? `**${title}**` : "", detail].filter(Boolean).join("\n\n");
|
|
3283
4094
|
}
|
|
3284
4095
|
function askUserAnswerContent(turn) {
|
|
3285
4096
|
const answerBlock = turn.blocks.find((block) => block.type === "ask_user_answer");
|
|
3286
|
-
if (!answerBlock || !
|
|
4097
|
+
if (!answerBlock || !isRecord(answerBlock.content)) return null;
|
|
3287
4098
|
const answer = answerBlock.content.answer;
|
|
3288
4099
|
return typeof answer === "string" && answer.trim().length > 0 ? answer : null;
|
|
3289
4100
|
}
|
|
@@ -3486,21 +4297,58 @@ function orderTurns(turns) {
|
|
|
3486
4297
|
return turns;
|
|
3487
4298
|
}
|
|
3488
4299
|
function reconcileOptimisticUserTurns(currentTurns, incomingTurns) {
|
|
3489
|
-
|
|
3490
|
-
|
|
4300
|
+
return reconcileUserTurns(currentTurns, incomingTurns, true, false);
|
|
4301
|
+
}
|
|
4302
|
+
function reconcileHistoricalUserTurns(currentTurns, incomingTurns) {
|
|
4303
|
+
return reconcileUserTurns(currentTurns, incomingTurns, false, true);
|
|
4304
|
+
}
|
|
4305
|
+
function reconcileUserTurns(currentTurns, incomingTurns, allowLiveFallback, keepUnclaimedOptimistic) {
|
|
4306
|
+
const preservedTurns = preserveClientRenderIds(currentTurns, incomingTurns);
|
|
4307
|
+
const optimisticUserTurns = currentTurns.filter(
|
|
3491
4308
|
(turn) => turn.role === "user" && (turn.kind ?? "message") === "message" && turn.turn_id.startsWith("local-user-")
|
|
3492
4309
|
);
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
4310
|
+
const optimisticByRequestId = new Map(
|
|
4311
|
+
optimisticUserTurns.map((turn) => [
|
|
4312
|
+
turn.turn_id.slice("local-user-".length),
|
|
4313
|
+
turn
|
|
4314
|
+
])
|
|
4315
|
+
);
|
|
4316
|
+
const currentTurnIds = new Set(currentTurns.map((turn) => turn.turn_id));
|
|
4317
|
+
const claimedOptimisticIds = /* @__PURE__ */ new Set();
|
|
4318
|
+
const reconciled = preservedTurns.map((turn) => {
|
|
4319
|
+
const exactOptimistic = typeof turn.client_request_id === "string" ? optimisticByRequestId.get(turn.client_request_id) : void 0;
|
|
4320
|
+
if (exactOptimistic && !claimedOptimisticIds.has(exactOptimistic.turn_id) && isUserConfirmation(turn)) {
|
|
4321
|
+
claimedOptimisticIds.add(exactOptimistic.turn_id);
|
|
4322
|
+
return { ...turn, client_render_id: exactOptimistic.turn_id };
|
|
4323
|
+
}
|
|
4324
|
+
const fallbackOptimistic = optimisticUserTurns.find(
|
|
4325
|
+
(candidate) => !claimedOptimisticIds.has(candidate.turn_id)
|
|
4326
|
+
);
|
|
4327
|
+
if (allowLiveFallback && typeof turn.client_request_id !== "string" && fallbackOptimistic && isUserConfirmation(turn) && !turn.turn_id.startsWith("local-user-") && !currentTurnIds.has(turn.turn_id)) {
|
|
4328
|
+
claimedOptimisticIds.add(fallbackOptimistic.turn_id);
|
|
4329
|
+
return { ...turn, client_render_id: fallbackOptimistic.turn_id };
|
|
4330
|
+
}
|
|
4331
|
+
return turn;
|
|
4332
|
+
});
|
|
4333
|
+
const result = claimedOptimisticIds.size === 0 ? reconciled : reconciled.filter((turn) => !claimedOptimisticIds.has(turn.turn_id));
|
|
4334
|
+
if (!keepUnclaimedOptimistic) return result;
|
|
4335
|
+
const resultIds = new Set(result.map((turn) => turn.turn_id));
|
|
4336
|
+
const unclaimedOptimisticTurns = optimisticUserTurns.filter(
|
|
4337
|
+
(turn) => !claimedOptimisticIds.has(turn.turn_id) && !resultIds.has(turn.turn_id)
|
|
4338
|
+
);
|
|
4339
|
+
return unclaimedOptimisticTurns.length === 0 ? result : orderTurns([...result, ...unclaimedOptimisticTurns]);
|
|
4340
|
+
}
|
|
4341
|
+
function isUserConfirmation(turn) {
|
|
4342
|
+
return turn.role === "user" && (turn.kind ?? "message") === "message" || turn.blocks.some((block) => block.type === "ask_user_answer");
|
|
4343
|
+
}
|
|
4344
|
+
function preserveClientRenderIds(currentTurns, incomingTurns) {
|
|
4345
|
+
const currentById = new Map(currentTurns.map((turn) => [turn.turn_id, turn]));
|
|
4346
|
+
return incomingTurns.map((turn) => {
|
|
4347
|
+
const current = currentById.get(turn.turn_id);
|
|
3496
4348
|
if (current) {
|
|
3497
4349
|
const currentRenderId = current.client_render_id;
|
|
3498
4350
|
return turn.client_render_id || !currentRenderId ? turn : { ...turn, client_render_id: currentRenderId };
|
|
3499
4351
|
}
|
|
3500
|
-
if (!optimisticUserMatched && optimisticUserTurn && turn.role === "user" && (turn.kind ?? "message") === "message" && !turn.turn_id.startsWith("local-user-")) {
|
|
3501
|
-
optimisticUserMatched = true;
|
|
3502
|
-
return { ...turn, client_render_id: optimisticUserTurn.turn_id };
|
|
3503
|
-
}
|
|
3504
4352
|
return turn;
|
|
3505
4353
|
});
|
|
3506
4354
|
}
|
|
@@ -3515,7 +4363,7 @@ function areAgentLoopsEqual(left, right) {
|
|
|
3515
4363
|
});
|
|
3516
4364
|
}
|
|
3517
4365
|
function withTurns(state, turns) {
|
|
3518
|
-
const orderedTurns = orderTurns(
|
|
4366
|
+
const orderedTurns = orderTurns(reconcileHistoricalUserTurns(state.turns, turns));
|
|
3519
4367
|
const { messages, agentLoops, activeCompaction } = materialize(orderedTurns);
|
|
3520
4368
|
const isWaitingForInput = orderedTurns.some(
|
|
3521
4369
|
(turn) => turn.tool_calls.some((toolCall) => toolCall.status === "awaiting_answer")
|
|
@@ -3556,14 +4404,22 @@ function withTurns(state, turns) {
|
|
|
3556
4404
|
status: isWaitingForInput ? "waiting_for_input" : state.status
|
|
3557
4405
|
};
|
|
3558
4406
|
}
|
|
4407
|
+
function withQueue(state, snapshot2) {
|
|
4408
|
+
if (!shouldApplySnapshot(state.queue, snapshot2)) return state;
|
|
4409
|
+
return { ...state, queue: snapshot2 };
|
|
4410
|
+
}
|
|
3559
4411
|
var localTurnCounter = 0;
|
|
3560
|
-
function addUserMessage(state, content) {
|
|
4412
|
+
function addUserMessage(state, content, clientRequestId) {
|
|
3561
4413
|
localTurnCounter += 1;
|
|
3562
|
-
const turnId = `local-user-${Date.now()}-${localTurnCounter}`;
|
|
4414
|
+
const turnId = `local-user-${clientRequestId ?? `${Date.now()}-${localTurnCounter}`}`;
|
|
3563
4415
|
const optimisticTurn = {
|
|
3564
4416
|
id: turnId,
|
|
3565
4417
|
sequence: Math.max(0, ...state.turns.map((turn) => turn.sequence)) + 1,
|
|
3566
4418
|
turn_id: turnId,
|
|
4419
|
+
// 乐观态就自赋,跟服务端确认后 reconcileUserTurns 补上的
|
|
4420
|
+
// client_render_id(= 乐观 turn 自己的 turn_id)保持同一个值,
|
|
4421
|
+
// 消费方才能拿它当"确认前后不变"的稳定标识使用。
|
|
4422
|
+
client_render_id: turnId,
|
|
3567
4423
|
loop_id: "root",
|
|
3568
4424
|
role: "user",
|
|
3569
4425
|
status: "streaming",
|
|
@@ -3578,10 +4434,9 @@ function addUserMessage(state, content) {
|
|
|
3578
4434
|
function upsertTurn(state, turn) {
|
|
3579
4435
|
let existing = [...state.turns];
|
|
3580
4436
|
const [nextTurn] = reconcileOptimisticUserTurns(existing, [turn]);
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
);
|
|
4437
|
+
const claimedOptimisticId = nextTurn.client_render_id;
|
|
4438
|
+
if (claimedOptimisticId?.startsWith("local-user-")) {
|
|
4439
|
+
existing = existing.filter((item) => item.turn_id !== claimedOptimisticId);
|
|
3585
4440
|
}
|
|
3586
4441
|
const index = existing.findIndex((item) => item.turn_id === nextTurn.turn_id);
|
|
3587
4442
|
if (index >= 0) {
|
|
@@ -3589,7 +4444,7 @@ function upsertTurn(state, turn) {
|
|
|
3589
4444
|
} else {
|
|
3590
4445
|
existing.push(nextTurn);
|
|
3591
4446
|
}
|
|
3592
|
-
return withTurns(state, existing);
|
|
4447
|
+
return withTurns({ ...state, turns: existing }, existing);
|
|
3593
4448
|
}
|
|
3594
4449
|
function applyTurnPatch(state, patch) {
|
|
3595
4450
|
const turn = patch.data.turn;
|
|
@@ -3600,13 +4455,7 @@ function applyTurnPatch(state, patch) {
|
|
|
3600
4455
|
if (lastSequence !== null && patch.sequence <= lastSequence) {
|
|
3601
4456
|
return state;
|
|
3602
4457
|
}
|
|
3603
|
-
|
|
3604
|
-
if (index >= 0) {
|
|
3605
|
-
existing[index] = nextTurn;
|
|
3606
|
-
} else {
|
|
3607
|
-
existing.push(nextTurn);
|
|
3608
|
-
}
|
|
3609
|
-
return withTurns(state, existing);
|
|
4458
|
+
return upsertTurn(state, { ...turn, sequence: patch.sequence });
|
|
3610
4459
|
}
|
|
3611
4460
|
function addErrorMessage(state, content) {
|
|
3612
4461
|
const anchorTurnId = state.turns[state.turns.length - 1]?.turn_id ?? null;
|
|
@@ -3636,21 +4485,49 @@ function markStreamingTurns(state, turnStatus, toolCallStatus) {
|
|
|
3636
4485
|
return withTurns(state, turns);
|
|
3637
4486
|
}
|
|
3638
4487
|
|
|
4488
|
+
// src/session/errors.ts
|
|
4489
|
+
var AgentSessionError = class extends Error {
|
|
4490
|
+
name = "AgentSessionError";
|
|
4491
|
+
code;
|
|
4492
|
+
constructor(message, code) {
|
|
4493
|
+
super(message);
|
|
4494
|
+
this.code = code;
|
|
4495
|
+
}
|
|
4496
|
+
};
|
|
4497
|
+
|
|
3639
4498
|
// src/session/agent-session.ts
|
|
3640
4499
|
var OOM_MESSAGE = "\u6C99\u76D2\u5185\u5B58\u4F7F\u7528\u8D85\u51FA\u9650\u5236\uFF0C\u5DF2\u81EA\u52A8\u91CD\u542F\u3002\u5982\u679C\u7ECF\u5E38\u89E6\u53D1\uFF0C\u53EF\u4EE5\u8054\u7CFB\u7BA1\u7406\u5458\u8C03\u6574\u989D\u5EA6\u3002";
|
|
3641
|
-
var
|
|
3642
|
-
|
|
3643
|
-
|
|
4500
|
+
var COMMAND_RESTORE_RETRY_BASE_MS = 1e3;
|
|
4501
|
+
var COMMAND_RESTORE_RETRY_MAX_MS = 3e4;
|
|
4502
|
+
var QUEUE_ACK_TIMEOUT_MS = 1e4;
|
|
4503
|
+
var fallbackRequestCounter = 0;
|
|
4504
|
+
function createClientRequestId() {
|
|
4505
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
4506
|
+
return globalThis.crypto.randomUUID();
|
|
4507
|
+
}
|
|
4508
|
+
fallbackRequestCounter += 1;
|
|
4509
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}-${fallbackRequestCounter}`;
|
|
3644
4510
|
}
|
|
4511
|
+
var OOM_KEYWORDS_RE = /(?:\b(?:exit_code|ExitedWith|exited with)|退出码)\D*(?<!\d)137(?!\d)/i;
|
|
3645
4512
|
function isOomText(value) {
|
|
3646
4513
|
return typeof value === "string" && OOM_KEYWORDS_RE.test(value);
|
|
3647
4514
|
}
|
|
4515
|
+
function describeSessionError(error, fallback) {
|
|
4516
|
+
if (error instanceof AgentSessionError) {
|
|
4517
|
+
return {
|
|
4518
|
+
message: error.message || fallback,
|
|
4519
|
+
...error.code ? { code: error.code } : {}
|
|
4520
|
+
};
|
|
4521
|
+
}
|
|
4522
|
+
if (error instanceof Error && error.message) return { message: error.message };
|
|
4523
|
+
return { message: fallback };
|
|
4524
|
+
}
|
|
3648
4525
|
function parseJsonRecord(value) {
|
|
3649
|
-
if (
|
|
4526
|
+
if (isRecord(value)) return value;
|
|
3650
4527
|
if (typeof value !== "string") return null;
|
|
3651
4528
|
try {
|
|
3652
4529
|
const parsed = JSON.parse(value);
|
|
3653
|
-
return
|
|
4530
|
+
return isRecord(parsed) ? parsed : null;
|
|
3654
4531
|
} catch {
|
|
3655
4532
|
return null;
|
|
3656
4533
|
}
|
|
@@ -3676,6 +4553,21 @@ function isOomResult(value) {
|
|
|
3676
4553
|
function isToolBridgeContent(value) {
|
|
3677
4554
|
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.action === "string";
|
|
3678
4555
|
}
|
|
4556
|
+
function isCursorInvalidError(error) {
|
|
4557
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 409;
|
|
4558
|
+
}
|
|
4559
|
+
function queueRequestFailure(error) {
|
|
4560
|
+
const failure = error instanceof AgentSessionError ? { message: error.message, ...error.code ? { code: error.code } : {} } : { message: error instanceof Error && error.message ? error.message : "\u961F\u5217\u64CD\u4F5C\u5931\u8D25" };
|
|
4561
|
+
return {
|
|
4562
|
+
ok: false,
|
|
4563
|
+
conflict: false,
|
|
4564
|
+
snapshot: null,
|
|
4565
|
+
...failure
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
function commandDeliveryKey(deliveryId) {
|
|
4569
|
+
return `tool-command:${deliveryId}`;
|
|
4570
|
+
}
|
|
3679
4571
|
function sessionStatusFromChatEnd(status) {
|
|
3680
4572
|
if (status === "paused") return "waiting_for_input";
|
|
3681
4573
|
if (status === "interrupted") return "interrupted";
|
|
@@ -3691,14 +4583,6 @@ function extractTerminalChatEndStatus(events) {
|
|
|
3691
4583
|
}
|
|
3692
4584
|
return null;
|
|
3693
4585
|
}
|
|
3694
|
-
function hasChatRunEvent2(events) {
|
|
3695
|
-
return events.some(
|
|
3696
|
-
(event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
|
|
3697
|
-
);
|
|
3698
|
-
}
|
|
3699
|
-
function isUiMetaLike(value) {
|
|
3700
|
-
return isRecord6(value) && ("resourceHTML" in value || "resourceUri" in value || "resourceURI" in value);
|
|
3701
|
-
}
|
|
3702
4586
|
var AgentSession = class _AgentSession {
|
|
3703
4587
|
sessionId;
|
|
3704
4588
|
state;
|
|
@@ -3716,15 +4600,50 @@ var AgentSession = class _AgentSession {
|
|
|
3716
4600
|
// 状态并临时设置 replay source,禁止并发,否则较早操作的 finally 会清掉
|
|
3717
4601
|
// 较晚操作仍在使用的来源上下文。
|
|
3718
4602
|
subscriptionQueue = Promise.resolve();
|
|
4603
|
+
replaceQueue = Promise.resolve();
|
|
4604
|
+
timelineEpoch = 0;
|
|
4605
|
+
/** 定位请求的流水号:被后一次选择取代的那次定位不再提交窗口(见 locateEntry)。 */
|
|
4606
|
+
locateToken = 0;
|
|
4607
|
+
/** 在途那次定位的请求句柄。**只**服务定位:权威刷新不共用它(见 cancelPendingLocate)。 */
|
|
4608
|
+
locateAbort = null;
|
|
4609
|
+
pagingGeneration = 0;
|
|
4610
|
+
liveRevision = 0;
|
|
4611
|
+
modeRevision = 0;
|
|
4612
|
+
liveRevisions = /* @__PURE__ */ new Map();
|
|
4613
|
+
pendingOptimisticTurnIds = /* @__PURE__ */ new Set();
|
|
4614
|
+
olderPending = null;
|
|
4615
|
+
replaceInFlight = 0;
|
|
3719
4616
|
// 回放控制请求排成一条队列。连着点"5x"再点"退出回放"时,并发发出去的两个
|
|
3720
4617
|
// 请求响应可能乱序回来,晚到的旧值会把已经退出的会话又显示成回放中;用
|
|
3721
4618
|
// 序号丢弃过期响应又会在失败时留下中间态。串行执行就没有这些情况:每个
|
|
3722
4619
|
// 响应都是当时最新的,失败也只影响自己。
|
|
3723
4620
|
replayQueue = Promise.resolve();
|
|
4621
|
+
// Historical snapshots and live commands share one delivery barrier. A
|
|
4622
|
+
// repair closes the barrier before joining, snapshots the raw command log
|
|
4623
|
+
// after the join, then releases buffered commands in socket arrival order.
|
|
4624
|
+
commandRestoreQueue = Promise.resolve();
|
|
4625
|
+
commandRecoveryGeneration = 0;
|
|
4626
|
+
commandRestoreRetryAttempt = 0;
|
|
4627
|
+
commandRestoreRetryHandle = null;
|
|
4628
|
+
bufferedCommandDeliveries = null;
|
|
3724
4629
|
pendingReplayMessage = null;
|
|
3725
4630
|
pendingReplayMode;
|
|
3726
4631
|
pendingReplayKbIds;
|
|
3727
4632
|
recentChatEndAt = 0;
|
|
4633
|
+
/**
|
|
4634
|
+
* 最近一次已经通过 `error` 事件投递过的服务端失败(`message` + `code`)。
|
|
4635
|
+
*
|
|
4636
|
+
* `chat:send` 被代理时服务端会先发 `system:error`、再用同一条失败回 ack,两条
|
|
4637
|
+
* 通道都会到达这里:前者走 `_handleSystemError`,后者走发送失败的回滚。不去重
|
|
4638
|
+
* 的话调用方会收到两次同一个 `error`(重复提示、重试逻辑跑两遍)。
|
|
4639
|
+
*
|
|
4640
|
+
* 带代际边界,只认「本次 `emitWithAck` 等待期间」投递的那一条:否则队列/停止
|
|
4641
|
+
* 失败留下的标记会一直挂着,之后一次无关的订阅失败只是文案码相同就被当成
|
|
4642
|
+
* 重复而静音。用单调递增的代际号而不是时间戳,同一毫秒内的两条也不会混淆。
|
|
4643
|
+
*/
|
|
4644
|
+
lastDeliveredSystemError = null;
|
|
4645
|
+
/** 每次投递 `system:error` 递增,用于判定某条失败是否属于本次等待。 */
|
|
4646
|
+
systemErrorGeneration = 0;
|
|
3728
4647
|
subscriptionReplaySource = null;
|
|
3729
4648
|
disposed = false;
|
|
3730
4649
|
getAppContext = null;
|
|
@@ -3736,11 +4655,19 @@ var AgentSession = class _AgentSession {
|
|
|
3736
4655
|
seenToolCallIds = /* @__PURE__ */ new Set();
|
|
3737
4656
|
finishedToolCallIds = /* @__PURE__ */ new Set();
|
|
3738
4657
|
emittedMessageTurnIds = /* @__PURE__ */ new Set();
|
|
3739
|
-
constructor(sessionId, runtime, connection) {
|
|
4658
|
+
constructor(sessionId, runtime, connection, options = {}) {
|
|
3740
4659
|
this.sessionId = sessionId;
|
|
3741
4660
|
this.runtime = runtime;
|
|
3742
4661
|
this.state = { ...createInitialSessionState(sessionId), connection };
|
|
4662
|
+
this.isStatic = options.staticHistory === true;
|
|
3743
4663
|
}
|
|
4664
|
+
/**
|
|
4665
|
+
* 静态历史(分享页 / `fromTurns`):没有目录接口,也**没有**"目录还没到位"这个中间态。
|
|
4666
|
+
*
|
|
4667
|
+
* 宿主据此决定要不要拿已加载窗口当兜底条目——真实会话在 timeline 被重写后,窗口里还是
|
|
4668
|
+
* 旧分支的发言,拿它兜底会把已经失效的 entry id 重新摆到轨道上,点了必然找不到。
|
|
4669
|
+
*/
|
|
4670
|
+
isStatic;
|
|
3744
4671
|
/** 从一份静态 turn 快照构造只展示历史的会话(例如公开分享页)。 */
|
|
3745
4672
|
static fromTurns(sessionId, turns) {
|
|
3746
4673
|
const readonlyError = () => {
|
|
@@ -3752,7 +4679,10 @@ var AgentSession = class _AgentSession {
|
|
|
3752
4679
|
emitEvent: readonlyError,
|
|
3753
4680
|
emitWithAck: async () => readonlyError(),
|
|
3754
4681
|
ensureConnected: readonlyError,
|
|
3755
|
-
|
|
4682
|
+
fetchTurnsPage: async () => ({ turns, nextBefore: null, currentMode: "executing" }),
|
|
4683
|
+
// 静态会话(分享页)没有会话目录接口,宿主用已加载的历史兜底条目。
|
|
4684
|
+
fetchTurnIndex: async () => ({ sessionId, items: [] }),
|
|
4685
|
+
fetchCommands: async () => [],
|
|
3756
4686
|
// 静态历史没有回放身份可言,给个空详情即可,不要抛错——_bootstrap
|
|
3757
4687
|
// 不会跑到这里,但保持只读语义一致。
|
|
3758
4688
|
fetchSessionInfo: async () => ({ replay_state: null }),
|
|
@@ -3763,7 +4693,8 @@ var AgentSession = class _AgentSession {
|
|
|
3763
4693
|
attachAppCli: async () => readonlyError(),
|
|
3764
4694
|
onDispose: () => true
|
|
3765
4695
|
},
|
|
3766
|
-
"connected"
|
|
4696
|
+
"connected",
|
|
4697
|
+
{ staticHistory: true }
|
|
3767
4698
|
);
|
|
3768
4699
|
session.state = {
|
|
3769
4700
|
...withTurns(session.state, turns),
|
|
@@ -3776,6 +4707,101 @@ var AgentSession = class _AgentSession {
|
|
|
3776
4707
|
getState() {
|
|
3777
4708
|
return this.state;
|
|
3778
4709
|
}
|
|
4710
|
+
get hasOlderHistory() {
|
|
4711
|
+
return this.state.nextBefore !== null;
|
|
4712
|
+
}
|
|
4713
|
+
/** Load the page immediately preceding the current display window. */
|
|
4714
|
+
loadOlderHistory(options = {}) {
|
|
4715
|
+
if (this.disposed) return Promise.resolve();
|
|
4716
|
+
if (this.olderPending) return this.olderPending;
|
|
4717
|
+
if (this.replaceInFlight > 0) {
|
|
4718
|
+
return this.replaceQueue.then(() => this.loadOlderHistory(options));
|
|
4719
|
+
}
|
|
4720
|
+
const before = this.state.nextBefore;
|
|
4721
|
+
if (!before) return Promise.resolve();
|
|
4722
|
+
const epoch = this.timelineEpoch;
|
|
4723
|
+
const generation = this.pagingGeneration;
|
|
4724
|
+
this.update((state) => ({ ...state, loadingOlder: true }));
|
|
4725
|
+
const request = this.runtime.fetchTurnsPage(this.sessionId, { before }).then((page) => {
|
|
4726
|
+
if (epoch !== this.timelineEpoch || generation !== this.pagingGeneration || this.state.nextBefore !== before) return;
|
|
4727
|
+
options.beforeCommit?.();
|
|
4728
|
+
this.update((state) => ({
|
|
4729
|
+
...withTurns(state, prependOlder(state.turns, page.turns)),
|
|
4730
|
+
nextBefore: page.nextBefore,
|
|
4731
|
+
loadingOlder: false
|
|
4732
|
+
}));
|
|
4733
|
+
}).catch(async (error) => {
|
|
4734
|
+
if (isCursorInvalidError(error) && epoch === this.timelineEpoch && generation === this.pagingGeneration && this.state.nextBefore === before) {
|
|
4735
|
+
this.invalidateTimeline();
|
|
4736
|
+
await this.replaceFromHistory();
|
|
4737
|
+
return;
|
|
4738
|
+
}
|
|
4739
|
+
}).finally(() => {
|
|
4740
|
+
if (this.olderPending !== request) return;
|
|
4741
|
+
this.olderPending = null;
|
|
4742
|
+
this.update((state) => ({ ...state, loadingOlder: false }));
|
|
4743
|
+
});
|
|
4744
|
+
this.olderPending = request;
|
|
4745
|
+
return request;
|
|
4746
|
+
}
|
|
4747
|
+
/**
|
|
4748
|
+
* 定位到一条窗口外的历史发言:请服务端返回"从这条发言到最新"的窗口并整窗替换。
|
|
4749
|
+
*
|
|
4750
|
+
* 返回 false 表示这次定位没有落地(请求失败,或期间 timeline 被重写、会话已释放),
|
|
4751
|
+
* 当前窗口保持不变,调用方可以原样重试。已加载的发言不要走这里——宿主直接滚动,
|
|
4752
|
+
* 一次网络请求都不该发。
|
|
4753
|
+
*/
|
|
4754
|
+
async locateEntry(entryId) {
|
|
4755
|
+
const token = ++this.locateToken;
|
|
4756
|
+
const controller = new AbortController();
|
|
4757
|
+
this.locateAbort?.abort();
|
|
4758
|
+
this.locateAbort = controller;
|
|
4759
|
+
try {
|
|
4760
|
+
return await this.replaceFromHistory({
|
|
4761
|
+
suppressErrors: false,
|
|
4762
|
+
fromEntryId: entryId,
|
|
4763
|
+
signal: controller.signal,
|
|
4764
|
+
isStale: () => this.locateToken !== token
|
|
4765
|
+
});
|
|
4766
|
+
} catch {
|
|
4767
|
+
return false;
|
|
4768
|
+
} finally {
|
|
4769
|
+
if (this.locateAbort === controller) this.locateAbort = null;
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
/**
|
|
4773
|
+
* 取消在途定位:用户改选了另一条发言(哪怕那条已经加载、不需要请求),
|
|
4774
|
+
* 先前那次定位既不该再落地,也不该继续占着替换队列——排队中的下一次定位
|
|
4775
|
+
* 不然要等它跑完(用户看到的是"点了新目标一直转圈")。
|
|
4776
|
+
*
|
|
4777
|
+
* **只**中止定位自己的请求:权威刷新(运行结束 / 重连补数)不共用这个 controller,
|
|
4778
|
+
* 否则"运行刚结束时点一下侧栏"就会把终态对账一起中止,流式与历史对不上。
|
|
4779
|
+
*/
|
|
4780
|
+
cancelPendingLocate() {
|
|
4781
|
+
this.locateToken += 1;
|
|
4782
|
+
this.locateAbort?.abort();
|
|
4783
|
+
this.locateAbort = null;
|
|
4784
|
+
}
|
|
4785
|
+
/**
|
|
4786
|
+
* timeline 被整体改写(rewind / checkout / 释放):推进纪元,并把在途定位请求一并中止
|
|
4787
|
+
* ——它已经属于旧 timeline,却还占着替换队列,会让改写后的权威窗口排在后面等它跑完。
|
|
4788
|
+
* 作废号仍然保留:中止只是让它更早结束。
|
|
4789
|
+
*/
|
|
4790
|
+
invalidateTimeline() {
|
|
4791
|
+
this.timelineEpoch += 1;
|
|
4792
|
+
this.locateAbort?.abort();
|
|
4793
|
+
this.locateAbort = null;
|
|
4794
|
+
}
|
|
4795
|
+
/**
|
|
4796
|
+
* 拉取当前分支的用户发言目录(消息导航侧栏用)。
|
|
4797
|
+
*
|
|
4798
|
+
* 不在这里缓存:什么时候该刷新只有调用方知道(会话切换、历史被替换、新发言),
|
|
4799
|
+
* 缓存只会让旧条目在 rewind 之后继续可点。
|
|
4800
|
+
*/
|
|
4801
|
+
async loadTurnIndex() {
|
|
4802
|
+
const index = await this.runtime.fetchTurnIndex(this.sessionId);
|
|
4803
|
+
return index.items;
|
|
4804
|
+
}
|
|
3779
4805
|
/** 订阅状态变化。返回取消订阅函数。 */
|
|
3780
4806
|
subscribe(listener) {
|
|
3781
4807
|
this.listeners.add(listener);
|
|
@@ -3828,7 +4854,7 @@ var AgentSession = class _AgentSession {
|
|
|
3828
4854
|
/** 发送一条消息。字符串或多模态内容均可。 */
|
|
3829
4855
|
async send(content, options = {}) {
|
|
3830
4856
|
this.runtime.ensureConnected();
|
|
3831
|
-
if (this.state.isStreaming || this.state.activeCompaction || this.sendPending || this.manualCompactionPending) {
|
|
4857
|
+
if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.sendPending || this.manualCompactionPending) {
|
|
3832
4858
|
this.update(
|
|
3833
4859
|
(s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\u6216\u5BF9\u8BDD\u6B63\u5728\u6574\u7406\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002")
|
|
3834
4860
|
);
|
|
@@ -3841,9 +4867,125 @@ var AgentSession = class _AgentSession {
|
|
|
3841
4867
|
this.sendPending = false;
|
|
3842
4868
|
}
|
|
3843
4869
|
}
|
|
4870
|
+
/**
|
|
4871
|
+
* 入队一条等待消息(运行中排队 / 暂停期间照常入队)。
|
|
4872
|
+
*
|
|
4873
|
+
* ack 超时或传输失败时结果未知:用**同一个** `client_request_id` 重试一次,
|
|
4874
|
+
* 服务端按 `(session_id, client_request_id)` 幂等,不会制造第二条。
|
|
4875
|
+
*/
|
|
4876
|
+
async queueMessage(content) {
|
|
4877
|
+
const text = content.trim();
|
|
4878
|
+
if (!text) {
|
|
4879
|
+
return { ok: false, conflict: false, snapshot: null, message: "\u6D88\u606F\u4E0D\u80FD\u4E3A\u7A7A" };
|
|
4880
|
+
}
|
|
4881
|
+
const payload = {
|
|
4882
|
+
session_id: this.sessionId,
|
|
4883
|
+
message: text,
|
|
4884
|
+
client_request_id: createClientRequestId()
|
|
4885
|
+
};
|
|
4886
|
+
try {
|
|
4887
|
+
return await this.requestQueueAck("chat:append", payload);
|
|
4888
|
+
} catch {
|
|
4889
|
+
try {
|
|
4890
|
+
return await this.requestQueueAck("chat:append", payload);
|
|
4891
|
+
} catch (error) {
|
|
4892
|
+
return queueRequestFailure(error);
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
/** 修改一条等待消息的正文;`expectedVersion` 是客户端看到的条目版本。 */
|
|
4897
|
+
async updateQueuedMessage(itemId, expectedVersion, content) {
|
|
4898
|
+
return this.runQueueCommand("chat:queue:update", {
|
|
4899
|
+
session_id: this.sessionId,
|
|
4900
|
+
item_id: itemId,
|
|
4901
|
+
expected_version: expectedVersion,
|
|
4902
|
+
message: content
|
|
4903
|
+
});
|
|
4904
|
+
}
|
|
4905
|
+
/**
|
|
4906
|
+
* 重排等待消息。`orderedIds` 必须是当前 pending 条目 id 的**完整集合**(顺序任意),
|
|
4907
|
+
* 服务端按 `expectedRevision` 校验后整体替换顺序。
|
|
4908
|
+
*
|
|
4909
|
+
* 拖动排序就该像本地操作一样落地:版本冲突(运行中的任务刚领走一条 / 另一个端也在
|
|
4910
|
+
* 动队列)不甩给用户「刷新后重试」,而是拿冲突响应里那份**最新快照**重算一次再提交
|
|
4911
|
+
* ——掉队的条目(已经被领取执行的)从顺序里剔除,剩下的保持用户拖出来的相对次序。
|
|
4912
|
+
* 最多补两次;仍旧冲突才把结果交回调用方。
|
|
4913
|
+
*/
|
|
4914
|
+
async reorderQueuedMessages(expectedRevision, orderedIds) {
|
|
4915
|
+
let revision = expectedRevision;
|
|
4916
|
+
let ids = orderedIds;
|
|
4917
|
+
let result = await this.runQueueCommand("chat:queue:reorder", {
|
|
4918
|
+
session_id: this.sessionId,
|
|
4919
|
+
expected_revision: revision,
|
|
4920
|
+
ordered_ids: ids
|
|
4921
|
+
});
|
|
4922
|
+
for (let attempt = 0; attempt < 2 && !result.ok && result.conflict; attempt++) {
|
|
4923
|
+
const snapshot2 = result.snapshot;
|
|
4924
|
+
if (!snapshot2) break;
|
|
4925
|
+
const pending = snapshot2.items.filter(canEditQueuedMessage);
|
|
4926
|
+
const live = new Set(pending.map((item) => item.id));
|
|
4927
|
+
const next = ids.filter((id) => live.has(id));
|
|
4928
|
+
for (const item of pending) if (!next.includes(item.id)) next.push(item.id);
|
|
4929
|
+
if (next.length === 0) {
|
|
4930
|
+
return { ok: true, conflict: false, snapshot: snapshot2, message: null };
|
|
4931
|
+
}
|
|
4932
|
+
revision = snapshot2.revision;
|
|
4933
|
+
ids = next;
|
|
4934
|
+
result = await this.runQueueCommand("chat:queue:reorder", {
|
|
4935
|
+
session_id: this.sessionId,
|
|
4936
|
+
expected_revision: revision,
|
|
4937
|
+
ordered_ids: ids
|
|
4938
|
+
});
|
|
4939
|
+
}
|
|
4940
|
+
return result;
|
|
4941
|
+
}
|
|
4942
|
+
/** 删除一条等待消息;不传版本时由服务端自行判定。 */
|
|
4943
|
+
async cancelQueuedMessage(itemId, expectedVersion) {
|
|
4944
|
+
return this.runQueueCommand("chat:queue:cancel", {
|
|
4945
|
+
session_id: this.sessionId,
|
|
4946
|
+
item_id: itemId,
|
|
4947
|
+
expected_version: expectedVersion
|
|
4948
|
+
});
|
|
4949
|
+
}
|
|
4950
|
+
/** 立即补充:把等待消息交给当前运行,在下一个接收点消费,不停止也不另起一轮。 */
|
|
4951
|
+
async deliverQueuedMessage(itemId, expectedVersion) {
|
|
4952
|
+
return this.runQueueCommand("chat:queue:deliver", {
|
|
4953
|
+
session_id: this.sessionId,
|
|
4954
|
+
item_id: itemId,
|
|
4955
|
+
expected_version: expectedVersion
|
|
4956
|
+
});
|
|
4957
|
+
}
|
|
4958
|
+
/** 继续执行:清除暂停,并按当前顺序恢复逐条接续。 */
|
|
4959
|
+
async resumeQueue() {
|
|
4960
|
+
return this.runQueueCommand("chat:queue:resume", { session_id: this.sessionId });
|
|
4961
|
+
}
|
|
4962
|
+
/** 拉取一份权威队列快照。首次连接与每次重连后由实例自动调用,也可以手动刷新。 */
|
|
4963
|
+
async refreshQueue() {
|
|
4964
|
+
return this.runQueueCommand("chat:queue:list", { session_id: this.sessionId });
|
|
4965
|
+
}
|
|
4966
|
+
/**
|
|
4967
|
+
* 发出一条队列命令并安装 ack 里的快照。
|
|
4968
|
+
* 传输层失败(超时 / 断线)会 reject,由调用方决定是否重试。
|
|
4969
|
+
*/
|
|
4970
|
+
async requestQueueAck(event, payload) {
|
|
4971
|
+
this.runtime.ensureConnected();
|
|
4972
|
+
const response = await this.runtime.emitWithAck(event, payload, QUEUE_ACK_TIMEOUT_MS);
|
|
4973
|
+
const result = parseQueueAck(response, this.sessionId);
|
|
4974
|
+
const snapshot2 = result.snapshot;
|
|
4975
|
+
if (snapshot2) this.update((state) => withQueue(state, snapshot2));
|
|
4976
|
+
return result;
|
|
4977
|
+
}
|
|
4978
|
+
/** 队列命令的对外入口:传输失败只体现为失败结果,不向调用方抛异常。 */
|
|
4979
|
+
async runQueueCommand(event, payload) {
|
|
4980
|
+
try {
|
|
4981
|
+
return await this.requestQueueAck(event, payload);
|
|
4982
|
+
} catch (error) {
|
|
4983
|
+
return queueRequestFailure(error);
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
3844
4986
|
async sendUnlocked(content, options) {
|
|
3845
4987
|
await this.ensureIdleBeforeSend();
|
|
3846
|
-
if (this.state.isStreaming || this.state.activeCompaction || this.manualCompactionPending) {
|
|
4988
|
+
if (this.state.isStreaming && options.queueWhenRunning !== true || this.state.activeCompaction || this.manualCompactionPending) {
|
|
3847
4989
|
this.update(
|
|
3848
4990
|
(s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\u6216\u5BF9\u8BDD\u6B63\u5728\u6574\u7406\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002")
|
|
3849
4991
|
);
|
|
@@ -3853,7 +4995,7 @@ var AgentSession = class _AgentSession {
|
|
|
3853
4995
|
if (this.getAppContext) {
|
|
3854
4996
|
try {
|
|
3855
4997
|
const rawContext = await this.getAppContext();
|
|
3856
|
-
if (!
|
|
4998
|
+
if (!isRecord(rawContext)) {
|
|
3857
4999
|
throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
|
|
3858
5000
|
}
|
|
3859
5001
|
const serialized = JSON.stringify(rawContext);
|
|
@@ -3861,7 +5003,7 @@ var AgentSession = class _AgentSession {
|
|
|
3861
5003
|
throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE\u53EF\u5E8F\u5217\u5316\u7684 JSON \u5BF9\u8C61");
|
|
3862
5004
|
}
|
|
3863
5005
|
const normalized = JSON.parse(serialized);
|
|
3864
|
-
if (!
|
|
5006
|
+
if (!isRecord(normalized)) {
|
|
3865
5007
|
throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
|
|
3866
5008
|
}
|
|
3867
5009
|
appContext = normalized;
|
|
@@ -3871,15 +5013,18 @@ var AgentSession = class _AgentSession {
|
|
|
3871
5013
|
return false;
|
|
3872
5014
|
}
|
|
3873
5015
|
}
|
|
5016
|
+
const wasStreaming = this.state.isStreaming;
|
|
3874
5017
|
const askUserAnswer = options.askUserAnswer;
|
|
3875
5018
|
let optimisticTurnId = null;
|
|
5019
|
+
const clientRequestId = createClientRequestId();
|
|
3876
5020
|
let optimisticAskAnswer;
|
|
3877
5021
|
let previousAskAnswer;
|
|
3878
5022
|
this.update((state) => {
|
|
3879
|
-
const next = addUserMessage(state, content);
|
|
5023
|
+
const next = addUserMessage(state, content, clientRequestId);
|
|
3880
5024
|
optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
|
|
3881
5025
|
return next;
|
|
3882
5026
|
});
|
|
5027
|
+
if (optimisticTurnId) this.markOptimisticTurnPending(optimisticTurnId);
|
|
3883
5028
|
this.update((s) => ({ ...s, isStreaming: true }));
|
|
3884
5029
|
if (askUserAnswer && typeof askUserAnswer.tool_call_id === "string") {
|
|
3885
5030
|
const { tool_call_id, ...rest } = askUserAnswer;
|
|
@@ -3898,6 +5043,7 @@ var AgentSession = class _AgentSession {
|
|
|
3898
5043
|
const payload = {
|
|
3899
5044
|
session_id: this.sessionId,
|
|
3900
5045
|
message: content,
|
|
5046
|
+
client_request_id: clientRequestId,
|
|
3901
5047
|
mode: options.mode,
|
|
3902
5048
|
askuser_answer: askUserAnswer,
|
|
3903
5049
|
model: options.model,
|
|
@@ -3905,48 +5051,90 @@ var AgentSession = class _AgentSession {
|
|
|
3905
5051
|
whatif: options.whatif,
|
|
3906
5052
|
replay_decision: options.replayDecision,
|
|
3907
5053
|
app_context: appContext,
|
|
3908
|
-
kb_ids: options.kbIds
|
|
5054
|
+
kb_ids: options.kbIds,
|
|
5055
|
+
// 运行中提交不必由客户端挑命令:服务端按会话运行状态决定直接执行还是入队。
|
|
5056
|
+
queue_when_running: options.queueWhenRunning === true
|
|
3909
5057
|
};
|
|
3910
5058
|
try {
|
|
3911
5059
|
await this.ensureJoined();
|
|
3912
5060
|
} catch (error) {
|
|
5061
|
+
const failure = describeSessionError(error, "\u4F1A\u8BDD\u8BA2\u9605\u5931\u8D25\uFF0C\u6D88\u606F\u672A\u53D1\u9001");
|
|
3913
5062
|
this.rollbackOptimisticSend(
|
|
3914
5063
|
optimisticTurnId,
|
|
3915
5064
|
askUserAnswer,
|
|
3916
5065
|
optimisticAskAnswer,
|
|
3917
5066
|
previousAskAnswer,
|
|
3918
|
-
|
|
5067
|
+
wasStreaming,
|
|
5068
|
+
failure.message,
|
|
5069
|
+
failure.code
|
|
3919
5070
|
);
|
|
3920
5071
|
return false;
|
|
3921
5072
|
}
|
|
5073
|
+
const waitStartedAt = this.systemErrorGeneration;
|
|
3922
5074
|
try {
|
|
3923
5075
|
const response = await this.runtime.emitWithAck("chat:send", payload, 3e4);
|
|
5076
|
+
if (response?.status === "queued") {
|
|
5077
|
+
this.discardOptimisticSend(
|
|
5078
|
+
optimisticTurnId,
|
|
5079
|
+
askUserAnswer,
|
|
5080
|
+
optimisticAskAnswer,
|
|
5081
|
+
previousAskAnswer
|
|
5082
|
+
);
|
|
5083
|
+
const snapshot2 = parseQueueAck(response, this.sessionId).snapshot;
|
|
5084
|
+
const running = response.running === true;
|
|
5085
|
+
this.update((current) => {
|
|
5086
|
+
const next = running ? current : { ...current, isStreaming: false };
|
|
5087
|
+
return snapshot2 ? withQueue(next, snapshot2) : next;
|
|
5088
|
+
});
|
|
5089
|
+
return true;
|
|
5090
|
+
}
|
|
3924
5091
|
if (response?.status !== "accepted") {
|
|
3925
|
-
throw new
|
|
3926
|
-
typeof response?.message === "string" ? response.message : "\u6D88\u606F\u672A\u88AB\u670D\u52A1\u7AEF\u63A5\u53D7"
|
|
5092
|
+
throw new AgentSessionError(
|
|
5093
|
+
typeof response?.message === "string" ? response.message : "\u6D88\u606F\u672A\u88AB\u670D\u52A1\u7AEF\u63A5\u53D7",
|
|
5094
|
+
typeof response?.code === "string" ? response.code : void 0
|
|
3927
5095
|
);
|
|
3928
5096
|
}
|
|
3929
5097
|
return true;
|
|
3930
5098
|
} catch (error) {
|
|
5099
|
+
const failure = describeSessionError(error, "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
|
|
3931
5100
|
this.rollbackOptimisticSend(
|
|
3932
5101
|
optimisticTurnId,
|
|
3933
5102
|
askUserAnswer,
|
|
3934
5103
|
optimisticAskAnswer,
|
|
3935
5104
|
previousAskAnswer,
|
|
3936
|
-
|
|
5105
|
+
wasStreaming,
|
|
5106
|
+
failure.message,
|
|
5107
|
+
failure.code,
|
|
5108
|
+
waitStartedAt
|
|
3937
5109
|
);
|
|
3938
5110
|
return false;
|
|
3939
5111
|
}
|
|
3940
5112
|
}
|
|
3941
|
-
rollbackOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer, message) {
|
|
5113
|
+
rollbackOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer, wasStreaming, message, code, waitStartedAt) {
|
|
5114
|
+
this.discardOptimisticSend(
|
|
5115
|
+
optimisticTurnId,
|
|
5116
|
+
askUserAnswer,
|
|
5117
|
+
optimisticAskAnswer,
|
|
5118
|
+
previousAskAnswer
|
|
5119
|
+
);
|
|
5120
|
+
this.update(
|
|
5121
|
+
(current) => addErrorMessage(wasStreaming ? current : { ...current, isStreaming: false }, message)
|
|
5122
|
+
);
|
|
5123
|
+
const delivered = this.lastDeliveredSystemError;
|
|
5124
|
+
if (waitStartedAt !== void 0 && delivered && delivered.generation > waitStartedAt && delivered.message === message && delivered.code === code) {
|
|
5125
|
+
this.lastDeliveredSystemError = null;
|
|
5126
|
+
return;
|
|
5127
|
+
}
|
|
5128
|
+
this.emitter.emit("error", { message, ...code ? { code } : {} });
|
|
5129
|
+
}
|
|
5130
|
+
/** 撤回一次乐观提交:移除乐观消息、恢复提问作答、清掉回放草稿。 */
|
|
5131
|
+
discardOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer) {
|
|
3942
5132
|
this.pendingReplayMessage = null;
|
|
3943
5133
|
this.pendingReplayMode = void 0;
|
|
3944
5134
|
this.pendingReplayKbIds = void 0;
|
|
3945
5135
|
this.update((current) => {
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
current.turns.filter((turn) => turn.turn_id !== optimisticTurnId)
|
|
3949
|
-
) : current;
|
|
5136
|
+
const filtered = optimisticTurnId ? current.turns.filter((turn) => turn.turn_id !== optimisticTurnId) : current.turns;
|
|
5137
|
+
let next = optimisticTurnId ? withTurns({ ...current, turns: filtered }, filtered) : current;
|
|
3950
5138
|
const toolCallId = askUserAnswer && typeof askUserAnswer.tool_call_id === "string" ? askUserAnswer.tool_call_id : null;
|
|
3951
5139
|
if (toolCallId && next.askAnswers[toolCallId] === optimisticAskAnswer) {
|
|
3952
5140
|
const askAnswers = { ...next.askAnswers };
|
|
@@ -3957,18 +5145,20 @@ var AgentSession = class _AgentSession {
|
|
|
3957
5145
|
}
|
|
3958
5146
|
next = { ...next, askAnswers };
|
|
3959
5147
|
}
|
|
3960
|
-
return
|
|
5148
|
+
return next;
|
|
3961
5149
|
});
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
this.runtime.emitEvent("chat:append", { session_id: this.sessionId, message: text });
|
|
3967
|
-
this.update((s) => addUserMessage(s, text));
|
|
5150
|
+
if (optimisticTurnId) {
|
|
5151
|
+
this.pendingOptimisticTurnIds.delete(optimisticTurnId);
|
|
5152
|
+
this.liveRevisions.delete(optimisticTurnId);
|
|
5153
|
+
}
|
|
3968
5154
|
}
|
|
3969
5155
|
/** 停止当前回复。 */
|
|
3970
5156
|
stop() {
|
|
3971
|
-
return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) =>
|
|
5157
|
+
return this.runtime.emitWithAck("chat:stop", { session_id: this.sessionId }, 35e3).then((response) => {
|
|
5158
|
+
const result = response ?? {};
|
|
5159
|
+
if (result.status !== "accepted") this.manualCompactionPending = false;
|
|
5160
|
+
return result;
|
|
5161
|
+
});
|
|
3972
5162
|
}
|
|
3973
5163
|
/** 手动触发上下文压缩。 */
|
|
3974
5164
|
compact() {
|
|
@@ -3988,8 +5178,10 @@ var AgentSession = class _AgentSession {
|
|
|
3988
5178
|
trigger: "manual"
|
|
3989
5179
|
}
|
|
3990
5180
|
}));
|
|
3991
|
-
return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {}).
|
|
5181
|
+
return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {}).catch((error) => {
|
|
3992
5182
|
this.manualCompactionPending = false;
|
|
5183
|
+
throw error;
|
|
5184
|
+
}).finally(() => {
|
|
3993
5185
|
if (this.state.activeCompaction?.compaction_id === "pending-manual-compaction") {
|
|
3994
5186
|
this.update((state) => ({ ...state, activeCompaction: null }));
|
|
3995
5187
|
}
|
|
@@ -4028,10 +5220,20 @@ var AgentSession = class _AgentSession {
|
|
|
4028
5220
|
if (this.disposed) return;
|
|
4029
5221
|
if (!this.runtime.onDispose(this)) return;
|
|
4030
5222
|
this.disposed = true;
|
|
5223
|
+
this.invalidateTimeline();
|
|
5224
|
+
this.invalidateOlderPaging();
|
|
5225
|
+
this.state = { ...this.state, nextBefore: null, loadingOlder: false };
|
|
4031
5226
|
this.cancelPatchFlush();
|
|
4032
5227
|
this.listeners.clear();
|
|
4033
5228
|
this.emitter.clear();
|
|
4034
5229
|
this.commands.clear();
|
|
5230
|
+
this.commandRecoveryGeneration += 1;
|
|
5231
|
+
this.commandRestoreRetryAttempt = 0;
|
|
5232
|
+
this.bufferedCommandDeliveries = null;
|
|
5233
|
+
if (this.commandRestoreRetryHandle !== null) {
|
|
5234
|
+
globalThis.clearTimeout(this.commandRestoreRetryHandle);
|
|
5235
|
+
this.commandRestoreRetryHandle = null;
|
|
5236
|
+
}
|
|
4035
5237
|
}
|
|
4036
5238
|
get isDisposed() {
|
|
4037
5239
|
return this.disposed;
|
|
@@ -4042,23 +5244,37 @@ var AgentSession = class _AgentSession {
|
|
|
4042
5244
|
if (this.state.connection === connection) return;
|
|
4043
5245
|
this.update((s) => ({ ...s, connection }));
|
|
4044
5246
|
}
|
|
5247
|
+
/**
|
|
5248
|
+
* @internal 服务端广播的权威队列快照。
|
|
5249
|
+
* 结构不合法或 revision 不新的一律丢弃,绝不用旧快照覆盖新状态。
|
|
5250
|
+
*/
|
|
5251
|
+
_handleQueueUpdated(payload) {
|
|
5252
|
+
if (!isSessionQueueSnapshot(payload)) return;
|
|
5253
|
+
const snapshot2 = payload;
|
|
5254
|
+
this.update((state) => withQueue(state, snapshot2));
|
|
5255
|
+
}
|
|
4045
5256
|
/** @internal socket 重连后由 Hub 调用:重新加入房间并补数。 */
|
|
4046
5257
|
_rejoinAfterReconnect() {
|
|
4047
|
-
|
|
5258
|
+
this.invalidateOlderPaging();
|
|
5259
|
+
this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
|
|
5260
|
+
const reconnectLiveRevision = this.liveRevision;
|
|
5261
|
+
const reconnectModeRevision = this.modeRevision;
|
|
5262
|
+
const commandRecoveryGeneration = this.beginCommandRecovery();
|
|
4048
5263
|
return this.enqueueSubscription(async () => {
|
|
4049
5264
|
this.joined = false;
|
|
4050
5265
|
this.joinPending = null;
|
|
4051
5266
|
try {
|
|
4052
5267
|
this.subscriptionReplaySource = "reconnect_replay";
|
|
4053
5268
|
try {
|
|
4054
|
-
await this.ensureJoined();
|
|
5269
|
+
await this.ensureJoined(reconnectLiveRevision);
|
|
4055
5270
|
} finally {
|
|
4056
5271
|
this.subscriptionReplaySource = null;
|
|
5272
|
+
this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
|
|
4057
5273
|
}
|
|
4058
5274
|
await Promise.all([
|
|
4059
|
-
this.
|
|
4060
|
-
|
|
4061
|
-
|
|
5275
|
+
this.refreshAuthoritativeHistory({
|
|
5276
|
+
liveRevisionAtStart: reconnectLiveRevision,
|
|
5277
|
+
modeRevisionAtStart: reconnectModeRevision
|
|
4062
5278
|
}),
|
|
4063
5279
|
this.syncReplayFromSession()
|
|
4064
5280
|
]);
|
|
@@ -4068,15 +5284,17 @@ var AgentSession = class _AgentSession {
|
|
|
4068
5284
|
}
|
|
4069
5285
|
/** @internal 首次连接:加载历史 + 加入房间。 */
|
|
4070
5286
|
_bootstrap() {
|
|
5287
|
+
const commandRecoveryGeneration = this.beginCommandRecovery();
|
|
4071
5288
|
return this.enqueueSubscription(async () => {
|
|
4072
|
-
await this.
|
|
5289
|
+
await this.replaceFromHistory({ suppressErrors: false });
|
|
4073
5290
|
this.subscriptionReplaySource = "bootstrap_replay";
|
|
4074
5291
|
try {
|
|
4075
5292
|
await this.ensureJoined();
|
|
4076
5293
|
} finally {
|
|
4077
5294
|
this.subscriptionReplaySource = null;
|
|
4078
5295
|
}
|
|
4079
|
-
|
|
5296
|
+
this.scheduleHistoricalCommandRestore(commandRecoveryGeneration);
|
|
5297
|
+
await Promise.all([this.syncReplayFromSession(), this.refreshQueue()]);
|
|
4080
5298
|
});
|
|
4081
5299
|
}
|
|
4082
5300
|
/** 订阅生命周期串行执行;一次失败只由自己的调用方观察,不截断后续重连。 */
|
|
@@ -4142,8 +5360,14 @@ var AgentSession = class _AgentSession {
|
|
|
4142
5360
|
/** @internal 历史回放通道(event_format: raw)。 */
|
|
4143
5361
|
_handleTurnEvents(events, replayed = false) {
|
|
4144
5362
|
if (!events.length) return;
|
|
5363
|
+
if (events.some(
|
|
5364
|
+
(event) => event.type === "compaction:end" || event.type === "compaction:error" || event.type === "compaction:abort"
|
|
5365
|
+
)) {
|
|
5366
|
+
this.manualCompactionPending = false;
|
|
5367
|
+
}
|
|
5368
|
+
this.dispatchRawCommands(events);
|
|
4145
5369
|
const terminalChatEndStatus = extractTerminalChatEndStatus(events);
|
|
4146
|
-
if (!terminalChatEndStatus &&
|
|
5370
|
+
if (!terminalChatEndStatus && hasChatRunEvent(events) && this.state.status !== "waiting_for_input") {
|
|
4147
5371
|
this.markRunningIfIdle();
|
|
4148
5372
|
}
|
|
4149
5373
|
if (this.state.turns.length) {
|
|
@@ -4166,7 +5390,12 @@ var AgentSession = class _AgentSession {
|
|
|
4166
5390
|
}
|
|
4167
5391
|
/** @internal */
|
|
4168
5392
|
_handleChatStart() {
|
|
4169
|
-
this.update((s) => ({
|
|
5393
|
+
this.update((s) => ({
|
|
5394
|
+
...s,
|
|
5395
|
+
status: "running",
|
|
5396
|
+
errorMessage: null,
|
|
5397
|
+
isStreaming: true
|
|
5398
|
+
}));
|
|
4170
5399
|
}
|
|
4171
5400
|
/** @internal */
|
|
4172
5401
|
_handleChatEnd(status) {
|
|
@@ -4174,14 +5403,31 @@ var AgentSession = class _AgentSession {
|
|
|
4174
5403
|
this.completeChat(status);
|
|
4175
5404
|
}
|
|
4176
5405
|
/** @internal */
|
|
4177
|
-
_handleSystemError(message) {
|
|
5406
|
+
_handleSystemError(message, code) {
|
|
4178
5407
|
this.flushPatches();
|
|
4179
5408
|
const finalMessage = isOomText(message) ? OOM_MESSAGE : message;
|
|
4180
5409
|
this.update(
|
|
4181
|
-
(s) => addErrorMessage(
|
|
5410
|
+
(s) => addErrorMessage(
|
|
5411
|
+
markFailed({
|
|
5412
|
+
...s,
|
|
5413
|
+
status: "failed",
|
|
5414
|
+
errorMessage: finalMessage,
|
|
5415
|
+
isStreaming: false
|
|
5416
|
+
}),
|
|
5417
|
+
finalMessage
|
|
5418
|
+
)
|
|
4182
5419
|
);
|
|
4183
5420
|
this.recentChatEndAt = Date.now();
|
|
4184
|
-
this.
|
|
5421
|
+
this.systemErrorGeneration += 1;
|
|
5422
|
+
this.lastDeliveredSystemError = {
|
|
5423
|
+
message: finalMessage,
|
|
5424
|
+
...code ? { code } : {},
|
|
5425
|
+
generation: this.systemErrorGeneration
|
|
5426
|
+
};
|
|
5427
|
+
this.emitter.emit("error", {
|
|
5428
|
+
message: finalMessage,
|
|
5429
|
+
...code ? { code } : {}
|
|
5430
|
+
});
|
|
4185
5431
|
}
|
|
4186
5432
|
/** @internal */
|
|
4187
5433
|
_handleTaskUpdated(tasks) {
|
|
@@ -4201,6 +5447,8 @@ var AgentSession = class _AgentSession {
|
|
|
4201
5447
|
this.update((s) => ({ ...s, replay }));
|
|
4202
5448
|
}
|
|
4203
5449
|
this.emitter.emit("sessionUpdated", {
|
|
5450
|
+
workspacePath: data.workspace_path,
|
|
5451
|
+
chatGroupId: data.chat_group_id,
|
|
4204
5452
|
intent: data.intent,
|
|
4205
5453
|
status: data.status,
|
|
4206
5454
|
model: data.model,
|
|
@@ -4210,8 +5458,16 @@ var AgentSession = class _AgentSession {
|
|
|
4210
5458
|
}
|
|
4211
5459
|
/** @internal */
|
|
4212
5460
|
_handleRewind(checkpointId) {
|
|
4213
|
-
this.
|
|
4214
|
-
|
|
5461
|
+
this.invalidateTimeline();
|
|
5462
|
+
this.invalidateOlderPaging();
|
|
5463
|
+
this.update((s) => ({
|
|
5464
|
+
...s,
|
|
5465
|
+
mode: null,
|
|
5466
|
+
errorMessage: null,
|
|
5467
|
+
nextBefore: null,
|
|
5468
|
+
loadingOlder: false
|
|
5469
|
+
}));
|
|
5470
|
+
void this.replaceFromHistory();
|
|
4215
5471
|
this.emitter.emit("rewind", { checkpointId });
|
|
4216
5472
|
}
|
|
4217
5473
|
/** @internal */
|
|
@@ -4230,7 +5486,7 @@ var AgentSession = class _AgentSession {
|
|
|
4230
5486
|
message,
|
|
4231
5487
|
mode,
|
|
4232
5488
|
replay_decision: decision,
|
|
4233
|
-
kb_ids: kbIds
|
|
5489
|
+
kb_ids: kbIds
|
|
4234
5490
|
},
|
|
4235
5491
|
3e4
|
|
4236
5492
|
).then((response) => {
|
|
@@ -4271,9 +5527,9 @@ var AgentSession = class _AgentSession {
|
|
|
4271
5527
|
detail,
|
|
4272
5528
|
status,
|
|
4273
5529
|
loopId,
|
|
4274
|
-
metadata:
|
|
5530
|
+
metadata: isRecord(notification.metadata) ? notification.metadata : void 0
|
|
4275
5531
|
});
|
|
4276
|
-
if (notificationType === "bg:started" &&
|
|
5532
|
+
if (notificationType === "bg:started" && isRecord(notification.metadata)) {
|
|
4277
5533
|
const taskId = typeof notification.metadata.task_id === "string" ? notification.metadata.task_id : "";
|
|
4278
5534
|
if (taskId) {
|
|
4279
5535
|
this.emitter.emit("backgroundTask", {
|
|
@@ -4295,12 +5551,12 @@ var AgentSession = class _AgentSession {
|
|
|
4295
5551
|
this.emitter.emit("artifact", event);
|
|
4296
5552
|
}
|
|
4297
5553
|
/** @internal 加入房间的应答里带 is_running,据此校准流式状态。 */
|
|
4298
|
-
_applyRunningState(isRunning) {
|
|
5554
|
+
_applyRunningState(isRunning, liveRevisionAtJoin = this.liveRevision) {
|
|
4299
5555
|
if (isRunning) {
|
|
4300
5556
|
this.update((s) => ({ ...s, status: "running", isStreaming: true }));
|
|
4301
5557
|
} else if (this.state.isStreaming) {
|
|
4302
5558
|
this.update((s) => ({ ...s, isStreaming: false }));
|
|
4303
|
-
void this.
|
|
5559
|
+
void this.refreshAuthoritativeHistory({ liveRevisionAtStart: liveRevisionAtJoin });
|
|
4304
5560
|
}
|
|
4305
5561
|
}
|
|
4306
5562
|
// ---------------------------------------------------------------- 内部实现
|
|
@@ -4321,13 +5577,35 @@ var AgentSession = class _AgentSession {
|
|
|
4321
5577
|
completeChat(status) {
|
|
4322
5578
|
this.finishStreaming(status);
|
|
4323
5579
|
this.recentChatEndAt = Date.now();
|
|
4324
|
-
void this.
|
|
5580
|
+
void this.refreshAuthoritativeHistory();
|
|
4325
5581
|
this.emitter.emit("chatEnd", { status });
|
|
4326
5582
|
}
|
|
5583
|
+
markTurnFresh(turnId) {
|
|
5584
|
+
this.liveRevision += 1;
|
|
5585
|
+
this.liveRevisions.set(turnId, this.liveRevision);
|
|
5586
|
+
}
|
|
5587
|
+
markOptimisticTurnPending(turnId) {
|
|
5588
|
+
this.pendingOptimisticTurnIds.add(turnId);
|
|
5589
|
+
this.markTurnFresh(turnId);
|
|
5590
|
+
}
|
|
5591
|
+
pruneOptimisticLiveRevisions() {
|
|
5592
|
+
const retained = new Set(
|
|
5593
|
+
this.state.turns.map((turn) => turn.turn_id).filter((turnId) => turnId.startsWith("local-user-"))
|
|
5594
|
+
);
|
|
5595
|
+
for (const turnId of this.liveRevisions.keys()) {
|
|
5596
|
+
if (turnId.startsWith("local-user-") && !retained.has(turnId)) {
|
|
5597
|
+
this.pendingOptimisticTurnIds.delete(turnId);
|
|
5598
|
+
this.liveRevisions.delete(turnId);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
}
|
|
4327
5602
|
applyTurn(turn, source = "live") {
|
|
4328
5603
|
this.flushPatches();
|
|
5604
|
+
this.markTurnFresh(turn.turn_id);
|
|
5605
|
+
if (extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
|
|
4329
5606
|
const previous = this.state;
|
|
4330
5607
|
this.update((s) => upsertTurn(s, turn));
|
|
5608
|
+
this.pruneOptimisticLiveRevisions();
|
|
4331
5609
|
this.emitTurnDerivedEvents(previous, turn, source);
|
|
4332
5610
|
}
|
|
4333
5611
|
schedulePatchFlush() {
|
|
@@ -4361,11 +5639,15 @@ var AgentSession = class _AgentSession {
|
|
|
4361
5639
|
this.pendingTurnPatches.clear();
|
|
4362
5640
|
for (const patch of patches) {
|
|
4363
5641
|
const previous = this.state;
|
|
4364
|
-
|
|
5642
|
+
const next = applyTurnPatch(previous, patch);
|
|
5643
|
+
if (next === previous) continue;
|
|
5644
|
+
this.liveRevision += 1;
|
|
5645
|
+
this.liveRevisions.set(patch.turn_id, this.liveRevision);
|
|
4365
5646
|
const turn = patch.data.turn;
|
|
4366
|
-
if (turn)
|
|
4367
|
-
|
|
4368
|
-
|
|
5647
|
+
if (turn && extractModeFromBlocks(turn.blocks)) this.modeRevision += 1;
|
|
5648
|
+
this.setState(next);
|
|
5649
|
+
this.pruneOptimisticLiveRevisions();
|
|
5650
|
+
if (turn) this.emitTurnDerivedEvents(previous, turn);
|
|
4369
5651
|
}
|
|
4370
5652
|
if (this.patchFlushCancel) {
|
|
4371
5653
|
this.patchFlushCancel();
|
|
@@ -4387,23 +5669,29 @@ var AgentSession = class _AgentSession {
|
|
|
4387
5669
|
this.dispatchCommand(turn, block);
|
|
4388
5670
|
continue;
|
|
4389
5671
|
}
|
|
4390
|
-
if (block.type === "tool_ui" && block.tool_call_id &&
|
|
5672
|
+
if (block.type === "tool_ui" && block.tool_call_id && isToolUiCard(block.content)) {
|
|
4391
5673
|
const ui = block.content;
|
|
4392
5674
|
if (ui.target !== "preview") continue;
|
|
4393
|
-
const
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
const
|
|
4397
|
-
|
|
4398
|
-
|
|
5675
|
+
const resolved = resolveToolUiCardContent(ui);
|
|
5676
|
+
if (!resolved) continue;
|
|
5677
|
+
const key = buildToolUiCardKey(block.tool_call_id, resolved.type, resolved.content, ui.sourcePath);
|
|
5678
|
+
const fingerprint = JSON.stringify([
|
|
5679
|
+
resolved.content,
|
|
5680
|
+
ui.mcpApp?.sourceId,
|
|
5681
|
+
resolved.type === "resource-file" ? block.tool_call_id : void 0
|
|
5682
|
+
]);
|
|
5683
|
+
if (this.deliveredToolPreviews.get(key) === fingerprint) continue;
|
|
5684
|
+
this.deliveredToolPreviews.set(key, fingerprint);
|
|
4399
5685
|
this.emitter.emit("toolPreview", {
|
|
4400
5686
|
toolCallId: block.tool_call_id,
|
|
4401
|
-
type:
|
|
4402
|
-
content:
|
|
4403
|
-
title: ui.title ?? "\u5DE5\u5177\u9884\u89C8"
|
|
5687
|
+
type: resolved.type,
|
|
5688
|
+
content: resolved.content,
|
|
5689
|
+
title: ui.title ?? "\u5DE5\u5177\u9884\u89C8",
|
|
5690
|
+
...ui.sourcePath ? { sourcePath: ui.sourcePath } : {},
|
|
5691
|
+
...ui.mcpApp ? { mcpApp: ui.mcpApp } : {}
|
|
4404
5692
|
});
|
|
4405
5693
|
}
|
|
4406
|
-
if (block.type === "system_notification" &&
|
|
5694
|
+
if (block.type === "system_notification" && isRecord(block.content)) {
|
|
4407
5695
|
this._handleSystemNotification(turn.loop_id, block.content);
|
|
4408
5696
|
}
|
|
4409
5697
|
}
|
|
@@ -4440,17 +5728,36 @@ var AgentSession = class _AgentSession {
|
|
|
4440
5728
|
dispatchCommand(turn, block) {
|
|
4441
5729
|
if (!isToolBridgeContent(block.content)) return;
|
|
4442
5730
|
const { action, payload } = block.content;
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
5731
|
+
const deliveryKey = block.delivery_id ? commandDeliveryKey(block.delivery_id) : `legacy-tool-command:${turn.turn_id}:${block.tool_call_id ?? ""}:${action}:${JSON.stringify(payload)}`;
|
|
5732
|
+
this.deliverCommand(deliveryKey, action, payload, block.tool_call_id ?? void 0);
|
|
5733
|
+
}
|
|
5734
|
+
dispatchRawCommands(events) {
|
|
5735
|
+
for (const event of events) {
|
|
5736
|
+
if (event.type !== "loop:tool_bridge") continue;
|
|
5737
|
+
const deliveryId = String(event.payload.delivery_id ?? "");
|
|
5738
|
+
const bridge = event.payload.bridge;
|
|
5739
|
+
if (!deliveryId || !isToolBridgeContent(bridge)) continue;
|
|
5740
|
+
this.deliverCommand(
|
|
5741
|
+
commandDeliveryKey(deliveryId),
|
|
5742
|
+
bridge.action,
|
|
5743
|
+
bridge.payload,
|
|
5744
|
+
String(event.payload.tool_call_id ?? "") || void 0
|
|
5745
|
+
);
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
deliverCommand(deliveryKey, action, payload, toolCallId) {
|
|
5749
|
+
if (this.bufferedCommandDeliveries) {
|
|
5750
|
+
this.bufferedCommandDeliveries.push({ deliveryKey, action, payload, toolCallId });
|
|
5751
|
+
return;
|
|
4448
5752
|
}
|
|
4449
|
-
|
|
5753
|
+
this.deliverCommandNow({ deliveryKey, action, payload, toolCallId });
|
|
5754
|
+
}
|
|
5755
|
+
deliverCommandNow(command) {
|
|
5756
|
+
const { deliveryKey, action, payload, toolCallId } = command;
|
|
4450
5757
|
if (this.deliveredCommandKeys.has(deliveryKey)) return;
|
|
4451
5758
|
this.deliveredCommandKeys.add(deliveryKey);
|
|
4452
|
-
const meta = { toolCallId
|
|
4453
|
-
this.emitter.emit("command", { action, payload, toolCallId
|
|
5759
|
+
const meta = { toolCallId };
|
|
5760
|
+
this.emitter.emit("command", { action, payload, toolCallId });
|
|
4454
5761
|
this.commands.dispatch(action, payload, meta);
|
|
4455
5762
|
}
|
|
4456
5763
|
notifySandboxOom(key) {
|
|
@@ -4459,29 +5766,181 @@ var AgentSession = class _AgentSession {
|
|
|
4459
5766
|
this.update((s) => addErrorMessage(s, OOM_MESSAGE));
|
|
4460
5767
|
this.emitter.emit("sandboxOom", { toolCallId: key });
|
|
4461
5768
|
}
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
5769
|
+
/**
|
|
5770
|
+
* 权威刷新(运行结束 / 重连补数):**按当前窗口第一条发言取窗口**,不再另记"用户想看哪段"。
|
|
5771
|
+
*
|
|
5772
|
+
* 窗口本身就是"用户在看哪段":首屏就是最新一页,向上翻页 / 定位之后首条就是用户滚到的位置。
|
|
5773
|
+
* 直接拿 `from_entry_id = 当前窗口第一条 turn_id` 去取,运行结束、重连补数、翻页之后的对账
|
|
5774
|
+
* 是同一条规则,不需要意图、锚点、已落地锚点这些状态;窗口首条已经不在分支上(404 / 409)时
|
|
5775
|
+
* 回落到最新一页。
|
|
5776
|
+
*/
|
|
5777
|
+
refreshAuthoritativeHistory(options = {}) {
|
|
5778
|
+
let entryGone = false;
|
|
5779
|
+
const locateTokenAtStart = this.locateToken;
|
|
5780
|
+
return this.replaceFromHistory({
|
|
5781
|
+
...options,
|
|
5782
|
+
// 锚点在**这次操作真正开始**时采样:排队期间用户可能刚定位过,那时窗口首条才是他要的。
|
|
5783
|
+
fromEntryId: () => this.state.turns[0]?.turn_id,
|
|
5784
|
+
onError: (error) => {
|
|
5785
|
+
entryGone = isEntryGoneError(error);
|
|
5786
|
+
}
|
|
5787
|
+
}).then((located) => {
|
|
5788
|
+
if (located || !entryGone) return located;
|
|
5789
|
+
if (this.locateToken !== locateTokenAtStart) return false;
|
|
5790
|
+
return this.replaceFromHistory(options);
|
|
5791
|
+
});
|
|
5792
|
+
}
|
|
5793
|
+
replaceFromHistory(options = {}) {
|
|
5794
|
+
if (this.disposed) return Promise.resolve(false);
|
|
5795
|
+
const operationEpoch = this.timelineEpoch;
|
|
5796
|
+
const liveRevisionAtStart = options.liveRevisionAtStart ?? this.liveRevision;
|
|
5797
|
+
const modeRevisionAtStart = options.modeRevisionAtStart ?? this.modeRevision;
|
|
5798
|
+
this.invalidateOlderPaging();
|
|
5799
|
+
this.replaceInFlight += 1;
|
|
5800
|
+
const operation = async () => {
|
|
5801
|
+
let previousNextBefore = null;
|
|
5802
|
+
const restoreCursor = () => {
|
|
5803
|
+
if (this.disposed || previousNextBefore === null) return;
|
|
5804
|
+
this.update((state) => ({
|
|
5805
|
+
...state,
|
|
5806
|
+
nextBefore: previousNextBefore,
|
|
5807
|
+
loadingOlder: false
|
|
5808
|
+
}));
|
|
5809
|
+
};
|
|
5810
|
+
try {
|
|
5811
|
+
if (this.disposed || operationEpoch !== this.timelineEpoch) return false;
|
|
5812
|
+
if (options.isStale?.()) return false;
|
|
5813
|
+
this.invalidateOlderPaging();
|
|
5814
|
+
previousNextBefore = this.state.nextBefore;
|
|
5815
|
+
this.update((state) => ({ ...state, nextBefore: null, loadingOlder: false }));
|
|
5816
|
+
const signal = options.signal;
|
|
5817
|
+
const fromEntryId = typeof options.fromEntryId === "function" ? options.fromEntryId() : options.fromEntryId;
|
|
5818
|
+
const page = await this.runtime.fetchTurnsPage(
|
|
5819
|
+
this.sessionId,
|
|
5820
|
+
fromEntryId ? { fromEntryId } : void 0,
|
|
5821
|
+
signal ? { signal } : void 0
|
|
5822
|
+
);
|
|
5823
|
+
if (this.disposed || operationEpoch !== this.timelineEpoch) {
|
|
5824
|
+
restoreCursor();
|
|
5825
|
+
return false;
|
|
4479
5826
|
}
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
5827
|
+
if (options.isStale?.()) {
|
|
5828
|
+
restoreCursor();
|
|
5829
|
+
return false;
|
|
5830
|
+
}
|
|
5831
|
+
if (!this.joined) this.seedToolCallDeliveryBaseline(page.turns);
|
|
5832
|
+
this.update((state) => {
|
|
5833
|
+
const next = withTurns(
|
|
5834
|
+
state,
|
|
5835
|
+
replaceWindow(state.turns, page.turns, liveRevisionAtStart, {
|
|
5836
|
+
current: this.liveRevision,
|
|
5837
|
+
byTurnId: this.liveRevisions
|
|
5838
|
+
})
|
|
5839
|
+
);
|
|
5840
|
+
return {
|
|
5841
|
+
...next,
|
|
5842
|
+
mode: this.modeRevision === modeRevisionAtStart ? page.currentMode : state.mode,
|
|
5843
|
+
nextBefore: page.nextBefore,
|
|
5844
|
+
loadingOlder: false
|
|
5845
|
+
};
|
|
5846
|
+
});
|
|
5847
|
+
this.pruneOptimisticLiveRevisions();
|
|
5848
|
+
this.projectionBuilder._seedTurns(this.state.turns);
|
|
5849
|
+
return true;
|
|
5850
|
+
} catch (error) {
|
|
5851
|
+
options.onError?.(error);
|
|
5852
|
+
if (!this.disposed && operationEpoch === this.timelineEpoch) {
|
|
5853
|
+
this.update((state) => ({
|
|
5854
|
+
...state,
|
|
5855
|
+
nextBefore: previousNextBefore,
|
|
5856
|
+
loadingOlder: false
|
|
5857
|
+
}));
|
|
5858
|
+
}
|
|
5859
|
+
if (options.suppressErrors === false) throw error;
|
|
5860
|
+
return false;
|
|
5861
|
+
} finally {
|
|
5862
|
+
this.replaceInFlight -= 1;
|
|
5863
|
+
}
|
|
5864
|
+
};
|
|
5865
|
+
const run = this.replaceQueue.then(operation);
|
|
5866
|
+
this.replaceQueue = run.catch(() => void 0);
|
|
5867
|
+
return run;
|
|
5868
|
+
}
|
|
5869
|
+
invalidateOlderPaging() {
|
|
5870
|
+
this.pagingGeneration += 1;
|
|
5871
|
+
this.olderPending = null;
|
|
5872
|
+
}
|
|
5873
|
+
beginCommandRecovery() {
|
|
5874
|
+
this.commandRecoveryGeneration += 1;
|
|
5875
|
+
this.commandRestoreRetryAttempt = 0;
|
|
5876
|
+
this.bufferedCommandDeliveries ??= [];
|
|
5877
|
+
if (this.commandRestoreRetryHandle !== null) {
|
|
5878
|
+
globalThis.clearTimeout(this.commandRestoreRetryHandle);
|
|
5879
|
+
this.commandRestoreRetryHandle = null;
|
|
5880
|
+
}
|
|
5881
|
+
return this.commandRecoveryGeneration;
|
|
5882
|
+
}
|
|
5883
|
+
scheduleHistoricalCommandRestore(generation) {
|
|
5884
|
+
const restore = async () => {
|
|
5885
|
+
if (this.disposed || generation !== this.commandRecoveryGeneration) return;
|
|
5886
|
+
try {
|
|
5887
|
+
const commands = await this.runtime.fetchCommands(this.sessionId);
|
|
5888
|
+
if (this.disposed) return;
|
|
5889
|
+
for (const command of commands) {
|
|
5890
|
+
if (this.disposed) return;
|
|
5891
|
+
this.deliverCommandNow({
|
|
5892
|
+
deliveryKey: commandDeliveryKey(command.deliveryId),
|
|
5893
|
+
action: command.action,
|
|
5894
|
+
payload: command.payload,
|
|
5895
|
+
toolCallId: command.toolCallId
|
|
5896
|
+
});
|
|
5897
|
+
}
|
|
5898
|
+
if (generation !== this.commandRecoveryGeneration) return;
|
|
5899
|
+
this.commandRestoreRetryAttempt = 0;
|
|
5900
|
+
this.flushBufferedCommands(generation);
|
|
5901
|
+
} catch {
|
|
5902
|
+
if (!this.disposed && generation === this.commandRecoveryGeneration && this.commandRestoreRetryHandle === null) {
|
|
5903
|
+
const retryDelay = Math.min(
|
|
5904
|
+
COMMAND_RESTORE_RETRY_BASE_MS * 2 ** this.commandRestoreRetryAttempt,
|
|
5905
|
+
COMMAND_RESTORE_RETRY_MAX_MS
|
|
5906
|
+
);
|
|
5907
|
+
this.commandRestoreRetryAttempt += 1;
|
|
5908
|
+
this.commandRestoreRetryHandle = globalThis.setTimeout(() => {
|
|
5909
|
+
this.commandRestoreRetryHandle = null;
|
|
5910
|
+
if (!this.disposed && generation === this.commandRecoveryGeneration) {
|
|
5911
|
+
this.scheduleHistoricalCommandRestore(generation);
|
|
5912
|
+
}
|
|
5913
|
+
}, retryDelay);
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
};
|
|
5917
|
+
const run = this.commandRestoreQueue.then(restore);
|
|
5918
|
+
this.commandRestoreQueue = run.catch(() => void 0);
|
|
5919
|
+
}
|
|
5920
|
+
flushBufferedCommands(generation) {
|
|
5921
|
+
const pending = [...this.bufferedCommandDeliveries ?? []];
|
|
5922
|
+
this.bufferedCommandDeliveries = [];
|
|
5923
|
+
let index = 0;
|
|
5924
|
+
while (index < pending.length) {
|
|
5925
|
+
if (this.disposed) return;
|
|
5926
|
+
if (generation !== this.commandRecoveryGeneration) {
|
|
5927
|
+
const currentGenerationBuffer = this.bufferedCommandDeliveries ?? [];
|
|
5928
|
+
this.bufferedCommandDeliveries = [
|
|
5929
|
+
...pending.slice(index),
|
|
5930
|
+
...currentGenerationBuffer
|
|
5931
|
+
];
|
|
5932
|
+
return;
|
|
5933
|
+
}
|
|
5934
|
+
this.deliverCommandNow(pending[index]);
|
|
5935
|
+
index += 1;
|
|
5936
|
+
const reentrantBuffer = this.bufferedCommandDeliveries;
|
|
5937
|
+
if (index === pending.length && reentrantBuffer && reentrantBuffer.length > 0) {
|
|
5938
|
+
pending.push(...reentrantBuffer);
|
|
5939
|
+
this.bufferedCommandDeliveries = [];
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5942
|
+
if (!this.disposed && generation === this.commandRecoveryGeneration) {
|
|
5943
|
+
this.bufferedCommandDeliveries = null;
|
|
4485
5944
|
}
|
|
4486
5945
|
}
|
|
4487
5946
|
/** 首次订阅前的 REST 历史建立语义事件基线,随后同一工具的订阅回放不再投递。 */
|
|
@@ -4495,25 +5954,7 @@ var AgentSession = class _AgentSession {
|
|
|
4495
5954
|
}
|
|
4496
5955
|
}
|
|
4497
5956
|
}
|
|
4498
|
-
|
|
4499
|
-
* 把历史里的指令补投给宿主页面。
|
|
4500
|
-
*
|
|
4501
|
-
* 重开会话、断线补数时,地图高亮这类"页面当前状态"必须能恢复——
|
|
4502
|
-
* 否则刷新一次页面,之前智能体让页面做的事就全丢了。
|
|
4503
|
-
* 只回放指令:toolCall / message 这些事件如果按历史全量重放,
|
|
4504
|
-
* 监听器会在挂载瞬间收到几百条陈年事件。
|
|
4505
|
-
* dispatch 内部按 deliveredCommandKeys 去重,重复调用安全。
|
|
4506
|
-
*/
|
|
4507
|
-
replayCommandsFromTurns(turns) {
|
|
4508
|
-
for (const turn of turns) {
|
|
4509
|
-
for (const block of turn.blocks) {
|
|
4510
|
-
if (block.type === "tool_bridge" && isToolBridgeContent(block.content)) {
|
|
4511
|
-
this.dispatchCommand(turn, block);
|
|
4512
|
-
}
|
|
4513
|
-
}
|
|
4514
|
-
}
|
|
4515
|
-
}
|
|
4516
|
-
ensureJoined() {
|
|
5957
|
+
ensureJoined(liveRevisionBoundary = this.liveRevision) {
|
|
4517
5958
|
if (this.joined) return Promise.resolve();
|
|
4518
5959
|
if (this.joinPending) return this.joinPending;
|
|
4519
5960
|
const join = this.runtime.emitWithAck(
|
|
@@ -4526,12 +5967,13 @@ var AgentSession = class _AgentSession {
|
|
|
4526
5967
|
5e3
|
|
4527
5968
|
).then((response) => {
|
|
4528
5969
|
if (response && response.ok === false) {
|
|
4529
|
-
throw new
|
|
4530
|
-
typeof response.message === "string" && response.message ? response.message : "Session subscription failed"
|
|
5970
|
+
throw new AgentSessionError(
|
|
5971
|
+
typeof response.message === "string" && response.message ? response.message : "Session subscription failed",
|
|
5972
|
+
typeof response.code === "string" ? response.code : void 0
|
|
4531
5973
|
);
|
|
4532
5974
|
}
|
|
4533
5975
|
this.joined = true;
|
|
4534
|
-
this._applyRunningState(Boolean(response?.is_running));
|
|
5976
|
+
this._applyRunningState(Boolean(response?.is_running), liveRevisionBoundary);
|
|
4535
5977
|
}).finally(() => {
|
|
4536
5978
|
this.joinPending = null;
|
|
4537
5979
|
});
|
|
@@ -4539,6 +5981,7 @@ var AgentSession = class _AgentSession {
|
|
|
4539
5981
|
return join;
|
|
4540
5982
|
}
|
|
4541
5983
|
async ensureIdleBeforeSend() {
|
|
5984
|
+
if (this.state.isStreaming) return;
|
|
4542
5985
|
if (Date.now() - this.recentChatEndAt > 3e3) return;
|
|
4543
5986
|
const deadline = Date.now() + 3e3;
|
|
4544
5987
|
while (Date.now() < deadline) {
|
|
@@ -4573,6 +6016,7 @@ var SessionHub = class {
|
|
|
4573
6016
|
connectionListeners = /* @__PURE__ */ new Set();
|
|
4574
6017
|
bound = false;
|
|
4575
6018
|
hasConnected = false;
|
|
6019
|
+
authBusyReconnect = new AuthBusyReconnect();
|
|
4576
6020
|
constructor(client) {
|
|
4577
6021
|
this.client = client;
|
|
4578
6022
|
}
|
|
@@ -4639,6 +6083,7 @@ var SessionHub = class {
|
|
|
4639
6083
|
/** 手动重连(例如登录态变化后)。 */
|
|
4640
6084
|
reconnect() {
|
|
4641
6085
|
this.ensureBound();
|
|
6086
|
+
this.authBusyReconnect.cancel();
|
|
4642
6087
|
const socket = this.client.socket();
|
|
4643
6088
|
socket.disconnect();
|
|
4644
6089
|
this.setConnection("connecting");
|
|
@@ -4646,11 +6091,13 @@ var SessionHub = class {
|
|
|
4646
6091
|
}
|
|
4647
6092
|
/** 断开连接(不清理 session 实例)。 */
|
|
4648
6093
|
disconnect() {
|
|
6094
|
+
this.authBusyReconnect.cancel();
|
|
4649
6095
|
this.client.socket().disconnect();
|
|
4650
6096
|
this.setConnection("disconnected");
|
|
4651
6097
|
}
|
|
4652
6098
|
/** 清理当前身份下缓存的全部会话,避免切换登录身份后复用旧快照。 */
|
|
4653
6099
|
disposeAll() {
|
|
6100
|
+
this.authBusyReconnect.cancel();
|
|
4654
6101
|
this.bootstrapPending.clear();
|
|
4655
6102
|
for (const session of [...this.sessions.values()]) {
|
|
4656
6103
|
this.forceRelease(session);
|
|
@@ -4662,6 +6109,7 @@ var SessionHub = class {
|
|
|
4662
6109
|
* turn:* / chat:* 事件都会静默丢弃。
|
|
4663
6110
|
*/
|
|
4664
6111
|
_handleSocketReplaced() {
|
|
6112
|
+
this.authBusyReconnect.cancel();
|
|
4665
6113
|
this.bound = false;
|
|
4666
6114
|
this.setConnection("disconnected");
|
|
4667
6115
|
if (this.sessions.size === 0) return;
|
|
@@ -4674,18 +6122,11 @@ var SessionHub = class {
|
|
|
4674
6122
|
;
|
|
4675
6123
|
this.client.socket().emit(event, data);
|
|
4676
6124
|
},
|
|
4677
|
-
emitWithAck: (event, data, timeoutMs) =>
|
|
4678
|
-
;
|
|
4679
|
-
this.client.socket().timeout(timeoutMs).emit(event, data, (err, response) => {
|
|
4680
|
-
if (err) {
|
|
4681
|
-
reject(err);
|
|
4682
|
-
return;
|
|
4683
|
-
}
|
|
4684
|
-
resolve(response);
|
|
4685
|
-
});
|
|
4686
|
-
}),
|
|
6125
|
+
emitWithAck: (event, data, timeoutMs) => this.client.requestCommand(event, data, timeoutMs),
|
|
4687
6126
|
ensureConnected: () => this.ensureConnected(),
|
|
4688
|
-
|
|
6127
|
+
fetchTurnsPage: (sessionId, query, init) => this.client.sessions.getSessionTurnsPage(sessionId, query, init),
|
|
6128
|
+
fetchTurnIndex: (sessionId) => this.client.sessions.getSessionTurnIndex(sessionId),
|
|
6129
|
+
fetchCommands: (sessionId) => this.client.sessions.getSessionTurnCommands(sessionId),
|
|
4689
6130
|
fetchSessionInfo: (sessionId) => this.client.sessions.getSession(sessionId),
|
|
4690
6131
|
updateReplay: (sessionId, payload) => this.client.sessions.updateReplaySession(sessionId, payload).then((result) => result.replay_state ?? null),
|
|
4691
6132
|
listDir: (sessionId, dirPath) => this.client.sessions.listDir(sessionId, dirPath),
|
|
@@ -4713,6 +6154,9 @@ var SessionHub = class {
|
|
|
4713
6154
|
if (this.sessions.get(session.sessionId) === session) {
|
|
4714
6155
|
this.sessions.delete(session.sessionId);
|
|
4715
6156
|
}
|
|
6157
|
+
if (this.sessions.size === 0) {
|
|
6158
|
+
this.authBusyReconnect.cancel();
|
|
6159
|
+
}
|
|
4716
6160
|
;
|
|
4717
6161
|
this.client.socket().emit("session:unsubscribe", { session_id: session.sessionId });
|
|
4718
6162
|
return true;
|
|
@@ -4721,10 +6165,13 @@ var SessionHub = class {
|
|
|
4721
6165
|
forceRelease(session) {
|
|
4722
6166
|
this.holdCounts.delete(session.sessionId);
|
|
4723
6167
|
session.dispose();
|
|
6168
|
+
if (this.sessions.size === 0) {
|
|
6169
|
+
this.authBusyReconnect.cancel();
|
|
6170
|
+
}
|
|
4724
6171
|
}
|
|
4725
6172
|
ensureConnected() {
|
|
4726
6173
|
const socket = this.client.socket();
|
|
4727
|
-
if (socket.connected || socket.active) return;
|
|
6174
|
+
if (socket.connected || socket.active || this.authBusyReconnect.pending) return;
|
|
4728
6175
|
this.setConnection("connecting");
|
|
4729
6176
|
socket.connect();
|
|
4730
6177
|
}
|
|
@@ -4756,17 +6203,35 @@ var SessionHub = class {
|
|
|
4756
6203
|
this.bound = true;
|
|
4757
6204
|
const socket = this.client.socket();
|
|
4758
6205
|
socket.on("connect", () => {
|
|
6206
|
+
this.authBusyReconnect.cancel();
|
|
4759
6207
|
const isReconnect = this.hasConnected;
|
|
4760
6208
|
this.hasConnected = true;
|
|
4761
6209
|
this.setConnection("connected");
|
|
4762
6210
|
if (isReconnect) {
|
|
4763
6211
|
for (const session of this.sessions.values()) {
|
|
4764
6212
|
void session._rejoinAfterReconnect();
|
|
6213
|
+
void session.refreshQueue();
|
|
4765
6214
|
}
|
|
4766
6215
|
}
|
|
4767
6216
|
});
|
|
4768
|
-
socket.on("connect_error", () =>
|
|
4769
|
-
|
|
6217
|
+
socket.on("connect_error", (error) => {
|
|
6218
|
+
this.setConnection("disconnected");
|
|
6219
|
+
if (this.sessions.size === 0) {
|
|
6220
|
+
this.authBusyReconnect.cancel();
|
|
6221
|
+
return;
|
|
6222
|
+
}
|
|
6223
|
+
if (this.authBusyReconnect.schedule(error, () => {
|
|
6224
|
+
const current = this.client.socket();
|
|
6225
|
+
if (current !== socket || current.connected || current.active) return;
|
|
6226
|
+
this.setConnection("connecting");
|
|
6227
|
+
current.connect();
|
|
6228
|
+
})) return;
|
|
6229
|
+
this.authBusyReconnect.cancel();
|
|
6230
|
+
});
|
|
6231
|
+
socket.on("disconnect", () => {
|
|
6232
|
+
this.authBusyReconnect.cancel();
|
|
6233
|
+
this.setConnection("disconnected");
|
|
6234
|
+
});
|
|
4770
6235
|
socket.io.on("reconnect_attempt", () => this.setConnection("reconnecting"));
|
|
4771
6236
|
socket.on("turn:start", (data) => {
|
|
4772
6237
|
for (const session of this.route(data.session_id)) session._handleTurnStart(data);
|
|
@@ -4778,8 +6243,19 @@ var SessionHub = class {
|
|
|
4778
6243
|
for (const session of this.route(data.session_id)) session._handleTurnEnd(data);
|
|
4779
6244
|
});
|
|
4780
6245
|
socket.on(
|
|
4781
|
-
"turn:
|
|
6246
|
+
"turn:timing",
|
|
4782
6247
|
(data) => {
|
|
6248
|
+
for (const session of this.route(data.session_id)) {
|
|
6249
|
+
session._handleTurnEvents([
|
|
6250
|
+
{ type: "llm:first_token:delivered", payload: data }
|
|
6251
|
+
]);
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
);
|
|
6255
|
+
socket.on(
|
|
6256
|
+
"turn:events",
|
|
6257
|
+
(data, acknowledge) => {
|
|
6258
|
+
acknowledgeTurnEvents(acknowledge);
|
|
4783
6259
|
for (const session of this.route(data.session_id)) {
|
|
4784
6260
|
session._handleTurnEvents(data.events ?? [], data.replayed === true);
|
|
4785
6261
|
}
|
|
@@ -4793,8 +6269,17 @@ var SessionHub = class {
|
|
|
4793
6269
|
session._handleChatEnd(data?.status ?? "completed");
|
|
4794
6270
|
}
|
|
4795
6271
|
});
|
|
4796
|
-
socket.on(
|
|
4797
|
-
|
|
6272
|
+
socket.on(
|
|
6273
|
+
"system:error",
|
|
6274
|
+
(data) => {
|
|
6275
|
+
const code = data.code == null ? void 0 : String(data.code);
|
|
6276
|
+
for (const session of this.route(data.session_id)) {
|
|
6277
|
+
session._handleSystemError(data.message, code);
|
|
6278
|
+
}
|
|
6279
|
+
}
|
|
6280
|
+
);
|
|
6281
|
+
socket.on("chat:queue:updated", (data) => {
|
|
6282
|
+
for (const session of this.route(data?.session_id)) session._handleQueueUpdated(data);
|
|
4798
6283
|
});
|
|
4799
6284
|
socket.on(
|
|
4800
6285
|
"system:notification",
|
|
@@ -4912,21 +6397,15 @@ function resolveAuthToken(options) {
|
|
|
4912
6397
|
return token ? token : null;
|
|
4913
6398
|
}
|
|
4914
6399
|
|
|
4915
|
-
// src/version.ts
|
|
4916
|
-
var SDK_NAME = "agent-client";
|
|
4917
|
-
var SDK_VERSION = true ? "2610.0.0-rc.0" : "1.1.1";
|
|
4918
|
-
|
|
4919
6400
|
// src/socket.ts
|
|
4920
|
-
function withSdkIdentity(auth) {
|
|
4921
|
-
return { ...auth ?? {}, sdk: SDK_NAME, sdk_version: SDK_VERSION };
|
|
4922
|
-
}
|
|
4923
6401
|
function createSocket(options) {
|
|
4924
6402
|
const token = resolveAuthToken(options);
|
|
4925
6403
|
return io(options.baseUrl, {
|
|
4926
6404
|
path: options.path ?? "/socket.io",
|
|
4927
6405
|
withCredentials: true,
|
|
6406
|
+
...options.transports ? { transports: options.transports } : {},
|
|
4928
6407
|
query: token ? { token } : void 0,
|
|
4929
|
-
auth: typeof options.token === "function" ? (cb) => cb(
|
|
6408
|
+
auth: typeof options.token === "function" ? (cb) => cb(buildSocketAuth(options) ?? {}) : buildSocketAuth(options) ?? {},
|
|
4930
6409
|
autoConnect: false
|
|
4931
6410
|
});
|
|
4932
6411
|
}
|
|
@@ -4945,6 +6424,14 @@ var BladeClient = class {
|
|
|
4945
6424
|
computers;
|
|
4946
6425
|
models;
|
|
4947
6426
|
sessions;
|
|
6427
|
+
/** 按用户读取的插件目录与图标;没有会话时也能用。 */
|
|
6428
|
+
plugins;
|
|
6429
|
+
chatGroups;
|
|
6430
|
+
/**
|
|
6431
|
+
* 全站访问统计:匿名读写,不带 bearer 也不触发登录刷新。
|
|
6432
|
+
* 只有内置 Web 的同源页面使用,第三方宿主页面不会被自动统计。
|
|
6433
|
+
*/
|
|
6434
|
+
siteVisits;
|
|
4948
6435
|
/** 实时会话中枢:client.sessions.connect() 内部使用,一般不直接访问。 */
|
|
4949
6436
|
hub;
|
|
4950
6437
|
constructor(options) {
|
|
@@ -4957,6 +6444,9 @@ var BladeClient = class {
|
|
|
4957
6444
|
this.computers = new ComputersResource(this);
|
|
4958
6445
|
this.models = new ModelsResource(this);
|
|
4959
6446
|
this.sessions = new SessionsResource(this);
|
|
6447
|
+
this.plugins = new PluginsResource(this);
|
|
6448
|
+
this.chatGroups = new ChatGroupsResource(this);
|
|
6449
|
+
this.siteVisits = new SiteVisitsResource(this);
|
|
4960
6450
|
this.hub = new SessionHub(this);
|
|
4961
6451
|
if (this.options.token === void 0) {
|
|
4962
6452
|
this.runtimeToken = readStoredToken(this.options.baseUrl);
|
|
@@ -5051,6 +6541,16 @@ var BladeClient = class {
|
|
|
5051
6541
|
}
|
|
5052
6542
|
return this.socketInstance;
|
|
5053
6543
|
}
|
|
6544
|
+
requestCommand(event, payload, timeoutMs) {
|
|
6545
|
+
const send = () => new Promise((resolve, reject) => {
|
|
6546
|
+
;
|
|
6547
|
+
this.socket().timeout(timeoutMs).emit(event, payload, (error, ack) => {
|
|
6548
|
+
if (error) reject(error);
|
|
6549
|
+
else resolve(ack ?? {});
|
|
6550
|
+
});
|
|
6551
|
+
});
|
|
6552
|
+
return this.options.commandTransport?.(event, payload, send) ?? send();
|
|
6553
|
+
}
|
|
5054
6554
|
async json(method, path, body) {
|
|
5055
6555
|
const response = await this.fetch(method, path, {
|
|
5056
6556
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
@@ -5115,7 +6615,7 @@ var BladeClient = class {
|
|
|
5115
6615
|
signal: init.signal
|
|
5116
6616
|
});
|
|
5117
6617
|
if (response.status === 401 && !isRetry && this.shouldRefreshFor401(url)) {
|
|
5118
|
-
const refreshed = await this.
|
|
6618
|
+
const refreshed = await this.tryRefreshUnlessAborted(init.signal);
|
|
5119
6619
|
if (refreshed) {
|
|
5120
6620
|
return this.fetch(method, path, init, true);
|
|
5121
6621
|
}
|
|
@@ -5127,7 +6627,7 @@ var BladeClient = class {
|
|
|
5127
6627
|
return response;
|
|
5128
6628
|
}
|
|
5129
6629
|
buildAuthedUrl(path) {
|
|
5130
|
-
const url =
|
|
6630
|
+
const url = buildBackendUrl(this.options.baseUrl, path);
|
|
5131
6631
|
const token = this.resolveTokenForUrl(url);
|
|
5132
6632
|
if (token) {
|
|
5133
6633
|
url.searchParams.set("token", token);
|
|
@@ -5139,6 +6639,7 @@ var BladeClient = class {
|
|
|
5139
6639
|
if (!this.isSameBackendUrl(url)) {
|
|
5140
6640
|
return result;
|
|
5141
6641
|
}
|
|
6642
|
+
result.set("X-Blade-Client", `${SDK_NAME}/${SDK_VERSION}`);
|
|
5142
6643
|
const token = this.resolveRestToken();
|
|
5143
6644
|
if (token) {
|
|
5144
6645
|
result.set("Authorization", `Bearer ${token}`);
|
|
@@ -5146,14 +6647,11 @@ var BladeClient = class {
|
|
|
5146
6647
|
return result;
|
|
5147
6648
|
}
|
|
5148
6649
|
buildUrl(path) {
|
|
5149
|
-
return
|
|
6650
|
+
return buildBackendUrl(this.options.baseUrl, path);
|
|
5150
6651
|
}
|
|
5151
6652
|
baseUrlWithTrailingSlash() {
|
|
5152
6653
|
return this.options.baseUrl.endsWith("/") ? this.options.baseUrl : `${this.options.baseUrl}/`;
|
|
5153
6654
|
}
|
|
5154
|
-
toBaseRelativePath(path) {
|
|
5155
|
-
return path.startsWith("/") ? path.slice(1) : path;
|
|
5156
|
-
}
|
|
5157
6655
|
isSameBackendUrl(url) {
|
|
5158
6656
|
const base = new URL(this.baseUrlWithTrailingSlash());
|
|
5159
6657
|
if (url.origin !== base.origin) {
|
|
@@ -5172,6 +6670,27 @@ var BladeClient = class {
|
|
|
5172
6670
|
hasExplicitBearerToken() {
|
|
5173
6671
|
return Boolean(this.resolveRestToken());
|
|
5174
6672
|
}
|
|
6673
|
+
/**
|
|
6674
|
+
* 等一次令牌刷新,但调用方中止请求时不再等下去。
|
|
6675
|
+
*
|
|
6676
|
+
* 刷新本身是共享的(`refreshPromise`,别的请求也在等它),这里只是让这次已被取消的
|
|
6677
|
+
* 请求尽早结束——否则它会一直挂在调用方(定位)的串行队列里,用户改选的新目标要等到
|
|
6678
|
+
* 刷新结束才轮得到。
|
|
6679
|
+
*/
|
|
6680
|
+
async tryRefreshUnlessAborted(signal) {
|
|
6681
|
+
if (!signal) return this.tryRefresh();
|
|
6682
|
+
if (signal.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
6683
|
+
let onAbort;
|
|
6684
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
6685
|
+
onAbort = () => reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
6686
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
6687
|
+
});
|
|
6688
|
+
try {
|
|
6689
|
+
return await Promise.race([this.tryRefresh(), aborted]);
|
|
6690
|
+
} finally {
|
|
6691
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
5175
6694
|
async tryRefresh() {
|
|
5176
6695
|
if (this.refreshPromise) {
|
|
5177
6696
|
return this.refreshPromise;
|
|
@@ -5182,7 +6701,9 @@ var BladeClient = class {
|
|
|
5182
6701
|
this.buildUrl(REFRESH_PATH).toString(),
|
|
5183
6702
|
{
|
|
5184
6703
|
method: "POST",
|
|
5185
|
-
credentials: "include"
|
|
6704
|
+
credentials: "include",
|
|
6705
|
+
// Refresh remains cookie-only; do not attach the REST bearer.
|
|
6706
|
+
headers: { "X-Blade-Client": `${SDK_NAME}/${SDK_VERSION}` }
|
|
5186
6707
|
}
|
|
5187
6708
|
);
|
|
5188
6709
|
if (!response.ok) {
|
|
@@ -5309,6 +6830,42 @@ function parseXhrHeaders(rawHeaders) {
|
|
|
5309
6830
|
return headers;
|
|
5310
6831
|
}
|
|
5311
6832
|
|
|
6833
|
+
// src/polling-backoff.ts
|
|
6834
|
+
var MAX_BACKOFF_MS = 6e4;
|
|
6835
|
+
var MAX_TIMER_MS = 2147483647;
|
|
6836
|
+
var PollingBackoff = class {
|
|
6837
|
+
constructor(intervalMs) {
|
|
6838
|
+
this.intervalMs = intervalMs;
|
|
6839
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0 || intervalMs > MAX_BACKOFF_MS) {
|
|
6840
|
+
throw new RangeError("Polling interval must be between 0 and 60000 ms");
|
|
6841
|
+
}
|
|
6842
|
+
}
|
|
6843
|
+
intervalMs;
|
|
6844
|
+
failures = 0;
|
|
6845
|
+
nextAt = 0;
|
|
6846
|
+
reset() {
|
|
6847
|
+
this.failures = 0;
|
|
6848
|
+
this.nextAt = 0;
|
|
6849
|
+
}
|
|
6850
|
+
failed(error) {
|
|
6851
|
+
if (error instanceof Error && error.name === "AbortError" || error instanceof BladeApiError && error.status < 500 && error.status !== 408 && error.status !== 429) {
|
|
6852
|
+
this.nextAt = false;
|
|
6853
|
+
return;
|
|
6854
|
+
}
|
|
6855
|
+
this.failures = Math.min(this.failures + 1, 10);
|
|
6856
|
+
const ceiling = Math.min(MAX_BACKOFF_MS, this.intervalMs * 2 ** this.failures);
|
|
6857
|
+
const backoff = Math.max(this.intervalMs, ceiling * (0.5 + Math.random() * 0.5));
|
|
6858
|
+
const header = error instanceof BladeApiError ? error.response.headers.get("Retry-After")?.trim() : void 0;
|
|
6859
|
+
const retryAfter = header ? /^\d+$/.test(header) ? Number(header) * 1e3 : Date.parse(header) - Date.now() : 0;
|
|
6860
|
+
const delay = Math.max(backoff, Number.isNaN(retryAfter) ? 0 : retryAfter);
|
|
6861
|
+
this.nextAt = delay > MAX_TIMER_MS ? false : Date.now() + delay;
|
|
6862
|
+
}
|
|
6863
|
+
/** Zero means ready, false means stop, otherwise wait at least this many ms. */
|
|
6864
|
+
waitMs() {
|
|
6865
|
+
return this.nextAt === false ? false : Math.max(0, this.nextAt - Date.now());
|
|
6866
|
+
}
|
|
6867
|
+
};
|
|
6868
|
+
|
|
5312
6869
|
// src/platform-endpoints.ts
|
|
5313
6870
|
var PLATFORM_SERVICE_NAMES = [
|
|
5314
6871
|
"os",
|
|
@@ -5328,9 +6885,6 @@ function defaultDocumentBaseUrl() {
|
|
|
5328
6885
|
if (typeof location !== "undefined" && location.origin) return `${location.origin}/`;
|
|
5329
6886
|
return document.baseURI;
|
|
5330
6887
|
}
|
|
5331
|
-
function isRecord7(value) {
|
|
5332
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5333
|
-
}
|
|
5334
6888
|
function normalizeServiceUrl(value) {
|
|
5335
6889
|
if (typeof value !== "string" || !value.trim()) return null;
|
|
5336
6890
|
if (value.includes("?") || value.includes("#")) return null;
|
|
@@ -5344,7 +6898,7 @@ function normalizeServiceUrl(value) {
|
|
|
5344
6898
|
}
|
|
5345
6899
|
}
|
|
5346
6900
|
function parsePlatformEndpoints(value) {
|
|
5347
|
-
if (!
|
|
6901
|
+
if (!isRecord(value) || !isRecord(value.services)) return null;
|
|
5348
6902
|
const services = {};
|
|
5349
6903
|
for (const name of PLATFORM_SERVICE_NAMES) {
|
|
5350
6904
|
if (!(name in value.services)) continue;
|
|
@@ -5492,14 +7046,20 @@ var Task = type2({
|
|
|
5492
7046
|
});
|
|
5493
7047
|
export {
|
|
5494
7048
|
AgentSession,
|
|
7049
|
+
AgentSessionError,
|
|
7050
|
+
AuthBusyReconnect,
|
|
5495
7051
|
BladeApiError,
|
|
5496
7052
|
BladeClient,
|
|
7053
|
+
ChatGroupsResource,
|
|
5497
7054
|
ClientProjectionBuilder,
|
|
5498
7055
|
ComputersResource,
|
|
5499
7056
|
DEFAULT_REPLAY_SPEED,
|
|
5500
7057
|
EMPTY_PLATFORM_ENDPOINTS,
|
|
7058
|
+
EMPTY_SESSION_QUEUE,
|
|
5501
7059
|
LayoutType,
|
|
5502
7060
|
ModelsResource,
|
|
7061
|
+
PluginsResource,
|
|
7062
|
+
PollingBackoff,
|
|
5503
7063
|
SDK_NAME,
|
|
5504
7064
|
SDK_VERSION,
|
|
5505
7065
|
SessionHub,
|
|
@@ -5509,9 +7069,20 @@ export {
|
|
|
5509
7069
|
Task,
|
|
5510
7070
|
TaskStatus,
|
|
5511
7071
|
acceptedPostChatFollowupCompletesLatestRun,
|
|
7072
|
+
acknowledgeTurnEvents,
|
|
7073
|
+
authBusyRetryDelayMs,
|
|
5512
7074
|
buildMessageContent,
|
|
7075
|
+
buildToolUiCardKey,
|
|
7076
|
+
canCancelQueuedMessage,
|
|
7077
|
+
canDeliverQueuedMessage,
|
|
7078
|
+
canEditQueuedMessage,
|
|
5513
7079
|
canToggleComputer,
|
|
5514
7080
|
chatErrorForDisplay,
|
|
7081
|
+
collectInlineToolUiCards,
|
|
7082
|
+
collectPreviewToolUiCards,
|
|
7083
|
+
computerDaemonVersion,
|
|
7084
|
+
computerOS,
|
|
7085
|
+
computerPlatformLabel,
|
|
5515
7086
|
computerState,
|
|
5516
7087
|
connectEmbedded,
|
|
5517
7088
|
contentPreview,
|
|
@@ -5526,14 +7097,31 @@ export {
|
|
|
5526
7097
|
getTextContent,
|
|
5527
7098
|
groupAdjacentContextRuns,
|
|
5528
7099
|
groupMessagesByLoop,
|
|
7100
|
+
hasChatRunEvent,
|
|
7101
|
+
inferToolStatus,
|
|
7102
|
+
isAppDevToolUiCard,
|
|
7103
|
+
isBackgroundWakeMessage,
|
|
5529
7104
|
isCommandEnvelope,
|
|
7105
|
+
isEntryGoneError,
|
|
5530
7106
|
isHiddenInternalMessage,
|
|
5531
7107
|
isInboundEnvelope,
|
|
7108
|
+
isInternalStatusToolUiCard,
|
|
7109
|
+
isMcpAppData,
|
|
7110
|
+
isSessionQueueSnapshot,
|
|
7111
|
+
isToolUiCard,
|
|
5532
7112
|
latestPostChatFollowup,
|
|
5533
7113
|
loadPlatformEndpoints,
|
|
5534
7114
|
normalizeMessageContent,
|
|
7115
|
+
parseQueueAck,
|
|
7116
|
+
prependOlder,
|
|
7117
|
+
queuePauseReasonLabel,
|
|
7118
|
+
queuedMessageStatusLabel,
|
|
7119
|
+
reconcileHistoricalUserTurns,
|
|
5535
7120
|
reconcileOptimisticUserTurns,
|
|
7121
|
+
replaceWindow,
|
|
5536
7122
|
resolveServiceUrl,
|
|
7123
|
+
resolveToolUiCardContent,
|
|
7124
|
+
shouldApplySnapshot,
|
|
5537
7125
|
sortComputers,
|
|
5538
7126
|
toReplaySnapshot,
|
|
5539
7127
|
transformSlashCommand
|