From 0f019122b001917529286eee49820480ac9af974 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Mon, 9 Mar 2026 08:23:00 -0700 Subject: [PATCH 1/6] Docs: Make documentation links relative (#21490) --- CONTRIBUTING.md | 5 +- docs/cli/plan-mode.md | 141 ++++++++++++++-------------- docs/core/subagents.md | 2 +- docs/get-started/authentication.md | 2 +- docs/get-started/gemini-3.md | 2 +- docs/get-started/installation.md | 2 +- docs/hooks/best-practices.md | 2 +- docs/hooks/index.md | 12 +-- docs/hooks/reference.md | 4 +- docs/reference/policy-engine.md | 6 +- docs/resources/quota-and-pricing.md | 2 +- docs/resources/tos-privacy.md | 4 +- 12 files changed, 93 insertions(+), 91 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0902b2e97..f77d0f9152 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -267,7 +267,8 @@ npm run test:e2e ``` For more detailed information on the integration testing framework, please see -the [Integration Tests documentation](/docs/integration-tests.md). +the +[Integration Tests documentation](https://geminicli.com/docs/integration-tests). ### Linting and preflight checks @@ -546,7 +547,7 @@ Before submitting your documentation pull request, please: If you have questions about contributing documentation: -- Check our [FAQ](/docs/resources/faq.md). +- Check our [FAQ](https://geminicli.com/docs/resources/faq). - Review existing documentation for examples. - Open [an issue](https://github.com/google-gemini/gemini-cli/issues) to discuss your proposed changes. diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index 41f8ededcd..149278f50b 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -62,8 +62,11 @@ To start Plan Mode while using Gemini CLI: - **Command:** Type `/plan` in the input box. - **Natural Language:** Ask Gemini CLI to "start a plan for...". Gemini CLI - calls the [`enter_plan_mode`] tool to switch modes. - > **Note:** This tool is not available when Gemini CLI is in [YOLO mode]. + calls the + [`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode) tool + to switch modes. + > **Note:** This tool is not available when Gemini CLI is in + > [YOLO mode](../reference/configuration.md#command-line-arguments). ## How to use Plan Mode @@ -74,7 +77,8 @@ Gemini CLI takes action. will then enter Plan Mode (if it's not already) to research the task. 2. **Review research and provide input:** As Gemini CLI analyzes your codebase, it may ask you questions or present different implementation options using - [`ask_user`]. Provide your preferences to help guide the design. + [`ask_user`](../tools/ask-user.md). Provide your preferences to help guide + the design. 3. **Review the plan:** Once Gemini CLI has a proposed strategy, it creates a detailed implementation plan as a Markdown file in your plans directory. You can open and read this file to understand the proposed changes. @@ -116,25 +120,33 @@ Plan Mode enforces strict safety policies to prevent accidental changes. These are the only allowed tools: -- **FileSystem (Read):** [`read_file`], [`list_directory`], [`glob`] -- **Search:** [`grep_search`], [`google_web_search`] -- **Research Subagents:** [`codebase_investigator`], [`cli_help`] -- **Interaction:** [`ask_user`] -- **MCP tools (Read):** Read-only [MCP tools] (for example, `github_read_issue`, - `postgres_read_schema`) are allowed. -- **Planning (Write):** [`write_file`] and [`replace`] only allowed for `.md` +- **FileSystem (Read):** + [`read_file`](../tools/file-system.md#2-read_file-readfile), + [`list_directory`](../tools/file-system.md#1-list_directory-readfolder), + [`glob`](../tools/file-system.md#4-glob-findfiles) +- **Search:** [`grep_search`](../tools/file-system.md#5-grep_search-searchtext), + [`google_web_search`](../tools/web-search.md) +- **Research Subagents:** + [`codebase_investigator`](../core/subagents.md#codebase-investigator), + [`cli_help`](../core/subagents.md#cli-help-agent) +- **Interaction:** [`ask_user`](../tools/ask-user.md) +- **MCP tools (Read):** Read-only [MCP tools](../tools/mcp-server.md) (for + example, `github_read_issue`, `postgres_read_schema`) are allowed. +- **Planning (Write):** + [`write_file`](../tools/file-system.md#3-write_file-writefile) and + [`replace`](../tools/file-system.md#6-replace-edit) only allowed for `.md` files in the `~/.gemini/tmp///plans/` directory or your [custom plans directory](#custom-plan-directory-and-policies). -- **Memory:** [`save_memory`] -- **Skills:** [`activate_skill`] (allows loading specialized instructions and - resources in a read-only manner) +- **Memory:** [`save_memory`](../tools/memory.md) +- **Skills:** [`activate_skill`](../cli/skills.md) (allows loading specialized + instructions and resources in a read-only manner) ### Custom planning with skills -You can use [Agent Skills] to customize how Gemini CLI approaches planning for -specific types of tasks. When a skill is activated during Plan Mode, its -specialized instructions and procedural workflows will guide the research, -design, and planning phases. +You can use [Agent Skills](../cli/skills.md) to customize how Gemini CLI +approaches planning for specific types of tasks. When a skill is activated +during Plan Mode, its specialized instructions and procedural workflows will +guide the research, design, and planning phases. For example: @@ -151,10 +163,11 @@ based on the task description. ### Custom policies -Plan Mode's default tool restrictions are managed by the [policy engine] and -defined in the built-in [`plan.toml`] file. The built-in policy (Tier 1) -enforces the read-only state, but you can customize these rules by creating your -own policies in your `~/.gemini/policies/` directory (Tier 2). +Plan Mode's default tool restrictions are managed by the +[policy engine](../reference/policy-engine.md) and defined in the built-in +[`plan.toml`] file. The built-in policy (Tier 1) enforces the read-only state, +but you can customize these rules by creating your own policies in your +`~/.gemini/policies/` directory (Tier 2). #### Example: Automatically approve read-only MCP tools @@ -173,8 +186,8 @@ priority = 100 modes = ["plan"] ``` -For more information on how the policy engine works, see the [policy engine] -docs. +For more information on how the policy engine works, see the +[policy engine](../reference/policy-engine.md) docs. #### Example: Allow git commands in Plan Mode @@ -194,9 +207,12 @@ modes = ["plan"] #### Example: Enable custom subagents in Plan Mode -Built-in research [subagents] like [`codebase_investigator`] and [`cli_help`] -are enabled by default in Plan Mode. You can enable additional [custom -subagents] by adding a rule to your policy. +Built-in research [subagents](../core/subagents.md) like +[`codebase_investigator`](../core/subagents.md#codebase-investigator) and +[`cli_help`](../core/subagents.md#cli-help-agent) are enabled by default in Plan +Mode. You can enable additional +[custom subagents](../core/subagents.md#creating-custom-subagents) by adding a +rule to your policy. `~/.gemini/policies/research-subagents.toml` @@ -235,10 +251,11 @@ locations defined within a project's workspace cannot be used to escape and overwrite sensitive files elsewhere. Any user-configured directory must reside within the project boundary. -Using a custom directory requires updating your [policy engine] configurations -to allow `write_file` and `replace` in that specific location. For example, to -allow writing to the `.gemini/plans` directory within your project, create a -policy file at `~/.gemini/policies/plan-custom-directory.toml`: +Using a custom directory requires updating your +[policy engine](../reference/policy-engine.md) configurations to allow +`write_file` and `replace` in that specific location. For example, to allow +writing to the `.gemini/plans` directory within your project, create a policy +file at `~/.gemini/policies/plan-custom-directory.toml`: ```toml [[rule]] @@ -254,13 +271,16 @@ argsPattern = "\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+plans[\\\\/]+[\\w- ## Planning workflows Plan Mode provides building blocks for structured research and design. These are -implemented as [extensions] using core planning tools like [`enter_plan_mode`], -[`exit_plan_mode`], and [`ask_user`]. +implemented as [extensions](../extensions/index.md) using core planning tools +like [`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode), +[`exit_plan_mode`](../tools/planning.md#2-exit_plan_mode-exitplanmode), and +[`ask_user`](../tools/ask-user.md). ### Built-in planning workflow The built-in planner uses an adaptive workflow to analyze your project, consult -you on trade-offs via [`ask_user`], and draft a plan for your approval. +you on trade-offs via [`ask_user`](../tools/ask-user.md), and draft a plan for +your approval. ### Custom planning workflows @@ -272,23 +292,29 @@ You can install or create specialized planners to suit your workflow. "tracks" and stores persistent artifacts in your project's `conductor/` directory: -- **Automate transitions:** Switches to read-only mode via [`enter_plan_mode`]. -- **Streamline decisions:** Uses [`ask_user`] for architectural choices. +- **Automate transitions:** Switches to read-only mode via + [`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode). +- **Streamline decisions:** Uses [`ask_user`](../tools/ask-user.md) for + architectural choices. - **Maintain project context:** Stores artifacts in the project directory using [custom plan directory and policies](#custom-plan-directory-and-policies). -- **Handoff execution:** Transitions to implementation via [`exit_plan_mode`]. +- **Handoff execution:** Transitions to implementation via + [`exit_plan_mode`](../tools/planning.md#2-exit_plan_mode-exitplanmode). #### Build your own Since Plan Mode is built on modular building blocks, you can develop your own -custom planning workflow as an [extensions]. By leveraging core tools and -[custom policies](#custom-policies), you can define how Gemini CLI researches -and stores plans for your specific domain. +custom planning workflow as an [extensions](../extensions/index.md). By +leveraging core tools and [custom policies](#custom-policies), you can define +how Gemini CLI researches and stores plans for your specific domain. To build a custom planning workflow, you can use: -- **Tool usage:** Use core tools like [`enter_plan_mode`], [`ask_user`], and - [`exit_plan_mode`] to manage the research and design process. +- **Tool usage:** Use core tools like + [`enter_plan_mode`](../tools/planning.md#1-enter_plan_mode-enterplanmode), + [`ask_user`](../tools/ask-user.md), and + [`exit_plan_mode`](../tools/planning.md#2-exit_plan_mode-exitplanmode) to + manage the research and design process. - **Customization:** Set your own storage locations and policy rules using [custom plan directories](#custom-plan-directory-and-policies) and [custom policies](#custom-policies). @@ -302,8 +328,9 @@ high-reasoning model routing. ## Automatic Model Routing -When using an [auto model], Gemini CLI automatically optimizes [model routing] -based on the current phase of your task: +When using an [auto model](../reference/configuration.md#model), Gemini CLI +automatically optimizes [model routing](../cli/telemetry.md#model-routing) based +on the current phase of your task: 1. **Planning Phase:** While in Plan Mode, the CLI routes requests to a high-reasoning **Pro** model to ensure robust architectural decisions and @@ -334,7 +361,8 @@ associated plan files and task trackers. - **Default behavior:** Sessions (and their plans) are retained for **30 days**. - **Configuration:** You can customize this behavior via the `/settings` command (search for **Session Retention**) or in your `settings.json` file. See - [session retention] for more details. + [session retention](../cli/session-management.md#session-retention) for more + details. Manual deletion also removes all associated artifacts: @@ -344,32 +372,7 @@ Manual deletion also removes all associated artifacts: If you use a [custom plans directory](#custom-plan-directory-and-policies), those files are not automatically deleted and must be managed manually. -[`list_directory`]: /docs/tools/file-system.md#1-list_directory-readfolder -[`read_file`]: /docs/tools/file-system.md#2-read_file-readfile -[`grep_search`]: /docs/tools/file-system.md#5-grep_search-searchtext -[`write_file`]: /docs/tools/file-system.md#3-write_file-writefile -[`glob`]: /docs/tools/file-system.md#4-glob-findfiles -[`google_web_search`]: /docs/tools/web-search.md -[`replace`]: /docs/tools/file-system.md#6-replace-edit -[MCP tools]: /docs/tools/mcp-server.md -[`save_memory`]: /docs/tools/memory.md -[`activate_skill`]: /docs/cli/skills.md -[`codebase_investigator`]: /docs/core/subagents.md#codebase-investigator -[`cli_help`]: /docs/core/subagents.md#cli-help-agent -[subagents]: /docs/core/subagents.md -[custom subagents]: /docs/core/subagents.md#creating-custom-subagents -[policy engine]: /docs/reference/policy-engine.md -[`enter_plan_mode`]: /docs/tools/planning.md#1-enter_plan_mode-enterplanmode -[`exit_plan_mode`]: /docs/tools/planning.md#2-exit_plan_mode-exitplanmode -[`ask_user`]: /docs/tools/ask-user.md -[YOLO mode]: /docs/reference/configuration.md#command-line-arguments [`plan.toml`]: https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/policy/policies/plan.toml -[auto model]: /docs/reference/configuration.md#model -[model routing]: /docs/cli/telemetry.md#model-routing -[preferred external editor]: /docs/reference/configuration.md#general -[session retention]: /docs/cli/session-management.md#session-retention -[extensions]: /docs/extensions/ [Conductor]: https://github.com/gemini-cli-extensions/conductor [open an issue]: https://github.com/google-gemini/gemini-cli/issues -[Agent Skills]: /docs/cli/skills.md diff --git a/docs/core/subagents.md b/docs/core/subagents.md index e84f46dd8c..37085569af 100644 --- a/docs/core/subagents.md +++ b/docs/core/subagents.md @@ -297,7 +297,7 @@ Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent > **Note: Remote subagents are currently an experimental feature.** -See the [Remote Subagents documentation](/docs/core/remote-agents) for detailed +See the [Remote Subagents documentation](remote-agents) for detailed configuration and usage instructions. ## Extension subagents diff --git a/docs/get-started/authentication.md b/docs/get-started/authentication.md index 8b8f592335..bc603bbdf3 100644 --- a/docs/get-started/authentication.md +++ b/docs/get-started/authentication.md @@ -6,7 +6,7 @@ using the CLI. > **Note:** Looking for a high-level comparison of all available subscriptions? > To compare features and find the right quota for your needs, see our -> [Plans page](/plans/). +> [Plans page](https://geminicli.com/plans/). For most users, we recommend starting Gemini CLI and logging in with your personal Google account. diff --git a/docs/get-started/gemini-3.md b/docs/get-started/gemini-3.md index bc83d990d5..d22baaa0c0 100644 --- a/docs/get-started/gemini-3.md +++ b/docs/get-started/gemini-3.md @@ -41,7 +41,7 @@ limit resets and Gemini 3 Pro can be used again. > **Note:** Looking to upgrade for higher limits? To compare subscription > options and find the right quota for your needs, see our -> [Plans page](/plans/). +> [Plans page](https://geminicli.com/plans/). Similarly, when you reach your daily usage limit for Gemini 2.5 Pro, you’ll see a message prompting fallback to Gemini 2.5 Flash. diff --git a/docs/get-started/installation.md b/docs/get-started/installation.md index c345584b69..e56d98d889 100644 --- a/docs/get-started/installation.md +++ b/docs/get-started/installation.md @@ -70,7 +70,7 @@ gemini ``` For a list of options and additional commands, see the -[CLI cheatsheet](/docs/cli/cli-reference.md). +[CLI cheatsheet](../cli/cli-reference.md). You can also run Gemini CLI using one of the following advanced methods: diff --git a/docs/hooks/best-practices.md b/docs/hooks/best-practices.md index 08dae0fdf8..5158cfc5eb 100644 --- a/docs/hooks/best-practices.md +++ b/docs/hooks/best-practices.md @@ -449,7 +449,7 @@ When you open a project with hooks defined in `.gemini/settings.json`: Hooks inherit the environment of the Gemini CLI process, which may include sensitive API keys. Gemini CLI provides a -[redaction system](/docs/reference/configuration.md#environment-variable-redaction) +[redaction system](../reference/configuration.md#environment-variable-redaction) that automatically filters variables matching sensitive patterns (e.g., `KEY`, `TOKEN`). diff --git a/docs/hooks/index.md b/docs/hooks/index.md index b19ceab438..7d526dd885 100644 --- a/docs/hooks/index.md +++ b/docs/hooks/index.md @@ -22,11 +22,11 @@ With hooks, you can: ### Getting started -- **[Writing hooks guide](/docs/hooks/writing-hooks)**: A tutorial on creating - your first hook with comprehensive examples. -- **[Best practices](/docs/hooks/best-practices)**: Guidelines on security, +- **[Writing hooks guide](../hooks/writing-hooks)**: A tutorial on creating your + first hook with comprehensive examples. +- **[Best practices](../hooks/best-practices)**: Guidelines on security, performance, and debugging. -- **[Hooks reference](/docs/hooks/reference)**: The definitive technical +- **[Hooks reference](../hooks/reference)**: The definitive technical specification of I/O schemas and exit codes. ## Core concepts @@ -152,8 +152,8 @@ Gemini CLI **fingerprints** project hooks. If a hook's name or command changes (e.g., via `git pull`), it is treated as a **new, untrusted hook** and you will be warned before it executes. -See [Security Considerations](/docs/hooks/best-practices#using-hooks-securely) -for a detailed threat model. +See [Security Considerations](../hooks/best-practices#using-hooks-securely) for +a detailed threat model. ## Managing hooks diff --git a/docs/hooks/reference.md b/docs/hooks/reference.md index 445035b1aa..a750bc94b3 100644 --- a/docs/hooks/reference.md +++ b/docs/hooks/reference.md @@ -82,8 +82,8 @@ For `BeforeTool` and `AfterTool` events, the `matcher` field in your settings is compared against the name of the tool being executed. - **Built-in Tools**: You can match any built-in tool (e.g., `read_file`, - `run_shell_command`). See the [Tools Reference](/docs/reference/tools) for a - full list of available tool names. + `run_shell_command`). See the [Tools Reference](../reference/tools) for a full + list of available tool names. - **MCP Tools**: Tools from MCP servers follow the naming pattern `mcp____`. - **Regex Support**: Matchers support regular expressions (e.g., diff --git a/docs/reference/policy-engine.md b/docs/reference/policy-engine.md index 17d958acd0..e8de8c5aff 100644 --- a/docs/reference/policy-engine.md +++ b/docs/reference/policy-engine.md @@ -143,8 +143,8 @@ always active. confirmation. - `autoEdit`: Optimized for automated code editing; some write tools may be auto-approved. -- `plan`: A strict, read-only mode for research and design. See [Customizing - Plan Mode Policies]. +- `plan`: A strict, read-only mode for research and design. See + [Customizing Plan Mode Policies](../cli/plan-mode.md#customizing-policies). - `yolo`: A mode where all tools are auto-approved (use with extreme caution). ## Rule matching @@ -360,5 +360,3 @@ out-of-the-box experience. - In **`yolo`** mode, a high-priority rule allows all tools. - In **`autoEdit`** mode, rules allow certain write operations to happen without prompting. - -[Customizing Plan Mode Policies]: /docs/cli/plan-mode.md#customizing-policies diff --git a/docs/resources/quota-and-pricing.md b/docs/resources/quota-and-pricing.md index 3d03dc395e..16d6b407b8 100644 --- a/docs/resources/quota-and-pricing.md +++ b/docs/resources/quota-and-pricing.md @@ -5,7 +5,7 @@ use cases. For enterprise or professional usage, or if you need increased quota, several options are available depending on your authentication account type. For a high-level comparison of available subscriptions and to select the right -quota for your needs, see the [Plans page](/plans/). +quota for your needs, see the [Plans page](https://geminicli.com/plans/). ## Overview diff --git a/docs/resources/tos-privacy.md b/docs/resources/tos-privacy.md index 88daf2639c..98d4a58b98 100644 --- a/docs/resources/tos-privacy.md +++ b/docs/resources/tos-privacy.md @@ -16,8 +16,8 @@ account. Your Gemini CLI Usage Statistics are handled in accordance with Google's Privacy Policy. -**Note:** See [quotas and pricing](/docs/resources/quota-and-pricing.md) for the -quota and pricing details that apply to your usage of the Gemini CLI. +**Note:** See [quotas and pricing](quota-and-pricing.md) for the quota and +pricing details that apply to your usage of the Gemini CLI. ## Supported authentication methods From 37ffd608fdd73174cd43ea02a3bfff3f42ff250b Mon Sep 17 00:00:00 2001 From: aworki <1224518406@qq.com> Date: Mon, 9 Mar 2026 23:31:05 +0800 Subject: [PATCH 2/6] feat(cli): expose /tools desc as explicit subcommand for discoverability (#21241) Co-authored-by: Coco Sheng Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com> --- .../cli/src/ui/commands/toolsCommand.test.ts | 24 ++++++ packages/cli/src/ui/commands/toolsCommand.ts | 77 +++++++++++-------- 2 files changed, 71 insertions(+), 30 deletions(-) diff --git a/packages/cli/src/ui/commands/toolsCommand.test.ts b/packages/cli/src/ui/commands/toolsCommand.test.ts index 257e6ba167..cfb6d4368e 100644 --- a/packages/cli/src/ui/commands/toolsCommand.test.ts +++ b/packages/cli/src/ui/commands/toolsCommand.test.ts @@ -110,4 +110,28 @@ describe('toolsCommand', () => { ); expect(message.tools[1].description).toBe('Edits code files.'); }); + + it('should expose a desc subcommand for TUI discoverability', async () => { + const descSubCommand = toolsCommand.subCommands?.find( + (cmd) => cmd.name === 'desc', + ); + expect(descSubCommand).toBeDefined(); + expect(descSubCommand?.description).toContain('descriptions'); + + const mockContext = createMockCommandContext({ + services: { + config: { + getToolRegistry: () => ({ getAllTools: () => mockTools }), + }, + }, + }); + + if (!descSubCommand?.action) throw new Error('Action not defined'); + await descSubCommand.action(mockContext, ''); + + const [message] = (mockContext.ui.addItem as ReturnType).mock + .calls[0]; + expect(message.type).toBe(MessageType.TOOLS_LIST); + expect(message.showDescriptions).toBe(true); + }); }); diff --git a/packages/cli/src/ui/commands/toolsCommand.ts b/packages/cli/src/ui/commands/toolsCommand.ts index ff772c5cc8..6a26d4f3d6 100644 --- a/packages/cli/src/ui/commands/toolsCommand.ts +++ b/packages/cli/src/ui/commands/toolsCommand.ts @@ -11,43 +11,60 @@ import { } from './types.js'; import { MessageType, type HistoryItemToolsList } from '../types.js'; +async function listTools( + context: CommandContext, + showDescriptions: boolean, +): Promise { + const toolRegistry = context.services.config?.getToolRegistry(); + if (!toolRegistry) { + context.ui.addItem({ + type: MessageType.ERROR, + text: 'Could not retrieve tool registry.', + }); + return; + } + + const tools = toolRegistry.getAllTools(); + // Filter out MCP tools by checking for the absence of a serverName property + const geminiTools = tools.filter((tool) => !('serverName' in tool)); + + const toolsListItem: HistoryItemToolsList = { + type: MessageType.TOOLS_LIST, + tools: geminiTools.map((tool) => ({ + name: tool.name, + displayName: tool.displayName, + description: tool.description, + })), + showDescriptions, + }; + + context.ui.addItem(toolsListItem); +} + +const toolsDescSubCommand: SlashCommand = { + name: 'desc', + altNames: ['descriptions'], + description: 'List available Gemini CLI tools with descriptions.', + kind: CommandKind.BUILT_IN, + autoExecute: true, + action: async (context: CommandContext): Promise => + listTools(context, true), +}; + export const toolsCommand: SlashCommand = { name: 'tools', - description: 'List available Gemini CLI tools. Usage: /tools [desc]', + description: + 'List available Gemini CLI tools. Use /tools desc to include descriptions.', kind: CommandKind.BUILT_IN, autoExecute: false, + subCommands: [toolsDescSubCommand], action: async (context: CommandContext, args?: string): Promise => { const subCommand = args?.trim(); - // Default to NOT showing descriptions. The user must opt in with an argument. - let useShowDescriptions = false; - if (subCommand === 'desc' || subCommand === 'descriptions') { - useShowDescriptions = true; - } + // Keep backward compatibility for typed arguments while exposing desc in TUI via subcommands. + const useShowDescriptions = + subCommand === 'desc' || subCommand === 'descriptions'; - const toolRegistry = context.services.config?.getToolRegistry(); - if (!toolRegistry) { - context.ui.addItem({ - type: MessageType.ERROR, - text: 'Could not retrieve tool registry.', - }); - return; - } - - const tools = toolRegistry.getAllTools(); - // Filter out MCP tools by checking for the absence of a serverName property - const geminiTools = tools.filter((tool) => !('serverName' in tool)); - - const toolsListItem: HistoryItemToolsList = { - type: MessageType.TOOLS_LIST, - tools: geminiTools.map((tool) => ({ - name: tool.name, - displayName: tool.displayName, - description: tool.description, - })), - showDescriptions: useShowDescriptions, - }; - - context.ui.addItem(toolsListItem); + await listTools(context, useShowDescriptions); }, }; From a253938ac511268a191894c3f6cb7326ace2e234 Mon Sep 17 00:00:00 2001 From: Jack Wotherspoon Date: Mon, 9 Mar 2026 16:45:42 +0100 Subject: [PATCH 3/6] feat(cli): add /compact alias for /compress command (#21711) --- packages/cli/src/ui/commands/compressCommand.test.ts | 8 ++++++++ packages/cli/src/ui/commands/compressCommand.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/ui/commands/compressCommand.test.ts b/packages/cli/src/ui/commands/compressCommand.test.ts index ed1e134560..5fd6f8dc6a 100644 --- a/packages/cli/src/ui/commands/compressCommand.test.ts +++ b/packages/cli/src/ui/commands/compressCommand.test.ts @@ -131,4 +131,12 @@ describe('compressCommand', () => { await compressCommand.action!(context, ''); expect(context.ui.setPendingItem).toHaveBeenCalledWith(null); }); + + describe('metadata', () => { + it('should have the correct name and aliases', () => { + expect(compressCommand.name).toBe('compress'); + expect(compressCommand.altNames).toContain('summarize'); + expect(compressCommand.altNames).toContain('compact'); + }); + }); }); diff --git a/packages/cli/src/ui/commands/compressCommand.ts b/packages/cli/src/ui/commands/compressCommand.ts index 3bb5b34383..560426b917 100644 --- a/packages/cli/src/ui/commands/compressCommand.ts +++ b/packages/cli/src/ui/commands/compressCommand.ts @@ -11,7 +11,7 @@ import { CommandKind } from './types.js'; export const compressCommand: SlashCommand = { name: 'compress', - altNames: ['summarize'], + altNames: ['summarize', 'compact'], description: 'Compresses the context by replacing it with a summary', kind: CommandKind.BUILT_IN, autoExecute: true, From 35ee2a841a7ea9e82772232f38b1200c622e02a6 Mon Sep 17 00:00:00 2001 From: Jerop Kipruto Date: Mon, 9 Mar 2026 11:58:46 -0400 Subject: [PATCH 4/6] feat(plan): enable Plan Mode by default (#21713) --- docs/cli/plan-mode.md | 25 +++---------------- docs/cli/settings.md | 2 +- docs/reference/commands.md | 4 +-- docs/reference/configuration.md | 4 +-- packages/cli/src/acp/acpClient.test.ts | 4 +-- packages/cli/src/acp/acpResume.test.ts | 7 +++++- packages/cli/src/config/config.test.ts | 4 +-- .../cli/src/config/settingsSchema.test.ts | 6 ++--- packages/cli/src/config/settingsSchema.ts | 4 +-- .../src/services/BuiltinCommandLoader.test.ts | 4 +-- .../cli/src/ui/components/Composer.test.tsx | 2 +- .../ui/hooks/useApprovalModeIndicator.test.ts | 2 +- packages/core/src/config/config.test.ts | 4 +-- packages/core/src/config/config.ts | 2 +- schemas/settings.schema.json | 6 ++--- 15 files changed, 32 insertions(+), 48 deletions(-) diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index 149278f50b..617f8492fb 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -1,4 +1,4 @@ -# Plan Mode (experimental) +# Plan Mode Plan Mode is a read-only environment for architecting robust solutions before implementation. With Plan Mode, you can: @@ -8,27 +8,8 @@ implementation. With Plan Mode, you can: - **Design:** Understand problems, evaluate trade-offs, and choose a solution. - **Plan:** Align on an execution strategy before any code is modified. -> **Note:** This is a preview feature currently under active development. Your -> feedback is invaluable as we refine this feature. If you have ideas, -> suggestions, or encounter issues: -> -> - [Open an issue] on GitHub. -> - Use the **/bug** command within Gemini CLI to file an issue. - -## How to enable Plan Mode - -Enable Plan Mode in **Settings** or by editing your configuration file. - -- **Settings:** Use the `/settings` command and set **Plan** to `true`. -- **Configuration:** Add the following to your `settings.json`: - - ```json - { - "experimental": { - "plan": true - } - } - ``` +Plan Mode is enabled by default. You can manage this setting using the +`/settings` command. ## How to enter Plan Mode diff --git a/docs/cli/settings.md b/docs/cli/settings.md index 182217fc0e..5565a5e1f6 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -144,7 +144,7 @@ they appear in the UI. | Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` | | Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` | | Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` | -| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` | +| Plan | `experimental.plan` | Enable Plan Mode. | `true` | | Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` | | Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` | diff --git a/docs/reference/commands.md b/docs/reference/commands.md index b23f545501..aafb8c8566 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -279,8 +279,8 @@ Slash commands provide meta-level control over the CLI itself. - **Description:** Switch to Plan Mode (read-only) and view the current plan if one has been generated. - - **Note:** This feature requires the `experimental.plan` setting to be - enabled in your configuration. + - **Note:** This feature is enabled by default. It can be disabled via the + `experimental.plan` setting in your configuration. - **Sub-commands:** - **`copy`**: - **Description:** Copy the currently approved plan to your clipboard. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 9b89fe75a8..b1d1f7f021 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1021,8 +1021,8 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `false` - **`experimental.plan`** (boolean): - - **Description:** Enable planning features (Plan Mode and tools). - - **Default:** `false` + - **Description:** Enable Plan Mode. + - **Default:** `true` - **Requires restart:** Yes - **`experimental.taskTracker`** (boolean): diff --git a/packages/cli/src/acp/acpClient.test.ts b/packages/cli/src/acp/acpClient.test.ts index 0922e3a510..e2fc0f0d33 100644 --- a/packages/cli/src/acp/acpClient.test.ts +++ b/packages/cli/src/acp/acpClient.test.ts @@ -172,7 +172,7 @@ describe('GeminiAgent', () => { unsubscribe: vi.fn(), }), getApprovalMode: vi.fn().mockReturnValue('default'), - isPlanEnabled: vi.fn().mockReturnValue(false), + isPlanEnabled: vi.fn().mockReturnValue(true), getGemini31LaunchedSync: vi.fn().mockReturnValue(false), getHasAccessToPreviewModel: vi.fn().mockReturnValue(false), getCheckpointingEnabled: vi.fn().mockReturnValue(false), @@ -650,7 +650,7 @@ describe('Session', () => { getMessageBus: vi.fn().mockReturnValue(mockMessageBus), setApprovalMode: vi.fn(), setModel: vi.fn(), - isPlanEnabled: vi.fn().mockReturnValue(false), + isPlanEnabled: vi.fn().mockReturnValue(true), getCheckpointingEnabled: vi.fn().mockReturnValue(false), getGitService: vi.fn().mockResolvedValue({} as GitService), waitForMcpInit: vi.fn(), diff --git a/packages/cli/src/acp/acpResume.test.ts b/packages/cli/src/acp/acpResume.test.ts index 37354af5c9..9668ef74f8 100644 --- a/packages/cli/src/acp/acpResume.test.ts +++ b/packages/cli/src/acp/acpResume.test.ts @@ -92,7 +92,7 @@ describe('GeminiAgent Session Resume', () => { getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'), }, getApprovalMode: vi.fn().mockReturnValue('default'), - isPlanEnabled: vi.fn().mockReturnValue(false), + isPlanEnabled: vi.fn().mockReturnValue(true), getModel: vi.fn().mockReturnValue('gemini-pro'), getHasAccessToPreviewModel: vi.fn().mockReturnValue(false), getGemini31LaunchedSync: vi.fn().mockReturnValue(false), @@ -204,6 +204,11 @@ describe('GeminiAgent Session Resume', () => { name: 'YOLO', description: 'Auto-approves all tools', }, + { + id: ApprovalMode.PLAN, + name: 'Plan', + description: 'Read-only mode', + }, ], currentModeId: ApprovalMode.DEFAULT, }, diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index a66d5e6589..22ff209cb6 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -2622,13 +2622,13 @@ describe('loadCliConfig approval mode', () => { expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT); }); - it('should throw error when --approval-mode=plan is used but experimental.plan setting is missing', async () => { + it('should allow plan approval mode by default when --approval-mode=plan is used', async () => { process.argv = ['node', 'script.js', '--approval-mode', 'plan']; const argv = await parseArguments(createTestMergedSettings()); const settings = createTestMergedSettings({}); const config = await loadCliConfig(settings, 'test-session', argv); - expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT); + expect(config.getApprovalMode()).toBe(ApprovalMode.PLAN); }); it('should pass planSettings.directory from settings to config', async () => { diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index 389cabe3cf..53d75bd436 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -424,12 +424,10 @@ describe('SettingsSchema', () => { expect(setting).toBeDefined(); expect(setting.type).toBe('boolean'); expect(setting.category).toBe('Experimental'); - expect(setting.default).toBe(false); + expect(setting.default).toBe(true); expect(setting.requiresRestart).toBe(true); expect(setting.showInDialog).toBe(true); - expect(setting.description).toBe( - 'Enable planning features (Plan Mode and tools).', - ); + expect(setting.description).toBe('Enable Plan Mode.'); }); it('should have hooksConfig.notifications setting in schema', () => { diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 4fa17916c4..bd1f9d82a4 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1823,8 +1823,8 @@ const SETTINGS_SCHEMA = { label: 'Plan', category: 'Experimental', requiresRestart: true, - default: false, - description: 'Enable planning features (Plan Mode and tools).', + default: true, + description: 'Enable Plan Mode.', showInDialog: true, }, taskTracker: { diff --git a/packages/cli/src/services/BuiltinCommandLoader.test.ts b/packages/cli/src/services/BuiltinCommandLoader.test.ts index 7b7832bfbe..6eb27862e3 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.test.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.test.ts @@ -151,7 +151,7 @@ describe('BuiltinCommandLoader', () => { vi.clearAllMocks(); mockConfig = { getFolderTrust: vi.fn().mockReturnValue(true), - isPlanEnabled: vi.fn().mockReturnValue(false), + isPlanEnabled: vi.fn().mockReturnValue(true), getEnableExtensionReloading: () => false, getEnableHooks: () => false, getEnableHooksUI: () => false, @@ -351,7 +351,7 @@ describe('BuiltinCommandLoader profile', () => { vi.resetModules(); mockConfig = { getFolderTrust: vi.fn().mockReturnValue(false), - isPlanEnabled: vi.fn().mockReturnValue(false), + isPlanEnabled: vi.fn().mockReturnValue(true), getCheckpointingEnabled: () => false, getEnableExtensionReloading: () => false, getEnableHooks: () => false, diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx index 9a6155da00..b1f804dd42 100644 --- a/packages/cli/src/ui/components/Composer.test.tsx +++ b/packages/cli/src/ui/components/Composer.test.tsx @@ -231,7 +231,7 @@ const createMockConfig = (overrides = {}): Config => getDebugMode: vi.fn(() => false), getAccessibility: vi.fn(() => ({})), getMcpServers: vi.fn(() => ({})), - isPlanEnabled: vi.fn(() => false), + isPlanEnabled: vi.fn(() => true), getToolRegistry: () => ({ getTool: vi.fn(), }), diff --git a/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts b/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts index 08ddd362f7..10d36ae01f 100644 --- a/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts +++ b/packages/cli/src/ui/hooks/useApprovalModeIndicator.test.ts @@ -86,7 +86,7 @@ describe('useApprovalModeIndicator', () => { (value: ApprovalMode) => void >, isYoloModeDisabled: vi.fn().mockReturnValue(false), - isPlanEnabled: vi.fn().mockReturnValue(false), + isPlanEnabled: vi.fn().mockReturnValue(true), isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>, getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>, getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock< diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 31e081c350..fc262e2b13 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -2788,9 +2788,9 @@ describe('Config Quota & Preview Model Access', () => { }); describe('isPlanEnabled', () => { - it('should return false by default', () => { + it('should return true by default', () => { const config = new Config(baseParams); - expect(config.isPlanEnabled()).toBe(false); + expect(config.isPlanEnabled()).toBe(true); }); it('should return true when plan is enabled', () => { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index e3201aa521..775547e3ec 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -885,7 +885,7 @@ export class Config implements McpContext { this.enableAgents = params.enableAgents ?? false; this.agents = params.agents ?? {}; this.disableLLMCorrection = params.disableLLMCorrection ?? true; - this.planEnabled = params.plan ?? false; + this.planEnabled = params.plan ?? true; this.trackerEnabled = params.tracker ?? false; this.planModeRoutingEnabled = params.planSettings?.modelRouting ?? true; this.enableEventDrivenScheduler = params.enableEventDrivenScheduler ?? true; diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index b2ef942711..280ad18db5 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -1712,9 +1712,9 @@ }, "plan": { "title": "Plan", - "description": "Enable planning features (Plan Mode and tools).", - "markdownDescription": "Enable planning features (Plan Mode and tools).\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`", - "default": false, + "description": "Enable Plan Mode.", + "markdownDescription": "Enable Plan Mode.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `true`", + "default": true, "type": "boolean" }, "taskTracker": { From 7837194ab54fb66ee88153734c9be53b10085340 Mon Sep 17 00:00:00 2001 From: Adib234 <30782825+Adib234@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:02:13 -0400 Subject: [PATCH 5/6] fix(core): resolve symlinks for non-existent paths during validation (#21487) --- packages/core/src/config/config.ts | 17 ++--------- packages/core/src/config/storage.test.ts | 8 ++---- packages/core/src/utils/paths.test.ts | 25 +++++++++++++++-- packages/core/src/utils/paths.ts | 31 ++++++++++++++++----- packages/core/src/utils/workspaceContext.ts | 30 ++------------------ 5 files changed, 55 insertions(+), 56 deletions(-) diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 775547e3ec..adb36ca3a3 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -6,7 +6,6 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import * as os from 'node:os'; import { inspect } from 'node:util'; import process from 'node:process'; import { @@ -146,7 +145,7 @@ import { SkillManager, type SkillDefinition } from '../skills/skillManager.js'; import { startupProfiler } from '../telemetry/startupProfiler.js'; import type { AgentDefinition } from '../agents/types.js'; import { fetchAdminControls } from '../code_assist/admin/admin_controls.js'; -import { isSubpath } from '../utils/paths.js'; +import { isSubpath, resolveToRealPath } from '../utils/paths.js'; import { UserHintService } from './userHintService.js'; import { WORKSPACE_POLICY_TIER } from '../policy/config.js'; import { loadPoliciesFromToml } from '../policy/toml-loader.js'; @@ -2389,17 +2388,7 @@ export class Config implements McpContext { * @returns true if the path is allowed, false otherwise. */ isPathAllowed(absolutePath: string): boolean { - const realpath = (p: string) => { - let resolved: string; - try { - resolved = fs.realpathSync(p); - } catch { - resolved = path.resolve(p); - } - return os.platform() === 'win32' ? resolved.toLowerCase() : resolved; - }; - - const resolvedPath = realpath(absolutePath); + const resolvedPath = resolveToRealPath(absolutePath); const workspaceContext = this.getWorkspaceContext(); if (workspaceContext.isPathWithinWorkspace(resolvedPath)) { @@ -2407,7 +2396,7 @@ export class Config implements McpContext { } const projectTempDir = this.storage.getProjectTempDir(); - const resolvedTempDir = realpath(projectTempDir); + const resolvedTempDir = resolveToRealPath(projectTempDir); return isSubpath(resolvedTempDir, resolvedPath); } diff --git a/packages/core/src/config/storage.test.ts b/packages/core/src/config/storage.test.ts index 15b49d12f1..6b1cd39d88 100644 --- a/packages/core/src/config/storage.test.ts +++ b/packages/core/src/config/storage.test.ts @@ -24,7 +24,7 @@ vi.mock('fs', async (importOriginal) => { }); import { Storage } from './storage.js'; -import { GEMINI_DIR, homedir } from '../utils/paths.js'; +import { GEMINI_DIR, homedir, resolveToRealPath } from '../utils/paths.js'; import { ProjectRegistry } from './projectRegistry.js'; import { StorageMigration } from './storageMigration.js'; @@ -279,8 +279,7 @@ describe('Storage – additional helpers', () => { name: 'custom absolute path outside throws', customDir: '/absolute/path/to/plans', expected: '', - expectedError: - "Custom plans directory '/absolute/path/to/plans' resolves to '/absolute/path/to/plans', which is outside the project root '/tmp/project'.", + expectedError: `Custom plans directory '/absolute/path/to/plans' resolves to '/absolute/path/to/plans', which is outside the project root '${resolveToRealPath(projectRoot)}'.`, }, { name: 'absolute path that happens to be inside project root', @@ -306,8 +305,7 @@ describe('Storage – additional helpers', () => { name: 'escaping relative path throws', customDir: '../escaped-plans', expected: '', - expectedError: - "Custom plans directory '../escaped-plans' resolves to '/tmp/escaped-plans', which is outside the project root '/tmp/project'.", + expectedError: `Custom plans directory '../escaped-plans' resolves to '${resolveToRealPath(path.resolve(projectRoot, '../escaped-plans'))}', which is outside the project root '${resolveToRealPath(projectRoot)}'.`, }, { name: 'hidden directory starting with ..', diff --git a/packages/core/src/utils/paths.test.ts b/packages/core/src/utils/paths.test.ts index a3151438bb..227afaf44a 100644 --- a/packages/core/src/utils/paths.test.ts +++ b/packages/core/src/utils/paths.test.ts @@ -510,9 +510,11 @@ describe('resolveToRealPath', () => { expect(resolveToRealPath(input)).toBe(expected); }); - it('should return decoded path even if fs.realpathSync fails', () => { + it('should return decoded path even if fs.realpathSync fails with ENOENT', () => { vi.spyOn(fs, 'realpathSync').mockImplementationOnce(() => { - throw new Error('File not found'); + const err = new Error('File not found') as NodeJS.ErrnoException; + err.code = 'ENOENT'; + throw err; }); const p = path.resolve('path', 'to', 'New Project'); @@ -521,6 +523,25 @@ describe('resolveToRealPath', () => { expect(resolveToRealPath(input)).toBe(expected); }); + + it('should recursively resolve symlinks for non-existent child paths', () => { + const parentPath = path.resolve('/some/parent/path'); + const resolvedParentPath = path.resolve('/resolved/parent/path'); + const childPath = path.resolve(parentPath, 'child', 'file.txt'); + const expectedPath = path.resolve(resolvedParentPath, 'child', 'file.txt'); + + vi.spyOn(fs, 'realpathSync').mockImplementation((p) => { + const pStr = p.toString(); + if (pStr === parentPath) { + return resolvedParentPath; + } + const err = new Error('ENOENT') as NodeJS.ErrnoException; + err.code = 'ENOENT'; + throw err; + }); + + expect(resolveToRealPath(childPath)).toBe(expectedPath); + }); }); describe('normalizePath', () => { diff --git a/packages/core/src/utils/paths.ts b/packages/core/src/utils/paths.ts index f446f31d90..aa167e3558 100644 --- a/packages/core/src/utils/paths.ts +++ b/packages/core/src/utils/paths.ts @@ -359,8 +359,8 @@ export function isSubpath(parentPath: string, childPath: string): boolean { * @param pathStr The path string to resolve. * @returns The resolved real path. */ -export function resolveToRealPath(path: string): string { - let resolvedPath = path; +export function resolveToRealPath(pathStr: string): string { + let resolvedPath = pathStr; try { if (resolvedPath.startsWith('file://')) { @@ -372,11 +372,28 @@ export function resolveToRealPath(path: string): string { // Ignore error (e.g. malformed URI), keep path from previous step } + return robustRealpath(path.resolve(resolvedPath)); +} + +function robustRealpath(p: string): string { try { - return fs.realpathSync(resolvedPath); - } catch (_e) { - // If realpathSync fails, it might be because the path doesn't exist. - // In that case, we can fall back to the path processed. - return resolvedPath; + return fs.realpathSync(p); + } catch (e: unknown) { + if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') { + try { + const stat = fs.lstatSync(p); + if (stat.isSymbolicLink()) { + const target = fs.readlinkSync(p); + const resolvedTarget = path.resolve(path.dirname(p), target); + return robustRealpath(resolvedTarget); + } + } catch { + // Not a symlink, or lstat failed. Just resolve parent. + } + const parent = path.dirname(p); + if (parent === p) return p; + return path.join(robustRealpath(parent), path.basename(p)); + } + throw e; } } diff --git a/packages/core/src/utils/workspaceContext.ts b/packages/core/src/utils/workspaceContext.ts index dfb47ce3be..7ca59fb184 100755 --- a/packages/core/src/utils/workspaceContext.ts +++ b/packages/core/src/utils/workspaceContext.ts @@ -4,10 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { isNodeError } from '../utils/errors.js'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { debugLogger } from './debugLogger.js'; +import { resolveToRealPath } from './paths.js'; export type Unsubscribe = () => void; @@ -227,22 +227,7 @@ export class WorkspaceContext { * if it did exist. */ private fullyResolvedPath(pathToCheck: string): string { - try { - return fs.realpathSync(path.resolve(this.targetDir, pathToCheck)); - } catch (e: unknown) { - if ( - isNodeError(e) && - e.code === 'ENOENT' && - e.path && - // realpathSync does not set e.path correctly for symlinks to - // non-existent files. - !this.isFileSymlink(e.path) - ) { - // If it doesn't exist, e.path contains the fully resolved path. - return e.path; - } - throw e; - } + return resolveToRealPath(path.resolve(this.targetDir, pathToCheck)); } /** @@ -262,15 +247,4 @@ export class WorkspaceContext { !path.isAbsolute(relative) ); } - - /** - * Checks if a file path is a symbolic link that points to a file. - */ - private isFileSymlink(filePath: string): boolean { - try { - return !fs.readlinkSync(filePath).endsWith('/'); - } catch (_error) { - return false; - } - } } From 96b939f63a7eae3010082a92df810ab659227b5c Mon Sep 17 00:00:00 2001 From: joshualitt Date: Mon, 9 Mar 2026 09:02:20 -0700 Subject: [PATCH 6/6] feat(core): Introduce `AgentLoopContext`. (#21198) --- .../a2a-server/src/utils/testing_utils.ts | 8 ++ .../core/src/agents/agent-scheduler.test.ts | 4 +- .../core/src/config/agent-loop-context.ts | 27 ++++ packages/core/src/config/config.ts | 123 +++++++++++------- .../core/src/core/coreToolScheduler.test.ts | 2 + packages/core/src/core/coreToolScheduler.ts | 2 +- packages/core/src/index.ts | 1 + packages/core/src/scheduler/policy.test.ts | 56 +++++++- packages/core/src/scheduler/scheduler.test.ts | 6 +- packages/core/src/scheduler/scheduler.ts | 15 ++- .../src/scheduler/scheduler_parallel.test.ts | 4 +- .../scheduler_waiting_callback.test.ts | 2 +- .../core/src/scheduler/tool-executor.test.ts | 2 +- packages/core/src/scheduler/tool-executor.ts | 10 +- 14 files changed, 196 insertions(+), 66 deletions(-) create mode 100644 packages/core/src/config/agent-loop-context.ts diff --git a/packages/a2a-server/src/utils/testing_utils.ts b/packages/a2a-server/src/utils/testing_utils.ts index 977daedf16..7d77d8dc9a 100644 --- a/packages/a2a-server/src/utils/testing_utils.ts +++ b/packages/a2a-server/src/utils/testing_utils.ts @@ -75,6 +75,14 @@ export function createMockConfig( validatePathAccess: vi.fn().mockReturnValue(undefined), ...overrides, } as unknown as Config; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (mockConfig as unknown as { config: Config; promptId: string }).config = + mockConfig; + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (mockConfig as unknown as { config: Config; promptId: string }).promptId = + 'test-prompt-id'; + mockConfig.getMessageBus = vi.fn().mockReturnValue(createMockMessageBus()); mockConfig.getHookSystem = vi .fn() diff --git a/packages/core/src/agents/agent-scheduler.test.ts b/packages/core/src/agents/agent-scheduler.test.ts index 5edcb664b6..dd6749d3a0 100644 --- a/packages/core/src/agents/agent-scheduler.test.ts +++ b/packages/core/src/agents/agent-scheduler.test.ts @@ -30,7 +30,7 @@ describe('agent-scheduler', () => { } as unknown as Mocked; mockConfig = { getMessageBus: vi.fn().mockReturnValue(mockMessageBus), - getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry), + toolRegistry: mockToolRegistry, } as unknown as Mocked; }); @@ -69,6 +69,6 @@ describe('agent-scheduler', () => { // Verify that the scheduler's config has the overridden tool registry const schedulerConfig = vi.mocked(Scheduler).mock.calls[0][0].config; - expect(schedulerConfig.getToolRegistry()).toBe(mockToolRegistry); + expect(schedulerConfig.toolRegistry).toBe(mockToolRegistry); }); }); diff --git a/packages/core/src/config/agent-loop-context.ts b/packages/core/src/config/agent-loop-context.ts new file mode 100644 index 0000000000..0a7334c334 --- /dev/null +++ b/packages/core/src/config/agent-loop-context.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { GeminiClient } from '../core/client.js'; +import type { MessageBus } from '../confirmation-bus/message-bus.js'; +import type { ToolRegistry } from '../tools/tool-registry.js'; + +/** + * AgentLoopContext represents the execution-scoped view of the world for a single + * agent turn or sub-agent loop. + */ +export interface AgentLoopContext { + /** The unique ID for the current user turn or agent thought loop. */ + readonly promptId: string; + + /** The registry of tools available to the agent in this context. */ + readonly toolRegistry: ToolRegistry; + + /** The bus for user confirmations and messages in this context. */ + readonly messageBus: MessageBus; + + /** The client used to communicate with the LLM in this context. */ + readonly geminiClient: GeminiClient; +} diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index adb36ca3a3..f615564533 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -96,6 +96,7 @@ import type { import { ModelAvailabilityService } from '../availability/modelAvailabilityService.js'; import { ModelRouterService } from '../routing/modelRouterService.js'; import { OutputFormat } from '../output/types.js'; +//import { type AgentLoopContext } from './agent-loop-context.js'; import { ModelConfigService, type ModelConfig, @@ -154,6 +155,7 @@ import { CheckerRunner } from '../safety/checker-runner.js'; import { ContextBuilder } from '../safety/context-builder.js'; import { CheckerRegistry } from '../safety/registry.js'; import { ConsecaSafetyChecker } from '../safety/conseca/conseca.js'; +import type { AgentLoopContext } from './agent-loop-context.js'; export interface AccessibilitySettings { /** @deprecated Use ui.loadingPhrases instead. */ @@ -598,8 +600,8 @@ export interface ConfigParameters { }; } -export class Config implements McpContext { - private toolRegistry!: ToolRegistry; +export class Config implements McpContext, AgentLoopContext { + private _toolRegistry!: ToolRegistry; private mcpClientManager?: McpClientManager; private allowedMcpServers: string[]; private blockedMcpServers: string[]; @@ -611,7 +613,7 @@ export class Config implements McpContext { private agentRegistry!: AgentRegistry; private readonly acknowledgedAgentsService: AcknowledgedAgentsService; private skillManager!: SkillManager; - private sessionId: string; + private _sessionId: string; private clientVersion: string; private fileSystemService: FileSystemService; private trackerService?: TrackerService; @@ -645,7 +647,7 @@ export class Config implements McpContext { private readonly accessibility: AccessibilitySettings; private readonly telemetrySettings: TelemetrySettings; private readonly usageStatisticsEnabled: boolean; - private geminiClient!: GeminiClient; + private _geminiClient!: GeminiClient; private baseLlmClient!: BaseLlmClient; private localLiteRtLmClient?: LocalLiteRtLmClient; private modelRouterService: ModelRouterService; @@ -740,7 +742,7 @@ export class Config implements McpContext { private readonly fileExclusions: FileExclusions; private readonly eventEmitter?: EventEmitter; private readonly useWriteTodos: boolean; - private readonly messageBus: MessageBus; + private readonly _messageBus: MessageBus; private readonly policyEngine: PolicyEngine; private policyUpdateConfirmationRequest: | PolicyUpdateConfirmationRequest @@ -806,7 +808,7 @@ export class Config implements McpContext { private approvedPlanPath: string | undefined; constructor(params: ConfigParameters) { - this.sessionId = params.sessionId; + this._sessionId = params.sessionId; this.clientVersion = params.clientVersion ?? 'unknown'; this.approvedPlanPath = undefined; this.embeddingModel = @@ -961,7 +963,7 @@ export class Config implements McpContext { (params.shellToolInactivityTimeout ?? 300) * 1000; // 5 minutes this.extensionManagement = params.extensionManagement ?? true; this.enableExtensionReloading = params.enableExtensionReloading ?? false; - this.storage = new Storage(this.targetDir, this.sessionId); + this.storage = new Storage(this.targetDir, this._sessionId); this.storage.setCustomPlansDir(params.planSettings?.directory); this.fakeResponses = params.fakeResponses; @@ -997,7 +999,7 @@ export class Config implements McpContext { ConsecaSafetyChecker.getInstance().setConfig(this); } - this.messageBus = new MessageBus(this.policyEngine, this.debugMode); + this._messageBus = new MessageBus(this.policyEngine, this.debugMode); this.acknowledgedAgentsService = new AcknowledgedAgentsService(); this.skillManager = new SkillManager(); this.outputSettings = { @@ -1057,7 +1059,7 @@ export class Config implements McpContext { ); } } - this.geminiClient = new GeminiClient(this); + this._geminiClient = new GeminiClient(this); this.modelRouterService = new ModelRouterService(this); // HACK: The settings loading logic doesn't currently merge the default @@ -1142,11 +1144,11 @@ export class Config implements McpContext { coreEvents.on(CoreEvent.AgentsRefreshed, this.onAgentsRefreshed); - this.toolRegistry = await this.createToolRegistry(); + this._toolRegistry = await this.createToolRegistry(); discoverToolsHandle?.end(); this.mcpClientManager = new McpClientManager( this.clientVersion, - this.toolRegistry, + this._toolRegistry, this, this.eventEmitter, ); @@ -1181,7 +1183,7 @@ export class Config implements McpContext { if (this.getSkillManager().getSkills().length > 0) { this.getToolRegistry().unregisterTool(ActivateSkillTool.Name); this.getToolRegistry().registerTool( - new ActivateSkillTool(this, this.messageBus), + new ActivateSkillTool(this, this._messageBus), ); } } @@ -1198,7 +1200,7 @@ export class Config implements McpContext { await this.contextManager.refresh(); } - await this.geminiClient.initialize(); + await this._geminiClient.initialize(); this.initialized = true; } @@ -1222,7 +1224,7 @@ export class Config implements McpContext { authMethod !== AuthType.USE_GEMINI ) { // Restore the conversation history to the new client - this.geminiClient.stripThoughtsFromHistory(); + this._geminiClient.stripThoughtsFromHistory(); } // Reset availability status when switching auth (e.g. from limited key to OAuth) @@ -1343,12 +1345,28 @@ export class Config implements McpContext { return this.localLiteRtLmClient; } + get promptId(): string { + return this._sessionId; + } + + get toolRegistry(): ToolRegistry { + return this._toolRegistry; + } + + get messageBus(): MessageBus { + return this._messageBus; + } + + get geminiClient(): GeminiClient { + return this._geminiClient; + } + getSessionId(): string { - return this.sessionId; + return this.promptId; } setSessionId(sessionId: string): void { - this.sessionId = sessionId; + this._sessionId = sessionId; } setTerminalBackground(terminalBackground: string | undefined): void { @@ -1613,6 +1631,7 @@ export class Config implements McpContext { return this.acknowledgedAgentsService; } + /** @deprecated Use toolRegistry getter */ getToolRegistry(): ToolRegistry { return this.toolRegistry; } @@ -1889,9 +1908,9 @@ export class Config implements McpContext { ); await refreshServerHierarchicalMemory(this); } - if (this.geminiClient?.isInitialized()) { - await this.geminiClient.setTools(); - this.geminiClient.updateSystemInstruction(); + if (this._geminiClient?.isInitialized()) { + await this._geminiClient.setTools(); + this._geminiClient.updateSystemInstruction(); } } @@ -2045,8 +2064,8 @@ export class Config implements McpContext { (currentMode === ApprovalMode.YOLO || mode === ApprovalMode.YOLO); if (isPlanModeTransition || isYoloModeTransition) { - if (this.geminiClient?.isInitialized()) { - this.geminiClient.setTools().catch((err) => { + if (this._geminiClient?.isInitialized()) { + this._geminiClient.setTools().catch((err) => { debugLogger.error('Failed to update tools', err); }); } @@ -2142,6 +2161,7 @@ export class Config implements McpContext { return this.telemetrySettings.useCliAuth ?? false; } + /** @deprecated Use geminiClient getter */ getGeminiClient(): GeminiClient { return this.geminiClient; } @@ -2577,7 +2597,7 @@ export class Config implements McpContext { if (this.getSkillManager().getSkills().length > 0) { this.getToolRegistry().unregisterTool(ActivateSkillTool.Name); this.getToolRegistry().registerTool( - new ActivateSkillTool(this, this.messageBus), + new ActivateSkillTool(this, this._messageBus), ); } else { this.getToolRegistry().unregisterTool(ActivateSkillTool.Name); @@ -2703,6 +2723,7 @@ export class Config implements McpContext { return this.fileExclusions; } + /** @deprecated Use messageBus getter */ getMessageBus(): MessageBus { return this.messageBus; } @@ -2760,7 +2781,7 @@ export class Config implements McpContext { } async createToolRegistry(): Promise { - const registry = new ToolRegistry(this, this.messageBus); + const registry = new ToolRegistry(this, this._messageBus); // helper to create & register core tools that are enabled const maybeRegister = ( @@ -2790,10 +2811,10 @@ export class Config implements McpContext { }; maybeRegister(LSTool, () => - registry.registerTool(new LSTool(this, this.messageBus)), + registry.registerTool(new LSTool(this, this._messageBus)), ); maybeRegister(ReadFileTool, () => - registry.registerTool(new ReadFileTool(this, this.messageBus)), + registry.registerTool(new ReadFileTool(this, this._messageBus)), ); if (this.getUseRipgrep()) { @@ -2806,81 +2827,85 @@ export class Config implements McpContext { } if (useRipgrep) { maybeRegister(RipGrepTool, () => - registry.registerTool(new RipGrepTool(this, this.messageBus)), + registry.registerTool(new RipGrepTool(this, this._messageBus)), ); } else { logRipgrepFallback(this, new RipgrepFallbackEvent(errorString)); maybeRegister(GrepTool, () => - registry.registerTool(new GrepTool(this, this.messageBus)), + registry.registerTool(new GrepTool(this, this._messageBus)), ); } } else { maybeRegister(GrepTool, () => - registry.registerTool(new GrepTool(this, this.messageBus)), + registry.registerTool(new GrepTool(this, this._messageBus)), ); } maybeRegister(GlobTool, () => - registry.registerTool(new GlobTool(this, this.messageBus)), + registry.registerTool(new GlobTool(this, this._messageBus)), ); maybeRegister(ActivateSkillTool, () => - registry.registerTool(new ActivateSkillTool(this, this.messageBus)), + registry.registerTool(new ActivateSkillTool(this, this._messageBus)), ); maybeRegister(EditTool, () => - registry.registerTool(new EditTool(this, this.messageBus)), + registry.registerTool(new EditTool(this, this._messageBus)), ); maybeRegister(WriteFileTool, () => - registry.registerTool(new WriteFileTool(this, this.messageBus)), + registry.registerTool(new WriteFileTool(this, this._messageBus)), ); maybeRegister(WebFetchTool, () => - registry.registerTool(new WebFetchTool(this, this.messageBus)), + registry.registerTool(new WebFetchTool(this, this._messageBus)), ); maybeRegister(ShellTool, () => - registry.registerTool(new ShellTool(this, this.messageBus)), + registry.registerTool(new ShellTool(this, this._messageBus)), ); maybeRegister(MemoryTool, () => - registry.registerTool(new MemoryTool(this.messageBus)), + registry.registerTool(new MemoryTool(this._messageBus)), ); maybeRegister(WebSearchTool, () => - registry.registerTool(new WebSearchTool(this, this.messageBus)), + registry.registerTool(new WebSearchTool(this, this._messageBus)), ); maybeRegister(AskUserTool, () => - registry.registerTool(new AskUserTool(this.messageBus)), + registry.registerTool(new AskUserTool(this._messageBus)), ); if (this.getUseWriteTodos()) { maybeRegister(WriteTodosTool, () => - registry.registerTool(new WriteTodosTool(this.messageBus)), + registry.registerTool(new WriteTodosTool(this._messageBus)), ); } if (this.isPlanEnabled()) { maybeRegister(ExitPlanModeTool, () => - registry.registerTool(new ExitPlanModeTool(this, this.messageBus)), + registry.registerTool(new ExitPlanModeTool(this, this._messageBus)), ); maybeRegister(EnterPlanModeTool, () => - registry.registerTool(new EnterPlanModeTool(this, this.messageBus)), + registry.registerTool(new EnterPlanModeTool(this, this._messageBus)), ); } if (this.isTrackerEnabled()) { maybeRegister(TrackerCreateTaskTool, () => - registry.registerTool(new TrackerCreateTaskTool(this, this.messageBus)), + registry.registerTool( + new TrackerCreateTaskTool(this, this._messageBus), + ), ); maybeRegister(TrackerUpdateTaskTool, () => - registry.registerTool(new TrackerUpdateTaskTool(this, this.messageBus)), + registry.registerTool( + new TrackerUpdateTaskTool(this, this._messageBus), + ), ); maybeRegister(TrackerGetTaskTool, () => - registry.registerTool(new TrackerGetTaskTool(this, this.messageBus)), + registry.registerTool(new TrackerGetTaskTool(this, this._messageBus)), ); maybeRegister(TrackerListTasksTool, () => - registry.registerTool(new TrackerListTasksTool(this, this.messageBus)), + registry.registerTool(new TrackerListTasksTool(this, this._messageBus)), ); maybeRegister(TrackerAddDependencyTool, () => registry.registerTool( - new TrackerAddDependencyTool(this, this.messageBus), + new TrackerAddDependencyTool(this, this._messageBus), ), ); maybeRegister(TrackerVisualizeTool, () => - registry.registerTool(new TrackerVisualizeTool(this, this.messageBus)), + registry.registerTool(new TrackerVisualizeTool(this, this._messageBus)), ); } @@ -3007,8 +3032,8 @@ export class Config implements McpContext { } private onAgentsRefreshed = async () => { - if (this.toolRegistry) { - this.registerSubAgentTools(this.toolRegistry); + if (this._toolRegistry) { + this.registerSubAgentTools(this._toolRegistry); } // Propagate updates to the active chat session const client = this.getGeminiClient(); @@ -3029,7 +3054,7 @@ export class Config implements McpContext { this.logCurrentModeDuration(this.getApprovalMode()); coreEvents.off(CoreEvent.AgentsRefreshed, this.onAgentsRefreshed); this.agentRegistry?.dispose(); - this.geminiClient?.dispose(); + this._geminiClient?.dispose(); if (this.mcpClientManager) { await this.mcpClientManager.stop(); } diff --git a/packages/core/src/core/coreToolScheduler.test.ts b/packages/core/src/core/coreToolScheduler.test.ts index fcddc05a44..a2f98dde98 100644 --- a/packages/core/src/core/coreToolScheduler.test.ts +++ b/packages/core/src/core/coreToolScheduler.test.ts @@ -290,6 +290,8 @@ function createMockConfig(overrides: Partial = {}): Config { const finalConfig = { ...baseConfig, ...overrides } as Config; + (finalConfig as unknown as { config: Config }).config = finalConfig; + // Patch the policy engine to use the final config if not overridden if (!overrides.getPolicyEngine) { finalConfig.getPolicyEngine = () => diff --git a/packages/core/src/core/coreToolScheduler.ts b/packages/core/src/core/coreToolScheduler.ts index 23473e199d..15b7f1932b 100644 --- a/packages/core/src/core/coreToolScheduler.ts +++ b/packages/core/src/core/coreToolScheduler.ts @@ -133,7 +133,7 @@ export class CoreToolScheduler { this.onAllToolCallsComplete = options.onAllToolCallsComplete; this.onToolCallsUpdate = options.onToolCallsUpdate; this.getPreferredEditor = options.getPreferredEditor; - this.toolExecutor = new ToolExecutor(this.config); + this.toolExecutor = new ToolExecutor(this.config, this.config); this.toolModifier = new ToolModificationHandler(); // Subscribe to message bus for ASK_USER policy decisions diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 64b27493a0..5dfd74ad61 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -6,6 +6,7 @@ // Export config export * from './config/config.js'; +export * from './config/agent-loop-context.js'; export * from './config/memory.js'; export * from './config/defaultModelConfigs.js'; export * from './config/models.js'; diff --git a/packages/core/src/scheduler/policy.test.ts b/packages/core/src/scheduler/policy.test.ts index 05f5b08a2f..9320893bd6 100644 --- a/packages/core/src/scheduler/policy.test.ts +++ b/packages/core/src/scheduler/policy.test.ts @@ -49,6 +49,9 @@ describe('policy.ts', () => { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), } as unknown as Mocked; + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; + const toolCall = { request: { name: 'test-tool', args: {} }, tool: { name: 'test-tool' }, @@ -72,6 +75,9 @@ describe('policy.ts', () => { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), } as unknown as Mocked; + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; + const mcpTool = Object.create(DiscoveredMCPTool.prototype); mcpTool.serverName = 'my-server'; mcpTool._toolAnnotations = { readOnlyHint: true }; @@ -99,6 +105,9 @@ describe('policy.ts', () => { isInteractive: vi.fn().mockReturnValue(false), } as unknown as Mocked; + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; + const toolCall = { request: { name: 'test-tool', args: {} }, tool: { name: 'test-tool' }, @@ -118,6 +127,9 @@ describe('policy.ts', () => { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), } as unknown as Mocked; + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; + const toolCall = { request: { name: 'test-tool', args: {} }, tool: { name: 'test-tool' }, @@ -137,6 +149,9 @@ describe('policy.ts', () => { isInteractive: vi.fn().mockReturnValue(true), } as unknown as Mocked; + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; + const toolCall = { request: { name: 'test-tool', args: {} }, tool: { name: 'test-tool' }, @@ -152,6 +167,9 @@ describe('policy.ts', () => { const mockConfig = { setApprovalMode: vi.fn(), } as unknown as Mocked; + + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; const mockMessageBus = { publish: vi.fn(), } as unknown as Mocked; @@ -175,6 +193,9 @@ describe('policy.ts', () => { const mockConfig = { setApprovalMode: vi.fn(), } as unknown as Mocked; + + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; const mockMessageBus = { publish: vi.fn(), } as unknown as Mocked; @@ -200,6 +221,9 @@ describe('policy.ts', () => { const mockConfig = { setApprovalMode: vi.fn(), } as unknown as Mocked; + + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; const mockMessageBus = { publish: vi.fn(), } as unknown as Mocked; @@ -225,6 +249,9 @@ describe('policy.ts', () => { const mockConfig = { setApprovalMode: vi.fn(), } as unknown as Mocked; + + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; const mockMessageBus = { publish: vi.fn(), } as unknown as Mocked; @@ -256,6 +283,9 @@ describe('policy.ts', () => { const mockConfig = { setApprovalMode: vi.fn(), } as unknown as Mocked; + + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; const mockMessageBus = { publish: vi.fn(), } as unknown as Mocked; @@ -290,6 +320,9 @@ describe('policy.ts', () => { const mockConfig = { setApprovalMode: vi.fn(), } as unknown as Mocked; + + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; const mockMessageBus = { publish: vi.fn(), } as unknown as Mocked; @@ -308,6 +341,9 @@ describe('policy.ts', () => { const mockConfig = { setApprovalMode: vi.fn(), } as unknown as Mocked; + + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; const mockMessageBus = { publish: vi.fn(), } as unknown as Mocked; @@ -325,6 +361,9 @@ describe('policy.ts', () => { const mockConfig = { setApprovalMode: vi.fn(), } as unknown as Mocked; + + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; const mockMessageBus = { publish: vi.fn(), } as unknown as Mocked; @@ -344,6 +383,9 @@ describe('policy.ts', () => { const mockConfig = { setApprovalMode: vi.fn(), } as unknown as Mocked; + + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; const mockMessageBus = { publish: vi.fn(), } as unknown as Mocked; @@ -378,6 +420,9 @@ describe('policy.ts', () => { const mockConfig = { setApprovalMode: vi.fn(), } as unknown as Mocked; + + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; const mockMessageBus = { publish: vi.fn(), } as unknown as Mocked; @@ -410,6 +455,9 @@ describe('policy.ts', () => { const mockConfig = { setApprovalMode: vi.fn(), } as unknown as Mocked; + + (mockConfig as unknown as { config: Config }).config = + mockConfig as Config; const mockMessageBus = { publish: vi.fn(), } as unknown as Mocked; @@ -447,6 +495,8 @@ describe('policy.ts', () => { getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), } as unknown as Config; + (mockConfig as unknown as { config: Config }).config = mockConfig; + const { errorMessage, errorType } = getPolicyDenialError(mockConfig); expect(errorMessage).toBe('Tool execution denied by policy.'); @@ -457,6 +507,8 @@ describe('policy.ts', () => { const mockConfig = { getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), } as unknown as Config; + + (mockConfig as unknown as { config: Config }).config = mockConfig; const rule = { decision: PolicyDecision.DENY, denyMessage: 'Custom Deny', @@ -517,7 +569,8 @@ describe('Plan Mode Denial Consistency', () => { mockConfig = { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), - getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry), + toolRegistry: mockToolRegistry, + getToolRegistry: () => mockToolRegistry, getMessageBus: vi.fn().mockReturnValue(mockMessageBus), isInteractive: vi.fn().mockReturnValue(true), getEnableHooks: vi.fn().mockReturnValue(false), @@ -525,6 +578,7 @@ describe('Plan Mode Denial Consistency', () => { setApprovalMode: vi.fn(), getUsageStatisticsEnabled: vi.fn().mockReturnValue(false), } as unknown as Mocked; + (mockConfig as unknown as { config: Config }).config = mockConfig as Config; }); afterEach(() => { diff --git a/packages/core/src/scheduler/scheduler.test.ts b/packages/core/src/scheduler/scheduler.test.ts index ee5438c319..4d40101140 100644 --- a/packages/core/src/scheduler/scheduler.test.ts +++ b/packages/core/src/scheduler/scheduler.test.ts @@ -169,13 +169,15 @@ describe('Scheduler (Orchestrator)', () => { mockConfig = { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), - getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry), + toolRegistry: mockToolRegistry, isInteractive: vi.fn().mockReturnValue(true), getEnableHooks: vi.fn().mockReturnValue(true), setApprovalMode: vi.fn(), getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), } as unknown as Mocked; + (mockConfig as unknown as { config: Config }).config = mockConfig as Config; + mockMessageBus = { publish: vi.fn(), subscribe: vi.fn(), @@ -1320,6 +1322,8 @@ describe('Scheduler MCP Progress', () => { getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), } as unknown as Mocked; + (mockConfig as unknown as { config: Config }).config = mockConfig as Config; + mockMessageBus = { publish: vi.fn(), subscribe: vi.fn(), diff --git a/packages/core/src/scheduler/scheduler.ts b/packages/core/src/scheduler/scheduler.ts index 38e001ea90..613e23b2d6 100644 --- a/packages/core/src/scheduler/scheduler.ts +++ b/packages/core/src/scheduler/scheduler.ts @@ -5,6 +5,7 @@ */ import type { Config } from '../config/config.js'; +import type { AgentLoopContext } from '../config/agent-loop-context.js'; import type { MessageBus } from '../confirmation-bus/message-bus.js'; import { SchedulerStateManager } from './state-manager.js'; import { resolveConfirmation } from './confirmation.js'; @@ -57,7 +58,7 @@ interface SchedulerQueueItem { export interface SchedulerOptions { config: Config; - messageBus: MessageBus; + messageBus?: MessageBus; getPreferredEditor: () => EditorType | undefined; schedulerId: string; parentCallId?: string; @@ -97,6 +98,7 @@ export class Scheduler { private readonly executor: ToolExecutor; private readonly modifier: ToolModificationHandler; private readonly config: Config; + private readonly context: AgentLoopContext; private readonly messageBus: MessageBus; private readonly getPreferredEditor: () => EditorType | undefined; private readonly schedulerId: string; @@ -109,7 +111,8 @@ export class Scheduler { constructor(options: SchedulerOptions) { this.config = options.config; - this.messageBus = options.messageBus; + this.context = options.config; + this.messageBus = options.messageBus ?? this.context.messageBus; this.getPreferredEditor = options.getPreferredEditor; this.schedulerId = options.schedulerId; this.parentCallId = options.parentCallId; @@ -119,7 +122,7 @@ export class Scheduler { this.schedulerId, (call) => logToolCall(this.config, new ToolCallEvent(call)), ); - this.executor = new ToolExecutor(this.config); + this.executor = new ToolExecutor(this.config, this.context); this.modifier = new ToolModificationHandler(); this.setupMessageBusListener(this.messageBus); @@ -294,7 +297,7 @@ export class Scheduler { const currentApprovalMode = this.config.getApprovalMode(); try { - const toolRegistry = this.config.getToolRegistry(); + const toolRegistry = this.context.toolRegistry; const newCalls: ToolCall[] = requests.map((request) => { const enrichedRequest: ToolCallRequestInfo = { ...request, @@ -697,7 +700,7 @@ export class Scheduler { const originalRequestName = result.request.originalRequestName || result.request.name; - const newTool = this.config.getToolRegistry().getTool(tailRequest.name); + const newTool = this.context.toolRegistry.getTool(tailRequest.name); const newRequest: ToolCallRequestInfo = { callId: originalCallId, @@ -713,7 +716,7 @@ export class Scheduler { // Enqueue an errored tool call const errorCall = this._createToolNotFoundErroredToolCall( newRequest, - this.config.getToolRegistry().getAllToolNames(), + this.context.toolRegistry.getAllToolNames(), ); this.state.replaceActiveCallWithTailCall(callId, errorCall); } else { diff --git a/packages/core/src/scheduler/scheduler_parallel.test.ts b/packages/core/src/scheduler/scheduler_parallel.test.ts index 56e6e26243..5342d3ac20 100644 --- a/packages/core/src/scheduler/scheduler_parallel.test.ts +++ b/packages/core/src/scheduler/scheduler_parallel.test.ts @@ -211,13 +211,15 @@ describe('Scheduler Parallel Execution', () => { mockConfig = { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), - getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry), + toolRegistry: mockToolRegistry, isInteractive: vi.fn().mockReturnValue(true), getEnableHooks: vi.fn().mockReturnValue(true), setApprovalMode: vi.fn(), getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), } as unknown as Mocked; + (mockConfig as unknown as { config: Config }).config = mockConfig as Config; + mockMessageBus = { publish: vi.fn(), subscribe: vi.fn(), diff --git a/packages/core/src/scheduler/scheduler_waiting_callback.test.ts b/packages/core/src/scheduler/scheduler_waiting_callback.test.ts index e878a80669..03b754fc86 100644 --- a/packages/core/src/scheduler/scheduler_waiting_callback.test.ts +++ b/packages/core/src/scheduler/scheduler_waiting_callback.test.ts @@ -36,7 +36,7 @@ describe('Scheduler waiting callback', () => { mockTool = new MockTool({ name: 'test_tool' }); toolRegistry = new ToolRegistry(mockConfig, messageBus); - vi.spyOn(mockConfig, 'getToolRegistry').mockReturnValue(toolRegistry); + vi.spyOn(mockConfig, 'toolRegistry', 'get').mockReturnValue(toolRegistry); toolRegistry.registerTool(mockTool); vi.mocked(checkPolicy).mockResolvedValue({ diff --git a/packages/core/src/scheduler/tool-executor.test.ts b/packages/core/src/scheduler/tool-executor.test.ts index e744738341..a193c8ae69 100644 --- a/packages/core/src/scheduler/tool-executor.test.ts +++ b/packages/core/src/scheduler/tool-executor.test.ts @@ -64,7 +64,7 @@ describe('ToolExecutor', () => { beforeEach(() => { // Use the standard fake config factory config = makeFakeConfig(); - executor = new ToolExecutor(config); + executor = new ToolExecutor(config, config); // Reset mocks vi.resetAllMocks(); diff --git a/packages/core/src/scheduler/tool-executor.ts b/packages/core/src/scheduler/tool-executor.ts index 8269f1fc41..e5491630d2 100644 --- a/packages/core/src/scheduler/tool-executor.ts +++ b/packages/core/src/scheduler/tool-executor.ts @@ -13,6 +13,7 @@ import { type ToolCallResponseInfo, type ToolResult, type Config, + type AgentLoopContext, type ToolLiveOutput, } from '../index.js'; import { SHELL_TOOL_NAME } from '../tools/tool-names.js'; @@ -49,7 +50,10 @@ export interface ToolExecutionContext { } export class ToolExecutor { - constructor(private readonly config: Config) {} + constructor( + private readonly config: Config, + private readonly context: AgentLoopContext, + ) {} async execute(context: ToolExecutionContext): Promise { const { call, signal, outputUpdateHandler, onUpdateToolCall } = context; @@ -202,7 +206,7 @@ export class ToolExecutor { toolName, callId, this.config.storage.getProjectTempDir(), - this.config.getSessionId(), + this.context.promptId, ); outputFile = savedPath; const truncatedContent = formatTruncatedToolOutput( @@ -241,7 +245,7 @@ export class ToolExecutor { toolName, callId, this.config.storage.getProjectTempDir(), - this.config.getSessionId(), + this.context.promptId, ); outputFile = savedPath; const truncatedText = formatTruncatedToolOutput(