@tea-agent/loop-agent 0.44.0-next.3 → 0.44.0-next.4
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 +4 -3
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +2 -2
- package/dist/task/config-types.js +2 -1
- package/dist/task/contract/schema.js +1 -0
- package/dist/worker/console/chat/chat-event-store.js +32 -0
- package/dist/worker/console/chat/conversation-nodes.js +383 -0
- package/dist/worker/console/chat/pi-runtime.js +128 -12
- package/dist/worker/console/chat/provider-error.js +0 -12
- package/dist/worker/console/chat/routes.js +136 -3
- package/dist/worker/console/chat/turn-process.js +128 -7
- package/dist/worker/console/prd-intake-bridge.js +3 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-COQlLTI7.js → abnfDiagram-N423BO3Z-B_lmJjOm.js} +1 -1
- package/dist/worker/console/static/assets/{arc-zN3QkNaV.js → arc-pymM4Qeh.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Pyoyj5f2.js → architectureDiagram-T3A2C74G-Bw9EPAhi.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BNppA5WW.js → blockDiagram-VBNYF7ZC-C7_oXJp2.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-lyxL8nTx.js → c4Diagram-5PPSVZJV-LnLWDbh8.js} +1 -1
- package/dist/worker/console/static/assets/channel-Ym13M3Lb.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-0hgjcdK4.js → chunk-2GRJ4B5K-BBiu3sCW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D63N9d4i.js → chunk-2Q5K7J3B-DiXqDQml.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CUw2AyB5.js → chunk-5RXB4S5H-lmNq9nYk.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-Q_kwZT30.js → chunk-5VM5RSS4-boBu74dU.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-C7vMiKcw.js → chunk-6Q2QTUOP-CjxqoJLQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-T9GbV5Hz.js → chunk-GF5L2VYU-DL0vQ9MG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D8nUSUJI.js → chunk-JWPE2WC7-DGPqE-O0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BCH2ewdp.js → chunk-KBJHAD2P-v4BbT_r2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BTdDuDBN.js → chunk-RYQCIY6F-Ci0ynvyL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Cqm2RcKF.js → chunk-XXDRQBXY-CGsbJoTV.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeFR1wGo.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeFR1wGo.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-pjwfpVu4.js → cose-bilkent-JH36ORCC-CzoRDEI7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BvalptKA.js → cynefin-VYW2F7L2-BOwFwbX4.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BU4qJ6d7.js → cynefinDiagram-MW4NZA55-2Cfg451l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DVTpq8J9.js → dagre-VZM6K2ZE-DbT0D6Y5.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B8s1W6Fc.js → diagram-7IWD3JNH-Dfd_JoMs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DPrFk9yH.js → diagram-B4RE2ZJO-DaGm82kh.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CnQIQsHC.js → diagram-LBJQPF4R-DMoI8Ibc.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-op0DEpCv.js → diagram-Q27KOJAE-BtIFA3aY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-DXQamm4T.js → diagram-UB23O5K3-D_2X1DTZ.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-b1ad2C0r.js → ebnfDiagram-BXEA7PRR-yFW5ZYlZ.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-B4cD-ZFJ.js → erDiagram-JOGREHBK-DBXh5Svj.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CrgGjlQ1.js → flowDiagram-UKHOOZJN-C4ycm8KM.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DovmXQNV.js → ganttDiagram-PKOTCBZU-qr-_Pekr.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DpFyu2Jn.js → gitGraphDiagram-DS77QQ5N-DzOSLv_A.js} +1 -1
- package/dist/worker/console/static/assets/index-PFjB6g6l.js +482 -0
- package/dist/worker/console/static/assets/index-ZtyB6Ol-.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-D3MoQZIl.js → infoDiagram-6WML65LV-C7Hl8D7l.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Ci6C1yjS.js → ishikawaDiagram-WSZJBQD7-fBxVlr6t.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BWMo2lIP.js → journeyDiagram-NVQOT4AX-CoiJEVCl.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdthIAg3.js → kanban-definition-27J2QSJJ-TJXpxNuV.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DuFAlSOj.js → linear-CjQWz1pG.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Dqz3Slj9.js → mermaid.core-DY9_apIE.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DNVTrD09.js → mindmap-definition-FAOFIHXS-HB0Id8gM.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CgfZPfBh.js → pegDiagram-VL7TDLO6-0OSWgr6v.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-ClM9P7yo.js → pieDiagram-7S7Q4E2Y-Cei1GGC2.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DwnEak2J.js → quadrantDiagram-CIZ2JOQS-CDy0Adfu.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-rK4hxt9P.js → railroadDiagram-AXF67PYL-BwqzUWwN.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Cjqa6vo3.js → requirementDiagram-LRYGKXZP-Bif2IBRL.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D3Fa250Z.js → sankeyDiagram-W5VNT64P-CYIoNMLg.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-2PdrNajj.js → sequenceDiagram-SI44F4Z6-BDS7Y0aQ.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BBcuGeZn.js → sizeCapture-X5ZJPWSS-lZic-_Bp.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CSJX9X6s.js → stateDiagram-OKZ733FA-WCVlCqSd.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-cj5UUB6m.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-pOUU1bic.js → swimlanes-SLNWSIFB-pGccxTw_.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-C_Qp6bnh.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CAeqfQn0.js → timeline-definition-Z64GVDOM-D29RlrQA.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DYPLOMVO.js → vennDiagram-T6HMQDX7-01fEM1NU.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Dy2V0Cha.js → wardleyDiagram-T6FBY63Y-C4GeB6dq.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Dj9o7jrn.js → xychartDiagram-ELKLHX3M-BXHrXHps.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +97 -3
- package/dist/worker/console/static-src/operator-chat/process-label.js +11 -2
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +9 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +3 -1
- package/dist/workflows/dag/frontend-plan-completeness.js +13 -3
- package/dist/workflows/dag/frontend-recovery-plan.js +10 -6
- package/dist/workflows/dag/frontend-typed-event-store.js +5 -0
- package/dist/workflows/dag/frontend-writer-admission.js +1 -0
- package/dist/workflows/dag/output-protocol.js +1 -0
- package/dist/workflows/dag/rerun-task.js +1 -0
- package/docs/governance/harness-methodology-verification.md +11 -0
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-CgLPkTw2.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuSD1Mhb.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuSD1Mhb.js +0 -1
- package/dist/worker/console/static/assets/index-DHBWEwz_.css +0 -1
- package/dist/worker/console/static/assets/index-JRfTUgP7.js +0 -478
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cqskj0zt.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CM0ZIhNP.js +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
5
|
- Console 新增低饱和的「暮色蓝」深色与「雾光白」浅色主题,保留默认暖纸色;柔和背景、轻量选中态与独立按钮配色减少视觉刺激。顶部仅显示带「主题」悬停提示的紧凑明暗图标按钮,点击后展开「默认 / 深色 / 浅色」,选完自动收起,支持点击外部或 Escape 关闭;初始使用默认主题;即时切换对话、操作与观测界面,刷新后记住选择,浏览器存储不可用时仍可在当前页面切换。
|
|
6
|
-
- Operator Chat
|
|
6
|
+
- Operator Chat 消息流后续对齐:折叠摘要现在统计 retry/step/command/unknown 等 durable 过程节点,Trajectory 与 HTML 导出保留 DAG progress,并使用稳定过程身份避免直播插入导致 Thinking/工具行重挂载。
|
|
7
|
+
- Operator Chat 统一主时间线、Trajectory 与导出使用同一 DSH flow projection:Thinking、工具、重试、命令和完成态节点按事件序交错,保留 completed-turn 折叠;request/unknown 详情改为有界结构化摘要,不再回显裸节点 JSON。
|
|
8
|
+
- Operator Chat 的消息流对齐 DSH Conversation Node:模型步骤、重试、终止错误、输出上限、每次请求上下文、嵌套工具、`/sdd-bingo` 命令回执和未知事件均以稳定节点留痕;同一模型步骤的多次重试聚合为一条紧凑记录,Provider 终态错误改为轻量行且不再暴露原始响应;流式文本按动画帧合并发布,刷新和断线恢复保持同一身份与顺序,轨迹页面和导出同步展示。
|
|
9
|
+
- Operator Chat 在后台断线重连后不再丢消息:检测到中断会自动补齐缺口内容,也不会把正常的历史回放误判成丢事件而反复重连。
|
|
7
10
|
- Operator Chat 的恢复快照现在只会合并属于目标会话的乐观提交;切换会话后,旧会话尚未确认的输入不会再因延迟重连恢复而显示到新会话中。
|
|
8
11
|
- `/sdd-bingo` 成功提示现在展示上报的 Skill 与 Session ID;轨迹顶部会出现当前会话的常驻 SDD 身份栏,可一键复制、首次请求后自动更新;该上报以独立“操作”事件留痕,刷新或导出后仍在,不再伪装成工具调用。
|
|
9
12
|
- Operator Chat 发送消息后的空白光标改为 Codex 式“正在思考”,真实思考、工具过程或最终回答出现后自然接替;并修复了状态偶发显示在用户输入上方的问题。
|
|
@@ -702,5 +705,3 @@
|
|
|
702
705
|
## 历史版本
|
|
703
706
|
|
|
704
707
|
0.35.x 及更早(0.8.0 – 0.35.4-beta.0)的发布说明已归档到 [`CHANGELOG/CHANGELOG-0.35-and-earlier.md`](CHANGELOG/CHANGELOG-0.35-and-earlier.md);版本线维护边界与归档规则见 [`CHANGELOG/README.md`](CHANGELOG/README.md)。
|
|
705
|
-
|
|
706
|
-
|
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.44.0-next.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-09-
|
|
3
|
+
"version": "0.44.0-next.4",
|
|
4
|
+
"gitSha": "6d1cd2f6dd13854cd48bcd4b38718a53cc24109a",
|
|
5
|
+
"builtAt": "2026-09-15T17:21:15.173Z"
|
|
6
6
|
}
|
|
@@ -6274,7 +6274,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
6274
6274
|
});
|
|
6275
6275
|
}
|
|
6276
6276
|
}
|
|
6277
|
-
const promptForMissingPhase = (missing) => session.coverageOnly
|
|
6277
|
+
const promptForMissingPhase = (missing) => `Actionable completeness findings (repair only these facts; preserve unrelated committed facts):\n${missing.map((item) => `- fact=${item.kind}${item.id ? `/${item.id}` : ""}; requirements=${item.requirementIds.join(",")}; reason=${item.reason}; owner=${item.owner ?? "Plan"}; repairScope=${item.repairScope ?? "fact"}; next=${item.nextAction ?? "replace-fact-and-finalize"}`).join("\n")}\n\n` + (session.coverageOnly
|
|
6278
6278
|
? buildCoveragePrompt(session.coverageSlice ?? [], missing)
|
|
6279
6279
|
: isCompactLocalSession
|
|
6280
6280
|
? buildCompactLocalPrompt(missing)
|
|
@@ -6282,7 +6282,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
6282
6282
|
? buildUxRegistryPrompt(missing)
|
|
6283
6283
|
: isUxLocalSession
|
|
6284
6284
|
? buildUxPrompt(session.requirementSlice ?? [], missing)
|
|
6285
|
-
: buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds);
|
|
6285
|
+
: buildPhasePrompt(globalMockDataSegment, missing, session.requirementSlice ?? allRequirementIds));
|
|
6286
6286
|
const recovery = classifyFrontendPlanRecovery({ ...result, stopReason: readWriterThinkingExhaustionEvidence(result).stopReason });
|
|
6287
6287
|
if (recovery === "stop")
|
|
6288
6288
|
return { ...result, ok: false };
|
|
@@ -308,9 +308,10 @@ const taskConfigObjectSchema = z.object({
|
|
|
308
308
|
.object({
|
|
309
309
|
allowed: z.array(z.string().min(1)).optional(),
|
|
310
310
|
forbidden: z.array(z.string().min(1)).optional(),
|
|
311
|
+
requiredClauses: z.array(z.string().min(1)).optional(),
|
|
311
312
|
})
|
|
312
313
|
.strict()
|
|
313
|
-
.refine((entry) => (entry.allowed?.length ?? 0) > 0 || (entry.forbidden?.length ?? 0) > 0, { message: "capabilityBoundary needs allowed or forbidden entries" })
|
|
314
|
+
.refine((entry) => (entry.allowed?.length ?? 0) > 0 || (entry.forbidden?.length ?? 0) > 0 || (entry.requiredClauses?.length ?? 0) > 0, { message: "capabilityBoundary needs allowed or forbidden entries" })
|
|
314
315
|
.optional(),
|
|
315
316
|
/** Generation-frozen interaction id vocabulary. Contract interaction names
|
|
316
317
|
* must come from this set (plan finalize deterministic check). */
|
|
@@ -327,6 +327,10 @@ export function projectChatToolEventPayload(input) {
|
|
|
327
327
|
toolName: input.toolName,
|
|
328
328
|
argsPreview: preview,
|
|
329
329
|
previewHash,
|
|
330
|
+
...(input.step === undefined ? {} : { step: input.step }),
|
|
331
|
+
...(input.parentToolCallId
|
|
332
|
+
? { parentToolCallId: input.parentToolCallId }
|
|
333
|
+
: {}),
|
|
330
334
|
};
|
|
331
335
|
}
|
|
332
336
|
const { preview, previewHash } = projectToolResultPreview(input.value);
|
|
@@ -336,9 +340,37 @@ export function projectChatToolEventPayload(input) {
|
|
|
336
340
|
resultPreview: preview,
|
|
337
341
|
previewHash,
|
|
338
342
|
isError: Boolean(input.isError),
|
|
343
|
+
...(input.step === undefined ? {} : { step: input.step }),
|
|
344
|
+
...(input.parentToolCallId
|
|
345
|
+
? { parentToolCallId: input.parentToolCallId }
|
|
346
|
+
: {}),
|
|
339
347
|
...(input.operationId ? { operationId: input.operationId } : {}),
|
|
340
348
|
};
|
|
341
349
|
}
|
|
350
|
+
export function projectUnknownChatEventPayload(input) {
|
|
351
|
+
const redact = (value, depth) => {
|
|
352
|
+
if (depth >= 6)
|
|
353
|
+
return "[DEPTH_LIMIT]";
|
|
354
|
+
if (Array.isArray(value)) {
|
|
355
|
+
return value.slice(0, 50).map((item) => redact(item, depth + 1));
|
|
356
|
+
}
|
|
357
|
+
if (!value || typeof value !== "object")
|
|
358
|
+
return value;
|
|
359
|
+
const result = {};
|
|
360
|
+
for (const [key, child] of Object.entries(value).slice(0, 100)) {
|
|
361
|
+
result[key] = /authorization|cookie|password|secret|token|api[-_]?key/i.test(key)
|
|
362
|
+
? "[REDACTED]"
|
|
363
|
+
: redact(child, depth + 1);
|
|
364
|
+
}
|
|
365
|
+
return result;
|
|
366
|
+
};
|
|
367
|
+
const { preview, previewHash } = boundedRedactedJson(redact(input.value, 0));
|
|
368
|
+
return {
|
|
369
|
+
eventType: input.eventType.slice(0, 120),
|
|
370
|
+
preview,
|
|
371
|
+
previewHash,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
342
374
|
export function projectOperationEventSummary(event) {
|
|
343
375
|
const messagePreview = boundedRedactedText(event.message, 600);
|
|
344
376
|
const dataPreview = event.data === undefined
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
function finiteStep(data) {
|
|
2
|
+
return typeof data.step === "number" && Number.isSafeInteger(data.step) && data.step >= 0
|
|
3
|
+
? data.step
|
|
4
|
+
: undefined;
|
|
5
|
+
}
|
|
6
|
+
function locationFor(event, step = finiteStep(event.data)) {
|
|
7
|
+
return step === undefined
|
|
8
|
+
? { kind: "turn", turnId: event.turnId }
|
|
9
|
+
: { kind: "step", turnId: event.turnId, step };
|
|
10
|
+
}
|
|
11
|
+
function status(value) {
|
|
12
|
+
return value === "scheduled" ||
|
|
13
|
+
value === "started" ||
|
|
14
|
+
value === "cancelled" ||
|
|
15
|
+
value === "running" ||
|
|
16
|
+
value === "completed" ||
|
|
17
|
+
value === "failed" ||
|
|
18
|
+
value === "interrupted"
|
|
19
|
+
? value
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
function positiveInteger(value) {
|
|
23
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0
|
|
24
|
+
? value
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
function nonNegativeNumber(value) {
|
|
28
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
29
|
+
? value
|
|
30
|
+
: undefined;
|
|
31
|
+
}
|
|
32
|
+
function nonEmptyString(value) {
|
|
33
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
34
|
+
}
|
|
35
|
+
function unknownRetryNode(event, base) {
|
|
36
|
+
return {
|
|
37
|
+
...base,
|
|
38
|
+
key: `unknown:${event.eventId}`,
|
|
39
|
+
kind: "unknown",
|
|
40
|
+
location: locationFor(event),
|
|
41
|
+
data: {
|
|
42
|
+
eventType: "model_retry",
|
|
43
|
+
preview: "Incomplete model retry lifecycle event",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function retryStateFromEvent(data) {
|
|
48
|
+
if (data.state === "scheduled")
|
|
49
|
+
return "scheduled";
|
|
50
|
+
return data.success === false || data.state === "failed"
|
|
51
|
+
? "cancelled"
|
|
52
|
+
: "started";
|
|
53
|
+
}
|
|
54
|
+
function isRetryAttempt(value) {
|
|
55
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
56
|
+
return false;
|
|
57
|
+
const attempt = value;
|
|
58
|
+
return (positiveInteger(attempt.attempt) !== undefined &&
|
|
59
|
+
positiveInteger(attempt.maxAttempts) !== undefined &&
|
|
60
|
+
nonNegativeNumber(attempt.delayMs) !== undefined &&
|
|
61
|
+
nonEmptyString(attempt.errorMessage) !== undefined &&
|
|
62
|
+
(attempt.state === "scheduled" ||
|
|
63
|
+
attempt.state === "started" ||
|
|
64
|
+
attempt.state === "cancelled") &&
|
|
65
|
+
typeof attempt.seq === "number" &&
|
|
66
|
+
typeof attempt.at === "number");
|
|
67
|
+
}
|
|
68
|
+
/** Read the current complete attempt from a projected retry-chain node. */
|
|
69
|
+
export function modelRetryCurrent(node) {
|
|
70
|
+
if (node.kind !== "retry")
|
|
71
|
+
return undefined;
|
|
72
|
+
return isRetryAttempt(node.data.current) ? node.data.current : undefined;
|
|
73
|
+
}
|
|
74
|
+
function modelRetryAttempts(node) {
|
|
75
|
+
return Array.isArray(node.data.attempts)
|
|
76
|
+
? node.data.attempts.filter(isRetryAttempt)
|
|
77
|
+
: [];
|
|
78
|
+
}
|
|
79
|
+
function retryUpdate(node) {
|
|
80
|
+
const value = node.data.retryUpdate;
|
|
81
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
82
|
+
return undefined;
|
|
83
|
+
const candidate = value;
|
|
84
|
+
if (positiveInteger(candidate.attempt) === undefined ||
|
|
85
|
+
(candidate.state !== "started" && candidate.state !== "cancelled"))
|
|
86
|
+
return undefined;
|
|
87
|
+
return candidate;
|
|
88
|
+
}
|
|
89
|
+
/** Convert one already-redacted durable event into a stable UI node. */
|
|
90
|
+
export function conversationNodeFromEvent(event) {
|
|
91
|
+
const at = Date.parse(event.at);
|
|
92
|
+
const base = {
|
|
93
|
+
anchorSeq: event.seq,
|
|
94
|
+
updatedSeq: event.seq,
|
|
95
|
+
at: Number.isFinite(at) ? at : 0,
|
|
96
|
+
data: event.data,
|
|
97
|
+
};
|
|
98
|
+
const step = finiteStep(event.data);
|
|
99
|
+
switch (event.kind) {
|
|
100
|
+
case "step_start":
|
|
101
|
+
case "step_end": {
|
|
102
|
+
if (step === undefined)
|
|
103
|
+
return undefined;
|
|
104
|
+
const outcome = status(event.data.outcome);
|
|
105
|
+
return {
|
|
106
|
+
...base,
|
|
107
|
+
key: `step:${event.turnId}:${step}`,
|
|
108
|
+
kind: "step",
|
|
109
|
+
location: { kind: "step", turnId: event.turnId, step },
|
|
110
|
+
status: event.kind === "step_start" ? "running" : (outcome ?? "completed"),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
case "model_retry": {
|
|
114
|
+
const attempt = positiveInteger(event.data.attempt);
|
|
115
|
+
if (attempt === undefined)
|
|
116
|
+
return unknownRetryNode(event, base);
|
|
117
|
+
const retryState = retryStateFromEvent(event.data);
|
|
118
|
+
const key = `retry:${event.turnId}:${step ?? 0}`;
|
|
119
|
+
if (retryState !== "scheduled") {
|
|
120
|
+
return {
|
|
121
|
+
...base,
|
|
122
|
+
key,
|
|
123
|
+
kind: "retry",
|
|
124
|
+
location: locationFor(event),
|
|
125
|
+
status: retryState,
|
|
126
|
+
data: {
|
|
127
|
+
retryUpdate: {
|
|
128
|
+
attempt,
|
|
129
|
+
state: retryState,
|
|
130
|
+
seq: event.seq,
|
|
131
|
+
at: base.at,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const maxAttempts = positiveInteger(event.data.maxAttempts);
|
|
137
|
+
const delayMs = nonNegativeNumber(event.data.delayMs);
|
|
138
|
+
const errorMessage = nonEmptyString(event.data.errorMessage);
|
|
139
|
+
if (maxAttempts === undefined || delayMs === undefined || !errorMessage) {
|
|
140
|
+
return unknownRetryNode(event, base);
|
|
141
|
+
}
|
|
142
|
+
const current = {
|
|
143
|
+
attempt,
|
|
144
|
+
maxAttempts,
|
|
145
|
+
delayMs,
|
|
146
|
+
errorMessage,
|
|
147
|
+
state: "scheduled",
|
|
148
|
+
seq: event.seq,
|
|
149
|
+
at: base.at,
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
...base,
|
|
153
|
+
key,
|
|
154
|
+
kind: "retry",
|
|
155
|
+
location: locationFor(event),
|
|
156
|
+
status: "scheduled",
|
|
157
|
+
data: { attempts: [current], current },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
case "turn_error":
|
|
161
|
+
return {
|
|
162
|
+
...base,
|
|
163
|
+
key: `turn-error:${event.turnId}`,
|
|
164
|
+
kind: "turn-error",
|
|
165
|
+
location: locationFor(event),
|
|
166
|
+
status: "failed",
|
|
167
|
+
};
|
|
168
|
+
case "turn_max_tokens":
|
|
169
|
+
return {
|
|
170
|
+
...base,
|
|
171
|
+
key: `max-tokens:${event.turnId}:${step ?? 0}`,
|
|
172
|
+
kind: "max-tokens",
|
|
173
|
+
location: locationFor(event),
|
|
174
|
+
status: "completed",
|
|
175
|
+
};
|
|
176
|
+
case "request_context": {
|
|
177
|
+
const requestId = typeof event.data.requestId === "string" && event.data.requestId
|
|
178
|
+
? event.data.requestId
|
|
179
|
+
: event.eventId;
|
|
180
|
+
return {
|
|
181
|
+
...base,
|
|
182
|
+
key: `request:${requestId}`,
|
|
183
|
+
kind: "request",
|
|
184
|
+
location: locationFor(event),
|
|
185
|
+
status: "completed",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
case "command": {
|
|
189
|
+
const commandId = typeof event.data.commandId === "string" && event.data.commandId
|
|
190
|
+
? event.data.commandId
|
|
191
|
+
: event.eventId;
|
|
192
|
+
return {
|
|
193
|
+
...base,
|
|
194
|
+
key: `command:${commandId}`,
|
|
195
|
+
kind: "command",
|
|
196
|
+
location: { kind: "turn", turnId: event.turnId },
|
|
197
|
+
status: status(event.data.outcome) ?? "completed",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
case "unknown":
|
|
201
|
+
return {
|
|
202
|
+
...base,
|
|
203
|
+
key: `unknown:${event.eventId}`,
|
|
204
|
+
kind: "unknown",
|
|
205
|
+
location: locationFor(event),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export function conversationNodesFromEvents(events) {
|
|
210
|
+
let nodes = [];
|
|
211
|
+
for (const event of events) {
|
|
212
|
+
const node = conversationNodeFromEvent(event);
|
|
213
|
+
if (node)
|
|
214
|
+
nodes = upsertConversationNode(nodes, node);
|
|
215
|
+
}
|
|
216
|
+
return nodes;
|
|
217
|
+
}
|
|
218
|
+
/** Keyed upsert that preserves the first structural anchor and location. */
|
|
219
|
+
export function upsertConversationNode(nodes, next) {
|
|
220
|
+
if (next.kind === "retry")
|
|
221
|
+
return upsertRetryNode(nodes, next);
|
|
222
|
+
const merged = upsertBase(nodes, next);
|
|
223
|
+
if (next.kind === "step" && next.location.kind === "step") {
|
|
224
|
+
const { step: nextStep, turnId } = next.location;
|
|
225
|
+
if (next.status === "running") {
|
|
226
|
+
return transitionScheduledRetries(merged, (node) => node.location.kind === "step" &&
|
|
227
|
+
node.location.turnId === turnId &&
|
|
228
|
+
node.location.step < nextStep, "started", next.updatedSeq);
|
|
229
|
+
}
|
|
230
|
+
return transitionScheduledRetries(merged, (node) => node.location.kind === "step" &&
|
|
231
|
+
node.location.turnId === turnId &&
|
|
232
|
+
node.location.step === nextStep &&
|
|
233
|
+
node.anchorSeq < next.updatedSeq, "cancelled", next.updatedSeq);
|
|
234
|
+
}
|
|
235
|
+
if (next.kind === "turn-error") {
|
|
236
|
+
return transitionScheduledRetries(merged, (node) => node.location.kind !== "session" &&
|
|
237
|
+
next.location.kind !== "session" &&
|
|
238
|
+
node.location.turnId === next.location.turnId, "cancelled", next.updatedSeq);
|
|
239
|
+
}
|
|
240
|
+
return merged;
|
|
241
|
+
}
|
|
242
|
+
function upsertBase(nodes, next) {
|
|
243
|
+
const index = nodes.findIndex((node) => node.key === next.key);
|
|
244
|
+
if (index < 0) {
|
|
245
|
+
return [...nodes, next].sort((left, right) => left.anchorSeq - right.anchorSeq || left.key.localeCompare(right.key));
|
|
246
|
+
}
|
|
247
|
+
const previous = nodes[index];
|
|
248
|
+
if (next.updatedSeq < previous.updatedSeq)
|
|
249
|
+
return [...nodes];
|
|
250
|
+
const merged = {
|
|
251
|
+
...previous,
|
|
252
|
+
...next,
|
|
253
|
+
anchorSeq: previous.anchorSeq,
|
|
254
|
+
at: previous.at,
|
|
255
|
+
location: previous.location,
|
|
256
|
+
data: { ...previous.data, ...next.data },
|
|
257
|
+
};
|
|
258
|
+
const result = [...nodes];
|
|
259
|
+
result[index] = merged;
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
function retryTurnId(node) {
|
|
263
|
+
return node.location.kind === "session" ? undefined : node.location.turnId;
|
|
264
|
+
}
|
|
265
|
+
function retryCandidateIndex(nodes, next) {
|
|
266
|
+
const exact = nodes.findIndex((node) => node.key === next.key);
|
|
267
|
+
if (exact >= 0)
|
|
268
|
+
return exact;
|
|
269
|
+
const update = retryUpdate(next);
|
|
270
|
+
const nextCurrent = modelRetryCurrent(next);
|
|
271
|
+
const attempt = update?.attempt ?? nextCurrent?.attempt;
|
|
272
|
+
const turnId = retryTurnId(next);
|
|
273
|
+
if (attempt === undefined || !turnId)
|
|
274
|
+
return -1;
|
|
275
|
+
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
276
|
+
const candidate = nodes[index];
|
|
277
|
+
const current = modelRetryCurrent(candidate);
|
|
278
|
+
if (candidate.kind !== "retry" || retryTurnId(candidate) !== turnId || !current)
|
|
279
|
+
continue;
|
|
280
|
+
if (current.attempt === attempt)
|
|
281
|
+
return index;
|
|
282
|
+
if (nextCurrent?.state === "scheduled" && current.attempt + 1 === attempt)
|
|
283
|
+
return index;
|
|
284
|
+
}
|
|
285
|
+
return -1;
|
|
286
|
+
}
|
|
287
|
+
function upsertRetryNode(nodes, next) {
|
|
288
|
+
const index = retryCandidateIndex(nodes, next);
|
|
289
|
+
const nextCurrent = modelRetryCurrent(next);
|
|
290
|
+
const update = retryUpdate(next);
|
|
291
|
+
if (index < 0) {
|
|
292
|
+
if (nextCurrent)
|
|
293
|
+
return upsertBase(nodes, next);
|
|
294
|
+
return upsertBase(nodes, {
|
|
295
|
+
...next,
|
|
296
|
+
key: `unknown:${next.key}:${next.updatedSeq}`,
|
|
297
|
+
kind: "unknown",
|
|
298
|
+
status: undefined,
|
|
299
|
+
data: {
|
|
300
|
+
eventType: "model_retry",
|
|
301
|
+
preview: "Orphan model retry lifecycle update",
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const previous = nodes[index];
|
|
306
|
+
if (next.updatedSeq < previous.updatedSeq)
|
|
307
|
+
return [...nodes];
|
|
308
|
+
let attempts = modelRetryAttempts(previous);
|
|
309
|
+
if (nextCurrent) {
|
|
310
|
+
attempts = attempts.map((attempt) => attempt.state === "scheduled" && attempt.attempt < nextCurrent.attempt
|
|
311
|
+
? { ...attempt, state: "started" }
|
|
312
|
+
: attempt);
|
|
313
|
+
const existing = attempts.findIndex((attempt) => attempt.attempt === nextCurrent.attempt);
|
|
314
|
+
if (existing >= 0)
|
|
315
|
+
attempts[existing] = { ...attempts[existing], ...nextCurrent };
|
|
316
|
+
else
|
|
317
|
+
attempts = [...attempts, nextCurrent];
|
|
318
|
+
}
|
|
319
|
+
else if (update) {
|
|
320
|
+
attempts = attempts.map((attempt) => attempt.attempt === update.attempt
|
|
321
|
+
? { ...attempt, state: update.state }
|
|
322
|
+
: attempt);
|
|
323
|
+
}
|
|
324
|
+
const current = attempts.at(-1);
|
|
325
|
+
if (!current)
|
|
326
|
+
return [...nodes];
|
|
327
|
+
const result = [...nodes];
|
|
328
|
+
result[index] = {
|
|
329
|
+
...previous,
|
|
330
|
+
updatedSeq: next.updatedSeq,
|
|
331
|
+
status: current.state,
|
|
332
|
+
data: { attempts, current },
|
|
333
|
+
};
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
function transitionScheduledRetries(nodes, predicate, state, updatedSeq) {
|
|
337
|
+
return nodes.map((node) => {
|
|
338
|
+
const current = modelRetryCurrent(node);
|
|
339
|
+
if (node.kind !== "retry" || current?.state !== "scheduled" || !predicate(node)) {
|
|
340
|
+
return node;
|
|
341
|
+
}
|
|
342
|
+
const attempts = modelRetryAttempts(node).map((attempt) => attempt.attempt === current.attempt
|
|
343
|
+
? { ...attempt, state }
|
|
344
|
+
: attempt);
|
|
345
|
+
const nextCurrent = attempts.at(-1);
|
|
346
|
+
return {
|
|
347
|
+
...node,
|
|
348
|
+
updatedSeq: Math.max(node.updatedSeq, updatedSeq),
|
|
349
|
+
status: state,
|
|
350
|
+
data: { attempts, current: nextCurrent },
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/** Resolve explicit parent links only. Cycles and excessive chains fail flat. */
|
|
355
|
+
export function toolNestingDepths(tools, maxDepth = 32) {
|
|
356
|
+
const byId = new Map(tools.map((tool) => [tool.id, tool]));
|
|
357
|
+
const depths = new Map();
|
|
358
|
+
const cyclic = new Set();
|
|
359
|
+
const resolve = (id, path) => {
|
|
360
|
+
const cached = depths.get(id);
|
|
361
|
+
if (cached !== undefined)
|
|
362
|
+
return cached;
|
|
363
|
+
const cycleAt = path.indexOf(id);
|
|
364
|
+
if (cycleAt >= 0) {
|
|
365
|
+
for (const key of path.slice(cycleAt))
|
|
366
|
+
cyclic.add(key);
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
if (path.length >= maxDepth)
|
|
370
|
+
return 0;
|
|
371
|
+
const tool = byId.get(id);
|
|
372
|
+
const parent = tool?.parentToolCallId;
|
|
373
|
+
if (!parent || !byId.has(parent) || parent === id)
|
|
374
|
+
return 0;
|
|
375
|
+
const depth = Math.min(maxDepth, resolve(parent, [...path, id]) + 1);
|
|
376
|
+
return cyclic.has(id) ? 0 : depth;
|
|
377
|
+
};
|
|
378
|
+
for (const tool of tools)
|
|
379
|
+
depths.set(tool.id, resolve(tool.id, []));
|
|
380
|
+
for (const id of cyclic)
|
|
381
|
+
depths.set(id, 0);
|
|
382
|
+
return depths;
|
|
383
|
+
}
|