@tea-agent/loop-agent 0.44.0-next.6 → 0.44.0-next.7
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/CHANGELOG.md +285 -454
- package/dist/build-stamp.json +3 -3
- package/dist/shared/git-workspace-status.js +2 -0
- package/dist/worker/console/chat/assistant-content.js +5 -34
- package/dist/worker/console/chat/chat-event-store.js +27 -10
- package/dist/worker/console/chat/db-connections.js +72 -8
- package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +52 -24
- package/dist/worker/console/chat/pi-runtime.js +34 -0
- package/dist/worker/console/chat/routes.js +138 -9
- package/dist/worker/console/chat/session-store.js +147 -19
- package/dist/worker/console/chat/turn-process.js +69 -23
- package/dist/worker/console/chat/user-turn-outline.js +19 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Brn2sPzL.js → abnfDiagram-N423BO3Z-DTuku_-7.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BIMNTG0W.js → arc-CQbCsSqb.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G--WtcgR_A.js → architectureDiagram-T3A2C74G-C-xUntTR.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BT_Ztog1.js → blockDiagram-VBNYF7ZC-DhNlwGAl.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5hbVr-l.js → c4Diagram-5PPSVZJV-B7j3nx8o.js} +1 -1
- package/dist/worker/console/static/assets/channel-5_os7t_x.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BKKnyR9Z.js → chunk-2GRJ4B5K-BCrcxEh2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-BHs6biqA.js → chunk-2Q5K7J3B-CsaePDb3.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BdSFy_vP.js → chunk-5RXB4S5H--SRQc1rN.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BpJIdTKV.js → chunk-5VM5RSS4-C5BFY2Po.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C_iWJhkT.js → chunk-6Q2QTUOP-Caso6uN7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-BTbmx3-k.js → chunk-GF5L2VYU-AY2ZSAx2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D4Ee6DmP.js → chunk-JWPE2WC7-DiMqZQ_8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-B487SORR.js → chunk-KBJHAD2P-gpvvriOp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BKYHjNrr.js → chunk-RYQCIY6F-CUdyoJhI.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DrTdoYZG.js → chunk-XXDRQBXY-Bp1xW3T4.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CttUY_Jo.js → cose-bilkent-JH36ORCC-D-JIcOIT.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B4Rm5gnR.js → cynefin-VYW2F7L2-C-NKFflx.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-ByxHBq_S.js → cynefinDiagram-MW4NZA55-CSOcvOAA.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DGrfNFA0.js → dagre-VZM6K2ZE-BQSBltMy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-KjrUPquv.js → diagram-7IWD3JNH-B0LVC45G.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-VS4mlm75.js → diagram-B4RE2ZJO-Cypg3nqy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BLXYmFTM.js → diagram-LBJQPF4R-BBDU88XJ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C6xoFAqH.js → diagram-Q27KOJAE-BZusLqSb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-C478dCe7.js → diagram-UB23O5K3-Cfolotcr.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DHEpW5zN.js → ebnfDiagram-BXEA7PRR-CjfyUy0m.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CA5GKfo9.js → erDiagram-JOGREHBK-DJdz7txE.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-zysi0Fl-.js → flowDiagram-UKHOOZJN-CQqgPO6y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DGzcKA8Y.js → ganttDiagram-PKOTCBZU-BOpGfS2m.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CHsuIG1w.js → gitGraphDiagram-DS77QQ5N-C6UCu0qx.js} +1 -1
- package/dist/worker/console/static/assets/index-CUT440A8.css +1 -0
- package/dist/worker/console/static/assets/index-DWKejNVJ.js +486 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BexZHPCp.js → infoDiagram-6WML65LV-BWeaVEUB.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Gkx8q-DB.js → ishikawaDiagram-WSZJBQD7-C8gmlBbk.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-XDW3qrJ7.js → journeyDiagram-NVQOT4AX-DF3bm8Bp.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DJ07eEaN.js → kanban-definition-27J2QSJJ-D2fG2Vxi.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DL-zeqOn.js → linear-BGKgMc2T.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-C0HdXtOf.js → mermaid.core-DF6mMPJ9.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CqRX0lrt.js → mindmap-definition-FAOFIHXS-AMhePheC.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVM9JZ2H.js → pegDiagram-VL7TDLO6-DN1Cl0W0.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BA3ozdKk.js → pieDiagram-7S7Q4E2Y-Cx9ngUeN.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B-0_p9_4.js → quadrantDiagram-CIZ2JOQS-BcYPTQJb.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DHRzPWFc.js → railroadDiagram-AXF67PYL-BqHUZWnH.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-9BEC_PcW.js → requirementDiagram-LRYGKXZP-CmqWZ0-B.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DMxeW4rN.js → sankeyDiagram-W5VNT64P--4YetH1y.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DixP6LIU.js → sequenceDiagram-SI44F4Z6-DGD4VMfD.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B_EifJBo.js → sizeCapture-X5ZJPWSS-QVw9q7rh.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BwikwWbX.js → stateDiagram-OKZ733FA-CuemV9pe.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-C162l9o1.js → swimlanes-SLNWSIFB-C7oKzZKs.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-mpe_u1i4.js → timeline-definition-Z64GVDOM-C2Of60nT.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DyUk54HX.js → vennDiagram-T6HMQDX7-DHtpQHyL.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-HYyMkzfm.js → wardleyDiagram-T6FBY63Y-COi9OhSy.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DRoh7skR.js → xychartDiagram-ELKLHX3M-DPHQfRrt.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +58 -3
- package/dist/worker/console/static-src/operator-chat/process-label.js +19 -6
- package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -0
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
- package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +202 -43
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -23
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +55 -18
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +5 -2
- package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
- package/dist/worker/scheduler/scheduled-goal-evidence.js +6 -1
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-DNWB4hpR.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-COhFgJji.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-COhFgJji.js +0 -1
- package/dist/worker/console/static/assets/index-CJh6pVLm.js +0 -482
- package/dist/worker/console/static/assets/index-C_kvKmFM.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-S2ZHMo1c.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BZm4ByfV.js +0 -8
|
@@ -19,6 +19,7 @@ import { OPERATOR_CHAT_ALLOWED_TOOLS, OPERATOR_CHAT_DENIED_TOOLS, OPERATOR_CHAT_
|
|
|
19
19
|
import { InvalidChatSessionModeError, SESSION_MODE_ACTION_DENIED_CODE, SESSION_MODE_ACTION_DENIED_MESSAGE, SESSION_MODE_IMMUTABLE_CODE, UnknownChatSessionModeError, isWorkflowSessionMode, parseCreateSessionMode, readStoredSessionMode, shouldAcceptOperatorSnapshot, } from "./session-mode.js";
|
|
20
20
|
import { buildModelCallableToolSchemas } from "./tool-adapter.js";
|
|
21
21
|
import { cleanChatTitle, acceptedOperationIdFromResult, isBlankChatSession } from "./session-store.js";
|
|
22
|
+
import { projectUserTurnOutline } from "./user-turn-outline.js";
|
|
22
23
|
import { hashChatTurnPayload, projectChatToolEventPayload, projectUnknownChatEventPayload, projectCompactSnapshot, } from "./chat-event-store.js";
|
|
23
24
|
import { projectDeferredTurnSummary, projectDeferredTurnSummaries, } from "./deferred-turn.js";
|
|
24
25
|
import { THINKING_LEVELS, RuntimeSnapshotUnavailableError, } from "./pi-runtime.js";
|
|
@@ -231,6 +232,43 @@ function turnSummary(turn) {
|
|
|
231
232
|
abortRequestedAt: turn.abortRequestedAt ?? null,
|
|
232
233
|
};
|
|
233
234
|
}
|
|
235
|
+
function turnStateSummary(turn) {
|
|
236
|
+
return {
|
|
237
|
+
turnId: turn.turnId,
|
|
238
|
+
state: turn.state === "queued" ? "running" : turn.state,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function turnStatesForIds(events, sessionId, turnIds) {
|
|
242
|
+
const projected = new Map();
|
|
243
|
+
for (const turnId of turnIds) {
|
|
244
|
+
if (!turnId || projected.has(turnId))
|
|
245
|
+
continue;
|
|
246
|
+
const turn = events.getTurn(sessionId, turnId);
|
|
247
|
+
if (turn)
|
|
248
|
+
projected.set(turnId, turnStateSummary(turn));
|
|
249
|
+
}
|
|
250
|
+
return [...projected.values()];
|
|
251
|
+
}
|
|
252
|
+
const MAX_STATE_QUERY_TURN_IDS = 8;
|
|
253
|
+
const MAX_STATE_QUERY_TURN_ID_CHARS = 240;
|
|
254
|
+
function requestedStateTurnIds(req) {
|
|
255
|
+
const result = [];
|
|
256
|
+
const seen = new Set();
|
|
257
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
258
|
+
for (const raw of url.searchParams.getAll("turnId")) {
|
|
259
|
+
const turnId = raw.trim();
|
|
260
|
+
if (!turnId ||
|
|
261
|
+
turnId.length > MAX_STATE_QUERY_TURN_ID_CHARS ||
|
|
262
|
+
seen.has(turnId)) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
seen.add(turnId);
|
|
266
|
+
result.push(turnId);
|
|
267
|
+
if (result.length >= MAX_STATE_QUERY_TURN_IDS)
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
234
272
|
function isTerminalTurnState(state) {
|
|
235
273
|
return state === "settled" || state === "aborted" || state === "failed";
|
|
236
274
|
}
|
|
@@ -421,6 +459,20 @@ export function turnEventToStorePartial(event) {
|
|
|
421
459
|
: {}),
|
|
422
460
|
}),
|
|
423
461
|
};
|
|
462
|
+
case "tool_update":
|
|
463
|
+
return {
|
|
464
|
+
kind: "tool_update",
|
|
465
|
+
data: projectChatToolEventPayload({
|
|
466
|
+
kind: "tool_update",
|
|
467
|
+
toolCallId: String(event.toolCallId ?? ""),
|
|
468
|
+
toolName: String(event.toolName ?? ""),
|
|
469
|
+
value: event.partialResult,
|
|
470
|
+
...(typeof event.step === "number" ? { step: event.step } : {}),
|
|
471
|
+
...(typeof event.parentToolCallId === "string"
|
|
472
|
+
? { parentToolCallId: event.parentToolCallId }
|
|
473
|
+
: {}),
|
|
474
|
+
}),
|
|
475
|
+
};
|
|
424
476
|
case "tool_result": {
|
|
425
477
|
// Plan A (2026-08-16): surface the accepted-operation link on the live
|
|
426
478
|
// SSE frame so the frontend semantic event can resolve operation:<id>.
|
|
@@ -924,25 +976,47 @@ export async function handleForkChatSession(req, res, deps, sourceSessionId) {
|
|
|
924
976
|
});
|
|
925
977
|
return;
|
|
926
978
|
}
|
|
979
|
+
let forkedHandle;
|
|
980
|
+
let forkPersisted = false;
|
|
927
981
|
try {
|
|
982
|
+
const body = await readJsonBody(req);
|
|
983
|
+
const throughMessageId = typeof body.throughMessageId === "string" && body.throughMessageId.trim()
|
|
984
|
+
? body.throughMessageId.trim()
|
|
985
|
+
: undefined;
|
|
986
|
+
const throughUserMessageIndex = throughMessageId
|
|
987
|
+
? source.messages
|
|
988
|
+
.filter((message) => message.role === "user")
|
|
989
|
+
.findIndex((message) => message.id === throughMessageId)
|
|
990
|
+
: undefined;
|
|
991
|
+
if (throughMessageId && throughUserMessageIndex === -1) {
|
|
992
|
+
throw new Error("FORK_MESSAGE_NOT_FOUND");
|
|
993
|
+
}
|
|
928
994
|
const handle = await deps.runtime.forkSession({
|
|
929
995
|
sourceSessionFile: source.sessionFile ?? "",
|
|
930
996
|
sessionMode: source.sessionMode,
|
|
931
997
|
...(source.modelProvider && source.modelId
|
|
932
998
|
? { model: { provider: source.modelProvider, modelId: source.modelId } }
|
|
933
999
|
: {}),
|
|
1000
|
+
...(throughUserMessageIndex !== undefined
|
|
1001
|
+
? { throughUserMessageIndex }
|
|
1002
|
+
: {}),
|
|
934
1003
|
});
|
|
1004
|
+
forkedHandle = handle;
|
|
935
1005
|
await deps.store.forkFrom({
|
|
936
1006
|
sourceSessionId,
|
|
937
1007
|
targetSessionId: handle.sessionId,
|
|
938
1008
|
targetSessionFile: handle.sessionFile,
|
|
1009
|
+
...(throughMessageId ? { throughMessageId } : {}),
|
|
939
1010
|
});
|
|
1011
|
+
forkPersisted = true;
|
|
940
1012
|
await deps.attachSession?.(handle.sessionId).catch(() => undefined);
|
|
941
|
-
const copied =
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1013
|
+
const copied = throughMessageId
|
|
1014
|
+
? { copiedCount: 0 }
|
|
1015
|
+
: deps.events.forkContextRefs({
|
|
1016
|
+
sourceSessionId,
|
|
1017
|
+
targetSessionId: handle.sessionId,
|
|
1018
|
+
targetTurnId: `${handle.sessionId}:fork`,
|
|
1019
|
+
});
|
|
946
1020
|
await deps.operationLinker.recoverSession(handle.sessionId);
|
|
947
1021
|
sendJson(res, 201, {
|
|
948
1022
|
ok: true,
|
|
@@ -956,6 +1030,12 @@ export async function handleForkChatSession(req, res, deps, sourceSessionId) {
|
|
|
956
1030
|
});
|
|
957
1031
|
}
|
|
958
1032
|
catch (error) {
|
|
1033
|
+
if (forkedHandle && !forkPersisted) {
|
|
1034
|
+
deps.runtime.dispose(forkedHandle.sessionId);
|
|
1035
|
+
await deps.store
|
|
1036
|
+
.discardUnownedSession(forkedHandle.sessionId, forkedHandle.sessionFile)
|
|
1037
|
+
.catch(() => undefined);
|
|
1038
|
+
}
|
|
959
1039
|
sendJson(res, 409, {
|
|
960
1040
|
ok: false,
|
|
961
1041
|
error: {
|
|
@@ -1105,12 +1185,18 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
|
|
|
1105
1185
|
const sddSessionIdentity = deps.sddDataAlignment && (await deps.sddDataAlignment.isEligible())
|
|
1106
1186
|
? { telemetrySessionId: record.telemetrySessionId ?? null }
|
|
1107
1187
|
: undefined;
|
|
1188
|
+
const projectedMessages = projectStoredMessagesWithEventOrder(messagePage.messages, snapshot);
|
|
1189
|
+
const turnStates = turnStatesForIds(deps.events, sessionId, [
|
|
1190
|
+
...projectedMessages.map((message) => message.turnId),
|
|
1191
|
+
...snapshot.map((event) => event.turnId),
|
|
1192
|
+
]);
|
|
1108
1193
|
sendJson(res, 200, {
|
|
1109
1194
|
ok: true,
|
|
1110
1195
|
eventEpoch: deps.events.epoch(sessionId),
|
|
1111
1196
|
session: {
|
|
1112
1197
|
...projectBrowserChatSession(record),
|
|
1113
|
-
messages:
|
|
1198
|
+
messages: projectedMessages,
|
|
1199
|
+
turnStates,
|
|
1114
1200
|
composerDraft,
|
|
1115
1201
|
activeTurnId: activeTurn?.turnId,
|
|
1116
1202
|
turnPhase: activeTurn
|
|
@@ -1130,6 +1216,7 @@ export async function handleGetChatSession(_req, res, deps, sessionId) {
|
|
|
1130
1216
|
sessionStats: telemetry.stats,
|
|
1131
1217
|
usage: telemetry.usage,
|
|
1132
1218
|
turnTelemetry: deriveChatTurnTelemetry(snapshot),
|
|
1219
|
+
userTurnOutline: projectUserTurnOutline(record.messages ?? []),
|
|
1133
1220
|
...(sddSessionIdentity
|
|
1134
1221
|
? { sddDataAlignment: sddSessionIdentity }
|
|
1135
1222
|
: {}),
|
|
@@ -1408,7 +1495,14 @@ function emitUserMessageEvent(deps, sessionId, turnId, userMessage, clientReques
|
|
|
1408
1495
|
data: {
|
|
1409
1496
|
...(clientRequestId ? { clientRequestId } : {}),
|
|
1410
1497
|
...(steering ? { steering: true } : {}),
|
|
1411
|
-
message: {
|
|
1498
|
+
message: {
|
|
1499
|
+
id: userMessage.id,
|
|
1500
|
+
text: userMessage.text,
|
|
1501
|
+
at: userMessage.at,
|
|
1502
|
+
...(userMessage.attachments?.length
|
|
1503
|
+
? { attachments: userMessage.attachments }
|
|
1504
|
+
: {}),
|
|
1505
|
+
},
|
|
1412
1506
|
},
|
|
1413
1507
|
});
|
|
1414
1508
|
}
|
|
@@ -1416,7 +1510,7 @@ function emitUserMessageEvent(deps, sessionId, turnId, userMessage, clientReques
|
|
|
1416
1510
|
* get_state equivalent): exposes the active turn + the latest turn's durable
|
|
1417
1511
|
* outcome WITHOUT the full message payload, so the UI can poll it on an
|
|
1418
1512
|
* interval / visibility / online without paying for session detail. */
|
|
1419
|
-
export async function handleGetChatSessionState(
|
|
1513
|
+
export async function handleGetChatSessionState(req, res, deps, sessionId) {
|
|
1420
1514
|
const record = await deps.store.get(sessionId);
|
|
1421
1515
|
if (!record) {
|
|
1422
1516
|
sendJson(res, 404, {
|
|
@@ -1447,9 +1541,15 @@ export async function handleGetChatSessionState(_req, res, deps, sessionId) {
|
|
|
1447
1541
|
: runtimeBusy
|
|
1448
1542
|
? "running"
|
|
1449
1543
|
: "idle";
|
|
1544
|
+
const turnStates = turnStatesForIds(deps.events, sessionId, [
|
|
1545
|
+
active?.turnId,
|
|
1546
|
+
latest?.turnId,
|
|
1547
|
+
...requestedStateTurnIds(req),
|
|
1548
|
+
]);
|
|
1450
1549
|
sendJson(res, 200, {
|
|
1451
1550
|
ok: true,
|
|
1452
1551
|
state: {
|
|
1552
|
+
turnStates,
|
|
1453
1553
|
// Legacy flat fields retained for older clients.
|
|
1454
1554
|
activeTurnId: active?.turnId ?? null,
|
|
1455
1555
|
activeTurnState: active?.state ?? null,
|
|
@@ -4484,7 +4584,14 @@ export async function handleChatMessages(res, deps, sessionId, url) {
|
|
|
4484
4584
|
limit: Number.isFinite(limit) ? limit : 30,
|
|
4485
4585
|
beforeId: url.searchParams.get("beforeId") ?? undefined,
|
|
4486
4586
|
});
|
|
4487
|
-
|
|
4587
|
+
const snapshot = deps.events.snapshot(sessionId);
|
|
4588
|
+
const messages = projectStoredMessagesWithEventOrder(page.messages, snapshot);
|
|
4589
|
+
sendJson(res, 200, {
|
|
4590
|
+
ok: true,
|
|
4591
|
+
...page,
|
|
4592
|
+
messages,
|
|
4593
|
+
turnStates: turnStatesForIds(deps.events, sessionId, messages.map((message) => message.turnId)),
|
|
4594
|
+
});
|
|
4488
4595
|
}
|
|
4489
4596
|
catch (error) {
|
|
4490
4597
|
sendJson(res, 404, {
|
|
@@ -4496,6 +4603,23 @@ export async function handleChatMessages(res, deps, sessionId, url) {
|
|
|
4496
4603
|
});
|
|
4497
4604
|
}
|
|
4498
4605
|
}
|
|
4606
|
+
export async function handleChatAttachment(res, deps, sessionId, attachmentId) {
|
|
4607
|
+
const attachment = await deps.store.readAttachment(sessionId, attachmentId);
|
|
4608
|
+
if (!attachment) {
|
|
4609
|
+
sendJson(res, 404, {
|
|
4610
|
+
ok: false,
|
|
4611
|
+
error: { code: "NOT_FOUND", message: "chat attachment not found" },
|
|
4612
|
+
});
|
|
4613
|
+
return;
|
|
4614
|
+
}
|
|
4615
|
+
res.writeHead(200, {
|
|
4616
|
+
"content-type": attachment.metadata.mimeType,
|
|
4617
|
+
"content-length": String(attachment.bytes.length),
|
|
4618
|
+
"cache-control": "private, max-age=31536000, immutable",
|
|
4619
|
+
"x-content-type-options": "nosniff",
|
|
4620
|
+
});
|
|
4621
|
+
res.end(attachment.bytes);
|
|
4622
|
+
}
|
|
4499
4623
|
/**
|
|
4500
4624
|
* Dispatch a chat HTTP request. Returns true if handled.
|
|
4501
4625
|
* Mounted under /api/operator/v1/chat/*.
|
|
@@ -4933,6 +5057,11 @@ async function dispatchChatRequest(req, res, deps, pathname, workspaceScope) {
|
|
|
4933
5057
|
await handleChatMessages(res, deps, decodeURIComponent(sessionMessages[1]), url);
|
|
4934
5058
|
return true;
|
|
4935
5059
|
}
|
|
5060
|
+
const sessionAttachment = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/attachments\/([^/]+)$/);
|
|
5061
|
+
if (method === "GET" && sessionAttachment) {
|
|
5062
|
+
await handleChatAttachment(res, deps, decodeURIComponent(sessionAttachment[1]), decodeURIComponent(sessionAttachment[2]));
|
|
5063
|
+
return true;
|
|
5064
|
+
}
|
|
4936
5065
|
const sessionPrompt = pathname.match(/^\/api\/operator\/v1\/chat\/sessions\/([^/]+)\/prompt$/);
|
|
4937
5066
|
if (method === "POST" && sessionPrompt) {
|
|
4938
5067
|
await handleChatPrompt(req, res, deps, decodeURIComponent(sessionPrompt[1]));
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* operator action call. Every invocation is re-authorized here.
|
|
12
12
|
*/
|
|
13
13
|
import path from "node:path";
|
|
14
|
-
import {
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { copyFile, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
15
16
|
import { readJsonIfExists, writeSecureJson, } from "../../../infrastructure/console/app-data.js";
|
|
16
17
|
import { dispatchOperatorAction } from "../operator-actions.js";
|
|
17
18
|
import { authorizeOperatorChatTool } from "./tools.js";
|
|
@@ -19,6 +20,18 @@ import { authorizeChatSessionTool, DEFAULT_CONSOLE_CHAT_SESSION_MODE, readStored
|
|
|
19
20
|
import { projectOperationForChat } from "./chat-event-store.js";
|
|
20
21
|
import { scrubSecrets } from "./explore-tools.js";
|
|
21
22
|
import { hashDeferredTurnPayload, parseDeferredTurns, } from "./deferred-turn.js";
|
|
23
|
+
const CHAT_IMAGE_MIME_TYPES = new Set([
|
|
24
|
+
"image/png",
|
|
25
|
+
"image/jpeg",
|
|
26
|
+
"image/gif",
|
|
27
|
+
"image/webp",
|
|
28
|
+
]);
|
|
29
|
+
function isChatImageMimeType(value) {
|
|
30
|
+
return typeof value === "string" && CHAT_IMAGE_MIME_TYPES.has(value);
|
|
31
|
+
}
|
|
32
|
+
function isChatAttachmentId(value) {
|
|
33
|
+
return typeof value === "string" && /^[0-9a-f-]{36}$/i.test(value);
|
|
34
|
+
}
|
|
22
35
|
/** True only for a durable session that has never received user work. */
|
|
23
36
|
export function isBlankChatSession(record) {
|
|
24
37
|
return (record.state === "active" &&
|
|
@@ -48,6 +61,11 @@ export function cleanChatTitle(value) {
|
|
|
48
61
|
function recordPath(appData, sessionId) {
|
|
49
62
|
return path.join(appData.chats, `${sessionId}.json`);
|
|
50
63
|
}
|
|
64
|
+
function attachmentDir(appData, sessionId) {
|
|
65
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(sessionId))
|
|
66
|
+
throw new Error("invalid chat session id");
|
|
67
|
+
return path.join(appData.chats, "attachments", sessionId);
|
|
68
|
+
}
|
|
51
69
|
/** True when `sessionFile` lives inside this store's dedicated Pi JSONL dir. */
|
|
52
70
|
export function isConsoleOwnedSessionFile(appData, sessionFile) {
|
|
53
71
|
const value = sessionFile?.trim();
|
|
@@ -196,6 +214,12 @@ export class ChatSessionStore {
|
|
|
196
214
|
const source = await this.get(input.sourceSessionId);
|
|
197
215
|
if (!source)
|
|
198
216
|
throw new Error(`chat session not found: ${input.sourceSessionId}`);
|
|
217
|
+
const throughIndex = input.throughMessageId
|
|
218
|
+
? source.messages.findIndex((message) => message.id === input.throughMessageId && message.role === "user")
|
|
219
|
+
: -1;
|
|
220
|
+
if (input.throughMessageId && throughIndex < 0) {
|
|
221
|
+
throw new Error("FORK_MESSAGE_NOT_FOUND");
|
|
222
|
+
}
|
|
199
223
|
return this.serialize(input.targetSessionId, async () => {
|
|
200
224
|
const now = new Date().toISOString();
|
|
201
225
|
const record = {
|
|
@@ -208,11 +232,30 @@ export class ChatSessionStore {
|
|
|
208
232
|
updatedAt: now,
|
|
209
233
|
deferredTurns: [],
|
|
210
234
|
deferredQueueRevision: 0,
|
|
211
|
-
messages: structuredClone(
|
|
212
|
-
|
|
235
|
+
messages: structuredClone(throughIndex >= 0
|
|
236
|
+
? source.messages.slice(0, throughIndex + 1)
|
|
237
|
+
: source.messages),
|
|
238
|
+
toolInvocations: throughIndex >= 0 ? [] : structuredClone(source.toolInvocations),
|
|
213
239
|
};
|
|
214
|
-
|
|
215
|
-
|
|
240
|
+
const attachmentIds = record.messages.flatMap((message) => Array.isArray(message.attachments)
|
|
241
|
+
? message.attachments.map((attachment) => attachment.id)
|
|
242
|
+
: []);
|
|
243
|
+
if (!attachmentIds.every(isChatAttachmentId)) {
|
|
244
|
+
throw new Error("INVALID_CHAT_ATTACHMENT_REFERENCE");
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
if (attachmentIds.length > 0) {
|
|
248
|
+
const targetDir = attachmentDir(this.appData, input.targetSessionId);
|
|
249
|
+
await mkdir(targetDir, { recursive: true });
|
|
250
|
+
await Promise.all(attachmentIds.map((attachmentId) => copyFile(path.join(attachmentDir(this.appData, input.sourceSessionId), attachmentId), path.join(targetDir, attachmentId))));
|
|
251
|
+
}
|
|
252
|
+
await this.persist(record);
|
|
253
|
+
return record;
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
await this.removeAttachmentFiles(input.targetSessionId).catch(() => undefined);
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
216
259
|
});
|
|
217
260
|
}
|
|
218
261
|
/** Atomically backfill the per-conversation simulated identity on first use. */
|
|
@@ -286,17 +329,84 @@ export class ChatSessionStore {
|
|
|
286
329
|
const record = await this.get(sessionId);
|
|
287
330
|
if (!record)
|
|
288
331
|
return false;
|
|
289
|
-
|
|
290
|
-
await rm(recordPath(this.appData, sessionId), { force: true });
|
|
291
|
-
this.options.onRecordChange?.(sessionId, undefined);
|
|
292
|
-
// Record first, then best-effort JSONL unlink. Final consistency is
|
|
293
|
-
// "both gone" — a leftover JSONL is harmless, a leftover record is not.
|
|
294
|
-
if (isConsoleOwnedSessionFile(this.appData, sessionFile) && sessionFile) {
|
|
295
|
-
await rm(sessionFile, { force: true }).catch(() => undefined);
|
|
296
|
-
}
|
|
332
|
+
await this.removeSessionArtifacts(sessionId, record.sessionFile);
|
|
297
333
|
return true;
|
|
298
334
|
});
|
|
299
335
|
}
|
|
336
|
+
/** Roll back a runtime-created session that never acquired a durable owner. */
|
|
337
|
+
async discardUnownedSession(sessionId, sessionFile) {
|
|
338
|
+
await this.serialize(sessionId, () => this.removeSessionArtifacts(sessionId, sessionFile));
|
|
339
|
+
}
|
|
340
|
+
async removeSessionArtifacts(sessionId, sessionFile) {
|
|
341
|
+
await rm(recordPath(this.appData, sessionId), { force: true });
|
|
342
|
+
await this.removeAttachmentFiles(sessionId).catch(() => undefined);
|
|
343
|
+
this.options.onRecordChange?.(sessionId, undefined);
|
|
344
|
+
// Record first, then best-effort JSONL unlink. Final consistency is
|
|
345
|
+
// "both gone" — a leftover JSONL is harmless, a leftover record is not.
|
|
346
|
+
if (isConsoleOwnedSessionFile(this.appData, sessionFile) && sessionFile) {
|
|
347
|
+
await rm(sessionFile, { force: true }).catch(() => undefined);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async removeAttachmentFiles(sessionId, attachmentIds) {
|
|
351
|
+
const dir = attachmentDir(this.appData, sessionId);
|
|
352
|
+
if (!attachmentIds) {
|
|
353
|
+
await rm(dir, { recursive: true, force: true });
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
await Promise.all(attachmentIds.map((attachmentId) => rm(path.join(dir, attachmentId), { force: true })));
|
|
357
|
+
}
|
|
358
|
+
async saveAttachments(sessionId, images) {
|
|
359
|
+
if (images.length === 0)
|
|
360
|
+
return [];
|
|
361
|
+
if (!(await this.get(sessionId)))
|
|
362
|
+
throw new Error(`chat session not found: ${sessionId}`);
|
|
363
|
+
const dir = attachmentDir(this.appData, sessionId);
|
|
364
|
+
await mkdir(dir, { recursive: true });
|
|
365
|
+
const stored = [];
|
|
366
|
+
try {
|
|
367
|
+
for (const [index, image] of images.entries()) {
|
|
368
|
+
if (!isChatImageMimeType(image.mimeType)) {
|
|
369
|
+
throw new Error("unsupported chat attachment mime type");
|
|
370
|
+
}
|
|
371
|
+
const id = randomUUID();
|
|
372
|
+
const bytes = Buffer.from(image.data, "base64");
|
|
373
|
+
await writeFile(path.join(dir, id), bytes, { flag: "wx", mode: 0o600 });
|
|
374
|
+
stored.push({
|
|
375
|
+
id,
|
|
376
|
+
name: `图片 ${index + 1}`,
|
|
377
|
+
mimeType: image.mimeType,
|
|
378
|
+
byteLength: bytes.length,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
await this.removeAttachmentFiles(sessionId, stored.map((attachment) => attachment.id)).catch(() => undefined);
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
return stored;
|
|
387
|
+
}
|
|
388
|
+
async readAttachment(sessionId, attachmentId) {
|
|
389
|
+
if (!isChatAttachmentId(attachmentId))
|
|
390
|
+
return undefined;
|
|
391
|
+
const record = await this.get(sessionId);
|
|
392
|
+
const metadata = record?.messages
|
|
393
|
+
.flatMap((message) => Array.isArray(message.attachments) ? message.attachments : [])
|
|
394
|
+
.find((attachment) => attachment.id === attachmentId);
|
|
395
|
+
if (!metadata || !isChatImageMimeType(metadata.mimeType))
|
|
396
|
+
return undefined;
|
|
397
|
+
try {
|
|
398
|
+
return {
|
|
399
|
+
metadata,
|
|
400
|
+
bytes: await readFile(path.join(attachmentDir(this.appData, sessionId), attachmentId)),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async removeAttachments(sessionId, attachments) {
|
|
408
|
+
await this.removeAttachmentFiles(sessionId, attachments.map((attachment) => attachment.id));
|
|
409
|
+
}
|
|
300
410
|
async putDeferredTurn(sessionId, input) {
|
|
301
411
|
return this.serialize(sessionId, async () => {
|
|
302
412
|
const record = await this.get(sessionId);
|
|
@@ -454,11 +564,13 @@ export class ChatSessionStore {
|
|
|
454
564
|
const deferredTurn = index >= 0 ? queue[index] : undefined;
|
|
455
565
|
if (!deferredTurn || deferredTurn.state !== "dispatching")
|
|
456
566
|
return undefined;
|
|
567
|
+
const attachments = await this.saveAttachments(sessionId, deferredTurn.images);
|
|
457
568
|
const message = {
|
|
458
569
|
id: `m_${record.messages.length + 1}_${Math.random().toString(36).slice(2, 8)}`,
|
|
459
570
|
role: "user",
|
|
460
571
|
text,
|
|
461
572
|
at: new Date().toISOString(),
|
|
573
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
462
574
|
};
|
|
463
575
|
queue.splice(index, 1);
|
|
464
576
|
record.messages.push(message);
|
|
@@ -468,7 +580,13 @@ export class ChatSessionStore {
|
|
|
468
580
|
if (!record.displayTitle)
|
|
469
581
|
record.displayTitle = cleanChatTitle(message.text);
|
|
470
582
|
record.updatedAt = message.at;
|
|
471
|
-
|
|
583
|
+
try {
|
|
584
|
+
await this.persist(record);
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
await this.removeAttachments(sessionId, attachments).catch(() => undefined);
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
472
590
|
return {
|
|
473
591
|
record,
|
|
474
592
|
message,
|
|
@@ -623,11 +741,20 @@ export async function runChatTurn(options) {
|
|
|
623
741
|
};
|
|
624
742
|
}
|
|
625
743
|
if (options.persistUserMessage !== false) {
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
744
|
+
const attachments = await store.saveAttachments(sessionId, options.images ?? []);
|
|
745
|
+
let userMessage;
|
|
746
|
+
try {
|
|
747
|
+
userMessage = await store.appendMessage(sessionId, {
|
|
748
|
+
role: "user",
|
|
749
|
+
text,
|
|
750
|
+
...(options.clientRequestId ? { clientRequestId: options.clientRequestId } : {}),
|
|
751
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
await store.removeAttachments(sessionId, attachments).catch(() => undefined);
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
631
758
|
const afterUserMessage = await store.get(sessionId);
|
|
632
759
|
if (afterUserMessage)
|
|
633
760
|
await options.onUserMessagePersisted?.(afterUserMessage, userMessage);
|
|
@@ -687,6 +814,7 @@ export async function runChatTurn(options) {
|
|
|
687
814
|
// persisted + framed above. Nothing further to persist here.
|
|
688
815
|
break;
|
|
689
816
|
case "tool_call":
|
|
817
|
+
case "tool_update":
|
|
690
818
|
// Forwarded to the caller (SSE) for UI feedback. The actual tool
|
|
691
819
|
// execution happens in the SDK's customTools[].execute(), which
|
|
692
820
|
// routes to dispatchChatToolCall (Gate 3). Do NOT dispatch here —
|
|
@@ -51,7 +51,7 @@ export function projectTurnFlow(group) {
|
|
|
51
51
|
(item.kind === "step" && item.node.status === "running"))
|
|
52
52
|
.sort((a, b) => compareChatOrder(conversationNodeOrder(a.node), conversationNodeOrder(b.node)));
|
|
53
53
|
const postAnswer = conversationItems
|
|
54
|
-
.filter((item) => item.kind === "max-tokens")
|
|
54
|
+
.filter((item) => item.kind === "max-tokens" || item.kind === "turn-error")
|
|
55
55
|
.sort((a, b) => compareChatOrder(conversationNodeOrder(a.node), conversationNodeOrder(b.node)));
|
|
56
56
|
const preludeKeys = new Set(prelude.map((item) => item.node.key));
|
|
57
57
|
const postAnswerKeys = new Set(postAnswer.map((item) => item.node.key));
|
|
@@ -86,6 +86,19 @@ export function projectTurnFlow(group) {
|
|
|
86
86
|
}
|
|
87
87
|
return { prelude, process, postAnswer };
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Main-chat projection: request provenance and model-step bookkeeping remain
|
|
91
|
+
* available to Trajectory, but do not occupy the user-facing message stream.
|
|
92
|
+
*/
|
|
93
|
+
export function projectTimelineTurnFlow(group) {
|
|
94
|
+
const flow = projectTurnFlow(group);
|
|
95
|
+
return {
|
|
96
|
+
prelude: [],
|
|
97
|
+
process: flow.process.filter((entry) => entry.source === "process" ||
|
|
98
|
+
(entry.item.kind !== "request" && entry.item.kind !== "step")),
|
|
99
|
+
postAnswer: flow.postAnswer,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
89
102
|
function emptyGroup(key, turnId) {
|
|
90
103
|
return {
|
|
91
104
|
key,
|
|
@@ -94,6 +107,7 @@ function emptyGroup(key, turnId) {
|
|
|
94
107
|
tools: [],
|
|
95
108
|
processItems: [],
|
|
96
109
|
conversationItems: [],
|
|
110
|
+
turnClosed: false,
|
|
97
111
|
live: false,
|
|
98
112
|
};
|
|
99
113
|
}
|
|
@@ -129,6 +143,7 @@ export function hydrateSessionThread(raw) {
|
|
|
129
143
|
...(item.clientRequestId ? { clientRequestId: item.clientRequestId } : {}),
|
|
130
144
|
...(item.eventSeq === undefined ? {} : { eventSeq: item.eventSeq }),
|
|
131
145
|
...(item.turnId ? { turnId: item.turnId } : {}),
|
|
146
|
+
...(item.attachments?.length ? { attachments: item.attachments } : {}),
|
|
132
147
|
});
|
|
133
148
|
continue;
|
|
134
149
|
}
|
|
@@ -225,12 +240,17 @@ function flushToolBuffer(processItems, buffer) {
|
|
|
225
240
|
};
|
|
226
241
|
const dagProgress = dagProgressActivityForTool(tool);
|
|
227
242
|
if (dagProgress) {
|
|
228
|
-
processItems.push({
|
|
243
|
+
processItems.push({
|
|
244
|
+
kind: "dag-progress",
|
|
245
|
+
progress: dagProgress,
|
|
246
|
+
toolCallCount: 1,
|
|
247
|
+
...order,
|
|
248
|
+
});
|
|
229
249
|
continue;
|
|
230
250
|
}
|
|
231
251
|
const activity = semanticActivityForTool(tool);
|
|
232
252
|
if (activity) {
|
|
233
|
-
processItems.push({ kind: "activity", activity, ...order });
|
|
253
|
+
processItems.push({ kind: "activity", activity, toolCallCount: 1, ...order });
|
|
234
254
|
}
|
|
235
255
|
else {
|
|
236
256
|
processItems.push({ kind: "tool", tool, ...order });
|
|
@@ -238,15 +258,28 @@ function flushToolBuffer(processItems, buffer) {
|
|
|
238
258
|
}
|
|
239
259
|
}
|
|
240
260
|
function collapseDagProgressItems(items) {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
261
|
+
const collapsed = [];
|
|
262
|
+
const progressIndexByRef = new Map();
|
|
263
|
+
for (const item of items) {
|
|
264
|
+
if (item.kind !== "dag-progress") {
|
|
265
|
+
collapsed.push(item);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const existingIndex = progressIndexByRef.get(item.progress.activityRef);
|
|
269
|
+
if (existingIndex === undefined) {
|
|
270
|
+
progressIndexByRef.set(item.progress.activityRef, collapsed.length);
|
|
271
|
+
collapsed.push(item);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const existing = collapsed[existingIndex];
|
|
275
|
+
if (existing?.kind === "dag-progress") {
|
|
276
|
+
collapsed[existingIndex] = {
|
|
277
|
+
...existing,
|
|
278
|
+
toolCallCount: (existing.toolCallCount ?? 1) + (item.toolCallCount ?? 1),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return collapsed;
|
|
250
283
|
}
|
|
251
284
|
function fillProcessDetails(group) {
|
|
252
285
|
const usedToolIds = new Set();
|
|
@@ -289,6 +322,7 @@ function fillProcessDetails(group) {
|
|
|
289
322
|
items.push({
|
|
290
323
|
kind: "text",
|
|
291
324
|
text: block.text,
|
|
325
|
+
assistantMessageId: assistant.id,
|
|
292
326
|
...(anchorSeq === undefined ? {} : { eventSeq: anchorSeq }),
|
|
293
327
|
createdAt: anchorAt,
|
|
294
328
|
});
|
|
@@ -353,8 +387,16 @@ function fillProcessDetails(group) {
|
|
|
353
387
|
}
|
|
354
388
|
addAssistantNode(assistant, getDisplayableAssistantBlocks(assistant.content ?? []));
|
|
355
389
|
}
|
|
356
|
-
const blocks = finalAssistant?.content ??
|
|
357
|
-
|
|
390
|
+
const blocks = finalAssistant?.content ??
|
|
391
|
+
(finalAssistant?.text.trim()
|
|
392
|
+
? [{ type: "text", text: finalAssistant.text }]
|
|
393
|
+
: []);
|
|
394
|
+
// Without durable step-completion metadata, only a normally settled Turn can
|
|
395
|
+
// prove that its latest assistant generation is a final answer. Keep partial
|
|
396
|
+
// text in process for running/aborted/failed and legacy unknown states.
|
|
397
|
+
const split = splitFinalAssistantBlocks(blocks, {
|
|
398
|
+
isStreaming: group.live || group.turnState !== "settled",
|
|
399
|
+
});
|
|
358
400
|
if (finalAssistant) {
|
|
359
401
|
addAssistantNode(finalAssistant, split.processBlocks);
|
|
360
402
|
}
|
|
@@ -391,10 +433,6 @@ function fillProcessDetails(group) {
|
|
|
391
433
|
group.processItems = collapseDagProgressItems(processItems);
|
|
392
434
|
if (!finalAssistant)
|
|
393
435
|
return;
|
|
394
|
-
if (blocks.length === 0) {
|
|
395
|
-
group.answer = finalAssistant;
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
436
|
if (split.answerBlocks.length === 0)
|
|
399
437
|
return;
|
|
400
438
|
const answerText = joinAssistantText(split.answerBlocks);
|
|
@@ -404,9 +442,6 @@ function fillProcessDetails(group) {
|
|
|
404
442
|
content: split.answerBlocks,
|
|
405
443
|
};
|
|
406
444
|
}
|
|
407
|
-
/**
|
|
408
|
-
* Build user → process → answer groups for the message timeline.
|
|
409
|
-
*/
|
|
410
445
|
export function buildTurnProcessGroups(input) {
|
|
411
446
|
const visible = ensureLinkedSubmissionOrder(input.messages
|
|
412
447
|
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
@@ -523,10 +558,21 @@ export function buildTurnProcessGroups(input) {
|
|
|
523
558
|
}
|
|
524
559
|
return a.createdAt - b.createdAt;
|
|
525
560
|
});
|
|
561
|
+
const turnState = group.turnId
|
|
562
|
+
? input.turnStatesById?.[group.turnId]
|
|
563
|
+
: undefined;
|
|
564
|
+
if (turnState !== undefined)
|
|
565
|
+
group.turnState = turnState;
|
|
566
|
+
group.turnClosed =
|
|
567
|
+
turnState === "settled" ||
|
|
568
|
+
turnState === "aborted" ||
|
|
569
|
+
turnState === "failed";
|
|
570
|
+
const targetsActiveAssistant = input.activeAssistantId !== null &&
|
|
571
|
+
group.assistants.some((assistant) => assistant.id === input.activeAssistantId);
|
|
526
572
|
group.live =
|
|
527
573
|
input.streaming &&
|
|
528
|
-
|
|
529
|
-
|
|
574
|
+
!group.turnClosed &&
|
|
575
|
+
(targetsActiveAssistant || turnState === "running");
|
|
530
576
|
fillProcessDetails(group);
|
|
531
577
|
group.conversationItems = (input.conversationNodes ?? [])
|
|
532
578
|
.filter((node) => node.location.kind !== "session" && node.location.turnId === group.turnId)
|