minimal-agent 0.10.0 → 0.12.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 +11 -3
- package/package.json +52 -5
- package/plugins/computer_use/.claude-plugin/plugin.json +6 -0
- package/plugins/computer_use/README.md +94 -0
- package/plugins/computer_use/app/_template.md +142 -0
- package/plugins/computer_use/app/chrome.md +165 -0
- package/plugins/computer_use/app/finder.md +76 -0
- package/plugins/computer_use/app/macos-system-settings.md +81 -0
- package/plugins/computer_use/app/microsoft-excel.md +80 -0
- package/plugins/computer_use/app/microsoft-powerpoint.md +80 -0
- package/plugins/computer_use/app/microsoft-word.md +79 -0
- package/plugins/computer_use/app/wechat.md +413 -0
- package/plugins/computer_use/app/weread.md +319 -0
- package/plugins/computer_use/commands/computeruse.md +25 -0
- package/plugins/computer_use/commands/cu.md +9 -0
- package/plugins/computer_use/plugin.js +26 -0
- package/plugins/computer_use/plugin.ts +30 -0
- package/plugins/computer_use/src/capture.js +90 -0
- package/plugins/computer_use/src/capture.ts +104 -0
- package/plugins/computer_use/src/commands.js +91 -0
- package/plugins/computer_use/src/commands.ts +103 -0
- package/plugins/computer_use/src/daemon.js +134 -0
- package/plugins/computer_use/src/daemon.ts +161 -0
- package/plugins/computer_use/src/manuals.js +135 -0
- package/plugins/computer_use/src/manuals.ts +159 -0
- package/plugins/computer_use/src/verify.js +168 -0
- package/plugins/computer_use/src/verify.ts +219 -0
- package/plugins/computer_use/test/commands.test.ts +73 -0
- package/plugins/cron-runner/plugin.ts +38 -0
- package/plugins/cron-runner/src/commands.ts +373 -0
- package/plugins/cron-runner/src/cronTime.ts +155 -0
- package/plugins/cron-runner/src/lock.ts +79 -0
- package/plugins/cron-runner/src/runs.ts +99 -0
- package/plugins/cron-runner/src/scheduleEngine.ts +76 -0
- package/plugins/cron-runner/src/scheduler.ts +376 -0
- package/plugins/cron-runner/src/schema.ts +139 -0
- package/plugins/cron-runner/src/service.ts +193 -0
- package/plugins/cron-runner/src/store.ts +333 -0
- package/plugins/cron-runner/src/types.ts +141 -0
- package/plugins/cron-runner/test/cronTime.test.ts +41 -0
- package/plugins/cron-runner/test/plugin-integration.test.ts +47 -0
- package/plugins/cron-runner/test/scheduleEngine.test.ts +41 -0
- package/plugins/cron-runner/test/service.test.ts +76 -0
- package/plugins/cron-runner/test/store-and-scheduler.test.ts +501 -0
- package/plugins/workflow-runner/plugin.js +56 -2
- package/plugins/workflow-runner/plugin.ts +113 -0
- package/plugins/workflow-runner/src/agentCommon.js +43 -4
- package/plugins/workflow-runner/src/agentCommon.ts +435 -0
- package/plugins/workflow-runner/src/controlFlow/branch.js +40 -0
- package/plugins/workflow-runner/src/controlFlow/branch.ts +46 -0
- package/plugins/workflow-runner/src/controlFlow/loop.js +211 -0
- package/plugins/workflow-runner/src/controlFlow/loop.ts +248 -0
- package/plugins/workflow-runner/src/controlFlow/pause.js +129 -0
- package/plugins/workflow-runner/src/controlFlow/pause.ts +141 -0
- package/plugins/workflow-runner/src/controlFlow/signal.js +21 -0
- package/plugins/workflow-runner/src/controlFlow/signal.ts +29 -0
- package/plugins/workflow-runner/src/controlFlow/types.js +18 -0
- package/plugins/workflow-runner/src/controlFlow/types.ts +63 -0
- package/plugins/workflow-runner/src/dslManifest.ts +127 -0
- package/plugins/workflow-runner/src/expressions.ts +622 -0
- package/plugins/workflow-runner/src/index.js +13 -4
- package/plugins/workflow-runner/src/index.ts +338 -0
- package/plugins/workflow-runner/src/jsonPayload.ts +84 -0
- package/plugins/workflow-runner/src/loader.js +23 -0
- package/plugins/workflow-runner/src/loader.ts +721 -0
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +64 -0
- package/plugins/workflow-runner/src/recovery.ts +149 -0
- package/plugins/workflow-runner/src/runner.js +75 -659
- package/plugins/workflow-runner/src/runner.ts +604 -0
- package/plugins/workflow-runner/src/stepExecutors/agent.ts +234 -0
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +50 -0
- package/plugins/workflow-runner/src/stepExecutors/echo.js +31 -0
- package/plugins/workflow-runner/src/stepExecutors/echo.ts +38 -0
- package/plugins/workflow-runner/src/stepExecutors/llm.ts +133 -0
- package/plugins/workflow-runner/src/stepExecutors/set.js +49 -0
- package/plugins/workflow-runner/src/stepExecutors/set.ts +56 -0
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +140 -0
- package/plugins/workflow-runner/src/stepExecutors/tool.js +80 -8
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +256 -0
- package/plugins/workflow-runner/src/stepExecutors/verify.ts +68 -0
- package/plugins/workflow-runner/src/stepPipeline.js +277 -0
- package/plugins/workflow-runner/src/stepPipeline.ts +366 -0
- package/plugins/workflow-runner/src/taskIdentity.ts +405 -0
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.js +37 -1
- package/plugins/workflow-runner/src/types.ts +602 -0
- package/plugins/workflow-runner/src/workflowState.ts +733 -0
- package/plugins/workflow-runner/test/agent.test.ts +587 -0
- package/plugins/workflow-runner/test/agentLlmError.test.ts +373 -0
- package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +65 -0
- package/plugins/workflow-runner/test/captureMisbind.test.ts +99 -0
- package/plugins/workflow-runner/test/cli.e2e.test.ts +147 -0
- package/plugins/workflow-runner/test/context-files.test.ts +320 -0
- package/plugins/workflow-runner/test/e2e.test.ts +370 -0
- package/plugins/workflow-runner/test/engineRobustness.test.ts +226 -0
- package/plugins/workflow-runner/test/expressions.test.ts +397 -0
- package/plugins/workflow-runner/test/foundation.test.ts +126 -0
- package/plugins/workflow-runner/test/graceful.test.ts +139 -0
- package/plugins/workflow-runner/test/greedyInputs.load.test.ts +47 -0
- package/plugins/workflow-runner/test/hostCapability.test.ts +108 -0
- package/plugins/workflow-runner/test/illustrate.load.test.ts +183 -0
- package/plugins/workflow-runner/test/jsonPayload.test.ts +83 -0
- package/plugins/workflow-runner/test/loader.test.ts +617 -0
- package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +33 -0
- package/plugins/workflow-runner/test/multimodal.test.ts +342 -0
- package/plugins/workflow-runner/test/output-schema.test.ts +213 -0
- package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +87 -0
- package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +269 -0
- package/plugins/workflow-runner/test/ppt.load.test.ts +1220 -0
- package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +220 -0
- package/plugins/workflow-runner/test/recovery.test.ts +265 -0
- package/plugins/workflow-runner/test/runner.test.ts +911 -0
- package/plugins/workflow-runner/test/runnerPreflight.test.ts +379 -0
- package/plugins/workflow-runner/test/skillTurnDone.test.ts +71 -0
- package/plugins/workflow-runner/test/stateLifecycle.test.ts +150 -0
- package/plugins/workflow-runner/test/stepPipeline.test.ts +114 -0
- package/plugins/workflow-runner/test/taskArbitration.test.ts +338 -0
- package/plugins/workflow-runner/test/taskIdentity.test.ts +237 -0
- package/plugins/workflow-runner/test/taskLifecycle.test.ts +270 -0
- package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +221 -0
- package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +29 -0
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/plugins/workflow-runner/test/webppt.load.test.ts +250 -0
- package/plugins/workflow-runner/test/word.load.test.ts +208 -0
- package/plugins/workflow-runner/test/workflows.smoke.test.ts +76 -0
- package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +207 -0
- package/skills/browser-automation/SKILL.md +9 -19
- package/skills/web-crawler/SKILL.md +9 -11
- package/src/askBridge.ts +249 -0
- package/src/bootstrap/cwdArg.js +10 -3
- package/src/bootstrap/cwdArg.ts +31 -0
- package/src/bootstrap/workingDir.ts +48 -0
- package/src/cli/args.js +70 -7
- package/src/cli/args.ts +146 -0
- package/src/cli/computerUse.js +185 -0
- package/src/cli/computerUse.ts +221 -0
- package/src/cli/configMenu.js +274 -202
- package/src/cli/configMenu.tsx +521 -0
- package/src/cli/memoryAccess.js +45 -0
- package/src/cli/memoryAccess.ts +53 -0
- package/src/cli/print.js +119 -5
- package/src/cli/print.ts +597 -0
- package/src/cli/quickModelSetup.js +60 -0
- package/src/cli/quickModelSetup.tsx +96 -0
- package/src/cli/streamJson.js +80 -4
- package/src/cli/streamJson.ts +433 -0
- package/src/cli/wizards/advisorWizard.js +1 -0
- package/src/cli/wizards/advisorWizard.tsx +195 -0
- package/src/cli/wizards/agentRuntimeWizard.js +84 -0
- package/src/cli/wizards/agentRuntimeWizard.tsx +134 -0
- package/src/cli/wizards/browserWizard.js +55 -0
- package/src/cli/wizards/browserWizard.tsx +77 -0
- package/src/cli/wizards/contextAdvisorWizard.js +84 -0
- package/src/cli/wizards/contextAdvisorWizard.tsx +124 -0
- package/src/cli/wizards/fieldForm.js +7 -4
- package/src/cli/wizards/fieldForm.tsx +250 -0
- package/src/cli/wizards/imageWizard.js +4 -5
- package/src/cli/wizards/imageWizard.tsx +96 -0
- package/src/cli/wizards/openRouterWizard.tsx +48 -0
- package/src/cli/wizards/providerPresets.js +1 -8
- package/src/cli/wizards/providerPresets.ts +106 -0
- package/src/cli/wizards/providerWizard.js +152 -205
- package/src/cli/wizards/providerWizard.tsx +266 -0
- package/src/cli/wizards/searchWizard.tsx +47 -0
- package/src/cli/wizards/ttsWizard.js +4 -3
- package/src/cli/wizards/ttsWizard.tsx +61 -0
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configDiagnostics.js +117 -0
- package/src/config/configDiagnostics.ts +166 -0
- package/src/config/configFile.js +52 -32
- package/src/config/configFile.ts +633 -0
- package/src/config/configManagement.js +126 -0
- package/src/config/configManagement.ts +159 -0
- package/src/config/configService.js +66 -11
- package/src/config/configService.ts +554 -0
- package/src/config/numericLimits.js +61 -0
- package/src/config/numericLimits.ts +80 -0
- package/src/config.ts +109 -0
- package/src/context/agentHome.ts +11 -0
- package/src/context/archive.ts +144 -0
- package/src/context/budgetPlan.js +54 -0
- package/src/context/budgetPlan.ts +88 -0
- package/src/context/compact.js +179 -106
- package/src/context/compact.ts +584 -0
- package/src/context/contextBundle.js +119 -0
- package/src/context/contextBundle.ts +176 -0
- package/src/context/contextLock.ts +84 -0
- package/src/context/microCompactLite.ts +218 -0
- package/src/context/persistContext.ts +177 -0
- package/src/context/reactiveCompact.js +19 -4
- package/src/context/reactiveCompact.ts +240 -0
- package/src/context/recentDirs.ts +42 -0
- package/src/context/sessionPath.ts +206 -0
- package/src/context/sessionRegistry.js +8 -2
- package/src/context/sessionRegistry.ts +414 -0
- package/src/context/snipCompact.ts +163 -0
- package/src/context/tokenCounter.ts +106 -0
- package/src/context/workspaceIndex.js +2 -2
- package/src/context/workspaceIndex.ts +751 -0
- package/src/entry.compiled.tsx +20 -0
- package/src/harness/claimValidation.js +9 -0
- package/src/harness/claimValidation.ts +22 -0
- package/src/harness/evidenceLedger.js +92 -0
- package/src/harness/evidenceLedger.ts +96 -0
- package/src/harness/stallGuard.js +60 -0
- package/src/harness/stallGuard.ts +81 -0
- package/src/humanIntervention.js +129 -0
- package/src/humanIntervention.ts +216 -0
- package/src/infra/verification.ts +295 -0
- package/src/irreversible/rules.js +216 -0
- package/src/irreversible/rules.ts +261 -0
- package/src/llm/client.js +40 -3
- package/src/llm/client.ts +621 -0
- package/src/llm/promptCache.js +27 -0
- package/src/llm/promptCache.ts +41 -0
- package/src/llm/providers/anthropicFormat.js +41 -5
- package/src/llm/providers/anthropicFormat.ts +290 -0
- package/src/llm/providers/openaiFormat.js +186 -8
- package/src/llm/providers/openaiFormat.ts +428 -0
- package/src/loop.js +614 -63
- package/src/loop.ts +1454 -0
- package/src/main.js +67 -10
- package/src/main.tsx +344 -0
- package/src/memory/codec.js +204 -0
- package/src/memory/codec.ts +249 -0
- package/src/memory/constants.js +34 -0
- package/src/memory/constants.ts +50 -0
- package/src/memory/episodicMemory.js +684 -0
- package/src/memory/episodicMemory.ts +815 -0
- package/src/memory/fileStore.js +148 -0
- package/src/memory/fileStore.ts +184 -0
- package/src/memory/memoryService.js +570 -0
- package/src/memory/memoryService.ts +859 -0
- package/src/memory/migration.js +128 -0
- package/src/memory/migration.ts +160 -0
- package/src/memory/repository.js +789 -0
- package/src/memory/repository.ts +893 -0
- package/src/memory/retrieval.js +108 -0
- package/src/memory/retrieval.ts +145 -0
- package/src/memory/searchIndex.js +107 -0
- package/src/memory/searchIndex.ts +153 -0
- package/src/memory/topicView.js +82 -0
- package/src/memory/topicView.ts +95 -0
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.js +2 -0
- package/src/memory/types.ts +186 -0
- package/src/messageQueue.ts +123 -0
- package/src/multimodal.js +128 -46
- package/src/multimodal.ts +349 -0
- package/src/plugin-sdk.js +21 -1
- package/src/plugin-sdk.ts +134 -0
- package/src/plugins/builtinPluginIds.js +47 -0
- package/src/plugins/builtinPluginIds.ts +51 -0
- package/src/plugins/builtinRegistry.ts +43 -0
- package/src/plugins/commandRouter.js +40 -26
- package/src/plugins/commandRouter.ts +261 -0
- package/src/plugins/hookEngine.ts +341 -0
- package/src/plugins/pluginApi.ts +133 -0
- package/src/plugins/pluginLifecycle.ts +34 -0
- package/src/plugins/pluginLoader.ts +147 -0
- package/src/plugins/pluginManifest.js +127 -0
- package/src/plugins/pluginManifest.ts +152 -0
- package/src/plugins/pluginRunner.js +47 -2
- package/src/plugins/pluginRunner.ts +207 -0
- package/src/plugins/transcript.ts +208 -0
- package/src/prompts/lint.js +169 -0
- package/src/prompts/lint.ts +220 -0
- package/src/prompts/pluginList.js +32 -16
- package/src/prompts/pluginList.ts +86 -0
- package/src/prompts/policyRegistry.js +318 -0
- package/src/prompts/policyRegistry.ts +358 -0
- package/src/prompts/projectInstructions.js +24 -0
- package/src/prompts/projectInstructions.ts +81 -0
- package/src/prompts/promptAssembly.js +311 -202
- package/src/prompts/promptAssembly.ts +510 -0
- package/src/prompts/skillList.js +106 -45
- package/src/prompts/skillList.ts +198 -0
- package/src/prompts/system.js +31 -6
- package/src/prompts/system.ts +81 -0
- package/src/runtime/agentRuntimeContext.js +53 -0
- package/src/runtime/agentRuntimeContext.ts +112 -0
- package/src/session/runAgentTurn.js +71 -12
- package/src/session/runAgentTurn.ts +181 -0
- package/src/session/runTurn.js +29 -0
- package/src/session/runTurn.ts +160 -0
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +79 -3
- package/src/session/sessionState.ts +134 -0
- package/src/toolGate.ts +50 -0
- package/src/tools/advisor/advisor.js +76 -15
- package/src/tools/advisor/advisor.ts +512 -0
- package/src/tools/agent/agent.js +245 -29
- package/src/tools/agent/agent.ts +837 -0
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +20 -8
- package/src/tools/agent/agentTypes.ts +94 -0
- package/src/tools/agent/coordinator.ts +111 -0
- package/src/tools/agent/worktree.ts +296 -0
- package/src/tools/ask/ask.js +17 -0
- package/src/tools/ask/ask.ts +122 -0
- package/src/tools/bash/attachImage.js +223 -0
- package/src/tools/bash/attachImage.ts +281 -0
- package/src/tools/bash/bash.js +132 -14
- package/src/tools/bash/bash.ts +642 -0
- package/src/tools/bash/semantics.ts +103 -0
- package/src/tools/bash/warnings.js +17 -11
- package/src/tools/bash/warnings.ts +117 -0
- package/src/tools/capabilities.js +42 -0
- package/src/tools/capabilities.ts +69 -0
- package/src/tools/computeruse/accessibilityPermission.js +261 -0
- package/src/tools/computeruse/accessibilityPermission.ts +294 -0
- package/src/tools/computeruse/axSearch.js +96 -0
- package/src/tools/computeruse/axSearch.ts +120 -0
- package/src/tools/computeruse/axSnap.js +272 -0
- package/src/tools/computeruse/axSnap.ts +349 -0
- package/src/tools/computeruse/capabilityLabels.js +95 -0
- package/src/tools/computeruse/capabilityLabels.ts +115 -0
- package/src/tools/computeruse/computerUseService.js +2235 -0
- package/src/tools/computeruse/computerUseService.ts +2686 -0
- package/src/tools/computeruse/computeruse.js +2742 -0
- package/src/tools/computeruse/computeruse.ts +3265 -0
- package/src/tools/computeruse/diagnosticTrace.js +154 -0
- package/src/tools/computeruse/diagnosticTrace.ts +195 -0
- package/src/tools/computeruse/diagnostics.js +369 -0
- package/src/tools/computeruse/diagnostics.ts +453 -0
- package/src/tools/computeruse/grants.js +53 -0
- package/src/tools/computeruse/grants.ts +70 -0
- package/src/tools/computeruse/humanBoundary.js +159 -0
- package/src/tools/computeruse/humanBoundary.ts +199 -0
- package/src/tools/computeruse/intervention.js +77 -0
- package/src/tools/computeruse/intervention.ts +110 -0
- package/src/tools/computeruse/manualSchema.js +709 -0
- package/src/tools/computeruse/manualSchema.ts +916 -0
- package/src/tools/computeruse/observationImages.js +705 -0
- package/src/tools/computeruse/observationImages.ts +889 -0
- package/src/tools/computeruse/protocol.js +60 -0
- package/src/tools/computeruse/protocol.ts +515 -0
- package/src/tools/computeruse/telemetryLog.js +57 -0
- package/src/tools/computeruse/telemetryLog.ts +67 -0
- package/src/tools/computeruse/vision/appGuide.js +325 -0
- package/src/tools/computeruse/vision/appGuide.ts +482 -0
- package/src/tools/computeruse/vision/focus.js +291 -0
- package/src/tools/computeruse/vision/focus.ts +382 -0
- package/src/tools/computeruse/vision/frames.js +248 -0
- package/src/tools/computeruse/vision/frames.ts +329 -0
- package/src/tools/computeruse/vision/geometry.js +131 -0
- package/src/tools/computeruse/vision/geometry.ts +163 -0
- package/src/tools/computeruse/vision/imaging.js +201 -0
- package/src/tools/computeruse/vision/imaging.ts +281 -0
- package/src/tools/computeruse/vision/png.js +274 -0
- package/src/tools/computeruse/vision/png.ts +317 -0
- package/src/tools/computeruse/vision/raster.js +320 -0
- package/src/tools/computeruse/vision/raster.ts +408 -0
- package/src/tools/computeruse/vision/settle.js +131 -0
- package/src/tools/computeruse/vision/settle.ts +220 -0
- package/src/tools/computeruse/vision/telemetry.js +196 -0
- package/src/tools/computeruse/vision/telemetry.ts +241 -0
- package/src/tools/computeruse/visionOcr.js +115 -0
- package/src/tools/computeruse/visionOcr.ts +128 -0
- package/src/tools/computeruse/waitFor.js +237 -0
- package/src/tools/computeruse/waitFor.ts +291 -0
- package/src/tools/download/download.js +10 -6
- package/src/tools/download/download.ts +187 -0
- package/src/tools/edit/edit.js +30 -18
- package/src/tools/edit/edit.ts +349 -0
- package/src/tools/edit/multi-edit.js +25 -14
- package/src/tools/edit/multi-edit.ts +223 -0
- package/src/tools/effectiveToolSet.js +73 -0
- package/src/tools/effectiveToolSet.ts +91 -0
- package/src/tools/genimage/genimage.ts +257 -0
- package/src/tools/genspeech/genspeech.ts +83 -0
- package/src/tools/glob/glob.js +176 -29
- package/src/tools/glob/glob.ts +283 -0
- package/src/tools/grep/grep.js +61 -19
- package/src/tools/grep/grep.ts +239 -0
- package/src/tools/grep/rgPath.ts +206 -0
- package/src/tools/index.js +99 -5
- package/src/tools/index.ts +293 -0
- package/src/tools/lsp/config.js +113 -0
- package/src/tools/lsp/config.ts +113 -0
- package/src/tools/lsp/lsp.js +256 -0
- package/src/tools/lsp/lsp.ts +274 -0
- package/src/tools/lsp/service.js +303 -0
- package/src/tools/lsp/service.ts +296 -0
- package/src/tools/memory/memory.js +520 -0
- package/src/tools/memory/memory.ts +625 -0
- package/src/tools/memory/policy.js +53 -0
- package/src/tools/memory/policy.ts +72 -0
- package/src/tools/notebook/notebookEdit.js +114 -0
- package/src/tools/notebook/notebookEdit.ts +126 -0
- package/src/tools/powershell/powershell.js +210 -0
- package/src/tools/powershell/powershell.ts +239 -0
- package/src/tools/presentresult/presentresult.js +145 -0
- package/src/tools/presentresult/presentresult.ts +156 -0
- package/src/tools/read/read.js +371 -163
- package/src/tools/read/read.ts +558 -0
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +51 -0
- package/src/tools/shared/fileActivity.ts +66 -0
- package/src/tools/shared/fileMutationBus.js +13 -0
- package/src/tools/shared/fileMutationBus.ts +14 -0
- package/src/tools/shared/fileState.js +73 -65
- package/src/tools/shared/fileState.ts +145 -0
- package/src/tools/shared/fileUtils.js +81 -4
- package/src/tools/shared/fileUtils.ts +504 -0
- package/src/tools/shared/killTree.ts +50 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/schemas.ts +20 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +275 -0
- package/src/tools/tasks/backgroundTaskService.ts +369 -0
- package/src/tools/tasks/processEvidence.js +24 -0
- package/src/tools/tasks/processEvidence.ts +34 -0
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +113 -0
- package/src/tools/tasks/taskOutput.ts +145 -0
- package/src/tools/tasks/taskStop.js +74 -0
- package/src/tools/tasks/taskStop.ts +99 -0
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/types.js +1 -1
- package/src/tools/types.ts +24 -0
- package/src/tools/webbrowser/agentBrowserPath.ts +286 -0
- package/src/tools/webbrowser/browser.js +214 -11
- package/src/tools/webbrowser/browser.ts +293 -0
- package/src/tools/webbrowser/browserControl.js +153 -48
- package/src/tools/webbrowser/browserControl.ts +262 -0
- package/src/tools/webbrowser/browserDaemon.js +147 -39
- package/src/tools/webbrowser/browserDaemon.ts +270 -0
- package/src/tools/webbrowser/browserOutcome.js +273 -0
- package/src/tools/webbrowser/browserOutcome.ts +306 -0
- package/src/tools/webbrowser/browserRunRegistry.js +150 -0
- package/src/tools/webbrowser/browserRunRegistry.ts +179 -0
- package/src/tools/webbrowser/browserService.js +20 -6
- package/src/tools/webbrowser/browserService.ts +56 -0
- package/src/tools/webbrowser/browserStream.js +30 -1
- package/src/tools/webbrowser/browserStream.ts +216 -0
- package/src/tools/webbrowser/browserTabs.js +143 -0
- package/src/tools/webbrowser/browserTabs.ts +210 -0
- package/src/tools/webbrowser/webbrowser.js +831 -150
- package/src/tools/webbrowser/webbrowser.ts +1537 -0
- package/src/tools/webfetch/preapproved.ts +274 -0
- package/src/tools/webfetch/webfetch.js +24 -23
- package/src/tools/webfetch/webfetch.ts +383 -0
- package/src/tools/websearch/websearch.js +12 -11
- package/src/tools/websearch/websearch.ts +292 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +27 -44
- package/src/tools/write/write.ts +136 -0
- package/src/types.ts +960 -0
- package/src/ui/App.js +216 -12
- package/src/ui/App.tsx +879 -0
- package/src/ui/AskQuestion.tsx +300 -0
- package/src/ui/InputBox.js +77 -1
- package/src/ui/InputBox.tsx +567 -0
- package/src/ui/LiveArea.tsx +344 -0
- package/src/ui/MessageList.js +27 -6
- package/src/ui/MessageList.tsx +559 -0
- package/src/ui/ModelPicker.tsx +107 -0
- package/src/ui/QueuedMessagesBanner.tsx +51 -0
- package/src/ui/Root.js +6 -8
- package/src/ui/Root.tsx +100 -0
- package/src/ui/SessionPicker.tsx +169 -0
- package/src/ui/StatusLine.js +22 -1
- package/src/ui/StatusLine.tsx +145 -0
- package/src/ui/WorkspacePicker.tsx +84 -0
- package/src/ui/advisorCommand.js +54 -0
- package/src/ui/advisorCommand.ts +63 -0
- package/src/ui/computerCommand.js +19 -0
- package/src/ui/computerCommand.ts +35 -0
- package/src/ui/figures.ts +45 -0
- package/src/ui/hooks/askSlot.ts +112 -0
- package/src/ui/hooks/useChat.js +271 -7
- package/src/ui/hooks/useChat.ts +1578 -0
- package/src/ui/hooks/useInputHistory.ts +235 -0
- package/src/ui/hooks/usePasteHandler.ts +220 -0
- package/src/ui/hooks/useTerminalSize.ts +41 -0
- package/src/ui/hooks/useTextBuffer.ts +108 -0
- package/src/ui/hooks/useTokenUsage.ts +44 -0
- package/src/ui/liveViewport.ts +189 -0
- package/src/ui/markdown.ts +230 -0
- package/src/ui/textBuffer.ts +239 -0
- package/src/ui/theme.ts +35 -0
- package/src/utils/clipboardImage.js +358 -0
- package/src/utils/clipboardImage.ts +420 -0
- package/src/utils/debugLog.ts +50 -0
- package/src/utils/greenRoot.ts +35 -0
- package/src/utils/packageRoot.ts +40 -0
- package/src/utils/pathContains.js +28 -0
- package/src/utils/pathContains.ts +31 -0
- package/src/utils/resourcePaths.ts +71 -0
- package/src/utils/stripThink.ts +49 -0
- package/src/utils/zodToJson.ts +73 -0
- package/src/web/browserBridge.js +365 -66
- package/src/web/browserBridge.ts +672 -0
- package/src/web/protocol.ts +1804 -0
- package/vendor/computer-use/arm64-darwin/cu-helperd +0 -0
- package/workflows/ppt.yaml +13 -2
- package/workflows/wechat-reading-shots.yaml +315 -0
- package/workflows/word.yaml +11 -1
- package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
- package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
- package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
- /package/skills/ai-image/scripts/{generate_test.mjs → generate.selftest.mjs} +0 -0
- /package/skills/ai-image/scripts/{generate_from_plan_test.mjs → generate_from_plan.selftest.mjs} +0 -0
package/src/loop.ts
ADDED
|
@@ -0,0 +1,1454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* src/loop.ts —— Agent 主循环(query loop)
|
|
4
|
+
* ------------------------------------------------------------
|
|
5
|
+
* 这是整个 agent 的"心脏"。一句话描述:
|
|
6
|
+
*
|
|
7
|
+
* 用户说话 → 调 LLM → LLM 想调工具就执行工具 → 把结果喂回去 → 继续调 LLM →
|
|
8
|
+
* 直到 LLM 不再调工具,输出最终回答。
|
|
9
|
+
*
|
|
10
|
+
* 和 kakadeai 主仓库 query.ts 用同样的 AsyncGenerator 模式:
|
|
11
|
+
* - 函数是 async function*,可以 yield 各种事件给 UI
|
|
12
|
+
* - UI 通过 for await 拿到事件,把它们映射到 React state
|
|
13
|
+
*
|
|
14
|
+
* 循环每轮(turn)做的事:
|
|
15
|
+
* a. 自动压缩检查(防止 context 撑爆)
|
|
16
|
+
* b. 调 LLM,把流式响应组装成完整的 assistant message
|
|
17
|
+
* c. 如果 assistant 不调工具 → 结束,return
|
|
18
|
+
* d. 否则并行执行所有工具调用(OpenAI 协议要求 tool 消息按 tool_calls 顺序回填)
|
|
19
|
+
* e. 进入下一轮(让模型看到 tool_result 后继续推理)
|
|
20
|
+
*
|
|
21
|
+
* 失控保护:maxTurns(默认 50)。
|
|
22
|
+
* 中断支持:AbortSignal 透传到 chat() 和 tool.call()。
|
|
23
|
+
*
|
|
24
|
+
* "工作过程显示"事件:
|
|
25
|
+
* - stage_change:thinking / streaming / tool_executing / compacting
|
|
26
|
+
* - reasoning:思维链流式增量(仅字符串型 field)
|
|
27
|
+
* ============================================================
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import crypto from 'node:crypto';
|
|
31
|
+
import { autoCompactIfNeeded, getCompactThreshold } from './context/compact.js';
|
|
32
|
+
import { microCompact, incrementTurn, expireOldEntries } from './context/microCompactLite.js';
|
|
33
|
+
import {
|
|
34
|
+
isPromptTooLongError,
|
|
35
|
+
reactiveCompactIfApplicable,
|
|
36
|
+
} from './context/reactiveCompact.js';
|
|
37
|
+
import { snipCompactIfNeeded } from './context/snipCompact.js';
|
|
38
|
+
import {
|
|
39
|
+
countMessagesTokens,
|
|
40
|
+
countTextTokens,
|
|
41
|
+
estimateToolsTokens,
|
|
42
|
+
} from './context/tokenCounter.js';
|
|
43
|
+
import { buildContextBudgetPlan, fitToolsForEmergency } from './context/budgetPlan.js';
|
|
44
|
+
import { resizeContextBundle, type ContextBundle } from './context/contextBundle.js';
|
|
45
|
+
import { chat as defaultChat } from './llm/client.js';
|
|
46
|
+
import { buildUserContent } from './multimodal.js';
|
|
47
|
+
import type { SessionState } from './session/sessionState.js';
|
|
48
|
+
import {
|
|
49
|
+
ALL_TOOLS,
|
|
50
|
+
executeTool as defaultExecuteTool,
|
|
51
|
+
getToolByName,
|
|
52
|
+
getToolGateDetails,
|
|
53
|
+
} from './tools/index.js';
|
|
54
|
+
import type { ToolGate } from './toolGate.js';
|
|
55
|
+
import type {
|
|
56
|
+
DeliverableManifest,
|
|
57
|
+
ImageDetail,
|
|
58
|
+
ImageInputSpec,
|
|
59
|
+
LoopEvent,
|
|
60
|
+
Message,
|
|
61
|
+
Provider,
|
|
62
|
+
Tool,
|
|
63
|
+
ToolCall,
|
|
64
|
+
ToolCallContext,
|
|
65
|
+
ToolResult,
|
|
66
|
+
} from './types.js';
|
|
67
|
+
import { configService } from './config/configService.js';
|
|
68
|
+
import { capabilityForToolName } from './tools/capabilities.js';
|
|
69
|
+
import { createEffectiveToolSet } from './tools/effectiveToolSet.js';
|
|
70
|
+
import { hydrateSessionWorktree } from './tools/worktree/sessionWorktree.js';
|
|
71
|
+
import {
|
|
72
|
+
capabilitiesForTools,
|
|
73
|
+
enabledPolicyIds,
|
|
74
|
+
} from './prompts/policyRegistry.js';
|
|
75
|
+
import { EvidenceLedger } from './harness/evidenceLedger.js';
|
|
76
|
+
import { StallGuard } from './harness/stallGuard.js';
|
|
77
|
+
import { validateFinalClaims } from './harness/claimValidation.js';
|
|
78
|
+
import {
|
|
79
|
+
createAgentRuntimeContext,
|
|
80
|
+
type AgentRuntimeContext,
|
|
81
|
+
} from './runtime/agentRuntimeContext.js';
|
|
82
|
+
|
|
83
|
+
const DELIVERY_ARTIFACT = '(?:文件|合同|文档|表格|报告|图片|图像|海报|幻灯片|PPT|PDF|代码|脚本|压缩包|网页|页面|HTML|DOCX|XLSX|CSV|ZIP)';
|
|
84
|
+
const DELIVERY_ACTION = '(?:发|发送|交付|提交|给|传|制作|生成|创建|导出|输出|修改|更新|完成)';
|
|
85
|
+
const ENGLISH_DELIVERY_ARTIFACT = '(?:file|document|report|spreadsheet|workbook|slides?|presentation|pdf|image|code|script|archive|zip|html|webpage|website)';
|
|
86
|
+
const ENGLISH_DELIVERY_ACTION = '(?:send|deliver|submit|provide|create|generate|export|update|modify|finish|produce)';
|
|
87
|
+
|
|
88
|
+
/** Whether this turn explicitly asks for a concrete artifact that should be formally delivered. */
|
|
89
|
+
export function expectsFormalDelivery(input: string): boolean {
|
|
90
|
+
const compact = input.replace(/\s+/g, '');
|
|
91
|
+
const chinese = new RegExp(
|
|
92
|
+
`${DELIVERY_ACTION}(?:给)?我?.{0,24}${DELIVERY_ARTIFACT}|${DELIVERY_ARTIFACT}.{0,24}${DELIVERY_ACTION}(?:给)?我?`,
|
|
93
|
+
'i',
|
|
94
|
+
);
|
|
95
|
+
const english = new RegExp(
|
|
96
|
+
`\\b${ENGLISH_DELIVERY_ACTION}\\b.{0,80}\\b${ENGLISH_DELIVERY_ARTIFACT}\\b|\\b${ENGLISH_DELIVERY_ARTIFACT}\\b.{0,80}\\b${ENGLISH_DELIVERY_ACTION}\\b`,
|
|
97
|
+
'i',
|
|
98
|
+
);
|
|
99
|
+
return chinese.test(compact) || english.test(input);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const DELIVERY_CORRECTION_REMINDER = `The user requested a concrete final deliverable, but no successful PresentResult call has been made.
|
|
103
|
+
Do not repeat the answer or mention a path only. Call PresentResult now with the actual final files and/or final http(s) links.
|
|
104
|
+
Submit only final outputs, never temporary scripts, unpacked assets, caches, or reference sources. If no valid result exists, explain that honestly without inventing one.`;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 每轮 LLM 请求都会带上工具 schema,但 countMessagesTokens 只数历史消息、
|
|
108
|
+
* 漏了这部分 → 系统性低估。autoCompact 判定阈值时加上它才准。
|
|
109
|
+
*
|
|
110
|
+
* ⚠️ 必须懒求值而非模块顶层求值:Agent 工具引入了 tools/index → agent → loop →
|
|
111
|
+
* tools/index 的模块环,若从 tools/index 一侧先进入,顶层求值 ALL_TOOLS 时它还在
|
|
112
|
+
* TDZ(临时死区),启动即 ReferenceError。函数体内访问时模块图早已初始化完毕。
|
|
113
|
+
*/
|
|
114
|
+
/**
|
|
115
|
+
* 本轮**真正发出去的**工具 schema 估算 token 数。
|
|
116
|
+
*
|
|
117
|
+
* 曾经这里数的是 `ALL_TOOLS`,那是错的:每回合发给模型的是 `activeTools`,
|
|
118
|
+
* 会被 `allowedTools`(工作流步骤 / 子 agent 收窄)、`subagents.enabled`、
|
|
119
|
+
* `computerRun` 是否注入等条件过滤掉一部分。ComputerUse 单个工具的 schema
|
|
120
|
+
* 实测就有 ~5800 token(全量 ~18000 的三分之一),在没暴露它的会话里照样
|
|
121
|
+
* 被计入压缩阈值 → **压缩提前触发**;被 `allowed_tools` 收窄到两三个工具的
|
|
122
|
+
* 子 agent 偏差更大。
|
|
123
|
+
*
|
|
124
|
+
* 缓存必须按工具集**键控**:模块级单值缓存配上按回合变化的工具集,会让所有
|
|
125
|
+
* 后续回合都拿到第一回合那个数字 —— 比不缓存更糟,因为它错得很稳定。
|
|
126
|
+
*/
|
|
127
|
+
const toolsSchemaTokensCache = new Map<string, number>();
|
|
128
|
+
function getToolsSchemaTokens(tools: readonly Tool[], signature?: string): number {
|
|
129
|
+
const key = signature ?? tools.map((t) => t.name).join(',');
|
|
130
|
+
let cached = toolsSchemaTokensCache.get(key);
|
|
131
|
+
if (cached === undefined) {
|
|
132
|
+
cached = estimateToolsTokens(tools as Tool[]);
|
|
133
|
+
toolsSchemaTokensCache.set(key, cached);
|
|
134
|
+
}
|
|
135
|
+
return cached;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 顾问咨询配额:基线 1 次,之后每累计 N 次工具调用解锁 1 次,封顶 3 次。
|
|
140
|
+
*
|
|
141
|
+
* 曾经是硬编码 1 次/回合。那个数字隐含「一个用户回合 = 一个决策点」的假设,
|
|
142
|
+
* 在短问答里成立,在「继续,完成后续所有工作」这类一句话跑几十步的长回合里
|
|
143
|
+
* 直接失效 —— 真正有价值的咨询点至少有三个(方案定型前、高风险批次做完后、
|
|
144
|
+
* 收尾汇报前),它们全落在同一个用户回合内,配额只够挑一个:
|
|
145
|
+
* 挑方案阶段就没法收尾复核,挑收尾就等于方案已经错着做完了。
|
|
146
|
+
*
|
|
147
|
+
* 用「已执行的工具调用数」当刻度,而不是轮数或时间:它是本回合真实工作量的
|
|
148
|
+
* 确定性度量,不引入任何模型判断。反向激励可忽略 —— 咨询对模型不是奖励,
|
|
149
|
+
* 刷工具调用换配额的成本远高于收益。
|
|
150
|
+
*/
|
|
151
|
+
const ADVISOR_QUOTA_BASE = 1;
|
|
152
|
+
const ADVISOR_QUOTA_MAX = 3;
|
|
153
|
+
const ADVISOR_QUOTA_UNLOCK_EVERY_TOOL_CALLS = 15;
|
|
154
|
+
|
|
155
|
+
export function advisorQuotaFor(executedToolCalls: number): number {
|
|
156
|
+
return Math.min(
|
|
157
|
+
ADVISOR_QUOTA_MAX,
|
|
158
|
+
ADVISOR_QUOTA_BASE + Math.floor(executedToolCalls / ADVISOR_QUOTA_UNLOCK_EVERY_TOOL_CALLS),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface RunQueryOptions {
|
|
163
|
+
provider: Provider;
|
|
164
|
+
/** Canonical host-issued trust context. Legacy fields below remain adapters. */
|
|
165
|
+
runtimeContext?: AgentRuntimeContext;
|
|
166
|
+
/** 历史会被 mutate(追加新消息、压缩时整体替换内容);调用方持同一引用 */
|
|
167
|
+
history: Message[];
|
|
168
|
+
/** 最多多少轮(防止失控调工具) */
|
|
169
|
+
maxTurns?: number;
|
|
170
|
+
/**
|
|
171
|
+
* 工作流 agent/llm 步的工具白名单:只把这些工具下发给模型并允许执行。
|
|
172
|
+
* 缺省 = 当前宿主工具全集;空数组 = 无工具。
|
|
173
|
+
*/
|
|
174
|
+
allowedTools?: string[];
|
|
175
|
+
/** Tools the host/workflow explicitly requires; emergency schema reduction preserves them. */
|
|
176
|
+
requiredTools?: string[];
|
|
177
|
+
/**
|
|
178
|
+
* Host-provided tool catalog. Production hosts normally omit this and use ALL_TOOLS;
|
|
179
|
+
* tests and embedders with custom executors must provide matching schemas so the
|
|
180
|
+
* model-visible catalog and execution boundary remain identical.
|
|
181
|
+
*/
|
|
182
|
+
availableTools?: readonly Tool[];
|
|
183
|
+
/** 用户取消 */
|
|
184
|
+
signal?: AbortSignal;
|
|
185
|
+
/**
|
|
186
|
+
* 回调:宿主检查是否有排队输入需要在安全点处理。
|
|
187
|
+
* loop只在安全检查点调用:每轮while开始时、工具全部执行完成后。
|
|
188
|
+
* 返回true表示应该结束当前turn(自然return,不抛异常)。
|
|
189
|
+
*/
|
|
190
|
+
hasQueuedInput?: () => boolean;
|
|
191
|
+
/**
|
|
192
|
+
* 回调:工具开始/结束执行,用于宿主跟踪运行中工具状态。
|
|
193
|
+
*/
|
|
194
|
+
onToolStart?: (toolName: string, toolCallId: string) => void;
|
|
195
|
+
onToolEnd?: (toolCallId: string, wasAborted: boolean) => void;
|
|
196
|
+
/**
|
|
197
|
+
* 多租户场景下注入隔离的 session 级状态;缺省走模块单例。
|
|
198
|
+
* TUI / print(单进程单 session)不传即可;HTTP server per-request 必须传。
|
|
199
|
+
*/
|
|
200
|
+
sessionState?: SessionState;
|
|
201
|
+
/**
|
|
202
|
+
* 注入 chat 实现(仅测试用)。
|
|
203
|
+
*
|
|
204
|
+
* 之前用 bun:test 的 mock.module 替换 llm/client.js 会进程全局污染所有跨文件
|
|
205
|
+
* import;改用显式 DI 后,测试传 stub 进来即可,零污染。生产代码不需要传。
|
|
206
|
+
*/
|
|
207
|
+
chat?: typeof defaultChat;
|
|
208
|
+
/**
|
|
209
|
+
* 注入 executeTool 实现(仅测试用)。同上,DI 而非 mock.module,零污染。
|
|
210
|
+
*/
|
|
211
|
+
executeTool?: (
|
|
212
|
+
name: string,
|
|
213
|
+
rawArgsJson: string,
|
|
214
|
+
signal?: AbortSignal,
|
|
215
|
+
ctx?: ToolCallContext,
|
|
216
|
+
) => Promise<ToolResult>;
|
|
217
|
+
/**
|
|
218
|
+
* Trusted host persistence boundary for formal results. It runs after
|
|
219
|
+
* PresentResult validates the files but before tool_end is published or the
|
|
220
|
+
* result is appended to history. Throwing turns the tool call into a failure,
|
|
221
|
+
* so a UI can never display an uncommitted delivery as durable.
|
|
222
|
+
*/
|
|
223
|
+
commitFormalDelivery?: (input: {
|
|
224
|
+
toolCallId: string;
|
|
225
|
+
turnId: string;
|
|
226
|
+
manifest: DeliverableManifest;
|
|
227
|
+
}) => Promise<void>;
|
|
228
|
+
/**
|
|
229
|
+
* 工具执行闸门(通用机制,见 src/toolGate.ts):loop 在执行每个工具前
|
|
230
|
+
* await 它拿放行/拒绝;拒绝时把 reason 当 tool_result 回填。内核零策略——审批语义
|
|
231
|
+
* 全在宿主(如 gateway 跨机指令同意闸门)。缺省不传 = 全放行,行为不变。
|
|
232
|
+
*/
|
|
233
|
+
toolGate?: ToolGate;
|
|
234
|
+
/**
|
|
235
|
+
* 仅对本次回合可见的系统上下文。它会随每次模型调用注入,但不会写入 history、
|
|
236
|
+
* 会话文件或压缩摘要,适合项目事实、远程委托和附件概要等外部上下文。
|
|
237
|
+
*
|
|
238
|
+
* 注入到本轮用户消息的模型副本,不改变 UI 或持久化历史。
|
|
239
|
+
*/
|
|
240
|
+
transientContext?: string;
|
|
241
|
+
/** Structured transient fragments allow deterministic request-time degradation. */
|
|
242
|
+
transientBundle?: ContextBundle;
|
|
243
|
+
/** Provider cache routing for this conversation; never persisted. */
|
|
244
|
+
promptCache?: import('./types.js').PromptCacheContext;
|
|
245
|
+
/** 仅供宿主 UI 恢复附件卡片;不会发送给 LLM。 */
|
|
246
|
+
userAttachments?: import('./types.js').MessageAttachmentRef[];
|
|
247
|
+
/** Stable host-provided identity for the user message of this turn. */
|
|
248
|
+
userMessageId?: string;
|
|
249
|
+
/** Browser element chips are persisted for UI but stripped before provider calls. */
|
|
250
|
+
userBrowserReferences?: import('./types.js').MessageBrowserReference[];
|
|
251
|
+
/** 显式传入的图片附件(-p --image / stream-json images)。 */
|
|
252
|
+
imageInputs?: ImageInputSpec[];
|
|
253
|
+
/** 是否解析用户文本里的 @./image.png / 图片 URL。TUI 默认开启。 */
|
|
254
|
+
parseInlineImages?: boolean;
|
|
255
|
+
/** 图片 detail 默认值:low/default/high。 */
|
|
256
|
+
imageDetail?: ImageDetail;
|
|
257
|
+
/** OpenAI 兼容扩展:图片最长边像素。 */
|
|
258
|
+
imageMaxLongSidePixel?: number;
|
|
259
|
+
/** Host-provided browser ownership scope; never exposed to the model. */
|
|
260
|
+
browserRun?: ToolCallContext['browserRun'];
|
|
261
|
+
/** Host-provided native desktop-control scope; absent means ComputerUse is not exposed. */
|
|
262
|
+
computerRun?: ToolCallContext['computerRun'];
|
|
263
|
+
/** Trusted host-only human handoff bridge for native Computer Use. */
|
|
264
|
+
computerIntervention?: ToolCallContext['computerIntervention'];
|
|
265
|
+
memoryContext?: {
|
|
266
|
+
access: import('./runtime/agentRuntimeContext.js').MemoryAccessInput;
|
|
267
|
+
workspaceId?: string;
|
|
268
|
+
sessionId?: string;
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 执行一次"用户输入 → 模型回答完成"的完整流程。
|
|
274
|
+
*
|
|
275
|
+
* yield 出来的 LoopEvent 会被 UI 用来更新界面。
|
|
276
|
+
*/
|
|
277
|
+
export async function* runQuery(
|
|
278
|
+
userInput: string,
|
|
279
|
+
options: RunQueryOptions,
|
|
280
|
+
): AsyncGenerator<LoopEvent, void, void> {
|
|
281
|
+
const { provider, history, signal, sessionState } = options;
|
|
282
|
+
// Tool screenshots are transient evidence for the tool continuation that produced them.
|
|
283
|
+
// Never carry a prior turn's base64 image into a new user request.
|
|
284
|
+
evictPriorVisionShots(history);
|
|
285
|
+
const maxTurns = options.maxTurns ?? 50;
|
|
286
|
+
const runtimeContext = options.runtimeContext ?? createAgentRuntimeContext({
|
|
287
|
+
surface: 'headless',
|
|
288
|
+
profile: 'main',
|
|
289
|
+
cwd: process.cwd(),
|
|
290
|
+
workspaceId: options.memoryContext?.workspaceId,
|
|
291
|
+
sessionId: options.memoryContext?.sessionId,
|
|
292
|
+
owner: options.memoryContext?.access !== 'denied' ? 'local-owner' : 'untrusted',
|
|
293
|
+
memoryAccess: options.memoryContext?.access ?? 'denied',
|
|
294
|
+
interactiveAsk: false,
|
|
295
|
+
browser: options.browserRun,
|
|
296
|
+
computer: options.computerRun,
|
|
297
|
+
computerIntervention: options.computerIntervention,
|
|
298
|
+
toolGate: options.toolGate,
|
|
299
|
+
});
|
|
300
|
+
// DI fallback:测试可传 stub,生产走真实模块单例
|
|
301
|
+
const chatFn = options.chat ?? defaultChat;
|
|
302
|
+
const executeToolFn = options.executeTool ?? defaultExecuteTool;
|
|
303
|
+
const explicitFileDeliveryRequested = expectsFormalDelivery(userInput);
|
|
304
|
+
|
|
305
|
+
const transientFor = (tools: readonly Tool[]): string => {
|
|
306
|
+
const opaque = options.transientContext?.trim() ?? '';
|
|
307
|
+
const bundle = options.transientBundle;
|
|
308
|
+
if (!bundle) return opaque;
|
|
309
|
+
const basePlan = buildContextBudgetPlan({
|
|
310
|
+
provider,
|
|
311
|
+
history,
|
|
312
|
+
tools,
|
|
313
|
+
transientContext: opaque,
|
|
314
|
+
});
|
|
315
|
+
const availableForBundle = Math.max(
|
|
316
|
+
0,
|
|
317
|
+
basePlan.inputLimit - basePlan.historyTokens - basePlan.toolTokens - basePlan.transientTokens,
|
|
318
|
+
);
|
|
319
|
+
const selected = availableForBundle >= bundle.diagnostics.usedTokens
|
|
320
|
+
? bundle
|
|
321
|
+
: resizeContextBundle(bundle, availableForBundle);
|
|
322
|
+
return [selected.text, opaque].filter(Boolean).join('\n\n');
|
|
323
|
+
};
|
|
324
|
+
let resultPresented = false;
|
|
325
|
+
let presentedResultUnverified = false;
|
|
326
|
+
let deliveryCorrectionAttempted = false;
|
|
327
|
+
const subagents = await configService.subagents();
|
|
328
|
+
await hydrateSessionWorktree({
|
|
329
|
+
sessionState,
|
|
330
|
+
workspaceId: runtimeContext.workspaceId,
|
|
331
|
+
sessionId: runtimeContext.sessionId,
|
|
332
|
+
cwd: runtimeContext.cwd,
|
|
333
|
+
});
|
|
334
|
+
const toolCallContext: ToolCallContext = {
|
|
335
|
+
history,
|
|
336
|
+
sessionState,
|
|
337
|
+
// 配额随本回合工作量增长(见 advisorQuotaFor);下面每轮工具执行完后上调。
|
|
338
|
+
advisorCalls: { used: 0, max: advisorQuotaFor(0) },
|
|
339
|
+
advisorInvocation: 'agent',
|
|
340
|
+
// Agent 等派生工具起嵌套子查询所需的运行态:与主循环同一 provider、
|
|
341
|
+
// 同一审批闸门(子 agent 不绕过宿主策略)。agentCalls 与 advisorCalls 同构,
|
|
342
|
+
// 限同一回合内的子任务派生总数,防失控 fan-out。
|
|
343
|
+
provider,
|
|
344
|
+
toolGate: runtimeContext.toolGate ?? options.toolGate,
|
|
345
|
+
agentCalls: { used: 0, max: subagents.enabled ? subagents.maxCallsPerTurn : 0 },
|
|
346
|
+
subagentConfig: subagents,
|
|
347
|
+
browserRun: runtimeContext.browser ?? options.browserRun,
|
|
348
|
+
computerRun: runtimeContext.computer ?? options.computerRun,
|
|
349
|
+
computerIntervention: runtimeContext.computerIntervention ?? options.computerIntervention,
|
|
350
|
+
currentUserInput: userInput,
|
|
351
|
+
userMessageId: options.userMessageId,
|
|
352
|
+
memoryAccess: runtimeContext.memoryAccess,
|
|
353
|
+
workspaceId: runtimeContext.workspaceId,
|
|
354
|
+
sessionId: runtimeContext.sessionId,
|
|
355
|
+
cwd: runtimeContext.cwd,
|
|
356
|
+
promptCache: options.promptCache,
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/** 本回合累计执行的工具调用数(顾问配额的刻度,见 advisorQuotaFor)。 */
|
|
360
|
+
let executedToolCalls = 0;
|
|
361
|
+
|
|
362
|
+
// 实际下发给模型的工具集:allowedTools 未传时给全量;
|
|
363
|
+
// 传入数组时严格按数组过滤,[] 表示本轮不下发任何工具。
|
|
364
|
+
// 仅影响呈现给 LLM 的工具清单,不影响 executeTool 的内部调度能力。
|
|
365
|
+
const hostTools = options.availableTools ?? ALL_TOOLS;
|
|
366
|
+
const availableTools = hostTools.filter((tool) =>
|
|
367
|
+
(tool.name !== 'Agent' || subagents.enabled)
|
|
368
|
+
&& (tool.name !== 'ComputerUse' || runtimeContext.computer !== undefined));
|
|
369
|
+
const effectiveToolSet = createEffectiveToolSet({
|
|
370
|
+
availableTools,
|
|
371
|
+
runtime: runtimeContext,
|
|
372
|
+
allowedNames: options.allowedTools,
|
|
373
|
+
requiredNames: options.requiredTools,
|
|
374
|
+
});
|
|
375
|
+
const activeTools = effectiveToolSet.tools;
|
|
376
|
+
const formalDeliveryRequired = explicitFileDeliveryRequested
|
|
377
|
+
&& runtimeContext.profile === 'main'
|
|
378
|
+
&& runtimeContext.surface !== 'workflow'
|
|
379
|
+
&& effectiveToolSet.names.has('PresentResult');
|
|
380
|
+
let currentEffectiveToolSet = effectiveToolSet;
|
|
381
|
+
let lastRequestTransientContext = [options.transientBundle?.text, options.transientContext]
|
|
382
|
+
.filter(Boolean).join('\n\n');
|
|
383
|
+
const evidenceLedger = new EvidenceLedger();
|
|
384
|
+
const stallGuard = new StallGuard();
|
|
385
|
+
const harnessDiagnostics = (
|
|
386
|
+
stopReason: import('./types.js').HarnessDiagnostics['stopReason'],
|
|
387
|
+
): import('./types.js').HarnessDiagnostics => ({
|
|
388
|
+
promptVersion: 'minimal-agent:prompt:v2',
|
|
389
|
+
basePromptHash: crypto.createHash('sha256')
|
|
390
|
+
.update(history[0]?.role === 'system' ? history[0].content : '')
|
|
391
|
+
.digest('hex'),
|
|
392
|
+
enabledPolicyIds: enabledPolicyIds({
|
|
393
|
+
profile: runtimeContext.profile,
|
|
394
|
+
surface: runtimeContext.surface,
|
|
395
|
+
cwd: runtimeContext.cwd,
|
|
396
|
+
capabilities: capabilitiesForTools(currentEffectiveToolSet.tools),
|
|
397
|
+
toolNames: currentEffectiveToolSet.names,
|
|
398
|
+
}),
|
|
399
|
+
enabledCapabilities: [...new Set(currentEffectiveToolSet.tools
|
|
400
|
+
.map((tool) => capabilityForToolName(tool.name))
|
|
401
|
+
.filter((capability): capability is NonNullable<typeof capability> => Boolean(capability)))],
|
|
402
|
+
effectiveToolSetSignature: currentEffectiveToolSet.signature,
|
|
403
|
+
tokenBudget: buildContextBudgetPlan({
|
|
404
|
+
provider,
|
|
405
|
+
history,
|
|
406
|
+
tools: currentEffectiveToolSet.tools,
|
|
407
|
+
transientContext: lastRequestTransientContext,
|
|
408
|
+
}),
|
|
409
|
+
stopReason,
|
|
410
|
+
evidence: evidenceLedger.snapshot(),
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
let userContent: Message['content'];
|
|
414
|
+
// 行内图片是从用户文本里**猜**出来的(拖拽进终端的路径就走这条路),猜错很正常:
|
|
415
|
+
// 路径可能还没生成、已删除、或纯粹是在讨论一个文件名。这类失败只降级成文本 + 一条
|
|
416
|
+
// warning,绝不能像下面 catch 那样打死整个回合——否则「随口提一个 .png 路径」=回合当场死。
|
|
417
|
+
const skippedInlineImages: { source: string; reason: string }[] = [];
|
|
418
|
+
try {
|
|
419
|
+
userContent = await buildUserContent({
|
|
420
|
+
text: userInput,
|
|
421
|
+
images: options.imageInputs,
|
|
422
|
+
parseInlineImages: options.parseInlineImages,
|
|
423
|
+
detail: options.imageDetail,
|
|
424
|
+
maxLongSidePixel: options.imageMaxLongSidePixel,
|
|
425
|
+
onInlineImageSkipped: (source, reason) => skippedInlineImages.push({ source, reason }),
|
|
426
|
+
});
|
|
427
|
+
} catch (e) {
|
|
428
|
+
// 走到这里只剩「显式传入的图」读失败(-p --image / 工具实况截图)——调用方明确要了这张图。
|
|
429
|
+
yield {
|
|
430
|
+
type: 'error',
|
|
431
|
+
error: `图片输入处理失败:${(e as Error).message}`,
|
|
432
|
+
code: 'llm_error',
|
|
433
|
+
};
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
for (const skipped of skippedInlineImages) {
|
|
437
|
+
yield {
|
|
438
|
+
type: 'warning',
|
|
439
|
+
message: `未作为图片附加:${skipped.source}(${skipped.reason}),已按普通文本处理。`,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 1. 用户消息入栈(v2:填 timestamp,让 MessageList 的 turn 边界横线
|
|
444
|
+
// 能拿到首条 user 的真实时间,否则 fallback Date.now() 会丢失"提交时刻"语义)
|
|
445
|
+
const userMessage: Extract<Message, { role: 'user' }> = {
|
|
446
|
+
role: 'user',
|
|
447
|
+
content: userContent,
|
|
448
|
+
...(options.userAttachments?.length ? { attachments: options.userAttachments } : {}),
|
|
449
|
+
...(options.userBrowserReferences?.length ? { browserReferences: options.userBrowserReferences } : {}),
|
|
450
|
+
timestamp: Date.now(),
|
|
451
|
+
id: options.userMessageId ?? crypto.randomUUID(),
|
|
452
|
+
};
|
|
453
|
+
history.push(userMessage);
|
|
454
|
+
// 立即通知 UI:用户消息已入栈,触发 bump 让 <Static> 马上把它 commit 进 scrollback,
|
|
455
|
+
// 不必等整轮 assistant 落定(保证用户输入第一时间置顶,符合 T-A-O-R 展示顺序)。
|
|
456
|
+
yield { type: 'user_message_committed', message: userMessage };
|
|
457
|
+
|
|
458
|
+
// 反应式压缩自救:本 runQuery 内最多触发 MAX_REACTIVE_PER_QUERY 次(防本轮死循环);
|
|
459
|
+
// 跨 runQuery 的连续失败由 reactiveCompact.ts 的 circuit breaker(consecutiveFailures)兜底。
|
|
460
|
+
let reactiveCount = 0;
|
|
461
|
+
const MAX_REACTIVE_PER_QUERY = 3;
|
|
462
|
+
|
|
463
|
+
// 上一轮 LLM 返回的真实 prompt token 数(done.promptTokens;provider 开 include_usage 才有)。
|
|
464
|
+
// 作为 autoCompact 的权威判据,校正本地估算的系统性低估;压缩后清空(历史已变,旧值失真)。
|
|
465
|
+
let lastPromptTokens: number | undefined;
|
|
466
|
+
// 提示词缓存观测位(纯展示,不参与任何控制流)。与 lastPromptTokens 同生命周期:
|
|
467
|
+
// 压缩后一并清空,避免把压缩前的命中数据挂到压缩后的新历史上误导排障。
|
|
468
|
+
let lastCachedPromptTokens: number | undefined;
|
|
469
|
+
let lastCacheMissTokens: number | undefined;
|
|
470
|
+
let lastCacheWriteTokens: number | undefined;
|
|
471
|
+
let lastPromptCacheUsage: import('./types.js').PromptCacheUsage = { reported: false };
|
|
472
|
+
|
|
473
|
+
let turn = 0;
|
|
474
|
+
let workStarted = false;
|
|
475
|
+
while (turn < maxTurns) {
|
|
476
|
+
turn++;
|
|
477
|
+
incrementTurn(sessionState?.microCompact);
|
|
478
|
+
expireOldEntries(sessionState?.microCompact);
|
|
479
|
+
|
|
480
|
+
if (signal?.aborted) {
|
|
481
|
+
pushManualInterruptMarker(history);
|
|
482
|
+
if (!workStarted) {
|
|
483
|
+
yield { type: 'error', error: '用户中断了任务', code: 'aborted' };
|
|
484
|
+
} else {
|
|
485
|
+
yield { type: 'interrupted', reason: 'manual' };
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (options.hasQueuedInput?.()) {
|
|
491
|
+
pushQueuedInputMarker(history);
|
|
492
|
+
if (!workStarted) {
|
|
493
|
+
yield { type: 'error', error: '收到新输入,取消当前任务', code: 'aborted' };
|
|
494
|
+
} else {
|
|
495
|
+
yield { type: 'interrupted', reason: 'queued_input' };
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 2. 自动压缩(判据 = max(上一轮真实 usage, 历史估算 + 工具 schema),
|
|
501
|
+
// 阈值 provider 无关、按 contextWindow × compactRatio 比例触发)
|
|
502
|
+
let requestTransientContext = [
|
|
503
|
+
transientFor(activeTools),
|
|
504
|
+
deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
|
|
505
|
+
].filter(Boolean).join('\n\n');
|
|
506
|
+
lastRequestTransientContext = requestTransientContext;
|
|
507
|
+
const preCompactBudget = buildContextBudgetPlan({
|
|
508
|
+
provider,
|
|
509
|
+
history,
|
|
510
|
+
tools: activeTools,
|
|
511
|
+
transientContext: requestTransientContext,
|
|
512
|
+
});
|
|
513
|
+
if (preCompactBudget.overBudget || lastPromptTokens !== undefined) {
|
|
514
|
+
try {
|
|
515
|
+
const compact = await autoCompactIfNeeded(history, provider, {
|
|
516
|
+
actualPromptTokens: lastPromptTokens,
|
|
517
|
+
toolsTokens: getToolsSchemaTokens(activeTools, effectiveToolSet.signature),
|
|
518
|
+
transientTokens: countTextTokens(requestTransientContext),
|
|
519
|
+
signal,
|
|
520
|
+
promptCache: options.promptCache,
|
|
521
|
+
chat: chatFn,
|
|
522
|
+
});
|
|
523
|
+
if (compact.compacted) {
|
|
524
|
+
yield { type: 'compact_start' };
|
|
525
|
+
yield { type: 'stage_change', stage: 'compacting' };
|
|
526
|
+
history.length = 0;
|
|
527
|
+
history.push(...compact.messages);
|
|
528
|
+
lastPromptTokens = undefined;
|
|
529
|
+
lastCachedPromptTokens = undefined;
|
|
530
|
+
lastCacheMissTokens = undefined;
|
|
531
|
+
lastCacheWriteTokens = undefined;
|
|
532
|
+
lastPromptCacheUsage = { reported: false };
|
|
533
|
+
yield {
|
|
534
|
+
type: 'compact_done',
|
|
535
|
+
before: compact.before,
|
|
536
|
+
after: compact.after,
|
|
537
|
+
trigger: 'threshold',
|
|
538
|
+
mode: compact.mode,
|
|
539
|
+
degraded: compact.degraded,
|
|
540
|
+
...(compact.fallbackReason ? { fallbackReason: compact.fallbackReason } : {}),
|
|
541
|
+
...(compact.messagesRemoved !== undefined ? { messagesRemoved: compact.messagesRemoved } : {}),
|
|
542
|
+
...(compact.tokensFreed !== undefined ? { tokensFreed: compact.tokensFreed } : {}),
|
|
543
|
+
};
|
|
544
|
+
yield { type: 'stage_change', stage: 'thinking' };
|
|
545
|
+
}
|
|
546
|
+
} catch (e) {
|
|
547
|
+
if (signal?.aborted || (e as Error).name === 'AbortError') throw e;
|
|
548
|
+
// 压缩失败不要让整个对话挂掉:先 snip 砍最老消息释放空间再继续。
|
|
549
|
+
let recovered = false;
|
|
550
|
+
try {
|
|
551
|
+
const before = countMessagesTokens(history);
|
|
552
|
+
const snipped = snipCompactIfNeeded(history, { force: true, snipPercent: 0.3 });
|
|
553
|
+
if (snipped.messagesRemoved > 0) {
|
|
554
|
+
history.length = 0;
|
|
555
|
+
history.push(...snipped.messages);
|
|
556
|
+
lastPromptTokens = undefined;
|
|
557
|
+
lastCachedPromptTokens = undefined;
|
|
558
|
+
lastCacheMissTokens = undefined;
|
|
559
|
+
lastCacheWriteTokens = undefined;
|
|
560
|
+
lastPromptCacheUsage = { reported: false };
|
|
561
|
+
recovered = true;
|
|
562
|
+
yield {
|
|
563
|
+
type: 'compact_done',
|
|
564
|
+
before,
|
|
565
|
+
after: countMessagesTokens(history),
|
|
566
|
+
trigger: 'threshold',
|
|
567
|
+
mode: 'snip-fallback',
|
|
568
|
+
degraded: true,
|
|
569
|
+
fallbackReason: 'llm-error',
|
|
570
|
+
messagesRemoved: snipped.messagesRemoved,
|
|
571
|
+
tokensFreed: snipped.tokensFreed,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
} catch {
|
|
575
|
+
/* snip 也失败 → 真的只能硬上 */
|
|
576
|
+
}
|
|
577
|
+
if (!recovered) {
|
|
578
|
+
// 事件是 provider 原始报错唯一该去的地方:只到 stderr / UI,不进 history、
|
|
579
|
+
// 不落盘、不会在后续轮次重新发给模型(compact.ts 里那条降级消息因此保持无错误体)。
|
|
580
|
+
yield {
|
|
581
|
+
type: 'error',
|
|
582
|
+
error: `自动压缩失败,规则式截断也未能释放空间;已继续尝试回答:${(e as Error).message}`,
|
|
583
|
+
code: 'compact_failed',
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Re-open transient capacity after compaction. If the compacted tail is still
|
|
590
|
+
// too large, trim old history before sacrificing any model-visible ability.
|
|
591
|
+
requestTransientContext = [
|
|
592
|
+
transientFor(activeTools),
|
|
593
|
+
deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
|
|
594
|
+
].filter(Boolean).join('\n\n');
|
|
595
|
+
lastRequestTransientContext = requestTransientContext;
|
|
596
|
+
let postCompactBudget = buildContextBudgetPlan({
|
|
597
|
+
provider,
|
|
598
|
+
history,
|
|
599
|
+
tools: activeTools,
|
|
600
|
+
transientContext: requestTransientContext,
|
|
601
|
+
});
|
|
602
|
+
if (postCompactBudget.overBudget) {
|
|
603
|
+
const before = countMessagesTokens(history);
|
|
604
|
+
const snipped = snipCompactIfNeeded(history, { force: true, snipPercent: 0.3 });
|
|
605
|
+
if (snipped.messagesRemoved > 0) {
|
|
606
|
+
history.length = 0;
|
|
607
|
+
history.push(...snipped.messages);
|
|
608
|
+
yield {
|
|
609
|
+
type: 'compact_done',
|
|
610
|
+
before,
|
|
611
|
+
after: countMessagesTokens(history),
|
|
612
|
+
trigger: 'post-budget',
|
|
613
|
+
mode: 'snip-fallback',
|
|
614
|
+
degraded: true,
|
|
615
|
+
fallbackReason: 'still-over-budget',
|
|
616
|
+
messagesRemoved: snipped.messagesRemoved,
|
|
617
|
+
tokensFreed: snipped.tokensFreed,
|
|
618
|
+
};
|
|
619
|
+
requestTransientContext = [
|
|
620
|
+
transientFor(activeTools),
|
|
621
|
+
deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
|
|
622
|
+
].filter(Boolean).join('\n\n');
|
|
623
|
+
lastRequestTransientContext = requestTransientContext;
|
|
624
|
+
postCompactBudget = buildContextBudgetPlan({
|
|
625
|
+
provider,
|
|
626
|
+
history,
|
|
627
|
+
tools: activeTools,
|
|
628
|
+
transientContext: requestTransientContext,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const emergencyFit = fitToolsForEmergency({
|
|
634
|
+
provider,
|
|
635
|
+
history,
|
|
636
|
+
tools: activeTools,
|
|
637
|
+
transientContext: requestTransientContext,
|
|
638
|
+
requiredNames: effectiveToolSet.requiredNames,
|
|
639
|
+
});
|
|
640
|
+
currentEffectiveToolSet = emergencyFit.removedNames.length === 0
|
|
641
|
+
? effectiveToolSet
|
|
642
|
+
: createEffectiveToolSet({
|
|
643
|
+
availableTools: emergencyFit.tools,
|
|
644
|
+
runtime: runtimeContext,
|
|
645
|
+
requiredNames: [...effectiveToolSet.requiredNames],
|
|
646
|
+
});
|
|
647
|
+
if (emergencyFit.removedNames.length > 0) {
|
|
648
|
+
yield {
|
|
649
|
+
type: 'warning',
|
|
650
|
+
message: `当前模型窗口压力过高,本次请求临时省略工具 Schema:${emergencyFit.removedNames.join(', ')};下一轮会重新评估并恢复。`,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (emergencyFit.plan.overBudget) {
|
|
654
|
+
yield {
|
|
655
|
+
type: 'error',
|
|
656
|
+
error: `当前请求即使经过上下文降级仍需要约 ${emergencyFit.plan.totalInputTokens} tokens,超过模型输入上限 ${emergencyFit.plan.inputLimit}。已停止发送,避免 Provider 接收必然失败的超限请求。`,
|
|
657
|
+
code: 'context_overflow',
|
|
658
|
+
};
|
|
659
|
+
yield { type: 'harness_diagnostics', diagnostics: harnessDiagnostics('error') };
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// 3. 调 LLM 并组装 assistant 消息
|
|
664
|
+
let assistantText = '';
|
|
665
|
+
/** 按 index 累积 tool_calls(OpenAI 协议下 tool_calls 是分片返回的) */
|
|
666
|
+
const toolCallsByIndex: ToolCall[] = [];
|
|
667
|
+
/**
|
|
668
|
+
* 思维链累积器。三个 provider 字段名各管一份;最后哪个非空就挂哪个,
|
|
669
|
+
* 多个非空就都挂上(不会冲突,下一轮请求时整个 message 透传给 API)。
|
|
670
|
+
*/
|
|
671
|
+
let reasoningContent = '';
|
|
672
|
+
let reasoningString = '';
|
|
673
|
+
const reasoningDetails: unknown[] = [];
|
|
674
|
+
|
|
675
|
+
/** 首个 text_delta 时切到 streaming,再来不重复 yield */
|
|
676
|
+
let stageStreamingYielded = false;
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* R2:本轮 LLM 流的终止原因(来自 client 的 done.stopReason)。
|
|
680
|
+
* 'length' = 被 provider 因 max_tokens 截断 —— 收尾时据此给 warning,
|
|
681
|
+
* 避免「半截答案」被静默当成正常 end_turn。
|
|
682
|
+
*/
|
|
683
|
+
let lastFinishReason: 'stop' | 'tool_calls' | 'length' | 'unknown' | 'aborted' =
|
|
684
|
+
'unknown';
|
|
685
|
+
|
|
686
|
+
// 进入 LLM 流前:等首 token / reasoning,本质是 thinking
|
|
687
|
+
yield { type: 'stage_change', stage: 'thinking' };
|
|
688
|
+
workStarted = true;
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
const modelMessages = buildModelMessages(history, requestTransientContext, userMessage.id);
|
|
692
|
+
for await (const ev of chatFn({
|
|
693
|
+
provider,
|
|
694
|
+
messages: modelMessages,
|
|
695
|
+
tools: currentEffectiveToolSet.tools,
|
|
696
|
+
signal,
|
|
697
|
+
promptCache: options.promptCache,
|
|
698
|
+
})) {
|
|
699
|
+
if (ev.type === 'text_delta') {
|
|
700
|
+
if (!stageStreamingYielded) {
|
|
701
|
+
stageStreamingYielded = true;
|
|
702
|
+
yield { type: 'stage_change', stage: 'streaming' };
|
|
703
|
+
}
|
|
704
|
+
assistantText += ev.delta;
|
|
705
|
+
yield { type: 'text', delta: ev.delta };
|
|
706
|
+
} else if (ev.type === 'tool_call_delta') {
|
|
707
|
+
mergeToolCallDelta(toolCallsByIndex, ev);
|
|
708
|
+
} else if (ev.type === 'reasoning_delta') {
|
|
709
|
+
if (ev.field === 'reasoning_content' && ev.delta) {
|
|
710
|
+
reasoningContent += ev.delta;
|
|
711
|
+
yield { type: 'reasoning', delta: ev.delta };
|
|
712
|
+
} else if (ev.field === 'reasoning' && ev.delta) {
|
|
713
|
+
reasoningString += ev.delta;
|
|
714
|
+
yield { type: 'reasoning', delta: ev.delta };
|
|
715
|
+
} else if (ev.field === 'reasoning_details' && ev.items) {
|
|
716
|
+
// reasoning_details 是结构化数组,没有 delta 字符串,跳过 yield
|
|
717
|
+
reasoningDetails.push(...ev.items);
|
|
718
|
+
}
|
|
719
|
+
} else if (ev.type === 'done') {
|
|
720
|
+
// R2:记下流的终止原因。'length'(max_tokens 截断)会在本轮收尾时触发 warning。
|
|
721
|
+
lastFinishReason = ev.stopReason;
|
|
722
|
+
// 真实 prompt token(provider 开 include_usage 才有)→ 喂给下一轮 autoCompact 做权威判据
|
|
723
|
+
if (typeof ev.promptTokens === 'number') lastPromptTokens = ev.promptTokens;
|
|
724
|
+
// 缓存命中观测:只在 provider 真实报告时才记录(undefined ≠ 0,见 types.ts 注释)
|
|
725
|
+
if (typeof ev.cachedPromptTokens === 'number') lastCachedPromptTokens = ev.cachedPromptTokens;
|
|
726
|
+
if (typeof ev.cacheMissTokens === 'number') lastCacheMissTokens = ev.cacheMissTokens;
|
|
727
|
+
if (typeof ev.cacheWriteTokens === 'number') lastCacheWriteTokens = ev.cacheWriteTokens;
|
|
728
|
+
if (ev.promptCacheUsage) lastPromptCacheUsage = ev.promptCacheUsage;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
} catch (e) {
|
|
732
|
+
if ((e as Error).name === 'AbortError') {
|
|
733
|
+
if (options.hasQueuedInput?.()) {
|
|
734
|
+
pushQueuedInputMarker(history);
|
|
735
|
+
yield { type: 'interrupted', reason: 'queued_input' };
|
|
736
|
+
} else {
|
|
737
|
+
pushManualInterruptMarker(history);
|
|
738
|
+
yield { type: 'interrupted', reason: 'manual' };
|
|
739
|
+
}
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// prompt_too_long 反应式自救:压缩历史 → 同 turn 重发 LLM
|
|
744
|
+
// LLM 看到压缩后的 9 段摘要 + 近期 verbatim 消息,能接着干活,
|
|
745
|
+
// 不会丢失中途的工具调用上下文。
|
|
746
|
+
if (isPromptTooLongError(e) && reactiveCount < MAX_REACTIVE_PER_QUERY) {
|
|
747
|
+
reactiveCount++;
|
|
748
|
+
yield { type: 'compact_start' };
|
|
749
|
+
yield { type: 'stage_change', stage: 'compacting' };
|
|
750
|
+
const result = await reactiveCompactIfApplicable(
|
|
751
|
+
history,
|
|
752
|
+
provider,
|
|
753
|
+
e,
|
|
754
|
+
sessionState?.reactive,
|
|
755
|
+
signal,
|
|
756
|
+
{},
|
|
757
|
+
options.promptCache,
|
|
758
|
+
);
|
|
759
|
+
// 仅当确实压下去了(after < before)才重试;否则(没压动 / 未恢复)退回错误路径,杜绝死循环。
|
|
760
|
+
const shrank = (result.after ?? 0) < (result.before ?? 0);
|
|
761
|
+
if (result.recovered && shrank) {
|
|
762
|
+
history.length = 0;
|
|
763
|
+
history.push(...result.messages);
|
|
764
|
+
lastPromptTokens = undefined; // 历史已变小,旧真实 usage 失真
|
|
765
|
+
lastCachedPromptTokens = undefined;
|
|
766
|
+
lastCacheMissTokens = undefined;
|
|
767
|
+
lastCacheWriteTokens = undefined;
|
|
768
|
+
lastPromptCacheUsage = { reported: false };
|
|
769
|
+
yield {
|
|
770
|
+
type: 'compact_done',
|
|
771
|
+
before: result.before ?? 0,
|
|
772
|
+
after: result.after ?? 0,
|
|
773
|
+
trigger: 'provider-overflow',
|
|
774
|
+
mode: result.mode,
|
|
775
|
+
degraded: result.degraded,
|
|
776
|
+
...(result.fallbackReason ? { fallbackReason: result.fallbackReason } : {}),
|
|
777
|
+
...(result.messagesRemoved !== undefined ? { messagesRemoved: result.messagesRemoved } : {}),
|
|
778
|
+
...(result.tokensFreed !== undefined ? { tokensFreed: result.tokensFreed } : {}),
|
|
779
|
+
};
|
|
780
|
+
// 压缩完回到 LLM 调用
|
|
781
|
+
yield { type: 'stage_change', stage: 'thinking' };
|
|
782
|
+
turn--; // 不消耗 turn 配额,本轮重新走
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
// reactive 没压动或未恢复 → 退回正常错误路径
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// code='llm_error':终结性错误,stop_reason='error'
|
|
789
|
+
yield {
|
|
790
|
+
type: 'error',
|
|
791
|
+
error: `LLM 调用失败:${(e as Error).message}`,
|
|
792
|
+
code: 'llm_error',
|
|
793
|
+
};
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (lastFinishReason === 'aborted' || signal?.aborted || options.hasQueuedInput?.()) {
|
|
798
|
+
if (options.hasQueuedInput?.() && !signal?.aborted) {
|
|
799
|
+
pushQueuedInputMarker(history);
|
|
800
|
+
yield { type: 'interrupted', reason: 'queued_input' };
|
|
801
|
+
} else {
|
|
802
|
+
pushManualInterruptMarker(history);
|
|
803
|
+
yield { type: 'interrupted', reason: 'manual' };
|
|
804
|
+
}
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// S2 防御:OpenAI 协议保证 tool_call 的 index 连续;万一某 provider 跳号产生稀疏数组,
|
|
809
|
+
// 这里压实掉空洞,避免后续遍历 / 回填遇到 undefined(assistantMsg.tool_calls 与执行都用它)。
|
|
810
|
+
{
|
|
811
|
+
const dense = toolCallsByIndex.filter((tc): tc is ToolCall => tc != null);
|
|
812
|
+
if (dense.length !== toolCallsByIndex.length) {
|
|
813
|
+
toolCallsByIndex.length = 0;
|
|
814
|
+
toolCallsByIndex.push(...dense);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const assistantMsg: Message = {
|
|
819
|
+
role: 'assistant',
|
|
820
|
+
content: assistantText.length > 0 ? assistantText : null,
|
|
821
|
+
...(toolCallsByIndex.length > 0 ? { tool_calls: toolCallsByIndex } : {}),
|
|
822
|
+
...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
|
|
823
|
+
...(reasoningString ? { reasoning: reasoningString } : {}),
|
|
824
|
+
...(reasoningDetails.length > 0 ? { reasoning_details: reasoningDetails } : {}),
|
|
825
|
+
timestamp: Date.now(),
|
|
826
|
+
id: crypto.randomUUID(),
|
|
827
|
+
};
|
|
828
|
+
history.push(assistantMsg);
|
|
829
|
+
yield { type: 'assistant_message', message: assistantMsg as Extract<Message, { role: 'assistant' }> };
|
|
830
|
+
|
|
831
|
+
// 4. 没有工具调用 → 整轮交互结束
|
|
832
|
+
if (toolCallsByIndex.length === 0) {
|
|
833
|
+
// A model occasionally writes a path/link in prose and stops without formally
|
|
834
|
+
// presenting it. Give it one model-only corrective pass; the reminder is
|
|
835
|
+
// transient and therefore never pollutes persisted conversation history.
|
|
836
|
+
if (
|
|
837
|
+
formalDeliveryRequired
|
|
838
|
+
&& !resultPresented
|
|
839
|
+
&& !deliveryCorrectionAttempted
|
|
840
|
+
&& turn < maxTurns
|
|
841
|
+
) {
|
|
842
|
+
deliveryCorrectionAttempted = true;
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
// R2:最终回答被 provider 因 max_tokens 截断(finish_reason='length')时,
|
|
846
|
+
// 不能静默当正常 end_turn 收尾 —— 否则 -p 宿主会拿到半截答案却以为成功。
|
|
847
|
+
// 走通用 warning(text/json 都落 stderr,不污染 json 那一行 stdout 契约)。
|
|
848
|
+
if (lastFinishReason === 'length') {
|
|
849
|
+
yield {
|
|
850
|
+
type: 'warning',
|
|
851
|
+
message:
|
|
852
|
+
'⚠️ 模型回答可能被截断(finish_reason=length,触顶 max_tokens),本轮回答或不完整。' +
|
|
853
|
+
'可调大模型的 max_tokens 上限,或让模型分段输出。',
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
// The final answer has already consumed the latest screenshot. Persist only a compact
|
|
857
|
+
// marker so the next turn does not resend a large screen image.
|
|
858
|
+
evictPriorVisionShots(history);
|
|
859
|
+
const estimatedTokens = countMessagesTokens(history) + getToolsSchemaTokens(activeTools);
|
|
860
|
+
const usedTokens = Math.min(
|
|
861
|
+
provider.contextWindow,
|
|
862
|
+
Math.max(lastPromptTokens ?? 0, estimatedTokens),
|
|
863
|
+
);
|
|
864
|
+
yield { type: 'harness_diagnostics', diagnostics: harnessDiagnostics('complete') };
|
|
865
|
+
const claimValidation = validateFinalClaims(assistantText, evidenceLedger.snapshot());
|
|
866
|
+
for (const warning of claimValidation.warnings) yield { type: 'warning', message: warning };
|
|
867
|
+
yield {
|
|
868
|
+
type: 'context_usage',
|
|
869
|
+
usedTokens,
|
|
870
|
+
contextWindow: provider.contextWindow,
|
|
871
|
+
remainingTokens: Math.max(0, provider.contextWindow - usedTokens),
|
|
872
|
+
compactThreshold: getCompactThreshold(provider),
|
|
873
|
+
source: lastPromptTokens === undefined ? 'estimated' : 'actual',
|
|
874
|
+
// 缓存观测只在有真实 usage 时才附带——估算路径没有缓存信息,
|
|
875
|
+
// 不能拿估算值冒充命中率(见 types.ts 的 context_usage 注释)。
|
|
876
|
+
...(lastPromptTokens !== undefined && lastCachedPromptTokens !== undefined
|
|
877
|
+
? { cachedPromptTokens: lastCachedPromptTokens }
|
|
878
|
+
: {}),
|
|
879
|
+
...(lastPromptTokens !== undefined && lastCacheWriteTokens !== undefined
|
|
880
|
+
? { cacheWriteTokens: lastCacheWriteTokens }
|
|
881
|
+
: {}),
|
|
882
|
+
...(lastPromptTokens !== undefined && lastCacheMissTokens !== undefined
|
|
883
|
+
? { cacheMissTokens: lastCacheMissTokens }
|
|
884
|
+
: {}),
|
|
885
|
+
promptCacheUsage: lastPromptCacheUsage,
|
|
886
|
+
};
|
|
887
|
+
if (formalDeliveryRequired && !resultPresented) {
|
|
888
|
+
const warning = '交付未完成:用户明确要求提供文件,但本轮没有成功提交可打开的文件结果。';
|
|
889
|
+
yield { type: 'warning', message: warning };
|
|
890
|
+
yield { type: 'turn_done', completionStatus: 'delivery_incomplete', warning };
|
|
891
|
+
} else if (resultPresented && presentedResultUnverified) {
|
|
892
|
+
const warning = '结果已提交,但至少一个文件尚未通过结构化验证。';
|
|
893
|
+
yield { type: 'warning', message: warning };
|
|
894
|
+
yield { type: 'turn_done', completionStatus: 'delivery_unverified', warning };
|
|
895
|
+
} else if (!claimValidation.verified) {
|
|
896
|
+
yield {
|
|
897
|
+
type: 'turn_done',
|
|
898
|
+
completionStatus: 'claims_unverified',
|
|
899
|
+
warning: claimValidation.warnings.join('\n'),
|
|
900
|
+
};
|
|
901
|
+
} else {
|
|
902
|
+
yield { type: 'turn_done', completionStatus: 'completed' };
|
|
903
|
+
}
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// 5. 执行工具调用
|
|
908
|
+
//
|
|
909
|
+
// R1:按 isConcurrencySafe 决定并发策略 —— 工具早就声明了这个字段,此前却没有
|
|
910
|
+
// 任何执行路径消费它(全程无条件并行),这里让它真正生效:
|
|
911
|
+
// - 本轮工具「全部并发安全」(只读类 Read/Grep/Glob/WebSearch/WebFetch)且不止一个
|
|
912
|
+
// → 并行:用事件队列把多个 worker 的 tool_start/tool_end 按真实完成顺序交错 yield。
|
|
913
|
+
// - 只要有一个写类/未知工具(Edit/Write/MultiEdit/Bash/WebBrowser)→ 整轮串行:
|
|
914
|
+
// 彻底杜绝两个写操作并行落到同一文件导致的 lost-update / fileState TOCTOU。
|
|
915
|
+
// 两种路径下,history 里的 tool 消息都严格按 toolCallsByIndex 索引顺序回填
|
|
916
|
+
// (OpenAI 协议要求 tool 消息与上一条 assistant 的 tool_calls 一一对应)。
|
|
917
|
+
yield { type: 'stage_change', stage: 'tool_executing' };
|
|
918
|
+
|
|
919
|
+
type WorkerOutcome = {
|
|
920
|
+
tc: ToolCall;
|
|
921
|
+
result: ToolResult;
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
// 执行单个 tool call → WorkerOutcome(内部吞掉 abort/异常,永不 reject)。
|
|
925
|
+
const runOne = async (
|
|
926
|
+
tc: ToolCall,
|
|
927
|
+
reportProgress?: (message: string, detail?: import('./types.js').ToolProgressDetail) => void,
|
|
928
|
+
): Promise<WorkerOutcome> => {
|
|
929
|
+
try {
|
|
930
|
+
if (stallGuard.shouldReject(tc)) {
|
|
931
|
+
return {
|
|
932
|
+
tc,
|
|
933
|
+
result: {
|
|
934
|
+
ok: false as const,
|
|
935
|
+
error:
|
|
936
|
+
'检测到同一工具和参数已连续两次返回相同失败;第三次调用已在执行前拒绝,请改变输入或改用其他路径。',
|
|
937
|
+
recovery: 'change-input',
|
|
938
|
+
sensitivity: 'public',
|
|
939
|
+
},
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
// The model-visible catalog and the execution boundary are the same set.
|
|
943
|
+
// A provider may still emit a stale or fabricated tool call; return a
|
|
944
|
+
// recoverable result so the model can choose an available path.
|
|
945
|
+
if (!currentEffectiveToolSet.names.has(tc.function.name)) {
|
|
946
|
+
const deniedReason = tc.function.name === 'Agent'
|
|
947
|
+
? '当前上下文不允许调用 Agent;请直接完成子任务或改用已提供的工具'
|
|
948
|
+
: `工具 ${tc.function.name} 不在当前宿主授权的有效工具集中,请改用已提供的工具继续任务`;
|
|
949
|
+
return {
|
|
950
|
+
tc,
|
|
951
|
+
result: {
|
|
952
|
+
ok: false as const,
|
|
953
|
+
error: deniedReason,
|
|
954
|
+
recovery: 'change-input',
|
|
955
|
+
sensitivity: 'public',
|
|
956
|
+
},
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
// 工具执行闸门:宿主注入后覆盖所有工具,包括只读工具。deny → 把 reason 当
|
|
960
|
+
// tool_result 回填,模型据此改道/如实报告,不抛异常不中断整轮。宿主异常一律 deny。
|
|
961
|
+
const isReadOnly = getToolByName(tc.function.name)?.isReadOnly === true;
|
|
962
|
+
const effectiveToolGate = runtimeContext.toolGate ?? options.toolGate;
|
|
963
|
+
if (effectiveToolGate) {
|
|
964
|
+
let decision;
|
|
965
|
+
try {
|
|
966
|
+
const gateDetails = getToolGateDetails(
|
|
967
|
+
tc.function.name,
|
|
968
|
+
tc.function.arguments,
|
|
969
|
+
{ ...toolCallContext, cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd, toolCallId: tc.id, reportProgress },
|
|
970
|
+
);
|
|
971
|
+
decision = await effectiveToolGate(
|
|
972
|
+
{
|
|
973
|
+
name: tc.function.name,
|
|
974
|
+
argsJson: tc.function.arguments,
|
|
975
|
+
isReadOnly,
|
|
976
|
+
toolCallId: tc.id,
|
|
977
|
+
...gateDetails,
|
|
978
|
+
},
|
|
979
|
+
signal,
|
|
980
|
+
);
|
|
981
|
+
} catch {
|
|
982
|
+
decision = { allow: false as const, reason: '工具执行审批失败(宿主异常)' };
|
|
983
|
+
}
|
|
984
|
+
if (!decision.allow) {
|
|
985
|
+
return { tc, result: { ok: false as const, error: decision.reason } };
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
options.onToolStart?.(tc.function.name, tc.id);
|
|
989
|
+
// 浅拷贝填入本次调用的 toolCallId:进度按调用实例路由(并行同名工具不串扰)。
|
|
990
|
+
// advisorCalls/agentCalls 是共享对象引用,计数仍全轮共享,不受拷贝影响。
|
|
991
|
+
let result = await executeToolFn(
|
|
992
|
+
tc.function.name,
|
|
993
|
+
tc.function.arguments,
|
|
994
|
+
signal,
|
|
995
|
+
{ ...toolCallContext, cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd, toolCallId: tc.id, reportProgress },
|
|
996
|
+
);
|
|
997
|
+
if (
|
|
998
|
+
tc.function.name === 'PresentResult'
|
|
999
|
+
&& result.ok
|
|
1000
|
+
&& result.metadata?.kind === 'result_manifest'
|
|
1001
|
+
&& options.commitFormalDelivery
|
|
1002
|
+
) {
|
|
1003
|
+
try {
|
|
1004
|
+
await options.commitFormalDelivery({
|
|
1005
|
+
toolCallId: tc.id,
|
|
1006
|
+
turnId: userMessage.id ?? options.userMessageId ?? 'turn:unknown',
|
|
1007
|
+
manifest: result.metadata.manifest,
|
|
1008
|
+
});
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
result = {
|
|
1011
|
+
ok: false,
|
|
1012
|
+
error: `正式交付未能可靠保存:${(error as Error).message}`,
|
|
1013
|
+
recovery: 'retry',
|
|
1014
|
+
sensitivity: 'local',
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
options.onToolEnd?.(tc.id, false);
|
|
1019
|
+
return { tc, result };
|
|
1020
|
+
} catch (e) {
|
|
1021
|
+
const wasAborted = (e as Error).name === 'AbortError';
|
|
1022
|
+
options.onToolEnd?.(tc.id, wasAborted);
|
|
1023
|
+
if (wasAborted) {
|
|
1024
|
+
return { tc, result: { ok: false as const, error: 'aborted' } };
|
|
1025
|
+
}
|
|
1026
|
+
return { tc, result: { ok: false as const, error: (e as Error).message } };
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
const startEvent = (
|
|
1031
|
+
tc: ToolCall,
|
|
1032
|
+
): Extract<LoopEvent, { type: 'tool_start' }> => ({
|
|
1033
|
+
type: 'tool_start',
|
|
1034
|
+
toolCallId: tc.id,
|
|
1035
|
+
toolName: tc.function.name,
|
|
1036
|
+
argsPreview: previewArgs(tc.function.arguments),
|
|
1037
|
+
argsFriendly: friendlyToolDescription(tc.function.name, tc.function.arguments),
|
|
1038
|
+
});
|
|
1039
|
+
const endEvent = (
|
|
1040
|
+
o: WorkerOutcome,
|
|
1041
|
+
): Extract<LoopEvent, { type: 'tool_end' }> => ({
|
|
1042
|
+
type: 'tool_end',
|
|
1043
|
+
toolCallId: o.tc.id,
|
|
1044
|
+
toolName: o.tc.function.name,
|
|
1045
|
+
ok: o.result.ok,
|
|
1046
|
+
content: o.result.ok
|
|
1047
|
+
? o.result.content
|
|
1048
|
+
: o.result.error === 'aborted'
|
|
1049
|
+
? '(已中断)'
|
|
1050
|
+
: `Error: ${o.result.error}`,
|
|
1051
|
+
metadata: o.result.metadata,
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
const allConcurrencySafe = toolCallsByIndex.every((tc) => {
|
|
1055
|
+
const tool = getToolByName(tc.function.name);
|
|
1056
|
+
return tool?.isConcurrencySafe === true;
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
let settled: WorkerOutcome[];
|
|
1060
|
+
|
|
1061
|
+
if (allConcurrencySafe && toolCallsByIndex.length > 1) {
|
|
1062
|
+
// —— 并行:事件队列把各 worker 的 start/end 按真实完成顺序交错 yield 给 UI。
|
|
1063
|
+
const queue: LoopEvent[] = [];
|
|
1064
|
+
let signalNew: (() => void) | null = null;
|
|
1065
|
+
const enqueue = (ev: LoopEvent) => {
|
|
1066
|
+
queue.push(ev);
|
|
1067
|
+
signalNew?.();
|
|
1068
|
+
signalNew = null;
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
const workers = toolCallsByIndex.map(async (tc) => {
|
|
1072
|
+
enqueue(startEvent(tc));
|
|
1073
|
+
const o = await runOne(tc, (message, detail) => enqueue({
|
|
1074
|
+
type: 'tool_progress',
|
|
1075
|
+
toolCallId: tc.id,
|
|
1076
|
+
toolName: tc.function.name,
|
|
1077
|
+
message,
|
|
1078
|
+
detail,
|
|
1079
|
+
}));
|
|
1080
|
+
enqueue(endEvent(o));
|
|
1081
|
+
return o;
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
const allDone = Promise.all(workers);
|
|
1085
|
+
let finished = false;
|
|
1086
|
+
void allDone.then(() => {
|
|
1087
|
+
finished = true;
|
|
1088
|
+
signalNew?.();
|
|
1089
|
+
signalNew = null;
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
while (!finished || queue.length > 0) {
|
|
1093
|
+
while (queue.length > 0) yield queue.shift()!;
|
|
1094
|
+
if (finished) break;
|
|
1095
|
+
await new Promise<void>((r) => {
|
|
1096
|
+
signalNew = r;
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
settled = await allDone;
|
|
1101
|
+
} else {
|
|
1102
|
+
// —— 串行:逐个执行(含写类工具,或只有一个工具)。事件直接顺序 yield,无需队列。
|
|
1103
|
+
settled = [];
|
|
1104
|
+
for (const tc of toolCallsByIndex) {
|
|
1105
|
+
yield startEvent(tc);
|
|
1106
|
+
const queue: LoopEvent[] = [];
|
|
1107
|
+
let wake: (() => void) | null = null;
|
|
1108
|
+
let finished = false;
|
|
1109
|
+
const worker = runOne(tc, (message, detail) => {
|
|
1110
|
+
queue.push({
|
|
1111
|
+
type: 'tool_progress',
|
|
1112
|
+
toolCallId: tc.id,
|
|
1113
|
+
toolName: tc.function.name,
|
|
1114
|
+
message,
|
|
1115
|
+
detail,
|
|
1116
|
+
});
|
|
1117
|
+
wake?.();
|
|
1118
|
+
wake = null;
|
|
1119
|
+
}).finally(() => {
|
|
1120
|
+
finished = true;
|
|
1121
|
+
wake?.();
|
|
1122
|
+
wake = null;
|
|
1123
|
+
});
|
|
1124
|
+
while (!finished || queue.length > 0) {
|
|
1125
|
+
while (queue.length > 0) yield queue.shift()!;
|
|
1126
|
+
if (finished) break;
|
|
1127
|
+
await new Promise<void>((resolve) => { wake = resolve; });
|
|
1128
|
+
}
|
|
1129
|
+
const o = await worker;
|
|
1130
|
+
yield endEvent(o);
|
|
1131
|
+
settled.push(o);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// 本回合工作量累计 → 上调顾问配额(只增不减;已用掉的次数不会因此回退)。
|
|
1136
|
+
executedToolCalls += settled.length;
|
|
1137
|
+
toolCallContext.advisorCalls!.max = advisorQuotaFor(executedToolCalls);
|
|
1138
|
+
|
|
1139
|
+
// 严格按 toolCallsByIndex 顺序把 tool 消息回填 history(settled 已保持该顺序)。
|
|
1140
|
+
let anyAborted = false;
|
|
1141
|
+
for (const { tc, result } of settled) {
|
|
1142
|
+
evidenceLedger.record(tc.function.name, tc.function.arguments, result);
|
|
1143
|
+
if (!result.ok && result.error === 'aborted') anyAborted = true;
|
|
1144
|
+
if (tc.function.name === 'PresentResult' && result.ok && result.metadata?.kind === 'result_manifest') {
|
|
1145
|
+
resultPresented = true;
|
|
1146
|
+
presentedResultUnverified ||= result.metadata.manifest.files.some(
|
|
1147
|
+
(file) => file.verification !== 'passed',
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
const rawContent = result.ok ? result.content : `Error: ${result.error}`;
|
|
1151
|
+
// 微压缩仅对成功结果走(失败结果通常很短,不需要压缩)
|
|
1152
|
+
const content = result.ok
|
|
1153
|
+
? microCompact(tc.function.name, rawContent, sessionState?.microCompact)
|
|
1154
|
+
: rawContent;
|
|
1155
|
+
history.push({
|
|
1156
|
+
role: 'tool',
|
|
1157
|
+
content,
|
|
1158
|
+
tool_call_id: tc.id,
|
|
1159
|
+
...(result.metadata ? { display_metadata: result.metadata } : {}),
|
|
1160
|
+
timestamp: Date.now(),
|
|
1161
|
+
id: crypto.randomUUID(),
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// If the third identical observation is also the configured final turn, retain the
|
|
1166
|
+
// established max_turns termination contract. The stall guard is valuable only when
|
|
1167
|
+
// it can stop a failing path before the caller's explicit turn budget does.
|
|
1168
|
+
const preemptedRepeatedFailure = settled.some(
|
|
1169
|
+
({ result }) =>
|
|
1170
|
+
!result.ok && result.error.includes('第三次调用已在执行前拒绝'),
|
|
1171
|
+
);
|
|
1172
|
+
const stall = preemptedRepeatedFailure
|
|
1173
|
+
? ({
|
|
1174
|
+
stalled: true as const,
|
|
1175
|
+
reason: 'repeated_failure' as const,
|
|
1176
|
+
})
|
|
1177
|
+
: turn + 1 < maxTurns
|
|
1178
|
+
? stallGuard.observe(
|
|
1179
|
+
settled.map((item) => item.tc),
|
|
1180
|
+
settled.map((item) => item.result),
|
|
1181
|
+
assistantText,
|
|
1182
|
+
)
|
|
1183
|
+
: { stalled: false as const };
|
|
1184
|
+
if (stall.stalled) {
|
|
1185
|
+
const message = stall.reason === 'repeated_failure'
|
|
1186
|
+
? '检测到同一工具以相同参数连续返回相同失败,已停止重复重试。'
|
|
1187
|
+
: '检测到连续工具轮次没有产生新文本或新结果,已停止无进展循环。';
|
|
1188
|
+
yield { type: 'warning', message };
|
|
1189
|
+
yield { type: 'harness_diagnostics', diagnostics: harnessDiagnostics('stalled') };
|
|
1190
|
+
yield { type: 'error', error: message, code: 'stalled' };
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// 关键节点截图回填:把工具附带的实况截图作为一条带图 user 消息喂给(视觉)模型自我核验,
|
|
1195
|
+
// 并 yield injected_message 让宿主在对话里即时展示这张实况截图。
|
|
1196
|
+
// 纯增强:读图/编码等任何失败都静默跳过,绝不打断本轮(与工具「失败不抛异常」一致)。
|
|
1197
|
+
let injectedShot: Message | null = null;
|
|
1198
|
+
try {
|
|
1199
|
+
injectedShot = await injectVisionFollowups(history, settled, provider);
|
|
1200
|
+
} catch { /* 截图回填失败不影响主流程 */ }
|
|
1201
|
+
// The history copy is intentionally downgraded after the turn. Emit a detached snapshot
|
|
1202
|
+
// so TUI/Desktop stores do not see their already-rendered image mutate underneath them.
|
|
1203
|
+
if (injectedShot) {
|
|
1204
|
+
yield { type: 'injected_message', message: structuredClone(injectedShot) };
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (anyAborted || signal?.aborted) {
|
|
1208
|
+
if (options.hasQueuedInput?.()) {
|
|
1209
|
+
pushQueuedInputMarker(history);
|
|
1210
|
+
yield { type: 'interrupted', reason: 'queued_input' };
|
|
1211
|
+
} else {
|
|
1212
|
+
pushManualInterruptMarker(history);
|
|
1213
|
+
yield { type: 'interrupted', reason: 'manual' };
|
|
1214
|
+
}
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (options.hasQueuedInput?.()) {
|
|
1219
|
+
pushQueuedInputMarker(history);
|
|
1220
|
+
yield { type: 'interrupted', reason: 'queued_input' };
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// 6. 继续 while 让模型看到 tool_result 后继续推理
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// code='max_turns':撞最大轮数上限而终结,stop_reason='max_turns'
|
|
1228
|
+
yield { type: 'harness_diagnostics', diagnostics: harnessDiagnostics('max_turns') };
|
|
1229
|
+
yield {
|
|
1230
|
+
type: 'error',
|
|
1231
|
+
error: `达到最大轮数 ${maxTurns},提前结束(防止失控)。如果合理可以提高 maxTurns。`,
|
|
1232
|
+
code: 'max_turns',
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// ---------------- 辅助函数 ----------------
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* 易变上下文只存在于 provider 请求副本中,不会进入会话历史。
|
|
1240
|
+
*/
|
|
1241
|
+
export const SYSTEM_REMINDER_OPEN = '<system-reminder>';
|
|
1242
|
+
export const SYSTEM_REMINDER_CLOSE = '</system-reminder>';
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Attach transient context to the current turn's user message in a model-only
|
|
1246
|
+
* clone. Later tool-loop requests reuse the same decorated message, making each
|
|
1247
|
+
* serialized request a strict prefix extension of the previous one.
|
|
1248
|
+
*/
|
|
1249
|
+
export function buildModelMessages(
|
|
1250
|
+
history: Message[],
|
|
1251
|
+
transientContext?: string,
|
|
1252
|
+
currentUserMessageId?: string,
|
|
1253
|
+
): Message[] {
|
|
1254
|
+
const transient = transientContext?.trim();
|
|
1255
|
+
if (!transient) return history;
|
|
1256
|
+
let index = currentUserMessageId
|
|
1257
|
+
? history.findIndex((message) => message.role === 'user' && message.id === currentUserMessageId)
|
|
1258
|
+
: -1;
|
|
1259
|
+
if (index < 0) {
|
|
1260
|
+
for (let cursor = history.length - 1; cursor >= 0; cursor--) {
|
|
1261
|
+
if (history[cursor]?.role === 'user') {
|
|
1262
|
+
index = cursor;
|
|
1263
|
+
break;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (index < 0) return history;
|
|
1268
|
+
const target = history[index];
|
|
1269
|
+
if (target.role !== 'user') return history;
|
|
1270
|
+
const reminder = `${SYSTEM_REMINDER_OPEN}\n${transient}\n${SYSTEM_REMINDER_CLOSE}`;
|
|
1271
|
+
const content = typeof target.content === 'string'
|
|
1272
|
+
? `${target.content}\n\n${reminder}`
|
|
1273
|
+
: [...target.content, { type: 'text' as const, text: reminder }];
|
|
1274
|
+
const next = [...history];
|
|
1275
|
+
next[index] = { ...target, content };
|
|
1276
|
+
return next;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// 关键节点图像核验的合成消息标记:既是给模型的提示前缀,也是 evictPriorVisionShots 识别旧图消息的锚。
|
|
1280
|
+
export const VISION_SHOT_MARKER = '【实况图像】';
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* 历史标记。这套机制最初只有 WebBrowser 用得上,文案写死了「浏览器」;
|
|
1284
|
+
* 现在 Bash 的 ATTACH_IMAGE sentinel 也能产图(屏幕截图 / 生成的产物),文案必须中性。
|
|
1285
|
+
* 但**持久化会话里仍存着旧前缀的消息**,淘汰逻辑必须继续认它 ——
|
|
1286
|
+
* 否则老会话里的旧截图永远不会被降级,图片一路累积撑爆上下文。
|
|
1287
|
+
*/
|
|
1288
|
+
const LEGACY_VISION_SHOT_MARKERS = ['【浏览器实况截图】'] as const;
|
|
1289
|
+
|
|
1290
|
+
const VISION_SHOT_PROMPT = `${VISION_SHOT_MARKER}上面是刚才工具执行后附带的真实图像(浏览器页面 / 屏幕截图 / 生成的产物)。请仔细看图核对:是否真的到达/呈现了你预期的状态?若与预期不符(空白/报错/弹窗遮挡/验证拦截/内容不对),不要假设成功,如实说明并调整下一步。`;
|
|
1291
|
+
|
|
1292
|
+
/** 一条 user 消息的文本是否是「实况图像」合成消息(含历史前缀)。 */
|
|
1293
|
+
function isVisionShotText(text: string): boolean {
|
|
1294
|
+
return (
|
|
1295
|
+
text.startsWith(VISION_SHOT_MARKER) ||
|
|
1296
|
+
LEGACY_VISION_SHOT_MARKERS.some((marker) => text.startsWith(marker))
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* 把本轮工具附带的实况截图(ToolResult.visionImages)组装成一条带图 user 消息喂给模型自我核验。
|
|
1302
|
+
* - 仅在模型明确支持识图时注入(provider.supportsVision === true);否则丢弃(不假装核验、也不给非视觉端点发图触发 400)。
|
|
1303
|
+
* - 注入前先把历史里旧的截图消息降级为纯文字占位(evictPriorVisionShots),保证上下文里最多一张实况图,
|
|
1304
|
+
* 避免图片累积撑爆上下文(呼应浏览器 ADR「高频帧只存内存、只持久化最终结果」)。
|
|
1305
|
+
* - **失败的工具结果同样回填**:出错现场那张图往往比成功时更有信息量(权限弹窗、
|
|
1306
|
+
* 验证拦截、意外的前台窗口)。此前这里要求 `ok === true`,逼得想附图的工具只能
|
|
1307
|
+
* 把失败伪装成成功——见 types.ts 里 ToolResult 那段注释。
|
|
1308
|
+
*/
|
|
1309
|
+
async function injectVisionFollowups(
|
|
1310
|
+
history: Message[],
|
|
1311
|
+
settled: { result: ToolResult }[],
|
|
1312
|
+
provider: Provider,
|
|
1313
|
+
): Promise<Message | null> {
|
|
1314
|
+
if (provider.supportsVision !== true) return null;
|
|
1315
|
+
const images = settled.flatMap((s) => s.result.visionImages ?? []);
|
|
1316
|
+
if (images.length === 0) return null;
|
|
1317
|
+
evictPriorVisionShots(history);
|
|
1318
|
+
// 工具可以给自己这批图配一句说明(visionNote)。内核只负责把它拼在通用提示之后,
|
|
1319
|
+
// 不解读内容——「这是一组 1Hz 连续帧」这类语义只有产图的工具知道。
|
|
1320
|
+
const notes = settled
|
|
1321
|
+
.flatMap((s) => (s.result.visionNote ? [s.result.visionNote] : []))
|
|
1322
|
+
.filter((note, index, all) => all.indexOf(note) === index);
|
|
1323
|
+
const text = notes.length > 0
|
|
1324
|
+
? `${VISION_SHOT_PROMPT}\n\n${notes.join('\n\n')}`
|
|
1325
|
+
: VISION_SHOT_PROMPT;
|
|
1326
|
+
const content = await buildUserContent({ text, images });
|
|
1327
|
+
const msg: Message = { role: 'user', content, timestamp: Date.now(), id: crypto.randomUUID() };
|
|
1328
|
+
history.push(msg);
|
|
1329
|
+
return msg;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/** 把历史里旧的实况图像 user 消息降级为纯文字占位、剔除其图片,释放上下文(只保留最近一张真图)。 */
|
|
1333
|
+
function evictPriorVisionShots(history: Message[]): void {
|
|
1334
|
+
for (const msg of history) {
|
|
1335
|
+
if (msg.role !== 'user' || !Array.isArray(msg.content)) continue;
|
|
1336
|
+
const textPart = msg.content.find((p) => p.type === 'text');
|
|
1337
|
+
if (textPart?.type === 'text' && isVisionShotText(textPart.text)) {
|
|
1338
|
+
msg.content = `${VISION_SHOT_MARKER}(此前的实况图像已从上下文移除以节省空间。)`;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* 用户按ESC手动中断
|
|
1345
|
+
*/
|
|
1346
|
+
function pushManualInterruptMarker(history: Message[]): void {
|
|
1347
|
+
history.push({
|
|
1348
|
+
role: 'user',
|
|
1349
|
+
content: '(用户按下了 ESC/Ctrl+C 中断了任务。等待用户新指令。)',
|
|
1350
|
+
timestamp: Date.now(),
|
|
1351
|
+
id: crypto.randomUUID(),
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* 有新的用户输入在队列中,当前安全停靠
|
|
1357
|
+
*/
|
|
1358
|
+
function pushQueuedInputMarker(history: Message[]): void {
|
|
1359
|
+
history.push({
|
|
1360
|
+
role: 'user',
|
|
1361
|
+
content: [
|
|
1362
|
+
'(系统提示:用户在工作过程中发送了新的消息。',
|
|
1363
|
+
'你现在看到的是上一轮工作在被打断前已完成的部分。',
|
|
1364
|
+
'请阅读用户的新消息,判断:',
|
|
1365
|
+
'1. 如果是不相关的新任务:简要确认已收到,转向新任务,可在新任务完成后询问是否回到之前工作',
|
|
1366
|
+
'2. 如果是补充信息或前置依赖:把新信息纳入考虑,判断是否需要调整已做的工作,然后继续',
|
|
1367
|
+
'3. 如果是改变方向或取消:评估已完成的部分,说明你对变更的理解,按新方向推进)',
|
|
1368
|
+
].join('\n'),
|
|
1369
|
+
timestamp: Date.now(),
|
|
1370
|
+
id: crypto.randomUUID(),
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/** 把一条 tool_call_delta 合并到累积数组上 */
|
|
1375
|
+
function mergeToolCallDelta(
|
|
1376
|
+
acc: ToolCall[],
|
|
1377
|
+
ev: {
|
|
1378
|
+
index: number;
|
|
1379
|
+
id?: string;
|
|
1380
|
+
name?: string;
|
|
1381
|
+
argumentsDelta?: string;
|
|
1382
|
+
},
|
|
1383
|
+
): void {
|
|
1384
|
+
let slot = acc[ev.index];
|
|
1385
|
+
if (!slot) {
|
|
1386
|
+
slot = {
|
|
1387
|
+
id: '',
|
|
1388
|
+
type: 'function',
|
|
1389
|
+
function: { name: '', arguments: '' },
|
|
1390
|
+
};
|
|
1391
|
+
acc[ev.index] = slot;
|
|
1392
|
+
}
|
|
1393
|
+
if (ev.id) slot.id = ev.id;
|
|
1394
|
+
if (ev.name) slot.function.name += ev.name;
|
|
1395
|
+
if (ev.argumentsDelta) slot.function.arguments += ev.argumentsDelta;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/** 给 UI 展示用的参数预览(只取前 60 字符) */
|
|
1399
|
+
function previewArgs(rawJson: string): string {
|
|
1400
|
+
const oneLine = rawJson.replace(/\s+/g, ' ').trim();
|
|
1401
|
+
if (oneLine.length <= 60) return oneLine;
|
|
1402
|
+
return oneLine.slice(0, 60) + '...';
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* 把工具名 + raw arguments JSON 转换成人话描述。
|
|
1407
|
+
* UI 优先用这个显示在 spinner 行(如 "Reading src/foo.ts")。
|
|
1408
|
+
* 已知工具走 switch;未知工具或 JSON parse 失败 fallback 到 `${toolName}(...)`。
|
|
1409
|
+
*/
|
|
1410
|
+
function friendlyToolDescription(toolName: string, rawArgsJson: string): string {
|
|
1411
|
+
let args: Record<string, unknown> = {};
|
|
1412
|
+
try {
|
|
1413
|
+
args = JSON.parse(rawArgsJson) ?? {};
|
|
1414
|
+
} catch {
|
|
1415
|
+
return `${toolName}(...)`;
|
|
1416
|
+
}
|
|
1417
|
+
const truncate = (s: unknown, n: number): string => {
|
|
1418
|
+
if (typeof s !== 'string') return '';
|
|
1419
|
+
return s.length > n ? s.slice(0, n) + '…' : s;
|
|
1420
|
+
};
|
|
1421
|
+
switch (toolName) {
|
|
1422
|
+
case 'Read': return `Reading ${truncate(args.file_path, 60)}`;
|
|
1423
|
+
case 'Edit': return `Editing ${truncate(args.file_path, 60)}`;
|
|
1424
|
+
case 'MultiEdit': {
|
|
1425
|
+
const n = Array.isArray(args.edits) ? args.edits.length : 0;
|
|
1426
|
+
return `Editing ${truncate(args.file_path, 50)}${n ? ` (${n} 处)` : ''}`;
|
|
1427
|
+
}
|
|
1428
|
+
case 'Write': return `Writing ${truncate(args.file_path, 60)}`;
|
|
1429
|
+
case 'Bash': return `Running \`${truncate(args.command, 40)}\``;
|
|
1430
|
+
case 'Grep': {
|
|
1431
|
+
const p = truncate(args.pattern, 30);
|
|
1432
|
+
const g = typeof args.glob === 'string' && args.glob.length > 0 ? ` in ${truncate(args.glob, 25)}` : '';
|
|
1433
|
+
return `Searching \`${p}\`${g}`;
|
|
1434
|
+
}
|
|
1435
|
+
case 'Glob': return `Globbing \`${truncate(args.pattern, 50)}\``;
|
|
1436
|
+
case 'WebFetch':
|
|
1437
|
+
case 'WebBrowser': {
|
|
1438
|
+
try {
|
|
1439
|
+
return `Fetching ${new URL(String(args.url)).host}`;
|
|
1440
|
+
} catch {
|
|
1441
|
+
return `Fetching ...`;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
case 'WebSearch': return `Searching \`${truncate(args.query, 50)}\``;
|
|
1445
|
+
case 'Agent': {
|
|
1446
|
+
const t = typeof args.agent_type === 'string' ? args.agent_type : 'explore';
|
|
1447
|
+
return `子任务[${t}] ${truncate(args.description, 50)}`;
|
|
1448
|
+
}
|
|
1449
|
+
default: return `${toolName}(...)`;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// 导出供测试用(friendlyToolDescription 是 module-private 纯函数)
|
|
1454
|
+
export { friendlyToolDescription };
|