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
|
@@ -0,0 +1,1304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* plugins/workflow-runner/src/runtime/context.ts —— WorkflowContext V2 组装
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 对外形状由 workflowContextCapabilities.json 统一登记;本文件实现这些能力并
|
|
6
|
+
* 组装冻结的 wf 对象。工作流按受信任本地代码执行,不宣称安全沙箱。
|
|
7
|
+
*
|
|
8
|
+
* **不在这里的东西,是因为 JS 原生就有**:if / for / while / break / continue /
|
|
9
|
+
* try、JSON 与字符串处理。受控扇出统一使用 `wf.flow.pipeline()`,由运行时执行
|
|
10
|
+
* 有界调度和稳定恢复;业务分支本身仍是普通 JavaScript。
|
|
11
|
+
*
|
|
12
|
+
* ------------------------------------------------------------
|
|
13
|
+
* ⚠️ **哪些调用进账本**(journal),这条分界线是有原则的:
|
|
14
|
+
*
|
|
15
|
+
* 进账本:模型、工具、进程、询问和子工作流 —— 贵的、有副作用的、要问人的
|
|
16
|
+
* 不进账本:输出、进度、文件系统读写、确定性检查和状态读取
|
|
17
|
+
*
|
|
18
|
+
* `exists()` / `readJson()` **绝不能重放** —— 重放会返回上次运行时的文件系统事实,
|
|
19
|
+
* 而 resume 的全部意义就在于「现在磁盘上到底有什么」。重跑既便宜又更正确。
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ **`llm_error` / `max_turns` 的 raw 事件不得原样漏给顶层**。真机事故:`-p` 模式
|
|
22
|
+
* 收到任意 `type:'error'` 就 `exitCode:1` 并 `process.exit()`,把整个进程杀死在
|
|
23
|
+
* 这里 —— 比脚本自己的 try/catch 还早,等于跳过了作者写的全部降级逻辑。
|
|
24
|
+
* 这两类信号在此转成抛出的 Error,交给脚本的 try/catch 处理。
|
|
25
|
+
* ============================================================
|
|
26
|
+
*/
|
|
27
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
28
|
+
import { randomUUID } from 'node:crypto';
|
|
29
|
+
import { existsSync } from 'node:fs';
|
|
30
|
+
import { readFile } from 'node:fs/promises';
|
|
31
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
32
|
+
import { ALL_TOOLS, createAgentRuntimeContext, parseCheckString, recordRead, resolveProviderForModel, resolveProviderForWorkflowRole, runQuery, runVerification, executeTool as sdkExecuteTool, stripThinkBlocks, } from '../../../../src/plugin-sdk.js';
|
|
33
|
+
import { withGoalAnchor } from '../goalAnchor.js';
|
|
34
|
+
import { MAIN_LANE } from '../journal.js';
|
|
35
|
+
import { extractJsonPayload } from '../jsonPayload.js';
|
|
36
|
+
import { renderViolations, validateAgainstSchema, } from '../jsonSchema.js';
|
|
37
|
+
import { buildWorkflowToolContext, effectiveWorkflowCwd, } from '../toolRuntime.js';
|
|
38
|
+
import { truncate } from '../util.js';
|
|
39
|
+
import { sharedWorkflowScheduler, } from './scheduler.js';
|
|
40
|
+
import { executeWorkflowFile } from './exec.js';
|
|
41
|
+
import { atomicWriteJson, atomicWriteText, ensureWorkflowDir, } from './fs.js';
|
|
42
|
+
import { structuredWorkflowError } from './flow.js';
|
|
43
|
+
import { assertWorkflowTimeout } from './model.js';
|
|
44
|
+
import { workflowProgressEvent } from './output.js';
|
|
45
|
+
import { workflowBudget } from './state.js';
|
|
46
|
+
import { unknownToolNames } from './tools.js';
|
|
47
|
+
import { assertWorkflowContextShape, } from '../workflowContext.js';
|
|
48
|
+
/**
|
|
49
|
+
* 内核里**存在**的全部工具名 —— 用来区分「作者把名字拼错了」和
|
|
50
|
+
* 「名字对但宿主没授权」。这两种情况的处置完全不同,混在一起报会让
|
|
51
|
+
* 真正的拼写错误淹没在正常的宿主过滤里。
|
|
52
|
+
*/
|
|
53
|
+
const KNOWN_TOOL_NAMES = new Set(ALL_TOOLS.map((t) => t.name));
|
|
54
|
+
/** One shared scope for the entire workflow tree. */
|
|
55
|
+
export class ExecutionScope {
|
|
56
|
+
scheduler;
|
|
57
|
+
rootRunId;
|
|
58
|
+
calls = 0;
|
|
59
|
+
constructor(scheduler = sharedWorkflowScheduler(), rootRunId = randomUUID()) {
|
|
60
|
+
this.scheduler = scheduler;
|
|
61
|
+
this.rootRunId = rootRunId;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function createWorkflowContext(deps) {
|
|
65
|
+
const { ctx, state, journal, emit, args, session, task, workflowDir } = deps;
|
|
66
|
+
const scope = deps.scope ?? new ExecutionScope();
|
|
67
|
+
const runId = deps.runId ?? task;
|
|
68
|
+
const parentRunId = deps.parentRunId;
|
|
69
|
+
const depth = deps.depth ?? 0;
|
|
70
|
+
const eventIdentity = {
|
|
71
|
+
workflow: deps.workflowName,
|
|
72
|
+
runId,
|
|
73
|
+
rootRunId: scope.rootRunId,
|
|
74
|
+
...(parentRunId ? { parentRunId } : {}),
|
|
75
|
+
depth,
|
|
76
|
+
};
|
|
77
|
+
const emitProgressEvent = (update) => {
|
|
78
|
+
const event = workflowProgressEvent(eventIdentity, update);
|
|
79
|
+
emit(event);
|
|
80
|
+
void state.appendEvent(event).catch(() => undefined);
|
|
81
|
+
};
|
|
82
|
+
const modelScope = new AsyncLocalStorage();
|
|
83
|
+
const modelCall = async (opts, fn) => {
|
|
84
|
+
assertWorkflowTimeout(opts.timeoutMs);
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const timer = opts.timeoutMs === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: setTimeout(() => controller.abort(new Error(`LLM 调用超过工作流步骤时限 ${opts.timeoutMs}ms(含排队和结构化修复);保留断点,可重试。`)), opts.timeoutMs);
|
|
89
|
+
const signal = ctx.signal
|
|
90
|
+
? AbortSignal.any([ctx.signal, controller.signal])
|
|
91
|
+
: controller.signal;
|
|
92
|
+
const start = Date.now();
|
|
93
|
+
const heartbeat = setInterval(() => {
|
|
94
|
+
const message = `${opts.label ?? '模型步骤'}:等待/执行中,已用 ${Math.round((Date.now() - start) / 1000)} 秒${opts.timeoutMs ? ` / 上限 ${Math.round(opts.timeoutMs / 1000)} 秒` : ''}`;
|
|
95
|
+
void state.appendProgress(`ℹ ${message}`).catch(() => { });
|
|
96
|
+
emitProgressEvent({
|
|
97
|
+
phase: 'model',
|
|
98
|
+
status: 'update',
|
|
99
|
+
message,
|
|
100
|
+
elapsedMs: Date.now() - start,
|
|
101
|
+
});
|
|
102
|
+
}, 30_000);
|
|
103
|
+
let failed = false;
|
|
104
|
+
try {
|
|
105
|
+
return await modelScope.run(signal, () => scope.scheduler.run('model', scope.rootRunId, async (queuedMs) => {
|
|
106
|
+
if (queuedMs > 0) {
|
|
107
|
+
emitProgressEvent({
|
|
108
|
+
phase: 'model',
|
|
109
|
+
status: 'update',
|
|
110
|
+
message: `${opts.label ?? '模型步骤'}:排队 ${queuedMs}ms 后开始`,
|
|
111
|
+
queuedMs,
|
|
112
|
+
elapsedMs: Date.now() - start,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return fn();
|
|
116
|
+
}, signal));
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
failed = true;
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
clearInterval(heartbeat);
|
|
125
|
+
emitProgressEvent({
|
|
126
|
+
phase: 'model',
|
|
127
|
+
status: failed ? 'warning' : 'complete',
|
|
128
|
+
message: `${opts.label ?? '模型步骤'}:${failed ? '失败' : '完成'}`,
|
|
129
|
+
elapsedMs: Date.now() - start,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const countCall = () => {
|
|
134
|
+
ctx.signal?.throwIfAborted();
|
|
135
|
+
scope.calls++;
|
|
136
|
+
if (scope.calls > scope.scheduler.config.maxCalls) {
|
|
137
|
+
throw new Error(`工作流调用次数超过上限 ${scope.scheduler.config.maxCalls} —— 请检查不收敛循环,或调整 MINIMAL_AGENT_WORKFLOW_MAX_CALLS。`);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* 工作流启动时的 history 长度。`resetContext()` 截回这里 —— 那个位置上是刷新过的
|
|
142
|
+
* 稳定系统提示(含 Skills 目录),截到它意味着「回到干净的入口快照」而不是清空。
|
|
143
|
+
*/
|
|
144
|
+
const historyBaseline = ctx.history.length;
|
|
145
|
+
// ---------------------------------------------------------------------
|
|
146
|
+
// 车道(lane):并发下让账本定位保持确定
|
|
147
|
+
// ---------------------------------------------------------------------
|
|
148
|
+
//
|
|
149
|
+
// 账本按「车道内序号」定位,而车道内是严格串行的,所以序号与调度无关。
|
|
150
|
+
// 当前车道靠 AsyncLocalStorage 随 await 链传播 —— 模块级变量在并发交错下必错。
|
|
151
|
+
const laneStore = new AsyncLocalStorage();
|
|
152
|
+
const currentLane = () => laneStore.getStore() ?? MAIN_LANE;
|
|
153
|
+
/** 每条车道内的 pipeline 计数器:嵌套扇出的车道名也要确定。 */
|
|
154
|
+
const pipelineCounters = new Map();
|
|
155
|
+
const childCounters = new Map();
|
|
156
|
+
const nextPipelineId = (lane) => {
|
|
157
|
+
const n = (pipelineCounters.get(lane) ?? 0) + 1;
|
|
158
|
+
pipelineCounters.set(lane, n);
|
|
159
|
+
return n;
|
|
160
|
+
};
|
|
161
|
+
/** 展示用序号(▶/✓ 配对),与账本定位无关。 */
|
|
162
|
+
let displaySeq = 0;
|
|
163
|
+
// 同一车道上并发发起记账调用 = 该车道序号取决于调度 = resume 会失效。
|
|
164
|
+
// 这是**作者用错了 API** 的信号(裸 `Promise.all` 套 agent),报一次就够。
|
|
165
|
+
const laneActive = new Map();
|
|
166
|
+
let warnedConcurrentLane = false;
|
|
167
|
+
const noteLaneEntry = (lane) => {
|
|
168
|
+
const n = (laneActive.get(lane) ?? 0) + 1;
|
|
169
|
+
laneActive.set(lane, n);
|
|
170
|
+
if (n > 1 && !warnedConcurrentLane) {
|
|
171
|
+
warnedConcurrentLane = true;
|
|
172
|
+
emit({
|
|
173
|
+
type: 'warning',
|
|
174
|
+
message: '⚠ 同一条执行链上有并发的记账调用(多半是裸 `Promise.all([agent(...), agent(...)])`)。' +
|
|
175
|
+
'这会让账本序号取决于谁先返回,**断点续跑随之失效**。改用 `pipeline(items, 阶段…)` —— ' +
|
|
176
|
+
'它给每一项一条确定的车道,扇出与 resume 可以兼得。',
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const noteLaneExit = (lane) => {
|
|
181
|
+
const n = (laneActive.get(lane) ?? 1) - 1;
|
|
182
|
+
if (n <= 0)
|
|
183
|
+
laneActive.delete(lane);
|
|
184
|
+
else
|
|
185
|
+
laneActive.set(lane, n);
|
|
186
|
+
};
|
|
187
|
+
// ---------------------------------------------------------------------
|
|
188
|
+
// model 选项:按 id 解析 provider,解析不到就回落基座并告警
|
|
189
|
+
// ---------------------------------------------------------------------
|
|
190
|
+
const providerCache = new Map();
|
|
191
|
+
const warnedModels = new Set();
|
|
192
|
+
const resolveModel = async (modelId) => {
|
|
193
|
+
if (!modelId)
|
|
194
|
+
return undefined;
|
|
195
|
+
if (!providerCache.has(modelId)) {
|
|
196
|
+
let resolved = null;
|
|
197
|
+
try {
|
|
198
|
+
// A portable workflow role wins when mapped on this machine. Otherwise
|
|
199
|
+
// retain backwards compatibility with explicit local model ids.
|
|
200
|
+
resolved =
|
|
201
|
+
(await resolveProviderForWorkflowRole(modelId)) ??
|
|
202
|
+
(await resolveProviderForModel(modelId));
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
resolved = null; // 配置读不动 → 当解析不到处理,绝不让它拖垮工作流
|
|
206
|
+
}
|
|
207
|
+
providerCache.set(modelId, resolved);
|
|
208
|
+
}
|
|
209
|
+
const provider = providerCache.get(modelId) ?? null;
|
|
210
|
+
if (!provider) {
|
|
211
|
+
if (!warnedModels.has(modelId)) {
|
|
212
|
+
warnedModels.add(modelId);
|
|
213
|
+
emit({
|
|
214
|
+
type: 'warning',
|
|
215
|
+
message: `⚠ 找不到模型「${modelId}」(工作流角色未映射、模型不存在或没填 API Key),本步回落到基座模型。随包工作流应声明 capability role,不应写死机器本地模型 id。`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
return provider;
|
|
221
|
+
};
|
|
222
|
+
/** Fail before expensive work when a workflow's declared model capability is absent. */
|
|
223
|
+
const requireModel = async (role, needs = {}) => {
|
|
224
|
+
// Role mappings are optional overrides. This keeps packaged workflows
|
|
225
|
+
// usable in CLI/Desktop before either surface grows a role-mapping UI.
|
|
226
|
+
// resolveModel already emits one visible fallback warning when unmapped.
|
|
227
|
+
const provider = (await resolveModel(role)) ?? ctx.provider;
|
|
228
|
+
if (needs.vision && provider.supportsVision !== true) {
|
|
229
|
+
throw new Error(`工作流角色「${role}」未映射到可用的视觉模型,且当前基座模型也不支持视觉输入。`);
|
|
230
|
+
}
|
|
231
|
+
if (needs.jsonSchema && provider.supportsJsonSchema !== true) {
|
|
232
|
+
throw new Error(`工作流角色「${role}」的映射模型/基座模型未声明可靠 JSON Schema 支持。`);
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
role,
|
|
236
|
+
model: provider.model,
|
|
237
|
+
supportsVision: provider.supportsVision === true,
|
|
238
|
+
supportsJsonSchema: provider.supportsJsonSchema === true,
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
// ---------------------------------------------------------------------
|
|
242
|
+
// schema:校验 + 有界修复
|
|
243
|
+
// ---------------------------------------------------------------------
|
|
244
|
+
/** 修复轮数上限。够纠正格式问题,又不至于在一个搞不定的 schema 上烧钱。 */
|
|
245
|
+
const SCHEMA_REPAIR_ROUNDS = 2;
|
|
246
|
+
/**
|
|
247
|
+
* provider 原生结构化输出的参数片段 —— **只对 OpenAI 格式给**。
|
|
248
|
+
*
|
|
249
|
+
* Anthropic 格式的 provider 上 `chat()` 会在发请求前直接抛错,那会把一次本可以
|
|
250
|
+
* 靠「校验 + 修复」完成的调用变成硬失败。所以原生约束是加成,可移植的地板是
|
|
251
|
+
* 引擎侧的校验与修复循环。`test/api.schema.test.ts` 锁住「Anthropic 不得抛」。
|
|
252
|
+
*/
|
|
253
|
+
const nativeSchema = (provider, schema, enabled = true) => !enabled ||
|
|
254
|
+
provider.apiFormat === 'anthropic' ||
|
|
255
|
+
provider.supportsJsonSchema === false
|
|
256
|
+
? {}
|
|
257
|
+
: { responseFormat: toResponseFormat(schema) };
|
|
258
|
+
const toResponseFormat = (schema) => ({
|
|
259
|
+
type: 'json_schema',
|
|
260
|
+
json_schema: {
|
|
261
|
+
name: 'workflow_output',
|
|
262
|
+
schema: schema,
|
|
263
|
+
strict: false,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
/**
|
|
267
|
+
* `isolated: true` 时给出一份全新 history:只带稳定系统提示。
|
|
268
|
+
*
|
|
269
|
+
* ⚠️ **不得触碰 `historyBaseline`** —— 那是 `resetContext()` 截回的锚点,
|
|
270
|
+
* 指向主线 history 里刷新过的系统提示。隔离调用完全不碰主线,锚自然不动。
|
|
271
|
+
*/
|
|
272
|
+
const isolatedHistory = (isolated) => {
|
|
273
|
+
if (!isolated)
|
|
274
|
+
return undefined;
|
|
275
|
+
const system = ctx.history[0];
|
|
276
|
+
return system && system.role === 'system' ? [{ ...system }] : [];
|
|
277
|
+
};
|
|
278
|
+
/** 从模型文本里抽出 JSON 并校验;返回对象或 null(附违规清单)。 */
|
|
279
|
+
const parseAndValidate = (text, schema) => {
|
|
280
|
+
let parsed;
|
|
281
|
+
try {
|
|
282
|
+
parsed = JSON.parse(extractJsonPayload(stripThinkBlocks(String(text ?? ''))));
|
|
283
|
+
}
|
|
284
|
+
catch (e) {
|
|
285
|
+
return {
|
|
286
|
+
value: null,
|
|
287
|
+
violations: ` · (根):不是合法 JSON —— ${e instanceof Error ? e.message : e}`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const violations = validateAgainstSchema(parsed, schema);
|
|
291
|
+
if (violations.length === 0)
|
|
292
|
+
return { value: parsed, violations: [] };
|
|
293
|
+
return { value: null, violations: renderViolations(violations) };
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* 把一次「产文本」的调用变成「产校验过的对象」。
|
|
297
|
+
*
|
|
298
|
+
* ⚠️ **整个修复循环必须在 journaled 的 `run()` 内部**:一次逻辑调用只该记一条账,
|
|
299
|
+
* 记录的是**校验后的对象**(重放时直接拿到对象,不必再解析一遍,也就不会
|
|
300
|
+
* 在重放路径上重演解析失败)。把修复摊成多条账会让车道序号随修复次数漂移。
|
|
301
|
+
*/
|
|
302
|
+
const withSchema = async (schema, first, repair) => {
|
|
303
|
+
let text = await first();
|
|
304
|
+
let attempt = 0;
|
|
305
|
+
for (;;) {
|
|
306
|
+
const r = parseAndValidate(text, schema);
|
|
307
|
+
if (r.value !== null)
|
|
308
|
+
return r.value;
|
|
309
|
+
if (attempt >= SCHEMA_REPAIR_ROUNDS) {
|
|
310
|
+
throw new Error(`结构化输出在 ${SCHEMA_REPAIR_ROUNDS} 次修复后仍不符合 schema:\n${r.violations}\n` +
|
|
311
|
+
`最后一次原始输出(截断):${truncate(String(text ?? '').replace(/\s+/g, ' '), 300)}`);
|
|
312
|
+
}
|
|
313
|
+
attempt++;
|
|
314
|
+
emit({
|
|
315
|
+
type: 'warning',
|
|
316
|
+
message: `↻ 结构化输出不合 schema,第 ${attempt}/${SCHEMA_REPAIR_ROUNDS} 次修复:\n${r.violations}`,
|
|
317
|
+
});
|
|
318
|
+
text = await repair(`你上一次的输出不符合要求的 JSON Schema。逐条问题如下:\n${r.violations}\n\n请**只输出修正后的 JSON**,不要任何解释、不要 markdown 代码围栏。\n需要满足的 Schema:\n${JSON.stringify(schema)}\n\n你上一次的输出:\n${truncate(String(text ?? ''), 4000)}`);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
/** 把 contextFiles 渲染成一段「有这些文件可读」的提示,不 inline 内容。 */
|
|
322
|
+
const renderContextFiles = (files) => {
|
|
323
|
+
if (!files?.length)
|
|
324
|
+
return '';
|
|
325
|
+
const lines = files.map((f) => f.hint ? ` · ${f.path} —— ${f.hint}` : ` · ${f.path}`);
|
|
326
|
+
return `\n\n可供参考的文件(**按需用 Read 打开**,不要假设内容):\n${lines.join('\n')}`;
|
|
327
|
+
};
|
|
328
|
+
/** 跑一轮 runQuery 并归纳结果。llm_error / max_turns 转成抛出的 Error。 */
|
|
329
|
+
const query = async (prompt, opts) => {
|
|
330
|
+
const history = opts.history ?? ctx.history;
|
|
331
|
+
const hostTools = new Set((ctx.tools ?? []).map((t) => t.name));
|
|
332
|
+
const allowed = opts.allowedTools === undefined
|
|
333
|
+
? [...hostTools]
|
|
334
|
+
: opts.allowedTools.filter((n) => hostTools.has(n));
|
|
335
|
+
// ⚠️ 过滤掉的名字必须报出来,而且要**区分两种原因** ——
|
|
336
|
+
// · 名字压根不存在(拼错了:'WriteFile' 而不是 'Write')→ 作者的 bug,响亮警告
|
|
337
|
+
// · 名字存在但宿主没授权 → 宿主策略,说明一下即可
|
|
338
|
+
// 早先这里只 filter 不吭声:作者拼错一个工具名,agent 就少了那个能力,
|
|
339
|
+
// 然后它要么绕路、要么烂尾,而 progress.md 里**什么都看不出来**。
|
|
340
|
+
if (opts.allowedTools?.length) {
|
|
341
|
+
const missing = opts.allowedTools.filter((n) => !hostTools.has(n));
|
|
342
|
+
if (missing.length > 0) {
|
|
343
|
+
const unknown = unknownToolNames(missing, KNOWN_TOOL_NAMES);
|
|
344
|
+
const filtered = missing.filter((n) => KNOWN_TOOL_NAMES.has(n));
|
|
345
|
+
if (unknown.length > 0) {
|
|
346
|
+
emit({
|
|
347
|
+
type: 'warning',
|
|
348
|
+
message: `⚠ allowedTools 里有 ${unknown.length} 个**不存在**的工具名:${unknown.join('、')}。拼错的名字会被静默丢掉,agent 因此少了这些能力。跑 \`node skills/workflow-creator/scripts/list-resources.mjs --tools\` 看全部可用工具名。`,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if (filtered.length > 0) {
|
|
352
|
+
emit({
|
|
353
|
+
type: 'warning',
|
|
354
|
+
message: `ℹ allowedTools 里的 ${filtered.join('、')} 存在但本次运行的宿主没授权,已跳过。`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const historyLenBefore = history.length;
|
|
360
|
+
const runtimeContext = createAgentRuntimeContext({
|
|
361
|
+
...(ctx.runtimeContext ?? {
|
|
362
|
+
surface: 'workflow',
|
|
363
|
+
profile: 'main',
|
|
364
|
+
interactiveAsk: false,
|
|
365
|
+
}),
|
|
366
|
+
cwd: effectiveWorkflowCwd(ctx),
|
|
367
|
+
owner: ctx.runtimeContext?.owner ?? 'untrusted',
|
|
368
|
+
memoryAccess: ctx.runtimeContext?.memoryAccess ?? 'denied',
|
|
369
|
+
});
|
|
370
|
+
const requestSignal = modelScope.getStore() ?? ctx.signal;
|
|
371
|
+
requestSignal?.throwIfAborted();
|
|
372
|
+
const capabilityContext = buildWorkflowToolContext(ctx, `wf-model-${task}-${randomUUID()}`, () => undefined, opts.uses);
|
|
373
|
+
if ('error' in capabilityContext)
|
|
374
|
+
throw new Error(capabilityContext.error);
|
|
375
|
+
let llmError = '';
|
|
376
|
+
let maxTurnsHit = false;
|
|
377
|
+
let sawToolCall = false;
|
|
378
|
+
let usage;
|
|
379
|
+
const toolNames = [];
|
|
380
|
+
const selectedProvider = opts.provider ?? ctx.provider;
|
|
381
|
+
const boundedProvider = opts.maxTokens && opts.maxTokens > 0
|
|
382
|
+
? {
|
|
383
|
+
...selectedProvider,
|
|
384
|
+
maxTokens: typeof selectedProvider.maxTokens === 'number'
|
|
385
|
+
? Math.min(selectedProvider.maxTokens, opts.maxTokens)
|
|
386
|
+
: opts.maxTokens,
|
|
387
|
+
}
|
|
388
|
+
: selectedProvider;
|
|
389
|
+
for await (const ev of runQuery(prompt, {
|
|
390
|
+
provider: boundedProvider,
|
|
391
|
+
runtimeContext,
|
|
392
|
+
history,
|
|
393
|
+
signal: requestSignal,
|
|
394
|
+
maxTurns: opts.maxTurns,
|
|
395
|
+
allowedTools: allowed,
|
|
396
|
+
...(opts.allowedTools === undefined ? {} : { requiredTools: allowed }),
|
|
397
|
+
chat: ctx.chat,
|
|
398
|
+
executeTool: ctx.executeTool,
|
|
399
|
+
sessionState: ctx.sessionState,
|
|
400
|
+
...(opts.responseFormat ? { responseFormat: opts.responseFormat } : {}),
|
|
401
|
+
...(opts.images?.length
|
|
402
|
+
? {
|
|
403
|
+
imageInputs: opts.images.map((i) => typeof i === 'string' ? { source: i } : i),
|
|
404
|
+
}
|
|
405
|
+
: {}),
|
|
406
|
+
transientContext: ctx.transientContext,
|
|
407
|
+
inputResources: ctx.inputResources,
|
|
408
|
+
transientBundle: ctx.transientBundle,
|
|
409
|
+
promptCache: ctx.promptCache,
|
|
410
|
+
...(capabilityContext.computerRun
|
|
411
|
+
? { computerRun: capabilityContext.computerRun }
|
|
412
|
+
: {}),
|
|
413
|
+
...(capabilityContext.computerIntervention
|
|
414
|
+
? { computerIntervention: capabilityContext.computerIntervention }
|
|
415
|
+
: {}),
|
|
416
|
+
...(capabilityContext.browserRun
|
|
417
|
+
? { browserRun: capabilityContext.browserRun }
|
|
418
|
+
: {}),
|
|
419
|
+
...(capabilityContext.toolGate
|
|
420
|
+
? { toolGate: capabilityContext.toolGate }
|
|
421
|
+
: {}),
|
|
422
|
+
})) {
|
|
423
|
+
if (ev.type === 'error') {
|
|
424
|
+
if (ev.code === 'llm_error') {
|
|
425
|
+
llmError = ev.error;
|
|
426
|
+
continue; // ⚠️ 见文件头注:不得漏给顶层,否则 -p 直接杀进程
|
|
427
|
+
}
|
|
428
|
+
if (ev.code === 'max_turns') {
|
|
429
|
+
maxTurnsHit = true;
|
|
430
|
+
continue; // 同上
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else if (ev.type === 'tool_start' || ev.type === 'tool_end') {
|
|
434
|
+
sawToolCall = true;
|
|
435
|
+
if (!toolNames.includes(ev.toolName))
|
|
436
|
+
toolNames.push(ev.toolName);
|
|
437
|
+
}
|
|
438
|
+
// turn_done 是本次 runQuery 的收尾,不是整条工作流的收尾 —— 吞掉,
|
|
439
|
+
// 否则宿主会以为工作流已经结束。⚠️ 但吞掉之前要把它带的资源账本取走:
|
|
440
|
+
// 这是本次调用花了多少 token 的唯一来源,丢了预算闸就没有刻度。
|
|
441
|
+
if (ev.type === 'turn_done') {
|
|
442
|
+
usage = ev.usage;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
// Isolated calls are internal workers. Their committed messages may contain base64
|
|
446
|
+
// images and reasoning; forwarding them makes Desktop look as if a completed
|
|
447
|
+
// workflow is still chatting. Keep only explicit workflow progress/warnings/errors.
|
|
448
|
+
if (opts.isolated && ev.type !== 'warning' && ev.type !== 'error')
|
|
449
|
+
continue;
|
|
450
|
+
// ⚠️ **模型的原始文本不外泄**:`llm()`/`agent()` 在工作流里是**产值调用**,
|
|
451
|
+
// 产出交给脚本,由脚本用 `echo()` 决定给用户看什么。全放出去有两个实际
|
|
452
|
+
// 后果:① 二十步的工作流糊出一堵墙;② `-p --output-format json` 的
|
|
453
|
+
// `result` 字段(累积的 assistant 文本)被中间产物污染,宿主拿不到干净结论。
|
|
454
|
+
// 工具活动与进度照常发 —— 用户仍然看得见「在干什么」。
|
|
455
|
+
// ⚠️ 只吞 `text`,**不吞 `assistant_message`**:后者在 print.ts 里只给
|
|
456
|
+
// `num_turns` 计数、不进 result buffer,吞掉它只会让宿主看到「0 轮」
|
|
457
|
+
// 却做了真活;而 TUI 侧它也不负责渲染内容(MessageList 从 history 渲染,
|
|
458
|
+
// runQuery 已直接 push),吞了照样挡不住显示。纯亏。
|
|
459
|
+
if (ev.type === 'text')
|
|
460
|
+
continue;
|
|
461
|
+
emit(ev);
|
|
462
|
+
}
|
|
463
|
+
requestSignal?.throwIfAborted();
|
|
464
|
+
// 从 history 反查本次产出的最后一条非空 assistant 文本。
|
|
465
|
+
// 经 stripThinkBlocks 净化:末条若是纯 <think>(模型收尾只剩思考)继续往前找 ——
|
|
466
|
+
// 拿到手的必须是交付文本,不是思考过程。
|
|
467
|
+
let text = '';
|
|
468
|
+
for (let i = history.length - 1; i >= historyLenBefore; i--) {
|
|
469
|
+
const msg = history[i];
|
|
470
|
+
if (msg.role === 'assistant' && typeof msg.content === 'string') {
|
|
471
|
+
const cleaned = stripThinkBlocks(msg.content).trim();
|
|
472
|
+
if (cleaned) {
|
|
473
|
+
text = cleaned;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (llmError && !text) {
|
|
479
|
+
throw new Error(`LLM 调用失败:${llmError}`);
|
|
480
|
+
}
|
|
481
|
+
if (maxTurnsHit) {
|
|
482
|
+
throw new Error(`撞到轮数上限 ${opts.maxTurns} 被截断 —— 此刻的产出是半截工作叙述而不是可信交付。请提高 maxTurns,或把任务拆小。`);
|
|
483
|
+
}
|
|
484
|
+
return { text, sawToolCall, toolNames, usage };
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* 把一次调用的用量记进账本。
|
|
488
|
+
*
|
|
489
|
+
* ⚠️ **必须在 journaled 回调内部调用**:账本重放命中时那个回调根本不执行,
|
|
490
|
+
* 于是重放天然不重复记账。挪到外面就会每次 resume 都把历史用量再加一遍。
|
|
491
|
+
*
|
|
492
|
+
* ⚠️ provider 没报 usage 时(`usage.trustworthy === false`)照样把已知部分记进去,
|
|
493
|
+
* 但把 ledger 的 trustworthy 拉黑 —— 让 `budget()` 的消费方知道这份账偏低。
|
|
494
|
+
* 直接跳过不记会更糟:那等于默认「没报 = 没花」。
|
|
495
|
+
*/
|
|
496
|
+
const recordCallUsage = async (usage) => {
|
|
497
|
+
if (!usage)
|
|
498
|
+
return;
|
|
499
|
+
ctx.resourceLedger ??= {
|
|
500
|
+
inputTokens: 0,
|
|
501
|
+
outputTokens: 0,
|
|
502
|
+
limit: null,
|
|
503
|
+
trustworthy: true,
|
|
504
|
+
};
|
|
505
|
+
const ledger = ctx.resourceLedger;
|
|
506
|
+
if (!usage.trustworthy)
|
|
507
|
+
ledger.trustworthy = false;
|
|
508
|
+
const recordUsage = ctx.usageRecorder ??
|
|
509
|
+
ctx.managedBridge?.recordUsage?.bind(ctx.managedBridge);
|
|
510
|
+
if (recordUsage) {
|
|
511
|
+
// 有外层控制器 → 以它持久化的累计快照为准(跨进程 resume 不丢账)。
|
|
512
|
+
try {
|
|
513
|
+
const snap = await recordUsage({
|
|
514
|
+
inputTokens: usage.inputTokens,
|
|
515
|
+
outputTokens: usage.outputTokens,
|
|
516
|
+
});
|
|
517
|
+
ledger.inputTokens = snap.inputTokens;
|
|
518
|
+
ledger.outputTokens = snap.outputTokens;
|
|
519
|
+
ledger.limit = snap.limit;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
// 记账失败不能让工作流挂掉 —— 退回进程内累计并拉黑可信度。
|
|
524
|
+
ledger.trustworthy = false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
ledger.inputTokens += usage.inputTokens;
|
|
528
|
+
ledger.outputTokens += usage.outputTokens;
|
|
529
|
+
};
|
|
530
|
+
/** 上一条 text 输出是否以换行收尾 —— echo() 据此决定要不要补一个前置换行。 */
|
|
531
|
+
let lastEmittedEndedWithNewline = true;
|
|
532
|
+
/**
|
|
533
|
+
* 给**脚本自己**读文件用的确定性读取(`readText` / `readJson` 的共同实现)。
|
|
534
|
+
*
|
|
535
|
+
* ⚠️ **刻意不走 `Read` 工具**。那个工具的输出是**给模型看**的,带两样对程序化
|
|
536
|
+
* 调用有害的东西:
|
|
537
|
+
* ① 每行的 `<行号><TAB>` 前缀;
|
|
538
|
+
* ② 重复读同一文件时开头插一句「⚠️ 重复读取空转:该区间未变化…」。
|
|
539
|
+
* 第 ② 条尤其阴险 —— 它只在**第二次**读时出现,于是同一段代码时对时错。
|
|
540
|
+
* 真实事故:research 阶段2 的 agent 写完 branches.json 后自己 Read 校验了一遍,
|
|
541
|
+
* 脚本随后 `readJson()` 就命中这条提示、`JSON.parse` 失败、静默返回 null,
|
|
542
|
+
* 最后报出来的是「JSON 格式错误或数组为空」——而那个文件完全合法。
|
|
543
|
+
*
|
|
544
|
+
* 相对路径基于 `effectiveWorkflowCwd` 解析(进了 worktree 就跟着走)。
|
|
545
|
+
*/
|
|
546
|
+
const readFileForScript = async (path) => {
|
|
547
|
+
try {
|
|
548
|
+
const base = effectiveWorkflowCwd(ctx);
|
|
549
|
+
if (isAbsolute(path))
|
|
550
|
+
return await readFile(path, 'utf8');
|
|
551
|
+
// User data remains rooted at the effective workspace cwd. If that path
|
|
552
|
+
// does not exist, also resolve the documented package-resource form
|
|
553
|
+
// (`workflows/<name>/...`) against the discovered workflow root. Desktop
|
|
554
|
+
// runs commonly use a workspace outside the installed package, so without
|
|
555
|
+
// this fallback bundled prompts fail with a misleading "missing prompt".
|
|
556
|
+
const candidates = [resolve(base, path)];
|
|
557
|
+
if (workflowDir) {
|
|
558
|
+
candidates.push(resolve(workflowDir, path));
|
|
559
|
+
candidates.push(resolve(dirname(dirname(workflowDir)), path));
|
|
560
|
+
}
|
|
561
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
562
|
+
try {
|
|
563
|
+
return await readFile(candidate, 'utf8');
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
// Try the next resource root; a missing workspace file is normal.
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
const progress = (message) => {
|
|
576
|
+
emitProgressEvent({ phase: 'step', status: 'update', message });
|
|
577
|
+
};
|
|
578
|
+
const runScheduled = async (resource, label, run) => {
|
|
579
|
+
const queuedAt = Date.now();
|
|
580
|
+
return scope.scheduler.run(resource, scope.rootRunId, async (queuedMs) => {
|
|
581
|
+
const startedAt = Date.now();
|
|
582
|
+
emitProgressEvent({
|
|
583
|
+
phase: resource,
|
|
584
|
+
status: 'start',
|
|
585
|
+
message: label,
|
|
586
|
+
queuedMs,
|
|
587
|
+
elapsedMs: startedAt - queuedAt,
|
|
588
|
+
});
|
|
589
|
+
try {
|
|
590
|
+
return await run();
|
|
591
|
+
}
|
|
592
|
+
finally {
|
|
593
|
+
emitProgressEvent({
|
|
594
|
+
phase: resource,
|
|
595
|
+
status: 'complete',
|
|
596
|
+
message: label,
|
|
597
|
+
queuedMs,
|
|
598
|
+
elapsedMs: Date.now() - startedAt,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}, ctx.signal);
|
|
602
|
+
};
|
|
603
|
+
/** `tool()` 与 `toolResult()` 的共同实现 —— 走同一条账本与授权路径。 */
|
|
604
|
+
const callTool = async (name, input, opts) => journaled('tool', `🔧 ${name}`, { name, input, uses: opts.uses ?? null }, async () => {
|
|
605
|
+
const toolCallId = `wf-${task}-${randomUUID()}`;
|
|
606
|
+
const built = buildWorkflowToolContext(ctx, toolCallId, () => undefined, opts.uses);
|
|
607
|
+
if ('error' in built)
|
|
608
|
+
throw new Error(built.error);
|
|
609
|
+
// Write 工具有「覆盖已存在文件前必须先 Read 过」的防手滑闸门——那是为
|
|
610
|
+
// agent 在长对话里可能凭过时印象盲写设计的。工作流脚本的 tool('Write', …)
|
|
611
|
+
// 不是这种情况:它是作者写死的、无条件覆盖自己声明的产物路径(CONTRACT.md/
|
|
612
|
+
// STATUS.json/manifest.json 这类),语义上就是"这次跑完的最新状态",
|
|
613
|
+
// 从不依赖"没人动过"这个假设。断点续跑(尤其 goal/ralph-loop 这类跨进程
|
|
614
|
+
// 恢复的 managedRuntime 工作流)天然会在一个**全新进程**里对着上一次尝试
|
|
615
|
+
// 已经写过的路径再写一遍——新进程的 sessionState 从空白起步,闸门会把
|
|
616
|
+
// "从没人读过"误判成"可能盲写覆盖外部改动",实际内容完全正确也照样拒绝。
|
|
617
|
+
// 用 recordRead 在动手之前把当前磁盘内容"报到"给同一份 freshness 状态,
|
|
618
|
+
// 等价于工作流自己先 Read 了一遍——不改闸门本身(agent 走普通 Write 时这
|
|
619
|
+
// 层保护原样保留),只解除它对确定性脚本重复覆盖自身产物的误伤。
|
|
620
|
+
if (name === 'Write') {
|
|
621
|
+
const target = String(input.file_path ?? '');
|
|
622
|
+
if (target) {
|
|
623
|
+
// resolve() 而不是 isAbsolute() 分支:即便 target 已是绝对路径,
|
|
624
|
+
// 也要走同一次 resolve 规范化(盘符大小写、正斜杠/反斜杠混用),
|
|
625
|
+
// 否则这里算出的 key 和 write.ts::validateAndResolvePath 算出的
|
|
626
|
+
// 不是同一个字符串,Map 查找对不上,等于没修——曾经这样漏过一次。
|
|
627
|
+
const abs = resolve(effectiveWorkflowCwd(ctx), target);
|
|
628
|
+
if (existsSync(abs))
|
|
629
|
+
recordRead(abs, {}, built.sessionState?.fileReads);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const exec = ctx.executeTool ?? sdkExecuteTool;
|
|
633
|
+
const r = await runScheduled('tool', name, () => exec(name, JSON.stringify(input), ctx.signal, built));
|
|
634
|
+
if (!r.ok)
|
|
635
|
+
throw new Error(`工具 ${name} 失败:${r.error}`);
|
|
636
|
+
return {
|
|
637
|
+
content: r.content,
|
|
638
|
+
images: (r.visionImages ?? [])
|
|
639
|
+
.map((img) => (typeof img.source === 'string' ? img.source : ''))
|
|
640
|
+
.filter((p) => p.length > 0),
|
|
641
|
+
metadata: r.metadata,
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
/**
|
|
645
|
+
* 记一次引擎中介调用:状态行显示 + **落盘 progress.md**。
|
|
646
|
+
*
|
|
647
|
+
* ⚠️ 落盘那半不是锦上添花。TUI 的 scrollback 会被刷掉,而排障几乎总是发生在
|
|
648
|
+
* 「跑完之后」—— 一条跑了五分钟、九次调用的工作流出了问题,唯一还能回看的现场
|
|
649
|
+
* 就是这个文件。legacy 的 progress.md 逐步记录,新引擎一开始只写了最后一行
|
|
650
|
+
* 「✓ 完成」,等于把这个能力丢了(真实 plan 端到端跑完才发现)。
|
|
651
|
+
*
|
|
652
|
+
* 失败也要记 —— 只记成功等于把最需要看的那条抹掉。
|
|
653
|
+
*/
|
|
654
|
+
const journaled = async (kind, label, args_, run, alreadyCounted = false) => {
|
|
655
|
+
if (!alreadyCounted)
|
|
656
|
+
countCall();
|
|
657
|
+
progress(label);
|
|
658
|
+
const lane = currentLane();
|
|
659
|
+
// 展示序号用引擎自己的计数器:账本的写入序号在并发下是「谁先返回谁先写」,
|
|
660
|
+
// 拿它当 ▶/✓ 的配对号会串行。这个只用于人眼配对,不参与任何定位。
|
|
661
|
+
const seq = ++displaySeq;
|
|
662
|
+
noteLaneEntry(lane);
|
|
663
|
+
// fire-and-forget(同 258/1650/1660 行的心跳/structuredProgress/log()):
|
|
664
|
+
// WorkflowState.appendProgress 内部的 progressWrites 链在第一个 await 之前就把
|
|
665
|
+
// 写入按调用顺序排进队,调用方等不等待不影响落盘顺序。journaled() 是
|
|
666
|
+
// agent/llm/skill/tool/sh/execFile 的唯一必经关卡,同步等待这三次磁盘往返会把
|
|
667
|
+
// 调度器本该提供的并发压成串行(实测:16 路并发 generate() 因此只叠出 2-3 路
|
|
668
|
+
// 峰值)。落盘保证由 runner.ts 每条终止路径(中断/失败/成功)对同一条
|
|
669
|
+
// progressWrites 链的 await 兜底——那次等待会等到它之前排队的全部写入都完成,
|
|
670
|
+
// 不需要在这里额外补 flush。
|
|
671
|
+
void state.appendProgress(`▶ #${seq} ${kind}: ${label}`).catch(() => { });
|
|
672
|
+
try {
|
|
673
|
+
const r = await journal.step(kind, args_, run, lane);
|
|
674
|
+
const preview = truncate(String(typeof r === 'object' && r !== null ? JSON.stringify(r) : (r ?? ''))
|
|
675
|
+
.replace(/\s+/g, ' ')
|
|
676
|
+
.trim(), 120);
|
|
677
|
+
void state.appendProgress(`✓ #${seq} ${kind}: ${preview}`).catch(() => { });
|
|
678
|
+
return r;
|
|
679
|
+
}
|
|
680
|
+
catch (e) {
|
|
681
|
+
void state
|
|
682
|
+
.appendProgress(`✗ #${seq} ${kind}: ${e instanceof Error ? e.message : String(e)}`)
|
|
683
|
+
.catch(() => { });
|
|
684
|
+
throw e;
|
|
685
|
+
}
|
|
686
|
+
finally {
|
|
687
|
+
noteLaneExit(lane);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
const capabilities = {
|
|
691
|
+
args,
|
|
692
|
+
session,
|
|
693
|
+
task,
|
|
694
|
+
goal: ctx.goalAnchor ?? '',
|
|
695
|
+
// Absolute directory of the discovered workflow. This is for immutable
|
|
696
|
+
// bundled resources (prompts/scripts), not user output; output paths remain
|
|
697
|
+
// relative to the effective workspace cwd.
|
|
698
|
+
workflowDir: workflowDir ?? '',
|
|
699
|
+
// Bound input resources are runtime-only host paths. Do not persist or publish them.
|
|
700
|
+
resources: deps.inputResourceMap ?? {},
|
|
701
|
+
/** Validate a portable model role before the workflow starts costly image work. */
|
|
702
|
+
requireModel,
|
|
703
|
+
/** 有界 TAOR 子任务:模型自主选工具干活。 */
|
|
704
|
+
agent: async (prompt, opts = {}) => {
|
|
705
|
+
// 默认 true:账本重放命中时 run 回调根本不执行,此值保持不变 —— 不该为
|
|
706
|
+
// 一次「上次已经报过」的调用重复报警。只有真跑过才可能被置成 false。
|
|
707
|
+
let sawTools = true;
|
|
708
|
+
const seq = ++displaySeq;
|
|
709
|
+
const provider = await resolveModel(opts.model);
|
|
710
|
+
const history = isolatedHistory(opts.isolated);
|
|
711
|
+
const text = await modelCall(opts, () => journaled('agent', `🤖 ${opts.label ?? truncate(prompt, 40)}`,
|
|
712
|
+
// ⚠️ provider 的**真实 model 串**必须进指纹:同一份脚本换台机器解析到
|
|
713
|
+
// 另一个模型时,指纹若不变就会拿 A 模型的结果冒充 B 模型的 —— 与账本
|
|
714
|
+
// 要挡的那类静默错误同源。schema 同理(换 schema = 换契约)。
|
|
715
|
+
{
|
|
716
|
+
prompt,
|
|
717
|
+
allowedTools: opts.allowedTools ?? null,
|
|
718
|
+
uses: opts.uses ?? null,
|
|
719
|
+
images: opts.images ?? null,
|
|
720
|
+
contextFiles: opts.contextFiles ?? null,
|
|
721
|
+
maxTurns: opts.maxTurns ?? 60,
|
|
722
|
+
model: provider?.model ?? null,
|
|
723
|
+
schema: opts.schema ?? null,
|
|
724
|
+
nativeSchema: opts.nativeSchema ?? true,
|
|
725
|
+
isolated: opts.isolated ?? false,
|
|
726
|
+
timeoutMs: opts.timeoutMs ?? null,
|
|
727
|
+
maxTokens: opts.maxTokens ?? null,
|
|
728
|
+
}, async () => {
|
|
729
|
+
const run = async (p) => {
|
|
730
|
+
const r = await query(p, {
|
|
731
|
+
maxTurns: opts.maxTurns ?? 60,
|
|
732
|
+
allowedTools: opts.allowedTools,
|
|
733
|
+
uses: opts.uses,
|
|
734
|
+
images: opts.images,
|
|
735
|
+
maxTokens: opts.maxTokens,
|
|
736
|
+
...(provider ? { provider } : {}),
|
|
737
|
+
...(history ? { history } : {}),
|
|
738
|
+
isolated: opts.isolated,
|
|
739
|
+
});
|
|
740
|
+
sawTools = r.sawToolCall;
|
|
741
|
+
// 记账在 journaled 回调内 → 重放不重复计(见 recordCallUsage 头注)。
|
|
742
|
+
// schema 的修复轮也走这里,每一轮都真花了钱,都要记。
|
|
743
|
+
await recordCallUsage(r.usage);
|
|
744
|
+
return r.text;
|
|
745
|
+
};
|
|
746
|
+
const anchored = withGoalAnchor(ctx, prompt + renderContextFiles(opts.contextFiles));
|
|
747
|
+
if (!opts.schema)
|
|
748
|
+
return run(anchored);
|
|
749
|
+
// ⚠️ 带工具的循环**不能**开 provider 原生 response_format:那会强制每条
|
|
750
|
+
// assistant 消息都是 JSON,而工具调用轮带的是 tool_calls,两者互斥。
|
|
751
|
+
// 所以 agent 走「先带工具把活干完,再零工具把结论整形」——修复轮才开原生约束。
|
|
752
|
+
return withSchema(opts.schema, () => run(anchored), (instruction) => query(instruction, {
|
|
753
|
+
maxTurns: 1,
|
|
754
|
+
allowedTools: [],
|
|
755
|
+
uses: opts.uses,
|
|
756
|
+
...(provider ? { provider } : {}),
|
|
757
|
+
...(history ? { history } : {}),
|
|
758
|
+
isolated: opts.isolated,
|
|
759
|
+
maxTokens: opts.maxTokens,
|
|
760
|
+
...nativeSchema(provider ?? ctx.provider, opts.schema, opts.nativeSchema !== false),
|
|
761
|
+
}).then(async (r) => {
|
|
762
|
+
await recordCallUsage(r.usage);
|
|
763
|
+
return r.text;
|
|
764
|
+
}));
|
|
765
|
+
}));
|
|
766
|
+
// ⚠️ **announce-not-do 的廉价通用信号**:agent 产出了文本却一次工具都没调,
|
|
767
|
+
// 几乎总是「宣告我将要做…」然后停下 —— 弱模型的典型烂尾。legacy 靠 `done_when`
|
|
768
|
+
// 声明来兜,代码形态里守卫是作者写的,作者忘了写时这条就是唯一的线索。
|
|
769
|
+
//
|
|
770
|
+
// 只记录不抛:有些 agent 调用本就该纯思考(分类、判断、总结),报错会误伤。
|
|
771
|
+
// 落进 progress.md 是关键 —— TUI 会刷掉,而排障总发生在跑完之后。
|
|
772
|
+
if (!sawTools && String(text ?? '').trim()) {
|
|
773
|
+
const hint = `⚠ #${seq} agent 零工具调用:「${truncate(opts.label ?? prompt, 40)}」产出了文本但一次工具都没调`;
|
|
774
|
+
await state.appendProgress(hint);
|
|
775
|
+
emit({
|
|
776
|
+
type: 'warning',
|
|
777
|
+
message: `${hint} —— 若这一步本该产出东西,多半是 announce-not-do 烂尾,请在其后加 exists()/verify() 核实。`,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
return text;
|
|
781
|
+
},
|
|
782
|
+
/** 纯文本生成,不给任何工具(弱模型也跑得动)。 */
|
|
783
|
+
llm: async (prompt, opts = {}) => {
|
|
784
|
+
const provider = await resolveModel(opts.model);
|
|
785
|
+
const history = isolatedHistory(opts.isolated);
|
|
786
|
+
return modelCall(opts, () => journaled('llm', `✍ ${opts.label ?? truncate(prompt, 40)}`, {
|
|
787
|
+
prompt,
|
|
788
|
+
allowedTools: opts.allowedTools ?? null,
|
|
789
|
+
uses: opts.uses ?? null,
|
|
790
|
+
images: opts.images ?? null,
|
|
791
|
+
contextFiles: opts.contextFiles ?? null,
|
|
792
|
+
maxTurns: opts.maxTurns ?? 1,
|
|
793
|
+
model: provider?.model ?? null,
|
|
794
|
+
schema: opts.schema ?? null,
|
|
795
|
+
nativeSchema: opts.nativeSchema ?? true,
|
|
796
|
+
isolated: opts.isolated ?? false,
|
|
797
|
+
timeoutMs: opts.timeoutMs ?? null,
|
|
798
|
+
maxTokens: opts.maxTokens ?? null,
|
|
799
|
+
}, async () => {
|
|
800
|
+
// ⚠️ 带图时**不能**锁死无工具:识图要模型看得见图,而 contextFiles
|
|
801
|
+
// 往往还要它 Read。纯文本场景才是零工具。
|
|
802
|
+
const allowedTools = opts.allowedTools ?? (opts.contextFiles ? ['Read'] : []);
|
|
803
|
+
const run = async (p, tools = allowedTools) => {
|
|
804
|
+
const r = await query(p, {
|
|
805
|
+
maxTurns: opts.maxTurns ?? 1,
|
|
806
|
+
allowedTools: tools,
|
|
807
|
+
uses: opts.uses,
|
|
808
|
+
images: opts.images,
|
|
809
|
+
maxTokens: opts.maxTokens,
|
|
810
|
+
...(provider ? { provider } : {}),
|
|
811
|
+
...(history ? { history } : {}),
|
|
812
|
+
isolated: opts.isolated,
|
|
813
|
+
// 零工具时才开原生约束(有工具会与 tool_calls 互斥)。
|
|
814
|
+
...(opts.schema && tools.length === 0
|
|
815
|
+
? nativeSchema(provider ?? ctx.provider, opts.schema, opts.nativeSchema !== false)
|
|
816
|
+
: {}),
|
|
817
|
+
});
|
|
818
|
+
// 同 agent():记账在 journaled 回调内,重放不重复计。
|
|
819
|
+
await recordCallUsage(r.usage);
|
|
820
|
+
return r.text;
|
|
821
|
+
};
|
|
822
|
+
const anchored = withGoalAnchor(ctx, prompt + renderContextFiles(opts.contextFiles));
|
|
823
|
+
if (!opts.schema)
|
|
824
|
+
return run(anchored);
|
|
825
|
+
return withSchema(opts.schema, () => run(anchored),
|
|
826
|
+
// 修复轮一律零工具:此刻要的是整形,不是再干一遍活。
|
|
827
|
+
(instruction) => run(instruction, []));
|
|
828
|
+
}));
|
|
829
|
+
},
|
|
830
|
+
/**
|
|
831
|
+
* 把对话历史截回工作流入口的干净快照(对应 yaml 的 `fresh_context: true`)。
|
|
832
|
+
*
|
|
833
|
+
* 长循环里每轮都带着前几十轮的对话跑,模型会被旧上下文带偏,token 也线性膨胀。
|
|
834
|
+
* 截回的位置是**刷新过的稳定系统提示**(含 Skills 目录),不是清空 —— 清空会
|
|
835
|
+
* 让后续调用失去地基信息。
|
|
836
|
+
*
|
|
837
|
+
* ⚠️ 不影响账本:journal 记的是调用序号与内容指纹,与 history 无关。
|
|
838
|
+
*/
|
|
839
|
+
resetContext: () => {
|
|
840
|
+
if (ctx.history.length > historyBaseline) {
|
|
841
|
+
ctx.history.length = historyBaseline;
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
/** 跑一个 SKILL.md 定义的完整 agent。 */
|
|
845
|
+
skill: async (name, input, opts = {}) => {
|
|
846
|
+
const provider = await resolveModel(opts.model);
|
|
847
|
+
const history = isolatedHistory(opts.isolated);
|
|
848
|
+
return modelCall(opts, () => journaled('skill', `🧩 skill ${name}`, {
|
|
849
|
+
name,
|
|
850
|
+
input,
|
|
851
|
+
allowedTools: opts.allowedTools ?? null,
|
|
852
|
+
uses: opts.uses ?? null,
|
|
853
|
+
images: opts.images ?? null,
|
|
854
|
+
contextFiles: opts.contextFiles ?? null,
|
|
855
|
+
maxTurns: opts.maxTurns ?? 60,
|
|
856
|
+
model: provider?.model ?? null,
|
|
857
|
+
schema: opts.schema ?? null,
|
|
858
|
+
nativeSchema: opts.nativeSchema ?? true,
|
|
859
|
+
isolated: opts.isolated ?? false,
|
|
860
|
+
timeoutMs: opts.timeoutMs ?? null,
|
|
861
|
+
maxTokens: opts.maxTokens ?? null,
|
|
862
|
+
}, async () => {
|
|
863
|
+
const prompt = withGoalAnchor(ctx, `请使用 skill「${name}」完成以下任务:\n\n${input}${renderContextFiles(opts.contextFiles)}`);
|
|
864
|
+
const run = async (value, allowedTools = opts.allowedTools, responseFormat) => {
|
|
865
|
+
const result = await query(value, {
|
|
866
|
+
maxTurns: opts.maxTurns ?? 60,
|
|
867
|
+
allowedTools,
|
|
868
|
+
uses: opts.uses,
|
|
869
|
+
images: opts.images,
|
|
870
|
+
maxTokens: opts.maxTokens,
|
|
871
|
+
...(provider ? { provider } : {}),
|
|
872
|
+
...(history ? { history } : {}),
|
|
873
|
+
isolated: opts.isolated,
|
|
874
|
+
...(responseFormat ? { responseFormat } : {}),
|
|
875
|
+
});
|
|
876
|
+
await recordCallUsage(result.usage);
|
|
877
|
+
return result.text;
|
|
878
|
+
};
|
|
879
|
+
if (!opts.schema)
|
|
880
|
+
return run(prompt);
|
|
881
|
+
return withSchema(opts.schema, () => run(prompt), (instruction) => run(instruction, [], nativeSchema(provider ?? ctx.provider, opts.schema, opts.nativeSchema !== false).responseFormat));
|
|
882
|
+
}));
|
|
883
|
+
},
|
|
884
|
+
/** 确定性地调一个原子工具,返回它的文本结果。 */
|
|
885
|
+
tool: async (name, input = {}, opts = {}) => {
|
|
886
|
+
const r = await callTool(name, input, opts);
|
|
887
|
+
return r.content;
|
|
888
|
+
},
|
|
889
|
+
/**
|
|
890
|
+
* 调工具并拿到**完整结果**:`{ content, images, metadata }`。
|
|
891
|
+
*
|
|
892
|
+
* `tool()` 只给 content 字符串 —— 绝大多数场景要的就是它。但有些工具的产出
|
|
893
|
+
* **根本不在文本里**:ComputerUse 的 `observe` 返回截图,任何看图类工具都一样。
|
|
894
|
+
* 只取 content 的话那些图**被静默丢弃**,后续要看图的步骤没图可用,还不报错。
|
|
895
|
+
*
|
|
896
|
+
* ⚠️ legacy 引擎为这件事专门修过一次(`stepExecutors/tool.ts` 里那段
|
|
897
|
+
* 「工具返回的图片不能丢」的注释),新引擎一开始又漏了同一处 ——
|
|
898
|
+
* 迁移 wechat-reading-shots 时才发现。
|
|
899
|
+
*
|
|
900
|
+
* `images` 取的是**路径字符串数组**而不是整个 `ImageInputSpec`:消费方要的是
|
|
901
|
+
* 能直接喂给 `llm(prompt, { images: [...] })` 的东西,结构体传下去反而要每个
|
|
902
|
+
* 消费方各自解包。这条判断也是照搬 legacy 的。
|
|
903
|
+
*/
|
|
904
|
+
toolResult: async (name, input = {}, opts = {}) => callTool(name, input, opts),
|
|
905
|
+
/**
|
|
906
|
+
* 跑一条 shell 命令,返回**结构化**结果 `{ stdout, stderr, exitCode }`。
|
|
907
|
+
*
|
|
908
|
+
* 为什么值得单开一个名字(而不是让作者自己调 `tool('Bash', …)`):
|
|
909
|
+
* Bash 工具返回的是给**模型看**的展示文本 ——
|
|
910
|
+
* `<stdout>\n83 notes.md\n</stdout>\n\n[exit code: 0]`。
|
|
911
|
+
* 作者想拿行数就得手工剥这层包装,`split('\n')[0]` 拿到的是字面量
|
|
912
|
+
* `<stdout>`。这个坑第一次真实迁移就踩到了,而 shell 出去在这批工作流里
|
|
913
|
+
* 遍地都是(ppt / word / research 全都大量跑脚本)—— 让每个作者各写一遍
|
|
914
|
+
* 剥壳逻辑,等于把同一个 bug 复制十几份。
|
|
915
|
+
*
|
|
916
|
+
* 仍然经 `tool()` 走同一条账本与授权路径,不是旁路。
|
|
917
|
+
*/
|
|
918
|
+
sh: async (command, opts = {}) => {
|
|
919
|
+
const raw = (await journaled('tool', `🔧 ${truncate(command, 50)}`, { name: 'Bash', input: { command, ...opts } }, async () => {
|
|
920
|
+
const toolCallId = `wf-${task}-${randomUUID()}`;
|
|
921
|
+
const built = buildWorkflowToolContext(ctx, toolCallId, () => undefined);
|
|
922
|
+
if ('error' in built)
|
|
923
|
+
throw new Error(built.error);
|
|
924
|
+
const exec = ctx.executeTool ?? sdkExecuteTool;
|
|
925
|
+
const r = await runScheduled('process', truncate(command, 50), () => exec('Bash', JSON.stringify({ command, ...opts }), ctx.signal, built));
|
|
926
|
+
// ⚠️ 命令**非零退出**不抛异常:shell 的退出码本身常常就是要判断的信息
|
|
927
|
+
// (`grep -q` 没找到、`test -f` 不存在)。抛不抛由作者看 exitCode 决定。
|
|
928
|
+
return r.ok
|
|
929
|
+
? r.content
|
|
930
|
+
: `<stderr>\n${r.error}\n</stderr>\n\n[exit code: 1]`;
|
|
931
|
+
}));
|
|
932
|
+
const pick = (tag) => {
|
|
933
|
+
const m = new RegExp(`<${tag}>\\n?([\\s\\S]*?)\\n?</${tag}>`).exec(raw);
|
|
934
|
+
return m ? m[1] : '';
|
|
935
|
+
};
|
|
936
|
+
const code = /\[exit code:\s*(-?\d+)\]/.exec(raw);
|
|
937
|
+
return {
|
|
938
|
+
stdout: pick('stdout'),
|
|
939
|
+
stderr: pick('stderr'),
|
|
940
|
+
exitCode: code ? Number(code[1]) : 0,
|
|
941
|
+
raw,
|
|
942
|
+
};
|
|
943
|
+
},
|
|
944
|
+
/** HITL 闸门:真停等用户拍板。非交互宿主由 askBridge 自动选推荐项,不挂起。 */
|
|
945
|
+
ask: async (question, options) => {
|
|
946
|
+
return journaled('ask', '⏸ 等待用户确认…', { question, options }, async () => {
|
|
947
|
+
const exec = ctx.executeTool ?? sdkExecuteTool;
|
|
948
|
+
const r = await runScheduled('tool', '等待用户确认', () => exec('AskUserQuestion', JSON.stringify({
|
|
949
|
+
questions: [
|
|
950
|
+
{
|
|
951
|
+
question: truncate(question, 400),
|
|
952
|
+
header: '工作流',
|
|
953
|
+
options,
|
|
954
|
+
},
|
|
955
|
+
],
|
|
956
|
+
}), ctx.signal));
|
|
957
|
+
if (!r.ok)
|
|
958
|
+
throw new Error(`提问失败:${r.error}`);
|
|
959
|
+
// ⚠️ 只解析【答案行】。回执里 Q 行带着问题原文,而问题里常含选项文案 ——
|
|
960
|
+
// 看整个 content 会把用户选的 A 误判成 B。这是 legacy 踩过的真事故。
|
|
961
|
+
const answer = r.content
|
|
962
|
+
.split('\n')
|
|
963
|
+
.map((l) => l.trim())
|
|
964
|
+
.filter((l) => /^A\d+[::]/.test(l))
|
|
965
|
+
.join('\n')
|
|
966
|
+
.replace(/^A\d+[::]\s*/, '');
|
|
967
|
+
return answer;
|
|
968
|
+
});
|
|
969
|
+
},
|
|
970
|
+
/**
|
|
971
|
+
* 本次运行的资源账本快照。**不进账本** —— 重放要返回**当下**的真实余额,
|
|
972
|
+
* 而不是上次跑到这里时的旧余额。
|
|
973
|
+
*
|
|
974
|
+
* ⚠️ 只在**轮次边界**用它(循环开头判「要不要开下一轮」)。
|
|
975
|
+
* 不要在一轮进行到一半时拿它中断 —— 产品决策是「宁可超一点预算,
|
|
976
|
+
* 也不能把 agent 停在半成品状态」,引擎内核也因此完全不做预算检查。
|
|
977
|
+
*
|
|
978
|
+
* ⚠️ `trustworthy:false` 表示至少一次调用的 provider 没回报 usage,
|
|
979
|
+
* 这份账**偏低**。脚本作者要么照常跑(默认),要么明确告知用户,
|
|
980
|
+
* 但**不要**把它当成「花得少」。
|
|
981
|
+
*/
|
|
982
|
+
budget: async () => workflowBudget(ctx),
|
|
983
|
+
/** 确定性检查。**不进账本** —— 重放会返回过期的文件系统事实。 */
|
|
984
|
+
verify: async (checks) => {
|
|
985
|
+
const raw = Array.isArray(checks) ? checks : [checks];
|
|
986
|
+
const parsed = raw.map((c) => ({ raw: c, check: parseCheckString(c) }));
|
|
987
|
+
// ⚠️ 解析不出来的 check 串必须**响亮报错**。静默丢弃 = 一个拼错的检查项
|
|
988
|
+
// 让整组验证「通过」,而作者以为自己加了防线 —— 典型的掩耳盗铃。
|
|
989
|
+
const bad = parsed.filter((p) => p.check === null).map((p) => p.raw);
|
|
990
|
+
if (bad.length > 0) {
|
|
991
|
+
throw new Error(`无法解析的检查项:${bad.map((b) => JSON.stringify(b)).join(', ')}。格式是 \`类型:值\`,支持 file_exists / file_contains / shell / test_count。`);
|
|
992
|
+
}
|
|
993
|
+
const r = await runVerification(parsed.map((p) => p.check), effectiveWorkflowCwd(ctx));
|
|
994
|
+
return { passed: r.passed, summary: r.summary, details: r.details };
|
|
995
|
+
},
|
|
996
|
+
/** 文件是否存在(相对当前有效 cwd)。不进账本,理由同 verify。 */
|
|
997
|
+
exists: async (path) => {
|
|
998
|
+
const check = parseCheckString(`file_exists:${path}`);
|
|
999
|
+
if (!check)
|
|
1000
|
+
return false;
|
|
1001
|
+
// ⚠️ runVerification 吃的是**数组**(内部 for...of)。传单个对象会抛
|
|
1002
|
+
// `{} is not iterable` —— 这条是端到端实跑抓出来的,不是推理出来的。
|
|
1003
|
+
const r = await runVerification([check], effectiveWorkflowCwd(ctx));
|
|
1004
|
+
return r.passed;
|
|
1005
|
+
},
|
|
1006
|
+
/**
|
|
1007
|
+
* 读文件的**纯文本**(剥掉 Read 工具的行号前缀)。读不到返回 null。
|
|
1008
|
+
*
|
|
1009
|
+
* 为什么单开一个名字:`tool('Read', …)` 返回的是**给模型看**的格式 ——
|
|
1010
|
+
* 每行都带 `<行号><TAB>` 前缀。工作流拿它读 prompt 模板、JSON 数据、
|
|
1011
|
+
* 配置文件时,那些行号是纯噪音:喂给模型浪费 token,喂给 JSON.parse 直接失败。
|
|
1012
|
+
* 和 `sh()` 之于 `tool('Bash')` 是同一件事。
|
|
1013
|
+
*
|
|
1014
|
+
* ⚠️ 剥离规则是 `^\s*\d+\t` —— **分隔符是 TAB 不是箭头**。写错过一次,
|
|
1015
|
+
* 后果是 `readJson()` 对任何文件都静默返回 null(`null?.x` 不报错,
|
|
1016
|
+
* 于是错误一路飘到下游才炸,且报的是「JSON 格式错误或数组为空」,
|
|
1017
|
+
* 而文件其实完全合法)。端到端跑 research 时被抓到。
|
|
1018
|
+
* 已知边界:文件内容里若有某行以「数字 + TAB」开头,那个前缀也会被剥掉。
|
|
1019
|
+
*/
|
|
1020
|
+
readText: async (path) => readFileForScript(path),
|
|
1021
|
+
/** 读一个 JSON 文件。读不到或解析失败返回 null(不抛,让脚本自己决定怎么办)。 */
|
|
1022
|
+
readJson: async (path) => {
|
|
1023
|
+
const text = await readFileForScript(path);
|
|
1024
|
+
if (text === null)
|
|
1025
|
+
return null;
|
|
1026
|
+
try {
|
|
1027
|
+
return JSON.parse(text);
|
|
1028
|
+
}
|
|
1029
|
+
catch {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
/**
|
|
1034
|
+
* 确定性输出给用户:**真正进对话历史**,不烧 LLM、零幻觉。
|
|
1035
|
+
*
|
|
1036
|
+
* ⚠️ 为什么不能只发 `text`(这是实测出来的行为缺陷,不是设计):
|
|
1037
|
+
* 宿主把 `text` 事件当**流式预览**——TUI 累进 LiveArea、Desktop 累进
|
|
1038
|
+
* `streamingText`——而预览在 `turn_done` 时被清掉,只有 `assistant_message`
|
|
1039
|
+
* 才会落进 `history` / `setMessages`。于是一条纯确定性工作流(只有 echo、
|
|
1040
|
+
* 没有 llm/agent)跑完之后,历史里只剩用户那句 `/workflow xxx`,助手侧一片
|
|
1041
|
+
* 空白,用户完全看不出发生过什么。有 llm 的工作流更糟:中间 agent 的碎话
|
|
1042
|
+
* 留下了,`echo()` 写的最终结论反而没了——恰恰是最该留下的那句。
|
|
1043
|
+
*
|
|
1044
|
+
* 所以这里照 `loop.ts` 的范式办:push 进 `ctx.history` 再 yield
|
|
1045
|
+
* `assistant_message`。两个后果都是想要的:① 两端 UI 立刻正确;
|
|
1046
|
+
* ② 进了 history 就会被 `saveContext` 持久化,重开会话仍在。
|
|
1047
|
+
*
|
|
1048
|
+
* `text` 照发不误——`-p --output-format text/json` 的 `result` 累的是它,
|
|
1049
|
+
* 而 `assistant_message` 在 `print.ts` 里只计 `num_turns`、不进 result
|
|
1050
|
+
* buffer,两者不会重复。
|
|
1051
|
+
*/
|
|
1052
|
+
echo: (message) => {
|
|
1053
|
+
const prefix = lastEmittedEndedWithNewline ? '' : '\n';
|
|
1054
|
+
lastEmittedEndedWithNewline = true;
|
|
1055
|
+
emit({ type: 'text', delta: `${prefix}${message}\n` });
|
|
1056
|
+
// ⚠️ 断点续跑的快进段里**不能**再 push:resume 时 history 是从会话文件加载的,
|
|
1057
|
+
// 上一轮写下的这条 echo 已经在里面了,再 push 一次就是重复消息。
|
|
1058
|
+
// 进账本的调用不需要这层判断(`journal.step` 重放时压根不执行 run),
|
|
1059
|
+
// 但 echo 是有意不进账本的(它便宜、无副作用、每次重算)。
|
|
1060
|
+
if (journal.replaying)
|
|
1061
|
+
return;
|
|
1062
|
+
const msg = {
|
|
1063
|
+
role: 'assistant',
|
|
1064
|
+
content: message,
|
|
1065
|
+
id: randomUUID(),
|
|
1066
|
+
};
|
|
1067
|
+
ctx.history.push(msg);
|
|
1068
|
+
emit({ type: 'assistant_message', message: msg });
|
|
1069
|
+
},
|
|
1070
|
+
execFile: async (path, args = [], opts = {}) => journaled('process', `⚙ ${path}`, {
|
|
1071
|
+
path,
|
|
1072
|
+
args,
|
|
1073
|
+
cwd: opts.cwd ?? null,
|
|
1074
|
+
input: opts.input ?? null,
|
|
1075
|
+
timeoutMs: opts.timeoutMs ?? null,
|
|
1076
|
+
env: opts.env ?? null,
|
|
1077
|
+
}, () => runScheduled('process', path, () => executeWorkflowFile(path, args, opts, {
|
|
1078
|
+
cwd: effectiveWorkflowCwd(ctx),
|
|
1079
|
+
workflowDir: workflowDir ?? '',
|
|
1080
|
+
signal: ctx.signal,
|
|
1081
|
+
}))),
|
|
1082
|
+
writeText: async (path, content) => atomicWriteText(effectiveWorkflowCwd(ctx), path, content),
|
|
1083
|
+
writeJson: async (path, value) => atomicWriteJson(effectiveWorkflowCwd(ctx), path, value),
|
|
1084
|
+
mkdir: async (path) => ensureWorkflowDir(effectiveWorkflowCwd(ctx), path),
|
|
1085
|
+
/** Await an explicitly declared child; isolate state while sharing the execution ceiling. */
|
|
1086
|
+
workflow: async (name, inputs = {}, opts = {}) => {
|
|
1087
|
+
if (!deps.invokeWorkflow)
|
|
1088
|
+
throw new Error('当前执行器没有提供子工作流能力');
|
|
1089
|
+
if (typeof name !== 'string' ||
|
|
1090
|
+
!inputs ||
|
|
1091
|
+
typeof inputs !== 'object' ||
|
|
1092
|
+
Array.isArray(inputs))
|
|
1093
|
+
throw new Error('workflow需要工作流名和输入对象');
|
|
1094
|
+
countCall(); // Reserve the shared allowance before starting child side effects.
|
|
1095
|
+
const lane = currentLane();
|
|
1096
|
+
const index = (childCounters.get(lane) ?? 0) + 1;
|
|
1097
|
+
childCounters.set(lane, index);
|
|
1098
|
+
// Always visit the child's state before journal replay: files can disappear
|
|
1099
|
+
// and a child can have finished immediately before its parent was killed.
|
|
1100
|
+
const child = await deps.invokeWorkflow(name, inputs, `${lane}/w${index}`, opts.label);
|
|
1101
|
+
if (!child.reused)
|
|
1102
|
+
journal.markDiverged(lane);
|
|
1103
|
+
return journaled('workflow', opts.label ?? name, { name, inputs, site: `${lane}/w${index}` }, async () => child.value, true);
|
|
1104
|
+
},
|
|
1105
|
+
/**
|
|
1106
|
+
* 无屏障流水线:每项独立走完所有阶段,以确定车道隔离 journal。
|
|
1107
|
+
* 车道 `<父车道>/p<第几个>i<第几项>` 与调度顺序无关;不要用裸 Promise.all
|
|
1108
|
+
* 并发执行同车道的可重放调用。阶段参数为 (prev, item, index)。
|
|
1109
|
+
* 单项失败返回结构化错误,其他项继续;取消必须穿透,不能伪装为普通失败。
|
|
1110
|
+
*/
|
|
1111
|
+
pipeline: async (items, options) => {
|
|
1112
|
+
if (!Array.isArray(items)) {
|
|
1113
|
+
throw new Error('pipeline(items, …) 的第一个参数必须是数组');
|
|
1114
|
+
}
|
|
1115
|
+
if (!options || !Array.isArray(options.stages)) {
|
|
1116
|
+
throw new Error('pipeline 第二个参数必须包含 stages 数组');
|
|
1117
|
+
}
|
|
1118
|
+
const stages = options.stages;
|
|
1119
|
+
const concurrency = options.concurrency ?? scope.scheduler.config.pipelineConcurrency;
|
|
1120
|
+
if (!Number.isInteger(concurrency) ||
|
|
1121
|
+
concurrency < 1 ||
|
|
1122
|
+
concurrency > scope.scheduler.config.pipelineMaxConcurrency) {
|
|
1123
|
+
throw new Error(`pipeline concurrency 必须是 1..${scope.scheduler.config.pipelineMaxConcurrency} 的整数`);
|
|
1124
|
+
}
|
|
1125
|
+
const errorMode = options.errorMode ?? 'collect';
|
|
1126
|
+
if (errorMode !== 'collect' && errorMode !== 'fail-fast') {
|
|
1127
|
+
throw new Error('pipeline errorMode 必须是 collect 或 fail-fast');
|
|
1128
|
+
}
|
|
1129
|
+
if (stages.length === 0) {
|
|
1130
|
+
return {
|
|
1131
|
+
results: items.map((value, index) => ({ ok: true, index, value })),
|
|
1132
|
+
succeeded: items.length,
|
|
1133
|
+
failed: 0,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
const parent = currentLane();
|
|
1137
|
+
const blockId = nextPipelineId(parent);
|
|
1138
|
+
const lanes = [];
|
|
1139
|
+
const results = new Array(items.length);
|
|
1140
|
+
let nextIndex = 0;
|
|
1141
|
+
let completed = 0;
|
|
1142
|
+
let succeeded = 0;
|
|
1143
|
+
let failed = 0;
|
|
1144
|
+
let firstFailure;
|
|
1145
|
+
const worker = async () => {
|
|
1146
|
+
while (true) {
|
|
1147
|
+
if (errorMode === 'fail-fast' && firstFailure)
|
|
1148
|
+
return;
|
|
1149
|
+
const index = nextIndex++;
|
|
1150
|
+
if (index >= items.length)
|
|
1151
|
+
return;
|
|
1152
|
+
const item = items[index];
|
|
1153
|
+
const lane = `${parent}/p${blockId}i${index}`;
|
|
1154
|
+
lanes.push(lane);
|
|
1155
|
+
results[index] = await laneStore.run(lane, async () => {
|
|
1156
|
+
let acc = item;
|
|
1157
|
+
for (const stage of stages) {
|
|
1158
|
+
try {
|
|
1159
|
+
acc = await stage(acc, item, index);
|
|
1160
|
+
}
|
|
1161
|
+
catch (e) {
|
|
1162
|
+
ctx.signal?.throwIfAborted();
|
|
1163
|
+
if (e instanceof Error && e.name === 'AbortError')
|
|
1164
|
+
throw e;
|
|
1165
|
+
// 该项作废:其后阶段跳过,车道标脏(余下记录不得再被重放冒充)。
|
|
1166
|
+
journal.markDiverged(lane);
|
|
1167
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
1168
|
+
firstFailure ??= error;
|
|
1169
|
+
failed++;
|
|
1170
|
+
return {
|
|
1171
|
+
ok: false,
|
|
1172
|
+
index,
|
|
1173
|
+
error: structuredWorkflowError(error),
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
succeeded++;
|
|
1178
|
+
return { ok: true, index, value: acc };
|
|
1179
|
+
});
|
|
1180
|
+
completed++;
|
|
1181
|
+
emitProgressEvent({
|
|
1182
|
+
phase: options.label ?? 'pipeline',
|
|
1183
|
+
status: 'update',
|
|
1184
|
+
current: completed,
|
|
1185
|
+
total: items.length,
|
|
1186
|
+
succeeded,
|
|
1187
|
+
failed,
|
|
1188
|
+
message: `${options.label ?? 'pipeline'} ${completed}/${items.length}`,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
1193
|
+
// 任一子车道分叉 → 污染父车道:下游串行步骤依赖扇出的聚合结果,
|
|
1194
|
+
// 上游变了就不能再拿旧结果冒充。
|
|
1195
|
+
if (lanes.some((l) => journal.laneDiverged(l))) {
|
|
1196
|
+
journal.markDiverged(parent);
|
|
1197
|
+
}
|
|
1198
|
+
if (errorMode === 'fail-fast' && firstFailure) {
|
|
1199
|
+
throw new AggregateError([firstFailure], `${options.label ?? 'pipeline'} 在第一个失败项后停止调度:${firstFailure.message}`);
|
|
1200
|
+
}
|
|
1201
|
+
return { results, succeeded, failed };
|
|
1202
|
+
},
|
|
1203
|
+
structuredProgress: (update) => {
|
|
1204
|
+
emitProgressEvent(update);
|
|
1205
|
+
if (typeof update.message === 'string' && update.message.trim()) {
|
|
1206
|
+
void state
|
|
1207
|
+
.appendProgress(`ℹ ${update.message.trim()}`)
|
|
1208
|
+
.catch(() => undefined);
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
log: (message) => {
|
|
1212
|
+
progress(message);
|
|
1213
|
+
// log() is intentionally non-blocking for workflow authors, but retain
|
|
1214
|
+
// the same evidence on disk as the transient plugin_progress event.
|
|
1215
|
+
// The UI can be cleared; progress.md is what post-mortem diagnostics use.
|
|
1216
|
+
void state.appendProgress(`ℹ ${message}`).catch(() => undefined);
|
|
1217
|
+
},
|
|
1218
|
+
/**
|
|
1219
|
+
* 运行时追加一条产物声明。
|
|
1220
|
+
*
|
|
1221
|
+
* `meta.artifacts` 是执行前静态解析的纯字面量,写不出 `out/${session}/page-3.png`
|
|
1222
|
+
* 这种带循环下标的路径 —— 循环体里的逐迭代产物只能在这里登记。不登记的后果是
|
|
1223
|
+
* new-task 清理漏掉它们、completed 少报产物,且**表现为静默漏档而不是报错**。
|
|
1224
|
+
*/
|
|
1225
|
+
declareArtifact: (path) => {
|
|
1226
|
+
ctx.declaredArtifacts?.add(path);
|
|
1227
|
+
emitProgressEvent({
|
|
1228
|
+
phase: 'artifact',
|
|
1229
|
+
status: 'update',
|
|
1230
|
+
message: `登记产物:${path}`,
|
|
1231
|
+
});
|
|
1232
|
+
},
|
|
1233
|
+
/** 追加一条用户补充要求,与原始目标同等贯穿后续每次调用。 */
|
|
1234
|
+
addGoalSupplement: async (text) => {
|
|
1235
|
+
const t = String(text ?? '').trim();
|
|
1236
|
+
if (!t)
|
|
1237
|
+
return;
|
|
1238
|
+
ctx.goalSupplements ??= [];
|
|
1239
|
+
if (!ctx.goalSupplements.includes(t))
|
|
1240
|
+
ctx.goalSupplements.push(t);
|
|
1241
|
+
await state.appendSupplement(t);
|
|
1242
|
+
},
|
|
1243
|
+
/** 让具体工作流把确认后的契约交回其外层控制器;Runner 不解释契约内容。 */
|
|
1244
|
+
syncManagedContract: async (input) => {
|
|
1245
|
+
if (!ctx.managedBridge) {
|
|
1246
|
+
throw new Error('syncManagedContract 只能在外层运行时管理的工作流中使用');
|
|
1247
|
+
}
|
|
1248
|
+
const update = await ctx.managedBridge.updateContract(input);
|
|
1249
|
+
if (ctx.managedExecution) {
|
|
1250
|
+
ctx.managedExecution.version = update.version;
|
|
1251
|
+
ctx.managedExecution.anchor = update.anchor;
|
|
1252
|
+
}
|
|
1253
|
+
return update.value;
|
|
1254
|
+
},
|
|
1255
|
+
};
|
|
1256
|
+
const wf = {
|
|
1257
|
+
input: Object.freeze({ values: args, resources: deps.inputResourceMap ?? {} }),
|
|
1258
|
+
runtime: Object.freeze({
|
|
1259
|
+
workflowName: deps.workflowName,
|
|
1260
|
+
workflowDir: workflowDir ?? '',
|
|
1261
|
+
cwd: effectiveWorkflowCwd(ctx),
|
|
1262
|
+
sessionId: session,
|
|
1263
|
+
taskId: task,
|
|
1264
|
+
runId,
|
|
1265
|
+
rootRunId: scope.rootRunId,
|
|
1266
|
+
goal: ctx.goalAnchor ?? '',
|
|
1267
|
+
}),
|
|
1268
|
+
model: Object.freeze({
|
|
1269
|
+
generate: capabilities.llm,
|
|
1270
|
+
agent: capabilities.agent,
|
|
1271
|
+
skill: capabilities.skill,
|
|
1272
|
+
require: capabilities.requireModel,
|
|
1273
|
+
}),
|
|
1274
|
+
tools: Object.freeze({ call: capabilities.tool, callResult: capabilities.toolResult }),
|
|
1275
|
+
exec: Object.freeze({ shell: capabilities.sh, file: capabilities.execFile }),
|
|
1276
|
+
fs: Object.freeze({
|
|
1277
|
+
exists: capabilities.exists,
|
|
1278
|
+
readText: capabilities.readText,
|
|
1279
|
+
readJson: capabilities.readJson,
|
|
1280
|
+
writeText: capabilities.writeText,
|
|
1281
|
+
writeJson: capabilities.writeJson,
|
|
1282
|
+
mkdir: capabilities.mkdir,
|
|
1283
|
+
}),
|
|
1284
|
+
flow: Object.freeze({
|
|
1285
|
+
pipeline: capabilities.pipeline,
|
|
1286
|
+
child: capabilities.workflow,
|
|
1287
|
+
ask: capabilities.ask,
|
|
1288
|
+
}),
|
|
1289
|
+
check: Object.freeze({ verify: capabilities.verify }),
|
|
1290
|
+
state: Object.freeze({
|
|
1291
|
+
budget: capabilities.budget,
|
|
1292
|
+
resetModelContext: capabilities.resetContext,
|
|
1293
|
+
addGoalSupplement: capabilities.addGoalSupplement,
|
|
1294
|
+
syncManagedContract: capabilities.syncManagedContract,
|
|
1295
|
+
}),
|
|
1296
|
+
output: Object.freeze({
|
|
1297
|
+
message: capabilities.echo,
|
|
1298
|
+
progress: capabilities.structuredProgress,
|
|
1299
|
+
artifact: capabilities.declareArtifact,
|
|
1300
|
+
}),
|
|
1301
|
+
};
|
|
1302
|
+
assertWorkflowContextShape(wf);
|
|
1303
|
+
return Object.freeze(wf);
|
|
1304
|
+
}
|