Compare commits

..

1 Commits

Author SHA1 Message Date
Jacob Richman 9cedc6131e Speculative changes to address react bug with overflowing state 2026-04-12 18:34:32 -07:00
579 changed files with 10097 additions and 41153 deletions
+1 -5
View File
@@ -2,11 +2,7 @@
"experimental": {
"extensionReloading": true,
"modelSteering": true,
"autoMemory": true,
"gemma": true,
"memoryManager": true,
"topicUpdateNarration": true,
"voiceMode": true
"topicUpdateNarration": true
},
"general": {
"devtools": true
+6 -15
View File
@@ -85,25 +85,17 @@ accessible.
- **Callouts**: Use GitHub-flavored markdown alerts to highlight important
information. To ensure the formatting is preserved by `npm run format`, place
an empty line, then a prettier ignore comment directly before the callout
block. Use `<!-- prettier-ignore -->` for standard Markdown files (`.md`) and
`{/* prettier-ignore */}` for MDX files (`.mdx`). The callout type (`[!TYPE]`)
should be on the first line, followed by a newline, and then the content, with
each subsequent line of content starting with `>`. Available types are `NOTE`,
`TIP`, `IMPORTANT`, `WARNING`, and `CAUTION`.
an empty line, then the `<!-- prettier-ignore -->` comment directly before
the callout block. The callout type (`[!TYPE]`) should be on the first line,
followed by a newline, and then the content, with each subsequent line of
content starting with `>`. Available types are `NOTE`, `TIP`, `IMPORTANT`,
`WARNING`, and `CAUTION`.
Example (.md):
Example:
<!-- prettier-ignore -->
> [!NOTE]
> This is an example of a multi-line note that will be preserved
> by Prettier.
Example (.mdx):
{/* prettier-ignore */}
> [!NOTE]
> This is an example of a multi-line note that will be preserved
> by Prettier.
### Links
@@ -126,7 +118,6 @@ accessible.
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development.
(Note: Use `{/* prettier-ignore */}` if editing an `.mdx` file.)
- **Headings:** Use hierarchical headings to support the user journey.
- **Procedures:**
-1
View File
@@ -28,7 +28,6 @@ runs:
- name: 'Run Tests'
env:
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
GEMINI_CLI_TRUST_WORKSPACE: true
working-directory: '${{ inputs.working-directory }}'
run: |-
echo "::group::Build"
@@ -98,7 +98,6 @@ runs:
working-directory: '${{ inputs.working-directory }}'
env:
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
GEMINI_CLI_TRUST_WORKSPACE: true
INTEGRATION_TEST_USE_INSTALLED_GEMINI: 'true'
# We must diable CI mode here because it interferes with interactive tests.
# See https://github.com/google-gemini/gemini-cli/issues/10517
@@ -1,132 +0,0 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: 'Agent Session Drift Check'
on:
pull_request:
branches:
- 'main'
- 'release/**'
paths:
- 'packages/cli/src/nonInteractiveCli.ts'
- 'packages/cli/src/nonInteractiveCliAgentSession.ts'
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: true
jobs:
check-drift:
name: 'Check Agent Session Drift'
runs-on: 'ubuntu-latest'
if: "github.repository == 'google-gemini/gemini-cli'"
permissions:
contents: 'read'
pull-requests: 'write'
steps:
- name: 'Detect drift and comment'
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # ratchet:actions/github-script@v8
with:
script: |-
// === Pair configuration — append here to cover more pairs ===
const PAIRS = [
{
legacy: 'packages/cli/src/nonInteractiveCli.ts',
session: 'packages/cli/src/nonInteractiveCliAgentSession.ts',
label: 'non-interactive CLI',
},
// Future pairs can be added here. Remember to also add both
// paths to the `paths:` filter at the top of this workflow.
// Example:
// {
// legacy: 'packages/core/src/agents/local-invocation.ts',
// session: 'packages/core/src/agents/local-session-invocation.ts',
// label: 'local subagent invocation',
// },
];
// ============================================================
const prNumber = context.payload.pull_request.number;
const { owner, repo } = context.repo;
// Use the API to list changed files — no checkout/git diff needed.
const files = await github.paginate(github.rest.pulls.listFiles, {
owner,
repo,
pull_number: prNumber,
per_page: 100,
});
const changed = new Set(files.map((f) => f.filename));
const warnings = [];
for (const { legacy, session, label } of PAIRS) {
const legacyChanged = changed.has(legacy);
const sessionChanged = changed.has(session);
if (legacyChanged && !sessionChanged) {
warnings.push(
`**${label}**: \`${legacy}\` was modified but \`${session}\` was not.`,
);
} else if (!legacyChanged && sessionChanged) {
warnings.push(
`**${label}**: \`${session}\` was modified but \`${legacy}\` was not.`,
);
}
}
const MARKER = '<!-- agent-session-drift-check -->';
// Look up our existing drift comment (for upsert/cleanup).
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
const existing = comments.find(
(c) => c.user?.type === 'Bot' && c.body?.includes(MARKER),
);
if (warnings.length === 0) {
core.info('No drift detected.');
// If drift was previously flagged and is now resolved, remove the comment.
if (existing) {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: existing.id,
});
core.info(`Deleted stale drift comment ${existing.id}.`);
}
return;
}
const body = [
MARKER,
'### ⚠️ Invocation Drift Warning',
'',
'The following file pairs should generally be kept in sync during the AgentSession migration:',
'',
...warnings.map((w) => `- ${w}`),
'',
'If this is intentional (e.g., a bug fix specific to one implementation), you can ignore this comment.',
'',
'_This check will be removed once the legacy implementations are deleted._',
].join('\n');
if (existing) {
core.info(`Updating existing drift comment ${existing.id}.`);
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
core.info('Creating new drift comment.');
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
});
}
+1 -5
View File
@@ -167,7 +167,6 @@ jobs:
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
VERBOSE: 'true'
BUILD_SANDBOX_FLAGS: '--cache-from type=gha --cache-to type=gha,mode=max'
@@ -184,7 +183,7 @@ jobs:
needs:
- 'merge_queue_skipper'
- 'parse_run_context'
runs-on: 'macos-latest-large'
runs-on: 'macos-latest'
if: |
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
steps:
@@ -213,7 +212,6 @@ jobs:
if: "${{runner.os != 'Windows'}}"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
@@ -290,7 +288,6 @@ jobs:
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
@@ -337,7 +334,6 @@ jobs:
if: "${{ steps.check_evals.outputs.should_run == 'true' }}"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
GEMINI_MODEL: 'gemini-3-pro-preview'
# Only run always passes behavioral tests.
EVAL_SUITE_TYPE: 'behavioral'
+1 -10
View File
@@ -102,12 +102,6 @@ jobs:
- name: 'Run yamllint'
run: 'node scripts/lint.js --yamllint'
- name: 'Build project for typecheck'
run: 'npm run build'
- name: 'Run typecheck'
run: 'npm run typecheck'
- name: 'Run Prettier'
run: 'node scripts/lint.js --prettier'
@@ -179,7 +173,6 @@ jobs:
- name: 'Run tests and generate reports'
env:
NO_COLOR: true
GEMINI_CLI_TRUST_WORKSPACE: true
run: |
if [[ "${{ matrix.shard }}" == "cli" ]]; then
npm run test:ci --workspace "@google/gemini-cli"
@@ -231,7 +224,7 @@ jobs:
test_mac:
name: 'Test (Mac) - ${{ matrix.node-version }}, ${{ matrix.shard }}'
runs-on: 'macos-latest-large'
runs-on: 'macos-latest'
needs:
- 'merge_queue_skipper'
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
@@ -268,7 +261,6 @@ jobs:
- name: 'Run tests and generate reports'
env:
NO_COLOR: true
GEMINI_CLI_TRUST_WORKSPACE: true
run: |
if [[ "${{ matrix.shard }}" == "cli" ]]; then
npm run test:ci --workspace "@google/gemini-cli" -- --coverage.enabled=false
@@ -432,7 +424,6 @@ jobs:
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
NO_COLOR: true
GEMINI_CLI_TRUST_WORKSPACE: true
NODE_OPTIONS: '--max-old-space-size=32768 --max-semi-space-size=256'
UV_THREADPOOL_SIZE: '32'
NODE_ENV: 'test'
+1 -4
View File
@@ -62,7 +62,6 @@ jobs:
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
IS_DOCKER: "${{ matrix.sandbox == 'sandbox:docker' }}"
KEEP_OUTPUT: 'true'
RUNS: '${{ github.event.inputs.runs }}'
@@ -78,7 +77,7 @@ jobs:
deflake_e2e_mac:
name: 'E2E Test (macOS)'
runs-on: 'macos-latest-large'
runs-on: 'macos-latest'
if: "github.repository == 'google-gemini/gemini-cli'"
steps:
- name: 'Checkout'
@@ -106,7 +105,6 @@ jobs:
if: "runner.os != 'Windows'"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
RUNS: '${{ github.event.inputs.runs }}'
SANDBOX: 'sandbox:none'
@@ -161,7 +159,6 @@ jobs:
- name: 'Run E2E tests'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
KEEP_OUTPUT: 'true'
SANDBOX: 'sandbox:none'
VERBOSE: 'true'
-2
View File
@@ -28,8 +28,6 @@ jobs:
- name: 'Run Docs Audit with Gemini'
id: 'run_gemini'
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31'
env:
GEMINI_CLI_TRUST_WORKSPACE: true
with:
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
prompt: |
-1
View File
@@ -66,7 +66,6 @@ jobs:
continue-on-error: true
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
GEMINI_MODEL: '${{ matrix.model }}'
RUN_EVALS: 'true'
EVAL_SUITE_TYPE: "${{ github.event.inputs.suite_type || 'behavioral' }}"
-221
View File
@@ -1,221 +0,0 @@
name: '🧠 Gemini CLI Bot: Brain'
on:
schedule:
- cron: '0 0 * * *' # Every 24 hours
workflow_dispatch:
inputs:
clear_memory:
description: 'Clear memory (drops learnings from previous runs)'
type: 'boolean'
default: false
enable_prs:
description: 'Enable PRs (automatically promote changes to PRs)'
type: 'boolean'
default: false
concurrency:
group: '${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: true
jobs:
reasoning:
name: 'Brain (Reasoning Layer)'
runs-on: 'ubuntu-latest'
if: "github.repository == 'google-gemini/gemini-cli'"
# The reasoning phase is strictly readonly.
permissions:
contents: 'read'
issues: 'read'
pull-requests: 'read'
actions: 'read'
env:
GEMINI_CLI_TRUST_WORKSPACE: 'true'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Build Gemini CLI'
run: 'npm run bundle'
- name: 'Download Previous State'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |
if [ "${{ github.event.inputs.clear_memory }}" = "true" ]; then
echo "Memory clear requested. Skipping previous state download."
exit 0
fi
# Find the last successful run of this workflow
LAST_RUN_ID=$(gh run list --workflow "${{ github.workflow }}" --status success --limit 1 --json databaseId --jq '.[0].databaseId')
if [ -n "$LAST_RUN_ID" ]; then
echo "Found previous successful run: $LAST_RUN_ID"
# Download brain memory (all state in one artifact)
gh run download "$LAST_RUN_ID" -n brain-data -D . || echo "brain-data not found"
else
echo "No previous successful run found."
fi
- name: 'Collect Current Metrics'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: 'npx tsx tools/gemini-cli-bot/metrics/index.ts'
- name: 'Run Brain Phases'
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
GEMINI_MODEL: 'gemini-3-flash-preview'
ENABLE_PRS: "${{ github.event.inputs.enable_prs || 'false' }}"
run: 'node bundle/gemini.js --policy tools/gemini-cli-bot/ci-policy.toml -p "$(cat tools/gemini-cli-bot/brain/metrics.md)"'
- name: 'Run Critique Phase'
if: "${{ github.event.inputs.enable_prs == 'true' }}"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
# This token is strictly readonly as enforced by the job-level permissions.
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
GEMINI_MODEL: 'gemini-3-flash-preview'
run: |
if git diff --staged --quiet; then
echo "No changes staged. Skipping critique."
echo "[APPROVED]" > critique_result.txt
else
node bundle/gemini.js --policy tools/gemini-cli-bot/ci-policy.toml -p "$(cat tools/gemini-cli-bot/brain/critique.md)" 2>&1 | tee critique_output.log
# PIPESTATUS[0] captures the exit code of the node command before the pipe
if [ "${PIPESTATUS[0]}" -ne 0 ] || grep -q "\[REJECTED\]" critique_output.log; then
echo "Critique failed or rejected changes. Skipping PR creation."
echo "[REJECTED]" > critique_result.txt
else
echo "[APPROVED]" > critique_result.txt
fi
fi
- name: 'Generate Patch'
if: "${{ github.event.inputs.enable_prs == 'true' }}"
run: |
touch bot-changes.patch
touch pr-description.md
if [ -f critique_result.txt ] && grep -q "\[REJECTED\]" critique_result.txt; then
echo "Critique rejected. Skipping patch generation."
else
git diff --staged > bot-changes.patch
fi
- name: 'Archive Brain Data'
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
with:
name: 'brain-data'
path: |
tools/gemini-cli-bot/lessons-learned.md
tools/gemini-cli-bot/history/*.csv
bot-changes.patch
pr-description.md
branch-name.txt
pr-comment.md
pr-number.txt
retention-days: 90
publish:
name: 'Publish Artifacts (Archive Layer)'
needs: 'reasoning'
runs-on: 'ubuntu-latest'
if: "github.repository == 'google-gemini/gemini-cli'"
# The publish phase is for archiving artifacts and optionally creating PRs.
permissions:
contents: 'write'
pull-requests: 'write'
actions: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
ref: 'main'
fetch-depth: 0
persist-credentials: false
- name: 'Download Brain Data'
uses: 'actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093' # ratchet:actions/download-artifact@v4
with:
name: 'brain-data'
path: '${{ runner.temp }}/brain-data/'
- name: 'Create or Update PR'
if: "${{ github.event.inputs.enable_prs == 'true' }}"
env:
GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
run: |
if [ -s "${{ runner.temp }}/brain-data/bot-changes.patch" ]; then
git config user.name "gemini-cli-robot"
git config user.email "gemini-cli-robot@google.com"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
BRANCH_NAME="bot/productivity-updates-$(date +'%Y%m%d%H%M%S')-${{ github.run_id }}"
if [ -f "${{ runner.temp }}/brain-data/branch-name.txt" ]; then
BRANCH_NAME=$(cat "${{ runner.temp }}/brain-data/branch-name.txt")
fi
# SECURITY: Only allow pushing to branches starting with 'bot/'
if [[ ! "$BRANCH_NAME" =~ ^bot/ ]]; then
echo "Error: Branch name '$BRANCH_NAME' does not start with 'bot/'. Safety abort."
exit 1
fi
git checkout -b "$BRANCH_NAME"
git apply "${{ runner.temp }}/brain-data/bot-changes.patch"
git add .
if [ -s "${{ runner.temp }}/brain-data/pr-description.md" ]; then
git commit -F "${{ runner.temp }}/brain-data/pr-description.md"
else
git commit -m "🤖 Gemini Bot Productivity Optimizations"
fi
# Use force to update existing PR branches
git push origin "$BRANCH_NAME" --force
PR_TITLE="🤖 Gemini Bot Productivity Optimizations"
if [ -s "${{ runner.temp }}/brain-data/pr-description.md" ]; then
PR_TITLE=$(head -n 1 "${{ runner.temp }}/brain-data/pr-description.md")
fi
# Create PR if it doesn't exist
if ! gh pr view "$BRANCH_NAME" > /dev/null 2>&1; then
gh pr create --draft --title "$PR_TITLE" --body-file "${{ runner.temp }}/brain-data/pr-description.md" --head "$BRANCH_NAME" --base main || \
gh pr create --draft --title "🤖 Gemini Bot Productivity Optimizations" --body "Automated changes generated by Gemini CLI Bot." --head "$BRANCH_NAME" --base main
fi
fi
- name: 'Post PR Comment'
if: "${{ github.event.inputs.enable_prs == 'true' }}"
env:
GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
run: |
if [ -s "${{ runner.temp }}/brain-data/pr-comment.md" ] && [ -f "${{ runner.temp }}/brain-data/pr-number.txt" ]; then
PR_NUM=$(cat "${{ runner.temp }}/brain-data/pr-number.txt")
# SECURITY: Only allow commenting on PRs authored by the bot
PR_AUTHOR=$(gh pr view "$PR_NUM" --json author --jq '.author.login')
if [ "$PR_AUTHOR" != "gemini-cli-robot" ]; then
echo "Error: PR #$PR_NUM is authored by '$PR_AUTHOR', not 'gemini-cli-robot'. Safety abort."
exit 1
fi
gh pr comment "$PR_NUM" -F "${{ runner.temp }}/brain-data/pr-comment.md"
fi
@@ -1,48 +0,0 @@
name: '🔄 Gemini CLI Bot: Pulse'
on:
schedule:
- cron: '*/30 * * * *' # Every 30 minutes
workflow_dispatch:
concurrency:
group: '${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: true
permissions:
contents: 'write'
issues: 'write'
pull-requests: 'write'
jobs:
pulse:
name: 'Pulse (Reflex Layer)'
runs-on: 'ubuntu-latest'
if: "github.repository == 'google-gemini/gemini-cli'"
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
fetch-depth: 0
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: 'Install dependencies'
run: 'npm ci'
- name: 'Run Reflex Processes'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |
if [ -d "tools/gemini-cli-bot/reflexes/scripts" ] && [ "$(ls -A tools/gemini-cli-bot/reflexes/scripts)" ]; then
for script in tools/gemini-cli-bot/reflexes/scripts/*.ts; do
echo "Running reflex script: $script"
npx tsx "$script"
done
else
echo "No reflex scripts found."
fi
-2
View File
@@ -70,8 +70,6 @@ jobs:
- name: 'Generate Changelog with Gemini'
if: "steps.validate_version.outputs.CONTINUE == 'true'"
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
env:
GEMINI_CLI_TRUST_WORKSPACE: true
with:
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
prompt: |
-1
View File
@@ -141,7 +141,6 @@ jobs:
if: "github.event_name != 'pull_request'"
env:
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
GEMINI_CLI_TRUST_WORKSPACE: true
run: |
echo "Running integration tests with binary..."
if [[ "${{ matrix.os }}" == 'windows-latest' ]]; then
-1
View File
@@ -22,4 +22,3 @@ Thumbs.db
.pytest_cache
**/SKILL.md
packages/sdk/test-data/*.json
*.mdx
+1 -3
View File
@@ -110,9 +110,7 @@ assign or unassign the issue as requested, provided the conditions are met
(e.g., an issue must be unassigned to be assigned).
Please note that you can have a maximum of 3 issues assigned to you at any given
time and that only
[issues labeled "help wanted"](https://github.com/google-gemini/gemini-cli/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22)
may be self-assigned.
time.
### Pull request guidelines
+2 -2
View File
@@ -40,8 +40,8 @@ ENV PATH=$PATH:/usr/local/share/npm-global/bin
USER node
# install gemini-cli and clean up
COPY --chown=node:node packages/cli/dist/google-gemini-cli-*.tgz /tmp/gemini-cli.tgz
COPY --chown=node:node packages/core/dist/google-gemini-cli-core-*.tgz /tmp/gemini-core.tgz
COPY packages/cli/dist/google-gemini-cli-*.tgz /tmp/gemini-cli.tgz
COPY packages/core/dist/google-gemini-cli-core-*.tgz /tmp/gemini-core.tgz
RUN npm install -g /tmp/gemini-core.tgz \
&& npm install -g /tmp/gemini-cli.tgz \
&& node -e "const fs=require('node:fs'); JSON.parse(fs.readFileSync('/usr/local/share/npm-global/lib/node_modules/@google/gemini-cli/package.json','utf8')); JSON.parse(fs.readFileSync('/usr/local/share/npm-global/lib/node_modules/@google/gemini-cli-core/package.json','utf8'));" \
-2
View File
@@ -371,8 +371,6 @@ for planned features and priorities.
## 📖 Resources
- **[Free Course](https://learn.deeplearning.ai/courses/gemini-cli-code-and-create-with-an-open-source-agent/information)** -
Learn the basics.
- **[Official Roadmap](./ROADMAP.md)** - See what's coming next.
- **[Changelog](https://www.geminicli.com/docs/changelogs)** - See recent
notable updates.
-39
View File
@@ -18,45 +18,6 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.39.0 - 2026-04-23
- **Skill Management:** Added a new `/memory` inbox command for reviewing and
patching skills extracted during sessions
([#24544](https://github.com/google-gemini/gemini-cli/pull/24544) by
@SandyTao520, [#25148](https://github.com/google-gemini/gemini-cli/pull/25148)
by @SandyTao520).
- **Improved Transparency:** Plan Mode now requires confirmation for skill
activation and allows plan inspection
([#24946](https://github.com/google-gemini/gemini-cli/pull/24946),
[#25058](https://github.com/google-gemini/gemini-cli/pull/25058) by
@ruomengz).
- **Architecture & Reliability:** Introduced a decoupled `ContextManager`
architecture and resolved several critical memory leaks and PTY exhaustion
issues ([#24752](https://github.com/google-gemini/gemini-cli/pull/24752) by
@joshualitt, [#25079](https://github.com/google-gemini/gemini-cli/pull/25079)
by @spencer426).
## Announcements: v0.38.0 - 2026-04-14
- **Chapters Narrative Flow:** Group agent interactions into "Chapters" based on
intent and tool usage for better session structure
([#23150](https://github.com/google-gemini/gemini-cli/pull/23150) by
@Abhijit-2592,
[#24079](https://github.com/google-gemini/gemini-cli/pull/24079) by
@gundermanc).
- **Context Compression Service:** Advanced context management to efficiently
distill conversation history
([#24483](https://github.com/google-gemini/gemini-cli/pull/24483) by
@joshualitt).
- **UI Flicker & UX Enhancements:** Solved rendering flicker with "Terminal
Buffer" mode and introduced selective topic expansion
([#24512](https://github.com/google-gemini/gemini-cli/pull/24512) by
@jacob314, [#24793](https://github.com/google-gemini/gemini-cli/pull/24793) by
@Abhijit-2592).
- **Persistent Policy Approvals:** Implemented context-aware persistent
approvals for tool execution
([#23257](https://github.com/google-gemini/gemini-cli/pull/23257) by @jerop).
## Announcements: v0.37.0 - 2026-04-08
- **Dynamic Sandbox Expansion:** Implemented dynamic sandbox expansion and
+406 -243
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.39.0
# Latest stable release: v0.37.1
Released: April 23, 2026
Released: April 09, 2026
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -11,252 +11,415 @@ npm install -g @google/gemini-cli
## Highlights
- **Skill Extractor & Memory Inbox:** Introduced the `/memory` command to review
and patch skills extracted during agent sessions, streamlining the continuous
learning workflow.
- **Enhanced Plan Mode Security:** Increased transparency in Plan Mode by
requiring user confirmation for skill activation and allowing users to view
the full content of generated plans.
- **Advanced Display Protocol:** Implemented a tool-controlled display protocol,
enabling agents to provide richer, more structured visual feedback during
execution.
- **Core Architecture Refactor:** Introduced a decoupled `ContextManager` and
`Sidecar` architecture to improve state management and session resilience.
- **Streamlined Agent Feedback:** Restored the display of model thoughts and raw
text in responses, ensuring full visibility into the agent's reasoning
process.
- **Dynamic Sandbox Expansion:** Implemented dynamic sandbox expansion and
worktree support for both Linux and Windows, enhancing development flexibility
in restricted environments.
- **Tool-Based Topic Grouping (Chapters):** Introduced "Chapters" to logically
group agent interactions based on tool usage and intent, providing a clearer
narrative flow in long sessions.
- **Enhanced Browser Agent:** Added persistent session management, dynamic
read-only tool discovery, and sandbox-aware initialization for the browser
agent.
- **Security & Permission Hardening:** Implemented secret visibility lockdown
for environment files and integrated integrity controls for Windows
sandboxing.
## What's Changed
- refactor(plan): simplify policy priorities and consolidate read-only rules by
@ruomengz in [#24849](https://github.com/google-gemini/gemini-cli/pull/24849)
- feat(test-utils): add memory usage integration test harness by @sripasg in
[#24876](https://github.com/google-gemini/gemini-cli/pull/24876)
- feat(memory): add /memory inbox command for reviewing extracted skills by
@SandyTao520 in
[#24544](https://github.com/google-gemini/gemini-cli/pull/24544)
- chore(release): bump version to 0.39.0-nightly.20260408.e77b22e63 by
@gemini-cli-robot in
[#24939](https://github.com/google-gemini/gemini-cli/pull/24939)
- fix(core): ensure robust sandbox cleanup in all process execution paths by
@ehedlund in [#24763](https://github.com/google-gemini/gemini-cli/pull/24763)
- chore: update ink version to 6.6.8 by @jacob314 in
[#24934](https://github.com/google-gemini/gemini-cli/pull/24934)
- Changelog for v0.38.0-preview.0 by @gemini-cli-robot in
[#24938](https://github.com/google-gemini/gemini-cli/pull/24938)
- chore: ignore conductor directory by @JayadityaGit in
[#22128](https://github.com/google-gemini/gemini-cli/pull/22128)
- Changelog for v0.37.0 by @gemini-cli-robot in
[#24940](https://github.com/google-gemini/gemini-cli/pull/24940)
- feat(plan): require user confirmation for activate_skill in Plan Mode by
@ruomengz in [#24946](https://github.com/google-gemini/gemini-cli/pull/24946)
- feat(test-utils): add CPU performance integration test harness by @sripasg in
[#24951](https://github.com/google-gemini/gemini-cli/pull/24951)
- fix(cli-ui): enable Ctrl+Backspace for word deletion in Windows Terminal by
@dogukanozen in
[#21447](https://github.com/google-gemini/gemini-cli/pull/21447)
- test(sdk): add unit tests for GeminiCliSession by @AdamyaSingh7 in
[#21897](https://github.com/google-gemini/gemini-cli/pull/21897)
- fix(core): resolve windows symlink bypass and stabilize sandbox integration
tests by @ehedlund in
[#24834](https://github.com/google-gemini/gemini-cli/pull/24834)
- fix(cli): restore file path display in edit and write tool confirmations by
@jwhelangoog in
[#24974](https://github.com/google-gemini/gemini-cli/pull/24974)
- feat(core): refine shell tool description display logic by @jwhelangoog in
[#24903](https://github.com/google-gemini/gemini-cli/pull/24903)
- fix(core): dynamic session ID injection to resolve resume bugs by @scidomino
in [#24972](https://github.com/google-gemini/gemini-cli/pull/24972)
- Update ink version to 6.6.9 by @jacob314 in
[#24980](https://github.com/google-gemini/gemini-cli/pull/24980)
- Generalize evals infra to support more types of evals, organization and
queuing of named suites by @gundermanc in
[#24941](https://github.com/google-gemini/gemini-cli/pull/24941)
- fix(cli): optimize startup with lightweight parent process by @sehoon38 in
[#24667](https://github.com/google-gemini/gemini-cli/pull/24667)
- refactor(sandbox): use centralized sandbox paths in macOS Seatbelt
implementation by @ehedlund in
[#24984](https://github.com/google-gemini/gemini-cli/pull/24984)
- feat(cli): refine tool output formatting for compact mode by @jwhelangoog in
[#24677](https://github.com/google-gemini/gemini-cli/pull/24677)
- fix(sdk): skip broken sendStream tests to unblock nightly by @SandyTao520 in
[#25000](https://github.com/google-gemini/gemini-cli/pull/25000)
- refactor(core): use centralized path resolution for Linux sandbox by @ehedlund
in [#24985](https://github.com/google-gemini/gemini-cli/pull/24985)
- Support ctrl+shift+g by @jacob314 in
[#25035](https://github.com/google-gemini/gemini-cli/pull/25035)
- feat(core): refactor subagent tool to unified invoke_subagent tool by
@abhipatel12 in
[#24489](https://github.com/google-gemini/gemini-cli/pull/24489)
- fix(core): add explicit git identity env vars to prevent sandbox checkpointing
error by @mrpmohiburrahman in
[#19775](https://github.com/google-gemini/gemini-cli/pull/19775)
- fix: respect hideContextPercentage when FooterConfigDialog is closed without
changes by @chernistry in
[#24773](https://github.com/google-gemini/gemini-cli/pull/24773)
- fix(cli): suppress unhandled AbortError logs during request cancellation by
@euxaristia in
[#22621](https://github.com/google-gemini/gemini-cli/pull/22621)
- Automated documentation audit by @g-samroberts in
[#24567](https://github.com/google-gemini/gemini-cli/pull/24567)
- feat(cli): implement useAgentStream hook by @mbleigh in
[#24292](https://github.com/google-gemini/gemini-cli/pull/24292)
- refactor(plan) Clean default plan toml by @ruomengz in
[#25037](https://github.com/google-gemini/gemini-cli/pull/25037)
- refactor(core): remove legacy subagent wrapping tools by @abhipatel12 in
[#25053](https://github.com/google-gemini/gemini-cli/pull/25053)
- fix(core): honor retryDelay in RetryInfo for 503 errors by @yunaseoul in
[#25057](https://github.com/google-gemini/gemini-cli/pull/25057)
- fix(core): remediate subagent memory leaks using AbortSignal in MessageBus by
@abhipatel12 in
[#25048](https://github.com/google-gemini/gemini-cli/pull/25048)
- feat(cli): wire up useAgentStream in AppContainer by @mbleigh in
[#24297](https://github.com/google-gemini/gemini-cli/pull/24297)
- feat(core): migrate chat recording to JSONL streaming by @spencer426 in
[#23749](https://github.com/google-gemini/gemini-cli/pull/23749)
- fix(core): clear 5-minute timeouts in oauth flow to prevent memory leaks by
@spencer426 in
[#24968](https://github.com/google-gemini/gemini-cli/pull/24968)
- fix(sandbox): centralize async git worktree resolution and enforce read-only
security by @ehedlund in
[#25040](https://github.com/google-gemini/gemini-cli/pull/25040)
- feat(test): add high-volume shell test and refine perf harness by @sripasg in
[#24983](https://github.com/google-gemini/gemini-cli/pull/24983)
- fix(core): silently handle EPERM when listing dir structure by @scidomino in
[#25066](https://github.com/google-gemini/gemini-cli/pull/25066)
- Changelog for v0.37.1 by @gemini-cli-robot in
[#25055](https://github.com/google-gemini/gemini-cli/pull/25055)
- fix: decode Uint8Array and multi-byte UTF-8 in API error messages by
@kimjune01 in [#23341](https://github.com/google-gemini/gemini-cli/pull/23341)
- Automated documentation audit results by @g-samroberts in
[#22755](https://github.com/google-gemini/gemini-cli/pull/22755)
- debugging(ui): add optional debugRainbow setting by @jacob314 in
[#25088](https://github.com/google-gemini/gemini-cli/pull/25088)
- fix: resolve lifecycle memory leaks by cleaning up listeners and root closures
by @spencer426 in
[#25049](https://github.com/google-gemini/gemini-cli/pull/25049)
- docs(cli): updates f12 description to be more precise by @JayadityaGit in
[#15816](https://github.com/google-gemini/gemini-cli/pull/15816)
- fix(cli): mark /settings as unsafe to run concurrently by @jacob314 in
[#25061](https://github.com/google-gemini/gemini-cli/pull/25061)
- fix(core): remove buffer slice to prevent OOM on large output streams by
@spencer426 in
[#25094](https://github.com/google-gemini/gemini-cli/pull/25094)
- feat(core): persist subagent agentId in tool call records by @abhipatel12 in
[#25092](https://github.com/google-gemini/gemini-cli/pull/25092)
- chore(core): increase codebase investigator turn limits to 50 by @abhipatel12
in [#25125](https://github.com/google-gemini/gemini-cli/pull/25125)
- refactor(core): consolidate execute() arguments into ExecuteOptions by
@mbleigh in [#25101](https://github.com/google-gemini/gemini-cli/pull/25101)
- feat(core): add Strategic Re-evaluation guidance to system prompt by
@aishaneeshah in
[#25062](https://github.com/google-gemini/gemini-cli/pull/25062)
- fix(core): preserve shell execution config fields on update by
@jasonmatthewsuhari in
[#25113](https://github.com/google-gemini/gemini-cli/pull/25113)
- docs: add vi shortcuts and clarify MCP sandbox setup by @chrisjcthomas in
[#21679](https://github.com/google-gemini/gemini-cli/pull/21679)
- fix(cli): pass session id to interactive shell executions by
@jasonmatthewsuhari in
[#25114](https://github.com/google-gemini/gemini-cli/pull/25114)
- fix(cli): resolve text sanitization data loss due to C1 control characters by
@euxaristia in
[#22624](https://github.com/google-gemini/gemini-cli/pull/22624)
- feat(core): add large memory regression test by @cynthialong0-0 in
[#25059](https://github.com/google-gemini/gemini-cli/pull/25059)
- fix(core): resolve PTY exhaustion and orphan MCP subprocess leaks by
@spencer426 in
[#25079](https://github.com/google-gemini/gemini-cli/pull/25079)
- chore(deps): update vulnerable dependencies via npm audit fix by @scidomino in
[#25140](https://github.com/google-gemini/gemini-cli/pull/25140)
- perf(sandbox): optimize Windows sandbox initialization via native ACL
application by @ehedlund in
[#25077](https://github.com/google-gemini/gemini-cli/pull/25077)
- chore: switch from keytar to @github/keytar by @cocosheng-g in
[#25143](https://github.com/google-gemini/gemini-cli/pull/25143)
- fix: improve audio MIME normalization and validation in file reads by
@junaiddshaukat in
[#21636](https://github.com/google-gemini/gemini-cli/pull/21636)
- docs: Update docs-audit to include changes in PR body by @g-samroberts in
[#25153](https://github.com/google-gemini/gemini-cli/pull/25153)
- docs: correct documentation for enforced authentication type by @cocosheng-g
in [#25142](https://github.com/google-gemini/gemini-cli/pull/25142)
- fix(cli): exclude update_topic from confirmation queue count by @Abhijit-2592
in [#24945](https://github.com/google-gemini/gemini-cli/pull/24945)
- Memory fix for trace's streamWrapper. by @anthraxmilkshake in
[#25089](https://github.com/google-gemini/gemini-cli/pull/25089)
- fix(core): fix quota footer for non-auto models and improve display by
@jackwotherspoon in
[#25121](https://github.com/google-gemini/gemini-cli/pull/25121)
- docs(contributing): clarify self-assignment policy for issues by @jmr in
[#23087](https://github.com/google-gemini/gemini-cli/pull/23087)
- feat(core): add skill patching support with /memory inbox integration by
@SandyTao520 in
[#25148](https://github.com/google-gemini/gemini-cli/pull/25148)
- Stop suppressing thoughts and text in model response by @gundermanc in
[#25073](https://github.com/google-gemini/gemini-cli/pull/25073)
- fix(release): prefix git hash in nightly versions to prevent semver
normalization by @SandyTao520 in
[#25304](https://github.com/google-gemini/gemini-cli/pull/25304)
- feat(cli): extract QuotaContext and resolve infinite render loop by @Adib234
in [#24959](https://github.com/google-gemini/gemini-cli/pull/24959)
- refactor(core): extract and centralize sandbox path utilities by @ehedlund in
[#25305](https://github.com/google-gemini/gemini-cli/pull/25305)
- feat(ui): added enhancements to scroll momentum by @devr0306 in
[#24447](https://github.com/google-gemini/gemini-cli/pull/24447)
- fix(core): replace custom binary detection with isbinaryfile to correctly
handle UTF-8 (U+FFFD) by @Anjaligarhwal in
[#25297](https://github.com/google-gemini/gemini-cli/pull/25297)
- feat(agent): implement tool-controlled display protocol (Steps 2-3) by
@mbleigh in [#25134](https://github.com/google-gemini/gemini-cli/pull/25134)
- Stop showing scrollbar unless we are in terminalBuffer mode by @jacob314 in
[#25320](https://github.com/google-gemini/gemini-cli/pull/25320)
- feat: support auth block in MCP servers config in agents by @TanmayVartak in
[#24770](https://github.com/google-gemini/gemini-cli/pull/24770)
- fix(core): expose GEMINI_PLANS_DIR to hook environment by @Adib234 in
[#25296](https://github.com/google-gemini/gemini-cli/pull/25296)
- feat(core): implement silent fallback for Plan Mode model routing by @jerop in
[#25317](https://github.com/google-gemini/gemini-cli/pull/25317)
- fix: correct redirect count increment in fetchJson by @KevinZhao in
[#24896](https://github.com/google-gemini/gemini-cli/pull/24896)
- fix(core): prevent secondary crash in ModelRouterService finally block by
@gundermanc in
[#25333](https://github.com/google-gemini/gemini-cli/pull/25333)
- feat(core): introduce decoupled ContextManager and Sidecar architecture by
- fix(acp): handle all InvalidStreamError types gracefully in prompt
[#24540](https://github.com/google-gemini/gemini-cli/pull/24540)
- feat(acp): add support for /about command
[#24649](https://github.com/google-gemini/gemini-cli/pull/24649)
- feat(acp): add /help command
[#24839](https://github.com/google-gemini/gemini-cli/pull/24839)
- feat(evals): centralize test agents into test-utils for reuse by @Samee24 in
[#23616](https://github.com/google-gemini/gemini-cli/pull/23616)
- revert: chore(config): disable agents by default by @abhipatel12 in
[#23672](https://github.com/google-gemini/gemini-cli/pull/23672)
- fix(plan): update telemetry attribute keys and add timestamp by @Adib234 in
[#23685](https://github.com/google-gemini/gemini-cli/pull/23685)
- fix(core): prevent premature MCP discovery completion by @jackwotherspoon in
[#23637](https://github.com/google-gemini/gemini-cli/pull/23637)
- feat(browser): add maxActionsPerTask for browser agent setting by
@cynthialong0-0 in
[#23216](https://github.com/google-gemini/gemini-cli/pull/23216)
- fix(core): improve agent loader error formatting for empty paths by
@adamfweidman in
[#23690](https://github.com/google-gemini/gemini-cli/pull/23690)
- fix(cli): only show updating spinner when auto-update is in progress by
@scidomino in [#23709](https://github.com/google-gemini/gemini-cli/pull/23709)
- Refine onboarding metrics to log the duration explicitly and use the tier
name. by @yunaseoul in
[#23678](https://github.com/google-gemini/gemini-cli/pull/23678)
- chore(tools): add toJSON to tools and invocations to reduce logging verbosity
by @alisa-alisa in
[#22899](https://github.com/google-gemini/gemini-cli/pull/22899)
- fix(cli): stabilize copy mode to prevent flickering and cursor resets by
@mattKorwel in
[#22584](https://github.com/google-gemini/gemini-cli/pull/22584)
- fix(test): move flaky ctrl-c-exit test to non-blocking suite by @mattKorwel in
[#23732](https://github.com/google-gemini/gemini-cli/pull/23732)
- feat(skills): add ci skill for automated failure replication by @mattKorwel in
[#23720](https://github.com/google-gemini/gemini-cli/pull/23720)
- feat(sandbox): implement forbiddenPaths for OS-specific sandbox managers by
@ehedlund in [#23282](https://github.com/google-gemini/gemini-cli/pull/23282)
- fix(core): conditionally expose additional_permissions in shell tool by
@galz10 in [#23729](https://github.com/google-gemini/gemini-cli/pull/23729)
- refactor(core): standardize OS-specific sandbox tests and extract linux helper
methods by @ehedlund in
[#23715](https://github.com/google-gemini/gemini-cli/pull/23715)
- format recently added script by @scidomino in
[#23739](https://github.com/google-gemini/gemini-cli/pull/23739)
- fix(ui): prevent over-eager slash subcommand completion by @keithguerin in
[#20136](https://github.com/google-gemini/gemini-cli/pull/20136)
- Fix dynamic model routing for gemini 3.1 pro to customtools model by
@kevinjwang1 in
[#23641](https://github.com/google-gemini/gemini-cli/pull/23641)
- feat(core): support inline agentCardJson for remote agents by @adamfweidman in
[#23743](https://github.com/google-gemini/gemini-cli/pull/23743)
- fix(cli): skip console log/info in headless mode by @cynthialong0-0 in
[#22739](https://github.com/google-gemini/gemini-cli/pull/22739)
- test(core): install bubblewrap on Linux CI for sandbox integration tests by
@ehedlund in [#23583](https://github.com/google-gemini/gemini-cli/pull/23583)
- docs(reference): split tools table into category sections by @sheikhlimon in
[#21516](https://github.com/google-gemini/gemini-cli/pull/21516)
- fix(browser): detect embedded URLs in query params to prevent allowedDomains
bypass by @tony-shi in
[#23225](https://github.com/google-gemini/gemini-cli/pull/23225)
- fix(browser): add proxy bypass constraint to domain restriction system prompt
by @tony-shi in
[#23229](https://github.com/google-gemini/gemini-cli/pull/23229)
- fix(policy): relax write_file argsPattern in plan mode to allow paths without
session ID by @Adib234 in
[#23695](https://github.com/google-gemini/gemini-cli/pull/23695)
- docs: fix grammar in CONTRIBUTING and numbering in sandbox docs by
@splint-disk-8i in
[#23448](https://github.com/google-gemini/gemini-cli/pull/23448)
- fix(acp): allow attachments by adding a permission prompt by @sripasg in
[#23680](https://github.com/google-gemini/gemini-cli/pull/23680)
- fix(core): thread AbortSignal to chat compression requests (#20405) by
@SH20RAJ in [#20778](https://github.com/google-gemini/gemini-cli/pull/20778)
- feat(core): implement Windows sandbox dynamic expansion Phase 1 and 2.1 by
@scidomino in [#23691](https://github.com/google-gemini/gemini-cli/pull/23691)
- Add note about root privileges in sandbox docs by @diodesign in
[#23314](https://github.com/google-gemini/gemini-cli/pull/23314)
- docs(core): document agent_card_json string literal options for remote agents
by @adamfweidman in
[#23797](https://github.com/google-gemini/gemini-cli/pull/23797)
- fix(cli): resolve TTY hang on headless environments by unconditionally
resuming process.stdin before React Ink launch by @cocosheng-g in
[#23673](https://github.com/google-gemini/gemini-cli/pull/23673)
- fix(ui): cleanup estimated string length hacks in composer by @keithguerin in
[#23694](https://github.com/google-gemini/gemini-cli/pull/23694)
- feat(browser): dynamically discover read-only tools by @cynthialong0-0 in
[#23805](https://github.com/google-gemini/gemini-cli/pull/23805)
- docs: clarify policy requirement for `general.plan.directory` in settings
schema by @jerop in
[#23784](https://github.com/google-gemini/gemini-cli/pull/23784)
- Revert "perf(cli): optimize --version startup time (#23671)" by @scidomino in
[#23812](https://github.com/google-gemini/gemini-cli/pull/23812)
- don't silence errors from wombat by @scidomino in
[#23822](https://github.com/google-gemini/gemini-cli/pull/23822)
- fix(ui): prevent escape key from cancelling requests in shell mode by
@PrasannaPal21 in
[#21245](https://github.com/google-gemini/gemini-cli/pull/21245)
- Changelog for v0.36.0-preview.0 by @gemini-cli-robot in
[#23702](https://github.com/google-gemini/gemini-cli/pull/23702)
- feat(core,ui): Add experiment-gated support for gemini flash 3.1 lite by
@chrstnb in [#23794](https://github.com/google-gemini/gemini-cli/pull/23794)
- Changelog for v0.36.0-preview.3 by @gemini-cli-robot in
[#23827](https://github.com/google-gemini/gemini-cli/pull/23827)
- new linting check: github-actions-pinning by @alisa-alisa in
[#23808](https://github.com/google-gemini/gemini-cli/pull/23808)
- fix(cli): show helpful guidance when no skills are available by @Niralisj in
[#23785](https://github.com/google-gemini/gemini-cli/pull/23785)
- fix: Chat logs and errors handle tail tool calls correctly by @googlestrobe in
[#22460](https://github.com/google-gemini/gemini-cli/pull/22460)
- Don't try removing a tag from a non-existent release. by @scidomino in
[#23830](https://github.com/google-gemini/gemini-cli/pull/23830)
- fix(cli): allow ask question dialog to take full window height by @jacob314 in
[#23693](https://github.com/google-gemini/gemini-cli/pull/23693)
- fix(core): strip leading underscores from error types in telemetry by
@yunaseoul in [#23824](https://github.com/google-gemini/gemini-cli/pull/23824)
- Changelog for v0.35.0 by @gemini-cli-robot in
[#23819](https://github.com/google-gemini/gemini-cli/pull/23819)
- feat(evals): add reliability harvester and 500/503 retry support by
@alisa-alisa in
[#23626](https://github.com/google-gemini/gemini-cli/pull/23626)
- feat(sandbox): dynamic Linux sandbox expansion and worktree support by @galz10
in [#23692](https://github.com/google-gemini/gemini-cli/pull/23692)
- Merge examples of use into quickstart documentation by @diodesign in
[#23319](https://github.com/google-gemini/gemini-cli/pull/23319)
- fix(cli): prioritize primary name matches in slash command search by @sehoon38
in [#23850](https://github.com/google-gemini/gemini-cli/pull/23850)
- Changelog for v0.35.1 by @gemini-cli-robot in
[#23840](https://github.com/google-gemini/gemini-cli/pull/23840)
- fix(browser): keep input blocker active across navigations by @kunal-10-cloud
in [#22562](https://github.com/google-gemini/gemini-cli/pull/22562)
- feat(core): new skill to look for duplicated code while reviewing PRs by
@devr0306 in [#23704](https://github.com/google-gemini/gemini-cli/pull/23704)
- fix(core): replace hardcoded non-interactive ASK_USER denial with explicit
policy rules by @ruomengz in
[#23668](https://github.com/google-gemini/gemini-cli/pull/23668)
- fix(plan): after exiting plan mode switches model to a flash model by @Adib234
in [#23885](https://github.com/google-gemini/gemini-cli/pull/23885)
- feat(gcp): add development worker infrastructure by @mattKorwel in
[#23814](https://github.com/google-gemini/gemini-cli/pull/23814)
- fix(a2a-server): A2A server should execute ask policies in interactive mode by
@kschaab in [#23831](https://github.com/google-gemini/gemini-cli/pull/23831)
- feat(core): define TrajectoryProvider interface by @sehoon38 in
[#23050](https://github.com/google-gemini/gemini-cli/pull/23050)
- Docs: Update quotas and pricing by @jkcinouye in
[#23835](https://github.com/google-gemini/gemini-cli/pull/23835)
- fix(core): allow disabling environment variable redaction by @galz10 in
[#23927](https://github.com/google-gemini/gemini-cli/pull/23927)
- feat(cli): enable notifications cross-platform via terminal bell fallback by
@genneth in [#21618](https://github.com/google-gemini/gemini-cli/pull/21618)
- feat(sandbox): implement secret visibility lockdown for env files by
@DavidAPierce in
[#23712](https://github.com/google-gemini/gemini-cli/pull/23712)
- fix(core): remove shell outputChunks buffer caching to prevent memory bloat
and sanitize prompt input by @spencer426 in
[#23751](https://github.com/google-gemini/gemini-cli/pull/23751)
- feat(core): implement persistent browser session management by @kunal-10-cloud
in [#21306](https://github.com/google-gemini/gemini-cli/pull/21306)
- refactor(core): delegate sandbox denial parsing to SandboxManager by
@scidomino in [#23928](https://github.com/google-gemini/gemini-cli/pull/23928)
- dep(update) Update Ink version to 6.5.0 by @jacob314 in
[#23843](https://github.com/google-gemini/gemini-cli/pull/23843)
- Docs: Update 'docs-writer' skill for relative links by @jkcinouye in
[#21463](https://github.com/google-gemini/gemini-cli/pull/21463)
- Changelog for v0.36.0-preview.4 by @gemini-cli-robot in
[#23935](https://github.com/google-gemini/gemini-cli/pull/23935)
- fix(acp): Update allow approval policy flow for ACP clients to fix config
persistence and compatible with TUI by @sripasg in
[#23818](https://github.com/google-gemini/gemini-cli/pull/23818)
- Changelog for v0.35.2 by @gemini-cli-robot in
[#23960](https://github.com/google-gemini/gemini-cli/pull/23960)
- ACP integration documents by @g-samroberts in
[#22254](https://github.com/google-gemini/gemini-cli/pull/22254)
- fix(core): explicitly set error names to avoid bundling renaming issues by
@yunaseoul in [#23913](https://github.com/google-gemini/gemini-cli/pull/23913)
- feat(core): subagent isolation and cleanup hardening by @abhipatel12 in
[#23903](https://github.com/google-gemini/gemini-cli/pull/23903)
- disable extension-reload test by @scidomino in
[#24018](https://github.com/google-gemini/gemini-cli/pull/24018)
- feat(core): add forbiddenPaths to GlobalSandboxOptions and refactor
createSandboxManager by @ehedlund in
[#23936](https://github.com/google-gemini/gemini-cli/pull/23936)
- refactor(core): improve ignore resolution and fix directory-matching bug by
@ehedlund in [#23816](https://github.com/google-gemini/gemini-cli/pull/23816)
- revert(core): support custom base URL via env vars by @spencer426 in
[#23976](https://github.com/google-gemini/gemini-cli/pull/23976)
- Increase memory limited for eslint. by @jacob314 in
[#24022](https://github.com/google-gemini/gemini-cli/pull/24022)
- fix(acp): prevent crash on empty response in ACP mode by @sripasg in
[#23952](https://github.com/google-gemini/gemini-cli/pull/23952)
- feat(core): Land `AgentHistoryProvider`. by @joshualitt in
[#23978](https://github.com/google-gemini/gemini-cli/pull/23978)
- fix(core): switch to subshells for shell tool wrapping to fix heredocs and
edge cases by @abhipatel12 in
[#24024](https://github.com/google-gemini/gemini-cli/pull/24024)
- Debug command. by @jacob314 in
[#23851](https://github.com/google-gemini/gemini-cli/pull/23851)
- Changelog for v0.36.0-preview.5 by @gemini-cli-robot in
[#24046](https://github.com/google-gemini/gemini-cli/pull/24046)
- Fix test flakes by globally mocking ink-spinner by @jacob314 in
[#24044](https://github.com/google-gemini/gemini-cli/pull/24044)
- Enable network access in sandbox configuration by @galz10 in
[#24055](https://github.com/google-gemini/gemini-cli/pull/24055)
- feat(context): add configurable memoryBoundaryMarkers setting by @SandyTao520
in [#24020](https://github.com/google-gemini/gemini-cli/pull/24020)
- feat(core): implement windows sandbox expansion and denial detection by
@scidomino in [#24027](https://github.com/google-gemini/gemini-cli/pull/24027)
- fix(core): resolve ACP Operation Aborted Errors in grep_search by @ivanporty
in [#23821](https://github.com/google-gemini/gemini-cli/pull/23821)
- fix(hooks): prevent SessionEnd from firing twice in non-interactive mode by
@krishdef7 in [#22139](https://github.com/google-gemini/gemini-cli/pull/22139)
- Re-word intro to Gemini 3 page. by @g-samroberts in
[#24069](https://github.com/google-gemini/gemini-cli/pull/24069)
- fix(cli): resolve layout contention and flashing loop in StatusRow by
@keithguerin in
[#24065](https://github.com/google-gemini/gemini-cli/pull/24065)
- fix(sandbox): implement Windows Mandatory Integrity Control for GeminiSandbox
by @galz10 in [#24057](https://github.com/google-gemini/gemini-cli/pull/24057)
- feat(core): implement tool-based topic grouping (Chapters) by @Abhijit-2592 in
[#23150](https://github.com/google-gemini/gemini-cli/pull/23150)
- feat(cli): support 'tab to queue' for messages while generating by @gundermanc
in [#24052](https://github.com/google-gemini/gemini-cli/pull/24052)
- feat(core): agnostic background task UI with CompletionBehavior by
@adamfweidman in
[#22740](https://github.com/google-gemini/gemini-cli/pull/22740)
- UX for topic narration tool by @gundermanc in
[#24079](https://github.com/google-gemini/gemini-cli/pull/24079)
- fix: shellcheck warnings in scripts by @scidomino in
[#24035](https://github.com/google-gemini/gemini-cli/pull/24035)
- test(evals): add comprehensive subagent delegation evaluations by @abhipatel12
in [#24132](https://github.com/google-gemini/gemini-cli/pull/24132)
- fix(a2a-server): prioritize ADC before evaluating headless constraints for
auth initialization by @spencer426 in
[#23614](https://github.com/google-gemini/gemini-cli/pull/23614)
- Text can be added after /plan command by @rambleraptor in
[#22833](https://github.com/google-gemini/gemini-cli/pull/22833)
- fix(cli): resolve missing F12 logs via global console store by @scidomino in
[#24235](https://github.com/google-gemini/gemini-cli/pull/24235)
- fix broken tests by @scidomino in
[#24279](https://github.com/google-gemini/gemini-cli/pull/24279)
- fix(evals): add update_topic behavioral eval by @gundermanc in
[#24223](https://github.com/google-gemini/gemini-cli/pull/24223)
- feat(core): Unified Context Management and Tool Distillation. by @joshualitt
in [#24157](https://github.com/google-gemini/gemini-cli/pull/24157)
- Default enable narration for the team. by @gundermanc in
[#24224](https://github.com/google-gemini/gemini-cli/pull/24224)
- fix(core): ensure default agents provide tools and use model-specific schemas
by @abhipatel12 in
[#24268](https://github.com/google-gemini/gemini-cli/pull/24268)
- feat(cli): show Flash Lite Preview model regardless of user tier by @sehoon38
in [#23904](https://github.com/google-gemini/gemini-cli/pull/23904)
- feat(cli): implement compact tool output by @jwhelangoog in
[#20974](https://github.com/google-gemini/gemini-cli/pull/20974)
- Add security settings for tool sandboxing by @galz10 in
[#23923](https://github.com/google-gemini/gemini-cli/pull/23923)
- chore(test-utils): switch integration tests to use PREVIEW_GEMINI_MODEL by
@sehoon38 in [#24276](https://github.com/google-gemini/gemini-cli/pull/24276)
- feat(core): enable topic update narration for legacy models by @Abhijit-2592
in [#24241](https://github.com/google-gemini/gemini-cli/pull/24241)
- feat(core): add project-level memory scope to save_memory tool by @SandyTao520
in [#24161](https://github.com/google-gemini/gemini-cli/pull/24161)
- test(integration): fix plan mode write denial test false positive by @sehoon38
in [#24299](https://github.com/google-gemini/gemini-cli/pull/24299)
- feat(plan): support `Plan` mode in untrusted folders by @Adib234 in
[#17586](https://github.com/google-gemini/gemini-cli/pull/17586)
- fix(core): enable mid-stream retries for all models and re-enable compression
test by @sehoon38 in
[#24302](https://github.com/google-gemini/gemini-cli/pull/24302)
- Changelog for v0.36.0-preview.6 by @gemini-cli-robot in
[#24082](https://github.com/google-gemini/gemini-cli/pull/24082)
- Changelog for v0.35.3 by @gemini-cli-robot in
[#24083](https://github.com/google-gemini/gemini-cli/pull/24083)
- feat(cli): add auth info to footer by @sehoon38 in
[#24042](https://github.com/google-gemini/gemini-cli/pull/24042)
- fix(browser): reset action counter for each agent session and let it ignore
internal actions by @cynthialong0-0 in
[#24228](https://github.com/google-gemini/gemini-cli/pull/24228)
- feat(plan): promote planning feature to stable by @ruomengz in
[#24282](https://github.com/google-gemini/gemini-cli/pull/24282)
- fix(browser): terminate subagent immediately on domain restriction violations
by @gsquared94 in
[#24313](https://github.com/google-gemini/gemini-cli/pull/24313)
- feat(cli): add UI to update extensions by @ruomengz in
[#23682](https://github.com/google-gemini/gemini-cli/pull/23682)
- Fix(browser): terminate immediately for "browser is already running" error by
@cynthialong0-0 in
[#24233](https://github.com/google-gemini/gemini-cli/pull/24233)
- docs: Add 'plan' option to approval mode in CLI reference by @YifanRuan in
[#24134](https://github.com/google-gemini/gemini-cli/pull/24134)
- fix(core): batch macOS seatbelt rules into a profile file to prevent ARG_MAX
errors by @ehedlund in
[#24255](https://github.com/google-gemini/gemini-cli/pull/24255)
- fix(core): fix race condition between browser agent and main closing process
by @cynthialong0-0 in
[#24340](https://github.com/google-gemini/gemini-cli/pull/24340)
- perf(build): optimize build scripts for parallel execution and remove
redundant checks by @sehoon38 in
[#24307](https://github.com/google-gemini/gemini-cli/pull/24307)
- ci: install bubblewrap on Linux for release workflows by @ehedlund in
[#24347](https://github.com/google-gemini/gemini-cli/pull/24347)
- chore(release): allow bundling for all builds, including stable by @sehoon38
in [#24305](https://github.com/google-gemini/gemini-cli/pull/24305)
- Revert "Add security settings for tool sandboxing" by @jerop in
[#24357](https://github.com/google-gemini/gemini-cli/pull/24357)
- docs: update subagents docs to not be experimental by @abhipatel12 in
[#24343](https://github.com/google-gemini/gemini-cli/pull/24343)
- fix(core): implement **read and **write commands in sandbox managers by
@galz10 in [#24283](https://github.com/google-gemini/gemini-cli/pull/24283)
- don't try to remove tags in dry run by @scidomino in
[#24356](https://github.com/google-gemini/gemini-cli/pull/24356)
- fix(config): disable JIT context loading by default by @SandyTao520 in
[#24364](https://github.com/google-gemini/gemini-cli/pull/24364)
- test(sandbox): add integration test for dynamic permission expansion by
@galz10 in [#24359](https://github.com/google-gemini/gemini-cli/pull/24359)
- docs(policy): remove unsupported mcpName wildcard edge case by @abhipatel12 in
[#24133](https://github.com/google-gemini/gemini-cli/pull/24133)
- docs: fix broken GEMINI.md link in CONTRIBUTING.md by @Panchal-Tirth in
[#24182](https://github.com/google-gemini/gemini-cli/pull/24182)
- feat(core): infrastructure for event-driven subagent history by @abhipatel12
in [#23914](https://github.com/google-gemini/gemini-cli/pull/23914)
- fix(core): resolve Plan Mode deadlock during plan file creation due to sandbox
restrictions by @DavidAPierce in
[#24047](https://github.com/google-gemini/gemini-cli/pull/24047)
- fix(core): fix browser agent UX issues and improve E2E test reliability by
@gsquared94 in
[#24312](https://github.com/google-gemini/gemini-cli/pull/24312)
- fix(ui): wrap topic and intent fields in TopicMessage by @jwhelangoog in
[#24386](https://github.com/google-gemini/gemini-cli/pull/24386)
- refactor(core): Centralize context management logic into src/context by
@joshualitt in
[#24752](https://github.com/google-gemini/gemini-cli/pull/24752)
- docs(core): update generalist agent documentation by @abhipatel12 in
[#25325](https://github.com/google-gemini/gemini-cli/pull/25325)
- chore(mcp): check MCP error code over brittle string match by @jackwotherspoon
in [#25381](https://github.com/google-gemini/gemini-cli/pull/25381)
- feat(plan): update plan mode prompt to allow showing plan content by @ruomengz
in [#25058](https://github.com/google-gemini/gemini-cli/pull/25058)
- test(core): improve sandbox integration test coverage and fix OS-specific
failures by @ehedlund in
[#25307](https://github.com/google-gemini/gemini-cli/pull/25307)
- fix(core): use debug level for keychain fallback logging by @ehedlund in
[#25398](https://github.com/google-gemini/gemini-cli/pull/25398)
- feat(test): add a performance test in asian language by @cynthialong0-0 in
[#25392](https://github.com/google-gemini/gemini-cli/pull/25392)
- feat(cli): enable mouse clicking for cursor positioning in AskUser multi-line
answers by @Adib234 in
[#24630](https://github.com/google-gemini/gemini-cli/pull/24630)
- fix(core): detect kmscon terminal as supporting true color by @claygeo in
[#25282](https://github.com/google-gemini/gemini-cli/pull/25282)
- ci: add agent session drift check workflow by @adamfweidman in
[#25389](https://github.com/google-gemini/gemini-cli/pull/25389)
- use macos-latest-large runner where applicable. by @scidomino in
[#25413](https://github.com/google-gemini/gemini-cli/pull/25413)
- Changelog for v0.37.2 by @gemini-cli-robot in
[#25336](https://github.com/google-gemini/gemini-cli/pull/25336)
- fix(patch): cherry-pick a4e98c0 to release/v0.39.0-preview.0-pr-25138 to patch
version v0.39.0-preview.0 and create version 0.39.0-preview.1 by
[#24380](https://github.com/google-gemini/gemini-cli/pull/24380)
- fix(core): pin AuthType.GATEWAY to use Gemini 3.1 Pro/Flash Lite by default by
@sripasg in [#24375](https://github.com/google-gemini/gemini-cli/pull/24375)
- feat(ui): add Tokyo Night theme by @danrneal in
[#24054](https://github.com/google-gemini/gemini-cli/pull/24054)
- fix(cli): refactor test config loading and mock debugLogger in test-setup by
@mattKorwel in
[#24389](https://github.com/google-gemini/gemini-cli/pull/24389)
- Set memoryManager to false in settings.json by @mattKorwel in
[#24393](https://github.com/google-gemini/gemini-cli/pull/24393)
- ink 6.6.3 by @jacob314 in
[#24372](https://github.com/google-gemini/gemini-cli/pull/24372)
- fix(core): resolve subagent chat recording gaps and directory inheritance by
@abhipatel12 in
[#24368](https://github.com/google-gemini/gemini-cli/pull/24368)
- fix(cli): cap shell output at 10 MB to prevent RangeError crash by @ProthamD
in [#24168](https://github.com/google-gemini/gemini-cli/pull/24168)
- feat(plan): conditionally add enter/exit plan mode tools based on current mode
by @ruomengz in
[#24378](https://github.com/google-gemini/gemini-cli/pull/24378)
- feat(core): prioritize discussion before formal plan approval by @jerop in
[#24423](https://github.com/google-gemini/gemini-cli/pull/24423)
- fix(ui): add accelerated scrolling on alternate buffer mode by @devr0306 in
[#23940](https://github.com/google-gemini/gemini-cli/pull/23940)
- feat(core): populate sandbox forbidden paths with project ignore file contents
by @ehedlund in
[#24038](https://github.com/google-gemini/gemini-cli/pull/24038)
- fix(core): ensure blue border overlay and input blocker to act correctly
depending on browser agent activities by @cynthialong0-0 in
[#24385](https://github.com/google-gemini/gemini-cli/pull/24385)
- fix(ui): removed additional vertical padding for tables by @devr0306 in
[#24381](https://github.com/google-gemini/gemini-cli/pull/24381)
- fix(build): upload full bundle directory archive to GitHub releases by
@sehoon38 in [#24403](https://github.com/google-gemini/gemini-cli/pull/24403)
- fix(build): wire bundle:browser-mcp into bundle pipeline by @gsquared94 in
[#24424](https://github.com/google-gemini/gemini-cli/pull/24424)
- feat(browser): add sandbox-aware browser agent initialization by @gsquared94
in [#24419](https://github.com/google-gemini/gemini-cli/pull/24419)
- feat(core): enhance tracker task schemas for detailed titles and descriptions
by @anj-s in [#23902](https://github.com/google-gemini/gemini-cli/pull/23902)
- refactor(core): Unified context management settings schema by @joshualitt in
[#24391](https://github.com/google-gemini/gemini-cli/pull/24391)
- feat(core): update browser agent prompt to check open pages first when
bringing up by @cynthialong0-0 in
[#24431](https://github.com/google-gemini/gemini-cli/pull/24431)
- fix(acp) refactor(core,cli): centralize model discovery logic in
ModelConfigService by @sripasg in
[#24392](https://github.com/google-gemini/gemini-cli/pull/24392)
- Changelog for v0.36.0-preview.7 by @gemini-cli-robot in
[#24346](https://github.com/google-gemini/gemini-cli/pull/24346)
- fix: update task tracker storage location in system prompt by @anj-s in
[#24034](https://github.com/google-gemini/gemini-cli/pull/24034)
- feat(browser): supersede stale snapshots to reclaim context-window tokens by
@gsquared94 in
[#24440](https://github.com/google-gemini/gemini-cli/pull/24440)
- docs(core): add subagent tool isolation draft doc by @akh64bit in
[#23275](https://github.com/google-gemini/gemini-cli/pull/23275)
- fix(patch): cherry-pick 64c928f to release/v0.37.0-preview.0-pr-23257 to patch
version v0.37.0-preview.0 and create version 0.37.0-preview.1 by
@gemini-cli-robot in
[#25766](https://github.com/google-gemini/gemini-cli/pull/25766)
- fix(patch): cherry-pick d6f88f8 to release/v0.39.0-preview.1-pr-25670 to patch
version v0.39.0-preview.1 and create version 0.39.0-preview.2 by
[#24561](https://github.com/google-gemini/gemini-cli/pull/24561)
- fix(patch): cherry-pick cb7f7d6 to release/v0.37.0-preview.1-pr-24342 to patch
version v0.37.0-preview.1 and create version 0.37.0-preview.2 by
@gemini-cli-robot in
[#25776](https://github.com/google-gemini/gemini-cli/pull/25776)
[#24842](https://github.com/google-gemini/gemini-cli/pull/24842)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.38.2...v0.39.0
https://github.com/google-gemini/gemini-cli/compare/v0.36.0...v0.37.1
+247 -386
View File
@@ -1,6 +1,6 @@
# Preview release: v0.40.0-preview.3
# Preview release: v0.38.0-preview.0
Released: April 24, 2026
Released: April 08, 2026
Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md).
@@ -13,395 +13,256 @@ npm install -g @google/gemini-cli@preview
## Highlights
- **Ripgrep Binary Bundling:** Ripgrep binaries are now bundled into the Single
Executable Application (SEA), enabling grep functionality in offline
environments.
- **MCP Resource Tools:** New core tools added to list and read MCP (Model
Context Protocol) resources, expanding the agent's ability to interact with
MCP servers.
- **Local Model Setup:** Introduced a streamlined `gemini gemma` command for
easier local model setup and integration.
- **Prompt-Driven Memory Management:** Refactored memory management into a
prompt-driven, four-tier system and integrated `skill-creator` for robust
skill extraction.
- **Enhanced UI and Accessibility:** Added support for OSC 777 terminal
notifications and GitHub colorblind themes for better user feedback and
accessibility.
- **Context Management:** Introduced a Context Compression Service to optimize
context window usage and landed a background memory service for skill
extraction.
- **Enhanced Security:** Implemented context-aware persistent policy approvals
for smarter tool permissions and enabled `web_fetch` in plan mode with user
confirmation.
- **Workflow Monitoring:** Added background process monitoring and inspection
tools for better visibility into long-running tasks.
- **UI/UX Refinements:** Enhanced the tool confirmation UI, selection layout,
and added support for selective topic expansion and click-to-expand.
- **Core Stability:** Improved sandbox reliability on Linux and Windows,
resolved shebang compatibility issues, and fixed various crashes in the CLI
and core services.
## What's Changed
- update FatalUntrustedWorkspaceError message to include doc link by @ehedlund
in [#25874](https://github.com/google-gemini/gemini-cli/pull/25874)
- feat(core): enhance shell command validation and add core tools allowlist by
@galz10 in [#25720](https://github.com/google-gemini/gemini-cli/pull/25720)
- feat(cli): secure .env loading and enforce workspace trust in headless mode by
@ehedlund in [#25814](https://github.com/google-gemini/gemini-cli/pull/25814)
- chore(release): bump version to 0.40.0-nightly.20260414.g5b1f7375a by
@gemini-cli-robot in
[#25420](https://github.com/google-gemini/gemini-cli/pull/25420)
- Fix(core): retry additional OpenSSL 3.x SSL errors during streaming (#16075)
by @rcleveng in
[#25187](https://github.com/google-gemini/gemini-cli/pull/25187)
- fix(core): prevent YOLO mode from being downgraded by @galz10 in
[#25341](https://github.com/google-gemini/gemini-cli/pull/25341)
- feat: bundle ripgrep binaries into SEA for offline support by @scidomino in
[#25342](https://github.com/google-gemini/gemini-cli/pull/25342)
- Changelog for v0.39.0-preview.0 by @gemini-cli-robot in
[#25417](https://github.com/google-gemini/gemini-cli/pull/25417)
- feat(test): add large conversation scenario for performance test by
@cynthialong0-0 in
[#25331](https://github.com/google-gemini/gemini-cli/pull/25331)
- improve(core): require recurrence evidence before extracting skills by
@SandyTao520 in
[#25147](https://github.com/google-gemini/gemini-cli/pull/25147)
- test(evals): add subagent delegation evaluation tests by @anj-s in
[#24619](https://github.com/google-gemini/gemini-cli/pull/24619)
- feat: add github colorblind themes by @Z1xus in
[#15504](https://github.com/google-gemini/gemini-cli/pull/15504)
- fix(core): honor GOOGLE_GEMINI_BASE_URL and GOOGLE_VERTEX_BASE_URL by
@chrisjcthomas in
[#25357](https://github.com/google-gemini/gemini-cli/pull/25357)
- fix(cli): clean up slash command IDE listeners by @jasonmatthewsuhari in
[#24397](https://github.com/google-gemini/gemini-cli/pull/24397)
- Changelog for v0.38.0 by @gemini-cli-robot in
[#25470](https://github.com/google-gemini/gemini-cli/pull/25470)
- fix(evals): update eval tests for invoke_agent telemetry and project-scoped
memory by @SandyTao520 in
[#25502](https://github.com/google-gemini/gemini-cli/pull/25502)
- Changelog for v0.38.1 by @gemini-cli-robot in
[#25476](https://github.com/google-gemini/gemini-cli/pull/25476)
- feat(core): integrate skill-creator into skill extraction agent by
@SandyTao520 in
[#25421](https://github.com/google-gemini/gemini-cli/pull/25421)
- feat(cli): provide default post-submit prompt for skill command by @ruomengz
in [#25327](https://github.com/google-gemini/gemini-cli/pull/25327)
- feat(core): add tools to list and read MCP resources by @ruomengz in
[#25395](https://github.com/google-gemini/gemini-cli/pull/25395)
- fix(evals): add typecheck coverage for evals, integration-tests, and
memory-tests by @SandyTao520 in
[#25480](https://github.com/google-gemini/gemini-cli/pull/25480)
- Use OSC 777 for terminal notifications by @jackyliuxx in
[#25300](https://github.com/google-gemini/gemini-cli/pull/25300)
- fix(extensions): fix bundling for examples by @abhipatel12 in
[#25542](https://github.com/google-gemini/gemini-cli/pull/25542)
- fix(cli): reset plan session state on /clear by @jasonmatthewsuhari in
[#25515](https://github.com/google-gemini/gemini-cli/pull/25515)
- feat(core): add .mdx support to get-internal-docs tool by @g-samroberts in
[#25090](https://github.com/google-gemini/gemini-cli/pull/25090)
- docs(policy): mention that workspace policies are broken by @6112 in
[#24367](https://github.com/google-gemini/gemini-cli/pull/24367)
- fix(core): allow explicit write permissions to override governance file
protections in sandboxes by @galz10 in
[#25338](https://github.com/google-gemini/gemini-cli/pull/25338)
- feat(sandbox): resolve custom seatbelt profiles from $HOME/.gemini first by
@mvanhorn in [#25427](https://github.com/google-gemini/gemini-cli/pull/25427)
- Reduce blank lines. by @gundermanc in
[#25563](https://github.com/google-gemini/gemini-cli/pull/25563)
- fix(ui): revert preview theme on dialog unmount by @JayadityaGit in
[#22542](https://github.com/google-gemini/gemini-cli/pull/22542)
- fix(core): fix ShellExecutionConfig spread and add ProjectRegistry save
backoff by @mahimashanware in
[#25382](https://github.com/google-gemini/gemini-cli/pull/25382)
- feat(core): Disable topic updates for subagents by @gundermanc in
[#25567](https://github.com/google-gemini/gemini-cli/pull/25567)
- feat(core): enable topic update narration by default and promote to general by
@gundermanc in
[#25586](https://github.com/google-gemini/gemini-cli/pull/25586)
- docs: migrate installation and authentication to mdx with tabbed layouts by
@g-samroberts in
[#25155](https://github.com/google-gemini/gemini-cli/pull/25155)
- feat(config): split memoryManager flag into autoMemory by @SandyTao520 in
[#25601](https://github.com/google-gemini/gemini-cli/pull/25601)
- fix(core): allow Cloud Shell users to use PRO_MODEL_NO_ACCESS experiment by
@sehoon38 in [#25702](https://github.com/google-gemini/gemini-cli/pull/25702)
- fix(cli): round slow render latency to avoid opentelemetry float warning by
@scidomino in [#25709](https://github.com/google-gemini/gemini-cli/pull/25709)
- docs(tracker): introduce experimental task tracker feature by @anj-s in
[#24556](https://github.com/google-gemini/gemini-cli/pull/24556)
- docs(cli): fix inconsistent system.md casing in system prompt docs by @Bodlux
in [#25414](https://github.com/google-gemini/gemini-cli/pull/25414)
- feat(cli): add streamlined `gemini gemma` local model setup by @Samee24 in
[#25498](https://github.com/google-gemini/gemini-cli/pull/25498)
- Changelog for v0.38.2 by @gemini-cli-robot in
[#25593](https://github.com/google-gemini/gemini-cli/pull/25593)
- Fix: Disallow overriding IDE stdio via workspace .env (RCE) by @M0nd0R in
[#25022](https://github.com/google-gemini/gemini-cli/pull/25022)
- feat(test): refactor the memory usage test to use metrics from CLI process
instead of test runner by @cynthialong0-0 in
[#25708](https://github.com/google-gemini/gemini-cli/pull/25708)
- feat(vertex): add settings for Vertex AI request routing by @gordonhwc in
[#25513](https://github.com/google-gemini/gemini-cli/pull/25513)
- Fix/allow for session persistence by @ahsanfarooq210 in
[#25176](https://github.com/google-gemini/gemini-cli/pull/25176)
- Allow dots on GEMINI_API_KEY by @DKbyo in
[#25497](https://github.com/google-gemini/gemini-cli/pull/25497)
- feat(telemetry): add flag for enabling traces specifically by @spencer426 in
[#25343](https://github.com/google-gemini/gemini-cli/pull/25343)
- fix(core): resolve nested plan directory duplication and relative path
policies by @mahimashanware in
[#25138](https://github.com/google-gemini/gemini-cli/pull/25138)
- feat: detect new files in @ recommendations with watcher based updates by
@prassamin in [#25256](https://github.com/google-gemini/gemini-cli/pull/25256)
- fix(cli): use newline in shell command wrapping to avoid breaking heredocs by
@cocosheng-g in
[#25537](https://github.com/google-gemini/gemini-cli/pull/25537)
- fix(cli): ensure theme dialog labels are rendered for all themes by
@JayadityaGit in
[#24599](https://github.com/google-gemini/gemini-cli/pull/24599)
- fix(core): disable detached mode in Bun to prevent immediate SIGHUP of child
processes by @euxaristia in
[#22620](https://github.com/google-gemini/gemini-cli/pull/22620)
- feat: add /new as alias for /clear and refine command description by @ved015
in [#17865](https://github.com/google-gemini/gemini-cli/pull/17865)
- fix(cli): start auto memory in ACP sessions by @jasonmatthewsuhari in
[#25626](https://github.com/google-gemini/gemini-cli/pull/25626)
- fix(core): remove duplicate initialize call on agents refreshed by
@adamfweidman in
[#25670](https://github.com/google-gemini/gemini-cli/pull/25670)
- test(e2e): default integration tests to Flash Preview by @SandyTao520 in
[#25753](https://github.com/google-gemini/gemini-cli/pull/25753)
- refactor(memory): replace MemoryManagerAgent with prompt-driven memory editing
across four tiers by @SandyTao520 in
[#25716](https://github.com/google-gemini/gemini-cli/pull/25716)
- fix(cli): fix "/clear (new)" command by @mini2s in
[#25801](https://github.com/google-gemini/gemini-cli/pull/25801)
- fix(core): use dynamic CLI version for IDE client instead of hardcoded '1.0.0'
by @thekishandev in
[#24414](https://github.com/google-gemini/gemini-cli/pull/24414)
- fix(core): handle line endings in ignore file parsing by @xoma-zver in
[#23895](https://github.com/google-gemini/gemini-cli/pull/23895)
- Fix/command injection shell by @Famous077 in
[#24170](https://github.com/google-gemini/gemini-cli/pull/24170)
- fix(ui): removed background color for input by @devr0306 in
[#25339](https://github.com/google-gemini/gemini-cli/pull/25339)
- fix(devtools): reduce memory usage and defer connection by @SandyTao520 in
[#24496](https://github.com/google-gemini/gemini-cli/pull/24496)
- fix(core): support jsonl session logs in memory and summary services by
@SandyTao520 in
[#25816](https://github.com/google-gemini/gemini-cli/pull/25816)
- fix(release): exclude ripgrep binaries from npm tarballs by @SandyTao520 in
[#25841](https://github.com/google-gemini/gemini-cli/pull/25841)
- refactor(plan): simplify policy priorities and consolidate read-only rules by
@ruomengz in [#24849](https://github.com/google-gemini/gemini-cli/pull/24849)
- feat(test-utils): add memory usage integration test harness by @sripasg in
[#24876](https://github.com/google-gemini/gemini-cli/pull/24876)
- feat(memory): add /memory inbox command for reviewing extracted skills by
@SandyTao520 in
[#24544](https://github.com/google-gemini/gemini-cli/pull/24544)
- chore(release): bump version to 0.39.0-nightly.20260408.e77b22e63 by
@gemini-cli-robot in
[#24939](https://github.com/google-gemini/gemini-cli/pull/24939)
- fix(core): ensure robust sandbox cleanup in all process execution paths by
@ehedlund in [#24763](https://github.com/google-gemini/gemini-cli/pull/24763)
- chore: update ink version to 6.6.8 by @jacob314 in
[#24934](https://github.com/google-gemini/gemini-cli/pull/24934)
- Changelog for v0.38.0-preview.0 by @gemini-cli-robot in
[#24938](https://github.com/google-gemini/gemini-cli/pull/24938)
- chore: ignore conductor directory by @JayadityaGit in
[#22128](https://github.com/google-gemini/gemini-cli/pull/22128)
- Changelog for v0.37.0 by @gemini-cli-robot in
[#24940](https://github.com/google-gemini/gemini-cli/pull/24940)
- feat(plan): require user confirmation for activate_skill in Plan Mode by
@ruomengz in [#24946](https://github.com/google-gemini/gemini-cli/pull/24946)
- feat(test-utils): add CPU performance integration test harness by @sripasg in
[#24951](https://github.com/google-gemini/gemini-cli/pull/24951)
- fix(cli-ui): enable Ctrl+Backspace for word deletion in Windows Terminal by
@dogukanozen in
[#21447](https://github.com/google-gemini/gemini-cli/pull/21447)
- test(sdk): add unit tests for GeminiCliSession by @AdamyaSingh7 in
[#21897](https://github.com/google-gemini/gemini-cli/pull/21897)
- fix(core): resolve windows symlink bypass and stabilize sandbox integration
tests by @ehedlund in
[#24834](https://github.com/google-gemini/gemini-cli/pull/24834)
- fix(cli): restore file path display in edit and write tool confirmations by
@jwhelangoog in
[#24974](https://github.com/google-gemini/gemini-cli/pull/24974)
- feat(core): refine shell tool description display logic by @jwhelangoog in
[#24903](https://github.com/google-gemini/gemini-cli/pull/24903)
- fix(core): dynamic session ID injection to resolve resume bugs by @scidomino
in [#24972](https://github.com/google-gemini/gemini-cli/pull/24972)
- Update ink version to 6.6.9 by @jacob314 in
[#24980](https://github.com/google-gemini/gemini-cli/pull/24980)
- Generalize evals infra to support more types of evals, organization and
queuing of named suites by @gundermanc in
[#24941](https://github.com/google-gemini/gemini-cli/pull/24941)
- fix(cli): optimize startup with lightweight parent process by @sehoon38 in
[#24667](https://github.com/google-gemini/gemini-cli/pull/24667)
- refactor(sandbox): use centralized sandbox paths in macOS Seatbelt
implementation by @ehedlund in
[#24984](https://github.com/google-gemini/gemini-cli/pull/24984)
- feat(cli): refine tool output formatting for compact mode by @jwhelangoog in
[#24677](https://github.com/google-gemini/gemini-cli/pull/24677)
- fix(sdk): skip broken sendStream tests to unblock nightly by @SandyTao520 in
[#25000](https://github.com/google-gemini/gemini-cli/pull/25000)
- refactor(core): use centralized path resolution for Linux sandbox by @ehedlund
in [#24985](https://github.com/google-gemini/gemini-cli/pull/24985)
- Support ctrl+shift+g by @jacob314 in
[#25035](https://github.com/google-gemini/gemini-cli/pull/25035)
- feat(core): refactor subagent tool to unified invoke_subagent tool by
- fix(cli): refresh slash command list after /skills reload by @NTaylorMullen in
[#24454](https://github.com/google-gemini/gemini-cli/pull/24454)
- Update README.md for links. by @g-samroberts in
[#22759](https://github.com/google-gemini/gemini-cli/pull/22759)
- fix(core): ensure complete_task tool calls are recorded in chat history by
@abhipatel12 in
[#24489](https://github.com/google-gemini/gemini-cli/pull/24489)
- fix(core): add explicit git identity env vars to prevent sandbox checkpointing
error by @mrpmohiburrahman in
[#19775](https://github.com/google-gemini/gemini-cli/pull/19775)
- fix: respect hideContextPercentage when FooterConfigDialog is closed without
changes by @chernistry in
[#24773](https://github.com/google-gemini/gemini-cli/pull/24773)
- fix(cli): suppress unhandled AbortError logs during request cancellation by
@euxaristia in
[#22621](https://github.com/google-gemini/gemini-cli/pull/22621)
- Automated documentation audit by @g-samroberts in
[#24567](https://github.com/google-gemini/gemini-cli/pull/24567)
- feat(cli): implement useAgentStream hook by @mbleigh in
[#24292](https://github.com/google-gemini/gemini-cli/pull/24292)
- refactor(plan) Clean default plan toml by @ruomengz in
[#25037](https://github.com/google-gemini/gemini-cli/pull/25037)
- refactor(core): remove legacy subagent wrapping tools by @abhipatel12 in
[#25053](https://github.com/google-gemini/gemini-cli/pull/25053)
- fix(core): honor retryDelay in RetryInfo for 503 errors by @yunaseoul in
[#25057](https://github.com/google-gemini/gemini-cli/pull/25057)
- fix(core): remediate subagent memory leaks using AbortSignal in MessageBus by
@abhipatel12 in
[#25048](https://github.com/google-gemini/gemini-cli/pull/25048)
- feat(cli): wire up useAgentStream in AppContainer by @mbleigh in
[#24297](https://github.com/google-gemini/gemini-cli/pull/24297)
- feat(core): migrate chat recording to JSONL streaming by @spencer426 in
[#23749](https://github.com/google-gemini/gemini-cli/pull/23749)
- fix(core): clear 5-minute timeouts in oauth flow to prevent memory leaks by
@spencer426 in
[#24968](https://github.com/google-gemini/gemini-cli/pull/24968)
- fix(sandbox): centralize async git worktree resolution and enforce read-only
security by @ehedlund in
[#25040](https://github.com/google-gemini/gemini-cli/pull/25040)
- feat(test): add high-volume shell test and refine perf harness by @sripasg in
[#24983](https://github.com/google-gemini/gemini-cli/pull/24983)
- fix(core): silently handle EPERM when listing dir structure by @scidomino in
[#25066](https://github.com/google-gemini/gemini-cli/pull/25066)
- Changelog for v0.37.1 by @gemini-cli-robot in
[#25055](https://github.com/google-gemini/gemini-cli/pull/25055)
- fix: decode Uint8Array and multi-byte UTF-8 in API error messages by
@kimjune01 in [#23341](https://github.com/google-gemini/gemini-cli/pull/23341)
- Automated documentation audit results by @g-samroberts in
[#22755](https://github.com/google-gemini/gemini-cli/pull/22755)
- debugging(ui): add optional debugRainbow setting by @jacob314 in
[#25088](https://github.com/google-gemini/gemini-cli/pull/25088)
- fix: resolve lifecycle memory leaks by cleaning up listeners and root closures
by @spencer426 in
[#25049](https://github.com/google-gemini/gemini-cli/pull/25049)
- docs(cli): updates f12 description to be more precise by @JayadityaGit in
[#15816](https://github.com/google-gemini/gemini-cli/pull/15816)
- fix(cli): mark /settings as unsafe to run concurrently by @jacob314 in
[#25061](https://github.com/google-gemini/gemini-cli/pull/25061)
- fix(core): remove buffer slice to prevent OOM on large output streams by
@spencer426 in
[#25094](https://github.com/google-gemini/gemini-cli/pull/25094)
- feat(core): persist subagent agentId in tool call records by @abhipatel12 in
[#25092](https://github.com/google-gemini/gemini-cli/pull/25092)
- chore(core): increase codebase investigator turn limits to 50 by @abhipatel12
in [#25125](https://github.com/google-gemini/gemini-cli/pull/25125)
- refactor(core): consolidate execute() arguments into ExecuteOptions by
@mbleigh in [#25101](https://github.com/google-gemini/gemini-cli/pull/25101)
- feat(core): add Strategic Re-evaluation guidance to system prompt by
@aishaneeshah in
[#25062](https://github.com/google-gemini/gemini-cli/pull/25062)
- fix(core): preserve shell execution config fields on update by
@jasonmatthewsuhari in
[#25113](https://github.com/google-gemini/gemini-cli/pull/25113)
- docs: add vi shortcuts and clarify MCP sandbox setup by @chrisjcthomas in
[#21679](https://github.com/google-gemini/gemini-cli/pull/21679)
- fix(cli): pass session id to interactive shell executions by
@jasonmatthewsuhari in
[#25114](https://github.com/google-gemini/gemini-cli/pull/25114)
- fix(cli): resolve text sanitization data loss due to C1 control characters by
@euxaristia in
[#22624](https://github.com/google-gemini/gemini-cli/pull/22624)
- feat(core): add large memory regression test by @cynthialong0-0 in
[#25059](https://github.com/google-gemini/gemini-cli/pull/25059)
- fix(core): resolve PTY exhaustion and orphan MCP subprocess leaks by
@spencer426 in
[#25079](https://github.com/google-gemini/gemini-cli/pull/25079)
- chore(deps): update vulnerable dependencies via npm audit fix by @scidomino in
[#25140](https://github.com/google-gemini/gemini-cli/pull/25140)
- perf(sandbox): optimize Windows sandbox initialization via native ACL
application by @ehedlund in
[#25077](https://github.com/google-gemini/gemini-cli/pull/25077)
- chore: switch from keytar to @github/keytar by @cocosheng-g in
[#25143](https://github.com/google-gemini/gemini-cli/pull/25143)
- fix: improve audio MIME normalization and validation in file reads by
@junaiddshaukat in
[#21636](https://github.com/google-gemini/gemini-cli/pull/21636)
- docs: Update docs-audit to include changes in PR body by @g-samroberts in
[#25153](https://github.com/google-gemini/gemini-cli/pull/25153)
- docs: correct documentation for enforced authentication type by @cocosheng-g
in [#25142](https://github.com/google-gemini/gemini-cli/pull/25142)
- fix(cli): exclude update_topic from confirmation queue count by @Abhijit-2592
in [#24945](https://github.com/google-gemini/gemini-cli/pull/24945)
- Memory fix for trace's streamWrapper. by @anthraxmilkshake in
[#25089](https://github.com/google-gemini/gemini-cli/pull/25089)
- fix(core): fix quota footer for non-auto models and improve display by
[#24437](https://github.com/google-gemini/gemini-cli/pull/24437)
- feat(policy): explicitly allow web_fetch in plan mode with ask_user by
@Adib234 in [#24456](https://github.com/google-gemini/gemini-cli/pull/24456)
- fix(core): refactor linux sandbox to fix ARG_MAX crashes by @ehedlund in
[#24286](https://github.com/google-gemini/gemini-cli/pull/24286)
- feat(config): add experimental.adk.agentSessionNoninteractiveEnabled setting
by @adamfweidman in
[#24439](https://github.com/google-gemini/gemini-cli/pull/24439)
- Changelog for v0.36.0-preview.8 by @gemini-cli-robot in
[#24453](https://github.com/google-gemini/gemini-cli/pull/24453)
- feat(cli): change default loadingPhrases to 'off' to hide tips by @keithguerin
in [#24342](https://github.com/google-gemini/gemini-cli/pull/24342)
- fix(cli): ensure agent stops when all declinable tools are cancelled by
@NTaylorMullen in
[#24479](https://github.com/google-gemini/gemini-cli/pull/24479)
- fix(core): enhance sandbox usability and fix build error by @galz10 in
[#24460](https://github.com/google-gemini/gemini-cli/pull/24460)
- Terminal Serializer Optimization by @jacob314 in
[#24485](https://github.com/google-gemini/gemini-cli/pull/24485)
- Auto configure memory. by @jacob314 in
[#24474](https://github.com/google-gemini/gemini-cli/pull/24474)
- Unused error variables in catch block are not allowed by @alisa-alisa in
[#24487](https://github.com/google-gemini/gemini-cli/pull/24487)
- feat(core): add background memory service for skill extraction by @SandyTao520
in [#24274](https://github.com/google-gemini/gemini-cli/pull/24274)
- feat: implement high-signal PR regression check for evaluations by
@alisa-alisa in
[#23937](https://github.com/google-gemini/gemini-cli/pull/23937)
- Fix shell output display by @jacob314 in
[#24490](https://github.com/google-gemini/gemini-cli/pull/24490)
- fix(ui): resolve unwanted vertical spacing around various tool output
treatments by @jwhelangoog in
[#24449](https://github.com/google-gemini/gemini-cli/pull/24449)
- revert(cli): bring back input box and footer visibility in copy mode by
@sehoon38 in [#24504](https://github.com/google-gemini/gemini-cli/pull/24504)
- fix(cli): prevent crash in AnsiOutputText when handling non-array data by
@sehoon38 in [#24498](https://github.com/google-gemini/gemini-cli/pull/24498)
- feat(cli): support default values for environment variables by @ruomengz in
[#24469](https://github.com/google-gemini/gemini-cli/pull/24469)
- Implement background process monitoring and inspection tools by @cocosheng-g
in [#23799](https://github.com/google-gemini/gemini-cli/pull/23799)
- docs(browser-agent): update stale browser agent documentation by @gsquared94
in [#24463](https://github.com/google-gemini/gemini-cli/pull/24463)
- fix: enable browser_agent in integration tests and add localhost fixture tests
by @gsquared94 in
[#24523](https://github.com/google-gemini/gemini-cli/pull/24523)
- fix(browser): handle computer-use model detection for analyze_screenshot by
@gsquared94 in
[#24502](https://github.com/google-gemini/gemini-cli/pull/24502)
- feat(core): Land ContextCompressionService by @joshualitt in
[#24483](https://github.com/google-gemini/gemini-cli/pull/24483)
- feat(core): scope subagent workspace directories via AsyncLocalStorage by
@SandyTao520 in
[#24445](https://github.com/google-gemini/gemini-cli/pull/24445)
- Update ink version to 6.6.7 by @jacob314 in
[#24514](https://github.com/google-gemini/gemini-cli/pull/24514)
- fix(acp): handle all InvalidStreamError types gracefully in prompt by @sripasg
in [#24540](https://github.com/google-gemini/gemini-cli/pull/24540)
- Fix crash when vim editor is not found in PATH on Windows by
@Nagajyothi-tammisetti in
[#22423](https://github.com/google-gemini/gemini-cli/pull/22423)
- fix(core): move project memory dir under tmp directory by @SandyTao520 in
[#24542](https://github.com/google-gemini/gemini-cli/pull/24542)
- Enable 'Other' option for yesno question type by @ruomengz in
[#24545](https://github.com/google-gemini/gemini-cli/pull/24545)
- fix(cli): clear stale retry/loading state after cancellation (#21096) by
@Aaxhirrr in [#21960](https://github.com/google-gemini/gemini-cli/pull/21960)
- Changelog for v0.37.0-preview.0 by @gemini-cli-robot in
[#24464](https://github.com/google-gemini/gemini-cli/pull/24464)
- feat(core): implement context-aware persistent policy approvals by @jerop in
[#23257](https://github.com/google-gemini/gemini-cli/pull/23257)
- docs: move agent disabling instructions and update remote agent status by
@jackwotherspoon in
[#25121](https://github.com/google-gemini/gemini-cli/pull/25121)
- docs(contributing): clarify self-assignment policy for issues by @jmr in
[#23087](https://github.com/google-gemini/gemini-cli/pull/23087)
- feat(core): add skill patching support with /memory inbox integration by
@SandyTao520 in
[#25148](https://github.com/google-gemini/gemini-cli/pull/25148)
- Stop suppressing thoughts and text in model response by @gundermanc in
[#25073](https://github.com/google-gemini/gemini-cli/pull/25073)
- fix(release): prefix git hash in nightly versions to prevent semver
normalization by @SandyTao520 in
[#25304](https://github.com/google-gemini/gemini-cli/pull/25304)
- feat(cli): extract QuotaContext and resolve infinite render loop by @Adib234
in [#24959](https://github.com/google-gemini/gemini-cli/pull/24959)
- refactor(core): extract and centralize sandbox path utilities by @ehedlund in
[#25305](https://github.com/google-gemini/gemini-cli/pull/25305)
- feat(ui): added enhancements to scroll momentum by @devr0306 in
[#24447](https://github.com/google-gemini/gemini-cli/pull/24447)
- fix(core): replace custom binary detection with isbinaryfile to correctly
handle UTF-8 (U+FFFD) by @Anjaligarhwal in
[#25297](https://github.com/google-gemini/gemini-cli/pull/25297)
- feat(agent): implement tool-controlled display protocol (Steps 2-3) by
@mbleigh in [#25134](https://github.com/google-gemini/gemini-cli/pull/25134)
- Stop showing scrollbar unless we are in terminalBuffer mode by @jacob314 in
[#25320](https://github.com/google-gemini/gemini-cli/pull/25320)
- feat: support auth block in MCP servers config in agents by @TanmayVartak in
[#24770](https://github.com/google-gemini/gemini-cli/pull/24770)
- fix(core): expose GEMINI_PLANS_DIR to hook environment by @Adib234 in
[#25296](https://github.com/google-gemini/gemini-cli/pull/25296)
- feat(core): implement silent fallback for Plan Mode model routing by @jerop in
[#25317](https://github.com/google-gemini/gemini-cli/pull/25317)
- fix: correct redirect count increment in fetchJson by @KevinZhao in
[#24896](https://github.com/google-gemini/gemini-cli/pull/24896)
- fix(core): prevent secondary crash in ModelRouterService finally block by
[#24559](https://github.com/google-gemini/gemini-cli/pull/24559)
- feat(cli): migrate nonInteractiveCli to LegacyAgentSession by @adamfweidman in
[#22987](https://github.com/google-gemini/gemini-cli/pull/22987)
- fix(core): unsafe type assertions in Core File System #19712 by
@aniketsaurav18 in
[#19739](https://github.com/google-gemini/gemini-cli/pull/19739)
- fix(ui): hide model quota in /stats and refactor quota display by @danzaharia1
in [#24206](https://github.com/google-gemini/gemini-cli/pull/24206)
- Changelog for v0.36.0 by @gemini-cli-robot in
[#24558](https://github.com/google-gemini/gemini-cli/pull/24558)
- Changelog for v0.37.0-preview.1 by @gemini-cli-robot in
[#24568](https://github.com/google-gemini/gemini-cli/pull/24568)
- docs: add missing .md extensions to internal doc links by @ishaan-arora-1 in
[#24145](https://github.com/google-gemini/gemini-cli/pull/24145)
- fix(ui): fixed table styling by @devr0306 in
[#24565](https://github.com/google-gemini/gemini-cli/pull/24565)
- fix(core): pass includeDirectories to sandbox configuration by @galz10 in
[#24573](https://github.com/google-gemini/gemini-cli/pull/24573)
- feat(ui): enable "TerminalBuffer" mode to solve flicker by @jacob314 in
[#24512](https://github.com/google-gemini/gemini-cli/pull/24512)
- docs: clarify release coordination by @scidomino in
[#24575](https://github.com/google-gemini/gemini-cli/pull/24575)
- fix(core): remove broken PowerShell translation and fix native \_\_write in
Windows sandbox by @scidomino in
[#24571](https://github.com/google-gemini/gemini-cli/pull/24571)
- Add instructions for how to start react in prod and force react to prod mode
by @jacob314 in
[#24590](https://github.com/google-gemini/gemini-cli/pull/24590)
- feat(cli): minimalist sandbox status labels by @galz10 in
[#24582](https://github.com/google-gemini/gemini-cli/pull/24582)
- Feat/browser agent metrics by @kunal-10-cloud in
[#24210](https://github.com/google-gemini/gemini-cli/pull/24210)
- test: fix Windows CI execution and resolve exposed platform failures by
@ehedlund in [#24476](https://github.com/google-gemini/gemini-cli/pull/24476)
- feat(core,cli): prioritize summary for topics (#24608) by @Abhijit-2592 in
[#24609](https://github.com/google-gemini/gemini-cli/pull/24609)
- show color by @jacob314 in
[#24613](https://github.com/google-gemini/gemini-cli/pull/24613)
- feat(cli): enable compact tool output by default (#24509) by @jwhelangoog in
[#24510](https://github.com/google-gemini/gemini-cli/pull/24510)
- fix(core): inject skill system instructions into subagent prompts if activated
by @abhipatel12 in
[#24620](https://github.com/google-gemini/gemini-cli/pull/24620)
- fix(core): improve windows sandbox reliability and fix integration tests by
@ehedlund in [#24480](https://github.com/google-gemini/gemini-cli/pull/24480)
- fix(core): ensure sandbox approvals are correctly persisted and matched for
proactive expansions by @galz10 in
[#24577](https://github.com/google-gemini/gemini-cli/pull/24577)
- feat(cli) Scrollbar for input prompt by @jacob314 in
[#21992](https://github.com/google-gemini/gemini-cli/pull/21992)
- Do not run pr-eval workflow when no steering changes detected by @alisa-alisa
in [#24621](https://github.com/google-gemini/gemini-cli/pull/24621)
- Fix restoration of topic headers. by @gundermanc in
[#24650](https://github.com/google-gemini/gemini-cli/pull/24650)
- feat(core): discourage update topic tool for simple tasks by @Samee24 in
[#24640](https://github.com/google-gemini/gemini-cli/pull/24640)
- fix(core): ensure global temp directory is always in sandbox allowed paths by
@galz10 in [#24638](https://github.com/google-gemini/gemini-cli/pull/24638)
- fix(core): detect uninitialized lines by @jacob314 in
[#24646](https://github.com/google-gemini/gemini-cli/pull/24646)
- docs: update sandboxing documentation and toolSandboxing settings by @galz10
in [#24655](https://github.com/google-gemini/gemini-cli/pull/24655)
- feat(cli): enhance tool confirmation UI and selection layout by @galz10 in
[#24376](https://github.com/google-gemini/gemini-cli/pull/24376)
- feat(acp): add support for `/about` command by @sripasg in
[#24649](https://github.com/google-gemini/gemini-cli/pull/24649)
- feat(cli): add role specific metrics to /stats by @cynthialong0-0 in
[#24659](https://github.com/google-gemini/gemini-cli/pull/24659)
- split context by @jacob314 in
[#24623](https://github.com/google-gemini/gemini-cli/pull/24623)
- fix(cli): remove -S from shebang to fix Windows and BSD execution by
@scidomino in [#24756](https://github.com/google-gemini/gemini-cli/pull/24756)
- Fix issue where topic headers can be posted back to back by @gundermanc in
[#24759](https://github.com/google-gemini/gemini-cli/pull/24759)
- fix(core): handle partial llm_request in BeforeModel hook override by
@krishdef7 in [#22326](https://github.com/google-gemini/gemini-cli/pull/22326)
- fix(ui): improve narration suppression and reduce flicker by @gundermanc in
[#24635](https://github.com/google-gemini/gemini-cli/pull/24635)
- fix(ui): fixed auth race condition causing logo to flicker by @devr0306 in
[#24652](https://github.com/google-gemini/gemini-cli/pull/24652)
- fix(browser): remove premature browser cleanup after subagent invocation by
@gsquared94 in
[#24753](https://github.com/google-gemini/gemini-cli/pull/24753)
- Revert "feat(core,cli): prioritize summary for topics (#24608)" by
@Abhijit-2592 in
[#24777](https://github.com/google-gemini/gemini-cli/pull/24777)
- relax tool sandboxing overrides for plan mode to match defaults. by
@DavidAPierce in
[#24762](https://github.com/google-gemini/gemini-cli/pull/24762)
- fix(cli): respect global environment variable allowlist by @scidomino in
[#24767](https://github.com/google-gemini/gemini-cli/pull/24767)
- fix(cli): ensure skills list outputs to stdout in non-interactive environments
by @spencer426 in
[#24566](https://github.com/google-gemini/gemini-cli/pull/24566)
- Add an eval for and fix unsafe cloning behavior. by @gundermanc in
[#24457](https://github.com/google-gemini/gemini-cli/pull/24457)
- fix(policy): allow complete_task in plan mode by @abhipatel12 in
[#24771](https://github.com/google-gemini/gemini-cli/pull/24771)
- feat(telemetry): add browser agent clearcut metrics by @gsquared94 in
[#24688](https://github.com/google-gemini/gemini-cli/pull/24688)
- feat(cli): support selective topic expansion and click-to-expand by
@Abhijit-2592 in
[#24793](https://github.com/google-gemini/gemini-cli/pull/24793)
- temporarily disable sandbox integration test on windows by @ehedlund in
[#24786](https://github.com/google-gemini/gemini-cli/pull/24786)
- Remove flakey test by @scidomino in
[#24837](https://github.com/google-gemini/gemini-cli/pull/24837)
- Alisa/approve button by @alisa-alisa in
[#24645](https://github.com/google-gemini/gemini-cli/pull/24645)
- feat(hooks): display hook system messages in UI by @mbleigh in
[#24616](https://github.com/google-gemini/gemini-cli/pull/24616)
- fix(core): propagate BeforeModel hook model override end-to-end by @krishdef7
in [#24784](https://github.com/google-gemini/gemini-cli/pull/24784)
- chore: fix formatting for behavioral eval skill reference file by @abhipatel12
in [#24846](https://github.com/google-gemini/gemini-cli/pull/24846)
- fix: use directory junctions on Windows for skill linking by @enjoykumawat in
[#24823](https://github.com/google-gemini/gemini-cli/pull/24823)
- fix(cli): prevent multiple banner increments on remount by @sehoon38 in
[#24843](https://github.com/google-gemini/gemini-cli/pull/24843)
- feat(acp): add /help command by @sripasg in
[#24839](https://github.com/google-gemini/gemini-cli/pull/24839)
- fix(core): remove tmux alternate buffer warning by @jackwotherspoon in
[#24852](https://github.com/google-gemini/gemini-cli/pull/24852)
- Improve sandbox error matching and caching by @DavidAPierce in
[#24550](https://github.com/google-gemini/gemini-cli/pull/24550)
- feat(core): add agent protocol UI types and experimental flag by @mbleigh in
[#24275](https://github.com/google-gemini/gemini-cli/pull/24275)
- feat(core): use experiment flags for default fetch timeouts by @yunaseoul in
[#24261](https://github.com/google-gemini/gemini-cli/pull/24261)
- Revert "fix(ui): improve narration suppression and reduce flicker (#2… by
@gundermanc in
[#25333](https://github.com/google-gemini/gemini-cli/pull/25333)
- feat(core): introduce decoupled ContextManager and Sidecar architecture by
@joshualitt in
[#24752](https://github.com/google-gemini/gemini-cli/pull/24752)
- docs(core): update generalist agent documentation by @abhipatel12 in
[#25325](https://github.com/google-gemini/gemini-cli/pull/25325)
- chore(mcp): check MCP error code over brittle string match by @jackwotherspoon
in [#25381](https://github.com/google-gemini/gemini-cli/pull/25381)
- feat(plan): update plan mode prompt to allow showing plan content by @ruomengz
in [#25058](https://github.com/google-gemini/gemini-cli/pull/25058)
- test(core): improve sandbox integration test coverage and fix OS-specific
failures by @ehedlund in
[#25307](https://github.com/google-gemini/gemini-cli/pull/25307)
- fix(core): use debug level for keychain fallback logging by @ehedlund in
[#25398](https://github.com/google-gemini/gemini-cli/pull/25398)
- feat(test): add a performance test in asian language by @cynthialong0-0 in
[#25392](https://github.com/google-gemini/gemini-cli/pull/25392)
- feat(cli): enable mouse clicking for cursor positioning in AskUser multi-line
answers by @Adib234 in
[#24630](https://github.com/google-gemini/gemini-cli/pull/24630)
- fix(core): detect kmscon terminal as supporting true color by @claygeo in
[#25282](https://github.com/google-gemini/gemini-cli/pull/25282)
- ci: add agent session drift check workflow by @adamfweidman in
[#25389](https://github.com/google-gemini/gemini-cli/pull/25389)
- use macos-latest-large runner where applicable. by @scidomino in
[#25413](https://github.com/google-gemini/gemini-cli/pull/25413)
- Changelog for v0.37.2 by @gemini-cli-robot in
[#25336](https://github.com/google-gemini/gemini-cli/pull/25336)
[#24857](https://github.com/google-gemini/gemini-cli/pull/24857)
- refactor(cli): remove duplication in interactive shell awaiting input hint by
@JayadityaGit in
[#24801](https://github.com/google-gemini/gemini-cli/pull/24801)
- refactor(core): make LegacyAgentSession dependencies optional by @mbleigh in
[#24287](https://github.com/google-gemini/gemini-cli/pull/24287)
- Changelog for v0.37.0-preview.2 by @gemini-cli-robot in
[#24848](https://github.com/google-gemini/gemini-cli/pull/24848)
- fix(cli): always show shell command description or actual command by @jacob314
in [#24774](https://github.com/google-gemini/gemini-cli/pull/24774)
- Added flag for ept size and increased default size by @devr0306 in
[#24859](https://github.com/google-gemini/gemini-cli/pull/24859)
- fix(core): dispose Scheduler to prevent McpProgress listener leak by
@Anjaligarhwal in
[#24870](https://github.com/google-gemini/gemini-cli/pull/24870)
- fix(cli): switch default back to terminalBuffer=false and fix regressions
introduced for that mode by @jacob314 in
[#24873](https://github.com/google-gemini/gemini-cli/pull/24873)
- feat(cli): switch to ctrl+g from ctrl-x by @jacob314 in
[#24861](https://github.com/google-gemini/gemini-cli/pull/24861)
- fix: isolate concurrent browser agent instances by @gsquared94 in
[#24794](https://github.com/google-gemini/gemini-cli/pull/24794)
- docs: update MCP server OAuth redirect port documentation by @adamfweidman in
[#24844](https://github.com/google-gemini/gemini-cli/pull/24844)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.38.0-preview.0...v0.40.0-preview.3
https://github.com/google-gemini/gemini-cli/compare/v0.37.0-preview.2...v0.38.0-preview.0
-143
View File
@@ -1,143 +0,0 @@
# Auto Memory
Auto Memory is an experimental feature that mines your past Gemini CLI sessions
in the background and turns recurring workflows into reusable
[Agent Skills](./skills.md). You review, accept, or discard each extracted skill
before it becomes available to future sessions.
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development.
## Overview
Every session you run with Gemini CLI is recorded locally as a transcript. Auto
Memory scans those transcripts for procedural patterns that recur across
sessions, then drafts each pattern as a `SKILL.md` file in a project-local
inbox. You inspect the draft, decide whether it captures real expertise, and
promote it to your global or workspace skills directory if you want it.
You'll use Auto Memory when you want to:
- **Capture team workflows** that you find yourself walking the agent through
more than once.
- **Codify hard-won fixes** for project-specific landmines so future sessions
avoid them.
- **Bootstrap a skills library** without writing every `SKILL.md` by hand.
Auto Memory complements—but does not replace—the
[`save_memory` tool](../tools/memory.md), which captures single facts into
`GEMINI.md`. Auto Memory captures multi-step procedures into skills.
## Prerequisites
- Gemini CLI installed and authenticated.
- At least 10 user messages across recent, idle sessions in the project. Auto
Memory ignores active or trivial sessions.
## How to enable Auto Memory
Auto Memory is off by default. Enable it in your settings file:
1. Open your global settings file at `~/.gemini/settings.json`. If you only
want Auto Memory in one project, edit `.gemini/settings.json` in that
project instead.
2. Add the experimental flag:
```json
{
"experimental": {
"autoMemory": true
}
}
```
3. Restart Gemini CLI. The flag requires a restart because the extraction
service starts during session boot.
## How Auto Memory works
Auto Memory runs as a background task on session startup. It does not block the
UI, consume your interactive turns, or surface tool prompts.
1. **Eligibility scan.** The service indexes recent sessions from
`~/.gemini/tmp/<project>/chats/`. Sessions are eligible only if they have
been idle for at least three hours and contain at least 10 user messages.
2. **Lock acquisition.** A lock file in the project's memory directory
coordinates across multiple CLI instances so extraction runs at most once at
a time.
3. **Sub-agent extraction.** A specialized sub-agent (named `confucius`)
reviews the session index, reads any sessions that look like they contain
repeated procedural workflows, and drafts new `SKILL.md` files. Its
instructions tell it to default to creating zero skills unless the evidence
is strong, so most runs produce no inbox items.
4. **Patch validation.** If the sub-agent proposes edits to skills outside the
inbox (for example, an existing global skill), it writes a unified diff
`.patch` file. Auto Memory dry-runs each patch and discards any that do not
apply cleanly.
5. **Notification.** When a run produces new skills or patches, Gemini CLI
surfaces an inline message telling you how many items are waiting.
## How to review extracted skills
Use the `/memory inbox` slash command to open the inbox dialog at any time:
**Command:** `/memory inbox`
The dialog lists each draft skill with its name, description, and source
sessions. From there you can:
- **Read** the full `SKILL.md` body before deciding.
- **Promote** a skill to your user (`~/.gemini/skills/`) or workspace
(`.gemini/skills/`) directory.
- **Discard** a skill you do not want.
- **Apply** or reject a `.patch` proposal against an existing skill.
Promoted skills become discoverable in the next session and follow the standard
[skill discovery precedence](./skills.md#skill-discovery-tiers).
## How to disable Auto Memory
To turn off background extraction, set the flag back to `false` in your settings
file and restart Gemini CLI:
```json
{
"experimental": {
"autoMemory": false
}
}
```
Disabling the flag stops the background service immediately on the next session
start. Existing inbox items remain on disk; you can either drain them with
`/memory inbox` first or remove the project memory directory manually.
## Data and privacy
- Auto Memory only reads session files that already exist locally on your
machine. Nothing is uploaded to Gemini outside the normal API calls the
extraction sub-agent makes during its run.
- The sub-agent is instructed to redact secrets, tokens, and credentials it
encounters and to never copy large tool outputs verbatim.
- Drafted skills live in your project's memory directory until you promote or
discard them. They are not automatically loaded into any session.
## Limitations
- The sub-agent runs on a preview Gemini Flash model. Extraction quality depends
on the model's ability to recognize durable patterns versus one-off incidents.
- Auto Memory does not extract skills from the current session. It only
considers sessions that have been idle for three hours or more.
- Inbox items are stored per project. Skills extracted in one workspace are not
visible from another until you promote them to the user-scope skills
directory.
## Next steps
- Learn how skills are discovered and activated in [Agent Skills](./skills.md).
- Explore the [memory management tutorial](./tutorials/memory-management.md) for
the complementary `save_memory` and `GEMINI.md` workflows.
- Review the experimental settings catalog in
[Settings](./settings.md#experimental).
-1
View File
@@ -52,7 +52,6 @@ These commands are available within the interactive REPL.
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
| `--worktree` | `-w` | string | - | Start Gemini in a new git worktree. If no name is provided, one is generated automatically. Requires `experimental.worktrees: true` in settings. |
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
| `--skip-trust` | - | boolean | `false` | Trust the current workspace for this session, skipping the folder trust check. |
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo`, `plan` |
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
+1 -1
View File
@@ -507,7 +507,7 @@ events. For more information, see the [telemetry documentation](./telemetry.md).
You can enforce a specific authentication method for all users by setting the
`security.auth.enforcedType` in the system-level `settings.json` file. This
prevents users from choosing a different authentication method. See the
[Authentication docs](../get-started/authentication.mdx) for more details.
[Authentication docs](../get-started/authentication.md) for more details.
**Example:** Enforce the use of Google login for all users.
+5 -15
View File
@@ -130,9 +130,7 @@ These are the only allowed tools:
[`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`) and core
[MCP resource tools](../tools/mcp-resources.md) (`list_mcp_resources`,
`read_mcp_resource`) are allowed.
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`
@@ -329,11 +327,8 @@ Storage whenever Gemini CLI exits Plan Mode to start the implementation.
```bash
#!/usr/bin/env bash
# Extract the plan filename from the tool input JSON
plan_filename=$(jq -r '.tool_input.plan_filename // empty')
# Construct the absolute path using the GEMINI_PLANS_DIR environment variable
plan_path="$GEMINI_PLANS_DIR/$plan_filename"
# Extract the plan path from the tool input JSON
plan_path=$(jq -r '.tool_input.plan_path // empty')
if [ -f "$plan_path" ]; then
# Generate a unique filename using a timestamp
@@ -359,7 +354,7 @@ To register this `AfterTool` hook, add it to your `settings.json`:
{
"name": "archive-plan",
"type": "command",
"command": "~/.gemini/hooks/archive-plan.sh"
"command": "./.gemini/hooks/archive-plan.sh"
}
]
}
@@ -446,10 +441,6 @@ on the current phase of your task:
switches to a high-speed **Flash** model. This provides a faster, more
responsive experience during the implementation of the plan.
If the high-reasoning model is unavailable or you don't have access to it,
Gemini CLI automatically and silently falls back to a faster model to ensure
your workflow isn't interrupted.
This behavior is enabled by default to provide the best balance of quality and
performance. You can disable this automatic switching in your settings:
@@ -470,8 +461,7 @@ 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 **Enable Session Cleanup** or **Keep chat history**) or in your
`settings.json` file. See
(search for **Session Retention**) or in your `settings.json` file. See
[session retention](../cli/session-management.md#session-retention) for more
details.
+48 -165
View File
@@ -31,53 +31,6 @@ The benefits of sandboxing include:
- **Safety**: Reduce risk when working with untrusted code or experimental
commands.
## Quickstart
You can enable sandboxing using a command flag, environment variable, or
configuration file.
### Using the command flag
```bash
gemini -s -p "analyze the code structure"
```
### Using an environment variable
**macOS/Linux**
```bash
export GEMINI_SANDBOX=true
gemini -p "run the test suite"
```
**Windows (PowerShell)**
```powershell
$env:GEMINI_SANDBOX="true"
gemini -p "run the test suite"
```
### Configuring via settings.json
```json
{
"tools": {
"sandbox": "docker"
}
}
```
## Configuration
Enable sandboxing using one of the following methods (in order of precedence):
1. **Command flag**: `-s` or `--sandbox`
2. **Environment variable**:
`GEMINI_SANDBOX=true|docker|podman|sandbox-exec|runsc|lxc`
3. **Settings file**: `"sandbox": true` in the `tools` object of your
`settings.json` file (for example, `{"tools": {"sandbox": true}}`).
## Sandboxing methods
Your ideal method of sandboxing may differ depending on your platform and your
@@ -90,92 +43,12 @@ Lightweight, built-in sandboxing using `sandbox-exec`.
**Default profile**: `permissive-open` - restricts writes outside project
directory but allows most other operations.
Built-in profiles (set via `SEATBELT_PROFILE` env var):
- `permissive-open` (default): Write restrictions, network allowed
- `permissive-proxied`: Write restrictions, network via proxy
- `restrictive-open`: Strict restrictions, network allowed
- `restrictive-proxied`: Strict restrictions, network via proxy
- `strict-open`: Read and write restrictions, network allowed
- `strict-proxied`: Read and write restrictions, network via proxy
### 2. Container-based (Docker/Podman)
Cross-platform sandboxing with complete process isolation using container
technology. By default, it uses the `ghcr.io/google/gemini-cli:latest` image.
Cross-platform sandboxing with complete process isolation.
**Prerequisites:**
- Docker or Podman must be installed and running on your system.
**How it works (Workspace directory):**
Inside the sandbox container, your current working directory is mounted at the
**exact same absolute path** as it is on your host machine. For example, if you
run the CLI from `/Users/you/project` on your host machine, the sandbox will
mount your local project folder and operate within `/Users/you/project` inside
the container. This allows the AI to seamlessly read and modify your project
files while remaining isolated from the rest of your system.
**Quick setup:**
To enable Docker sandboxing, run Gemini CLI with the sandbox flag and specify
Docker as the provider:
```bash
# Using the environment variable (Recommended)
export GEMINI_SANDBOX=docker
gemini -p "build the project"
# Or configure it permanently in your settings.json
# {"tools": {"sandbox": "docker"}}
```
**Customizing the Sandbox Image:**
If your project requires specific dependencies, you can specify a custom image
name or have Gemini CLI build one for you automatically. You can use any Docker
or Podman image as your sandbox, provided it has standard shell utilities (like
`bash`) available.
**Option A: Using an existing custom image (e.g., Artifact Registry)**
To configure a custom image that is hosted on a registry (or built locally),
update your `settings.json` to use an object for the sandbox configuration, or
set the `GEMINI_SANDBOX_IMAGE` environment variable.
_Example: Configuring via `settings.json`_
```json
{
"tools": {
"sandbox": {
"command": "docker",
"image": "us-central1-docker.pkg.dev/my-project/my-repo/my-custom-sandbox:latest"
}
}
}
```
_Example: Configuring via environment variable_
```bash
export GEMINI_SANDBOX_IMAGE="us-central1-docker.pkg.dev/my-project/my-repo/my-custom-sandbox:latest"
```
**Option B: Building a local custom image automatically**
If you prefer to define your environment as code, you can provide a Dockerfile
and Gemini CLI will build the image automatically.
1. Create a `.gemini/sandbox.Dockerfile` in your project root.
2. Ensure you have the `gh` CLI installed and authenticated (if you are using
the default `ghcr.io/google/gemini-cli` image as a base).
3. Run your command with the `BUILD_SANDBOX` environment variable set:
```bash
BUILD_SANDBOX=1 GEMINI_SANDBOX=docker gemini -p "run my custom build"
```
**Note**: Requires building the sandbox image locally or using a published image
from your organization's registry.
### 3. Windows Native Sandbox (Windows only)
@@ -315,49 +188,59 @@ This mechanism ensures you don't have to manually re-run commands with more
permissive sandbox settings, while still maintaining control over what the AI
can access.
### Including files outside the workspace
By default, the sandbox only has access to the current project workspace. If you
need the sandbox to have permission to operate on certain files or directories
from the local file system outside of the project workspace, you can mount them
using the `SANDBOX_MOUNTS` environment variable.
Provide a comma-separated list of mount definitions in the format
`from:to:opts`. If `to` is omitted, it defaults to the same path as `from`. If
`opts` is omitted, it defaults to `ro` (read-only). Note that the `from` path
must be an absolute path.
**Example**:
## Quickstart
```bash
export SANDBOX_MOUNTS="/path/on/host:/path/in/container:rw,/another/path:ro"
# Enable sandboxing with command flag
gemini -s -p "analyze the code structure"
```
## Running inside a Docker container
**Use environment variable**
If you are running Gemini CLI itself from within an official or custom Docker
container and want to enable sandboxing, you must share the host's Docker socket
and ensure your workspace paths align.
1. **Mount the Docker socket**: Map `/var/run/docker.sock` so the CLI can spawn
sibling sandbox containers via the host's Docker daemon.
2. **Align workspace paths**: The path to your workspace inside the container
must exactly match the absolute path on the host. Because the sandbox
container is spawned by the host's Docker daemon, it resolves volume mounts
against the host file system.
**Example**:
**macOS/Linux**
```bash
docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /absolute/path/on/host/project:/absolute/path/on/host/project \
-w /absolute/path/on/host/project \
-e GEMINI_SANDBOX=docker \
ghcr.io/google/gemini-cli:latest
export GEMINI_SANDBOX=true
gemini -p "run the test suite"
```
## Advanced settings
**Windows (PowerShell)**
```powershell
$env:GEMINI_SANDBOX="true"
gemini -p "run the test suite"
```
**Configure in settings.json**
```json
{
"tools": {
"sandbox": "docker"
}
}
```
## Configuration
### Enable sandboxing (in order of precedence)
1. **Command flag**: `-s` or `--sandbox`
2. **Environment variable**:
`GEMINI_SANDBOX=true|docker|podman|sandbox-exec|runsc|lxc`
3. **Settings file**: `"sandbox": true` in the `tools` object of your
`settings.json` file (for example, `{"tools": {"sandbox": true}}`).
### macOS Seatbelt profiles
Built-in profiles (set via `SEATBELT_PROFILE` env var):
- `permissive-open` (default): Write restrictions, network allowed
- `permissive-proxied`: Write restrictions, network via proxy
- `restrictive-open`: Strict restrictions, network allowed
- `restrictive-proxied`: Strict restrictions, network via proxy
- `strict-open`: Read and write restrictions, network allowed
- `strict-proxied`: Read and write restrictions, network via proxy
### Custom sandbox flags
@@ -396,7 +279,7 @@ export SANDBOX_FLAGS="--flag1 --flag2=value"
$env:SANDBOX_FLAGS="--flag1 --flag2=value"
```
### Linux UID/GID handling
## Linux UID/GID handling
The sandbox automatically handles user permissions on Linux. Override these
permissions with:
+25 -35
View File
@@ -24,22 +24,20 @@ they appear in the UI.
### General
| UI Label | Setting | Description | Default |
| ----------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. YOLO mode (auto-approve all actions) can only be enabled via command line (--yolo or --approval-mode=yolo). | `"default"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Terminal Notifications | `general.enableNotifications` | Enable terminal run-event notifications for action-required prompts and session completion. | `false` |
| Terminal Notification Method | `general.notificationMethod` | How to send terminal notifications. | `"auto"` |
| Enable Plan Mode | `general.plan.enabled` | Enable Plan Mode for read-only safety during planning. | `true` |
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. A custom directory requires a policy to allow write access in Plan Mode. | `undefined` |
| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` |
| Retry Fetch Errors | `general.retryFetchErrors` | Retry on "exception TypeError: fetch failed sending request" errors. | `true` |
| Max Chat Model Attempts | `general.maxAttempts` | Maximum number of attempts for requests to the main chat model. Cannot exceed 10. | `10` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `true` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` |
| Topic & Update Narration | `general.topicUpdateNarration` | Enable the Topic & Update communication model for reduced chattiness and structured progress reporting. | `true` |
| UI Label | Setting | Description | Default |
| ----------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. YOLO mode (auto-approve all actions) can only be enabled via command line (--yolo or --approval-mode=yolo). | `"default"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. | `false` |
| Enable Plan Mode | `general.plan.enabled` | Enable Plan Mode for read-only safety during planning. | `true` |
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. A custom directory requires a policy to allow write access in Plan Mode. | `undefined` |
| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` |
| Retry Fetch Errors | `general.retryFetchErrors` | Retry on "exception TypeError: fetch failed sending request" errors. | `true` |
| Max Chat Model Attempts | `general.maxAttempts` | Maximum number of attempts for requests to the main chat model. Cannot exceed 10. | `10` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `true` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` |
### Output
@@ -161,25 +159,17 @@ they appear in the UI.
### Experimental
| UI Label | Setting | Description | Default |
| ---------------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| Gemma Models | `experimental.gemma` | Enable access to Gemma 4 models (experimental). | `false` |
| Voice Mode | `experimental.voiceMode` | Enable experimental voice dictation and commands (/voice, /voice model). | `false` |
| Voice Activation Mode | `experimental.voice.activationMode` | How to trigger voice recording with the Space key. | `"push-to-talk"` |
| Voice Transcription Backend | `experimental.voice.backend` | The backend to use for voice transcription. | `"gemini-live"` |
| Whisper Model | `experimental.voice.whisperModel` | The Whisper model to use for local transcription. | `"ggml-base.en.bin"` |
| Voice Stop Grace Period (ms) | `experimental.voice.stopGracePeriodMs` | How long to wait for final transcription after stopping recording. | `1000` |
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
| 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` |
| 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` |
| Enable Gemma Model Router | `experimental.gemmaModelRouter.enabled` | Enable the Gemma Model Router (experimental). Requires a local endpoint serving Gemma via the Gemini API using LiteRT-LM shim. | `false` |
| Auto-start LiteRT Server | `experimental.gemmaModelRouter.autoStartServer` | Automatically start the LiteRT-LM server when Gemini CLI starts and the Gemma router is enabled. | `false` |
| Memory v2 | `experimental.memoryV2` | Disable the built-in save_memory tool and let the main agent persist project context by editing markdown files directly with edit/write_file. Route facts across four tiers: team-shared conventions go to project GEMINI.md files, project-specific personal notes go to the per-project private memory folder (MEMORY.md as index + sibling .md files for detail), and cross-project personal preferences go to the global ~/.gemini/GEMINI.md (the only file under ~/.gemini/ that the agent can edit — settings, credentials, etc. remain off-limits). Set to false to fall back to the legacy save_memory tool. | `true` |
| Auto Memory | `experimental.autoMemory` | Automatically extract reusable skills from past sessions in the background. Review results with /memory inbox. | `false` |
| Use the generalist profile to manage agent contexts. | `experimental.generalistProfile` | Suitable for general coding and software development tasks. | `false` |
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` |
| UI Label | Setting | Description | Default |
| ---------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
| 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` |
| 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` |
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
| Use the generalist profile to manage agent contexts. | `experimental.generalistProfile` | Suitable for general coding and software development tasks. | `false` |
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` |
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
### Skills
+6 -6
View File
@@ -24,7 +24,7 @@ project-specific behavior or create a customized persona.
You can set the environment variable temporarily in your shell, or persist it
via a `.gemini/.env` file. See
[Persisting Environment Variables](../get-started/authentication.mdx#persisting-environment-variables).
[Persisting Environment Variables](../get-started/authentication.md#persisting-environment-variables).
- Use the project default path (`.gemini/system.md`):
- `GEMINI_SYSTEM_MD=true` or `GEMINI_SYSTEM_MD=1`
@@ -51,7 +51,7 @@ error with: `missing system prompt file '<path>'`.
- Create `.gemini/system.md`, then add to `.gemini/.env`:
- `GEMINI_SYSTEM_MD=1`
- Use a custom file under your home directory:
- `GEMINI_SYSTEM_MD=~/prompts/system.md gemini`
- `GEMINI_SYSTEM_MD=~/prompts/SYSTEM.md gemini`
## UI indicator
@@ -102,17 +102,17 @@ safety and workflow rules.
This creates the file and writes the current builtin system prompt to it.
## Best practices: system.md vs GEMINI.md
## Best practices: SYSTEM.md vs GEMINI.md
- system.md (firmware):
- SYSTEM.md (firmware):
- Nonnegotiable operational rules: safety, tooluse protocols, approvals, and
mechanics that keep the CLI reliable.
- Stable across tasks and projects (or per project when needed).
- GEMINI.md (strategy):
- Persona, goals, methodologies, and project/domain context.
- Evolves per task; relies on system.md for safe execution.
- Evolves per task; relies on SYSTEM.md for safe execution.
Keep system.md minimal but complete for safety and tool operation. Keep
Keep SYSTEM.md minimal but complete for safety and tool operation. Keep
GEMINI.md focused on highlevel guidance and project specifics.
## Troubleshooting
+11 -18
View File
@@ -35,18 +35,17 @@ The observability system provides:
You control telemetry behavior through the `.gemini/settings.json` file.
Environment variables can override these settings.
| Setting | Environment Variable | Description | Values | Default |
| -------------- | --------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- |
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
| `traces` | `GEMINI_TELEMETRY_TRACES_ENABLED` | Enable detailed attribute tracing | `true`/`false` | `false` |
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
| `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` |
| - | `GEMINI_CLI_SURFACE` | Optional custom label for traffic reporting | string | - |
| Setting | Environment Variable | Description | Values | Default |
| -------------- | -------------------------------- | --------------------------------------------------- | ----------------- | ----------------------- |
| `enabled` | `GEMINI_TELEMETRY_ENABLED` | Enable or disable telemetry | `true`/`false` | `false` |
| `target` | `GEMINI_TELEMETRY_TARGET` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` |
| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | OTLP collector endpoint | URL string | `http://localhost:4317` |
| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` |
| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - |
| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | Include prompts in telemetry logs | `true`/`false` | `true` |
| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | Use external OTLP collector (advanced) | `true`/`false` | `false` |
| `useCliAuth` | `GEMINI_TELEMETRY_USE_CLI_AUTH` | Use CLI credentials for telemetry (GCP target only) | `true`/`false` | `false` |
| - | `GEMINI_CLI_SURFACE` | Optional custom label for traffic reporting | string | - |
**Note on boolean environment variables:** For boolean settings like `enabled`,
setting the environment variable to `true` or `1` enables the feature.
@@ -1236,12 +1235,6 @@ These metrics follow standard [OpenTelemetry GenAI semantic conventions].
Traces provide an "under-the-hood" view of agent and backend operations. Use
traces to debug tool interactions and optimize performance.
<!-- prettier-ignore -->
> [!NOTE]
> Detailed trace attributes (like full prompts and tool outputs) are disabled by default
> to minimize overhead. You must explicitly set `telemetry.traces` to `true` (or set
> `GEMINI_TELEMETRY_TRACES_ENABLED=true`) to capture them.
Every trace captures rich metadata via standard span attributes.
<details open>
-28
View File
@@ -100,34 +100,6 @@ protect you. In this mode, the following features are disabled:
Granting trust to a folder unlocks the full functionality of Gemini CLI for that
workspace.
## Headless and automated environments
When running Gemini CLI in a headless environment (for example, a CI/CD
pipeline) where interactive prompts are not possible, the trust dialog cannot be
displayed. If the folder is untrusted and the Folder Trust feature is enabled,
the CLI will throw a `FatalUntrustedWorkspaceError` and exit.
To proceed in these environments, you can bypass the trust check using one of
the following methods:
- **Command-line flag:** Run the CLI with the `--skip-trust` flag.
- **Environment variable:** Set the `GEMINI_CLI_TRUST_WORKSPACE=true`
environment variable.
These methods will trust the current workspace for the duration of the session
without prompting.
For detailed instructions on managing folder trust within CI/CD workflows,
review the
[Gemini CLI trust guidance for GitHub Actions](https://github.com/google-github-actions/run-gemini-cli/blob/main/docs/trust-guidance.md).
## Overriding the trust file location
By default, trust settings are saved to `~/.gemini/trustedFolders.json`. If you
need to store this file in a different location, you can set the
`GEMINI_CLI_TRUSTED_FOLDERS_PATH` environment variable to the desired absolute
file path.
## Managing your trust settings
If you need to change a decision or see all your settings, you have a couple of
-2
View File
@@ -124,5 +124,3 @@ immediately. Force a reload with:
- Explore the [Command reference](../../reference/commands.md) for more
`/memory` options.
- Read the technical spec for [Project context](../../cli/gemini-md.md).
- Try the experimental [Auto Memory](../auto-memory.md) feature to extract
reusable skills from your past sessions automatically.
+5 -17
View File
@@ -87,23 +87,11 @@ Gemini CLI comes with the following built-in subagents:
### Generalist Agent
- **Name:** `generalist`
- **Purpose:** A general, all-purpose subagent that uses the inherited tool
access and configurations from the main agent. Useful for executing broad,
resource-heavy subtasks in an isolated conversation, optimizing your main
agent's context by returning only the final result of that given task.
- **When to use:** Use this agent when a task requires many steps, handles large
volumes of information, or requires the same full capabilities as the main
agent. It is ideal for:
- **Multi-file modifications:** Applying refactors or fixing errors across
several files at once.
- **High-volume execution:** Running commands or tests that produce extensive
terminal output.
- **Action-oriented research:** Investigations where the agent needs to both
search code and run commands or make edits to find a solution. By delegating
these tasks, you prevent your main conversation from becoming cluttered or
slow. You can invoke it explicitly using `@generalist`.
- **Configuration:** Enabled by default.
- **Name:** `generalist_agent`
- **Purpose:** Route tasks to the appropriate specialized subagent.
- **When to use:** Implicitly used by the main agent for routing. Not directly
invoked by the user.
- **Configuration:** Enabled by default. No specific configuration options.
### Browser Agent (experimental)
@@ -1,11 +1,10 @@
import { Tabs, TabItem } from '@astrojs/starlight/components';
# Gemini CLI authentication setup
To use Gemini CLI, you'll need to authenticate with Google. This guide helps you
quickly find the best way to sign in based on your account type and how you're
using the CLI.
<!-- prettier-ignore -->
> [!TIP]
> Looking for a high-level comparison of all available subscriptions?
> To compare features and find the right quota for your needs, see our
@@ -24,7 +23,7 @@ Select the authentication method that matches your situation in the table below:
| Organization users with a company, school, or Google Workspace account | [Sign in with Google](#login-google) | [Yes](#set-gcp) |
| AI Studio user with a Gemini API key | [Use Gemini API Key](#gemini-api) | No |
| Google Cloud Vertex AI user | [Vertex AI](#vertex-ai) | [Yes](#set-gcp) |
| [Headless mode](#headless) | [Use Gemini API Key](#gemini-api) or<br /> [Vertex AI](#vertex-ai) | No (for Gemini API Key)<br /> [Yes](#set-gcp) (for Vertex AI) |
| [Headless mode](#headless) | [Use Gemini API Key](#gemini-api) or<br> [Vertex AI](#vertex-ai) | No (for Gemini API Key)<br> [Yes](#set-gcp) (for Vertex AI) |
### What is my Google account type?
@@ -85,24 +84,19 @@ To authenticate and use Gemini CLI with a Gemini API key:
2. Set the `GEMINI_API_KEY` environment variable to your key. For example:
<Tabs>
<TabItem label="macOS/Linux">
**macOS/Linux**
```bash
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
```
```bash
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**Windows (PowerShell)**
```powershell
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
$env:GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
```
</TabItem>
</Tabs>
```powershell
# Replace YOUR_GEMINI_API_KEY with the key from AI Studio
$env:GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
```
To make this setting persistent, see
[Persisting Environment Variables](#persisting-vars).
@@ -115,6 +109,7 @@ To authenticate and use Gemini CLI with a Gemini API key:
4. Select **Use Gemini API key**.
<!-- prettier-ignore -->
> [!WARNING]
> Treat API keys, especially for services like Gemini, as sensitive
> credentials. Protect them to prevent unauthorized access and potential misuse
@@ -136,26 +131,21 @@ or the location where you want to run your jobs.
For example:
<Tabs>
<TabItem label="macOS/Linux">
**macOS/Linux**
```bash
```bash
# Replace with your project ID and desired location (for example, us-central1)
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
```
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**Windows (PowerShell)**
```powershell
```powershell
# Replace with your project ID and desired location (for example, us-central1)
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
$env:GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"
```
</TabItem>
</Tabs>
```
To make any Vertex AI environment variable settings persistent, see
[Persisting Environment Variables](#persisting-vars).
@@ -167,22 +157,17 @@ Consider this authentication method if you have Google Cloud CLI installed.
If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you must unset
them to use ADC.
<Tabs>
<TabItem label="macOS/Linux">
**macOS/Linux**
```bash
```bash
unset GOOGLE_API_KEY GEMINI_API_KEY
```
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**Windows (PowerShell)**
```powershell
```powershell
Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
```
</TabItem>
</Tabs>
```
1. Verify you have a Google Cloud project and Vertex AI API is enabled.
@@ -210,22 +195,17 @@ pipelines, or if your organization restricts user-based ADC or API key creation.
If you have previously set `GOOGLE_API_KEY` or `GEMINI_API_KEY`, you must unset
them:
<Tabs>
<TabItem label="macOS/Linux">
**macOS/Linux**
```bash
```bash
unset GOOGLE_API_KEY GEMINI_API_KEY
```
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**Windows (PowerShell)**
```powershell
```powershell
Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
```
</TabItem>
</Tabs>
```
1. [Create a service account and key](https://cloud.google.com/iam/docs/keys-create-delete)
and download the provided JSON file. Assign the "Vertex AI User" role to the
@@ -234,24 +214,19 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
2. Set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to the JSON
file's absolute path. For example:
<Tabs>
<TabItem label="macOS/Linux">
**macOS/Linux**
```bash
# Replace /path/to/your/keyfile.json with the actual path
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/keyfile.json"
```
```bash
# Replace /path/to/your/keyfile.json with the actual path
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/keyfile.json"
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**Windows (PowerShell)**
```powershell
# Replace C:\path\to\your\keyfile.json with the actual path
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\path\to\your\keyfile.json"
```
</TabItem>
</Tabs>
```powershell
# Replace C:\path\to\your\keyfile.json with the actual path
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\path\to\your\keyfile.json"
```
3. [Configure your Google Cloud Project](#set-gcp).
@@ -263,6 +238,7 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
5. Select **Vertex AI**.
<!-- prettier-ignore -->
> [!WARNING]
> Protect your service account key file as it gives access to
> your resources.
@@ -274,24 +250,19 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
2. Set the `GOOGLE_API_KEY` environment variable:
<Tabs>
<TabItem label="macOS/Linux">
**macOS/Linux**
```bash
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
```bash
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**Windows (PowerShell)**
```powershell
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
$env:GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
</TabItem>
</Tabs>
```powershell
# Replace YOUR_GOOGLE_API_KEY with your Vertex AI API key
$env:GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
```
If you see errors like `"API keys are not supported by this API..."`, your
organization might restrict API key usage for this service. Try the other
@@ -309,6 +280,7 @@ Remove-Item Env:\GOOGLE_API_KEY, Env:\GEMINI_API_KEY -ErrorAction Ignore
## Set your Google Cloud project <a id="set-gcp"></a>
<!-- prettier-ignore -->
> [!IMPORTANT]
> Most individual Google accounts (free and paid) don't require a
> Google Cloud project for authentication.
@@ -336,24 +308,19 @@ To configure Gemini CLI to use a Google Cloud project, do the following:
For example, to set the `GOOGLE_CLOUD_PROJECT_ID` variable:
<Tabs>
<TabItem label="macOS/Linux">
**macOS/Linux**
```bash
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
```
```bash
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**Windows (PowerShell)**
```powershell
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
```
</TabItem>
</Tabs>
```powershell
# Replace YOUR_PROJECT_ID with your actual Google Cloud project ID
$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
```
To make this setting persistent, see
[Persisting Environment Variables](#persisting-vars).
@@ -366,29 +333,21 @@ persist them with the following methods:
1. **Add your environment variables to your shell configuration file:** Append
the environment variable commands to your shell's startup file.
<Tabs>
<TabItem label="macOS/Linux">
**macOS/Linux** (for example, `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
(for example, `~/.bashrc`, `~/.zshrc`, or `~/.profile`):
```bash
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
source ~/.bashrc
```
```bash
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
source ~/.bashrc
```
**Windows (PowerShell)** (for example, `$PROFILE`):
</TabItem>
<TabItem label="Windows (PowerShell)">
(for example, `$PROFILE`):
```powershell
Add-Content -Path $PROFILE -Value '$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"'
. $PROFILE
```
</TabItem>
</Tabs>
```powershell
Add-Content -Path $PROFILE -Value '$env:GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"'
. $PROFILE
```
<!-- prettier-ignore -->
> [!WARNING]
> Be aware that when you export API keys or service account
> paths in your shell configuration file, any process launched from that
@@ -402,30 +361,25 @@ persist them with the following methods:
Example for user-wide settings:
<Tabs>
<TabItem label="macOS/Linux">
**macOS/Linux**
```bash
mkdir -p ~/.gemini
cat >> ~/.gemini/.env <<'EOF'
GOOGLE_CLOUD_PROJECT="your-project-id"
# Add other variables like GEMINI_API_KEY as needed
EOF
```
```bash
mkdir -p ~/.gemini
cat >> ~/.gemini/.env <<'EOF'
GOOGLE_CLOUD_PROJECT="your-project-id"
# Add other variables like GEMINI_API_KEY as needed
EOF
```
</TabItem>
<TabItem label="Windows (PowerShell)">
**Windows (PowerShell)**
```powershell
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini"
@"
GOOGLE_CLOUD_PROJECT="your-project-id"
# Add other variables like GEMINI_API_KEY as needed
"@ | Out-File -FilePath "$env:USERPROFILE\.gemini\.env" -Encoding utf8 -Append
```
</TabItem>
</Tabs>
```powershell
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini"
@"
GOOGLE_CLOUD_PROJECT="your-project-id"
# Add other variables like GEMINI_API_KEY as needed
"@ | Out-File -FilePath "$env:USERPROFILE\.gemini\.env" -Encoding utf8 -Append
```
Variables are loaded from the first file found, not merged.
+2 -3
View File
@@ -24,8 +24,7 @@ Once Gemini CLI is installed, run Gemini CLI from your command line:
gemini
```
For more installation options, see
[Gemini CLI Installation](./installation.mdx).
For more installation options, see [Gemini CLI Installation](./installation.md).
## Authenticate
@@ -47,7 +46,7 @@ cases, you can log in with your existing Google account:
Certain account types may require you to configure a Google Cloud project. For
more information, including other authentication methods, see
[Gemini CLI Authentication Setup](./authentication.mdx).
[Gemini CLI Authentication Setup](./authentication.md).
## Configure
+181
View File
@@ -0,0 +1,181 @@
# Gemini CLI installation, execution, and releases
This document provides an overview of Gemini CLI's system requirements,
installation methods, and release types.
## Recommended system specifications
- **Operating System:**
- macOS 15+
- Windows 11 24H2+
- Ubuntu 20.04+
- **Hardware:**
- "Casual" usage: 4GB+ RAM (short sessions, common tasks and edits)
- "Power" usage: 16GB+ RAM (long sessions, large codebases, deep context)
- **Runtime:** Node.js 20.0.0+
- **Shell:** Bash, Zsh, or PowerShell
- **Location:**
[Gemini Code Assist supported locations](https://developers.google.com/gemini-code-assist/resources/available-locations#americas)
- **Internet connection required**
## Install Gemini CLI
We recommend most users install Gemini CLI using one of the following
installation methods:
- npm
- Homebrew
- MacPorts
- Anaconda
Note that Gemini CLI comes pre-installed on
[**Cloud Shell**](https://docs.cloud.google.com/shell/docs) and
[**Cloud Workstations**](https://cloud.google.com/workstations).
### Install globally with npm
```bash
npm install -g @google/gemini-cli
```
### Install globally with Homebrew (macOS/Linux)
```bash
brew install gemini-cli
```
### Install globally with MacPorts (macOS)
```bash
sudo port install gemini-cli
```
### Install with Anaconda (for restricted environments)
```bash
# Create and activate a new environment
conda create -y -n gemini_env -c conda-forge nodejs
conda activate gemini_env
# Install Gemini CLI globally via npm (inside the environment)
npm install -g @google/gemini-cli
```
## Run Gemini CLI
For most users, we recommend running Gemini CLI with the `gemini` command:
```bash
gemini
```
For a list of options and additional commands, see the
[CLI cheatsheet](../cli/cli-reference.md).
You can also run Gemini CLI using one of the following advanced methods:
- Run instantly with npx. You can run Gemini CLI without permanent installation.
- In a sandbox. This method offers increased security and isolation.
- From the source. This is recommended for contributors to the project.
### Run instantly with npx
```bash
# Using npx (no installation required)
npx @google/gemini-cli
```
You can also execute the CLI directly from the main branch on GitHub, which is
helpful for testing features still in development:
```bash
npx https://github.com/google-gemini/gemini-cli
```
### Run in a sandbox (Docker/Podman)
For security and isolation, Gemini CLI can be run inside a container. This is
the default way that the CLI executes tools that might have side effects.
- **Directly from the registry:** You can run the published sandbox image
directly. This is useful for environments where you only have Docker and want
to run the CLI.
```bash
# Run the published sandbox image
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.1
```
- **Using the `--sandbox` flag:** If you have Gemini CLI installed locally
(using the standard installation described above), you can instruct it to run
inside the sandbox container.
```bash
gemini --sandbox -y -p "your prompt here"
```
### Run from source (recommended for Gemini CLI contributors)
Contributors to the project will want to run the CLI directly from the source
code.
- **Development mode:** This method provides hot-reloading and is useful for
active development.
```bash
# From the root of the repository
npm run start
```
- **Production mode (React optimizations):** This method runs the CLI with React
production mode enabled, which is useful for testing performance without
development overhead.
```bash
# From the root of the repository
npm run start:prod
```
- **Production-like mode (linked package):** This method simulates a global
installation by linking your local package. It's useful for testing a local
build in a production workflow.
```bash
# Link the local cli package to your global node_modules
npm link packages/cli
# Now you can run your local version using the `gemini` command
gemini
```
## Releases
Gemini CLI has three release channels: nightly, preview, and stable. For most
users, we recommend the stable release, which is the default installation.
### Stable
New stable releases are published each week. The stable release is the promotion
of last week's `preview` release along with any bug fixes. The stable release
uses `latest` tag, but omitting the tag also installs the latest stable release
by default:
```bash
# Both commands install the latest stable release.
npm install -g @google/gemini-cli
npm install -g @google/gemini-cli@latest
```
### Preview
New preview releases will be published each week. These releases are not fully
vetted and may contain regressions or other outstanding issues. Try out the
preview release by using the `preview` tag:
```bash
npm install -g @google/gemini-cli@preview
```
### Nightly
Nightly releases are published every day. The nightly release includes all
changes from the main branch at time of release. It should be assumed there are
pending validations and issues. You can help test the latest changes by
installing with the `nightly` tag:
```bash
npm install -g @google/gemini-cli@nightly
```
-201
View File
@@ -1,201 +0,0 @@
import { Tabs, TabItem } from '@astrojs/starlight/components';
# Gemini CLI installation, execution, and releases
This document provides an overview of Gemini CLI's system requirements,
installation methods, and release types.
## Recommended system specifications
- **Operating System:**
- macOS 15+
- Windows 11 24H2+
- Ubuntu 20.04+
- **Hardware:**
- "Casual" usage: 4GB+ RAM (short sessions, common tasks and edits)
- "Power" usage: 16GB+ RAM (long sessions, large codebases, deep context)
- **Runtime:** Node.js 20.0.0+
- **Shell:** Bash, Zsh, or PowerShell
- **Location:**
[Gemini Code Assist supported locations](https://developers.google.com/gemini-code-assist/resources/available-locations#americas)
- **Internet connection required**
## Install Gemini CLI
We recommend most users install Gemini CLI using one of the following
installation methods. Note that Gemini CLI comes pre-installed on
[**Cloud Shell**](https://docs.cloud.google.com/shell/docs) and
[**Cloud Workstations**](https://cloud.google.com/workstations).
<Tabs>
<TabItem label="npm">
Install globally with npm:
```bash
npm install -g @google/gemini-cli
```
</TabItem>
<TabItem label="Homebrew (macOS/Linux)">
Install globally with Homebrew:
```bash
brew install gemini-cli
```
</TabItem>
<TabItem label="MacPorts (macOS)">
Install globally with MacPorts:
```bash
sudo port install gemini-cli
```
</TabItem>
<TabItem label="Anaconda">
Install with Anaconda (for restricted environments):
```bash
# Create and activate a new environment
conda create -y -n gemini_env -c conda-forge nodejs
conda activate gemini_env
# Install Gemini CLI globally via npm (inside the environment)
npm install -g @google/gemini-cli
```
</TabItem>
</Tabs>
## Run Gemini CLI
For most users, we recommend running Gemini CLI with the `gemini` command:
```bash
gemini
```
For a list of options and additional commands, see the
[CLI cheatsheet](../cli/cli-reference.md).
You can also run Gemini CLI using one of the following advanced methods:
<Tabs>
<TabItem label="npx">
Run instantly with npx. You can run Gemini CLI without permanent installation.
```bash
# Using npx (no installation required)
npx @google/gemini-cli
```
You can also execute the CLI directly from the main branch on GitHub, which is
helpful for testing features still in development:
```bash
npx https://github.com/google-gemini/gemini-cli
```
</TabItem>
<TabItem label="Docker/Podman Sandbox">
For security and isolation, Gemini CLI can be run inside a container. This is
the default way that the CLI executes tools that might have side effects.
- **Directly from the registry:** You can run the published sandbox image
directly. This is useful for environments where you only have Docker and want
to run the CLI.
```bash
# Run the published sandbox image
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.1
```
- **Using the `--sandbox` flag:** If you have Gemini CLI installed locally
(using the standard installation described above), you can instruct it to run
inside the sandbox container.
```bash
gemini --sandbox -y -p "your prompt here"
```
</TabItem>
<TabItem label="From source">
Contributors to the project will want to run the CLI directly from the source
code.
- **Development mode:** This method provides hot-reloading and is useful for
active development.
```bash
# From the root of the repository
npm run start
```
- **Production mode (React optimizations):** This method runs the CLI with React
production mode enabled, which is useful for testing performance without
development overhead.
```bash
# From the root of the repository
npm run start:prod
```
- **Production-like mode (linked package):** This method simulates a global
installation by linking your local package. It's useful for testing a local
build in a production workflow.
```bash
# Link the local cli package to your global node_modules
npm link packages/cli
# Now you can run your local version using the `gemini` command
gemini
```
</TabItem>
</Tabs>
## Releases
Gemini CLI has three release channels: stable, preview, and nightly. For most
users, we recommend the stable release, which is the default installation.
<Tabs>
<TabItem label="Stable">
Stable releases are published each week. A stable release is created from the
previous week's preview release along with any bug fixes. The stable release
uses the `latest` tag. Omitting the tag also installs the latest stable
release by default.
```bash
# Both commands install the latest stable release.
npm install -g @google/gemini-cli
npm install -g @google/gemini-cli@latest
```
</TabItem>
<TabItem label="Preview">
New preview releases will be published each week. These releases are not fully
vetted and may contain regressions or other outstanding issues. Try out the
preview release by using the `preview` tag:
```bash
npm install -g @google/gemini-cli@preview
```
</TabItem>
<TabItem label="Nightly">
Nightly releases are published every day. The nightly release includes all
changes from the main branch at time of release. It should be assumed there are
pending validations and issues. You can help test the latest changes by
installing with the `nightly` tag:
```bash
npm install -g @google/gemini-cli@nightly
```
</TabItem>
</Tabs>
-1
View File
@@ -138,7 +138,6 @@ multiple layers in the following order of precedence (highest to lowest):
Hooks are executed with a sanitized environment.
- `GEMINI_PROJECT_DIR`: The absolute path to the project root.
- `GEMINI_PLANS_DIR`: The absolute path to the plans directory.
- `GEMINI_SESSION_ID`: The unique ID for the current session.
- `GEMINI_CWD`: The current working directory.
- `CLAUDE_PROJECT_DIR`: (Alias) Provided for compatibility.
+2 -2
View File
@@ -15,9 +15,9 @@ npm install -g @google/gemini-cli
Jump in to Gemini CLI.
- **[Quickstart](./get-started/index.md):** Your first session with Gemini CLI.
- **[Installation](./get-started/installation.mdx):** How to install Gemini CLI
- **[Installation](./get-started/installation.md):** How to install Gemini CLI
on your system.
- **[Authentication](./get-started/authentication.mdx):** Setup instructions for
- **[Authentication](./get-started/authentication.md):** Setup instructions for
personal and enterprise accounts.
- **[CLI cheatsheet](./cli/cli-reference.md):** A quick reference for common
commands and options.
+19 -186
View File
@@ -134,15 +134,10 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **`general.enableNotifications`** (boolean):
- **Description:** Enable terminal run-event notifications for action-required
prompts and session completion.
- **Description:** Enable run-event notifications for action-required prompts
and session completion.
- **Default:** `false`
- **`general.notificationMethod`** (enum):
- **Description:** How to send terminal notifications.
- **Default:** `"auto"`
- **Values:** `"auto"`, `"osc9"`, `"osc777"`, `"bell"`
- **`general.checkpointing.enabled`** (boolean):
- **Description:** Enable session checkpointing for recovery
- **Default:** `false`
@@ -198,11 +193,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Minimum retention period (safety limit, defaults to "1d")
- **Default:** `"1d"`
- **`general.topicUpdateNarration`** (boolean):
- **Description:** Enable the Topic & Update communication model for reduced
chattiness and structured progress reporting.
- **Default:** `true`
#### `output`
- **`output.format`** (enum):
@@ -436,20 +426,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `"ask"`
- **Values:** `"ask"`, `"always"`, `"never"`
- **`billing.vertexAi.requestType`** (enum):
- **Description:** Sets the X-Vertex-AI-LLM-Request-Type header for Vertex AI
requests.
- **Default:** `undefined`
- **Values:** `"dedicated"`, `"shared"`
- **Requires restart:** Yes
- **`billing.vertexAi.sharedRequestType`** (enum):
- **Description:** Sets the X-Vertex-AI-LLM-Shared-Request-Type header for
Vertex AI requests.
- **Default:** `undefined`
- **Values:** `"priority"`, `"flex"`
- **Requires restart:** Yes
#### `model`
- **`model.name`** (string):
@@ -563,18 +539,6 @@ their corresponding top-level category object in your `settings.json` file.
"model": "gemini-2.5-flash-lite"
}
},
"gemma-4-31b-it": {
"extends": "chat-base-3",
"modelConfig": {
"model": "gemma-4-31b-it"
}
},
"gemma-4-26b-a4b-it": {
"extends": "chat-base-3",
"modelConfig": {
"model": "gemma-4-26b-a4b-it"
}
},
"gemini-2.5-flash-base": {
"extends": "base",
"modelConfig": {
@@ -846,28 +810,6 @@ their corresponding top-level category object in your `settings.json` file.
"multimodalToolUse": false
}
},
"gemma-4-31b-it": {
"displayName": "gemma-4-31b-it",
"tier": "custom",
"family": "gemma-4",
"isPreview": false,
"isVisible": true,
"features": {
"thinking": true,
"multimodalToolUse": false
}
},
"gemma-4-26b-a4b-it": {
"displayName": "gemma-4-26b-a4b-it",
"tier": "custom",
"family": "gemma-4",
"isPreview": false,
"isVisible": true,
"features": {
"thinking": true,
"multimodalToolUse": false
}
},
"auto": {
"tier": "auto",
"isPreview": true,
@@ -938,12 +880,6 @@ their corresponding top-level category object in your `settings.json` file.
```json
{
"gemma-4-31b-it": {
"default": "gemma-4-31b-it"
},
"gemma-4-26b-a4b-it": {
"default": "gemma-4-26b-a4b-it"
},
"gemini-3.1-pro-preview": {
"default": "gemini-3.1-pro-preview",
"contexts": [
@@ -1191,7 +1127,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "sticky_retry",
"transient": "terminal",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1207,7 +1143,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "sticky_retry",
"transient": "terminal",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1224,7 +1160,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "sticky_retry",
"transient": "terminal",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1240,7 +1176,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "sticky_retry",
"transient": "terminal",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1257,7 +1193,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "sticky_retry",
"transient": "terminal",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1272,7 +1208,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "sticky_retry",
"transient": "terminal",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1288,7 +1224,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"stateTransitions": {
"terminal": "terminal",
"transient": "sticky_retry",
"transient": "terminal",
"not_found": "terminal",
"unknown": "terminal"
}
@@ -1413,12 +1349,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **Requires restart:** Yes
- **`context.fileFiltering.enableFileWatcher`** (boolean):
- **Description:** Enable file watcher updates for @ file suggestions
(experimental).
- **Default:** `false`
- **Requires restart:** Yes
- **`context.fileFiltering.enableRecursiveFileSearch`** (boolean):
- **Description:** Enable recursive file search functionality when completing
@ references in the prompt.
@@ -1507,12 +1437,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`tools.confirmationRequired`** (array):
- **Description:** Tool names that always require user confirmation. Takes
precedence over allowed tools and core tool allowlists.
- **Default:** `undefined`
- **Requires restart:** Yes
- **`tools.exclude`** (array):
- **Description:** Tool names to exclude from discovery.
- **Default:** `undefined`
@@ -1686,37 +1610,6 @@ their corresponding top-level category object in your `settings.json` file.
#### `experimental`
- **`experimental.gemma`** (boolean):
- **Description:** Enable access to Gemma 4 models (experimental).
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.voiceMode`** (boolean):
- **Description:** Enable experimental voice dictation and commands (/voice,
/voice model).
- **Default:** `false`
- **`experimental.voice.activationMode`** (enum):
- **Description:** How to trigger voice recording with the Space key.
- **Default:** `"push-to-talk"`
- **Values:** `"push-to-talk"`, `"toggle"`
- **`experimental.voice.backend`** (enum):
- **Description:** The backend to use for voice transcription.
- **Default:** `"gemini-live"`
- **Values:** `"gemini-live"`, `"whisper"`
- **`experimental.voice.whisperModel`** (enum):
- **Description:** The Whisper model to use for local transcription.
- **Default:** `"ggml-base.en.bin"`
- **Values:** `"ggml-tiny.en.bin"`, `"ggml-base.en.bin"`,
`"ggml-large-v3-turbo-q5_0.bin"`, `"ggml-large-v3-turbo-q8_0.bin"`
- **`experimental.voice.stopGracePeriodMs`** (number):
- **Description:** How long to wait for final transcription after stopping
recording.
- **Default:** `1000`
- **`experimental.adk.agentSessionNoninteractiveEnabled`** (boolean):
- **Description:** Enable non-interactive agent sessions.
- **Default:** `false`
@@ -1765,10 +1658,8 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes
- **`experimental.jitContext`** (boolean):
- **Description:** Enable Just-In-Time (JIT) context loading. Defaults to
true; set to false to opt out and load all GEMINI.md files into the system
instruction up-front.
- **Default:** `true`
- **Description:** Enable Just-In-Time (JIT) context loading.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.useOSC52Paste`** (boolean):
@@ -1810,18 +1701,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.gemmaModelRouter.autoStartServer`** (boolean):
- **Description:** Automatically start the LiteRT-LM server when Gemini CLI
starts and the Gemma router is enabled.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.gemmaModelRouter.binaryPath`** (string):
- **Description:** Custom path to the LiteRT-LM binary. Leave empty to use the
default location (~/.gemini/bin/litert/).
- **Default:** `""`
- **Requires restart:** Yes
- **`experimental.gemmaModelRouter.classifier.host`** (string):
- **Description:** The host of the classifier.
- **Default:** `"http://localhost:9379"`
@@ -1833,28 +1712,10 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `"gemma3-1b-gpu-custom"`
- **Requires restart:** Yes
- **`experimental.memoryV2`** (boolean):
- **Description:** Disable the built-in save_memory tool and let the main
agent persist project context by editing markdown files directly with
edit/write_file. Route facts across four tiers: team-shared conventions go
to project GEMINI.md files, project-specific personal notes go to the
per-project private memory folder (MEMORY.md as index + sibling .md files
for detail), and cross-project personal preferences go to the global
~/.gemini/GEMINI.md (the only file under ~/.gemini/ that the agent can edit
— settings, credentials, etc. remain off-limits). Set to false to fall back
to the legacy save_memory tool.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.stressTestProfile`** (boolean):
- **Description:** Significantly lowers token limits to force early garbage
collection and distillation for testing purposes.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.autoMemory`** (boolean):
- **Description:** Automatically extract reusable skills from past sessions in
the background. Review results with /memory inbox.
- **`experimental.memoryManager`** (boolean):
- **Description:** Replace the built-in save_memory tool with a memory manager
subagent that supports adding, removing, de-duplicating, and organizing
memories.
- **Default:** `false`
- **Requires restart:** Yes
@@ -1869,7 +1730,8 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes
- **`experimental.topicUpdateNarration`** (boolean):
- **Description:** Deprecated: Use general.topicUpdateNarration instead.
- **Description:** Enable the experimental Topic & Update communication model
for reduced chattiness and structured progress reporting.
- **Default:** `false`
#### `skills`
@@ -2109,8 +1971,6 @@ see [Telemetry](../cli/telemetry.md).
- **Properties:**
- **`enabled`** (boolean): Whether or not telemetry is enabled.
- **`traces`** (boolean): Whether detailed traces with large attributes (like
tool outputs and file reads) are captured. Defaults to `false`.
- **`target`** (string): The destination for collected telemetry. Supported
values are `local` and `gcp`.
- **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter.
@@ -2210,7 +2070,7 @@ within your user's home folder.
Environment variables are a common way to configure applications, especially for
sensitive information like API keys or for settings that might change between
environments. For authentication setup, see the
[Authentication documentation](../get-started/authentication.mdx) which covers
[Authentication documentation](../get-started/authentication.md) which covers
all available authentication methods.
The CLI automatically loads environment variables from an `.env` file. The
@@ -2231,7 +2091,7 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- **`GEMINI_API_KEY`**:
- Your API key for the Gemini API.
- One of several available
[authentication methods](../get-started/authentication.mdx).
[authentication methods](../get-started/authentication.md).
- Set this in your shell profile (for example, `~/.bashrc`, `~/.zshrc`) or an
`.env` file.
- **`GEMINI_MODEL`**:
@@ -2239,14 +2099,6 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- Overrides the hardcoded default
- Example: `export GEMINI_MODEL="gemini-3-flash-preview"` (Windows PowerShell:
`$env:GEMINI_MODEL="gemini-3-flash-preview"`)
- **`GEMINI_CLI_TRUST_WORKSPACE`**:
- If set to `"true"`, trusts the current workspace for the duration of the
session, bypassing the folder trust check.
- Useful for headless environments (for example, CI/CD pipelines).
- **`GEMINI_CLI_TRUSTED_FOLDERS_PATH`**:
- Overrides the default location for the `trustedFolders.json` file.
- Useful if you want to store this configuration in a custom location instead
of the default `~/.gemini/`.
- **`GEMINI_CLI_IDE_PID`**:
- Manually specifies the PID of the IDE process to use for integration. This
is useful when running Gemini CLI in a standalone terminal while still
@@ -2296,21 +2148,6 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- When set, overrides the default API version used by the SDK.
- Example: `export GOOGLE_GENAI_API_VERSION="v1"` (Windows PowerShell:
`$env:GOOGLE_GENAI_API_VERSION="v1"`)
- **`GOOGLE_GEMINI_BASE_URL`**:
- Overrides the default base URL for Gemini API requests (when using
`gemini-api-key` authentication).
- Must be a valid URL. For security, it must use HTTPS unless pointing to
`localhost` (or `127.0.0.1` / `[::1]`).
- Example: `export GOOGLE_GEMINI_BASE_URL="https://my-proxy.com"` (Windows
PowerShell: `$env:GOOGLE_GEMINI_BASE_URL="https://my-proxy.com"`)
- **`GOOGLE_VERTEX_BASE_URL`**:
- Overrides the default base URL for Vertex AI API requests (when using
`vertex-ai` authentication).
- Must be a valid URL. For security, it must use HTTPS unless pointing to
`localhost` (or `127.0.0.1` / `[::1]`).
- Example: `export GOOGLE_VERTEX_BASE_URL="https://my-vertex-proxy.com"`
(Windows PowerShell:
`$env:GOOGLE_VERTEX_BASE_URL="https://my-vertex-proxy.com"`)
- **`OTLP_GOOGLE_CLOUD_PROJECT`**:
- Your Google Cloud Project ID for Telemetry in Google Cloud
- Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"` (Windows
@@ -2319,10 +2156,6 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
- Set to `true` or `1` to enable telemetry. Any other value is treated as
disabling it.
- Overrides the `telemetry.enabled` setting.
- **`GEMINI_TELEMETRY_TRACES_ENABLED`**:
- Set to `true` or `1` to enable detailed tracing with large attributes. Any
other value is treated as disabling it.
- Overrides the `telemetry.traces` setting.
- **`GEMINI_TELEMETRY_TARGET`**:
- Sets the telemetry target (`local` or `gcp`).
- Overrides the `telemetry.target` setting.
-1
View File
@@ -115,7 +115,6 @@ available combinations.
| `app.restart` | Restart the application. | `R`<br />`Shift+R` |
| `app.suspend` | Suspend the CLI and move it to the background. | `Ctrl+Z` |
| `app.showShellUnfocusWarning` | Show warning when trying to move focus away from shell input. | `Tab` |
| `app.voiceModePTT` | Hold to speak in Voice Mode. | `Space` |
#### Background Shell Controls
+12 -18
View File
@@ -120,12 +120,6 @@ There are three possible decisions a rule can enforce:
### Priority system and tiers
> [!WARNING] The **Workspace** tier (project-level policies) is currently
> non-functional. Defining policies in a workspace's `.gemini/policies`
> directory will not have any effect. See
> [issue #18186](https://github.com/google-gemini/gemini-cli/issues/18186). Use
> User or Admin policies instead.
The policy engine uses a sophisticated priority system to resolve conflicts when
multiple rules match a single tool call. The core principle is simple: **the
rule with the highest priority wins**.
@@ -133,13 +127,13 @@ rule with the highest priority wins**.
To provide a clear hierarchy, policies are organized into three tiers. Each tier
has a designated number that forms the base of the final priority calculation.
| Tier | Base | Description |
| :-------- | :--- | :-------------------------------------------------------------------------------------------- |
| Default | 1 | Built-in policies that ship with Gemini CLI. |
| Extension | 2 | Policies defined in extensions. |
| Workspace | 3 | **(Currently disabled)** Policies defined in the current workspace's configuration directory. |
| User | 4 | Custom policies defined by the user. |
| Admin | 5 | Policies managed by an administrator (for example, in an enterprise environment). |
| Tier | Base | Description |
| :-------- | :--- | :-------------------------------------------------------------------------------- |
| Default | 1 | Built-in policies that ship with Gemini CLI. |
| Extension | 2 | Policies defined in extensions. |
| Workspace | 3 | Policies defined in the current workspace's configuration directory. |
| User | 4 | Custom policies defined by the user. |
| Admin | 5 | Policies managed by an administrator (for example, in an enterprise environment). |
Within a TOML policy file, you assign a priority value from **0 to 999**. The
engine transforms this into a final priority using the following formula:
@@ -220,11 +214,11 @@ User, and (if configured) Admin directories.
### Policy locations
| Tier | Type | Location |
| :------------ | :----- | :------------------------------------------------------- |
| **User** | Custom | `~/.gemini/policies/*.toml` |
| **Workspace** | Custom | **(Disabled)** `$WORKSPACE_ROOT/.gemini/policies/*.toml` |
| **Admin** | System | _See below (OS specific)_ |
| Tier | Type | Location |
| :------------ | :----- | :---------------------------------------- |
| **User** | Custom | `~/.gemini/policies/*.toml` |
| **Workspace** | Custom | `$WORKSPACE_ROOT/.gemini/policies/*.toml` |
| **Admin** | System | _See below (OS specific)_ |
#### System-wide policies (Admin)
-22
View File
@@ -92,28 +92,6 @@ each tool.
| [`ask_user`](../tools/ask-user.md) | `Communicate` | Requests clarification or missing information via an interactive dialog. |
| [`write_todos`](../tools/todos.md) | `Other` | Maintains an internal list of subtasks. The model uses this to track its own progress. |
### Task Tracker (Experimental)
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development. Enable via `experimental.taskTracker`.
| Tool | Kind | Description |
| :---------------------------------------------- | :------ | :-------------------------------------------------------------------------- |
| [`tracker_create_task`](../tools/tracker.md) | `Other` | Creates a new task in the experimental tracker. |
| [`tracker_update_task`](../tools/tracker.md) | `Other` | Updates an existing task's status, description, or dependencies. |
| [`tracker_get_task`](../tools/tracker.md) | `Other` | Retrieves the full details of a specific task. |
| [`tracker_list_tasks`](../tools/tracker.md) | `Other` | Lists tasks in the tracker, optionally filtered by status, type, or parent. |
| [`tracker_add_dependency`](../tools/tracker.md) | `Other` | Adds a dependency between two tasks, ensuring topological execution. |
| [`tracker_visualize`](../tools/tracker.md) | `Other` | Renders an ASCII tree visualization of the current task graph. |
### MCP
| Tool | Kind | Description |
| :------------------------------------------------ | :------- | :--------------------------------------------------------------------- |
| [`list_mcp_resources`](../tools/mcp-resources.md) | `Search` | Lists all available resources exposed by connected MCP servers. |
| [`read_mcp_resource`](../tools/mcp-resources.md) | `Read` | Reads the content of a specific Model Context Protocol (MCP) resource. |
### Memory
| Tool | Kind | Description |
+1 -13
View File
@@ -96,11 +96,6 @@
]
},
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
{
"label": "Auto Memory",
"badge": "🔬",
"slug": "docs/cli/auto-memory"
},
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
{ "label": "Headless mode", "slug": "docs/cli/headless" },
{
@@ -127,14 +122,7 @@
}
]
},
{
"label": "MCP servers",
"collapsed": true,
"items": [
{ "label": "Overview", "slug": "docs/tools/mcp-server" },
{ "label": "Resource tools", "slug": "docs/tools/mcp-resources" }
]
},
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
{ "label": "Model routing", "slug": "docs/cli/model-routing" },
{ "label": "Model selection", "slug": "docs/cli/model" },
{
-44
View File
@@ -1,44 +0,0 @@
# MCP resource tools
MCP resource tools let Gemini CLI discover and retrieve data from contextual
resources exposed by Model Context Protocol (MCP) servers.
## 1. `list_mcp_resources` (ListMcpResources)
`list_mcp_resources` retrieves a list of all available resources from connected
MCP servers. This is primarily a discovery tool that helps the model understand
what external data sources are available for reference.
- **Tool name:** `list_mcp_resources`
- **Display name:** List MCP Resources
- **Kind:** `Search`
- **File:** `list-mcp-resources.ts`
- **Parameters:**
- `serverName` (string, optional): An optional filter to list resources from a
specific server.
- **Behavior:**
- Iterates through all connected MCP servers.
- Fetches the list of resources each server exposes.
- Formats the results into a plain-text list of URIs and descriptions.
- **Output (`llmContent`):** A formatted list of available resources, including
their URI, server name, and optional description.
- **Confirmation:** No. This is a read-only discovery tool.
## 2. `read_mcp_resource` (ReadMcpResource)
`read_mcp_resource` retrieves the content of a specific resource identified by
its URI.
- **Tool name:** `read_mcp_resource`
- **Display name:** Read MCP Resource
- **Kind:** `Read`
- **File:** `read-mcp-resource.ts`
- **Parameters:**
- `uri` (string, required): The URI of the MCP resource to read.
- **Behavior:**
- Locates the resource and its associated server by URI.
- Calls the server's `resources/read` method.
- Processes the response, extracting text or binary data.
- **Output (`llmContent`):** The content of the resource. For binary data, it
returns a placeholder indicating the data type.
- **Confirmation:** No. This is a read-only retrieval tool.
+1 -2
View File
@@ -64,8 +64,7 @@ Gemini CLI supports three MCP transport types:
Some MCP servers expose contextual “resources” in addition to the tools and
prompts. Gemini CLI discovers these automatically and gives you the possibility
to reference them in the chat. For more information on the tools used to
interact with these resources, see [MCP resource tools](mcp-resources.md).
to reference them in the chat.
### Discovery and listing
-61
View File
@@ -1,61 +0,0 @@
# Tracker tools (`tracker_*`)
<!-- prettier-ignore -->
> [!NOTE]
> This is an experimental feature currently under active development.
The `tracker_*` tools allow the Gemini agent to maintain an internal, persistent
graph of tasks and dependencies for multi-step requests. This suite of tools
provides a more robust and granular way to manage execution plans than the
legacy `write_todos` tool.
## Technical reference
The agent uses these tools to manage its execution plan, decompose complex goals
into actionable sub-tasks, and provide real-time progress updates to the CLI
interface. The task state is stored in the `.gemini/tmp/tracker/<session-id>`
directory, allowing the agent to manage its plan for the current session.
### Available Tools
- `tracker_create_task`: Creates a new task in the tracker. You can specify a
title, description, and task type (`epic`, `task`, `bug`).
- `tracker_update_task`: Updates an existing task's status (`open`,
`in_progress`, `blocked`, `closed`), description, or dependencies.
- `tracker_get_task`: Retrieves the full details of a specific task by its
6-character hex ID.
- `tracker_list_tasks`: Lists tasks in the tracker, optionally filtered by
status, type, or parent ID.
- `tracker_add_dependency`: Adds a dependency between two tasks, ensuring
topological execution.
- `tracker_visualize`: Renders an ASCII tree visualization of the current task
graph.
## Technical behavior
- **Interface:** Updates the progress indicator and task tree above the CLI
input prompt.
- **Persistence:** Task state is saved automatically to the
`.gemini/tmp/tracker/<session-id>` directory. Task states are session-specific
and do not persist across different sessions.
- **Dependencies:** Tasks can depend on other tasks, forming a directed acyclic
graph (DAG). The agent must resolve dependencies before starting blocked
tasks.
- **Interaction:** Users can view the current state of the tracker by asking the
agent to visualize it, or by running `gemini-cli` commands if implemented.
## Use cases
- Coordinating multi-file refactoring projects.
- Breaking down a mission into a hierarchy of epics and tasks for better
visibility.
- Tracking bugs and feature requests directly within the context of an active
codebase.
- Providing visibility into the agent's current focus and remaining work.
## Next steps
- Follow the [Task planning tutorial](../cli/tutorials/task-planning.md) for
usage details and migration from the legacy todo list.
- Learn about [Session management](../cli/session-management.md) for context on
persistent state.
-4
View File
@@ -11,8 +11,6 @@ import path from 'node:path';
describe('Background Process Monitoring', () => {
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should naturally use read output tool to find token',
prompt:
"Run the script using 'bash generate_token.sh'. It will emit a token after a short delay and continue running. Find the token and tell me what it is.",
@@ -52,8 +50,6 @@ sleep 100
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should naturally use list tool to verify multiple processes',
prompt:
"Start three background processes that run 'sleep 100', 'sleep 200', and 'sleep 300' respectively. Verify that all three are currently running.",
+3 -11
View File
@@ -17,17 +17,9 @@ describe('CliHelpAgent Delegation', () => {
timeout: 60000,
assert: async (rig, _result) => {
const toolLogs = rig.readToolLogs();
const toolCallIndex = toolLogs.findIndex((log) => {
if (log.toolRequest.name === 'invoke_agent') {
try {
const args = JSON.parse(log.toolRequest.args);
return args.agent_name === 'cli_help';
} catch {
return false;
}
}
return false;
});
const toolCallIndex = toolLogs.findIndex(
(log) => log.toolRequest.name === 'cli_help',
);
expect(toolCallIndex).toBeGreaterThan(-1);
expect(toolCallIndex).toBeLessThan(5); // Called within first 5 turns
},
+1 -17
View File
@@ -16,7 +16,6 @@ import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { randomUUID } from 'node:crypto';
import { vi } from 'vitest';
import {
Config,
type ConfigParameters,
@@ -53,7 +52,6 @@ export interface ComponentEvalCase extends BaseEvalCase {
export class ComponentRig {
public config: Config | undefined;
public testDir: string;
public homeDir: string;
public sessionId: string;
constructor(
@@ -63,9 +61,6 @@ export class ComponentRig {
this.testDir = fs.mkdtempSync(
path.join(os.tmpdir(), `gemini-component-rig-${uniqueId.slice(0, 8)}-`),
);
this.homeDir = fs.mkdtempSync(
path.join(os.tmpdir(), `gemini-component-home-${uniqueId.slice(0, 8)}-`),
);
this.sessionId = `test-session-${uniqueId}`;
}
@@ -94,23 +89,12 @@ export class ComponentRig {
this.config = makeFakeConfig(configParams);
await this.config.initialize();
// Refresh auth using USE_GEMINI to initialize the real BaseLlmClient.
// This must happen BEFORE stubbing GEMINI_CLI_HOME because OAuth credential
// lookup resolves through homedir() → GEMINI_CLI_HOME.
// Refresh auth using USE_GEMINI to initialize the real BaseLlmClient
await this.config.refreshAuth(AuthType.USE_GEMINI);
// Isolate storage paths (session files, skills, extraction state) by
// pointing GEMINI_CLI_HOME at a per-test temp directory. Storage resolves
// global paths through `homedir()` which reads this env var. This is set
// after auth so credential lookup uses the real home directory.
vi.stubEnv('GEMINI_CLI_HOME', this.homeDir);
}
async cleanup() {
await this.config?.dispose();
vi.unstubAllEnvs();
fs.rmSync(this.testDir, { recursive: true, force: true });
fs.rmSync(this.homeDir, { recursive: true, force: true });
}
}
+3 -14
View File
@@ -26,22 +26,11 @@ describe('generalist_agent', () => {
prompt:
'Please use the generalist agent to create a file called "generalist_test_file.txt" containing exactly the following text: success',
assert: async (rig) => {
// 1) Verify the generalist agent was invoked via invoke_agent
const foundToolCall = await rig.waitForToolCall(
'invoke_agent',
undefined,
(args) => {
try {
const parsed = JSON.parse(args);
return parsed.agent_name === 'generalist';
} catch {
return false;
}
},
);
// 1) Verify the generalist agent was invoked
const foundToolCall = await rig.waitForToolCall('generalist');
expect(
foundToolCall,
'Expected to find an invoke_agent tool call for generalist agent',
'Expected to find a tool call for generalist agent',
).toBeTruthy();
// 2) Verify the file was created as expected
+3 -3
View File
@@ -17,7 +17,7 @@ describe('Hierarchical Memory', () => {
params: {
settings: {
security: {
folderTrust: { enabled: false },
folderTrust: { enabled: true },
},
},
},
@@ -55,7 +55,7 @@ What is my favorite fruit? Tell me just the name of the fruit.`,
params: {
settings: {
security: {
folderTrust: { enabled: false },
folderTrust: { enabled: true },
},
},
},
@@ -96,7 +96,7 @@ Provide the answer as an XML block like this:
params: {
settings: {
security: {
folderTrust: { enabled: false },
folderTrust: { enabled: true },
},
},
},
+2 -50
View File
@@ -298,14 +298,12 @@ describe('plan_mode', () => {
});
evalTest('ALWAYS_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should transition from plan mode to normal execution and create a plan file from scratch',
params: {
settings,
},
prompt:
'I agree with your strategy. Please enter plan mode and draft the plan to create a new module called foo. The plan should be saved as foo-plan.md. Then, exit plan mode.',
'Enter plan mode and plan to create a new module called foo. The plan should be saved as foo-plan.md. Then, exit plan mode.',
assert: async (rig, result) => {
const enterPlanCalled = await rig.waitForToolCall('enter_plan_mode');
expect(
@@ -335,7 +333,7 @@ describe('plan_mode', () => {
expect(
planWrite?.toolRequest.success,
`Expected write_file to succeed, but got error: ${(planWrite?.toolRequest as any).error}`,
`Expected write_file to succeed, but got error: ${planWrite?.toolRequest.error}`,
).toBe(true);
assertModelHasOutput(result);
@@ -343,8 +341,6 @@ describe('plan_mode', () => {
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should not exit plan mode or draft before informal agreement',
approvalMode: ApprovalMode.PLAN,
params: {
@@ -376,48 +372,4 @@ describe('plan_mode', () => {
assertModelHasOutput(result);
},
});
evalTest('USUALLY_PASSES', {
name: 'should handle nested plan directories correctly',
suiteName: 'plan_mode',
suiteType: 'behavioral',
approvalMode: ApprovalMode.PLAN,
params: {
settings,
},
prompt:
'Please create a new architectural plan in a nested folder called "architecture/frontend-v2.md" within the plans directory. The plan should contain the text "# Frontend V2 Plan". Then, exit plan mode',
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const writeCalls = toolLogs.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
);
const wroteToNestedPath = writeCalls.some((log) => {
try {
const args = JSON.parse(log.toolRequest.args);
if (!args.file_path) return false;
// In plan mode, paths can be passed as relative (architecture/frontend-v2.md)
// or they might be resolved as absolute by the tool depending on the exact mock state.
// We strictly ensure it ends exactly with the expected nested path and doesn't contain extra nesting.
const normalizedPath = args.file_path.replace(/\\/g, '/');
return (
normalizedPath === 'architecture/frontend-v2.md' ||
normalizedPath.endsWith('/plans/architecture/frontend-v2.md')
);
} catch {
return false;
}
});
expect(
wroteToNestedPath,
'Expected model to successfully target the nested plan file path',
).toBe(true);
assertModelHasOutput(result);
},
});
});
+42 -562
View File
@@ -5,78 +5,12 @@
*/
import { describe, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import {
loadConversationRecord,
SESSION_FILE_PREFIX,
} from '@google/gemini-cli-core';
import {
evalTest,
assertModelHasOutput,
checkModelOutputContent,
} from './test-helper.js';
function findDir(base: string, name: string): string | null {
if (!fs.existsSync(base)) return null;
const files = fs.readdirSync(base);
for (const file of files) {
const fullPath = path.join(base, file);
if (fs.statSync(fullPath).isDirectory()) {
if (file === name) return fullPath;
const found = findDir(fullPath, name);
if (found) return found;
}
}
return null;
}
async function loadLatestSessionRecord(homeDir: string, sessionId: string) {
const chatsDir = findDir(path.join(homeDir, '.gemini'), 'chats');
if (!chatsDir) {
throw new Error('Could not find chats directory for eval session logs');
}
const candidates = fs
.readdirSync(chatsDir)
.filter(
(file) =>
file.startsWith(SESSION_FILE_PREFIX) &&
(file.endsWith('.json') || file.endsWith('.jsonl')),
);
const matchingRecords = [];
for (const file of candidates) {
const filePath = path.join(chatsDir, file);
const record = await loadConversationRecord(filePath);
if (record?.sessionId === sessionId) {
matchingRecords.push(record);
}
}
matchingRecords.sort(
(a, b) => Date.parse(b.lastUpdated) - Date.parse(a.lastUpdated),
);
return matchingRecords[0] ?? null;
}
async function waitForSessionScratchpad(
homeDir: string,
sessionId: string,
timeoutMs = 30000,
) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const record = await loadLatestSessionRecord(homeDir, sessionId);
if (record?.memoryScratchpad) {
return record;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return loadLatestSessionRecord(homeDir, sessionId);
}
describe('save_memory', () => {
const TEST_PREFIX = 'Save memory test: ';
const rememberingFavoriteColor = "Agent remembers user's favorite color";
@@ -84,11 +18,6 @@ describe('save_memory', () => {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingFavoriteColor,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `remember that my favorite color is blue.
@@ -111,11 +40,6 @@ describe('save_memory', () => {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingCommandRestrictions,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `I don't want you to ever run npm commands.`,
assert: async (rig, result) => {
@@ -137,11 +61,6 @@ describe('save_memory', () => {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingWorkflow,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `I want you to always lint after building.`,
assert: async (rig, result) => {
@@ -164,11 +83,6 @@ describe('save_memory', () => {
suiteName: 'default',
suiteType: 'behavioral',
name: ignoringTemporaryInformation,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `I'm going to get a coffee.`,
assert: async (rig, result) => {
@@ -194,11 +108,6 @@ describe('save_memory', () => {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingPetName,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `Please remember that my dog's name is Buddy.`,
assert: async (rig, result) => {
@@ -220,11 +129,6 @@ describe('save_memory', () => {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingCommandAlias,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `When I say 'start server', you should run 'npm run dev'.`,
assert: async (rig, result) => {
@@ -241,35 +145,22 @@ describe('save_memory', () => {
},
});
const savingDbSchemaLocationAsProjectMemory =
'Agent saves workspace database schema location as project memory';
const ignoringDbSchemaLocation =
"Agent ignores workspace's database schema location";
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: savingDbSchemaLocationAsProjectMemory,
params: {
settings: {
experimental: { memoryV2: false },
},
},
name: ignoringDbSchemaLocation,
prompt: `The database schema for this workspace is located in \`db/schema.sql\`.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall(
'save_memory',
undefined,
(args) => {
try {
const params = JSON.parse(args);
return params.scope === 'project';
} catch {
return false;
}
},
);
await rig.waitForTelemetryReady();
const wasToolCalled = rig
.readToolLogs()
.some((log) => log.toolRequest.name === 'save_memory');
expect(
wasToolCalled,
'Expected save_memory to be called with scope="project" for workspace-specific information',
).toBe(true);
'save_memory should not be called for workspace-specific information',
).toBe(false);
assertModelHasOutput(result);
},
@@ -281,11 +172,6 @@ describe('save_memory', () => {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingCodingStyle,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `I prefer to use tabs instead of spaces for indentation.`,
assert: async (rig, result) => {
@@ -302,69 +188,42 @@ describe('save_memory', () => {
},
});
const savingBuildArtifactLocationAsProjectMemory =
'Agent saves workspace build artifact location as project memory';
const ignoringBuildArtifactLocation =
'Agent ignores workspace build artifact location';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: savingBuildArtifactLocationAsProjectMemory,
params: {
settings: {
experimental: { memoryV2: false },
},
},
name: ignoringBuildArtifactLocation,
prompt: `In this workspace, build artifacts are stored in the \`dist/artifacts\` directory.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall(
'save_memory',
undefined,
(args) => {
try {
const params = JSON.parse(args);
return params.scope === 'project';
} catch {
return false;
}
},
);
await rig.waitForTelemetryReady();
const wasToolCalled = rig
.readToolLogs()
.some((log) => log.toolRequest.name === 'save_memory');
expect(
wasToolCalled,
'Expected save_memory to be called with scope="project" for workspace-specific information',
).toBe(true);
'save_memory should not be called for workspace-specific information',
).toBe(false);
assertModelHasOutput(result);
},
});
const savingMainEntryPointAsProjectMemory =
'Agent saves workspace main entry point as project memory';
const ignoringMainEntryPoint = "Agent ignores workspace's main entry point";
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: savingMainEntryPointAsProjectMemory,
params: {
settings: {
experimental: { memoryV2: false },
},
},
name: ignoringMainEntryPoint,
prompt: `The main entry point for this workspace is \`src/index.js\`.`,
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall(
'save_memory',
undefined,
(args) => {
try {
const params = JSON.parse(args);
return params.scope === 'project';
} catch {
return false;
}
},
);
await rig.waitForTelemetryReady();
const wasToolCalled = rig
.readToolLogs()
.some((log) => log.toolRequest.name === 'save_memory');
expect(
wasToolCalled,
'Expected save_memory to be called with scope="project" for workspace-specific information',
).toBe(true);
'save_memory should not be called for workspace-specific information',
).toBe(false);
assertModelHasOutput(result);
},
@@ -375,11 +234,6 @@ describe('save_memory', () => {
suiteName: 'default',
suiteType: 'behavioral',
name: rememberingBirthday,
params: {
settings: {
experimental: { memoryV2: false },
},
},
prompt: `My birthday is on June 15th.`,
assert: async (rig, result) => {
@@ -404,7 +258,7 @@ describe('save_memory', () => {
name: proactiveMemoryFromLongSession,
params: {
settings: {
experimental: { memoryV2: true },
experimental: { memoryManager: true },
},
},
messages: [
@@ -462,75 +316,29 @@ describe('save_memory', () => {
prompt:
'Please save any persistent preferences or facts about me from our conversation to memory.',
assert: async (rig, result) => {
// Under experimental.memoryV2, the agent persists memories by
// editing markdown files directly with write_file or replace — not via
// a save_memory subagent. The user said "I always prefer Vitest over
// Jest for testing in all my projects" — that matches the new
// cross-project cue phrase ("across all my projects"), so under the
// 4-tier model the correct destination is the global personal memory
// file (~/.gemini/GEMINI.md). It must NOT land in a committed project
// GEMINI.md (that tier is for team conventions) or the per-project
// private memory folder (that tier is for project-specific personal
// notes). The chat history mixes this durable preference with
// transient debugging chatter, so the eval also verifies the agent
// picks out the persistent fact among the noise.
await rig.waitForToolCall('write_file').catch(() => {});
const writeCalls = rig
.readToolLogs()
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
);
const wroteVitestToGlobal = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/GEMINI\.md/i.test(args) &&
!/tmp\/[^/]+\/memory/i.test(args) &&
/vitest/i.test(args)
);
});
const wasToolCalled = await rig.waitForToolCall(
'save_memory',
undefined,
(args) => /vitest/i.test(args),
);
expect(
wroteVitestToGlobal,
'Expected the cross-project Vitest preference to be written to the global personal memory file (~/.gemini/GEMINI.md) via write_file or replace',
wasToolCalled,
'Expected save_memory to be called with the Vitest preference from the conversation history',
).toBe(true);
const leakedToCommittedProject = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/GEMINI\.md/i.test(args) &&
!/\.gemini\//i.test(args) &&
/vitest/i.test(args)
);
});
expect(
leakedToCommittedProject,
'Cross-project Vitest preference must NOT be mirrored into a committed project ./GEMINI.md (that tier is for team-shared conventions only)',
).toBe(false);
const leakedToPrivateProject = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/tmp\/[^/]+\/memory\//i.test(args) && /vitest/i.test(args)
);
});
expect(
leakedToPrivateProject,
'Cross-project Vitest preference must NOT be mirrored into the private project memory folder (that tier is for project-specific personal notes only)',
).toBe(false);
assertModelHasOutput(result);
},
});
const memoryV2RoutesTeamConventionsToProjectGemini =
'Agent routes team-shared project conventions to ./GEMINI.md';
const memoryManagerRoutingPreferences =
'Agent routes global and project preferences to memory';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: memoryV2RoutesTeamConventionsToProjectGemini,
name: memoryManagerRoutingPreferences,
params: {
settings: {
experimental: { memoryV2: true },
experimental: { memoryManager: true },
},
},
messages: [
@@ -539,7 +347,7 @@ describe('save_memory', () => {
type: 'user',
content: [
{
text: 'For this project, the team always runs tests with `npm run test` — please remember that as our project convention.',
text: 'I always use dark mode in all my editors and terminals.',
},
],
timestamp: '2026-01-01T00:00:00Z',
@@ -547,9 +355,7 @@ describe('save_memory', () => {
{
id: 'msg-2',
type: 'gemini',
content: [
{ text: 'Got it, I will keep `npm run test` in mind for tests.' },
],
content: [{ text: 'Got it, I will keep that in mind!' }],
timestamp: '2026-01-01T00:00:05Z',
},
{
@@ -573,334 +379,8 @@ describe('save_memory', () => {
],
prompt: 'Please save the preferences I mentioned earlier to memory.',
assert: async (rig, result) => {
// Under experimental.memoryV2, the prompt enforces an explicit
// one-tier-per-fact rule: team-shared project conventions (the team's
// test command, project-wide indentation rules) belong in the
// committed project-root ./GEMINI.md and must NOT be mirrored or
// cross-referenced into the private project memory folder
// (~/.gemini/tmp/<hash>/memory/). The global ~/.gemini/GEMINI.md must
// never be touched in this mode either.
await rig.waitForToolCall('write_file').catch(() => {});
const writeCalls = rig
.readToolLogs()
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
);
const wroteToProjectRoot = (factPattern: RegExp) =>
writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/GEMINI\.md/i.test(args) &&
!/\.gemini\//i.test(args) &&
factPattern.test(args)
);
});
expect(
wroteToProjectRoot(/npm run test/i),
'Expected the team test-command convention to be written to the project-root ./GEMINI.md',
).toBe(true);
expect(
wroteToProjectRoot(/2[- ]space/i),
'Expected the project-wide "2-space indentation" convention to be written to the project-root ./GEMINI.md',
).toBe(true);
const leakedToPrivateMemory = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/tmp\/[^/]+\/memory\//i.test(args) &&
(/npm run test/i.test(args) || /2[- ]space/i.test(args))
);
});
expect(
leakedToPrivateMemory,
'Team-shared project conventions must NOT be mirrored into the private project memory folder (~/.gemini/tmp/<hash>/memory/) — each fact lives in exactly one tier.',
).toBe(false);
const leakedToGlobal = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/GEMINI\.md/i.test(args) &&
!/tmp\/[^/]+\/memory/i.test(args)
);
});
expect(
leakedToGlobal,
'Project preferences must NOT be written to the global ~/.gemini/GEMINI.md',
).toBe(false);
assertModelHasOutput(result);
},
});
const memoryV2SessionScratchpad =
'Session summary persists memory scratchpad for memory-saving sessions';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: memoryV2SessionScratchpad,
sessionId: 'memory-scratchpad-eval',
params: {
settings: {
experimental: { memoryV2: true },
},
},
messages: [
{
id: 'msg-1',
type: 'user',
content: [
{
text: 'Across all my projects, I prefer Vitest over Jest for testing.',
},
],
timestamp: '2026-01-01T00:00:00Z',
},
{
id: 'msg-2',
type: 'gemini',
content: [{ text: 'Noted. What else should I keep in mind?' }],
timestamp: '2026-01-01T00:00:05Z',
},
{
id: 'msg-3',
type: 'user',
content: [
{
text: 'For this repo I was debugging a flaky API test earlier, but that was just transient context.',
},
],
timestamp: '2026-01-01T00:01:00Z',
},
{
id: 'msg-4',
type: 'gemini',
content: [
{ text: 'Understood. I will only save the durable preference.' },
],
timestamp: '2026-01-01T00:01:05Z',
},
],
prompt:
'Please save any persistent preferences or facts about me from our conversation to memory.',
assert: async (rig, result) => {
await rig.waitForToolCall('write_file').catch(() => {});
const writeCalls = rig
.readToolLogs()
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
);
expect(
writeCalls.length,
'Expected memoryV2 save flow to edit a markdown memory file',
).toBeGreaterThan(0);
await rig.run({
args: ['--list-sessions'],
approvalMode: 'yolo',
timeout: 120000,
});
const record = await waitForSessionScratchpad(
rig.homeDir!,
'memory-scratchpad-eval',
);
expect(
record?.memoryScratchpad,
'Expected the resumed session log to contain a memoryScratchpad after session summary generation',
).toBeDefined();
expect(record?.memoryScratchpad?.version).toBe(1);
expect(
record?.memoryScratchpad?.toolSequence?.some((toolName) =>
['write_file', 'replace'].includes(toolName),
),
'Expected memoryScratchpad.toolSequence to include the markdown editing tool used for memory persistence',
).toBe(true);
expect(
record?.memoryScratchpad?.touchedPaths?.length,
'Expected memoryScratchpad to capture at least one touched path',
).toBeGreaterThan(0);
expect(
record?.memoryScratchpad?.workflowSummary,
'Expected memoryScratchpad.workflowSummary to be populated',
).toMatch(/write_file|replace/i);
assertModelHasOutput(result);
},
});
const memoryV2RoutesUserProject =
'Agent routes personal-to-user project notes to user-project memory';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: memoryV2RoutesUserProject,
params: {
settings: {
experimental: { memoryV2: true },
},
},
prompt: `Please remember my personal local dev setup for THIS project's Postgres database. This is private to my machine — do NOT commit it to the repo.
Connection details:
- Host: localhost
- Port: 6543 (non-standard, I run multiple Postgres instances)
- Database: myproj_dev
- User: sandy_local
- Password: read from the SANDY_PG_LOCAL_PASS env var in my shell
How I start it locally:
1. Run \`brew services start postgresql@15\` to bring the server up.
2. Run \`./scripts/seed-local-db.sh\` from the repo root to load my personal seed data.
3. Verify with \`psql -h localhost -p 6543 -U sandy_local myproj_dev -c '\\dt'\`.
Quirks to remember:
- The migrations runner sometimes hangs on my machine if I forget step 1; kill it with Ctrl+C and rerun.
- I keep an extra \`scratch\` schema for ad-hoc experiments — never reference it from project code.`,
assert: async (rig, result) => {
// Under experimental.memoryV2 with the Private Project Memory bullet
// surfaced in the prompt, a fact that is project-specific AND
// personal-to-the-user (must not be committed) should land in the
// private project memory folder under ~/.gemini/tmp/<hash>/memory/. The
// detailed note should be written to a sibling markdown file, with
// MEMORY.md updated as the index. It must NOT go to committed
// ./GEMINI.md or the global ~/.gemini/GEMINI.md.
await rig.waitForToolCall('write_file').catch(() => {});
const writeCalls = rig
.readToolLogs()
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
);
const wroteUserProjectDetail = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/tmp\/[^/]+\/memory\/(?!MEMORY\.md)[^"]+\.md/i.test(args) &&
/6543/.test(args)
);
});
expect(
wroteUserProjectDetail,
'Expected the personal-to-user project note to be written to a private project memory detail file (~/.gemini/tmp/<hash>/memory/*.md)',
).toBe(true);
const wroteUserProjectIndex = writeCalls.some((log) => {
const args = log.toolRequest.args;
return /\.gemini\/tmp\/[^/]+\/memory\/MEMORY\.md/i.test(args);
});
expect(
wroteUserProjectIndex,
'Expected the personal-to-user project note to update the private project memory index (~/.gemini/tmp/<hash>/memory/MEMORY.md)',
).toBe(true);
// Defensive: should NOT have written this private note to the
// committed project GEMINI.md or the global GEMINI.md.
const leakedToCommittedProject = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\/GEMINI\.md/i.test(args) &&
!/\.gemini\//i.test(args) &&
/6543/.test(args)
);
});
expect(
leakedToCommittedProject,
'Personal-to-user note must NOT be written to the committed project GEMINI.md',
).toBe(false);
const leakedToGlobal = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/GEMINI\.md/i.test(args) &&
!/tmp\/[^/]+\/memory/i.test(args) &&
/6543/.test(args)
);
});
expect(
leakedToGlobal,
'Personal-to-user project note must NOT be written to the global ~/.gemini/GEMINI.md',
).toBe(false);
assertModelHasOutput(result);
},
});
const memoryV2RoutesCrossProjectToGlobal =
'Agent routes cross-project personal preferences to ~/.gemini/GEMINI.md';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: memoryV2RoutesCrossProjectToGlobal,
params: {
settings: {
experimental: { memoryV2: true },
},
},
prompt:
'Please remember this about me in general: across all my projects I always prefer Prettier with single quotes and trailing commas, and I always prefer tabs over spaces for indentation. These are my personal coding-style defaults that follow me into every workspace.',
assert: async (rig, result) => {
// Under experimental.memoryV2 with the Global Personal Memory
// tier surfaced in the prompt, a fact that explicitly applies to the
// user "across all my projects" / "in every workspace" must land in
// the global ~/.gemini/GEMINI.md (the cross-project tier). It must
// NOT be mirrored into a committed project-root ./GEMINI.md (that
// tier is for team-shared conventions) or into the per-project
// private memory folder (that tier is for project-specific personal
// notes). Each fact lives in exactly one tier across all four tiers.
await rig.waitForToolCall('write_file').catch(() => {});
const writeCalls = rig
.readToolLogs()
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
);
const wroteToGlobal = (factPattern: RegExp) =>
writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/GEMINI\.md/i.test(args) &&
!/tmp\/[^/]+\/memory/i.test(args) &&
factPattern.test(args)
);
});
expect(
wroteToGlobal(/Prettier/i),
'Expected the cross-project Prettier preference to be written to the global personal memory file (~/.gemini/GEMINI.md)',
).toBe(true);
expect(
wroteToGlobal(/tabs/i),
'Expected the cross-project "tabs over spaces" preference to be written to the global personal memory file (~/.gemini/GEMINI.md)',
).toBe(true);
const leakedToCommittedProject = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/GEMINI\.md/i.test(args) &&
!/\.gemini\//i.test(args) &&
(/Prettier/i.test(args) || /tabs/i.test(args))
);
});
expect(
leakedToCommittedProject,
'Cross-project personal preferences must NOT be mirrored into a committed project ./GEMINI.md (that tier is for team-shared conventions only)',
).toBe(false);
const leakedToPrivateProject = writeCalls.some((log) => {
const args = log.toolRequest.args;
return (
/\.gemini\/tmp\/[^/]+\/memory\//i.test(args) &&
(/Prettier/i.test(args) || /tabs/i.test(args))
);
});
expect(
leakedToPrivateProject,
'Cross-project personal preferences must NOT be mirrored into the private project memory folder (that tier is for project-specific personal notes only)',
).toBe(false);
const wasToolCalled = await rig.waitForToolCall('save_memory');
expect(wasToolCalled, 'Expected save_memory to be called').toBe(true);
assertModelHasOutput(result);
},
-962
View File
@@ -1,962 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fsp from 'node:fs/promises';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import {
type Config,
ApprovalMode,
type MemoryScratchpad,
SESSION_FILE_PREFIX,
getProjectHash,
startMemoryService,
} from '@google/gemini-cli-core';
import { ComponentRig, componentEvalTest } from './component-test-helper.js';
import {
average,
averageNullable,
countMatchingIds,
roundStat,
} from './statistics-helper.js';
import { prepareWorkspace } from './test-helper.js';
interface SeedSession {
sessionId: string;
summary: string;
userTurns: string[];
timestampOffsetMinutes: number;
memoryScratchpad?: MemoryScratchpad;
}
interface MessageRecord {
id: string;
timestamp: string;
type: string;
content: Array<{ text: string }>;
}
interface SessionVersion {
sessionId: string;
lastUpdated: string;
}
interface ExtractionRunSnapshot {
sessionIds: string[];
skillsCreated: string[];
candidateSessions: SessionVersion[];
processedSessions: SessionVersion[];
turnCount?: number;
durationMs?: number;
terminateReason?: string;
}
interface ExtractionOutcome {
state: { runs: ExtractionRunSnapshot[] };
skillsDir: string;
skillBodies: string[];
}
interface SkillQualitySignal {
label: string;
pattern: RegExp;
}
interface ScratchpadRunMetrics {
turnCount: number | null;
durationMs: number | null;
terminateReason: string | null;
skillsCreated: number;
candidateSessions: number;
processedSessions: number;
relevantReads: number;
distractorReads: number;
totalReads: number;
recall: number;
precision: number;
signalScore: number;
skillQualityScore: number;
skillQualityMax: number;
skillQualityRatio: number;
missingQualitySignals: string[];
}
interface ScratchpadStatsTrial {
trial: number;
baseline: ScratchpadRunMetrics;
enhanced: ScratchpadRunMetrics;
}
interface ScratchpadStatsAggregate {
turnCountAvg: number | null;
durationMsAvg: number | null;
recallAvg: number;
precisionAvg: number;
signalScoreAvg: number;
relevantReadsAvg: number;
distractorReadsAvg: number;
skillsCreatedAvg: number;
skillQualityScoreAvg: number;
skillQualityRatioAvg: number;
}
interface ScratchpadStatsReport {
generatedAt: string;
trials: number;
aggregate: {
baseline: ScratchpadStatsAggregate;
enhanced: ScratchpadStatsAggregate;
};
deltas: ScratchpadStatsAggregate;
results: ScratchpadStatsTrial[];
}
const WORKSPACE_FILES = {
'package.json': JSON.stringify(
{
name: 'skill-extraction-eval',
private: true,
scripts: {
build: 'echo build',
lint: 'echo lint',
test: 'echo test',
},
},
null,
2,
),
'README.md': `# Skill Extraction Eval
This workspace exists to exercise background skill extraction from prior chats.
`,
};
function buildMessages(userTurns: string[]): MessageRecord[] {
const baseTime = new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString();
return userTurns.flatMap((text, index) => [
{
id: `u${index + 1}`,
timestamp: baseTime,
type: 'user',
content: [{ text }],
},
{
id: `a${index + 1}`,
timestamp: baseTime,
type: 'gemini',
content: [{ text: `Acknowledged: ${index + 1}` }],
},
]);
}
function padTurns(turns: string[]): string[] {
if (turns.length >= 10) {
return turns;
}
const padded = [...turns];
for (let i = turns.length; i < 10; i++) {
padded.push(`${turns[i % turns.length]} (repeat ${i + 1})`);
}
return padded;
}
function createScratchpad(
workflowSummary: string,
touchedPaths: string[],
validationStatus: MemoryScratchpad['validationStatus'] = 'passed',
): MemoryScratchpad {
return {
version: 1,
workflowSummary,
toolSequence: ['run_shell_command'],
touchedPaths,
validationStatus,
};
}
function createWorkflowComparisonSessions(withScratchpad: boolean): {
sessions: SeedSession[];
relevantSessionIds: string[];
distractorSessionIds: string[];
} {
const relevantWorkflowSummary =
'run_shell_command -> run_shell_command | paths packages/cli/src/config/settings.ts, docs/settings.md | validated';
const relevantScratchpad = withScratchpad
? createScratchpad(relevantWorkflowSummary, [
'packages/cli/src/config/settings.ts',
'docs/settings.md',
])
: undefined;
const sessions: SeedSession[] = [
{
sessionId: 'hidden-settings-workflow-a',
summary: 'Prepare release notes for settings launch',
timestampOffsetMinutes: 420,
memoryScratchpad: relevantScratchpad,
userTurns: padTurns([
'When we add a new setting, the durable workflow is to regenerate the settings docs instead of editing them by hand.',
'The sequence that worked was npm run predocs:settings, npm run schema:settings, then npm run docs:settings.',
'Skipping predocs leaves stale defaults in the generated docs.',
'We verify the workflow by checking that both the schema output and docs update together.',
'This exact command order is the recurring workflow we use for settings changes.',
]),
},
{
sessionId: 'hidden-settings-workflow-b',
summary: 'Investigate CI drift in generated config reference',
timestampOffsetMinutes: 390,
memoryScratchpad: relevantScratchpad,
userTurns: padTurns([
'The config reference drift was fixed by rerunning the standard settings regeneration workflow.',
'We again used npm run predocs:settings before npm run schema:settings and npm run docs:settings.',
'The recurring rule is never to hand-edit generated settings docs.',
'The validation step is to confirm the schema artifact and docs changed together after regeneration.',
'This is the same recurring workflow we use every time a setting changes.',
]),
},
{
sessionId: 'distractor-release-notes',
summary: 'Prepare release notes for auth launch',
timestampOffsetMinutes: 360,
memoryScratchpad: undefined,
userTurns: padTurns([
'This release-notes task was one-off and just needed manual wording updates.',
'I edited CHANGELOG.md and docs/release-notes.md directly.',
'There was no reusable command sequence here beyond proofreading the copy.',
'This task should not become a standing workflow.',
'Once the wording landed, we were done.',
]),
},
{
sessionId: 'distractor-ci-snapshots',
summary: 'Investigate CI drift in auth snapshots',
timestampOffsetMinutes: 330,
memoryScratchpad: undefined,
userTurns: padTurns([
'This auth snapshot issue was specific to a flaky test in CI.',
'The only commands we ran were npm test -- auth and an isolated snapshot update.',
'It was not the recurring settings-doc workflow.',
'Once the flaky snapshot passed, there was no broader reusable procedure.',
'Treat this as a one-off CI cleanup.',
]),
},
{
sessionId: 'distractor-onboarding-docs',
summary: 'Refresh onboarding documentation copy',
timestampOffsetMinutes: 300,
memoryScratchpad: undefined,
userTurns: padTurns([
'This was just a docs wording cleanup in docs/onboarding.md.',
'No command sequence was involved.',
'We manually edited the copy and reviewed it.',
'There is no recurring operational workflow to capture here.',
'This should stay a one-off docs edit.',
]),
},
{
sessionId: 'distractor-deploy-copy',
summary: 'Adjust deployment checklist wording',
timestampOffsetMinutes: 270,
memoryScratchpad: undefined,
userTurns: padTurns([
'This was a wording-only change to docs/deploy.md.',
'We did not run a reusable command sequence.',
'It should not become a skill.',
'The edit was only for this deploy checklist cleanup.',
'After the copy change, the task was complete.',
]),
},
];
return {
sessions,
relevantSessionIds: [
'hidden-settings-workflow-a',
'hidden-settings-workflow-b',
],
distractorSessionIds: [
'distractor-release-notes',
'distractor-ci-snapshots',
'distractor-onboarding-docs',
'distractor-deploy-copy',
],
};
}
async function seedSessions(
config: Config,
sessions: SeedSession[],
): Promise<void> {
const chatsDir = path.join(config.storage.getProjectTempDir(), 'chats');
await fsp.mkdir(chatsDir, { recursive: true });
const projectRoot = config.storage.getProjectRoot();
for (const session of sessions) {
const sessionTimestamp = new Date(
Date.now() - session.timestampOffsetMinutes * 60 * 1000,
);
const timestamp = sessionTimestamp
.toISOString()
.slice(0, 16)
.replace(/:/g, '-');
const filename = `${SESSION_FILE_PREFIX}${timestamp}-${session.sessionId.slice(0, 8)}.json`;
const conversation = {
sessionId: session.sessionId,
projectHash: getProjectHash(projectRoot),
summary: session.summary,
memoryScratchpad: session.memoryScratchpad,
startTime: new Date(Date.now() - 7 * 60 * 60 * 1000).toISOString(),
lastUpdated: sessionTimestamp.toISOString(),
messages: buildMessages(session.userTurns),
};
await fsp.writeFile(
path.join(chatsDir, filename),
JSON.stringify(conversation, null, 2),
);
}
}
async function runExtractionAndReadState(
config: Config,
): Promise<ExtractionOutcome> {
await startMemoryService(config);
const memoryDir = config.storage.getProjectMemoryTempDir();
const skillsDir = config.storage.getProjectSkillsMemoryDir();
const statePath = path.join(memoryDir, '.extraction-state.json');
const raw = await fsp.readFile(statePath, 'utf-8');
const state = JSON.parse(raw) as {
runs?: Array<{
sessionIds?: string[];
skillsCreated?: string[];
candidateSessions?: SessionVersion[];
processedSessions?: SessionVersion[];
turnCount?: number;
durationMs?: number;
terminateReason?: string;
}>;
};
if (!Array.isArray(state.runs) || state.runs.length === 0) {
throw new Error('Skill extraction finished without writing any run state');
}
return {
state: {
runs: state.runs.map((run) => ({
sessionIds: Array.isArray(run.sessionIds) ? run.sessionIds : [],
skillsCreated: Array.isArray(run.skillsCreated)
? run.skillsCreated
: [],
candidateSessions: Array.isArray(run.candidateSessions)
? run.candidateSessions
: [],
processedSessions: Array.isArray(run.processedSessions)
? run.processedSessions
: [],
turnCount:
typeof run.turnCount === 'number' ? run.turnCount : undefined,
durationMs:
typeof run.durationMs === 'number' ? run.durationMs : undefined,
terminateReason:
typeof run.terminateReason === 'string'
? run.terminateReason
: undefined,
})),
},
skillsDir,
skillBodies: await readSkillBodies(skillsDir),
};
}
async function summarizeScratchpadRun(
outcome: ExtractionOutcome,
run: ExtractionRunSnapshot,
scenario: ReturnType<typeof createWorkflowComparisonSessions>,
): Promise<ScratchpadRunMetrics> {
const relevantReads = countMatchingIds(
run.processedSessions,
scenario.relevantSessionIds,
);
const distractorReads = countMatchingIds(
run.processedSessions,
scenario.distractorSessionIds,
);
const totalReads = run.processedSessions.length;
const quality = scoreSkillQuality(
outcome.skillBodies,
SETTINGS_SKILL_QUALITY_SIGNALS,
);
return {
turnCount: run.turnCount ?? null,
durationMs: run.durationMs ?? null,
terminateReason: run.terminateReason ?? null,
skillsCreated: run.skillsCreated.length,
candidateSessions: run.candidateSessions.length,
processedSessions: totalReads,
relevantReads,
distractorReads,
totalReads,
recall: relevantReads / scenario.relevantSessionIds.length,
precision: totalReads === 0 ? 0 : relevantReads / totalReads,
signalScore: relevantReads - distractorReads,
skillQualityScore: quality.score,
skillQualityMax: quality.maxScore,
skillQualityRatio:
quality.maxScore === 0 ? 0 : quality.score / quality.maxScore,
missingQualitySignals: quality.missing,
};
}
function averageScratchpadRuns(
runs: ScratchpadRunMetrics[],
): ScratchpadStatsAggregate {
return {
turnCountAvg: roundStat(averageNullable(runs.map((run) => run.turnCount))),
durationMsAvg: roundStat(
averageNullable(runs.map((run) => run.durationMs)),
),
recallAvg: roundStat(average(runs.map((run) => run.recall))) ?? 0,
precisionAvg: roundStat(average(runs.map((run) => run.precision))) ?? 0,
signalScoreAvg: roundStat(average(runs.map((run) => run.signalScore))) ?? 0,
relevantReadsAvg:
roundStat(average(runs.map((run) => run.relevantReads))) ?? 0,
distractorReadsAvg:
roundStat(average(runs.map((run) => run.distractorReads))) ?? 0,
skillsCreatedAvg:
roundStat(average(runs.map((run) => run.skillsCreated))) ?? 0,
skillQualityScoreAvg:
roundStat(average(runs.map((run) => run.skillQualityScore))) ?? 0,
skillQualityRatioAvg:
roundStat(average(runs.map((run) => run.skillQualityRatio))) ?? 0,
};
}
function diffScratchpadAggregates(
baseline: ScratchpadStatsAggregate,
enhanced: ScratchpadStatsAggregate,
): ScratchpadStatsAggregate {
return {
turnCountAvg:
baseline.turnCountAvg === null || enhanced.turnCountAvg === null
? null
: roundStat(enhanced.turnCountAvg - baseline.turnCountAvg),
durationMsAvg:
baseline.durationMsAvg === null || enhanced.durationMsAvg === null
? null
: roundStat(enhanced.durationMsAvg - baseline.durationMsAvg),
recallAvg: roundStat(enhanced.recallAvg - baseline.recallAvg) ?? 0,
precisionAvg: roundStat(enhanced.precisionAvg - baseline.precisionAvg) ?? 0,
signalScoreAvg:
roundStat(enhanced.signalScoreAvg - baseline.signalScoreAvg) ?? 0,
relevantReadsAvg:
roundStat(enhanced.relevantReadsAvg - baseline.relevantReadsAvg) ?? 0,
distractorReadsAvg:
roundStat(enhanced.distractorReadsAvg - baseline.distractorReadsAvg) ?? 0,
skillsCreatedAvg:
roundStat(enhanced.skillsCreatedAvg - baseline.skillsCreatedAvg) ?? 0,
skillQualityScoreAvg:
roundStat(
enhanced.skillQualityScoreAvg - baseline.skillQualityScoreAvg,
) ?? 0,
skillQualityRatioAvg:
roundStat(
enhanced.skillQualityRatioAvg - baseline.skillQualityRatioAvg,
) ?? 0,
};
}
async function runScenarioWithFreshRig(
sessions: SeedSession[],
): Promise<ExtractionOutcome> {
const rig = new ComponentRig({
configOverrides: EXTRACTION_CONFIG_OVERRIDES,
});
try {
await rig.initialize();
await prepareWorkspace(rig.testDir, rig.testDir, WORKSPACE_FILES);
await seedSessions(rig.config!, sessions);
return await runExtractionAndReadState(rig.config!);
} finally {
await rig.cleanup();
}
}
async function runScratchpadStatsTrial(
trial: number,
): Promise<ScratchpadStatsTrial> {
const baselineScenario = createWorkflowComparisonSessions(false);
const enhancedScenario = createWorkflowComparisonSessions(true);
const baselineOutcome = await runScenarioWithFreshRig(
baselineScenario.sessions,
);
const enhancedOutcome = await runScenarioWithFreshRig(
enhancedScenario.sessions,
);
const baselineRun = baselineOutcome.state.runs.at(-1);
const enhancedRun = enhancedOutcome.state.runs.at(-1);
if (!baselineRun || !enhancedRun) {
throw new Error('Expected both baseline and scratchpad runs to exist');
}
expectSuccessfulExtractionRun(baselineRun);
expectSuccessfulExtractionRun(enhancedRun);
return {
trial,
baseline: await summarizeScratchpadRun(
baselineOutcome,
baselineRun,
baselineScenario,
),
enhanced: await summarizeScratchpadRun(
enhancedOutcome,
enhancedRun,
enhancedScenario,
),
};
}
async function runScratchpadStatsReport(
trials: number,
): Promise<ScratchpadStatsReport> {
const results: ScratchpadStatsTrial[] = [];
for (let trial = 1; trial <= trials; trial++) {
results.push(await runScratchpadStatsTrial(trial));
}
const baseline = averageScratchpadRuns(
results.map((result) => result.baseline),
);
const enhanced = averageScratchpadRuns(
results.map((result) => result.enhanced),
);
return {
generatedAt: new Date().toISOString(),
trials,
aggregate: {
baseline,
enhanced,
},
deltas: diffScratchpadAggregates(baseline, enhanced),
results,
};
}
async function writeScratchpadStatsReport(
report: ScratchpadStatsReport,
): Promise<string> {
const outputPath = path.resolve(
process.cwd(),
'evals/logs/skill_extraction_scratchpad_stats.json',
);
await fsp.mkdir(path.dirname(outputPath), { recursive: true });
await fsp.writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`);
return outputPath;
}
async function readSkillBodies(skillsDir: string): Promise<string[]> {
const bodies: string[] = [];
try {
const entries = await fsp.readdir(skillsDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
try {
bodies.push(
await fsp.readFile(
path.join(skillsDir, entry.name, 'SKILL.md'),
'utf-8',
),
);
} catch {
// Ignore incomplete skill directories so one bad artifact does not hide
// valid skills created in the same eval run.
}
}
return bodies;
} catch {
return [];
}
}
function expectSuccessfulExtractionRun(run: ExtractionRunSnapshot): void {
expect(run.turnCount).toBeGreaterThan(0);
expect(run.turnCount).toBeLessThanOrEqual(30);
expect(run.durationMs).toBeGreaterThan(0);
expect(run.terminateReason).toBe('GOAL');
}
function scoreSkillQuality(
skillBodies: string[],
signals: SkillQualitySignal[],
): { score: number; maxScore: number; missing: string[] } {
const combined = skillBodies.join('\n\n');
const matched = signals.filter((signal) => signal.pattern.test(combined));
return {
score: matched.length,
maxScore: signals.length,
missing: signals
.filter((signal) => !signal.pattern.test(combined))
.map((signal) => signal.label),
};
}
const SETTINGS_SKILL_QUALITY_SIGNALS: SkillQualitySignal[] = [
{ label: 'predocs command', pattern: /npm run predocs:settings/i },
{ label: 'schema command', pattern: /npm run schema:settings/i },
{ label: 'docs command', pattern: /npm run docs:settings/i },
{ label: 'verification guidance', pattern: /verif(?:y|ication)/i },
{
label: 'generated docs warning or ordering constraint',
pattern:
/do not hand-edit|manual edits|exact command order|preserve.*order/i,
},
];
const DB_MIGRATION_SKILL_QUALITY_SIGNALS: SkillQualitySignal[] = [
{ label: 'db check command', pattern: /npm run db:check/i },
{ label: 'db migrate command', pattern: /npm run db:migrate/i },
{ label: 'db validate command', pattern: /npm run db:validate/i },
{ label: 'rollback guidance', pattern: /npm run db:rollback|rollback/i },
{
label: 'ordering constraint',
pattern: /check.*migrate.*validate|ordering is critical|mandatory/i,
},
];
/**
* Shared configOverrides for all skill extraction component evals.
* - experimentalAutoMemory: enables the Auto Memory skill extraction pipeline.
* - approvalMode: YOLO auto-approves tool calls (write_file, read_file) so the
* background agent can execute without interactive confirmation.
*/
const EXTRACTION_CONFIG_OVERRIDES = {
experimentalAutoMemory: true,
approvalMode: ApprovalMode.YOLO,
};
function parseScratchpadStatsTrials(): number {
const configured = Number.parseInt(
process.env['SCRATCHPAD_STATS_TRIALS'] ?? '8',
10,
);
return Number.isFinite(configured) && configured > 0 ? configured : 8;
}
const SCRATCHPAD_STATS_TRIALS = parseScratchpadStatsTrials();
describe('Skill Extraction', () => {
componentEvalTest('USUALLY_PASSES', {
suiteName: 'skill-extraction',
suiteType: 'component-level',
name: 'ignores one-off incidents even when session summaries look similar',
files: WORKSPACE_FILES,
timeout: 180000,
configOverrides: EXTRACTION_CONFIG_OVERRIDES,
setup: async (config) => {
await seedSessions(config, [
{
sessionId: 'incident-login-redirect',
summary: 'Debug login redirect loop in staging',
timestampOffsetMinutes: 420,
userTurns: [
'We only need a one-off fix for incident INC-4412 on branch hotfix/login-loop.',
'The exact failing string is ERR_REDIRECT_4412 and this workaround is incident-specific.',
'Patch packages/auth/src/redirect.ts just for this branch and do not generalize it.',
'The thing that worked was deleting the stale staging cookie before retrying.',
'This is not a normal workflow and should not become a reusable instruction.',
'It only reproduced against the 2026-04-08 staging rollout.',
'After the cookie clear, the branch-specific redirect logic passed.',
'Do not turn this incident writeup into a standing process.',
'Yes, the hotfix worked for this exact redirect-loop incident.',
'Close out INC-4412 once the staging login succeeds again.',
],
},
{
sessionId: 'incident-login-timeout',
summary: 'Debug login callback timeout in staging',
timestampOffsetMinutes: 360,
userTurns: [
'This is another one-off staging incident, this time TICKET-991 for callback timeout.',
'The exact failing string is ERR_CALLBACK_TIMEOUT_991 and it is unrelated to the redirect loop.',
'The temporary fix was rotating the staging secret and deleting a bad feature-flag row.',
'Do not write a generic login-debugging playbook from this.',
'This only applied to the callback timeout during the April rollout.',
'The successful fix was specific to the stale secret in staging.',
'It does not define a durable repo workflow for future tasks.',
'After rotating the secret, the callback timeout stopped reproducing.',
'Treat this as incident response only, not a reusable skill.',
'Once staging passed again, we closed TICKET-991.',
],
},
]);
},
assert: async (config) => {
const { state, skillsDir } = await runExtractionAndReadState(config);
const skillBodies = await readSkillBodies(skillsDir);
expect(state.runs).toHaveLength(1);
expect(state.runs[0].sessionIds).toHaveLength(2);
expect(state.runs[0].skillsCreated).toEqual([]);
expect(skillBodies).toEqual([]);
},
});
componentEvalTest('USUALLY_PASSES', {
suiteName: 'skill-extraction',
suiteType: 'component-level',
name: 'extracts a repeated project-specific workflow into a skill',
files: WORKSPACE_FILES,
timeout: 180000,
configOverrides: EXTRACTION_CONFIG_OVERRIDES,
setup: async (config) => {
await seedSessions(config, [
{
sessionId: 'settings-docs-regen-1',
summary: 'Update settings docs after adding a config option',
timestampOffsetMinutes: 420,
userTurns: [
'When we add a new config option, we have to regenerate the settings docs in a specific order.',
'The sequence that worked was npm run predocs:settings, npm run schema:settings, then npm run docs:settings.',
'Do not hand-edit generated settings docs.',
'If predocs is skipped, the generated schema docs miss the new defaults.',
'Update the source first, then run that generation sequence.',
'After regenerating, verify the schema output and docs changed together.',
'We used this same sequence the last time we touched settings docs.',
'That ordered workflow passed and produced the expected generated files.',
'Please keep the exact command order because reversing it breaks the output.',
'Yes, the generated settings docs were correct after those three commands.',
],
},
{
sessionId: 'settings-docs-regen-2',
summary: 'Regenerate settings schema docs for another new setting',
timestampOffsetMinutes: 360,
userTurns: [
'We are touching another setting, so follow the same settings-doc regeneration workflow again.',
'Run npm run predocs:settings before npm run schema:settings and npm run docs:settings.',
'The project keeps generated settings docs in sync through those commands, not manual edits.',
'Skipping predocs caused stale defaults in the generated output before.',
'Change the source, then execute the same three commands in order.',
'Verify both the schema artifact and docs update together after regeneration.',
'This is the recurring workflow we use whenever a setting changes.',
'The exact order worked again on this second settings update.',
'Please preserve that ordering constraint for future settings changes.',
'Confirmed: the settings docs regenerated correctly with the same command sequence.',
],
},
]);
},
assert: async (config) => {
const { state, skillsDir } = await runExtractionAndReadState(config);
const skillBodies = await readSkillBodies(skillsDir);
const combinedSkills = skillBodies.join('\n\n');
const quality = scoreSkillQuality(
skillBodies,
SETTINGS_SKILL_QUALITY_SIGNALS,
);
expect(state.runs).toHaveLength(1);
expect(state.runs[0].sessionIds).toHaveLength(2);
expectSuccessfulExtractionRun(state.runs[0]);
expect(state.runs[0].skillsCreated.length).toBeGreaterThanOrEqual(1);
expect(skillBodies.length).toBeGreaterThanOrEqual(1);
expect(combinedSkills).toContain('npm run predocs:settings');
expect(combinedSkills).toContain('npm run schema:settings');
expect(combinedSkills).toContain('npm run docs:settings');
expect(combinedSkills).toMatch(/verif(?:y|ication)/i);
expect(
quality.score,
`missing quality signals: ${quality.missing.join(', ')}`,
).toBeGreaterThanOrEqual(4);
// Verify the extraction agent activated skill-creator for design guidance.
expect(config.getSkillManager().isSkillActive('skill-creator')).toBe(
true,
);
},
});
componentEvalTest('USUALLY_PASSES', {
suiteName: 'skill-extraction',
suiteType: 'component-level',
name: 'memory scratchpad improves repeated-workflow recall versus summary-only index',
files: WORKSPACE_FILES,
timeout: 360000,
configOverrides: EXTRACTION_CONFIG_OVERRIDES,
assert: async () => {
const baselineScenario = createWorkflowComparisonSessions(false);
const enhancedScenario = createWorkflowComparisonSessions(true);
const baselineOutcome = await runScenarioWithFreshRig(
baselineScenario.sessions,
);
const enhancedOutcome = await runScenarioWithFreshRig(
enhancedScenario.sessions,
);
const baselineRun = baselineOutcome.state.runs.at(-1);
const enhancedRun = enhancedOutcome.state.runs.at(-1);
if (!baselineRun || !enhancedRun) {
throw new Error('Expected both baseline and scratchpad runs to exist');
}
expectSuccessfulExtractionRun(baselineRun);
expectSuccessfulExtractionRun(enhancedRun);
const baselineRelevantReads = countMatchingIds(
baselineRun.processedSessions,
baselineScenario.relevantSessionIds,
);
const enhancedRelevantReads = countMatchingIds(
enhancedRun.processedSessions,
enhancedScenario.relevantSessionIds,
);
const baselineDistractorReads = countMatchingIds(
baselineRun.processedSessions,
baselineScenario.distractorSessionIds,
);
const enhancedDistractorReads = countMatchingIds(
enhancedRun.processedSessions,
enhancedScenario.distractorSessionIds,
);
const baselineSignalScore =
baselineRelevantReads - baselineDistractorReads;
const enhancedSignalScore =
enhancedRelevantReads - enhancedDistractorReads;
expect(enhancedRun.candidateSessions).toHaveLength(
enhancedScenario.sessions.length,
);
expect(enhancedRelevantReads).toBeGreaterThanOrEqual(2);
expect(enhancedRelevantReads).toBeGreaterThanOrEqual(
baselineRelevantReads,
);
expect(enhancedDistractorReads).toBeLessThanOrEqual(
baselineDistractorReads,
);
expect(enhancedSignalScore).toBeGreaterThan(baselineSignalScore);
},
});
if (process.env['RUN_SCRATCHPAD_STATS'] === '1') {
componentEvalTest('USUALLY_PASSES', {
suiteName: 'skill-extraction',
suiteType: 'component-level',
name: 'reports memory scratchpad retrieval statistics',
timeout: Math.max(360000, SCRATCHPAD_STATS_TRIALS * 150000),
configOverrides: EXTRACTION_CONFIG_OVERRIDES,
assert: async () => {
const report = await runScratchpadStatsReport(SCRATCHPAD_STATS_TRIALS);
const outputPath = await writeScratchpadStatsReport(report);
console.info(
`Wrote scratchpad stats report to ${outputPath}\n${JSON.stringify(
report.aggregate,
null,
2,
)}`,
);
expect(report.results).toHaveLength(SCRATCHPAD_STATS_TRIALS);
expect(report.aggregate.baseline.recallAvg).toBeGreaterThan(0);
expect(report.aggregate.enhanced.recallAvg).toBeGreaterThan(0);
},
});
} else {
it.skip('reports memory scratchpad retrieval statistics', () => {});
}
componentEvalTest('USUALLY_PASSES', {
suiteName: 'skill-extraction',
suiteType: 'component-level',
name: 'extracts a repeated multi-step migration workflow with ordering constraints',
files: WORKSPACE_FILES,
timeout: 180000,
configOverrides: EXTRACTION_CONFIG_OVERRIDES,
setup: async (config) => {
await seedSessions(config, [
{
sessionId: 'db-migration-v12',
summary: 'Run database migration for v12 schema update',
timestampOffsetMinutes: 420,
userTurns: [
'Every time we change the database schema we follow a specific migration workflow.',
'First run npm run db:check to verify no pending migrations conflict.',
'Then run npm run db:migrate to apply the new migration files.',
'After migration, always run npm run db:validate to confirm schema integrity.',
'If db:validate fails, immediately run npm run db:rollback before anything else.',
'Never skip db:check — last time we did, two migrations collided and corrupted the index.',
'The ordering is critical: check, migrate, validate. Reversing migrate and validate caused silent data loss before.',
'This v12 migration passed after following that exact sequence.',
'We use this same three-step workflow every time the schema changes.',
'Confirmed: db:check, db:migrate, db:validate completed successfully for v12.',
],
},
{
sessionId: 'db-migration-v13',
summary: 'Run database migration for v13 schema update',
timestampOffsetMinutes: 360,
userTurns: [
'New schema change for v13, following the same database migration workflow as before.',
'Start with npm run db:check to ensure no conflicting pending migrations.',
'Then npm run db:migrate to apply the v13 migration files.',
'Then npm run db:validate to confirm the schema is consistent.',
'If validation fails, run npm run db:rollback immediately — do not attempt manual fixes.',
'We learned the hard way that skipping db:check causes index corruption.',
'The check-migrate-validate order is mandatory for every schema change.',
'This is the same recurring workflow we used for v12 and earlier migrations.',
'The v13 migration passed with the same three-step sequence.',
'Confirmed: the standard db migration workflow succeeded again for v13.',
],
},
]);
},
assert: async (config) => {
const { state, skillsDir } = await runExtractionAndReadState(config);
const skillBodies = await readSkillBodies(skillsDir);
const combinedSkills = skillBodies.join('\n\n');
const quality = scoreSkillQuality(
skillBodies,
DB_MIGRATION_SKILL_QUALITY_SIGNALS,
);
expect(state.runs).toHaveLength(1);
expect(state.runs[0].sessionIds).toHaveLength(2);
expectSuccessfulExtractionRun(state.runs[0]);
expect(state.runs[0].skillsCreated.length).toBeGreaterThanOrEqual(1);
expect(skillBodies.length).toBeGreaterThanOrEqual(1);
expect(combinedSkills).toContain('npm run db:check');
expect(combinedSkills).toContain('npm run db:migrate');
expect(combinedSkills).toContain('npm run db:validate');
expect(combinedSkills).toMatch(/rollback/i);
expect(
quality.score,
`missing quality signals: ${quality.missing.join(', ')}`,
).toBeGreaterThanOrEqual(4);
// Verify the extraction agent activated skill-creator for design guidance.
expect(config.getSkillManager().isSkillActive('skill-creator')).toBe(
true,
);
},
});
});
-26
View File
@@ -1,26 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export function countMatchingIds<T extends { sessionId: string }>(
items: T[],
expectedIds: string[],
): number {
const expected = new Set(expectedIds);
return items.filter((item) => expected.has(item.sessionId)).length;
}
export function roundStat(value: number | null): number | null {
return value === null ? null : Number(value.toFixed(4));
}
export function average(values: number[]): number {
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
export function averageNullable(values: Array<number | null>): number | null {
const numericValues = values.filter((value) => value !== null);
return numericValues.length === 0 ? null : average(numericValues);
}
-131
View File
@@ -1,131 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { TRACKER_CREATE_TASK_TOOL_NAME } from '@google/gemini-cli-core';
import { evalTest, TEST_AGENTS } from './test-helper.js';
describe('subtask delegation eval test cases', () => {
/**
* Checks that the main agent can correctly decompose a complex, sequential
* task into subtasks using the task tracker and delegate each to the appropriate expert subagent.
*
* The task requires:
* 1. Reading requirements (researcher)
* 2. Implementing logic (developer)
* 3. Documenting (doc expert)
*/
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should delegate sequential subtasks to relevant experts using the task tracker',
params: {
settings: {
experimental: {
enableAgents: true,
taskTracker: true,
},
},
},
prompt:
'Please read the requirements in requirements.txt using a researcher, then implement the requested logic in src/logic.ts using a developer, and finally document the implementation in docs/logic.md using a documentation expert.',
files: {
'.gemini/agents/researcher.md': `---
name: researcher
description: Expert in reading files and extracting requirements.
tools:
- read_file
---
You are the researcher. Read the provided file and extract requirements.`,
'.gemini/agents/developer.md': `---
name: developer
description: Expert in implementing logic in TypeScript.
tools:
- write_file
---
You are the developer. Implement the requested logic in the specified file.`,
'.gemini/agents/doc-expert.md': `---
name: doc-expert
description: Expert in writing technical documentation.
tools:
- write_file
---
You are the doc expert. Document the provided implementation clearly.`,
'requirements.txt':
'Implement a function named "calculateSum" that adds two numbers.',
},
assert: async (rig, _result) => {
// Verify tracker tasks were created
const wasCreateCalled = await rig.waitForToolCall(
TRACKER_CREATE_TASK_TOOL_NAME,
);
expect(wasCreateCalled).toBe(true);
const toolLogs = rig.readToolLogs();
const createCalls = toolLogs.filter(
(l) => l.toolRequest.name === TRACKER_CREATE_TASK_TOOL_NAME,
);
expect(createCalls.length).toBeGreaterThanOrEqual(3);
await rig.expectToolCallSuccess([
'researcher',
'developer',
'doc-expert',
]);
const logicFile = rig.readFile('src/logic.ts');
const docFile = rig.readFile('docs/logic.md');
expect(logicFile).toContain('calculateSum');
expect(docFile).toBeTruthy();
},
});
/**
* Checks that the main agent can delegate a batch of independent subtasks
* to multiple subagents in parallel using the task tracker to manage state.
*/
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should delegate independent subtasks to specialists using the task tracker',
params: {
settings: {
experimental: {
enableAgents: true,
taskTracker: true,
},
},
},
prompt:
'Please update the project for internationalization (i18n), audit the security of the current code, and update the CSS to use a blue theme. Use specialized experts for each task.',
files: {
...TEST_AGENTS.I18N_AGENT.asFile(),
...TEST_AGENTS.SECURITY_AGENT.asFile(),
...TEST_AGENTS.CSS_AGENT.asFile(),
'index.ts': 'console.log("Hello World");',
},
assert: async (rig, _result) => {
// Verify tracker tasks were created
const wasCreateCalled = await rig.waitForToolCall(
TRACKER_CREATE_TASK_TOOL_NAME,
);
expect(wasCreateCalled).toBe(true);
const toolLogs = rig.readToolLogs();
const createCalls = toolLogs.filter(
(l) => l.toolRequest.name === TRACKER_CREATE_TASK_TOOL_NAME,
);
expect(createCalls.length).toBeGreaterThanOrEqual(3);
await rig.expectToolCallSuccess([
TEST_AGENTS.I18N_AGENT.name,
TEST_AGENTS.SECURITY_AGENT.name,
TEST_AGENTS.CSS_AGENT.name,
]);
},
});
});
-1
View File
@@ -172,7 +172,6 @@ export async function internalEvalTest(evalCase: EvalCase) {
timeout: evalCase.timeout,
env: {
GEMINI_CLI_ACTIVITY_LOG_TARGET: activityLogFile,
GEMINI_CLI_TRUST_WORKSPACE: 'true',
},
});
+8 -52
View File
@@ -62,13 +62,11 @@ describe('tracker_mode', () => {
'Expected tracker_update_task tool to be called',
).toBe(true);
const updateCalls = toolLogs.filter(
const updateCall = toolLogs.find(
(log) => log.toolRequest.name === TRACKER_UPDATE_TASK_TOOL_NAME,
);
expect(updateCalls.length).toBeGreaterThan(0);
const updateArgs = JSON.parse(
updateCalls[updateCalls.length - 1].toolRequest.args,
);
expect(updateCall).toBeDefined();
const updateArgs = JSON.parse(updateCall!.toolRequest.args);
expect(updateArgs.status).toBe('closed');
const loginContent = fs.readFileSync(
@@ -121,8 +119,6 @@ describe('tracker_mode', () => {
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should correctly identify the task tracker storage location from the system prompt',
params: {
settings: { experimental: { taskTracker: true } },
@@ -130,52 +126,12 @@ describe('tracker_mode', () => {
prompt:
'Where is my task tracker storage located? Please provide the absolute path in your response.',
assert: async (rig, result) => {
// The response should contain the dynamic path which follows the .gemini/tmp/.../tracker structure.
// The rig sets GEMINI_CLI_HOME to rig.homeDir
const homeDir = rig.homeDir!;
// The response should contain the dynamic path which includes the home directory
// and follows the .gemini/tmp/.../tracker structure.
expect(result).toContain(homeDir);
expect(result).toMatch(/\.gemini\/tmp\/.*\/tracker/);
},
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'should update the tracker in the same turn as the task completion to save turns',
params: {
settings: { experimental: { taskTracker: true } },
},
files: FILES,
prompt:
'We have a bug in src/login.js: the password check is missing. Fix this bug. Then, create a new file src/auth.js that exports a simple verifyToken function. Please organize this into tasks and execute them.',
assert: async (rig, result) => {
await rig.waitForToolCall(TRACKER_CREATE_TASK_TOOL_NAME);
await rig.waitForToolCall(TRACKER_UPDATE_TASK_TOOL_NAME);
const toolLogs = rig.readToolLogs();
// Get the prompt ID of the fix for login.js
const loginEditCalls = toolLogs.filter(
(log) =>
(log.toolRequest.name === 'replace' ||
log.toolRequest.name === 'write_file') &&
log.toolRequest.args.includes('login.js'),
);
expect(loginEditCalls.length).toBeGreaterThan(0);
const loginEditPromptId =
loginEditCalls[loginEditCalls.length - 1].toolRequest.prompt_id;
// Verify there is an update to the tracker in the exact same turn
const parallelTrackerUpdates = toolLogs.filter(
(log) =>
log.toolRequest.name === TRACKER_UPDATE_TASK_TOOL_NAME &&
log.toolRequest.prompt_id === loginEditPromptId,
);
expect(
parallelTrackerUpdates.length,
'Expected tracker_update_task to be called in the same turn as the login.js fix',
).toBeGreaterThan(0);
assertModelHasOutput(result);
},
});
});
-13
View File
@@ -1,13 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"paths": {
"@google/gemini-cli-core": ["../packages/core/index.ts"],
"@google/gemini-cli": ["../packages/cli/index.ts"]
}
},
"include": ["**/*.ts"],
"exclude": ["logs"],
"references": [{ "path": "../packages/core" }, { "path": "../packages/cli" }]
}
-2
View File
@@ -7,8 +7,6 @@
import { evalTest, TestRig } from './test-helper.js';
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'Reproduction: Agent uses Object.create() for cloning/delegation',
prompt:
'Create a utility function `createScopedConfig(config: Config, additionalDirectories: string[]): Config` in `packages/core/src/config/scoped-config.ts` that returns a new Config instance. This instance should override `getWorkspaceContext()` to include the additional directories, but delegate all other method calls (like `isPathAllowed` or `validatePathAccess`) to the original config. Note that `Config` is a complex class with private state and cannot be easily shallow-copied or reconstructed.',
+6 -18
View File
@@ -21,8 +21,6 @@ describe('update_topic_behavior', () => {
* more than 1/4 turns.
*/
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'update_topic should be used at start, end and middle for complex tasks',
prompt: `Create a simple users REST API using Express.
1. Initialize a new npm project and install express.
@@ -43,7 +41,7 @@ describe('update_topic_behavior', () => {
2,
),
'.gemini/settings.json': JSON.stringify({
general: {
experimental: {
topicUpdateNarration: true,
},
}),
@@ -119,15 +117,13 @@ describe('update_topic_behavior', () => {
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'update_topic should NOT be used for informational coding tasks (Obvious)',
approvalMode: 'default',
prompt:
'Explain the difference between Map and Object in JavaScript and provide a performance-focused code snippet for each.',
files: {
'.gemini/settings.json': JSON.stringify({
general: {
experimental: {
topicUpdateNarration: true,
},
}),
@@ -146,8 +142,6 @@ describe('update_topic_behavior', () => {
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'update_topic should NOT be used for surgical symbol searches (Grey Area)',
approvalMode: 'default',
prompt:
@@ -156,7 +150,7 @@ describe('update_topic_behavior', () => {
'packages/core/src/tools/tool-names.ts':
"export const UPDATE_TOPIC_TOOL_NAME = 'update_topic';",
'.gemini/settings.json': JSON.stringify({
general: {
experimental: {
topicUpdateNarration: true,
},
}),
@@ -175,8 +169,6 @@ describe('update_topic_behavior', () => {
});
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'update_topic should be used for medium complexity multi-step tasks',
prompt:
'Refactor the `users-api` project. Move the routing logic from src/app.ts into a new file src/routes.ts, and update app.ts to use the new routes file.',
@@ -204,7 +196,7 @@ app.post('/users', (req, res) => {
export default app;
`,
'.gemini/settings.json': JSON.stringify({
general: {
experimental: {
topicUpdateNarration: true,
},
}),
@@ -220,9 +212,7 @@ export default app;
expect(topicCalls.length).toBeGreaterThanOrEqual(2);
// Verify it actually did the refactoring to ensure it didn't just fail immediately
expect(fs.existsSync(path.join(rig.testDir!, 'src/routes.ts'))).toBe(
true,
);
expect(fs.existsSync(path.join(rig.testDir, 'src/routes.ts'))).toBe(true);
},
});
@@ -234,8 +224,6 @@ export default app;
* the prompt change that improves the behavior.
*/
evalTest('USUALLY_PASSES', {
suiteName: 'default',
suiteType: 'behavioral',
name: 'update_topic should not be called twice in a row',
prompt: `
We need to build a C compiler.
@@ -249,7 +237,7 @@ export default app;
files: {
'package.json': JSON.stringify({ name: 'test-project' }),
'.gemini/settings.json': JSON.stringify({
general: {
experimental: {
topicUpdateNarration: true,
},
}),
-1
View File
@@ -70,7 +70,6 @@ describe('ACP telemetry', () => {
GEMINI_API_KEY: 'fake-key',
GEMINI_CLI_HOME: rig.homeDir!,
GEMINI_TELEMETRY_ENABLED: 'true',
GEMINI_TELEMETRY_TRACES_ENABLED: 'true',
GEMINI_TELEMETRY_TARGET: 'local',
GEMINI_TELEMETRY_OUTFILE: telemetryPath,
},
+1 -5
View File
@@ -39,11 +39,7 @@ describe('web-fetch rate limiting', () => {
const rateLimitedCalls = toolLogs.filter(
(log) =>
log.toolRequest.name === 'web_fetch' &&
(
('error' in log.toolRequest
? (log.toolRequest as unknown as Record<string, string>)['error']
: '') as string
)?.includes('Rate limit exceeded'),
log.toolRequest.error?.includes('Rate limit exceeded'),
);
expect(rateLimitedCalls.length).toBeGreaterThan(0);
@@ -8,16 +8,7 @@ import { expect, describe, it, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';
// Skip on macOS: every interactive test in this file is chronically flaky
// because the captured pty buffer contains the CLI's startup escape
// sequences (`q4;?m...true color warning`) instead of the streamed output,
// causing `expectText(...)` to time out. Reproducible across unrelated
// runs on `main` (24740161950, 24739323404) and on consecutive merge-queue
// gates for #25753 (24743605639, 24747624513) — different tests in the
// same describe fail on different runs. Not specific to any model.
const skipOnDarwin = process.platform === 'darwin';
describe.skipIf(skipOnDarwin)('Interactive Mode', () => {
describe('Interactive Mode', () => {
let rig: TestRig;
beforeEach(() => {
+1 -3
View File
@@ -134,9 +134,7 @@ describe('file-system', () => {
).toBeTruthy();
const newFileContent = rig.readFile(fileName);
// Trim to tolerate models that idiomatically append a trailing newline.
// This test is about path-with-spaces handling, not whitespace fidelity.
expect(newFileContent.trim()).toBe('hello');
expect(newFileContent).toBe('hello');
});
it('should perform a read-then-write sequence', async () => {
+1 -2
View File
@@ -164,8 +164,7 @@ describe.skipIf(skipFlaky)(
);
expect(blockHook).toBeDefined();
expect(
(blockHook?.hookCall.stdout || '') +
(blockHook?.hookCall.stderr || ''),
blockHook?.hookCall.stdout + blockHook?.hookCall.stderr,
).toContain(blockMsg);
});
@@ -1,2 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"list_mcp_resources","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Here are the resources: test://resource1"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
@@ -1,2 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"read_mcp_resource","args":{"uri":"test://resource1"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The content is: content of resource 1"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
@@ -1,4 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"list_mcp_resources","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Here are the resources: test://resource1"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"read_mcp_resource","args":{"uri":"test://resource1"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The content is: content of resource 1"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":10,"totalTokenCount":20}}]}
-178
View File
@@ -1,178 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import fs from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
describe('mcp-resources-integration', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => await rig.cleanup());
it('should list mcp resources', async () => {
await rig.setup('mcp-list-resources-test', {
settings: {
model: {
name: 'gemini-3-flash-preview',
},
},
fakeResponsesPath: join(__dirname, 'mcp-list-resources.responses'),
});
// Workaround for ProjectRegistry save issue
const userGeminiDir = join(rig.homeDir!, '.gemini');
fs.writeFileSync(join(userGeminiDir, 'projects.json'), '{"projects":{}}');
// Add a dummy server to get setup done
rig.addTestMcpServer('resource-server', {
name: 'resource-server',
tools: [],
});
// Overwrite the script with resource support
const scriptPath = join(rig.testDir!, 'test-mcp-resource-server.mjs');
const scriptContent = `
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
ListResourcesRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{
name: 'resource-server',
version: '1.0.0',
},
{
capabilities: {
resources: {},
},
},
);
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'test://resource1',
name: 'Resource 1',
mimeType: 'text/plain',
description: 'A test resource',
}
],
};
});
const transport = new StdioServerTransport();
await server.connect(transport);
`;
fs.writeFileSync(scriptPath, scriptContent);
const output = await rig.run({
args: 'List all available MCP resources.',
env: { GEMINI_API_KEY: 'dummy' },
});
const foundCall = await rig.waitForToolCall('list_mcp_resources');
expect(foundCall).toBeTruthy();
expect(output).toContain('test://resource1');
}, 60000);
it('should read mcp resource', async () => {
await rig.setup('mcp-read-resource-test', {
settings: {
model: {
name: 'gemini-3-flash-preview',
},
},
fakeResponsesPath: join(__dirname, 'mcp-read-resource.responses'),
});
// Workaround for ProjectRegistry save issue
const userGeminiDir = join(rig.homeDir!, '.gemini');
fs.writeFileSync(join(userGeminiDir, 'projects.json'), '{"projects":{}}');
// Add a dummy server to get setup done
rig.addTestMcpServer('resource-server', {
name: 'resource-server',
tools: [],
});
// Overwrite the script with resource support
const scriptPath = join(rig.testDir!, 'test-mcp-resource-server.mjs');
const scriptContent = `
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{
name: 'resource-server',
version: '1.0.0',
},
{
capabilities: {
resources: {},
},
},
);
// Need to provide list resources so the tool is active!
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'test://resource1',
name: 'Resource 1',
mimeType: 'text/plain',
description: 'A test resource',
}
],
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (request.params.uri === 'test://resource1') {
return {
contents: [
{
uri: 'test://resource1',
mimeType: 'text/plain',
text: 'This is the content of resource 1',
}
],
};
}
throw new Error('Resource not found');
});
const transport = new StdioServerTransport();
await server.connect(transport);
`;
fs.writeFileSync(scriptPath, scriptContent);
const output = await rig.run({
args: 'Read the MCP resource test://resource1.',
env: { GEMINI_API_KEY: 'dummy' },
});
const foundCall = await rig.waitForToolCall('read_mcp_resource');
expect(foundCall).toBeTruthy();
expect(output).toContain('content of resource 1');
}, 60000);
});
+7 -25
View File
@@ -81,10 +81,7 @@ describe('Plan Mode', () => {
await rig.run({
approvalMode: 'plan',
args:
'Create a file called plan.md in the plans directory with the ' +
'content "# Plan". Treat this as a Directive and write the file ' +
'immediately without proposing strategy or asking for confirmation.',
args: 'Create a file called plan.md in the plans directory.',
});
const toolLogs = rig.readToolLogs();
@@ -111,7 +108,7 @@ describe('Plan Mode', () => {
).toBeDefined();
expect(
planWrite?.toolRequest.success,
`Expected write_file to succeed, but it failed with error: ${'error' in (planWrite?.toolRequest || {}) ? (planWrite?.toolRequest as unknown as Record<string, string>)['error'] : 'unknown'}`,
`Expected write_file to succeed, but it failed with error: ${planWrite?.toolRequest.error}`,
).toBe(true);
});
@@ -197,11 +194,7 @@ describe('Plan Mode', () => {
await rig.run({
approvalMode: 'plan',
args:
'Create a file called plan-no-session.md in the plans directory ' +
'with the content "# Plan". Treat this as a Directive and write ' +
'the file immediately without proposing strategy or asking for ' +
'confirmation.',
args: 'Create a file called plan-no-session.md in the plans directory.',
});
const toolLogs = rig.readToolLogs();
@@ -228,7 +221,7 @@ describe('Plan Mode', () => {
).toBeDefined();
expect(
planWrite?.toolRequest.success,
`Expected write_file to succeed, but it failed with error: ${'error' in (planWrite?.toolRequest || {}) ? (planWrite?.toolRequest as unknown as Record<string, string>)['error'] : 'unknown'}`,
`Expected write_file to succeed, but it failed with error: ${planWrite?.toolRequest.error}`,
).toBe(true);
});
it('should switch from a pro model to a flash model after exiting plan mode', async () => {
@@ -277,24 +270,13 @@ describe('Plan Mode', () => {
);
const apiRequests = rig.readAllApiRequest();
const modelNames = apiRequests.map(
(r) =>
('model' in (r.attributes || {})
? (r.attributes as unknown as Record<string, string>)['model']
: 'unknown') || 'unknown',
);
const modelNames = apiRequests.map((r) => r.attributes?.model || 'unknown');
const proRequests = apiRequests.filter((r) =>
('model' in (r.attributes || {})
? (r.attributes as unknown as Record<string, string>)['model']
: 'unknown'
)?.includes('pro'),
r.attributes?.model?.includes('pro'),
);
const flashRequests = apiRequests.filter((r) =>
('model' in (r.attributes || {})
? (r.attributes as unknown as Record<string, string>)['model']
: 'unknown'
)?.includes('flash'),
r.attributes?.model?.includes('flash'),
);
expect(
+1 -5
View File
@@ -5,9 +5,5 @@
"allowJs": true
},
"include": ["**/*.ts"],
"references": [
{ "path": "../packages/core" },
{ "path": "../packages/test-utils" },
{ "path": "../packages/cli" }
]
"references": [{ "path": "../packages/core" }]
}
-76
View File
@@ -1,76 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import {
WhisperModelManager,
WhisperTranscriptionProvider,
} from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import commandExists from 'command-exists';
describe('Voice Mode Integration', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => await rig.cleanup());
it('should be able to download tiny whisper model', async () => {
// This test doesn't require the binary, only network access.
// However, it's slow and downloads 75MB. We'll keep it for now but
// wrap it in a try-catch to avoid failing on network flakiness in CI.
const manager = new WhisperModelManager();
const modelName = 'ggml-tiny.en.bin';
try {
// Cleanup if already exists to ensure we actually test download
const modelPath = manager.getModelPath(modelName);
if (fs.existsSync(modelPath)) {
fs.unlinkSync(modelPath);
}
await manager.downloadModel(modelName);
expect(fs.existsSync(modelPath)).toBe(true);
expect(fs.statSync(modelPath).size).toBeGreaterThan(70 * 1024 * 1024); // ~75MB
} catch (e) {
console.warn(
'Skipping whisper model download test due to error (possibly network):',
e,
);
}
}, 300000); // 5 min timeout for download
it('should initialize WhisperTranscriptionProvider and handle process', async () => {
// Skip this test if whisper-stream is not installed (typical for CI)
try {
await commandExists('whisper-stream');
} catch {
console.log(
'Skipping Whisper transcription test: whisper-stream not found',
);
return;
}
const manager = new WhisperModelManager();
const modelName = 'ggml-tiny.en.bin';
if (!manager.isModelInstalled(modelName)) {
await manager.downloadModel(modelName);
}
const provider = new WhisperTranscriptionProvider({
modelPath: manager.getModelPath(modelName),
});
// Since we can't easily provide real mic input in CI,
// we just verify it can start and be disconnected.
await provider.connect();
provider.disconnect();
});
});
+36 -36
View File
@@ -1,55 +1,55 @@
{
"version": 1,
"updatedAt": "2026-04-20T18:04:59.671Z",
"updatedAt": "2026-04-10T15:36:04.547Z",
"scenarios": {
"multi-turn-conversation": {
"heapUsedMB": 68.8,
"heapTotalMB": 91.2,
"rssMB": 215.4,
"externalMB": 93.8,
"timestamp": "2026-04-20T18:02:40.101Z"
"heapUsedBytes": 120082704,
"heapTotalBytes": 177586176,
"rssBytes": 269172736,
"externalBytes": 4304053,
"timestamp": "2026-04-10T15:35:17.603Z"
},
"multi-function-call-repo-search": {
"heapUsedMB": 73.5,
"heapTotalMB": 93.1,
"rssMB": 223.6,
"externalMB": 97.7,
"timestamp": "2026-04-20T18:02:42.032Z"
"heapUsedBytes": 104644984,
"heapTotalBytes": 111575040,
"rssBytes": 204079104,
"externalBytes": 4304053,
"timestamp": "2026-04-10T15:35:22.480Z"
},
"idle-session-startup": {
"heapUsedMB": 69.8,
"heapTotalMB": 92.4,
"rssMB": 217.4,
"externalMB": 93.8,
"timestamp": "2026-04-20T18:02:36.294Z"
"heapUsedBytes": 119813672,
"heapTotalBytes": 177061888,
"rssBytes": 267943936,
"externalBytes": 4304053,
"timestamp": "2026-04-10T15:35:08.035Z"
},
"simple-prompt-response": {
"heapUsedMB": 69.5,
"heapTotalMB": 92.4,
"rssMB": 216.1,
"externalMB": 93.8,
"timestamp": "2026-04-20T18:02:38.198Z"
"heapUsedBytes": 119722064,
"heapTotalBytes": 177324032,
"rssBytes": 268812288,
"externalBytes": 4304053,
"timestamp": "2026-04-10T15:35:12.770Z"
},
"resume-large-chat-with-messages": {
"heapUsedMB": 887.1,
"heapTotalMB": 954.3,
"rssMB": 1109.6,
"externalMB": 103.2,
"timestamp": "2026-04-20T18:04:59.671Z"
"heapUsedBytes": 106545568,
"heapTotalBytes": 111509504,
"rssBytes": 202596352,
"externalBytes": 4306101,
"timestamp": "2026-04-10T15:36:04.547Z"
},
"resume-large-chat": {
"heapUsedMB": 885.6,
"heapTotalMB": 955.6,
"rssMB": 1107.8,
"externalMB": 110.5,
"timestamp": "2026-04-20T18:04:06.526Z"
"heapUsedBytes": 106513760,
"heapTotalBytes": 111509504,
"rssBytes": 202596352,
"externalBytes": 4306101,
"timestamp": "2026-04-10T15:35:59.528Z"
},
"large-chat": {
"heapUsedMB": 158.5,
"heapTotalMB": 193,
"rssMB": 787.9,
"externalMB": 104,
"timestamp": "2026-04-20T18:03:12.486Z"
"heapUsedBytes": 106471568,
"heapTotalBytes": 111509504,
"rssBytes": 202596352,
"externalBytes": 4306101,
"timestamp": "2026-04-10T15:35:53.180Z"
}
}
}
+10 -36
View File
@@ -16,21 +16,15 @@ import {
mkdirSync,
rmSync,
} from 'node:fs';
import { randomUUID, createHash } from 'node:crypto';
import { randomUUID } from 'node:crypto';
const __dirname = dirname(fileURLToPath(import.meta.url));
const BASELINES_PATH = join(__dirname, 'baselines.json');
const UPDATE_BASELINES = process.env['UPDATE_MEMORY_BASELINES'] === 'true';
function getProjectHash(projectRoot: string): string {
return createHash('sha256').update(projectRoot).digest('hex');
}
const TOLERANCE_PERCENT = 10;
// Fake API key for tests using fake responses
const TEST_ENV = {
GEMINI_API_KEY: 'fake-memory-test-key',
GEMINI_MEMORY_MONITOR_INTERVAL: '100',
};
const TEST_ENV = { GEMINI_API_KEY: 'fake-memory-test-key' };
describe('Memory Usage Tests', () => {
let harness: MemoryTestHarness;
@@ -62,7 +56,6 @@ describe('Memory Usage Tests', () => {
});
const result = await harness.runScenario(
rig,
'idle-session-startup',
async (recordSnapshot) => {
await rig.run({
@@ -92,7 +85,6 @@ describe('Memory Usage Tests', () => {
});
const result = await harness.runScenario(
rig,
'simple-prompt-response',
async (recordSnapshot) => {
await rig.run({
@@ -130,7 +122,6 @@ describe('Memory Usage Tests', () => {
];
const result = await harness.runScenario(
rig,
'multi-turn-conversation',
async (recordSnapshot) => {
// Run through all turns as a piped sequence
@@ -153,9 +144,6 @@ describe('Memory Usage Tests', () => {
);
} else {
harness.assertWithinBaseline(result);
harness.assertMemoryReturnsToBaseline(result.snapshots, 20);
const { leaked, message } = harness.analyzeSnapshots(result.snapshots);
if (leaked) console.warn(`${message}`);
}
});
@@ -180,7 +168,6 @@ describe('Memory Usage Tests', () => {
);
const result = await harness.runScenario(
rig,
'multi-function-call-repo-search',
async (recordSnapshot) => {
await rig.run({
@@ -202,7 +189,6 @@ describe('Memory Usage Tests', () => {
);
} else {
harness.assertWithinBaseline(result);
harness.assertMemoryReturnsToBaseline(result.snapshots, 20);
}
});
@@ -242,7 +228,6 @@ describe('Memory Usage Tests', () => {
});
const result = await harness.runScenario(
rig,
'large-chat',
async (recordSnapshot) => {
await rig.run({
@@ -272,21 +257,19 @@ describe('Memory Usage Tests', () => {
});
const result = await harness.runScenario(
rig,
'resume-large-chat',
async (recordSnapshot) => {
// Ensure the history file is linked
const targetChatsDir = join(
rig.homeDir!,
'.gemini',
rig.testDir!,
'tmp',
getProjectHash(rig.testDir!),
'test-project-hash',
'chats',
);
mkdirSync(targetChatsDir, { recursive: true });
const targetHistoryPath = join(
targetChatsDir,
'session-large-chat.json',
'large-chat-session.json',
);
if (existsSync(targetHistoryPath)) rmSync(targetHistoryPath);
copyFileSync(sharedHistoryPath, targetHistoryPath);
@@ -319,21 +302,19 @@ describe('Memory Usage Tests', () => {
});
const result = await harness.runScenario(
rig,
'resume-large-chat-with-messages',
async (recordSnapshot) => {
// Ensure the history file is linked
const targetChatsDir = join(
rig.homeDir!,
'.gemini',
rig.testDir!,
'tmp',
getProjectHash(rig.testDir!),
'test-project-hash',
'chats',
);
mkdirSync(targetChatsDir, { recursive: true });
const targetHistoryPath = join(
targetChatsDir,
'session-large-chat.json',
'large-chat-session.json',
);
if (existsSync(targetHistoryPath)) rmSync(targetHistoryPath);
copyFileSync(sharedHistoryPath, targetHistoryPath);
@@ -476,9 +457,6 @@ async function generateSharedLargeChatData(tempDir: string) {
// Generate responses for resumed chat
const resumeResponsesStream = createWriteStream(resumeResponsesPath);
for (let i = 0; i < 5; i++) {
// Doubling up on non-streaming responses to satisfy classifier and complexity checks
resumeResponsesStream.write(JSON.stringify(complexityResponse) + '\n');
resumeResponsesStream.write(JSON.stringify(summaryResponse) + '\n');
resumeResponsesStream.write(JSON.stringify(complexityResponse) + '\n');
resumeResponsesStream.write(
JSON.stringify({
@@ -511,12 +489,8 @@ async function generateSharedLargeChatData(tempDir: string) {
// Wait for streams to finish
await Promise.all([
new Promise((res) =>
activeResponsesStream.on('finish', () => res(undefined)),
),
new Promise((res) =>
resumeResponsesStream.on('finish', () => res(undefined)),
),
new Promise((res) => activeResponsesStream.on('finish', res)),
new Promise((res) => resumeResponsesStream.on('finish', res)),
]);
return {
+563 -865
View File
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.42.0-nightly.20260428.g59b2dea0e",
"version": "0.39.0-nightly.20260408.e77b22e63",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.42.0-nightly.20260428.g59b2dea0e"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.39.0-nightly.20260408.e77b22e63"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
@@ -62,7 +62,7 @@
"lint:ci": "npm run lint:all",
"lint:all": "node scripts/lint.js",
"format": "prettier --experimental-cli --write .",
"typecheck": "npm run typecheck --workspaces --if-present && tsc -b evals/tsconfig.json integration-tests/tsconfig.json memory-tests/tsconfig.json",
"typecheck": "npm run typecheck --workspaces --if-present",
"preflight": "npm run clean && npm ci && npm run format && npm run build && npm run lint:ci && npm run typecheck && npm run test:ci",
"prepare": "husky && npm run bundle",
"prepare:package": "node scripts/prepare-package.js",
@@ -81,7 +81,8 @@
"glob": "^12.0.0",
"node-domexception": "npm:empty@^0.10.1",
"prebuild-install": "npm:nop@1.0.0",
"cross-spawn": "^7.0.6"
"cross-spawn": "^7.0.6",
"minimatch": "^10.2.2"
},
"bin": {
"gemini": "bundle/gemini.js"
@@ -93,7 +94,6 @@
],
"devDependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
"read-package-up": "^11.0.0",
"@octokit/rest": "^22.0.0",
"@types/marked": "^5.0.2",
"@types/mime-types": "^3.0.1",
@@ -137,7 +137,6 @@
"strip-ansi": "^7.1.2",
"ts-prune": "^0.10.3",
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.30.1",
"vitest": "^3.2.4",
"yargs": "^17.7.2"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.42.0-nightly.20260428.g59b2dea0e",
"version": "0.39.0-nightly.20260408.e77b22e63",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
@@ -98,7 +98,6 @@ export function createMockConfig(
getMcpServers: vi.fn().mockReturnValue({}),
}),
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
getGitService: vi.fn(),
validatePathAccess: vi.fn().mockReturnValue(undefined),
getShellExecutionConfig: vi.fn().mockReturnValue({
@@ -112,7 +111,6 @@ export function createMockConfig(
}),
isContextManagementEnabled: vi.fn().mockReturnValue(false),
getContextManagementConfig: vi.fn().mockReturnValue({ enabled: false }),
getExperimentalGemma: vi.fn().mockReturnValue(false),
...overrides,
} as unknown as Config;
+12 -9
View File
@@ -9,11 +9,6 @@
import { spawn } from 'node:child_process';
import os from 'node:os';
import v8 from 'node:v8';
import {
RELAUNCH_EXIT_CODE,
getSpawnConfig,
getScriptArgs,
} from './src/utils/processUtils.js';
// --- Global Entry Point ---
@@ -79,10 +74,18 @@ async function run() {
// --- Lightweight Parent Process / Daemon ---
// We avoid importing heavy dependencies here to save ~1.5s of startup time.
const scriptArgs = getScriptArgs();
const memoryArgs = await getMemoryNodeArgs();
const { spawnArgs, env: newEnv } = getSpawnConfig(memoryArgs, scriptArgs);
const nodeArgs: string[] = [...process.execArgv];
const scriptArgs = process.argv.slice(2);
const memoryArgs = await getMemoryNodeArgs();
nodeArgs.push(...memoryArgs);
const script = process.argv[1];
nodeArgs.push(script);
nodeArgs.push(...scriptArgs);
const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' };
const RELAUNCH_EXIT_CODE = 199;
let latestAdminSettings: unknown = undefined;
// Prevent the parent process from exiting prematurely on signals.
@@ -94,7 +97,7 @@ async function run() {
const runner = () => {
process.stdin.pause();
const child = spawn(process.execPath, spawnArgs, {
const child = spawn(process.execPath, nodeArgs, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: newEnv,
});
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.42.0-nightly.20260428.g59b2dea0e",
"version": "0.39.0-nightly.20260408.e77b22e63",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -27,7 +27,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.42.0-nightly.20260428.g59b2dea0e"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.39.0-nightly.20260408.e77b22e63"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.16.1",
-81
View File
@@ -1,81 +0,0 @@
# Agent Client Protocol (ACP) Implementation
This directory contains the implementation of the Agent Client Protocol (ACP)
for the Gemini CLI. The ACP allows external clients (like IDE extensions) to
communicate with the Gemini CLI agent over a structured JSON-RPC based protocol.
## Directory Structure
Following Phase 1 of the modularization refactor, the ACP client is organized
into the following specialized modules, all sharing the `acp` prefix for
consistency:
- **[acpStdioTransport.ts](./acpStdioTransport.ts)**: Handles raw I/O. It sets
up the Web streams for standard input/output and creates the
`AgentSideConnection` using line-delimited JSON (ndjson).
- **[acpRpcDispatcher.ts](./acpRpcDispatcher.ts)**: Contains the `GeminiAgent`
class. This is the main entry point for incoming JSON-RPC messages. It
implements the protocol methods and delegates session-specific work to the
manager and individual sessions.
- **[acpSessionManager.ts](./acpSessionManager.ts)**: Manages multi-session
state. It handles session creation (`newSession`), loading (`loadSession`),
and configuration, isolating session state from the RPC routing.
- **[acpSession.ts](./acpSession.ts)**: Manages individual active chat sessions.
It handles prompt execution, `@path` file resolution, tool execution, command
interception, and streaming updates back to the client.
- **[acpUtils.ts](./acpUtils.ts)**: Contains shared helper functions, type
mappers (e.g., mapping internal tool kinds to ACP kinds), and Zod schemas used
across the modules.
- **[acpErrors.ts](./acpErrors.ts)**: Centralized error handling and mapping to
ACP-compliant error codes.
- **[acpCommandHandler.ts](./acpCommandHandler.ts)**: Handles interception and
execution of slash commands (e.g., `/memory`, `/init`) sent via ACP prompts.
- **[acpFileSystemService.ts](./acpFileSystemService.ts)**: Provides access to
the file system restricted by the workspace boundaries and permissions.
## Development Instructions
### Running Tests
Tests are co-located with the source files:
- `acpRpcDispatcher.test.ts`: Tests for initialization, authentication, and
handler delegation.
- `acpSessionManager.test.ts`: Tests for session lifecycle and configuration.
- `acpSession.test.ts`: Tests for prompt loops, tool execution, and @path
resolution.
- `acpResume.test.ts`: Integration tests for loading/resuming sessions.
To run specific tests, use Vitest with the workspace filter:
```bash
# General pattern
npm test -w @google/gemini-cli -- src/acp/<test-file-name>.ts
# Example
npm test -w @google/gemini-cli -- src/acp/acpRpcDispatcher.test.ts
```
Note: You may need to ensure your environment has Node available. If running in
a restricted environment, try sourcing NVM first:
```bash
source ~/.nvm/nvm.sh && nvm use default && npm test -w @google/gemini-cli -- src/acp/acpSession.test.ts
```
### Adding New Features
- **New RPC Method**: Add the method to `GeminiAgent` in `acpRpcDispatcher.ts`
and register it in the `AgentSideConnection` setup if necessary.
- **Session State**: If a feature requires storing state across turns within a
session, add it to the `Session` class in `acpSession.ts`.
- **Protocol Helpers**: Add any new mapping or serialization logic to
`acpUtils.ts`.
### Coding Conventions
- **Imports**: Use specific imports and do not import across package boundaries
using relative paths.
- **License Headers**: All new files must include the Apache-2.0 license header.
- **Type Safety**: Avoid using `any` assertions. Use Zod schemas to validate
untrusted input from the protocol.
File diff suppressed because it is too large Load Diff
@@ -1,20 +1,27 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
type ApprovalMode,
type Config,
type GeminiChat,
type ToolResult,
type ToolCallConfirmationDetails,
type FilterFilesOptions,
type ConversationRecord,
CoreToolCallStatus,
AuthType,
logToolCall,
convertToFunctionResponse,
ToolConfirmationOutcome,
clearCachedCredentialFile,
isNodeError,
getErrorMessage,
isWithinRoot,
getErrorStatus,
MCPServerConfig,
DiscoveredMCPTool,
StreamEventType,
ToolCallEvent,
@@ -22,42 +29,544 @@ import {
ReadManyFilesTool,
REFERENCE_CONTENT_START,
type RoutingContext,
createWorkingStdio,
startupProfiler,
Kind,
partListUnionToString,
LlmRole,
ApprovalMode,
getVersion,
convertSessionToClientHistory,
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
PREVIEW_GEMINI_MODEL,
PREVIEW_GEMINI_3_1_MODEL,
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL_AUTO,
getDisplayString,
processSingleFileContent,
InvalidStreamError,
type AgentLoopContext,
updatePolicy,
isNodeError,
getErrorMessage,
type FilterFilesOptions,
isTextPart,
} from '@google/gemini-cli-core';
import * as acp from '@agentclientprotocol/sdk';
import { AcpFileSystemService } from './fileSystemService.js';
import { getAcpErrorMessage } from './acpErrors.js';
import { Readable, Writable } from 'node:stream';
function hasMeta(obj: unknown): obj is { _meta?: Record<string, unknown> } {
return typeof obj === 'object' && obj !== null && '_meta' in obj;
}
import type { Content, Part, FunctionCall } from '@google/genai';
import type { LoadedSettings } from '../config/settings.js';
import {
SettingScope,
loadSettings,
type LoadedSettings,
} from '../config/settings.js';
import { createPolicyUpdater } from '../config/policy.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { randomUUID } from 'node:crypto';
import { CommandHandler } from './acpCommandHandler.js';
import {
toToolCallContent,
toPermissionOptions,
toAcpToolKind,
buildAvailableModes,
RequestPermissionResponseSchema,
} from './acpUtils.js';
import { z } from 'zod';
import { getAcpErrorMessage } from './acpErrors.js';
import { randomUUID } from 'node:crypto';
import { loadCliConfig, type CliArgs } from '../config/config.js';
import { runExitCleanup } from '../utils/cleanup.js';
import { SessionSelector } from '../utils/sessionUtils.js';
import { CommandHandler } from './commandHandler.js';
const RequestPermissionResponseSchema = z.object({
outcome: z.discriminatedUnion('outcome', [
z.object({ outcome: z.literal('cancelled') }),
z.object({
outcome: z.literal('selected'),
optionId: z.string(),
}),
]),
});
export async function runAcpClient(
config: Config,
settings: LoadedSettings,
argv: CliArgs,
) {
// ... (skip unchanged lines) ...
const { stdout: workingStdout } = createWorkingStdio();
const stdout = Writable.toWeb(workingStdout) as WritableStream;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const stdin = Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>;
const stream = acp.ndJsonStream(stdout, stdin);
const connection = new acp.AgentSideConnection(
(connection) => new GeminiAgent(config, settings, argv, connection),
stream,
);
// SIGTERM/SIGINT handlers (in sdk.ts) don't fire when stdin closes.
// We must explicitly await the connection close to flush telemetry.
// Use finally() to ensure cleanup runs even on stream errors.
await connection.closed.finally(runExitCleanup);
}
export class GeminiAgent {
private static callIdCounter = 0;
static generateCallId(name: string): string {
return `${name}-${Date.now()}-${++GeminiAgent.callIdCounter}`;
}
private sessions: Map<string, Session> = new Map();
private clientCapabilities: acp.ClientCapabilities | undefined;
private apiKey: string | undefined;
private baseUrl: string | undefined;
private customHeaders: Record<string, string> | undefined;
constructor(
private context: AgentLoopContext,
private settings: LoadedSettings,
private argv: CliArgs,
private connection: acp.AgentSideConnection,
) {}
async initialize(
args: acp.InitializeRequest,
): Promise<acp.InitializeResponse> {
this.clientCapabilities = args.clientCapabilities;
const authMethods = [
{
id: AuthType.LOGIN_WITH_GOOGLE,
name: 'Log in with Google',
description: 'Log in with your Google account',
},
{
id: AuthType.USE_GEMINI,
name: 'Gemini API key',
description: 'Use an API key with Gemini Developer API',
_meta: {
'api-key': {
provider: 'google',
},
},
},
{
id: AuthType.USE_VERTEX_AI,
name: 'Vertex AI',
description: 'Use an API key with Vertex AI GenAI API',
},
{
id: AuthType.GATEWAY,
name: 'AI API Gateway',
description: 'Use a custom AI API Gateway',
_meta: {
gateway: {
protocol: 'google',
restartRequired: 'false',
},
},
},
];
await this.context.config.initialize();
const version = await getVersion();
return {
protocolVersion: acp.PROTOCOL_VERSION,
authMethods,
agentInfo: {
name: 'gemini-cli',
title: 'Gemini CLI',
version,
},
agentCapabilities: {
loadSession: true,
promptCapabilities: {
image: true,
audio: true,
embeddedContext: true,
},
mcpCapabilities: {
http: true,
sse: true,
},
},
};
}
async authenticate(req: acp.AuthenticateRequest): Promise<void> {
const { methodId } = req;
const method = z.nativeEnum(AuthType).parse(methodId);
const selectedAuthType = this.settings.merged.security.auth.selectedType;
// Only clear credentials when switching to a different auth method
if (selectedAuthType && selectedAuthType !== method) {
await clearCachedCredentialFile();
}
// Check for api-key in _meta
const meta = hasMeta(req) ? req._meta : undefined;
const apiKey =
typeof meta?.['api-key'] === 'string' ? meta['api-key'] : undefined;
// Refresh auth with the requested method
// This will reuse existing credentials if they're valid,
// or perform new authentication if needed
try {
if (apiKey) {
this.apiKey = apiKey;
}
// Extract gateway details if present
const gatewaySchema = z.object({
baseUrl: z.string().optional(),
headers: z.record(z.string()).optional(),
});
let baseUrl: string | undefined;
let headers: Record<string, string> | undefined;
if (meta?.['gateway']) {
const result = gatewaySchema.safeParse(meta['gateway']);
if (result.success) {
baseUrl = result.data.baseUrl;
headers = result.data.headers;
} else {
throw new acp.RequestError(
-32602,
`Malformed gateway payload: ${result.error.message}`,
);
}
}
this.baseUrl = baseUrl;
this.customHeaders = headers;
await this.context.config.refreshAuth(
method,
apiKey ?? this.apiKey,
baseUrl,
headers,
);
} catch (e) {
throw new acp.RequestError(-32000, getAcpErrorMessage(e));
}
this.settings.setValue(
SettingScope.User,
'security.auth.selectedType',
method,
);
}
async newSession({
cwd,
mcpServers,
}: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
const sessionId = randomUUID();
const loadedSettings = loadSettings(cwd);
const config = await this.newSessionConfig(
sessionId,
cwd,
mcpServers,
loadedSettings,
);
const authType =
loadedSettings.merged.security.auth.selectedType || AuthType.USE_GEMINI;
let isAuthenticated = false;
let authErrorMessage = '';
try {
await config.refreshAuth(
authType,
this.apiKey,
this.baseUrl,
this.customHeaders,
);
isAuthenticated = true;
// Extra validation for Gemini API key
const contentGeneratorConfig = config.getContentGeneratorConfig();
if (
authType === AuthType.USE_GEMINI &&
(!contentGeneratorConfig || !contentGeneratorConfig.apiKey)
) {
isAuthenticated = false;
authErrorMessage = 'Gemini API key is missing or not configured.';
}
} catch (e) {
isAuthenticated = false;
authErrorMessage = getAcpErrorMessage(e);
debugLogger.error(
`Authentication failed: ${e instanceof Error ? e.stack : e}`,
);
}
if (!isAuthenticated) {
throw new acp.RequestError(
-32000,
authErrorMessage || 'Authentication required.',
);
}
if (this.clientCapabilities?.fs) {
const acpFileSystemService = new AcpFileSystemService(
this.connection,
sessionId,
this.clientCapabilities.fs,
config.getFileSystemService(),
cwd,
);
config.setFileSystemService(acpFileSystemService);
}
await config.initialize();
startupProfiler.flush(config);
const geminiClient = config.getGeminiClient();
const chat = await geminiClient.startChat();
const session = new Session(
sessionId,
chat,
config,
this.connection,
this.settings,
);
this.sessions.set(sessionId, session);
setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
session.sendAvailableCommands();
}, 0);
const { availableModels, currentModelId } = buildAvailableModels(
config,
loadedSettings,
);
const response = {
sessionId,
modes: {
availableModes: buildAvailableModes(config.isPlanEnabled()),
currentModeId: config.getApprovalMode(),
},
models: {
availableModels,
currentModelId,
},
};
return response;
}
async loadSession({
sessionId,
cwd,
mcpServers,
}: acp.LoadSessionRequest): Promise<acp.LoadSessionResponse> {
const config = await this.initializeSessionConfig(
sessionId,
cwd,
mcpServers,
);
const sessionSelector = new SessionSelector(config.storage);
const { sessionData, sessionPath } =
await sessionSelector.resolveSession(sessionId);
const clientHistory = convertSessionToClientHistory(sessionData.messages);
const geminiClient = config.getGeminiClient();
await geminiClient.initialize();
await geminiClient.resumeChat(clientHistory, {
conversation: sessionData,
filePath: sessionPath,
});
const session = new Session(
sessionId,
geminiClient.getChat(),
config,
this.connection,
this.settings,
);
this.sessions.set(sessionId, session);
// Stream history back to client
// eslint-disable-next-line @typescript-eslint/no-floating-promises
session.streamHistory(sessionData.messages);
setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
session.sendAvailableCommands();
}, 0);
const { availableModels, currentModelId } = buildAvailableModels(
config,
this.settings,
);
const response = {
modes: {
availableModes: buildAvailableModes(config.isPlanEnabled()),
currentModeId: config.getApprovalMode(),
},
models: {
availableModels,
currentModelId,
},
};
return response;
}
private async initializeSessionConfig(
sessionId: string,
cwd: string,
mcpServers: acp.McpServer[],
): Promise<Config> {
const selectedAuthType = this.settings.merged.security.auth.selectedType;
if (!selectedAuthType) {
throw acp.RequestError.authRequired();
}
// 1. Create config WITHOUT initializing it (no MCP servers started yet)
const config = await this.newSessionConfig(sessionId, cwd, mcpServers);
// 2. Authenticate BEFORE initializing configuration or starting MCP servers.
// This satisfies the security requirement to verify the user before executing
// potentially unsafe server definitions.
try {
await config.refreshAuth(
selectedAuthType,
this.apiKey,
this.baseUrl,
this.customHeaders,
);
} catch (e) {
debugLogger.error(`Authentication failed: ${e}`);
throw acp.RequestError.authRequired();
}
// 3. Set the ACP FileSystemService (if supported) before config initialization
if (this.clientCapabilities?.fs) {
const acpFileSystemService = new AcpFileSystemService(
this.connection,
sessionId,
this.clientCapabilities.fs,
config.getFileSystemService(),
cwd,
);
config.setFileSystemService(acpFileSystemService);
}
// 4. Now that we are authenticated, it is safe to initialize the config
// which starts the MCP servers and other heavy resources.
await config.initialize();
startupProfiler.flush(config);
return config;
}
async newSessionConfig(
sessionId: string,
cwd: string,
mcpServers: acp.McpServer[],
loadedSettings?: LoadedSettings,
): Promise<Config> {
const currentSettings = loadedSettings || this.settings;
const mergedMcpServers = { ...currentSettings.merged.mcpServers };
for (const server of mcpServers) {
if (
'type' in server &&
(server.type === 'sse' || server.type === 'http')
) {
// HTTP or SSE MCP server
const headers = Object.fromEntries(
server.headers.map(({ name, value }) => [name, value]),
);
mergedMcpServers[server.name] = new MCPServerConfig(
undefined, // command
undefined, // args
undefined, // env
undefined, // cwd
server.type === 'sse' ? server.url : undefined, // url (sse)
server.type === 'http' ? server.url : undefined, // httpUrl
headers,
);
} else if ('command' in server) {
// Stdio MCP server
const env: Record<string, string> = {};
for (const { name: envName, value } of server.env) {
env[envName] = value;
}
mergedMcpServers[server.name] = new MCPServerConfig(
server.command,
server.args,
env,
cwd,
);
}
}
const settings = {
...currentSettings.merged,
mcpServers: mergedMcpServers,
};
const config = await loadCliConfig(settings, sessionId, this.argv, { cwd });
createPolicyUpdater(
config.getPolicyEngine(),
config.messageBus,
config.storage,
);
return config;
}
async cancel(params: acp.CancelNotification): Promise<void> {
const session = this.sessions.get(params.sessionId);
if (!session) {
throw new Error(`Session not found: ${params.sessionId}`);
}
await session.cancelPendingPrompt();
}
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
const session = this.sessions.get(params.sessionId);
if (!session) {
throw new Error(`Session not found: ${params.sessionId}`);
}
return session.prompt(params);
}
async setSessionMode(
params: acp.SetSessionModeRequest,
): Promise<acp.SetSessionModeResponse> {
const session = this.sessions.get(params.sessionId);
if (!session) {
throw new Error(`Session not found: ${params.sessionId}`);
}
return session.setMode(params.modeId);
}
async unstable_setSessionModel(
params: acp.SetSessionModelRequest,
): Promise<acp.SetSessionModelResponse> {
const session = this.sessions.get(params.sessionId);
if (!session) {
throw new Error(`Session not found: ${params.sessionId}`);
}
return session.setModel(params.modelId);
}
}
export class Session {
private pendingPrompt: AbortController | null = null;
private commandHandler = new CommandHandler();
private callIdCounter = 0;
private generateCallId(name: string): string {
return `${name}-${Date.now()}-${++this.callIdCounter}`;
}
constructor(
private readonly id: string,
@@ -197,10 +706,13 @@ export class Session {
for (const part of parts) {
if (typeof part === 'object' && part !== null) {
if (isTextPart(part)) {
if ('text' in part) {
// It is a text part
const text = part.text;
commandText += text;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-type-assertion
const text = (part as any).text;
if (typeof text === 'string') {
commandText += text;
}
} else {
// Non-text part (image, embedded resource)
// Stop looking for command
@@ -457,7 +969,7 @@ export class Session {
promptId: string,
fc: FunctionCall,
): Promise<Part[]> {
const callId = fc.id ?? this.generateCallId(fc.name || 'unknown');
const callId = fc.id ?? GeminiAgent.generateCallId(fc.name || 'unknown');
const args = fc.args ?? {};
const startTime = Date.now();
@@ -1146,7 +1658,7 @@ export class Session {
include: pathSpecsToRead,
};
const callId = this.generateCallId(readManyFilesTool.name);
const callId = GeminiAgent.generateCallId(readManyFilesTool.name);
try {
const invocation = readManyFilesTool.build(toolArgs);
@@ -1284,3 +1796,333 @@ export class Session {
}
}
}
function toToolCallContent(toolResult: ToolResult): acp.ToolCallContent | null {
if (toolResult.error?.message) {
throw new Error(toolResult.error.message);
}
if (toolResult.returnDisplay) {
if (typeof toolResult.returnDisplay === 'string') {
return {
type: 'content',
content: { type: 'text', text: toolResult.returnDisplay },
};
} else {
if ('fileName' in toolResult.returnDisplay) {
return {
type: 'diff',
path:
toolResult.returnDisplay.filePath ??
toolResult.returnDisplay.fileName,
oldText: toolResult.returnDisplay.originalContent,
newText: toolResult.returnDisplay.newContent,
_meta: {
kind: !toolResult.returnDisplay.originalContent
? 'add'
: toolResult.returnDisplay.newContent === ''
? 'delete'
: 'modify',
},
};
}
return null;
}
} else {
return null;
}
}
const basicPermissionOptions = [
{
optionId: ToolConfirmationOutcome.ProceedOnce,
name: 'Allow',
kind: 'allow_once',
},
{
optionId: ToolConfirmationOutcome.Cancel,
name: 'Reject',
kind: 'reject_once',
},
] as const;
function toPermissionOptions(
confirmation: ToolCallConfirmationDetails,
config: Config,
enablePermanentToolApproval: boolean = false,
): acp.PermissionOption[] {
const disableAlwaysAllow = config.getDisableAlwaysAllow();
const options: acp.PermissionOption[] = [];
if (!disableAlwaysAllow) {
switch (confirmation.type) {
case 'edit':
options.push({
optionId: ToolConfirmationOutcome.ProceedAlways,
name: 'Allow for this session',
kind: 'allow_always',
});
if (enablePermanentToolApproval) {
options.push({
optionId: ToolConfirmationOutcome.ProceedAlwaysAndSave,
name: 'Allow for this file in all future sessions',
kind: 'allow_always',
});
}
break;
case 'exec':
options.push({
optionId: ToolConfirmationOutcome.ProceedAlways,
name: 'Allow for this session',
kind: 'allow_always',
});
if (enablePermanentToolApproval) {
options.push({
optionId: ToolConfirmationOutcome.ProceedAlwaysAndSave,
name: 'Allow this command for all future sessions',
kind: 'allow_always',
});
}
break;
case 'mcp':
options.push(
{
optionId: ToolConfirmationOutcome.ProceedAlwaysServer,
name: 'Allow all server tools for this session',
kind: 'allow_always',
},
{
optionId: ToolConfirmationOutcome.ProceedAlwaysTool,
name: 'Allow tool for this session',
kind: 'allow_always',
},
);
if (enablePermanentToolApproval) {
options.push({
optionId: ToolConfirmationOutcome.ProceedAlwaysAndSave,
name: 'Allow tool for all future sessions',
kind: 'allow_always',
});
}
break;
case 'info':
options.push({
optionId: ToolConfirmationOutcome.ProceedAlways,
name: 'Allow for this session',
kind: 'allow_always',
});
if (enablePermanentToolApproval) {
options.push({
optionId: ToolConfirmationOutcome.ProceedAlwaysAndSave,
name: 'Allow for all future sessions',
kind: 'allow_always',
});
}
break;
case 'ask_user':
case 'exit_plan_mode':
// askuser and exit_plan_mode don't need "always allow" options
break;
default:
// No "always allow" options for other types
break;
}
}
options.push(...basicPermissionOptions);
// Exhaustive check
switch (confirmation.type) {
case 'edit':
case 'exec':
case 'mcp':
case 'info':
case 'ask_user':
case 'exit_plan_mode':
case 'sandbox_expansion':
break;
default: {
const unreachable: never = confirmation;
throw new Error(`Unexpected: ${unreachable}`);
}
}
return options;
}
/**
* Maps our internal tool kind to the ACP ToolKind.
* Fallback to 'other' for kinds that are not supported by the ACP protocol.
*/
function toAcpToolKind(kind: Kind): acp.ToolKind {
switch (kind) {
case Kind.Read:
case Kind.Edit:
case Kind.Execute:
case Kind.Search:
case Kind.Delete:
case Kind.Move:
case Kind.Think:
case Kind.Fetch:
case Kind.SwitchMode:
case Kind.Other:
return kind as acp.ToolKind;
case Kind.Agent:
return 'think';
case Kind.Plan:
case Kind.Communicate:
default:
return 'other';
}
}
function buildAvailableModes(isPlanEnabled: boolean): acp.SessionMode[] {
const modes: acp.SessionMode[] = [
{
id: ApprovalMode.DEFAULT,
name: 'Default',
description: 'Prompts for approval',
},
{
id: ApprovalMode.AUTO_EDIT,
name: 'Auto Edit',
description: 'Auto-approves edit tools',
},
{
id: ApprovalMode.YOLO,
name: 'YOLO',
description: 'Auto-approves all tools',
},
];
if (isPlanEnabled) {
modes.push({
id: ApprovalMode.PLAN,
name: 'Plan',
description: 'Read-only mode',
});
}
return modes;
}
function buildAvailableModels(
config: Config,
settings: LoadedSettings,
): {
availableModels: Array<{
modelId: string;
name: string;
description?: string;
}>;
currentModelId: string;
} {
const preferredModel = config.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
const shouldShowPreviewModels = config.getHasAccessToPreviewModel();
const useGemini31 = config.getGemini31LaunchedSync?.() ?? false;
const useGemini31FlashLite =
config.getGemini31FlashLiteLaunchedSync?.() ?? false;
const selectedAuthType = settings.merged.security.auth.selectedType;
const useCustomToolModel =
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
// --- DYNAMIC PATH ---
if (
config.getExperimentalDynamicModelConfiguration?.() === true &&
config.getModelConfigService
) {
const options = config.getModelConfigService().getAvailableModelOptions({
useGemini3_1: useGemini31,
useGemini3_1FlashLite: useGemini31FlashLite,
useCustomTools: useCustomToolModel,
hasAccessToPreview: shouldShowPreviewModels,
});
return {
availableModels: options,
currentModelId: preferredModel,
};
}
// --- LEGACY PATH ---
const mainOptions = [
{
value: DEFAULT_GEMINI_MODEL_AUTO,
title: getDisplayString(DEFAULT_GEMINI_MODEL_AUTO),
description:
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
},
];
if (shouldShowPreviewModels) {
mainOptions.unshift({
value: PREVIEW_GEMINI_MODEL_AUTO,
title: getDisplayString(PREVIEW_GEMINI_MODEL_AUTO),
description: useGemini31
? 'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash'
: 'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
});
}
const manualOptions = [
{
value: DEFAULT_GEMINI_MODEL,
title: getDisplayString(DEFAULT_GEMINI_MODEL),
},
{
value: DEFAULT_GEMINI_FLASH_MODEL,
title: getDisplayString(DEFAULT_GEMINI_FLASH_MODEL),
},
{
value: DEFAULT_GEMINI_FLASH_LITE_MODEL,
title: getDisplayString(DEFAULT_GEMINI_FLASH_LITE_MODEL),
},
];
if (shouldShowPreviewModels) {
const previewProModel = useGemini31
? PREVIEW_GEMINI_3_1_MODEL
: PREVIEW_GEMINI_MODEL;
const previewProValue = useCustomToolModel
? PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL
: previewProModel;
const previewOptions = [
{
value: previewProValue,
title: getDisplayString(previewProModel),
},
{
value: PREVIEW_GEMINI_FLASH_MODEL,
title: getDisplayString(PREVIEW_GEMINI_FLASH_MODEL),
},
];
if (useGemini31FlashLite) {
previewOptions.push({
value: PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
title: getDisplayString(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL),
});
}
manualOptions.unshift(...previewOptions);
}
const scaleOptions = (
options: Array<{ value: string; title: string; description?: string }>,
) =>
options.map((o) => ({
modelId: o.value,
name: o.title,
description: o.description,
}));
return {
availableModels: [
...scaleOptions(mainOptions),
...scaleOptions(manualOptions),
],
currentModelId: preferredModel,
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+8 -8
View File
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -13,7 +13,7 @@ import {
type Mocked,
type Mock,
} from 'vitest';
import { GeminiAgent } from './acpRpcDispatcher.js';
import { GeminiAgent } from './acpClient.js';
import * as acp from '@agentclientprotocol/sdk';
import {
ApprovalMode,
@@ -28,7 +28,6 @@ import {
} from '../utils/sessionUtils.js';
import { convertSessionToClientHistory } from '@google/gemini-cli-core';
import type { LoadedSettings } from '../config/settings.js';
import { waitFor } from '../test-utils/async.js';
vi.mock('../config/config.js', () => ({
loadCliConfig: vi.fn(),
@@ -101,15 +100,11 @@ describe('GeminiAgent Session Resume', () => {
unsubscribe: vi.fn(),
},
getApprovalMode: vi.fn().mockReturnValue('default'),
isAutoMemoryEnabled: 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),
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
toolRegistry: {
getTool: vi.fn().mockReturnValue({ kind: 'read' }),
},
get config() {
return this;
},
@@ -174,6 +169,11 @@ describe('GeminiAgent Session Resume', () => {
],
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockConfig as any).toolRegistry = {
getTool: vi.fn().mockReturnValue({ kind: 'read' }),
};
(SessionSelector as unknown as Mock).mockImplementation(() => ({
resolveSession: vi.fn().mockResolvedValue({
sessionData,
@@ -239,7 +239,7 @@ describe('GeminiAgent Session Resume', () => {
}),
);
await waitFor(() => {
await vi.waitFor(() => {
// User message
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1,338 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
type Mock,
type Mocked,
} from 'vitest';
import { GeminiAgent } from './acpRpcDispatcher.js';
import * as acp from '@agentclientprotocol/sdk';
import {
AuthType,
type Config,
type MessageBus,
type Storage,
} from '@google/gemini-cli-core';
import type { LoadedSettings } from '../config/settings.js';
import { loadCliConfig, type CliArgs } from '../config/config.js';
import { loadSettings, SettingScope } from '../config/settings.js';
vi.mock('../config/config.js', () => ({
loadCliConfig: vi.fn(),
}));
vi.mock('../config/settings.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../config/settings.js')>();
return {
...actual,
loadSettings: vi.fn(),
};
});
describe('GeminiAgent - RPC Dispatcher', () => {
let mockConfig: Mocked<Config>;
let mockSettings: Mocked<LoadedSettings>;
let mockArgv: CliArgs;
let mockConnection: Mocked<acp.AgentSideConnection>;
let agent: GeminiAgent;
beforeEach(() => {
mockConfig = {
refreshAuth: vi.fn(),
initialize: vi.fn(),
waitForMcpInit: vi.fn(),
getFileSystemService: vi.fn(),
setFileSystemService: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getActiveModel: vi.fn().mockReturnValue('gemini-pro'),
getModel: vi.fn().mockReturnValue('gemini-pro'),
getGeminiClient: vi.fn().mockReturnValue({
startChat: vi.fn().mockResolvedValue({}),
}),
getMessageBus: vi.fn().mockReturnValue({
publish: vi.fn(),
subscribe: vi.fn(),
unsubscribe: vi.fn(),
}),
getApprovalMode: vi.fn().mockReturnValue('default'),
isPlanEnabled: vi.fn().mockReturnValue(true),
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
validatePathAccess: vi.fn().mockReturnValue(null),
getWorkspaceContext: vi.fn().mockReturnValue({
addReadOnlyPath: vi.fn(),
}),
getPolicyEngine: vi.fn().mockReturnValue({
addRule: vi.fn(),
}),
messageBus: {
publish: vi.fn(),
subscribe: vi.fn(),
unsubscribe: vi.fn(),
} as unknown as MessageBus,
storage: {
getWorkspaceAutoSavedPolicyPath: vi.fn(),
getAutoSavedPolicyPath: vi.fn(),
} as unknown as Storage,
get config() {
return this;
},
} as unknown as Mocked<Config>;
mockSettings = {
merged: {
security: { auth: { selectedType: 'login_with_google' } },
mcpServers: {},
},
setValue: vi.fn(),
} as unknown as Mocked<LoadedSettings>;
mockArgv = {} as unknown as CliArgs;
mockConnection = {
sessionUpdate: vi.fn(),
requestPermission: vi.fn(),
} as unknown as Mocked<acp.AgentSideConnection>;
(loadCliConfig as unknown as Mock).mockResolvedValue(mockConfig);
(loadSettings as unknown as Mock).mockImplementation(() => ({
merged: {
security: {
auth: { selectedType: AuthType.LOGIN_WITH_GOOGLE },
enablePermanentToolApproval: true,
},
mcpServers: {},
},
setValue: vi.fn(),
}));
agent = new GeminiAgent(mockConfig, mockSettings, mockArgv, mockConnection);
});
it('should initialize correctly', async () => {
const response = await agent.initialize({
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
protocolVersion: 1,
});
expect(response.protocolVersion).toBe(acp.PROTOCOL_VERSION);
expect(response.authMethods).toHaveLength(4);
const gatewayAuth = response.authMethods?.find(
(m) => m.id === AuthType.GATEWAY,
);
expect(gatewayAuth?._meta).toEqual({
gateway: {
protocol: 'google',
restartRequired: 'false',
},
});
const geminiAuth = response.authMethods?.find(
(m) => m.id === AuthType.USE_GEMINI,
);
expect(geminiAuth?._meta).toEqual({
'api-key': {
provider: 'google',
},
});
expect(response.agentCapabilities?.loadSession).toBe(true);
});
it('should authenticate correctly', async () => {
await agent.authenticate({
methodId: AuthType.LOGIN_WITH_GOOGLE,
});
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
AuthType.LOGIN_WITH_GOOGLE,
undefined,
undefined,
undefined,
);
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'security.auth.selectedType',
AuthType.LOGIN_WITH_GOOGLE,
);
});
it('should authenticate correctly with api-key in _meta', async () => {
await agent.authenticate({
methodId: AuthType.USE_GEMINI,
_meta: {
'api-key': 'test-api-key',
},
} as unknown as acp.AuthenticateRequest);
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
AuthType.USE_GEMINI,
'test-api-key',
undefined,
undefined,
);
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'security.auth.selectedType',
AuthType.USE_GEMINI,
);
});
it('should authenticate correctly with gateway method', async () => {
await agent.authenticate({
methodId: AuthType.GATEWAY,
_meta: {
gateway: {
baseUrl: 'https://example.com',
headers: { Authorization: 'Bearer token' },
},
},
} as unknown as acp.AuthenticateRequest);
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(
AuthType.GATEWAY,
undefined,
'https://example.com',
{ Authorization: 'Bearer token' },
);
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'security.auth.selectedType',
AuthType.GATEWAY,
);
});
it('should throw acp.RequestError when gateway payload is malformed', async () => {
await expect(
agent.authenticate({
methodId: AuthType.GATEWAY,
_meta: {
gateway: {
baseUrl: 123,
headers: { Authorization: 'Bearer token' },
},
},
} as unknown as acp.AuthenticateRequest),
).rejects.toThrow(/Malformed gateway payload/);
});
it('should cancel a session', async () => {
const mockSession = {
cancelPendingPrompt: vi.fn(),
};
(
agent as unknown as { sessionManager: { getSession: Mock } }
).sessionManager = {
getSession: vi.fn().mockReturnValue(mockSession),
};
await agent.cancel({ sessionId: 'test-session-id' });
expect(mockSession.cancelPendingPrompt).toHaveBeenCalled();
});
it('should throw error when cancelling non-existent session', async () => {
(
agent as unknown as { sessionManager: { getSession: Mock } }
).sessionManager = {
getSession: vi.fn().mockReturnValue(undefined),
};
await expect(agent.cancel({ sessionId: 'unknown' })).rejects.toThrow(
'Session not found',
);
});
it('should delegate prompt to session', async () => {
const mockSession = {
prompt: vi.fn().mockResolvedValue({ stopReason: 'end_turn' }),
};
(
agent as unknown as { sessionManager: { getSession: Mock } }
).sessionManager = {
getSession: vi.fn().mockReturnValue(mockSession),
};
const result = await agent.prompt({
sessionId: 'test-session-id',
prompt: [],
});
expect(mockSession.prompt).toHaveBeenCalled();
expect(result).toMatchObject({ stopReason: 'end_turn' });
});
it('should delegate setMode to session', async () => {
const mockSession = {
setMode: vi.fn().mockReturnValue({}),
};
(
agent as unknown as { sessionManager: { getSession: Mock } }
).sessionManager = {
getSession: vi.fn().mockReturnValue(mockSession),
};
const result = await agent.setSessionMode({
sessionId: 'test-session-id',
modeId: 'plan',
});
expect(mockSession.setMode).toHaveBeenCalledWith('plan');
expect(result).toEqual({});
});
it('should throw error when setting mode on non-existent session', async () => {
(
agent as unknown as { sessionManager: { getSession: Mock } }
).sessionManager = {
getSession: vi.fn().mockReturnValue(undefined),
};
await expect(
agent.setSessionMode({
sessionId: 'unknown',
modeId: 'plan',
}),
).rejects.toThrow('Session not found: unknown');
});
it('should delegate setModel to session (unstable)', async () => {
const mockSession = {
setModel: vi.fn().mockReturnValue({}),
};
(
agent as unknown as { sessionManager: { getSession: Mock } }
).sessionManager = {
getSession: vi.fn().mockReturnValue(mockSession),
};
const result = await agent.unstable_setSessionModel({
sessionId: 'test-session-id',
modelId: 'gemini-2.0-pro-exp',
});
expect(mockSession.setModel).toHaveBeenCalledWith('gemini-2.0-pro-exp');
expect(result).toEqual({});
});
it('should throw error when setting model on non-existent session (unstable)', async () => {
(
agent as unknown as { sessionManager: { getSession: Mock } }
).sessionManager = {
getSession: vi.fn().mockReturnValue(undefined),
};
await expect(
agent.unstable_setSessionModel({
sessionId: 'unknown',
modelId: 'gemini-2.0-pro-exp',
}),
).rejects.toThrow('Session not found: unknown');
});
});
-232
View File
@@ -1,232 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
type AgentLoopContext,
AuthType,
clearCachedCredentialFile,
getVersion,
} from '@google/gemini-cli-core';
import * as acp from '@agentclientprotocol/sdk';
import { z } from 'zod';
import { SettingScope, type LoadedSettings } from '../config/settings.js';
import type { CliArgs } from '../config/config.js';
import { getAcpErrorMessage } from './acpErrors.js';
import { AcpSessionManager, type AuthDetails } from './acpSessionManager.js';
import { hasMeta } from './acpUtils.js';
export class GeminiAgent {
private apiKey: string | undefined;
private baseUrl: string | undefined;
private customHeaders: Record<string, string> | undefined;
private sessionManager: AcpSessionManager;
constructor(
private context: AgentLoopContext,
private settings: LoadedSettings,
argv: CliArgs,
connection: acp.AgentSideConnection,
) {
this.sessionManager = new AcpSessionManager(settings, argv, connection);
}
async initialize(
args: acp.InitializeRequest,
): Promise<acp.InitializeResponse> {
if (args.clientCapabilities) {
this.sessionManager.setClientCapabilities(args.clientCapabilities);
}
const authMethods = [
{
id: AuthType.LOGIN_WITH_GOOGLE,
name: 'Log in with Google',
description: 'Log in with your Google account',
},
{
id: AuthType.USE_GEMINI,
name: 'Gemini API key',
description: 'Use an API key with Gemini Developer API',
_meta: {
'api-key': {
provider: 'google',
},
},
},
{
id: AuthType.USE_VERTEX_AI,
name: 'Vertex AI',
description: 'Use an API key with Vertex AI GenAI API',
},
{
id: AuthType.GATEWAY,
name: 'AI API Gateway',
description: 'Use a custom AI API Gateway',
_meta: {
gateway: {
protocol: 'google',
restartRequired: 'false',
},
},
},
];
await this.context.config.initialize();
const version = await getVersion();
return {
protocolVersion: acp.PROTOCOL_VERSION,
authMethods,
agentInfo: {
name: 'gemini-cli',
title: 'Gemini CLI',
version,
},
agentCapabilities: {
loadSession: true,
promptCapabilities: {
image: true,
audio: true,
embeddedContext: true,
},
mcpCapabilities: {
http: true,
sse: true,
},
},
};
}
async authenticate(req: acp.AuthenticateRequest): Promise<void> {
const { methodId } = req;
const method = z.nativeEnum(AuthType).parse(methodId);
const selectedAuthType = this.settings.merged.security.auth.selectedType;
// Only clear credentials when switching to a different auth method
if (selectedAuthType && selectedAuthType !== method) {
await clearCachedCredentialFile();
}
// Check for api-key in _meta
const meta = hasMeta(req) ? req._meta : undefined;
const apiKey =
typeof meta?.['api-key'] === 'string' ? meta['api-key'] : undefined;
// Refresh auth with the requested method
// This will reuse existing credentials if they're valid,
// or perform new authentication if needed
try {
if (apiKey) {
this.apiKey = apiKey;
}
// Extract gateway details if present
const gatewaySchema = z.object({
baseUrl: z.string().optional(),
headers: z.record(z.string()).optional(),
});
let baseUrl: string | undefined;
let headers: Record<string, string> | undefined;
if (meta?.['gateway']) {
const result = gatewaySchema.safeParse(meta['gateway']);
if (result.success) {
baseUrl = result.data.baseUrl;
headers = result.data.headers;
} else {
throw new acp.RequestError(
-32602,
`Malformed gateway payload: ${result.error.message}`,
);
}
}
this.baseUrl = baseUrl;
this.customHeaders = headers;
await this.context.config.refreshAuth(
method,
apiKey ?? this.apiKey,
baseUrl,
headers,
);
} catch (e) {
throw new acp.RequestError(-32000, getAcpErrorMessage(e));
}
this.settings.setValue(
SettingScope.User,
'security.auth.selectedType',
method,
);
}
private getAuthDetails(): AuthDetails {
return {
apiKey: this.apiKey,
baseUrl: this.baseUrl,
customHeaders: this.customHeaders,
};
}
async newSession(
params: acp.NewSessionRequest,
): Promise<acp.NewSessionResponse> {
return this.sessionManager.newSession(params, this.getAuthDetails());
}
async loadSession(
params: acp.LoadSessionRequest,
): Promise<acp.LoadSessionResponse> {
return this.sessionManager.loadSession(params, this.getAuthDetails());
}
async cancel(params: acp.CancelNotification): Promise<void> {
const session = this.sessionManager.getSession(params.sessionId);
if (!session) {
throw new acp.RequestError(
-32602,
`Session not found: ${params.sessionId}`,
);
}
await session.cancelPendingPrompt();
}
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
const session = this.sessionManager.getSession(params.sessionId);
if (!session) {
throw new acp.RequestError(
-32602,
`Session not found: ${params.sessionId}`,
);
}
return session.prompt(params);
}
async setSessionMode(
params: acp.SetSessionModeRequest,
): Promise<acp.SetSessionModeResponse> {
const session = this.sessionManager.getSession(params.sessionId);
if (!session) {
throw new acp.RequestError(
-32602,
`Session not found: ${params.sessionId}`,
);
}
return session.setMode(params.modeId);
}
async unstable_setSessionModel(
params: acp.SetSessionModelRequest,
): Promise<acp.SetSessionModelResponse> {
const session = this.sessionManager.getSession(params.sessionId);
if (!session) {
throw new acp.RequestError(
-32602,
`Session not found: ${params.sessionId}`,
);
}
return session.setModel(params.modelId);
}
}
-463
View File
@@ -1,463 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type Mock,
type Mocked,
} from 'vitest';
import { Session } from './acpSession.js';
import type * as acp from '@agentclientprotocol/sdk';
import {
StreamEventType,
ReadManyFilesTool,
type GeminiChat,
type Config,
type MessageBus,
LlmRole,
type GitService,
type ModelRouterService,
InvalidStreamError,
} from '@google/gemini-cli-core';
import type { LoadedSettings } from '../config/settings.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import type { CommandHandler } from './acpCommandHandler.js';
vi.mock('node:fs/promises');
vi.mock('node:path', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:path')>();
return {
...actual,
resolve: vi.fn(),
};
});
vi.mock(
'@google/gemini-cli-core',
async (
importOriginal: () => Promise<typeof import('@google/gemini-cli-core')>,
) => {
const actual = await importOriginal();
return {
...actual,
updatePolicy: vi.fn(),
ReadManyFilesTool: vi.fn(),
logToolCall: vi.fn(),
processSingleFileContent: vi.fn(),
};
},
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function* createMockStream(items: any[]) {
for (const item of items) {
yield item;
}
}
describe('Session', () => {
let mockChat: Mocked<GeminiChat>;
let mockConfig: Mocked<Config>;
let mockConnection: Mocked<acp.AgentSideConnection>;
let session: Session;
let mockToolRegistry: { getTool: Mock };
let mockTool: { kind: string; build: Mock };
let mockMessageBus: Mocked<MessageBus>;
beforeEach(() => {
mockChat = {
sendMessageStream: vi.fn(),
addHistory: vi.fn(),
recordCompletedToolCalls: vi.fn(),
getHistory: vi.fn().mockReturnValue([]),
} as unknown as Mocked<GeminiChat>;
mockTool = {
kind: 'read',
build: vi.fn().mockReturnValue({
getDescription: () => 'Test Tool',
toolLocations: () => [],
shouldConfirmExecute: vi.fn().mockResolvedValue(null),
execute: vi.fn().mockResolvedValue({ llmContent: 'Tool Result' }),
}),
};
mockToolRegistry = {
getTool: vi.fn().mockReturnValue(mockTool),
};
mockMessageBus = {
publish: vi.fn(),
subscribe: vi.fn(),
unsubscribe: vi.fn(),
} as unknown as Mocked<MessageBus>;
mockConfig = {
getModel: vi.fn().mockReturnValue('gemini-pro'),
getActiveModel: vi.fn().mockReturnValue('gemini-pro'),
getModelRouterService: vi.fn().mockReturnValue({
route: vi.fn().mockResolvedValue({ model: 'resolved-model' }),
}),
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
getFileService: vi.fn().mockReturnValue({
shouldIgnoreFile: vi.fn().mockReturnValue(false),
}),
getFileFilteringOptions: vi.fn().mockReturnValue({}),
getFileSystemService: vi.fn().mockReturnValue({}),
getTargetDir: vi.fn().mockReturnValue('/tmp'),
getEnableRecursiveFileSearch: vi.fn().mockReturnValue(false),
getDebugMode: vi.fn().mockReturnValue(false),
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
setApprovalMode: vi.fn(),
setModel: vi.fn(),
isPlanEnabled: vi.fn().mockReturnValue(true),
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
getGitService: vi.fn().mockResolvedValue({} as GitService),
validatePathAccess: vi.fn().mockReturnValue(null),
getWorkspaceContext: vi.fn().mockReturnValue({
addReadOnlyPath: vi.fn(),
}),
waitForMcpInit: vi.fn(),
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
get config() {
return this;
},
get toolRegistry() {
return mockToolRegistry;
},
} as unknown as Mocked<Config>;
mockConnection = {
sessionUpdate: vi.fn(),
requestPermission: vi.fn(),
} as unknown as Mocked<acp.AgentSideConnection>;
session = new Session('session-1', mockChat, mockConfig, mockConnection, {
merged: {
security: { enablePermanentToolApproval: true },
mcpServers: {},
},
errors: [],
} as unknown as LoadedSettings);
(ReadManyFilesTool as unknown as Mock).mockImplementation(() => ({
name: 'read_many_files',
kind: 'read',
build: vi.fn().mockReturnValue({
getDescription: () => 'Read files',
toolLocations: () => [],
execute: vi.fn().mockResolvedValue({
llmContent: ['--- file.txt ---\n\nFile content\n\n'],
}),
}),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should send available commands', async () => {
await session.sendAvailableCommands();
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
expect.objectContaining({
update: expect.objectContaining({
sessionUpdate: 'available_commands_update',
}),
}),
);
});
it('should await MCP initialization before processing a prompt', async () => {
const stream = createMockStream([
{
type: StreamEventType.CHUNK,
value: { candidates: [{ content: { parts: [{ text: 'Hi' }] } }] },
},
]);
mockChat.sendMessageStream.mockResolvedValue(stream);
await session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: 'test' }],
});
expect(mockConfig.waitForMcpInit).toHaveBeenCalledOnce();
});
it('should handle prompt with text response', async () => {
const stream = createMockStream([
{
type: StreamEventType.CHUNK,
value: {
candidates: [{ content: { parts: [{ text: 'Hello' }] } }],
},
},
]);
mockChat.sendMessageStream.mockResolvedValue(stream);
const result = await session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: 'Hi' }],
});
expect(mockChat.sendMessageStream).toHaveBeenCalled();
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith({
sessionId: 'session-1',
update: {
sessionUpdate: 'agent_message_chunk',
content: { type: 'text', text: 'Hello' },
},
});
expect(result).toMatchObject({ stopReason: 'end_turn' });
});
it('should use model router to determine model', async () => {
const mockRouter = {
route: vi.fn().mockResolvedValue({ model: 'routed-model' }),
} as unknown as ModelRouterService;
mockConfig.getModelRouterService.mockReturnValue(mockRouter);
const stream = createMockStream([
{
type: StreamEventType.CHUNK,
value: {
candidates: [{ content: { parts: [{ text: 'Hello' }] } }],
},
},
]);
mockChat.sendMessageStream.mockResolvedValue(stream);
await session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: 'Hi' }],
});
expect(mockRouter.route).toHaveBeenCalled();
expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
expect.objectContaining({ model: 'routed-model' }),
expect.any(Array),
expect.any(String),
expect.any(Object),
expect.any(String),
);
});
it('should handle prompt with empty response (InvalidStreamError)', async () => {
mockChat.sendMessageStream.mockRejectedValue(
new InvalidStreamError('Empty response', 'NO_RESPONSE_TEXT'),
);
const result = await session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: 'Hi' }],
});
expect(result).toMatchObject({ stopReason: 'end_turn' });
});
it('should handle prompt with no finish reason (InvalidStreamError)', async () => {
mockChat.sendMessageStream.mockRejectedValue(
new InvalidStreamError('No finish reason', 'NO_FINISH_REASON'),
);
const result = await session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: 'Hi' }],
});
expect(result).toMatchObject({ stopReason: 'end_turn' });
});
it('should handle /memory command', async () => {
const handleCommandSpy = vi
.spyOn(
(session as unknown as { commandHandler: CommandHandler })
.commandHandler,
'handleCommand',
)
.mockResolvedValue(true);
const result = await session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: '/memory view' }],
});
expect(result).toMatchObject({ stopReason: 'end_turn' });
expect(handleCommandSpy).toHaveBeenCalledWith(
'/memory view',
expect.any(Object),
);
});
it('should handle tool calls', async () => {
const stream1 = createMockStream([
{
type: StreamEventType.CHUNK,
value: {
functionCalls: [{ name: 'test_tool', args: { foo: 'bar' } }],
},
},
]);
const stream2 = createMockStream([
{
type: StreamEventType.CHUNK,
value: {
candidates: [{ content: { parts: [{ text: 'Result' }] } }],
},
},
]);
mockChat.sendMessageStream
.mockResolvedValueOnce(stream1)
.mockResolvedValueOnce(stream2);
const result = await session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: 'Call tool' }],
});
expect(mockToolRegistry.getTool).toHaveBeenCalledWith('test_tool');
expect(result).toMatchObject({ stopReason: 'end_turn' });
});
it('should handle tool call permission request', async () => {
const confirmationDetails = {
type: 'info',
onConfirm: vi.fn(),
};
mockTool.build.mockReturnValue({
getDescription: () => 'Test Tool',
toolLocations: () => [],
shouldConfirmExecute: vi.fn().mockResolvedValue(confirmationDetails),
execute: vi.fn().mockResolvedValue({ llmContent: 'Tool Result' }),
});
mockConnection.requestPermission.mockResolvedValue({
outcome: {
outcome: 'selected',
optionId: 'proceed_once',
},
});
const stream1 = createMockStream([
{
type: StreamEventType.CHUNK,
value: {
functionCalls: [{ name: 'test_tool', args: {} }],
},
},
]);
const stream2 = createMockStream([
{
type: StreamEventType.CHUNK,
value: { candidates: [] },
},
]);
mockChat.sendMessageStream
.mockResolvedValueOnce(stream1)
.mockResolvedValueOnce(stream2);
await session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: 'Call tool' }],
});
expect(mockConnection.requestPermission).toHaveBeenCalled();
expect(confirmationDetails.onConfirm).toHaveBeenCalled();
});
it('should handle @path resolution', async () => {
(path.resolve as unknown as Mock).mockReturnValue('/tmp/file.txt');
(fs.stat as unknown as Mock).mockResolvedValue({
isDirectory: () => false,
});
const stream = createMockStream([
{
type: StreamEventType.CHUNK,
value: { candidates: [] },
},
]);
mockChat.sendMessageStream.mockResolvedValue(stream);
await session.prompt({
sessionId: 'session-1',
prompt: [
{ type: 'text', text: 'Read' },
{
type: 'resource_link',
uri: 'file://file.txt',
mimeType: 'text/plain',
name: 'file.txt',
},
],
});
expect(path.resolve).toHaveBeenCalled();
expect(fs.stat).toHaveBeenCalled();
expect(mockChat.sendMessageStream).toHaveBeenCalledWith(
expect.anything(),
expect.arrayContaining([
expect.objectContaining({
text: expect.stringContaining('Content from @file.txt'),
}),
]),
expect.anything(),
expect.any(AbortSignal),
LlmRole.MAIN,
);
});
it('should handle rate limit error', async () => {
const error = new Error('Rate limit');
(error as unknown as { status: number }).status = 429;
mockChat.sendMessageStream.mockRejectedValue(error);
await expect(
session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: 'Hi' }],
}),
).rejects.toMatchObject({
code: 429,
message: 'Rate limit exceeded. Try again later.',
});
});
it('should handle missing tool', async () => {
mockToolRegistry.getTool.mockReturnValue(undefined);
const stream1 = createMockStream([
{
type: StreamEventType.CHUNK,
value: {
functionCalls: [{ name: 'unknown_tool', args: {} }],
},
},
]);
const stream2 = createMockStream([
{
type: StreamEventType.CHUNK,
value: { candidates: [] },
},
]);
mockChat.sendMessageStream
.mockResolvedValueOnce(stream1)
.mockResolvedValueOnce(stream2);
await session.prompt({
sessionId: 'session-1',
prompt: [{ type: 'text', text: 'Call tool' }],
});
expect(mockChat.sendMessageStream).toHaveBeenCalledTimes(2);
});
});
@@ -1,386 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type Mock,
type Mocked,
} from 'vitest';
import { AcpSessionManager } from './acpSessionManager.js';
import type * as acp from '@agentclientprotocol/sdk';
import {
AuthType,
type Config,
type MessageBus,
type Storage,
} from '@google/gemini-cli-core';
import type { LoadedSettings } from '../config/settings.js';
import { loadCliConfig, type CliArgs } from '../config/config.js';
import { loadSettings } from '../config/settings.js';
vi.mock('../config/config.js', () => ({
loadCliConfig: vi.fn(),
}));
vi.mock('../config/settings.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../config/settings.js')>();
return {
...actual,
loadSettings: vi.fn(),
};
});
const startAutoMemoryIfEnabledMock = vi.fn();
vi.mock('../utils/autoMemory.js', () => ({
startAutoMemoryIfEnabled: (config: Config) =>
startAutoMemoryIfEnabledMock(config),
}));
describe('AcpSessionManager', () => {
let mockConfig: Mocked<Config>;
let mockSettings: Mocked<LoadedSettings>;
let mockArgv: CliArgs;
let mockConnection: Mocked<acp.AgentSideConnection>;
let manager: AcpSessionManager;
beforeEach(() => {
mockConfig = {
refreshAuth: vi.fn(),
initialize: vi.fn(),
waitForMcpInit: vi.fn(),
getFileSystemService: vi.fn(),
setFileSystemService: vi.fn(),
getContentGeneratorConfig: vi.fn(),
getActiveModel: vi.fn().mockReturnValue('gemini-pro'),
getModel: vi.fn().mockReturnValue('gemini-pro'),
getGeminiClient: vi.fn().mockReturnValue({
startChat: vi.fn().mockResolvedValue({}),
}),
getMessageBus: vi.fn().mockReturnValue({
publish: vi.fn(),
subscribe: vi.fn(),
unsubscribe: vi.fn(),
}),
getApprovalMode: vi.fn().mockReturnValue('default'),
isPlanEnabled: vi.fn().mockReturnValue(true),
getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
getCheckpointingEnabled: vi.fn().mockReturnValue(false),
getDisableAlwaysAllow: vi.fn().mockReturnValue(false),
validatePathAccess: vi.fn().mockReturnValue(null),
getWorkspaceContext: vi.fn().mockReturnValue({
addReadOnlyPath: vi.fn(),
}),
getPolicyEngine: vi.fn().mockReturnValue({
addRule: vi.fn(),
}),
messageBus: {
publish: vi.fn(),
subscribe: vi.fn(),
unsubscribe: vi.fn(),
} as unknown as MessageBus,
storage: {
getWorkspaceAutoSavedPolicyPath: vi.fn(),
getAutoSavedPolicyPath: vi.fn(),
} as unknown as Storage,
get config() {
return this;
},
} as unknown as Mocked<Config>;
mockSettings = {
merged: {
security: { auth: { selectedType: 'login_with_google' } },
mcpServers: {},
},
setValue: vi.fn(),
} as unknown as Mocked<LoadedSettings>;
mockArgv = {} as unknown as CliArgs;
mockConnection = {
sessionUpdate: vi.fn(),
requestPermission: vi.fn(),
} as unknown as Mocked<acp.AgentSideConnection>;
(loadCliConfig as unknown as Mock).mockResolvedValue(mockConfig);
(loadSettings as unknown as Mock).mockImplementation(() => ({
merged: {
security: {
auth: { selectedType: AuthType.LOGIN_WITH_GOOGLE },
enablePermanentToolApproval: true,
},
mcpServers: {},
},
setValue: vi.fn(),
}));
manager = new AcpSessionManager(mockSettings, mockArgv, mockConnection);
vi.mock('node:crypto', () => ({
randomUUID: () => 'test-session-id',
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should create a new session', async () => {
vi.useFakeTimers();
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
});
const response = await manager.newSession(
{
cwd: '/tmp',
mcpServers: [],
},
{},
);
expect(response.sessionId).toBe('test-session-id');
expect(loadCliConfig).toHaveBeenCalled();
expect(mockConfig.initialize).toHaveBeenCalled();
expect(mockConfig.getGeminiClient).toHaveBeenCalled();
// Verify deferred call (sendAvailableCommands)
await vi.runAllTimersAsync();
expect(mockConnection.sessionUpdate).toHaveBeenCalledWith(
expect.objectContaining({
update: expect.objectContaining({
sessionUpdate: 'available_commands_update',
}),
}),
);
vi.useRealTimers();
});
it('should return modes without plan mode when plan is disabled', async () => {
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
});
mockConfig.isPlanEnabled = vi.fn().mockReturnValue(false);
mockConfig.getApprovalMode = vi.fn().mockReturnValue('default');
const response = await manager.newSession(
{
cwd: '/tmp',
mcpServers: [],
},
{},
);
expect(response.modes).toEqual({
availableModes: [
{ id: 'default', name: 'Default', description: 'Prompts for approval' },
{
id: 'autoEdit',
name: 'Auto Edit',
description: 'Auto-approves edit tools',
},
{ id: 'yolo', name: 'YOLO', description: 'Auto-approves all tools' },
],
currentModeId: 'default',
});
});
it('should include preview models when user has access', async () => {
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
});
mockConfig.getHasAccessToPreviewModel = vi.fn().mockReturnValue(true);
mockConfig.getGemini31LaunchedSync = vi.fn().mockReturnValue(true);
const response = await manager.newSession(
{
cwd: '/tmp',
mcpServers: [],
},
{},
);
expect(response.models?.availableModels).toEqual(
expect.arrayContaining([
expect.objectContaining({
modelId: 'auto-gemini-3',
name: expect.stringContaining('Auto'),
}),
]),
);
});
it('should include gemini-3.1-flash-lite when useGemini31FlashLite is true', async () => {
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
});
mockConfig.getHasAccessToPreviewModel = vi.fn().mockReturnValue(true);
mockConfig.getGemini31LaunchedSync = vi.fn().mockReturnValue(true);
mockConfig.getGemini31FlashLiteLaunchedSync = vi.fn().mockReturnValue(true);
const response = await manager.newSession(
{
cwd: '/tmp',
mcpServers: [],
},
{},
);
expect(response.models?.availableModels).toEqual(
expect.arrayContaining([
expect.objectContaining({
modelId: 'gemini-3.1-flash-lite-preview',
name: 'gemini-3.1-flash-lite-preview',
}),
]),
);
});
it('should return modes with plan mode when plan is enabled', async () => {
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
});
mockConfig.isPlanEnabled = vi.fn().mockReturnValue(true);
mockConfig.getApprovalMode = vi.fn().mockReturnValue('plan');
const response = await manager.newSession(
{
cwd: '/tmp',
mcpServers: [],
},
{},
);
expect(response.modes).toEqual({
availableModes: [
{ id: 'default', name: 'Default', description: 'Prompts for approval' },
{
id: 'autoEdit',
name: 'Auto Edit',
description: 'Auto-approves edit tools',
},
{ id: 'yolo', name: 'YOLO', description: 'Auto-approves all tools' },
{ id: 'plan', name: 'Plan', description: 'Read-only mode' },
],
currentModeId: 'plan',
});
});
it('should fail session creation if Gemini API key is missing', async () => {
(loadSettings as unknown as Mock).mockImplementation(() => ({
merged: {
security: { auth: { selectedType: AuthType.USE_GEMINI } },
mcpServers: {},
},
setValue: vi.fn(),
}));
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: undefined,
});
await expect(
manager.newSession(
{
cwd: '/tmp',
mcpServers: [],
},
{},
),
).rejects.toMatchObject({
message: 'Gemini API key is missing or not configured.',
});
});
it('should create a new session with mcp servers', async () => {
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
});
const mcpServers = [
{
name: 'test-server',
command: 'node',
args: ['server.js'],
env: [{ name: 'KEY', value: 'VALUE' }],
},
];
await manager.newSession(
{
cwd: '/tmp',
mcpServers,
},
{},
);
expect(loadCliConfig).toHaveBeenCalledWith(
expect.objectContaining({
mcpServers: expect.objectContaining({
'test-server': expect.objectContaining({
command: 'node',
args: ['server.js'],
env: { KEY: 'VALUE' },
}),
}),
}),
'test-session-id',
mockArgv,
{ cwd: '/tmp' },
);
});
it('should handle authentication failure gracefully', async () => {
mockConfig.refreshAuth.mockRejectedValue(new Error('Auth failed'));
await expect(
manager.newSession(
{
cwd: '/tmp',
mcpServers: [],
},
{},
),
).rejects.toMatchObject({
message: 'Auth failed',
});
});
it('should initialize file system service if client supports it', async () => {
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
});
manager.setClientCapabilities({
fs: { readTextFile: true, writeTextFile: true },
});
await manager.newSession(
{
cwd: '/tmp',
mcpServers: [],
},
{},
);
expect(mockConfig.setFileSystemService).toHaveBeenCalled();
});
it('should start auto memory for new ACP sessions', async () => {
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
});
await manager.newSession(
{
cwd: '/tmp',
mcpServers: [],
},
{},
);
expect(startAutoMemoryIfEnabledMock).toHaveBeenCalledWith(mockConfig);
});
});
-322
View File
@@ -1,322 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
type Config,
AuthType,
MCPServerConfig,
debugLogger,
startupProfiler,
convertSessionToClientHistory,
createPolicyUpdater,
} from '@google/gemini-cli-core';
import * as acp from '@agentclientprotocol/sdk';
import { randomUUID } from 'node:crypto';
import { loadSettings, type LoadedSettings } from '../config/settings.js';
import { SessionSelector } from '../utils/sessionUtils.js';
import { Session } from './acpSession.js';
import { AcpFileSystemService } from './acpFileSystemService.js';
import { getAcpErrorMessage } from './acpErrors.js';
import { buildAvailableModels, buildAvailableModes } from './acpUtils.js';
import { loadCliConfig, type CliArgs } from '../config/config.js';
import { startAutoMemoryIfEnabled } from '../utils/autoMemory.js';
export interface AuthDetails {
apiKey?: string;
baseUrl?: string;
customHeaders?: Record<string, string>;
}
export class AcpSessionManager {
private sessions: Map<string, Session> = new Map();
private clientCapabilities: acp.ClientCapabilities | undefined;
constructor(
private settings: LoadedSettings,
private argv: CliArgs,
private connection: acp.AgentSideConnection,
) {}
setClientCapabilities(capabilities: acp.ClientCapabilities) {
this.clientCapabilities = capabilities;
}
getSession(sessionId: string): Session | undefined {
return this.sessions.get(sessionId);
}
async newSession(
{ cwd, mcpServers }: acp.NewSessionRequest,
authDetails: AuthDetails,
): Promise<acp.NewSessionResponse> {
const sessionId = randomUUID();
const loadedSettings = loadSettings(cwd);
const config = await this.newSessionConfig(
sessionId,
cwd,
mcpServers,
loadedSettings,
);
const authType =
loadedSettings.merged.security.auth.selectedType || AuthType.USE_GEMINI;
let isAuthenticated = false;
let authErrorMessage = '';
try {
await config.refreshAuth(
authType,
authDetails.apiKey,
authDetails.baseUrl,
authDetails.customHeaders,
);
isAuthenticated = true;
// Extra validation for Gemini API key
const contentGeneratorConfig = config.getContentGeneratorConfig();
if (
authType === AuthType.USE_GEMINI &&
(!contentGeneratorConfig || !contentGeneratorConfig.apiKey)
) {
isAuthenticated = false;
authErrorMessage = 'Gemini API key is missing or not configured.';
}
} catch (e) {
isAuthenticated = false;
authErrorMessage = getAcpErrorMessage(e);
debugLogger.error(
`Authentication failed: ${e instanceof Error ? e.stack : e}`,
);
}
if (!isAuthenticated) {
throw new acp.RequestError(
-32000,
authErrorMessage || 'Authentication required.',
);
}
if (this.clientCapabilities?.fs) {
const acpFileSystemService = new AcpFileSystemService(
this.connection,
sessionId,
this.clientCapabilities.fs,
config.getFileSystemService(),
cwd,
);
config.setFileSystemService(acpFileSystemService);
}
await config.initialize();
startupProfiler.flush(config);
startAutoMemoryIfEnabled(config);
const geminiClient = config.getGeminiClient();
const chat = await geminiClient.startChat();
const session = new Session(
sessionId,
chat,
config,
this.connection,
this.settings,
);
this.sessions.set(sessionId, session);
setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
session.sendAvailableCommands();
}, 0);
const { availableModels, currentModelId } = buildAvailableModels(
config,
loadedSettings,
);
const response = {
sessionId,
modes: {
availableModes: buildAvailableModes(config.isPlanEnabled()),
currentModeId: config.getApprovalMode(),
},
models: {
availableModels,
currentModelId,
},
};
return response;
}
async loadSession(
{ sessionId, cwd, mcpServers }: acp.LoadSessionRequest,
authDetails: AuthDetails,
): Promise<acp.LoadSessionResponse> {
const config = await this.initializeSessionConfig(
sessionId,
cwd,
mcpServers,
authDetails,
);
const sessionSelector = new SessionSelector(config.storage);
const { sessionData, sessionPath } =
await sessionSelector.resolveSession(sessionId);
const clientHistory = convertSessionToClientHistory(sessionData.messages);
const geminiClient = config.getGeminiClient();
await geminiClient.initialize();
await geminiClient.resumeChat(clientHistory, {
conversation: sessionData,
filePath: sessionPath,
});
const session = new Session(
sessionId,
geminiClient.getChat(),
config,
this.connection,
this.settings,
);
this.sessions.set(sessionId, session);
// Stream history back to client
// eslint-disable-next-line @typescript-eslint/no-floating-promises
session.streamHistory(sessionData.messages);
setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
session.sendAvailableCommands();
}, 0);
const { availableModels, currentModelId } = buildAvailableModels(
config,
this.settings,
);
const response = {
modes: {
availableModes: buildAvailableModes(config.isPlanEnabled()),
currentModeId: config.getApprovalMode(),
},
models: {
availableModels,
currentModelId,
},
};
return response;
}
private async initializeSessionConfig(
sessionId: string,
cwd: string,
mcpServers: acp.McpServer[],
authDetails: AuthDetails,
): Promise<Config> {
const selectedAuthType = this.settings.merged.security.auth.selectedType;
if (!selectedAuthType) {
throw acp.RequestError.authRequired();
}
// 1. Create config WITHOUT initializing it (no MCP servers started yet)
const config = await this.newSessionConfig(sessionId, cwd, mcpServers);
// 2. Authenticate BEFORE initializing configuration or starting MCP servers.
// This satisfies the security requirement to verify the user before executing
// potentially unsafe server definitions.
try {
await config.refreshAuth(
selectedAuthType,
authDetails.apiKey,
authDetails.baseUrl,
authDetails.customHeaders,
);
} catch (e) {
debugLogger.error(`Authentication failed: ${e}`);
throw acp.RequestError.authRequired();
}
// 3. Set the ACP FileSystemService (if supported) before config initialization
if (this.clientCapabilities?.fs) {
const acpFileSystemService = new AcpFileSystemService(
this.connection,
sessionId,
this.clientCapabilities.fs,
config.getFileSystemService(),
cwd,
);
config.setFileSystemService(acpFileSystemService);
}
// 4. Now that we are authenticated, it is safe to initialize the config
// which starts the MCP servers and other heavy resources.
await config.initialize();
startupProfiler.flush(config);
startAutoMemoryIfEnabled(config);
return config;
}
async newSessionConfig(
sessionId: string,
cwd: string,
mcpServers: acp.McpServer[],
loadedSettings?: LoadedSettings,
): Promise<Config> {
const currentSettings = loadedSettings || this.settings;
const mergedMcpServers = { ...currentSettings.merged.mcpServers };
for (const server of mcpServers) {
if (
'type' in server &&
(server.type === 'sse' || server.type === 'http')
) {
// HTTP or SSE MCP server
const headers = Object.fromEntries(
server.headers.map(({ name, value }) => [name, value]),
);
mergedMcpServers[server.name] = new MCPServerConfig(
undefined, // command
undefined, // args
undefined, // env
undefined, // cwd
server.type === 'sse' ? server.url : undefined, // url (sse)
server.type === 'http' ? server.url : undefined, // httpUrl
headers,
);
} else if ('command' in server) {
// Stdio MCP server
const env: Record<string, string> = {};
for (const { name: envName, value } of server.env) {
env[envName] = value;
}
mergedMcpServers[server.name] = new MCPServerConfig(
server.command,
server.args,
env,
cwd,
);
}
}
const settings = {
...currentSettings.merged,
mcpServers: mergedMcpServers,
};
const config = await loadCliConfig(settings, sessionId, this.argv, { cwd });
createPolicyUpdater(
config.getPolicyEngine(),
config.messageBus,
config.storage,
);
return config;
}
}
-35
View File
@@ -1,35 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type Config, createWorkingStdio } from '@google/gemini-cli-core';
import { runExitCleanup } from '../utils/cleanup.js';
import * as acp from '@agentclientprotocol/sdk';
import { Readable, Writable } from 'node:stream';
import type { LoadedSettings } from '../config/settings.js';
import type { CliArgs } from '../config/config.js';
import { GeminiAgent } from './acpRpcDispatcher.js';
export async function runAcpClient(
config: Config,
settings: LoadedSettings,
argv: CliArgs,
) {
const { stdout: workingStdout } = createWorkingStdio();
const stdout = Writable.toWeb(workingStdout) as WritableStream;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const stdin = Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>;
const stream = acp.ndJsonStream(stdout, stdin);
const connection = new acp.AgentSideConnection(
(connection) => new GeminiAgent(config, settings, argv, connection),
stream,
);
// SIGTERM/SIGINT handlers (in sdk.ts) don't fire when stdin closes.
// We must explicitly await the connection close to flush telemetry.
// Use finally() to ensure cleanup runs even on stream errors.
await connection.closed.finally(runExitCleanup);
}
-373
View File
@@ -1,373 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
type Config,
type ToolResult,
type ToolCallConfirmationDetails,
Kind,
ApprovalMode,
DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL_AUTO,
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
PREVIEW_GEMINI_3_1_MODEL,
PREVIEW_GEMINI_MODEL,
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
getDisplayString,
AuthType,
ToolConfirmationOutcome,
} from '@google/gemini-cli-core';
import type * as acp from '@agentclientprotocol/sdk';
import { z } from 'zod';
import type { LoadedSettings } from '../config/settings.js';
export function hasMeta(
obj: unknown,
): obj is { _meta?: Record<string, unknown> } {
return typeof obj === 'object' && obj !== null && '_meta' in obj;
}
export const RequestPermissionResponseSchema = z.object({
outcome: z.discriminatedUnion('outcome', [
z.object({ outcome: z.literal('cancelled') }),
z.object({
outcome: z.literal('selected'),
optionId: z.string(),
}),
]),
});
export function toToolCallContent(
toolResult: ToolResult,
): acp.ToolCallContent | null {
if (toolResult.error?.message) {
throw new Error(toolResult.error.message);
}
if (toolResult.returnDisplay) {
if (typeof toolResult.returnDisplay === 'string') {
return {
type: 'content',
content: { type: 'text', text: toolResult.returnDisplay },
};
} else {
if ('fileName' in toolResult.returnDisplay) {
return {
type: 'diff',
path:
toolResult.returnDisplay.filePath ??
toolResult.returnDisplay.fileName,
oldText: toolResult.returnDisplay.originalContent,
newText: toolResult.returnDisplay.newContent,
_meta: {
kind: !toolResult.returnDisplay.originalContent
? 'add'
: toolResult.returnDisplay.newContent === ''
? 'delete'
: 'modify',
},
};
}
return null;
}
} else {
return null;
}
}
const basicPermissionOptions = [
{
optionId: ToolConfirmationOutcome.ProceedOnce,
name: 'Allow',
kind: 'allow_once',
},
{
optionId: ToolConfirmationOutcome.Cancel,
name: 'Reject',
kind: 'reject_once',
},
] as const;
export function toPermissionOptions(
confirmation: ToolCallConfirmationDetails,
config: Config,
enablePermanentToolApproval: boolean = false,
): acp.PermissionOption[] {
const disableAlwaysAllow = config.getDisableAlwaysAllow();
const options: acp.PermissionOption[] = [];
if (!disableAlwaysAllow) {
switch (confirmation.type) {
case 'edit':
options.push({
optionId: ToolConfirmationOutcome.ProceedAlways,
name: 'Allow for this session',
kind: 'allow_always',
});
if (enablePermanentToolApproval) {
options.push({
optionId: ToolConfirmationOutcome.ProceedAlwaysAndSave,
name: 'Allow for this file in all future sessions',
kind: 'allow_always',
});
}
break;
case 'exec':
options.push({
optionId: ToolConfirmationOutcome.ProceedAlways,
name: 'Allow for this session',
kind: 'allow_always',
});
if (enablePermanentToolApproval) {
options.push({
optionId: ToolConfirmationOutcome.ProceedAlwaysAndSave,
name: 'Allow this command for all future sessions',
kind: 'allow_always',
});
}
break;
case 'mcp':
options.push(
{
optionId: ToolConfirmationOutcome.ProceedAlwaysServer,
name: 'Allow all server tools for this session',
kind: 'allow_always',
},
{
optionId: ToolConfirmationOutcome.ProceedAlwaysTool,
name: 'Allow tool for this session',
kind: 'allow_always',
},
);
if (enablePermanentToolApproval) {
options.push({
optionId: ToolConfirmationOutcome.ProceedAlwaysAndSave,
name: 'Allow tool for all future sessions',
kind: 'allow_always',
});
}
break;
case 'info':
options.push({
optionId: ToolConfirmationOutcome.ProceedAlways,
name: 'Allow for this session',
kind: 'allow_always',
});
if (enablePermanentToolApproval) {
options.push({
optionId: ToolConfirmationOutcome.ProceedAlwaysAndSave,
name: 'Allow for all future sessions',
kind: 'allow_always',
});
}
break;
case 'ask_user':
case 'exit_plan_mode':
// askuser and exit_plan_mode don't need "always allow" options
break;
default:
// No "always allow" options for other types
break;
}
}
options.push(...basicPermissionOptions);
// Exhaustive check
switch (confirmation.type) {
case 'edit':
case 'exec':
case 'mcp':
case 'info':
case 'ask_user':
case 'exit_plan_mode':
case 'sandbox_expansion':
break;
default: {
const unreachable: never = confirmation;
throw new Error(`Unexpected: ${unreachable}`);
}
}
return options;
}
export function toAcpToolKind(kind: Kind): acp.ToolKind {
switch (kind) {
case Kind.Read:
case Kind.Edit:
case Kind.Execute:
case Kind.Search:
case Kind.Delete:
case Kind.Move:
case Kind.Think:
case Kind.Fetch:
case Kind.SwitchMode:
case Kind.Other:
return kind as acp.ToolKind;
case Kind.Agent:
return 'think';
case Kind.Plan:
case Kind.Communicate:
default:
return 'other';
}
}
export function buildAvailableModes(isPlanEnabled: boolean): acp.SessionMode[] {
const modes: acp.SessionMode[] = [
{
id: ApprovalMode.DEFAULT,
name: 'Default',
description: 'Prompts for approval',
},
{
id: ApprovalMode.AUTO_EDIT,
name: 'Auto Edit',
description: 'Auto-approves edit tools',
},
{
id: ApprovalMode.YOLO,
name: 'YOLO',
description: 'Auto-approves all tools',
},
];
if (isPlanEnabled) {
modes.push({
id: ApprovalMode.PLAN,
name: 'Plan',
description: 'Read-only mode',
});
}
return modes;
}
export function buildAvailableModels(
config: Config,
settings: LoadedSettings,
): {
availableModels: Array<{
modelId: string;
name: string;
description?: string;
}>;
currentModelId: string;
} {
const preferredModel = config.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
const shouldShowPreviewModels = config.getHasAccessToPreviewModel();
const useGemini31 = config.getGemini31LaunchedSync?.() ?? false;
const useGemini31FlashLite =
config.getGemini31FlashLiteLaunchedSync?.() ?? false;
const selectedAuthType = settings.merged.security.auth.selectedType;
const useCustomToolModel =
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
// --- DYNAMIC PATH ---
if (
config.getExperimentalDynamicModelConfiguration?.() === true &&
config.getModelConfigService
) {
const options = config.getModelConfigService().getAvailableModelOptions({
useGemini3_1: useGemini31,
useGemini3_1FlashLite: useGemini31FlashLite,
useCustomTools: useCustomToolModel,
hasAccessToPreview: shouldShowPreviewModels,
});
return {
availableModels: options,
currentModelId: preferredModel,
};
}
// --- LEGACY PATH ---
const mainOptions = [
{
value: DEFAULT_GEMINI_MODEL_AUTO,
title: getDisplayString(DEFAULT_GEMINI_MODEL_AUTO),
description:
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
},
];
if (shouldShowPreviewModels) {
mainOptions.unshift({
value: PREVIEW_GEMINI_MODEL_AUTO,
title: getDisplayString(PREVIEW_GEMINI_MODEL_AUTO),
description: useGemini31
? 'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash'
: 'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
});
}
const manualOptions = [
{
value: DEFAULT_GEMINI_MODEL,
title: getDisplayString(DEFAULT_GEMINI_MODEL),
},
{
value: DEFAULT_GEMINI_FLASH_MODEL,
title: getDisplayString(DEFAULT_GEMINI_FLASH_MODEL),
},
{
value: DEFAULT_GEMINI_FLASH_LITE_MODEL,
title: getDisplayString(DEFAULT_GEMINI_FLASH_LITE_MODEL),
},
];
if (shouldShowPreviewModels) {
const previewProModel = useGemini31
? PREVIEW_GEMINI_3_1_MODEL
: PREVIEW_GEMINI_MODEL;
const previewProValue = useCustomToolModel
? PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL
: previewProModel;
const previewOptions = [
{
value: previewProValue,
title: getDisplayString(previewProModel),
},
{
value: PREVIEW_GEMINI_FLASH_MODEL,
title: getDisplayString(PREVIEW_GEMINI_FLASH_MODEL),
},
];
if (useGemini31FlashLite) {
previewOptions.push({
value: PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
title: getDisplayString(PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL),
});
}
manualOptions.unshift(...previewOptions);
}
const scaleOptions = (
options: Array<{ value: string; title: string; description?: string }>,
) =>
options.map((o) => ({
modelId: o.value,
name: o.title,
description: o.description,
}));
return {
availableModels: [
...scaleOptions(mainOptions),
...scaleOptions(manualOptions),
],
currentModelId: preferredModel,
};
}
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { CommandHandler } from './acpCommandHandler.js';
import { CommandHandler } from './commandHandler.js';
import { describe, it, expect } from 'vitest';
describe('CommandHandler', () => {
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
+10 -23
View File
@@ -1,13 +1,12 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
addMemory,
listInboxSkills,
listInboxPatches,
listMemoryFiles,
refreshMemory,
showMemory,
@@ -135,41 +134,29 @@ export class InboxMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
if (!context.agentContext.config.isAutoMemoryEnabled()) {
if (!context.agentContext.config.isMemoryManagerEnabled()) {
return {
name: this.name,
data: 'The memory inbox requires Auto Memory. Enable it with: experimental.autoMemory = true in settings.',
data: 'The memory inbox requires the experimental memory manager. Enable it with: experimental.memoryManager = true in settings.',
};
}
const [skills, patches] = await Promise.all([
listInboxSkills(context.agentContext.config),
listInboxPatches(context.agentContext.config),
]);
const skills = await listInboxSkills(context.agentContext.config);
if (skills.length === 0 && patches.length === 0) {
return { name: this.name, data: 'No items in inbox.' };
if (skills.length === 0) {
return { name: this.name, data: 'No extracted skills in inbox.' };
}
const lines: string[] = [];
for (const s of skills) {
const lines = skills.map((s) => {
const date = s.extractedAt
? ` (extracted: ${new Date(s.extractedAt).toLocaleDateString()})`
: '';
lines.push(`- **${s.name}**: ${s.description}${date}`);
}
for (const p of patches) {
const targets = p.entries.map((e) => e.targetPath).join(', ');
const date = p.extractedAt
? ` (extracted: ${new Date(p.extractedAt).toLocaleDateString()})`
: '';
lines.push(`- **${p.name}** (update): patches ${targets}${date}`);
}
return `- **${s.name}**: ${s.description}${date}`;
});
const total = skills.length + patches.length;
return {
name: this.name,
data: `Memory inbox (${total}):\n${lines.join('\n')}`,
data: `Skill inbox (${skills.length}):\n${lines.join('\n')}`,
};
}
}

Some files were not shown because too many files have changed in this diff Show More