@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.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/AGENTS.md +4 -1
- package/CHANGELOG.md +187 -92
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +429 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/executors/dag-pi-executor.js +1691 -29
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1152 -171
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +19 -5
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +60 -17
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +36 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operation-store.js +226 -6
- package/dist/worker/console/operator-actions.js +263 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +151 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +366 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
- package/dist/workflows/dag/frontend-prewrite-gate.js +343 -391
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +195 -8
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +425 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +439 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
- package/dist/workflows/dag/frontend-writer-admission.js +260 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +572 -500
- package/dist/workflows/dag/node-execution.js +307 -70
- package/dist/workflows/dag/prompt.js +10 -18
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +228 -4
- package/dist/workflows/dag/runner.js +314 -119
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +235 -23
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +60 -10
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -3
- package/package.json +5 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/SKILL.md +4 -4
- package/skills/frontend-implementation/references/design-spec.md +11 -7
- package/skills/frontend-implementation/references/node-contracts.md +20 -11
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { assertTaskContractBindingFresh } from "./task-contract-binding.js";
|
|
4
4
|
import { isSafeReadOnlyPiRetryCandidate, isTransportCrashedExclusiveWriter } from "./retry-policy.js";
|
|
5
5
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
6
|
+
import { composeDagPromptOverridePrompt, MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS, } from "../../shared/dag-prompt-override.js";
|
|
6
7
|
const REVIEW_VERDICT_RECOVERY_NODE_ID = "review-verdict-recovery-pi";
|
|
7
8
|
const SAFE_SHELL_GATE_KEYS = [
|
|
8
9
|
"verdictGate",
|
|
@@ -159,12 +160,16 @@ function isCanonicalGeneratedVerifyShell(task) {
|
|
|
159
160
|
return false;
|
|
160
161
|
if (shell.projectGovernanceGate)
|
|
161
162
|
return false;
|
|
162
|
-
if (shell.
|
|
163
|
+
if (shell.frontendDesignPolicy)
|
|
164
|
+
return false;
|
|
165
|
+
if (shell.frontendWriterAdmission)
|
|
163
166
|
return false;
|
|
164
167
|
if (shell.frontendVerificationBundle)
|
|
165
168
|
return false;
|
|
166
169
|
if (shell.frontendReviewContext)
|
|
167
170
|
return false;
|
|
171
|
+
if (shell.frontendCloseout)
|
|
172
|
+
return false;
|
|
168
173
|
if (shell.backendTestPipeline)
|
|
169
174
|
return false;
|
|
170
175
|
if (shell.nonZeroExitPolicy === "record")
|
|
@@ -190,12 +195,16 @@ function isSafeGateOnlyShell(task) {
|
|
|
190
195
|
return false;
|
|
191
196
|
if (shell.preset)
|
|
192
197
|
return false;
|
|
193
|
-
if (shell.
|
|
198
|
+
if (shell.frontendDesignPolicy)
|
|
199
|
+
return false;
|
|
200
|
+
if (shell.frontendWriterAdmission)
|
|
194
201
|
return false;
|
|
195
202
|
if (shell.frontendVerificationBundle)
|
|
196
203
|
return false;
|
|
197
204
|
if (shell.frontendReviewContext)
|
|
198
205
|
return false;
|
|
206
|
+
if (shell.frontendCloseout)
|
|
207
|
+
return false;
|
|
199
208
|
if (shell.backendTestPipeline)
|
|
200
209
|
return false;
|
|
201
210
|
if (shell.verifyEvidence)
|
|
@@ -306,6 +315,9 @@ export function computeResetClosure(spec, effectiveFromNodeId) {
|
|
|
306
315
|
...collectReachableDescendants(spec, effectiveFromNodeId),
|
|
307
316
|
].sort();
|
|
308
317
|
}
|
|
318
|
+
function isNeverStartedNodeStatus(status) {
|
|
319
|
+
return status === undefined || status === "SKIPPED" || status === "PENDING";
|
|
320
|
+
}
|
|
309
321
|
export function assessResetClosureSafety(spec, resetNodeIds, options = {}) {
|
|
310
322
|
const tasks = taskById(spec);
|
|
311
323
|
const blockingNodes = [];
|
|
@@ -314,9 +326,17 @@ export function assessResetClosureSafety(spec, resetNodeIds, options = {}) {
|
|
|
314
326
|
const task = tasks.get(nodeId);
|
|
315
327
|
if (!task)
|
|
316
328
|
continue;
|
|
329
|
+
if (options.parentNodes !== undefined &&
|
|
330
|
+
isNeverStartedNodeStatus(options.parentNodes[nodeId]?.status)) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
317
333
|
if (isWriterTask(task, spec)) {
|
|
318
334
|
if (nodeId === options.allowedTransportWriterNodeId)
|
|
319
335
|
continue;
|
|
336
|
+
if (nodeId === options.allowedExclusiveShellFromNodeId &&
|
|
337
|
+
task.executor === "shell") {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
320
340
|
blockingNodes.push({
|
|
321
341
|
nodeId,
|
|
322
342
|
reasonCode: "restart-subgraph-contains-writer",
|
|
@@ -370,6 +390,100 @@ export function assessResetClosureSafety(spec, resetNodeIds, options = {}) {
|
|
|
370
390
|
reasonCodes: [...new Set(reasonCodes)],
|
|
371
391
|
};
|
|
372
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Experimental prompt-override admission (fail-closed):
|
|
395
|
+
* - requires the internal switch (see shared/dag-prompt-override.ts);
|
|
396
|
+
* - the target must be a failed (ERROR) node inside the reset closure, so an
|
|
397
|
+
* override can only adjust how an already-failed node retries — never a
|
|
398
|
+
* finished, skipped, or not-yet-started node;
|
|
399
|
+
* - only `subtask_prompt` semantics change: "append" keeps the frozen base
|
|
400
|
+
* and adds a marked addendum block, "replace" swaps the whole prompt;
|
|
401
|
+
* base/final sha256 are recorded for provenance and bound into planHash.
|
|
402
|
+
*/
|
|
403
|
+
function assessPromptOverrides(input) {
|
|
404
|
+
const blockingNodes = [];
|
|
405
|
+
const reasonCodes = [];
|
|
406
|
+
const tasks = taskById(input.spec);
|
|
407
|
+
const resetSet = new Set(input.resetNodeIds);
|
|
408
|
+
const seen = new Set();
|
|
409
|
+
const entries = [];
|
|
410
|
+
if (!input.enabled) {
|
|
411
|
+
reasonCodes.push("prompt-override-disabled");
|
|
412
|
+
}
|
|
413
|
+
for (const override of input.overrides) {
|
|
414
|
+
const nodeId = override.nodeId?.trim() ?? "";
|
|
415
|
+
if (seen.has(nodeId)) {
|
|
416
|
+
blockingNodes.push({
|
|
417
|
+
nodeId,
|
|
418
|
+
reasonCode: "prompt-override-duplicate-node",
|
|
419
|
+
});
|
|
420
|
+
reasonCodes.push("prompt-override-duplicate-node");
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
seen.add(nodeId);
|
|
424
|
+
const task = tasks.get(nodeId);
|
|
425
|
+
if (!task) {
|
|
426
|
+
blockingNodes.push({
|
|
427
|
+
nodeId,
|
|
428
|
+
reasonCode: "prompt-override-node-missing",
|
|
429
|
+
});
|
|
430
|
+
reasonCodes.push("prompt-override-node-missing");
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (!resetSet.has(nodeId)) {
|
|
434
|
+
blockingNodes.push({
|
|
435
|
+
nodeId,
|
|
436
|
+
reasonCode: "prompt-override-outside-reset-closure",
|
|
437
|
+
});
|
|
438
|
+
reasonCodes.push("prompt-override-outside-reset-closure");
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (input.state.nodes[nodeId]?.status !== "ERROR") {
|
|
442
|
+
blockingNodes.push({
|
|
443
|
+
nodeId,
|
|
444
|
+
reasonCode: "prompt-override-node-not-failed",
|
|
445
|
+
});
|
|
446
|
+
reasonCodes.push("prompt-override-node-not-failed");
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const mode = override.mode === "append" ? "append" : "replace";
|
|
450
|
+
const prompt = override.prompt ?? "";
|
|
451
|
+
if (!prompt.trim() ||
|
|
452
|
+
prompt.length > MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS) {
|
|
453
|
+
blockingNodes.push({
|
|
454
|
+
nodeId,
|
|
455
|
+
reasonCode: "prompt-override-invalid-prompt",
|
|
456
|
+
});
|
|
457
|
+
reasonCodes.push("prompt-override-invalid-prompt");
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const basePromptSha256 = createHash("sha256")
|
|
461
|
+
.update(task.subtask_prompt, "utf8")
|
|
462
|
+
.digest("hex");
|
|
463
|
+
const finalPrompt = composeDagPromptOverridePrompt({
|
|
464
|
+
basePrompt: task.subtask_prompt,
|
|
465
|
+
text: prompt,
|
|
466
|
+
mode,
|
|
467
|
+
});
|
|
468
|
+
const promptSha256 = createHash("sha256")
|
|
469
|
+
.update(finalPrompt, "utf8")
|
|
470
|
+
.digest("hex");
|
|
471
|
+
if (promptSha256 === basePromptSha256) {
|
|
472
|
+
blockingNodes.push({
|
|
473
|
+
nodeId,
|
|
474
|
+
reasonCode: "prompt-override-unchanged",
|
|
475
|
+
});
|
|
476
|
+
reasonCodes.push("prompt-override-unchanged");
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
entries.push({ nodeId, mode, basePromptSha256, promptSha256 });
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
entries: entries.length > 0 ? entries : undefined,
|
|
483
|
+
blockingNodes,
|
|
484
|
+
reasonCodes: [...new Set(reasonCodes)],
|
|
485
|
+
};
|
|
486
|
+
}
|
|
373
487
|
export function computePlanHash(planWithoutHash) {
|
|
374
488
|
const canonical = JSON.stringify(sortKeysDeep(planWithoutHash));
|
|
375
489
|
return createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
@@ -594,9 +708,17 @@ export async function evaluateDagRerunPlan(input) {
|
|
|
594
708
|
blockedReasons.push("ambiguous-skipped-rewrite");
|
|
595
709
|
}
|
|
596
710
|
}
|
|
597
|
-
const closureSafety = assessResetClosureSafety(input.parentSpec, resetNodeIds,
|
|
598
|
-
|
|
599
|
-
|
|
711
|
+
const closureSafety = assessResetClosureSafety(input.parentSpec, resetNodeIds, {
|
|
712
|
+
...(transportWriterRestart && effectiveFromNodeId
|
|
713
|
+
? { allowedTransportWriterNodeId: effectiveFromNodeId }
|
|
714
|
+
: {}),
|
|
715
|
+
parentNodes: input.parentState.nodes,
|
|
716
|
+
...(effectiveFromNodeId &&
|
|
717
|
+
effectiveTask?.executor === "shell" &&
|
|
718
|
+
effectiveRecord?.status === "ERROR"
|
|
719
|
+
? { allowedExclusiveShellFromNodeId: effectiveFromNodeId }
|
|
720
|
+
: {}),
|
|
721
|
+
});
|
|
600
722
|
blockingNodes.push(...closureSafety.blockingNodes);
|
|
601
723
|
for (const code of closureSafety.reasonCodes) {
|
|
602
724
|
reasonCodes.push(code);
|
|
@@ -629,6 +751,22 @@ export async function evaluateDagRerunPlan(input) {
|
|
|
629
751
|
blockingNodes.push({ nodeId, reasonCode: "parent-facts-invalid" });
|
|
630
752
|
}
|
|
631
753
|
}
|
|
754
|
+
let promptOverrideEntries;
|
|
755
|
+
if (input.promptOverrides !== undefined && input.promptOverrides.length > 0) {
|
|
756
|
+
const overrideAssessment = assessPromptOverrides({
|
|
757
|
+
spec: input.parentSpec,
|
|
758
|
+
state: input.parentState,
|
|
759
|
+
resetNodeIds,
|
|
760
|
+
overrides: input.promptOverrides,
|
|
761
|
+
enabled: input.promptOverrideEnabled === true,
|
|
762
|
+
});
|
|
763
|
+
blockingNodes.push(...overrideAssessment.blockingNodes);
|
|
764
|
+
for (const code of overrideAssessment.reasonCodes) {
|
|
765
|
+
reasonCodes.push(code);
|
|
766
|
+
blockedReasons.push(code);
|
|
767
|
+
}
|
|
768
|
+
promptOverrideEntries = overrideAssessment.entries;
|
|
769
|
+
}
|
|
632
770
|
const uniqueReasonCodes = [...new Set(reasonCodes)];
|
|
633
771
|
const uniqueBlockedReasons = [...new Set(blockedReasons)];
|
|
634
772
|
const eligible = uniqueBlockedReasons.length === 0 &&
|
|
@@ -672,6 +810,7 @@ export async function evaluateDagRerunPlan(input) {
|
|
|
672
810
|
parentLifecycle: input.parentLifecycle,
|
|
673
811
|
workerManaged: input.workerManaged,
|
|
674
812
|
}),
|
|
813
|
+
...(promptOverrideEntries ? { promptOverrides: promptOverrideEntries } : {}),
|
|
675
814
|
};
|
|
676
815
|
return {
|
|
677
816
|
...planWithoutHash,
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { access, copyFile, mkdir, readdir, readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
5
|
+
import { composeDagPromptOverridePrompt, isDagRerunPromptOverrideEnabled, } from "../../shared/dag-prompt-override.js";
|
|
5
6
|
import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
|
|
6
7
|
import { readControllerIdentityArtifact, captureControllerIdentity } from "./controller-identity.js";
|
|
7
8
|
import { DAG_RUNS_DIR, getDagRunDir, isDagNonExecutionLifecycle, locateDagRun, readDagRunSpec, readDagRunState, } from "./lifecycle.js";
|
|
@@ -190,6 +191,8 @@ export async function planDagRerun(input) {
|
|
|
190
191
|
parentSpec: nonCompletedSpec,
|
|
191
192
|
parentState: nonCompletedState,
|
|
192
193
|
parentLifecycle,
|
|
194
|
+
promptOverrides: input.promptOverrides,
|
|
195
|
+
promptOverrideEnabled: isDagRerunPromptOverrideEnabled(),
|
|
193
196
|
workerManaged: nonCompletedWorkerHints.workerManaged,
|
|
194
197
|
workerAssociation: nonCompletedWorkerHints.workerManaged
|
|
195
198
|
? {
|
|
@@ -229,6 +232,8 @@ export async function planDagRerun(input) {
|
|
|
229
232
|
parentTerminalWorkspace,
|
|
230
233
|
currentWorkspace,
|
|
231
234
|
skillSnapshotOk,
|
|
235
|
+
promptOverrides: input.promptOverrides,
|
|
236
|
+
promptOverrideEnabled: isDagRerunPromptOverrideEnabled(),
|
|
232
237
|
workerManaged: workerHints.workerManaged,
|
|
233
238
|
workerAssociation: workerHints.workerManaged
|
|
234
239
|
? {
|
|
@@ -384,15 +389,57 @@ function buildPendingNodeRecord(task) {
|
|
|
384
389
|
...(task.outputMode ? { outputMode: task.outputMode } : {}),
|
|
385
390
|
};
|
|
386
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Stage the continuation spec with operator prompt overrides applied. The
|
|
394
|
+
* parent spec stays frozen; every override must match the plan entry (sha256
|
|
395
|
+
* of the composed final prompt and of the parent base prompt) so the
|
|
396
|
+
* two-phase planHash remains the single source of truth for what changed.
|
|
397
|
+
* "append" entries keep the frozen base and append the operator addendum in a
|
|
398
|
+
* marked block; "replace" entries swap the whole prompt.
|
|
399
|
+
*/
|
|
400
|
+
function applyPromptOverridesToSpec(input) {
|
|
401
|
+
const planned = new Map((input.plan.promptOverrides ?? []).map((entry) => [entry.nodeId, entry]));
|
|
402
|
+
const spec = structuredClone(input.spec);
|
|
403
|
+
for (const override of input.overrides) {
|
|
404
|
+
const entry = planned.get(override.nodeId);
|
|
405
|
+
if (!entry) {
|
|
406
|
+
throw new Error(`prompt override is not present in the rerun plan: ${override.nodeId}`);
|
|
407
|
+
}
|
|
408
|
+
const task = spec.tasks.find((candidate) => candidate.id === override.nodeId);
|
|
409
|
+
if (!task) {
|
|
410
|
+
throw new Error(`prompt override node missing from spec: ${override.nodeId}`);
|
|
411
|
+
}
|
|
412
|
+
if (sha256Hex(task.subtask_prompt) !== entry.basePromptSha256) {
|
|
413
|
+
throw new Error(`prompt override base prompt drifted from plan: ${override.nodeId}`);
|
|
414
|
+
}
|
|
415
|
+
const finalPrompt = composeDagPromptOverridePrompt({
|
|
416
|
+
basePrompt: task.subtask_prompt,
|
|
417
|
+
text: override.prompt,
|
|
418
|
+
mode: override.mode === "append" ? "append" : "replace",
|
|
419
|
+
});
|
|
420
|
+
if (sha256Hex(finalPrompt) !== entry.promptSha256) {
|
|
421
|
+
throw new Error(`prompt override text does not match the planned sha256: ${override.nodeId}`);
|
|
422
|
+
}
|
|
423
|
+
task.subtask_prompt = finalPrompt;
|
|
424
|
+
}
|
|
425
|
+
return spec;
|
|
426
|
+
}
|
|
387
427
|
async function stageContinuationRun(input) {
|
|
388
|
-
const
|
|
389
|
-
|
|
428
|
+
const specForRun = input.promptOverrides !== undefined && input.promptOverrides.length > 0
|
|
429
|
+
? applyPromptOverridesToSpec({
|
|
430
|
+
spec: input.parentSpec,
|
|
431
|
+
plan: input.plan,
|
|
432
|
+
overrides: input.promptOverrides,
|
|
433
|
+
})
|
|
434
|
+
: input.parentSpec;
|
|
435
|
+
const { ranks } = topoSortToRanks(specForRun);
|
|
436
|
+
const runId = await buildDagRunId(specForRun, { cwd: input.cwd });
|
|
390
437
|
const runDir = getDagRunDir(input.cwd, "active", runId);
|
|
391
438
|
await prepareActiveRunDir(runDir);
|
|
392
|
-
await writeRunSpec(runDir,
|
|
439
|
+
await writeRunSpec(runDir, specForRun);
|
|
393
440
|
const resetSet = new Set(input.plan.resetNodeIds);
|
|
394
441
|
const importedSet = new Set(input.plan.importedNodeIds);
|
|
395
|
-
const tasksById = new Map(
|
|
442
|
+
const tasksById = new Map(specForRun.tasks.map((task) => [task.id, task]));
|
|
396
443
|
const importManifestNodes = [];
|
|
397
444
|
const nodes = {};
|
|
398
445
|
for (const nodeId of input.plan.importedNodeIds) {
|
|
@@ -433,6 +480,9 @@ async function stageContinuationRun(input) {
|
|
|
433
480
|
reason: input.reason,
|
|
434
481
|
requestId: input.requestId,
|
|
435
482
|
planHash: input.plan.planHash,
|
|
483
|
+
...(input.plan.promptOverrides
|
|
484
|
+
? { promptOverrides: input.plan.promptOverrides }
|
|
485
|
+
: {}),
|
|
436
486
|
createdAt,
|
|
437
487
|
};
|
|
438
488
|
const state = {
|
|
@@ -482,7 +532,7 @@ async function stageContinuationRun(input) {
|
|
|
482
532
|
await writeJsonAtomic(path.join(runDir, IMPORT_MANIFEST_REL_PATH), importManifest);
|
|
483
533
|
await writeJsonAtomic(path.join(runDir, RERUN_PLAN_REL_PATH), input.plan);
|
|
484
534
|
await writeRunState(runDir, state);
|
|
485
|
-
return { runId, runDir, state };
|
|
535
|
+
return { runId, runDir, state, spec: specForRun };
|
|
486
536
|
}
|
|
487
537
|
export async function executeDagRerun(input) {
|
|
488
538
|
const existing = await readExistingOperatorRequest(input.cwd, input.requestId);
|
|
@@ -493,6 +543,7 @@ export async function executeDagRerun(input) {
|
|
|
493
543
|
cwd: input.cwd,
|
|
494
544
|
parentRunId: input.parentRunId,
|
|
495
545
|
selectedNodeId: input.selectedNodeId,
|
|
546
|
+
promptOverrides: input.promptOverrides,
|
|
496
547
|
});
|
|
497
548
|
if (plan.planHash !== input.planHash) {
|
|
498
549
|
return {
|
|
@@ -530,10 +581,11 @@ export async function executeDagRerun(input) {
|
|
|
530
581
|
plan,
|
|
531
582
|
reason: input.reason,
|
|
532
583
|
requestId: input.requestId,
|
|
584
|
+
promptOverrides: input.promptOverrides,
|
|
533
585
|
});
|
|
534
586
|
const continuationOptions = {
|
|
535
587
|
cwd: input.cwd,
|
|
536
|
-
spec:
|
|
588
|
+
spec: staged.spec,
|
|
537
589
|
state: staged.state,
|
|
538
590
|
runDir: staged.runDir,
|
|
539
591
|
...(input.maxConcurrent !== undefined
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { projectProtocolFailureReason, } from "./frontend-recovery-capsule.js";
|
|
3
|
+
import { TYPED_EVENT_FACT_KINDS } from "./frontend-typed-event-store.js";
|
|
2
4
|
/**
|
|
3
5
|
* Failure categories that are safe to auto-retry for read-only Pi nodes.
|
|
4
6
|
*
|
|
@@ -165,6 +167,36 @@ export const WRITER_TRANSPORT_RETRY_POLICY = {
|
|
|
165
167
|
maxDelayMs: 30000,
|
|
166
168
|
retryCategories: [WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY],
|
|
167
169
|
};
|
|
170
|
+
/**
|
|
171
|
+
* Implicit runtime retry for canonical final verify shells. Not a DagRetryPolicy:
|
|
172
|
+
* `spawn-error` / `test-timeout` are runtime command categories and must not enter
|
|
173
|
+
* `dagRetryPolicySchema` / `ALL_DAG_RETRY_CATEGORIES`.
|
|
174
|
+
*/
|
|
175
|
+
export const VERIFY_SHELL_RETRY_CATEGORIES = [
|
|
176
|
+
"timeout",
|
|
177
|
+
"spawn-error",
|
|
178
|
+
"test-timeout",
|
|
179
|
+
];
|
|
180
|
+
export const VERIFY_SHELL_RETRY_POLICY = {
|
|
181
|
+
maxAttempts: 2,
|
|
182
|
+
backoff: "exponential",
|
|
183
|
+
initialDelayMs: 2000,
|
|
184
|
+
maxDelayMs: 30000,
|
|
185
|
+
retryCategories: VERIFY_SHELL_RETRY_CATEGORIES,
|
|
186
|
+
};
|
|
187
|
+
export const TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE = "standard-reviewed-supervised-v1";
|
|
188
|
+
/**
|
|
189
|
+
* Target-template exclusive implement-pi transport retry. Same schema category
|
|
190
|
+
* as frontend (`writer-clean-timeout`) so DagSpec validation stays unchanged;
|
|
191
|
+
* only the delay cap is wider. Do not mutate {@link WRITER_TRANSPORT_RETRY_POLICY}.
|
|
192
|
+
*/
|
|
193
|
+
export const TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY = {
|
|
194
|
+
maxAttempts: 2,
|
|
195
|
+
backoff: "exponential",
|
|
196
|
+
initialDelayMs: 5000,
|
|
197
|
+
maxDelayMs: 60000,
|
|
198
|
+
retryCategories: [WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY],
|
|
199
|
+
};
|
|
168
200
|
/**
|
|
169
201
|
* Backend-test generation writers: empty-diff once, plus bounded incomplete-write-set
|
|
170
202
|
* recovery attempts driven by Completeness Gate (missing/broken target files).
|
|
@@ -229,15 +261,77 @@ export function isWriterEmptyDiffRetryCandidate(task) {
|
|
|
229
261
|
task.writerOutcomePolicy?.requireChangedFiles === true);
|
|
230
262
|
}
|
|
231
263
|
/**
|
|
232
|
-
* Standard exclusive implementers (implementation-outcome
|
|
233
|
-
* single clean-timeout transport retry. Backend
|
|
234
|
-
* have their own policy and are excluded here.
|
|
264
|
+
* Standard exclusive implementers (implementation-outcome and frontend-facts
|
|
265
|
+
* writers) may take a single clean-timeout transport retry. Backend
|
|
266
|
+
* completeness writers already have their own policy and are excluded here.
|
|
235
267
|
*/
|
|
236
268
|
export function isWriterTransportRetryCandidate(task) {
|
|
237
269
|
return (isExclusivePiImplementer(task) &&
|
|
238
|
-
task.writerOutcomePolicy?.type === "implementation-outcome-v1"
|
|
270
|
+
(task.writerOutcomePolicy?.type === "implementation-outcome-v1" ||
|
|
271
|
+
task.writerOutcomePolicy?.type === "frontend-facts-v1") &&
|
|
239
272
|
task.writerOutcomePolicy.requireChangedFiles !== true);
|
|
240
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Canonical standard exclusive `implement-pi` only. Frontend designers and
|
|
276
|
+
* completeness writers stay on the existing timeout-only transport candidate.
|
|
277
|
+
*/
|
|
278
|
+
export function isTargetTemplateImplementPi(task) {
|
|
279
|
+
return isWriterTransportRetryCandidate(task) && task.id === "implement-pi";
|
|
280
|
+
}
|
|
281
|
+
export function hasTargetTemplateTransientRetryProfile(task) {
|
|
282
|
+
return (task?.transientRetryProfile === TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Dual-condition eligibility: canonical shape AND generated-path stamp.
|
|
286
|
+
* Custom / other-template nodes that only match id or shape stay ineligible.
|
|
287
|
+
*/
|
|
288
|
+
export function isTargetTemplateTransientRetryNode(task) {
|
|
289
|
+
if (!hasTargetTemplateTransientRetryProfile(task))
|
|
290
|
+
return false;
|
|
291
|
+
return (isCanonicalFinalVerifyShellRetryCandidate(task) ||
|
|
292
|
+
isTargetTemplateImplementPi(task));
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Shape-based eligibility for implicit canonical final verify-shell retry.
|
|
296
|
+
* Does not consult node id or a template field.
|
|
297
|
+
*/
|
|
298
|
+
export function isCanonicalFinalVerifyShellRetryCandidate(task) {
|
|
299
|
+
if (task.executor !== "shell")
|
|
300
|
+
return false;
|
|
301
|
+
if (task.role !== "verifier")
|
|
302
|
+
return false;
|
|
303
|
+
if (task.writePolicy !== undefined &&
|
|
304
|
+
task.writePolicy !== "read-only" &&
|
|
305
|
+
task.writePolicy !== "none") {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if ((task.writeSet?.length ?? 0) > 0)
|
|
309
|
+
return false;
|
|
310
|
+
const shell = task.shell;
|
|
311
|
+
if (!shell || (shell.commands?.length ?? 0) === 0)
|
|
312
|
+
return false;
|
|
313
|
+
const evidence = shell.verifyEvidence;
|
|
314
|
+
if (!evidence)
|
|
315
|
+
return false;
|
|
316
|
+
if (evidence.phase !== "final")
|
|
317
|
+
return false;
|
|
318
|
+
if (evidence.quota !== "full")
|
|
319
|
+
return false;
|
|
320
|
+
if (evidence.commandSource !== "adapter")
|
|
321
|
+
return false;
|
|
322
|
+
if (evidence.finalFullRequired !== true)
|
|
323
|
+
return false;
|
|
324
|
+
if (shell.frontendPrewriteGate ||
|
|
325
|
+
shell.frontendVerificationBundle ||
|
|
326
|
+
shell.frontendBrowserToolPreflight ||
|
|
327
|
+
shell.backendTestPipeline ||
|
|
328
|
+
shell.jacocoCoverage ||
|
|
329
|
+
shell.repairArtifactGate ||
|
|
330
|
+
shell.verdictGate) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
241
335
|
export function isSafeReadOnlyPiRetryCandidate(task) {
|
|
242
336
|
if (task.executor !== "pi")
|
|
243
337
|
return false;
|
|
@@ -279,3 +373,133 @@ export function computeBackoffDelayMs(attemptNumber, policy) {
|
|
|
279
373
|
const raw = policy.initialDelayMs * 2 ** exponent;
|
|
280
374
|
return Math.min(raw, policy.maxDelayMs);
|
|
281
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* A+B (AC-001/AC-005): project raw executor signals to the canonical 7-value
|
|
378
|
+
* protocol failure reason. Specific signals win first, then the existing
|
|
379
|
+
* alias/enum passthrough; unknown signals fail closed to `undefined`.
|
|
380
|
+
*/
|
|
381
|
+
export function projectFrontendNodeProtocolFailureReason(input) {
|
|
382
|
+
// 1. length stop reason without a terminal → length-before-terminal.
|
|
383
|
+
if (input.stopReason === "length" && input.hasTerminalFact === false) {
|
|
384
|
+
return "length-before-terminal";
|
|
385
|
+
}
|
|
386
|
+
// 2. timeout with reasoning-side evidence → reasoning-timeout.
|
|
387
|
+
if (input.failureCategory === "timeout" &&
|
|
388
|
+
/(?:reasoning|thinking|thought)/i.test(input.assistantText ?? "")) {
|
|
389
|
+
return "reasoning-timeout";
|
|
390
|
+
}
|
|
391
|
+
// 3. empty output without a terminal → provider-ended-without-terminal.
|
|
392
|
+
if (input.failureCategory === "empty-output" &&
|
|
393
|
+
input.hasTerminalFact === false) {
|
|
394
|
+
return "provider-ended-without-terminal";
|
|
395
|
+
}
|
|
396
|
+
// 4. Alias/enum passthrough; unknown fails closed to undefined.
|
|
397
|
+
return projectProtocolFailureReason(input.failureCategory ?? "");
|
|
398
|
+
}
|
|
399
|
+
export const FRONTEND_PLAN_LADDER = [
|
|
400
|
+
"normal",
|
|
401
|
+
"compact-terminal-first",
|
|
402
|
+
"bounded-tool-only",
|
|
403
|
+
"backup-model",
|
|
404
|
+
];
|
|
405
|
+
/**
|
|
406
|
+
* Map a 7-value protocol failure reason to the closest provider capability
|
|
407
|
+
* fact kind for backup-model routing. `unsupported-tool-capability` is a
|
|
408
|
+
* protocol reason, not a provider capability kind, so it is deliberately NOT
|
|
409
|
+
* a valid route (fail-closed to `unsupported-provider-capability`).
|
|
410
|
+
*/
|
|
411
|
+
export function frontendPlanProtocolReasonToCapabilityKind(reason) {
|
|
412
|
+
switch (reason) {
|
|
413
|
+
case "length-before-terminal":
|
|
414
|
+
return "length-before-terminal";
|
|
415
|
+
case "reasoning-timeout":
|
|
416
|
+
return "reasoning-timeout";
|
|
417
|
+
case "provider-ended-without-terminal":
|
|
418
|
+
return "provider-ended-without-terminal";
|
|
419
|
+
case "terminal-event-missing":
|
|
420
|
+
case "terminal-conflict":
|
|
421
|
+
return "terminal-commit";
|
|
422
|
+
case "typed-fact-invalid":
|
|
423
|
+
return "read-only-fact-channel";
|
|
424
|
+
case "unsupported-tool-capability":
|
|
425
|
+
return "unsupported-tool-capability";
|
|
426
|
+
default:
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* A+B (AC-003): resolve a backup-model route against the frozen seven-kind
|
|
432
|
+
* provider capability catalog (the exact set `routeFrontendProviderCapability`
|
|
433
|
+
* consults). `unsupported-tool-capability` is a protocol reason, not a
|
|
434
|
+
* provider capability kind, so it (and any unmapped reason) fails closed to
|
|
435
|
+
* `unsupported-provider-capability` — never swapped for the 7-value reason.
|
|
436
|
+
*/
|
|
437
|
+
export function resolveFrontendPlanBackupRoute(reason) {
|
|
438
|
+
const factKind = frontendPlanProtocolReasonToCapabilityKind(reason);
|
|
439
|
+
if (!factKind) {
|
|
440
|
+
return { ok: false, reason: "unsupported-provider-capability" };
|
|
441
|
+
}
|
|
442
|
+
return TYPED_EVENT_FACT_KINDS.includes(factKind)
|
|
443
|
+
? { ok: true, factKind }
|
|
444
|
+
: { ok: false, reason: "unsupported-provider-capability" };
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* A+B (AC-002): deterministic ladder resolution. Reason-mandated upgrades
|
|
448
|
+
* (length → compact-terminal-first; terminal/timeout/empty → bounded-tool-only)
|
|
449
|
+
* never regress below the current rung. A repeated failure fingerprint with no
|
|
450
|
+
* new committed fact escalates one rung instead of re-running the same step;
|
|
451
|
+
* at `backup-model` that becomes `non-converging`, and an unavailable backup
|
|
452
|
+
* provider route fails closed to `unsupported-provider-capability`.
|
|
453
|
+
*/
|
|
454
|
+
export function resolveFrontendPlanRetryStep(input) {
|
|
455
|
+
if (input.currentStep === "non-converging" ||
|
|
456
|
+
input.currentStep === "unsupported-provider-capability") {
|
|
457
|
+
return input.currentStep;
|
|
458
|
+
}
|
|
459
|
+
const currentIndex = FRONTEND_PLAN_LADDER.indexOf(input.currentStep);
|
|
460
|
+
const reasonStep = reasonMandatedFrontendPlanRetryStep(input.protocolFailureReason);
|
|
461
|
+
const reasonIndex = reasonStep
|
|
462
|
+
? FRONTEND_PLAN_LADDER.indexOf(reasonStep)
|
|
463
|
+
: -1;
|
|
464
|
+
let nextIndex = Math.max(currentIndex, reasonIndex);
|
|
465
|
+
const repeated = input.priorFingerprints.includes(input.fingerprint);
|
|
466
|
+
if (repeated && !input.hasNewCommittedFact) {
|
|
467
|
+
nextIndex = Math.max(nextIndex, currentIndex + 1);
|
|
468
|
+
}
|
|
469
|
+
const nextStep = nextIndex >= FRONTEND_PLAN_LADDER.length
|
|
470
|
+
? "non-converging"
|
|
471
|
+
: (FRONTEND_PLAN_LADDER[nextIndex] ?? "non-converging");
|
|
472
|
+
if (nextStep === "backup-model" && input.backupRouteAvailable === false) {
|
|
473
|
+
return "unsupported-provider-capability";
|
|
474
|
+
}
|
|
475
|
+
return nextStep;
|
|
476
|
+
}
|
|
477
|
+
function reasonMandatedFrontendPlanRetryStep(reason) {
|
|
478
|
+
switch (reason) {
|
|
479
|
+
case "length-before-terminal":
|
|
480
|
+
return "compact-terminal-first";
|
|
481
|
+
case "terminal-event-missing":
|
|
482
|
+
case "reasoning-timeout":
|
|
483
|
+
case "provider-ended-without-terminal":
|
|
484
|
+
return "bounded-tool-only";
|
|
485
|
+
default:
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Opt-in retry policy mounted on the frontend-plan-pi node. maxAttempts=5 is
|
|
491
|
+
* the schema cap, matching the four-rung ladder plus one non-converging stop.
|
|
492
|
+
* `invalid-output` (typed-fact schema violations, e.g. verification targets
|
|
493
|
+
* referencing undeclared UI states) retries at the `normal` rung with the
|
|
494
|
+
* contractCheck reason injected via `<retry_instruction>` (§5.1 ladder).
|
|
495
|
+
*/
|
|
496
|
+
export const FRONTEND_PLAN_LADDER_RETRY_POLICY = {
|
|
497
|
+
maxAttempts: 5,
|
|
498
|
+
backoff: "exponential",
|
|
499
|
+
initialDelayMs: 2000,
|
|
500
|
+
maxDelayMs: 30000,
|
|
501
|
+
retryCategories: [
|
|
502
|
+
...DEFAULT_DAG_RETRY_CATEGORIES,
|
|
503
|
+
STRUCTURED_ARTIFACT_INVALID_RETRY_CATEGORY,
|
|
504
|
+
],
|
|
505
|
+
};
|