@cjhyy/code-shell-core 0.5.0-rc.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/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/agent/coordinator.d.ts +49 -0
- package/dist/agent/coordinator.js +77 -0
- package/dist/arena/arena.d.ts +43 -0
- package/dist/arena/arena.js +334 -0
- package/dist/arena/context/context-tools.d.ts +16 -0
- package/dist/arena/context/context-tools.js +231 -0
- package/dist/arena/detect-mode.d.ts +20 -0
- package/dist/arena/detect-mode.js +78 -0
- package/dist/arena/digest-builder.d.ts +25 -0
- package/dist/arena/digest-builder.js +120 -0
- package/dist/arena/index.d.ts +29 -0
- package/dist/arena/index.js +29 -0
- package/dist/arena/iterate/convergence.d.ts +25 -0
- package/dist/arena/iterate/convergence.js +103 -0
- package/dist/arena/iterate/formats/index.d.ts +22 -0
- package/dist/arena/iterate/formats/index.js +283 -0
- package/dist/arena/iterate/index.d.ts +11 -0
- package/dist/arena/iterate/index.js +9 -0
- package/dist/arena/iterate/iterative-arena.d.ts +23 -0
- package/dist/arena/iterate/iterative-arena.js +237 -0
- package/dist/arena/iterate/parse.d.ts +42 -0
- package/dist/arena/iterate/parse.js +123 -0
- package/dist/arena/iterate/phases/argue.d.ts +22 -0
- package/dist/arena/iterate/phases/argue.js +159 -0
- package/dist/arena/iterate/phases/revise.d.ts +16 -0
- package/dist/arena/iterate/phases/revise.js +62 -0
- package/dist/arena/iterate/phases/tournament.d.ts +34 -0
- package/dist/arena/iterate/phases/tournament.js +113 -0
- package/dist/arena/iterate/tools/web-tools.d.ts +13 -0
- package/dist/arena/iterate/tools/web-tools.js +54 -0
- package/dist/arena/iterate/types.d.ts +152 -0
- package/dist/arena/iterate/types.js +8 -0
- package/dist/arena/ledger.d.ts +47 -0
- package/dist/arena/ledger.js +151 -0
- package/dist/arena/lenses/architecture.d.ts +5 -0
- package/dist/arena/lenses/architecture.js +22 -0
- package/dist/arena/lenses/engineering.d.ts +5 -0
- package/dist/arena/lenses/engineering.js +22 -0
- package/dist/arena/lenses/general.d.ts +5 -0
- package/dist/arena/lenses/general.js +20 -0
- package/dist/arena/lenses/index.d.ts +16 -0
- package/dist/arena/lenses/index.js +47 -0
- package/dist/arena/lenses/product.d.ts +5 -0
- package/dist/arena/lenses/product.js +22 -0
- package/dist/arena/model-presets.d.ts +23 -0
- package/dist/arena/model-presets.js +44 -0
- package/dist/arena/phases/adjudication.d.ts +24 -0
- package/dist/arena/phases/adjudication.js +144 -0
- package/dist/arena/phases/build-consensus.d.ts +29 -0
- package/dist/arena/phases/build-consensus.js +86 -0
- package/dist/arena/phases/claim-registry.d.ts +26 -0
- package/dist/arena/phases/claim-registry.js +60 -0
- package/dist/arena/phases/cross-review.d.ts +45 -0
- package/dist/arena/phases/cross-review.js +224 -0
- package/dist/arena/phases/debate-rounds.d.ts +27 -0
- package/dist/arena/phases/debate-rounds.js +162 -0
- package/dist/arena/phases/participant-research.d.ts +38 -0
- package/dist/arena/phases/participant-research.js +317 -0
- package/dist/arena/phases/planning-detail-expansion.d.ts +38 -0
- package/dist/arena/phases/planning-detail-expansion.js +121 -0
- package/dist/arena/planner.d.ts +27 -0
- package/dist/arena/planner.js +312 -0
- package/dist/arena/providers/docs.d.ts +6 -0
- package/dist/arena/providers/docs.js +108 -0
- package/dist/arena/providers/git.d.ts +8 -0
- package/dist/arena/providers/git.js +174 -0
- package/dist/arena/providers/index.d.ts +32 -0
- package/dist/arena/providers/index.js +132 -0
- package/dist/arena/providers/none.d.ts +7 -0
- package/dist/arena/providers/none.js +11 -0
- package/dist/arena/providers/repo.d.ts +6 -0
- package/dist/arena/providers/repo.js +255 -0
- package/dist/arena/render/session.d.ts +17 -0
- package/dist/arena/render/session.js +190 -0
- package/dist/arena/render/terminal.d.ts +34 -0
- package/dist/arena/render/terminal.js +286 -0
- package/dist/arena/strategies/discussion.d.ts +25 -0
- package/dist/arena/strategies/discussion.js +143 -0
- package/dist/arena/strategies/index.d.ts +15 -0
- package/dist/arena/strategies/index.js +28 -0
- package/dist/arena/strategies/language-wrapper.d.ts +17 -0
- package/dist/arena/strategies/language-wrapper.js +102 -0
- package/dist/arena/strategies/lens-wrapper.d.ts +16 -0
- package/dist/arena/strategies/lens-wrapper.js +236 -0
- package/dist/arena/strategies/planning.d.ts +30 -0
- package/dist/arena/strategies/planning.js +225 -0
- package/dist/arena/strategies/review.d.ts +26 -0
- package/dist/arena/strategies/review.js +168 -0
- package/dist/arena/strategies/utils.d.ts +39 -0
- package/dist/arena/strategies/utils.js +647 -0
- package/dist/arena/tools/selector.d.ts +17 -0
- package/dist/arena/tools/selector.js +61 -0
- package/dist/arena/transitions.d.ts +48 -0
- package/dist/arena/transitions.js +92 -0
- package/dist/arena/types.d.ts +508 -0
- package/dist/arena/types.js +27 -0
- package/dist/cli/agent-server-stdio.d.ts +32 -0
- package/dist/cli/agent-server-stdio.js +116 -0
- package/dist/colorizer.d.ts +23 -0
- package/dist/colorizer.js +12 -0
- package/dist/context/compaction.d.ts +116 -0
- package/dist/context/compaction.js +534 -0
- package/dist/context/manager.d.ts +145 -0
- package/dist/context/manager.js +423 -0
- package/dist/context/token-counter.d.ts +27 -0
- package/dist/context/token-counter.js +91 -0
- package/dist/context/tool-result-storage.d.ts +96 -0
- package/dist/context/tool-result-storage.js +296 -0
- package/dist/cost-tracker.d.ts +64 -0
- package/dist/cost-tracker.js +314 -0
- package/dist/cron/scheduler.d.ts +34 -0
- package/dist/cron/scheduler.js +115 -0
- package/dist/data/deepseek-models.json +46 -0
- package/dist/data/gemini-models.json +67 -0
- package/dist/data/openai-models.json +175 -0
- package/dist/data/openrouter-models.d.ts +35 -0
- package/dist/data/openrouter-models.js +36 -0
- package/dist/data/openrouter-models.json +4626 -0
- package/dist/data/openrouter-sync.d.ts +16 -0
- package/dist/data/openrouter-sync.js +53 -0
- package/dist/data/static-catalogs.d.ts +25 -0
- package/dist/data/static-catalogs.js +26 -0
- package/dist/data/zai-models.json +56 -0
- package/dist/engine/cost-store.d.ts +22 -0
- package/dist/engine/cost-store.js +15 -0
- package/dist/engine/engine.d.ts +295 -0
- package/dist/engine/engine.js +1467 -0
- package/dist/engine/model-facade.d.ts +24 -0
- package/dist/engine/model-facade.js +194 -0
- package/dist/engine/parse-task.d.ts +59 -0
- package/dist/engine/parse-task.js +135 -0
- package/dist/engine/patch-orphaned-tools.d.ts +52 -0
- package/dist/engine/patch-orphaned-tools.js +95 -0
- package/dist/engine/query.d.ts +50 -0
- package/dist/engine/query.js +107 -0
- package/dist/engine/runtime.d.ts +50 -0
- package/dist/engine/runtime.js +59 -0
- package/dist/engine/streaming-tool-queue.d.ts +34 -0
- package/dist/engine/streaming-tool-queue.js +61 -0
- package/dist/engine/token-budget.d.ts +22 -0
- package/dist/engine/token-budget.js +40 -0
- package/dist/engine/tool-summary.d.ts +13 -0
- package/dist/engine/tool-summary.js +35 -0
- package/dist/engine/turn-loop.d.ts +122 -0
- package/dist/engine/turn-loop.js +602 -0
- package/dist/engine/turn-state.d.ts +20 -0
- package/dist/engine/turn-state.js +17 -0
- package/dist/exceptions.d.ts +61 -0
- package/dist/exceptions.js +116 -0
- package/dist/git/utils.d.ts +35 -0
- package/dist/git/utils.js +98 -0
- package/dist/git/worktree.d.ts +36 -0
- package/dist/git/worktree.js +139 -0
- package/dist/hooks/events.d.ts +114 -0
- package/dist/hooks/events.js +4 -0
- package/dist/hooks/inject.d.ts +19 -0
- package/dist/hooks/inject.js +29 -0
- package/dist/hooks/registry.d.ts +15 -0
- package/dist/hooks/registry.js +84 -0
- package/dist/hooks/shell-runner.d.ts +45 -0
- package/dist/hooks/shell-runner.js +173 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +145 -0
- package/dist/llm/api-key-sanitize.d.ts +22 -0
- package/dist/llm/api-key-sanitize.js +81 -0
- package/dist/llm/capabilities/index.d.ts +20 -0
- package/dist/llm/capabilities/index.js +39 -0
- package/dist/llm/capabilities/rules.d.ts +26 -0
- package/dist/llm/capabilities/rules.js +215 -0
- package/dist/llm/capabilities/types.d.ts +114 -0
- package/dist/llm/capabilities/types.js +24 -0
- package/dist/llm/client-base.d.ts +29 -0
- package/dist/llm/client-base.js +137 -0
- package/dist/llm/client-factory.d.ts +10 -0
- package/dist/llm/client-factory.js +29 -0
- package/dist/llm/model-cache.d.ts +21 -0
- package/dist/llm/model-cache.js +46 -0
- package/dist/llm/model-fetcher.d.ts +29 -0
- package/dist/llm/model-fetcher.js +282 -0
- package/dist/llm/model-pool.d.ts +95 -0
- package/dist/llm/model-pool.js +246 -0
- package/dist/llm/provider-catalog.d.ts +32 -0
- package/dist/llm/provider-catalog.js +50 -0
- package/dist/llm/provider-kinds.d.ts +21 -0
- package/dist/llm/provider-kinds.js +132 -0
- package/dist/llm/providers/anthropic.d.ts +19 -0
- package/dist/llm/providers/anthropic.js +267 -0
- package/dist/llm/providers/openai.d.ts +64 -0
- package/dist/llm/providers/openai.js +606 -0
- package/dist/llm/retry.d.ts +12 -0
- package/dist/llm/retry.js +17 -0
- package/dist/llm/stream-watchdog.d.ts +42 -0
- package/dist/llm/stream-watchdog.js +70 -0
- package/dist/llm/token-counter.d.ts +17 -0
- package/dist/llm/token-counter.js +36 -0
- package/dist/llm/types.d.ts +37 -0
- package/dist/llm/types.js +4 -0
- package/dist/logging/logger.d.ts +109 -0
- package/dist/logging/logger.js +390 -0
- package/dist/logging/sanitize-messages.d.ts +42 -0
- package/dist/logging/sanitize-messages.js +150 -0
- package/dist/logging/session-recorder.d.ts +90 -0
- package/dist/logging/session-recorder.js +304 -0
- package/dist/lsp/client.d.ts +41 -0
- package/dist/lsp/client.js +166 -0
- package/dist/lsp/manager.d.ts +40 -0
- package/dist/lsp/manager.js +124 -0
- package/dist/lsp/servers.d.ts +16 -0
- package/dist/lsp/servers.js +60 -0
- package/dist/migrate-models.d.ts +45 -0
- package/dist/migrate-models.js +179 -0
- package/dist/onboarding.d.ts +189 -0
- package/dist/onboarding.js +625 -0
- package/dist/plugins/gitOps.d.ts +32 -0
- package/dist/plugins/gitOps.js +62 -0
- package/dist/plugins/installedPlugins.d.ts +15 -0
- package/dist/plugins/installedPlugins.js +56 -0
- package/dist/plugins/knownMarketplaces.d.ts +10 -0
- package/dist/plugins/knownMarketplaces.js +46 -0
- package/dist/plugins/loadPluginHooks.d.ts +52 -0
- package/dist/plugins/loadPluginHooks.js +162 -0
- package/dist/plugins/marketplaceManager.d.ts +49 -0
- package/dist/plugins/marketplaceManager.js +142 -0
- package/dist/plugins/parseMarketplaceInput.d.ts +16 -0
- package/dist/plugins/parseMarketplaceInput.js +73 -0
- package/dist/plugins/pluginCommandHook.d.ts +54 -0
- package/dist/plugins/pluginCommandHook.js +180 -0
- package/dist/plugins/pluginCommandsLoader.d.ts +25 -0
- package/dist/plugins/pluginCommandsLoader.js +114 -0
- package/dist/plugins/pluginInstaller.d.ts +36 -0
- package/dist/plugins/pluginInstaller.js +228 -0
- package/dist/plugins/schemas.d.ts +9 -0
- package/dist/plugins/schemas.js +136 -0
- package/dist/plugins/types.d.ts +75 -0
- package/dist/plugins/types.js +7 -0
- package/dist/plugins/varRewrite.d.ts +42 -0
- package/dist/plugins/varRewrite.js +129 -0
- package/dist/preset/index.d.ts +60 -0
- package/dist/preset/index.js +159 -0
- package/dist/product/define.d.ts +60 -0
- package/dist/product/define.js +123 -0
- package/dist/product/index.d.ts +5 -0
- package/dist/product/index.js +4 -0
- package/dist/product/types.d.ts +111 -0
- package/dist/product/types.js +21 -0
- package/dist/prompt/composer.d.ts +49 -0
- package/dist/prompt/composer.js +168 -0
- package/dist/prompt/instruction-scanner.d.ts +54 -0
- package/dist/prompt/instruction-scanner.js +187 -0
- package/dist/prompt/section-cache.d.ts +15 -0
- package/dist/prompt/section-cache.js +31 -0
- package/dist/prompt/section-loader.d.ts +26 -0
- package/dist/prompt/section-loader.js +53 -0
- package/dist/prompt/sections/base.md +35 -0
- package/dist/prompt/sections/coding.md +31 -0
- package/dist/prompt/sections/orchestration.md +16 -0
- package/dist/prompt/sections/tone.md +9 -0
- package/dist/protocol/chat-session-manager.d.ts +33 -0
- package/dist/protocol/chat-session-manager.js +65 -0
- package/dist/protocol/chat-session.d.ts +49 -0
- package/dist/protocol/chat-session.js +86 -0
- package/dist/protocol/client.d.ts +104 -0
- package/dist/protocol/client.js +260 -0
- package/dist/protocol/factories.d.ts +93 -0
- package/dist/protocol/factories.js +64 -0
- package/dist/protocol/helpers.d.ts +42 -0
- package/dist/protocol/helpers.js +55 -0
- package/dist/protocol/index.d.ts +4 -0
- package/dist/protocol/index.js +4 -0
- package/dist/protocol/server.d.ts +80 -0
- package/dist/protocol/server.js +855 -0
- package/dist/protocol/transport.d.ts +37 -0
- package/dist/protocol/transport.js +85 -0
- package/dist/protocol/types.d.ts +221 -0
- package/dist/protocol/types.js +61 -0
- package/dist/remote/bridge.d.ts +37 -0
- package/dist/remote/bridge.js +103 -0
- package/dist/run/ArtifactTracker.d.ts +38 -0
- package/dist/run/ArtifactTracker.js +132 -0
- package/dist/run/CheckpointWriter.d.ts +53 -0
- package/dist/run/CheckpointWriter.js +135 -0
- package/dist/run/EngineRunner.d.ts +72 -0
- package/dist/run/EngineRunner.js +119 -0
- package/dist/run/Evaluator.d.ts +56 -0
- package/dist/run/Evaluator.js +62 -0
- package/dist/run/FileRunStore.d.ts +39 -0
- package/dist/run/FileRunStore.js +176 -0
- package/dist/run/Heartbeat.d.ts +54 -0
- package/dist/run/Heartbeat.js +123 -0
- package/dist/run/RunApprovalBackend.d.ts +53 -0
- package/dist/run/RunApprovalBackend.js +85 -0
- package/dist/run/RunLock.d.ts +40 -0
- package/dist/run/RunLock.js +98 -0
- package/dist/run/RunManager.d.ts +82 -0
- package/dist/run/RunManager.js +587 -0
- package/dist/run/RunQueue.d.ts +27 -0
- package/dist/run/RunQueue.js +73 -0
- package/dist/run/RunStore.d.ts +25 -0
- package/dist/run/RunStore.js +9 -0
- package/dist/run/factory.d.ts +63 -0
- package/dist/run/factory.js +51 -0
- package/dist/run/index.d.ts +17 -0
- package/dist/run/index.js +23 -0
- package/dist/run/types.d.ts +136 -0
- package/dist/run/types.js +28 -0
- package/dist/runtime/safe-spawn.d.ts +118 -0
- package/dist/runtime/safe-spawn.js +268 -0
- package/dist/services/analytics.d.ts +29 -0
- package/dist/services/analytics.js +84 -0
- package/dist/services/auto-dream.d.ts +49 -0
- package/dist/services/auto-dream.js +124 -0
- package/dist/services/diagnostics.d.ts +41 -0
- package/dist/services/diagnostics.js +99 -0
- package/dist/services/extract-memories.d.ts +31 -0
- package/dist/services/extract-memories.js +71 -0
- package/dist/services/index.d.ts +11 -0
- package/dist/services/index.js +17 -0
- package/dist/services/memory-orchestrator.d.ts +60 -0
- package/dist/services/memory-orchestrator.js +133 -0
- package/dist/services/notifier.d.ts +25 -0
- package/dist/services/notifier.js +69 -0
- package/dist/services/oauth.d.ts +27 -0
- package/dist/services/oauth.js +153 -0
- package/dist/services/session-memory.d.ts +37 -0
- package/dist/services/session-memory.js +81 -0
- package/dist/session/file-history.d.ts +41 -0
- package/dist/session/file-history.js +115 -0
- package/dist/session/memory.d.ts +104 -0
- package/dist/session/memory.js +310 -0
- package/dist/session/session-manager.d.ts +41 -0
- package/dist/session/session-manager.js +261 -0
- package/dist/session/transcript.d.ts +39 -0
- package/dist/session/transcript.js +186 -0
- package/dist/settings/manager.d.ts +36 -0
- package/dist/settings/manager.js +187 -0
- package/dist/settings/schema.d.ts +1219 -0
- package/dist/settings/schema.js +246 -0
- package/dist/skills/frontmatter.d.ts +13 -0
- package/dist/skills/frontmatter.js +70 -0
- package/dist/skills/index.d.ts +7 -0
- package/dist/skills/index.js +6 -0
- package/dist/skills/scanner.d.ts +31 -0
- package/dist/skills/scanner.js +171 -0
- package/dist/state.d.ts +160 -0
- package/dist/state.js +267 -0
- package/dist/tool-system/builtin/agent-notifications.d.ts +84 -0
- package/dist/tool-system/builtin/agent-notifications.js +197 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +67 -0
- package/dist/tool-system/builtin/agent-registry.js +126 -0
- package/dist/tool-system/builtin/agent-transcript-translator.d.ts +22 -0
- package/dist/tool-system/builtin/agent-transcript-translator.js +160 -0
- package/dist/tool-system/builtin/agent.d.ts +16 -0
- package/dist/tool-system/builtin/agent.js +364 -0
- package/dist/tool-system/builtin/apply-patch/applier.d.ts +26 -0
- package/dist/tool-system/builtin/apply-patch/applier.js +250 -0
- package/dist/tool-system/builtin/apply-patch/index.d.ts +20 -0
- package/dist/tool-system/builtin/apply-patch/index.js +101 -0
- package/dist/tool-system/builtin/apply-patch/parser.d.ts +17 -0
- package/dist/tool-system/builtin/apply-patch/parser.js +212 -0
- package/dist/tool-system/builtin/apply-patch/seek-sequence.d.ts +18 -0
- package/dist/tool-system/builtin/apply-patch/seek-sequence.js +123 -0
- package/dist/tool-system/builtin/apply-patch/types.d.ts +49 -0
- package/dist/tool-system/builtin/apply-patch/types.js +13 -0
- package/dist/tool-system/builtin/arena.d.ts +31 -0
- package/dist/tool-system/builtin/arena.js +416 -0
- package/dist/tool-system/builtin/ask-user.d.ts +20 -0
- package/dist/tool-system/builtin/ask-user.js +99 -0
- package/dist/tool-system/builtin/bash.d.ts +21 -0
- package/dist/tool-system/builtin/bash.js +149 -0
- package/dist/tool-system/builtin/brief.d.ts +6 -0
- package/dist/tool-system/builtin/brief.js +41 -0
- package/dist/tool-system/builtin/config.d.ts +6 -0
- package/dist/tool-system/builtin/config.js +67 -0
- package/dist/tool-system/builtin/cron.d.ts +10 -0
- package/dist/tool-system/builtin/cron.js +61 -0
- package/dist/tool-system/builtin/edit.d.ts +6 -0
- package/dist/tool-system/builtin/edit.js +100 -0
- package/dist/tool-system/builtin/file-cache.d.ts +11 -0
- package/dist/tool-system/builtin/file-cache.js +39 -0
- package/dist/tool-system/builtin/glob.d.ts +7 -0
- package/dist/tool-system/builtin/glob.js +76 -0
- package/dist/tool-system/builtin/grep.d.ts +7 -0
- package/dist/tool-system/builtin/grep.js +150 -0
- package/dist/tool-system/builtin/index.d.ts +20 -0
- package/dist/tool-system/builtin/index.js +449 -0
- package/dist/tool-system/builtin/lsp.d.ts +6 -0
- package/dist/tool-system/builtin/lsp.js +141 -0
- package/dist/tool-system/builtin/mcp-tools.d.ts +10 -0
- package/dist/tool-system/builtin/mcp-tools.js +102 -0
- package/dist/tool-system/builtin/memory.d.ts +26 -0
- package/dist/tool-system/builtin/memory.js +226 -0
- package/dist/tool-system/builtin/notebook-edit.d.ts +6 -0
- package/dist/tool-system/builtin/notebook-edit.js +124 -0
- package/dist/tool-system/builtin/plan.d.ts +15 -0
- package/dist/tool-system/builtin/plan.js +55 -0
- package/dist/tool-system/builtin/powershell.d.ts +11 -0
- package/dist/tool-system/builtin/powershell.js +64 -0
- package/dist/tool-system/builtin/read.d.ts +6 -0
- package/dist/tool-system/builtin/read.js +70 -0
- package/dist/tool-system/builtin/remote-trigger.d.ts +6 -0
- package/dist/tool-system/builtin/remote-trigger.js +54 -0
- package/dist/tool-system/builtin/repl.d.ts +11 -0
- package/dist/tool-system/builtin/repl.js +82 -0
- package/dist/tool-system/builtin/send-message.d.ts +6 -0
- package/dist/tool-system/builtin/send-message.js +47 -0
- package/dist/tool-system/builtin/skill-prompt.d.ts +20 -0
- package/dist/tool-system/builtin/skill-prompt.js +52 -0
- package/dist/tool-system/builtin/skill.d.ts +9 -0
- package/dist/tool-system/builtin/skill.js +55 -0
- package/dist/tool-system/builtin/sleep.d.ts +6 -0
- package/dist/tool-system/builtin/sleep.js +33 -0
- package/dist/tool-system/builtin/task.d.ts +48 -0
- package/dist/tool-system/builtin/task.js +139 -0
- package/dist/tool-system/builtin/tool-search.d.ts +11 -0
- package/dist/tool-system/builtin/tool-search.js +90 -0
- package/dist/tool-system/builtin/web-fetch.d.ts +10 -0
- package/dist/tool-system/builtin/web-fetch.js +328 -0
- package/dist/tool-system/builtin/web-search.d.ts +25 -0
- package/dist/tool-system/builtin/web-search.js +174 -0
- package/dist/tool-system/builtin/worktree.d.ts +10 -0
- package/dist/tool-system/builtin/worktree.js +95 -0
- package/dist/tool-system/builtin/write.d.ts +6 -0
- package/dist/tool-system/builtin/write.js +36 -0
- package/dist/tool-system/context.d.ts +165 -0
- package/dist/tool-system/context.js +18 -0
- package/dist/tool-system/executor.d.ts +49 -0
- package/dist/tool-system/executor.js +495 -0
- package/dist/tool-system/investigation-guard.d.ts +46 -0
- package/dist/tool-system/investigation-guard.js +155 -0
- package/dist/tool-system/mcp-manager.d.ts +67 -0
- package/dist/tool-system/mcp-manager.js +249 -0
- package/dist/tool-system/permission.d.ts +111 -0
- package/dist/tool-system/permission.js +774 -0
- package/dist/tool-system/registry.d.ts +32 -0
- package/dist/tool-system/registry.js +131 -0
- package/dist/tool-system/sandbox/bwrap.d.ts +13 -0
- package/dist/tool-system/sandbox/bwrap.js +63 -0
- package/dist/tool-system/sandbox/index.d.ts +78 -0
- package/dist/tool-system/sandbox/index.js +222 -0
- package/dist/tool-system/sandbox/off.d.ts +2 -0
- package/dist/tool-system/sandbox/off.js +8 -0
- package/dist/tool-system/sandbox/seatbelt.d.ts +18 -0
- package/dist/tool-system/sandbox/seatbelt.js +109 -0
- package/dist/tool-system/task-guard.d.ts +29 -0
- package/dist/tool-system/task-guard.js +70 -0
- package/dist/tool-system/validation.d.ts +5 -0
- package/dist/tool-system/validation.js +39 -0
- package/dist/types.d.ts +376 -0
- package/dist/types.js +4 -0
- package/dist/updater.d.ts +65 -0
- package/dist/updater.js +405 -0
- package/dist/utils/debug.d.ts +41 -0
- package/dist/utils/debug.js +110 -0
- package/dist/utils/earlyInput.d.ts +43 -0
- package/dist/utils/earlyInput.js +166 -0
- package/dist/utils/env.d.ts +24 -0
- package/dist/utils/env.js +104 -0
- package/dist/utils/envUtils.d.ts +51 -0
- package/dist/utils/envUtils.js +135 -0
- package/dist/utils/execFileNoThrow.d.ts +39 -0
- package/dist/utils/execFileNoThrow.js +74 -0
- package/dist/utils/format.d.ts +44 -0
- package/dist/utils/format.js +236 -0
- package/dist/utils/intl.d.ts +22 -0
- package/dist/utils/intl.js +83 -0
- package/dist/utils/lockfile.d.ts +15 -0
- package/dist/utils/lockfile.js +30 -0
- package/dist/utils/memoize.d.ts +19 -0
- package/dist/utils/memoize.js +14 -0
- package/dist/utils/semver.d.ts +6 -0
- package/dist/utils/semver.js +13 -0
- package/dist/utils/sliceAnsi.d.ts +12 -0
- package/dist/utils/sliceAnsi.js +62 -0
- package/dist/utils/systemTheme.d.ts +40 -0
- package/dist/utils/systemTheme.js +108 -0
- package/dist/utils/task-sanitizer.d.ts +6 -0
- package/dist/utils/task-sanitizer.js +24 -0
- package/dist/utils/theme.d.ts +93 -0
- package/dist/utils/theme.js +544 -0
- package/dist/utils/toolDisplay.d.ts +31 -0
- package/dist/utils/toolDisplay.js +188 -0
- package/package.json +72 -0
|
@@ -0,0 +1,1467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine — the main facade that wires all components together.
|
|
3
|
+
*/
|
|
4
|
+
import { createLLMClient } from "../llm/client-factory.js";
|
|
5
|
+
import { ToolRegistry } from "../tool-system/registry.js";
|
|
6
|
+
import { ToolExecutor } from "../tool-system/executor.js";
|
|
7
|
+
import { InvestigationGuard } from "../tool-system/investigation-guard.js";
|
|
8
|
+
import { TaskGuard } from "../tool-system/task-guard.js";
|
|
9
|
+
import { readLastTodoSnapshot } from "../tool-system/builtin/task.js";
|
|
10
|
+
import { PermissionClassifier, HeadlessApprovalBackend, AutoApprovalBackend, InteractiveApprovalBackend, getInteractiveApprovalBackend, } from "../tool-system/permission.js";
|
|
11
|
+
import { HookRegistry } from "../hooks/registry.js";
|
|
12
|
+
import { wrapHookMessages } from "../hooks/inject.js";
|
|
13
|
+
import { loadPluginHooks } from "../plugins/loadPluginHooks.js";
|
|
14
|
+
import { patchOrphanedToolUses } from "./patch-orphaned-tools.js";
|
|
15
|
+
import { runShellHook, shellHookMatches } from "../hooks/shell-runner.js";
|
|
16
|
+
import { ContextManager } from "../context/manager.js";
|
|
17
|
+
import { PromptComposer } from "../prompt/composer.js";
|
|
18
|
+
import { SessionManager } from "../session/session-manager.js";
|
|
19
|
+
import { ModelFacade } from "./model-facade.js";
|
|
20
|
+
import { logger, setCurrentSid, runWithSid } from "../logging/logger.js";
|
|
21
|
+
import { recordSessionStart, recordSessionEnd } from "../logging/session-recorder.js";
|
|
22
|
+
import { sanitizeContent, sanitizeTaskString } from "../logging/sanitize-messages.js";
|
|
23
|
+
import { TurnLoop } from "./turn-loop.js";
|
|
24
|
+
import { MCPManager } from "../tool-system/mcp-manager.js";
|
|
25
|
+
import { SettingsManager } from "../settings/manager.js";
|
|
26
|
+
import { FileHistory } from "../session/file-history.js";
|
|
27
|
+
import { defaultSandboxConfig, resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
|
|
28
|
+
import { resolveAgentPreset, resolveBuiltinToolNames, } from "../preset/index.js";
|
|
29
|
+
import { ModelPool } from "../llm/model-pool.js";
|
|
30
|
+
import { ProviderCatalog } from "../llm/provider-catalog.js";
|
|
31
|
+
import { defaultCacheDir } from "../llm/model-cache.js";
|
|
32
|
+
import { detectProviderFromApiKey, buildModelPool, } from "../onboarding.js";
|
|
33
|
+
import { detectPastedNoise } from "../utils/task-sanitizer.js";
|
|
34
|
+
import { parseTaskWithImages, } from "./parse-task.js";
|
|
35
|
+
import { capabilitiesFor } from "../llm/capabilities/index.js";
|
|
36
|
+
import { MemoryOrchestrator } from "../services/memory-orchestrator.js";
|
|
37
|
+
import { join } from "node:path";
|
|
38
|
+
import { homedir } from "node:os";
|
|
39
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
40
|
+
export class Engine {
|
|
41
|
+
config;
|
|
42
|
+
preset;
|
|
43
|
+
toolRegistry;
|
|
44
|
+
hooks;
|
|
45
|
+
sessionManager;
|
|
46
|
+
mcpManager;
|
|
47
|
+
modelPool;
|
|
48
|
+
/** Shared resources supplied at construction (adapter pattern — null when self-constructed). */
|
|
49
|
+
runtime;
|
|
50
|
+
/** Active permission mode for this Engine instance. */
|
|
51
|
+
permissionMode;
|
|
52
|
+
/** True when permissionMode === "plan". */
|
|
53
|
+
planMode;
|
|
54
|
+
// Lazy SettingsManager — reused across updateConfig/readSetting so we
|
|
55
|
+
// don't re-read 6+ JSON files on every /model, /login, etc. The manager
|
|
56
|
+
// handles its own cache invalidation in saveUserSetting().
|
|
57
|
+
settingsManager;
|
|
58
|
+
// Live state from the current/most-recent run, retained for /compact and
|
|
59
|
+
// for live-mutating PermissionClassifier on permission-mode switch.
|
|
60
|
+
lastContextManager;
|
|
61
|
+
lastMessages;
|
|
62
|
+
lastSessionId;
|
|
63
|
+
compactedMessagesBySession = new Map();
|
|
64
|
+
/**
|
|
65
|
+
* SIDs whose ctx-bar seed we've already emitted in this process. The seed
|
|
66
|
+
* is a rough char/4 estimate; only useful before the first real
|
|
67
|
+
* usage_update arrives (cold start or cross-process resume). On subsequent
|
|
68
|
+
* turns the UI already shows the previous turn's accurate ctx — re-seeding
|
|
69
|
+
* would visibly drop the bar on every submit.
|
|
70
|
+
*/
|
|
71
|
+
ctxSeedSent = new Set();
|
|
72
|
+
/**
|
|
73
|
+
* Per-sid cache of "non-messages overhead" (system prompt + tool defs, in
|
|
74
|
+
* tokens). Survives across turns so each fresh TurnLoop instance can seed
|
|
75
|
+
* its first pre-llm emit with the right offset — without this the ctx bar
|
|
76
|
+
* visibly drops on every user submit (e.g. from ~20k → 3k) until the next
|
|
77
|
+
* LLM response arrives.
|
|
78
|
+
*/
|
|
79
|
+
ctxOverheadBySid = new Map();
|
|
80
|
+
activePermission;
|
|
81
|
+
/** Public accessor so UI/clients can read the resolved per-model window. */
|
|
82
|
+
get maxContextTokens() {
|
|
83
|
+
return this.resolveMaxContextTokens();
|
|
84
|
+
}
|
|
85
|
+
resolveMaxContextTokens() {
|
|
86
|
+
const modelEntry = this.modelPool.get();
|
|
87
|
+
return modelEntry?.maxContextTokens ?? this.config.maxContextTokens ?? 200_000;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Emit a lifecycle hook with isSubAgent auto-merged into data so handlers
|
|
91
|
+
* can skip noisy injections for spawned children. All Engine-side hook
|
|
92
|
+
* emits should go through this wrapper to keep the context envelope
|
|
93
|
+
* uniform with TurnLoop.emitHook.
|
|
94
|
+
*/
|
|
95
|
+
async emitHook(event, data = {}) {
|
|
96
|
+
return this.hooks.emit(event, {
|
|
97
|
+
...data,
|
|
98
|
+
isSubAgent: this.config.isSubAgent === true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Read settings.hooks and register a shell-runner wrapper handler per
|
|
103
|
+
* entry. Sub-agents skip shell hooks entirely — spawning child processes
|
|
104
|
+
* per emit for every sub-agent run would multiply token-side overhead
|
|
105
|
+
* for marginal value; explicit users who want sub-agent observability
|
|
106
|
+
* should register SDK-side handlers.
|
|
107
|
+
*/
|
|
108
|
+
registerSettingsHooks() {
|
|
109
|
+
if (this.config.isSubAgent === true)
|
|
110
|
+
return;
|
|
111
|
+
let settings;
|
|
112
|
+
try {
|
|
113
|
+
settings = this.getSettingsManager().get();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const entries = settings.hooks ?? [];
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
this.hooks.register(entry.event, async (ctx) => {
|
|
121
|
+
if (!shellHookMatches(entry, ctx))
|
|
122
|
+
return {};
|
|
123
|
+
return runShellHook(entry, ctx);
|
|
124
|
+
}, 50, `shell:${entry.event}:${entry.command.slice(0, 32)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
constructor(config) {
|
|
128
|
+
this.config = config;
|
|
129
|
+
// Wire shared runtime (adapter pattern — null when self-constructing).
|
|
130
|
+
this.runtime = config.runtime ?? null;
|
|
131
|
+
// Instance-level permission/plan mode fields.
|
|
132
|
+
this.permissionMode = config.permissionMode ?? "acceptEdits";
|
|
133
|
+
this.planMode = this.permissionMode === "plan";
|
|
134
|
+
this.preset = resolveAgentPreset(config.preset);
|
|
135
|
+
this.toolRegistry = config.runtime?.toolRegistry ?? new ToolRegistry({
|
|
136
|
+
builtinTools: resolveBuiltinToolNames({
|
|
137
|
+
preset: this.preset.name,
|
|
138
|
+
enabledBuiltinTools: config.enabledBuiltinTools,
|
|
139
|
+
disabledBuiltinTools: config.disabledBuiltinTools,
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
this.hooks = new HookRegistry();
|
|
143
|
+
// Installed-plugin hooks — declared in each plugin's hooks/hooks.json.
|
|
144
|
+
// Registered first (priority 80) so user-authored hooks at lower
|
|
145
|
+
// priorities (settings: 50, SDK config: default 0) can post-process
|
|
146
|
+
// or stop a plugin's contribution. Sub-agents skip plugin hooks for
|
|
147
|
+
// the same reason they skip settings hooks: per-emit child-process
|
|
148
|
+
// overhead multiplied across sub-agents outweighs the value, and
|
|
149
|
+
// dispatched tasks should run with minimal surface area.
|
|
150
|
+
if (config.isSubAgent !== true) {
|
|
151
|
+
loadPluginHooks(this.hooks);
|
|
152
|
+
}
|
|
153
|
+
// settings.hooks → shell-command wrappers. Chain order:
|
|
154
|
+
// plugin (80) → shell (50) → code (default 0).
|
|
155
|
+
this.registerSettingsHooks();
|
|
156
|
+
for (const hook of config.hooks ?? []) {
|
|
157
|
+
this.hooks.register(hook.event, hook.handler, hook.priority, hook.name);
|
|
158
|
+
}
|
|
159
|
+
this.sessionManager = new SessionManager(config.sessionStorageDir);
|
|
160
|
+
// Initialize model pool — prefer runtime's shared pool, fall back to self-constructed.
|
|
161
|
+
this.modelPool = config.runtime?.modelPool ?? new ModelPool();
|
|
162
|
+
if (!config.runtime) {
|
|
163
|
+
this.populateModelPoolFromSettings();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Load models[] / providers[] from settings into the active ModelPool and
|
|
168
|
+
* resync this.config.llm with the matching entry. Called from the ctor and
|
|
169
|
+
* from reloadModelPool() (e.g. after onboarding writes new entries to disk).
|
|
170
|
+
*/
|
|
171
|
+
populateModelPoolFromSettings() {
|
|
172
|
+
try {
|
|
173
|
+
const sm = this.getSettingsManager();
|
|
174
|
+
sm.invalidate();
|
|
175
|
+
const settings = sm.get();
|
|
176
|
+
if (settings.models?.length) {
|
|
177
|
+
for (const m of settings.models) {
|
|
178
|
+
this.modelPool.register({
|
|
179
|
+
key: m.key,
|
|
180
|
+
label: m.label,
|
|
181
|
+
provider: m.provider ?? "",
|
|
182
|
+
model: m.model,
|
|
183
|
+
baseUrl: m.baseUrl,
|
|
184
|
+
apiKey: m.apiKey,
|
|
185
|
+
maxOutputTokens: m.maxOutputTokens,
|
|
186
|
+
maxContextTokens: m.maxContextTokens,
|
|
187
|
+
providerKey: m.providerKey,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// Build catalog from settings.providers[] and attach to the pool
|
|
191
|
+
// so model entries can resolve baseUrl/apiKey from their provider.
|
|
192
|
+
if (settings.providers?.length) {
|
|
193
|
+
this.modelPool.setProviderCatalog(new ProviderCatalog(settings.providers));
|
|
194
|
+
}
|
|
195
|
+
this.modelPool.setCacheDir(defaultCacheDir());
|
|
196
|
+
this.modelPool.reloadCachedContextWindows();
|
|
197
|
+
// Resolve active entry. Priority:
|
|
198
|
+
// 1. settings.activeKey — primary source of truth (new shape).
|
|
199
|
+
// 2. Match settings.model.name against models[].model — legacy
|
|
200
|
+
// pre-activeKey configs and the migration path.
|
|
201
|
+
// We then switch the pool and write the resolved entry's credentials
|
|
202
|
+
// into config.llm, so the first run() uses the right endpoint instead
|
|
203
|
+
// of whatever env-derived fallback repl.ts seeded earlier.
|
|
204
|
+
const activeKey = settings.activeKey;
|
|
205
|
+
let match;
|
|
206
|
+
if (activeKey) {
|
|
207
|
+
match = settings.models.find((m) => m.key === activeKey);
|
|
208
|
+
}
|
|
209
|
+
if (!match) {
|
|
210
|
+
const currentModel = this.config.llm.model;
|
|
211
|
+
// OpenRouter stores entries as "provider/model-name"; the top-level
|
|
212
|
+
// settings.model.name is just "model-name". Match either form.
|
|
213
|
+
match = settings.models.find((m) => m.model === currentModel ||
|
|
214
|
+
(currentModel && m.model?.endsWith(`/${currentModel}`)));
|
|
215
|
+
}
|
|
216
|
+
if (match) {
|
|
217
|
+
const entry = this.modelPool.switch(match.key);
|
|
218
|
+
this.config = {
|
|
219
|
+
...this.config,
|
|
220
|
+
llm: this.modelPool.toLLMConfig(entry, this.config.llm),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (this.config.llm.apiKey) {
|
|
225
|
+
// Auto-populate pool from the configured API key when models[] is empty.
|
|
226
|
+
// This lets users who only set model.apiKey (without models[]) still
|
|
227
|
+
// use /model to switch between the provider's available models.
|
|
228
|
+
this.autoPopulatePool(this.config.llm.apiKey, this.config.llm.baseUrl);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Settings not available — pool stays empty
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Re-read settings and refresh the model pool. Used after onboarding /login
|
|
237
|
+
* writes new providers[] / models[] to disk so the running engine picks them
|
|
238
|
+
* up without a process restart. Existing pool entries are kept (re-registering
|
|
239
|
+
* the same key overwrites them), so callers don't need to clear first.
|
|
240
|
+
*/
|
|
241
|
+
reloadModelPool() {
|
|
242
|
+
// When sharing a runtime, the owner of the runtime is responsible
|
|
243
|
+
// for populating the pool; reloading from settings here would
|
|
244
|
+
// blast that owner's contributions and affect every other Engine
|
|
245
|
+
// that shares this runtime.
|
|
246
|
+
if (!this.runtime) {
|
|
247
|
+
this.populateModelPoolFromSettings();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Auto-populate the model pool when settings.models[] is empty but
|
|
252
|
+
* the user has configured an API key. Detects the provider from the
|
|
253
|
+
* key prefix / baseUrl and registers all its known models.
|
|
254
|
+
*/
|
|
255
|
+
autoPopulatePool(apiKey, baseUrl) {
|
|
256
|
+
const provider = detectProviderFromApiKey(apiKey, baseUrl);
|
|
257
|
+
if (!provider)
|
|
258
|
+
return;
|
|
259
|
+
const entries = buildModelPool(provider, apiKey);
|
|
260
|
+
for (const e of entries) {
|
|
261
|
+
this.modelPool.register(e);
|
|
262
|
+
}
|
|
263
|
+
// Activate the first registered model as the zero-config default.
|
|
264
|
+
// Users override via the onboarding wizard or `/model`.
|
|
265
|
+
const defaultEntry = entries[0];
|
|
266
|
+
if (defaultEntry) {
|
|
267
|
+
const entry = this.modelPool.switch(defaultEntry.key);
|
|
268
|
+
this.config = {
|
|
269
|
+
...this.config,
|
|
270
|
+
llm: this.modelPool.toLLMConfig(entry, this.config.llm),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Register a custom tool (from product adapter) into the tool registry.
|
|
276
|
+
* Must be called before run().
|
|
277
|
+
*/
|
|
278
|
+
registerCustomTool(definition, executor) {
|
|
279
|
+
this.toolRegistry.registerTool(definition, executor);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Inject the askUser handler after construction. Used by AgentServer
|
|
283
|
+
* to wire its protocol-backed askUser into an Engine that was created
|
|
284
|
+
* before the server existed (chicken-and-egg: server takes engine in ctor).
|
|
285
|
+
*/
|
|
286
|
+
setAskUser(fn) {
|
|
287
|
+
this.config.askUser = fn;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Run a task from start to finish.
|
|
291
|
+
*/
|
|
292
|
+
async run(task, options) {
|
|
293
|
+
const cwd = options?.cwd ?? this.config.cwd ?? process.cwd();
|
|
294
|
+
// Wrap the caller's onStream so we can intercept `task_update`
|
|
295
|
+
// events emitted by TodoWrite and keep an in-engine snapshot.
|
|
296
|
+
// TaskGuard reads this snapshot at turn end to decide whether to
|
|
297
|
+
// nag about stale in_progress items. Without the wrapper we'd
|
|
298
|
+
// have no way to observe TodoWrite's emission — the canonical
|
|
299
|
+
// store is the transcript, but TaskGuard runs in-loop and can't
|
|
300
|
+
// afford a transcript scan per turn.
|
|
301
|
+
let latestTodos = [];
|
|
302
|
+
const userOnStream = options?.onStream;
|
|
303
|
+
const wrappedOnStream = (event) => {
|
|
304
|
+
if (event.type === "task_update") {
|
|
305
|
+
latestTodos = event.tasks;
|
|
306
|
+
}
|
|
307
|
+
userOnStream?.(event);
|
|
308
|
+
};
|
|
309
|
+
if (options)
|
|
310
|
+
options.onStream = wrappedOnStream;
|
|
311
|
+
// ── P2-6: image input ─────────────────────────────────────────────
|
|
312
|
+
// Parse `<codeshell-image>` blocks out of the raw task string before
|
|
313
|
+
// any other gate looks at it. Two concerns:
|
|
314
|
+
// 1. The noise detector below sees the raw base64 as gibberish and
|
|
315
|
+
// would reject the whole turn — split images out first so it
|
|
316
|
+
// only inspects the prose portion.
|
|
317
|
+
// 2. Models that don't accept vision must be refused immediately,
|
|
318
|
+
// with the image bytes intact for the user to retry on another
|
|
319
|
+
// model. Silent text-only fallback was the failure mode this
|
|
320
|
+
// gate is here to prevent.
|
|
321
|
+
let parsedTask;
|
|
322
|
+
try {
|
|
323
|
+
parsedTask = parseTaskWithImages(task);
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
const msg = err.message;
|
|
327
|
+
logger.warn("engine.run.image_parse_failed", { error: msg });
|
|
328
|
+
return {
|
|
329
|
+
text: `ERROR: image attachment is malformed (${msg}). Drop the image and try again, or re-attach it.`,
|
|
330
|
+
reason: "image_error",
|
|
331
|
+
sessionId: options?.sessionId ?? "image-parse-failed",
|
|
332
|
+
turnCount: 0,
|
|
333
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (parsedTask.hasImages) {
|
|
337
|
+
const cap = capabilitiesFor((this.config.llm.providerKind ?? this.config.llm.provider), this.config.llm.model);
|
|
338
|
+
if (!cap.supportsVision) {
|
|
339
|
+
logger.warn("engine.run.vision_not_supported", {
|
|
340
|
+
provider: this.config.llm.provider,
|
|
341
|
+
model: this.config.llm.model,
|
|
342
|
+
imageCount: parsedTask.images.length,
|
|
343
|
+
});
|
|
344
|
+
return {
|
|
345
|
+
text: `ERROR: model "${this.config.llm.model}" does not accept image input. ` +
|
|
346
|
+
`Switch to a vision-capable model (e.g. gpt-4o, claude-sonnet, gemini-1.5-pro) and resend.`,
|
|
347
|
+
reason: "image_error",
|
|
348
|
+
sessionId: options?.sessionId ?? "vision-not-supported",
|
|
349
|
+
turnCount: 0,
|
|
350
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// For downstream noise-detection + transcript persistence we want the
|
|
355
|
+
// *text* portion only — base64 bytes count as "noise" by the heuristic
|
|
356
|
+
// and would also bloat the transcript by megabytes per image. Image
|
|
357
|
+
// bytes ride in parsedTask.images and re-enter the message tree below.
|
|
358
|
+
const taskText = parsedTask.hasImages ? parsedTask.text : task;
|
|
359
|
+
const noise = detectPastedNoise(taskText);
|
|
360
|
+
if (noise.isNoise) {
|
|
361
|
+
const hint = `Your input looks like pasted terminal output (${noise.reason}). ` +
|
|
362
|
+
`I didn't start a task. What would you like to ask?` +
|
|
363
|
+
(noise.cleaned ? `\n\nExtracted text: ${noise.cleaned.slice(0, 200)}` : "");
|
|
364
|
+
logger.info("engine.run.rejected", { reason: noise.reason, len: task.length });
|
|
365
|
+
return {
|
|
366
|
+
text: hint,
|
|
367
|
+
reason: "completed",
|
|
368
|
+
sessionId: options?.sessionId ?? "noise-rejected",
|
|
369
|
+
turnCount: 0,
|
|
370
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
// Build the per-Engine ToolContext that will be threaded through every
|
|
374
|
+
// tool call. Replaces the old module-level singletons (setAskUserFn,
|
|
375
|
+
// setArenaLLMConfig, setSubAgentConfig, setToolSearchRegistry).
|
|
376
|
+
const subAgentSpawner = {
|
|
377
|
+
parentStream: options?.onStream,
|
|
378
|
+
describe: () => ({
|
|
379
|
+
cwd,
|
|
380
|
+
preset: this.preset.name,
|
|
381
|
+
permissionMode: this.config.permissionMode ?? "acceptEdits",
|
|
382
|
+
}),
|
|
383
|
+
spawn: async (req) => {
|
|
384
|
+
// No nested agents. Strip Agent / AgentStatus / AgentCancel from the
|
|
385
|
+
// child's tool pool so the LLM can't spawn grandchildren — matches
|
|
386
|
+
// Claude Code's ALL_AGENT_DISALLOWED_TOOLS approach. Without this
|
|
387
|
+
// guard a runaway model could fork-bomb sub-agents (token cost +
|
|
388
|
+
// background process explosion), and the sid / approval / dock
|
|
389
|
+
// model assumes a flat parent→children hierarchy. Layered with a
|
|
390
|
+
// runtime check in agent.ts as defense-in-depth.
|
|
391
|
+
const NESTED_AGENT_TOOLS = ["Agent", "AgentStatus", "AgentCancel"];
|
|
392
|
+
const childDisabled = Array.from(new Set([
|
|
393
|
+
...(this.config.disabledBuiltinTools ?? []),
|
|
394
|
+
...NESTED_AGENT_TOOLS,
|
|
395
|
+
]));
|
|
396
|
+
// If enabledBuiltinTools is set (explicit allow-list mode), strip
|
|
397
|
+
// the nested-agent tools from it too so the disable above isn't
|
|
398
|
+
// contradicted by an explicit allow.
|
|
399
|
+
const childEnabled = this.config.enabledBuiltinTools?.filter((t) => !NESTED_AGENT_TOOLS.includes(t));
|
|
400
|
+
const child = new Engine({
|
|
401
|
+
llm: { ...this.config.llm, retryMaxAttempts: 2 },
|
|
402
|
+
cwd,
|
|
403
|
+
permissionMode: this.config.permissionMode,
|
|
404
|
+
preset: this.preset.name,
|
|
405
|
+
enabledBuiltinTools: childEnabled,
|
|
406
|
+
disabledBuiltinTools: childDisabled,
|
|
407
|
+
customSystemPrompt: this.config.customSystemPrompt,
|
|
408
|
+
appendSystemPrompt: this.config.appendSystemPrompt,
|
|
409
|
+
maxTurns: req.maxTurns,
|
|
410
|
+
maxContextTokens: this.config.maxContextTokens ?? 200_000,
|
|
411
|
+
sessionStorageDir: this.config.sessionStorageDir,
|
|
412
|
+
headless: this.config.headless,
|
|
413
|
+
sandbox: this.config.sandbox,
|
|
414
|
+
isSubAgent: true,
|
|
415
|
+
});
|
|
416
|
+
// Where the spawned child Engine's stream events go. AgentTool's
|
|
417
|
+
// background path passes a `streamOverride` (transcriptSink) so the
|
|
418
|
+
// per-event detail is captured into the agent's transcript instead
|
|
419
|
+
// of flooding the main feed. Sync calls leave streamOverride unset
|
|
420
|
+
// and we fall back to the parent UI's onStream so synchronous
|
|
421
|
+
// sub-agents still render inline.
|
|
422
|
+
const destStream = req.streamOverride ?? options?.onStream;
|
|
423
|
+
const childStream = destStream
|
|
424
|
+
? (event) => {
|
|
425
|
+
// Filter ctx-bar signals: the bar tracks the main conversation's
|
|
426
|
+
// prompt size, and a sub-agent's own session emits would clobber
|
|
427
|
+
// it (its sid is new, its messages are tiny, the rough char/4
|
|
428
|
+
// seed lands the bar at <1% mid-turn). Sub-agent token accounting
|
|
429
|
+
// lives in CostTracker (recordUsage), not the ctx bar.
|
|
430
|
+
//
|
|
431
|
+
// - session_started: would seed main ctx with sub-agent's prompt
|
|
432
|
+
// - usage_update: would overwrite main ctx with sub-agent's prompt
|
|
433
|
+
// - context_compact: would reset main ctx to sub-agent's post-compact
|
|
434
|
+
// value AND print a misleading "context compacted" boundary in
|
|
435
|
+
// the main chat (the main session didn't compact).
|
|
436
|
+
if (event.type === "usage_update" ||
|
|
437
|
+
event.type === "session_started" ||
|
|
438
|
+
event.type === "context_compact") {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
destStream({ ...event, agentId: req.agentId });
|
|
442
|
+
}
|
|
443
|
+
: undefined;
|
|
444
|
+
// child.run() establishes its own runWithSid scope internally, so
|
|
445
|
+
// child log lines route to the child's sid and parent's ALS
|
|
446
|
+
// binding is unaffected when control returns here.
|
|
447
|
+
const result = await child.run(req.prompt, { signal: req.signal, onStream: childStream });
|
|
448
|
+
return result.text;
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
const sandboxConfig = this.config.sandbox ??
|
|
452
|
+
defaultSandboxConfig(this.config.headless ? "auto" : "off");
|
|
453
|
+
// A2: explicit sandbox modes (seatbelt, bwrap) must fail closed
|
|
454
|
+
// per standard §S4. resolveSandboxBackend throws when an explicit
|
|
455
|
+
// mode is unavailable on this host; we let it propagate. The
|
|
456
|
+
// previous behavior — catching the throw inside the hot turn and
|
|
457
|
+
// silently downgrading to "off" — was the leak A2 closes. The
|
|
458
|
+
// `auto` mode handles its own downgrade with a one-time warning
|
|
459
|
+
// inside resolveSandboxBackend; explicit modes do not.
|
|
460
|
+
//
|
|
461
|
+
// Backend is cached on EngineRuntime (when available) so the
|
|
462
|
+
// capability probe runs once per (mode, cwd) instead of every turn.
|
|
463
|
+
const sandboxBackend = this.runtime
|
|
464
|
+
? await this.runtime.resolveSandbox(sandboxConfig, cwd)
|
|
465
|
+
: await resolveSandboxBackend(sandboxConfig, cwd);
|
|
466
|
+
// sessionId is filled in after the session bundle is resolved below
|
|
467
|
+
// (the session may be cold-started or resumed). Until then this is
|
|
468
|
+
// intentionally shaped as a mutable local; we treat it as immutable
|
|
469
|
+
// after the assignment.
|
|
470
|
+
const toolCtx = {
|
|
471
|
+
...this.buildToolContext(),
|
|
472
|
+
subAgentSpawner,
|
|
473
|
+
sandbox: sandboxBackend,
|
|
474
|
+
cwd,
|
|
475
|
+
// TodoWrite reads this to push task_update events independently
|
|
476
|
+
// of its return value, so the UI's pinned task panel refreshes
|
|
477
|
+
// immediately rather than after the LLM next surfaces the
|
|
478
|
+
// snapshot. wrappedOnStream snoops the same channel to keep
|
|
479
|
+
// latestTodos current for TaskGuard.
|
|
480
|
+
streamCallback: options?.onStream,
|
|
481
|
+
};
|
|
482
|
+
logger.info("engine.run", {
|
|
483
|
+
task: taskText.slice(0, 200),
|
|
484
|
+
cwd,
|
|
485
|
+
model: this.config.llm.model,
|
|
486
|
+
preset: this.preset.name,
|
|
487
|
+
imageCount: parsedTask.images.length,
|
|
488
|
+
});
|
|
489
|
+
// Compose the user-turn payload once so resume + cold paths agree on
|
|
490
|
+
// shape. With images, content becomes a ContentBlock[] holding one
|
|
491
|
+
// text block (when prose is present) followed by one image block per
|
|
492
|
+
// attachment — the provider-specific clients translate this to OpenAI
|
|
493
|
+
// `image_url` or Anthropic `{type:image, source:base64}` downstream.
|
|
494
|
+
const userMessageContent = parsedTask.hasImages
|
|
495
|
+
? [
|
|
496
|
+
...(parsedTask.text
|
|
497
|
+
? [{ type: "text", text: parsedTask.text }]
|
|
498
|
+
: []),
|
|
499
|
+
...parsedTask.images.map((img) => ({
|
|
500
|
+
type: "image",
|
|
501
|
+
source: {
|
|
502
|
+
type: "base64",
|
|
503
|
+
media_type: img.mime,
|
|
504
|
+
data: img.base64,
|
|
505
|
+
},
|
|
506
|
+
})),
|
|
507
|
+
]
|
|
508
|
+
: taskText;
|
|
509
|
+
// Create or resume session.
|
|
510
|
+
//
|
|
511
|
+
// Three valid shapes:
|
|
512
|
+
// 1. options.sessionId names an EXISTING on-disk session → resume
|
|
513
|
+
// 2. options.sessionId names a fresh sid the host wants materialized
|
|
514
|
+
// (ChatSessionManager's "tui-main" first-turn case) → create
|
|
515
|
+
// with that explicit sid so subsequent turns can resume cleanly
|
|
516
|
+
// 3. no sessionId → create with nanoid
|
|
517
|
+
//
|
|
518
|
+
// Shape (2) was previously broken — engine threw SessionError,
|
|
519
|
+
// surfacing as `[-32603] Session not found: <sid>` on the very first
|
|
520
|
+
// TUI turn. Detection now uses `sessionManager.exists()` (one stat
|
|
521
|
+
// call) instead of a try/catch on resume.
|
|
522
|
+
let session;
|
|
523
|
+
let messages;
|
|
524
|
+
if (options?.sessionId && this.sessionManager.exists(options.sessionId)) {
|
|
525
|
+
session = this.sessionManager.resume(options.sessionId);
|
|
526
|
+
messages = this.compactedMessagesBySession.get(options.sessionId)
|
|
527
|
+
? [...this.compactedMessagesBySession.get(options.sessionId)]
|
|
528
|
+
: session.transcript.toMessages();
|
|
529
|
+
// If the previous run was Ctrl+C'd or crashed between an assistant
|
|
530
|
+
// tool_use and the matching tool_result being persisted, the
|
|
531
|
+
// loaded sequence is invalid for OpenAI (which 400s on dangling
|
|
532
|
+
// tool_calls). Patch synthetic tool_results so the next API call
|
|
533
|
+
// doesn't fail before the turn even starts.
|
|
534
|
+
const patched = patchOrphanedToolUses(messages);
|
|
535
|
+
if (patched.gapsPatched > 0) {
|
|
536
|
+
logger.warn("engine.resume.patched_orphaned_tool_uses", {
|
|
537
|
+
sessionId: options.sessionId,
|
|
538
|
+
gaps: patched.gapsPatched,
|
|
539
|
+
toolResults: patched.toolResultsInjected,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
// Restore cost state from previous session, if the caller injected a store
|
|
543
|
+
if (session.state.costState && this.config.costStore) {
|
|
544
|
+
this.config.costStore.restore(session.state.costState);
|
|
545
|
+
}
|
|
546
|
+
// Append new user message
|
|
547
|
+
const userMsg = { role: "user", content: userMessageContent };
|
|
548
|
+
messages.push(userMsg);
|
|
549
|
+
session.transcript.appendMessage("user", userMessageContent);
|
|
550
|
+
// Flush "active" status to disk immediately. resume() set it in memory
|
|
551
|
+
// (session-manager.ts), but without this write the on-disk state.json
|
|
552
|
+
// still shows the previous run's terminal reason — so any external
|
|
553
|
+
// observer (another CLI process, /sid, the session list) would think
|
|
554
|
+
// the session is still errored/aborted while we're actually running.
|
|
555
|
+
this.sessionManager.saveState(session.state);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
// Cold start: shape (2) reuses the host-supplied sid; shape (3)
|
|
559
|
+
// lets sessionManager generate one with nanoid.
|
|
560
|
+
session = this.sessionManager.create(cwd, this.config.llm.model, this.config.llm.provider, options?.sessionId);
|
|
561
|
+
messages = [{ role: "user", content: userMessageContent }];
|
|
562
|
+
session.transcript.appendMessage("user", userMessageContent);
|
|
563
|
+
// Save first user message as session summary — text only. The summary
|
|
564
|
+
// shows up in the session list; "[image]" is more informative than a
|
|
565
|
+
// truncated `[object Object]` when the prompt was purely visual.
|
|
566
|
+
const summarySrc = parsedTask.hasImages
|
|
567
|
+
? parsedTask.text || `[image${parsedTask.images.length > 1 ? `s × ${parsedTask.images.length}` : ""}]`
|
|
568
|
+
: taskText;
|
|
569
|
+
session.state.summary = summarySrc.slice(0, 80).replace(/\n/g, " ");
|
|
570
|
+
this.sessionManager.saveState(session.state);
|
|
571
|
+
}
|
|
572
|
+
// Stamp the resolved session id for downstream logging.
|
|
573
|
+
//
|
|
574
|
+
// `setCurrentSid` updates the module-level fallback so any code path
|
|
575
|
+
// running outside an ALS scope (bootstrap, /sid before a run starts)
|
|
576
|
+
// still sees the latest sid.
|
|
577
|
+
//
|
|
578
|
+
// The real isolation comes from wrapping the rest of `run` in
|
|
579
|
+
// `runWithSid(sid, async () => { ... })`: every `getCurrentSid()`
|
|
580
|
+
// call inside that closure — including those inside `await`ed child
|
|
581
|
+
// Engine.run() calls — reads sid from this scope's
|
|
582
|
+
// AsyncLocalStorage binding, not the module global. Sibling Engines
|
|
583
|
+
// running concurrently each get their own scope; an `enterSid` inside
|
|
584
|
+
// a child mutates that child's scope only and doesn't leak back to
|
|
585
|
+
// the parent's chain after `await child.run(...)` returns.
|
|
586
|
+
setCurrentSid(session.state.sessionId);
|
|
587
|
+
// B2 / Gate 1: stamp the resolved sid onto the tool context so
|
|
588
|
+
// session-scoped side effects (background-agent completion
|
|
589
|
+
// notifications) attribute to the right session. toolCtx is created
|
|
590
|
+
// before the session bundle is resolved (see ~line 635), so this is
|
|
591
|
+
// the first point we can set it. After this assignment treat the
|
|
592
|
+
// field as immutable for the rest of the run.
|
|
593
|
+
toolCtx.sessionId = session.state.sessionId;
|
|
594
|
+
return runWithSid(session.state.sessionId, async () => {
|
|
595
|
+
recordSessionStart(session.state.sessionId, {
|
|
596
|
+
// Strip <codeshell-image> base64 payloads before they reach
|
|
597
|
+
// <repo>/log/. Reader still sees the marker + byte count, just
|
|
598
|
+
// not the bytes. Transcript persistence keeps the full payload.
|
|
599
|
+
task: sanitizeTaskString(task),
|
|
600
|
+
cwd,
|
|
601
|
+
model: this.config.llm.model,
|
|
602
|
+
provider: this.config.llm.provider,
|
|
603
|
+
permissionMode: this.config.permissionMode ?? "acceptEdits",
|
|
604
|
+
resumed: !!options?.sessionId,
|
|
605
|
+
});
|
|
606
|
+
// Session-level hook: fired once per Engine.run() entry, regardless of
|
|
607
|
+
// cold-start vs resume. Handlers can return `messages` to inject a
|
|
608
|
+
// <system-reminder> at the head of the conversation (between
|
|
609
|
+
// userContext and the new user prompt). Used by the built-in
|
|
610
|
+
// superpowers injector to surface the `using-superpowers` ruleset.
|
|
611
|
+
const sessionStartHook = await this.emitHook("on_session_start", {
|
|
612
|
+
sessionId: session.state.sessionId,
|
|
613
|
+
cwd,
|
|
614
|
+
resumed: !!options?.sessionId,
|
|
615
|
+
});
|
|
616
|
+
// Per-turn hook: fired every time a new user prompt enters the loop.
|
|
617
|
+
// Equivalent to CC's UserPromptSubmit. Handlers can inject lightweight
|
|
618
|
+
// reminders that should accompany each user turn (e.g. "skills
|
|
619
|
+
// available — check before acting").
|
|
620
|
+
const promptSubmitHook = await this.emitHook("user_prompt_submit", {
|
|
621
|
+
sessionId: session.state.sessionId,
|
|
622
|
+
// Pass the text-only portion. Handlers reading the prompt for keyword
|
|
623
|
+
// detection / classification (e.g. superpowers' "did the user ask
|
|
624
|
+
// about X?") don't gain anything from megabytes of base64 inlined here,
|
|
625
|
+
// and silently leaking attachment bytes through hooks is the kind of
|
|
626
|
+
// exfiltration risk a curious user-installed shell hook shouldn't carry.
|
|
627
|
+
prompt: taskText,
|
|
628
|
+
resumed: !!options?.sessionId,
|
|
629
|
+
});
|
|
630
|
+
// updatedPrompt: handler rewrote the user's prompt text. Replace the
|
|
631
|
+
// last user message we just pushed (cold-start: line ~511; resume:
|
|
632
|
+
// line ~500). Original prompt is in the transcript already — we log
|
|
633
|
+
// the rewrite so audit chains know a hook touched user input.
|
|
634
|
+
if (typeof promptSubmitHook.updatedPrompt === "string") {
|
|
635
|
+
const lastIdx = messages.length - 1;
|
|
636
|
+
const last = messages[lastIdx];
|
|
637
|
+
if (last && last.role === "user" && typeof last.content === "string") {
|
|
638
|
+
logger.info("hook.updated_prompt", {
|
|
639
|
+
sessionId: session.state.sessionId,
|
|
640
|
+
originalChars: last.content.length,
|
|
641
|
+
updatedChars: promptSubmitHook.updatedPrompt.length,
|
|
642
|
+
});
|
|
643
|
+
messages[lastIdx] = { role: "user", content: promptSubmitHook.updatedPrompt };
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// Rough token estimate of the full prompt so the UI's ctx bar isn't 0%
|
|
647
|
+
// before the first real usage_update arrives. The authoritative count
|
|
648
|
+
// comes from `usage.promptTokens` after the first LLM response — this is
|
|
649
|
+
// just a display-friendly approximation for the first frame.
|
|
650
|
+
//
|
|
651
|
+
// Only seed once per (process, sid). On subsequent turns the UI already
|
|
652
|
+
// shows the previous turn's accurate ctx; overwriting it with this rough
|
|
653
|
+
// char/4 estimate would make the bar visibly drop on every submit.
|
|
654
|
+
const sid = session.state.sessionId;
|
|
655
|
+
const needsCtxSeed = !this.ctxSeedSent.has(sid);
|
|
656
|
+
const roughPromptTokens = needsCtxSeed
|
|
657
|
+
? messages.reduce((sum, m) => {
|
|
658
|
+
const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
|
|
659
|
+
return sum + Math.ceil(text.length / 4);
|
|
660
|
+
}, 0)
|
|
661
|
+
: 0;
|
|
662
|
+
if (needsCtxSeed)
|
|
663
|
+
this.ctxSeedSent.add(sid);
|
|
664
|
+
// Tell the client the sid *now* instead of waiting for run() to resolve.
|
|
665
|
+
// The user wants `/sid` to work mid-turn; without this, the client only
|
|
666
|
+
// learns the sid when the run completes.
|
|
667
|
+
options?.onStream?.({
|
|
668
|
+
type: "session_started",
|
|
669
|
+
sessionId: sid,
|
|
670
|
+
promptTokens: roughPromptTokens,
|
|
671
|
+
});
|
|
672
|
+
// Replay the last TodoWrite snapshot on resume so the UI's pinned
|
|
673
|
+
// task panel re-hydrates without the LLM needing to call TodoWrite
|
|
674
|
+
// again. Scans the resumed transcript newest-first (and tolerates
|
|
675
|
+
// legacy TaskCreate/Update events for sessions recorded against
|
|
676
|
+
// the pre-2026-05-24 API). New sessions have no transcript yet so
|
|
677
|
+
// readLastTodoSnapshot returns null and nothing is emitted.
|
|
678
|
+
if (options?.sessionId) {
|
|
679
|
+
const snap = readLastTodoSnapshot(session.transcript.getEvents());
|
|
680
|
+
if (snap && snap.length > 0) {
|
|
681
|
+
latestTodos = snap;
|
|
682
|
+
options?.onStream?.({ type: "task_update", tasks: snap });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// Kick off LLM client creation early (network handshake)
|
|
686
|
+
const llmClientPromise = createLLMClient(this.config.llm);
|
|
687
|
+
const mode = this.config.permissionMode ?? "acceptEdits";
|
|
688
|
+
const { rules: defaultRules, backend: approvalBackend } = this.buildPermissionConfig(mode, cwd);
|
|
689
|
+
const permission = new PermissionClassifier(defaultRules, mode, approvalBackend);
|
|
690
|
+
this.activePermission = permission;
|
|
691
|
+
// If the backend is the interactive one, wire it for project-scope
|
|
692
|
+
// persistence: it needs cwd to find settings.local.json, and a callback
|
|
693
|
+
// to apply newly-saved rules to the live classifier so subsequent calls
|
|
694
|
+
// in this same session don't re-prompt. Headless/auto backends skip
|
|
695
|
+
// this — they don't prompt, so there are no project rules to persist.
|
|
696
|
+
if (approvalBackend instanceof InteractiveApprovalBackend) {
|
|
697
|
+
approvalBackend.setCwd(cwd);
|
|
698
|
+
approvalBackend.setOnProjectRules((rules) => {
|
|
699
|
+
// Prepend the *full* accumulated list of session-saved project rules
|
|
700
|
+
// so user approvals win over defaults and earlier approvals aren't
|
|
701
|
+
// dropped when later ones come in.
|
|
702
|
+
permission.reconfigure(mode, approvalBackend, [...rules, ...defaultRules]);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
const toolExecutor = new ToolExecutor(this.toolRegistry, permission, this.hooks);
|
|
706
|
+
const investigationGuard = new InvestigationGuard();
|
|
707
|
+
if (this.config.headless)
|
|
708
|
+
investigationGuard.setSoftMode(true);
|
|
709
|
+
toolExecutor.setInvestigationGuard(investigationGuard);
|
|
710
|
+
toolExecutor.setTaskGuard(new TaskGuard(() => latestTodos));
|
|
711
|
+
// Wire abort signal for cascading cancellation + per-Engine ToolContext
|
|
712
|
+
toolExecutor.setSignal(options?.signal);
|
|
713
|
+
toolExecutor.setContext(toolCtx);
|
|
714
|
+
const contextManager = new ContextManager({
|
|
715
|
+
maxTokens: this.resolveMaxContextTokens(),
|
|
716
|
+
});
|
|
717
|
+
this.lastContextManager = contextManager;
|
|
718
|
+
const promptComposer = new PromptComposer({
|
|
719
|
+
cwd,
|
|
720
|
+
model: this.config.llm.model,
|
|
721
|
+
preset: this.preset,
|
|
722
|
+
customSystemPrompt: this.config.customSystemPrompt,
|
|
723
|
+
appendSystemPrompt: this.config.appendSystemPrompt,
|
|
724
|
+
disabledSkills: this.readDisabledSkills(),
|
|
725
|
+
});
|
|
726
|
+
// Connect MCP servers (if configured and not already connected).
|
|
727
|
+
// B1: prefer the Runtime-owned MCPManager so all sessions in a
|
|
728
|
+
// worker share one set of connections. Falling back to a
|
|
729
|
+
// per-Engine instance keeps the null-runtime path (tests, ad-hoc
|
|
730
|
+
// scripts) working.
|
|
731
|
+
const mcpServers = this.config.mcpServers ?? {};
|
|
732
|
+
if (Object.keys(mcpServers).length > 0 && !this.mcpManager) {
|
|
733
|
+
if (this.runtime) {
|
|
734
|
+
this.mcpManager = this.runtime.mcpPool;
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
this.mcpManager = new MCPManager(this.toolRegistry);
|
|
738
|
+
}
|
|
739
|
+
await this.mcpManager.connectAll(mcpServers);
|
|
740
|
+
}
|
|
741
|
+
// Parallelize slow initialization:
|
|
742
|
+
// 1. createLLMClient — network handshake (started earlier)
|
|
743
|
+
// 2. buildSystemPrompt — includes git status (3 execSync calls)
|
|
744
|
+
// 3. buildSystemContext — reads environment context
|
|
745
|
+
const allToolDefs = this.toolRegistry.getToolDefinitions();
|
|
746
|
+
// In plan mode, only expose read-only tools so the model won't attempt writes
|
|
747
|
+
const planModeAllowed = new Set([
|
|
748
|
+
"EnterPlanMode",
|
|
749
|
+
"ExitPlanMode",
|
|
750
|
+
"Read",
|
|
751
|
+
"Glob",
|
|
752
|
+
"Grep",
|
|
753
|
+
"WebSearch",
|
|
754
|
+
"WebFetch",
|
|
755
|
+
"AskUserQuestion",
|
|
756
|
+
"Agent",
|
|
757
|
+
"ToolSearch",
|
|
758
|
+
"TaskCreate",
|
|
759
|
+
"TaskUpdate",
|
|
760
|
+
"TaskList",
|
|
761
|
+
"TaskGet",
|
|
762
|
+
"Bash", // Bash is included but executor filters non-read-only commands
|
|
763
|
+
]);
|
|
764
|
+
const toolDefs = this.planMode
|
|
765
|
+
? allToolDefs.filter((t) => planModeAllowed.has(t.name))
|
|
766
|
+
: allToolDefs;
|
|
767
|
+
const [llmClient, systemPrompt, systemContext] = await Promise.all([
|
|
768
|
+
llmClientPromise,
|
|
769
|
+
promptComposer.buildSystemPrompt(toolDefs),
|
|
770
|
+
promptComposer.buildSystemContext(),
|
|
771
|
+
]);
|
|
772
|
+
const fullSystemPrompt = [systemPrompt, systemContext].filter(Boolean).join("\n\n");
|
|
773
|
+
// Prepend userContext (CLAUDE.md) as first message (sync, fast)
|
|
774
|
+
const userContextMsg = promptComposer.buildUserContextMessage();
|
|
775
|
+
if (userContextMsg) {
|
|
776
|
+
messages.unshift(userContextMsg);
|
|
777
|
+
}
|
|
778
|
+
// Inject hook-supplied reminders just before the most recent user task.
|
|
779
|
+
// Combined into one <system-reminder> block so a noisy handler chain
|
|
780
|
+
// doesn't turn into 3+ separate user turns in the API request.
|
|
781
|
+
const lifecycleReminder = wrapHookMessages([
|
|
782
|
+
...(sessionStartHook.messages ?? []),
|
|
783
|
+
...(promptSubmitHook.messages ?? []),
|
|
784
|
+
]);
|
|
785
|
+
if (lifecycleReminder) {
|
|
786
|
+
// messages[length - 1] is the user task we just pushed above. Insert
|
|
787
|
+
// the reminder immediately before it so the model reads: CLAUDE.md →
|
|
788
|
+
// reminder → user request.
|
|
789
|
+
messages.splice(messages.length - 1, 0, lifecycleReminder);
|
|
790
|
+
}
|
|
791
|
+
this.lastSessionId = session.state.sessionId;
|
|
792
|
+
this.lastMessages = messages;
|
|
793
|
+
// Wire up LLM summarization for context compaction
|
|
794
|
+
// Uses a lightweight call without tools
|
|
795
|
+
contextManager.setTranscriptPath(session.transcript.getFilePath());
|
|
796
|
+
// Re-derive frozen persistence decisions from the messages we just
|
|
797
|
+
// loaded. Skipped on cold start (messages == [userContextMsg] only).
|
|
798
|
+
// Critical for resume — otherwise a result that was persisted last
|
|
799
|
+
// run would be evaluated fresh and might get a different replacement
|
|
800
|
+
// string than the one already in the message, breaking idempotency.
|
|
801
|
+
contextManager.initReplacementStateFromMessages(messages);
|
|
802
|
+
contextManager.setSummarizeFn(async (prompt) => {
|
|
803
|
+
const summaryResponse = await llmClient.createMessage({
|
|
804
|
+
systemPrompt: "You are a conversation summarizer. Be concise and factual.",
|
|
805
|
+
messages: [{ role: "user", content: prompt }],
|
|
806
|
+
tools: [],
|
|
807
|
+
maxTokens: 1024,
|
|
808
|
+
// Auxiliary call — no need to burn reasoning tokens. On DeepSeek V4
|
|
809
|
+
// this flips thinking off (~3x faster, fewer tokens); on every other
|
|
810
|
+
// OpenAI-compatible provider the field is ignored.
|
|
811
|
+
thinking: "disabled",
|
|
812
|
+
});
|
|
813
|
+
return summaryResponse.text;
|
|
814
|
+
});
|
|
815
|
+
// Create components (requires resolved llmClient)
|
|
816
|
+
const modelFacade = new ModelFacade(llmClient, session.transcript);
|
|
817
|
+
// Wire getOutputTokens for token budget tracking
|
|
818
|
+
modelFacade.getOutputTokens = () => {
|
|
819
|
+
const usage = llmClient.getUsage();
|
|
820
|
+
return usage.totalCompletionTokens;
|
|
821
|
+
};
|
|
822
|
+
// Wire summarize for tool use summaries (uses lightweight call).
|
|
823
|
+
// recordUsage=false keeps these auxiliary sub-calls out of the main usage
|
|
824
|
+
// tracker so session_end.cost reflects only the user-facing turns and
|
|
825
|
+
// turns/requestCount stay aligned.
|
|
826
|
+
modelFacade.summarize = async (sysPrompt, userMsg) => {
|
|
827
|
+
const resp = await llmClient.createMessage({
|
|
828
|
+
systemPrompt: sysPrompt,
|
|
829
|
+
messages: [{ role: "user", content: userMsg }],
|
|
830
|
+
tools: [],
|
|
831
|
+
maxTokens: 256,
|
|
832
|
+
recordUsage: false,
|
|
833
|
+
// Auxiliary call — see contextManager.setSummarizeFn above.
|
|
834
|
+
thinking: "disabled",
|
|
835
|
+
});
|
|
836
|
+
logger.debug("summarize.call", {
|
|
837
|
+
sysPromptLen: sysPrompt.length,
|
|
838
|
+
userMsgLen: userMsg.length,
|
|
839
|
+
userMsgPreview: userMsg.slice(0, 300),
|
|
840
|
+
completionLen: resp.text.length,
|
|
841
|
+
completionPreview: resp.text.slice(0, 300),
|
|
842
|
+
stopReason: resp.stopReason,
|
|
843
|
+
promptTokens: resp.usage?.promptTokens,
|
|
844
|
+
completionTokens: resp.usage?.completionTokens,
|
|
845
|
+
});
|
|
846
|
+
return resp.text;
|
|
847
|
+
};
|
|
848
|
+
// File history: auto-backup before Write/Edit
|
|
849
|
+
const sessionDir = join(this.config.sessionStorageDir ?? join(homedir(), ".code-shell", "sessions"), session.state.sessionId);
|
|
850
|
+
const fileHistory = FileHistory.loadFromDir(sessionDir);
|
|
851
|
+
this.hooks.register("on_tool_start", async (context) => {
|
|
852
|
+
const toolName = context.data?.toolName;
|
|
853
|
+
const args = context.data?.args;
|
|
854
|
+
if ((toolName === "Write" || toolName === "Edit") && args?.file_path) {
|
|
855
|
+
fileHistory.saveSnapshot(args.file_path);
|
|
856
|
+
}
|
|
857
|
+
return {};
|
|
858
|
+
}, 100, "file_history_backup");
|
|
859
|
+
// Hook: agent start
|
|
860
|
+
await this.emitHook("on_agent_start", {
|
|
861
|
+
sessionId: session.state.sessionId,
|
|
862
|
+
task,
|
|
863
|
+
model: this.config.llm.model,
|
|
864
|
+
});
|
|
865
|
+
// Surface compaction events to the UI so the user knows when context was trimmed.
|
|
866
|
+
// Buffer the most recent event so TurnLoop can drain it and emit the
|
|
867
|
+
// post_compact hook on the next turn (ContextManager itself doesn't
|
|
868
|
+
// know about HookRegistry — the buffer is the seam).
|
|
869
|
+
let pendingCompactInfo = null;
|
|
870
|
+
contextManager.setOnCompact((info) => {
|
|
871
|
+
pendingCompactInfo = info;
|
|
872
|
+
options?.onStream?.({ type: "context_compact", ...info });
|
|
873
|
+
});
|
|
874
|
+
// Run turn loop
|
|
875
|
+
const turnLoop = new TurnLoop({
|
|
876
|
+
model: modelFacade,
|
|
877
|
+
toolExecutor,
|
|
878
|
+
contextManager,
|
|
879
|
+
hooks: this.hooks,
|
|
880
|
+
transcript: session.transcript,
|
|
881
|
+
systemPrompt: fullSystemPrompt,
|
|
882
|
+
tools: toolDefs,
|
|
883
|
+
sessionId: sid,
|
|
884
|
+
isSubAgent: this.config.isSubAgent === true,
|
|
885
|
+
consumePendingCompactInfo: () => {
|
|
886
|
+
const info = pendingCompactInfo;
|
|
887
|
+
pendingCompactInfo = null;
|
|
888
|
+
return info;
|
|
889
|
+
},
|
|
890
|
+
ctxOverheadStore: {
|
|
891
|
+
get: (s) => this.ctxOverheadBySid.get(s) ?? 0,
|
|
892
|
+
set: (s, n) => {
|
|
893
|
+
this.ctxOverheadBySid.set(s, n);
|
|
894
|
+
},
|
|
895
|
+
},
|
|
896
|
+
}, {
|
|
897
|
+
maxTurns: this.config.maxTurns ?? 100,
|
|
898
|
+
maxToolCallsPerTurn: this.config.maxToolCallsPerTurn ?? 10,
|
|
899
|
+
onStream: options?.onStream,
|
|
900
|
+
signal: options?.signal,
|
|
901
|
+
// Heartbeat: flush turnCount + tokens to state.json after every turn
|
|
902
|
+
// so external observers (other CLI processes, /sid, the session list)
|
|
903
|
+
// see live progress instead of a stale snapshot from the last
|
|
904
|
+
// completed run.
|
|
905
|
+
onTurnBoundary: (turnCount) => {
|
|
906
|
+
session.state.turnCount = turnCount;
|
|
907
|
+
const u = modelFacade.getUsage();
|
|
908
|
+
session.state.tokenUsage = {
|
|
909
|
+
promptTokens: u.totalPromptTokens,
|
|
910
|
+
completionTokens: u.totalCompletionTokens,
|
|
911
|
+
totalTokens: u.totalTokens,
|
|
912
|
+
};
|
|
913
|
+
if (this.config.costStore) {
|
|
914
|
+
session.state.costState = this.config.costStore.serialize();
|
|
915
|
+
}
|
|
916
|
+
this.sessionManager.saveState(session.state);
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
const result = await turnLoop.run(messages);
|
|
920
|
+
this.lastMessages = result.messages;
|
|
921
|
+
this.compactedMessagesBySession.set(session.state.sessionId, this.stripUserContextMessage(result.messages, userContextMsg));
|
|
922
|
+
logger.info("engine.done", {
|
|
923
|
+
sessionId: session.state.sessionId,
|
|
924
|
+
reason: result.reason,
|
|
925
|
+
turns: turnLoop.currentTurn,
|
|
926
|
+
tokens: modelFacade.getUsage().totalTokens,
|
|
927
|
+
});
|
|
928
|
+
recordSessionEnd(session.state.sessionId, {
|
|
929
|
+
reason: result.reason,
|
|
930
|
+
turns: turnLoop.currentTurn,
|
|
931
|
+
cost: modelFacade.getUsage(),
|
|
932
|
+
});
|
|
933
|
+
// Session-level hook: fired symmetrically with on_session_start once
|
|
934
|
+
// the turn loop has resolved (completion, error, or abort). Handlers
|
|
935
|
+
// are notify-only — any returned messages are dropped because the run
|
|
936
|
+
// is already over and there's no next turn to inject into.
|
|
937
|
+
await this.emitHook("on_session_end", {
|
|
938
|
+
sessionId: session.state.sessionId,
|
|
939
|
+
reason: result.reason,
|
|
940
|
+
turnCount: turnLoop.currentTurn,
|
|
941
|
+
});
|
|
942
|
+
// Fire-and-forget memory pipeline: extract durable memories from the
|
|
943
|
+
// transcript, save a session summary, and conditionally trigger
|
|
944
|
+
// auto-dream consolidation. Doesn't block the Engine result.
|
|
945
|
+
void this.runMemoryPipeline(session.transcript, session.state.sessionId, cwd, llmClient);
|
|
946
|
+
// Update session state. Persist the raw terminal reason as the status so
|
|
947
|
+
// callers can distinguish user-cancelled (aborted_streaming) from real
|
|
948
|
+
// failures (model_error, prompt_too_long, ...) — previously every
|
|
949
|
+
// non-completed outcome collapsed to "errored", which threw away the
|
|
950
|
+
// distinction and misled anyone reading state.json.
|
|
951
|
+
session.state.turnCount = turnLoop.currentTurn;
|
|
952
|
+
session.state.status = result.reason;
|
|
953
|
+
const usage = modelFacade.getUsage();
|
|
954
|
+
session.state.tokenUsage = {
|
|
955
|
+
promptTokens: usage.totalPromptTokens,
|
|
956
|
+
completionTokens: usage.totalCompletionTokens,
|
|
957
|
+
totalTokens: usage.totalTokens,
|
|
958
|
+
};
|
|
959
|
+
if (this.config.costStore) {
|
|
960
|
+
session.state.costState = this.config.costStore.serialize();
|
|
961
|
+
}
|
|
962
|
+
this.sessionManager.saveState(session.state);
|
|
963
|
+
// Hook: agent end
|
|
964
|
+
await this.emitHook("on_agent_end", {
|
|
965
|
+
sessionId: session.state.sessionId,
|
|
966
|
+
reason: result.reason,
|
|
967
|
+
turnCount: turnLoop.currentTurn,
|
|
968
|
+
});
|
|
969
|
+
// Emit completion
|
|
970
|
+
options?.onStream?.({ type: "turn_complete", reason: result.reason });
|
|
971
|
+
return {
|
|
972
|
+
text: result.text,
|
|
973
|
+
reason: result.reason,
|
|
974
|
+
sessionId: session.state.sessionId,
|
|
975
|
+
turnCount: turnLoop.currentTurn,
|
|
976
|
+
usage: {
|
|
977
|
+
promptTokens: usage.totalPromptTokens,
|
|
978
|
+
completionTokens: usage.totalCompletionTokens,
|
|
979
|
+
totalTokens: usage.totalTokens,
|
|
980
|
+
},
|
|
981
|
+
};
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Run the end-of-session memory pipeline as a fire-and-forget background
|
|
986
|
+
* task. Extracts durable memories from the transcript, saves a session
|
|
987
|
+
* summary, and conditionally triggers auto-dream consolidation.
|
|
988
|
+
*/
|
|
989
|
+
async runMemoryPipeline(transcript, sessionId, cwd, llmClient) {
|
|
990
|
+
try {
|
|
991
|
+
// Only run memory extraction for substantive sessions. The previous
|
|
992
|
+
// threshold of 4 user+assistant messages was low enough that two-line
|
|
993
|
+
// exchanges ("what's the time?" / "noon") triggered a full LLM
|
|
994
|
+
// extraction, which then padded the memory store with low-signal
|
|
995
|
+
// entries. 8 messages is roughly "more than a single back-and-forth"
|
|
996
|
+
// — substantive enough to be worth a durable note.
|
|
997
|
+
const messages = transcript.toMessages().filter((m) => m.role === "user" || m.role === "assistant");
|
|
998
|
+
if (messages.length < 8)
|
|
999
|
+
return;
|
|
1000
|
+
// Memory orchestrator + dream-loop calls are auxiliary LLM calls
|
|
1001
|
+
// that don't (and shouldn't) carry image payloads. Sanitize before
|
|
1002
|
+
// stringify so we don't pump a 10 MB base64 string into the
|
|
1003
|
+
// summarization prompt — provider 400s on it, and it leaks bytes
|
|
1004
|
+
// into a downstream cost-tracking path we don't audit as carefully
|
|
1005
|
+
// as the primary turn.
|
|
1006
|
+
const plainMessages = messages.map((m) => {
|
|
1007
|
+
const safe = sanitizeContent(m.content);
|
|
1008
|
+
return {
|
|
1009
|
+
role: m.role,
|
|
1010
|
+
content: typeof safe === "string" ? safe : JSON.stringify(safe),
|
|
1011
|
+
};
|
|
1012
|
+
});
|
|
1013
|
+
const orchestrator = new MemoryOrchestrator({
|
|
1014
|
+
callLLM: async (sysPrompt, userMsg) => {
|
|
1015
|
+
// Use a lightweight auxiliary call (no tools, no streaming, no
|
|
1016
|
+
// reasoning tokens).
|
|
1017
|
+
const resp = await llmClient.createMessage({
|
|
1018
|
+
systemPrompt: sysPrompt,
|
|
1019
|
+
messages: [{ role: "user", content: userMsg }],
|
|
1020
|
+
tools: [],
|
|
1021
|
+
maxTokens: 1024,
|
|
1022
|
+
recordUsage: false,
|
|
1023
|
+
thinking: "disabled",
|
|
1024
|
+
});
|
|
1025
|
+
return resp.text;
|
|
1026
|
+
},
|
|
1027
|
+
runDream: async ({ systemPrompt, userPrompt, projectDir }) => this.runDreamLoop({ systemPrompt, userPrompt, projectDir, llmClient, sessionId }),
|
|
1028
|
+
projectDir: cwd,
|
|
1029
|
+
});
|
|
1030
|
+
await orchestrator.run(plainMessages, sessionId);
|
|
1031
|
+
}
|
|
1032
|
+
catch (err) {
|
|
1033
|
+
// Memory pipeline is best-effort — never surface errors to the user.
|
|
1034
|
+
logger.warn("engine.memory_pipeline_failed", {
|
|
1035
|
+
sessionId,
|
|
1036
|
+
error: err.message,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Drive the auto-dream tool-call loop.
|
|
1042
|
+
*
|
|
1043
|
+
* Runs the LLM with a whitelisted subset of memory tools (MemoryList,
|
|
1044
|
+
* MemoryRead, MemorySave, MemoryDelete). The loop is intentionally small
|
|
1045
|
+
* and offline:
|
|
1046
|
+
* - No streaming, no UI events — runs in the background after a session.
|
|
1047
|
+
* - No permission prompts — UI isn't attached, so we hard-reject any
|
|
1048
|
+
* attempt to Save/Delete in the "user" scope before dispatching. Dream
|
|
1049
|
+
* scope is the LLM's workspace and goes through freely.
|
|
1050
|
+
* - Capped at MAX_TURNS LLM round-trips and MAX_WRITES total
|
|
1051
|
+
* mutations to bound damage on misbehavior.
|
|
1052
|
+
*
|
|
1053
|
+
* Returns true if the loop ran (with or without writes); false if we
|
|
1054
|
+
* bailed before the first LLM call (e.g. registry missing the tools).
|
|
1055
|
+
*/
|
|
1056
|
+
async runDreamLoop(opts) {
|
|
1057
|
+
const MAX_TURNS = 8;
|
|
1058
|
+
const MAX_WRITES = 10;
|
|
1059
|
+
const MEMORY_TOOL_NAMES = ["MemoryList", "MemoryRead", "MemorySave", "MemoryDelete"];
|
|
1060
|
+
const memoryTools = MEMORY_TOOL_NAMES
|
|
1061
|
+
.map((n) => this.toolRegistry.getTool(n))
|
|
1062
|
+
.filter((t) => t != null);
|
|
1063
|
+
if (memoryTools.length < MEMORY_TOOL_NAMES.length) {
|
|
1064
|
+
logger.warn("memory.auto_dream_missing_tools", {
|
|
1065
|
+
sessionId: opts.sessionId,
|
|
1066
|
+
found: memoryTools.map((t) => t.name),
|
|
1067
|
+
});
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
1070
|
+
// Strip RegisteredTool down to the shape createMessage expects.
|
|
1071
|
+
const toolDefs = memoryTools.map((t) => ({
|
|
1072
|
+
name: t.name,
|
|
1073
|
+
description: t.description,
|
|
1074
|
+
inputSchema: t.inputSchema,
|
|
1075
|
+
}));
|
|
1076
|
+
const toolCtx = {
|
|
1077
|
+
...this.buildToolContext(),
|
|
1078
|
+
cwd: opts.projectDir ?? process.cwd(),
|
|
1079
|
+
};
|
|
1080
|
+
const messages = [{ role: "user", content: opts.userPrompt }];
|
|
1081
|
+
let writeBudget = MAX_WRITES;
|
|
1082
|
+
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
1083
|
+
const resp = await opts.llmClient.createMessage({
|
|
1084
|
+
systemPrompt: opts.systemPrompt,
|
|
1085
|
+
messages,
|
|
1086
|
+
tools: toolDefs,
|
|
1087
|
+
maxTokens: 2048,
|
|
1088
|
+
recordUsage: false,
|
|
1089
|
+
thinking: "disabled",
|
|
1090
|
+
});
|
|
1091
|
+
if (resp.toolCalls.length === 0) {
|
|
1092
|
+
logger.info("memory.auto_dream_finished", {
|
|
1093
|
+
sessionId: opts.sessionId,
|
|
1094
|
+
turn,
|
|
1095
|
+
finalText: resp.text.slice(0, 500),
|
|
1096
|
+
});
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
// Echo the assistant turn back into the conversation so subsequent
|
|
1100
|
+
// turns see the tool_use ids they need to reference.
|
|
1101
|
+
const assistantContent = [];
|
|
1102
|
+
if (resp.text)
|
|
1103
|
+
assistantContent.push({ type: "text", text: resp.text });
|
|
1104
|
+
for (const tc of resp.toolCalls) {
|
|
1105
|
+
assistantContent.push({
|
|
1106
|
+
type: "tool_use",
|
|
1107
|
+
id: tc.id,
|
|
1108
|
+
name: tc.toolName,
|
|
1109
|
+
input: tc.args,
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
messages.push({ role: "assistant", content: assistantContent });
|
|
1113
|
+
// Dispatch every tool call requested in this turn.
|
|
1114
|
+
const toolResults = [];
|
|
1115
|
+
for (const tc of resp.toolCalls) {
|
|
1116
|
+
const result = await this.dispatchDreamTool(tc, toolCtx, () => {
|
|
1117
|
+
if (writeBudget <= 0)
|
|
1118
|
+
return false;
|
|
1119
|
+
writeBudget--;
|
|
1120
|
+
return true;
|
|
1121
|
+
});
|
|
1122
|
+
toolResults.push({
|
|
1123
|
+
type: "tool_result",
|
|
1124
|
+
tool_use_id: tc.id,
|
|
1125
|
+
content: result,
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
messages.push({ role: "user", content: toolResults });
|
|
1129
|
+
}
|
|
1130
|
+
logger.warn("memory.auto_dream_hit_turn_cap", {
|
|
1131
|
+
sessionId: opts.sessionId,
|
|
1132
|
+
maxTurns: MAX_TURNS,
|
|
1133
|
+
});
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Execute one memory tool call inside the dream loop. Enforces the two
|
|
1138
|
+
* dream-loop invariants the prompt also states:
|
|
1139
|
+
* - Only the 4 memory tools are dispatchable.
|
|
1140
|
+
* - Save/Delete in "user" scope is refused (returned as a tool error)
|
|
1141
|
+
* because dream runs without an interactive permission backend.
|
|
1142
|
+
*/
|
|
1143
|
+
async dispatchDreamTool(tc, ctx, consumeWriteBudget) {
|
|
1144
|
+
const allowed = new Set(["MemoryList", "MemoryRead", "MemorySave", "MemoryDelete"]);
|
|
1145
|
+
if (!allowed.has(tc.toolName)) {
|
|
1146
|
+
return `Error: tool "${tc.toolName}" is not available in the dream loop`;
|
|
1147
|
+
}
|
|
1148
|
+
const isWrite = tc.toolName === "MemorySave" || tc.toolName === "MemoryDelete";
|
|
1149
|
+
if (isWrite) {
|
|
1150
|
+
const scope = tc.args?.scope;
|
|
1151
|
+
if (scope !== "dream") {
|
|
1152
|
+
return (`Error: dream loop may only write to scope "dream", got "${scope}". ` +
|
|
1153
|
+
`User-scope changes require interactive permission, which is not available here.`);
|
|
1154
|
+
}
|
|
1155
|
+
if (!consumeWriteBudget()) {
|
|
1156
|
+
return "Error: dream write budget exhausted — stop calling write tools and summarize instead.";
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
const result = await this.toolRegistry.executeTool(tc.toolName, tc.args, { ctx });
|
|
1161
|
+
if (result.isError)
|
|
1162
|
+
return result.error ?? `Error executing ${tc.toolName}`;
|
|
1163
|
+
return result.result ?? "";
|
|
1164
|
+
}
|
|
1165
|
+
catch (err) {
|
|
1166
|
+
return `Error executing ${tc.toolName}: ${err.message}`;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
getToolRegistry() {
|
|
1170
|
+
return this.toolRegistry;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Switch the active model by pool key. Takes effect on the next run() call.
|
|
1174
|
+
* Returns the new model entry.
|
|
1175
|
+
*
|
|
1176
|
+
* Persists settings.activeKey (and a legacy settings.model.* mirror) so the
|
|
1177
|
+
* next process startup defaults to the same model — without this, switches
|
|
1178
|
+
* only live in memory and every restart reverts to the previously persisted
|
|
1179
|
+
* activeKey.
|
|
1180
|
+
*/
|
|
1181
|
+
switchModel(key) {
|
|
1182
|
+
const entry = this.modelPool.switch(key);
|
|
1183
|
+
const nextLlm = this.modelPool.toLLMConfig(entry, this.config.llm);
|
|
1184
|
+
this.config = { ...this.config, llm: nextLlm };
|
|
1185
|
+
this.persistActiveModel(entry, nextLlm);
|
|
1186
|
+
return entry;
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Write the active model selection to ~/.code-shell/settings.json.
|
|
1190
|
+
*
|
|
1191
|
+
* We mirror into the legacy settings.model.* block (provider/name/apiKey/
|
|
1192
|
+
* baseUrl) because boot paths in cli/main.ts, repl.ts, run.ts still read it.
|
|
1193
|
+
* The mirror uses resolved llm values (not raw entry.*) so credentials that
|
|
1194
|
+
* live on settings.providers[] flow through correctly — entry.apiKey is
|
|
1195
|
+
* undefined when the entry resolves via providerCatalog.
|
|
1196
|
+
*/
|
|
1197
|
+
persistActiveModel(entry, llm) {
|
|
1198
|
+
try {
|
|
1199
|
+
const dir = join(homedir(), ".code-shell");
|
|
1200
|
+
const file = join(dir, "settings.json");
|
|
1201
|
+
mkdirSync(dir, { recursive: true });
|
|
1202
|
+
let existing = {};
|
|
1203
|
+
if (existsSync(file)) {
|
|
1204
|
+
try {
|
|
1205
|
+
existing = JSON.parse(readFileSync(file, "utf-8"));
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
// corrupt file — bail rather than clobber the user's config
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
const prevModel = typeof existing.model === "object" && existing.model
|
|
1213
|
+
? existing.model
|
|
1214
|
+
: {};
|
|
1215
|
+
const updated = {
|
|
1216
|
+
...existing,
|
|
1217
|
+
activeKey: entry.key,
|
|
1218
|
+
model: {
|
|
1219
|
+
...prevModel,
|
|
1220
|
+
provider: llm.provider,
|
|
1221
|
+
name: entry.model,
|
|
1222
|
+
apiKey: llm.apiKey,
|
|
1223
|
+
baseUrl: llm.baseUrl,
|
|
1224
|
+
},
|
|
1225
|
+
};
|
|
1226
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
1227
|
+
const payload = JSON.stringify(updated, null, 2) + "\n";
|
|
1228
|
+
writeFileSync(tmp, payload, "utf-8");
|
|
1229
|
+
try {
|
|
1230
|
+
renameSync(tmp, file);
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
writeFileSync(file, payload, "utf-8");
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
logger.warn(`persistActiveModel failed: ${err.message}`);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
/** Get the model pool. */
|
|
1241
|
+
getModelPool() {
|
|
1242
|
+
return this.modelPool;
|
|
1243
|
+
}
|
|
1244
|
+
/** Get the current model name (full path). */
|
|
1245
|
+
getCurrentModel() {
|
|
1246
|
+
return this.config.llm.model;
|
|
1247
|
+
}
|
|
1248
|
+
getHookRegistry() {
|
|
1249
|
+
return this.hooks;
|
|
1250
|
+
}
|
|
1251
|
+
getSessionManager() {
|
|
1252
|
+
return this.sessionManager;
|
|
1253
|
+
}
|
|
1254
|
+
getConfig() {
|
|
1255
|
+
return this.config;
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Inject context into a session's transcript without triggering a LLM turn.
|
|
1259
|
+
* The injected content appears as an assistant message so the LLM can see it
|
|
1260
|
+
* in subsequent conversations. The transcript auto-flushes to disk.
|
|
1261
|
+
*
|
|
1262
|
+
* Also updates the in-memory compacted message cache so the next
|
|
1263
|
+
* engine.run() call for this session picks up the injected content
|
|
1264
|
+
* instead of a stale snapshot from the previous run.
|
|
1265
|
+
*/
|
|
1266
|
+
injectContext(sessionId, content) {
|
|
1267
|
+
const session = this.sessionManager.resume(sessionId);
|
|
1268
|
+
session.transcript.appendMessage("assistant", content);
|
|
1269
|
+
// Keep the compacted cache in sync so the next engine.run() call
|
|
1270
|
+
// (which reads from compactedMessagesBySession first) sees the
|
|
1271
|
+
// injected content rather than a stale pre-inject snapshot.
|
|
1272
|
+
const cached = this.compactedMessagesBySession.get(sessionId);
|
|
1273
|
+
if (cached) {
|
|
1274
|
+
cached.push({ role: "assistant", content });
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Force context compaction on the current session.
|
|
1279
|
+
* Returns token stats before/after.
|
|
1280
|
+
*/
|
|
1281
|
+
forceCompact() {
|
|
1282
|
+
const sessionId = this.lastSessionId;
|
|
1283
|
+
if (!this.lastContextManager || !sessionId) {
|
|
1284
|
+
return { before: 0, after: 0, strategy: "none (no active session)" };
|
|
1285
|
+
}
|
|
1286
|
+
const { estimateTokens } = require("../context/compaction.js");
|
|
1287
|
+
const sourceMessages = this.compactedMessagesBySession.get(sessionId) ??
|
|
1288
|
+
this.sessionManager.resume(sessionId).transcript.toMessages();
|
|
1289
|
+
const before = estimateTokens(sourceMessages);
|
|
1290
|
+
const compacted = this.lastContextManager.manage(sourceMessages);
|
|
1291
|
+
const after = estimateTokens(compacted);
|
|
1292
|
+
this.compactedMessagesBySession.set(sessionId, compacted);
|
|
1293
|
+
this.lastMessages = compacted;
|
|
1294
|
+
return {
|
|
1295
|
+
before,
|
|
1296
|
+
after,
|
|
1297
|
+
strategy: before === after ? "no compaction needed" : "compacted",
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
stripUserContextMessage(messages, userContextMsg) {
|
|
1301
|
+
if (!userContextMsg || messages[0] !== userContextMsg) {
|
|
1302
|
+
return [...messages];
|
|
1303
|
+
}
|
|
1304
|
+
return messages.slice(1);
|
|
1305
|
+
}
|
|
1306
|
+
getSettingsManager() {
|
|
1307
|
+
if (!this.settingsManager) {
|
|
1308
|
+
this.settingsManager = new SettingsManager(this.config.cwd);
|
|
1309
|
+
}
|
|
1310
|
+
return this.settingsManager;
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Update a config setting at runtime.
|
|
1314
|
+
*/
|
|
1315
|
+
updateConfig(key, value) {
|
|
1316
|
+
this.getSettingsManager().saveUserSetting(key, value);
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Read a settings value by dotted key (e.g. "arena.participants").
|
|
1320
|
+
* Returns undefined if any segment is missing.
|
|
1321
|
+
*/
|
|
1322
|
+
readSetting(key) {
|
|
1323
|
+
const settings = this.getSettingsManager().get();
|
|
1324
|
+
const parts = key.split(".");
|
|
1325
|
+
let target = settings;
|
|
1326
|
+
for (const p of parts) {
|
|
1327
|
+
if (target == null || typeof target !== "object")
|
|
1328
|
+
return undefined;
|
|
1329
|
+
target = target[p];
|
|
1330
|
+
}
|
|
1331
|
+
return target;
|
|
1332
|
+
}
|
|
1333
|
+
buildPermissionConfig(mode, cwd) {
|
|
1334
|
+
const rules = [...this.preset.defaultPermissionRules];
|
|
1335
|
+
// Memory tools: dream scope is the LLM's own workspace, so save/delete
|
|
1336
|
+
// there go through without prompting. user-scope save/delete fall through
|
|
1337
|
+
// to the tool's permissionDefault ("ask"), forcing the user to confirm
|
|
1338
|
+
// any modification of memories they own. Read tools are listed in the
|
|
1339
|
+
// tool definition as permissionDefault: "allow" — no rule needed here.
|
|
1340
|
+
rules.push({
|
|
1341
|
+
tool: "MemorySave",
|
|
1342
|
+
argsPattern: { scope: "^dream$" },
|
|
1343
|
+
decision: "allow",
|
|
1344
|
+
reason: "Dream scope is the LLM's auto-consolidation workspace",
|
|
1345
|
+
});
|
|
1346
|
+
rules.push({
|
|
1347
|
+
tool: "MemoryDelete",
|
|
1348
|
+
argsPattern: { scope: "^dream$" },
|
|
1349
|
+
decision: "allow",
|
|
1350
|
+
reason: "Dream scope is the LLM's auto-consolidation workspace",
|
|
1351
|
+
});
|
|
1352
|
+
if (mode === "acceptEdits" || mode === "bypassPermissions") {
|
|
1353
|
+
rules.push({ tool: "Write", decision: "allow" });
|
|
1354
|
+
rules.push({ tool: "Edit", decision: "allow" });
|
|
1355
|
+
}
|
|
1356
|
+
if (mode === "bypassPermissions") {
|
|
1357
|
+
rules.push({ tool: "Bash", decision: "allow" });
|
|
1358
|
+
}
|
|
1359
|
+
try {
|
|
1360
|
+
const settingsManager = new SettingsManager(cwd);
|
|
1361
|
+
const settings = settingsManager.get();
|
|
1362
|
+
if (settings.permissions?.rules?.length) {
|
|
1363
|
+
rules.unshift(...settings.permissions.rules);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
catch {
|
|
1367
|
+
// Settings not available — defaults only
|
|
1368
|
+
}
|
|
1369
|
+
let backend;
|
|
1370
|
+
if (this.config.approvalBackend) {
|
|
1371
|
+
backend =
|
|
1372
|
+
mode === "auto"
|
|
1373
|
+
? new AutoApprovalBackend(this.config.approvalBackend)
|
|
1374
|
+
: this.config.approvalBackend;
|
|
1375
|
+
}
|
|
1376
|
+
else if (mode === "auto") {
|
|
1377
|
+
backend = new AutoApprovalBackend();
|
|
1378
|
+
}
|
|
1379
|
+
else {
|
|
1380
|
+
// If a host installed an InteractiveApprovalBackend prompt fn
|
|
1381
|
+
// (agent-server-stdio does this on boot via setInteractiveApprovalFn),
|
|
1382
|
+
// use it so the UI gets a chance to approve/deny. Without this,
|
|
1383
|
+
// every `ask` permission silently fell through to deny-all and
|
|
1384
|
+
// the user saw "Permission denied by user" with NO modal — exactly
|
|
1385
|
+
// the bug that motivated this fix.
|
|
1386
|
+
const interactive = getInteractiveApprovalBackend();
|
|
1387
|
+
if (interactive.hasPromptFn()) {
|
|
1388
|
+
backend = interactive;
|
|
1389
|
+
}
|
|
1390
|
+
else {
|
|
1391
|
+
backend = new HeadlessApprovalBackend(mode === "bypassPermissions" ? "approve-all" : mode === "dontAsk" ? "deny-all" : "deny-all");
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
return { rules, backend };
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Switch permission mode at runtime. Takes effect immediately for any
|
|
1398
|
+
* in-flight ToolExecutor (which holds a reference to the same classifier),
|
|
1399
|
+
* and the new mode is used for any subsequent run() calls.
|
|
1400
|
+
* Session-only — does not persist to settings.
|
|
1401
|
+
*/
|
|
1402
|
+
setPermissionMode(mode) {
|
|
1403
|
+
this.config = { ...this.config, permissionMode: mode };
|
|
1404
|
+
this.permissionMode = mode;
|
|
1405
|
+
this.planMode = mode === "plan";
|
|
1406
|
+
if (this.activePermission) {
|
|
1407
|
+
const cwd = this.config.cwd ?? process.cwd();
|
|
1408
|
+
const { rules, backend } = this.buildPermissionConfig(mode, cwd);
|
|
1409
|
+
this.activePermission.reconfigure(mode, backend, rules);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
getPermissionMode() {
|
|
1413
|
+
return this.config.permissionMode ?? "acceptEdits";
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Toggle plan mode directly. Called by the Plan tool (Task 7) via ToolContext.engine.
|
|
1417
|
+
* Also syncs permissionMode to keep both fields consistent.
|
|
1418
|
+
*/
|
|
1419
|
+
setPlanMode(value) {
|
|
1420
|
+
if (value) {
|
|
1421
|
+
this.setPermissionMode("plan");
|
|
1422
|
+
}
|
|
1423
|
+
else if (this.permissionMode === "plan") {
|
|
1424
|
+
// Leaving plan mode: drop back to the default.
|
|
1425
|
+
this.setPermissionMode("acceptEdits");
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
this.planMode = value;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Build a base ToolContext for this Engine. Used by run() (which then
|
|
1433
|
+
* overlays turn-specific fields like sandbox and subAgentSpawner) and
|
|
1434
|
+
* by tests that want a ToolContext without a full run() cycle.
|
|
1435
|
+
*/
|
|
1436
|
+
buildToolContext() {
|
|
1437
|
+
return {
|
|
1438
|
+
cwd: this.config.cwd ?? process.cwd(),
|
|
1439
|
+
llmConfig: this.config.llm,
|
|
1440
|
+
modelPool: this.modelPool,
|
|
1441
|
+
toolRegistry: this.toolRegistry,
|
|
1442
|
+
askUser: this.config.askUser,
|
|
1443
|
+
isSubAgent: this.config.isSubAgent === true,
|
|
1444
|
+
hooks: this.hooks,
|
|
1445
|
+
planMode: this.planMode,
|
|
1446
|
+
engine: this,
|
|
1447
|
+
disabledSkills: this.readDisabledSkills(),
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Read settings.disabledSkills. Sub-agents skip this for the same
|
|
1452
|
+
* reason they skip settings.hooks / plugin hooks (registerSettingsHooks
|
|
1453
|
+
* at ~line 237): they run with a minimal surface area. Defaults to []
|
|
1454
|
+
* so callers don't have to null-check.
|
|
1455
|
+
*/
|
|
1456
|
+
readDisabledSkills() {
|
|
1457
|
+
if (this.config.isSubAgent === true)
|
|
1458
|
+
return [];
|
|
1459
|
+
try {
|
|
1460
|
+
const settings = this.getSettingsManager().get();
|
|
1461
|
+
return settings.disabledSkills ?? [];
|
|
1462
|
+
}
|
|
1463
|
+
catch {
|
|
1464
|
+
return [];
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|