@cjhyy/code-shell-core 0.5.0-rc.1 → 0.5.0-rc.2
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/dist/agent/agent-definition-registry.d.ts +18 -3
- package/dist/agent/agent-definition-registry.js +47 -18
- package/dist/agent/agent-definition.d.ts +17 -0
- package/dist/agent/agent-definition.js +22 -1
- package/dist/arena/arena.js +11 -12
- package/dist/arena/context/context-tools.js +2 -4
- package/dist/arena/context/within-root.d.ts +7 -0
- package/dist/arena/context/within-root.js +15 -0
- package/dist/arena/iterate/phases/argue.js +8 -2
- package/dist/arena/iterate/phases/revise.js +1 -1
- package/dist/arena/iterate/phases/tournament.js +3 -3
- package/dist/arena/phases/adjudication.js +1 -4
- package/dist/arena/phases/build-consensus.js +1 -4
- package/dist/arena/phases/cross-review.js +2 -8
- package/dist/arena/phases/debate-rounds.js +1 -4
- package/dist/arena/phases/participant-research.js +1 -4
- package/dist/arena/phases/planning-detail-expansion.js +8 -10
- package/dist/arena/planner.js +0 -1
- package/dist/arena/providers/docs.d.ts +1 -0
- package/dist/arena/providers/docs.js +5 -2
- package/dist/arena/providers/repo.d.ts +1 -0
- package/dist/arena/providers/repo.js +5 -2
- package/dist/arena/strategies/utils.js +36 -3
- package/dist/arena/transitions.d.ts +7 -2
- package/dist/arena/transitions.js +9 -4
- package/dist/arena/types.d.ts +8 -2
- package/dist/automation/cron-expr.d.ts +31 -0
- package/dist/automation/cron-expr.js +151 -0
- package/dist/automation/index.d.ts +41 -0
- package/dist/automation/index.js +39 -0
- package/dist/automation/runner.d.ts +67 -0
- package/dist/automation/runner.js +56 -0
- package/dist/automation/scheduler.d.ts +156 -0
- package/dist/automation/scheduler.js +546 -0
- package/dist/automation/store.d.ts +39 -0
- package/dist/automation/store.js +119 -0
- package/dist/automation/write-policy.d.ts +34 -0
- package/dist/automation/write-policy.js +113 -0
- package/dist/automation/write-run.d.ts +51 -0
- package/dist/automation/write-run.js +38 -0
- package/dist/capability-control/index.d.ts +5 -0
- package/dist/capability-control/index.js +3 -0
- package/dist/capability-control/overlay.d.ts +49 -0
- package/dist/capability-control/overlay.js +89 -0
- package/dist/capability-control/project.d.ts +65 -0
- package/dist/capability-control/project.js +147 -0
- package/dist/capability-control/service.d.ts +70 -0
- package/dist/capability-control/service.js +147 -0
- package/dist/capability-control/types.d.ts +62 -0
- package/dist/capability-control/types.js +17 -0
- package/dist/cli/agent-server-stdio.d.ts +12 -1
- package/dist/cli/agent-server-stdio.js +114 -19
- package/dist/cli/agent-server-tcp.d.ts +20 -0
- package/dist/cli/agent-server-tcp.js +120 -0
- package/dist/cli/graceful-shutdown.d.ts +27 -0
- package/dist/cli/graceful-shutdown.js +30 -0
- package/dist/context/compaction.js +23 -15
- package/dist/context/manager.d.ts +0 -20
- package/dist/context/manager.js +0 -37
- package/dist/cron/cron-runtime.d.ts +2 -0
- package/dist/cron/cron-runtime.js +2 -0
- package/dist/cron/cron-store.d.ts +2 -0
- package/dist/cron/cron-store.js +2 -0
- package/dist/cron/scheduler.d.ts +5 -32
- package/dist/cron/scheduler.js +5 -113
- package/dist/data/openrouter-sync.js +1 -1
- package/dist/engine/engine.d.ts +208 -14
- package/dist/engine/engine.js +754 -169
- package/dist/engine/goal.d.ts +42 -0
- package/dist/engine/goal.js +52 -0
- package/dist/engine/image-compression.d.ts +81 -0
- package/dist/engine/image-compression.js +186 -0
- package/dist/engine/image-policy.d.ts +152 -0
- package/dist/engine/image-policy.js +165 -0
- package/dist/engine/reactive-threshold.d.ts +13 -0
- package/dist/engine/reactive-threshold.js +17 -0
- package/dist/engine/runtime.js +7 -0
- package/dist/engine/session-title.d.ts +13 -0
- package/dist/engine/session-title.js +34 -0
- package/dist/engine/turn-loop.d.ts +29 -8
- package/dist/engine/turn-loop.js +498 -316
- package/dist/git/parse-log.d.ts +13 -0
- package/dist/git/parse-log.js +21 -0
- package/dist/git/utils.d.ts +8 -6
- package/dist/git/utils.js +57 -26
- package/dist/git/worktree.js +33 -18
- package/dist/hooks/events.d.ts +23 -1
- package/dist/hooks/goal-stop-hook.d.ts +30 -0
- package/dist/hooks/goal-stop-hook.js +102 -0
- package/dist/hooks/hook-output.d.ts +33 -0
- package/dist/hooks/hook-output.js +79 -0
- package/dist/hooks/registry.d.ts +7 -0
- package/dist/hooks/registry.js +38 -1
- package/dist/hooks/shell-runner.d.ts +2 -0
- package/dist/hooks/shell-runner.js +43 -2
- package/dist/index.d.ts +31 -6
- package/dist/index.js +30 -3
- package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
- package/dist/llm/capabilities/reasoning-control.js +38 -0
- package/dist/llm/capabilities/rules.js +33 -3
- package/dist/llm/capabilities/types.d.ts +22 -2
- package/dist/llm/clamp-max-tokens.d.ts +11 -0
- package/dist/llm/clamp-max-tokens.js +17 -0
- package/dist/llm/client-base.d.ts +41 -4
- package/dist/llm/client-base.js +67 -13
- package/dist/llm/client-factory.d.ts +3 -3
- package/dist/llm/client-factory.js +2 -2
- package/dist/llm/model-pool.d.ts +15 -11
- package/dist/llm/model-pool.js +23 -20
- package/dist/llm/provider-catalog.d.ts +2 -2
- package/dist/llm/providers/anthropic.d.ts +32 -2
- package/dist/llm/providers/anthropic.js +147 -8
- package/dist/llm/providers/openai.d.ts +11 -2
- package/dist/llm/providers/openai.js +253 -52
- package/dist/llm/reasoning-setting.d.ts +51 -0
- package/dist/llm/reasoning-setting.js +32 -0
- package/dist/llm/stop-reason.d.ts +13 -0
- package/dist/llm/stop-reason.js +21 -0
- package/dist/llm/strip-vision.d.ts +32 -0
- package/dist/llm/strip-vision.js +94 -0
- package/dist/llm/types.d.ts +5 -6
- package/dist/logging/logger.js +18 -4
- package/dist/logging/sanitize-messages.d.ts +10 -0
- package/dist/logging/sanitize-messages.js +90 -0
- package/dist/lsp/client.js +28 -8
- package/dist/lsp/manager.js +2 -1
- package/dist/lsp/root-path.d.ts +9 -0
- package/dist/lsp/root-path.js +12 -0
- package/dist/onboarding.js +18 -11
- package/dist/plugins/gitOps.d.ts +34 -0
- package/dist/plugins/gitOps.js +59 -3
- package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
- package/dist/plugins/installer/codex/convertAgents.js +55 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
- package/dist/plugins/installer/codex/convertMcp.js +45 -0
- package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
- package/dist/plugins/installer/codex/convertSkills.js +33 -0
- package/dist/plugins/installer/detectFormat.d.ts +2 -0
- package/dist/plugins/installer/detectFormat.js +6 -0
- package/dist/plugins/installer/install.d.ts +7 -0
- package/dist/plugins/installer/install.js +99 -0
- package/dist/plugins/installer/installFromSource.d.ts +13 -0
- package/dist/plugins/installer/installFromSource.js +45 -0
- package/dist/plugins/installer/list.d.ts +9 -0
- package/dist/plugins/installer/list.js +30 -0
- package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
- package/dist/plugins/installer/loadPluginAgents.js +23 -0
- package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
- package/dist/plugins/installer/loadPluginMcp.js +74 -0
- package/dist/plugins/installer/parseSource.d.ts +20 -0
- package/dist/plugins/installer/parseSource.js +74 -0
- package/dist/plugins/installer/paths.d.ts +5 -0
- package/dist/plugins/installer/paths.js +25 -0
- package/dist/plugins/installer/types.d.ts +49 -0
- package/dist/plugins/installer/types.js +27 -0
- package/dist/plugins/installer/uninstall.d.ts +2 -0
- package/dist/plugins/installer/uninstall.js +14 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +65 -0
- package/dist/plugins/loadPluginHooks.d.ts +8 -1
- package/dist/plugins/loadPluginHooks.js +11 -1
- package/dist/plugins/marketplaceManager.d.ts +8 -1
- package/dist/plugins/marketplaceManager.js +38 -4
- package/dist/plugins/pluginCommandHook.js +34 -0
- package/dist/plugins/pluginInstaller.d.ts +21 -0
- package/dist/plugins/pluginInstaller.js +128 -29
- package/dist/plugins/schemas.js +40 -6
- package/dist/plugins/types.d.ts +4 -0
- package/dist/preset/index.js +16 -0
- package/dist/prompt/composer.d.ts +4 -0
- package/dist/prompt/composer.js +28 -4
- package/dist/prompt/sections/orchestration.md +8 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -1
- package/dist/protocol/chat-session-manager.js +10 -0
- package/dist/protocol/chat-session.d.ts +17 -0
- package/dist/protocol/chat-session.js +28 -0
- package/dist/protocol/redact.d.ts +50 -0
- package/dist/protocol/redact.js +71 -0
- package/dist/protocol/server.d.ts +41 -2
- package/dist/protocol/server.js +231 -33
- package/dist/protocol/tcp-transport.d.ts +45 -0
- package/dist/protocol/tcp-transport.js +74 -0
- package/dist/protocol/types.d.ts +23 -0
- package/dist/protocol/types.js +6 -0
- package/dist/remote/bridge.d.ts +11 -2
- package/dist/remote/bridge.js +61 -33
- package/dist/run/ArtifactTracker.js +5 -4
- package/dist/run/EngineRunner.d.ts +27 -1
- package/dist/run/EngineRunner.js +46 -8
- package/dist/run/FileRunStore.js +32 -8
- package/dist/run/RunLock.d.ts +28 -2
- package/dist/run/RunLock.js +49 -7
- package/dist/run/RunManager.js +51 -3
- package/dist/run/RunQueue.d.ts +1 -0
- package/dist/run/RunQueue.js +8 -2
- package/dist/run/factory.d.ts +7 -0
- package/dist/run/factory.js +1 -0
- package/dist/run/index.d.ts +2 -2
- package/dist/run/index.js +1 -1
- package/dist/run/redirect-target.d.ts +7 -0
- package/dist/run/redirect-target.js +13 -0
- package/dist/run/types.js +1 -1
- package/dist/services/browser-open.d.ts +13 -0
- package/dist/services/browser-open.js +16 -0
- package/dist/services/dream-consolidation.d.ts +57 -0
- package/dist/services/dream-consolidation.js +151 -0
- package/dist/services/memory-orchestrator.js +18 -0
- package/dist/services/notifier.d.ts +8 -0
- package/dist/services/notifier.js +39 -25
- package/dist/services/oauth.js +5 -7
- package/dist/services/session-memory-sort.d.ts +8 -0
- package/dist/services/session-memory-sort.js +9 -0
- package/dist/services/session-memory.js +8 -9
- package/dist/session/file-history.js +10 -3
- package/dist/session/session-manager.d.ts +23 -1
- package/dist/session/session-manager.js +73 -2
- package/dist/settings/disk-defaults.d.ts +35 -0
- package/dist/settings/disk-defaults.js +24 -0
- package/dist/settings/manager.d.ts +33 -0
- package/dist/settings/manager.js +94 -1
- package/dist/settings/personalization.d.ts +19 -0
- package/dist/settings/personalization.js +7 -0
- package/dist/settings/schema.d.ts +1142 -126
- package/dist/settings/schema.js +119 -10
- package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
- package/dist/tool-system/builtin/add-marketplace.js +76 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +11 -0
- package/dist/tool-system/builtin/agent-registry.js +8 -4
- package/dist/tool-system/builtin/agent.d.ts +29 -10
- package/dist/tool-system/builtin/agent.js +108 -52
- package/dist/tool-system/builtin/apply-patch/index.js +22 -3
- package/dist/tool-system/builtin/arena.js +6 -7
- package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
- package/dist/tool-system/builtin/complete-goal.js +45 -0
- package/dist/tool-system/builtin/cron.js +53 -9
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +6 -1
- package/dist/tool-system/builtin/generate-image.d.ts +25 -0
- package/dist/tool-system/builtin/generate-image.js +146 -0
- package/dist/tool-system/builtin/glob.js +7 -0
- package/dist/tool-system/builtin/grep.js +6 -0
- package/dist/tool-system/builtin/index.d.ts +18 -1
- package/dist/tool-system/builtin/index.js +62 -1
- package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
- package/dist/tool-system/builtin/notebook-edit.js +8 -1
- package/dist/tool-system/builtin/read.d.ts +2 -1
- package/dist/tool-system/builtin/read.js +9 -1
- package/dist/tool-system/builtin/sleep.js +9 -3
- package/dist/tool-system/builtin/task.js +4 -1
- package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
- package/dist/tool-system/builtin/update-automation-memory.js +60 -0
- package/dist/tool-system/builtin/view-image.d.ts +18 -0
- package/dist/tool-system/builtin/view-image.js +96 -0
- package/dist/tool-system/builtin/web-search.d.ts +4 -1
- package/dist/tool-system/builtin/web-search.js +36 -3
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +7 -1
- package/dist/tool-system/context.d.ts +12 -0
- package/dist/tool-system/executor.d.ts +0 -1
- package/dist/tool-system/executor.js +34 -40
- package/dist/tool-system/investigation-guard.d.ts +4 -1
- package/dist/tool-system/investigation-guard.js +19 -3
- package/dist/tool-system/mcp-manager.d.ts +36 -12
- package/dist/tool-system/mcp-manager.js +173 -10
- package/dist/tool-system/path-policy.d.ts +94 -0
- package/dist/tool-system/path-policy.js +279 -0
- package/dist/tool-system/permission.js +43 -50
- package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
- package/dist/tool-system/plan-mode-allowlist.js +43 -0
- package/dist/tool-system/registry.js +12 -1
- package/dist/tool-system/validation.d.ts +10 -0
- package/dist/tool-system/validation.js +16 -3
- package/dist/types.d.ts +90 -15
- package/dist/utils/format.js +4 -2
- package/dist/utils/lockfile.js +9 -2
- package/dist/utils/theme.d.ts +6 -0
- package/dist/utils/theme.js +4 -2
- package/package.json +2 -1
package/dist/engine/engine.js
CHANGED
|
@@ -7,22 +7,32 @@ import { ToolExecutor } from "../tool-system/executor.js";
|
|
|
7
7
|
import { InvestigationGuard } from "../tool-system/investigation-guard.js";
|
|
8
8
|
import { TaskGuard } from "../tool-system/task-guard.js";
|
|
9
9
|
import { readLastTodoSnapshot } from "../tool-system/builtin/task.js";
|
|
10
|
+
import { agentToolDefWithTypes } from "../tool-system/builtin/agent.js";
|
|
11
|
+
import { BUILTIN_TOOL_GUARDS } from "../tool-system/builtin/index.js";
|
|
12
|
+
import { asyncAgentRegistry } from "../tool-system/builtin/agent-registry.js";
|
|
13
|
+
import { notificationQueue, buildNotificationMessage, } from "../tool-system/builtin/agent-notifications.js";
|
|
10
14
|
import { PermissionClassifier, HeadlessApprovalBackend, AutoApprovalBackend, InteractiveApprovalBackend, getInteractiveApprovalBackend, } from "../tool-system/permission.js";
|
|
11
15
|
import { HookRegistry } from "../hooks/registry.js";
|
|
12
16
|
import { wrapHookMessages } from "../hooks/inject.js";
|
|
17
|
+
import { createGoalStopHook } from "../hooks/goal-stop-hook.js";
|
|
18
|
+
import { normalizeGoal } from "./goal.js";
|
|
13
19
|
import { loadPluginHooks } from "../plugins/loadPluginHooks.js";
|
|
20
|
+
import { pluginAgentDirs } from "../plugins/installer/loadPluginAgents.js";
|
|
14
21
|
import { patchOrphanedToolUses } from "./patch-orphaned-tools.js";
|
|
15
22
|
import { runShellHook, shellHookMatches } from "../hooks/shell-runner.js";
|
|
16
23
|
import { ContextManager } from "../context/manager.js";
|
|
24
|
+
import { estimateTokens } from "../context/compaction.js";
|
|
25
|
+
import { PLAN_MODE_ALLOWED_TOOLS } from "../tool-system/plan-mode-allowlist.js";
|
|
17
26
|
import { PromptComposer } from "../prompt/composer.js";
|
|
18
27
|
import { SessionManager } from "../session/session-manager.js";
|
|
19
28
|
import { ModelFacade } from "./model-facade.js";
|
|
20
|
-
import { logger, setCurrentSid, runWithSid } from "../logging/logger.js";
|
|
29
|
+
import { logger, setCurrentSid, runWithSid, getCurrentSid } from "../logging/logger.js";
|
|
21
30
|
import { recordSessionStart, recordSessionEnd } from "../logging/session-recorder.js";
|
|
22
31
|
import { sanitizeContent, sanitizeTaskString } from "../logging/sanitize-messages.js";
|
|
23
32
|
import { TurnLoop } from "./turn-loop.js";
|
|
24
33
|
import { MCPManager } from "../tool-system/mcp-manager.js";
|
|
25
|
-
import { SettingsManager } from "../settings/manager.js";
|
|
34
|
+
import { SettingsManager, userHome } from "../settings/manager.js";
|
|
35
|
+
import { effectiveDisabledList, effectiveBuiltinLists } from "../capability-control/overlay.js";
|
|
26
36
|
import { FileHistory } from "../session/file-history.js";
|
|
27
37
|
import { defaultSandboxConfig, resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
|
|
28
38
|
import { resolveAgentPreset, resolveBuiltinToolNames, } from "../preset/index.js";
|
|
@@ -33,31 +43,112 @@ import { defaultCacheDir } from "../llm/model-cache.js";
|
|
|
33
43
|
import { detectProviderFromApiKey, buildModelPool, } from "../onboarding.js";
|
|
34
44
|
import { detectPastedNoise } from "../utils/task-sanitizer.js";
|
|
35
45
|
import { parseTaskWithImages, } from "./parse-task.js";
|
|
46
|
+
import { enforceImagePolicy, byteLengthFromBase64, dropOversizedImages, } from "./image-policy.js";
|
|
47
|
+
import { tryCompressImages } from "./image-compression.js";
|
|
48
|
+
import { buildSessionTitle } from "./session-title.js";
|
|
36
49
|
import { capabilitiesFor } from "../llm/capabilities/index.js";
|
|
37
50
|
import { MemoryOrchestrator } from "../services/memory-orchestrator.js";
|
|
51
|
+
import { runDreamConsolidation } from "../services/dream-consolidation.js";
|
|
38
52
|
import { join } from "node:path";
|
|
39
53
|
import { homedir } from "node:os";
|
|
40
54
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
55
|
+
/**
|
|
56
|
+
* Build ScanOptions.compatFileNames from the user's instruction compat toggles.
|
|
57
|
+
* Primary file name stays hard-wired to CODESHELL.md (not exposed). Turning a
|
|
58
|
+
* compat flag off only drops the same-named .md (CLAUDE.md / AGENTS.md); the
|
|
59
|
+
* .claude/ subdir, *.local.md and rules/ are intentionally NOT linked.
|
|
60
|
+
* undefined (instructions omitted) means both stay on — backward compatible.
|
|
61
|
+
*/
|
|
62
|
+
export function compatFileNamesFrom(instructions) {
|
|
63
|
+
const names = [];
|
|
64
|
+
if (instructions?.compatClaude !== false)
|
|
65
|
+
names.push("CLAUDE.md");
|
|
66
|
+
if (instructions?.compatCodex !== false)
|
|
67
|
+
names.push("AGENTS.md");
|
|
68
|
+
return names;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* True when two LLMConfigs name the SAME client identity — i.e. building a
|
|
72
|
+
* client from either would talk to the same model on the same endpoint with the
|
|
73
|
+
* same shaping. Used by resolveAuxClient to de-dup the aux client against the
|
|
74
|
+
* active model WITHOUT collapsing two distinct pool keys that merely share a
|
|
75
|
+
* `model` NAME but differ in reasoning/maxTokens/baseUrl/provider. Compares the
|
|
76
|
+
* fields that actually change request behavior; apiKey is intentionally NOT
|
|
77
|
+
* compared (two keys with the same endpoint+model but different credentials
|
|
78
|
+
* still produce equivalent aux work and don't warrant a second client). The
|
|
79
|
+
* reasoning object is compared by normalized JSON since it's a small
|
|
80
|
+
* discriminated union.
|
|
81
|
+
*/
|
|
82
|
+
function sameLlmIdentity(a, b) {
|
|
83
|
+
return (a.model === b.model &&
|
|
84
|
+
(a.baseUrl ?? undefined) === (b.baseUrl ?? undefined) &&
|
|
85
|
+
(a.provider ?? undefined) === (b.provider ?? undefined) &&
|
|
86
|
+
(a.providerKind ?? undefined) === (b.providerKind ?? undefined) &&
|
|
87
|
+
(a.maxTokens ?? undefined) === (b.maxTokens ?? undefined) &&
|
|
88
|
+
JSON.stringify(a.reasoning ?? null) === JSON.stringify(b.reasoning ?? null));
|
|
89
|
+
}
|
|
90
|
+
// Re-export the config hot-reload patch builder from here so the protocol
|
|
91
|
+
// server (and tests) can import it alongside Engine without reaching into the
|
|
92
|
+
// settings/ subtree directly. The implementation lives in settings/ to keep
|
|
93
|
+
// engine.ts from growing and to sit next to personalizationFrom it composes.
|
|
94
|
+
export { diskDefaultsFrom } from "../settings/disk-defaults.js";
|
|
41
95
|
/**
|
|
42
96
|
* Resolve the LLM config for a spawned child Engine.
|
|
43
|
-
* - `modelKey` set + present in pool → that model's config (
|
|
97
|
+
* - `modelKey` set + present in pool → that model's config (pure entry-derived
|
|
98
|
+
* identity; the parent's llm is NOT consulted).
|
|
44
99
|
* - otherwise (no key, no pool, or key miss) → the parent's llm unchanged.
|
|
45
100
|
* Key miss is a soft fallback, NOT an error: a stale agent definition must not
|
|
46
101
|
* crash the spawn.
|
|
102
|
+
*
|
|
103
|
+
* ClientDefaults (temperature/timeout/etc.) are inherited from the parent
|
|
104
|
+
* Engine directly via EngineConfig.clientDefaults — they do not flow through
|
|
105
|
+
* this helper because they're not part of LLMConfig anymore.
|
|
47
106
|
*/
|
|
48
107
|
export function resolveChildLlm(modelKey, pool, parentLlm) {
|
|
49
108
|
if (modelKey && pool?.has(modelKey)) {
|
|
50
|
-
const resolved = pool.resolveLLMConfig(modelKey
|
|
109
|
+
const resolved = pool.resolveLLMConfig(modelKey);
|
|
51
110
|
if (resolved)
|
|
52
111
|
return resolved;
|
|
53
112
|
}
|
|
54
113
|
return parentLlm;
|
|
55
114
|
}
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Load reusable sub-agent role definitions, merging:
|
|
117
|
+
* 1. project-level <cwd>/.code-shell/agents/*.md (ships built-ins)
|
|
118
|
+
* 2. user-level ~/.code-shell/agents/*.md (user wins on name)
|
|
119
|
+
* Names in `disabledAgents` are filtered out so the LLM never sees them.
|
|
120
|
+
*/
|
|
121
|
+
export function loadAgentDefinitionsForCwd(cwd, disabledAgents = [], disabledPlugins = []) {
|
|
122
|
+
const home = homedir();
|
|
123
|
+
// Increasing priority; loadFromDirs is last-dir-wins. ORDER ENCODES POLICY:
|
|
124
|
+
// user (cross-project personal default, lowest) → plugins (reusable baseline)
|
|
125
|
+
// → project (highest). A repo's in-tree agent therefore overrides a same-named
|
|
126
|
+
// user agent. This REVERSES the previous user>project behavior (spec §7.2);
|
|
127
|
+
// the descriptor's shadowedSources surfaces the override so the UI can warn.
|
|
128
|
+
return AgentDefinitionRegistry.loadFromDirs([
|
|
129
|
+
{ dir: `${home}/.code-shell/agents`, source: "user" },
|
|
130
|
+
...pluginAgentDirs(disabledPlugins),
|
|
131
|
+
// No project context (no-project bucket): cwd is "". Skip the project
|
|
132
|
+
// source rather than synthesizing "/.code-shell/agents" at the FS root,
|
|
133
|
+
// which silently resolves to nothing and drops every project-level
|
|
134
|
+
// (built-in) agent from the list.
|
|
135
|
+
...(cwd ? [{ dir: `${cwd}/.code-shell/agents`, source: "project" }] : []),
|
|
136
|
+
], disabledAgents);
|
|
59
137
|
}
|
|
60
138
|
const NESTED_AGENT_TOOLS = ["Agent", "AgentStatus", "AgentCancel"];
|
|
139
|
+
/**
|
|
140
|
+
* #7: apply a project's per-turn builtin capability override to a tool list.
|
|
141
|
+
* A builtin marked `off` for the current cwd is HIDDEN from the turn's tool
|
|
142
|
+
* list (matching how skills/plugins/agents `off` apply mid-session). `on` /
|
|
143
|
+
* `inherit` / absent keep the tool — we can't re-add a tool the ctor-frozen
|
|
144
|
+
* registry omitted, but `on` for a tool already present is a no-op. Pure +
|
|
145
|
+
* exported so it's unit-testable without a full run() turn.
|
|
146
|
+
*/
|
|
147
|
+
export function applyBuiltinOverrideVisibility(tools, override) {
|
|
148
|
+
if (!override)
|
|
149
|
+
return tools;
|
|
150
|
+
return tools.filter((t) => override[t.name] !== "off");
|
|
151
|
+
}
|
|
61
152
|
/**
|
|
62
153
|
* Compute a child Engine's tool scope.
|
|
63
154
|
* - `allowlist` set → child enabled = allowlist minus nested-agent tools
|
|
@@ -78,16 +169,38 @@ export function resolveChildToolScope(allowlist, parentDisabled, parentEnabled)
|
|
|
78
169
|
}
|
|
79
170
|
export class Engine {
|
|
80
171
|
config;
|
|
172
|
+
// Resolved per-session preset. Set in the ctor; re-resolved by
|
|
173
|
+
// refreshRuntimeConfig on a preset hot-reload so the next-turn PromptComposer
|
|
174
|
+
// picks up the new preset's system prompt / behavior (#2). NOT readonly for
|
|
175
|
+
// that reason. NOTE: the toolRegistry's builtin tool SET is still ctor-frozen
|
|
176
|
+
// and is NOT rebuilt on reload — a preset change that alters the builtin tool
|
|
177
|
+
// set only takes effect on session restart (logged in refreshRuntimeConfig).
|
|
81
178
|
preset;
|
|
82
179
|
toolRegistry;
|
|
83
180
|
hooks;
|
|
84
181
|
sessionManager;
|
|
85
182
|
mcpManager;
|
|
86
183
|
modelPool;
|
|
184
|
+
/**
|
|
185
|
+
* Handles for the settings-sourced hook handlers registered by
|
|
186
|
+
* registerSettingsHooks(), so reloadHooks() can unregister exactly those
|
|
187
|
+
* (and nothing else — plugin hooks, goal/builtin hooks are untouched) before
|
|
188
|
+
* re-registering from fresh settings. Without this, a reload would
|
|
189
|
+
* accumulate duplicate settings-hook handlers that all fire per event.
|
|
190
|
+
*/
|
|
191
|
+
settingsHookHandles = [];
|
|
192
|
+
/**
|
|
193
|
+
* Highest config-reload version applied so far. refreshRuntimeConfig drops
|
|
194
|
+
* any payload whose version is <= this, so out-of-order reload deliveries
|
|
195
|
+
* (multiple quick settings saves) can't let an older config clobber a newer
|
|
196
|
+
* one.
|
|
197
|
+
*/
|
|
198
|
+
lastAppliedConfigVersion = 0;
|
|
87
199
|
/** Memoized sub-agent role registry, keyed by the cwd it was loaded from. */
|
|
88
200
|
agentDefsCache;
|
|
89
201
|
/** Shared resources supplied at construction (adapter pattern — null when self-constructed). */
|
|
90
202
|
runtime;
|
|
203
|
+
sandboxCache = new Map();
|
|
91
204
|
/** Active permission mode for this Engine instance. */
|
|
92
205
|
permissionMode;
|
|
93
206
|
/** True when permissionMode === "plan". */
|
|
@@ -96,6 +209,13 @@ export class Engine {
|
|
|
96
209
|
// don't re-read 6+ JSON files on every /model, /login, etc. The manager
|
|
97
210
|
// handles its own cache invalidation in saveUserSetting().
|
|
98
211
|
settingsManager;
|
|
212
|
+
/**
|
|
213
|
+
* Cached auxiliary-task LLM client, keyed by the models[].key it was built
|
|
214
|
+
* from. Background calls (memory extraction, auto-dream) reuse it across
|
|
215
|
+
* runs so we don't redo the provider handshake every session. Invalidated
|
|
216
|
+
* implicitly: a changed auxModelKey produces a different cache key.
|
|
217
|
+
*/
|
|
218
|
+
auxClientCache;
|
|
99
219
|
// Live state from the current/most-recent run, retained for /compact and
|
|
100
220
|
// for live-mutating PermissionClassifier on permission-mode switch.
|
|
101
221
|
lastContextManager;
|
|
@@ -158,12 +278,47 @@ export class Engine {
|
|
|
158
278
|
}
|
|
159
279
|
const entries = settings.hooks ?? [];
|
|
160
280
|
for (const entry of entries) {
|
|
161
|
-
|
|
281
|
+
const event = entry.event;
|
|
282
|
+
const handler = async (ctx) => {
|
|
162
283
|
if (!shellHookMatches(entry, ctx))
|
|
163
284
|
return {};
|
|
164
285
|
return runShellHook(entry, ctx);
|
|
165
|
-
}
|
|
286
|
+
};
|
|
287
|
+
this.hooks.register(event, handler, 50, `shell:${entry.event}:${entry.command.slice(0, 32)}`);
|
|
288
|
+
// Track the (event, handler) so reloadHooks() can unregister exactly
|
|
289
|
+
// these settings-sourced handlers without touching plugin/goal/code hooks.
|
|
290
|
+
this.settingsHookHandles.push({ event, handler });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Re-apply settings.hooks onto the live HookRegistry after a settings
|
|
295
|
+
* change (config hot-reload layer 2). Surgical: removes ONLY the
|
|
296
|
+
* settings-sourced handlers this Engine previously registered (tracked in
|
|
297
|
+
* settingsHookHandles by identity) and re-runs registerSettingsHooks() from
|
|
298
|
+
* fresh disk settings. Plugin hooks (registered once at construction at
|
|
299
|
+
* priority 80) and goal/builtin/SDK-config hooks are never touched.
|
|
300
|
+
*
|
|
301
|
+
* The SettingsManager cache is invalidated first so the re-read reflects the
|
|
302
|
+
* latest settings.json on disk (mirrors freshSettings()'s load() semantics).
|
|
303
|
+
* Sub-agents never register settings hooks, so this is a no-op for them.
|
|
304
|
+
*/
|
|
305
|
+
reloadHooks() {
|
|
306
|
+
if (this.config.isSubAgent === true)
|
|
307
|
+
return;
|
|
308
|
+
// Drop the previously-registered settings handlers by identity.
|
|
309
|
+
for (const { event, handler } of this.settingsHookHandles) {
|
|
310
|
+
this.hooks.unregister(event, handler);
|
|
166
311
|
}
|
|
312
|
+
this.settingsHookHandles = [];
|
|
313
|
+
// Force the next get() to re-read disk so reloaded hooks reflect the
|
|
314
|
+
// newest settings.json, not a stale merged cache.
|
|
315
|
+
try {
|
|
316
|
+
this.getSettingsManager().invalidate();
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// best-effort; registerSettingsHooks below tolerates read failures
|
|
320
|
+
}
|
|
321
|
+
this.registerSettingsHooks();
|
|
167
322
|
}
|
|
168
323
|
constructor(config) {
|
|
169
324
|
this.config = config;
|
|
@@ -173,11 +328,26 @@ export class Engine {
|
|
|
173
328
|
this.permissionMode = config.permissionMode ?? "acceptEdits";
|
|
174
329
|
this.planMode = this.permissionMode === "plan";
|
|
175
330
|
this.preset = resolveAgentPreset(config.preset);
|
|
331
|
+
// Fold the project's capabilityOverrides.builtin overlay over the global
|
|
332
|
+
// enabled/disabled builtin lists so a project can force-enable a
|
|
333
|
+
// globally-disabled builtin tool or force-disable a globally-enabled one
|
|
334
|
+
// (tri-state). Mirrors readDisabledLists for skills/plugins/agents; no cwd
|
|
335
|
+
// / no overlay → the config lists pass through unchanged (zero regression).
|
|
336
|
+
//
|
|
337
|
+
// #7: this builds the ctor-FROZEN builtin tool SET in the registry — a
|
|
338
|
+
// mid-session project override can't rebuild it. To make a builtin `off`
|
|
339
|
+
// toggle apply mid-session, run()'s per-turn tool-list assembly re-reads
|
|
340
|
+
// readBuiltinOverride(cwd) and HIDES `off` builtins from the turn's tool
|
|
341
|
+
// list (see the allToolDefs filter). `on` here can force-enable a
|
|
342
|
+
// globally-disabled builtin INTO the frozen set at construction; the
|
|
343
|
+
// per-turn path can only hide, not add, so a freshly-`on`'d builtin not in
|
|
344
|
+
// the set needs a session restart to appear.
|
|
345
|
+
const builtinLists = effectiveBuiltinLists(config.enabledBuiltinTools ?? [], config.disabledBuiltinTools ?? [], this.readBuiltinOverride(config.cwd));
|
|
176
346
|
this.toolRegistry = config.runtime?.toolRegistry ?? new ToolRegistry({
|
|
177
347
|
builtinTools: resolveBuiltinToolNames({
|
|
178
348
|
preset: this.preset.name,
|
|
179
|
-
enabledBuiltinTools:
|
|
180
|
-
disabledBuiltinTools:
|
|
349
|
+
enabledBuiltinTools: builtinLists.enabledBuiltinTools,
|
|
350
|
+
disabledBuiltinTools: builtinLists.disabledBuiltinTools,
|
|
181
351
|
}),
|
|
182
352
|
});
|
|
183
353
|
this.hooks = new HookRegistry();
|
|
@@ -189,7 +359,10 @@ export class Engine {
|
|
|
189
359
|
// overhead multiplied across sub-agents outweighs the value, and
|
|
190
360
|
// dispatched tasks should run with minimal surface area.
|
|
191
361
|
if (config.isSubAgent !== true) {
|
|
192
|
-
|
|
362
|
+
// disabledPlugins suppresses a plugin's hooks too (not just its
|
|
363
|
+
// Skill-tool entries) — see loadPluginHooks. readDisabledLists reads
|
|
364
|
+
// the same settings the prompt composer / tool context use.
|
|
365
|
+
loadPluginHooks(this.hooks, this.readDisabledLists().disabledPlugins);
|
|
193
366
|
}
|
|
194
367
|
// settings.hooks → shell-command wrappers. Chain order:
|
|
195
368
|
// plugin (80) → shell (50) → code (default 0).
|
|
@@ -264,7 +437,7 @@ export class Engine {
|
|
|
264
437
|
const entry = this.modelPool.switch(match.key);
|
|
265
438
|
this.config = {
|
|
266
439
|
...this.config,
|
|
267
|
-
llm: this.modelPool.toLLMConfig(entry
|
|
440
|
+
llm: this.modelPool.toLLMConfig(entry),
|
|
268
441
|
};
|
|
269
442
|
}
|
|
270
443
|
}
|
|
@@ -275,6 +448,27 @@ export class Engine {
|
|
|
275
448
|
// use /model to switch between the provider's available models.
|
|
276
449
|
this.autoPopulatePool(this.config.llm.apiKey, this.config.llm.baseUrl);
|
|
277
450
|
}
|
|
451
|
+
// Carry image-attachment settings + sampling temperature into
|
|
452
|
+
// clientDefaults. Both are cross-model knobs — they apply to whatever
|
|
453
|
+
// model is currently active and survive hot-switches. (Pre-cleanup
|
|
454
|
+
// these were merged into llm.imageDetail / llm.temperature; that path
|
|
455
|
+
// is gone because hot-switching now rotates llm wholesale.)
|
|
456
|
+
const imageSettings = settings.images;
|
|
457
|
+
const modelBlock = settings.model;
|
|
458
|
+
const nextDefaults = { ...(this.config.clientDefaults ?? {}) };
|
|
459
|
+
let defaultsChanged = false;
|
|
460
|
+
if (imageSettings?.detail && nextDefaults.imageDetail !== imageSettings.detail) {
|
|
461
|
+
nextDefaults.imageDetail = imageSettings.detail;
|
|
462
|
+
defaultsChanged = true;
|
|
463
|
+
}
|
|
464
|
+
if (typeof modelBlock?.temperature === "number" &&
|
|
465
|
+
nextDefaults.temperature !== modelBlock.temperature) {
|
|
466
|
+
nextDefaults.temperature = modelBlock.temperature;
|
|
467
|
+
defaultsChanged = true;
|
|
468
|
+
}
|
|
469
|
+
if (defaultsChanged) {
|
|
470
|
+
this.config = { ...this.config, clientDefaults: nextDefaults };
|
|
471
|
+
}
|
|
278
472
|
}
|
|
279
473
|
catch {
|
|
280
474
|
// Settings not available — pool stays empty
|
|
@@ -315,7 +509,7 @@ export class Engine {
|
|
|
315
509
|
const entry = this.modelPool.switch(defaultEntry.key);
|
|
316
510
|
this.config = {
|
|
317
511
|
...this.config,
|
|
318
|
-
llm: this.modelPool.toLLMConfig(entry
|
|
512
|
+
llm: this.modelPool.toLLMConfig(entry),
|
|
319
513
|
};
|
|
320
514
|
}
|
|
321
515
|
}
|
|
@@ -334,6 +528,13 @@ export class Engine {
|
|
|
334
528
|
setAskUser(fn) {
|
|
335
529
|
this.config.askUser = fn;
|
|
336
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Whether this engine runs unattended (no interactive human). Used by the
|
|
533
|
+
* in-process AgentServer to decide whether to wire an interactive askUser.
|
|
534
|
+
*/
|
|
535
|
+
isHeadless() {
|
|
536
|
+
return this.config.headless === true;
|
|
537
|
+
}
|
|
337
538
|
/**
|
|
338
539
|
* Run a task from start to finish.
|
|
339
540
|
*/
|
|
@@ -352,6 +553,16 @@ export class Engine {
|
|
|
352
553
|
if (event.type === "task_update") {
|
|
353
554
|
latestTodos = event.tasks;
|
|
354
555
|
}
|
|
556
|
+
// Persist goal progress so replay/history shows how many rounds the
|
|
557
|
+
// goal ran. Display-only — toMessages() ignores this type, so it never
|
|
558
|
+
// re-enters the LLM context.
|
|
559
|
+
if (event.type === "goal_progress") {
|
|
560
|
+
session.transcript.append("goal_progress", {
|
|
561
|
+
status: event.status,
|
|
562
|
+
round: event.round,
|
|
563
|
+
...(event.gaps ? { gaps: event.gaps } : {}),
|
|
564
|
+
});
|
|
565
|
+
}
|
|
355
566
|
userOnStream?.(event);
|
|
356
567
|
};
|
|
357
568
|
if (options)
|
|
@@ -398,6 +609,67 @@ export class Engine {
|
|
|
398
609
|
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
399
610
|
};
|
|
400
611
|
}
|
|
612
|
+
// Size gate. Hosts (desktop renderer, TUI) are expected to
|
|
613
|
+
// pre-compress to IMAGE_TARGETS — if they didn't, we fail the turn
|
|
614
|
+
// fast with a clear message instead of letting the OpenAI client
|
|
615
|
+
// grind through three 16-second "Connection error" retries on a
|
|
616
|
+
// 4 MB body. See `image-policy.ts` for the rationale and limits.
|
|
617
|
+
let verdict = enforceImagePolicy(parsedTask.images);
|
|
618
|
+
if (!verdict.ok && verdict.code === "image_too_large") {
|
|
619
|
+
// One image blew the per-image cap. Try the engine-side
|
|
620
|
+
// compressor (jimp-backed when installed; no-op otherwise) so
|
|
621
|
+
// TUI / MCP paths that lack a host-side resize don't fail
|
|
622
|
+
// outright on a screenshot they could have rescaled. The
|
|
623
|
+
// re-check below is what decides whether we proceed.
|
|
624
|
+
const compressed = await tryCompressImages(parsedTask.images);
|
|
625
|
+
if (compressed.anyCompressed) {
|
|
626
|
+
parsedTask.images = compressed.images;
|
|
627
|
+
logger.info("engine.run.image_compressed", {
|
|
628
|
+
before: verdict.offender?.bytes,
|
|
629
|
+
after: compressed.images.reduce((s, i) => s + byteLengthFromBase64(i.base64), 0),
|
|
630
|
+
});
|
|
631
|
+
verdict = enforceImagePolicy(parsedTask.images);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// After compression, anything still over the per-image cap is
|
|
635
|
+
// dropped with a textual placeholder instead of failing the
|
|
636
|
+
// turn (TODO-week.md #9e). The "5MB brick session" failure
|
|
637
|
+
// mode from Claude Code (research doc §A) was the case where a
|
|
638
|
+
// poisoned image entered history and every subsequent request
|
|
639
|
+
// re-sent it; placeholders keep history clean while letting
|
|
640
|
+
// the rest of the turn run.
|
|
641
|
+
if (!verdict.ok && verdict.code === "image_too_large") {
|
|
642
|
+
const drop = dropOversizedImages(parsedTask.images);
|
|
643
|
+
if (drop.droppedCount > 0) {
|
|
644
|
+
parsedTask.images = drop.kept;
|
|
645
|
+
parsedTask.hasImages = drop.kept.length > 0;
|
|
646
|
+
parsedTask.text = drop.placeholder + "\n\n" + parsedTask.text;
|
|
647
|
+
logger.warn("engine.run.image_dropped", {
|
|
648
|
+
droppedCount: drop.droppedCount,
|
|
649
|
+
keptCount: drop.kept.length,
|
|
650
|
+
});
|
|
651
|
+
verdict = enforceImagePolicy(parsedTask.images);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (!verdict.ok) {
|
|
655
|
+
// Cumulative / count caps can't be rescued by per-image
|
|
656
|
+
// dropping (well — too_many_images could trim by FIFO, but
|
|
657
|
+
// that's a bigger UX call than we want to make silently).
|
|
658
|
+
// Refuse the turn with the policy message.
|
|
659
|
+
logger.warn("engine.run.image_policy_failed", {
|
|
660
|
+
code: verdict.code,
|
|
661
|
+
imageCount: verdict.totals.imageCount,
|
|
662
|
+
totalBytes: verdict.totals.totalBytes,
|
|
663
|
+
offender: verdict.offender,
|
|
664
|
+
});
|
|
665
|
+
return {
|
|
666
|
+
text: `ERROR: ${verdict.message}`,
|
|
667
|
+
reason: "image_error",
|
|
668
|
+
sessionId: options?.sessionId ?? `image-policy-${verdict.code}`,
|
|
669
|
+
turnCount: 0,
|
|
670
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
671
|
+
};
|
|
672
|
+
}
|
|
401
673
|
}
|
|
402
674
|
// For downstream noise-detection + transcript persistence we want the
|
|
403
675
|
// *text* portion only — base64 bytes count as "noise" by the heuristic
|
|
@@ -439,7 +711,11 @@ export class Engine {
|
|
|
439
711
|
const { enabled: childEnabled, disabled: childDisabled } = resolveChildToolScope(req.toolAllowlist, this.config.disabledBuiltinTools, this.config.enabledBuiltinTools);
|
|
440
712
|
const childLlm = resolveChildLlm(req.model, this.modelPool, this.config.llm);
|
|
441
713
|
const child = new Engine({
|
|
442
|
-
llm:
|
|
714
|
+
llm: childLlm,
|
|
715
|
+
// Inherit parent's runtime knobs (temperature, image detail, timeouts)
|
|
716
|
+
// but cap sub-agent retries at 2 — they're short-lived and we'd
|
|
717
|
+
// rather surface failures than burn a 9 s exponential backoff loop.
|
|
718
|
+
clientDefaults: { ...(this.config.clientDefaults ?? {}), retryMaxAttempts: 2 },
|
|
443
719
|
cwd,
|
|
444
720
|
permissionMode: this.config.permissionMode,
|
|
445
721
|
preset: this.preset.name,
|
|
@@ -449,10 +725,14 @@ export class Engine {
|
|
|
449
725
|
appendSystemPrompt: [this.config.appendSystemPrompt, req.appendSystemPrompt]
|
|
450
726
|
.filter(Boolean)
|
|
451
727
|
.join("\n\n") || undefined,
|
|
728
|
+
responseLanguage: this.config.responseLanguage,
|
|
729
|
+
userProfile: this.config.userProfile,
|
|
730
|
+
instructions: this.config.instructions,
|
|
452
731
|
maxTurns: req.maxTurns,
|
|
453
732
|
maxContextTokens: this.config.maxContextTokens ?? 200_000,
|
|
454
733
|
sessionStorageDir: this.config.sessionStorageDir,
|
|
455
734
|
headless: this.config.headless,
|
|
735
|
+
readOnlySession: req.readOnlySession,
|
|
456
736
|
sandbox: this.config.sandbox,
|
|
457
737
|
// Subagents inherit the parent's scope: a child runs in the same
|
|
458
738
|
// cwd/session, so it should see the same config layers the parent did.
|
|
@@ -504,11 +784,11 @@ export class Engine {
|
|
|
504
784
|
// `auto` mode handles its own downgrade with a one-time warning
|
|
505
785
|
// inside resolveSandboxBackend; explicit modes do not.
|
|
506
786
|
//
|
|
507
|
-
// Backend is cached
|
|
508
|
-
//
|
|
787
|
+
// Backend is cached per runtime/engine so the capability probe runs once
|
|
788
|
+
// per (mode, cwd) instead of every turn.
|
|
509
789
|
const sandboxBackend = this.runtime
|
|
510
790
|
? await this.runtime.resolveSandbox(sandboxConfig, cwd)
|
|
511
|
-
: await
|
|
791
|
+
: await this.resolveSandboxWithoutRuntime(sandboxConfig, cwd);
|
|
512
792
|
// sessionId is filled in after the session bundle is resolved below
|
|
513
793
|
// (the session may be cold-started or resumed). Until then this is
|
|
514
794
|
// intentionally shaped as a mutable local; we treat it as immutable
|
|
@@ -570,9 +850,8 @@ export class Engine {
|
|
|
570
850
|
let messages;
|
|
571
851
|
if (options?.sessionId && this.sessionManager.exists(options.sessionId)) {
|
|
572
852
|
session = this.sessionManager.resume(options.sessionId);
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
: session.transcript.toMessages();
|
|
853
|
+
const cachedCompacted = this.compactedMessagesBySession.get(options.sessionId);
|
|
854
|
+
messages = cachedCompacted ? [...cachedCompacted] : session.transcript.toMessages();
|
|
576
855
|
// If the previous run was Ctrl+C'd or crashed between an assistant
|
|
577
856
|
// tool_use and the matching tool_result being persisted, the
|
|
578
857
|
// loaded sequence is invalid for OpenAI (which 400s on dangling
|
|
@@ -604,7 +883,7 @@ export class Engine {
|
|
|
604
883
|
else {
|
|
605
884
|
// Cold start: shape (2) reuses the host-supplied sid; shape (3)
|
|
606
885
|
// lets sessionManager generate one with nanoid.
|
|
607
|
-
session = this.sessionManager.create(cwd, this.config.llm.model, this.config.llm.provider, options?.sessionId);
|
|
886
|
+
session = this.sessionManager.create(cwd, this.config.llm.model, this.config.llm.provider, options?.sessionId, this.config.isSubAgent === true ? getCurrentSid() : undefined, this.config.isSubAgent === true ? "subagent" : this.config.origin);
|
|
608
887
|
messages = [{ role: "user", content: userMessageContent }];
|
|
609
888
|
session.transcript.appendMessage("user", userMessageContent);
|
|
610
889
|
// Save first user message as session summary — text only. The summary
|
|
@@ -730,7 +1009,7 @@ export class Engine {
|
|
|
730
1009
|
}
|
|
731
1010
|
}
|
|
732
1011
|
// Kick off LLM client creation early (network handshake)
|
|
733
|
-
const llmClientPromise = createLLMClient(this.config.llm);
|
|
1012
|
+
const llmClientPromise = createLLMClient(this.config.llm, this.config.clientDefaults);
|
|
734
1013
|
const mode = this.config.permissionMode ?? "acceptEdits";
|
|
735
1014
|
const { rules: defaultRules, backend: approvalBackend } = this.buildPermissionConfig(mode, cwd);
|
|
736
1015
|
const permission = new PermissionClassifier(defaultRules, mode, approvalBackend);
|
|
@@ -751,8 +1030,12 @@ export class Engine {
|
|
|
751
1030
|
}
|
|
752
1031
|
const toolExecutor = new ToolExecutor(this.toolRegistry, permission, this.hooks);
|
|
753
1032
|
const investigationGuard = new InvestigationGuard();
|
|
754
|
-
if (this.config.
|
|
1033
|
+
if (this.config.readOnlySession) {
|
|
1034
|
+
investigationGuard.setPolicy("read-only-review");
|
|
1035
|
+
}
|
|
1036
|
+
else if (this.config.headless) {
|
|
755
1037
|
investigationGuard.setSoftMode(true);
|
|
1038
|
+
}
|
|
756
1039
|
toolExecutor.setInvestigationGuard(investigationGuard);
|
|
757
1040
|
toolExecutor.setTaskGuard(new TaskGuard(() => latestTodos));
|
|
758
1041
|
// Wire abort signal for cascading cancellation + per-Engine ToolContext
|
|
@@ -769,6 +1052,9 @@ export class Engine {
|
|
|
769
1052
|
preset: this.preset,
|
|
770
1053
|
customSystemPrompt: this.config.customSystemPrompt,
|
|
771
1054
|
appendSystemPrompt: this.config.appendSystemPrompt,
|
|
1055
|
+
responseLanguage: this.config.responseLanguage,
|
|
1056
|
+
userProfile: this.config.userProfile,
|
|
1057
|
+
instructionOptions: { compatFileNames: compatFileNamesFrom(this.config.instructions) },
|
|
772
1058
|
disabledSkills,
|
|
773
1059
|
disabledPlugins,
|
|
774
1060
|
});
|
|
@@ -791,27 +1077,58 @@ export class Engine {
|
|
|
791
1077
|
// 1. createLLMClient — network handshake (started earlier)
|
|
792
1078
|
// 2. buildSystemPrompt — includes git status (3 execSync calls)
|
|
793
1079
|
// 3. buildSystemContext — reads environment context
|
|
794
|
-
|
|
795
|
-
//
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1080
|
+
// Inject the live available-agent-types listing into the Agent tool's
|
|
1081
|
+
// description. The registry is per-engine (loaded from .code-shell/agents
|
|
1082
|
+
// for this cwd), so it can't live in the static tool def — without this
|
|
1083
|
+
// the model never learns the reusable roles exist and spawns nameless
|
|
1084
|
+
// ad-hoc agents instead (the Core A/B/C incident).
|
|
1085
|
+
// The Agent tool is always available: with configured roles, an omitted
|
|
1086
|
+
// agent_type falls back to one of them (see resolveAgentTypeOverrides); with
|
|
1087
|
+
// no roles configured it runs a true ephemeral agent, so workflows that need
|
|
1088
|
+
// sub-agents (e.g. superpowers) work in any project.
|
|
1089
|
+
// Availability guard (tool-visibility): a gated builtin (WebSearch needs a
|
|
1090
|
+
// search provider, GenerateImage needs an OpenAI provider) is hidden from
|
|
1091
|
+
// the toolDefs the model sees when its credential isn't configured for this
|
|
1092
|
+
// cwd. Recomputed every message, so configuring a key takes effect on the
|
|
1093
|
+
// NEXT message without a restart. Tools with no guard entry are always kept.
|
|
1094
|
+
const guardCwd = toolCtx.cwd;
|
|
1095
|
+
// #7: per-turn project builtin override. The toolRegistry's builtin tool
|
|
1096
|
+
// SET is ctor-frozen (and may be shared via runtime), so a mid-session
|
|
1097
|
+
// project override of a builtin can't rebuild the registry. But the tool
|
|
1098
|
+
// LIST handed to the LLM is assembled fresh every turn, so we apply the
|
|
1099
|
+
// override here: a builtin marked `off` for this cwd is HIDDEN from the
|
|
1100
|
+
// turn's tool list (matching how skills/plugins/agents `off` apply
|
|
1101
|
+
// mid-session via readDisabledLists). `on`/`inherit` keep whatever the
|
|
1102
|
+
// registry already has — we can't re-add a tool the frozen registry omits,
|
|
1103
|
+
// but `on` for a tool already present is a no-op (it stays). This makes a
|
|
1104
|
+
// builtin toggle take effect on the NEXT message, like other capability
|
|
1105
|
+
// kinds, without touching the registry.
|
|
1106
|
+
const builtinOverride = this.readBuiltinOverride(guardCwd);
|
|
1107
|
+
// Turn `off` from a prompt-visibility filter into a real execution gate:
|
|
1108
|
+
// collect the builtin tool names the override marks `off` and hand them to
|
|
1109
|
+
// the executor (via the shared toolCtx the executor already holds a
|
|
1110
|
+
// reference to, set at setContext above) so it rejects a call to a hidden
|
|
1111
|
+
// builtin instead of running it from the still-populated registry.
|
|
1112
|
+
if (builtinOverride) {
|
|
1113
|
+
const registryNames = new Set(this.toolRegistry.getToolDefinitions().map((t) => t.name));
|
|
1114
|
+
const disabledBuiltins = new Set(Object.keys(builtinOverride).filter((name) => builtinOverride[name] === "off" && registryNames.has(name)));
|
|
1115
|
+
toolCtx.disabledBuiltins = disabledBuiltins;
|
|
1116
|
+
}
|
|
1117
|
+
const allToolDefs = applyBuiltinOverrideVisibility(this.toolRegistry.getToolDefinitions(), builtinOverride)
|
|
1118
|
+
.filter((t) => {
|
|
1119
|
+
const guard = BUILTIN_TOOL_GUARDS.get(t.name);
|
|
1120
|
+
return guard ? guard(guardCwd) : true;
|
|
1121
|
+
})
|
|
1122
|
+
.map((t) => t.name === "Agent"
|
|
1123
|
+
? { ...t, description: agentToolDefWithTypes(toolCtx.agentDefinitions).description }
|
|
1124
|
+
: t);
|
|
1125
|
+
// In plan mode, only expose read-only/planning tools so the model won't
|
|
1126
|
+
// attempt writes. Shared with executor.ts's execution gate via
|
|
1127
|
+
// PLAN_MODE_ALLOWED_TOOLS so what the model SEES and what the executor
|
|
1128
|
+
// RUNS can't drift apart. (Bash is in the set; the executor additionally
|
|
1129
|
+
// gates Bash to read-only commands at call time.)
|
|
813
1130
|
const toolDefs = this.planMode
|
|
814
|
-
? allToolDefs.filter((t) =>
|
|
1131
|
+
? allToolDefs.filter((t) => PLAN_MODE_ALLOWED_TOOLS.has(t.name))
|
|
815
1132
|
: allToolDefs;
|
|
816
1133
|
const [llmClient, systemPrompt, systemContext] = await Promise.all([
|
|
817
1134
|
llmClientPromise,
|
|
@@ -848,8 +1165,15 @@ export class Engine {
|
|
|
848
1165
|
// run would be evaluated fresh and might get a different replacement
|
|
849
1166
|
// string than the one already in the message, breaking idempotency.
|
|
850
1167
|
contextManager.initReplacementStateFromMessages(messages);
|
|
1168
|
+
// Summarization (context-compaction + tool-result summaries) are auxiliary
|
|
1169
|
+
// calls — route them to the configured aux model so they don't burn the
|
|
1170
|
+
// expensive primary model every turn (same rationale as runMemoryPipeline).
|
|
1171
|
+
// Resolved once here (not per-call) so the magnetic-disk settings re-read
|
|
1172
|
+
// in resolveAuxClient stays off the compaction hot path. Falls back to the
|
|
1173
|
+
// primary client when no aux model is configured.
|
|
1174
|
+
const auxSummaryClient = await this.resolveAuxClient(llmClient);
|
|
851
1175
|
contextManager.setSummarizeFn(async (prompt) => {
|
|
852
|
-
const summaryResponse = await
|
|
1176
|
+
const summaryResponse = await auxSummaryClient.createMessage({
|
|
853
1177
|
systemPrompt: "You are a conversation summarizer. Be concise and factual.",
|
|
854
1178
|
messages: [{ role: "user", content: prompt }],
|
|
855
1179
|
tools: [],
|
|
@@ -857,7 +1181,7 @@ export class Engine {
|
|
|
857
1181
|
// Auxiliary call — no need to burn reasoning tokens. On DeepSeek V4
|
|
858
1182
|
// this flips thinking off (~3x faster, fewer tokens); on every other
|
|
859
1183
|
// OpenAI-compatible provider the field is ignored.
|
|
860
|
-
|
|
1184
|
+
reasoning: { mode: "off" },
|
|
861
1185
|
});
|
|
862
1186
|
return summaryResponse.text;
|
|
863
1187
|
});
|
|
@@ -873,14 +1197,14 @@ export class Engine {
|
|
|
873
1197
|
// tracker so session_end.cost reflects only the user-facing turns and
|
|
874
1198
|
// turns/requestCount stay aligned.
|
|
875
1199
|
modelFacade.summarize = async (sysPrompt, userMsg) => {
|
|
876
|
-
const resp = await
|
|
1200
|
+
const resp = await auxSummaryClient.createMessage({
|
|
877
1201
|
systemPrompt: sysPrompt,
|
|
878
1202
|
messages: [{ role: "user", content: userMsg }],
|
|
879
1203
|
tools: [],
|
|
880
1204
|
maxTokens: 256,
|
|
881
1205
|
recordUsage: false,
|
|
882
1206
|
// Auxiliary call — see contextManager.setSummarizeFn above.
|
|
883
|
-
|
|
1207
|
+
reasoning: { mode: "off" },
|
|
884
1208
|
});
|
|
885
1209
|
logger.debug("summarize.call", {
|
|
886
1210
|
sysPromptLen: sysPrompt.length,
|
|
@@ -895,7 +1219,7 @@ export class Engine {
|
|
|
895
1219
|
return resp.text;
|
|
896
1220
|
};
|
|
897
1221
|
// File history: auto-backup before Write/Edit
|
|
898
|
-
const sessionDir = join(this.config.sessionStorageDir ?? join(
|
|
1222
|
+
const sessionDir = join(this.config.sessionStorageDir ?? join(userHome(), ".code-shell", "sessions"), session.state.sessionId);
|
|
899
1223
|
const fileHistory = FileHistory.loadFromDir(sessionDir);
|
|
900
1224
|
this.hooks.register("on_tool_start", async (context) => {
|
|
901
1225
|
const toolName = context.data?.toolName;
|
|
@@ -911,6 +1235,24 @@ export class Engine {
|
|
|
911
1235
|
task,
|
|
912
1236
|
model: this.config.llm.model,
|
|
913
1237
|
});
|
|
1238
|
+
// Goal mode: register a GoalStopHook for the lifetime of THIS run so the
|
|
1239
|
+
// turn loop keeps going until the session model judges the goal met.
|
|
1240
|
+
// Registered per-run (and cleared in `finally`) so a later goal-less
|
|
1241
|
+
// send doesn't inherit a stale goal. The judge reuses `llmClient` — the
|
|
1242
|
+
// same model this session is talking to (per design).
|
|
1243
|
+
// Normalize the raw goal (string | GoalConfig) once at the run boundary;
|
|
1244
|
+
// everything inward uses the GoalConfig. normalizeGoal() returns undefined
|
|
1245
|
+
// when there's effectively no goal (empty objective).
|
|
1246
|
+
const normalizedGoal = normalizeGoal(options?.goal ?? this.config.goal);
|
|
1247
|
+
let goalHookHandler = null;
|
|
1248
|
+
if (normalizedGoal && this.config.isSubAgent !== true) {
|
|
1249
|
+
goalHookHandler = createGoalStopHook({
|
|
1250
|
+
goal: normalizedGoal,
|
|
1251
|
+
llm: llmClient,
|
|
1252
|
+
log: logger,
|
|
1253
|
+
});
|
|
1254
|
+
this.hooks.register("on_stop", goalHookHandler, 0, "goal-stop");
|
|
1255
|
+
}
|
|
914
1256
|
// Surface compaction events to the UI so the user knows when context was trimmed.
|
|
915
1257
|
// Buffer the most recent event so TurnLoop can drain it and emit the
|
|
916
1258
|
// post_compact hook on the next turn (ContextManager itself doesn't
|
|
@@ -947,6 +1289,9 @@ export class Engine {
|
|
|
947
1289
|
maxToolCallsPerTurn: this.config.maxToolCallsPerTurn ?? 10,
|
|
948
1290
|
onStream: options?.onStream,
|
|
949
1291
|
signal: options?.signal,
|
|
1292
|
+
// Goal mode: the active goal is surfaced to the on_stop handler via
|
|
1293
|
+
// ctx.data.goal; the GoalStopHook (registered above) judges it.
|
|
1294
|
+
goal: normalizedGoal,
|
|
950
1295
|
// Heartbeat: flush turnCount + tokens to state.json after every turn
|
|
951
1296
|
// so external observers (other CLI processes, /sid, the session list)
|
|
952
1297
|
// see live progress instead of a stale snapshot from the last
|
|
@@ -965,7 +1310,59 @@ export class Engine {
|
|
|
965
1310
|
this.sessionManager.saveState(session.state);
|
|
966
1311
|
},
|
|
967
1312
|
});
|
|
968
|
-
|
|
1313
|
+
let result;
|
|
1314
|
+
try {
|
|
1315
|
+
result = await turnLoop.run(messages);
|
|
1316
|
+
// ── Wait for background sub-agents, then summarize ───────────────
|
|
1317
|
+
// run_in_background sub-agents outlive the turn that spawned them. The
|
|
1318
|
+
// main agent must not resolve while ITS OWN background agents are still
|
|
1319
|
+
// working — otherwise their results land in the notification queue with
|
|
1320
|
+
// nobody to drain them and the run looks "done" while work is in flight
|
|
1321
|
+
// (the s-mpvf4rsj-bb6e4639 bug). We block here until none of this
|
|
1322
|
+
// session's background agents are running, then drain ALL their results
|
|
1323
|
+
// and feed them back as one more turn so the agent summarizes.
|
|
1324
|
+
//
|
|
1325
|
+
// Top-level only: a sub-agent must never wait on grandchildren (and
|
|
1326
|
+
// nested agents are disabled anyway). `signal` aborts the wait.
|
|
1327
|
+
const sid = session.state.sessionId;
|
|
1328
|
+
const isTopLevel = this.config.isSubAgent !== true;
|
|
1329
|
+
if (isTopLevel) {
|
|
1330
|
+
let aborted = options?.signal?.aborted === true;
|
|
1331
|
+
while (!aborted && asyncAgentRegistry.hasRunningForSession(sid)) {
|
|
1332
|
+
aborted = await this.waitForBackgroundAgentChange(sid, options?.signal);
|
|
1333
|
+
}
|
|
1334
|
+
// Drain everything that came back — including partial results when the
|
|
1335
|
+
// user aborted with one agent still stuck. Nothing already returned is
|
|
1336
|
+
// lost: it's injected into the transcript either way.
|
|
1337
|
+
const pending = notificationQueue.drainAll(sid);
|
|
1338
|
+
if (pending.length > 0) {
|
|
1339
|
+
const injected = {
|
|
1340
|
+
role: "user",
|
|
1341
|
+
content: `<system-reminder>\n${buildNotificationMessage(pending)}\n</system-reminder>`,
|
|
1342
|
+
};
|
|
1343
|
+
if (aborted) {
|
|
1344
|
+
// Aborted: preserve the results in context (transcript + messages)
|
|
1345
|
+
// but do NOT spin up another LLM turn — the user cancelled, and a
|
|
1346
|
+
// fresh turn would just be killed by the same signal. The next
|
|
1347
|
+
// user message in this session will see these results in history.
|
|
1348
|
+
session.transcript.appendMessage(injected.role, injected.content);
|
|
1349
|
+
result = { ...result, messages: [...result.messages, injected] };
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
// All background agents finished: one more turn so the agent reads
|
|
1353
|
+
// every result and summarizes. turnCount keeps accumulating, so
|
|
1354
|
+
// maxTurns still bounds runaway re-summarization.
|
|
1355
|
+
result = await turnLoop.run([...result.messages, injected]);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
finally {
|
|
1361
|
+
// Run-scoped: drop the GoalStopHook so a later goal-less send on this
|
|
1362
|
+
// long-lived engine doesn't keep blocking stops.
|
|
1363
|
+
if (goalHookHandler)
|
|
1364
|
+
this.hooks.unregister("on_stop", goalHookHandler);
|
|
1365
|
+
}
|
|
969
1366
|
this.lastMessages = result.messages;
|
|
970
1367
|
this.compactedMessagesBySession.set(session.state.sessionId, this.stripUserContextMessage(result.messages, userContextMsg));
|
|
971
1368
|
logger.info("engine.done", {
|
|
@@ -992,6 +1389,31 @@ export class Engine {
|
|
|
992
1389
|
// transcript, save a session summary, and conditionally trigger
|
|
993
1390
|
// auto-dream consolidation. Doesn't block the Engine result.
|
|
994
1391
|
void this.runMemoryPipeline(session.transcript, session.state.sessionId, cwd, llmClient);
|
|
1392
|
+
// Fire-and-forget session title generation — only after the FIRST turn.
|
|
1393
|
+
// Reuses the already-resolved auxSummaryClient (aux model, cheap). Best-
|
|
1394
|
+
// effort: failures never touch the run result. The renderer writes the
|
|
1395
|
+
// title into the sidebar on receipt of the session_title stream event.
|
|
1396
|
+
{
|
|
1397
|
+
const messageEvents = session.transcript.getEvents("message");
|
|
1398
|
+
const userMsgEvents = messageEvents.filter((e) => e.data.role === "user");
|
|
1399
|
+
const userMsgCount = userMsgEvents.length;
|
|
1400
|
+
const onStream = options?.onStream;
|
|
1401
|
+
if (userMsgCount === 1 && onStream && result.text) {
|
|
1402
|
+
const rawContent = userMsgEvents[0]?.data?.content;
|
|
1403
|
+
const firstUserText = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent ?? "");
|
|
1404
|
+
void buildSessionTitle(auxSummaryClient, firstUserText, result.text)
|
|
1405
|
+
.then((title) => {
|
|
1406
|
+
if (title) {
|
|
1407
|
+
onStream({
|
|
1408
|
+
type: "session_title",
|
|
1409
|
+
sessionId: session.state.sessionId,
|
|
1410
|
+
title,
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
})
|
|
1414
|
+
.catch(() => { });
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
995
1417
|
// Update session state. Persist the raw terminal reason as the status so
|
|
996
1418
|
// callers can distinguish user-cancelled (aborted_streaming) from real
|
|
997
1419
|
// failures (model_error, prompt_too_long, ...) — previously every
|
|
@@ -1035,8 +1457,69 @@ export class Engine {
|
|
|
1035
1457
|
* task. Extracts durable memories from the transcript, saves a session
|
|
1036
1458
|
* summary, and conditionally triggers auto-dream consolidation.
|
|
1037
1459
|
*/
|
|
1038
|
-
|
|
1460
|
+
/**
|
|
1461
|
+
* Resolve the LLM client for background/auxiliary work (memory extraction,
|
|
1462
|
+
* auto-dream). When settings.auxModelKey names a valid pool model, build (and
|
|
1463
|
+
* cache) a dedicated client for it so per-turn book-keeping runs on a cheap
|
|
1464
|
+
* fast model instead of the expensive primary. Falls back to `fallback` (the
|
|
1465
|
+
* active run's client) when unset, unknown, or on any build failure — aux
|
|
1466
|
+
* work is best-effort and must never break a run.
|
|
1467
|
+
*/
|
|
1468
|
+
async resolveAuxClient(fallback) {
|
|
1469
|
+
let auxKey;
|
|
1470
|
+
try {
|
|
1471
|
+
// Re-read from disk: settings may have been changed by the desktop
|
|
1472
|
+
// (a separate process) since this worker last cached them. This runs
|
|
1473
|
+
// once per run on the post-run background path, so the cost is fine.
|
|
1474
|
+
const sm = this.getSettingsManager();
|
|
1475
|
+
sm.invalidate();
|
|
1476
|
+
auxKey = sm.get().auxModelKey;
|
|
1477
|
+
}
|
|
1478
|
+
catch {
|
|
1479
|
+
return fallback;
|
|
1480
|
+
}
|
|
1481
|
+
if (!auxKey)
|
|
1482
|
+
return fallback;
|
|
1483
|
+
// Don't spin up a second client when the aux key resolves to the SAME
|
|
1484
|
+
// client config as this engine's active model. Compare FULL LLM IDENTITY
|
|
1485
|
+
// (model + reasoning + maxTokens + baseUrl + provider/providerKind) against
|
|
1486
|
+
// this engine's own per-session config.llm — NOT a separately-tracked active
|
|
1487
|
+
// key, and NOT just the model NAME. Two distinct pool keys can share the same
|
|
1488
|
+
// `model` string yet differ in reasoning/maxOutputTokens/baseUrl/apiKey/
|
|
1489
|
+
// providerKey; de-duping on the name alone would wrongly route the user's
|
|
1490
|
+
// chosen aux entry onto the primary's config. config.llm is isolated per
|
|
1491
|
+
// session and always set for a real run, so this is correct even for desktop
|
|
1492
|
+
// worker sessions built with a shared runtime (which never explicitly
|
|
1493
|
+
// switchModel, so the old activeModelKey field was undefined and defeated the
|
|
1494
|
+
// de-dup), AND immune to another session mutating the shared pool's activeKey.
|
|
1495
|
+
const entry = this.modelPool.get(auxKey);
|
|
1496
|
+
if (entry && sameLlmIdentity(this.modelPool.toLLMConfig(entry), this.config.llm)) {
|
|
1497
|
+
return fallback;
|
|
1498
|
+
}
|
|
1499
|
+
if (this.auxClientCache?.key === auxKey)
|
|
1500
|
+
return this.auxClientCache.client;
|
|
1501
|
+
if (!entry) {
|
|
1502
|
+
logger.warn("engine.aux_model_missing", { auxModelKey: auxKey });
|
|
1503
|
+
return fallback;
|
|
1504
|
+
}
|
|
1505
|
+
try {
|
|
1506
|
+
const client = await createLLMClient(this.modelPool.toLLMConfig(entry), this.config.clientDefaults);
|
|
1507
|
+
this.auxClientCache = { key: auxKey, client };
|
|
1508
|
+
return client;
|
|
1509
|
+
}
|
|
1510
|
+
catch (err) {
|
|
1511
|
+
logger.warn("engine.aux_model_build_failed", {
|
|
1512
|
+
auxModelKey: auxKey,
|
|
1513
|
+
error: err.message,
|
|
1514
|
+
});
|
|
1515
|
+
return fallback;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
async runMemoryPipeline(transcript, sessionId, cwd, primaryClient) {
|
|
1039
1519
|
try {
|
|
1520
|
+
// Background calls run on the auxiliary model when configured, so memory
|
|
1521
|
+
// book-keeping doesn't burn the expensive primary model every turn.
|
|
1522
|
+
const llmClient = await this.resolveAuxClient(primaryClient);
|
|
1040
1523
|
// Only run memory extraction for substantive sessions. The previous
|
|
1041
1524
|
// threshold of 4 user+assistant messages was low enough that two-line
|
|
1042
1525
|
// exchanges ("what's the time?" / "noon") triggered a full LLM
|
|
@@ -1069,7 +1552,7 @@ export class Engine {
|
|
|
1069
1552
|
tools: [],
|
|
1070
1553
|
maxTokens: 1024,
|
|
1071
1554
|
recordUsage: false,
|
|
1072
|
-
|
|
1555
|
+
reasoning: { mode: "off" },
|
|
1073
1556
|
});
|
|
1074
1557
|
return resp.text;
|
|
1075
1558
|
},
|
|
@@ -1103,117 +1586,20 @@ export class Engine {
|
|
|
1103
1586
|
* bailed before the first LLM call (e.g. registry missing the tools).
|
|
1104
1587
|
*/
|
|
1105
1588
|
async runDreamLoop(opts) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
return false;
|
|
1118
|
-
}
|
|
1119
|
-
// Strip RegisteredTool down to the shape createMessage expects.
|
|
1120
|
-
const toolDefs = memoryTools.map((t) => ({
|
|
1121
|
-
name: t.name,
|
|
1122
|
-
description: t.description,
|
|
1123
|
-
inputSchema: t.inputSchema,
|
|
1124
|
-
}));
|
|
1125
|
-
const toolCtx = {
|
|
1126
|
-
...this.buildToolContext(),
|
|
1127
|
-
cwd: opts.projectDir ?? process.cwd(),
|
|
1128
|
-
};
|
|
1129
|
-
const messages = [{ role: "user", content: opts.userPrompt }];
|
|
1130
|
-
let writeBudget = MAX_WRITES;
|
|
1131
|
-
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
1132
|
-
const resp = await opts.llmClient.createMessage({
|
|
1133
|
-
systemPrompt: opts.systemPrompt,
|
|
1134
|
-
messages,
|
|
1135
|
-
tools: toolDefs,
|
|
1136
|
-
maxTokens: 2048,
|
|
1137
|
-
recordUsage: false,
|
|
1138
|
-
thinking: "disabled",
|
|
1139
|
-
});
|
|
1140
|
-
if (resp.toolCalls.length === 0) {
|
|
1141
|
-
logger.info("memory.auto_dream_finished", {
|
|
1142
|
-
sessionId: opts.sessionId,
|
|
1143
|
-
turn,
|
|
1144
|
-
finalText: resp.text.slice(0, 500),
|
|
1145
|
-
});
|
|
1146
|
-
return true;
|
|
1147
|
-
}
|
|
1148
|
-
// Echo the assistant turn back into the conversation so subsequent
|
|
1149
|
-
// turns see the tool_use ids they need to reference.
|
|
1150
|
-
const assistantContent = [];
|
|
1151
|
-
if (resp.text)
|
|
1152
|
-
assistantContent.push({ type: "text", text: resp.text });
|
|
1153
|
-
for (const tc of resp.toolCalls) {
|
|
1154
|
-
assistantContent.push({
|
|
1155
|
-
type: "tool_use",
|
|
1156
|
-
id: tc.id,
|
|
1157
|
-
name: tc.toolName,
|
|
1158
|
-
input: tc.args,
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1161
|
-
messages.push({ role: "assistant", content: assistantContent });
|
|
1162
|
-
// Dispatch every tool call requested in this turn.
|
|
1163
|
-
const toolResults = [];
|
|
1164
|
-
for (const tc of resp.toolCalls) {
|
|
1165
|
-
const result = await this.dispatchDreamTool(tc, toolCtx, () => {
|
|
1166
|
-
if (writeBudget <= 0)
|
|
1167
|
-
return false;
|
|
1168
|
-
writeBudget--;
|
|
1169
|
-
return true;
|
|
1170
|
-
});
|
|
1171
|
-
toolResults.push({
|
|
1172
|
-
type: "tool_result",
|
|
1173
|
-
tool_use_id: tc.id,
|
|
1174
|
-
content: result,
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
messages.push({ role: "user", content: toolResults });
|
|
1178
|
-
}
|
|
1179
|
-
logger.warn("memory.auto_dream_hit_turn_cap", {
|
|
1589
|
+
// The loop body now lives in services/dream-consolidation.ts so it can
|
|
1590
|
+
// also be driven from the desktop host's manual "整理 / Dream" trigger.
|
|
1591
|
+
// The orchestrator built systemPrompt/userPrompt from this engine's
|
|
1592
|
+
// MemoryManager already; runDreamConsolidation rebuilds them from the same
|
|
1593
|
+
// projectDir, so passing them here would be redundant — we just hand it the
|
|
1594
|
+
// tool registry + a memory-scoped tool context.
|
|
1595
|
+
const { ran } = await runDreamConsolidation({
|
|
1596
|
+
llmClient: opts.llmClient,
|
|
1597
|
+
toolRegistry: this.toolRegistry,
|
|
1598
|
+
toolContext: this.buildToolContext(),
|
|
1599
|
+
projectDir: opts.projectDir,
|
|
1180
1600
|
sessionId: opts.sessionId,
|
|
1181
|
-
maxTurns: MAX_TURNS,
|
|
1182
1601
|
});
|
|
1183
|
-
return
|
|
1184
|
-
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Execute one memory tool call inside the dream loop. Enforces the two
|
|
1187
|
-
* dream-loop invariants the prompt also states:
|
|
1188
|
-
* - Only the 4 memory tools are dispatchable.
|
|
1189
|
-
* - Save/Delete in "user" scope is refused (returned as a tool error)
|
|
1190
|
-
* because dream runs without an interactive permission backend.
|
|
1191
|
-
*/
|
|
1192
|
-
async dispatchDreamTool(tc, ctx, consumeWriteBudget) {
|
|
1193
|
-
const allowed = new Set(["MemoryList", "MemoryRead", "MemorySave", "MemoryDelete"]);
|
|
1194
|
-
if (!allowed.has(tc.toolName)) {
|
|
1195
|
-
return `Error: tool "${tc.toolName}" is not available in the dream loop`;
|
|
1196
|
-
}
|
|
1197
|
-
const isWrite = tc.toolName === "MemorySave" || tc.toolName === "MemoryDelete";
|
|
1198
|
-
if (isWrite) {
|
|
1199
|
-
const scope = tc.args?.scope;
|
|
1200
|
-
if (scope !== "dream") {
|
|
1201
|
-
return (`Error: dream loop may only write to scope "dream", got "${scope}". ` +
|
|
1202
|
-
`User-scope changes require interactive permission, which is not available here.`);
|
|
1203
|
-
}
|
|
1204
|
-
if (!consumeWriteBudget()) {
|
|
1205
|
-
return "Error: dream write budget exhausted — stop calling write tools and summarize instead.";
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
try {
|
|
1209
|
-
const result = await this.toolRegistry.executeTool(tc.toolName, tc.args, { ctx });
|
|
1210
|
-
if (result.isError)
|
|
1211
|
-
return result.error ?? `Error executing ${tc.toolName}`;
|
|
1212
|
-
return result.result ?? "";
|
|
1213
|
-
}
|
|
1214
|
-
catch (err) {
|
|
1215
|
-
return `Error executing ${tc.toolName}: ${err.message}`;
|
|
1216
|
-
}
|
|
1602
|
+
return ran;
|
|
1217
1603
|
}
|
|
1218
1604
|
getToolRegistry() {
|
|
1219
1605
|
return this.toolRegistry;
|
|
@@ -1229,7 +1615,10 @@ export class Engine {
|
|
|
1229
1615
|
*/
|
|
1230
1616
|
switchModel(key) {
|
|
1231
1617
|
const entry = this.modelPool.switch(key);
|
|
1232
|
-
|
|
1618
|
+
// LLMConfig is pure model identity now — rotate it wholesale. Cross-model
|
|
1619
|
+
// runtime knobs (temperature/timeout/retryMaxAttempts/imageDetail) live on
|
|
1620
|
+
// this.config.clientDefaults and survive the switch untouched.
|
|
1621
|
+
const nextLlm = this.modelPool.toLLMConfig(entry);
|
|
1233
1622
|
this.config = { ...this.config, llm: nextLlm };
|
|
1234
1623
|
this.persistActiveModel(entry, nextLlm);
|
|
1235
1624
|
return entry;
|
|
@@ -1245,7 +1634,11 @@ export class Engine {
|
|
|
1245
1634
|
*/
|
|
1246
1635
|
persistActiveModel(entry, llm) {
|
|
1247
1636
|
try {
|
|
1248
|
-
|
|
1637
|
+
// userHome() (not raw homedir()) so a test that sets process.env.HOME to
|
|
1638
|
+
// a tmpdir gets its writes isolated too — the SettingsManager reader
|
|
1639
|
+
// already honors HOME; the writer must match or tests pollute the real
|
|
1640
|
+
// ~/.code-shell/settings.json (this happened: an A-key/model-a leak).
|
|
1641
|
+
const dir = join(userHome(), ".code-shell");
|
|
1249
1642
|
const file = join(dir, "settings.json");
|
|
1250
1643
|
mkdirSync(dir, { recursive: true });
|
|
1251
1644
|
let existing = {};
|
|
@@ -1303,6 +1696,83 @@ export class Engine {
|
|
|
1303
1696
|
getConfig() {
|
|
1304
1697
|
return this.config;
|
|
1305
1698
|
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Config hot-reload "layer 2": merge a disk-default config patch into this
|
|
1701
|
+
* ALREADY-RUNNING session's `this.config`, reload settings hooks, and
|
|
1702
|
+
* incrementally connect any newly-added MCP servers. Applied at the next
|
|
1703
|
+
* turn boundary — an in-flight turn is NOT interrupted: it keeps using the
|
|
1704
|
+
* PromptComposer it was built with, and the next turn rebuilds the composer
|
|
1705
|
+
* from the freshly-merged config (composer is rebuilt per-turn).
|
|
1706
|
+
*
|
|
1707
|
+
* `version` is a monotonic counter from the server: stale (<=last applied)
|
|
1708
|
+
* payloads are dropped so out-of-order reload deliveries can't let an older
|
|
1709
|
+
* config clobber a newer one (Q5).
|
|
1710
|
+
*
|
|
1711
|
+
* MCP: only connects (idempotent — already-connected servers are skipped);
|
|
1712
|
+
* never disconnects, so an in-flight tool call on an existing server is
|
|
1713
|
+
* never severed (Q3). Removed servers are deferred to the next session
|
|
1714
|
+
* rebuild. If mcpManager isn't built yet (no MCP run has happened), the
|
|
1715
|
+
* new servers will be connected on the next run via the existing per-run
|
|
1716
|
+
* connectAll path — so we skip the connect here.
|
|
1717
|
+
*
|
|
1718
|
+
* Preset (#2): a preset hot-reload re-resolves `this.preset` so the next-turn
|
|
1719
|
+
* PromptComposer picks up the new preset's system prompt / behavior — that's
|
|
1720
|
+
* the main user-visible preset effect and it IS hot. The toolRegistry's
|
|
1721
|
+
* builtin tool SET, however, is ctor-frozen (and may be shared via runtime):
|
|
1722
|
+
* it is NOT rebuilt here. So a preset change that alters the builtin tool set
|
|
1723
|
+
* (e.g. general → terminal-coding adds LSP/Brief) only takes effect on the
|
|
1724
|
+
* next session restart; we log a warning when that case is detected.
|
|
1725
|
+
*
|
|
1726
|
+
* disk-default-vs-slice caveat (#8): the patch carries pure DISK-default
|
|
1727
|
+
* values (preset/customSystemPrompt/appendSystemPrompt/responseLanguage/
|
|
1728
|
+
* userProfile — see diskDefaultsFrom). Spreading them here OVERRIDES any
|
|
1729
|
+
* per-request slice override of the same field. This is correct for the
|
|
1730
|
+
* desktop host today (its per-request slice only carries permissionMode+cwd,
|
|
1731
|
+
* which are excluded from the disk patch). A future host that sets
|
|
1732
|
+
* slice.preset (or the other prompt fields) per-request MUST exclude those
|
|
1733
|
+
* from the reload patch — or track per-request overrides separately — or this
|
|
1734
|
+
* reload will clobber them back to disk values.
|
|
1735
|
+
*/
|
|
1736
|
+
refreshRuntimeConfig(patch, version) {
|
|
1737
|
+
if (version <= this.lastAppliedConfigVersion)
|
|
1738
|
+
return;
|
|
1739
|
+
const prevServers = this.config.mcpServers ?? {};
|
|
1740
|
+
const prevPresetName = this.preset.name;
|
|
1741
|
+
this.config = { ...this.config, ...patch };
|
|
1742
|
+
// #2: re-resolve the prompt-affecting preset so the next-turn PromptComposer
|
|
1743
|
+
// (rebuilt per turn from this.preset) reflects the new preset's system
|
|
1744
|
+
// prompt / behavior. Only when the preset actually changed.
|
|
1745
|
+
if (patch.preset !== undefined && patch.preset !== prevPresetName) {
|
|
1746
|
+
const nextPreset = resolveAgentPreset(this.config.preset);
|
|
1747
|
+
// The builtin tool SET is ctor-frozen and may be shared via runtime — we
|
|
1748
|
+
// do NOT rebuild it here. If the new preset implies a different builtin
|
|
1749
|
+
// tool set, that part of the change only lands on session restart.
|
|
1750
|
+
const prevTools = resolveBuiltinToolNames({ preset: prevPresetName }).slice().sort().join(",");
|
|
1751
|
+
const nextTools = resolveBuiltinToolNames({ preset: nextPreset.name }).slice().sort().join(",");
|
|
1752
|
+
if (prevTools !== nextTools) {
|
|
1753
|
+
logger.warn("engine.preset_reload.tool_set_change_needs_restart", {
|
|
1754
|
+
from: prevPresetName,
|
|
1755
|
+
to: nextPreset.name,
|
|
1756
|
+
note: "preset system prompt hot-reloaded; builtin tool-set change takes effect on session restart",
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
this.preset = nextPreset;
|
|
1760
|
+
}
|
|
1761
|
+
this.reloadHooks();
|
|
1762
|
+
if (patch.mcpServers && this.mcpManager) {
|
|
1763
|
+
const added = {};
|
|
1764
|
+
for (const [name, cfg] of Object.entries(patch.mcpServers)) {
|
|
1765
|
+
if (!(name in prevServers))
|
|
1766
|
+
added[name] = cfg;
|
|
1767
|
+
}
|
|
1768
|
+
if (Object.keys(added).length > 0) {
|
|
1769
|
+
// connectAll is idempotent (skips already-connected); fire-and-forget
|
|
1770
|
+
// so a slow server handshake never blocks the reload call.
|
|
1771
|
+
void this.mcpManager.connectAll(added);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
this.lastAppliedConfigVersion = version;
|
|
1775
|
+
}
|
|
1306
1776
|
/**
|
|
1307
1777
|
* Inject context into a session's transcript without triggering a LLM turn.
|
|
1308
1778
|
* The injected content appears as an assistant message so the LLM can see it
|
|
@@ -1332,7 +1802,6 @@ export class Engine {
|
|
|
1332
1802
|
if (!this.lastContextManager || !sessionId) {
|
|
1333
1803
|
return { before: 0, after: 0, strategy: "none (no active session)" };
|
|
1334
1804
|
}
|
|
1335
|
-
const { estimateTokens } = require("../context/compaction.js");
|
|
1336
1805
|
const sourceMessages = this.compactedMessagesBySession.get(sessionId) ??
|
|
1337
1806
|
this.sessionManager.resume(sessionId).transcript.toMessages();
|
|
1338
1807
|
const before = estimateTokens(sourceMessages);
|
|
@@ -1477,22 +1946,128 @@ export class Engine {
|
|
|
1477
1946
|
this.planMode = value;
|
|
1478
1947
|
}
|
|
1479
1948
|
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Block until a background agent's state changes (finishes / its result is
|
|
1951
|
+
* enqueued) or `signal` aborts. Resolves `true` if aborted, `false` on a
|
|
1952
|
+
* change. The caller re-checks `hasRunningForSession` after each wake, so a
|
|
1953
|
+
* spurious wake (another session's agent) just loops again.
|
|
1954
|
+
*
|
|
1955
|
+
* Subscribes to BOTH the registry AND the notification queue — and that's
|
|
1956
|
+
* load-bearing, not belt-and-suspenders. A completing agent calls
|
|
1957
|
+
* `markCompleted` (registry notify) and only THEN `enqueue` (queue notify),
|
|
1958
|
+
* as two separate statements. If we woke on the registry notify alone, the
|
|
1959
|
+
* loop could re-check, see no running agents, and `drainAll` BEFORE the
|
|
1960
|
+
* result was enqueued — silently losing the last agent's output. Waking on
|
|
1961
|
+
* the queue notify guarantees the item is already in the bucket. But a
|
|
1962
|
+
* *cancelled* agent marks-but-never-enqueues (by design), so we also need
|
|
1963
|
+
* the registry notify or a final cancel would hang the wait forever. Hence
|
|
1964
|
+
* both. Subscribe-before-await closes the check/wait race either way.
|
|
1965
|
+
*/
|
|
1966
|
+
waitForBackgroundAgentChange(_sessionId, signal) {
|
|
1967
|
+
if (signal?.aborted)
|
|
1968
|
+
return Promise.resolve(true);
|
|
1969
|
+
return new Promise((resolve) => {
|
|
1970
|
+
let settled = false;
|
|
1971
|
+
const finish = (aborted) => {
|
|
1972
|
+
if (settled)
|
|
1973
|
+
return;
|
|
1974
|
+
settled = true;
|
|
1975
|
+
unsubRegistry();
|
|
1976
|
+
unsubQueue();
|
|
1977
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1978
|
+
resolve(aborted);
|
|
1979
|
+
};
|
|
1980
|
+
const onAbort = () => finish(true);
|
|
1981
|
+
const unsubRegistry = asyncAgentRegistry.subscribe(() => finish(false));
|
|
1982
|
+
const unsubQueue = notificationQueue.subscribe(() => finish(false));
|
|
1983
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1480
1986
|
/**
|
|
1481
1987
|
* Sub-agent role registry for the given cwd, memoized per-cwd so the
|
|
1482
1988
|
* directory is read once rather than every turn. A new cwd (e.g. via
|
|
1483
1989
|
* run({ cwd })) reloads.
|
|
1484
1990
|
*/
|
|
1485
1991
|
getAgentDefinitions(cwd) {
|
|
1486
|
-
|
|
1487
|
-
|
|
1992
|
+
const disabledAgents = this.readDisabledAgents(cwd);
|
|
1993
|
+
const disabledPlugins = this.readDisabledLists().disabledPlugins;
|
|
1994
|
+
const disabledKey = [...disabledAgents, "::", ...disabledPlugins]
|
|
1995
|
+
.slice()
|
|
1996
|
+
.sort()
|
|
1997
|
+
.join(" ");
|
|
1998
|
+
if (this.agentDefsCache?.cwd !== cwd ||
|
|
1999
|
+
this.agentDefsCache.disabledKey !== disabledKey) {
|
|
2000
|
+
this.agentDefsCache = {
|
|
2001
|
+
cwd,
|
|
2002
|
+
disabledKey,
|
|
2003
|
+
reg: loadAgentDefinitionsForCwd(cwd, disabledAgents, disabledPlugins),
|
|
2004
|
+
};
|
|
1488
2005
|
}
|
|
1489
2006
|
return this.agentDefsCache.reg;
|
|
1490
2007
|
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Read settings.disabledAgents, folded with the project's
|
|
2010
|
+
* capabilityOverrides.agents overlay for `cwd`. Unlike disabledSkills,
|
|
2011
|
+
* sub-agents do NOT skip this — a disabled role must stay invisible
|
|
2012
|
+
* everywhere. The overlay lets a project force-enable a globally-disabled
|
|
2013
|
+
* role or force-disable a globally-enabled one (tri-state); read UNMERGED
|
|
2014
|
+
* (getForScope) so inherit survives. No cwd / no overlay → baseline
|
|
2015
|
+
* unchanged. Mirrors readDisabledLists (skills/plugins).
|
|
2016
|
+
*/
|
|
2017
|
+
readDisabledAgents(cwd) {
|
|
2018
|
+
try {
|
|
2019
|
+
const sm = this.getSettingsManager();
|
|
2020
|
+
const settings = sm.get();
|
|
2021
|
+
const baseline = Array.isArray(settings.disabledAgents) ? settings.disabledAgents : [];
|
|
2022
|
+
const overrides = cwd
|
|
2023
|
+
? sm.getForScope("project", cwd).capabilityOverrides
|
|
2024
|
+
: undefined;
|
|
2025
|
+
return effectiveDisabledList(baseline, overrides?.agents);
|
|
2026
|
+
}
|
|
2027
|
+
catch {
|
|
2028
|
+
return [];
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Read the project's capabilityOverrides.builtin bucket for `cwd`, read
|
|
2033
|
+
* UNMERGED (getForScope) so tri-state inherit survives. Sub-agents skip the
|
|
2034
|
+
* overlay (minimal surface, same as readDisabledLists) — their builtin lists
|
|
2035
|
+
* are already narrowed by resolveChildToolScope. No cwd / error → undefined,
|
|
2036
|
+
* so the caller's baseline builtin lists pass through unchanged.
|
|
2037
|
+
*/
|
|
2038
|
+
readBuiltinOverride(cwd) {
|
|
2039
|
+
if (this.config.isSubAgent === true || !cwd)
|
|
2040
|
+
return undefined;
|
|
2041
|
+
try {
|
|
2042
|
+
const overrides = this.getSettingsManager().getForScope("project", cwd)
|
|
2043
|
+
.capabilityOverrides;
|
|
2044
|
+
return overrides?.builtin;
|
|
2045
|
+
}
|
|
2046
|
+
catch {
|
|
2047
|
+
return undefined;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
1491
2050
|
/**
|
|
1492
2051
|
* Build a base ToolContext for this Engine. Used by run() (which then
|
|
1493
2052
|
* overlays turn-specific fields like sandbox and subAgentSpawner) and
|
|
1494
2053
|
* by tests that want a ToolContext without a full run() cycle.
|
|
1495
2054
|
*/
|
|
2055
|
+
resolveSandboxWithoutRuntime(config, cwd) {
|
|
2056
|
+
const key = `${config.mode}:${cwd}`;
|
|
2057
|
+
let cached = this.sandboxCache.get(key);
|
|
2058
|
+
if (!cached) {
|
|
2059
|
+
cached = resolveSandboxBackend(config, cwd);
|
|
2060
|
+
// Mirror EngineRuntime.resolveSandbox: don't cache a rejection, or an
|
|
2061
|
+
// explicit-mode probe that throws stays sticky until process restart even
|
|
2062
|
+
// after the user fixes the config.
|
|
2063
|
+
cached.catch(() => {
|
|
2064
|
+
if (this.sandboxCache.get(key) === cached)
|
|
2065
|
+
this.sandboxCache.delete(key);
|
|
2066
|
+
});
|
|
2067
|
+
this.sandboxCache.set(key, cached);
|
|
2068
|
+
}
|
|
2069
|
+
return cached;
|
|
2070
|
+
}
|
|
1496
2071
|
buildToolContext() {
|
|
1497
2072
|
const { disabledSkills, disabledPlugins } = this.readDisabledLists();
|
|
1498
2073
|
return {
|
|
@@ -1525,10 +2100,20 @@ export class Engine {
|
|
|
1525
2100
|
return { disabledSkills: [], disabledPlugins: [] };
|
|
1526
2101
|
}
|
|
1527
2102
|
try {
|
|
1528
|
-
const
|
|
2103
|
+
const sm = this.getSettingsManager();
|
|
2104
|
+
const settings = sm.get();
|
|
2105
|
+
// Fold the project capabilityOverrides over the global baseline so a
|
|
2106
|
+
// project can force-enable a globally-disabled skill/plugin or vice
|
|
2107
|
+
// versa. Read the project overlay UNMERGED (getForScope), not the merged
|
|
2108
|
+
// get(), so tri-state inheritance survives. No cwd / no overlay → the
|
|
2109
|
+
// baseline is returned unchanged (zero regression).
|
|
2110
|
+
const cwd = this.config.cwd;
|
|
2111
|
+
const overrides = cwd
|
|
2112
|
+
? sm.getForScope("project", cwd).capabilityOverrides
|
|
2113
|
+
: undefined;
|
|
1529
2114
|
return {
|
|
1530
|
-
disabledSkills: settings.disabledSkills ?? [],
|
|
1531
|
-
disabledPlugins: settings.disabledPlugins ?? [],
|
|
2115
|
+
disabledSkills: effectiveDisabledList(settings.disabledSkills ?? [], overrides?.skills),
|
|
2116
|
+
disabledPlugins: effectiveDisabledList(settings.disabledPlugins ?? [], overrides?.plugins),
|
|
1532
2117
|
};
|
|
1533
2118
|
}
|
|
1534
2119
|
catch {
|