minimal-agent 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/package.json +60 -4
- package/plugins/computer_use/.claude-plugin/plugin.json +1 -1
- package/plugins/cron-runner/src/commands.js +4 -4
- package/plugins/cron-runner/src/commands.ts +4 -4
- package/plugins/workflow-runner/.claude-plugin/plugin.json +2 -2
- package/plugins/workflow-runner/commands/workflow.md +57 -90
- package/plugins/workflow-runner/commands/workflows.md +4 -4
- package/plugins/workflow-runner/plugin.js +24 -13
- package/plugins/workflow-runner/plugin.ts +28 -13
- package/plugins/workflow-runner/src/discovery.js +160 -0
- package/plugins/workflow-runner/src/discovery.ts +194 -0
- package/plugins/workflow-runner/src/goalAnchor.js +53 -0
- package/plugins/workflow-runner/src/goalAnchor.ts +59 -0
- package/plugins/workflow-runner/src/index.js +197 -206
- package/plugins/workflow-runner/src/index.ts +248 -249
- package/plugins/workflow-runner/src/inputCoercion.js +40 -0
- package/plugins/workflow-runner/src/inputCoercion.ts +43 -0
- package/plugins/workflow-runner/src/journal.js +224 -0
- package/plugins/workflow-runner/src/journal.ts +274 -0
- package/plugins/workflow-runner/src/jsonSchema.js +132 -0
- package/plugins/workflow-runner/src/jsonSchema.ts +162 -0
- package/plugins/workflow-runner/src/moduleResolution.js +64 -0
- package/plugins/workflow-runner/src/moduleResolution.ts +71 -0
- package/plugins/workflow-runner/src/resourceInputs.js +220 -0
- package/plugins/workflow-runner/src/resourceInputs.ts +300 -0
- package/plugins/workflow-runner/src/runner.js +400 -489
- package/plugins/workflow-runner/src/runner.ts +488 -560
- package/plugins/workflow-runner/src/runtime/config.js +46 -0
- package/plugins/workflow-runner/src/runtime/config.ts +69 -0
- package/plugins/workflow-runner/src/runtime/context.js +1304 -0
- package/plugins/workflow-runner/src/runtime/context.ts +1786 -0
- package/plugins/workflow-runner/src/runtime/exec.js +34 -0
- package/plugins/workflow-runner/src/runtime/exec.ts +53 -0
- package/plugins/workflow-runner/src/runtime/flow.js +8 -0
- package/plugins/workflow-runner/src/runtime/flow.ts +12 -0
- package/plugins/workflow-runner/src/runtime/fs.js +27 -0
- package/plugins/workflow-runner/src/runtime/fs.ts +41 -0
- package/plugins/workflow-runner/src/runtime/model.js +6 -0
- package/plugins/workflow-runner/src/runtime/model.ts +8 -0
- package/plugins/workflow-runner/src/runtime/output.js +7 -0
- package/plugins/workflow-runner/src/runtime/output.ts +21 -0
- package/plugins/workflow-runner/src/runtime/scheduler.js +141 -0
- package/plugins/workflow-runner/src/runtime/scheduler.ts +166 -0
- package/plugins/workflow-runner/src/runtime/state.js +16 -0
- package/plugins/workflow-runner/src/runtime/state.ts +19 -0
- package/plugins/workflow-runner/src/runtime/tools.js +3 -0
- package/plugins/workflow-runner/src/runtime/tools.ts +6 -0
- package/plugins/workflow-runner/src/sandbox.js +141 -0
- package/plugins/workflow-runner/src/sandbox.ts +182 -0
- package/plugins/workflow-runner/src/scriptGuard.d.mts +38 -0
- package/plugins/workflow-runner/src/scriptGuard.mjs +449 -0
- package/plugins/workflow-runner/src/taskIdentity.js +151 -307
- package/plugins/workflow-runner/src/taskIdentity.ts +207 -355
- package/plugins/workflow-runner/src/toolRuntime.js +29 -8
- package/plugins/workflow-runner/src/toolRuntime.ts +36 -11
- package/plugins/workflow-runner/src/types.js +7 -211
- package/plugins/workflow-runner/src/types.ts +154 -623
- package/plugins/workflow-runner/src/util.js +47 -0
- package/plugins/workflow-runner/src/util.ts +51 -0
- package/plugins/workflow-runner/src/workflowContext.js +20 -0
- package/plugins/workflow-runner/src/workflowContext.ts +58 -0
- package/plugins/workflow-runner/src/workflowDependencies.js +36 -0
- package/plugins/workflow-runner/src/workflowDependencies.ts +59 -0
- package/plugins/workflow-runner/src/workflowFingerprint.js +118 -0
- package/plugins/workflow-runner/src/workflowFingerprint.ts +122 -0
- package/plugins/workflow-runner/src/workflowState.js +474 -802
- package/plugins/workflow-runner/src/workflowState.ts +575 -970
- package/plugins/workflow-runner/test/api.test.ts +223 -0
- package/plugins/workflow-runner/test/apiCapabilities.test.ts +614 -0
- package/plugins/workflow-runner/test/apiConcurrencyGate.test.ts +104 -0
- package/plugins/workflow-runner/test/apiEcho.test.ts +210 -0
- package/plugins/workflow-runner/test/apiReadText.test.ts +178 -0
- package/plugins/workflow-runner/test/apiResourceLedger.test.ts +184 -0
- package/plugins/workflow-runner/test/childWorkflow.test.ts +304 -0
- package/plugins/workflow-runner/test/discovery.test.ts +234 -0
- package/plugins/workflow-runner/test/goalAnchor.test.ts +106 -0
- package/plugins/workflow-runner/test/goalController.test.ts +317 -0
- package/plugins/workflow-runner/test/journalLanes.test.ts +198 -0
- package/plugins/workflow-runner/test/listResources.test.ts +106 -0
- package/plugins/workflow-runner/test/resourceInputs.test.ts +294 -0
- package/plugins/workflow-runner/test/sandbox.test.ts +126 -0
- package/plugins/workflow-runner/test/scheduler.test.ts +70 -0
- package/plugins/workflow-runner/test/scriptGuard.test.ts +276 -0
- package/plugins/workflow-runner/test/skillDocSync.test.ts +78 -0
- package/plugins/workflow-runner/test/taskIdentity.test.ts +288 -179
- package/plugins/workflow-runner/test/workflowFingerprint.test.ts +106 -0
- package/plugins/workflow-runner/test/workflowState.test.ts +429 -0
- package/skills/pptx/examples/assets/cover-bg.svg +23 -0
- package/skills/pptx/examples/assets/icon-spark.svg +5 -0
- package/skills/ui-design/SKILL.md +5 -2
- package/skills/ui-design/references/layout-composition.md +1 -1
- package/skills/workflow-creator/SKILL.md +344 -0
- package/skills/workflow-creator/assets/template/workflow.mjs +48 -0
- package/skills/workflow-creator/references/api.md +216 -0
- package/skills/workflow-creator/references/migration.md +103 -0
- package/skills/workflow-creator/references/patterns.md +779 -0
- package/skills/workflow-creator/scripts/list-resources.mjs +205 -0
- package/skills/workflow-creator/scripts/scaffold.mjs +148 -0
- package/skills/workflow-creator/scripts/validate.mjs +275 -0
- package/skills/workflow-creator/scripts/workflow-creator.test.mjs +82 -0
- package/src/advisor/advisorCoordinator.js +503 -0
- package/src/advisor/advisorCoordinator.ts +657 -0
- package/src/cli/configMenu.tsx +0 -1
- package/src/cli/print.js +27 -6
- package/src/cli/print.ts +48 -4
- package/src/cli/streamJson.js +5 -6
- package/src/cli/streamJson.ts +8 -4
- package/src/cli/wizards/advisorWizard.js +4 -2
- package/src/cli/wizards/advisorWizard.tsx +8 -2
- package/src/cli/wizards/contextAdvisorWizard.js +5 -4
- package/src/cli/wizards/contextAdvisorWizard.tsx +7 -6
- package/src/collaboration/taskFlow.js +115 -0
- package/src/collaboration/taskFlow.ts +169 -0
- package/src/config/configDefaults.js +41 -1
- package/src/config/configDefaults.ts +43 -1
- package/src/config/configDiagnostics.ts +2 -2
- package/src/config/configFile.js +40 -9
- package/src/config/configFile.ts +60 -11
- package/src/config/configService.js +419 -74
- package/src/config/configService.ts +703 -145
- package/src/config/numericLimits.js +22 -0
- package/src/config/numericLimits.ts +24 -0
- package/src/config.js +1 -0
- package/src/config.ts +3 -2
- package/src/context/budgetPlan.js +1 -1
- package/src/context/budgetPlan.ts +1 -1
- package/src/context/compact.js +7 -2
- package/src/context/compact.ts +9 -2
- package/src/context/microCompactLite.js +212 -13
- package/src/context/microCompactLite.ts +261 -12
- package/src/context/sessionRegistry.js +8 -1
- package/src/context/sessionRegistry.ts +18 -5
- package/src/context/toolResultArchive.js +72 -0
- package/src/context/toolResultArchive.ts +95 -0
- package/src/goal/errors.js +8 -0
- package/src/goal/errors.ts +18 -0
- package/src/goal/goalRuntime.js +344 -0
- package/src/goal/goalRuntime.ts +466 -0
- package/src/goal/goalStore.js +200 -0
- package/src/goal/goalStore.ts +219 -0
- package/src/goal/index.js +4 -0
- package/src/goal/index.ts +4 -0
- package/src/goal/statusMapping.js +71 -0
- package/src/goal/statusMapping.ts +95 -0
- package/src/goal/types.js +7 -0
- package/src/goal/types.ts +179 -0
- package/src/goal/workflowController.js +306 -0
- package/src/goal/workflowController.ts +360 -0
- package/src/harness/evidenceLedger.js +2 -0
- package/src/harness/evidenceLedger.ts +1 -0
- package/src/harness/stallGuard.js +9 -3
- package/src/harness/stallGuard.ts +7 -3
- package/src/images/imageResolver.js +161 -0
- package/src/images/imageResolver.ts +189 -0
- package/src/infra/verification.js +34 -5
- package/src/infra/verification.ts +39 -4
- package/src/llm/client.js +79 -21
- package/src/llm/client.ts +182 -35
- package/src/llm/gatewayClient.js +201 -0
- package/src/llm/gatewayClient.ts +299 -0
- package/src/llm/messageProvenance.js +46 -0
- package/src/llm/messageProvenance.ts +58 -0
- package/src/llm/modelProvider.js +1 -0
- package/src/llm/modelProvider.ts +22 -0
- package/src/llm/providers/anthropicFormat.js +108 -1
- package/src/llm/providers/anthropicFormat.ts +115 -4
- package/src/llm/providers/openaiFormat.js +9 -0
- package/src/llm/providers/openaiFormat.ts +12 -3
- package/src/loop.js +319 -54
- package/src/loop.ts +373 -57
- package/src/main.js +4 -1
- package/src/main.tsx +4 -1
- package/src/managedExecution/index.js +2 -0
- package/src/managedExecution/index.ts +2 -0
- package/src/managedExecution/registry.js +15 -0
- package/src/managedExecution/registry.ts +20 -0
- package/src/managedExecution/types.js +1 -0
- package/src/managedExecution/types.ts +86 -0
- package/src/modelRef.js +9 -0
- package/src/modelRef.ts +31 -0
- package/src/multimodal.js +14 -28
- package/src/multimodal.ts +17 -33
- package/src/net/lanDiscovery.js +65 -0
- package/src/net/lanDiscovery.ts +82 -0
- package/src/plugin-sdk.js +28 -3
- package/src/plugin-sdk.ts +39 -6
- package/src/plugins/pluginApi.ts +16 -1
- package/src/plugins/pluginLoader.js +4 -0
- package/src/plugins/pluginLoader.ts +6 -0
- package/src/plugins/pluginRunner.js +139 -7
- package/src/plugins/pluginRunner.ts +173 -9
- package/src/plugins/slashFallback.js +113 -0
- package/src/plugins/slashFallback.ts +137 -0
- package/src/prompts/policyRegistry.js +12 -32
- package/src/prompts/policyRegistry.ts +15 -32
- package/src/prompts/promptAssembly.js +103 -12
- package/src/prompts/promptAssembly.ts +114 -12
- package/src/prompts/skillList.js +21 -3
- package/src/prompts/skillList.ts +27 -2
- package/src/prompts/stableSystemPrompt.js +18 -0
- package/src/prompts/stableSystemPrompt.ts +33 -0
- package/src/runtime/agentRuntimeContext.js +8 -2
- package/src/runtime/agentRuntimeContext.ts +17 -4
- package/src/session/runAgentTurn.js +78 -12
- package/src/session/runAgentTurn.ts +105 -23
- package/src/session/runTurn.js +10 -9
- package/src/session/runTurn.ts +17 -8
- package/src/session/sessionState.ts +2 -0
- package/src/team/auth.js +186 -0
- package/src/team/auth.ts +298 -0
- package/src/team/cli.js +105 -0
- package/src/team/cli.ts +122 -0
- package/src/team/onlineToolContract.js +64 -0
- package/src/team/onlineToolContract.ts +109 -0
- package/src/team/toolPolicy.js +36 -0
- package/src/team/toolPolicy.ts +75 -0
- package/src/tools/advisor/advisor.js +90 -46
- package/src/tools/advisor/advisor.ts +100 -47
- package/src/tools/advisor/advisorDecision.js +71 -0
- package/src/tools/advisor/advisorDecision.ts +85 -0
- package/src/tools/agent/agent.js +18 -5
- package/src/tools/agent/agent.ts +21 -7
- package/src/tools/agent/agentControl.js +246 -0
- package/src/tools/agent/agentControl.ts +284 -0
- package/src/tools/agent/agentTaskService.js +40 -1
- package/src/tools/agent/agentTaskService.ts +44 -1
- package/src/tools/agent/agentTypes.js +14 -2
- package/src/tools/agent/agentTypes.ts +15 -2
- package/src/tools/applypatch/applyPatch.js +362 -0
- package/src/tools/applypatch/applyPatch.ts +428 -0
- package/src/tools/bash/bash.js +22 -15
- package/src/tools/bash/bash.ts +23 -17
- package/src/tools/capabilities.js +10 -5
- package/src/tools/capabilities.ts +10 -5
- package/src/tools/computeruse/computerIntent.js +32 -0
- package/src/tools/computeruse/computerIntent.ts +46 -0
- package/src/tools/computeruse/telemetryLog.js +12 -0
- package/src/tools/computeruse/telemetryLog.ts +16 -0
- package/src/tools/computeruse/vision/telemetry.js +1 -0
- package/src/tools/computeruse/vision/telemetry.ts +1 -0
- package/src/tools/effectiveToolSet.js +4 -0
- package/src/tools/effectiveToolSet.ts +4 -0
- package/src/tools/index.js +21 -2
- package/src/tools/index.ts +33 -2
- package/src/tools/presentresult/presentresult.js +6 -15
- package/src/tools/presentresult/presentresult.ts +6 -15
- package/src/tools/read/read.js +62 -9
- package/src/tools/read/read.ts +63 -9
- package/src/tools/shared/fileUtils.js +1 -2
- package/src/tools/shared/fileUtils.ts +1 -2
- package/src/tools/shared/toolOutputStore.js +23 -1
- package/src/tools/shared/toolOutputStore.ts +22 -1
- package/src/tools/skill/skill.js +9 -3
- package/src/tools/skill/skill.ts +8 -4
- package/src/tools/toolPromptGuides.js +117 -0
- package/src/tools/toolPromptGuides.ts +137 -0
- package/src/tools/webbrowser/agentBrowserPath.js +19 -9
- package/src/tools/webbrowser/agentBrowserPath.ts +70 -28
- package/src/tools/webbrowser/browser.js +27 -11
- package/src/tools/webbrowser/browser.ts +68 -24
- package/src/tools/webbrowser/browserAutomation.js +329 -0
- package/src/tools/webbrowser/browserAutomation.ts +467 -0
- package/src/tools/webbrowser/browserAxState.js +97 -0
- package/src/tools/webbrowser/browserAxState.ts +142 -0
- package/src/tools/webbrowser/browserBatch.js +1578 -0
- package/src/tools/webbrowser/browserBatch.ts +2382 -0
- package/src/tools/webbrowser/browserCdp.js +208 -0
- package/src/tools/webbrowser/browserCdp.ts +255 -0
- package/src/tools/webbrowser/browserConditions.js +24 -0
- package/src/tools/webbrowser/browserConditions.ts +45 -0
- package/src/tools/webbrowser/browserContent.js +281 -0
- package/src/tools/webbrowser/browserContent.ts +438 -0
- package/src/tools/webbrowser/browserControl.js +131 -34
- package/src/tools/webbrowser/browserControl.ts +210 -56
- package/src/tools/webbrowser/browserDaemon.js +22 -14
- package/src/tools/webbrowser/browserDaemon.ts +54 -21
- package/src/tools/webbrowser/browserFrames.js +47 -0
- package/src/tools/webbrowser/browserFrames.ts +54 -0
- package/src/tools/webbrowser/browserInteractionResolver.js +17 -0
- package/src/tools/webbrowser/browserInteractionResolver.ts +40 -0
- package/src/tools/webbrowser/browserLocator.js +140 -0
- package/src/tools/webbrowser/browserLocator.ts +194 -0
- package/src/tools/webbrowser/browserOutcome.js +10 -17
- package/src/tools/webbrowser/browserOutcome.ts +48 -31
- package/src/tools/webbrowser/browserRecovery.js +58 -0
- package/src/tools/webbrowser/browserRecovery.ts +83 -0
- package/src/tools/webbrowser/browserRunRegistry.js +35 -3
- package/src/tools/webbrowser/browserRunRegistry.ts +91 -15
- package/src/tools/webbrowser/browserService.js +28 -2
- package/src/tools/webbrowser/browserService.ts +49 -9
- package/src/tools/webbrowser/browserStream.js +132 -33
- package/src/tools/webbrowser/browserStream.ts +224 -50
- package/src/tools/webbrowser/browserTabs.js +48 -15
- package/src/tools/webbrowser/browserTabs.ts +178 -96
- package/src/tools/webbrowser/webbrowser.js +679 -144
- package/src/tools/webbrowser/webbrowser.ts +975 -236
- package/src/tools/webfetch/preapproved.js +0 -2
- package/src/tools/webfetch/preapproved.ts +0 -2
- package/src/tools/websearch/websearch.js +129 -8
- package/src/tools/websearch/websearch.ts +138 -6
- package/src/types.ts +440 -38
- package/src/ui/App.js +377 -58
- package/src/ui/App.tsx +576 -111
- package/src/ui/LiveArea.js +14 -8
- package/src/ui/LiveArea.tsx +14 -10
- package/src/ui/MessageList.js +63 -20
- package/src/ui/MessageList.tsx +101 -16
- package/src/ui/ModelPicker.js +7 -4
- package/src/ui/ModelPicker.tsx +55 -8
- package/src/ui/hooks/useChat.js +152 -163
- package/src/ui/hooks/useChat.ts +396 -334
- package/src/ui/loginCommand.js +56 -0
- package/src/ui/loginCommand.ts +63 -0
- package/src/ui/toolProgress.js +41 -0
- package/src/ui/toolProgress.ts +44 -0
- package/src/web/browserBridge.js +20 -2
- package/src/web/browserBridge.ts +20 -2
- package/src/web/protocol.ts +1897 -194
- package/workflows/adaptive-research/workflow.mjs +86 -0
- package/workflows/e2e-capabilities/workflow.mjs +102 -0
- package/workflows/e2e-llm/workflow.mjs +52 -0
- package/workflows/e2e-smoke/workflow.mjs +59 -0
- package/workflows/e2e-vision-schema/README.md +48 -0
- package/workflows/e2e-vision-schema/workflow.mjs +149 -0
- package/workflows/e2e-write-greet/workflow.mjs +37 -0
- package/workflows/file-census/README.md +14 -0
- package/workflows/file-census/scripts/.gitkeep +2 -0
- package/workflows/file-census/workflow.mjs +110 -0
- package/workflows/goal/README.md +166 -0
- package/workflows/goal/prompts/audit.md +57 -0
- package/workflows/goal/prompts/contract.md +56 -0
- package/workflows/goal/prompts/iterate.md +37 -0
- package/workflows/goal/workflow.mjs +501 -0
- package/workflows/illustrate/README.md +90 -0
- package/workflows/illustrate/assets/engines.json +20 -0
- package/workflows/illustrate/assets/extract-baidu.js +21 -0
- package/workflows/illustrate/assets/extract-bing.js +33 -0
- package/workflows/illustrate/assets/extract-google.js +31 -0
- package/workflows/illustrate/workflow.mjs +301 -0
- package/workflows/plan/workflow.mjs +389 -0
- package/workflows/question-crops/README.md +35 -0
- package/workflows/question-crops/prompts/inventory.md +23 -0
- package/workflows/question-crops/scripts/image-ops.mjs +305 -0
- package/workflows/question-crops/scripts/image-ops.test.mjs +95 -0
- package/workflows/question-crops/workflow.mjs +172 -0
- package/workflows/ralph-loop/workflow.mjs +162 -0
- package/workflows/research/README.md +74 -0
- package/workflows/research/prompts/s0-goal.md +43 -0
- package/workflows/research/prompts/s1-early-exit.md +22 -0
- package/workflows/research/prompts/s1-map.md +58 -0
- package/workflows/research/prompts/s2-options.md +53 -0
- package/workflows/research/prompts/s3-deep-dive.md +56 -0
- package/workflows/research/prompts/s4-verify.md +74 -0
- package/workflows/research/prompts/s5-synthesis.md +81 -0
- package/workflows/research/prompts/s5b-fill-gaps.md +32 -0
- package/workflows/research/prompts/s6-package.md +32 -0
- package/workflows/research/scripts/build-report.mjs +98 -0
- package/workflows/research/workflow.mjs +288 -0
- package/workflows/wechat-reading-shots/prompts/collect-params.md +16 -0
- package/workflows/wechat-reading-shots/prompts/evaluate.md +20 -0
- package/workflows/wechat-reading-shots/prompts/open-book.md +28 -0
- package/workflows/wechat-reading-shots/prompts/read-page.md +13 -0
- package/workflows/wechat-reading-shots/prompts/turn-batch.md +24 -0
- package/workflows/wechat-reading-shots/test/control-flow.test.ts +329 -0
- package/workflows/wechat-reading-shots/workflow.mjs +266 -0
- package/workflows/wrong-question-docx/README.md +55 -0
- package/workflows/wrong-question-docx/prompts/figure-qa.md +8 -0
- package/workflows/wrong-question-docx/prompts/transcribe.md +25 -0
- package/workflows/wrong-question-docx/scripts/workbook.mjs +857 -0
- package/workflows/wrong-question-docx/test/control-flow.test.ts +165 -0
- package/workflows/wrong-question-docx/test/workbook.test.mjs +303 -0
- package/workflows/wrong-question-docx/workflow.mjs +485 -0
- package/plugins/workflow-runner/src/agentCommon.js +0 -354
- package/plugins/workflow-runner/src/agentCommon.ts +0 -435
- package/plugins/workflow-runner/src/controlFlow/branch.js +0 -40
- package/plugins/workflow-runner/src/controlFlow/branch.ts +0 -46
- package/plugins/workflow-runner/src/controlFlow/loop.js +0 -211
- package/plugins/workflow-runner/src/controlFlow/loop.ts +0 -248
- package/plugins/workflow-runner/src/controlFlow/pause.js +0 -129
- package/plugins/workflow-runner/src/controlFlow/pause.ts +0 -141
- package/plugins/workflow-runner/src/controlFlow/signal.js +0 -21
- package/plugins/workflow-runner/src/controlFlow/signal.ts +0 -29
- package/plugins/workflow-runner/src/controlFlow/types.js +0 -18
- package/plugins/workflow-runner/src/controlFlow/types.ts +0 -63
- package/plugins/workflow-runner/src/dslManifest.js +0 -72
- package/plugins/workflow-runner/src/dslManifest.ts +0 -127
- package/plugins/workflow-runner/src/expressions.js +0 -639
- package/plugins/workflow-runner/src/expressions.ts +0 -640
- package/plugins/workflow-runner/src/loader.js +0 -573
- package/plugins/workflow-runner/src/loader.ts +0 -721
- package/plugins/workflow-runner/src/multimodal.js +0 -38
- package/plugins/workflow-runner/src/multimodal.ts +0 -64
- package/plugins/workflow-runner/src/recovery.js +0 -126
- package/plugins/workflow-runner/src/recovery.ts +0 -156
- package/plugins/workflow-runner/src/stepExecutors/agent.js +0 -173
- package/plugins/workflow-runner/src/stepExecutors/agent.ts +0 -234
- package/plugins/workflow-runner/src/stepExecutors/assert.js +0 -42
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +0 -52
- package/plugins/workflow-runner/src/stepExecutors/echo.js +0 -31
- package/plugins/workflow-runner/src/stepExecutors/echo.ts +0 -38
- package/plugins/workflow-runner/src/stepExecutors/llm.js +0 -99
- package/plugins/workflow-runner/src/stepExecutors/llm.ts +0 -133
- package/plugins/workflow-runner/src/stepExecutors/set.js +0 -49
- package/plugins/workflow-runner/src/stepExecutors/set.ts +0 -56
- package/plugins/workflow-runner/src/stepExecutors/skill.js +0 -111
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +0 -140
- package/plugins/workflow-runner/src/stepExecutors/tool.js +0 -227
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +0 -274
- package/plugins/workflow-runner/src/stepExecutors/verify.js +0 -61
- package/plugins/workflow-runner/src/stepExecutors/verify.ts +0 -72
- package/plugins/workflow-runner/src/stepPipeline.js +0 -277
- package/plugins/workflow-runner/src/stepPipeline.ts +0 -366
- package/plugins/workflow-runner/test/agent.test.ts +0 -587
- package/plugins/workflow-runner/test/agentLlmError.test.ts +0 -373
- package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +0 -65
- package/plugins/workflow-runner/test/captureMisbind.test.ts +0 -99
- package/plugins/workflow-runner/test/cli.e2e.test.ts +0 -167
- package/plugins/workflow-runner/test/context-files.test.ts +0 -320
- package/plugins/workflow-runner/test/e2e.test.ts +0 -370
- package/plugins/workflow-runner/test/e2eNewLayout.test.ts +0 -314
- package/plugins/workflow-runner/test/engineRobustness.test.ts +0 -234
- package/plugins/workflow-runner/test/expressions.test.ts +0 -439
- package/plugins/workflow-runner/test/foundation.test.ts +0 -126
- package/plugins/workflow-runner/test/graceful.test.ts +0 -139
- package/plugins/workflow-runner/test/greedyInputs.load.test.ts +0 -47
- package/plugins/workflow-runner/test/hostCapability.test.ts +0 -108
- package/plugins/workflow-runner/test/illustrate.load.test.ts +0 -183
- package/plugins/workflow-runner/test/jsonPayload.test.ts +0 -83
- package/plugins/workflow-runner/test/loader.test.ts +0 -617
- package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +0 -33
- package/plugins/workflow-runner/test/multimodal.test.ts +0 -342
- package/plugins/workflow-runner/test/output-schema.test.ts +0 -213
- package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +0 -106
- package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +0 -269
- package/plugins/workflow-runner/test/ppt.load.test.ts +0 -1220
- package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +0 -221
- package/plugins/workflow-runner/test/recovery.test.ts +0 -265
- package/plugins/workflow-runner/test/research.load.test.ts +0 -328
- package/plugins/workflow-runner/test/runner.test.ts +0 -911
- package/plugins/workflow-runner/test/runnerPreflight.test.ts +0 -436
- package/plugins/workflow-runner/test/skillTurnDone.test.ts +0 -71
- package/plugins/workflow-runner/test/stateLifecycle.test.ts +0 -198
- package/plugins/workflow-runner/test/stepPipeline.test.ts +0 -114
- package/plugins/workflow-runner/test/taskArbitration.test.ts +0 -354
- package/plugins/workflow-runner/test/taskLifecycle.test.ts +0 -324
- package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +0 -221
- package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +0 -29
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +0 -168
- package/plugins/workflow-runner/test/webppt.load.test.ts +0 -250
- package/plugins/workflow-runner/test/word.load.test.ts +0 -208
- package/plugins/workflow-runner/test/workflows.smoke.test.ts +0 -76
- package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +0 -207
- package/src/advisorState.js +0 -22
- package/src/cli/configWizard.js +0 -293
- package/src/tools/toolProgressBridge.js +0 -48
- package/workflows/adaptive-research.yaml +0 -50
- package/workflows/book-review-short.yaml +0 -125
- package/workflows/e2e-write-greet.yaml +0 -31
- package/workflows/illustrate.yaml +0 -227
- package/workflows/plan.yaml +0 -470
- package/workflows/ppt.yaml +0 -1670
- package/workflows/ralph-loop.yaml +0 -201
- package/workflows/research.yaml +0 -873
- package/workflows/schema.json +0 -176
- package/workflows/webppt.yaml +0 -496
- package/workflows/wechat-reading-shots.yaml +0 -315
- package/workflows/word.yaml +0 -720
- package/workflows/youtube-shorts.yaml +0 -202
- /package/workflows/{research.md → research/DESIGN.md} +0 -0
|
@@ -1,753 +1,671 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ============================================================
|
|
3
|
-
* plugins/workflow-runner/src/workflowState.ts ——
|
|
3
|
+
* plugins/workflow-runner/src/workflowState.ts —— 任务状态持久化
|
|
4
4
|
* ------------------------------------------------------------
|
|
5
|
-
*
|
|
5
|
+
* 自 legacy 移植**持久化那一半**。legacy 版 1178 行里另一半是按「步骤索引 /
|
|
6
|
+
* 循环迭代 / VarStack 快照 / capture 变量」做的断点定位 —— 那些概念在代码
|
|
7
|
+
* 编排里不存在,由 `journal.ts` 的「调用序号 + 内容指纹」整体取代。
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
* completed 时据此报实际存在的产物。不再用从不写入的 artifacts/ 子目录。
|
|
13
|
-
* - resume 模式时回放已完成步骤的 capture 到 vars 中
|
|
14
|
-
* - completed 检测:成功后 archive() 把目录改名 .done,故 init() 在活动目录缺失时
|
|
15
|
-
* 回退读 .done/current.json(status=done + taskId 匹配 → completed),修活「completed 死代码」。
|
|
9
|
+
* 本文件负责的是**跨进程活得下来的东西**:
|
|
10
|
+
* - 目录布局与四态判定(fresh / new-task / completed / resume)
|
|
11
|
+
* - 声明产物的「先归档再清理」(含 glob 展开)
|
|
12
|
+
* - 用户补充要求(supplements)的持久化
|
|
13
|
+
* - 人类可读的 progress 日志
|
|
16
14
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* # (resume 可恢复到迭代中间,见 scanLoopResume)
|
|
27
|
-
* /new 按 .minimal-agent 前缀清理父目录,行为不变。
|
|
15
|
+
* ------------------------------------------------------------
|
|
16
|
+
* ⚠️ **清理必须先归档,不能直接 rm** —— 这条是实战事故换来的:
|
|
17
|
+
* 同主题重跑时 LLM 仲裁误判成 new-task,11 个声明产物被静默直接删除、不可找回。
|
|
18
|
+
* 现在一律先 `rename` 进 `<实例目录>.trash/<stamp>/`(保留相对路径结构),
|
|
19
|
+
* rm 只作为 rename 失败的兜底。`.trash/` 是状态目录的 sibling,跨任务留存
|
|
20
|
+
* (淘汰策略暂未实现,需要时由用户手动清)。
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ 归档失败时**绝不谎报归档位置** —— 一个文件都没成功移走就返回 undefined。
|
|
23
|
+
* 报了位置用户会去回收站找,找不到比一开始就说「已删除」更糟。
|
|
28
24
|
* ============================================================
|
|
29
25
|
*/
|
|
30
26
|
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
import { type Dirent, existsSync } from 'node:fs';
|
|
28
|
+
import {
|
|
29
|
+
appendFile,
|
|
30
|
+
mkdir,
|
|
31
|
+
readFile,
|
|
32
|
+
readdir,
|
|
33
|
+
rename,
|
|
34
|
+
rm,
|
|
35
|
+
writeFile,
|
|
36
|
+
} from 'node:fs/promises';
|
|
37
|
+
import { dirname, join, relative } from 'node:path';
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
type TaskArbitrator,
|
|
41
|
+
computeTaskId,
|
|
42
|
+
extractGoal,
|
|
43
|
+
sanitizeWorkflowDirName,
|
|
44
|
+
sessionStateDir,
|
|
45
|
+
taskInstanceDir,
|
|
46
|
+
} from './taskIdentity.js';
|
|
47
|
+
import type { InputDef } from './types.js';
|
|
48
|
+
import { safeArtifactPath } from './util.js';
|
|
37
49
|
|
|
38
50
|
const DONE_SUFFIX = '.done';
|
|
39
51
|
const TRASH_SUFFIX = '.trash';
|
|
40
|
-
const CHECKPOINTS_DIR = 'checkpoints';
|
|
41
|
-
const SUPPLEMENTS_FILE = 'supplements.json';
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 顶层 loop 内步的复合 checkpoint 文件名分隔符。
|
|
45
|
-
* 选 '@@':yaml step id 习惯用 [a-z0-9_],不会撞;且对文件系统安全。
|
|
46
|
-
*/
|
|
47
|
-
export const LOOP_CP_SEP = '@@';
|
|
48
|
-
|
|
49
|
-
/** 复合 checkpoint 名:<loopId>@@<迭代号>@@<stepId>(runner 的 CpScope 与本模块扫描共用)。 */
|
|
50
|
-
export function loopCheckpointName(loopId: string, iteration: number, stepId: string): string {
|
|
51
|
-
return `${loopId}${LOOP_CP_SEP}${iteration}${LOOP_CP_SEP}${stepId}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* glob 段(路径中的一节,不含分隔符)转正则源:`*` -> `[^/]*`、`?` -> `[^/]`,
|
|
56
|
-
* 其余字符全部转义(`[` `]` 按字面处理,不支持字符类--声明产物用 `*`/`?` 足够,
|
|
57
|
-
* 少一种语法就少一类转义事故面)。
|
|
58
|
-
*/
|
|
59
|
-
function artifactSegToPattern(seg: string): string {
|
|
60
|
-
return seg
|
|
61
|
-
.replace(/[.+^${}()|[\]\\!,$^-]/g, '\\$&')
|
|
62
|
-
.replace(/\*/g, '[^/]*')
|
|
63
|
-
.replace(/\?/g, '[^/]');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** 解析复合 checkpoint 文件名;非复合名(普通顶层步)返回 null。 */
|
|
67
|
-
function parseLoopCheckpointName(
|
|
68
|
-
name: string,
|
|
69
|
-
): { loopId: string; iteration: number; stepId: string } | null {
|
|
70
|
-
const parts = name.split(LOOP_CP_SEP);
|
|
71
|
-
if (parts.length !== 3) return null;
|
|
72
|
-
const iteration = Number(parts[1]);
|
|
73
|
-
if (!Number.isInteger(iteration) || iteration < 0) return null;
|
|
74
|
-
return { loopId: parts[0], iteration, stepId: parts[2] };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 工作流定义的**结构指纹**:步骤 id + 执行形态 + 嵌套块(then/else/do)骨架的 hash。
|
|
79
|
-
* prompt 文案、args、condition 等内容字段不参与——改提示词/参数不应作废断点;
|
|
80
|
-
* 改结构(增删步/换 id/换类型/调嵌套)才作废:resume 的恢复点是**索引**、循环断点是
|
|
81
|
-
* 复合名(loopId@@迭代@@stepId),结构一变旧 checkpoint 就对不上,按旧断点续跑会错跳步
|
|
82
|
-
* (曾真实发生:plan.yaml v3→v4 重构后同 goal 重跑提示「从第 2 步续跑」,跳过的步与
|
|
83
|
-
* 新结构毫无对应关系)。current.json 记录运行时的指纹,init 同任务重跑时比对。
|
|
84
|
-
*/
|
|
85
|
-
export function computeDefFingerprint(steps: StepDef[]): string {
|
|
86
|
-
const skeleton = (list: StepDef[] | undefined): unknown[] =>
|
|
87
|
-
(list ?? []).map((s) => ({
|
|
88
|
-
id: s.id,
|
|
89
|
-
kind:
|
|
90
|
-
s.type ??
|
|
91
|
-
(s.tool ? 'tool' : s.skill ? 'skill' : s.agent ? 'agent' : s.llm ? 'llm' : '?'),
|
|
92
|
-
then: s.then ? skeleton(s.then) : undefined,
|
|
93
|
-
else: s.else ? skeleton(s.else) : undefined,
|
|
94
|
-
do: s.do ? skeleton(s.do) : undefined,
|
|
95
|
-
}));
|
|
96
|
-
return createHash('sha1')
|
|
97
|
-
.update(JSON.stringify(skeleton(steps)))
|
|
98
|
-
.digest('hex')
|
|
99
|
-
.slice(0, 16);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** 进行中循环的恢复信息(buildResumeInfo 产出,runner 的 loop 处理器消费)。 */
|
|
103
|
-
export interface LoopResumeInfo {
|
|
104
|
-
/** 中断时正在执行的顶层 loop 步 id */
|
|
105
|
-
loopId: string;
|
|
106
|
-
/** 该 loop 在顶层 steps[] 中的索引(= resume 的顶层 startFrom) */
|
|
107
|
-
loopIndex: number;
|
|
108
|
-
/** 恢复到第几轮迭代(0-based,= 中断时的迭代号) */
|
|
109
|
-
iteration: number;
|
|
110
|
-
/** 该迭代内从第几个 do[] 步起跑(已含 backoff 回退) */
|
|
111
|
-
innerStartFrom: number;
|
|
112
|
-
/** 该迭代内已完成的 do[] 步 id(按 do[] 顺序) */
|
|
113
|
-
completedInnerIds: string[];
|
|
114
|
-
/** 中断时刻最近一个 checkpoint 的 vars 快照(runner 回放为全局变量,不含 inputs) */
|
|
115
|
-
lastVarsSnapshot?: Record<string, unknown>;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ---- 导出类型 ----
|
|
119
52
|
|
|
120
|
-
/**
|
|
53
|
+
/** 四态:首次 / 换了任务 / 同任务已完成 / 同任务未完成可续跑。 */
|
|
121
54
|
export type TaskResumeMode = 'fresh' | 'new-task' | 'completed' | 'resume';
|
|
122
55
|
|
|
123
|
-
/** init() 返回的恢复信息 */
|
|
124
56
|
export interface TaskResumeInfo {
|
|
125
57
|
mode: TaskResumeMode;
|
|
126
|
-
/** fresh/new-task
|
|
127
|
-
startFrom?: number;
|
|
128
|
-
/**
|
|
129
|
-
* fresh/new-task: 被 rm 直接删除的残留产物路径(展开 glob 后的实际文件)。
|
|
130
|
-
* 注意:正常路径清理先归档(archivedTo)再 rm 兜底,归档成功的文件已移走、
|
|
131
|
-
* 不在本列表里--本列表非空通常意味着归档失败的兜底删除。
|
|
132
|
-
*/
|
|
133
|
-
cleanedArtifacts?: string[];
|
|
134
|
-
/**
|
|
135
|
-
* fresh/new-task: 清理前旧产物被归档到的目录(相对 cwd,如
|
|
136
|
-
* .minimal-agent-workflow/plan.trash/1712...-ab12cd34/)。清理不再直接
|
|
137
|
-
* 删除而是先移入回收目录--LLM 仲裁误判 new-task 时用户成果可找回
|
|
138
|
-
* (实战事故:同主题重跑被仲裁判新任务,11 个声明产物被直接 rm)。
|
|
139
|
-
* 没有可归档文件或归档全部失败时缺省。
|
|
140
|
-
*/
|
|
58
|
+
/** fresh/new-task:旧产物归档到哪(相对 cwd)。归档一个都没成功时**不填**。 */
|
|
141
59
|
archivedTo?: string;
|
|
60
|
+
/** fresh/new-task:归档失败后被 rm 兜底删掉的路径。非空通常意味着归档出了问题。 */
|
|
61
|
+
cleanedArtifacts?: string[];
|
|
142
62
|
/**
|
|
143
|
-
* new-task
|
|
144
|
-
*
|
|
63
|
+
* new-task:同一个 taskId,但**脚本内容变了**(指纹不匹配)→ 旧断点作废。
|
|
64
|
+
* 调用方据此向用户明示原因,而不是静默从头跑。
|
|
145
65
|
*/
|
|
146
|
-
|
|
147
|
-
/** completed
|
|
66
|
+
scriptChanged?: boolean;
|
|
67
|
+
/** completed:磁盘上**实际存在**的产物(不是声明了就报)。 */
|
|
148
68
|
artifacts?: string[];
|
|
149
|
-
/** resume
|
|
150
|
-
completedStepIds?: string[];
|
|
151
|
-
/** resume: 失败的 step id */
|
|
152
|
-
failedStep?: string;
|
|
153
|
-
/** resume: 自愈回退到的起始索引 */
|
|
154
|
-
recoveryStart?: number;
|
|
155
|
-
/** resume: 上次错误信息 */
|
|
69
|
+
/** resume:上次失败的原因。 */
|
|
156
70
|
lastError?: string;
|
|
157
|
-
/** resume: 已完成步骤的 capture 变量快照 { stepId: { captureKey: value } } */
|
|
158
|
-
capturedVars?: Record<string, Record<string, unknown>>;
|
|
159
71
|
/**
|
|
160
|
-
* resume
|
|
161
|
-
*
|
|
162
|
-
*
|
|
72
|
+
* resume(仲裁判定的延续):旧任务的原始 goal。
|
|
73
|
+
*
|
|
74
|
+
* 本次输入是「继续」这类延续指令,**不能拿它当目标锚** —— 那会让后续每次
|
|
75
|
+
* 调用都把「继续」当成要达成的事。锚必须是旧任务的真实目标。
|
|
163
76
|
*/
|
|
164
77
|
prevGoal?: string;
|
|
165
|
-
/**
|
|
166
|
-
* resume(仲裁判定的延续): 本次的新输入文本,作为旧目标的补充指令
|
|
167
|
-
* 并入 goalAnchor(如「继续,但 PPT 改成 12 页」中的修改意见不丢失)。
|
|
168
|
-
*/
|
|
78
|
+
/** resume(仲裁判定的延续):本次新输入,作为旧目标的补充要求并入锚。 */
|
|
169
79
|
supplementaryGoal?: string;
|
|
170
|
-
/**
|
|
171
|
-
* resume: 中断发生在某顶层 loop 的迭代中间时的恢复信息。
|
|
172
|
-
* 存在时 startFrom = loopIndex(顶层直接定位到该 loop),迭代号与迭代内
|
|
173
|
-
* 起点由 runner 的 loop 处理器消费;backoff 作用在迭代内而非顶层。
|
|
174
|
-
*/
|
|
175
|
-
loopResume?: LoopResumeInfo;
|
|
176
80
|
}
|
|
177
81
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
/** 单个 checkpoint 的数据结构 */
|
|
181
|
-
interface CheckpointData {
|
|
182
|
-
stepId: string;
|
|
183
|
-
completedAt: string; // ISO timestamp
|
|
184
|
-
output: string; // step result preview
|
|
185
|
-
captures: Record<string, unknown>; // 该步绑定的 capture 变量
|
|
186
|
-
varsSnapshot: Record<string, unknown>; // 当时的全局 vars 快照
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ---- current.json 结构 ----
|
|
190
|
-
|
|
82
|
+
/** `current.json` 的形状。 */
|
|
191
83
|
interface CurrentState {
|
|
192
84
|
name: string;
|
|
193
85
|
taskId: string;
|
|
194
|
-
status:
|
|
86
|
+
status: 'running' | 'failed' | 'done';
|
|
195
87
|
startedAt: string;
|
|
196
|
-
|
|
88
|
+
goal: string;
|
|
197
89
|
lastError?: string;
|
|
198
|
-
/**
|
|
90
|
+
/** 声明产物(相对 cwd):new-task 清理与 completed 报告用。 */
|
|
199
91
|
artifacts?: string[];
|
|
200
|
-
/**
|
|
201
|
-
|
|
92
|
+
/** Exact successful outputs, including owned descendants; declarations may include failed optional outputs. */
|
|
93
|
+
completion?: { artifacts: string[]; stateFiles: string[] };
|
|
94
|
+
/**
|
|
95
|
+
* 编排脚本全文的哈希。取代 legacy 的 `defFingerprint`(那是按步骤骨架算的)。
|
|
96
|
+
*
|
|
97
|
+
* 作用相同:同一个 taskId 但脚本改过 → 旧 journal 的调用序号对不上新脚本的
|
|
98
|
+
* 控制流 → 断点作废、明示原因、全新开始。legacy 曾因索引断点在 yaml 重构后
|
|
99
|
+
* 错跳步,被误读成 bug(真实发生过)。
|
|
100
|
+
*/
|
|
101
|
+
scriptHash?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 把 glob 段翻成正则片段(`*` 不跨目录分隔符,`?` 单字符)。 */
|
|
105
|
+
function artifactSegToPattern(seg: string): string {
|
|
106
|
+
return seg
|
|
107
|
+
.split(/([*?])/)
|
|
108
|
+
.map((part) => {
|
|
109
|
+
if (part === '*') return '[^/]*';
|
|
110
|
+
if (part === '?') return '[^/]';
|
|
111
|
+
return part.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
112
|
+
})
|
|
113
|
+
.join('');
|
|
202
114
|
}
|
|
203
115
|
|
|
204
116
|
export class WorkflowState {
|
|
205
|
-
/**
|
|
206
|
-
dir
|
|
207
|
-
/**
|
|
208
|
-
readonly
|
|
117
|
+
/** 任务实例目录(绝对路径)。init 里确定 taskId 后才有值。 */
|
|
118
|
+
dir = '';
|
|
119
|
+
/** 当前 session 的目录(其下按 taskId 分实例)。 */
|
|
120
|
+
readonly sessionDir: string;
|
|
209
121
|
readonly workingDir: string;
|
|
210
122
|
readonly workflowName: string;
|
|
211
|
-
|
|
123
|
+
private readonly taskIdOverride?: string;
|
|
212
124
|
readonly sessionId: string;
|
|
125
|
+
/** init 里算出的任务身份。 */
|
|
126
|
+
taskId = '';
|
|
127
|
+
/**
|
|
128
|
+
* 输入声明。用于让 `extractGoal` 优先取作者标了 `required` 的那个输入 ——
|
|
129
|
+
* 任务身份与目标锚必须用**同一份**取值规则,否则「拿来算 taskId 的目标」
|
|
130
|
+
* 和「注入给模型的目标」会指向不同的东西。
|
|
131
|
+
*/
|
|
132
|
+
private readonly defs?: InputDef[];
|
|
133
|
+
private progressWrites: Promise<void> = Promise.resolve();
|
|
134
|
+
private eventWrites: Promise<void> = Promise.resolve();
|
|
213
135
|
|
|
214
|
-
constructor(
|
|
136
|
+
constructor(
|
|
137
|
+
workingDir: string,
|
|
138
|
+
workflowName: string,
|
|
139
|
+
sessionId: string,
|
|
140
|
+
defs?: InputDef[],
|
|
141
|
+
taskIdOverride?: string,
|
|
142
|
+
private readonly parentStateDir?: string,
|
|
143
|
+
) {
|
|
215
144
|
this.workingDir = workingDir;
|
|
216
145
|
this.workflowName = workflowName;
|
|
217
|
-
// Phase 3:session 即任务实例——实例目录 = <wfDir>/<sessionId>/
|
|
218
146
|
this.sessionId = sanitizeWorkflowDirName(sessionId);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.
|
|
147
|
+
this.sessionDir = parentStateDir
|
|
148
|
+
? join(parentStateDir, 'children', workflowName)
|
|
149
|
+
: sessionStateDir(workingDir, workflowName, sessionId);
|
|
150
|
+
this.defs = defs;
|
|
151
|
+
this.taskIdOverride = taskIdOverride;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** journal 落盘位置(由 journal.ts 消费,见任务 5)。 */
|
|
155
|
+
get journalPath(): string {
|
|
156
|
+
return join(this.dir, 'journal.jsonl');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private get currentPath(): string {
|
|
160
|
+
return join(this.dir, 'current.json');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private get supplementsPath(): string {
|
|
164
|
+
return join(this.dir, 'supplements.json');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private get progressPath(): string {
|
|
168
|
+
return join(this.dir, 'progress.md');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private get eventsPath(): string {
|
|
172
|
+
return join(this.dir, 'events.jsonl');
|
|
223
173
|
}
|
|
224
174
|
|
|
225
175
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* 4 种模式:
|
|
229
|
-
* 1. fresh — 首次运行此工作流(无旧状态)
|
|
230
|
-
* 2. new-task — 不同任务 → 清理旧产物后全新开始
|
|
231
|
-
* 3. completed — 同一任务已完成 → 返回产物列表
|
|
232
|
-
* 4. resume — 同一任务未完成 → 扫描 checkpoint 决定恢复点
|
|
176
|
+
* 判定本次运行属于四态里的哪一种,并把实例目录准备好。
|
|
233
177
|
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
178
|
+
* @param inputs 本次调用的输入(用于算 taskId 与 goal)
|
|
179
|
+
* @param scriptHash 编排脚本全文哈希(脚本改了 → 断点作废)
|
|
180
|
+
* @param declaredArtifacts meta.artifacts 种子(运行时还会经 declareArtifact 增长)
|
|
181
|
+
* @param arbitrate 可选的语义仲裁器;不传则 hash 不等一律 new-task
|
|
237
182
|
*/
|
|
238
183
|
async init(
|
|
239
|
-
name: string,
|
|
240
184
|
inputs: Record<string, unknown>,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
selfHealBackoff = 1,
|
|
244
|
-
// 工作流【声明】的产物路径(相对 cwd,= def.artifacts + step.artifacts),runWorkflow 注入。
|
|
245
|
-
declaredArtifacts: string[] = [],
|
|
246
|
-
// 语义仲裁器(hash 不等 + 旧任务未完成时的二审),runWorkflow 注入;缺省直接 new-task。
|
|
185
|
+
scriptHash: string,
|
|
186
|
+
declaredArtifacts: string[],
|
|
247
187
|
arbitrate?: TaskArbitrator,
|
|
248
188
|
): Promise<TaskResumeInfo> {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const oldState = await this.readCurrentState();
|
|
261
|
-
|
|
262
|
-
if (oldState) {
|
|
263
|
-
// ---- 有活动状态 ----
|
|
264
|
-
if (identity.isSameTask) {
|
|
265
|
-
// 同任务 → resume / recovery(下面有公共分支处理)
|
|
266
|
-
} else {
|
|
267
|
-
// 不同任务 + 旧任务未完成 → new-task(下面 oldState 非空分支处理)
|
|
268
|
-
}
|
|
269
|
-
} else {
|
|
270
|
-
// ---- 活动态为空 ----
|
|
271
|
-
// 先看是否有同 taskId 的归档(同一个任务之前成功完成了)
|
|
272
|
-
const archived = identity.isSameTask
|
|
273
|
-
? await this.findArchivedStateByTaskId(identity.hash)
|
|
274
|
-
: null;
|
|
275
|
-
|
|
276
|
-
if (archived && archived.taskId === identity.hash) {
|
|
277
|
-
// 模式 3: completed —— 同任务此前已成功
|
|
278
|
-
// 把 this.dir 指向归档目录(后续读产物等基于它)
|
|
279
|
-
this.dir = join(this.wfDir, `${this.sessionId}${DONE_SUFFIX}`);
|
|
280
|
-
return {
|
|
281
|
-
mode: 'completed',
|
|
282
|
-
artifacts: await this.existingArtifactsAsync(
|
|
283
|
-
archived.artifacts ?? declaredArtifacts,
|
|
284
|
-
),
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// 有其他任务实例(不同 goal)→ 先仲裁二审,再决定 new-task / resume
|
|
289
|
-
if (this.hasAnyTaskInstance()) {
|
|
290
|
-
let arbitratedResume = false;
|
|
291
|
-
if (
|
|
292
|
-
arbitrate &&
|
|
293
|
-
identity.reason === 'hash_diff' &&
|
|
294
|
-
identity.isSameTask === false
|
|
295
|
-
) {
|
|
296
|
-
const latestActive = await this.findLatestActiveState();
|
|
297
|
-
if (
|
|
298
|
-
latestActive &&
|
|
299
|
-
latestActive.state.name === name &&
|
|
300
|
-
latestActive.state.status !== 'done' &&
|
|
301
|
-
latestActive.state.defFingerprint === fp
|
|
302
|
-
) {
|
|
303
|
-
// 临时切换 this.dir 到旧任务目录,以便 scanCheckpoints / readInputs 工作
|
|
304
|
-
const prevDir = this.dir;
|
|
305
|
-
this.dir = latestActive.dir;
|
|
306
|
-
try {
|
|
307
|
-
const prevGoal = extractGoal((await this.readInputs()) ?? {});
|
|
308
|
-
const newGoal = extractGoal(inputs);
|
|
309
|
-
let verdict: 'resume' | 'new' = 'new';
|
|
310
|
-
try {
|
|
311
|
-
verdict = await arbitrate({
|
|
312
|
-
workflowName: name,
|
|
313
|
-
prevGoal,
|
|
314
|
-
newGoal,
|
|
315
|
-
completedStepIds: await this.scanCheckpoints(steps),
|
|
316
|
-
totalSteps: steps.length,
|
|
317
|
-
artifacts: latestActive.state.artifacts ?? [],
|
|
318
|
-
});
|
|
319
|
-
} catch {
|
|
320
|
-
verdict = 'new';
|
|
321
|
-
}
|
|
322
|
-
if (verdict === 'resume') {
|
|
323
|
-
// 先置位再 return:finally 不再把 this.dir 改回新任务目录,
|
|
324
|
-
// 延续旧实例目录。否则后续 readSupplements / recordCheckpoint
|
|
325
|
-
// 全部写到错误位置,resume 的断点续跑静默失效
|
|
326
|
-
// (实测:supplements 读空、checkpoint 写丢、current.json 停在旧值)。
|
|
327
|
-
arbitratedResume = true;
|
|
328
|
-
const info = await this.buildResumeInfo(
|
|
329
|
-
latestActive.state,
|
|
330
|
-
steps,
|
|
331
|
-
selfHealBackoff,
|
|
332
|
-
);
|
|
333
|
-
info.prevGoal = prevGoal;
|
|
334
|
-
info.supplementaryGoal = newGoal;
|
|
335
|
-
return info;
|
|
336
|
-
}
|
|
337
|
-
} finally {
|
|
338
|
-
// 仲裁判 new → 恢复 this.dir 为新任务目录(除非 resume 已 return)
|
|
339
|
-
if (!arbitratedResume) this.dir = prevDir;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// 模式 2: new-task —— 不同任务,清旧声明产物 + 旧状态后全新开始
|
|
345
|
-
const oldArtifacts = await this.collectAllInstanceArtifacts(name, declaredArtifacts);
|
|
346
|
-
const sweep = await this.archiveThenClean(
|
|
347
|
-
oldArtifacts.length > 0 ? oldArtifacts : declaredArtifacts,
|
|
348
|
-
`${Date.now()}-stale`,
|
|
189
|
+
const goal = extractGoal(inputs, this.defs);
|
|
190
|
+
this.taskId = this.taskIdOverride
|
|
191
|
+
? sanitizeWorkflowDirName(this.taskIdOverride)
|
|
192
|
+
: computeTaskId(this.workflowName, inputs, this.defs);
|
|
193
|
+
const taskDir = this.parentStateDir
|
|
194
|
+
? join(this.sessionDir, this.taskId)
|
|
195
|
+
: taskInstanceDir(
|
|
196
|
+
this.workingDir,
|
|
197
|
+
this.workflowName,
|
|
198
|
+
this.sessionId,
|
|
199
|
+
this.taskId,
|
|
349
200
|
);
|
|
350
|
-
|
|
201
|
+
const doneDir = taskDir + DONE_SUFFIX;
|
|
202
|
+
|
|
203
|
+
// ① 同任务、已完成 → 报实际存在的产物,不重跑。
|
|
204
|
+
if (existsSync(doneDir)) {
|
|
205
|
+
this.dir = doneDir;
|
|
206
|
+
const prev = await this.readCurrent();
|
|
207
|
+
// ⚠️ **脚本改过就不算已完成**。上次那份结论是**另一份脚本**产出的 ——
|
|
208
|
+
// 作者刚改完工作流重跑同一个目标,最想要的恰恰是「按新逻辑再来一遍」,
|
|
209
|
+
// 这时回一句「此前已完成」并甩出旧产物,是**静默给错答案**。
|
|
210
|
+
// (这条是端到端实跑抓到的:改完脚本重跑,1 秒返回旧结果。)
|
|
211
|
+
if (
|
|
212
|
+
prev &&
|
|
213
|
+
((prev.scriptHash && prev.scriptHash !== scriptHash) ||
|
|
214
|
+
!(await this.treeIntact()))
|
|
215
|
+
) {
|
|
216
|
+
const info = await this.restartChanged(
|
|
217
|
+
goal,
|
|
218
|
+
scriptHash,
|
|
351
219
|
declaredArtifacts,
|
|
352
|
-
|
|
220
|
+
prev,
|
|
221
|
+
taskDir,
|
|
353
222
|
);
|
|
354
|
-
|
|
355
|
-
return {
|
|
356
|
-
mode: 'new-task',
|
|
357
|
-
startFrom: 0,
|
|
358
|
-
cleanedArtifacts: [...sweep.cleaned, ...sweepOrphan.cleaned],
|
|
359
|
-
archivedTo: sweep.archivedTo ?? sweepOrphan.archivedTo,
|
|
360
|
-
};
|
|
223
|
+
return { ...info, mode: 'new-task', scriptChanged: true };
|
|
361
224
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const sweepF = await this.archiveThenClean(
|
|
365
|
-
declaredArtifacts,
|
|
366
|
-
`${Date.now()}-orphan`,
|
|
367
|
-
);
|
|
368
|
-
await this.cleanSlate(name, inputs, identity.hash, declaredArtifacts, fp);
|
|
369
|
-
return {
|
|
370
|
-
mode: 'fresh',
|
|
371
|
-
startFrom: 0,
|
|
372
|
-
cleanedArtifacts: sweepF.cleaned,
|
|
373
|
-
archivedTo: sweepF.archivedTo,
|
|
374
|
-
};
|
|
225
|
+
const artifacts = await this.allArtifacts();
|
|
226
|
+
return { mode: 'completed', artifacts };
|
|
375
227
|
}
|
|
376
228
|
|
|
377
|
-
//
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
// 会被误判 new-task 把没做完的产物清掉重开)。不同名 workflow / 旧任务已
|
|
382
|
-
// 完成 / 未提供仲裁器 → 维持 new-task。仲裁器自身抛错也兜底 new-task。
|
|
229
|
+
// ② 同任务、未完成 → resume(能续多远由 journal 说了算)。
|
|
230
|
+
if (existsSync(taskDir)) {
|
|
231
|
+
this.dir = taskDir;
|
|
232
|
+
const prev = await this.readCurrent();
|
|
383
233
|
if (
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
oldState.status !== 'done' &&
|
|
388
|
-
// 定义结构已变(或 legacy 无指纹)→ 旧断点不可续,仲裁 resume 无意义,
|
|
389
|
-
// 跳过仲裁直接走 new-task
|
|
390
|
-
oldState.defFingerprint === fp
|
|
234
|
+
prev &&
|
|
235
|
+
((prev.scriptHash && prev.scriptHash !== scriptHash) ||
|
|
236
|
+
!(await this.treeIntact()))
|
|
391
237
|
) {
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
238
|
+
// 脚本结构变了:旧 journal 的调用序号对不上新控制流,断点必须作废。
|
|
239
|
+
const info = await this.restartChanged(
|
|
240
|
+
goal,
|
|
241
|
+
scriptHash,
|
|
242
|
+
declaredArtifacts,
|
|
243
|
+
prev,
|
|
244
|
+
taskDir,
|
|
245
|
+
);
|
|
246
|
+
return { ...info, mode: 'new-task', scriptChanged: true };
|
|
247
|
+
}
|
|
248
|
+
await this.writeCurrent({
|
|
249
|
+
name: this.workflowName,
|
|
250
|
+
taskId: this.taskId,
|
|
251
|
+
status: 'running',
|
|
252
|
+
startedAt: new Date().toISOString(),
|
|
253
|
+
goal: prev?.goal ?? goal,
|
|
254
|
+
artifacts: [
|
|
255
|
+
...new Set([...(prev?.artifacts ?? []), ...declaredArtifacts]),
|
|
256
|
+
],
|
|
257
|
+
scriptHash,
|
|
258
|
+
});
|
|
259
|
+
return { mode: 'resume', lastError: prev?.lastError };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ③ 本 taskId 没有目录。看看同 session 下有没有**未完成的兄弟任务**可以仲裁延续。
|
|
263
|
+
const sibling = await this.findIncompleteSibling();
|
|
264
|
+
// Bound resources and their declared parameters are an exact contract.
|
|
265
|
+
// A different task hash must not be overridden by a semantic guess based
|
|
266
|
+
// only on the (possibly identical) human-readable goal.
|
|
267
|
+
if (sibling && arbitrate && !inputs.__resource_fingerprint) {
|
|
268
|
+
const prev = await this.readCurrentAt(sibling);
|
|
269
|
+
const verdict = await arbitrate({
|
|
270
|
+
workflowName: this.workflowName,
|
|
271
|
+
prevGoal: prev?.goal ?? '',
|
|
272
|
+
newGoal: goal,
|
|
273
|
+
completedCalls: await this.countJournalEntries(sibling),
|
|
274
|
+
artifacts: await this.existingArtifacts(prev?.artifacts ?? []),
|
|
275
|
+
});
|
|
276
|
+
if (verdict === 'resume') {
|
|
277
|
+
// 采纳旧实例目录继续跑:本次输入降级成补充要求,目标锚仍指向旧目标。
|
|
278
|
+
this.dir = sibling;
|
|
279
|
+
this.taskId = prev?.taskId ?? this.taskId;
|
|
280
|
+
return {
|
|
281
|
+
mode: 'resume',
|
|
282
|
+
prevGoal: prev?.goal,
|
|
283
|
+
supplementaryGoal: goal,
|
|
284
|
+
lastError: prev?.lastError,
|
|
285
|
+
};
|
|
415
286
|
}
|
|
416
|
-
|
|
417
|
-
// 模式 2: new-task —— 不同任务,清旧声明产物 + 旧状态后全新开始。
|
|
418
|
-
// legacy 状态文件无 artifacts 字段时:仅当同名 workflow 用本次声明清单兜底
|
|
419
|
-
//(不同名 workflow 的产物不是我们的,不能殃及)。
|
|
420
|
-
const sweep = await this.archiveThenClean(
|
|
421
|
-
oldState.artifacts ?? (oldState.name === name ? declaredArtifacts : []),
|
|
422
|
-
`${Date.now()}-${oldState.taskId.slice(0, 8)}`,
|
|
423
|
-
);
|
|
424
|
-
const sweepStale = await this.archiveThenClean(
|
|
425
|
-
declaredArtifacts,
|
|
426
|
-
`${Date.now()}-orphan-${identity.hash.slice(0, 8)}`,
|
|
427
|
-
);
|
|
428
|
-
await this.cleanSlate(name, inputs, identity.hash, declaredArtifacts, fp);
|
|
429
|
-
return {
|
|
430
|
-
mode: 'new-task',
|
|
431
|
-
startFrom: 0,
|
|
432
|
-
cleanedArtifacts: [...sweep.cleaned, ...sweepStale.cleaned],
|
|
433
|
-
archivedTo: sweep.archivedTo ?? sweepStale.archivedTo,
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// 4. 同一任务 —— 检查完成状态(兜底:正常成功会 archive,故活动目录 done 少见)
|
|
438
|
-
if (oldState.status === 'done') {
|
|
439
|
-
return {
|
|
440
|
-
mode: 'completed',
|
|
441
|
-
artifacts: await this.existingArtifactsAsync(
|
|
442
|
-
oldState.artifacts ?? declaredArtifacts,
|
|
443
|
-
),
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// 4.5 定义结构变更守卫:同任务但 yaml 结构改过(增删步/换 id/调嵌套)——
|
|
448
|
-
// resume 恢复点是索引、循环断点是复合名,结构一变旧 checkpoint 全对不上,
|
|
449
|
-
// 按旧断点续跑会错跳步(曾真实发生:plan v3→v4 重构后同 goal 重跑
|
|
450
|
-
// 「从第 2 步续跑」,用户困惑是 bug 还是残留)。旧现场作废、全新开始,
|
|
451
|
-
// runner 经 defChanged 旗标向用户明示。legacy 状态(无 defFingerprint
|
|
452
|
-
// 字段)无从验证兼容性,同样按变更处理(一次性升级成本,安全优先)。
|
|
453
|
-
if (oldState.defFingerprint !== fp) {
|
|
454
|
-
const sweep = await this.archiveThenClean(
|
|
455
|
-
oldState.artifacts ?? declaredArtifacts,
|
|
456
|
-
`${Date.now()}-${oldState.taskId.slice(0, 8)}`,
|
|
457
|
-
);
|
|
458
|
-
const sweepStale = await this.archiveThenClean(
|
|
459
|
-
declaredArtifacts,
|
|
460
|
-
`${Date.now()}-orphan-${identity.hash.slice(0, 8)}`,
|
|
461
|
-
);
|
|
462
|
-
await this.cleanSlate(name, inputs, identity.hash, declaredArtifacts, fp);
|
|
463
|
-
return {
|
|
464
|
-
mode: 'new-task',
|
|
465
|
-
startFrom: 0,
|
|
466
|
-
defChanged: true,
|
|
467
|
-
cleanedArtifacts: [...sweep.cleaned, ...sweepStale.cleaned],
|
|
468
|
-
archivedTo: sweep.archivedTo ?? sweepStale.archivedTo,
|
|
469
|
-
};
|
|
470
287
|
}
|
|
471
288
|
|
|
472
|
-
//
|
|
473
|
-
|
|
289
|
+
// ④ 全新任务(或首次运行)。
|
|
290
|
+
this.dir = taskDir;
|
|
291
|
+
const info = await this.startFresh(goal, scriptHash, declaredArtifacts);
|
|
292
|
+
return { ...info, mode: sibling ? 'new-task' : 'fresh' };
|
|
474
293
|
}
|
|
475
294
|
|
|
476
|
-
/**
|
|
477
|
-
|
|
478
|
-
*
|
|
479
|
-
*
|
|
295
|
+
/** 建实例目录、归档并清理旧产物残留、写 current.json。 */
|
|
296
|
+
/** Retire the actual old journal AND dynamic outputs, not only the status flag.
|
|
297
|
+
* A completed directory must also be retired, otherwise it shadows the fresh
|
|
298
|
+
* incomplete attempt on the next resume. Nothing is deleted on this path.
|
|
480
299
|
*/
|
|
481
|
-
private async
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
300
|
+
private async restartChanged(
|
|
301
|
+
goal: string,
|
|
302
|
+
scriptHash: string,
|
|
303
|
+
declaredArtifacts: string[],
|
|
304
|
+
previous: CurrentState,
|
|
305
|
+
taskDir: string,
|
|
485
306
|
): Promise<TaskResumeInfo> {
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
307
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
308
|
+
const archiveRoot = join(this.dir + TRASH_SUFFIX, stamp);
|
|
309
|
+
const existing = [
|
|
310
|
+
...new Set([
|
|
311
|
+
...(await this.allArtifacts()),
|
|
312
|
+
...(await this.existingArtifacts(declaredArtifacts)),
|
|
313
|
+
]),
|
|
314
|
+
];
|
|
315
|
+
await this.archiveDeclaredArtifacts(existing, stamp);
|
|
316
|
+
const remaining = existing.filter((path) =>
|
|
317
|
+
existsSync(join(this.workingDir, path)),
|
|
318
|
+
);
|
|
319
|
+
if (remaining.length)
|
|
320
|
+
throw new Error(`旧产物归档失败,停止覆盖:${remaining.join('、')}`);
|
|
321
|
+
await mkdir(archiveRoot, { recursive: true });
|
|
322
|
+
const oldDir = this.dir;
|
|
323
|
+
await rename(oldDir, join(archiveRoot, 'state'));
|
|
324
|
+
// Older versions could leave both .done and an incomplete directory.
|
|
325
|
+
if (oldDir !== taskDir && existsSync(taskDir)) {
|
|
326
|
+
await rename(taskDir, join(archiveRoot, 'incomplete-state'));
|
|
327
|
+
}
|
|
328
|
+
this.dir = taskDir;
|
|
329
|
+
const info = await this.startFresh(goal, scriptHash, declaredArtifacts);
|
|
330
|
+
return { ...info, archivedTo: relative(this.workingDir, archiveRoot) };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private async startFresh(
|
|
334
|
+
goal: string,
|
|
335
|
+
scriptHash: string,
|
|
336
|
+
declaredArtifacts: string[],
|
|
337
|
+
): Promise<TaskResumeInfo> {
|
|
338
|
+
// 时间戳只用作回收目录名,不参与任何身份判定 —— 故这里用真实时间是安全的。
|
|
339
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
340
|
+
const { cleaned, archivedTo } = await this.archiveThenClean(
|
|
341
|
+
declaredArtifacts,
|
|
342
|
+
stamp,
|
|
343
|
+
);
|
|
344
|
+
await mkdir(this.dir, { recursive: true });
|
|
345
|
+
await writeFile(this.supplementsPath, '[]', 'utf8');
|
|
346
|
+
await this.writeCurrent({
|
|
347
|
+
name: this.workflowName,
|
|
348
|
+
taskId: this.taskId,
|
|
349
|
+
status: 'running',
|
|
350
|
+
startedAt: new Date().toISOString(),
|
|
351
|
+
goal,
|
|
352
|
+
artifacts: declaredArtifacts,
|
|
353
|
+
scriptHash,
|
|
354
|
+
});
|
|
355
|
+
const info: TaskResumeInfo = { mode: 'fresh' };
|
|
356
|
+
if (cleaned.length > 0) info.cleanedArtifacts = cleaned;
|
|
357
|
+
if (archivedTo) info.archivedTo = archivedTo;
|
|
358
|
+
return info;
|
|
359
|
+
}
|
|
498
360
|
|
|
499
|
-
|
|
500
|
-
|
|
361
|
+
/** 同 session 下另一个**未完成**任务的目录(有多个时取最近修改的)。 */
|
|
362
|
+
private async findIncompleteSibling(): Promise<string | null> {
|
|
363
|
+
let entries: Dirent[];
|
|
364
|
+
try {
|
|
365
|
+
entries = await readdir(this.sessionDir, { withFileTypes: true });
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
const candidates = entries
|
|
370
|
+
.filter(
|
|
371
|
+
(e) =>
|
|
372
|
+
e.isDirectory() &&
|
|
373
|
+
!e.name.endsWith(DONE_SUFFIX) &&
|
|
374
|
+
!e.name.endsWith(TRASH_SUFFIX) &&
|
|
375
|
+
e.name !== this.taskId,
|
|
376
|
+
)
|
|
377
|
+
.map((e) => join(this.sessionDir, e.name))
|
|
378
|
+
.filter((d) => existsSync(join(d, 'current.json')));
|
|
379
|
+
if (candidates.length === 0) return null;
|
|
380
|
+
// 多个未完成实例时取最近的那个 —— 用户嘴里的「继续」几乎总是指最近一次。
|
|
381
|
+
let newest = candidates[0];
|
|
382
|
+
let newestAt = '';
|
|
383
|
+
for (const dir of candidates) {
|
|
384
|
+
const cur = await this.readCurrentAt(dir);
|
|
385
|
+
const at = cur?.startedAt ?? '';
|
|
386
|
+
if (at >= newestAt) {
|
|
387
|
+
newestAt = at;
|
|
388
|
+
newest = dir;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return newest;
|
|
392
|
+
}
|
|
501
393
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
return
|
|
508
|
-
mode: 'resume',
|
|
509
|
-
startFrom: loopResume.loopIndex,
|
|
510
|
-
completedStepIds: completedSteps,
|
|
511
|
-
failedStep: oldState.lastFailedStep,
|
|
512
|
-
recoveryStart: loopResume.loopIndex,
|
|
513
|
-
lastError: oldState.lastError,
|
|
514
|
-
capturedVars,
|
|
515
|
-
loopResume,
|
|
516
|
-
};
|
|
394
|
+
private async countJournalEntries(dir: string): Promise<number> {
|
|
395
|
+
try {
|
|
396
|
+
const raw = await readFile(join(dir, 'journal.jsonl'), 'utf8');
|
|
397
|
+
return raw.split('\n').filter((l) => l.trim() !== '').length;
|
|
398
|
+
} catch {
|
|
399
|
+
return 0;
|
|
517
400
|
}
|
|
401
|
+
}
|
|
518
402
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
failedStep: oldState.lastFailedStep,
|
|
524
|
-
recoveryStart,
|
|
525
|
-
lastError: oldState.lastError,
|
|
526
|
-
capturedVars,
|
|
527
|
-
};
|
|
403
|
+
// ---- current.json ----
|
|
404
|
+
|
|
405
|
+
private async readCurrent(): Promise<CurrentState | null> {
|
|
406
|
+
return this.readCurrentAt(this.dir);
|
|
528
407
|
}
|
|
529
408
|
|
|
530
|
-
|
|
531
|
-
* 扫描复合迭代 checkpoint(loopId@@iter@@stepId),产出进行中循环的恢复信息。
|
|
532
|
-
* 候选条件:顶层 type=loop 步 + 自身无完成 checkpoint(completedTopIds 不含它)。
|
|
533
|
-
* 多个候选(罕见,前一循环中断后又跑到后一循环)取顶层索引最大者(最远进度)。
|
|
534
|
-
*/
|
|
535
|
-
private async scanLoopResume(
|
|
536
|
-
steps: StepDef[],
|
|
537
|
-
completedTopIds: string[],
|
|
538
|
-
selfHealBackoff: number,
|
|
539
|
-
): Promise<LoopResumeInfo | null> {
|
|
540
|
-
const cpDir = join(this.dir, CHECKPOINTS_DIR);
|
|
541
|
-
let files: string[];
|
|
409
|
+
private async readCurrentAt(dir: string): Promise<CurrentState | null> {
|
|
542
410
|
try {
|
|
543
|
-
|
|
411
|
+
return JSON.parse(
|
|
412
|
+
await readFile(join(dir, 'current.json'), 'utf8'),
|
|
413
|
+
) as CurrentState;
|
|
544
414
|
} catch {
|
|
545
415
|
return null;
|
|
546
416
|
}
|
|
547
|
-
|
|
548
|
-
// loopId → iteration → stepId 集合
|
|
549
|
-
const byLoop = new Map<string, Map<number, Set<string>>>();
|
|
550
|
-
for (const f of files) {
|
|
551
|
-
if (!f.endsWith('.json')) continue;
|
|
552
|
-
const parsed = parseLoopCheckpointName(f.replace(/\.json$/, ''));
|
|
553
|
-
if (!parsed) continue;
|
|
554
|
-
let iters = byLoop.get(parsed.loopId);
|
|
555
|
-
if (!iters) byLoop.set(parsed.loopId, (iters = new Map()));
|
|
556
|
-
let ids = iters.get(parsed.iteration);
|
|
557
|
-
if (!ids) iters.set(parsed.iteration, (ids = new Set()));
|
|
558
|
-
ids.add(parsed.stepId);
|
|
559
|
-
}
|
|
560
|
-
if (byLoop.size === 0) return null;
|
|
417
|
+
}
|
|
561
418
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (completedTop.has(loopId)) continue; // 该循环已整体完成
|
|
567
|
-
const loopDef = steps[loopIndex];
|
|
568
|
-
const doSteps = loopDef.do ?? [];
|
|
569
|
-
const validInner = new Set(doSteps.map((s) => s.id));
|
|
419
|
+
private async writeCurrent(state: CurrentState): Promise<void> {
|
|
420
|
+
await mkdir(this.dir, { recursive: true });
|
|
421
|
+
await writeFile(this.currentPath, JSON.stringify(state, null, 2), 'utf8');
|
|
422
|
+
}
|
|
570
423
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
424
|
+
/** 记一次失败原因(下次 resume 时报给用户 / 喂给模型)。 */
|
|
425
|
+
async recordFailure(error: string, artifacts?: string[]): Promise<void> {
|
|
426
|
+
const cur = await this.readCurrent();
|
|
427
|
+
if (!cur) return;
|
|
428
|
+
await this.writeCurrent({
|
|
429
|
+
...cur,
|
|
430
|
+
status: 'failed',
|
|
431
|
+
lastError: error,
|
|
432
|
+
...(artifacts ? { artifacts: [...new Set(artifacts)] } : {}),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
579
435
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
436
|
+
/**
|
|
437
|
+
* 标记完成(随后 archive() 把目录改名成 `.done`)。
|
|
438
|
+
*
|
|
439
|
+
* @param artifacts 本次运行**最终**的声明产物集合。必须传 —— `meta.artifacts`
|
|
440
|
+
* 只是静态种子,循环体里经 `declareArtifact()` 登记的逐迭代产物只活在内存里。
|
|
441
|
+
* 不在这里落盘的话,下次 completed 判定会报「本次运行没有声明产物」,
|
|
442
|
+
* new-task 清理也会漏掉它们 —— 而且**表现为静默漏档而不是报错**。
|
|
443
|
+
* (这条是端到端实跑抓到的:上一轮明明 declareArtifact 了,重跑却报 0 产物。)
|
|
444
|
+
*/
|
|
445
|
+
async markDone(artifacts?: string[]): Promise<void> {
|
|
446
|
+
const cur = await this.readCurrent();
|
|
447
|
+
if (!cur) return;
|
|
448
|
+
if (!existsSync(join(this.dir, 'return.json')))
|
|
449
|
+
await this.writeResult(null);
|
|
450
|
+
const owned = artifacts ?? cur.artifacts ?? [];
|
|
451
|
+
const records = await this.treeRecords();
|
|
452
|
+
const treeArtifacts = [
|
|
453
|
+
...owned,
|
|
454
|
+
...records
|
|
455
|
+
.filter((r) => r.dir !== this.dir)
|
|
456
|
+
.flatMap((r) => r.state.artifacts ?? []),
|
|
457
|
+
];
|
|
458
|
+
await this.writeCurrent({
|
|
459
|
+
...cur,
|
|
460
|
+
status: 'done',
|
|
461
|
+
...(artifacts ? { artifacts: [...new Set(artifacts)] } : {}),
|
|
462
|
+
completion: {
|
|
463
|
+
artifacts: await this.existingArtifacts(treeArtifacts),
|
|
464
|
+
stateFiles: [
|
|
465
|
+
'return.json',
|
|
466
|
+
...records
|
|
467
|
+
.filter((r) => r.dir !== this.dir)
|
|
468
|
+
.map((r) => relative(this.dir, join(r.dir, 'current.json'))),
|
|
469
|
+
],
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
}
|
|
584
473
|
|
|
585
|
-
|
|
586
|
-
|
|
474
|
+
async writeResult(value: unknown): Promise<unknown> {
|
|
475
|
+
const json = JSON.stringify(value ?? null, (_key, v) => {
|
|
476
|
+
if (
|
|
477
|
+
typeof v === 'function' ||
|
|
478
|
+
typeof v === 'symbol' ||
|
|
479
|
+
typeof v === 'bigint' ||
|
|
480
|
+
(typeof v === 'number' && !Number.isFinite(v))
|
|
481
|
+
)
|
|
482
|
+
throw new Error('工作流返回值必须为JSON数据');
|
|
483
|
+
return v;
|
|
484
|
+
});
|
|
485
|
+
await writeFile(join(this.dir, 'return.json'), json, 'utf8');
|
|
486
|
+
return JSON.parse(json);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async readResult(): Promise<unknown> {
|
|
490
|
+
try {
|
|
491
|
+
return JSON.parse(await readFile(join(this.dir, 'return.json'), 'utf8'));
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (!(await this.readCurrent())?.completion) return null; // pre-composition state
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Children live inside the parent's state directory. Moving the parent also
|
|
499
|
+
* archives all child journals, never another standalone run of the same name. */
|
|
500
|
+
private async treeRecords(): Promise<{ dir: string; state: CurrentState }[]> {
|
|
501
|
+
const records: { dir: string; state: CurrentState }[] = [];
|
|
502
|
+
const own = await this.readCurrent();
|
|
503
|
+
if (own) records.push({ dir: this.dir, state: own });
|
|
504
|
+
const visit = async (dir: string, depth: number): Promise<void> => {
|
|
505
|
+
if (depth > 24) throw new Error('子工作流状态目录嵌套异常');
|
|
506
|
+
let entries: Dirent[];
|
|
587
507
|
try {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
508
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
for (const e of entries) {
|
|
514
|
+
if (!e.isDirectory() || e.name.endsWith(TRASH_SUFFIX)) continue;
|
|
515
|
+
const childDir = join(dir, e.name);
|
|
516
|
+
const current = await this.readCurrentAt(childDir);
|
|
517
|
+
if (current) {
|
|
518
|
+
records.push({ dir: childDir, state: current });
|
|
519
|
+
await visit(join(childDir, 'children'), depth + 1);
|
|
520
|
+
} else await visit(childDir, depth + 1);
|
|
595
521
|
}
|
|
522
|
+
};
|
|
523
|
+
await visit(join(this.dir, 'children'), 0);
|
|
524
|
+
return records;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async allArtifacts(): Promise<string[]> {
|
|
528
|
+
return this.existingArtifacts(
|
|
529
|
+
(await this.treeRecords()).flatMap((r) => r.state.artifacts ?? []),
|
|
530
|
+
);
|
|
531
|
+
}
|
|
596
532
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
533
|
+
private async treeIntact(): Promise<boolean> {
|
|
534
|
+
for (const { dir, state } of await this.treeRecords()) {
|
|
535
|
+
if (!state.completion) continue;
|
|
536
|
+
if (
|
|
537
|
+
state.completion.artifacts.some((p) => {
|
|
538
|
+
const safe = safeArtifactPath(this.workingDir, p);
|
|
539
|
+
return !safe || !existsSync(join(this.workingDir, safe));
|
|
540
|
+
})
|
|
541
|
+
)
|
|
542
|
+
return false;
|
|
543
|
+
if (
|
|
544
|
+
state.completion.stateFiles.some((p) => {
|
|
545
|
+
const safe = safeArtifactPath(dir, p);
|
|
546
|
+
return !safe || !existsSync(join(dir, safe));
|
|
547
|
+
})
|
|
548
|
+
)
|
|
549
|
+
return false;
|
|
550
|
+
try {
|
|
551
|
+
JSON.parse(await readFile(join(dir, 'return.json'), 'utf8'));
|
|
552
|
+
} catch {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
606
555
|
}
|
|
607
|
-
return
|
|
556
|
+
return true;
|
|
608
557
|
}
|
|
609
558
|
|
|
610
|
-
// ----
|
|
559
|
+
// ---- 补充要求 ----
|
|
611
560
|
|
|
612
|
-
/**
|
|
561
|
+
/**
|
|
562
|
+
* 追加一条用户补充要求。去重:已有条目的前缀重复不再追加。
|
|
563
|
+
*
|
|
564
|
+
* 持久化的理由:补充要求要与原始目标**同等贯穿始终**,跨进程 resume 时不能丢。
|
|
565
|
+
*/
|
|
613
566
|
async appendSupplement(text: string): Promise<void> {
|
|
567
|
+
const t = text.trim();
|
|
568
|
+
if (!t) return;
|
|
614
569
|
const list = await this.readSupplements();
|
|
615
|
-
list.
|
|
570
|
+
if (list.some((s) => s.startsWith(t.slice(0, 240)))) return;
|
|
571
|
+
list.push(t);
|
|
572
|
+
await mkdir(this.dir, { recursive: true });
|
|
616
573
|
await writeFile(
|
|
617
|
-
|
|
574
|
+
this.supplementsPath,
|
|
618
575
|
JSON.stringify(list, null, 2),
|
|
619
|
-
'
|
|
620
|
-
)
|
|
576
|
+
'utf8',
|
|
577
|
+
);
|
|
621
578
|
}
|
|
622
579
|
|
|
623
|
-
/** 读取已累积的补充要求列表(无文件 / 解析失败返回 [])。 */
|
|
624
580
|
async readSupplements(): Promise<string[]> {
|
|
625
581
|
try {
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === 'string') : [];
|
|
582
|
+
const parsed = JSON.parse(await readFile(this.supplementsPath, 'utf8'));
|
|
583
|
+
return Array.isArray(parsed) ? parsed.map((s) => String(s)) : [];
|
|
629
584
|
} catch {
|
|
630
585
|
return [];
|
|
631
586
|
}
|
|
632
587
|
}
|
|
633
588
|
|
|
634
|
-
// ----
|
|
635
|
-
|
|
636
|
-
/** 记录单个步骤成功完成(含 capture 变量和 vars 快照) */
|
|
637
|
-
async recordCheckpoint(
|
|
638
|
-
stepId: string,
|
|
639
|
-
output: string,
|
|
640
|
-
captures: Record<string, unknown>,
|
|
641
|
-
varsSnapshot: Record<string, unknown>,
|
|
642
|
-
): Promise<void> {
|
|
643
|
-
const cpDir = join(this.dir, CHECKPOINTS_DIR);
|
|
644
|
-
await mkdir(cpDir, { recursive: true }).catch(() => {});
|
|
645
|
-
|
|
646
|
-
const data: CheckpointData = {
|
|
647
|
-
stepId,
|
|
648
|
-
completedAt: new Date().toISOString(),
|
|
649
|
-
output,
|
|
650
|
-
captures,
|
|
651
|
-
varsSnapshot,
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
await writeFile(join(cpDir, `${stepId}.json`), JSON.stringify(data, null, 2), 'utf-8');
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/** 记录步骤失败:更新 current.json 的 lastFailedStep 和 lastError */
|
|
658
|
-
async recordFailure(stepId: string, error: string): Promise<void> {
|
|
659
|
-
const current = await this.readCurrentState();
|
|
660
|
-
if (current) {
|
|
661
|
-
current.lastFailedStep = stepId;
|
|
662
|
-
current.lastError = error;
|
|
663
|
-
current.status = 'failed';
|
|
664
|
-
await this.writeCurrent(current);
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/** 标记整个工作流完成 */
|
|
669
|
-
async markDone(): Promise<void> {
|
|
670
|
-
const current = await this.readCurrentState();
|
|
671
|
-
if (current) {
|
|
672
|
-
current.status = 'done';
|
|
673
|
-
await this.writeCurrent(current);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// ---- 公共方法:进度与变量(保持原有签名兼容) ----
|
|
589
|
+
// ---- 人类可读进度 ----
|
|
678
590
|
|
|
679
|
-
/**
|
|
591
|
+
/**
|
|
592
|
+
* 追加一行进度。
|
|
593
|
+
*
|
|
594
|
+
* 存在理由:TUI 的 scrollback 会被刷掉,而排障常常发生在「跑完之后」。
|
|
595
|
+
* 落盘的 progress.md 是唯一还能回看的现场(provider 的真实报错也写在这里)。
|
|
596
|
+
*/
|
|
680
597
|
async appendProgress(line: string): Promise<void> {
|
|
681
|
-
|
|
682
|
-
|
|
598
|
+
this.progressWrites = this.progressWrites.then(async () => {
|
|
599
|
+
try {
|
|
600
|
+
await mkdir(this.dir, { recursive: true });
|
|
601
|
+
await appendFile(this.progressPath, `${line}\n`, 'utf8');
|
|
602
|
+
} catch {
|
|
603
|
+
// 进度日志写失败不该影响任务本身
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
await this.progressWrites;
|
|
683
607
|
}
|
|
684
608
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
609
|
+
async appendEvent(event: unknown): Promise<void> {
|
|
610
|
+
this.eventWrites = this.eventWrites.then(async () => {
|
|
611
|
+
try {
|
|
612
|
+
await mkdir(this.dir, { recursive: true });
|
|
613
|
+
await appendFile(this.eventsPath, `${JSON.stringify(event)}\n`, 'utf8');
|
|
614
|
+
} catch {
|
|
615
|
+
// Structured diagnostics must never fail the workflow itself.
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
await this.eventWrites;
|
|
688
619
|
}
|
|
689
620
|
|
|
690
621
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
622
|
+
* 等待所有已排队的 progress/event 写入落盘(不做归档,无其它副作用)。
|
|
623
|
+
*
|
|
624
|
+
* `journaled()`(`runtime/context.ts`)里的 appendProgress 调用是 fire-and-forget
|
|
625
|
+
* ——生产路径的落盘保证来自 `archive()` 在 `runner.ts` 终止路径上对同一条
|
|
626
|
+
* `progressWrites`/`eventWrites` 链的 await。**绕开 `runner.ts` 直接调用能力对象
|
|
627
|
+
* 的测试**(例如只起 `createWorkflowContext()` 就调 `wf.model.generate()`)没有
|
|
628
|
+
* 这条终态保证,读 `progress.md`/`events.jsonl` 前必须显式调用这个方法。
|
|
693
629
|
*/
|
|
694
|
-
async
|
|
695
|
-
|
|
696
|
-
const validIds = new Set(steps.map((s) => s.id));
|
|
697
|
-
try {
|
|
698
|
-
const files = await readdir(cpDir);
|
|
699
|
-
const ids: string[] = [];
|
|
700
|
-
for (const f of files) {
|
|
701
|
-
if (!f.endsWith('.json')) continue;
|
|
702
|
-
const stepId = f.replace(/\.json$/, '');
|
|
703
|
-
if (validIds.has(stepId)) ids.push(stepId);
|
|
704
|
-
}
|
|
705
|
-
// 按 steps 数组中的原始顺序排列(非文件系统顺序)
|
|
706
|
-
return ids.sort((a, b) => {
|
|
707
|
-
const ai = steps.findIndex((s) => s.id === a);
|
|
708
|
-
const bi = steps.findIndex((s) => s.id === b);
|
|
709
|
-
return ai - bi;
|
|
710
|
-
});
|
|
711
|
-
} catch {
|
|
712
|
-
return [];
|
|
713
|
-
}
|
|
630
|
+
async flush(): Promise<void> {
|
|
631
|
+
await Promise.all([this.progressWrites, this.eventWrites]);
|
|
714
632
|
}
|
|
715
633
|
|
|
716
|
-
|
|
717
|
-
|
|
634
|
+
// ---- 归档与清理 ----
|
|
635
|
+
|
|
636
|
+
/** 成功收尾:`<taskId>/` → `<taskId>.done/`。 */
|
|
637
|
+
async archive(): Promise<void> {
|
|
638
|
+
await this.flush();
|
|
639
|
+
if (!this.dir || !existsSync(this.dir)) return;
|
|
640
|
+
const dest = this.dir.endsWith(DONE_SUFFIX)
|
|
641
|
+
? this.dir
|
|
642
|
+
: this.dir + DONE_SUFFIX;
|
|
643
|
+
if (dest === this.dir) return;
|
|
718
644
|
try {
|
|
719
|
-
|
|
720
|
-
|
|
645
|
+
await rm(dest, { recursive: true, force: true });
|
|
646
|
+
await rename(this.dir, dest);
|
|
647
|
+
this.dir = dest;
|
|
721
648
|
} catch {
|
|
722
|
-
|
|
649
|
+
// 归档失败不影响任务结论,下次 init 会按未完成处理
|
|
723
650
|
}
|
|
724
651
|
}
|
|
725
652
|
|
|
726
|
-
/**
|
|
727
|
-
async
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const cp: CheckpointData = JSON.parse(raw);
|
|
734
|
-
if (cp.captures && Object.keys(cp.captures).length > 0) {
|
|
735
|
-
result[sid] = cp.captures;
|
|
736
|
-
}
|
|
737
|
-
} catch {
|
|
738
|
-
// 单个 checkpoint 读失败不阻断整体
|
|
739
|
-
}
|
|
653
|
+
/** 删掉本实例的状态目录(不碰 `.trash/`,那是用户的找回通道)。 */
|
|
654
|
+
async cleanup(): Promise<void> {
|
|
655
|
+
if (!this.dir) return;
|
|
656
|
+
try {
|
|
657
|
+
await rm(this.dir, { recursive: true, force: true });
|
|
658
|
+
} catch {
|
|
659
|
+
/* 忽略 */
|
|
740
660
|
}
|
|
741
|
-
return result;
|
|
742
661
|
}
|
|
743
662
|
|
|
744
|
-
// ----
|
|
663
|
+
// ---- 声明产物的生命周期 ----
|
|
745
664
|
|
|
746
665
|
/**
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
* 该声明被丢弃(与旧行为一致:不存在的产物不参与清理/报告)。
|
|
666
|
+
* 把声明里的 glob(`research/04-*.md`)展开成 cwd 下实际匹配的相对路径;
|
|
667
|
+
* 非 glob 原样保留。越界路径被过滤(fail-closed)。
|
|
668
|
+
* 声明里写了 glob 却一个都不匹配 → 该声明被丢弃(不存在的产物不参与清理/报告)。
|
|
751
669
|
*/
|
|
752
670
|
private async expandDeclaredArtifacts(paths: string[]): Promise<string[]> {
|
|
753
671
|
const out: string[] = [];
|
|
@@ -755,18 +673,23 @@ export class WorkflowState {
|
|
|
755
673
|
const safe = safeArtifactPath(this.workingDir, p);
|
|
756
674
|
if (safe === null) continue;
|
|
757
675
|
if (!/[*?]/.test(safe)) {
|
|
758
|
-
|
|
676
|
+
// ⚠️ 必须归一成 `/`:`safeArtifactPath` 返回的是**平台分隔符**,而下面
|
|
677
|
+
// glob 分支里 walkGlob 拼出来的一律是 `/`。两个分支不统一的话,同一份
|
|
678
|
+
// 声明列表在 Windows 上会同时产出 `out\a.md` 与 `out/b.md` —— 去重
|
|
679
|
+
// (new Set)失效、与 current.json 里存的旧值比不上、报给用户的路径
|
|
680
|
+
// 时而斜杠时而反斜杠。`join()` 吃 `/` 没问题,所以统一到 `/` 是安全的。
|
|
681
|
+
out.push(safe.replace(/\\/g, '/'));
|
|
759
682
|
continue;
|
|
760
683
|
}
|
|
761
|
-
const
|
|
762
|
-
const
|
|
763
|
-
|
|
684
|
+
const segs = safe.replace(/\\/g, '/').split('/');
|
|
685
|
+
const fullRe = new RegExp(
|
|
686
|
+
`^${segs.map(artifactSegToPattern).join('/')}$`,
|
|
687
|
+
);
|
|
764
688
|
out.push(...(await this.walkGlob(this.workingDir, segs, fullRe, '')));
|
|
765
689
|
}
|
|
766
690
|
return out;
|
|
767
691
|
}
|
|
768
692
|
|
|
769
|
-
/** 逐段匹配目录树收集文件;dir 读失败(不存在/无权限)返回空。 */
|
|
770
693
|
private async walkGlob(
|
|
771
694
|
absDir: string,
|
|
772
695
|
segs: string[],
|
|
@@ -789,19 +712,25 @@ export class WorkflowState {
|
|
|
789
712
|
if (rest.length === 0) {
|
|
790
713
|
if (e.isFile() && fullRe.test(rel)) results.push(rel);
|
|
791
714
|
} else if (e.isDirectory()) {
|
|
792
|
-
results.push(
|
|
715
|
+
results.push(
|
|
716
|
+
...(await this.walkGlob(join(absDir, e.name), rest, fullRe, rel)),
|
|
717
|
+
);
|
|
793
718
|
}
|
|
794
719
|
}
|
|
795
720
|
return results;
|
|
796
721
|
}
|
|
797
722
|
|
|
723
|
+
/** 声明产物里**实际存在**的那些(completed 报告用;越界的视为不存在)。 */
|
|
724
|
+
private async existingArtifacts(paths: string[]): Promise<string[]> {
|
|
725
|
+
const expanded = await this.expandDeclaredArtifacts(paths);
|
|
726
|
+
return [...new Set(expanded)].filter((rel) =>
|
|
727
|
+
existsSync(join(this.workingDir, rel)),
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
|
|
798
731
|
/**
|
|
799
|
-
*
|
|
800
|
-
*
|
|
801
|
-
* 一个都没归档成功时返回 undefined(此时后续 rm 兜底直接删,不能谎报归档位置--
|
|
802
|
-
* 用户会按提示去回收站找文件)。单个 move 失败不阻断(残留交给 rm 兜底)。
|
|
803
|
-
* 回收目录是状态目录的 sibling:cleanSlate/cleanup 只重建 `<name>/`,
|
|
804
|
-
* `.trash/` 跨任务留存,直到用户手动清理(如保留最近 N 次的淘汰策略,暂未实现)。
|
|
732
|
+
* 把实际存在的声明产物移入 `<实例目录>.trash/<stamp>/`,保留相对路径结构。
|
|
733
|
+
* 一个都没成功移走时返回 undefined —— **绝不谎报归档位置**(见文件头注)。
|
|
805
734
|
*/
|
|
806
735
|
private async archiveDeclaredArtifacts(
|
|
807
736
|
existingRels: string[],
|
|
@@ -817,7 +746,7 @@ export class WorkflowState {
|
|
|
817
746
|
await rename(join(this.workingDir, rel), dest);
|
|
818
747
|
archived++;
|
|
819
748
|
} catch {
|
|
820
|
-
//
|
|
749
|
+
// 单个失败不阻断,残留交给后续 rm 兜底
|
|
821
750
|
}
|
|
822
751
|
}
|
|
823
752
|
if (archived === 0) return undefined;
|
|
@@ -825,10 +754,7 @@ export class WorkflowState {
|
|
|
825
754
|
return display.startsWith('..') ? trashRoot : display;
|
|
826
755
|
}
|
|
827
756
|
|
|
828
|
-
/**
|
|
829
|
-
* 仅删工作流自己声明的产物(glob 声明先展开为实际文件)、仅 force-rm 文件,
|
|
830
|
-
* **并强制路径在 cwd 内(防 ../ 跳出删用户家目录)**。
|
|
831
|
-
* 返回真正执行清理的路径列表(展开后的实际文件相对路径;越界路径被过滤掉,fail-closed)。 */
|
|
757
|
+
/** rm 兜底:只删工作流自己声明的产物,且路径强制在 cwd 内。 */
|
|
832
758
|
private async cleanupDeclaredArtifacts(paths: string[]): Promise<string[]> {
|
|
833
759
|
const done: string[] = [];
|
|
834
760
|
for (const rel of await this.expandDeclaredArtifacts(paths)) {
|
|
@@ -838,341 +764,20 @@ export class WorkflowState {
|
|
|
838
764
|
await rm(abs, { force: true });
|
|
839
765
|
done.push(rel);
|
|
840
766
|
} catch {
|
|
841
|
-
|
|
767
|
+
/* 单个失败不影响整体 */
|
|
842
768
|
}
|
|
843
769
|
}
|
|
844
770
|
return done;
|
|
845
771
|
}
|
|
846
772
|
|
|
847
|
-
/**
|
|
848
|
-
* (completed 报真实产物用;glob 声明先展开;越界声明不报告,视为不存在)。 */
|
|
849
|
-
private async existingArtifactsAsync(paths: string[]): Promise<string[]> {
|
|
850
|
-
const expanded = await this.expandDeclaredArtifacts(paths);
|
|
851
|
-
return expanded.filter((rel) => existsSync(join(this.workingDir, rel)));
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* 「先归档再清理」组合:把【实际存在】的声明产物移入 .trash 回收目录,
|
|
856
|
-
* 归档后残留(rename 失败者)由 cleanup 兜底 rm。返回清理列表与回收目录。
|
|
857
|
-
* fresh/new-task 启动清理统一走这里--直接 rm 旧产物曾在实战中造成不可找回的损失
|
|
858
|
-
* (LLM 仲裁误判 new-task,同主题调研的全部报告被静默删除)。
|
|
859
|
-
*/
|
|
773
|
+
/** 「先归档再清理」组合。fresh/new-task 的启动清理统一走这里。 */
|
|
860
774
|
private async archiveThenClean(
|
|
861
775
|
paths: string[],
|
|
862
776
|
stamp: string,
|
|
863
777
|
): Promise<{ cleaned: string[]; archivedTo?: string }> {
|
|
864
|
-
const existing = await this.
|
|
778
|
+
const existing = await this.existingArtifacts(paths);
|
|
865
779
|
const archivedTo = await this.archiveDeclaredArtifacts(existing, stamp);
|
|
866
780
|
const cleaned = await this.cleanupDeclaredArtifacts(paths);
|
|
867
781
|
return { cleaned, archivedTo };
|
|
868
782
|
}
|
|
869
|
-
|
|
870
|
-
// ---- 公共方法:归档与清理(保持原有签名兼容) ----
|
|
871
|
-
|
|
872
|
-
/**
|
|
873
|
-
* 确定当前任务的实例目录路径。
|
|
874
|
-
* Phase 3:实例目录 = <wfDir>/<sessionId>/(session 即任务实例)。
|
|
875
|
-
* isSameTask=true 时若旧布局(wfDir 本身)存在则沿用(兼容升级前状态)。
|
|
876
|
-
*/
|
|
877
|
-
private resolveTaskDir(_taskId: string, isSameTask: boolean): string {
|
|
878
|
-
if (isSameTask) {
|
|
879
|
-
const existing = this.findExistingTaskDir(_taskId);
|
|
880
|
-
if (existing) return existing;
|
|
881
|
-
}
|
|
882
|
-
return join(this.wfDir, this.sessionId);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
/**
|
|
886
|
-
* 找当前 session 对应的现存活动实例目录(含旧布局兼容)。
|
|
887
|
-
* 找不到返回 null。
|
|
888
|
-
*/
|
|
889
|
-
private findExistingTaskDir(_taskId: string): string | null {
|
|
890
|
-
// Phase 3:当前 session 的实例目录
|
|
891
|
-
const sessionDir = join(this.wfDir, this.sessionId);
|
|
892
|
-
if (existsSync(sessionDir)) return sessionDir;
|
|
893
|
-
|
|
894
|
-
// 旧布局兼容:wfDir 本身就是实例目录
|
|
895
|
-
if (existsSync(join(this.wfDir, 'current.json'))) {
|
|
896
|
-
try {
|
|
897
|
-
const entries = readdirSync(this.wfDir, { withFileTypes: true });
|
|
898
|
-
const hasInstanceSubdir = entries.some(
|
|
899
|
-
(e) => e.isDirectory() && /^[0-9a-f]{16}$/.test(e.name),
|
|
900
|
-
);
|
|
901
|
-
if (!hasInstanceSubdir) return this.wfDir;
|
|
902
|
-
} catch {
|
|
903
|
-
return this.wfDir;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
return null;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
/**
|
|
910
|
-
* 扫描当前 session 是否有任何任务实例(活动态或 .done 归档态,不包括 .trash)。
|
|
911
|
-
* 用于 isSameTask=false 时区分 fresh(从未跑过)和 new-task(有其他旧任务)。
|
|
912
|
-
*/
|
|
913
|
-
private hasAnyTaskInstance(): boolean {
|
|
914
|
-
if (!existsSync(this.wfDir)) return false;
|
|
915
|
-
// Phase 3:当前 session 的实例(活动 / .done)
|
|
916
|
-
if (existsSync(join(this.wfDir, this.sessionId))) return true;
|
|
917
|
-
if (existsSync(join(this.wfDir, `${this.sessionId}${DONE_SUFFIX}`))) return true;
|
|
918
|
-
// 旧布局:wfDir 本身就是实例(current.json 直接在 wfDir 下)
|
|
919
|
-
if (existsSync(join(this.wfDir, 'current.json'))) return true;
|
|
920
|
-
if (existsSync(join(this.wfDir + DONE_SUFFIX, 'current.json'))) return true;
|
|
921
|
-
return false;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* 扫描 wfDir 下所有 .done 归档,返回第一个(也是最新的,按修改时间排序)的 current.json。
|
|
926
|
-
* 用于 completed 检测的兜底——当 hash 不匹配但 wfDir 下有归档时,判断是否是同名 workflow 的旧归档。
|
|
927
|
-
*/
|
|
928
|
-
private findLatestArchivedState(): Promise<CurrentState | null> {
|
|
929
|
-
return this.findArchivedState(() => true);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
/**
|
|
933
|
-
* 在 wfDir 下找当前 session 对应的归档目录(.done)里的 current.json。
|
|
934
|
-
* 支持新布局(wfDir/<sessionId>.done/)和旧布局(wfDir.done/)。
|
|
935
|
-
*/
|
|
936
|
-
private findArchivedStateByTaskId(_taskId: string): Promise<CurrentState | null> {
|
|
937
|
-
return this.findArchivedState(
|
|
938
|
-
(name, isLegacy) => isLegacy || name === `${this.sessionId}${DONE_SUFFIX}`,
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
private async findArchivedState(
|
|
943
|
-
predicate: (dirName: string, isLegacy: boolean) => boolean,
|
|
944
|
-
): Promise<CurrentState | null> {
|
|
945
|
-
// 旧布局:wfDir.done/current.json
|
|
946
|
-
const legacyDone = this.wfDir + DONE_SUFFIX;
|
|
947
|
-
if (existsSync(join(legacyDone, 'current.json')) && predicate('', true)) {
|
|
948
|
-
try {
|
|
949
|
-
const raw = await readFile(join(legacyDone, 'current.json'), 'utf-8');
|
|
950
|
-
return JSON.parse(raw) as CurrentState;
|
|
951
|
-
} catch {
|
|
952
|
-
// fall through
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// 新布局:wfDir/<taskId>.done/current.json
|
|
957
|
-
if (!existsSync(this.wfDir)) return null;
|
|
958
|
-
try {
|
|
959
|
-
const entries = readdirSync(this.wfDir, { withFileTypes: true });
|
|
960
|
-
const doneDirs = entries
|
|
961
|
-
.filter((e) => e.isDirectory() && e.name.endsWith(DONE_SUFFIX))
|
|
962
|
-
.map((e) => e.name)
|
|
963
|
-
.filter((name) => predicate(name, false))
|
|
964
|
-
.sort();
|
|
965
|
-
for (const name of doneDirs) {
|
|
966
|
-
try {
|
|
967
|
-
const raw = await readFile(join(this.wfDir, name, 'current.json'), 'utf-8');
|
|
968
|
-
return JSON.parse(raw) as CurrentState;
|
|
969
|
-
} catch {
|
|
970
|
-
continue;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
} catch {
|
|
974
|
-
// ignore
|
|
975
|
-
}
|
|
976
|
-
return null;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
/**
|
|
980
|
-
* 收集【当前 session】的任务实例(活动态 + .done 归档态)的声明产物路径,
|
|
981
|
-
* 用于 new-task 时归档+清理旧任务的产物。去重后返回。
|
|
982
|
-
* Phase 3:只收集当前 session 的实例产物——不同 session 的产物各归各,
|
|
983
|
-
* new-task 绝不殃及其他 session 的产出。
|
|
984
|
-
*/
|
|
985
|
-
private async collectAllInstanceArtifacts(
|
|
986
|
-
workflowName: string,
|
|
987
|
-
fallback: string[],
|
|
988
|
-
): Promise<string[]> {
|
|
989
|
-
const seen = new Set<string>();
|
|
990
|
-
const allArtifacts: string[] = [];
|
|
991
|
-
const pushAll = (arr: string[] | undefined) => {
|
|
992
|
-
for (const a of arr ?? []) {
|
|
993
|
-
if (!seen.has(a)) {
|
|
994
|
-
seen.add(a);
|
|
995
|
-
allArtifacts.push(a);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
};
|
|
999
|
-
|
|
1000
|
-
// Phase 3:当前 session 的活动实例与 .done 归档
|
|
1001
|
-
for (const dirName of [this.sessionId, `${this.sessionId}${DONE_SUFFIX}`]) {
|
|
1002
|
-
const dir = join(this.wfDir, dirName);
|
|
1003
|
-
if (!existsSync(dir)) continue;
|
|
1004
|
-
try {
|
|
1005
|
-
const raw = await readFile(join(dir, 'current.json'), 'utf-8');
|
|
1006
|
-
const s = JSON.parse(raw) as CurrentState;
|
|
1007
|
-
if (s.name === workflowName) pushAll(s.artifacts);
|
|
1008
|
-
} catch {
|
|
1009
|
-
// ignore
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// 旧布局兼容:wfDir 本身有 current.json(且无子实例目录)或 wfDir.done/
|
|
1014
|
-
if (!existsSync(this.wfDir)) {
|
|
1015
|
-
const legacyDone = this.wfDir + DONE_SUFFIX;
|
|
1016
|
-
try {
|
|
1017
|
-
const raw = await readFile(join(legacyDone, 'current.json'), 'utf-8');
|
|
1018
|
-
const s = JSON.parse(raw) as CurrentState;
|
|
1019
|
-
if (s.name === workflowName) pushAll(s.artifacts);
|
|
1020
|
-
} catch {
|
|
1021
|
-
// ignore
|
|
1022
|
-
}
|
|
1023
|
-
} else {
|
|
1024
|
-
try {
|
|
1025
|
-
const entries = readdirSync(this.wfDir, { withFileTypes: true });
|
|
1026
|
-
const hasInstanceSubdir = entries.some(
|
|
1027
|
-
(e) => e.isDirectory() && /^[0-9a-f]{16}/.test(e.name),
|
|
1028
|
-
);
|
|
1029
|
-
if (!hasInstanceSubdir && existsSync(join(this.wfDir, 'current.json'))) {
|
|
1030
|
-
try {
|
|
1031
|
-
const raw = await readFile(join(this.wfDir, 'current.json'), 'utf-8');
|
|
1032
|
-
const s = JSON.parse(raw) as CurrentState;
|
|
1033
|
-
if (s.name === workflowName) pushAll(s.artifacts);
|
|
1034
|
-
} catch {
|
|
1035
|
-
// ignore
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
} catch {
|
|
1039
|
-
// ignore
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
return allArtifacts.length > 0 ? allArtifacts : fallback;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
/**
|
|
1047
|
-
* 找当前 session 的活动态实例(未完成),返回其目录 + current.json。
|
|
1048
|
-
* 用于 isSameTask=false 时的仲裁——拿到旧任务的 state 做二审。
|
|
1049
|
-
* 找不到返回 null。
|
|
1050
|
-
*/
|
|
1051
|
-
private async findLatestActiveState(): Promise<{
|
|
1052
|
-
dir: string;
|
|
1053
|
-
state: CurrentState;
|
|
1054
|
-
} | null> {
|
|
1055
|
-
// Phase 3:当前 session 的活动实例 <wfDir>/<sessionId>/
|
|
1056
|
-
const sessionDir = join(this.wfDir, this.sessionId);
|
|
1057
|
-
if (existsSync(sessionDir)) {
|
|
1058
|
-
try {
|
|
1059
|
-
const raw = await readFile(join(sessionDir, 'current.json'), 'utf-8');
|
|
1060
|
-
const state = JSON.parse(raw) as CurrentState;
|
|
1061
|
-
if (state.status !== 'done') {
|
|
1062
|
-
return { dir: sessionDir, state };
|
|
1063
|
-
}
|
|
1064
|
-
} catch {
|
|
1065
|
-
// fall through
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
// 旧布局兼容:wfDir 本身是实例目录(current.json 直接在其下)
|
|
1070
|
-
if (existsSync(join(this.wfDir, 'current.json'))) {
|
|
1071
|
-
try {
|
|
1072
|
-
const entries = readdirSync(this.wfDir, { withFileTypes: true });
|
|
1073
|
-
const hasInstanceSubdir = entries.some(
|
|
1074
|
-
(e) => e.isDirectory() && /^[0-9a-f]{16}/.test(e.name),
|
|
1075
|
-
);
|
|
1076
|
-
if (!hasInstanceSubdir) {
|
|
1077
|
-
const raw = await readFile(join(this.wfDir, 'current.json'), 'utf-8');
|
|
1078
|
-
const state = JSON.parse(raw) as CurrentState;
|
|
1079
|
-
if (state.status !== 'done') {
|
|
1080
|
-
return { dir: this.wfDir, state };
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
} catch {
|
|
1084
|
-
// fall through
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
return null;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
/**
|
|
1092
|
-
* 归档(成功完成时调用):重命名状态目录为 .done 后缀,保留所有中间产物。
|
|
1093
|
-
* vars.json / progress.md / inputs.json / checkpoints/ 全部保留——供后续对话 Read 回溯。
|
|
1094
|
-
*/
|
|
1095
|
-
async archive(): Promise<void> {
|
|
1096
|
-
const doneDir = this.dir + DONE_SUFFIX;
|
|
1097
|
-
try {
|
|
1098
|
-
await rm(doneDir, { recursive: true, force: true });
|
|
1099
|
-
} catch {
|
|
1100
|
-
// ignore
|
|
1101
|
-
}
|
|
1102
|
-
try {
|
|
1103
|
-
await rename(this.dir, doneDir);
|
|
1104
|
-
} catch {
|
|
1105
|
-
// rename 失败时静默(某些文件系统/权限问题),保留原目录
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
/** 强制清理(/new 或新任务 init 时调用):删除当前任务实例目录(含 .done 归档)。
|
|
1110
|
-
* 注意:.trash 回收目录不在这里删——它是产物清理前的归档安全网,
|
|
1111
|
-
* 跨任务保留,由用户或后续淘汰策略管理。 */
|
|
1112
|
-
async cleanup(): Promise<void> {
|
|
1113
|
-
await rm(this.dir, { recursive: true, force: true }).catch(() => {});
|
|
1114
|
-
await rm(this.dir + DONE_SUFFIX, { recursive: true, force: true }).catch(() => {});
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// ---- 私有方法 ----
|
|
1118
|
-
|
|
1119
|
-
/** 创建全新的空白状态目录(含 checkpoints/ 子目录),并记录声明产物清单。 */
|
|
1120
|
-
private async cleanSlate(
|
|
1121
|
-
name: string,
|
|
1122
|
-
inputs: Record<string, unknown>,
|
|
1123
|
-
taskId: string,
|
|
1124
|
-
declaredArtifacts: string[] = [],
|
|
1125
|
-
defFingerprint?: string,
|
|
1126
|
-
): Promise<void> {
|
|
1127
|
-
await this.cleanup();
|
|
1128
|
-
await mkdir(this.dir, { recursive: true });
|
|
1129
|
-
await mkdir(join(this.dir, CHECKPOINTS_DIR), { recursive: true });
|
|
1130
|
-
|
|
1131
|
-
await this.writeCurrent({
|
|
1132
|
-
name,
|
|
1133
|
-
taskId,
|
|
1134
|
-
status: 'running',
|
|
1135
|
-
startedAt: new Date().toISOString(),
|
|
1136
|
-
artifacts: declaredArtifacts,
|
|
1137
|
-
defFingerprint,
|
|
1138
|
-
});
|
|
1139
|
-
|
|
1140
|
-
await writeFile(join(this.dir, 'inputs.json'), JSON.stringify(inputs, null, 2), 'utf-8');
|
|
1141
|
-
await writeFile(join(this.dir, 'progress.md'), `# ${name}\n\n`, 'utf-8');
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
/** 读取旧任务的 inputs.json(仲裁时取旧 goal 用),失败返回 null */
|
|
1145
|
-
private async readInputs(): Promise<Record<string, unknown> | null> {
|
|
1146
|
-
try {
|
|
1147
|
-
const raw = await readFile(join(this.dir, 'inputs.json'), 'utf-8');
|
|
1148
|
-
return JSON.parse(raw);
|
|
1149
|
-
} catch {
|
|
1150
|
-
return null;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
/** 读取 current.json,解析失败或文件不存在返回 null */
|
|
1155
|
-
private async readCurrentState(): Promise<CurrentState | null> {
|
|
1156
|
-
try {
|
|
1157
|
-
const raw = await readFile(join(this.dir, 'current.json'), 'utf-8');
|
|
1158
|
-
return JSON.parse(raw);
|
|
1159
|
-
} catch {
|
|
1160
|
-
return null;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
/** 读取归档目录(.done)的 current.json,用于 completed 检测(成功后 dir 被 archive 改名)。 */
|
|
1165
|
-
private async readArchivedState(): Promise<CurrentState | null> {
|
|
1166
|
-
try {
|
|
1167
|
-
const raw = await readFile(join(this.dir + DONE_SUFFIX, 'current.json'), 'utf-8');
|
|
1168
|
-
return JSON.parse(raw);
|
|
1169
|
-
} catch {
|
|
1170
|
-
return null;
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
/** 写入 current.json */
|
|
1175
|
-
private async writeCurrent(data: CurrentState): Promise<void> {
|
|
1176
|
-
await writeFile(join(this.dir, 'current.json'), JSON.stringify(data, null, 2), 'utf-8');
|
|
1177
|
-
}
|
|
1178
783
|
}
|