@cjhyy/code-shell-core 0.5.0-rc.1 → 0.6.0-rc.1
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 +13 -10
- package/dist/agent/agent-definition-registry.d.ts +21 -3
- package/dist/agent/agent-definition-registry.js +49 -18
- package/dist/agent/agent-definition.d.ts +32 -0
- package/dist/agent/agent-definition.js +49 -4
- package/dist/arena/arena.js +11 -12
- package/dist/arena/context/context-tools.js +47 -6
- package/dist/arena/context/within-root.d.ts +7 -0
- package/dist/arena/context/within-root.js +15 -0
- package/dist/arena/iterate/phases/argue.js +8 -2
- package/dist/arena/iterate/phases/revise.js +1 -1
- package/dist/arena/iterate/phases/tournament.js +3 -3
- package/dist/arena/ledger.js +9 -1
- package/dist/arena/phases/adjudication.js +1 -4
- package/dist/arena/phases/build-consensus.js +1 -4
- package/dist/arena/phases/cross-review.js +2 -8
- package/dist/arena/phases/debate-rounds.js +1 -4
- package/dist/arena/phases/participant-research.js +1 -4
- package/dist/arena/phases/planning-detail-expansion.js +8 -10
- package/dist/arena/planner.js +0 -1
- package/dist/arena/providers/docs.d.ts +1 -0
- package/dist/arena/providers/docs.js +5 -2
- package/dist/arena/providers/repo.d.ts +1 -0
- package/dist/arena/providers/repo.js +5 -2
- package/dist/arena/strategies/utils.d.ts +1 -4
- package/dist/arena/strategies/utils.js +7 -51
- package/dist/arena/transitions.d.ts +7 -2
- package/dist/arena/transitions.js +9 -4
- package/dist/arena/types.d.ts +8 -2
- package/dist/automation/cron-expr.d.ts +41 -0
- package/dist/automation/cron-expr.js +169 -0
- package/dist/automation/index.d.ts +41 -0
- package/dist/automation/index.js +39 -0
- package/dist/automation/runner.d.ts +84 -0
- package/dist/automation/runner.js +92 -0
- package/dist/automation/scheduler.d.ts +194 -0
- package/dist/automation/scheduler.js +623 -0
- package/dist/automation/store.d.ts +39 -0
- package/dist/automation/store.js +122 -0
- package/dist/automation/write-policy.d.ts +34 -0
- package/dist/automation/write-policy.js +113 -0
- package/dist/automation/write-run.d.ts +51 -0
- package/dist/automation/write-run.js +38 -0
- package/dist/capability-control/disabled-lists.d.ts +25 -0
- package/dist/capability-control/disabled-lists.js +57 -0
- package/dist/capability-control/index.d.ts +5 -0
- package/dist/capability-control/index.js +3 -0
- package/dist/capability-control/overlay.d.ts +64 -0
- package/dist/capability-control/overlay.js +116 -0
- package/dist/capability-control/project.d.ts +65 -0
- package/dist/capability-control/project.js +147 -0
- package/dist/capability-control/service.d.ts +70 -0
- package/dist/capability-control/service.js +147 -0
- package/dist/capability-control/types.d.ts +62 -0
- package/dist/capability-control/types.js +17 -0
- package/dist/cc-orchestrator/agent-adapter.d.ts +50 -0
- package/dist/cc-orchestrator/agent-adapter.js +133 -0
- package/dist/cc-orchestrator/cc-capability.d.ts +19 -0
- package/dist/cc-orchestrator/cc-capability.js +53 -0
- package/dist/cc-orchestrator/codex-session-discovery.d.ts +24 -0
- package/dist/cc-orchestrator/codex-session-discovery.js +191 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +25 -0
- package/dist/cc-orchestrator/codex-session-history.js +187 -0
- package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
- package/dist/cc-orchestrator/external-agent-changes.js +214 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +18 -0
- package/dist/cc-orchestrator/external-agent-driver.js +69 -0
- package/dist/cc-orchestrator/index.d.ts +8 -0
- package/dist/cc-orchestrator/index.js +8 -0
- package/dist/cc-orchestrator/relevance-judge.d.ts +15 -0
- package/dist/cc-orchestrator/relevance-judge.js +29 -0
- package/dist/cc-orchestrator/session-discovery.d.ts +46 -0
- package/dist/cc-orchestrator/session-discovery.js +125 -0
- package/dist/cc-orchestrator/session-history.d.ts +19 -0
- package/dist/cc-orchestrator/session-history.js +67 -0
- package/dist/cli/agent-server-stdio.d.ts +26 -1
- package/dist/cli/agent-server-stdio.js +221 -28
- package/dist/cli/agent-server-tcp.d.ts +20 -0
- package/dist/cli/agent-server-tcp.js +134 -0
- package/dist/cli/graceful-shutdown.d.ts +27 -0
- package/dist/cli/graceful-shutdown.js +30 -0
- package/dist/context/compaction.d.ts +56 -0
- package/dist/context/compaction.js +209 -15
- package/dist/context/manager.d.ts +0 -20
- package/dist/context/manager.js +25 -41
- package/dist/cost-tracker.js +5 -61
- package/dist/credentials/cipher.d.ts +49 -0
- package/dist/credentials/cipher.js +45 -0
- package/dist/credentials/cookie-jar.d.ts +24 -0
- package/dist/credentials/cookie-jar.js +40 -0
- package/dist/credentials/index.d.ts +7 -0
- package/dist/credentials/index.js +5 -0
- package/dist/credentials/inject-credential-tool.d.ts +20 -0
- package/dist/credentials/inject-credential-tool.js +130 -0
- package/dist/credentials/store.d.ts +72 -0
- package/dist/credentials/store.js +184 -0
- package/dist/credentials/types.d.ts +56 -0
- package/dist/credentials/use-credential-tool.d.ts +29 -0
- package/dist/credentials/use-credential-tool.js +205 -0
- package/dist/credentials/use-gate.d.ts +56 -0
- package/dist/credentials/use-gate.js +52 -0
- package/dist/cron/cron-runtime.d.ts +2 -0
- package/dist/cron/cron-runtime.js +2 -0
- package/dist/cron/cron-store.d.ts +2 -0
- package/dist/cron/cron-store.js +2 -0
- package/dist/cron/scheduler.d.ts +5 -32
- package/dist/cron/scheduler.js +5 -113
- package/dist/data/model-metadata.d.ts +77 -0
- package/dist/data/model-metadata.js +56 -0
- package/dist/data/model-metadata.json +216 -0
- package/dist/data/openrouter-models.d.ts +18 -7
- package/dist/data/openrouter-models.js +35 -8
- package/dist/data/openrouter-sync.js +1 -1
- package/dist/engine/aux-key.d.ts +10 -0
- package/dist/engine/aux-key.js +11 -0
- package/dist/engine/dynamic-tool-defs.d.ts +19 -0
- package/dist/engine/dynamic-tool-defs.js +36 -0
- package/dist/engine/engine.d.ts +431 -108
- package/dist/engine/engine.js +1630 -279
- package/dist/engine/friendly-error.d.ts +18 -0
- package/dist/engine/friendly-error.js +63 -0
- package/dist/engine/goal.d.ts +187 -0
- package/dist/engine/goal.js +196 -0
- package/dist/engine/image-compression.d.ts +81 -0
- package/dist/engine/image-compression.js +186 -0
- package/dist/engine/image-policy.d.ts +165 -0
- package/dist/engine/image-policy.js +189 -0
- package/dist/engine/model-connections-pool.d.ts +17 -0
- package/dist/engine/model-connections-pool.js +66 -0
- package/dist/engine/model-facade.d.ts +10 -0
- package/dist/engine/model-facade.js +25 -0
- package/dist/engine/patch-orphaned-tools.js +3 -0
- package/dist/engine/reactive-threshold.d.ts +13 -0
- package/dist/engine/reactive-threshold.js +17 -0
- package/dist/engine/resolve-llm-config.d.ts +16 -0
- package/dist/engine/resolve-llm-config.js +44 -0
- package/dist/engine/runtime.js +9 -1
- package/dist/engine/sandbox-cache-key.d.ts +10 -0
- package/dist/engine/sandbox-cache-key.js +9 -0
- package/dist/engine/sandbox-config.d.ts +31 -0
- package/dist/engine/sandbox-config.js +38 -0
- package/dist/engine/session-title.d.ts +13 -0
- package/dist/engine/session-title.js +34 -0
- package/dist/engine/session-usage.d.ts +19 -0
- package/dist/engine/session-usage.js +25 -0
- package/dist/engine/steer-queue.d.ts +32 -0
- package/dist/engine/steer-queue.js +26 -0
- package/dist/engine/streaming-tool-queue.d.ts +12 -0
- package/dist/engine/streaming-tool-queue.js +50 -10
- package/dist/engine/turn-loop.d.ts +94 -9
- package/dist/engine/turn-loop.js +775 -342
- package/dist/engine/types.d.ts +175 -0
- package/dist/engine/types.js +13 -0
- package/dist/external-agents/config.d.ts +2 -0
- package/dist/external-agents/config.js +15 -0
- package/dist/external-agents/types.d.ts +31 -0
- package/dist/external-agents/types.js +1 -0
- package/dist/git/parse-log.d.ts +13 -0
- package/dist/git/parse-log.js +21 -0
- package/dist/git/utils.d.ts +8 -6
- package/dist/git/utils.js +62 -26
- package/dist/git/worktree.d.ts +48 -0
- package/dist/git/worktree.js +111 -20
- package/dist/hooks/events.d.ts +23 -1
- package/dist/hooks/goal-stop-hook.d.ts +58 -0
- package/dist/hooks/goal-stop-hook.js +279 -0
- package/dist/hooks/hook-output.d.ts +33 -0
- package/dist/hooks/hook-output.js +79 -0
- package/dist/hooks/registry.d.ts +15 -0
- package/dist/hooks/registry.js +57 -1
- package/dist/hooks/shell-runner.d.ts +2 -0
- package/dist/hooks/shell-runner.js +48 -18
- package/dist/index.d.ts +79 -11
- package/dist/index.js +89 -6
- package/dist/llm/capabilities/param-specs.d.ts +14 -0
- package/dist/llm/capabilities/param-specs.js +62 -0
- package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
- package/dist/llm/capabilities/reasoning-control.js +38 -0
- package/dist/llm/capabilities/rules.js +37 -3
- package/dist/llm/capabilities/types.d.ts +32 -2
- package/dist/llm/clamp-max-tokens.d.ts +11 -0
- package/dist/llm/clamp-max-tokens.js +17 -0
- package/dist/llm/client-base.d.ts +69 -5
- package/dist/llm/client-base.js +185 -25
- package/dist/llm/client-factory.d.ts +3 -3
- package/dist/llm/client-factory.js +2 -2
- package/dist/llm/model-cache.js +4 -2
- package/dist/llm/model-pool.d.ts +29 -11
- package/dist/llm/model-pool.js +36 -20
- package/dist/llm/provider-auth.d.ts +41 -0
- package/dist/llm/provider-auth.js +76 -0
- package/dist/llm/provider-catalog.d.ts +6 -2
- package/dist/llm/providers/anthropic.d.ts +32 -2
- package/dist/llm/providers/anthropic.js +207 -18
- package/dist/llm/providers/openai.d.ts +47 -2
- package/dist/llm/providers/openai.js +478 -71
- package/dist/llm/reasoning-setting.d.ts +51 -0
- package/dist/llm/reasoning-setting.js +40 -0
- package/dist/llm/stop-reason.d.ts +13 -0
- package/dist/llm/stop-reason.js +21 -0
- package/dist/llm/stream-watchdog.js +5 -1
- package/dist/llm/strip-vision.d.ts +32 -0
- package/dist/llm/strip-vision.js +94 -0
- package/dist/llm/token-counter.js +9 -2
- package/dist/llm/types.d.ts +9 -6
- package/dist/logging/logger.js +18 -4
- package/dist/logging/sanitize-messages.d.ts +10 -0
- package/dist/logging/sanitize-messages.js +112 -0
- package/dist/lsp/client.js +28 -8
- package/dist/lsp/manager.d.ts +1 -1
- package/dist/lsp/manager.js +42 -11
- package/dist/lsp/root-path.d.ts +9 -0
- package/dist/lsp/root-path.js +12 -0
- package/dist/model-catalog/builtin.d.ts +12 -0
- package/dist/model-catalog/builtin.js +407 -0
- package/dist/model-catalog/gen-connections.d.ts +20 -0
- package/dist/model-catalog/gen-connections.js +28 -0
- package/dist/model-catalog/index.d.ts +41 -0
- package/dist/model-catalog/index.js +90 -0
- package/dist/model-catalog/params.d.ts +20 -0
- package/dist/model-catalog/params.js +45 -0
- package/dist/model-catalog/resolve.d.ts +48 -0
- package/dist/model-catalog/resolve.js +33 -0
- package/dist/model-catalog/save-entry.d.ts +32 -0
- package/dist/model-catalog/save-entry.js +104 -0
- package/dist/model-catalog/types.d.ts +561 -0
- package/dist/model-catalog/types.js +93 -0
- package/dist/model-catalog/upsert.d.ts +9 -0
- package/dist/model-catalog/upsert.js +8 -0
- package/dist/onboarding.d.ts +12 -82
- package/dist/onboarding.js +75 -333
- package/dist/plugins/gitOps.d.ts +53 -0
- package/dist/plugins/gitOps.js +130 -5
- package/dist/plugins/installer/checkUpdate.d.ts +16 -0
- package/dist/plugins/installer/checkUpdate.js +32 -0
- package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
- package/dist/plugins/installer/codex/convertAgents.js +55 -0
- package/dist/plugins/installer/codex/convertCommands.d.ts +19 -0
- package/dist/plugins/installer/codex/convertCommands.js +46 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +11 -0
- package/dist/plugins/installer/codex/convertMcp.js +85 -0
- package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
- package/dist/plugins/installer/codex/convertSkills.js +33 -0
- package/dist/plugins/installer/detectFormat.d.ts +2 -0
- package/dist/plugins/installer/detectFormat.js +6 -0
- package/dist/plugins/installer/install.d.ts +7 -0
- package/dist/plugins/installer/install.js +123 -0
- package/dist/plugins/installer/installFromArchive.d.ts +43 -0
- package/dist/plugins/installer/installFromArchive.js +134 -0
- package/dist/plugins/installer/installFromSource.d.ts +13 -0
- package/dist/plugins/installer/installFromSource.js +51 -0
- package/dist/plugins/installer/list.d.ts +9 -0
- package/dist/plugins/installer/list.js +30 -0
- package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
- package/dist/plugins/installer/loadPluginAgents.js +27 -0
- package/dist/plugins/installer/loadPluginMcp.d.ts +14 -0
- package/dist/plugins/installer/loadPluginMcp.js +107 -0
- package/dist/plugins/installer/parseSource.d.ts +20 -0
- package/dist/plugins/installer/parseSource.js +74 -0
- package/dist/plugins/installer/paths.d.ts +5 -0
- package/dist/plugins/installer/paths.js +25 -0
- package/dist/plugins/installer/pruneDisabled.d.ts +24 -0
- package/dist/plugins/installer/pruneDisabled.js +73 -0
- package/dist/plugins/installer/types.d.ts +52 -0
- package/dist/plugins/installer/types.js +28 -0
- package/dist/plugins/installer/uninstall.d.ts +2 -0
- package/dist/plugins/installer/uninstall.js +18 -0
- package/dist/plugins/installer/unzip.d.ts +14 -0
- package/dist/plugins/installer/unzip.js +82 -0
- package/dist/plugins/installer/update.d.ts +28 -0
- package/dist/plugins/installer/update.js +97 -0
- package/dist/plugins/loadPluginHooks.d.ts +54 -1
- package/dist/plugins/loadPluginHooks.js +83 -1
- package/dist/plugins/marketplaceManager.d.ts +15 -1
- package/dist/plugins/marketplaceManager.js +58 -4
- package/dist/plugins/pluginCommandHook.js +34 -14
- package/dist/plugins/pluginContent.d.ts +30 -0
- package/dist/plugins/pluginContent.js +83 -0
- package/dist/plugins/pluginInstaller.d.ts +34 -0
- package/dist/plugins/pluginInstaller.js +168 -35
- package/dist/plugins/schemas.js +41 -6
- package/dist/plugins/types.d.ts +10 -0
- package/dist/preset/index.d.ts +7 -1
- package/dist/preset/index.js +112 -6
- package/dist/product/types.d.ts +1 -1
- package/dist/prompt/composer.d.ts +31 -0
- package/dist/prompt/composer.js +95 -25
- package/dist/prompt/instruction-scanner.js +5 -3
- package/dist/prompt/section-loader.js +1 -0
- package/dist/prompt/sections/base.md +1 -0
- package/dist/prompt/sections/browser.md +10 -0
- package/dist/prompt/sections/coding.md +4 -0
- package/dist/prompt/sections/orchestration.md +8 -0
- package/dist/protocol/chat-session-manager.d.ts +15 -2
- package/dist/protocol/chat-session-manager.js +46 -0
- package/dist/protocol/chat-session.d.ts +82 -0
- package/dist/protocol/chat-session.js +121 -1
- package/dist/protocol/client.d.ts +27 -0
- package/dist/protocol/client.js +38 -0
- package/dist/protocol/redact.d.ts +50 -0
- package/dist/protocol/redact.js +71 -0
- package/dist/protocol/server.d.ts +153 -2
- package/dist/protocol/server.js +807 -39
- package/dist/protocol/tcp-transport.d.ts +45 -0
- package/dist/protocol/tcp-transport.js +74 -0
- package/dist/protocol/transport.js +3 -2
- package/dist/protocol/types.d.ts +69 -0
- package/dist/protocol/types.js +22 -0
- package/dist/quota/credentials.d.ts +3 -0
- package/dist/quota/credentials.js +80 -0
- package/dist/quota/index.d.ts +36 -0
- package/dist/quota/index.js +155 -0
- package/dist/quota/types.d.ts +48 -0
- package/dist/quota/types.js +13 -0
- package/dist/remote/bridge.d.ts +11 -2
- package/dist/remote/bridge.js +61 -33
- package/dist/review/review-prompt.d.ts +28 -0
- package/dist/review/review-prompt.js +81 -0
- package/dist/run/ArtifactTracker.js +5 -4
- package/dist/run/EngineRunner.d.ts +27 -1
- package/dist/run/EngineRunner.js +46 -8
- package/dist/run/FileRunStore.js +40 -11
- package/dist/run/RunApprovalBackend.js +25 -5
- package/dist/run/RunLock.d.ts +28 -2
- package/dist/run/RunLock.js +49 -7
- package/dist/run/RunManager.d.ts +12 -0
- package/dist/run/RunManager.js +86 -3
- package/dist/run/RunQueue.d.ts +1 -0
- package/dist/run/RunQueue.js +8 -2
- package/dist/run/factory.d.ts +8 -1
- package/dist/run/factory.js +1 -0
- package/dist/run/index.d.ts +2 -2
- package/dist/run/index.js +1 -1
- package/dist/run/redirect-target.d.ts +7 -0
- package/dist/run/redirect-target.js +13 -0
- package/dist/run/types.js +1 -1
- package/dist/runtime/background-shell.d.ts +139 -0
- package/dist/runtime/background-shell.js +495 -0
- package/dist/runtime/output-clean.d.ts +24 -0
- package/dist/runtime/output-clean.js +41 -0
- package/dist/runtime/ring-file.d.ts +64 -0
- package/dist/runtime/ring-file.js +174 -0
- package/dist/runtime/safe-spawn.js +15 -34
- package/dist/runtime/spawn-common.d.ts +148 -0
- package/dist/runtime/spawn-common.js +303 -0
- package/dist/runtime/truncate-output.d.ts +22 -0
- package/dist/runtime/truncate-output.js +49 -0
- package/dist/runtime/utf8-cut.d.ts +11 -0
- package/dist/runtime/utf8-cut.js +23 -0
- package/dist/services/auto-dream.d.ts +4 -0
- package/dist/services/auto-dream.js +26 -26
- package/dist/services/browser-open.d.ts +13 -0
- package/dist/services/browser-open.js +16 -0
- package/dist/services/diagnostics.d.ts +1 -2
- package/dist/services/diagnostics.js +12 -7
- package/dist/services/dream-consolidation.d.ts +57 -0
- package/dist/services/dream-consolidation.js +151 -0
- package/dist/services/extract-memories.d.ts +14 -1
- package/dist/services/extract-memories.js +45 -6
- package/dist/services/memory-orchestrator.d.ts +21 -0
- package/dist/services/memory-orchestrator.js +125 -32
- package/dist/services/notifier.d.ts +8 -0
- package/dist/services/notifier.js +39 -25
- package/dist/services/oauth.js +5 -7
- package/dist/services/session-memory-sort.d.ts +8 -0
- package/dist/services/session-memory-sort.js +9 -0
- package/dist/services/session-memory.js +30 -19
- package/dist/session/file-history.d.ts +124 -1
- package/dist/session/file-history.js +232 -9
- package/dist/session/memory.d.ts +116 -2
- package/dist/session/memory.js +250 -28
- package/dist/session/session-manager.d.ts +63 -1
- package/dist/session/session-manager.js +186 -5
- package/dist/session/simple-diff.d.ts +23 -0
- package/dist/session/simple-diff.js +84 -0
- package/dist/session/transcript.d.ts +25 -1
- package/dist/session/transcript.js +35 -2
- package/dist/session/undo-target.d.ts +67 -0
- package/dist/session/undo-target.js +144 -0
- package/dist/settings/disk-defaults.d.ts +42 -0
- package/dist/settings/disk-defaults.js +33 -0
- package/dist/settings/feature-flags.d.ts +64 -0
- package/dist/settings/feature-flags.js +61 -0
- package/dist/settings/manager.d.ts +101 -1
- package/dist/settings/manager.js +345 -15
- package/dist/settings/migrate-config.d.ts +45 -0
- package/dist/settings/migrate-config.js +125 -0
- package/dist/settings/personalization.d.ts +19 -0
- package/dist/settings/personalization.js +7 -0
- package/dist/settings/schema-export.d.ts +25 -0
- package/dist/settings/schema-export.js +38 -0
- package/dist/settings/schema.d.ts +1827 -396
- package/dist/settings/schema.js +361 -81
- package/dist/skills/scanner.d.ts +9 -0
- package/dist/skills/scanner.js +30 -2
- package/dist/stt/resolve-transcribe.d.ts +31 -0
- package/dist/stt/resolve-transcribe.js +108 -0
- package/dist/stt/transcribe.d.ts +51 -0
- package/dist/stt/transcribe.js +65 -0
- package/dist/tool-system/browser-bridge.d.ts +226 -0
- package/dist/tool-system/browser-bridge.js +163 -0
- package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
- package/dist/tool-system/builtin/add-marketplace.js +76 -0
- package/dist/tool-system/builtin/agent-heartbeat.d.ts +49 -0
- package/dist/tool-system/builtin/agent-heartbeat.js +89 -0
- package/dist/tool-system/builtin/agent-notifications.d.ts +12 -3
- package/dist/tool-system/builtin/agent-notifications.js +9 -3
- package/dist/tool-system/builtin/agent-output-file.d.ts +38 -0
- package/dist/tool-system/builtin/agent-output-file.js +72 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +23 -0
- package/dist/tool-system/builtin/agent-registry.js +16 -4
- package/dist/tool-system/builtin/agent.d.ts +50 -11
- package/dist/tool-system/builtin/agent.js +576 -73
- package/dist/tool-system/builtin/apply-patch/applier.js +66 -8
- package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +10 -0
- package/dist/tool-system/builtin/apply-patch/backup-targets.js +30 -0
- package/dist/tool-system/builtin/apply-patch/index.js +7 -3
- package/dist/tool-system/builtin/arena.js +6 -7
- package/dist/tool-system/builtin/background-jobs.d.ts +76 -0
- package/dist/tool-system/builtin/background-jobs.js +124 -0
- package/dist/tool-system/builtin/background-shell-tools.d.ts +20 -0
- package/dist/tool-system/builtin/background-shell-tools.js +108 -0
- package/dist/tool-system/builtin/background-work.d.ts +67 -0
- package/dist/tool-system/builtin/background-work.js +86 -0
- package/dist/tool-system/builtin/bash-output-style.d.ts +32 -0
- package/dist/tool-system/builtin/bash-output-style.js +40 -0
- package/dist/tool-system/builtin/bash.d.ts +5 -2
- package/dist/tool-system/builtin/bash.js +98 -62
- package/dist/tool-system/builtin/browser-tools.d.ts +33 -0
- package/dist/tool-system/builtin/browser-tools.js +312 -0
- package/dist/tool-system/builtin/cancel-goal.d.ts +31 -0
- package/dist/tool-system/builtin/cancel-goal.js +64 -0
- package/dist/tool-system/builtin/check-quota.d.ts +15 -0
- package/dist/tool-system/builtin/check-quota.js +34 -0
- package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
- package/dist/tool-system/builtin/complete-goal.js +45 -0
- package/dist/tool-system/builtin/config.js +7 -0
- package/dist/tool-system/builtin/cron.d.ts +7 -0
- package/dist/tool-system/builtin/cron.js +105 -9
- package/dist/tool-system/builtin/drive-claude-code.d.ts +30 -0
- package/dist/tool-system/builtin/drive-claude-code.js +157 -0
- package/dist/tool-system/builtin/edit-model-catalog.d.ts +3 -0
- package/dist/tool-system/builtin/edit-model-catalog.js +94 -0
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +21 -12
- package/dist/tool-system/builtin/eol.d.ts +29 -0
- package/dist/tool-system/builtin/eol.js +37 -0
- package/dist/tool-system/builtin/file-cache.d.ts +6 -0
- package/dist/tool-system/builtin/file-cache.js +8 -0
- package/dist/tool-system/builtin/generate-image.d.ts +60 -0
- package/dist/tool-system/builtin/generate-image.js +374 -0
- package/dist/tool-system/builtin/generate-video.d.ts +55 -0
- package/dist/tool-system/builtin/generate-video.js +361 -0
- package/dist/tool-system/builtin/grep.js +6 -2
- package/dist/tool-system/builtin/image-providers.d.ts +86 -0
- package/dist/tool-system/builtin/image-providers.js +190 -0
- package/dist/tool-system/builtin/image-uploader.d.ts +33 -0
- package/dist/tool-system/builtin/image-uploader.js +74 -0
- package/dist/tool-system/builtin/index.d.ts +24 -1
- package/dist/tool-system/builtin/index.js +310 -25
- package/dist/tool-system/builtin/mcp-tools.js +23 -3
- package/dist/tool-system/builtin/memory.js +45 -7
- package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
- package/dist/tool-system/builtin/notebook-edit.js +1 -1
- package/dist/tool-system/builtin/powershell.js +4 -1
- package/dist/tool-system/builtin/read.d.ts +2 -1
- package/dist/tool-system/builtin/read.js +11 -3
- package/dist/tool-system/builtin/repl.js +4 -1
- package/dist/tool-system/builtin/skill.js +9 -0
- package/dist/tool-system/builtin/sleep.js +12 -5
- package/dist/tool-system/builtin/task.js +4 -1
- package/dist/tool-system/builtin/tool-search.js +25 -7
- package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
- package/dist/tool-system/builtin/update-automation-memory.js +60 -0
- package/dist/tool-system/builtin/video-providers.d.ts +154 -0
- package/dist/tool-system/builtin/video-providers.js +235 -0
- package/dist/tool-system/builtin/view-image.d.ts +18 -0
- package/dist/tool-system/builtin/view-image.js +96 -0
- package/dist/tool-system/builtin/web-fetch.js +12 -2
- package/dist/tool-system/builtin/web-search.d.ts +4 -1
- package/dist/tool-system/builtin/web-search.js +57 -10
- package/dist/tool-system/builtin/worktree.d.ts +2 -1
- package/dist/tool-system/builtin/worktree.js +31 -4
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +1 -1
- package/dist/tool-system/context.d.ts +166 -6
- package/dist/tool-system/executor.d.ts +3 -2
- package/dist/tool-system/executor.js +197 -119
- package/dist/tool-system/investigation-guard.d.ts +4 -1
- package/dist/tool-system/investigation-guard.js +20 -4
- package/dist/tool-system/mcp-manager.d.ts +78 -16
- package/dist/tool-system/mcp-manager.js +357 -28
- package/dist/tool-system/path-policy.d.ts +99 -0
- package/dist/tool-system/path-policy.js +578 -0
- package/dist/tool-system/permission.d.ts +36 -1
- package/dist/tool-system/permission.js +322 -117
- package/dist/tool-system/plan-mode-allowlist.d.ts +32 -0
- package/dist/tool-system/plan-mode-allowlist.js +65 -0
- package/dist/tool-system/registry.d.ts +1 -0
- package/dist/tool-system/registry.js +26 -1
- package/dist/tool-system/sandbox/index.d.ts +8 -0
- package/dist/tool-system/sandbox/index.js +7 -2
- package/dist/tool-system/validate-tool-metadata.d.ts +36 -0
- package/dist/tool-system/validate-tool-metadata.js +63 -0
- package/dist/tool-system/validation.d.ts +10 -0
- package/dist/tool-system/validation.js +16 -3
- package/dist/types.d.ts +301 -15
- package/dist/updater.js +20 -9
- package/dist/utils/envUtils.d.ts +0 -9
- package/dist/utils/envUtils.js +3 -28
- package/dist/utils/exec.d.ts +40 -0
- package/dist/utils/exec.js +144 -0
- package/dist/utils/format.js +4 -2
- package/dist/utils/json.d.ts +12 -0
- package/dist/utils/json.js +92 -0
- package/dist/utils/lockfile.js +9 -2
- package/dist/utils/theme.d.ts +7 -1
- package/dist/utils/theme.js +5 -3
- package/dist/utils/toolDisplay.js +0 -1
- package/package.json +14 -7
- package/dist/agent/coordinator.d.ts +0 -49
- package/dist/agent/coordinator.js +0 -77
- package/dist/settings/manager.test.js +0 -73
- package/dist/tool-system/builtin/remote-trigger.d.ts +0 -6
- package/dist/tool-system/builtin/remote-trigger.js +0 -54
- package/dist/tool-system/builtin/send-message.d.ts +0 -6
- package/dist/tool-system/builtin/send-message.js +0 -47
- /package/dist/{settings/manager.test.d.ts → credentials/types.js} +0 -0
package/dist/engine/turn-loop.js
CHANGED
|
@@ -5,12 +5,36 @@
|
|
|
5
5
|
* pre_check → model_call → post_check → tool_exec → context_mgmt → hook_notify → next turn
|
|
6
6
|
*/
|
|
7
7
|
import { initialTurnState, newTurnId } from "./turn-state.js";
|
|
8
|
+
import { formatFriendlyError } from "./friendly-error.js";
|
|
8
9
|
import { wrapHookMessages } from "../hooks/inject.js";
|
|
9
10
|
import { ContextLimitError } from "../exceptions.js";
|
|
10
11
|
import { logger } from "../logging/logger.js";
|
|
11
12
|
import { checkTokenBudget, createBudgetTracker } from "./token-budget.js";
|
|
12
13
|
import { StreamingToolQueue } from "./streaming-tool-queue.js";
|
|
13
14
|
import { estimateTokens } from "../context/compaction.js";
|
|
15
|
+
import { isTruncatedStop } from "../llm/stop-reason.js";
|
|
16
|
+
import { isAbortError } from "../llm/client-base.js";
|
|
17
|
+
import { crossedReactiveThreshold } from "./reactive-threshold.js";
|
|
18
|
+
import { COMPLETE_GOAL_TOOL_NAME } from "../tool-system/builtin/complete-goal.js";
|
|
19
|
+
import { CANCEL_GOAL_TOOL_NAME } from "../tool-system/builtin/cancel-goal.js";
|
|
20
|
+
import { createGoalBudgetTracker, recordGoalUsage, goalBudgetExceeded, applyGoalExtension, limitProximity, GOAL_DEFAULT_MAX_STOP_BLOCKS, } from "./goal.js";
|
|
21
|
+
/**
|
|
22
|
+
* 把一个 ToolResult 映射成发给 LLM 的 tool_result ContentBlock。
|
|
23
|
+
* 有 contentBlocks(view_image 的图片块)就原样用作 content;否则
|
|
24
|
+
* 用文本(成功用 result,失败用 "Error: ...")。抽成纯函数以便单测。
|
|
25
|
+
*/
|
|
26
|
+
export function toolResultToBlock(result) {
|
|
27
|
+
const block = {
|
|
28
|
+
type: "tool_result",
|
|
29
|
+
tool_use_id: result.id,
|
|
30
|
+
content: result.error
|
|
31
|
+
? `Error: ${result.error}`
|
|
32
|
+
: result.contentBlocks ?? (result.result ?? "(no output)"),
|
|
33
|
+
};
|
|
34
|
+
if (result.isError || result.error)
|
|
35
|
+
block.is_error = true;
|
|
36
|
+
return block;
|
|
37
|
+
}
|
|
14
38
|
export class TurnLoop {
|
|
15
39
|
deps;
|
|
16
40
|
config;
|
|
@@ -26,7 +50,99 @@ export class TurnLoop {
|
|
|
26
50
|
currentTurnLog = logger;
|
|
27
51
|
/** Last emitted ctx token estimate; used to skip no-op usage_update events. */
|
|
28
52
|
lastCtxEmit = -1;
|
|
29
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Consecutive on_stop blocks (Goal mode kept the agent going). Reset to 0
|
|
55
|
+
* on any unblocked completion. When it reaches config.maxStopBlocks the
|
|
56
|
+
* loop forces a stop so a stuck goal can't loop forever.
|
|
57
|
+
*/
|
|
58
|
+
stopBlockCount = 0;
|
|
59
|
+
/**
|
|
60
|
+
* Run-scoped goal budget tracker (Goal mode). Hoisted to an instance field
|
|
61
|
+
* (not a run() local) so extend() can bump its budgets mid-run. Null when no
|
|
62
|
+
* goal or between runs.
|
|
63
|
+
*/
|
|
64
|
+
goalTracker = null;
|
|
65
|
+
/**
|
|
66
|
+
* Whether an approaching_limit marker was already emitted for the current
|
|
67
|
+
* ceiling. Reset when the run advances past the moment (a fresh extension, or
|
|
68
|
+
* the goal completing) so the next approach re-announces. Prevents the marker
|
|
69
|
+
* being re-emitted every turn while still within the approach threshold.
|
|
70
|
+
*/
|
|
71
|
+
approachAnnounced = false;
|
|
72
|
+
/**
|
|
73
|
+
* Extend the in-flight run's limits (TODO 3.1 — 运行中续轮/加预算). Mutates
|
|
74
|
+
* the maxTurns ceiling, the maxStopBlocks cap, and/or the live goal budgets;
|
|
75
|
+
* the loop re-reads all of them each turn so the change takes effect on the
|
|
76
|
+
* next iteration. No-op for fields not supplied. Returns the resulting
|
|
77
|
+
* effective limits.
|
|
78
|
+
*/
|
|
79
|
+
extend(opts) {
|
|
80
|
+
const elapsedMs = this.goalTracker ? Date.now() - this.goalTracker.startedAtMs : 0;
|
|
81
|
+
const next = applyGoalExtension(this.config.maxTurns, this.goalTracker?.goal, this.goalTracker?.tokensUsed ?? 0, elapsedMs, opts);
|
|
82
|
+
this.config = { ...this.config, maxTurns: next.maxTurns };
|
|
83
|
+
if (this.goalTracker) {
|
|
84
|
+
// Replace the goal object rather than mutating its fields: the tracker's
|
|
85
|
+
// goal may be a shared/frozen reference, and goalBudgetExceeded reads
|
|
86
|
+
// tracker.goal.{tokenBudget,timeBudgetMs} live, so a fresh object with the
|
|
87
|
+
// new caps takes effect on the next turn either way.
|
|
88
|
+
this.goalTracker.goal = {
|
|
89
|
+
...this.goalTracker.goal,
|
|
90
|
+
tokenBudget: next.tokenBudget,
|
|
91
|
+
timeBudgetMs: next.timeBudgetMs,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Raise the consecutive-stop-block cap — for a re-blocked goal this is the
|
|
95
|
+
// limit that actually bites, so an extend that only bumped maxTurns/budgets
|
|
96
|
+
// couldn't keep it going. Resolve the current cap the same way the loop does.
|
|
97
|
+
const curCap = this.config.maxStopBlocks ?? GOAL_DEFAULT_MAX_STOP_BLOCKS;
|
|
98
|
+
const nextCap = typeof opts.addStopBlocks === "number" && opts.addStopBlocks > 0
|
|
99
|
+
? curCap + Math.floor(opts.addStopBlocks)
|
|
100
|
+
: curCap;
|
|
101
|
+
this.config = { ...this.config, maxStopBlocks: nextCap };
|
|
102
|
+
// ANY extension resets the consecutive stop-block streak: the user just
|
|
103
|
+
// asked to keep going, so a goal that was repeatedly re-blocked shouldn't be
|
|
104
|
+
// immediately re-capped. (Previously only addTurns reset it, leaving a
|
|
105
|
+
// budget-only extension unable to un-stick a capped goal.)
|
|
106
|
+
const extended = (opts.addTurns ?? 0) > 0 ||
|
|
107
|
+
(opts.addStopBlocks ?? 0) > 0 ||
|
|
108
|
+
(opts.addTokenBudget ?? 0) > 0 ||
|
|
109
|
+
(opts.addTimeBudgetMs ?? 0) > 0;
|
|
110
|
+
if (extended) {
|
|
111
|
+
this.stopBlockCount = 0;
|
|
112
|
+
// Let the next approach re-announce against the raised ceilings.
|
|
113
|
+
this.approachAnnounced = false;
|
|
114
|
+
}
|
|
115
|
+
return { ...next, maxStopBlocks: nextCap };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Goal mode only: if the run is nearing EITHER stop ceiling (maxTurns or
|
|
119
|
+
* maxStopBlocks) and we haven't announced it yet, emit one approaching_limit
|
|
120
|
+
* marker so the UI can offer a "再续" button while the run is still live.
|
|
121
|
+
* Watches both limits because a re-blocked goal hits the stop-block cap long
|
|
122
|
+
* before maxTurns. Idempotent within an approach window via approachAnnounced.
|
|
123
|
+
*/
|
|
124
|
+
maybeAnnounceApproachingLimit() {
|
|
125
|
+
if (!this.config.goal || this.approachAnnounced)
|
|
126
|
+
return;
|
|
127
|
+
const cap = this.config.maxStopBlocks ?? GOAL_DEFAULT_MAX_STOP_BLOCKS;
|
|
128
|
+
const prox = limitProximity(this.turnCount, this.config.maxTurns, this.stopBlockCount, cap);
|
|
129
|
+
if (!prox.approaching)
|
|
130
|
+
return;
|
|
131
|
+
this.approachAnnounced = true;
|
|
132
|
+
this.config.onStream?.({
|
|
133
|
+
type: "goal_progress",
|
|
134
|
+
status: "approaching_limit",
|
|
135
|
+
round: this.stopBlockCount,
|
|
136
|
+
turnsRemaining: prox.turnsRemaining,
|
|
137
|
+
stopBlocksRemaining: prox.stopBlocksRemaining,
|
|
138
|
+
nearest: prox.nearest,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
constructor(deps,
|
|
142
|
+
// Not readonly: extend() bumps maxTurns mid-run (TODO 3.1), and the
|
|
143
|
+
// constructor below rewrites onStream. The loop reads config fields fresh
|
|
144
|
+
// each turn so mutations take effect on the next iteration.
|
|
145
|
+
config) {
|
|
30
146
|
this.deps = deps;
|
|
31
147
|
this.config = config;
|
|
32
148
|
// Wrap onStream so a single throwing handler can't silently break
|
|
@@ -67,11 +183,29 @@ export class TurnLoop {
|
|
|
67
183
|
* `decision` / `stop`. Use this instead of `deps.hooks.emit` directly so
|
|
68
184
|
* every emit carries the same context envelope.
|
|
69
185
|
*/
|
|
186
|
+
/**
|
|
187
|
+
* Persist a turn_stopped marker on a user Stop so a resume can rebuild the
|
|
188
|
+
* renderer's "你在 Ns 后停止了" line (the in-memory turn_end is lost on reload,
|
|
189
|
+
* which makes the interrupted turn fold behind the process-card header).
|
|
190
|
+
* Skipped for sub-agents — their signal.aborted is usually a parent abort,
|
|
191
|
+
* not a user Stop, and sub-agent turns aren't shown as interruptible. The
|
|
192
|
+
* underlying append is idempotent, so calling this at several abort returns
|
|
193
|
+
* for one Stop still writes a single marker.
|
|
194
|
+
*/
|
|
195
|
+
markStopped() {
|
|
196
|
+
if (this.deps.isSubAgent === true)
|
|
197
|
+
return;
|
|
198
|
+
this.deps.transcript.appendTurnStopped();
|
|
199
|
+
}
|
|
70
200
|
async emitHook(event, data = {}) {
|
|
71
201
|
return this.deps.hooks.emit(event, {
|
|
72
202
|
...data,
|
|
73
203
|
isSubAgent: this.deps.isSubAgent === true,
|
|
74
204
|
sessionId: this.deps.sessionId,
|
|
205
|
+
// The run's abort signal, so handlers that make their own LLM calls
|
|
206
|
+
// (e.g. the Goal stop-judge) can be cut short when the user hits Stop
|
|
207
|
+
// mid-call instead of blocking until the sub-call returns.
|
|
208
|
+
signal: this.config.signal,
|
|
75
209
|
});
|
|
76
210
|
}
|
|
77
211
|
/**
|
|
@@ -104,9 +238,10 @@ export class TurnLoop {
|
|
|
104
238
|
this.lastCtxEmit = ctx;
|
|
105
239
|
this.config.onStream({ type: "usage_update", promptTokens: ctx });
|
|
106
240
|
}
|
|
107
|
-
emitCtxFromUsage(
|
|
241
|
+
emitCtxFromUsage(usage, messages) {
|
|
108
242
|
if (!this.config.onStream)
|
|
109
243
|
return;
|
|
244
|
+
const promptTokens = usage.promptTokens;
|
|
110
245
|
// Reverse-derive overhead from this authoritative reading so the next
|
|
111
246
|
// estimate-based emit (post-tool-result) is calibrated.
|
|
112
247
|
const msgsEstimate = estimateTokens(messages);
|
|
@@ -122,7 +257,21 @@ export class TurnLoop {
|
|
|
122
257
|
if (promptTokens === this.lastCtxEmit)
|
|
123
258
|
return;
|
|
124
259
|
this.lastCtxEmit = promptTokens;
|
|
125
|
-
|
|
260
|
+
// Forward the provider's cache counts so the UI can show a hit rate. Only
|
|
261
|
+
// attach fields the provider actually reported — a spread keeps them off
|
|
262
|
+
// the event entirely when undefined, so the renderer can tell "no cache
|
|
263
|
+
// info this turn" from "0 cached". Estimate-path emits don't call this and
|
|
264
|
+
// so carry no cache fields (correct: an estimate has no cache reading).
|
|
265
|
+
this.config.onStream({
|
|
266
|
+
type: "usage_update",
|
|
267
|
+
promptTokens,
|
|
268
|
+
...(usage.cacheReadTokens !== undefined
|
|
269
|
+
? { cacheReadTokens: usage.cacheReadTokens }
|
|
270
|
+
: {}),
|
|
271
|
+
...(usage.cacheCreationTokens !== undefined
|
|
272
|
+
? { cacheCreationTokens: usage.cacheCreationTokens }
|
|
273
|
+
: {}),
|
|
274
|
+
});
|
|
126
275
|
}
|
|
127
276
|
/**
|
|
128
277
|
* Run the multi-turn agent loop until completion.
|
|
@@ -131,298 +280,596 @@ export class TurnLoop {
|
|
|
131
280
|
let messages = [...initialMessages];
|
|
132
281
|
let finalText = "";
|
|
133
282
|
const budgetTracker = createBudgetTracker();
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
messages.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
283
|
+
// Goal-mode run-scoped budget tracker (P0). Null when no goal. Stamps a
|
|
284
|
+
// wall-clock start now and accumulates prompt+completion tokens across
|
|
285
|
+
// every turn; the guardrail below force-stops the run once any configured
|
|
286
|
+
// budget is blown — the unattended-safety backstop.
|
|
287
|
+
this.goalTracker = this.config.goal
|
|
288
|
+
? createGoalBudgetTracker(this.config.goal, Date.now())
|
|
289
|
+
: null;
|
|
290
|
+
const goalTracker = this.goalTracker;
|
|
291
|
+
// Fresh run: re-arm the approaching-limit announcement.
|
|
292
|
+
this.approachAnnounced = false;
|
|
293
|
+
// run() must never reject: the engine's post-run bookkeeping (saveState
|
|
294
|
+
// with the terminal reason, on_session_end hook) runs AFTER this call and
|
|
295
|
+
// outside the engine's try, so a throw here would leave the session
|
|
296
|
+
// frozen at status "active" on disk. Per-turn errors are already turned
|
|
297
|
+
// into return-reasons by callModelWithFallback; this outer guard catches
|
|
298
|
+
// throws from the surrounding scaffolding (contextManager.manageAsync,
|
|
299
|
+
// hook emits, guards) and surfaces them as a model_error result.
|
|
300
|
+
try {
|
|
301
|
+
while (this.turnCount < this.config.maxTurns) {
|
|
302
|
+
this.turnCount++;
|
|
303
|
+
// Abort fast-path: bail at the loop TOP before doing any per-turn work.
|
|
304
|
+
// Without this, an aborted child (parent abort, or the 30min per-call
|
|
305
|
+
// registry timeout) would run a full contextManager.manageAsync (itself
|
|
306
|
+
// an LLM summarization call) + model call + tool batch before the
|
|
307
|
+
// post-model check at the bottom of the loop noticed — exactly the
|
|
308
|
+
// sub-agent leak where a synchronous child kept burning turns/tokens for
|
|
309
|
+
// minutes after the parent Agent call already returned. The model call's
|
|
310
|
+
// own signal check only fires AFTER the call resolves; this guards the
|
|
311
|
+
// boundary between turns. (Mirrors Claude Code's query.ts, where the
|
|
312
|
+
// aborted check short-circuits before re-entering the streaming loop.)
|
|
313
|
+
if (this.config.signal?.aborted) {
|
|
314
|
+
this.markStopped();
|
|
315
|
+
return { text: finalText, reason: "aborted_streaming", messages };
|
|
316
|
+
}
|
|
317
|
+
// Step-gap steering: messages the host queued via Engine.enqueueSteer
|
|
318
|
+
// while the previous step was running. Splice them in as user messages so
|
|
319
|
+
// they join THIS step's request — no abort, no lost in-flight work. Same
|
|
320
|
+
// loop-top user-push pattern as turnStartInjection / turn-limit warnings
|
|
321
|
+
// below. Push to transcript too so they persist + survive resume.
|
|
322
|
+
const steered = this.deps.consumeSteer?.() ?? [];
|
|
323
|
+
for (const { id, text } of steered) {
|
|
324
|
+
if (!text)
|
|
325
|
+
continue;
|
|
326
|
+
messages.push({ role: "user", content: text });
|
|
327
|
+
this.deps.transcript.appendMessage("user", text);
|
|
328
|
+
this.config.onStream?.({ type: "steer_injected", text, id });
|
|
329
|
+
}
|
|
330
|
+
const state = initialTurnState(this.turnCount);
|
|
331
|
+
// Per-turn correlation ID. Every log written through `tlog` (or any
|
|
332
|
+
// child derived from it) is stamped with `turn` + `turnId`, so
|
|
333
|
+
// `jq 'select(.turnId == "...")'` reconstructs one turn's timeline.
|
|
334
|
+
// Span is *not* used for the loop itself because there are 6+ early
|
|
335
|
+
// returns; instead, each return-causing branch logs its own terminal
|
|
336
|
+
// event (model_error, completed, etc.).
|
|
337
|
+
const turnId = newTurnId();
|
|
338
|
+
const tlog = logger.child({ turn: this.turnCount, turnId });
|
|
339
|
+
this.currentTurnLog = tlog;
|
|
340
|
+
const turnStartedAt = Date.now();
|
|
341
|
+
tlog.info("turn.start", { cat: "turn", messageCount: messages.length });
|
|
342
|
+
// Tag downstream tool-exec / permission lines with this turn's IDs.
|
|
343
|
+
this.deps.toolExecutor.setLogger(tlog);
|
|
344
|
+
this.config.onStream?.({ type: "stream_request_start", turnNumber: this.turnCount });
|
|
345
|
+
const turnStartHook = await this.emitHook("on_turn_start", {
|
|
346
|
+
turnNumber: this.turnCount,
|
|
194
347
|
});
|
|
195
|
-
const
|
|
196
|
-
if (
|
|
197
|
-
messages.push(
|
|
348
|
+
const turnStartInjection = wrapHookMessages(turnStartHook.messages);
|
|
349
|
+
if (turnStartInjection) {
|
|
350
|
+
messages.push(turnStartInjection);
|
|
198
351
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
352
|
+
// Approaching max turns: inject a warning so the model can wrap up.
|
|
353
|
+
// (Model-facing only — the user-facing "再续" marker is handled by
|
|
354
|
+
// maybeAnnounceApproachingLimit below, which also watches the stop-block
|
|
355
|
+
// cap, the limit a re-blocked goal actually hits first.)
|
|
356
|
+
const turnsRemaining = this.config.maxTurns - this.turnCount;
|
|
357
|
+
if (turnsRemaining === 2) {
|
|
358
|
+
messages.push({
|
|
359
|
+
role: "user",
|
|
360
|
+
content: "<system-reminder>Warning: you have only 2 turns remaining before the turn limit is reached. " +
|
|
361
|
+
"Start wrapping up your work and prepare a summary of what you've accomplished and what remains to be done.</system-reminder>",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
else if (turnsRemaining === 1) {
|
|
365
|
+
messages.push({
|
|
366
|
+
role: "user",
|
|
367
|
+
content: "<system-reminder>Warning: you have only 1 turn remaining before the turn limit is reached. " +
|
|
368
|
+
"Wrap up your work now — your next turn will be your last.</system-reminder>",
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
else if (turnsRemaining === 0) {
|
|
372
|
+
messages.push({
|
|
373
|
+
role: "user",
|
|
374
|
+
content: "<system-reminder>This is your LAST turn. You MUST respond with a final text summary now. " +
|
|
375
|
+
"Do NOT call any tools. Summarize what you have accomplished and list any remaining work.</system-reminder>",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
// Goal mode: announce once when nearing EITHER stop ceiling (turns or
|
|
379
|
+
// stop-blocks) so the UI can offer a "再续" button while still live.
|
|
380
|
+
this.maybeAnnounceApproachingLimit();
|
|
381
|
+
// Pre-check: context management (async — may trigger LLM summarization)
|
|
382
|
+
messages = await this.deps.contextManager.manageAsync(messages);
|
|
383
|
+
// manageAsync can itself issue an LLM summarization call lasting several
|
|
384
|
+
// seconds; if the signal aborted during it, stop here rather than
|
|
385
|
+
// proceeding into the (expensive) main model call. Belt to the loop-top
|
|
386
|
+
// brace: this catches an abort that landed *inside* context management.
|
|
387
|
+
if (this.config.signal?.aborted) {
|
|
388
|
+
this.markStopped();
|
|
389
|
+
return { text: finalText, reason: "aborted_streaming", messages };
|
|
390
|
+
}
|
|
391
|
+
// No pre-llm ctx emit here: the messages-only estimate would be ~16k
|
|
392
|
+
// smaller than the real prompt (system + tools not included), making
|
|
393
|
+
// the bar visibly drop on every submit. Post-llm/post-tool-result
|
|
394
|
+
// events carry an accurate value; if compaction shrank the array, the
|
|
395
|
+
// dedicated context_compact event has already informed the UI.
|
|
396
|
+
// post_compact hook: ContextManager just finished a manage() pass.
|
|
397
|
+
// If any non-micro tier fired, give handlers a chance to inject a
|
|
398
|
+
// <system-reminder> ("context was compacted — recall earlier
|
|
399
|
+
// decisions from the transcript") into THIS turn before the model
|
|
400
|
+
// call. Microcompact is lossless (just clearing redundant
|
|
401
|
+
// tool_results) so we suppress hook emits for it to keep token
|
|
402
|
+
// overhead down.
|
|
403
|
+
const pending = this.deps.consumePendingCompactInfo?.();
|
|
404
|
+
if (pending && pending.strategy !== "micro") {
|
|
405
|
+
const compactHook = await this.emitHook("post_compact", {
|
|
406
|
+
strategy: pending.strategy,
|
|
407
|
+
beforeTokens: pending.before,
|
|
408
|
+
afterTokens: pending.after,
|
|
409
|
+
});
|
|
410
|
+
const compactInjection = wrapHookMessages(compactHook.messages);
|
|
411
|
+
if (compactInjection) {
|
|
412
|
+
messages.push(compactInjection);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Model call (with streaming fallback and max_output_tokens continuation)
|
|
416
|
+
// Track tool IDs streamed during this turn to avoid duplicate UI events
|
|
417
|
+
this.streamedToolIds.clear();
|
|
418
|
+
// Streaming tool queue: start concurrency-safe tools during streaming
|
|
419
|
+
const streamingQueue = new StreamingToolQueue(this.deps.toolExecutor);
|
|
420
|
+
let response;
|
|
421
|
+
try {
|
|
422
|
+
response = await this.callModelWithFallback(messages);
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
if (err instanceof ContextLimitError) {
|
|
426
|
+
// Progressive recovery: drop oldest API rounds, up to 3 retries
|
|
427
|
+
const { dropOldestRounds } = await import("../context/compaction.js");
|
|
428
|
+
let recovered = false;
|
|
429
|
+
for (let retry = 1; retry <= 3; retry++) {
|
|
430
|
+
tlog.warn("turn.ptl_recovery", { cat: "turn", retry, roundsToDrop: retry });
|
|
431
|
+
messages = dropOldestRounds(messages, retry);
|
|
432
|
+
try {
|
|
433
|
+
response = await this.callModelWithFallback(messages);
|
|
434
|
+
recovered = true;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
catch (retryErr) {
|
|
438
|
+
if (!(retryErr instanceof ContextLimitError)) {
|
|
439
|
+
this.config.onStream?.({ type: "error", error: formatFriendlyError(retryErr) });
|
|
440
|
+
return { text: finalText, reason: "model_error", messages };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (!recovered) {
|
|
445
|
+
this.patchOrphanedToolUses(messages);
|
|
446
|
+
this.config.onStream?.({
|
|
447
|
+
type: "error",
|
|
448
|
+
error: "Context limit exceeded after 3 recovery attempts",
|
|
449
|
+
});
|
|
450
|
+
return { text: finalText, reason: "prompt_too_long", messages };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
else if (isAbortError(err) || this.config.signal?.aborted) {
|
|
454
|
+
// User pressed Stop: the in-flight model call rejected with an
|
|
455
|
+
// AbortError. This is NOT a failure — the UI already shows the
|
|
456
|
+
// "你在 Ns 后停止了" line, so emitting an error event would stack a
|
|
457
|
+
// spurious red "Error:" block on top of an intentional stop.
|
|
458
|
+
// Persist a turn_stopped marker so a resume can rebuild that line
|
|
459
|
+
// (the renderer's turn_end is in-memory only; without this the
|
|
460
|
+
// interrupted turn folds behind the process-card header on reload).
|
|
461
|
+
this.patchOrphanedToolUses(messages);
|
|
462
|
+
this.markStopped();
|
|
463
|
+
return { text: finalText, reason: "aborted_streaming", messages };
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
this.patchOrphanedToolUses(messages);
|
|
467
|
+
this.config.onStream?.({ type: "error", error: formatFriendlyError(err) });
|
|
468
|
+
return { text: finalText, reason: "model_error", messages };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// UI ctx bar: prefer the provider's authoritative promptTokens.
|
|
472
|
+
if (response.usage?.promptTokens !== undefined) {
|
|
473
|
+
this.emitCtxFromUsage(response.usage, messages);
|
|
474
|
+
}
|
|
475
|
+
// Feed actual token usage back to the context manager so subsequent
|
|
476
|
+
// compaction decisions use hybrid (actual + delta) estimation rather than
|
|
477
|
+
// pure heuristics. Without this the manager falls back to char/4 estimates.
|
|
478
|
+
if (response.usage?.promptTokens !== undefined) {
|
|
479
|
+
this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length);
|
|
480
|
+
}
|
|
481
|
+
// Truncation that cut off a TOOL CALL: the model overflowed
|
|
482
|
+
// max_output_tokens mid tool-call, so the arg JSON is incomplete (e.g. a
|
|
483
|
+
// Write whose `content` was clipped, leaving file_path unset). Executing
|
|
484
|
+
// it raised a misleading "Missing required parameter: file_path". Instead,
|
|
485
|
+
// tell the model its output was truncated and let the next turn retry —
|
|
486
|
+
// bounded by the outer maxTurns loop.
|
|
487
|
+
if (isTruncatedStop(response.stopReason) && response.toolCalls.length > 0) {
|
|
488
|
+
tlog.info("turn.truncated_tool_call", {
|
|
489
|
+
cat: "turn",
|
|
490
|
+
toolCount: response.toolCalls.length,
|
|
491
|
+
});
|
|
492
|
+
if (response.text) {
|
|
493
|
+
messages.push({ role: "assistant", content: response.text });
|
|
494
|
+
}
|
|
495
|
+
messages.push({
|
|
496
|
+
role: "user",
|
|
497
|
+
content: "<system-reminder>Your previous response was truncated by the max output token limit before the tool call finished, so its arguments are incomplete. Do not assume it ran. Either retry with a smaller/more focused tool call (e.g. write the file in sections via Edit), or raise this model's maxOutputTokens.</system-reminder>",
|
|
498
|
+
});
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
// Handle max_output_tokens: if response was truncated, do continuation
|
|
502
|
+
// (up to 3 times). Truncation is reported as finish_reason "length"
|
|
503
|
+
// (OpenAI) or stop_reason "max_tokens" (Anthropic) — isTruncatedStop
|
|
504
|
+
// accepts both, so the OpenAI streaming path triggers continuation too.
|
|
505
|
+
if (isTruncatedStop(response.stopReason) &&
|
|
506
|
+
response.toolCalls.length === 0 &&
|
|
507
|
+
response.text) {
|
|
508
|
+
let combinedText = response.text;
|
|
509
|
+
for (let retry = 0; retry < 3; retry++) {
|
|
510
|
+
// Don't fire another continuation call if the user cancelled — without
|
|
511
|
+
// this an abort during a truncated response could still issue up to 3
|
|
512
|
+
// more model calls, emitting text after Stop.
|
|
513
|
+
if (this.config.signal?.aborted)
|
|
514
|
+
break;
|
|
515
|
+
tlog.info("turn.max_tokens_continuation", { cat: "turn", retry: retry + 1 });
|
|
516
|
+
const contMessages = [
|
|
517
|
+
...messages,
|
|
518
|
+
{ role: "assistant", content: combinedText },
|
|
519
|
+
{
|
|
520
|
+
role: "user",
|
|
521
|
+
content: "<system-reminder>Your previous response was truncated due to length. Please continue from where you left off.</system-reminder>",
|
|
522
|
+
},
|
|
523
|
+
];
|
|
217
524
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
525
|
+
const contResponse = await this.deps.model.call(this.deps.systemPrompt, contMessages, this.deps.tools, this.config.onStream, this.config.signal);
|
|
526
|
+
combinedText += contResponse.text;
|
|
527
|
+
if (!isTruncatedStop(contResponse.stopReason) || contResponse.toolCalls.length > 0) {
|
|
528
|
+
response = { ...contResponse, text: combinedText };
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
220
533
|
break;
|
|
221
534
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
535
|
+
}
|
|
536
|
+
response = { ...response, text: combinedText };
|
|
537
|
+
}
|
|
538
|
+
// After any continuation, send latest usage so ctx bar reflects real context
|
|
539
|
+
if (response.usage?.promptTokens !== undefined) {
|
|
540
|
+
this.emitCtxFromUsage(response.usage, messages);
|
|
541
|
+
}
|
|
542
|
+
// Goal-mode run-scoped accounting: add this turn's total token usage
|
|
543
|
+
// (prompt + completion) to the running total. Done after continuation so
|
|
544
|
+
// continued output is counted against the budget.
|
|
545
|
+
if (goalTracker && response.usage) {
|
|
546
|
+
const used = (response.usage.promptTokens ?? 0) + (response.usage.completionTokens ?? 0);
|
|
547
|
+
recordGoalUsage(goalTracker, used);
|
|
548
|
+
}
|
|
549
|
+
// Aborted?
|
|
550
|
+
if (this.config.signal?.aborted) {
|
|
551
|
+
this.markStopped();
|
|
552
|
+
return { text: finalText, reason: "aborted_streaming", messages };
|
|
553
|
+
}
|
|
554
|
+
// Accumulate text
|
|
555
|
+
if (response.text) {
|
|
556
|
+
finalText = response.text;
|
|
557
|
+
}
|
|
558
|
+
// Goal budget guardrail (P0): a run that has blown its token/time budget
|
|
559
|
+
// is force-stopped regardless of what the model wants to do next (stop OR
|
|
560
|
+
// continue with tool calls). This is the unattended-safety backstop, so
|
|
561
|
+
// it sits BEFORE the "tool calls?" branch — both paths pass the gate.
|
|
562
|
+
if (goalTracker && goalBudgetExceeded(goalTracker, Date.now())) {
|
|
563
|
+
tlog.info("turn.goal_budget_exhausted", {
|
|
564
|
+
cat: "goal",
|
|
565
|
+
tokensUsed: goalTracker.tokensUsed,
|
|
566
|
+
tokenBudget: this.config.goal?.tokenBudget,
|
|
567
|
+
timeBudgetMs: this.config.goal?.timeBudgetMs,
|
|
568
|
+
});
|
|
569
|
+
this.config.onStream?.({
|
|
570
|
+
type: "assistant_message",
|
|
571
|
+
message: {
|
|
572
|
+
role: "assistant",
|
|
573
|
+
content: "(Goal 预算已耗尽,强制停止。)",
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
return { text: finalText, reason: "goal_budget_exhausted", messages };
|
|
577
|
+
}
|
|
578
|
+
// Post-check: tool calls?
|
|
579
|
+
if (response.toolCalls.length === 0) {
|
|
580
|
+
// No tool use — final answer
|
|
581
|
+
this.config.onStream?.({
|
|
582
|
+
type: "assistant_message",
|
|
583
|
+
message: { role: "assistant", content: finalText },
|
|
584
|
+
});
|
|
585
|
+
await this.emitHook("on_turn_end", {
|
|
586
|
+
turnNumber: this.turnCount,
|
|
587
|
+
hasToolUse: false,
|
|
588
|
+
});
|
|
589
|
+
messages.push({ role: "assistant", content: finalText });
|
|
590
|
+
// on_stop seam: the model wants to stop. Give handlers (Goal mode)
|
|
591
|
+
// a chance to BLOCK termination and keep the agent working. A
|
|
592
|
+
// handler returning continueSession=true injects its messages and
|
|
593
|
+
// we run another turn instead of returning. Bounded by
|
|
594
|
+
// maxStopBlocks (consecutive) and the outer maxTurns ceiling.
|
|
595
|
+
const maxStopBlocks = this.config.maxStopBlocks ?? GOAL_DEFAULT_MAX_STOP_BLOCKS;
|
|
596
|
+
const stopHook = await this.emitHook("on_stop", {
|
|
597
|
+
goal: this.config.goal,
|
|
598
|
+
finalText,
|
|
599
|
+
turnCount: this.turnCount,
|
|
600
|
+
});
|
|
601
|
+
// The judge's structured verdict (set by GoalStopHook in result.data)
|
|
602
|
+
// rides back here so we can show goal progress WITHOUT a second LLM
|
|
603
|
+
// call — `gaps` is whatever the judge already computed.
|
|
604
|
+
const goalVerdict = stopHook.data?.goalVerdict;
|
|
605
|
+
if (stopHook.continueSession && this.stopBlockCount < maxStopBlocks) {
|
|
606
|
+
this.stopBlockCount++;
|
|
607
|
+
// Goal visibility: one not_met marker per re-prompt. round counts
|
|
608
|
+
// up with stopBlockCount so the UI can show "第 N 轮".
|
|
609
|
+
this.config.onStream?.({
|
|
610
|
+
type: "goal_progress",
|
|
611
|
+
status: "not_met",
|
|
612
|
+
round: this.stopBlockCount,
|
|
613
|
+
gaps: goalVerdict?.gaps || undefined,
|
|
614
|
+
});
|
|
615
|
+
// The streak just grew — we may now be nearing the stop-block cap.
|
|
616
|
+
// Announce here (before the next turn's top check) so the "再续"
|
|
617
|
+
// button shows up against the limit that's actually about to bite.
|
|
618
|
+
this.maybeAnnounceApproachingLimit();
|
|
619
|
+
const injection = wrapHookMessages(stopHook.messages);
|
|
620
|
+
if (injection) {
|
|
621
|
+
messages.push(injection);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
// No guidance from the handler — inject a generic nudge so the
|
|
625
|
+
// model knows it must keep going rather than re-emitting the
|
|
626
|
+
// same final answer.
|
|
627
|
+
messages.push({
|
|
628
|
+
role: "user",
|
|
629
|
+
content: "<system-reminder>The goal is not yet complete. Continue working toward it.</system-reminder>",
|
|
630
|
+
});
|
|
227
631
|
}
|
|
632
|
+
tlog.info("turn.stop_blocked", {
|
|
633
|
+
cat: "turn",
|
|
634
|
+
stopBlockCount: this.stopBlockCount,
|
|
635
|
+
maxStopBlocks,
|
|
636
|
+
hasGuidance: !!injection,
|
|
637
|
+
});
|
|
638
|
+
continue;
|
|
228
639
|
}
|
|
229
|
-
if (
|
|
230
|
-
|
|
640
|
+
if (stopHook.continueSession && this.stopBlockCount >= maxStopBlocks) {
|
|
641
|
+
// Cap hit: stop anyway, but tell the user why we're not looping
|
|
642
|
+
// forever on an unsatisfiable goal.
|
|
643
|
+
tlog.info("turn.stop_block_cap", {
|
|
644
|
+
cat: "turn",
|
|
645
|
+
stopBlockCount: this.stopBlockCount,
|
|
646
|
+
maxStopBlocks,
|
|
647
|
+
});
|
|
231
648
|
this.config.onStream?.({
|
|
232
|
-
type: "
|
|
233
|
-
|
|
649
|
+
type: "goal_progress",
|
|
650
|
+
status: "exhausted",
|
|
651
|
+
round: this.stopBlockCount,
|
|
652
|
+
});
|
|
653
|
+
this.config.onStream?.({
|
|
654
|
+
type: "assistant_message",
|
|
655
|
+
message: {
|
|
656
|
+
role: "assistant",
|
|
657
|
+
content: `(Goal 续跑已达 ${maxStopBlocks} 次上限,先停下。)`,
|
|
658
|
+
},
|
|
234
659
|
});
|
|
235
|
-
return { text: finalText, reason: "prompt_too_long", messages };
|
|
236
660
|
}
|
|
661
|
+
else if (this.config.goal && goalVerdict?.met) {
|
|
662
|
+
// Goal run completed cleanly: the judge says met. round = total
|
|
663
|
+
// rounds = prior blocks + this accepted final round.
|
|
664
|
+
this.config.onStream?.({
|
|
665
|
+
type: "goal_progress",
|
|
666
|
+
status: "met",
|
|
667
|
+
round: this.stopBlockCount + 1,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
this.stopBlockCount = 0;
|
|
671
|
+
return { text: finalText, reason: "completed", messages };
|
|
237
672
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
673
|
+
// Tool execution phase
|
|
674
|
+
tlog.info("turn.tool_use", { cat: "turn", tools: response.toolCalls.map((t) => t.toolName) });
|
|
675
|
+
const toolCalls = response.toolCalls.slice(0, this.config.maxToolCallsPerTurn);
|
|
676
|
+
// Per-turn cap: any calls beyond maxToolCallsPerTurn are NOT executed and
|
|
677
|
+
// NOT added to the assistant message below, so the model never sees a
|
|
678
|
+
// result for them. Without a heads-up it assumes all ran (acting on
|
|
679
|
+
// tool output that never happened). Remember the dropped ones so we can
|
|
680
|
+
// inject a reminder after the executed batch's results. (B-3)
|
|
681
|
+
const droppedToolCalls = response.toolCalls.slice(this.config.maxToolCallsPerTurn);
|
|
682
|
+
// Add assistant message with tool_use blocks to messages
|
|
683
|
+
const assistantBlocks = [];
|
|
684
|
+
if (response.text) {
|
|
685
|
+
assistantBlocks.push({ type: "text", text: response.text });
|
|
242
686
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
687
|
+
for (const tc of toolCalls) {
|
|
688
|
+
assistantBlocks.push({
|
|
689
|
+
type: "tool_use",
|
|
690
|
+
id: tc.id,
|
|
691
|
+
name: tc.toolName,
|
|
692
|
+
input: tc.args,
|
|
693
|
+
});
|
|
694
|
+
// Only emit tool_use_start if not already emitted during streaming
|
|
695
|
+
if (!this.streamedToolIds.has(tc.id)) {
|
|
696
|
+
this.config.onStream?.({ type: "tool_use_start", toolCall: tc });
|
|
697
|
+
}
|
|
698
|
+
// Record in transcript
|
|
699
|
+
this.deps.transcript.appendToolUse(tc.toolName, tc.id, tc.args);
|
|
700
|
+
}
|
|
701
|
+
messages.push({ role: "assistant", content: assistantBlocks });
|
|
702
|
+
// Execute tools — enqueue concurrency-safe tools for early start,
|
|
703
|
+
// drain remaining (unsafe) tools sequentially.
|
|
704
|
+
for (const tc of toolCalls) {
|
|
705
|
+
streamingQueue.enqueue(tc);
|
|
706
|
+
}
|
|
707
|
+
const results = await streamingQueue.drain();
|
|
708
|
+
// Record results in transcript and stream
|
|
709
|
+
const resultBlocks = [];
|
|
710
|
+
for (const result of results) {
|
|
711
|
+
resultBlocks.push(toolResultToBlock(result));
|
|
712
|
+
this.deps.transcript.appendToolResult(result.id, result.toolName, result.result, result.error);
|
|
713
|
+
this.config.onStream?.({ type: "tool_result", result });
|
|
714
|
+
}
|
|
715
|
+
// Fire-and-forget tool use summary (non-blocking). The whole chain is
|
|
716
|
+
// best-effort observability — a thrown onStream handler, a failed dynamic
|
|
717
|
+
// import, or a rejecting summarize must never surface as an unhandled
|
|
718
|
+
// rejection (Node can treat those as fatal). Swallow at the tail. (B-2)
|
|
719
|
+
if (this.config.onStream) {
|
|
720
|
+
void import("./tool-summary.js")
|
|
721
|
+
.then(({ generateToolUseSummary }) => {
|
|
722
|
+
if (!this.deps.model.summarize)
|
|
723
|
+
return;
|
|
724
|
+
return generateToolUseSummary(toolCalls, results, this.deps.model.summarize).then((summary) => {
|
|
725
|
+
if (summary) {
|
|
726
|
+
this.config.onStream?.({ type: "tool_summary", summary });
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
})
|
|
730
|
+
.catch((err) => {
|
|
731
|
+
this.currentTurnLog.warn("tool_summary.dispatch_failed", {
|
|
732
|
+
cat: "turn",
|
|
733
|
+
error: err.message,
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
messages.push({ role: "user", content: resultBlocks });
|
|
738
|
+
// B-3: tell the model which of its requested tool calls were dropped by
|
|
739
|
+
// the per-turn cap so it can re-issue them, instead of silently assuming
|
|
740
|
+
// they ran. Appended to the same user message that carries the results.
|
|
741
|
+
if (droppedToolCalls.length > 0) {
|
|
742
|
+
tlog.info("turn.tool_calls_capped", {
|
|
743
|
+
cat: "turn",
|
|
744
|
+
executed: toolCalls.length,
|
|
745
|
+
dropped: droppedToolCalls.length,
|
|
746
|
+
cap: this.config.maxToolCallsPerTurn,
|
|
747
|
+
});
|
|
748
|
+
const droppedNames = droppedToolCalls.map((t) => t.toolName).join(", ");
|
|
749
|
+
// Separate user message (not folded into the tool_result blocks) so the
|
|
750
|
+
// OpenAI converter — which lifts tool_results into standalone role:tool
|
|
751
|
+
// messages — keeps the reminder as a plain user turn after them.
|
|
752
|
+
messages.push({
|
|
753
|
+
role: "user",
|
|
754
|
+
content: `<system-reminder>Only the first ${toolCalls.length} of your ${response.toolCalls.length} ` +
|
|
755
|
+
`tool calls ran this turn (per-turn limit is ${this.config.maxToolCallsPerTurn}). ` +
|
|
756
|
+
`These were NOT executed and produced no result — do NOT assume they ran: ${droppedNames}. ` +
|
|
757
|
+
`Re-issue the ones you still need in the next turn.</system-reminder>`,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
// Tool results just pushed; recompute ctx so the bar updates *before*
|
|
761
|
+
// the next model round-trip — large tool outputs can move it sharply.
|
|
762
|
+
this.emitCtxFromMessages(messages);
|
|
763
|
+
// Goal mode P0: explicit completion. If the model called complete_goal,
|
|
764
|
+
// it has DECLARED the goal done — short-circuit to "completed" WITHOUT
|
|
765
|
+
// running the judge hook. The tool's result is already in `messages`
|
|
766
|
+
// above so the summary lands in the transcript. Reset the stop-block
|
|
767
|
+
// counter so a prior judge-driven block streak doesn't leak out. Also
|
|
768
|
+
// clear the PERSISTED goal so a later bare send doesn't re-inherit a goal
|
|
769
|
+
// the model just declared finished (the judge's onMet does this when IT
|
|
770
|
+
// decides; a self-report must do the same, else the goal outlives its
|
|
771
|
+
// completion and re-arms every follow-up turn).
|
|
772
|
+
if (goalTracker && toolCalls.some((tc) => tc.toolName === COMPLETE_GOAL_TOOL_NAME)) {
|
|
773
|
+
tlog.info("turn.goal_self_reported_complete", { cat: "goal" });
|
|
774
|
+
this.stopBlockCount = 0;
|
|
775
|
+
this.deps.clearPersistedGoal?.();
|
|
776
|
+
return { text: finalText, reason: "completed", messages };
|
|
777
|
+
}
|
|
778
|
+
// Goal mode: user-initiated cancellation. cancel_goal is the "strong
|
|
779
|
+
// intent" escape hatch — honor it ONLY when confirm===true (the guard the
|
|
780
|
+
// tool advertises). A confirmed cancel stops the run AND clears the
|
|
781
|
+
// persisted goal so it never re-arms; an unconfirmed call is a no-op here
|
|
782
|
+
// (the tool's own result string already told the model it was ignored).
|
|
783
|
+
const confirmedCancel = toolCalls.some((tc) => tc.toolName === CANCEL_GOAL_TOOL_NAME && tc.args?.confirm === true);
|
|
784
|
+
if (goalTracker && confirmedCancel) {
|
|
785
|
+
tlog.info("turn.goal_user_cancelled", { cat: "goal" });
|
|
786
|
+
this.stopBlockCount = 0;
|
|
787
|
+
this.deps.clearPersistedGoal?.();
|
|
788
|
+
return { text: finalText, reason: "completed", messages };
|
|
789
|
+
}
|
|
790
|
+
// Token budget check
|
|
791
|
+
const totalOutputTokens = this.deps.model.getOutputTokens?.() ?? 0;
|
|
792
|
+
const budgetDecision = checkTokenBudget(totalOutputTokens, this.config.tokenBudget ?? Infinity, budgetTracker);
|
|
793
|
+
if (budgetDecision === "stop") {
|
|
794
|
+
tlog.info("turn.budget_stop", {
|
|
795
|
+
cat: "turn",
|
|
796
|
+
outputTokens: totalOutputTokens,
|
|
797
|
+
budget: this.config.tokenBudget,
|
|
798
|
+
});
|
|
799
|
+
this.config.onStream?.({
|
|
800
|
+
type: "assistant_message",
|
|
801
|
+
message: { role: "assistant", content: finalText },
|
|
802
|
+
});
|
|
803
|
+
messages.push({ role: "assistant", content: finalText });
|
|
804
|
+
return { text: finalText, reason: "completed", messages };
|
|
805
|
+
}
|
|
806
|
+
if (budgetDecision === "nudge") {
|
|
807
|
+
messages.push({
|
|
808
|
+
role: "user",
|
|
809
|
+
content: "<system-reminder>You are approaching the token budget limit. Please start wrapping up your work and provide a summary.</system-reminder>",
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
// Investigation guard: end-of-turn check. If too many consecutive
|
|
813
|
+
// read-only turns went by without any user-visible text or side-effecting
|
|
814
|
+
// tool, inject a reminder that will land at the top of the next turn.
|
|
815
|
+
const guard = this.deps.toolExecutor.getInvestigationGuard();
|
|
816
|
+
if (guard) {
|
|
817
|
+
guard.noteText(response.text);
|
|
818
|
+
const turnReminder = guard.turnEnded(this.turnCount);
|
|
819
|
+
if (turnReminder) {
|
|
820
|
+
messages.push({ role: "user", content: turnReminder });
|
|
821
|
+
tlog.info("guard.silent_turn", { cat: "guard", turn: this.turnCount });
|
|
276
822
|
}
|
|
277
|
-
|
|
278
|
-
|
|
823
|
+
}
|
|
824
|
+
// Task guard: nudge the model if it has an in_progress task that
|
|
825
|
+
// hasn't moved in several turns. TaskCreate is sticky in working
|
|
826
|
+
// memory for the first few turns only; without this, the spinner
|
|
827
|
+
// runs forever on tasks the model has mentally finished.
|
|
828
|
+
const taskGuard = this.deps.toolExecutor.getTaskGuard();
|
|
829
|
+
if (taskGuard) {
|
|
830
|
+
const taskReminder = taskGuard.turnEnded(this.turnCount);
|
|
831
|
+
if (taskReminder) {
|
|
832
|
+
messages.push({ role: "user", content: taskReminder });
|
|
833
|
+
tlog.info("guard.stale_task", { cat: "guard", turn: this.turnCount });
|
|
279
834
|
}
|
|
280
835
|
}
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
// After any continuation, send latest usage so ctx bar reflects real context
|
|
284
|
-
if (response.usage?.promptTokens !== undefined) {
|
|
285
|
-
this.emitCtxFromUsage(response.usage.promptTokens, messages);
|
|
286
|
-
}
|
|
287
|
-
// Aborted?
|
|
288
|
-
if (this.config.signal?.aborted) {
|
|
289
|
-
return { text: finalText, reason: "aborted_streaming", messages };
|
|
290
|
-
}
|
|
291
|
-
// Accumulate text
|
|
292
|
-
if (response.text) {
|
|
293
|
-
finalText = response.text;
|
|
294
|
-
}
|
|
295
|
-
// Post-check: tool calls?
|
|
296
|
-
if (response.toolCalls.length === 0) {
|
|
297
|
-
// No tool use — final answer
|
|
298
|
-
this.config.onStream?.({
|
|
299
|
-
type: "assistant_message",
|
|
300
|
-
message: { role: "assistant", content: finalText },
|
|
301
|
-
});
|
|
836
|
+
// Hook: turn end
|
|
302
837
|
await this.emitHook("on_turn_end", {
|
|
303
838
|
turnNumber: this.turnCount,
|
|
304
|
-
hasToolUse:
|
|
839
|
+
hasToolUse: true,
|
|
840
|
+
toolCallCount: toolCalls.length,
|
|
305
841
|
});
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
tlog.info("turn.tool_use", { cat: "turn", tools: response.toolCalls.map((t) => t.toolName) });
|
|
311
|
-
const toolCalls = response.toolCalls.slice(0, this.config.maxToolCallsPerTurn);
|
|
312
|
-
// Add assistant message with tool_use blocks to messages
|
|
313
|
-
const assistantBlocks = [];
|
|
314
|
-
if (response.text) {
|
|
315
|
-
assistantBlocks.push({ type: "text", text: response.text });
|
|
316
|
-
}
|
|
317
|
-
for (const tc of toolCalls) {
|
|
318
|
-
assistantBlocks.push({
|
|
319
|
-
type: "tool_use",
|
|
320
|
-
id: tc.id,
|
|
321
|
-
name: tc.toolName,
|
|
322
|
-
input: tc.args,
|
|
323
|
-
});
|
|
324
|
-
// Only emit tool_use_start if not already emitted during streaming
|
|
325
|
-
if (!this.streamedToolIds.has(tc.id)) {
|
|
326
|
-
this.config.onStream?.({ type: "tool_use_start", toolCall: tc });
|
|
327
|
-
}
|
|
328
|
-
// Record in transcript
|
|
329
|
-
this.deps.transcript.appendToolUse(tc.toolName, tc.id, tc.args);
|
|
330
|
-
}
|
|
331
|
-
messages.push({ role: "assistant", content: assistantBlocks });
|
|
332
|
-
// Execute tools — enqueue concurrency-safe tools for early start,
|
|
333
|
-
// drain remaining (unsafe) tools sequentially.
|
|
334
|
-
for (const tc of toolCalls) {
|
|
335
|
-
streamingQueue.enqueue(tc);
|
|
336
|
-
}
|
|
337
|
-
const results = await streamingQueue.drain();
|
|
338
|
-
// Record results in transcript and stream
|
|
339
|
-
const resultBlocks = [];
|
|
340
|
-
for (const result of results) {
|
|
341
|
-
const content = result.error ? `Error: ${result.error}` : (result.result ?? "(no output)");
|
|
342
|
-
resultBlocks.push({
|
|
343
|
-
type: "tool_result",
|
|
344
|
-
tool_use_id: result.id,
|
|
345
|
-
content,
|
|
346
|
-
});
|
|
347
|
-
this.deps.transcript.appendToolResult(result.id, result.toolName, result.result, result.error);
|
|
348
|
-
this.config.onStream?.({ type: "tool_result", result });
|
|
349
|
-
}
|
|
350
|
-
// Fire-and-forget tool use summary (non-blocking)
|
|
351
|
-
if (this.config.onStream) {
|
|
352
|
-
import("./tool-summary.js").then(({ generateToolUseSummary }) => {
|
|
353
|
-
if (!this.deps.model.summarize)
|
|
354
|
-
return;
|
|
355
|
-
generateToolUseSummary(toolCalls, results, this.deps.model.summarize).then((summary) => {
|
|
356
|
-
if (summary) {
|
|
357
|
-
this.config.onStream?.({ type: "tool_summary", summary });
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
messages.push({ role: "user", content: resultBlocks });
|
|
363
|
-
// Tool results just pushed; recompute ctx so the bar updates *before*
|
|
364
|
-
// the next model round-trip — large tool outputs can move it sharply.
|
|
365
|
-
this.emitCtxFromMessages(messages);
|
|
366
|
-
// Token budget check
|
|
367
|
-
const totalOutputTokens = this.deps.model.getOutputTokens?.() ?? 0;
|
|
368
|
-
const budgetDecision = checkTokenBudget(totalOutputTokens, this.config.tokenBudget ?? Infinity, budgetTracker);
|
|
369
|
-
if (budgetDecision === "stop") {
|
|
370
|
-
tlog.info("turn.budget_stop", {
|
|
842
|
+
// Record turn boundary
|
|
843
|
+
this.deps.transcript.appendTurnBoundary();
|
|
844
|
+
this.config.onTurnBoundary?.(this.turnCount);
|
|
845
|
+
tlog.info("turn.end", {
|
|
371
846
|
cat: "turn",
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
});
|
|
375
|
-
this.config.onStream?.({
|
|
376
|
-
type: "assistant_message",
|
|
377
|
-
message: { role: "assistant", content: finalText },
|
|
378
|
-
});
|
|
379
|
-
messages.push({ role: "assistant", content: finalText });
|
|
380
|
-
return { text: finalText, reason: "completed", messages };
|
|
381
|
-
}
|
|
382
|
-
if (budgetDecision === "nudge") {
|
|
383
|
-
messages.push({
|
|
384
|
-
role: "user",
|
|
385
|
-
content: "<system-reminder>You are approaching the token budget limit. Please start wrapping up your work and provide a summary.</system-reminder>",
|
|
847
|
+
duration_ms: Date.now() - turnStartedAt,
|
|
848
|
+
outcome: "continue",
|
|
386
849
|
});
|
|
387
850
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
// memory for the first few turns only; without this, the spinner
|
|
403
|
-
// runs forever on tasks the model has mentally finished.
|
|
404
|
-
const taskGuard = this.deps.toolExecutor.getTaskGuard();
|
|
405
|
-
if (taskGuard) {
|
|
406
|
-
const taskReminder = taskGuard.turnEnded(this.turnCount);
|
|
407
|
-
if (taskReminder) {
|
|
408
|
-
messages.push({ role: "user", content: taskReminder });
|
|
409
|
-
tlog.info("guard.stale_task", { cat: "guard", turn: this.turnCount });
|
|
410
|
-
}
|
|
851
|
+
}
|
|
852
|
+
catch (err) {
|
|
853
|
+
// Unexpected throw from the per-turn scaffolding (manageAsync, hooks,
|
|
854
|
+
// guards). Patch any dangling tool_use so a later resume isn't poisoned,
|
|
855
|
+
// surface the error to the UI, and return a terminal reason so the
|
|
856
|
+
// engine's post-run saveState records model_error instead of leaving
|
|
857
|
+
// the session stuck at "active".
|
|
858
|
+
this.patchOrphanedToolUses(messages);
|
|
859
|
+
// A user-initiated Stop also bubbles here (an AbortError thrown from the
|
|
860
|
+
// per-turn scaffolding). Treat it as a clean abort, not a model_error —
|
|
861
|
+
// no error event, so the UI shows only the "你停止了本轮" line.
|
|
862
|
+
if (isAbortError(err) || this.config.signal?.aborted) {
|
|
863
|
+
this.markStopped();
|
|
864
|
+
return { text: finalText, reason: "aborted_streaming", messages };
|
|
411
865
|
}
|
|
412
|
-
|
|
413
|
-
await this.emitHook("on_turn_end", {
|
|
414
|
-
turnNumber: this.turnCount,
|
|
415
|
-
hasToolUse: true,
|
|
416
|
-
toolCallCount: toolCalls.length,
|
|
417
|
-
});
|
|
418
|
-
// Record turn boundary
|
|
419
|
-
this.deps.transcript.appendTurnBoundary();
|
|
420
|
-
this.config.onTurnBoundary?.(this.turnCount);
|
|
421
|
-
tlog.info("turn.end", {
|
|
866
|
+
this.currentTurnLog.error("turn.unhandled_error", {
|
|
422
867
|
cat: "turn",
|
|
423
|
-
|
|
424
|
-
|
|
868
|
+
error: err.message,
|
|
869
|
+
stack: err.stack?.split("\n").slice(0, 4).join("\n"),
|
|
425
870
|
});
|
|
871
|
+
this.config.onStream?.({ type: "error", error: formatFriendlyError(err) });
|
|
872
|
+
return { text: finalText, reason: "model_error", messages };
|
|
426
873
|
}
|
|
427
874
|
// Max turns reached — do one final summarization call (no tools)
|
|
428
875
|
logger.warn("turn.max_turns_reached", {
|
|
@@ -464,6 +911,7 @@ export class TurnLoop {
|
|
|
464
911
|
async callModelWithFallback(messages) {
|
|
465
912
|
// Wrap stream callback to track tool_use_start events and reactive compaction
|
|
466
913
|
let streamingResponseTokens = 0;
|
|
914
|
+
let reactiveBucket = -1;
|
|
467
915
|
const wrappedStream = this.config.onStream
|
|
468
916
|
? (event) => {
|
|
469
917
|
if (event.type === "tool_use_start" && event.toolCall?.id) {
|
|
@@ -474,8 +922,12 @@ export class TurnLoop {
|
|
|
474
922
|
streamingResponseTokens += Math.ceil(event.text.length / 4);
|
|
475
923
|
}
|
|
476
924
|
// Reactive compaction warning: if nearing context limit mid-stream,
|
|
477
|
-
// log a warning (actual compaction happens between turns)
|
|
478
|
-
|
|
925
|
+
// log a warning (actual compaction happens between turns). Gated to
|
|
926
|
+
// fire once per 2000-token bucket crossed — the old `% 2000 === 0`
|
|
927
|
+
// check essentially never matched the running accumulator.
|
|
928
|
+
const probe = crossedReactiveThreshold(streamingResponseTokens, reactiveBucket);
|
|
929
|
+
if (probe.crossed) {
|
|
930
|
+
reactiveBucket = probe.bucket;
|
|
479
931
|
if (this.deps.contextManager.shouldReactiveCompact(messages, streamingResponseTokens)) {
|
|
480
932
|
this.currentTurnLog.warn("turn.reactive_compact_warning", {
|
|
481
933
|
cat: "turn",
|
|
@@ -494,6 +946,20 @@ export class TurnLoop {
|
|
|
494
946
|
// If it's a context or rate limit error, don't fallback — propagate
|
|
495
947
|
if (err instanceof ContextLimitError)
|
|
496
948
|
throw err;
|
|
949
|
+
// User cancelled (ESC / Stop / run signal). Falling back to a
|
|
950
|
+
// non-streaming call here re-sends the whole request the user just
|
|
951
|
+
// aborted. Propagate so the run unwinds cleanly instead of doing
|
|
952
|
+
// more work after cancellation.
|
|
953
|
+
//
|
|
954
|
+
// Check the run signal too, not only isAbortError(err): when a streaming
|
|
955
|
+
// request is aborted the error sometimes surfaces as a generic
|
|
956
|
+
// `Error: Request was aborted.` whose `name` is NOT "AbortError"
|
|
957
|
+
// (wrapping strips it), so isAbortError misses it and we'd fall back —
|
|
958
|
+
// re-issuing the cancelled request, which then throws "Aborted before
|
|
959
|
+
// LLM request" from withRetry and surfaces as a scary error on what was
|
|
960
|
+
// really just a cancel. The signal is the authoritative cancel source.
|
|
961
|
+
if (isAbortError(err) || this.config.signal?.aborted)
|
|
962
|
+
throw err;
|
|
497
963
|
// Streaming might have partially emitted — send tombstone to revoke
|
|
498
964
|
this.config.onStream?.({ type: "tombstone", messageId: `turn_${this.turnCount}` });
|
|
499
965
|
this.currentTurnLog.warn("turn.streaming_fallback", {
|
|
@@ -504,51 +970,6 @@ export class TurnLoop {
|
|
|
504
970
|
return await this.deps.model.callWithoutStreaming(this.deps.systemPrompt, messages, this.deps.tools, this.config.signal);
|
|
505
971
|
}
|
|
506
972
|
}
|
|
507
|
-
/**
|
|
508
|
-
* Execute tools with overlap: start concurrent-safe (read-only) tools
|
|
509
|
-
* immediately in parallel while sequential (write) tools run one-by-one.
|
|
510
|
-
* Both groups run simultaneously — we don't wait for safe tools to finish
|
|
511
|
-
* before starting unsafe ones.
|
|
512
|
-
*/
|
|
513
|
-
async executeToolsOverlapped(calls) {
|
|
514
|
-
if (calls.length <= 1) {
|
|
515
|
-
// Single tool — no overlap needed
|
|
516
|
-
return this.deps.toolExecutor.executeAll(calls);
|
|
517
|
-
}
|
|
518
|
-
const safe = [];
|
|
519
|
-
const unsafe = [];
|
|
520
|
-
for (const call of calls) {
|
|
521
|
-
if (this.deps.toolExecutor.isConcurrencySafe(call.toolName)) {
|
|
522
|
-
safe.push(call);
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
unsafe.push(call);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
// If all same type, delegate directly
|
|
529
|
-
if (safe.length === 0 || unsafe.length === 0) {
|
|
530
|
-
return this.deps.toolExecutor.executeAll(calls);
|
|
531
|
-
}
|
|
532
|
-
// Run both groups simultaneously:
|
|
533
|
-
// - safe tools all in parallel
|
|
534
|
-
// - unsafe tools sequentially (but started at the same time as safe group)
|
|
535
|
-
const resultMap = new Map();
|
|
536
|
-
const safePromise = Promise.all(safe.map((c) => this.deps.toolExecutor.executeSingle(c)));
|
|
537
|
-
const unsafePromise = (async () => {
|
|
538
|
-
const results = [];
|
|
539
|
-
for (const call of unsafe) {
|
|
540
|
-
results.push(await this.deps.toolExecutor.executeSingle(call));
|
|
541
|
-
}
|
|
542
|
-
return results;
|
|
543
|
-
})();
|
|
544
|
-
const [safeResults, unsafeResults] = await Promise.all([safePromise, unsafePromise]);
|
|
545
|
-
for (const r of safeResults)
|
|
546
|
-
resultMap.set(r.id, r);
|
|
547
|
-
for (const r of unsafeResults)
|
|
548
|
-
resultMap.set(r.id, r);
|
|
549
|
-
// Return results in original call order for deterministic transcript
|
|
550
|
-
return calls.map((c) => resultMap.get(c.id));
|
|
551
|
-
}
|
|
552
973
|
get currentTurn() {
|
|
553
974
|
return this.turnCount;
|
|
554
975
|
}
|
|
@@ -558,45 +979,57 @@ export class TurnLoop {
|
|
|
558
979
|
* Prevents model confusion on the next turn.
|
|
559
980
|
*/
|
|
560
981
|
patchOrphanedToolUses(messages) {
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
982
|
+
// Pre-compute every answered tool_use id across the WHOLE array, then scan
|
|
983
|
+
// forward and patch EVERY assistant message with a gap — not just the most
|
|
984
|
+
// recent one. The old version scanned backward and returned at the first
|
|
985
|
+
// all-answered assistant message, so when several earlier turns each left
|
|
986
|
+
// orphaned tool_uses (multi-turn API failures) only the latest got patched
|
|
987
|
+
// and the rest stayed unpaired → a 400 on the next call / on resume.
|
|
988
|
+
const answeredIds = new Set();
|
|
989
|
+
for (const msg of messages) {
|
|
990
|
+
if (!Array.isArray(msg.content))
|
|
565
991
|
continue;
|
|
566
|
-
const toolUseIds = [];
|
|
567
992
|
for (const block of msg.content) {
|
|
568
|
-
if (block.type === "
|
|
569
|
-
|
|
993
|
+
if (block.type === "tool_result" && block.tool_use_id) {
|
|
994
|
+
answeredIds.add(block.tool_use_id);
|
|
995
|
+
}
|
|
570
996
|
}
|
|
571
|
-
|
|
997
|
+
}
|
|
998
|
+
let totalPatched = 0;
|
|
999
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1000
|
+
const msg = messages[i];
|
|
1001
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content))
|
|
572
1002
|
continue;
|
|
573
|
-
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (!Array.isArray(rm.content))
|
|
578
|
-
continue;
|
|
579
|
-
for (const block of rm.content) {
|
|
580
|
-
if (block.type === "tool_result" && block.tool_use_id) {
|
|
581
|
-
answeredIds.add(block.tool_use_id);
|
|
582
|
-
}
|
|
1003
|
+
const orphanedIds = [];
|
|
1004
|
+
for (const block of msg.content) {
|
|
1005
|
+
if (block.type === "tool_use" && block.id && !answeredIds.has(block.id)) {
|
|
1006
|
+
orphanedIds.push(block.id);
|
|
583
1007
|
}
|
|
584
1008
|
}
|
|
585
|
-
const orphanedIds = toolUseIds.filter((id) => !answeredIds.has(id));
|
|
586
1009
|
if (orphanedIds.length === 0)
|
|
587
|
-
|
|
588
|
-
//
|
|
1010
|
+
continue;
|
|
1011
|
+
// Synthetic error results, spliced in RIGHT AFTER the offending assistant
|
|
1012
|
+
// message (not appended to the end) so each tool_use is paired in place.
|
|
1013
|
+
// is_error must be set: the Anthropic provider only emits is_error when
|
|
1014
|
+
// the flag is present, otherwise the model reads the cancellation as plain
|
|
1015
|
+
// text output and assumes the tool succeeded.
|
|
589
1016
|
const errorBlocks = orphanedIds.map((id) => ({
|
|
590
1017
|
type: "tool_result",
|
|
591
1018
|
tool_use_id: id,
|
|
592
1019
|
content: "Error: Tool execution was cancelled because the previous API call failed.",
|
|
1020
|
+
is_error: true,
|
|
593
1021
|
}));
|
|
594
|
-
messages.
|
|
1022
|
+
messages.splice(i + 1, 0, { role: "user", content: errorBlocks });
|
|
1023
|
+
for (const id of orphanedIds)
|
|
1024
|
+
answeredIds.add(id);
|
|
1025
|
+
totalPatched += orphanedIds.length;
|
|
1026
|
+
i += 1; // skip the message we just spliced in
|
|
1027
|
+
}
|
|
1028
|
+
if (totalPatched > 0) {
|
|
595
1029
|
this.currentTurnLog.warn("turn.patched_orphaned_tool_uses", {
|
|
596
1030
|
cat: "turn",
|
|
597
|
-
count:
|
|
1031
|
+
count: totalPatched,
|
|
598
1032
|
});
|
|
599
|
-
return; // Only patch the most recent orphaned set
|
|
600
1033
|
}
|
|
601
1034
|
}
|
|
602
1035
|
}
|