@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/README.md +1 -1
- package/dist/cli.js +4685 -4614
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +374 -47
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +48 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +35 -0
- package/dist/types/live/visualizer.d.ts +30 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +321 -0
- package/dist/types/session/agent-session.d.ts +135 -753
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +51 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +24 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +158 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +7 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +10 -29
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli/usage-cli.ts +10 -1
- package/src/cli.ts +8 -0
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +16 -2
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +270 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +90 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +517 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +422 -0
- package/src/live/visualizer.ts +214 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.ts +28 -112
- package/src/modes/controllers/command-controller.ts +56 -15
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/input-controller.ts +47 -39
- package/src/modes/controllers/live-command-controller.ts +255 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +80 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +176 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +215 -136
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +343 -0
- package/src/session/agent-session.ts +1940 -12218
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +217 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +266 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.ts +6 -3
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +0 -46
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +980 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +257 -20
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +45 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +116 -86
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +14 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +9 -1
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +121 -29
- package/src/tts/streaming-player.ts +82 -283
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +3 -0
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
import type { Agent, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { logger, prompt, stringProperty } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { reset as resetCapabilities } from "../capability";
|
|
5
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
6
|
+
import { formatModelString } from "../config/model-resolver";
|
|
7
|
+
import type { Settings, SkillsSettings } from "../config/settings";
|
|
8
|
+
import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
|
|
9
|
+
import { CustomToolAdapter } from "../extensibility/custom-tools/wrapper";
|
|
10
|
+
import type { ExtensionRunner } from "../extensibility/extensions";
|
|
11
|
+
import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
12
|
+
import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "../extensibility/skills";
|
|
13
|
+
import type { LocalProtocolOptions } from "../internal-urls";
|
|
14
|
+
import { resolveMemoryBackend } from "../memory-backend/resolve";
|
|
15
|
+
import { MEMORY_BACKEND_TOOL_NAMES } from "../memory-backend/tool-names";
|
|
16
|
+
import type { MemoryBackendStartOptions } from "../memory-backend/types";
|
|
17
|
+
import xdevMountNoticePrompt from "../prompts/system/xdev-mount-notice.md" with { type: "text" };
|
|
18
|
+
import { usesCodexTaskPrompt } from "../task/prompt-policy";
|
|
19
|
+
import { isMCPToolName, normalizeToolNames } from "../tools/builtin-names";
|
|
20
|
+
import { wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
21
|
+
import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
22
|
+
import { isMountableUnderXdev, type XdevRegistry } from "../tools/xdev";
|
|
23
|
+
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
24
|
+
import { formatLocalCalendarDate } from "../utils/local-date";
|
|
25
|
+
import {
|
|
26
|
+
extractPermissionLocations,
|
|
27
|
+
getPermissionIntent,
|
|
28
|
+
PERMISSION_OPTIONS,
|
|
29
|
+
PERMISSION_OPTIONS_BY_ID,
|
|
30
|
+
PERMISSION_REQUIRED_TOOLS,
|
|
31
|
+
} from "./acp-permission-gate";
|
|
32
|
+
import type { ClientBridge, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
33
|
+
import type { CustomMessage } from "./messages";
|
|
34
|
+
import type { SessionManager } from "./session-manager";
|
|
35
|
+
|
|
36
|
+
/** Capabilities borrowed from the owning AgentSession. */
|
|
37
|
+
export interface SessionToolsHost {
|
|
38
|
+
agent: Agent;
|
|
39
|
+
sessionManager: SessionManager;
|
|
40
|
+
settings: Settings;
|
|
41
|
+
modelRegistry: ModelRegistry;
|
|
42
|
+
extensionRunner(): ExtensionRunner | undefined;
|
|
43
|
+
clientBridge(): ClientBridge | undefined;
|
|
44
|
+
agentKind(): "main" | "sub";
|
|
45
|
+
isDisposed(): boolean;
|
|
46
|
+
isStreaming(): boolean;
|
|
47
|
+
queuedMessageCount(): number;
|
|
48
|
+
planModeEnabled(): boolean;
|
|
49
|
+
model(): Model | undefined;
|
|
50
|
+
memoryBackendSession(): MemoryBackendStartOptions["session"];
|
|
51
|
+
clearInheritedProviderPromptCacheKey(): void;
|
|
52
|
+
clearMemoryPromotionSnapshot(): void;
|
|
53
|
+
captureMemoryPromotionSnapshot(prompt: string[]): void;
|
|
54
|
+
emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void;
|
|
55
|
+
notifyCommandMetadataChanged(): void;
|
|
56
|
+
localProtocolOptions(): LocalProtocolOptions;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SessionToolsOptions {
|
|
60
|
+
autoApprove?: boolean;
|
|
61
|
+
toolRegistry?: Map<string, AgentTool>;
|
|
62
|
+
createVibeTools?: () => AgentTool[];
|
|
63
|
+
createComputerTool?: () => Promise<AgentTool | null>;
|
|
64
|
+
builtInToolNames?: Iterable<string>;
|
|
65
|
+
presentationPinnedToolNames?: ReadonlySet<string>;
|
|
66
|
+
ensureWriteRegistered?: () => Promise<boolean>;
|
|
67
|
+
rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>;
|
|
68
|
+
getLocalCalendarDate?: () => string;
|
|
69
|
+
getMcpServerInstructions?: () => Map<string, string> | undefined;
|
|
70
|
+
xdevRegistry?: XdevRegistry;
|
|
71
|
+
initialMountedXdevToolNames?: string[];
|
|
72
|
+
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
73
|
+
baseSystemPrompt: string[];
|
|
74
|
+
skills?: Skill[];
|
|
75
|
+
skillWarnings?: SkillWarning[];
|
|
76
|
+
skillsSettings?: SkillsSettings;
|
|
77
|
+
skillsReloadable?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const XDEV_MOUNT_NOTICE_MESSAGE_TYPE = "xdev-mount-notice";
|
|
81
|
+
|
|
82
|
+
/** Owns tool registration, presentation, prompt rebuilding, skills, and permissions. */
|
|
83
|
+
export class SessionTools {
|
|
84
|
+
readonly #host: SessionToolsHost;
|
|
85
|
+
#autoApprove: boolean;
|
|
86
|
+
#toolRegistry: Map<string, AgentTool>;
|
|
87
|
+
#createVibeTools: (() => AgentTool[]) | undefined;
|
|
88
|
+
#createComputerTool: SessionToolsOptions["createComputerTool"];
|
|
89
|
+
#installedVibeToolNames = new Set<string>();
|
|
90
|
+
#builtInToolNames: Set<string>;
|
|
91
|
+
#rpcHostToolNames = new Set<string>();
|
|
92
|
+
#xdevRegistry: XdevRegistry | undefined;
|
|
93
|
+
#mountedXdevToolNames: Set<string>;
|
|
94
|
+
#pendingXdevMountDelta: { added: Set<string>; removed: Set<string> } | undefined;
|
|
95
|
+
#presentationPinnedToolNames: ReadonlySet<string> | undefined;
|
|
96
|
+
#runtimeSelectedToolNames: ReadonlySet<string> | undefined;
|
|
97
|
+
#baseSystemPrompt: string[];
|
|
98
|
+
#lastAppliedToolSignature: string | undefined;
|
|
99
|
+
#promptModelKey: string | undefined;
|
|
100
|
+
#rebuildSystemPrompt: SessionToolsOptions["rebuildSystemPrompt"];
|
|
101
|
+
#getLocalCalendarDate: () => string;
|
|
102
|
+
#getMcpServerInstructions: SessionToolsOptions["getMcpServerInstructions"];
|
|
103
|
+
#setActiveToolNames: SessionToolsOptions["setActiveToolNames"];
|
|
104
|
+
#ensureWriteRegistered: SessionToolsOptions["ensureWriteRegistered"];
|
|
105
|
+
#skills: Skill[];
|
|
106
|
+
#skillWarnings: SkillWarning[];
|
|
107
|
+
#skillsSettings: SkillsSettings | undefined;
|
|
108
|
+
#skillsReloadable: boolean;
|
|
109
|
+
#acpPermissionDecisions = new Map<string, "allow_always" | "reject_always">();
|
|
110
|
+
|
|
111
|
+
constructor(host: SessionToolsHost, options: SessionToolsOptions) {
|
|
112
|
+
this.#host = host;
|
|
113
|
+
this.#autoApprove = options.autoApprove === true;
|
|
114
|
+
this.#toolRegistry = options.toolRegistry ?? new Map();
|
|
115
|
+
this.#createVibeTools = options.createVibeTools;
|
|
116
|
+
this.#createComputerTool = options.createComputerTool;
|
|
117
|
+
this.#builtInToolNames = new Set(options.builtInToolNames ?? []);
|
|
118
|
+
this.#presentationPinnedToolNames = options.presentationPinnedToolNames;
|
|
119
|
+
this.#ensureWriteRegistered = options.ensureWriteRegistered;
|
|
120
|
+
this.#rebuildSystemPrompt = options.rebuildSystemPrompt;
|
|
121
|
+
this.#getLocalCalendarDate = options.getLocalCalendarDate ?? formatLocalCalendarDate;
|
|
122
|
+
this.#getMcpServerInstructions = options.getMcpServerInstructions;
|
|
123
|
+
this.#xdevRegistry = options.xdevRegistry;
|
|
124
|
+
this.#mountedXdevToolNames = new Set(options.initialMountedXdevToolNames ?? []);
|
|
125
|
+
this.#setActiveToolNames = options.setActiveToolNames;
|
|
126
|
+
this.#baseSystemPrompt = options.baseSystemPrompt;
|
|
127
|
+
this.#skills = options.skills ?? [];
|
|
128
|
+
this.#skillWarnings = options.skillWarnings ?? [];
|
|
129
|
+
this.#skillsSettings = options.skillsSettings;
|
|
130
|
+
this.#skillsReloadable = options.skillsReloadable ?? true;
|
|
131
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Mutable registry shared with controller hosts that inspect available tools. */
|
|
135
|
+
get registry(): Map<string, AgentTool> {
|
|
136
|
+
return this.#toolRegistry;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Current stable base system prompt. */
|
|
140
|
+
get baseSystemPrompt(): string[] {
|
|
141
|
+
return this.#baseSystemPrompt;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Replaces the controller-owned base prompt without applying it to the agent. */
|
|
145
|
+
setBaseSystemPrompt(prompt: string[]): void {
|
|
146
|
+
this.#baseSystemPrompt = prompt;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Skills currently rendered into the system prompt. */
|
|
150
|
+
get skills(): Skill[] {
|
|
151
|
+
return this.#skills;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Diagnostics produced while loading the current skills. */
|
|
155
|
+
get skillWarnings(): SkillWarning[] {
|
|
156
|
+
return this.#skillWarnings;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Settings snapshot used for the current skill discovery. */
|
|
160
|
+
get skillsSettings(): SkillsSettings | undefined {
|
|
161
|
+
return this.#skillsSettings;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Drops cached per-session ACP `allow_always`/`reject_always` decisions. */
|
|
165
|
+
clearAcpPermissionDecisions(): void {
|
|
166
|
+
this.#acpPermissionDecisions.clear();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Re-wraps active and mounted tools after the ACP client changes. */
|
|
170
|
+
refreshAcpPermissionGates(): void {
|
|
171
|
+
this.#acpPermissionDecisions.clear();
|
|
172
|
+
const activeTools = this.getActiveToolNames()
|
|
173
|
+
.map(name => this.#toolRegistry.get(name))
|
|
174
|
+
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
175
|
+
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
176
|
+
this.#host.agent.setTools(activeTools);
|
|
177
|
+
const mountedTools = [...this.#mountedXdevToolNames]
|
|
178
|
+
.map(name => this.#toolRegistry.get(name))
|
|
179
|
+
.filter((tool): tool is AgentTool => tool !== undefined)
|
|
180
|
+
.map(tool => this.#wrapToolForAcpPermission(tool));
|
|
181
|
+
this.#xdevRegistry?.reconcile(mountedTools);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#getActiveNonMCPToolNames(): string[] {
|
|
185
|
+
return this.getEnabledToolNames().filter(name => !isMCPToolName(name) && this.#toolRegistry.has(name));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Names of tools currently exposed at the top level. */
|
|
189
|
+
getActiveToolNames(): string[] {
|
|
190
|
+
return this.#host.agent.state.tools.map(t => t.name);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Enabled top-level and discoverable tool names. */
|
|
194
|
+
getEnabledToolNames(): string[] {
|
|
195
|
+
if (this.#mountedXdevToolNames.size === 0) return this.getActiveToolNames();
|
|
196
|
+
return [...this.getActiveToolNames(), ...this.#mountedXdevToolNames];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Names of dynamic tools mounted under `xd://`. */
|
|
200
|
+
getMountedXdevToolNames(): string[] {
|
|
201
|
+
return [...this.#mountedXdevToolNames];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Whether the edit tool is registered. */
|
|
205
|
+
get hasEditTool(): boolean {
|
|
206
|
+
return this.#toolRegistry.has("edit");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Looks up a registered tool by name. */
|
|
210
|
+
getToolByName(name: string): AgentTool | undefined {
|
|
211
|
+
return this.#toolRegistry.get(name);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Whether a registry entry came from a built-in factory. */
|
|
215
|
+
hasBuiltInTool(name: string): boolean {
|
|
216
|
+
return this.#builtInToolNames.has(name);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Names of every registered tool. */
|
|
220
|
+
getAllToolNames(): string[] {
|
|
221
|
+
return Array.from(this.#toolRegistry.keys());
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#wrapRuntimeTool(tool: AgentTool): AgentTool {
|
|
225
|
+
const wrapped = wrapToolWithMetaNotice(tool);
|
|
226
|
+
const extensionRunner = this.#host.extensionRunner();
|
|
227
|
+
return extensionRunner ? new ExtensionToolWrapper(wrapped, extensionRunner) : wrapped;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Installs and activates the ephemeral vibe tool set. */
|
|
231
|
+
async activateVibeTools(baseToolNames: string[]): Promise<void> {
|
|
232
|
+
const createVibeTools = this.#createVibeTools;
|
|
233
|
+
if (!createVibeTools) {
|
|
234
|
+
throw new Error("Vibe tools are unavailable in this session.");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const tools = createVibeTools();
|
|
238
|
+
const vibeToolNames = tools.map(tool => tool.name);
|
|
239
|
+
if (new Set(vibeToolNames).size !== vibeToolNames.length) {
|
|
240
|
+
throw new Error("Vibe tool names must be unique.");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const tool of tools) {
|
|
244
|
+
if (this.#toolRegistry.has(tool.name)) continue;
|
|
245
|
+
this.#toolRegistry.set(tool.name, this.#wrapRuntimeTool(tool));
|
|
246
|
+
this.#builtInToolNames.add(tool.name);
|
|
247
|
+
this.#installedVibeToolNames.add(tool.name);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await this.applyActiveToolsByName([...new Set([...baseToolNames, ...vibeToolNames])]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Uninstalls vibe tools and activates the replacement set. */
|
|
254
|
+
async deactivateVibeTools(nextToolNames: string[]): Promise<void> {
|
|
255
|
+
this.#uninstallVibeTools();
|
|
256
|
+
await this.applyActiveToolsByName(nextToolNames);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Removes vibe tools without restoring a source-session snapshot. */
|
|
260
|
+
async removeVibeToolsPreservingActive(): Promise<void> {
|
|
261
|
+
const removed = new Set(this.#installedVibeToolNames);
|
|
262
|
+
this.#uninstallVibeTools();
|
|
263
|
+
const nextActive = this.getActiveToolNames().filter(name => !removed.has(name));
|
|
264
|
+
await this.applyActiveToolsByName(nextActive);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#uninstallVibeTools(): void {
|
|
268
|
+
for (const name of this.#installedVibeToolNames) {
|
|
269
|
+
this.#toolRegistry.delete(name);
|
|
270
|
+
this.#builtInToolNames.delete(name);
|
|
271
|
+
}
|
|
272
|
+
this.#installedVibeToolNames.clear();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#getEditModeSession() {
|
|
276
|
+
return {
|
|
277
|
+
settings: this.#host.settings,
|
|
278
|
+
getActiveModelString: () => {
|
|
279
|
+
const model = this.#host.model();
|
|
280
|
+
return model ? formatModelString(model) : undefined;
|
|
281
|
+
},
|
|
282
|
+
} as const;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Resolves the edit mode for the active model and settings. */
|
|
286
|
+
resolveActiveEditMode(): EditMode {
|
|
287
|
+
return resolveEditMode(this.#getEditModeSession());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#currentPromptModelKey(): string | undefined {
|
|
291
|
+
const activeModel = this.#host.model();
|
|
292
|
+
const model = activeModel ? formatModelString(activeModel) : undefined;
|
|
293
|
+
if (!model || this.#host.settings.get("includeModelInPrompt")) return model;
|
|
294
|
+
return usesCodexTaskPrompt(model) ? "task-policy:gpt-5.6" : "task-policy:default";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Rebuilds model-dependent tool prompts after a model change. */
|
|
298
|
+
async syncAfterModelChange(previousEditMode: EditMode): Promise<void> {
|
|
299
|
+
const currentEditMode = this.resolveActiveEditMode();
|
|
300
|
+
const editModeChanged = previousEditMode !== currentEditMode && this.getActiveToolNames().includes("edit");
|
|
301
|
+
// The system prompt selects model-specific policy even when it does not display the model id.
|
|
302
|
+
const modelChanged = this.#currentPromptModelKey() !== this.#promptModelKey;
|
|
303
|
+
if (editModeChanged || modelChanged) {
|
|
304
|
+
await this.refreshBaseSystemPrompt();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Enabled MCP tools in their current presentation partition. */
|
|
309
|
+
getSelectedMCPToolNames(): string[] {
|
|
310
|
+
// Every connected MCP tool is enabled; presentation (top-level vs xd://) is
|
|
311
|
+
// decided by loadMode. Return the enabled MCP tools in the current set.
|
|
312
|
+
return this.getEnabledToolNames().filter(name => isMCPToolName(name) && this.#toolRegistry.has(name));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Wrap a tool with a permission-gate proxy when an ACP client is connected.
|
|
317
|
+
* Only wraps tools whose name is in PERMISSION_REQUIRED_TOOLS and only when
|
|
318
|
+
* the bridge exposes `requestPermission`. No-ops for all other cases.
|
|
319
|
+
*
|
|
320
|
+
* When the user has explicitly opted into `yolo` / auto-approve behavior (via
|
|
321
|
+
* the SDK/CLI `autoApprove` flag or a configured `tools.approvalMode: yolo`),
|
|
322
|
+
* skips the gate unless the per-tool policy explicitly requires a prompt or
|
|
323
|
+
* deny. The schema default is also `yolo`, so an explicit configuration or
|
|
324
|
+
* explicit session flag is required: default-config ACP sessions keep the
|
|
325
|
+
* client-side permission gate.
|
|
326
|
+
*/
|
|
327
|
+
#wrapToolForAcpPermission<T extends AgentTool>(tool: T): T {
|
|
328
|
+
const bridge = this.#host.clientBridge();
|
|
329
|
+
// Match the capability+method gating pattern used by read/write/bash.
|
|
330
|
+
if (!bridge?.capabilities.requestPermission || !bridge.requestPermission) return tool;
|
|
331
|
+
if (PERMISSION_REQUIRED_TOOLS[tool.name] !== true) return tool;
|
|
332
|
+
// Skip the gate only on explicit yolo opt-in; honour per-tool policies
|
|
333
|
+
// that require a prompt or deny (matching the normal approval wrapper).
|
|
334
|
+
if (this.#isExplicitAutoApproveMode()) {
|
|
335
|
+
const userPolicies = (this.#host.settings.get("tools.approval") ?? {}) as Record<string, unknown>;
|
|
336
|
+
const toolPolicy = userPolicies[tool.name];
|
|
337
|
+
if (!toolPolicy || toolPolicy === "allow") return tool;
|
|
338
|
+
}
|
|
339
|
+
return new Proxy(tool, {
|
|
340
|
+
get: (target, prop) => {
|
|
341
|
+
if (prop !== "execute") return target[prop as keyof T];
|
|
342
|
+
return async (
|
|
343
|
+
toolCallId: string,
|
|
344
|
+
args: unknown,
|
|
345
|
+
signal: AbortSignal | undefined,
|
|
346
|
+
onUpdate: never,
|
|
347
|
+
ctx: never,
|
|
348
|
+
) => {
|
|
349
|
+
const permissionIntent = getPermissionIntent(target.name, args);
|
|
350
|
+
if (!permissionIntent) {
|
|
351
|
+
return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
|
|
352
|
+
}
|
|
353
|
+
const command =
|
|
354
|
+
target.name === "bash" && args && typeof args === "object" && !Array.isArray(args)
|
|
355
|
+
? stringProperty(args, "command")
|
|
356
|
+
: undefined;
|
|
357
|
+
const commandContent = command
|
|
358
|
+
? [{ type: "content" as const, content: { type: "text" as const, text: `$ ${command}` } }]
|
|
359
|
+
: undefined;
|
|
360
|
+
// Short-circuit on persisted decisions.
|
|
361
|
+
const persisted = this.#acpPermissionDecisions.get(permissionIntent.cacheKey);
|
|
362
|
+
if (persisted === "allow_always") {
|
|
363
|
+
return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
|
|
364
|
+
}
|
|
365
|
+
if (persisted === "reject_always") {
|
|
366
|
+
throw new ToolError(`Tool call rejected by user (preference)`);
|
|
367
|
+
}
|
|
368
|
+
if (signal?.aborted) {
|
|
369
|
+
throw new ToolAbortError("Permission request cancelled");
|
|
370
|
+
}
|
|
371
|
+
type PermissionRaceResult =
|
|
372
|
+
| { kind: "permission"; outcome: ClientBridgePermissionOutcome }
|
|
373
|
+
| { kind: "aborted" };
|
|
374
|
+
const { promise: abortPromise, resolve: resolveAbort } = Promise.withResolvers<PermissionRaceResult>();
|
|
375
|
+
const onAbort = () => resolveAbort({ kind: "aborted" });
|
|
376
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
377
|
+
let raced: PermissionRaceResult;
|
|
378
|
+
try {
|
|
379
|
+
const permissionPromise = bridge.requestPermission!(
|
|
380
|
+
{
|
|
381
|
+
toolCallId,
|
|
382
|
+
toolName: target.name,
|
|
383
|
+
title: permissionIntent.title,
|
|
384
|
+
...(target.name === "bash" ? { kind: "execute" } : {}),
|
|
385
|
+
status: "pending",
|
|
386
|
+
rawInput: args,
|
|
387
|
+
...(commandContent ? { content: commandContent } : {}),
|
|
388
|
+
locations: extractPermissionLocations(
|
|
389
|
+
args,
|
|
390
|
+
this.#host.sessionManager.getCwd(),
|
|
391
|
+
permissionIntent.paths,
|
|
392
|
+
),
|
|
393
|
+
},
|
|
394
|
+
PERMISSION_OPTIONS,
|
|
395
|
+
signal,
|
|
396
|
+
).then(outcome => ({ kind: "permission" as const, outcome }));
|
|
397
|
+
raced = await Promise.race([permissionPromise, abortPromise]);
|
|
398
|
+
} finally {
|
|
399
|
+
signal?.removeEventListener("abort", onAbort);
|
|
400
|
+
}
|
|
401
|
+
if (raced.kind === "aborted" || signal?.aborted) {
|
|
402
|
+
throw new ToolAbortError("Permission request cancelled");
|
|
403
|
+
}
|
|
404
|
+
const outcome = raced.outcome;
|
|
405
|
+
if (outcome.outcome === "cancelled") {
|
|
406
|
+
throw new ToolAbortError("Permission request cancelled");
|
|
407
|
+
}
|
|
408
|
+
const selectedOption = PERMISSION_OPTIONS_BY_ID.get(outcome.optionId);
|
|
409
|
+
if (!selectedOption) {
|
|
410
|
+
throw new ToolError(`Tool permission response used unknown option ID: ${outcome.optionId}`);
|
|
411
|
+
}
|
|
412
|
+
if (selectedOption.kind === "allow_always") {
|
|
413
|
+
this.#acpPermissionDecisions.set(permissionIntent.cacheKey, "allow_always");
|
|
414
|
+
} else if (selectedOption.kind === "reject_always") {
|
|
415
|
+
this.#acpPermissionDecisions.set(permissionIntent.cacheKey, "reject_always");
|
|
416
|
+
}
|
|
417
|
+
if (selectedOption.kind === "reject_once" || selectedOption.kind === "reject_always") {
|
|
418
|
+
throw new ToolError(`Tool call rejected by user (${target.name})`);
|
|
419
|
+
}
|
|
420
|
+
return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
}) as T;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#isExplicitAutoApproveMode(): boolean {
|
|
427
|
+
return (
|
|
428
|
+
this.#autoApprove ||
|
|
429
|
+
(this.#host.settings.isConfigured("tools.approvalMode") &&
|
|
430
|
+
this.#host.settings.get("tools.approvalMode") === "yolo")
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Applies an enabled tool set and reconciles its `xd://` partition. */
|
|
435
|
+
async applyActiveToolsByName(toolNames: string[]): Promise<void> {
|
|
436
|
+
toolNames = normalizeToolNames(toolNames);
|
|
437
|
+
const selectedTools = toolNames.flatMap(name => {
|
|
438
|
+
const tool = this.#toolRegistry.get(name);
|
|
439
|
+
return tool ? [{ name, tool }] : [];
|
|
440
|
+
});
|
|
441
|
+
const xdevReadAvailable = this.#builtInToolNames.has("read") && selectedTools.some(({ name }) => name === "read");
|
|
442
|
+
const isPresentationPinned = (name: string): boolean =>
|
|
443
|
+
this.#presentationPinnedToolNames?.has(name) === true || this.#runtimeSelectedToolNames?.has(name) === true;
|
|
444
|
+
const mountCandidates = selectedTools.filter(
|
|
445
|
+
({ name, tool }) =>
|
|
446
|
+
this.#xdevRegistry !== undefined &&
|
|
447
|
+
xdevReadAvailable &&
|
|
448
|
+
!isPresentationPinned(name) &&
|
|
449
|
+
isMountableUnderXdev(tool),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
let builtInWriteAvailable = this.#builtInToolNames.has("write");
|
|
453
|
+
if (mountCandidates.length > 0 && !builtInWriteAvailable) {
|
|
454
|
+
builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
|
|
455
|
+
if (builtInWriteAvailable) this.#builtInToolNames.add("write");
|
|
456
|
+
}
|
|
457
|
+
const mountNames = builtInWriteAvailable ? new Set(mountCandidates.map(({ name }) => name)) : new Set<string>();
|
|
458
|
+
const tools: AgentTool[] = [];
|
|
459
|
+
const validToolNames: string[] = [];
|
|
460
|
+
const mountedTools: AgentTool[] = [];
|
|
461
|
+
for (const { name, tool } of selectedTools) {
|
|
462
|
+
if (mountNames.has(name)) {
|
|
463
|
+
mountedTools.push(this.#wrapToolForAcpPermission(tool));
|
|
464
|
+
} else {
|
|
465
|
+
tools.push(this.#wrapToolForAcpPermission(tool));
|
|
466
|
+
validToolNames.push(name);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const pinnedWrite = isPresentationPinned("write");
|
|
471
|
+
const activeDeferrableTool = tools.some(tool => tool.deferrable === true);
|
|
472
|
+
const transportNeeded = mountedTools.length > 0 || activeDeferrableTool || this.#host.planModeEnabled();
|
|
473
|
+
if (transportNeeded && !builtInWriteAvailable) {
|
|
474
|
+
builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
|
|
475
|
+
if (builtInWriteAvailable) this.#builtInToolNames.add("write");
|
|
476
|
+
}
|
|
477
|
+
if (transportNeeded && builtInWriteAvailable) {
|
|
478
|
+
const write = this.#toolRegistry.get("write");
|
|
479
|
+
if (write && !validToolNames.includes("write")) {
|
|
480
|
+
tools.push(this.#wrapToolForAcpPermission(write));
|
|
481
|
+
validToolNames.push("write");
|
|
482
|
+
}
|
|
483
|
+
} else if (
|
|
484
|
+
!pinnedWrite &&
|
|
485
|
+
(this.#presentationPinnedToolNames !== undefined || this.#runtimeSelectedToolNames !== undefined)
|
|
486
|
+
) {
|
|
487
|
+
const writeNameIndex = validToolNames.indexOf("write");
|
|
488
|
+
if (writeNameIndex >= 0 && this.#builtInToolNames.has("write")) validToolNames.splice(writeNameIndex, 1);
|
|
489
|
+
const writeToolIndex = tools.findIndex(tool => tool.name === "write" && this.#builtInToolNames.has("write"));
|
|
490
|
+
if (writeToolIndex >= 0) tools.splice(writeToolIndex, 1);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const previousMounted = this.#mountedXdevToolNames;
|
|
494
|
+
const previousMountedTools = [...previousMounted].flatMap(name => {
|
|
495
|
+
const tool = this.#xdevRegistry?.get(name);
|
|
496
|
+
return tool ? [tool] : [];
|
|
497
|
+
});
|
|
498
|
+
const previousActiveToolNames = this.getActiveToolNames();
|
|
499
|
+
this.#mountedXdevToolNames = new Set(mountedTools.map(tool => tool.name));
|
|
500
|
+
this.#xdevRegistry?.reconcile(mountedTools);
|
|
501
|
+
this.#setActiveToolNames?.(validToolNames);
|
|
502
|
+
|
|
503
|
+
let rebuiltSystemPrompt: string[] | undefined;
|
|
504
|
+
let rebuiltSignature: string | undefined;
|
|
505
|
+
try {
|
|
506
|
+
if (this.#rebuildSystemPrompt) {
|
|
507
|
+
const signature = this.#computeAppliedToolSignature(validToolNames, tools);
|
|
508
|
+
if (signature !== this.#lastAppliedToolSignature) {
|
|
509
|
+
const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
|
|
510
|
+
rebuiltSystemPrompt = built.systemPrompt;
|
|
511
|
+
rebuiltSignature = signature;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} catch (error) {
|
|
515
|
+
this.#mountedXdevToolNames = previousMounted;
|
|
516
|
+
this.#xdevRegistry?.reconcile(previousMountedTools);
|
|
517
|
+
this.#setActiveToolNames?.(previousActiveToolNames);
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
this.#notifyXdevMountDelta(previousMounted);
|
|
522
|
+
this.#host.agent.setTools(tools);
|
|
523
|
+
if (rebuiltSystemPrompt && rebuiltSignature) {
|
|
524
|
+
if (this.#lastAppliedToolSignature !== undefined) this.#host.clearInheritedProviderPromptCacheKey();
|
|
525
|
+
this.#baseSystemPrompt = rebuiltSystemPrompt;
|
|
526
|
+
this.#host.clearMemoryPromotionSnapshot();
|
|
527
|
+
this.#host.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
528
|
+
this.#lastAppliedToolSignature = rebuiltSignature;
|
|
529
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Record a mid-session `xd://` mount delta for the model without rewriting
|
|
535
|
+
* the system prompt: the prompt (and its provider cache prefix) stays
|
|
536
|
+
* byte-stable across MCP connects and disconnects. The delta is NOT steered
|
|
537
|
+
* immediately — a steered notice landing at a run's stop boundary (or while
|
|
538
|
+
* the session is idle) forces an unsolicited extra assistant turn — it is
|
|
539
|
+
* coalesced into {@link #pendingXdevMountDelta} and rides along with the
|
|
540
|
+
* next prompt (docs + schema stay one `read xd://<tool>` away). The full
|
|
541
|
+
* docs join the system prompt opportunistically on the next unrelated
|
|
542
|
+
* rebuild.
|
|
543
|
+
*/
|
|
544
|
+
#notifyXdevMountDelta(previousMounted: ReadonlySet<string>): void {
|
|
545
|
+
const registry = this.#xdevRegistry;
|
|
546
|
+
if (!registry) return;
|
|
547
|
+
const current = this.#mountedXdevToolNames;
|
|
548
|
+
const addedNames = [...current].filter(name => !previousMounted.has(name));
|
|
549
|
+
const removedNames = [...previousMounted].filter(name => !current.has(name));
|
|
550
|
+
if (addedNames.length === 0 && removedNames.length === 0) return;
|
|
551
|
+
// Coalesce against the unannounced delta: an unmount cancels a pending
|
|
552
|
+
// mount the model never learned about, and a remount cancels a pending
|
|
553
|
+
// unmount.
|
|
554
|
+
const pending = this.#pendingXdevMountDelta ?? { added: new Set<string>(), removed: new Set<string>() };
|
|
555
|
+
for (const name of addedNames) {
|
|
556
|
+
if (!pending.removed.delete(name)) pending.added.add(name);
|
|
557
|
+
}
|
|
558
|
+
for (const name of removedNames) {
|
|
559
|
+
if (!pending.added.delete(name)) pending.removed.add(name);
|
|
560
|
+
}
|
|
561
|
+
this.#pendingXdevMountDelta = pending.added.size > 0 || pending.removed.size > 0 ? pending : undefined;
|
|
562
|
+
if (this.#host.settings.get("startup.quiet")) return;
|
|
563
|
+
const parts: string[] = [];
|
|
564
|
+
if (addedNames.length > 0) parts.push(`mounted ${addedNames.join(", ")}`);
|
|
565
|
+
if (removedNames.length > 0) parts.push(`unmounted ${removedNames.join(", ")}`);
|
|
566
|
+
this.#host.emitNotice("info", `xd://: ${parts.join("; ")}`, "xdev");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Consumes the hidden notice for unannounced `xd://` mount changes. */
|
|
570
|
+
takePendingXdevMountNotice(): CustomMessage | undefined {
|
|
571
|
+
const pending = this.#pendingXdevMountDelta;
|
|
572
|
+
if (!pending) return undefined;
|
|
573
|
+
this.#pendingXdevMountDelta = undefined;
|
|
574
|
+
const summaries = new Map(this.#xdevRegistry?.entries().map(entry => [entry.name, entry.summary]) ?? []);
|
|
575
|
+
const added = [...pending.added].map(name => ({ name, summary: summaries.get(name) ?? "" }));
|
|
576
|
+
const removed = [...pending.removed].map(name => ({ name }));
|
|
577
|
+
const docs = this.#xdevRegistry?.docsFor(
|
|
578
|
+
pending.added,
|
|
579
|
+
this.#host.settings.get("tools.xdevDocs"),
|
|
580
|
+
this.#host.settings.get("tools.xdevInlineDevices"),
|
|
581
|
+
);
|
|
582
|
+
return {
|
|
583
|
+
role: "custom",
|
|
584
|
+
customType: XDEV_MOUNT_NOTICE_MESSAGE_TYPE,
|
|
585
|
+
content: prompt.render(xdevMountNoticePrompt, { added, removed, docs }),
|
|
586
|
+
attribution: "agent",
|
|
587
|
+
display: false,
|
|
588
|
+
timestamp: Date.now(),
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Rediscovers reloadable skills and refreshes prompt metadata. */
|
|
593
|
+
async refreshSkills(): Promise<void> {
|
|
594
|
+
if (!this.#skillsReloadable) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
resetCapabilities();
|
|
599
|
+
const skillsSettings = this.#host.settings.getGroup("skills");
|
|
600
|
+
const discovered = await loadSkills({
|
|
601
|
+
...skillsSettings,
|
|
602
|
+
cwd: this.#host.sessionManager.getCwd(),
|
|
603
|
+
disabledExtensions: this.#host.settings.get("disabledExtensions") ?? [],
|
|
604
|
+
});
|
|
605
|
+
this.#skills = discovered.skills;
|
|
606
|
+
this.#skillWarnings = discovered.warnings;
|
|
607
|
+
this.#skillsSettings = skillsSettings;
|
|
608
|
+
|
|
609
|
+
if (this.#host.agentKind() === "main") {
|
|
610
|
+
setActiveSkills(this.#skills);
|
|
611
|
+
}
|
|
612
|
+
await this.refreshBaseSystemPrompt();
|
|
613
|
+
this.#host.notifyCommandMetadataChanged();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Selects enabled tools, ignoring names absent from the registry. */
|
|
617
|
+
async setActiveToolsByName(toolNames: string[]): Promise<void> {
|
|
618
|
+
const normalized = normalizeToolNames(toolNames);
|
|
619
|
+
// Transport-write eligibility keys off the *current* active set: an ordinary
|
|
620
|
+
// selection change should not demote `write` unless it is already active.
|
|
621
|
+
await this.#applyToolPresentation(
|
|
622
|
+
normalized,
|
|
623
|
+
this.#mountedXdevToolNames,
|
|
624
|
+
this.getActiveToolNames().includes("write"),
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Restore an enabled tool set with its exact top-level versus `xd://` partition.
|
|
630
|
+
*
|
|
631
|
+
* Both inputs are required because {@link setActiveToolsByName} only receives the
|
|
632
|
+
* enabled name list and classifies mounts from the current `#mountedXdevToolNames`.
|
|
633
|
+
* Rollback/restore callers must pass the snapshotted mounted subset so names that
|
|
634
|
+
* were top-level stay pinned (`#runtimeSelectedToolNames`) and names that were under
|
|
635
|
+
* `xd://` remain mount-eligible, even when the live mount set has drifted.
|
|
636
|
+
*
|
|
637
|
+
* Names outside `mountedToolNames` are pinned top-level for this application;
|
|
638
|
+
* names in the mounted subset remain eligible for xdev mounting. Delegates the
|
|
639
|
+
* actual apply through {@link applyActiveToolsByName} and restores the prior runtime
|
|
640
|
+
* selection if that apply throws.
|
|
641
|
+
*/
|
|
642
|
+
async setActiveToolPresentation(toolNames: string[], mountedToolNames: string[]): Promise<void> {
|
|
643
|
+
const normalized = normalizeToolNames(toolNames);
|
|
644
|
+
// Restoration targets a snapshot, so write eligibility comes from the
|
|
645
|
+
// *target* set rather than whatever happens to be active mid-rollback.
|
|
646
|
+
await this.#applyToolPresentation(
|
|
647
|
+
normalized,
|
|
648
|
+
new Set(normalizeToolNames(mountedToolNames)),
|
|
649
|
+
normalized.includes("write"),
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Shared body for {@link setActiveToolsByName} and {@link setActiveToolPresentation}:
|
|
655
|
+
* pins non-mounted names as the runtime selection (holding `write` back when it is
|
|
656
|
+
* transport-only) and applies the set, rolling the selection back if apply throws.
|
|
657
|
+
*/
|
|
658
|
+
async #applyToolPresentation(
|
|
659
|
+
normalized: string[],
|
|
660
|
+
mounted: ReadonlySet<string>,
|
|
661
|
+
writeSelected: boolean,
|
|
662
|
+
): Promise<void> {
|
|
663
|
+
const transportWriteActive =
|
|
664
|
+
writeSelected &&
|
|
665
|
+
this.#builtInToolNames.has("write") &&
|
|
666
|
+
this.#presentationPinnedToolNames?.has("write") !== true &&
|
|
667
|
+
this.#runtimeSelectedToolNames?.has("write") !== true &&
|
|
668
|
+
(mounted.size > 0 || this.#host.planModeEnabled());
|
|
669
|
+
const previousRuntimeSelectedToolNames = this.#runtimeSelectedToolNames;
|
|
670
|
+
this.#runtimeSelectedToolNames = new Set(
|
|
671
|
+
normalized.filter(name => !mounted.has(name) && !(name === "write" && transportWriteActive)),
|
|
672
|
+
);
|
|
673
|
+
try {
|
|
674
|
+
await this.applyActiveToolsByName(normalized);
|
|
675
|
+
} catch (error) {
|
|
676
|
+
this.#runtimeSelectedToolNames = previousRuntimeSelectedToolNames;
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Replaces memory-backend tools while preserving unrelated selections. */
|
|
682
|
+
async replaceMemoryTools(tools: AgentTool[]): Promise<void> {
|
|
683
|
+
const removed = new Set<string>(MEMORY_BACKEND_TOOL_NAMES.filter(name => this.#builtInToolNames.has(name)));
|
|
684
|
+
const nextActive = this.getEnabledToolNames().filter(name => !removed.has(name));
|
|
685
|
+
for (const name of removed) {
|
|
686
|
+
this.#toolRegistry.delete(name);
|
|
687
|
+
this.#builtInToolNames.delete(name);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
for (const tool of tools) {
|
|
691
|
+
if (!MEMORY_BACKEND_TOOL_NAMES.some(name => name === tool.name) || this.#toolRegistry.has(tool.name)) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const wrapped = this.#wrapRuntimeTool(tool);
|
|
695
|
+
this.#toolRegistry.set(wrapped.name, wrapped);
|
|
696
|
+
this.#builtInToolNames.add(wrapped.name);
|
|
697
|
+
nextActive.push(wrapped.name);
|
|
698
|
+
}
|
|
699
|
+
await this.applyActiveToolsByName([...new Set(nextActive)]);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Session-scoped enable/disable for the settings-gated `computer` tool.
|
|
704
|
+
*
|
|
705
|
+
* `createTools` derives the built-in slate once at session start, so a runtime
|
|
706
|
+
* `computer.enabled` override alone never changes the active tools. Enabling
|
|
707
|
+
* builds the tool through the config factory on first use (later toggles reuse
|
|
708
|
+
* the registry entry, so only one desktop controller is ever registered) and
|
|
709
|
+
* activates it; disabling drops it from the active set while keeping the
|
|
710
|
+
* registry entry. Takes effect before the next model call.
|
|
711
|
+
*
|
|
712
|
+
* @returns false when enabling was requested but this session cannot build the
|
|
713
|
+
* tool (e.g. restricted child sessions have no factory).
|
|
714
|
+
*/
|
|
715
|
+
async setComputerToolEnabled(enabled: boolean): Promise<boolean> {
|
|
716
|
+
const active = this.getEnabledToolNames();
|
|
717
|
+
if (!enabled) {
|
|
718
|
+
if (active.includes("computer")) {
|
|
719
|
+
await this.applyActiveToolsByName(active.filter(name => name !== "computer"));
|
|
720
|
+
}
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
if (!this.#toolRegistry.has("computer")) {
|
|
724
|
+
const tool = await this.#createComputerTool?.();
|
|
725
|
+
if (tool?.name !== "computer") return false;
|
|
726
|
+
const wrapped = this.#wrapRuntimeTool(tool);
|
|
727
|
+
this.#toolRegistry.set(wrapped.name, wrapped);
|
|
728
|
+
this.#builtInToolNames.add(wrapped.name);
|
|
729
|
+
}
|
|
730
|
+
if (!active.includes("computer")) {
|
|
731
|
+
await this.applyActiveToolsByName([...active, "computer"]);
|
|
732
|
+
}
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Rebuilds the stable base prompt for the current tools and model. */
|
|
737
|
+
async refreshBaseSystemPrompt(): Promise<void> {
|
|
738
|
+
if (this.#host.isDisposed() || !this.#rebuildSystemPrompt) return;
|
|
739
|
+
const activeToolNames = this.getActiveToolNames();
|
|
740
|
+
this.#setActiveToolNames?.(activeToolNames);
|
|
741
|
+
const previousBaseSystemPrompt = this.#baseSystemPrompt;
|
|
742
|
+
const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
|
|
743
|
+
if (this.#host.isDisposed()) return;
|
|
744
|
+
this.#baseSystemPrompt = built.systemPrompt;
|
|
745
|
+
this.#host.clearMemoryPromotionSnapshot();
|
|
746
|
+
if (
|
|
747
|
+
previousBaseSystemPrompt.length !== this.#baseSystemPrompt.length ||
|
|
748
|
+
previousBaseSystemPrompt.some((part, index) => part !== this.#baseSystemPrompt[index])
|
|
749
|
+
) {
|
|
750
|
+
this.#host.clearInheritedProviderPromptCacheKey();
|
|
751
|
+
}
|
|
752
|
+
this.#host.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
753
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
754
|
+
// Refresh the cached signature so a subsequent `applyActiveToolsByName` with
|
|
755
|
+
// the same tool set does not re-rebuild on top of the explicit refresh we
|
|
756
|
+
// just performed (and conversely, a different set forces a fresh rebuild).
|
|
757
|
+
const activeTools = activeToolNames
|
|
758
|
+
.map(name => this.#toolRegistry.get(name))
|
|
759
|
+
.filter((tool): tool is AgentTool => tool != null);
|
|
760
|
+
this.#lastAppliedToolSignature = this.#computeAppliedToolSignature(activeToolNames, activeTools);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** Applies one-turn memory prompt injection before an agent run. */
|
|
764
|
+
async buildSystemPromptForAgentStart(promptText: string): Promise<string[]> {
|
|
765
|
+
const backend = await resolveMemoryBackend(this.#host.settings);
|
|
766
|
+
if (!backend.beforeAgentStartPrompt) return this.#baseSystemPrompt;
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
const injected = await backend.beforeAgentStartPrompt(this.#host.memoryBackendSession(), promptText);
|
|
770
|
+
if (!injected) return this.#baseSystemPrompt;
|
|
771
|
+
|
|
772
|
+
const previousBaseSystemPrompt = this.#baseSystemPrompt;
|
|
773
|
+
try {
|
|
774
|
+
await this.refreshBaseSystemPrompt();
|
|
775
|
+
} catch (refreshErr) {
|
|
776
|
+
logger.debug("Memory backend prompt refresh after beforeAgentStartPrompt failed", {
|
|
777
|
+
backend: backend.id,
|
|
778
|
+
error: String(refreshErr),
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (
|
|
783
|
+
this.#baseSystemPrompt.length !== previousBaseSystemPrompt.length ||
|
|
784
|
+
this.#baseSystemPrompt.some((part, index) => part !== previousBaseSystemPrompt[index])
|
|
785
|
+
) {
|
|
786
|
+
return this.#baseSystemPrompt;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
this.#host.captureMemoryPromotionSnapshot(previousBaseSystemPrompt);
|
|
790
|
+
const stablePrompt = [...previousBaseSystemPrompt, injected];
|
|
791
|
+
this.#baseSystemPrompt = stablePrompt;
|
|
792
|
+
this.#host.agent.setSystemPrompt(stablePrompt);
|
|
793
|
+
return stablePrompt;
|
|
794
|
+
} catch (err) {
|
|
795
|
+
logger.debug("Memory backend beforeAgentStartPrompt failed", {
|
|
796
|
+
backend: backend.id,
|
|
797
|
+
error: String(err),
|
|
798
|
+
});
|
|
799
|
+
return this.#baseSystemPrompt;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Compose a stable signature for the inputs that `rebuildSystemPrompt` reads.
|
|
805
|
+
* Two calls producing identical signatures are guaranteed to produce identical
|
|
806
|
+
* system prompt bytes, so the rebuild can be skipped.
|
|
807
|
+
*
|
|
808
|
+
* The signature covers:
|
|
809
|
+
* 1. Active tool names in order (the prompt renders them in this order).
|
|
810
|
+
* 2. Active tool labels, descriptions, and wire-visible names — all are
|
|
811
|
+
* rendered into the prompt body (see `system-prompt.md` `{{label}}: \`{{name}}\``
|
|
812
|
+
* and `toolPromptNames` in `buildSystemPrompt`). The wire name comes from
|
|
813
|
+
* `tool.customWireName` and overrides the internal name on the model wire
|
|
814
|
+
* (e.g. `edit` exposes itself as `apply_patch` to GPT-5 in apply_patch mode);
|
|
815
|
+
* a stale wire name would desync prompt guidance from actual tool routing.
|
|
816
|
+
* 3. When MCP discovery is on, every registry tool's name+label+description+
|
|
817
|
+
* customWireName, since `rebuildSystemPrompt` summarizes discoverable MCP
|
|
818
|
+
* tools that are not in the active set.
|
|
819
|
+
* 4. MCP server instructions text (per server), since `rebuildSystemPrompt`
|
|
820
|
+
* embeds these in the appended prompt under "## MCP Server Instructions".
|
|
821
|
+
* A server upgrade can change instructions while keeping tools identical.
|
|
822
|
+
*
|
|
823
|
+
* Settings-driven tool metadata is covered automatically: built-in tools that
|
|
824
|
+
* depend on settings expose `description`/`label` via getters (see `TaskTool`,
|
|
825
|
+
* `SearchToolBm25Tool`, `EditTool`), and the signature reads them live on every
|
|
826
|
+
* call - so a settings flip that mutates the rendered string differs the signature
|
|
827
|
+
* the next time {@link applyActiveToolsByName} runs. Do not refactor `describeTool`
|
|
828
|
+
* to cache per-tool strings without preserving this property.
|
|
829
|
+
*
|
|
830
|
+
* Inputs NOT covered: tool input schemas; memory instructions read from disk;
|
|
831
|
+
* and SDK-init-time closure constants in `sdk.ts` (`inlineToolDescriptors`,
|
|
832
|
+
* `eagerTasks`, `intentField`, `mcpDiscoveryEnabled`, `secretsEnabled`). The
|
|
833
|
+
* closure-captured ones cannot change at runtime regardless of skip behavior.
|
|
834
|
+
* For everything else, callers must explicitly call {@link refreshBaseSystemPrompt}
|
|
835
|
+
* after side-effecting changes; see the memory hooks and {@link syncAfterModelChange}.
|
|
836
|
+
*
|
|
837
|
+
* The current calendar date IS covered (appended as a segment) because
|
|
838
|
+
* `buildSystemPrompt` injects it into the prompt body (`Today is '{{date}}'`).
|
|
839
|
+
* Without this, a session spanning midnight with only tool-stable MCP
|
|
840
|
+
* reconnects would keep yesterday's date indefinitely.
|
|
841
|
+
*/
|
|
842
|
+
#computeAppliedToolSignature(toolNames: string[], tools: AgentTool[]): string {
|
|
843
|
+
// Order-preserving join: any reorder must produce a different signature so
|
|
844
|
+
// the rebuild fires and the new tool list reaches the API.
|
|
845
|
+
const nameSegment = toolNames.join("\u0001");
|
|
846
|
+
const describeTool = (tool: AgentTool): string =>
|
|
847
|
+
`${tool.name}=${tool.label ?? ""}|${tool.description ?? ""}|${tool.customWireName ?? ""}`;
|
|
848
|
+
const descriptionSegment = tools.map(describeTool).join("\u0002");
|
|
849
|
+
let instructionsSegment = "";
|
|
850
|
+
const serverInstructions = this.#getMcpServerInstructions?.();
|
|
851
|
+
if (serverInstructions && serverInstructions.size > 0) {
|
|
852
|
+
// Sort by server name so transport flap order does not perturb the signature.
|
|
853
|
+
const entries: string[] = [];
|
|
854
|
+
for (const [server, instructions] of serverInstructions) {
|
|
855
|
+
entries.push(`${server}=${instructions}`);
|
|
856
|
+
}
|
|
857
|
+
entries.sort();
|
|
858
|
+
instructionsSegment = entries.join("\u0006");
|
|
859
|
+
}
|
|
860
|
+
// The xd:// device inventory is deliberately NOT part of the signature:
|
|
861
|
+
// a mount/unmount announces itself via `#notifyXdevMountDelta` instead of
|
|
862
|
+
// rewriting the system prompt, so MCP connects/disconnects keep the
|
|
863
|
+
// prompt (and its provider cache prefix) byte-stable. Rebuilds triggered
|
|
864
|
+
// by other inputs pick up the current device docs opportunistically.
|
|
865
|
+
const date = this.#getLocalCalendarDate();
|
|
866
|
+
return `${nameSegment}\u0003${descriptionSegment}\u0007${instructionsSegment}|${date}`;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Replace MCP tools in the registry and enable them immediately. Every
|
|
871
|
+
* connected MCP tool becomes available (mounted under `xd://` when that
|
|
872
|
+
* transport is active, else top-level). Lets `/mcp add/remove/reauth` take
|
|
873
|
+
* effect without restarting the session.
|
|
874
|
+
*/
|
|
875
|
+
async refreshMCPTools(mcpTools: CustomTool[]): Promise<void> {
|
|
876
|
+
const existingNames = Array.from(this.#toolRegistry.keys());
|
|
877
|
+
const previousMcpTools = new Map(
|
|
878
|
+
existingNames.flatMap(name => {
|
|
879
|
+
const tool = this.#toolRegistry.get(name);
|
|
880
|
+
return isMCPToolName(name) && tool ? [[name, tool] as const] : [];
|
|
881
|
+
}),
|
|
882
|
+
);
|
|
883
|
+
for (const name of existingNames) {
|
|
884
|
+
if (isMCPToolName(name)) {
|
|
885
|
+
this.#toolRegistry.delete(name);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const getCustomToolContext = (): CustomToolContext => ({
|
|
890
|
+
sessionManager: this.#host.sessionManager,
|
|
891
|
+
modelRegistry: this.#host.modelRegistry,
|
|
892
|
+
model: this.#host.model(),
|
|
893
|
+
isIdle: () => !this.#host.isStreaming(),
|
|
894
|
+
hasQueuedMessages: () => this.#host.queuedMessageCount() > 0,
|
|
895
|
+
abort: () => {
|
|
896
|
+
this.#host.agent.abort();
|
|
897
|
+
},
|
|
898
|
+
settings: this.#host.settings,
|
|
899
|
+
localProtocolOptions: this.#host.localProtocolOptions(),
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
const extensionRunner = this.#host.extensionRunner();
|
|
903
|
+
for (const customTool of mcpTools) {
|
|
904
|
+
const wrapped = wrapToolWithMetaNotice(CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool);
|
|
905
|
+
const finalTool = (
|
|
906
|
+
extensionRunner ? new ExtensionToolWrapper(wrapped, extensionRunner) : wrapped
|
|
907
|
+
) as AgentTool;
|
|
908
|
+
this.#toolRegistry.set(finalTool.name, finalTool);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Every connected MCP tool is selected; centralized repartitioning owns
|
|
912
|
+
// presentation pins and write-transport activation/removal.
|
|
913
|
+
const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...mcpTools.map(tool => tool.name)])];
|
|
914
|
+
try {
|
|
915
|
+
await this.applyActiveToolsByName(nextActive);
|
|
916
|
+
} catch (error) {
|
|
917
|
+
for (const name of this.#toolRegistry.keys()) {
|
|
918
|
+
if (isMCPToolName(name)) this.#toolRegistry.delete(name);
|
|
919
|
+
}
|
|
920
|
+
for (const [name, tool] of previousMcpTools) this.#toolRegistry.set(name, tool);
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/** Replaces RPC host-owned tools and refreshes the active set before the next model call. */
|
|
926
|
+
async refreshRpcHostTools(rpcTools: AgentTool[]): Promise<void> {
|
|
927
|
+
const nextToolNames = rpcTools.map(tool => tool.name);
|
|
928
|
+
const uniqueToolNames = new Set(nextToolNames);
|
|
929
|
+
if (uniqueToolNames.size !== nextToolNames.length) {
|
|
930
|
+
throw new Error("RPC host tool names must be unique");
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
for (const name of uniqueToolNames) {
|
|
934
|
+
if (this.#toolRegistry.has(name) && !this.#rpcHostToolNames.has(name)) {
|
|
935
|
+
throw new Error(`RPC host tool "${name}" conflicts with an existing tool`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const previousRpcHostToolNames = new Set(this.#rpcHostToolNames);
|
|
940
|
+
const previousActiveToolNames = this.getEnabledToolNames();
|
|
941
|
+
const previousRpcHostTools = new Map(
|
|
942
|
+
[...previousRpcHostToolNames].flatMap(name => {
|
|
943
|
+
const tool = this.#toolRegistry.get(name);
|
|
944
|
+
return tool ? [[name, tool] as const] : [];
|
|
945
|
+
}),
|
|
946
|
+
);
|
|
947
|
+
for (const name of previousRpcHostToolNames) {
|
|
948
|
+
this.#toolRegistry.delete(name);
|
|
949
|
+
}
|
|
950
|
+
this.#rpcHostToolNames.clear();
|
|
951
|
+
|
|
952
|
+
const extensionRunner = this.#host.extensionRunner();
|
|
953
|
+
for (const tool of rpcTools) {
|
|
954
|
+
const metaWrapped = wrapToolWithMetaNotice(tool);
|
|
955
|
+
const finalTool = (
|
|
956
|
+
extensionRunner ? new ExtensionToolWrapper(metaWrapped, extensionRunner) : metaWrapped
|
|
957
|
+
) as AgentTool;
|
|
958
|
+
this.#toolRegistry.set(finalTool.name, finalTool);
|
|
959
|
+
this.#rpcHostToolNames.add(finalTool.name);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const activeNonRpcToolNames = previousActiveToolNames.filter(name => !previousRpcHostToolNames.has(name));
|
|
963
|
+
const preservedRpcToolNames = previousActiveToolNames.filter(
|
|
964
|
+
name => previousRpcHostToolNames.has(name) && this.#rpcHostToolNames.has(name),
|
|
965
|
+
);
|
|
966
|
+
const autoActivatedRpcToolNames = rpcTools
|
|
967
|
+
.filter(tool => !tool.hidden && !previousRpcHostToolNames.has(tool.name))
|
|
968
|
+
.map(tool => tool.name);
|
|
969
|
+
try {
|
|
970
|
+
await this.applyActiveToolsByName(
|
|
971
|
+
Array.from(new Set([...activeNonRpcToolNames, ...preservedRpcToolNames, ...autoActivatedRpcToolNames])),
|
|
972
|
+
);
|
|
973
|
+
} catch (error) {
|
|
974
|
+
for (const name of this.#rpcHostToolNames) this.#toolRegistry.delete(name);
|
|
975
|
+
this.#rpcHostToolNames = previousRpcHostToolNames;
|
|
976
|
+
for (const [name, tool] of previousRpcHostTools) this.#toolRegistry.set(name, tool);
|
|
977
|
+
throw error;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|