@tea-agent/loop-agent 0.36.4-beta.0 → 0.37.1-beta.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/AGENTS.md +2 -6
- package/CHANGELOG.md +72 -163
- package/README.md +1 -1
- package/bin/loop-agent.js +1 -37
- package/dist/application/task-lifecycle/advance.js +0 -1
- package/dist/application/task-lifecycle/observe.js +0 -15
- package/dist/application/task-lifecycle/plan-transitions.js +0 -15
- package/dist/cli/command-definitions.js +0 -7
- package/dist/cli/program.js +3 -8
- package/dist/commands/init-upgrade.js +19 -351
- package/dist/commands/init.js +67 -14
- package/dist/commands/run-dag-progress.js +0 -14
- package/dist/commands/task-advance.js +3 -33
- package/dist/executors/dag-pi-executor.js +30 -47
- package/dist/executors/pi-sdk-executor.js +0 -34
- package/dist/executors/shell-executor.js +95 -6
- package/dist/infrastructure/harness/atomic-write.js +1 -2
- package/dist/shared/operator/capabilities.js +13 -200
- package/dist/shared/package-metadata.js +0 -42
- package/dist/task/frontend-project-capability.js +12 -162
- package/dist/task/source-prepare/completeness.js +0 -17
- package/dist/task/source-prepare/parse-intent.js +1 -15
- package/dist/task/source-prepare/semantic-intake.js +23 -144
- package/dist/worker/console/chat/artifact-card.js +1 -8
- package/dist/worker/console/chat/chat-event-store.js +0 -61
- package/dist/worker/console/chat/human-gate-card.js +1 -9
- package/dist/worker/console/chat/operation-card.js +2 -71
- package/dist/worker/console/chat/pi-runtime.js +62 -86
- package/dist/worker/console/chat/routes.js +8 -36
- package/dist/worker/console/chat/session-store.js +0 -3
- package/dist/worker/console/chat/shortcuts.js +7 -9
- package/dist/worker/console/chat/turn-process.js +23 -97
- package/dist/worker/console/chat/workspace-landing.js +1 -2
- package/dist/worker/console/operation-runner.js +6 -155
- package/dist/worker/console/operator-actions.js +13 -373
- package/dist/worker/console/operator-user-error.js +0 -4
- package/dist/worker/console/recovery-cta.js +3 -50
- package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
- package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -13
- package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
- package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -89
- package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -30
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
- package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
- package/dist/worker/loop-agent/loop-agent-client.js +3 -17
- package/dist/worker/observability/read-model.js +0 -20
- package/dist/worker/observe/spec-evidence.js +8 -3
- package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
- package/dist/worker/observe/static/operator-chrome.js +1 -6
- package/dist/worker/observe/static/views/dag-inspector.js +71 -6
- package/dist/worker/observe/static/views/dag.js +0 -12
- package/dist/worker/preflight.js +1 -2
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
- package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
- package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
- package/dist/workflows/dag/failure-routing.js +4 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
- package/dist/workflows/dag/frontend-prewrite-gate.js +62 -394
- package/dist/workflows/dag/frontend-repair.js +18 -219
- package/dist/workflows/dag/frontend-verification-trace.js +32 -47
- package/dist/workflows/dag/init-hybrid.js +86 -238
- package/dist/workflows/dag/lifecycle.js +0 -4
- package/dist/workflows/dag/node-execution.js +6 -105
- package/dist/workflows/dag/recovery-recommendation.js +0 -58
- package/dist/workflows/dag/report.js +0 -6
- package/dist/workflows/dag/retry-policy.js +0 -11
- package/dist/workflows/dag/runner.js +12 -314
- package/dist/workflows/dag/scheduler.js +3 -257
- package/dist/workflows/dag/types.js +9 -132
- package/dist/workflows/dag/validate.js +2 -7
- package/docs/README.md +3 -3
- package/docs/architecture/evolution.md +67 -102
- package/docs/templates/backend-test-dag.json +50 -32
- package/docs/templates/frontend-design-contract.md +14 -34
- package/docs/templates/frontend-task-constraints.md +13 -33
- package/docs/templates/frontend-task-requirement.md +20 -54
- package/docs/templates/init-managed-agents.md +2 -5
- package/harness.json +2 -2
- package/package.json +3 -4
- package/skills/loop-agent/SKILL.md +0 -1
- package/skills/loop-agent/references/command-reference.md +0 -3
- package/dist/build-stamp.json +0 -6
- package/dist/commands/dag-request-interrupt.js +0 -20
- package/dist/worker/console/chat/assistant-content.js +0 -121
- package/dist/worker/console/chat/semantic-activity.js +0 -471
- package/dist/worker/console/operation-run-facts.js +0 -190
- package/dist/worker/console/operation-wait.js +0 -355
- package/dist/worker/console/recovery-error-copy.js +0 -198
- package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
- package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
- package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
- package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
- package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
- package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
- package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
- package/dist/worker/observability/interrupt-eligibility.js +0 -264
- package/dist/workflows/dag/contract-output-registry.js +0 -14
- package/dist/workflows/dag/contract-validator-registrations.js +0 -8
- package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
- package/dist/workflows/dag/frontend-recovery-run.js +0 -539
- package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
- package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
- package/dist/workflows/dag/interrupt-request.js +0 -559
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<link rel="stylesheet" href="/inspect/operator-chrome.css" />
|
|
8
8
|
<title>Loop 操作台 · Operator Console</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-M0BLEBfh.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-HX1pbOyl.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { formatIneligibleRerunMessage, nextActionForIneligibleRerun, } from "../../recovery-error-copy.js";
|
|
2
1
|
/** Legacy step ids from older sessions map onto the merged contract step. */
|
|
3
2
|
export function normalizeWizardStep(step) {
|
|
4
3
|
if (step === "validate" || step === "apply" || step === "spine") {
|
|
@@ -58,7 +57,6 @@ export const RECOVERY_FACT_LABEL = {
|
|
|
58
57
|
"terminal-failed": "已终止(失败)",
|
|
59
58
|
"terminal-succeeded": "已终止(成功)",
|
|
60
59
|
"needs-reconcile": "需要对账",
|
|
61
|
-
"running-active": "正在运行",
|
|
62
60
|
};
|
|
63
61
|
export const LAYER_VALUE_LABEL = {
|
|
64
62
|
idle: "空闲",
|
|
@@ -110,20 +108,18 @@ export const LONG_RUNNING_ACTIONS = new Set([
|
|
|
110
108
|
"runDag",
|
|
111
109
|
"dagRerun",
|
|
112
110
|
"dagReconcileRun",
|
|
113
|
-
"dagRequestInterrupt",
|
|
114
111
|
"standaloneTaskRerun",
|
|
115
112
|
]);
|
|
116
|
-
export function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
113
|
+
export function formatDagRerunIneligible(plan) {
|
|
114
|
+
const fromBlocked = (plan.blockedReasons ?? []).map(String);
|
|
115
|
+
const fromNodes = (plan.blockingNodes ?? []).map((n) => [n.nodeId, n.reasonCode].filter(Boolean).join(":"));
|
|
116
|
+
const reasons = [...fromBlocked, ...fromNodes].filter(Boolean);
|
|
117
|
+
if (reasons.length === 0) {
|
|
118
|
+
return "从节点重跑不具备安全资格(子图含 writer / unsafe shell / decision-gate 等)。可改用「完整重跑任务」,或另选只读安全子图的起始节点。";
|
|
119
|
+
}
|
|
120
|
+
return `从节点重跑不具备安全资格:${reasons.slice(0, 6).join("; ")}${reasons.length > 6 ? "…" : ""}。可改用「完整重跑任务」。`;
|
|
125
121
|
}
|
|
126
|
-
export async function waitForOperation(operationId, timeoutMs =
|
|
122
|
+
export async function waitForOperation(operationId, timeoutMs = 90_000) {
|
|
127
123
|
const started = Date.now();
|
|
128
124
|
while (Date.now() - started < timeoutMs) {
|
|
129
125
|
const res = await fetch(`/api/operator/v1/operations/${encodeURIComponent(operationId)}`, { credentials: "same-origin" });
|
|
@@ -6,8 +6,8 @@ import { confirmationToken, CONFIRMATION_HEADER, LONG_RUNNING_ACTIONS, postOpera
|
|
|
6
6
|
* seconds server-side (imports + intake). Prevents the "page looks frozen"
|
|
7
7
|
* gap between click and the terminal flash message. */
|
|
8
8
|
const ACTION_PROGRESS_COPY = {
|
|
9
|
-
bootstrapFromPrd: "正在导入 PRD 并创建任务:归档文档 → 确定性解析 →
|
|
10
|
-
importPrd: "正在导入 PRD:归档文档 → 确定性解析 →
|
|
9
|
+
bootstrapFromPrd: "正在导入 PRD 并创建任务:归档文档 → 确定性解析 → 语义结构化(约 10–60 秒,请勿关闭页面)",
|
|
10
|
+
importPrd: "正在导入 PRD:归档文档 → 确定性解析 → 语义结构化(约 10–60 秒,请勿关闭页面)",
|
|
11
11
|
interviewStart: "正在启动需求访谈…",
|
|
12
12
|
interviewTurn: "正在提交访谈回答…",
|
|
13
13
|
interviewSkip: "正在跳过访谈并生成评估…",
|
|
@@ -16,8 +16,6 @@ const ACTION_PROGRESS_COPY = {
|
|
|
16
16
|
contractApply: "正在应用契约(写入需求与执行约束)…",
|
|
17
17
|
spineCheck: "正在运行 Spine 审计…",
|
|
18
18
|
dagGenerate: "正在生成 DAG…",
|
|
19
|
-
standaloneTaskRerun: "正在完整重跑任务:服务端校验资格后启动新 run…",
|
|
20
|
-
workerTaskRetry: "正在重新排队 Worker Task…",
|
|
21
19
|
};
|
|
22
20
|
/** Central operator-action dispatcher: POST /api/operator/v1/operations with
|
|
23
21
|
* polling, long-running background waits, interview payload handoff and the
|
|
@@ -1,35 +1,10 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import { isAutoRecoveryReason } from "../../recovery-selection.js";
|
|
3
|
-
import { formatOperatorUserError } from "../../operator-user-error.js";
|
|
4
3
|
import { formatDagRerunIneligible, postOperator, requestId, } from "./console-types.js";
|
|
5
|
-
/** Destructive reconcile still consumes a browser Human Gate receipt.
|
|
6
|
-
* Same-task rerun / Worker retry are autonomous and must not enter here. */
|
|
7
|
-
async function consumeMutationGateAndRun(input) {
|
|
8
|
-
const prep = await input.runAction("prepareMutationGate", {
|
|
9
|
-
action: input.action,
|
|
10
|
-
actionParams: input.actionParams,
|
|
11
|
-
});
|
|
12
|
-
if (!input.isActionSuccess(prep))
|
|
13
|
-
return;
|
|
14
|
-
const receiptSource = (prep?.result ?? prep);
|
|
15
|
-
const receipt = receiptSource.receiptId
|
|
16
|
-
? receiptSource
|
|
17
|
-
: (receiptSource.result ?? receiptSource);
|
|
18
|
-
if (!receipt.receiptId || !receipt.humanGateToken) {
|
|
19
|
-
input.setOpError("prepareMutationGate 未返回可消费的 Human Gate receipt");
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
await input.runAction(input.action, {
|
|
23
|
-
...input.actionParams,
|
|
24
|
-
confirmationId: receipt.receiptId,
|
|
25
|
-
humanGateToken: receipt.humanGateToken,
|
|
26
|
-
}, { clientRequestId: input.clientRequestId });
|
|
27
|
-
}
|
|
28
4
|
/** Recovery CTA dispatch: diagnose/report/inspect shortcuts, reason-gated
|
|
29
|
-
* confirm flow with dagRerunPlan preflight,
|
|
30
|
-
* Human Gate only for destructive reconcile. */
|
|
5
|
+
* confirm flow with dagRerunPlan preflight, and gated reruns/retries. */
|
|
31
6
|
export function useRecoveryActions(params) {
|
|
32
|
-
const { shell, actions, taskId, dagRunId, fromNodeId, featureId, recoveryReason, recoveryReasonTouched, recoveryConfirm, recoveryPlanSummary,
|
|
7
|
+
const { shell, actions, taskId, dagRunId, fromNodeId, featureId, recoveryReason, recoveryReasonTouched, recoveryConfirm, recoveryPlanSummary, setRecoveryConfirm, setRecoveryPlanSummary, setRecoveryReport, setRecoveryReason, setRecoveryReasonTouched, buildCurrentRecoveryReason, openFullCanvas, } = params;
|
|
33
8
|
const { setOpError, setOpErrorCode, setOpMessage, setBusy } = shell;
|
|
34
9
|
const { runAction, isActionSuccess } = actions;
|
|
35
10
|
const handleRecoveryCta = useCallback(async (cta) => {
|
|
@@ -108,7 +83,7 @@ export function useRecoveryActions(params) {
|
|
|
108
83
|
setOpMessage(null);
|
|
109
84
|
}
|
|
110
85
|
else if (plan.eligible === false) {
|
|
111
|
-
setOpError(
|
|
86
|
+
setOpError(body.error?.message ?? formatDagRerunIneligible(plan));
|
|
112
87
|
}
|
|
113
88
|
else {
|
|
114
89
|
setOpMessage(body.error?.message ??
|
|
@@ -175,7 +150,7 @@ export function useRecoveryActions(params) {
|
|
|
175
150
|
planHash = plan.planHash?.trim() || planHash;
|
|
176
151
|
planEligible = plan.eligible;
|
|
177
152
|
if (plan.eligible === false) {
|
|
178
|
-
setOpError(
|
|
153
|
+
setOpError(planBody?.error?.message ?? formatDagRerunIneligible(plan));
|
|
179
154
|
return;
|
|
180
155
|
}
|
|
181
156
|
if (planBody && planBody.ok === false && !planHash) {
|
|
@@ -185,7 +160,7 @@ export function useRecoveryActions(params) {
|
|
|
185
160
|
}
|
|
186
161
|
}
|
|
187
162
|
if (planEligible === false) {
|
|
188
|
-
setOpError(
|
|
163
|
+
setOpError(formatDagRerunIneligible(recoveryPlanSummary ?? {}));
|
|
189
164
|
return;
|
|
190
165
|
}
|
|
191
166
|
if (!planHash) {
|
|
@@ -201,10 +176,26 @@ export function useRecoveryActions(params) {
|
|
|
201
176
|
setOpError("完整重跑任务需要 DAG 运行 ID");
|
|
202
177
|
return;
|
|
203
178
|
}
|
|
204
|
-
|
|
179
|
+
const actionParams = {
|
|
205
180
|
runId,
|
|
206
181
|
reason,
|
|
207
182
|
...(taskId.trim() ? { taskId: taskId.trim() } : {}),
|
|
183
|
+
};
|
|
184
|
+
const prep = await runAction("prepareMutationGate", {
|
|
185
|
+
action: "standaloneTaskRerun",
|
|
186
|
+
actionParams,
|
|
187
|
+
});
|
|
188
|
+
if (!isActionSuccess(prep))
|
|
189
|
+
return;
|
|
190
|
+
const receipt = (prep?.result ?? prep);
|
|
191
|
+
if (!receipt.receiptId || !receipt.humanGateToken) {
|
|
192
|
+
setOpError("prepareMutationGate 未返回可消费的 Human Gate receipt");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
await runAction("standaloneTaskRerun", {
|
|
196
|
+
...actionParams,
|
|
197
|
+
confirmationId: receipt.receiptId,
|
|
198
|
+
humanGateToken: receipt.humanGateToken,
|
|
208
199
|
}, { clientRequestId });
|
|
209
200
|
return;
|
|
210
201
|
}
|
|
@@ -214,35 +205,27 @@ export function useRecoveryActions(params) {
|
|
|
214
205
|
setOpError("重新排队 Worker Task 需要任务 ID");
|
|
215
206
|
return;
|
|
216
207
|
}
|
|
217
|
-
|
|
208
|
+
const actionParams = {
|
|
218
209
|
taskId: id,
|
|
219
210
|
reason,
|
|
220
211
|
...(featureId.trim() ? { featureId: featureId.trim() } : {}),
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (!
|
|
227
|
-
setOpError("中止运行需要 DAG 运行 ID");
|
|
212
|
+
};
|
|
213
|
+
const prep = await runAction("prepareMutationGate", {
|
|
214
|
+
action: "workerTaskRetry",
|
|
215
|
+
actionParams,
|
|
216
|
+
});
|
|
217
|
+
if (!isActionSuccess(prep))
|
|
228
218
|
return;
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
setOpError("
|
|
219
|
+
const receipt = (prep?.result ?? prep);
|
|
220
|
+
if (!receipt.receiptId || !receipt.humanGateToken) {
|
|
221
|
+
setOpError("prepareMutationGate 未返回可消费的 Human Gate receipt");
|
|
232
222
|
return;
|
|
233
223
|
}
|
|
234
|
-
await
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
actionParams: {
|
|
240
|
-
runId,
|
|
241
|
-
reasonCode: interruptReasonCode.trim() || "operator-request",
|
|
242
|
-
reasonDetail: reason,
|
|
243
|
-
},
|
|
244
|
-
clientRequestId,
|
|
245
|
-
});
|
|
224
|
+
await runAction("workerTaskRetry", {
|
|
225
|
+
...actionParams,
|
|
226
|
+
confirmationId: receipt.receiptId,
|
|
227
|
+
humanGateToken: receipt.humanGateToken,
|
|
228
|
+
}, { clientRequestId });
|
|
246
229
|
return;
|
|
247
230
|
}
|
|
248
231
|
if (cta.id === "reconcile") {
|
|
@@ -254,18 +237,27 @@ export function useRecoveryActions(params) {
|
|
|
254
237
|
// Default supersede: orphan/interrupted runs should not be resumed.
|
|
255
238
|
// The browser Human Gate binds the destructive action + reason; the CLI
|
|
256
239
|
// still fail-closes when runner is not proven dead/stopped.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
240
|
+
const actionParams = {
|
|
241
|
+
runId,
|
|
242
|
+
reason,
|
|
243
|
+
reconcileAction: "supersede",
|
|
244
|
+
};
|
|
245
|
+
const prep = await runAction("prepareMutationGate", {
|
|
261
246
|
action: "dagReconcileRun",
|
|
262
|
-
actionParams
|
|
263
|
-
runId,
|
|
264
|
-
reason,
|
|
265
|
-
reconcileAction: "supersede",
|
|
266
|
-
},
|
|
267
|
-
clientRequestId,
|
|
247
|
+
actionParams,
|
|
268
248
|
});
|
|
249
|
+
if (!isActionSuccess(prep))
|
|
250
|
+
return;
|
|
251
|
+
const receipt = (prep?.result ?? prep);
|
|
252
|
+
if (!receipt.receiptId || !receipt.humanGateToken) {
|
|
253
|
+
setOpError("prepareMutationGate 未返回可消费的 Human Gate receipt");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
await runAction("dagReconcileRun", {
|
|
257
|
+
...actionParams,
|
|
258
|
+
confirmationId: receipt.receiptId,
|
|
259
|
+
humanGateToken: receipt.humanGateToken,
|
|
260
|
+
}, { clientRequestId });
|
|
269
261
|
return;
|
|
270
262
|
}
|
|
271
263
|
setOpMessage(`${cta.label}:请经 CLI — ${cta.commandHint ?? ""}`);
|
|
@@ -276,7 +268,6 @@ export function useRecoveryActions(params) {
|
|
|
276
268
|
recoveryConfirm,
|
|
277
269
|
recoveryPlanSummary?.planHash,
|
|
278
270
|
recoveryReason,
|
|
279
|
-
interruptReasonCode,
|
|
280
271
|
runAction,
|
|
281
272
|
isActionSuccess,
|
|
282
273
|
setOpError,
|
|
@@ -16,16 +16,12 @@ export function useRecoveryConsole(params) {
|
|
|
16
16
|
const [recoveryReasonTouched, setRecoveryReasonTouched] = useState(false);
|
|
17
17
|
const [recoveryPlanSummary, setRecoveryPlanSummary] = useState(null);
|
|
18
18
|
const [recoveryConfirm, setRecoveryConfirm] = useState(null);
|
|
19
|
-
const [interruptIntent, setInterruptIntent] = useState(false);
|
|
20
|
-
const [interruptReasonCode, setInterruptReasonCode] = useState("operator-request");
|
|
21
|
-
const [interruptEligibility, setInterruptEligibility] = useState(null);
|
|
22
19
|
const [recoveryRuns, setRecoveryRuns] = useState([]);
|
|
23
20
|
const [recoveryNodes, setRecoveryNodes] = useState([]);
|
|
24
21
|
const [recoveryRunsLoading, setRecoveryRunsLoading] = useState(false);
|
|
25
22
|
const [recoveryNodesLoading, setRecoveryNodesLoading] = useState(false);
|
|
26
23
|
const [recoveryRunStatus, setRecoveryRunStatus] = useState(null);
|
|
27
24
|
const [recoveryFailureCategory, setRecoveryFailureCategory] = useState(null);
|
|
28
|
-
const [frontendRecovery, setFrontendRecovery] = useState(null);
|
|
29
25
|
const [manualRunId, setManualRunId] = useState(false);
|
|
30
26
|
const [manualNodeId, setManualNodeId] = useState(false);
|
|
31
27
|
const [recoveryReport, setRecoveryReport] = useState(null);
|
|
@@ -128,7 +124,6 @@ export function useRecoveryConsole(params) {
|
|
|
128
124
|
setRecoveryNodes([]);
|
|
129
125
|
setRecoveryRunStatus(null);
|
|
130
126
|
setRecoveryFailureCategory(null);
|
|
131
|
-
setFrontendRecovery(null);
|
|
132
127
|
return;
|
|
133
128
|
}
|
|
134
129
|
setRecoveryNodesLoading(true);
|
|
@@ -172,13 +167,11 @@ export function useRecoveryConsole(params) {
|
|
|
172
167
|
n.failureCategory &&
|
|
173
168
|
n.failureCategory !== "success")?.failureCategory;
|
|
174
169
|
setRecoveryFailureCategory(runFailure ?? nodeFailure ?? null);
|
|
175
|
-
setFrontendRecovery(body.frontendRecovery ?? null);
|
|
176
170
|
}
|
|
177
171
|
catch {
|
|
178
172
|
setRecoveryNodes([]);
|
|
179
173
|
setRecoveryRunStatus(null);
|
|
180
174
|
setRecoveryFailureCategory(null);
|
|
181
|
-
setFrontendRecovery(null);
|
|
182
175
|
}
|
|
183
176
|
finally {
|
|
184
177
|
setRecoveryNodesLoading(false);
|
|
@@ -211,37 +204,6 @@ export function useRecoveryConsole(params) {
|
|
|
211
204
|
return;
|
|
212
205
|
void loadRecoveryNodes(dagRunId);
|
|
213
206
|
}, [nav, dagRunId, loadRecoveryNodes]);
|
|
214
|
-
useEffect(() => {
|
|
215
|
-
if (nav !== "recovery")
|
|
216
|
-
return;
|
|
217
|
-
const runId = dagRunId.trim();
|
|
218
|
-
if (!runId) {
|
|
219
|
-
setInterruptEligibility(null);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
let cancelled = false;
|
|
223
|
-
const load = async () => {
|
|
224
|
-
const { body } = await postOperator("dagInterruptEligibility", { runId });
|
|
225
|
-
if (cancelled)
|
|
226
|
-
return;
|
|
227
|
-
const result = (body.result ?? body);
|
|
228
|
-
setInterruptEligibility({
|
|
229
|
-
eligible: result.eligible === true,
|
|
230
|
-
ownerKind: result.ownerKind,
|
|
231
|
-
message: result.message,
|
|
232
|
-
interruptStatus: result.interruptStatus,
|
|
233
|
-
reasonCodes: result.reasonCodes,
|
|
234
|
-
targetOperationId: result.targetOperationId,
|
|
235
|
-
targetOperationState: result.targetOperationState,
|
|
236
|
-
});
|
|
237
|
-
};
|
|
238
|
-
void load();
|
|
239
|
-
const timer = window.setInterval(() => void load(), 4000);
|
|
240
|
-
return () => {
|
|
241
|
-
cancelled = true;
|
|
242
|
-
window.clearInterval(timer);
|
|
243
|
-
};
|
|
244
|
-
}, [nav, dagRunId]);
|
|
245
207
|
useEffect(() => {
|
|
246
208
|
if (nav !== "recovery")
|
|
247
209
|
return;
|
|
@@ -267,14 +229,7 @@ export function useRecoveryConsole(params) {
|
|
|
267
229
|
dagStatus: recoveryRunStatus,
|
|
268
230
|
failureCategory: recoveryFailureCategory,
|
|
269
231
|
});
|
|
270
|
-
const recoveryCtas = useMemo(() =>
|
|
271
|
-
const ctas = consoleRecoveryCtas(recoveryFact);
|
|
272
|
-
if (recoveryFact !== "running-active")
|
|
273
|
-
return ctas;
|
|
274
|
-
if (interruptEligibility?.eligible)
|
|
275
|
-
return ctas;
|
|
276
|
-
return ctas.filter((cta) => cta.id !== "interrupt");
|
|
277
|
-
}, [recoveryFact, interruptEligibility?.eligible]);
|
|
232
|
+
const recoveryCtas = useMemo(() => consoleRecoveryCtas(recoveryFact), [recoveryFact]);
|
|
278
233
|
const recoveryLanes = useMemo(() => partitionRecoveryCtas(recoveryCtas), [recoveryCtas]);
|
|
279
234
|
const recommendedCtaId = useMemo(() => recommendedRecoveryCtaId(recoveryFact), [recoveryFact]);
|
|
280
235
|
const selectedRecoveryNode = useMemo(() => recoveryNodes.find((n) => n.nodeId === fromNodeId.trim()), [recoveryNodes, fromNodeId]);
|
|
@@ -340,7 +295,6 @@ export function useRecoveryConsole(params) {
|
|
|
340
295
|
recoveryReasonTouched,
|
|
341
296
|
recoveryConfirm,
|
|
342
297
|
recoveryPlanSummary,
|
|
343
|
-
interruptReasonCode,
|
|
344
298
|
setRecoveryConfirm,
|
|
345
299
|
setRecoveryPlanSummary,
|
|
346
300
|
setRecoveryReport,
|
|
@@ -349,25 +303,6 @@ export function useRecoveryConsole(params) {
|
|
|
349
303
|
buildCurrentRecoveryReason,
|
|
350
304
|
openFullCanvas,
|
|
351
305
|
});
|
|
352
|
-
useEffect(() => {
|
|
353
|
-
if (nav !== "recovery" || !interruptIntent)
|
|
354
|
-
return;
|
|
355
|
-
if (!interruptEligibility?.eligible)
|
|
356
|
-
return;
|
|
357
|
-
const interruptCta = recoveryCtas.find((cta) => cta.id === "interrupt");
|
|
358
|
-
if (!interruptCta)
|
|
359
|
-
return;
|
|
360
|
-
setRecoveryConfirm(interruptCta);
|
|
361
|
-
setRecoveryReason(buildCurrentRecoveryReason(interruptCta.label));
|
|
362
|
-
setRecoveryReasonTouched(false);
|
|
363
|
-
setInterruptIntent(false);
|
|
364
|
-
}, [
|
|
365
|
-
nav,
|
|
366
|
-
interruptIntent,
|
|
367
|
-
interruptEligibility?.eligible,
|
|
368
|
-
recoveryCtas,
|
|
369
|
-
buildCurrentRecoveryReason,
|
|
370
|
-
]);
|
|
371
306
|
return {
|
|
372
307
|
dagRunId,
|
|
373
308
|
setDagRunId,
|
|
@@ -388,8 +323,6 @@ export function useRecoveryConsole(params) {
|
|
|
388
323
|
recoveryRunsLoading,
|
|
389
324
|
recoveryNodesLoading,
|
|
390
325
|
recoveryRunStatus,
|
|
391
|
-
recoveryFailureCategory,
|
|
392
|
-
frontendRecovery,
|
|
393
326
|
manualRunId,
|
|
394
327
|
setManualRunId,
|
|
395
328
|
manualNodeId,
|
|
@@ -412,11 +345,6 @@ export function useRecoveryConsole(params) {
|
|
|
412
345
|
recoveryCtas,
|
|
413
346
|
recoveryLanes,
|
|
414
347
|
recommendedCtaId,
|
|
415
|
-
interruptIntent,
|
|
416
|
-
setInterruptIntent,
|
|
417
|
-
interruptReasonCode,
|
|
418
|
-
setInterruptReasonCode,
|
|
419
|
-
interruptEligibility,
|
|
420
348
|
recoveryConfirmRef,
|
|
421
349
|
buildCurrentRecoveryReason,
|
|
422
350
|
handleRecoveryCta,
|
|
@@ -2,7 +2,6 @@ import { artifactFromEventPayload, mergeArtifactCardState, } from "../../chat/ar
|
|
|
2
2
|
import { humanGateCardFromEventPayload, mergeHumanGateCardState, } from "../../chat/human-gate-card.js";
|
|
3
3
|
import { mergeOperationCardState, operationFromEventPayload, } from "../../chat/operation-card.js";
|
|
4
4
|
import { requestId } from "./format.js";
|
|
5
|
-
import { parseAssistantContent } from "../../chat/assistant-content.js";
|
|
6
5
|
/** Shared handler for a single parsed Chat SSE event (M1 reconnect).
|
|
7
6
|
*
|
|
8
7
|
* Used by BOTH the inline prompt POST SSE and the background reconnect
|
|
@@ -25,30 +24,25 @@ export function createChatSseEventApplier(deps) {
|
|
|
25
24
|
// global error; the snapshot replay frames that follow are still applied
|
|
26
25
|
// as ordinary blocks (deduped by eventId, replay text skipped).
|
|
27
26
|
const reason = typeof data.reason === "string" ? data.reason : "cursor_expired";
|
|
28
|
-
|
|
27
|
+
onReconcile?.({
|
|
29
28
|
reason,
|
|
30
29
|
...(typeof data.minSeq === "number" ? { minSeq: data.minSeq } : {}),
|
|
31
30
|
...(typeof data.epoch === "string" ? { epoch: data.epoch } : {}),
|
|
32
31
|
...(typeof data.latestTurnId === "string"
|
|
33
32
|
? { latestTurnId: data.latestTurnId }
|
|
34
33
|
: {}),
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
...(typeof data.latestEventId === "string"
|
|
35
|
+
? { latestEventId: data.latestEventId }
|
|
36
|
+
: data.latestEventId === null
|
|
37
|
+
? { latestEventId: null }
|
|
38
|
+
: {}),
|
|
39
|
+
});
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
const payload = data.data && typeof data.data === "object"
|
|
43
43
|
? data.data
|
|
44
44
|
: data;
|
|
45
45
|
const turnId = typeof data.turnId === "string" ? data.turnId : undefined;
|
|
46
|
-
const eventSeq = typeof data.seq === "number" && Number.isFinite(data.seq)
|
|
47
|
-
? data.seq
|
|
48
|
-
: undefined;
|
|
49
|
-
const eventAt = typeof data.at === "string" && Number.isFinite(Date.parse(data.at))
|
|
50
|
-
? Date.parse(data.at)
|
|
51
|
-
: Date.now();
|
|
52
46
|
if (turnId &&
|
|
53
47
|
(eventName === "agent_start" || eventName === "message_update") &&
|
|
54
48
|
refs.settledTurnIdsRef.current.has(turnId)) {
|
|
@@ -113,26 +107,14 @@ export function createChatSseEventApplier(deps) {
|
|
|
113
107
|
}
|
|
114
108
|
if (eventName === "human-gate-card") {
|
|
115
109
|
const card = humanGateCardFromEventPayload(payload);
|
|
116
|
-
if (card)
|
|
117
|
-
setHumanGateCards((current) => mergeHumanGateCardState(current,
|
|
118
|
-
...card,
|
|
119
|
-
...(eventSeq === undefined ? {} : { eventSeq }),
|
|
120
|
-
...(turnId ? { turnId } : {}),
|
|
121
|
-
activityRef: `gate:${card.cardId}`,
|
|
122
|
-
}));
|
|
123
|
-
}
|
|
110
|
+
if (card)
|
|
111
|
+
setHumanGateCards((current) => mergeHumanGateCardState(current, card));
|
|
124
112
|
return;
|
|
125
113
|
}
|
|
126
114
|
if (eventName === "artifact-ref") {
|
|
127
115
|
const artifact = artifactFromEventPayload(payload);
|
|
128
|
-
if (artifact)
|
|
129
|
-
setArtifactCards((current) => mergeArtifactCardState(current,
|
|
130
|
-
...artifact,
|
|
131
|
-
...(eventSeq === undefined ? {} : { eventSeq }),
|
|
132
|
-
...(turnId ? { turnId } : {}),
|
|
133
|
-
activityRef: `artifact:${artifact.artifactId}`,
|
|
134
|
-
}));
|
|
135
|
-
}
|
|
116
|
+
if (artifact)
|
|
117
|
+
setArtifactCards((current) => mergeArtifactCardState(current, artifact));
|
|
136
118
|
return;
|
|
137
119
|
}
|
|
138
120
|
if (eventName === "agent_start") {
|
|
@@ -189,18 +171,7 @@ export function createChatSseEventApplier(deps) {
|
|
|
189
171
|
if (eventName === "operation-ref" || eventName === "operation-status") {
|
|
190
172
|
const operation = operationFromEventPayload(payload);
|
|
191
173
|
if (operation) {
|
|
192
|
-
|
|
193
|
-
? payload.toolCallId
|
|
194
|
-
: undefined;
|
|
195
|
-
setOperationCards((current) => mergeOperationCardState(current, {
|
|
196
|
-
...operation,
|
|
197
|
-
...(eventSeq === undefined
|
|
198
|
-
? {}
|
|
199
|
-
: { eventSeq, updatedSeq: eventSeq }),
|
|
200
|
-
...(turnId ? { turnId } : {}),
|
|
201
|
-
...(toolCallId ? { toolCallId } : {}),
|
|
202
|
-
activityRef: `operation:${operation.operationId}`,
|
|
203
|
-
}));
|
|
174
|
+
setOperationCards((current) => mergeOperationCardState(current, operation));
|
|
204
175
|
}
|
|
205
176
|
return;
|
|
206
177
|
}
|
|
@@ -242,46 +213,24 @@ export function createChatSseEventApplier(deps) {
|
|
|
242
213
|
// a duplicate bubble.
|
|
243
214
|
return;
|
|
244
215
|
}
|
|
245
|
-
const sealedIds = refs.sealedAssistantIdsRef.current;
|
|
246
|
-
if (eventName === "message_update" &&
|
|
247
|
-
sealedIds.has(resolvedAssistantId)) {
|
|
248
|
-
const nextId = requestId("a");
|
|
249
|
-
if (turnId)
|
|
250
|
-
refs.turnAssistantIdsRef.current.set(turnId, nextId);
|
|
251
|
-
resolvedAssistantId = nextId;
|
|
252
|
-
setStreamingAssistantId(nextId);
|
|
253
|
-
}
|
|
254
216
|
const text = typeof payload.text === "string" ? payload.text : "";
|
|
255
|
-
const content = parseAssistantContent(payload.content);
|
|
256
|
-
const assistantIdForEvent = resolvedAssistantId;
|
|
257
217
|
setMessages((m) => {
|
|
258
|
-
const exists = m.some((msg) => msg.id ===
|
|
218
|
+
const exists = m.some((msg) => msg.id === resolvedAssistantId);
|
|
259
219
|
if (exists) {
|
|
260
|
-
return m.map((msg) => {
|
|
261
|
-
if (msg.id !== assistantIdForEvent)
|
|
262
|
-
return msg;
|
|
263
|
-
return {
|
|
264
|
-
...msg,
|
|
265
|
-
text,
|
|
266
|
-
...(content ? { content } : {}),
|
|
267
|
-
};
|
|
268
|
-
});
|
|
220
|
+
return m.map((msg) => msg.id === resolvedAssistantId ? { ...msg, text } : msg);
|
|
269
221
|
}
|
|
270
222
|
// Reconnect path: the assistant message may not exist yet
|
|
271
223
|
// (e.g. only tool_call events arrived). Append a placeholder.
|
|
272
224
|
return [
|
|
273
225
|
...m,
|
|
274
226
|
{
|
|
275
|
-
id:
|
|
227
|
+
id: resolvedAssistantId,
|
|
276
228
|
role: "assistant",
|
|
277
229
|
text,
|
|
278
|
-
createdAt:
|
|
279
|
-
...(content ? { content } : {}),
|
|
230
|
+
createdAt: Date.now(),
|
|
280
231
|
},
|
|
281
232
|
];
|
|
282
233
|
});
|
|
283
|
-
if (eventName === "message_end")
|
|
284
|
-
sealedIds.add(assistantIdForEvent);
|
|
285
234
|
}
|
|
286
235
|
else if (eventName === "tool_call") {
|
|
287
236
|
const id = typeof payload.toolCallId === "string"
|
|
@@ -289,20 +238,18 @@ export function createChatSseEventApplier(deps) {
|
|
|
289
238
|
: requestId("t");
|
|
290
239
|
const toolName = typeof payload.toolName === "string" ? payload.toolName : "tool";
|
|
291
240
|
const argsPreview = typeof payload.argsPreview === "string" ? payload.argsPreview : "{}";
|
|
241
|
+
const now = Date.now();
|
|
292
242
|
setToolCalls((prev) => {
|
|
293
243
|
if (prev.some((t) => t.id === id)) {
|
|
294
|
-
return prev.map((t) =>
|
|
295
|
-
|
|
296
|
-
return t;
|
|
297
|
-
return {
|
|
244
|
+
return prev.map((t) => t.id === id
|
|
245
|
+
? {
|
|
298
246
|
...t,
|
|
299
247
|
args: argsPreview,
|
|
300
248
|
status: "running",
|
|
301
249
|
turnId: turnId ?? t.turnId,
|
|
302
250
|
assistantMessageId: resolvedAssistantId,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
});
|
|
251
|
+
}
|
|
252
|
+
: t);
|
|
306
253
|
}
|
|
307
254
|
return [
|
|
308
255
|
...prev,
|
|
@@ -311,8 +258,7 @@ export function createChatSseEventApplier(deps) {
|
|
|
311
258
|
toolName,
|
|
312
259
|
args: argsPreview,
|
|
313
260
|
status: "running",
|
|
314
|
-
createdAt:
|
|
315
|
-
...(eventSeq === undefined ? {} : { eventSeq }),
|
|
261
|
+
createdAt: now,
|
|
316
262
|
turnId,
|
|
317
263
|
assistantMessageId: resolvedAssistantId,
|
|
318
264
|
},
|
|
@@ -328,23 +274,21 @@ export function createChatSseEventApplier(deps) {
|
|
|
328
274
|
const resultText = typeof payload.resultPreview === "string"
|
|
329
275
|
? payload.resultPreview
|
|
330
276
|
: "{}";
|
|
277
|
+
const now = Date.now();
|
|
331
278
|
setToolCalls((prev) => {
|
|
332
279
|
const exists = prev.find((t) => t.id === id);
|
|
333
280
|
if (exists) {
|
|
334
|
-
return prev.map((t) =>
|
|
335
|
-
|
|
336
|
-
return t;
|
|
337
|
-
return {
|
|
281
|
+
return prev.map((t) => t.id === id
|
|
282
|
+
? {
|
|
338
283
|
...t,
|
|
339
284
|
result: resultText,
|
|
340
285
|
resultText,
|
|
341
286
|
status: isError ? "error" : "ok",
|
|
342
|
-
finishedAt:
|
|
343
|
-
...(eventSeq === undefined ? {} : { finishedSeq: eventSeq }),
|
|
287
|
+
finishedAt: now,
|
|
344
288
|
turnId: turnId ?? t.turnId,
|
|
345
289
|
assistantMessageId: t.assistantMessageId ?? resolvedAssistantId,
|
|
346
|
-
}
|
|
347
|
-
|
|
290
|
+
}
|
|
291
|
+
: t);
|
|
348
292
|
}
|
|
349
293
|
return [
|
|
350
294
|
...prev,
|
|
@@ -354,11 +298,8 @@ export function createChatSseEventApplier(deps) {
|
|
|
354
298
|
result: resultText,
|
|
355
299
|
resultText,
|
|
356
300
|
status: isError ? "error" : "ok",
|
|
357
|
-
createdAt:
|
|
358
|
-
finishedAt:
|
|
359
|
-
...(eventSeq === undefined
|
|
360
|
-
? {}
|
|
361
|
-
: { eventSeq, finishedSeq: eventSeq }),
|
|
301
|
+
createdAt: now,
|
|
302
|
+
finishedAt: now,
|
|
362
303
|
turnId,
|
|
363
304
|
assistantMessageId: resolvedAssistantId,
|
|
364
305
|
},
|
|
@@ -12,7 +12,6 @@ export function useChatRefs() {
|
|
|
12
12
|
const settledTurnIdsRef = useRef(new Set());
|
|
13
13
|
const seenEventIdsRef = useRef(new Set());
|
|
14
14
|
const turnAssistantIdsRef = useRef(new Map());
|
|
15
|
-
const sealedAssistantIdsRef = useRef(new Set());
|
|
16
15
|
const reconnectAbortRef = useRef(null);
|
|
17
16
|
const activeTurnIdRef = useRef(null);
|
|
18
17
|
const submitInFlightRef = useRef(false);
|
|
@@ -41,7 +40,6 @@ export function useChatRefs() {
|
|
|
41
40
|
settledTurnIdsRef,
|
|
42
41
|
seenEventIdsRef,
|
|
43
42
|
turnAssistantIdsRef,
|
|
44
|
-
sealedAssistantIdsRef,
|
|
45
43
|
reconnectAbortRef,
|
|
46
44
|
activeTurnIdRef,
|
|
47
45
|
submitInFlightRef,
|
|
@@ -68,7 +66,6 @@ export function useChatRefs() {
|
|
|
68
66
|
settledTurnIdsRef,
|
|
69
67
|
seenEventIdsRef,
|
|
70
68
|
turnAssistantIdsRef,
|
|
71
|
-
sealedAssistantIdsRef,
|
|
72
69
|
reconnectAbortRef,
|
|
73
70
|
activeTurnIdRef,
|
|
74
71
|
submitInFlightRef,
|