Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96e5fc1663 | |||
| 3815de798f | |||
| ac3c643035 |
@@ -1,89 +0,0 @@
|
||||
# --- STAGE 1: Base Runtime ---
|
||||
FROM docker.io/library/node:20-slim AS base
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
curl \
|
||||
dnsutils \
|
||||
less \
|
||||
jq \
|
||||
ca-certificates \
|
||||
git \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# --- STAGE 2: Builder (Compile Main) ---
|
||||
FROM base AS builder
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN npm ci --ignore-scripts
|
||||
RUN npm run bundle
|
||||
# Run the official release preparation script to move the bundle and assets into packages/cli
|
||||
RUN node scripts/prepare-npm-release.js
|
||||
|
||||
# --- STAGE 3: Development Environment ---
|
||||
FROM base AS development
|
||||
|
||||
WORKDIR /home/node/dev/main
|
||||
|
||||
# Set up npm global package folder
|
||||
RUN mkdir -p /usr/local/share/npm-global \
|
||||
&& chown -R node:node /usr/local/share/npm-global
|
||||
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
|
||||
ENV PATH=$PATH:/usr/local/share/npm-global/bin
|
||||
|
||||
# Copy package.json to extract versions for global tools
|
||||
COPY package.json /tmp/package.json
|
||||
|
||||
# Install Build Tools, Global Dev Tools (pinned), and Linters
|
||||
ARG ACTIONLINT_VER=1.7.7
|
||||
ARG SHELLCHECK_VER=0.11.0
|
||||
ARG YAMLLINT_VER=1.35.1
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
make \
|
||||
g++ \
|
||||
gh \
|
||||
git \
|
||||
unzip \
|
||||
rsync \
|
||||
ripgrep \
|
||||
procps \
|
||||
psmisc \
|
||||
lsof \
|
||||
socat \
|
||||
tmux \
|
||||
docker.io \
|
||||
build-essential \
|
||||
libsecret-1-dev \
|
||||
libkrb5-dev \
|
||||
file \
|
||||
&& curl -sSLo /tmp/actionlint.tar.gz https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VER}/actionlint_${ACTIONLINT_VER}_linux_amd64.tar.gz \
|
||||
&& tar -xzf /tmp/actionlint.tar.gz -C /usr/local/bin actionlint \
|
||||
&& curl -sSLo /tmp/shellcheck.tar.xz https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VER}/shellcheck-v${SHELLCHECK_VER}.linux.x86_64.tar.xz \
|
||||
&& tar -xf /tmp/shellcheck.tar.xz -C /usr/local/bin --strip-components=1 shellcheck-v${SHELLCHECK_VER}/shellcheck \
|
||||
&& pip3 install --break-system-packages yamllint==${YAMLLINT_VER} \
|
||||
&& export TSX_VER=$(node -p "require('/tmp/package.json').devDependencies.tsx") \
|
||||
&& export VITEST_VER=$(node -p "require('/tmp/package.json').devDependencies.vitest") \
|
||||
&& export PRETTIER_VER=$(node -p "require('/tmp/package.json').devDependencies.prettier") \
|
||||
&& export ESLINT_VER=$(node -p "require('/tmp/package.json').devDependencies.eslint") \
|
||||
&& export CROSS_ENV_VER=$(node -p "require('/tmp/package.json').devDependencies['cross-env']") \
|
||||
&& npm install -g tsx@$TSX_VER vitest@$VITEST_VER prettier@$PRETTIER_VER eslint@$ESLINT_VER cross-env@$CROSS_ENV_VER typescript@5.3.3 \
|
||||
&& npm install -g @google/gemini-cli@nightly && mv /usr/local/share/npm-global/bin/gemini /usr/local/share/npm-global/bin/g-nightly \
|
||||
&& npm install -g @google/gemini-cli@preview && mv /usr/local/share/npm-global/bin/gemini /usr/local/share/npm-global/bin/g-preview \
|
||||
&& npm install -g @google/gemini-cli@latest && mv /usr/local/share/npm-global/bin/gemini /usr/local/share/npm-global/bin/g-stable \
|
||||
&& apt-get purge -y build-essential libsecret-1-dev libkrb5-dev \
|
||||
&& apt-get autoremove -y \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /root/.npm
|
||||
|
||||
# Copy the bundled CLI package to a permanent location and install it
|
||||
# We MUST not delete this source folder as 'npm install -g <folder>'
|
||||
# often symlinks to it for local folder installs.
|
||||
COPY --from=builder /build/packages/cli /usr/local/lib/gemini-cli
|
||||
RUN npm install -g /usr/local/lib/gemini-cli
|
||||
|
||||
USER node
|
||||
CMD ["/bin/bash"]
|
||||
@@ -1,10 +0,0 @@
|
||||
node_modules
|
||||
.git
|
||||
.gemini/workspaces
|
||||
dist
|
||||
!packages/*/dist/*.tgz
|
||||
bundle
|
||||
out
|
||||
*.log
|
||||
.env
|
||||
.DS_Store
|
||||
@@ -1,58 +0,0 @@
|
||||
substitutions:
|
||||
_IMAGE_NAME: 'development'
|
||||
_ARTIFACT_REGISTRY_REPO: 'us-docker.pkg.dev/gemini-code-dev/gemini-cli'
|
||||
|
||||
steps:
|
||||
# Step 1: Install root dependencies
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Install Dependencies'
|
||||
entrypoint: 'npm'
|
||||
args: ['install']
|
||||
|
||||
# Step 2: Authenticate for Docker
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Authenticate docker'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'auth']
|
||||
|
||||
# Step 3: Build workspace packages
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Build packages'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'build:packages']
|
||||
|
||||
# Step 4: Build Development Image
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Build Development Image'
|
||||
entrypoint: 'bash'
|
||||
env:
|
||||
- 'RAW_BRANCH_VALUE=${BRANCH_NAME}'
|
||||
args:
|
||||
- '-c'
|
||||
- |-
|
||||
IMAGE_BASE="${_ARTIFACT_REGISTRY_REPO}/${_IMAGE_NAME}"
|
||||
|
||||
# Determine the primary tag (branch name or 'latest' for main)
|
||||
# Use $$ for shell variables to avoid Cloud Build attempting premature substitution
|
||||
RAW_BRANCH="$$RAW_BRANCH_VALUE"
|
||||
if [ "$${RAW_BRANCH}" == "main" ]; then
|
||||
TAG_PRIMARY="latest"
|
||||
else
|
||||
TAG_PRIMARY=$$(echo "$${RAW_BRANCH}" | sed 's/[^a-zA-Z0-9]/-/g' | tr '[:upper:]' '[:lower:]')
|
||||
fi
|
||||
|
||||
# Use SHORT_SHA if available (Cloud Build) or fallback to latest-dev
|
||||
TAG_SHA="$${SHORT_SHA:-latest-dev}"
|
||||
|
||||
echo "📦 Building Development Image for: $${RAW_BRANCH} -> $${TAG_PRIMARY} ($${TAG_SHA})"
|
||||
|
||||
docker build -f .gcp/Dockerfile.development \
|
||||
-t "$${IMAGE_BASE}:$${TAG_SHA}" \
|
||||
-t "$${IMAGE_BASE}:$${TAG_PRIMARY}" .
|
||||
|
||||
docker push "$${IMAGE_BASE}:$${TAG_SHA}"
|
||||
docker push "$${IMAGE_BASE}:$${TAG_PRIMARY}"
|
||||
|
||||
options:
|
||||
defaultLogsBucketBehavior: 'REGIONAL_USER_OWNED_BUCKET'
|
||||
dynamicSubstitutions: true
|
||||
@@ -0,0 +1,60 @@
|
||||
description = "Check status of nightly evals, fix failures for key models, and re-run."
|
||||
prompt = """
|
||||
You are an expert at fixing behavioral evaluations.
|
||||
|
||||
1. **Investigate**:
|
||||
- Use 'gh' cli to fetch the results from the latest run from the main branch: https://github.com/google-gemini/gemini-cli/actions/workflows/evals-nightly.yml.
|
||||
- DO NOT push any changes or start any runs. The rest of your evaluation will be local.
|
||||
- Evals are in evals/ directory and are documented by evals/README.md.
|
||||
- The test case trajectory logs will be logged to evals/logs.
|
||||
- You should also enable and review the verbose agent logs by setting the GEMINI_DEBUG_LOG_FILE environment variable.
|
||||
- Identify the relevant test. Confine your investigation and validation to just this test.
|
||||
- Proactively add logging that will aid in gathering information or validating your hypotheses.
|
||||
|
||||
2. **Fix**:
|
||||
- If a relevant test is failing, locate the test file and the corresponding prompt/code.
|
||||
- It's often helpful to make an extreme, brute force change to see if you are changing the right place to make an improvement and then scope it back iteratively.
|
||||
- Your **final** change should be **minimal and targeted**.
|
||||
- Keep in mind the following:
|
||||
- The prompt has multiple configurations and pieces. Take care that your changes
|
||||
end up in the final prompt for the selected model and configuration.
|
||||
- The prompt chosen for the eval is intentional. It's often vague or indirect
|
||||
to see how the agent performs with ambiguous instructions. Changing it should
|
||||
be a last resort.
|
||||
- When changing the test prompt, carefully consider whether the prompt still tests
|
||||
the same scenario. We don't want to lose test fidelity by making the prompts too
|
||||
direct (i.e.: easy).
|
||||
- Your primary mechanism for improving the agent's behavior is to make changes to
|
||||
tool instructions, system prompt (snippets.ts), and/or modules that contribute to the prompt.
|
||||
- If prompt and description changes are unsuccessful, use logs and debugging to
|
||||
confirm that everything is working as expected.
|
||||
- If unable to fix the test, you can make recommendations for architecture changes
|
||||
that might help stablize the test. Be sure to THINK DEEPLY if offering architecture guidance.
|
||||
Some facts that might help with this are:
|
||||
- Agents may be composed of one or more agent loops.
|
||||
- AgentLoop == 'context + toolset + prompt'. Subagents are one type of agent loop.
|
||||
- Agent loops perform better when:
|
||||
- They have direct, unambiguous, and non-contradictory prompts.
|
||||
- They have fewer irrelevant tools.
|
||||
- They have fewer goals or steps to perform.
|
||||
- They have less low value or irrelevant context.
|
||||
- You may suggest compositions of existing primitives, like subagents, or
|
||||
propose a new one.
|
||||
- These recommendations should be high confidence and should be grounded
|
||||
in observed deficient behaviors rather than just parroting the facts above.
|
||||
Investigate as needed to ground your recommendations.
|
||||
|
||||
3. **Verify**:
|
||||
- Run just that one test if needed to validate that it is fixed. Be sure to run vitest in non-interactive mode.
|
||||
- Running the tests can take a long time, so consider whether you can diagnose via other means or log diagnostics before committing the time. You must minimize the number of test runs needed to diagnose the failure.
|
||||
- After the test completes, check whether it seems to have improved.
|
||||
- You will need to run the test 3 times for Gemini 3.0, Gemini 3 flash, and Gemini 2.5 pro to ensure that it is truly stable. Run these runs in parallel, using scripts if needed.
|
||||
- Some flakiness is expected; if it looks like a transient issue or the test is inherently unstable but passes 2/3 times, you might decide it cannot be improved.
|
||||
|
||||
4. **Report**:
|
||||
- Provide a summary of the test success rate for each of the tested models.
|
||||
- Success rate is calculated based on 3 runs per model (e.g., 3/3 = 100%).
|
||||
- If you couldn't fix it due to persistent flakiness, explain why.
|
||||
|
||||
{{args}}
|
||||
"""
|
||||
@@ -107,7 +107,7 @@ Gemini CLI project.
|
||||
set.
|
||||
- **Logging**: Use `debugLogger` for rethrown errors to avoid duplicate logging.
|
||||
- **Keyboard Shortcuts**: Define all new keyboard shortcuts in
|
||||
`packages/cli/src/ui/key/keyBindings.ts` and document them in
|
||||
`packages/cli/src/config/keyBindings.ts` and document them in
|
||||
`docs/cli/keyboard-shortcuts.md`. Be careful of keybindings that require the
|
||||
`Meta` key, as only certain meta key shortcuts are supported on Mac. Avoid
|
||||
function keys and shortcuts commonly bound in VSCode.
|
||||
|
||||
@@ -9,5 +9,4 @@ code_review:
|
||||
help: false
|
||||
summary: true
|
||||
code_review: true
|
||||
include_drafts: false
|
||||
ignore_patterns: []
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"experimental": {
|
||||
"extensionReloading": true,
|
||||
"modelSteering": true,
|
||||
"memoryManager": false,
|
||||
"topicUpdateNarration": true
|
||||
"plan": true,
|
||||
"extensionReloading": true
|
||||
},
|
||||
"general": {
|
||||
"devtools": true
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
name: async-pr-review
|
||||
description: Trigger this skill when the user wants to start an asynchronous PR review, run background checks on a PR, or check the status of a previously started async PR review.
|
||||
---
|
||||
|
||||
# Async PR Review
|
||||
|
||||
This skill provides a set of tools to asynchronously review a Pull Request. It will create a background job to run the project's preflight checks, execute Gemini-powered test plans, and perform a comprehensive code review using custom prompts.
|
||||
|
||||
This skill is designed to showcase an advanced "Agentic Asynchronous Pattern":
|
||||
1. **Native Background Shells vs Headless Inference**: While Gemini CLI can natively spawn and detach background shell commands (using the `run_shell_command` tool with `is_background: true`), a standard bash background job cannot perform LLM inference. To conduct AI-driven code reviews and test generation in the background, the shell script *must* invoke the `gemini` executable headlessly using `-p`. This offloads the AI tasks to independent worker agents.
|
||||
2. **Dynamic Git Scoping**: The review scripts avoid hardcoded paths. They use `git rev-parse --show-toplevel` to automatically resolve the root of the user's current project.
|
||||
3. **Ephemeral Worktrees**: Instead of checking out branches in the user's main workspace, the skill provisions temporary git worktrees in `.gemini/tmp/async-reviews/pr-<number>`. This prevents git lock conflicts and namespace pollution.
|
||||
4. **Agentic Evaluation (`check-async-review.sh`)**: The check script outputs clean JSON/text statuses for the main agent to parse. The interactive agent itself synthesizes the final assessment dynamically from the generated log files.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Determine Action**: Establish whether the user wants to start a new async review or check the status of an existing one.
|
||||
* If the user says "start an async review for PR #123" or similar, proceed to **Start Review**.
|
||||
* If the user says "check the status of my async review for PR #123" or similar, proceed to **Check Status**.
|
||||
|
||||
### Start Review
|
||||
|
||||
If the user wants to start a new async PR review:
|
||||
|
||||
1. Ask the user for the PR number if they haven't provided it.
|
||||
2. Execute the `async-review.sh` script, passing the PR number as the first argument. Be sure to run it with the `is_background` flag set to true to ensure it immediately detaches.
|
||||
```bash
|
||||
.gemini/skills/async-pr-review/scripts/async-review.sh <PR_NUMBER>
|
||||
```
|
||||
3. Inform the user that the tasks have started successfully and they can check the status later.
|
||||
|
||||
### Check Status
|
||||
|
||||
If the user wants to check the status or view the final assessment of a previously started async review:
|
||||
|
||||
1. Ask the user for the PR number if they haven't provided it.
|
||||
2. Execute the `check-async-review.sh` script, passing the PR number as the first argument:
|
||||
```bash
|
||||
.gemini/skills/async-pr-review/scripts/check-async-review.sh <PR_NUMBER>
|
||||
```
|
||||
3. **Evaluate Output**: Read the output from the script.
|
||||
* If the output contains `STATUS: IN_PROGRESS`, tell the user which tasks are still running.
|
||||
* If the output contains `STATUS: COMPLETE`, use your file reading tools (`read_file`) to retrieve the contents of `final-assessment.md`, `review.md`, `pr-diff.diff`, `npm-test.log`, and `test-execution.log` files from the `LOG_DIR` specified in the output.
|
||||
* **Final Assessment**: Read those files, synthesize their results, and give the user a concise recommendation on whether the PR builds successfully, passes tests, and if you recommend they approve it based on the review.
|
||||
@@ -1,148 +0,0 @@
|
||||
# --- CORE TOOLS ---
|
||||
[[rule]]
|
||||
toolName = "read_file"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "write_file"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "grep_search"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "glob"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "list_directory"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
[[rule]]
|
||||
toolName = "codebase_investigator"
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
# --- SHELL COMMANDS ---
|
||||
|
||||
# Git (Safe/Read-only)
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = [
|
||||
"git blame",
|
||||
"git show",
|
||||
"git grep",
|
||||
"git show-ref",
|
||||
"git ls-tree",
|
||||
"git ls-remote",
|
||||
"git reflog",
|
||||
"git remote -v",
|
||||
"git diff",
|
||||
"git rev-list",
|
||||
"git rev-parse",
|
||||
"git merge-base",
|
||||
"git cherry",
|
||||
"git fetch",
|
||||
"git status",
|
||||
"git st",
|
||||
"git branch",
|
||||
"git br",
|
||||
"git log",
|
||||
"git --version"
|
||||
]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
# GitHub CLI (Read-only)
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = [
|
||||
"gh workflow list",
|
||||
"gh auth status",
|
||||
"gh checkout view",
|
||||
"gh run view",
|
||||
"gh run job view",
|
||||
"gh run list",
|
||||
"gh run --help",
|
||||
"gh issue view",
|
||||
"gh issue list",
|
||||
"gh label list",
|
||||
"gh pr diff",
|
||||
"gh pr check",
|
||||
"gh pr checks",
|
||||
"gh pr view",
|
||||
"gh pr list",
|
||||
"gh pr status",
|
||||
"gh repo view",
|
||||
"gh job view",
|
||||
"gh api",
|
||||
"gh log"
|
||||
]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
# Node.js/NPM (Generic Tests, Checks, and Build)
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = [
|
||||
"npm run start",
|
||||
"npm install",
|
||||
"npm run",
|
||||
"npm test",
|
||||
"npm ci",
|
||||
"npm list",
|
||||
"npm --version"
|
||||
]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
|
||||
# Core Utilities (Safe)
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
commandPrefix = [
|
||||
"sleep",
|
||||
"env",
|
||||
"break",
|
||||
"xargs",
|
||||
"base64",
|
||||
"uniq",
|
||||
"sort",
|
||||
"echo",
|
||||
"which",
|
||||
"ls",
|
||||
"find",
|
||||
"tail",
|
||||
"head",
|
||||
"cat",
|
||||
"cd",
|
||||
"grep",
|
||||
"ps",
|
||||
"pwd",
|
||||
"wc",
|
||||
"file",
|
||||
"stat",
|
||||
"diff",
|
||||
"lsof",
|
||||
"date",
|
||||
"whoami",
|
||||
"uname",
|
||||
"du",
|
||||
"cut",
|
||||
"true",
|
||||
"false",
|
||||
"readlink",
|
||||
"awk",
|
||||
"jq",
|
||||
"rg",
|
||||
"less",
|
||||
"more",
|
||||
"tree"
|
||||
]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
@@ -1,245 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
notify() {
|
||||
local title="${1}"
|
||||
local message="${2}"
|
||||
local pr="${3}"
|
||||
# Terminal escape sequence
|
||||
printf "\e]9;%s | PR #%s | %s\a" "${title}" "${pr}" "${message}"
|
||||
# Native macOS notification
|
||||
os_type="$(uname || true)"
|
||||
if [[ "${os_type}" == "Darwin" ]]; then
|
||||
osascript -e "display notification \"${message}\" with title \"${title}\" subtitle \"PR #${pr}\""
|
||||
fi
|
||||
}
|
||||
|
||||
pr_number="${1}"
|
||||
if [[ -z "${pr_number}" ]]; then
|
||||
echo "Usage: async-review <pr_number>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
base_dir="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [[ -z "${base_dir}" ]]; then
|
||||
echo "❌ Must be run from within a git repository."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use the repository's local .gemini/tmp directory for ephemeral worktrees and logs
|
||||
pr_dir="${base_dir}/.gemini/tmp/async-reviews/pr-${pr_number}"
|
||||
target_dir="${pr_dir}/worktree"
|
||||
log_dir="${pr_dir}/logs"
|
||||
|
||||
cd "${base_dir}" || exit 1
|
||||
|
||||
mkdir -p "${log_dir}"
|
||||
rm -f "${log_dir}/setup.exit" "${log_dir}/final-assessment.exit" "${log_dir}/final-assessment.md"
|
||||
|
||||
echo "🧹 Cleaning up previous worktree if it exists..." | tee -a "${log_dir}/setup.log"
|
||||
git worktree remove -f "${target_dir}" >> "${log_dir}/setup.log" 2>&1 || true
|
||||
git branch -D "gemini-async-pr-${pr_number}" >> "${log_dir}/setup.log" 2>&1 || true
|
||||
git worktree prune >> "${log_dir}/setup.log" 2>&1 || true
|
||||
|
||||
echo "📡 Fetching PR #${pr_number}..." | tee -a "${log_dir}/setup.log"
|
||||
if ! git fetch origin -f "pull/${pr_number}/head:gemini-async-pr-${pr_number}" >> "${log_dir}/setup.log" 2>&1; then
|
||||
echo 1 > "${log_dir}/setup.exit"
|
||||
echo "❌ Fetch failed. Check ${log_dir}/setup.log"
|
||||
notify "Async Review Failed" "Fetch failed." "${pr_number}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "${target_dir}" ]]; then
|
||||
echo "🧹 Pruning missing worktrees..." | tee -a "${log_dir}/setup.log"
|
||||
git worktree prune >> "${log_dir}/setup.log" 2>&1
|
||||
echo "🌿 Creating worktree in ${target_dir}..." | tee -a "${log_dir}/setup.log"
|
||||
if ! git worktree add "${target_dir}" "gemini-async-pr-${pr_number}" >> "${log_dir}/setup.log" 2>&1; then
|
||||
echo 1 > "${log_dir}/setup.exit"
|
||||
echo "❌ Worktree creation failed. Check ${log_dir}/setup.log"
|
||||
notify "Async Review Failed" "Worktree creation failed." "${pr_number}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "🌿 Worktree already exists." | tee -a "${log_dir}/setup.log"
|
||||
fi
|
||||
echo 0 > "${log_dir}/setup.exit"
|
||||
|
||||
cd "${target_dir}" || exit 1
|
||||
|
||||
echo "🚀 Launching background tasks. Logs saving to: ${log_dir}"
|
||||
|
||||
echo " ↳ [1/5] Grabbing PR diff..."
|
||||
rm -f "${log_dir}/pr-diff.exit"
|
||||
{ gh pr diff "${pr_number}" > "${log_dir}/pr-diff.diff" 2>&1; echo $? > "${log_dir}/pr-diff.exit"; } &
|
||||
|
||||
echo " ↳ [2/5] Starting build and lint..."
|
||||
rm -f "${log_dir}/build-and-lint.exit"
|
||||
{ { npm run clean && npm ci && npm run format && npm run build && npm run lint:ci && npm run typecheck; } > "${log_dir}/build-and-lint.log" 2>&1; echo $? > "${log_dir}/build-and-lint.exit"; } &
|
||||
|
||||
# Dynamically resolve gemini binary (fallback to your nightly path)
|
||||
GEMINI_CMD="$(command -v gemini || echo "${HOME}/.gcli/nightly/node_modules/.bin/gemini")"
|
||||
# shellcheck disable=SC2312
|
||||
POLICY_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/policy.toml"
|
||||
|
||||
echo " ↳ [3/5] Starting Gemini code review..."
|
||||
rm -f "${log_dir}/review.exit"
|
||||
{ "${GEMINI_CMD}" --policy "${POLICY_PATH}" -p "/review-frontend ${pr_number}" > "${log_dir}/review.md" 2>&1; echo $? > "${log_dir}/review.exit"; } &
|
||||
|
||||
echo " ↳ [4/5] Starting automated tests (waiting for build and lint)..."
|
||||
rm -f "${log_dir}/npm-test.exit"
|
||||
{
|
||||
while [[ ! -f "${log_dir}/build-and-lint.exit" ]]; do sleep 1; done
|
||||
read -r build_exit < "${log_dir}/build-and-lint.exit" || build_exit=""
|
||||
if [[ "${build_exit}" == "0" ]]; then
|
||||
gh pr checks "${pr_number}" > "${log_dir}/ci-checks.log" 2>&1
|
||||
ci_status=$?
|
||||
|
||||
if [[ "${ci_status}" -eq 0 ]]; then
|
||||
echo "CI checks passed. Skipping local npm tests." > "${log_dir}/npm-test.log"
|
||||
echo 0 > "${log_dir}/npm-test.exit"
|
||||
elif [[ "${ci_status}" -eq 8 ]]; then
|
||||
echo "CI checks are still pending. Skipping local npm tests to avoid duplicate work. Please check GitHub for final results." > "${log_dir}/npm-test.log"
|
||||
echo 0 > "${log_dir}/npm-test.exit"
|
||||
else
|
||||
echo "CI checks failed. Failing checks:" > "${log_dir}/npm-test.log"
|
||||
gh pr checks "${pr_number}" --json name,bucket -q '.[] | select(.bucket=="fail") | .name' >> "${log_dir}/npm-test.log" 2>&1
|
||||
|
||||
echo "Attempting to extract failing test files from CI logs..." >> "${log_dir}/npm-test.log"
|
||||
pr_branch="$(gh pr view "${pr_number}" --json headRefName -q '.headRefName' 2>/dev/null || true)"
|
||||
run_id="$(gh run list --branch "${pr_branch}" --workflow ci.yml --json databaseId -q '.[0].databaseId' 2>/dev/null || true)"
|
||||
|
||||
failed_files=""
|
||||
if [[ -n "${run_id}" ]]; then
|
||||
failed_files="$(gh run view "${run_id}" --log-failed 2>/dev/null | grep -o -E '(packages/[a-zA-Z0-9_-]+|integration-tests|evals)/[a-zA-Z0-9_/-]+\.test\.ts(x)?' | sort | uniq || true)"
|
||||
fi
|
||||
|
||||
if [[ -n "${failed_files}" ]]; then
|
||||
echo "Found failing test files from CI:" >> "${log_dir}/npm-test.log"
|
||||
for f in ${failed_files}; do echo " - ${f}" >> "${log_dir}/npm-test.log"; done
|
||||
echo "Running ONLY failing tests locally..." >> "${log_dir}/npm-test.log"
|
||||
|
||||
exit_code=0
|
||||
for file in ${failed_files}; do
|
||||
if [[ "${file}" == packages/* ]]; then
|
||||
ws_dir="$(echo "${file}" | cut -d'/' -f1,2)"
|
||||
else
|
||||
ws_dir="$(echo "${file}" | cut -d'/' -f1)"
|
||||
fi
|
||||
rel_file="${file#"${ws_dir}"/}"
|
||||
|
||||
echo "--- Running ${rel_file} in workspace ${ws_dir} ---" >> "${log_dir}/npm-test.log"
|
||||
if ! npm run test:ci -w "${ws_dir}" -- "${rel_file}" >> "${log_dir}/npm-test.log" 2>&1; then
|
||||
exit_code=1
|
||||
fi
|
||||
done
|
||||
echo "${exit_code}" > "${log_dir}/npm-test.exit"
|
||||
else
|
||||
echo "Could not extract specific failing files. Skipping full local test suite as it takes too long. Please check CI logs manually." >> "${log_dir}/npm-test.log"
|
||||
echo 1 > "${log_dir}/npm-test.exit"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Skipped due to build-and-lint failure" > "${log_dir}/npm-test.log"
|
||||
echo 1 > "${log_dir}/npm-test.exit"
|
||||
fi
|
||||
} &
|
||||
|
||||
echo " ↳ [5/5] Starting Gemini test execution (waiting for build and lint)..."
|
||||
rm -f "${log_dir}/test-execution.exit"
|
||||
{
|
||||
while [[ ! -f "${log_dir}/build-and-lint.exit" ]]; do sleep 1; done
|
||||
read -r build_exit < "${log_dir}/build-and-lint.exit" || build_exit=""
|
||||
if [[ "${build_exit}" == "0" ]]; then
|
||||
"${GEMINI_CMD}" --policy "${POLICY_PATH}" -p "Analyze the diff for PR ${pr_number} using 'gh pr diff ${pr_number}'. Instead of running the project's automated test suite (like 'npm test'), physically exercise the newly changed code in the terminal (e.g., by writing a temporary script to call the new functions, or testing the CLI command directly). Verify the feature's behavior works as expected. IMPORTANT: Do NOT modify any source code to fix errors. Just exercise the code and log the results, reporting any failures clearly. Do not ask for user confirmation." > "${log_dir}/test-execution.log" 2>&1; echo $? > "${log_dir}/test-execution.exit"
|
||||
else
|
||||
echo "Skipped due to build-and-lint failure" > "${log_dir}/test-execution.log"
|
||||
echo 1 > "${log_dir}/test-execution.exit"
|
||||
fi
|
||||
} &
|
||||
|
||||
echo "✅ All tasks dispatched!"
|
||||
echo "You can monitor progress with: tail -f ${log_dir}/*.log"
|
||||
echo "Read your review later at: ${log_dir}/review.md"
|
||||
|
||||
# Polling loop to wait for all background tasks to finish
|
||||
tasks=("pr-diff" "build-and-lint" "review" "npm-test" "test-execution")
|
||||
log_files=("pr-diff.diff" "build-and-lint.log" "review.md" "npm-test.log" "test-execution.log")
|
||||
|
||||
declare -A task_done
|
||||
for t in "${tasks[@]}"; do task_done[${t}]=0; done
|
||||
|
||||
all_done=0
|
||||
while [[ "${all_done}" -eq 0 ]]; do
|
||||
clear
|
||||
echo "=================================================="
|
||||
echo "🚀 Async PR Review Status for PR #${pr_number}"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
|
||||
all_done=1
|
||||
for i in "${!tasks[@]}"; do
|
||||
t="${tasks[${i}]}"
|
||||
|
||||
if [[ -f "${log_dir}/${t}.exit" ]]; then
|
||||
read -r task_exit < "${log_dir}/${t}.exit" || task_exit=""
|
||||
if [[ "${task_exit}" == "0" ]]; then
|
||||
echo " ✅ ${t}: SUCCESS"
|
||||
else
|
||||
echo " ❌ ${t}: FAILED (exit code ${task_exit})"
|
||||
fi
|
||||
task_done[${t}]=1
|
||||
else
|
||||
echo " ⏳ ${t}: RUNNING"
|
||||
all_done=0
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=================================================="
|
||||
echo "📝 Live Logs (Last 5 lines of running tasks)"
|
||||
echo "=================================================="
|
||||
|
||||
for i in "${!tasks[@]}"; do
|
||||
t="${tasks[${i}]}"
|
||||
log_file="${log_files[${i}]}"
|
||||
|
||||
if [[ "${task_done[${t}]}" -eq 0 ]]; then
|
||||
if [[ -f "${log_dir}/${log_file}" ]]; then
|
||||
echo ""
|
||||
echo "--- ${t} ---"
|
||||
tail -n 5 "${log_dir}/${log_file}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${all_done}" -eq 0 ]]; then
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
|
||||
clear
|
||||
echo "=================================================="
|
||||
echo "🚀 Async PR Review Status for PR #${pr_number}"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
for t in "${tasks[@]}"; do
|
||||
read -r task_exit < "${log_dir}/${t}.exit" || task_exit=""
|
||||
if [[ "${task_exit}" == "0" ]]; then
|
||||
echo " ✅ ${t}: SUCCESS"
|
||||
else
|
||||
echo " ❌ ${t}: FAILED (exit code ${task_exit})"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
echo "⏳ Tasks complete! Synthesizing final assessment..."
|
||||
if ! "${GEMINI_CMD}" --policy "${POLICY_PATH}" -p "Read the review at ${log_dir}/review.md, the automated test logs at ${log_dir}/npm-test.log, and the manual test execution logs at ${log_dir}/test-execution.log. Summarize the results, state whether the build and tests passed based on ${log_dir}/build-and-lint.exit and ${log_dir}/npm-test.exit, and give a final recommendation for PR ${pr_number}." > "${log_dir}/final-assessment.md" 2>&1; then
|
||||
echo $? > "${log_dir}/final-assessment.exit"
|
||||
echo "❌ Final assessment synthesis failed!"
|
||||
echo "Check ${log_dir}/final-assessment.md for details."
|
||||
notify "Async Review Failed" "Final assessment synthesis failed." "${pr_number}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo 0 > "${log_dir}/final-assessment.exit"
|
||||
echo "✅ Final assessment complete! Check ${log_dir}/final-assessment.md"
|
||||
notify "Async Review Complete" "Review and test execution finished successfully." "${pr_number}"
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/bash
|
||||
pr_number="${1}"
|
||||
|
||||
if [[ -z "${pr_number}" ]]; then
|
||||
echo "Usage: check-async-review <pr_number>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
base_dir="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
if [[ -z "${base_dir}" ]]; then
|
||||
echo "❌ Must be run from within a git repository."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_dir="${base_dir}/.gemini/tmp/async-reviews/pr-${pr_number}/logs"
|
||||
|
||||
if [[ ! -d "${log_dir}" ]]; then
|
||||
echo "STATUS: NOT_FOUND"
|
||||
echo "❌ No logs found for PR #${pr_number} in ${log_dir}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tasks=(
|
||||
"setup|setup.log"
|
||||
"pr-diff|pr-diff.diff"
|
||||
"build-and-lint|build-and-lint.log"
|
||||
"review|review.md"
|
||||
"npm-test|npm-test.log"
|
||||
"test-execution|test-execution.log"
|
||||
"final-assessment|final-assessment.md"
|
||||
)
|
||||
|
||||
all_done=true
|
||||
echo "STATUS: CHECKING"
|
||||
|
||||
for task_info in "${tasks[@]}"; do
|
||||
IFS="|" read -r task_name log_file <<< "${task_info}"
|
||||
|
||||
file_path="${log_dir}/${log_file}"
|
||||
exit_file="${log_dir}/${task_name}.exit"
|
||||
|
||||
if [[ -f "${exit_file}" ]]; then
|
||||
read -r exit_code < "${exit_file}" || exit_code=""
|
||||
if [[ "${exit_code}" == "0" ]]; then
|
||||
echo "✅ ${task_name}: SUCCESS"
|
||||
else
|
||||
echo "❌ ${task_name}: FAILED (exit code ${exit_code})"
|
||||
echo " Last lines of ${file_path}:"
|
||||
tail -n 3 "${file_path}" | sed 's/^/ /' || true
|
||||
fi
|
||||
elif [[ -f "${file_path}" ]]; then
|
||||
echo "⏳ ${task_name}: RUNNING"
|
||||
all_done=false
|
||||
else
|
||||
echo "➖ ${task_name}: NOT STARTED"
|
||||
all_done=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${all_done}" == "true" ]]; then
|
||||
echo "STATUS: COMPLETE"
|
||||
echo "LOG_DIR: ${log_dir}"
|
||||
else
|
||||
echo "STATUS: IN_PROGRESS"
|
||||
fi
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
name: behavioral-evals
|
||||
description: Guidance for creating, running, fixing, and promoting behavioral evaluations. Use when verifying agent decision logic, debugging failures, debugging prompt steering, or adding workspace regression tests.
|
||||
---
|
||||
|
||||
# Behavioral Evals
|
||||
|
||||
## Overview
|
||||
|
||||
Behavioral evaluations (evals) are tests that validate the **agent's decision-making** (e.g., tool choice) rather than pure functionality. They are critical for verifying prompt changes, debugging steerability, and preventing regressions.
|
||||
|
||||
> [!NOTE]
|
||||
> **Single Source of Truth**: For core concepts, policies, running tests, and general best practices, always refer to **[evals/README.md](file:///Users/abhipatel/code/gemini-cli/docs/evals/README.md)**.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Workflow Decision Tree
|
||||
|
||||
1. **Does a prompt/tool change need validation?**
|
||||
* *No* -> Normal integration tests.
|
||||
* *Yes* -> Continue below.
|
||||
2. **Is it UI/Interaction heavy?**
|
||||
* *Yes* -> Use `appEvalTest` (`AppRig`). See **[creating.md](references/creating.md)**.
|
||||
* *No* -> Use `evalTest` (`TestRig`). See **[creating.md](references/creating.md)**.
|
||||
3. **Is it a new test?**
|
||||
* *Yes* -> Set policy to `USUALLY_PASSES`.
|
||||
* *No* -> `ALWAYS_PASSES` (locks in regression).
|
||||
4. **Are you fixing a failure or promoting a test?**
|
||||
* *Fixing* -> See **[fixing.md](references/fixing.md)**.
|
||||
* *Promoting* -> See **[promoting.md](references/promoting.md)**.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Quick Checklist
|
||||
|
||||
### 1. Setup Workspace
|
||||
Seed the workspace with necessary files using the `files` object to simulate a realistic scenario (e.g., NodeJS project with `package.json`).
|
||||
* *Details in **[creating.md](references/creating.md)***
|
||||
|
||||
### 2. Write Assertions
|
||||
Audit agent decisions using `rig.setBreakpoint()` (AppRig only) or index verification on `rig.readToolLogs()`.
|
||||
* *Details in **[creating.md](references/creating.md)***
|
||||
|
||||
### 3. Verify
|
||||
Run single tests locally with Vitest. Confirm stability locally before relying on CI workflows.
|
||||
* *See **[evals/README.md](file:///Users/abhipatel/code/gemini-cli/docs/evals/README.md)** for running commands.*
|
||||
|
||||
---
|
||||
|
||||
## 📦 Bundled Resources
|
||||
|
||||
Detailed procedural guides:
|
||||
* **[creating.md](references/creating.md)**: Assertion strategies, Rig selection, Mock MCPs.
|
||||
* **[fixing.md](references/fixing.md)**: Step-by-step automated investigation, architecture diagnosis guidelines.
|
||||
* **[promoting.md](references/promoting.md)**: Candidate identification criteria and threshold guidelines.
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { describe, expect } from 'vitest';
|
||||
import { appEvalTest } from './app-test-helper.js';
|
||||
|
||||
describe('interactive_feature', () => {
|
||||
// New tests MUST start as USUALLY_PASSES
|
||||
appEvalTest('USUALLY_PASSES', {
|
||||
name: 'should pause for user confirmation',
|
||||
files: {
|
||||
'package.json': JSON.stringify({ name: 'app' })
|
||||
},
|
||||
prompt: 'Task description here requiring approval',
|
||||
timeout: 60000,
|
||||
setup: async (rig) => {
|
||||
// ⚠️ Breakpoints are ONLY safe in appEvalTest
|
||||
rig.setBreakpoint(['ask_user']);
|
||||
},
|
||||
assert: async (rig) => {
|
||||
// 1. Wait for the breakpoint to trigger
|
||||
const confirmation = await rig.waitForPendingConfirmation('ask_user');
|
||||
expect(confirmation).toBeDefined();
|
||||
|
||||
// 2. Resolve it so the test can finish
|
||||
await rig.resolveTool(confirmation);
|
||||
await rig.waitForIdle();
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
describe('core_feature', () => {
|
||||
// New tests MUST start as USUALLY_PASSES
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should perform expected agent action',
|
||||
setup: async (rig) => {
|
||||
// For mocking offline MCP:
|
||||
// rig.addMockMcpServer('workspace-server', 'google-workspace');
|
||||
},
|
||||
files: {
|
||||
'src/app.ts': '// some code',
|
||||
},
|
||||
prompt: 'Task description here',
|
||||
timeout: 60000, // 1 minute safety limit
|
||||
assert: async (rig, result) => {
|
||||
// 1. Audit the trajectory (Safe for standard evalTest)
|
||||
const logs = rig.readToolLogs();
|
||||
const hasTool = logs.some((l) => l.toolRequest.name === 'read_file');
|
||||
expect(hasTool, 'Agent should have read the file').toBe(true);
|
||||
|
||||
// 2. Assert efficiency (Cost/Turn)
|
||||
expect(logs.length).toBeLessThan(5);
|
||||
|
||||
// 3. Assert final output
|
||||
expect(result).toContain('Expected Keyword');
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,151 +0,0 @@
|
||||
# Creating Behavioral Evals
|
||||
|
||||
## 🔬 Rig Selection
|
||||
|
||||
| Rig Type | Import From | Architecture | Use When |
|
||||
| :---------------- | :--------------------- | :------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- |
|
||||
| **`evalTest`** | `./test-helper.js` | **Subprocess**. Runs the CLI in a separate process + waits for exit. | Standard workspace tests. **Do not use `setBreakpoint`**; auditing history (`readToolLogs`) is safer. |
|
||||
| **`appEvalTest`** | `./app-test-helper.js` | **In-Process**. Runs directly inside the runner loop. | UI/Ink rendering. Safe for `setBreakpoint` triggers. |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Scenario Design
|
||||
|
||||
Evals must simulate realistic agent environments to effectively test
|
||||
decision-making.
|
||||
|
||||
- **Workspace State**: Seed with standard project anchors if testing general
|
||||
capabilities:
|
||||
- `package.json` for NodeJS environments.
|
||||
- Minimal configuration files (`tsconfig.json`, `GEMINI.md`).
|
||||
- **Structural Complexity**: Provide enough files to force the agent to _search_
|
||||
or _navigate_, rather than giving the answer directly. Avoid trivial one-file
|
||||
tests unless testing exact prompt steering.
|
||||
|
||||
---
|
||||
|
||||
## ❌ Fail First Principle
|
||||
|
||||
Before asserting a new capability or locking in a fix, **verify that the test
|
||||
fails first**.
|
||||
|
||||
- It is easy to accidentally write an eval that asserts behaviors that are
|
||||
already met or pass by default.
|
||||
- **Process**: reproduce failure with test -> apply fix (prompt/tool) -> verify
|
||||
test passes.
|
||||
|
||||
---
|
||||
|
||||
## ✋ Testing Patterns
|
||||
|
||||
### 1. Breakpoints
|
||||
|
||||
Verifies the agent _intends_ to use a tool BEFORE executing it. Useful for
|
||||
interactive prompts or safety checks.
|
||||
|
||||
```typescript
|
||||
// ⚠️ Only works with appEvalTest (AppRig)
|
||||
setup: async (rig) => {
|
||||
rig.setBreakpoint(['ask_user']);
|
||||
},
|
||||
assert: async (rig) => {
|
||||
const confirmation = await rig.waitForPendingConfirmation('ask_user');
|
||||
expect(confirmation).toBeDefined();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Tool Confirmation Race
|
||||
|
||||
When asserting multiple triggers (e.g., "enters plan mode then asks question"):
|
||||
|
||||
```typescript
|
||||
assert: async (rig) => {
|
||||
let confirmation = await rig.waitForPendingConfirmation([
|
||||
'enter_plan_mode',
|
||||
'ask_user',
|
||||
]);
|
||||
|
||||
if (confirmation?.name === 'enter_plan_mode') {
|
||||
rig.acceptConfirmation('enter_plan_mode');
|
||||
confirmation = await rig.waitForPendingConfirmation('ask_user');
|
||||
}
|
||||
expect(confirmation?.toolName).toBe('ask_user');
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Audit Tool Logs
|
||||
|
||||
Audit exact operations to ensure efficiency (e.g., no redundant reads).
|
||||
|
||||
```typescript
|
||||
assert: async (rig, result) => {
|
||||
await rig.waitForTelemetryReady();
|
||||
const toolLogs = rig.readToolLogs();
|
||||
|
||||
const writeCall = toolLogs.find(
|
||||
(log) => log.toolRequest.name === 'write_file',
|
||||
);
|
||||
expect(writeCall).toBeDefined();
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Mock MCP Facades
|
||||
|
||||
To evaluate tools connected via MCP without hitting live endpoints, load a mock
|
||||
server configuration in the `setup` hook.
|
||||
|
||||
```typescript
|
||||
setup: async (rig) => {
|
||||
rig.addMockMcpServer('workspace-server', 'google-workspace');
|
||||
},
|
||||
assert: async (rig) => {
|
||||
await rig.waitForTelemetryReady();
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const workspaceCall = toolLogs.find(
|
||||
(log) => log.toolRequest.name === 'mcp_workspace-server_docs.getText'
|
||||
);
|
||||
expect(workspaceCall).toBeDefined();
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Safety & Efficiency Guardrails
|
||||
|
||||
### 1. Breakpoint Deadlocks
|
||||
|
||||
Breakpoints (`setBreakpoint`) pause execution. In standard `evalTest`,
|
||||
`rig.run()` waits for the process to exit _before_ assertions run. **This will
|
||||
hang indefinitely.**
|
||||
|
||||
- **Use Breakpoints** for `appEvalTest` or interactive simulations.
|
||||
- **Use Audit Tool Logs** (above) for standard trajectory tests.
|
||||
|
||||
### 2. Runaway Timeout
|
||||
|
||||
Always set a budget boundary in the `EvalCase` to prevent runaway loops on
|
||||
quota:
|
||||
|
||||
```typescript
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: '...',
|
||||
timeout: 60000, // 1 minute safety limit
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Efficiency Assertion (Turn limits)
|
||||
|
||||
Check if a tool is called _early_ using index checks:
|
||||
|
||||
```typescript
|
||||
assert: async (rig) => {
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const toolCallIndex = toolLogs.findIndex(
|
||||
(log) => log.toolRequest.name === 'cli_help',
|
||||
);
|
||||
|
||||
expect(toolCallIndex).toBeGreaterThan(-1);
|
||||
expect(toolCallIndex).toBeLessThan(5); // Called within first 5 turns
|
||||
};
|
||||
```
|
||||
@@ -1,71 +0,0 @@
|
||||
# Fixing Behavioral Evals
|
||||
|
||||
Use this guide when asked to debug, troubleshoot, or fix a failing behavioral
|
||||
evaluation.
|
||||
|
||||
---
|
||||
|
||||
## 1. 🔍 Investigate
|
||||
|
||||
1. **Fetch Nightly Results**: Use the `gh` CLI to inspect the latest run from
|
||||
`evals-nightly.yml` if applicable.
|
||||
- _Example view URL_:
|
||||
`https://github.com/google-gemini/gemini-cli/actions/workflows/evals-nightly.yml`
|
||||
2. **Isolate**: DO NOT push changes or start remote runs. Confine investigation
|
||||
to the local workspace.
|
||||
3. **Read Logs**:
|
||||
- Eval logs live in `evals/logs/<test_name>.log`.
|
||||
- Enable verbose debugging via `export GEMINI_DEBUG_LOG_FILE="debug.log"`.
|
||||
4. **Diagnose**: Audit tool logs and telemetry. Note if due to setup/assert.
|
||||
- **Tip**: Proactively add custom logging/diagnostics to check hypotheses.
|
||||
|
||||
---
|
||||
|
||||
## 2. 🛠️ Fix Strategy
|
||||
|
||||
1. **Targeted Location**: Locate the test case and the corresponding
|
||||
prompt/code.
|
||||
2. **Iterative Scope**: Make extreme change first to verify scope, then refine
|
||||
to a minimal, targeted change.
|
||||
3. **Assertion Fidelity**:
|
||||
- Changing the test prompt is a **last resort** (prompts are often vague by
|
||||
design).
|
||||
- **Warning**: Do not lose test fidelity by making prompts too direct/easy.
|
||||
- **Primary Fix Trigger**: Adjust tool descriptions, system prompts
|
||||
(`snippets.ts`), or **modules that contribute to the prompt template**.
|
||||
- **Warning**: Prompts have multiple configurations; ensure your fix targets
|
||||
the correct config for the model in question.
|
||||
4. **Architecture Options**: If prompt or instruction tuning triggers no
|
||||
improvement, analyze loop composition.
|
||||
- **AgentLoop**: Defined by `context + toolset + prompt`.
|
||||
- **Enhancements**: Loops perform best with direct prompts, fewer irrelevant
|
||||
tools, low goal density, and minimal low-value/irrelevant context.
|
||||
- **Modifications**: Compose subagents or isolate tools. Ground in observed
|
||||
traces.
|
||||
- **Warning**: Think deeply before offering recommendations; avoid parroting
|
||||
abstract design guidelines.
|
||||
|
||||
---
|
||||
|
||||
## 3. ✅ Verify
|
||||
|
||||
1. **Run Local**: Run Vitest in non-interactive mode on just the file.
|
||||
2. **Log Audit**: Prioritize diagnosing failures via log comparison before
|
||||
triggering heavy test runs.
|
||||
3. **Stability Limit**: Run the test **3 times** locally on key models (can use
|
||||
scripts to run in parallel for speed):
|
||||
- **Gemini 3.0**
|
||||
- **Gemini 3 Flash**
|
||||
- **Gemini 2.5 Pro**
|
||||
4. **Flakiness Rule**: If it passes 2/3 times, it may be inherent noise
|
||||
difficult to improve without a structural split.
|
||||
|
||||
---
|
||||
|
||||
## 4. 📊 Report
|
||||
|
||||
Provide a summary of:
|
||||
|
||||
- Test success rate for each tested model (e.g., 3/3 = 100%).
|
||||
- Root cause identification and fix explanation.
|
||||
- If unfixed, provide high-confidence architecture recommendations.
|
||||
@@ -1,55 +0,0 @@
|
||||
# Promoting Behavioral Evals
|
||||
|
||||
Use this guide when asked to analyze nightly results and promote incubated tests
|
||||
to stable suites.
|
||||
|
||||
---
|
||||
|
||||
## 1. 🔍 Investigate candidates
|
||||
|
||||
1. **Audit Nightly Logs**: Use the `gh` CLI to fetch results from
|
||||
`evals-nightly.yml` (Direct URL:
|
||||
`https://github.com/google-gemini/gemini-cli/actions/workflows/evals-nightly.yml`).
|
||||
- **Tip**: The aggregate summary from the most recent run integrates the
|
||||
last 7 runs of history automatically.
|
||||
- **Safety**: DO NOT push changes or start remote runs. All verification is
|
||||
local.
|
||||
2. **Assess Stability**: Identify tests that pass **100% of the time** across
|
||||
ALL enabled models over the **last 7 nightly runs** in a row.
|
||||
- _100% means the test passed 3/3 times for every model and run._
|
||||
3. **Promotion Targets**: Tests meeting this criteria are candidates for
|
||||
promotion from `USUALLY_PASSES` to `ALWAYS_PASSES`.
|
||||
|
||||
---
|
||||
|
||||
## 2. 🚥 Promotion Steps
|
||||
|
||||
1. **Locate File**: Locate the eval file in the `evals/` directory.
|
||||
2. **Update Policy**: Modify the policy argument to `ALWAYS_PASSES`.
|
||||
```typescript
|
||||
evalTest('ALWAYS_PASSES', { ... })
|
||||
```
|
||||
3. **Targeting**: Follow guidelines in `evals/README.md` regarding stable suite
|
||||
organization.
|
||||
4. **Constraint**: Your final change must be **minimal and targeted** strictly
|
||||
to promoting the test status. Do not refactor the test or setup fixtures.
|
||||
|
||||
---
|
||||
|
||||
## 3. ✅ Verify
|
||||
|
||||
1. **Run Prompted Tests**: Run the promoted test locally using non-interactive
|
||||
Vitest to confirm structure validity.
|
||||
2. **Verify Suite Inclusion**: Check that the test is successfully picked up by
|
||||
standard runnable ranges.
|
||||
|
||||
---
|
||||
|
||||
## 4. 📊 Report
|
||||
|
||||
Provide a summary of:
|
||||
|
||||
- Which tests were promoted.
|
||||
- Provide the success rate evidence (e.g., 7/7 runs passed for all models).
|
||||
- If no candidates qualified, list the next closest candidates and their current
|
||||
pass rate.
|
||||
@@ -1,95 +0,0 @@
|
||||
# Running & Promoting Evals
|
||||
|
||||
## 🛠️ Prerequisites
|
||||
|
||||
Behavioral evals run against the compiled binary. You **must** build and bundle
|
||||
the project first after making changes:
|
||||
|
||||
```bash
|
||||
npm run build && npm run bundle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏃♂️ Running Tests
|
||||
|
||||
### 1. Configure Environment Variables
|
||||
|
||||
Evals require a standard API key. If your `.env` file has multiple keys or
|
||||
comments, use this precise extraction setup:
|
||||
|
||||
```bash
|
||||
export GEMINI_API_KEY=$(grep '^GEMINI_API_KEY=' .env | cut -d '=' -f2) && RUN_EVALS=1 npx vitest run --config evals/vitest.config.ts <file_name>
|
||||
```
|
||||
|
||||
### 2. Commands
|
||||
|
||||
| Command | Scope | Description |
|
||||
| :---------------------------------- | :-------------- | :------------------------------------------------- |
|
||||
| `npm run test:always_passing_evals` | `ALWAYS_PASSES` | Fast feedback, runs in CI. |
|
||||
| `npm run test:all_evals` | All | Runs nightly incubation tests. Sets `RUN_EVALS=1`. |
|
||||
|
||||
### Target Specific File
|
||||
|
||||
_Note: `RUN_EVALS=1` is required for incubated (`USUALLY_PASSES`) tests._
|
||||
|
||||
```bash
|
||||
RUN_EVALS=1 npx vitest run --config evals/vitest.config.ts my_feature.eval.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐞 Debugging and Logs
|
||||
|
||||
If a test fails, verify:
|
||||
|
||||
- **Tool Trajectory Logs**:序列 of calls in `evals/logs/<test_name>.log`.
|
||||
- **Verbose Reasoning**: Capture raw buffer traces by setting
|
||||
`GEMINI_DEBUG_LOG_FILE`:
|
||||
```bash
|
||||
export GEMINI_DEBUG_LOG_FILE="debug.log"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🎯 Verify Model Targeting
|
||||
|
||||
- **Tip:** Standard evals benchmark against model variations. If a test passes
|
||||
on Flash but fails on Pro (or vice versa), the issue is usually in the **tool
|
||||
description**, not the prompt definition. Flash is sensitive to "instruction
|
||||
bloat," while Pro is sensitive to "ambiguous intent."
|
||||
|
||||
---
|
||||
|
||||
## 🚥 deflaking & Promotion
|
||||
|
||||
To maintain CI stability, all new evals follow a strict incubation period.
|
||||
|
||||
### 1. Incubation (`USUALLY_PASSES`)
|
||||
|
||||
New tests must be created with the `USUALLY_PASSES` policy.
|
||||
|
||||
```typescript
|
||||
evalTest('USUALLY_PASSES', { ... })
|
||||
```
|
||||
|
||||
They run in **Evals: Nightly** workflows and do not block PR merges.
|
||||
|
||||
### 2. Investigate Failures
|
||||
|
||||
If a nightly eval regresses, investigate via agent:
|
||||
|
||||
```bash
|
||||
gemini /fix-behavioral-eval [optional-run-uri]
|
||||
```
|
||||
|
||||
### 3. Promotion (`ALWAYS_PASSES`)
|
||||
|
||||
Once a test scores 100% consistency over multiple nightly cycles:
|
||||
|
||||
```bash
|
||||
gemini /promote-behavioral-eval
|
||||
```
|
||||
|
||||
_Do not promote manually._ The command verifies trajectory logs before updating
|
||||
the file policy.
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
name: ci
|
||||
description:
|
||||
A specialized skill for Gemini CLI that provides high-performance, fail-fast
|
||||
monitoring of GitHub Actions workflows and automated local verification of CI
|
||||
failures. It handles run discovery automatically—simply provide the branch name.
|
||||
---
|
||||
|
||||
# CI Replicate & Status
|
||||
|
||||
This skill enables the agent to efficiently monitor GitHub Actions, triage
|
||||
failures, and bridge remote CI errors to local development. It defaults to
|
||||
**automatic replication** of failures to streamline the fix cycle.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
- **Automatic Replication**: Automatically monitors CI and immediately executes
|
||||
suggested test or lint commands locally upon failure.
|
||||
- **Real-time Monitoring**: Aggregated status line for all concurrent workflows
|
||||
on the current branch.
|
||||
- **Fail-Fast Triage**: Immediately stops on the first job failure to provide a
|
||||
structured report.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. CI Replicate (`replicate`) - DEFAULT
|
||||
Use this as the primary path to monitor CI and **automatically** replicate
|
||||
failures locally for immediate triage and fixing.
|
||||
- **Behavior**: When this workflow is triggered, the agent will monitor the CI
|
||||
and **immediately and automatically execute** all suggested test or lint
|
||||
commands (marked with 🚀) as soon as a failure is detected.
|
||||
- **Tool**: `node .gemini/skills/ci/scripts/ci.mjs [branch]`
|
||||
- **Discovery**: The script **automatically** finds the latest active or recent
|
||||
run for the branch. Do NOT manually search for run IDs.
|
||||
- **Goal**: Reproduce the failure locally without manual intervention, then
|
||||
proceed to analyze and fix the code.
|
||||
|
||||
### 1. CI Status (`status`)
|
||||
Use this when you have pushed changes and need to monitor the CI and reproduce
|
||||
any failures locally.
|
||||
- **Tool**: `node .gemini/skills/ci/scripts/ci.mjs [branch] [run_id]`
|
||||
- **Discovery**: The script **automatically** finds the latest active or recent
|
||||
run for the branch. You should NOT manually search for \`run_id\` using \`gh run list\`
|
||||
unless a specific historical run is requested. Simply provide the branch name.
|
||||
- **Step 1 (Monitor)**: Execute the tool with the branch name.
|
||||
- **Step 2 (Extract)**: Extract suggested \`npm test\` or \`npm run lint\` commands
|
||||
from the output (marked with 🚀).
|
||||
- **Step 3 (Reproduce)**: Execute those commands locally to confirm the failure.
|
||||
- **Behavior**: It will poll every 15 seconds. If it detects a failure, it will
|
||||
exit with a structured report and provide the exact commands to run locally.
|
||||
|
||||
## Failure Categories & Actions
|
||||
|
||||
- **Test Failures**: Agent should run the specific `npm test -w <pkg> -- <path>`
|
||||
command suggested.
|
||||
- **Lint Errors**: Agent should run `npm run lint:all` or the specific package
|
||||
lint command.
|
||||
- **Build Errors**: Agent should check `tsc` output or build logs to resolve
|
||||
compilation issues.
|
||||
- **Job Errors**: Investigate `gh run view --job <job_id> --log` for
|
||||
infrastructure or setup failures.
|
||||
|
||||
## Noise Filtering
|
||||
The underlying scripts automatically filter noise (Git logs, NPM warnings, stack
|
||||
trace overhead). The agent should focus on the "Structured Failure Report"
|
||||
provided by the tool.
|
||||
@@ -1,281 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const BRANCH =
|
||||
process.argv[2] || execSync('git branch --show-current').toString().trim();
|
||||
const RUN_ID_OVERRIDE = process.argv[3];
|
||||
|
||||
let REPO;
|
||||
try {
|
||||
const remoteUrl = execSync('git remote get-url origin').toString().trim();
|
||||
REPO = remoteUrl
|
||||
.replace(/.*github\.com[\/:]/, '')
|
||||
.replace(/\.git$/, '')
|
||||
.trim();
|
||||
} catch (e) {
|
||||
REPO = 'google-gemini/gemini-cli';
|
||||
}
|
||||
|
||||
const FAILED_FILES = new Set();
|
||||
|
||||
function runGh(args) {
|
||||
try {
|
||||
return execSync(`gh ${args}`, {
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).toString();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchFailuresViaApi(jobId) {
|
||||
try {
|
||||
const cmd = `gh api repos/${REPO}/actions/jobs/${jobId}/logs | grep -iE " FAIL |❌|ERROR|Lint failed|Build failed|Exception|failed with exit code"`;
|
||||
return execSync(cmd, {
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
}).toString();
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isNoise(line) {
|
||||
const lower = line.toLowerCase();
|
||||
return (
|
||||
lower.includes('* [new branch]') ||
|
||||
lower.includes('npm warn') ||
|
||||
lower.includes('fetching updates') ||
|
||||
lower.includes('node:internal/errors') ||
|
||||
lower.includes('at ') || // Stack traces
|
||||
lower.includes('checkexecsyncerror') ||
|
||||
lower.includes('node_modules')
|
||||
);
|
||||
}
|
||||
|
||||
function extractTestFile(failureText) {
|
||||
const cleanLine = failureText
|
||||
.replace(/[|#\[\]()]/g, ' ')
|
||||
.replace(/<[^>]*>/g, ' ')
|
||||
.trim();
|
||||
const fileMatch = cleanLine.match(/([\w\/._-]+\.test\.[jt]sx?)/);
|
||||
if (fileMatch) return fileMatch[1];
|
||||
return null;
|
||||
}
|
||||
|
||||
function generateTestCommand(failedFilesMap) {
|
||||
const workspaceToFiles = new Map();
|
||||
for (const [file, info] of failedFilesMap.entries()) {
|
||||
if (
|
||||
['Job Error', 'Unknown File', 'Build Error', 'Lint Error'].includes(file)
|
||||
)
|
||||
continue;
|
||||
let workspace = '@google/gemini-cli';
|
||||
let relPath = file;
|
||||
if (file.startsWith('packages/core/')) {
|
||||
workspace = '@google/gemini-cli-core';
|
||||
relPath = file.replace('packages/core/', '');
|
||||
} else if (file.startsWith('packages/cli/')) {
|
||||
workspace = '@google/gemini-cli';
|
||||
relPath = file.replace('packages/cli/', '');
|
||||
}
|
||||
relPath = relPath.replace(/^.*packages\/[^\/]+\//, '');
|
||||
if (!workspaceToFiles.has(workspace))
|
||||
workspaceToFiles.set(workspace, new Set());
|
||||
workspaceToFiles.get(workspace).add(relPath);
|
||||
}
|
||||
const commands = [];
|
||||
for (const [workspace, files] of workspaceToFiles.entries()) {
|
||||
commands.push(`npm test -w ${workspace} -- ${Array.from(files).join(' ')}`);
|
||||
}
|
||||
return commands.join(' && ');
|
||||
}
|
||||
|
||||
async function monitor() {
|
||||
let targetRunIds = [];
|
||||
if (RUN_ID_OVERRIDE) {
|
||||
targetRunIds = [RUN_ID_OVERRIDE];
|
||||
} else {
|
||||
// 1. Get runs directly associated with the branch
|
||||
const runListOutput = runGh(
|
||||
`run list --branch "${BRANCH}" --limit 10 --json databaseId,status,workflowName,createdAt`,
|
||||
);
|
||||
if (runListOutput) {
|
||||
const runs = JSON.parse(runListOutput);
|
||||
const activeRuns = runs.filter((r) => r.status !== 'completed');
|
||||
if (activeRuns.length > 0) {
|
||||
targetRunIds = activeRuns.map((r) => r.databaseId);
|
||||
} else if (runs.length > 0) {
|
||||
const latestTime = new Date(runs[0].createdAt).getTime();
|
||||
targetRunIds = runs
|
||||
.filter((r) => latestTime - new Date(r.createdAt).getTime() < 60000)
|
||||
.map((r) => r.databaseId);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Get runs associated with commit statuses (handles chained/indirect runs)
|
||||
try {
|
||||
const headSha = execSync(`git rev-parse "${BRANCH}"`).toString().trim();
|
||||
const statusOutput = runGh(
|
||||
`api repos/${REPO}/commits/${headSha}/status -q '.statuses[] | select(.target_url | contains("actions/runs/")) | .target_url'`,
|
||||
);
|
||||
if (statusOutput) {
|
||||
const statusRunIds = statusOutput
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((url) => {
|
||||
const match = url.match(/actions\/runs\/(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
for (const runId of statusRunIds) {
|
||||
if (!targetRunIds.includes(runId)) {
|
||||
targetRunIds.push(runId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore if branch/SHA not found or API fails
|
||||
}
|
||||
|
||||
if (targetRunIds.length > 0) {
|
||||
const runNames = [];
|
||||
for (const runId of targetRunIds) {
|
||||
const runInfo = runGh(`run view "${runId}" --json workflowName`);
|
||||
if (runInfo) {
|
||||
runNames.push(JSON.parse(runInfo).workflowName);
|
||||
}
|
||||
}
|
||||
console.log(`Monitoring workflows: ${[...new Set(runNames)].join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetRunIds.length === 0) {
|
||||
console.log(`No runs found for branch ${BRANCH}.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
let allPassed = 0,
|
||||
allFailed = 0,
|
||||
allRunning = 0,
|
||||
allQueued = 0,
|
||||
totalJobs = 0;
|
||||
let anyRunInProgress = false;
|
||||
const fileToTests = new Map();
|
||||
let failuresFoundInLoop = false;
|
||||
|
||||
for (const runId of targetRunIds) {
|
||||
const runOutput = runGh(
|
||||
`run view "${runId}" --json databaseId,status,conclusion,workflowName`,
|
||||
);
|
||||
if (!runOutput) continue;
|
||||
const run = JSON.parse(runOutput);
|
||||
if (run.status !== 'completed') anyRunInProgress = true;
|
||||
|
||||
const jobsOutput = runGh(`run view "${runId}" --json jobs`);
|
||||
if (jobsOutput) {
|
||||
const { jobs } = JSON.parse(jobsOutput);
|
||||
totalJobs += jobs.length;
|
||||
const failedJobs = jobs.filter((j) => j.conclusion === 'failure');
|
||||
if (failedJobs.length > 0) {
|
||||
failuresFoundInLoop = true;
|
||||
for (const job of failedJobs) {
|
||||
const failures = fetchFailuresViaApi(job.databaseId);
|
||||
if (failures.trim()) {
|
||||
failures.split('\n').forEach((line) => {
|
||||
if (!line.trim() || isNoise(line)) return;
|
||||
const file = extractTestFile(line);
|
||||
const filePath =
|
||||
file ||
|
||||
(line.toLowerCase().includes('lint')
|
||||
? 'Lint Error'
|
||||
: line.toLowerCase().includes('build')
|
||||
? 'Build Error'
|
||||
: 'Unknown File');
|
||||
let testName = line;
|
||||
if (line.includes(' > ')) {
|
||||
testName = line.split(' > ').slice(1).join(' > ').trim();
|
||||
}
|
||||
if (!fileToTests.has(filePath))
|
||||
fileToTests.set(filePath, new Set());
|
||||
fileToTests.get(filePath).add(testName);
|
||||
});
|
||||
} else {
|
||||
const step =
|
||||
job.steps?.find((s) => s.conclusion === 'failure')?.name ||
|
||||
'unknown';
|
||||
const category = step.toLowerCase().includes('lint')
|
||||
? 'Lint Error'
|
||||
: step.toLowerCase().includes('build')
|
||||
? 'Build Error'
|
||||
: 'Job Error';
|
||||
if (!fileToTests.has(category))
|
||||
fileToTests.set(category, new Set());
|
||||
fileToTests
|
||||
.get(category)
|
||||
.add(`${job.name}: Failed at step "${step}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const job of jobs) {
|
||||
if (job.status === 'in_progress') allRunning++;
|
||||
else if (job.status === 'queued') allQueued++;
|
||||
else if (job.conclusion === 'success') allPassed++;
|
||||
else if (job.conclusion === 'failure') allFailed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failuresFoundInLoop) {
|
||||
console.log(
|
||||
`\n\n❌ Failures detected across ${allFailed} job(s). Stopping monitor...`,
|
||||
);
|
||||
console.log('\n--- Structured Failure Report (Noise Filtered) ---');
|
||||
for (const [file, tests] of fileToTests.entries()) {
|
||||
console.log(`\nCategory/File: ${file}`);
|
||||
// Limit output per file if it's too large
|
||||
const testsArr = Array.from(tests).map((t) =>
|
||||
t.length > 500 ? t.substring(0, 500) + '... [TRUNCATED]' : t,
|
||||
);
|
||||
testsArr.slice(0, 10).forEach((t) => console.log(` - ${t}`));
|
||||
if (testsArr.length > 10)
|
||||
console.log(` ... and ${testsArr.length - 10} more`);
|
||||
}
|
||||
const testCmd = generateTestCommand(fileToTests);
|
||||
if (testCmd) {
|
||||
console.log('\n🚀 Run this to verify fixes:');
|
||||
console.log(testCmd);
|
||||
} else if (
|
||||
Array.from(fileToTests.keys()).some((k) => k.includes('Lint'))
|
||||
) {
|
||||
console.log('\n🚀 Run this to verify lint fixes:\nnpm run lint:all');
|
||||
}
|
||||
console.log('---------------------------------');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const completed = allPassed + allFailed;
|
||||
process.stdout.write(
|
||||
`\r⏳ Monitoring ${targetRunIds.length} runs... ${completed}/${totalJobs} jobs (${allPassed} passed, ${allFailed} failed, ${allRunning} running, ${allQueued} queued) `,
|
||||
);
|
||||
if (!anyRunInProgress) {
|
||||
console.log('\n✅ All workflows passed!');
|
||||
process.exit(0);
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 15000));
|
||||
}
|
||||
}
|
||||
|
||||
monitor().catch((err) => {
|
||||
console.error('\nMonitor error:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -59,10 +59,6 @@ To standardize the process of updating changelog files (`latest.md`,
|
||||
|
||||
*Use this path if the version number ends in `.0`.*
|
||||
|
||||
**Important:** Based on the version, you must choose to follow either section
|
||||
A.1 for stable releases or A.2 for preview releases. Do not follow the
|
||||
instructions for the other section.
|
||||
|
||||
### A.1: Stable Release (e.g., `v0.28.0`)
|
||||
|
||||
For a stable release, you will generate two distinct summaries from the
|
||||
@@ -77,8 +73,7 @@ detailed **highlights** section for the release-specific page.
|
||||
use the existing announcements in `docs/changelogs/index.md` and the
|
||||
example within
|
||||
`.gemini/skills/docs-changelog/references/index_template.md` as your
|
||||
guide. This format includes PR links and authors. Stick to 1 or 2 PR
|
||||
links and authors.
|
||||
guide. This format includes PR links and authors.
|
||||
- Add this new announcement to the top of `docs/changelogs/index.md`.
|
||||
|
||||
2. **Create Highlights and Update `latest.md`**:
|
||||
@@ -110,10 +105,6 @@ detailed **highlights** section for the release-specific page.
|
||||
|
||||
*Use this path if the version number does **not** end in `.0`.*
|
||||
|
||||
**Important:** Based on the version, you must choose to follow either section
|
||||
B.1 for stable patches or B.2 for preview patches. Do not follow the
|
||||
instructions for the other section.
|
||||
|
||||
### B.1: Stable Patch (e.g., `v0.28.1`)
|
||||
|
||||
- **Target File**: `docs/changelogs/latest.md`
|
||||
@@ -122,12 +113,10 @@ instructions for the other section.
|
||||
`# Latest stable release: {{version}}`
|
||||
2. Update the rease date. The line should read,
|
||||
`Released: {{release_date_month_dd_yyyy}}`
|
||||
3. Determine if a "What's Changed" section exists in the temporary file
|
||||
If so, continue to step 4. Otherwise, skip to step 5.
|
||||
4. **Prepend** the processed "What's Changed" list from the temporary file
|
||||
3. **Prepend** the processed "What's Changed" list from the temporary file
|
||||
to the existing "What's Changed" list in `latest.md`. Do not change or
|
||||
replace the existing list, **only add** to the beginning of it.
|
||||
5. In the "Full Changelog", edit **only** the end of the URL. Identify the
|
||||
4. In the "Full Changelog", edit **only** the end of the URL. Identify the
|
||||
last part of the URL that looks like `...{previous_version}` and update
|
||||
it to be `...{version}`.
|
||||
|
||||
@@ -144,12 +133,10 @@ instructions for the other section.
|
||||
`# Preview release: {{version}}`
|
||||
2. Update the rease date. The line should read,
|
||||
`Released: {{release_date_month_dd_yyyy}}`
|
||||
3. Determine if a "What's Changed" section exists in the temporary file
|
||||
If so, continue to step 4. Otherwise, skip to step 5.
|
||||
4. **Prepend** the processed "What's Changed" list from the temporary file
|
||||
3. **Prepend** the processed "What's Changed" list from the temporary file
|
||||
to the existing "What's Changed" list in `preview.md`. Do not change or
|
||||
replace the existing list, **only add** to the beginning of it.
|
||||
5. In the "Full Changelog", edit **only** the end of the URL. Identify the
|
||||
4. In the "Full Changelog", edit **only** the end of the URL. Identify the
|
||||
last part of the URL that looks like `...{previous_version}` and update
|
||||
it to be `...{version}`.
|
||||
|
||||
@@ -162,5 +149,5 @@ instructions for the other section.
|
||||
|
||||
## Finalize
|
||||
|
||||
- After making changes, run `npm run format` ONLY to ensure consistency.
|
||||
- After making changes, run `npm run format` to ensure consistency.
|
||||
- Delete any temporary files created during the process.
|
||||
|
||||
@@ -45,10 +45,6 @@ Write precisely to ensure your instructions are unambiguous.
|
||||
specific verbs.
|
||||
- **Examples:** Use meaningful names in examples; avoid placeholders like
|
||||
"foo" or "bar."
|
||||
- **Quota and limit terminology:** For any content involving resource capacity
|
||||
or using the word "quota" or "limit", strictly adhere to the guidelines in
|
||||
the `quota-limit-style-guide.md` resource file. Generally, Use "quota" for the
|
||||
administrative bucket and "limit" for the numerical ceiling.
|
||||
|
||||
### Formatting and syntax
|
||||
Apply consistent formatting to make documentation visually organized and
|
||||
@@ -65,60 +61,18 @@ accessible.
|
||||
- **UI and code:** Use **bold** for UI elements and `code font` for filenames,
|
||||
snippets, commands, and API elements. Focus on the task when discussing
|
||||
interaction.
|
||||
- **Links:** Use descriptive anchor text; avoid "click here." Ensure the link
|
||||
makes sense out of context.
|
||||
- **Accessibility:** Use semantic HTML elements correctly (headings, lists,
|
||||
tables).
|
||||
- **Media:** Use lowercase hyphenated filenames. Provide descriptive alt text
|
||||
for all images.
|
||||
- **Details section:** Use the `<details>` tag to create a collapsible section.
|
||||
This is useful for supplementary or data-heavy information that isn't critical
|
||||
to the main flow.
|
||||
|
||||
Example:
|
||||
|
||||
<details>
|
||||
<summary>Title</summary>
|
||||
|
||||
- First entry
|
||||
- Second entry
|
||||
|
||||
</details>
|
||||
|
||||
- **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 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:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an example of a multi-line note that will be preserved
|
||||
> by Prettier.
|
||||
|
||||
### Links
|
||||
- **Accessibility:** Use descriptive anchor text; avoid "click here." Ensure the
|
||||
link makes sense out of context, such as when being read by a screen reader.
|
||||
- **Use relative links in docs:** Use relative links in documentation (`/docs/`)
|
||||
to ensure portability. Use paths relative to the current file's directory
|
||||
(for example, `../tools/` from `docs/cli/`). Do not include the `/docs/`
|
||||
section of a path, but do verify that the resulting relative link exists. This
|
||||
does not apply to meta files such as README.MD and CONTRIBUTING.MD.
|
||||
- **When changing headings, check for deep links:** If a user is changing a
|
||||
heading, check for deep links to that heading in other pages and update
|
||||
accordingly.
|
||||
|
||||
### Structure
|
||||
- **BLUF:** Start with an introduction explaining what to expect.
|
||||
- **Experimental features:** If a feature is clearly noted as experimental,
|
||||
add the following note immediately after the introductory paragraph:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> This is an experimental feature currently under active development.
|
||||
|
||||
add the following note immediately after the introductory paragraph:
|
||||
`> **Note:** This is a preview feature currently under active development.`
|
||||
- **Headings:** Use hierarchical headings to support the user journey.
|
||||
- **Procedures:**
|
||||
- Introduce lists of steps with a complete sentence.
|
||||
@@ -127,7 +81,8 @@ accessible.
|
||||
- Put conditions before instructions (e.g., "On the Settings page, click...").
|
||||
- Provide clear context for where the action takes place.
|
||||
- Indicate optional steps clearly (e.g., "Optional: ...").
|
||||
- **Elements:** Use bullet lists, tables, details, and callouts.
|
||||
- **Elements:** Use bullet lists, tables, notes (`> **Note:**`), and warnings
|
||||
(`> **Warning:**`).
|
||||
- **Avoid using a table of contents:** If a table of contents is present, remove
|
||||
it.
|
||||
- **Next steps:** Conclude with a "Next steps" section if applicable.
|
||||
@@ -159,14 +114,13 @@ documentation.
|
||||
reflects existing code.
|
||||
- **Structure:** Apply "Structure (New Docs)" rules (BLUF, headings, etc.) when
|
||||
adding new sections to existing pages.
|
||||
- **Headers**: If you change a header, you must check for links that lead to
|
||||
that header and update them.
|
||||
- **Tone:** Ensure the tone is active and engaging. Use "you" and contractions.
|
||||
- **Clarity:** Correct awkward wording, spelling, and grammar. Rephrase
|
||||
sentences to make them easier for users to understand.
|
||||
- **Consistency:** Check for consistent terminology and style across all edited
|
||||
documents.
|
||||
|
||||
|
||||
## Phase 4: Verification and finalization
|
||||
Perform a final quality check to ensure that all changes are correctly formatted
|
||||
and that all links are functional.
|
||||
@@ -175,8 +129,7 @@ and that all links are functional.
|
||||
technical behavior.
|
||||
2. **Self-review:** Re-read changes for formatting, correctness, and flow.
|
||||
3. **Link check:** Verify all new and existing links leading to or from modified
|
||||
pages. If you changed a header, ensure that any links that lead to it are
|
||||
updated.
|
||||
pages.
|
||||
4. **Format:** Once all changes are complete, ask to execute `npm run format`
|
||||
to ensure consistent formatting across the project. If the user confirms,
|
||||
execute the command.
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
# Style Guide: Quota vs. Limit
|
||||
|
||||
This guide defines the usage of "quota," "limit," and related terms in
|
||||
user-facing interfaces.
|
||||
|
||||
## TL;DR
|
||||
|
||||
- **`quota`**: The administrative "bucket." Use for settings, billing, and
|
||||
requesting increases. (e.g., "Adjust your storage **quota**.")
|
||||
- **`limit`**: The real-time numerical "ceiling." Use for error messages when a
|
||||
user is blocked. (e.g., "You've reached your request **limit**.")
|
||||
- **When blocked, combine them:** Explain the **limit** that was hit and the
|
||||
**quota** that is the remedy. (e.g., "You've reached the request **limit** for
|
||||
your developer **quota**.")
|
||||
- **Related terms:** Use `usage` for consumption tracking, `restriction` for
|
||||
fixed rules, and `reset` for when a limit refreshes.
|
||||
|
||||
---
|
||||
|
||||
## Detailed Guidelines
|
||||
|
||||
### Definitions
|
||||
|
||||
- **Quota is the "what":** It identifies the category of resource being managed
|
||||
(e.g., storage quota, GPU quota, request/prompt quota).
|
||||
- **Limit is the "how much":** It defines the numerical boundary.
|
||||
|
||||
Use **quota** when referring to the administrative concept or the request for
|
||||
more. Use **limit** when discussing the specific point of exhaustion.
|
||||
|
||||
### When to use "quota"
|
||||
|
||||
Use this term for **account management, billing, and settings.** It describes
|
||||
the entitlement the user has purchased or been assigned.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- **Navigation label:** Quota and usage
|
||||
- **Contextual help:** Your **usage quota** is managed by your organization. To
|
||||
request an increase, contact your administrator.
|
||||
|
||||
### When to use "limit"
|
||||
|
||||
Use this term for **real-time feedback, notifications, and error messages.** It
|
||||
identifies the specific wall the user just hit.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- **Error message:** You’ve reached the 50-request-per-minute **limit**.
|
||||
- **Inline warning:** Input exceeds the 32k token **limit**.
|
||||
|
||||
### How to use both together
|
||||
|
||||
When a user is blocked, combine both terms to explain the **event** (limit) and
|
||||
the **remedy** (quota).
|
||||
|
||||
**Example:**
|
||||
|
||||
- **Heading:** Daily usage limit reached
|
||||
- **Body:** You've reached the maximum daily capacity for your developer quota.
|
||||
To continue working today, upgrade your quota.
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
name: github-issue-creator
|
||||
description:
|
||||
Use this skill when asked to create a GitHub issue. It handles different issue
|
||||
types (bug, feature, etc.) using repository templates and ensures proper
|
||||
labeling.
|
||||
---
|
||||
|
||||
# GitHub Issue Creator
|
||||
|
||||
This skill guides the creation of high-quality GitHub issues that adhere to the
|
||||
repository's standards and use the appropriate templates.
|
||||
|
||||
## Workflow
|
||||
|
||||
Follow these steps to create a GitHub issue:
|
||||
|
||||
1. **Identify Issue Type**: Determine if the request is a bug report, feature
|
||||
request, or other category.
|
||||
|
||||
2. **Locate Template**: Search for issue templates in
|
||||
`.github/ISSUE_TEMPLATE/`.
|
||||
- `bug_report.yml`
|
||||
- `feature_request.yml`
|
||||
- `website_issue.yml`
|
||||
- If no relevant YAML template is found, look for `.md` templates in the same
|
||||
directory.
|
||||
|
||||
3. **Read Template**: Read the content of the identified template file to
|
||||
understand the required fields.
|
||||
|
||||
4. **Draft Content**: Draft the issue title and body/fields.
|
||||
- If using a YAML template (form), prepare values for each `id` defined in
|
||||
the template.
|
||||
- If using a Markdown template, follow its structure exactly.
|
||||
- **Default Label**: Always include the `🔒 maintainer only` label unless the
|
||||
user explicitly requests otherwise.
|
||||
|
||||
5. **Create Issue**: Use the `gh` CLI to create the issue.
|
||||
- **CRITICAL:** To avoid shell escaping and formatting issues with
|
||||
multi-line Markdown or complex text, ALWAYS write the description/body to
|
||||
a temporary file first.
|
||||
|
||||
**For Markdown Templates or Simple Body:**
|
||||
```bash
|
||||
# 1. Write the drafted content to a temporary file
|
||||
# 2. Create the issue using the --body-file flag
|
||||
gh issue create --title "Succinct title" --body-file <temp_file_path> --label "🔒 maintainer only"
|
||||
# 3. Remove the temporary file
|
||||
rm <temp_file_path>
|
||||
```
|
||||
|
||||
**For YAML Templates (Forms):**
|
||||
While `gh issue create` supports `--body-file`, YAML forms usually expect
|
||||
key-value pairs via flags if you want to bypass the interactive prompt.
|
||||
However, the most reliable non-interactive way to ensure formatting is
|
||||
preserved for long text fields is to use the `--body` or `--body-file` if the
|
||||
form has been converted to a standard body, OR to use the `--field` flags
|
||||
for YAML forms.
|
||||
|
||||
*Note: For the `gemini-cli` repository which uses YAML forms, you can often
|
||||
submit the content as a single body if a specific field-based submission is
|
||||
not required by the automation.*
|
||||
|
||||
6. **Verify**: Confirm the issue was created successfully and provide the link
|
||||
to the user.
|
||||
|
||||
## Principles
|
||||
|
||||
- **Clarity**: Titles should be descriptive and follow project conventions.
|
||||
- **Defensive Formatting**: Always use temporary files with `--body-file` to
|
||||
prevent newline and special character issues.
|
||||
- **Maintainer Priority**: Default to internal/maintainer labels to keep the
|
||||
backlog organized.
|
||||
- **Completeness**: Provide all requested information (e.g., version info,
|
||||
reproduction steps).
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
name: review-duplication
|
||||
description: Use this skill during code reviews to proactively investigate the codebase for duplicated functionality, reinvented wheels, or failure to reuse existing project best practices and shared utilities.
|
||||
---
|
||||
|
||||
# Review Duplication
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides a structured workflow for investigating a codebase during a code review to identify duplicated logic, reinvented utilities, and missed opportunities to reuse established patterns. By executing this workflow, you ensure that new code integrates seamlessly with the existing project architecture.
|
||||
|
||||
## Workflow: Investigating for Duplication
|
||||
|
||||
When reviewing code, perform the following steps before finalizing your review:
|
||||
|
||||
### 1. Extract Core Logic
|
||||
Analyze the new code to identify the core algorithms, utility functions, generic data structures, or UI components being introduced. Look beyond the specific business logic to see the underlying mechanics.
|
||||
|
||||
### 2. Hypothesize Existing Locations & Trace Dependencies
|
||||
Think about where this type of code *would* live if it already existed in the project. Provide absolute paths from the repo root to disambiguate.
|
||||
- **Utilities:** `packages/core/src/utils/`, `packages/cli/src/utils/`
|
||||
- **UI Components:** `packages/cli/src/ui/components/`, `packages/cli/src/ui/`
|
||||
- **Services:** `packages/core/src/services/`, `packages/cli/src/services/`
|
||||
- **Configuration:** `packages/core/src/config/`, `packages/cli/src/config/`
|
||||
- **Core Logic:** Call out `packages/core/` if functionality does not appear React UI specific.
|
||||
|
||||
**Trace Third-Party Dependencies:** If the PR introduces a new import for a utility library (e.g., `lodash.merge`, `date-fns`), trace how and where the project currently uses that library. There is likely an existing wrapper or shared utility.
|
||||
|
||||
**Check Package Files:** Before flagging a custom implementation of a complex algorithm, check `package.json` to see if a standard library (like `lodash` or `uuid`) is already installed that provides this functionality.
|
||||
|
||||
### 3. Investigate the Codebase (Sub-Agent Delegation)
|
||||
Delegate the heavy lifting of codebase investigation to specialized sub-agents. They are optimized to perform deep searches and semantic mapping without bloating your session history.
|
||||
|
||||
To ensure a comprehensive review, you MUST formulate highly specific objectives for the sub-agents, providing them with the "scents" you discovered in Step 1.
|
||||
|
||||
- **Codebase Investigator:** Use the `codebase_investigator` as your primary researcher. When delegating, formulate an objective that asks specific, investigative questions about the codebase, explicitly including these search vectors:
|
||||
- **Structural Similarity:** Ask if existing code uses the same underlying APIs (e.g., "Does any existing code use `Intl.DateTimeFormat` or `setTimeout` for similar purposes?").
|
||||
- **Naming Conventions:** Ask if there are existing symbols with similar naming patterns (e.g., "Are there existing symbols with naming patterns like `*Format*` or `*Debounce*`?").
|
||||
- **Comments & Documentation:** Ask if keywords from the PR's comments or JSDoc exist in describing similar behavior elsewhere.
|
||||
- **Architectural Fit:** Ask where this type of logic is currently centralized (e.g., "Where is centralized date formatting logic located?").
|
||||
- **Refactoring Guidance:** Crucially, ask the sub-agent to explain *how* the new code could be refactored to use any existing logic it finds.
|
||||
- **Generalist Agent:** Use the `generalist` for detailed, turn-intensive comparisons. For example: "Review the implementation of `MyNewComponent` in the PR and compare it semantically against all components in `packages/ui/src`. Are there any existing components that could be extended or used instead?"
|
||||
- **Retain Fast Path for Simple Searches:** For extremely simple, unambiguous checks (e.g., "Does `package.json` include `lodash`?"), perform a direct search to save time. Default to delegation for any open-ended "investigations."
|
||||
|
||||
### 4. Evaluate Best Practices
|
||||
Check if the new code aligns with the project's established conventions.
|
||||
- **Error Handling:** Does it use the project's standard error classes or logging mechanisms?
|
||||
- **State Management:** Does it bypass established stores or contexts?
|
||||
- **Styling:** Does it hardcode colors or spacing instead of using theme variables?
|
||||
If the PR introduces a new pattern, compare it against the documented standards and explicitly confirm if an existing project pattern should have been used instead.
|
||||
|
||||
### 5. Formulate Constructive Feedback
|
||||
If you discover that the PR duplicates existing functionality or ignores a best practice:
|
||||
- Provide a clear review comment.
|
||||
- **Identify the Source:** Explicitly mention the absolute or project-relative file path and the specific symbol (function, component, class) that should be reused.
|
||||
- **Implementation Guidance:** Provide a brief code snippet or a clear explanation showing **how** to integrate the existing code to fulfill the task's requirements.
|
||||
- **Explain the Value:** Briefly explain why reusing the existing code is beneficial (e.g., maintainability, consistency, built-in edge case handling).
|
||||
|
||||
Example comment:
|
||||
> "It looks like this PR introduces a new `formatDate` utility. We already have a robust, tested `formatDate` function in `src/utils/dateHelpers.ts`.
|
||||
>
|
||||
> You can replace your implementation by importing it like this:
|
||||
> ```typescript
|
||||
> import { formatDate } from '../utils/dateHelpers';
|
||||
>
|
||||
> // Then use it here:
|
||||
> const displayDate = formatDate(userDate, 'MMM Do, YYYY');
|
||||
> ```
|
||||
> Reusing this ensures that the date formatting remains consistent with the rest of the application and handles timezone conversions correctly."
|
||||
@@ -1,99 +0,0 @@
|
||||
---
|
||||
name: string-reviewer
|
||||
description: >
|
||||
Use this skill when asked to review text and user-facing strings within the codebase. It ensures that these strings follow rules on clarity,
|
||||
usefulness, brevity and style.
|
||||
---
|
||||
|
||||
# String Reviewer
|
||||
|
||||
## Instructions
|
||||
|
||||
Act as a Senior UX Writer. Look for user-facing strings that are too long,
|
||||
unclear, or inconsistent. This includes inline text, error messages, and other
|
||||
user-facing text.
|
||||
|
||||
Do NOT automatically change strings without user approval. You must only suggest
|
||||
changes and do not attempt to rewrite them directly unless the user explicitly
|
||||
asks you to do so.
|
||||
|
||||
## Core voice principles
|
||||
|
||||
The system prioritizes deterministic clarity over conversational fluff. We
|
||||
provide telemetry, not etiquette, ensuring the user retains absolute agency..
|
||||
|
||||
1. **Deterministic clarity:** Distinguish between certain system/service states
|
||||
(Cloud Billing, IAM, the System) and probabilistic AI analysis (Gemini).
|
||||
2. **System transparency:** Replace "Loading..." with active technical telemetry
|
||||
(e.g., Tracing stack traces...). Keep status updates under 5 words.
|
||||
3. **Front-loaded actionability:** Always use the [Goal] + [Action] pattern.
|
||||
Lead with intent so users can scan left-to-right.
|
||||
4. **Agentic error recovery:** Every error must be a pivot point. Pair failures
|
||||
with one-click recovery commands or suggested prompts.
|
||||
5. **Contextual humility:** Reserve disclaimers and "be careful" warnings for P0
|
||||
(destructive/irreversible) tasks only. Stop warning-fatigue.
|
||||
|
||||
## The writing checklist
|
||||
|
||||
Use this checklist to audit UI strings and AI responses.
|
||||
|
||||
### Identity and voice
|
||||
- **Eliminate the "I":** Remove all first-person pronouns (I, me, my, mine).
|
||||
- **Subject attribution:** Refer to the AI as Gemini and the infrastructure as
|
||||
the - system or the CLI.
|
||||
- **Active voice:** Ensure the subject (Gemini or the system) is clearly
|
||||
performing the action.
|
||||
- **Ownership rule:** Use the system for execution (doing) and Gemini for
|
||||
analysis (thinking)
|
||||
|
||||
### Structural scannability
|
||||
- **The skip test:** Do the first 3 words describe the user’s intent? If not,
|
||||
rewrite.
|
||||
- **Goal-first sequence:** Use the template: [To Accomplish X] + [Do Y].
|
||||
- **The 5-word rule:** Keep status updates and loading states under 5 words.
|
||||
- **Telemetry over etiquette:** Remove polite filler (Please wait, Thank you,
|
||||
Certainly). Replace with raw data or progress indicators.
|
||||
- **Micro-state cycles:** For tasks $> 3$ seconds, cycle through specific
|
||||
sub-states (e.g., Parsing logs... ➔ Identifying patterns...) to show momentum.
|
||||
|
||||
|
||||
### Technical accuracy and humility
|
||||
- **Verb signal check:** Use deterministic verbs (is, will, must) for system
|
||||
state/infrastructure.
|
||||
- Use probabilistic verbs (suggests, appears, may, identifies) for AI output.
|
||||
- **No 100% certainty:** Never attribute absolute certainty to model-generated
|
||||
content.
|
||||
- **Precision over fuzziness:** Use technical metrics (latency, tokens, compute) instead of "speed" or "cost."
|
||||
- **Instructional warnings:** Every warning must include a specific corrective action (e.g., "Perform a dry-run first" or "Review line 42").
|
||||
|
||||
### Agentic error recovery
|
||||
- **The one-step rule:** Pair every error message with exactly one immediate
|
||||
path to a fix (command, link, or prompt).
|
||||
- **Human-first:** Provide a human-readable explanation before machine error
|
||||
codes (e.g., 404, 500).
|
||||
- **Suggested prompts:** Offer specific text for the user to copy/click like
|
||||
“Ask Gemini: 'Explain this port error.'”
|
||||
|
||||
### Use consistent terminology
|
||||
|
||||
Ensure all terminology aligns with the project [word
|
||||
list](./references/word-list.md).
|
||||
|
||||
If a string uses a term marked "do not use" or "use with caution," provide a
|
||||
correction based on the preferred terms.
|
||||
|
||||
## Ensure consistent style for settings
|
||||
|
||||
If `packages/cli/src/config/settingsSchema.ts` is modified, confirm labels and
|
||||
descriptions specifically follow the unique [Settings
|
||||
guidelines](./references/settings.md).
|
||||
|
||||
## Output format
|
||||
When suggesting changes, always present your review using the following list
|
||||
format. Do not provide suggestions outside of this list..
|
||||
|
||||
```
|
||||
1. **{Rationale/Principle Violated}**
|
||||
- ❌ "{incorrect phrase}"
|
||||
- ✅ `"{corrected phrase}"`
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
# Settings
|
||||
|
||||
## Noun-First Labeling (Scannability)
|
||||
|
||||
Labels must start with the subject of the setting, not the action. This allows
|
||||
users to scan for the feature they want to change.
|
||||
|
||||
- **Rule:** `[Noun]` `[Attribute/Action]`
|
||||
- **Example:** `Show line numbers` becomes simply `Line numbers`
|
||||
|
||||
## Positive Boolean Logic (Cognitive Ease)
|
||||
|
||||
Eliminate "double negatives." Booleans should represent the presence of a
|
||||
feature, not its absence.
|
||||
|
||||
- **Rule:** Replace `Disable {feature}` or `Hide {Feature}` with
|
||||
`{Feature} enabled` or simply `{Feature}`.
|
||||
- **Example:** Change "Disable auto update" to "Auto update".
|
||||
- **Implementation:** Invert the boolean value in your config loader so true
|
||||
always equals `On`
|
||||
|
||||
## Verb Stripping (Brevity)
|
||||
|
||||
Remove redundant leading verbs like "Enable," "Use," "Display," or "Show" unless
|
||||
they are part of a specific technical term.
|
||||
|
||||
- **Rule**: If the label works without the verb, remove it
|
||||
- **Example**: Change `Enable prompt completion` to `Prompt completion`
|
||||
@@ -1,61 +0,0 @@
|
||||
## Terms
|
||||
|
||||
### Preferred
|
||||
|
||||
- Use **create** when a user is creating or setting up something.
|
||||
- Use **allow** instead of **may** to indicate that permission has been granted
|
||||
to perform some action.
|
||||
- Use **canceled**, not **cancelled**.
|
||||
- Use **configure** to refer to the process of changing the attributes of a
|
||||
feature, even if that includes turning on or off the feature.
|
||||
- Use **delete** when the action being performed is destructive.
|
||||
- Use **enable** for binary operations that turn a feature or API on. Use "turn
|
||||
on" and "turn off" instead of "enable" and "disable" for other situations.
|
||||
- Use **key combination** to refer to pressing multiple keys simultaneously.
|
||||
- Use **key sequence** to refer to pressing multiple keys separately in order.
|
||||
- Use **modify** to refer to something that has changed vs obtaining the latest
|
||||
version of something.
|
||||
- Use **remove** when the action being performed takes an item out of a larger
|
||||
whole, but doesn't destroy the item itself.
|
||||
- Use **set up** as a verb. Use **setup** as a noun or adjective.
|
||||
- Use **show**. In general, use paired with **hide**.
|
||||
- Use **sign in**, **sign out** as a verb. Use **sign-in** or **sign-out** as a
|
||||
noun or adjective.
|
||||
- Use **update** when you mean to obtain the latest version of something.
|
||||
- Use **want** instead of **like** or **would like**.
|
||||
|
||||
#### Don't use
|
||||
|
||||
- Don't use **etc.** It's redundant. To convey that a series is incomplete,
|
||||
introduce it with "such as" instead.
|
||||
- Don't use **hostname**, use "host name" instead.
|
||||
- Don't use **in order to**. It's too formal. "Before you can" is usually better
|
||||
in UI text.
|
||||
- Don't use **one or more**. Specify the quantity where possible. Use "at least
|
||||
one" when the quantity is 1+ but you can't be sure of the number. Likewise,
|
||||
use "at least one" when the user must choose a quantity of 1+.
|
||||
- Don't use the terms **log in**, **log on**, **login**, **logout** or **log
|
||||
out**.
|
||||
- Don't use **like** or **would you like**. Use **want** instead. Better yet,
|
||||
rephrase so that it's not referring to the user's emotional state, but rather
|
||||
what is required.
|
||||
|
||||
#### Use with caution
|
||||
|
||||
- Avoid using **leverage**, especially as a verb. "Leverage" is considered a
|
||||
buzzword largely devoid of meaning apart from the simpler "use".
|
||||
- Avoid using **once** as a synonym for "after". Typically, when "once" is used
|
||||
in this way, it is followed by a verb in the perfect tense.
|
||||
- Don't use **e.g.** Use "example", "such as", "like", or "for example". The
|
||||
phrase is always followed by a comma.
|
||||
- Don't use **i.e.** unless absolutely essential to make text fit. Use "that is"
|
||||
instead.
|
||||
- Use **disable** for binary operations that turn a feature or API off. Use
|
||||
"turn on" and "turn off" instead of "enable" and "disable" for other
|
||||
situations. For UI elements that are not available, use "dimmed" instead of
|
||||
"disabled".
|
||||
- Use **please** only when you're asking the user to do something inconvenient,
|
||||
not just following the instructions in a typical flow.
|
||||
- Use **really** sparingly in such constructions as "Do you really want to..."
|
||||
Because of the weight it puts on the decision, it should be used to confirm
|
||||
actions that the user is extremely unlikely to make.
|
||||
@@ -1 +0,0 @@
|
||||
packages/core/src/services/scripts/*.exe
|
||||
@@ -14,9 +14,3 @@
|
||||
|
||||
# Docs have a dedicated approver group in addition to maintainers
|
||||
/docs/ @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
|
||||
/README.md @google-gemini/gemini-cli-maintainers @google-gemini/gemini-cli-docs
|
||||
|
||||
# Prompt contents, tool definitions, and evals require reviews from prompt approvers
|
||||
/packages/core/src/prompts/ @google-gemini/gemini-cli-prompt-approvers
|
||||
/packages/core/src/tools/ @google-gemini/gemini-cli-prompt-approvers
|
||||
/evals/ @google-gemini/gemini-cli-prompt-approvers
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
name: 'Website issue'
|
||||
description: 'Report an issue with the Gemini CLI Website and Gemini CLI Extensions Gallery'
|
||||
title: 'GeminiCLI.com Feedback: [ISSUE]'
|
||||
labels:
|
||||
- 'area/extensions'
|
||||
- 'area/documentation'
|
||||
body:
|
||||
- type: 'markdown'
|
||||
attributes:
|
||||
|
||||
@@ -39,22 +39,18 @@ runs:
|
||||
if: "inputs.dry-run != 'true'"
|
||||
env:
|
||||
GH_TOKEN: '${{ inputs.github-token }}'
|
||||
INPUTS_BRANCH_NAME: '${{ inputs.branch-name }}'
|
||||
INPUTS_PR_TITLE: '${{ inputs.pr-title }}'
|
||||
INPUTS_PR_BODY: '${{ inputs.pr-body }}'
|
||||
INPUTS_BASE_BRANCH: '${{ inputs.base-branch }}'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
set -e
|
||||
if ! git ls-remote --exit-code --heads origin "${INPUTS_BRANCH_NAME}"; then
|
||||
echo "::error::Branch '${INPUTS_BRANCH_NAME}' does not exist on the remote repository."
|
||||
if ! git ls-remote --exit-code --heads origin "${{ inputs.branch-name }}"; then
|
||||
echo "::error::Branch '${{ inputs.branch-name }}' does not exist on the remote repository."
|
||||
exit 1
|
||||
fi
|
||||
PR_URL=$(gh pr create \
|
||||
--title "${INPUTS_PR_TITLE}" \
|
||||
--body "${INPUTS_PR_BODY}" \
|
||||
--base "${INPUTS_BASE_BRANCH}" \
|
||||
--head "${INPUTS_BRANCH_NAME}" \
|
||||
--title "${{ inputs.pr-title }}" \
|
||||
--body "${{ inputs.pr-body }}" \
|
||||
--base "${{ inputs.base-branch }}" \
|
||||
--head "${{ inputs.branch-name }}" \
|
||||
--fill)
|
||||
gh pr merge "$PR_URL" --auto
|
||||
|
||||
@@ -30,22 +30,16 @@ runs:
|
||||
id: 'npm_auth_token'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
AUTH_TOKEN="${INPUTS_GITHUB_TOKEN}"
|
||||
PACKAGE_NAME="${INPUTS_PACKAGE_NAME}"
|
||||
AUTH_TOKEN="${{ inputs.github-token }}"
|
||||
PACKAGE_NAME="${{ inputs.package-name }}"
|
||||
PRIVATE_REPO="@google-gemini/"
|
||||
if [[ "$PACKAGE_NAME" == "$PRIVATE_REPO"* ]]; then
|
||||
AUTH_TOKEN="${INPUTS_GITHUB_TOKEN}"
|
||||
AUTH_TOKEN="${{ inputs.github-token }}"
|
||||
elif [[ "$PACKAGE_NAME" == "@google/gemini-cli" ]]; then
|
||||
AUTH_TOKEN="${INPUTS_WOMBAT_TOKEN_CLI}"
|
||||
AUTH_TOKEN="${{ inputs.wombat-token-cli }}"
|
||||
elif [[ "$PACKAGE_NAME" == "@google/gemini-cli-core" ]]; then
|
||||
AUTH_TOKEN="${INPUTS_WOMBAT_TOKEN_CORE}"
|
||||
AUTH_TOKEN="${{ inputs.wombat-token-core }}"
|
||||
elif [[ "$PACKAGE_NAME" == "@google/gemini-cli-a2a-server" ]]; then
|
||||
AUTH_TOKEN="${INPUTS_WOMBAT_TOKEN_A2A_SERVER}"
|
||||
AUTH_TOKEN="${{ inputs.wombat-token-a2a-server }}"
|
||||
fi
|
||||
echo "auth-token=$AUTH_TOKEN" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
INPUTS_GITHUB_TOKEN: '${{ inputs.github-token }}'
|
||||
INPUTS_PACKAGE_NAME: '${{ inputs.package-name }}'
|
||||
INPUTS_WOMBAT_TOKEN_CLI: '${{ inputs.wombat-token-cli }}'
|
||||
INPUTS_WOMBAT_TOKEN_CORE: '${{ inputs.wombat-token-core }}'
|
||||
INPUTS_WOMBAT_TOKEN_A2A_SERVER: '${{ inputs.wombat-token-a2a-server }}'
|
||||
|
||||
@@ -93,19 +93,15 @@ runs:
|
||||
id: 'release_branch'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
BRANCH_NAME="release/${INPUTS_RELEASE_TAG}"
|
||||
BRANCH_NAME="release/${{ inputs.release-tag }}"
|
||||
git switch -c "${BRANCH_NAME}"
|
||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
INPUTS_RELEASE_TAG: '${{ inputs.release-tag }}'
|
||||
|
||||
- name: '⬆️ Update package versions'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm run release:version "${INPUTS_RELEASE_VERSION}"
|
||||
env:
|
||||
INPUTS_RELEASE_VERSION: '${{ inputs.release-version }}'
|
||||
npm run release:version "${{ inputs.release-version }}"
|
||||
|
||||
- name: '💾 Commit and Conditionally Push package versions'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
@@ -167,39 +163,23 @@ runs:
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
|
||||
INPUTS_DRY_RUN: '${{ inputs.dry-run }}'
|
||||
INPUTS_CORE_PACKAGE_NAME: '${{ inputs.core-package-name }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm publish \
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_CORE_PACKAGE_NAME}" \
|
||||
--dry-run="${{ inputs.dry-run }}" \
|
||||
--workspace="${{ inputs.core-package-name }}" \
|
||||
--no-tag
|
||||
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
|
||||
npm dist-tag rm ${INPUTS_CORE_PACKAGE_NAME} false
|
||||
fi
|
||||
npm dist-tag rm ${{ inputs.core-package-name }} false --silent
|
||||
|
||||
- name: '🔗 Install latest core package'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
if: "${{ inputs.dry-run != 'true' }}"
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm install "${INPUTS_CORE_PACKAGE_NAME}@${INPUTS_RELEASE_VERSION}" \
|
||||
--workspace="${INPUTS_CLI_PACKAGE_NAME}" \
|
||||
--workspace="${INPUTS_A2A_PACKAGE_NAME}" \
|
||||
npm install "${{ inputs.core-package-name }}@${{ inputs.release-version }}" \
|
||||
--workspace="${{ inputs.cli-package-name }}" \
|
||||
--workspace="${{ inputs.a2a-package-name }}" \
|
||||
--save-exact
|
||||
env:
|
||||
INPUTS_CORE_PACKAGE_NAME: '${{ inputs.core-package-name }}'
|
||||
INPUTS_RELEASE_VERSION: '${{ inputs.release-version }}'
|
||||
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
|
||||
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
|
||||
|
||||
- name: '📦 Prepare bundled CLI for npm release'
|
||||
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/'"
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
node ${{ github.workspace }}/scripts/prepare-npm-release.js
|
||||
|
||||
- name: 'Get CLI Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
@@ -215,17 +195,13 @@ runs:
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
|
||||
INPUTS_DRY_RUN: '${{ inputs.dry-run }}'
|
||||
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
npm publish \
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_CLI_PACKAGE_NAME}" \
|
||||
--dry-run="${{ inputs.dry-run }}" \
|
||||
--workspace="${{ inputs.cli-package-name }}" \
|
||||
--no-tag
|
||||
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
|
||||
npm dist-tag rm ${INPUTS_CLI_PACKAGE_NAME} false
|
||||
fi
|
||||
npm dist-tag rm ${{ inputs.cli-package-name }} false --silent
|
||||
|
||||
- name: 'Get a2a-server Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
@@ -241,18 +217,14 @@ runs:
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
|
||||
INPUTS_DRY_RUN: '${{ inputs.dry-run }}'
|
||||
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
|
||||
shell: 'bash'
|
||||
# Tag staging for initial release
|
||||
run: |
|
||||
npm publish \
|
||||
--dry-run="${INPUTS_DRY_RUN}" \
|
||||
--workspace="${INPUTS_A2A_PACKAGE_NAME}" \
|
||||
--dry-run="${{ inputs.dry-run }}" \
|
||||
--workspace="${{ inputs.a2a-package-name }}" \
|
||||
--no-tag
|
||||
if [[ "${INPUTS_DRY_RUN}" == "false" ]]; then
|
||||
npm dist-tag rm ${INPUTS_A2A_PACKAGE_NAME} false
|
||||
fi
|
||||
npm dist-tag rm ${{ inputs.a2a-package-name }} false --silent
|
||||
|
||||
- name: '🔬 Verify NPM release by version'
|
||||
uses: './.github/actions/verify-release'
|
||||
@@ -286,33 +258,13 @@ runs:
|
||||
if: "${{ inputs.dry-run != 'true' && inputs.skip-github-release != 'true' && inputs.npm-tag != 'dev' && inputs.npm-registry-url != 'https://npm.pkg.github.com/' }}"
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ inputs.github-release-token || inputs.github-token }}'
|
||||
INPUTS_RELEASE_TAG: '${{ inputs.release-tag }}'
|
||||
STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
INPUTS_PREVIOUS_TAG: '${{ inputs.previous-tag }}'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
rm -f gemini-cli-bundle.zip
|
||||
(cd bundle && chmod +x gemini.js && zip -r ../gemini-cli-bundle.zip .)
|
||||
|
||||
echo "Testing the generated bundle archive..."
|
||||
rm -rf test-bundle
|
||||
mkdir -p test-bundle
|
||||
unzip -q gemini-cli-bundle.zip -d test-bundle
|
||||
|
||||
# Verify it runs and outputs a version
|
||||
BUNDLE_VERSION=$(node test-bundle/gemini.js --version | xargs)
|
||||
echo "Bundle version output: ${BUNDLE_VERSION}"
|
||||
if [[ -z "${BUNDLE_VERSION}" ]]; then
|
||||
echo "Error: Bundle failed to execute or return version."
|
||||
exit 1
|
||||
fi
|
||||
rm -rf test-bundle
|
||||
|
||||
gh release create "${INPUTS_RELEASE_TAG}" \
|
||||
gemini-cli-bundle.zip \
|
||||
--target "${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}" \
|
||||
--title "Release ${INPUTS_RELEASE_TAG}" \
|
||||
--notes-start-tag "${INPUTS_PREVIOUS_TAG}" \
|
||||
gh release create "${{ inputs.release-tag }}" \
|
||||
bundle/gemini.js \
|
||||
--target "${{ steps.release_branch.outputs.BRANCH_NAME }}" \
|
||||
--title "Release ${{ inputs.release-tag }}" \
|
||||
--notes-start-tag "${{ inputs.previous-tag }}" \
|
||||
--generate-notes \
|
||||
${{ inputs.npm-tag != 'latest' && '--prerelease' || '' }}
|
||||
|
||||
@@ -322,8 +274,5 @@ runs:
|
||||
continue-on-error: true
|
||||
shell: 'bash'
|
||||
run: |
|
||||
echo "Cleaning up release branch ${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}..."
|
||||
git push origin --delete "${STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME}"
|
||||
|
||||
env:
|
||||
STEPS_RELEASE_BRANCH_OUTPUTS_BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
echo "Cleaning up release branch ${{ steps.release_branch.outputs.BRANCH_NAME }}..."
|
||||
git push origin --delete "${{ steps.release_branch.outputs.BRANCH_NAME }}"
|
||||
|
||||
@@ -52,10 +52,8 @@ runs:
|
||||
id: 'branch_name'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
REF_NAME="${INPUTS_REF_NAME}"
|
||||
REF_NAME="${{ inputs.ref-name }}"
|
||||
echo "name=${REF_NAME%/merge}" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
INPUTS_REF_NAME: '${{ inputs.ref-name }}'
|
||||
- name: 'Build and Push the Docker Image'
|
||||
uses: 'docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83' # ratchet:docker/build-push-action@v6
|
||||
with:
|
||||
|
||||
@@ -34,7 +34,7 @@ runs:
|
||||
JSON_INPUTS: '${{ toJSON(inputs) }}'
|
||||
run: 'echo "$JSON_INPUTS"'
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
uses: 'actions/checkout@v4'
|
||||
with:
|
||||
ref: '${{ inputs.github-sha }}'
|
||||
fetch-depth: 0
|
||||
@@ -44,12 +44,10 @@ runs:
|
||||
- name: 'npm build'
|
||||
shell: 'bash'
|
||||
run: 'npm run build'
|
||||
- name: 'Set up QEMU'
|
||||
uses: 'docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130' # ratchet:docker/setup-qemu-action@v3
|
||||
- name: 'Set up Docker Buildx'
|
||||
uses: 'docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f' # ratchet:docker/setup-buildx-action@v3
|
||||
uses: 'docker/setup-buildx-action@v3'
|
||||
- name: 'Log in to GitHub Container Registry'
|
||||
uses: 'docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9' # ratchet:docker/login-action@v3
|
||||
uses: 'docker/login-action@v3'
|
||||
with:
|
||||
registry: 'docker.io'
|
||||
username: '${{ inputs.dockerhub-username }}'
|
||||
@@ -58,8 +56,8 @@ runs:
|
||||
id: 'image_tag'
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
SHELL_TAG_NAME="${INPUTS_GITHUB_REF_NAME}"
|
||||
FINAL_TAG="${INPUTS_GITHUB_SHA}"
|
||||
SHELL_TAG_NAME="${{ inputs.github-ref-name }}"
|
||||
FINAL_TAG="${{ inputs.github-sha }}"
|
||||
if [[ "$SHELL_TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
|
||||
echo "Release detected."
|
||||
FINAL_TAG="${SHELL_TAG_NAME#v}"
|
||||
@@ -68,22 +66,15 @@ runs:
|
||||
fi
|
||||
echo "Determined image tag: $FINAL_TAG"
|
||||
echo "FINAL_TAG=$FINAL_TAG" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
INPUTS_GITHUB_REF_NAME: '${{ inputs.github-ref-name }}'
|
||||
INPUTS_GITHUB_SHA: '${{ inputs.github-sha }}'
|
||||
# We build amd64 just so we can verify it.
|
||||
# We build and push both amd64 and arm64 in the publish step.
|
||||
- name: 'build'
|
||||
id: 'docker_build'
|
||||
shell: 'bash'
|
||||
env:
|
||||
GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
|
||||
GEMINI_SANDBOX: 'docker'
|
||||
BUILD_SANDBOX_FLAGS: '--platform linux/amd64 --load'
|
||||
STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
|
||||
run: |-
|
||||
npm run build:sandbox -- \
|
||||
--image "google/gemini-cli-sandbox:${STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG}" \
|
||||
--image google/gemini-cli-sandbox:${{ steps.image_tag.outputs.FINAL_TAG }} \
|
||||
--output-file final_image_uri.txt
|
||||
echo "uri=$(cat final_image_uri.txt)" >> $GITHUB_OUTPUT
|
||||
- name: 'verify'
|
||||
@@ -97,14 +88,8 @@ runs:
|
||||
- name: 'publish'
|
||||
shell: 'bash'
|
||||
if: "${{ inputs.dry-run != 'true' }}"
|
||||
env:
|
||||
GEMINI_SANDBOX_IMAGE_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
|
||||
GEMINI_SANDBOX: 'docker'
|
||||
BUILD_SANDBOX_FLAGS: '--platform linux/amd64,linux/arm64 --push'
|
||||
STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG: '${{ steps.image_tag.outputs.FINAL_TAG }}'
|
||||
run: |-
|
||||
npm run build:sandbox -- \
|
||||
--image "google/gemini-cli-sandbox:${STEPS_IMAGE_TAG_OUTPUTS_FINAL_TAG}"
|
||||
docker push "${{ steps.docker_build.outputs.uri }}"
|
||||
- name: 'Create issue on failure'
|
||||
if: |-
|
||||
${{ failure() }}
|
||||
|
||||
@@ -18,13 +18,6 @@ runs:
|
||||
env:
|
||||
JSON_INPUTS: '${{ toJSON(inputs) }}'
|
||||
run: 'echo "$JSON_INPUTS"'
|
||||
- name: 'Install system dependencies'
|
||||
if: "runner.os == 'Linux'"
|
||||
run: |
|
||||
sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bubblewrap
|
||||
# Ubuntu 24.04+ requires this to allow bwrap to function in CI
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
shell: 'bash'
|
||||
- name: 'Run Tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
|
||||
|
||||
@@ -18,7 +18,5 @@ runs:
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
echo ""@google-gemini:registry=https://npm.pkg.github.com"" > ~/.npmrc
|
||||
echo ""//npm.pkg.github.com/:_authToken=${INPUTS_GITHUB_TOKEN}"" >> ~/.npmrc
|
||||
echo ""//npm.pkg.github.com/:_authToken=${{ inputs.github-token }}"" >> ~/.npmrc
|
||||
echo ""@google:registry=https://wombat-dressing-room.appspot.com"" >> ~/.npmrc
|
||||
env:
|
||||
INPUTS_GITHUB_TOKEN: '${{ inputs.github-token }}'
|
||||
|
||||
@@ -71,13 +71,10 @@ runs:
|
||||
${{ inputs.dry-run != 'true' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.core-token.outputs.auth-token }}'
|
||||
INPUTS_CORE_PACKAGE_NAME: '${{ inputs.core-package-name }}'
|
||||
INPUTS_VERSION: '${{ inputs.version }}'
|
||||
INPUTS_CHANNEL: '${{ inputs.channel }}'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
npm dist-tag add ${INPUTS_CORE_PACKAGE_NAME}@${INPUTS_VERSION} ${INPUTS_CHANNEL}
|
||||
npm dist-tag add ${{ inputs.core-package-name }}@${{ inputs.version }} ${{ inputs.channel }}
|
||||
|
||||
- name: 'Get cli Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
@@ -94,13 +91,10 @@ runs:
|
||||
${{ inputs.dry-run != 'true' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.cli-token.outputs.auth-token }}'
|
||||
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
|
||||
INPUTS_VERSION: '${{ inputs.version }}'
|
||||
INPUTS_CHANNEL: '${{ inputs.channel }}'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
npm dist-tag add ${INPUTS_CLI_PACKAGE_NAME}@${INPUTS_VERSION} ${INPUTS_CHANNEL}
|
||||
npm dist-tag add ${{ inputs.cli-package-name }}@${{ inputs.version }} ${{ inputs.channel }}
|
||||
|
||||
- name: 'Get a2a Token'
|
||||
uses: './.github/actions/npm-auth-token'
|
||||
@@ -117,13 +111,10 @@ runs:
|
||||
${{ inputs.dry-run == 'false' }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: '${{ steps.a2a-token.outputs.auth-token }}'
|
||||
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
|
||||
INPUTS_VERSION: '${{ inputs.version }}'
|
||||
INPUTS_CHANNEL: '${{ inputs.channel }}'
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
npm dist-tag add ${INPUTS_A2A_PACKAGE_NAME}@${INPUTS_VERSION} ${INPUTS_CHANNEL}
|
||||
npm dist-tag add ${{ inputs.a2a-package-name }}@${{ inputs.version }} ${{ inputs.channel }}
|
||||
|
||||
- name: 'Log dry run'
|
||||
if: |-
|
||||
@@ -131,15 +122,4 @@ runs:
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |
|
||||
echo "Dry run: Would have added tag '${INPUTS_CHANNEL}' to version '${INPUTS_VERSION}' for ${INPUTS_CLI_PACKAGE_NAME}, ${INPUTS_CORE_PACKAGE_NAME}, and ${INPUTS_A2A_PACKAGE_NAME}."
|
||||
|
||||
env:
|
||||
INPUTS_CHANNEL: '${{ inputs.channel }}'
|
||||
|
||||
INPUTS_VERSION: '${{ inputs.version }}'
|
||||
|
||||
INPUTS_CLI_PACKAGE_NAME: '${{ inputs.cli-package-name }}'
|
||||
|
||||
INPUTS_CORE_PACKAGE_NAME: '${{ inputs.core-package-name }}'
|
||||
|
||||
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
|
||||
echo "Dry run: Would have added tag '${{ inputs.channel }}' to version '${{ inputs.version }}' for ${{ inputs.cli-package-name }}, ${{ inputs.core-package-name }}, and ${{ inputs.a2a-package-name }}."
|
||||
|
||||
@@ -36,7 +36,7 @@ runs:
|
||||
run: 'echo "$JSON_INPUTS"'
|
||||
|
||||
- name: 'setup node'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
uses: 'actions/setup-node@v4'
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
@@ -64,13 +64,10 @@ runs:
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |-
|
||||
gemini_version=$(gemini --version)
|
||||
if [ "$gemini_version" != "${INPUTS_EXPECTED_VERSION}" ]; then
|
||||
echo "❌ NPM Version mismatch: Got $gemini_version from ${INPUTS_NPM_PACKAGE}, expected ${INPUTS_EXPECTED_VERSION}"
|
||||
if [ "$gemini_version" != "${{ inputs.expected-version }}" ]; then
|
||||
echo "❌ NPM Version mismatch: Got $gemini_version from ${{ inputs.npm-package }}, expected ${{ inputs.expected-version }}"
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
INPUTS_EXPECTED_VERSION: '${{ inputs.expected-version }}'
|
||||
INPUTS_NPM_PACKAGE: '${{ inputs.npm-package }}'
|
||||
|
||||
- name: 'Clear npm cache'
|
||||
shell: 'bash'
|
||||
@@ -80,14 +77,11 @@ runs:
|
||||
shell: 'bash'
|
||||
working-directory: '${{ inputs.working-directory }}'
|
||||
run: |-
|
||||
gemini_version=$(npx --prefer-online "${INPUTS_NPM_PACKAGE}" --version)
|
||||
if [ "$gemini_version" != "${INPUTS_EXPECTED_VERSION}" ]; then
|
||||
echo "❌ NPX Run Version mismatch: Got $gemini_version from ${INPUTS_NPM_PACKAGE}, expected ${INPUTS_EXPECTED_VERSION}"
|
||||
gemini_version=$(npx --prefer-online "${{ inputs.npm-package}}" --version)
|
||||
if [ "$gemini_version" != "${{ inputs.expected-version }}" ]; then
|
||||
echo "❌ NPX Run Version mismatch: Got $gemini_version from ${{ inputs.npm-package }}, expected ${{ inputs.expected-version }}"
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
INPUTS_NPM_PACKAGE: '${{ inputs.npm-package }}'
|
||||
INPUTS_EXPECTED_VERSION: '${{ inputs.expected-version }}'
|
||||
|
||||
- name: 'Install dependencies for integration tests'
|
||||
shell: 'bash'
|
||||
|
||||
@@ -347,36 +347,6 @@ async function run() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove status/need-triage from maintainer-only issues since they
|
||||
// don't need community triage. We always attempt removal rather than
|
||||
// checking the (potentially stale) label snapshot, because the
|
||||
// issue-opened-labeler workflow runs concurrently and may add the
|
||||
// label after our snapshot was taken.
|
||||
if (isDryRun) {
|
||||
console.log(
|
||||
`[DRY RUN] Would remove status/need-triage from ${issueKey}`,
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
await octokit.rest.issues.removeLabel({
|
||||
owner: issueInfo.owner,
|
||||
repo: issueInfo.repo,
|
||||
issue_number: issueInfo.number,
|
||||
name: 'status/need-triage',
|
||||
});
|
||||
console.log(`Removed status/need-triage from ${issueKey}`);
|
||||
} catch (removeError) {
|
||||
// 404 means the label wasn't present — that's fine.
|
||||
if (removeError.status === 404) {
|
||||
console.log(
|
||||
`status/need-triage not present on ${issueKey}, skipping.`,
|
||||
);
|
||||
} else {
|
||||
throw removeError;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing label for ${issueKey}: ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ jobs:
|
||||
name: 'Merge Queue Skipper'
|
||||
permissions: 'read-all'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
outputs:
|
||||
skip: '${{ steps.merge-queue-e2e-skipper.outputs.skip-check }}'
|
||||
steps:
|
||||
@@ -43,7 +42,7 @@ jobs:
|
||||
|
||||
download_repo_name:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run')"
|
||||
if: "${{github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_run'}}"
|
||||
outputs:
|
||||
repo_name: '${{ steps.output-repo-name.outputs.repo_name }}'
|
||||
head_sha: '${{ steps.output-repo-name.outputs.head_sha }}'
|
||||
@@ -54,7 +53,7 @@ jobs:
|
||||
REPO_NAME: '${{ github.event.inputs.repo_name }}'
|
||||
run: |
|
||||
mkdir -p ./pr
|
||||
echo "${REPO_NAME}" > ./pr/repo_name
|
||||
echo '${{ env.REPO_NAME }}' > ./pr/repo_name
|
||||
- uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'repo_name'
|
||||
@@ -92,7 +91,7 @@ jobs:
|
||||
name: 'Parse run context'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs: 'download_repo_name'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
outputs:
|
||||
repository: '${{ steps.set_context.outputs.REPO }}'
|
||||
sha: '${{ steps.set_context.outputs.SHA }}'
|
||||
@@ -112,11 +111,11 @@ jobs:
|
||||
permissions: 'write-all'
|
||||
needs:
|
||||
- 'parse_run_context'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
steps:
|
||||
- name: 'Set pending status'
|
||||
uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
with:
|
||||
allowForks: 'true'
|
||||
repo: '${{ github.repository }}'
|
||||
@@ -132,7 +131,7 @@ jobs:
|
||||
- 'parse_run_context'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -185,7 +184,7 @@ jobs:
|
||||
- 'parse_run_context'
|
||||
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')
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
@@ -223,7 +222,7 @@ jobs:
|
||||
- 'merge_queue_skipper'
|
||||
- 'parse_run_context'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
@@ -264,27 +263,6 @@ jobs:
|
||||
run: 'npm run build'
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Ensure Chrome is available'
|
||||
shell: 'pwsh'
|
||||
run: |
|
||||
$chromePaths = @(
|
||||
"${env:ProgramFiles}\Google\Chrome\Application\chrome.exe",
|
||||
"${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe"
|
||||
)
|
||||
$chromeExists = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if (-not $chromeExists) {
|
||||
Write-Host 'Chrome not found, installing via Chocolatey...'
|
||||
choco install googlechrome -y --no-progress --ignore-checksums
|
||||
}
|
||||
$installed = $chromePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if ($installed) {
|
||||
Write-Host "Chrome found at: $installed"
|
||||
& $installed --version
|
||||
} else {
|
||||
Write-Error 'Chrome installation failed'
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: 'Run E2E tests'
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
@@ -304,14 +282,13 @@ jobs:
|
||||
- 'parse_run_context'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
ref: '${{ needs.parse_run_context.outputs.sha }}'
|
||||
repository: '${{ needs.parse_run_context.outputs.repository }}'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Set up Node.js 20.x'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions-node@v4
|
||||
@@ -324,34 +301,15 @@ jobs:
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Check if evals should run'
|
||||
id: 'check_evals'
|
||||
run: |
|
||||
SHOULD_RUN=$(node scripts/changed_prompt.js)
|
||||
echo "should_run=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Run Evals (Required to pass)'
|
||||
if: "${{ steps.check_evals.outputs.should_run == 'true' }}"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GEMINI_MODEL: 'gemini-3-pro-preview'
|
||||
# Disable Vitest internal retries to avoid double-retrying;
|
||||
# custom retry logic is handled in evals/test-helper.ts
|
||||
VITEST_RETRY: 0
|
||||
run: 'npm run test:always_passing_evals'
|
||||
|
||||
- name: 'Upload Reliability Logs'
|
||||
if: "always() && steps.check_evals.outputs.should_run == 'true'"
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'eval-logs-${{ github.run_id }}-${{ github.run_attempt }}'
|
||||
path: 'evals/logs/api-reliability.jsonl'
|
||||
retention-days: 7
|
||||
|
||||
e2e:
|
||||
name: 'E2E'
|
||||
if: |
|
||||
github.repository == 'google-gemini/gemini-cli' && always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
always() && (needs.merge_queue_skipper.result !='success' || needs.merge_queue_skipper.outputs.skip != 'true')
|
||||
needs:
|
||||
- 'e2e_linux'
|
||||
- 'e2e_mac'
|
||||
@@ -362,31 +320,26 @@ jobs:
|
||||
steps:
|
||||
- name: 'Check E2E test results'
|
||||
run: |
|
||||
if [[ ${NEEDS_E2E_LINUX_RESULT} != 'success' || \
|
||||
${NEEDS_E2E_MAC_RESULT} != 'success' || \
|
||||
${NEEDS_E2E_WINDOWS_RESULT} != 'success' || \
|
||||
${NEEDS_EVALS_RESULT} != 'success' ]]; then
|
||||
if [[ ${{ needs.e2e_linux.result }} != 'success' || \
|
||||
${{ needs.e2e_mac.result }} != 'success' || \
|
||||
${{ needs.e2e_windows.result }} != 'success' || \
|
||||
${{ needs.evals.result }} != 'success' ]]; then
|
||||
echo "One or more E2E jobs failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "All required E2E jobs passed!"
|
||||
env:
|
||||
NEEDS_E2E_LINUX_RESULT: '${{ needs.e2e_linux.result }}'
|
||||
NEEDS_E2E_MAC_RESULT: '${{ needs.e2e_mac.result }}'
|
||||
NEEDS_E2E_WINDOWS_RESULT: '${{ needs.e2e_windows.result }}'
|
||||
NEEDS_EVALS_RESULT: '${{ needs.evals.result }}'
|
||||
|
||||
set_workflow_status:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions: 'write-all'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
needs:
|
||||
- 'parse_run_context'
|
||||
- 'e2e'
|
||||
steps:
|
||||
- name: 'Set workflow status'
|
||||
uses: 'myrotvorets/set-commit-status-action@16037e056d73b2d3c88e37e393ff369047f70886' # ratchet:myrotvorets/set-commit-status-action@master
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
with:
|
||||
allowForks: 'true'
|
||||
repo: '${{ github.repository }}'
|
||||
|
||||
@@ -37,7 +37,6 @@ jobs:
|
||||
permissions: 'read-all'
|
||||
name: 'Merge Queue Skipper'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
outputs:
|
||||
skip: '${{ steps.merge-queue-ci-skipper.outputs.skip-check }}'
|
||||
steps:
|
||||
@@ -50,7 +49,7 @@ jobs:
|
||||
name: 'Lint'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
env:
|
||||
GEMINI_LINT_TEMP_DIR: '${{ github.workspace }}/.gemini-linters'
|
||||
steps:
|
||||
@@ -67,7 +66,7 @@ jobs:
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Cache Linters'
|
||||
uses: 'actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830' # ratchet:actions/cache@v4
|
||||
uses: 'actions/cache@v4'
|
||||
with:
|
||||
path: '${{ env.GEMINI_LINT_TEMP_DIR }}'
|
||||
key: "${{ runner.os }}-${{ runner.arch }}-linters-${{ hashFiles('scripts/lint.js') }}"
|
||||
@@ -76,7 +75,7 @@ jobs:
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Cache ESLint'
|
||||
uses: 'actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830' # ratchet:actions/cache@v4
|
||||
uses: 'actions/cache@v4'
|
||||
with:
|
||||
path: '.eslintcache'
|
||||
key: "${{ runner.os }}-eslint-${{ hashFiles('package-lock.json', 'eslint.config.js') }}"
|
||||
@@ -114,13 +113,9 @@ jobs:
|
||||
- name: 'Run sensitive keyword linter'
|
||||
run: 'node scripts/lint.js --sensitive-keywords'
|
||||
|
||||
- name: 'Run GitHub Actions pinning linter'
|
||||
run: 'node scripts/lint.js --check-github-actions-pinning'
|
||||
|
||||
link_checker:
|
||||
name: 'Link Checker'
|
||||
runs-on: 'ubuntu-latest'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
|
||||
@@ -134,7 +129,7 @@ jobs:
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
permissions:
|
||||
contents: 'read'
|
||||
checks: 'write'
|
||||
@@ -161,12 +156,6 @@ jobs:
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Install system dependencies'
|
||||
run: |
|
||||
sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bubblewrap
|
||||
# Ubuntu 24.04+ requires this to allow bwrap to function in CI
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
|
||||
|
||||
- name: 'Install dependencies for testing'
|
||||
run: 'npm ci'
|
||||
|
||||
@@ -175,10 +164,10 @@ jobs:
|
||||
NO_COLOR: true
|
||||
run: |
|
||||
if [[ "${{ matrix.shard }}" == "cli" ]]; then
|
||||
npm run test:ci --workspace "@google/gemini-cli"
|
||||
npm run test:ci --workspace @google/gemini-cli
|
||||
else
|
||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
||||
npm run test:ci --workspace "@google/gemini-cli-core" --workspace "@google/gemini-cli-a2a-server" --workspace "gemini-cli-vscode-ide-companion" --workspace "@google/gemini-cli-test-utils" --if-present -- --coverage.enabled=false
|
||||
npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present
|
||||
npm run test:scripts
|
||||
fi
|
||||
|
||||
@@ -227,7 +216,7 @@ jobs:
|
||||
runs-on: 'macos-latest'
|
||||
needs:
|
||||
- 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
permissions:
|
||||
contents: 'read'
|
||||
checks: 'write'
|
||||
@@ -263,10 +252,10 @@ jobs:
|
||||
NO_COLOR: true
|
||||
run: |
|
||||
if [[ "${{ matrix.shard }}" == "cli" ]]; then
|
||||
npm run test:ci --workspace "@google/gemini-cli" -- --coverage.enabled=false
|
||||
npm run test:ci --workspace @google/gemini-cli -- --coverage.enabled=false
|
||||
else
|
||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
||||
npm run test:ci --workspace "@google/gemini-cli-core" --workspace "@google/gemini-cli-a2a-server" --workspace "gemini-cli-vscode-ide-companion" --workspace "@google/gemini-cli-test-utils" --if-present -- --coverage.enabled=false
|
||||
npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present -- --coverage.enabled=false
|
||||
npm run test:scripts
|
||||
fi
|
||||
|
||||
@@ -322,7 +311,7 @@ jobs:
|
||||
name: 'CodeQL'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
permissions:
|
||||
actions: 'read'
|
||||
contents: 'read'
|
||||
@@ -345,7 +334,7 @@ jobs:
|
||||
bundle_size:
|
||||
name: 'Check Bundle Size'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && github.event_name == 'pull_request' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
if: "${{github.event_name == 'pull_request' && needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
permissions:
|
||||
contents: 'read' # For checkout
|
||||
@@ -370,7 +359,7 @@ jobs:
|
||||
name: 'Slow Test - Win - ${{ matrix.shard }}'
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
needs: 'merge_queue_skipper'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && needs.merge_queue_skipper.outputs.skip == 'false'"
|
||||
if: "${{needs.merge_queue_skipper.outputs.skip == 'false'}}"
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -429,14 +418,11 @@ jobs:
|
||||
NODE_ENV: 'test'
|
||||
run: |
|
||||
if ("${{ matrix.shard }}" -eq "cli") {
|
||||
npm run test:ci --workspace "@google/gemini-cli" -- --coverage.enabled=false
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
npm run test:ci --workspace @google/gemini-cli -- --coverage.enabled=false
|
||||
} else {
|
||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
||||
npm run test:ci --workspace "@google/gemini-cli-core" --workspace "@google/gemini-cli-a2a-server" --workspace "gemini-cli-vscode-ide-companion" --workspace "@google/gemini-cli-test-utils" --if-present -- --coverage.enabled=false
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present -- --coverage.enabled=false
|
||||
npm run test:scripts
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
shell: 'pwsh'
|
||||
|
||||
@@ -465,7 +451,7 @@ jobs:
|
||||
|
||||
ci:
|
||||
name: 'CI'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
needs:
|
||||
- 'lint'
|
||||
- 'link_checker'
|
||||
@@ -478,22 +464,14 @@ jobs:
|
||||
steps:
|
||||
- name: 'Check all job results'
|
||||
run: |
|
||||
if [[ (${NEEDS_LINT_RESULT} != 'success' && ${NEEDS_LINT_RESULT} != 'skipped') || \
|
||||
(${NEEDS_LINK_CHECKER_RESULT} != 'success' && ${NEEDS_LINK_CHECKER_RESULT} != 'skipped') || \
|
||||
(${NEEDS_TEST_LINUX_RESULT} != 'success' && ${NEEDS_TEST_LINUX_RESULT} != 'skipped') || \
|
||||
(${NEEDS_TEST_MAC_RESULT} != 'success' && ${NEEDS_TEST_MAC_RESULT} != 'skipped') || \
|
||||
(${NEEDS_TEST_WINDOWS_RESULT} != 'success' && ${NEEDS_TEST_WINDOWS_RESULT} != 'skipped') || \
|
||||
(${NEEDS_CODEQL_RESULT} != 'success' && ${NEEDS_CODEQL_RESULT} != 'skipped') || \
|
||||
(${NEEDS_BUNDLE_SIZE_RESULT} != 'success' && ${NEEDS_BUNDLE_SIZE_RESULT} != 'skipped') ]]; then
|
||||
if [[ (${{ needs.lint.result }} != 'success' && ${{ needs.lint.result }} != 'skipped') || \
|
||||
(${{ needs.link_checker.result }} != 'success' && ${{ needs.link_checker.result }} != 'skipped') || \
|
||||
(${{ needs.test_linux.result }} != 'success' && ${{ needs.test_linux.result }} != 'skipped') || \
|
||||
(${{ needs.test_mac.result }} != 'success' && ${{ needs.test_mac.result }} != 'skipped') || \
|
||||
(${{ needs.test_windows.result }} != 'success' && ${{ needs.test_windows.result }} != 'skipped') || \
|
||||
(${{ needs.codeql.result }} != 'success' && ${{ needs.codeql.result }} != 'skipped') || \
|
||||
(${{ needs.bundle_size.result }} != 'success' && ${{ needs.bundle_size.result }} != 'skipped') ]]; then
|
||||
echo "One or more CI jobs failed."
|
||||
exit 1
|
||||
fi
|
||||
echo "All CI jobs passed!"
|
||||
env:
|
||||
NEEDS_LINT_RESULT: '${{ needs.lint.result }}'
|
||||
NEEDS_LINK_CHECKER_RESULT: '${{ needs.link_checker.result }}'
|
||||
NEEDS_TEST_LINUX_RESULT: '${{ needs.test_linux.result }}'
|
||||
NEEDS_TEST_MAC_RESULT: '${{ needs.test_mac.result }}'
|
||||
NEEDS_TEST_WINDOWS_RESULT: '${{ needs.test_windows.result }}'
|
||||
NEEDS_CODEQL_RESULT: '${{ needs.codeql.result }}'
|
||||
NEEDS_BUNDLE_SIZE_RESULT: '${{ needs.bundle_size.result }}'
|
||||
|
||||
@@ -27,7 +27,6 @@ jobs:
|
||||
deflake_e2e_linux:
|
||||
name: 'E2E Test (Linux) - ${{ matrix.sandbox }}'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -69,16 +68,15 @@ jobs:
|
||||
VERBOSE: 'true'
|
||||
shell: 'bash'
|
||||
run: |
|
||||
if [[ "${IS_DOCKER}" == "true" ]]; then
|
||||
npm run deflake:test:integration:sandbox:docker -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
|
||||
if [[ "${{ env.IS_DOCKER }}" == "true" ]]; then
|
||||
npm run deflake:test:integration:sandbox:docker -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
else
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
fi
|
||||
|
||||
deflake_e2e_mac:
|
||||
name: 'E2E Test (macOS)'
|
||||
runs-on: 'macos-latest'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
@@ -111,12 +109,12 @@ jobs:
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
VERBOSE: 'true'
|
||||
run: |
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${RUNS}" -- --testNamePattern "'${TEST_NAME_PATTERN}'"
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
|
||||
deflake_e2e_windows:
|
||||
name: 'Slow E2E - Win'
|
||||
runs-on: 'gemini-cli-windows-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
@@ -169,4 +167,4 @@ jobs:
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
shell: 'pwsh'
|
||||
run: |
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="$env:RUNS" -- --testNamePattern "'$env:TEST_NAME_PATTERN'"
|
||||
npm run deflake:test:integration:sandbox:none -- --runs="${{ env.RUNS }}" -- --testNamePattern "'${{ env.TEST_NAME_PATTERN }}'"
|
||||
|
||||
@@ -19,7 +19,8 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && !contains(github.ref_name, 'nightly')"
|
||||
if: |-
|
||||
${{ !contains(github.ref_name, 'nightly') }}
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
@@ -38,7 +39,6 @@ jobs:
|
||||
uses: 'actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa' # ratchet:actions/upload-pages-artifact@v3
|
||||
|
||||
deploy:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
environment:
|
||||
name: 'github-pages'
|
||||
url: '${{ steps.deployment.outputs.page_url }}'
|
||||
|
||||
@@ -7,7 +7,6 @@ on:
|
||||
- 'docs/**'
|
||||
jobs:
|
||||
trigger-rebuild:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Trigger rebuild'
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
name: 'Evals: PR Evaluation & Regression'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: ['opened', 'synchronize', 'reopened', 'ready_for_review']
|
||||
paths:
|
||||
- 'packages/core/src/prompts/**'
|
||||
- 'packages/core/src/tools/**'
|
||||
- 'packages/core/src/agents/**'
|
||||
- 'evals/**'
|
||||
- '!**/*.test.ts'
|
||||
- '!**/*.test.tsx'
|
||||
workflow_dispatch:
|
||||
|
||||
# Prevents multiple runs for the same PR simultaneously (saves tokens)
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
pull-requests: 'write'
|
||||
contents: 'read'
|
||||
actions: 'read'
|
||||
|
||||
jobs:
|
||||
pr-evaluation:
|
||||
name: 'Evaluate Steering & Regressions'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository))"
|
||||
# External contributors' PRs will wait for approval in this environment
|
||||
environment: |-
|
||||
${{ (github.event.pull_request.head.repo.full_name == github.repository) && 'internal' || 'external-evals' }}
|
||||
env:
|
||||
# CENTRALIZED MODEL LIST
|
||||
MODEL_LIST: 'gemini-3-flash-preview'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955' # ratchet:actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Detect Steering Changes'
|
||||
id: 'detect'
|
||||
run: |
|
||||
SHOULD_RUN=$(node scripts/changed_prompt.js)
|
||||
STEERING_DETECTED=$(node scripts/changed_prompt.js --steering-only)
|
||||
echo "SHOULD_RUN=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
|
||||
echo "STEERING_DETECTED=$STEERING_DETECTED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Install dependencies'
|
||||
if: "steps.detect.outputs.SHOULD_RUN == 'true'"
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
if: "steps.detect.outputs.SHOULD_RUN == 'true'"
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Analyze PR Content (Guidance)'
|
||||
if: "steps.detect.outputs.STEERING_DETECTED == 'true'"
|
||||
id: 'analysis'
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
# Check for behavioral eval changes
|
||||
EVAL_CHANGES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep "^evals/" || true)
|
||||
if [ -z "$EVAL_CHANGES" ]; then
|
||||
echo "MISSING_EVALS=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Check if user is a maintainer
|
||||
USER_PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission --jq '.permission')
|
||||
if [[ "$USER_PERMISSION" == "admin" || "$USER_PERMISSION" == "write" ]]; then
|
||||
echo "IS_MAINTAINER=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: 'Execute Regression Check'
|
||||
if: "steps.detect.outputs.SHOULD_RUN == 'true'"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
MODEL_LIST: '${{ env.MODEL_LIST }}'
|
||||
run: |
|
||||
# Run the regression check loop. The script saves the report to a file.
|
||||
node scripts/run_eval_regression.js
|
||||
|
||||
# Use the generated report file if it exists
|
||||
if [[ -f eval_regression_report.md ]]; then
|
||||
echo "REPORT_FILE=eval_regression_report.md" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: 'Post or Update PR Comment'
|
||||
if: "always() && (steps.detect.outputs.STEERING_DETECTED == 'true' || env.REPORT_FILE != '')"
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
# 1. Build the full comment body
|
||||
{
|
||||
if [[ -f eval_regression_report.md ]]; then
|
||||
cat eval_regression_report.md
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [[ "${{ steps.detect.outputs.STEERING_DETECTED }}" == "true" ]]; then
|
||||
echo "### 🧠 Model Steering Guidance"
|
||||
echo ""
|
||||
echo "This PR modifies files that affect the model's behavior (prompts, tools, or instructions)."
|
||||
echo ""
|
||||
|
||||
if [[ "${{ steps.analysis.outputs.MISSING_EVALS }}" == "true" ]]; then
|
||||
echo "- ⚠️ **Consider adding Evals:** No behavioral evaluations (\`evals/*.eval.ts\`) were added or updated in this PR. Consider [adding a test case](https://github.com/google-gemini/gemini-cli/blob/main/evals/README.md#creating-an-evaluation) to verify the new behavior and prevent regressions."
|
||||
fi
|
||||
|
||||
if [[ "${{ steps.analysis.outputs.IS_MAINTAINER }}" == "true" ]]; then
|
||||
echo "- 🚀 **Maintainer Reminder:** Please ensure that these changes do not regress results on benchmark evals before merging."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "---"
|
||||
echo "*This is an automated guidance message triggered by steering logic signatures.*"
|
||||
echo "<!-- eval-pr-report -->"
|
||||
} > full_comment.md
|
||||
|
||||
# 2. Find if a comment with our unique tag already exists
|
||||
# We extract the numeric ID from the URL to ensure compatibility with the REST API
|
||||
COMMENT_ID=$(gh pr view ${{ github.event.pull_request.number }} --json comments --jq '.comments[] | select(.body | contains("<!-- eval-pr-report -->")) | .url' | grep -oE "[0-9]+$" | head -n 1)
|
||||
|
||||
# 3. Update or Create the comment
|
||||
if [ -n "$COMMENT_ID" ]; then
|
||||
echo "Updating existing comment $COMMENT_ID via API..."
|
||||
gh api -X PATCH "repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -F body=@full_comment.md
|
||||
else
|
||||
echo "Creating new PR comment..."
|
||||
gh pr comment ${{ github.event.pull_request.number }} --body-file full_comment.md
|
||||
fi
|
||||
@@ -44,5 +44,5 @@ jobs:
|
||||
- name: 'Run evaluation'
|
||||
working-directory: '/app'
|
||||
run: |
|
||||
poetry run exp_run --experiment-mode=on-demand --branch-or-commit="${GITHUB_REF_NAME}" --model-name=gemini-2.5-pro --dataset=swebench_verified --concurrency=15
|
||||
poetry run exp_run --experiment-mode=on-demand --branch-or-commit=${{ github.ref_name }} --model-name=gemini-2.5-pro --dataset=swebench_verified --concurrency=15
|
||||
poetry run python agent_prototypes/scripts/parse_gcli_logs_experiment.py --experiment_dir=experiments/adhoc/gcli_temp_exp --gcs-bucket="${EVAL_GCS_BUCKET}" --gcs-path=gh_action_artifacts
|
||||
|
||||
@@ -23,7 +23,6 @@ jobs:
|
||||
evals:
|
||||
name: 'Evals (USUALLY_PASSING) nightly run'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -61,12 +60,9 @@ jobs:
|
||||
GEMINI_MODEL: '${{ matrix.model }}'
|
||||
RUN_EVALS: "${{ github.event.inputs.run_all != 'false' }}"
|
||||
TEST_NAME_PATTERN: '${{ github.event.inputs.test_name_pattern }}'
|
||||
# Disable Vitest internal retries to avoid double-retrying;
|
||||
# custom retry logic is handled in evals/test-helper.ts
|
||||
VITEST_RETRY: 0
|
||||
run: |
|
||||
CMD="npm run test:all_evals"
|
||||
PATTERN="${TEST_NAME_PATTERN}"
|
||||
PATTERN="${{ env.TEST_NAME_PATTERN }}"
|
||||
|
||||
if [[ -n "$PATTERN" ]]; then
|
||||
if [[ "$PATTERN" == *.ts || "$PATTERN" == *.js || "$PATTERN" == */* ]]; then
|
||||
@@ -89,7 +85,7 @@ jobs:
|
||||
aggregate-results:
|
||||
name: 'Aggregate Results'
|
||||
needs: ['evals']
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && always()"
|
||||
if: 'always()'
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
|
||||
@@ -121,7 +121,6 @@ jobs:
|
||||
'area/security',
|
||||
'area/platform',
|
||||
'area/extensions',
|
||||
'area/documentation',
|
||||
'area/unknown'
|
||||
];
|
||||
const labelNames = labels.map(label => label.name).filter(name => allowedLabels.includes(name));
|
||||
@@ -159,7 +158,7 @@ jobs:
|
||||
},
|
||||
"coreTools": [
|
||||
"run_shell_command(echo)"
|
||||
]
|
||||
],
|
||||
}
|
||||
prompt: |-
|
||||
## Role
|
||||
@@ -256,14 +255,6 @@ jobs:
|
||||
"Issues with a specific extension."
|
||||
"Feature request for the extension ecosystem."
|
||||
|
||||
area/documentation
|
||||
- Description: Issues related to user-facing documentation and other content on the documentation website.
|
||||
- Example Issues:
|
||||
"A typo in a README file."
|
||||
"DOCS: A command is not working as described in the documentation."
|
||||
"A request for a new documentation page."
|
||||
"Instructions missing for skills feature"
|
||||
|
||||
area/unknown
|
||||
- Description: Issues that do not clearly fit into any other defined area/ category, or where information is too limited to make a determination. Use this when no other area is appropriate.
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
echo '🔍 Finding issues missing area labels...'
|
||||
NO_AREA_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
|
||||
--search 'is:open is:issue -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/documentation -label:area/unknown' --limit 100 --json number,title,body)"
|
||||
--search 'is:open is:issue -label:area/core -label:area/agent -label:area/enterprise -label:area/non-interactive -label:area/security -label:area/platform -label:area/extensions -label:area/unknown' --limit 100 --json number,title,body)"
|
||||
|
||||
echo '🔍 Finding issues missing kind labels...'
|
||||
NO_KIND_ISSUES="$(gh issue list --repo "${GITHUB_REPOSITORY}" \
|
||||
@@ -204,7 +204,6 @@ jobs:
|
||||
Categorization Guidelines (Area):
|
||||
area/agent: Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality
|
||||
area/core: User Interface, OS Support, Core Functionality
|
||||
area/documentation: End-user and contributor-facing documentation, website-related
|
||||
area/enterprise: Telemetry, Policy, Quota / Licensing
|
||||
area/extensions: Gemini CLI extensions capability
|
||||
area/non-interactive: GitHub Actions, SDK, 3P Integrations, Shell Scripting, Command line automation
|
||||
|
||||
@@ -21,21 +21,20 @@ defaults:
|
||||
|
||||
jobs:
|
||||
close-stale-issues:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
|
||||
uses: 'actions/create-github-app-token@v2'
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
permission-issues: 'write'
|
||||
|
||||
- name: 'Process Stale Issues'
|
||||
uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
|
||||
uses: 'actions/github-script@v7'
|
||||
env:
|
||||
DRY_RUN: '${{ inputs.dry_run }}'
|
||||
with:
|
||||
|
||||
@@ -23,25 +23,19 @@ jobs:
|
||||
steps:
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
env:
|
||||
APP_ID: '${{ secrets.APP_ID }}'
|
||||
if: |-
|
||||
${{ env.APP_ID != '' }}
|
||||
uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
|
||||
uses: 'actions/create-github-app-token@v2'
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
|
||||
- name: 'Process Stale PRs'
|
||||
uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
|
||||
uses: 'actions/github-script@v7'
|
||||
env:
|
||||
DRY_RUN: '${{ inputs.dry_run }}'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token || secrets.GITHUB_TOKEN }}'
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |
|
||||
const dryRun = process.env.DRY_RUN === 'true';
|
||||
const fourteenDaysAgo = new Date();
|
||||
fourteenDaysAgo.setDate(fourteenDaysAgo.getDate() - 14);
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
@@ -58,38 +52,48 @@ jobs:
|
||||
for (const m of members) maintainerLogins.add(m.login.toLowerCase());
|
||||
core.info(`Successfully fetched ${members.length} team members from ${team_slug}`);
|
||||
} catch (e) {
|
||||
// Silently skip if permissions are insufficient; we will rely on author_association
|
||||
core.debug(`Skipped team fetch for ${team_slug}: ${e.message}`);
|
||||
core.warning(`Failed to fetch team members from ${team_slug}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const isMaintainer = async (login, assoc) => {
|
||||
// Reliably identify maintainers using authorAssociation (provided by GitHub)
|
||||
// and organization membership (if available).
|
||||
const isTeamMember = maintainerLogins.has(login.toLowerCase());
|
||||
const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
|
||||
const isGooglerCache = new Map();
|
||||
const isGoogler = async (login) => {
|
||||
if (isGooglerCache.has(login)) return isGooglerCache.get(login);
|
||||
|
||||
if (isTeamMember || isRepoMaintainer) return true;
|
||||
|
||||
// Fallback: Check if user belongs to the 'google' or 'googlers' orgs (requires permission)
|
||||
try {
|
||||
// Check membership in 'googlers' or 'google' orgs
|
||||
const orgs = ['googlers', 'google'];
|
||||
for (const org of orgs) {
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({ org: org, username: login });
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: org,
|
||||
username: login
|
||||
});
|
||||
core.info(`User ${login} is a member of ${org} organization.`);
|
||||
isGooglerCache.set(login, true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
// 404 just means they aren't a member, which is fine
|
||||
if (e.status !== 404) throw e;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Gracefully ignore failures here
|
||||
core.warning(`Failed to check org membership for ${login}: ${e.message}`);
|
||||
}
|
||||
|
||||
isGooglerCache.set(login, false);
|
||||
return false;
|
||||
};
|
||||
|
||||
// 2. Fetch all open PRs
|
||||
const isMaintainer = async (login, assoc) => {
|
||||
const isTeamMember = maintainerLogins.has(login.toLowerCase());
|
||||
const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
|
||||
if (isTeamMember || isRepoMaintainer) return true;
|
||||
|
||||
return await isGoogler(login);
|
||||
};
|
||||
|
||||
// 2. Determine which PRs to check
|
||||
let prs = [];
|
||||
if (context.eventName === 'pull_request') {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
@@ -110,77 +114,64 @@ jobs:
|
||||
for (const pr of prs) {
|
||||
const maintainerPr = await isMaintainer(pr.user.login, pr.author_association);
|
||||
const isBot = pr.user.type === 'Bot' || pr.user.login.endsWith('[bot]');
|
||||
if (maintainerPr || isBot) continue;
|
||||
|
||||
// Helper: Fetch labels and linked issues via GraphQL
|
||||
const prDetailsQuery = `query($owner:String!, $repo:String!, $number:Int!) {
|
||||
// Detection Logic for Linked Issues
|
||||
// Check 1: Official GitHub "Closing Issue" link (GraphQL)
|
||||
const linkedIssueQuery = `query($owner:String!, $repo:String!, $number:Int!) {
|
||||
repository(owner:$owner, name:$repo) {
|
||||
pullRequest(number:$number) {
|
||||
closingIssuesReferences(first: 10) {
|
||||
nodes {
|
||||
number
|
||||
labels(first: 20) {
|
||||
nodes { name }
|
||||
}
|
||||
}
|
||||
}
|
||||
closingIssuesReferences(first: 1) { totalCount }
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
let linkedIssues = [];
|
||||
let hasClosingLink = false;
|
||||
try {
|
||||
const res = await github.graphql(prDetailsQuery, {
|
||||
const res = await github.graphql(linkedIssueQuery, {
|
||||
owner: context.repo.owner, repo: context.repo.repo, number: pr.number
|
||||
});
|
||||
linkedIssues = res.repository.pullRequest.closingIssuesReferences.nodes;
|
||||
} catch (e) {
|
||||
core.warning(`GraphQL fetch failed for PR #${pr.number}: ${e.message}`);
|
||||
}
|
||||
hasClosingLink = res.repository.pullRequest.closingIssuesReferences.totalCount > 0;
|
||||
} catch (e) {}
|
||||
|
||||
// Check for mentions in body as fallback (regex)
|
||||
// Check 2: Regex for mentions (e.g., "Related to #123", "Part of #123", "#123")
|
||||
// We check for # followed by numbers or direct URLs to issues.
|
||||
const body = pr.body || '';
|
||||
const mentionRegex = /(?:#|https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/)(\d+)/i;
|
||||
const matches = body.match(mentionRegex);
|
||||
if (matches && linkedIssues.length === 0) {
|
||||
const issueNumber = parseInt(matches[1]);
|
||||
try {
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber
|
||||
});
|
||||
linkedIssues = [{ number: issueNumber, labels: { nodes: issue.labels.map(l => ({ name: l.name })) } }];
|
||||
} catch (e) {}
|
||||
const hasMentionLink = mentionRegex.test(body);
|
||||
|
||||
const hasLinkedIssue = hasClosingLink || hasMentionLink;
|
||||
|
||||
// Logic for Closed PRs (Auto-Reopen)
|
||||
if (pr.state === 'closed' && context.eventName === 'pull_request' && context.payload.action === 'edited') {
|
||||
if (hasLinkedIssue) {
|
||||
core.info(`PR #${pr.number} now has a linked issue. Reopening.`);
|
||||
if (!dryRun) {
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
state: 'open'
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: "Thank you for linking an issue! This pull request has been automatically reopened."
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Enforcement Logic
|
||||
const prLabels = pr.labels.map(l => l.name.toLowerCase());
|
||||
const hasHelpWanted = prLabels.includes('help wanted') ||
|
||||
linkedIssues.some(issue => issue.labels.nodes.some(l => l.name.toLowerCase() === 'help wanted'));
|
||||
|
||||
const hasMaintainerOnly = prLabels.includes('🔒 maintainer only') ||
|
||||
linkedIssues.some(issue => issue.labels.nodes.some(l => l.name.toLowerCase() === '🔒 maintainer only'));
|
||||
|
||||
const hasLinkedIssue = linkedIssues.length > 0;
|
||||
|
||||
// Closure Policy: No help-wanted label = Close after 14 days
|
||||
if (pr.state === 'open' && !hasHelpWanted && !hasMaintainerOnly) {
|
||||
const prCreatedAt = new Date(pr.created_at);
|
||||
|
||||
// We give a 14-day grace period for non-help-wanted PRs to be manually reviewed/labeled by an EM
|
||||
if (prCreatedAt > fourteenDaysAgo) {
|
||||
core.info(`PR #${pr.number} is new and lacks 'help wanted'. Giving 14-day grace period for EM review.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
core.info(`PR #${pr.number} is older than 14 days and lacks 'help wanted' association. Closing.`);
|
||||
// Logic for Open PRs (Immediate Closure)
|
||||
if (pr.state === 'open' && !maintainerPr && !hasLinkedIssue && !isBot) {
|
||||
core.info(`PR #${pr.number} is missing a linked issue. Closing.`);
|
||||
if (!dryRun) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: "Hi there! Thank you for your interest in contributing to Gemini CLI. \n\nTo ensure we maintain high code quality and focus on our prioritized roadmap, we have updated our contribution policy (see [Discussion #17383](https://github.com/google-gemini/gemini-cli/discussions/17383)). \n\n**We only *guarantee* review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.** All other community pull requests are subject to closure after 14 days if they do not align with our current focus areas. For this reason, we strongly recommend that contributors only submit pull requests against issues explicitly labeled as **'help-wanted'**. \n\nThis pull request is being closed as it has been open for 14 days without a 'help wanted' designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding and for being part of our community!"
|
||||
body: "Hi there! Thank you for your contribution to Gemini CLI. \n\nTo improve our contribution process and better track changes, we now require all pull requests to be associated with an existing issue, as announced in our [recent discussion](https://github.com/google-gemini/gemini-cli/discussions/16706) and as detailed in our [CONTRIBUTING.md](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md#1-link-to-an-existing-issue).\n\nThis pull request is being closed because it is not currently linked to an issue. **Once you have updated the description of this PR to link an issue (e.g., by adding `Fixes #123` or `Related to #123`), it will be automatically reopened.**\n\n**How to link an issue:**\nAdd a keyword followed by the issue number (e.g., `Fixes #123`) in the description of your pull request. For more details on supported keywords and how linking works, please refer to the [GitHub Documentation on linking pull requests to issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).\n\nThank you for your understanding and for being a part of our community!"
|
||||
});
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
@@ -192,22 +183,27 @@ jobs:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Also check for linked issue even if it has help wanted (redundant but safe)
|
||||
if (pr.state === 'open' && !hasLinkedIssue) {
|
||||
// Already covered by hasHelpWanted check above, but good for future-proofing
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Staleness Check (Scheduled only)
|
||||
// Staleness check (Scheduled runs only)
|
||||
if (pr.state === 'open' && context.eventName !== 'pull_request') {
|
||||
const labels = pr.labels.map(l => l.name.toLowerCase());
|
||||
if (labels.includes('help wanted') || labels.includes('🔒 maintainer only')) continue;
|
||||
|
||||
// Skip PRs that were created less than 30 days ago - they cannot be stale yet
|
||||
const prCreatedAt = new Date(pr.created_at);
|
||||
if (prCreatedAt > thirtyDaysAgo) continue;
|
||||
if (prCreatedAt > thirtyDaysAgo) {
|
||||
const daysOld = Math.floor((Date.now() - prCreatedAt.getTime()) / (1000 * 60 * 60 * 24));
|
||||
core.info(`PR #${pr.number} was created ${daysOld} days ago. Skipping staleness check.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialize lastActivity to PR creation date (not epoch) as a safety baseline.
|
||||
// This ensures we never incorrectly mark a PR as stale due to failed activity lookups.
|
||||
let lastActivity = new Date(pr.created_at);
|
||||
try {
|
||||
const reviews = await github.paginate(github.rest.pulls.listReviews, {
|
||||
owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number
|
||||
});
|
||||
for (const r of reviews) {
|
||||
if (await isMaintainer(r.user.login, r.author_association)) {
|
||||
@@ -216,7 +212,9 @@ jobs:
|
||||
}
|
||||
}
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number
|
||||
});
|
||||
for (const c of comments) {
|
||||
if (await isMaintainer(c.user.login, c.author_association)) {
|
||||
@@ -224,23 +222,25 @@ jobs:
|
||||
if (d > lastActivity) lastActivity = d;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
core.warning(`Failed to fetch reviews/comments for PR #${pr.number}: ${e.message}`);
|
||||
}
|
||||
|
||||
// For maintainer PRs, the PR creation itself counts as maintainer activity.
|
||||
// (Now redundant since we initialize to pr.created_at, but kept for clarity)
|
||||
if (maintainerPr) {
|
||||
const d = new Date(pr.created_at);
|
||||
if (d > lastActivity) lastActivity = d;
|
||||
}
|
||||
|
||||
if (lastActivity < thirtyDaysAgo) {
|
||||
const labels = pr.labels.map(l => l.name.toLowerCase());
|
||||
const isProtected = labels.includes('help wanted') || labels.includes('🔒 maintainer only');
|
||||
if (isProtected) {
|
||||
core.info(`PR #${pr.number} is stale but has a protected label. Skipping closure.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
core.info(`PR #${pr.number} is stale (no maintainer activity for 30+ days). Closing.`);
|
||||
core.info(`PR #${pr.number} is stale.`);
|
||||
if (!dryRun) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: "Hi there! Thank you for your contribution. To keep our backlog manageable, we are closing pull requests that haven't seen maintainer activity for 30 days. If you're still working on this, please let us know!"
|
||||
body: "Hi there! Thank you for your contribution to Gemini CLI. We really appreciate the time and effort you've put into this pull request.\n\nTo keep our backlog manageable and ensure we're focusing on current priorities, we are closing pull requests that haven't seen maintainer activity for 30 days. Currently, the team is prioritizing work associated with **🔒 maintainer only** or **help wanted** issues.\n\nIf you believe this change is still critical, please feel free to comment with updated details. Otherwise, we encourage contributors to focus on open issues labeled as **help wanted**. Thank you for your understanding!"
|
||||
});
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
if: |-
|
||||
github.repository == 'google-gemini/gemini-cli' &&
|
||||
github.event_name == 'issue_comment' &&
|
||||
(contains(github.event.comment.body, '/assign') || contains(github.event.comment.body, '/unassign'))
|
||||
contains(github.event.comment.body, '/assign')
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Generate GitHub App Token'
|
||||
@@ -38,7 +38,6 @@ jobs:
|
||||
permission-issues: 'write'
|
||||
|
||||
- name: 'Assign issue to user'
|
||||
if: "contains(github.event.comment.body, '/assign')"
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
@@ -109,42 +108,3 @@ jobs:
|
||||
issue_number: issueNumber,
|
||||
body: `👋 @${commenter}, you've been assigned to this issue! Thank you for taking the time to contribute. Make sure to check out our [contributing guidelines](https://github.com/google-gemini/gemini-cli/blob/main/CONTRIBUTING.md).`
|
||||
});
|
||||
|
||||
- name: 'Unassign issue from user'
|
||||
if: "contains(github.event.comment.body, '/unassign')"
|
||||
uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |
|
||||
const issueNumber = context.issue.number;
|
||||
const commenter = context.actor;
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const commentBody = context.payload.comment.body.trim();
|
||||
|
||||
if (commentBody !== '/unassign') {
|
||||
return;
|
||||
}
|
||||
|
||||
const issue = await github.rest.issues.get({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
|
||||
const isAssigned = issue.data.assignees.some(assignee => assignee.login === commenter);
|
||||
|
||||
if (isAssigned) {
|
||||
await github.rest.issues.removeAssignees({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
issue_number: issueNumber,
|
||||
assignees: [commenter]
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
issue_number: issueNumber,
|
||||
body: `👋 @${commenter}, you have been unassigned from this issue.`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,14 +14,14 @@ permissions:
|
||||
jobs:
|
||||
# Event-based: Quick reaction to new/edited issues in THIS repo
|
||||
labeler:
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && github.event_name == 'issues'"
|
||||
if: "github.event_name == 'issues'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
uses: 'actions/checkout@v4'
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
uses: 'actions/setup-node@v4'
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Scheduled/Manual: Recursive sync across multiple repos
|
||||
sync-maintainer-labels:
|
||||
if: "github.repository == 'google-gemini/gemini-cli' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')"
|
||||
if: "github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
uses: 'actions/checkout@v4'
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
uses: 'actions/setup-node@v4'
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
@@ -9,13 +9,12 @@ on:
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Check for Parent Workstream and Apply Label'
|
||||
uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
|
||||
uses: 'actions/github-script@v7'
|
||||
with:
|
||||
script: |
|
||||
const labelToAdd = 'workstream-rollup';
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
APP_ID: '${{ secrets.APP_ID }}'
|
||||
if: |-
|
||||
${{ env.APP_ID != '' }}
|
||||
uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
|
||||
uses: 'actions/create-github-app-token@v2'
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
|
||||
@@ -32,7 +32,6 @@ on:
|
||||
|
||||
jobs:
|
||||
change-tags:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
@@ -40,7 +39,7 @@ jobs:
|
||||
issues: 'write'
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
uses: 'actions/checkout@v4'
|
||||
with:
|
||||
ref: '${{ github.ref }}'
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -47,7 +47,6 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
branch-name: 'release/${{ steps.nightly_version.outputs.RELEASE_TAG }}'
|
||||
pr-title: 'chore/release: bump version to ${{ steps.nightly_version.outputs.RELEASE_VERSION }}'
|
||||
pr-body: 'Automated version bump for nightly release.'
|
||||
github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
dry-run: '${{ steps.vars.outputs.is_dry_run }}'
|
||||
working-directory: './release'
|
||||
|
||||
|
||||
@@ -22,21 +22,20 @@ on:
|
||||
|
||||
jobs:
|
||||
generate-release-notes:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
uses: 'actions/checkout@v4'
|
||||
with:
|
||||
# The user-level skills need to be available to the workflow
|
||||
fetch-depth: 0
|
||||
ref: 'main'
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
uses: 'actions/setup-node@v4'
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
@@ -57,18 +56,7 @@ jobs:
|
||||
GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
BODY: '${{ github.event.inputs.body || github.event.release.body }}'
|
||||
|
||||
- name: 'Validate version'
|
||||
id: 'validate_version'
|
||||
run: |
|
||||
if echo "${{ steps.release_info.outputs.VERSION }}" | grep -q "nightly"; then
|
||||
echo "Nightly release detected. Stopping workflow."
|
||||
echo "CONTINUE=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "CONTINUE=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- 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
|
||||
with:
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
@@ -82,11 +70,8 @@ jobs:
|
||||
|
||||
Execute the release notes generation process using the information provided.
|
||||
|
||||
When you are done, please output your thought process and the steps you took for future debugging purposes.
|
||||
|
||||
- name: 'Create Pull Request'
|
||||
if: "steps.validate_version.outputs.CONTINUE == 'true'"
|
||||
uses: 'peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c' # ratchet:peter-evans/create-pull-request@v6
|
||||
uses: 'peter-evans/create-pull-request@v6'
|
||||
with:
|
||||
token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
commit-message: 'docs(changelog): update for ${{ steps.release_info.outputs.VERSION }}'
|
||||
@@ -95,8 +80,6 @@ jobs:
|
||||
This PR contains the auto-generated changelog for the ${{ steps.release_info.outputs.VERSION }} release.
|
||||
|
||||
Please review and merge.
|
||||
|
||||
Related to #18505
|
||||
branch: 'changelog-${{ steps.release_info.outputs.VERSION }}'
|
||||
base: 'main'
|
||||
team-reviewers: 'gemini-cli-docs, gemini-cli-maintainers'
|
||||
|
||||
@@ -120,9 +120,6 @@ jobs:
|
||||
if (recentRuns.length > 0) {
|
||||
core.setOutput('dispatched_run_urls', recentRuns.map(r => r.html_url).join(','));
|
||||
core.setOutput('dispatched_run_ids', recentRuns.map(r => r.id).join(','));
|
||||
|
||||
const markdownLinks = recentRuns.map(r => `- [View dispatched workflow run](${r.html_url})`).join('\n');
|
||||
core.setOutput('dispatched_run_links', markdownLinks);
|
||||
}
|
||||
|
||||
- name: 'Comment on Failure'
|
||||
@@ -141,19 +138,16 @@ jobs:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
issue-number: '${{ github.event.issue.number }}'
|
||||
body: |
|
||||
🚀 **[Step 1/4] Patch workflow(s) waiting for approval!**
|
||||
✅ **Patch workflow(s) dispatched successfully!**
|
||||
|
||||
**📋 Details:**
|
||||
- **Channels**: `${{ steps.dispatch_patch.outputs.dispatched_channels }}`
|
||||
- **Commit**: `${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}`
|
||||
- **Workflows Created**: ${{ steps.dispatch_patch.outputs.dispatched_run_count }}
|
||||
|
||||
**⏳ Status:** The patch creation workflow has been triggered and is waiting for deployment approval. Please visit the specific workflow links below and approve the runs.
|
||||
|
||||
**🔗 Track Progress:**
|
||||
${{ steps.dispatch_patch.outputs.dispatched_run_links }}
|
||||
- [View patch workflow history](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
- [This trigger workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
- [View patch workflows](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
- [This workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
- name: 'Final Status Comment - Dispatch Success (No URL)'
|
||||
if: "always() && startsWith(github.event.comment.body, '/patch') && steps.dispatch_patch.outcome == 'success' && !steps.dispatch_patch.outputs.dispatched_run_urls"
|
||||
@@ -162,18 +156,16 @@ jobs:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
issue-number: '${{ github.event.issue.number }}'
|
||||
body: |
|
||||
🚀 **[Step 1/4] Patch workflow(s) waiting for approval!**
|
||||
✅ **Patch workflow(s) dispatched successfully!**
|
||||
|
||||
**📋 Details:**
|
||||
- **Channels**: `${{ steps.dispatch_patch.outputs.dispatched_channels }}`
|
||||
- **Commit**: `${{ steps.pr_status.outputs.MERGE_COMMIT_SHA }}`
|
||||
- **Workflows Created**: ${{ steps.dispatch_patch.outputs.dispatched_run_count }}
|
||||
|
||||
**⏳ Status:** The patch creation workflow has been triggered and is waiting for deployment approval. Please visit the workflow history link below and approve the runs.
|
||||
|
||||
**🔗 Track Progress:**
|
||||
- [View patch workflow history](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
- [This trigger workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
- [View patch workflows](https://github.com/${{ github.repository }}/actions/workflows/release-patch-1-create-pr.yml)
|
||||
- [This workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
- name: 'Final Status Comment - Failure'
|
||||
if: "always() && startsWith(github.event.comment.body, '/patch') && (steps.dispatch_patch.outcome == 'failure' || steps.dispatch_patch.outcome == 'cancelled')"
|
||||
@@ -182,7 +174,7 @@ jobs:
|
||||
token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
issue-number: '${{ github.event.issue.number }}'
|
||||
body: |
|
||||
❌ **[Step 1/4] Patch workflow dispatch failed!**
|
||||
❌ **Patch workflow dispatch failed!**
|
||||
|
||||
There was an error dispatching the patch creation workflow.
|
||||
|
||||
|
||||
@@ -118,7 +118,6 @@ jobs:
|
||||
ORIGINAL_RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
|
||||
ORIGINAL_RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
|
||||
ORIGINAL_PREVIOUS_TAG: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'
|
||||
VARS_CLI_PACKAGE_NAME: '${{ vars.CLI_PACKAGE_NAME }}'
|
||||
run: |
|
||||
echo "🔍 Verifying no concurrent patch releases have occurred..."
|
||||
|
||||
@@ -130,7 +129,7 @@ jobs:
|
||||
|
||||
# Re-run the same version calculation script
|
||||
echo "Re-calculating version to check for changes..."
|
||||
CURRENT_PATCH_JSON=$(node scripts/get-release-version.js --cli-package-name="${VARS_CLI_PACKAGE_NAME}" --type=patch --patch-from="${CHANNEL}")
|
||||
CURRENT_PATCH_JSON=$(node scripts/get-release-version.js --cli-package-name="${{vars.CLI_PACKAGE_NAME}}" --type=patch --patch-from="${CHANNEL}")
|
||||
CURRENT_RELEASE_VERSION=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseVersion)
|
||||
CURRENT_RELEASE_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .releaseTag)
|
||||
CURRENT_PREVIOUS_TAG=$(echo "${CURRENT_PATCH_JSON}" | jq -r .previousReleaseTag)
|
||||
@@ -163,15 +162,10 @@ jobs:
|
||||
- name: 'Print Calculated Version'
|
||||
run: |-
|
||||
echo "Patch Release Summary:"
|
||||
echo " Release Version: ${STEPS_PATCH_VERSION_OUTPUTS_RELEASE_VERSION}"
|
||||
echo " Release Tag: ${STEPS_PATCH_VERSION_OUTPUTS_RELEASE_TAG}"
|
||||
echo " NPM Tag: ${STEPS_PATCH_VERSION_OUTPUTS_NPM_TAG}"
|
||||
echo " Previous Tag: ${STEPS_PATCH_VERSION_OUTPUTS_PREVIOUS_TAG}"
|
||||
env:
|
||||
STEPS_PATCH_VERSION_OUTPUTS_RELEASE_VERSION: '${{ steps.patch_version.outputs.RELEASE_VERSION }}'
|
||||
STEPS_PATCH_VERSION_OUTPUTS_RELEASE_TAG: '${{ steps.patch_version.outputs.RELEASE_TAG }}'
|
||||
STEPS_PATCH_VERSION_OUTPUTS_NPM_TAG: '${{ steps.patch_version.outputs.NPM_TAG }}'
|
||||
STEPS_PATCH_VERSION_OUTPUTS_PREVIOUS_TAG: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'
|
||||
echo " Release Version: ${{ steps.patch_version.outputs.RELEASE_VERSION }}"
|
||||
echo " Release Tag: ${{ steps.patch_version.outputs.RELEASE_TAG }}"
|
||||
echo " NPM Tag: ${{ steps.patch_version.outputs.NPM_TAG }}"
|
||||
echo " Previous Tag: ${{ steps.patch_version.outputs.PREVIOUS_TAG }}"
|
||||
|
||||
- name: 'Run Tests'
|
||||
if: "${{github.event.inputs.force_skip_tests != 'true'}}"
|
||||
|
||||
@@ -335,7 +335,6 @@ jobs:
|
||||
name: 'Create Nightly PR'
|
||||
needs: ['publish-stable', 'calculate-versions']
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
permissions:
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
@@ -363,28 +362,23 @@ jobs:
|
||||
- name: 'Create and switch to a new branch'
|
||||
id: 'release_branch'
|
||||
run: |
|
||||
BRANCH_NAME="chore/nightly-version-bump-${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"
|
||||
BRANCH_NAME="chore/nightly-version-bump-${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}"
|
||||
git switch -c "${BRANCH_NAME}"
|
||||
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
|
||||
env:
|
||||
NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: '${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
|
||||
- name: 'Update package versions'
|
||||
run: 'npm run release:version "${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"'
|
||||
env:
|
||||
NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: '${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
run: 'npm run release:version "${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}"'
|
||||
|
||||
- name: 'Commit and Push package versions'
|
||||
env:
|
||||
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
DRY_RUN: '${{ github.event.inputs.dry_run }}'
|
||||
NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION: '${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
run: |-
|
||||
git add package.json packages/*/package.json
|
||||
if [ -f package-lock.json ]; then
|
||||
git add package-lock.json
|
||||
fi
|
||||
git commit -m "chore(release): bump version to ${NEEDS_CALCULATE_VERSIONS_OUTPUTS_NEXT_NIGHTLY_VERSION}"
|
||||
git commit -m "chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}"
|
||||
if [[ "${DRY_RUN}" == "false" ]]; then
|
||||
echo "Pushing release branch to remote..."
|
||||
git push --set-upstream origin "${BRANCH_NAME}"
|
||||
@@ -398,7 +392,7 @@ jobs:
|
||||
branch-name: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
|
||||
pr-title: 'chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
|
||||
pr-body: 'Automated version bump to prepare for the next nightly release.'
|
||||
github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
dry-run: '${{ github.event.inputs.dry_run }}'
|
||||
|
||||
- name: 'Create Issue on Failure'
|
||||
|
||||
@@ -42,7 +42,6 @@ on:
|
||||
|
||||
jobs:
|
||||
change-tags:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
@@ -204,7 +203,7 @@ jobs:
|
||||
run: |
|
||||
ROLLBACK_COMMIT=$(git rev-parse -q --verify "$TARGET_TAG")
|
||||
if [ "$ROLLBACK_COMMIT" != "$TARGET_HASH" ]; then
|
||||
echo "❌ Failed to add tag ${TARGET_TAG} to commit ${TARGET_HASH}"
|
||||
echo '❌ Failed to add tag $TARGET_TAG to commit $TARGET_HASH'
|
||||
echo '❌ This means the tag was not added, and the workflow should fail.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -16,7 +16,6 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
@@ -20,7 +20,6 @@ on:
|
||||
|
||||
jobs:
|
||||
smoke-test:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
name: 'Test Build Binary'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
build-node-binary:
|
||||
name: 'Build Binary (${{ matrix.os }})'
|
||||
runs-on: '${{ matrix.os }}'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: 'ubuntu-latest'
|
||||
platform_name: 'linux-x64'
|
||||
arch: 'x64'
|
||||
- os: 'windows-latest'
|
||||
platform_name: 'win32-x64'
|
||||
arch: 'x64'
|
||||
- os: 'macos-latest' # Apple Silicon (ARM64)
|
||||
platform_name: 'darwin-arm64'
|
||||
arch: 'arm64'
|
||||
- os: 'macos-latest' # Intel (x64) running on ARM via Rosetta
|
||||
platform_name: 'darwin-x64'
|
||||
arch: 'x64'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5' # ratchet:actions/checkout@v4
|
||||
|
||||
- name: 'Optimize Windows Performance'
|
||||
if: "matrix.os == 'windows-latest'"
|
||||
run: |
|
||||
Set-MpPreference -DisableRealtimeMonitoring $true
|
||||
Stop-Service -Name "wsearch" -Force -ErrorAction SilentlyContinue
|
||||
Set-Service -Name "wsearch" -StartupType Disabled
|
||||
Stop-Service -Name "SysMain" -Force -ErrorAction SilentlyContinue
|
||||
Set-Service -Name "SysMain" -StartupType Disabled
|
||||
shell: 'powershell'
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
architecture: '${{ matrix.arch }}'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Check Secrets'
|
||||
id: 'check_secrets'
|
||||
run: |
|
||||
echo "has_win_cert=${{ secrets.WINDOWS_PFX_BASE64 != '' }}" >> "$GITHUB_OUTPUT"
|
||||
echo "has_mac_cert=${{ secrets.MACOS_CERT_P12_BASE64 != '' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Setup Windows SDK (Windows)'
|
||||
if: "matrix.os == 'windows-latest'"
|
||||
uses: 'microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce' # ratchet:microsoft/setup-msbuild@v2
|
||||
|
||||
- name: 'Add Signtool to Path (Windows)'
|
||||
if: "matrix.os == 'windows-latest'"
|
||||
run: |
|
||||
$signtoolPath = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin" -Recurse -Filter "signtool.exe" | Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty DirectoryName
|
||||
echo "Found signtool at: $signtoolPath"
|
||||
echo "$signtoolPath" >> $env:GITHUB_PATH
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Setup macOS Keychain'
|
||||
if: "startsWith(matrix.os, 'macos') && steps.check_secrets.outputs.has_mac_cert == 'true' && github.event_name != 'pull_request'"
|
||||
env:
|
||||
BUILD_CERTIFICATE_BASE64: '${{ secrets.MACOS_CERT_P12_BASE64 }}'
|
||||
P12_PASSWORD: '${{ secrets.MACOS_CERT_PASSWORD }}'
|
||||
KEYCHAIN_PASSWORD: 'temp-password'
|
||||
run: |
|
||||
# Create the P12 file
|
||||
echo "$BUILD_CERTIFICATE_BASE64" | base64 --decode > certificate.p12
|
||||
|
||||
# Create a temporary keychain
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
|
||||
# Import the certificate
|
||||
security import certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
|
||||
|
||||
# Allow codesign to access it
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
|
||||
# Set Identity for build script
|
||||
echo "APPLE_IDENTITY=${{ secrets.MACOS_CERT_IDENTITY }}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: 'Setup Windows Certificate'
|
||||
if: "matrix.os == 'windows-latest' && steps.check_secrets.outputs.has_win_cert == 'true' && github.event_name != 'pull_request'"
|
||||
env:
|
||||
PFX_BASE64: '${{ secrets.WINDOWS_PFX_BASE64 }}'
|
||||
PFX_PASSWORD: '${{ secrets.WINDOWS_PFX_PASSWORD }}'
|
||||
run: |
|
||||
$pfx_cert_byte = [System.Convert]::FromBase64String("$env:PFX_BASE64")
|
||||
$certPath = Join-Path (Get-Location) "cert.pfx"
|
||||
[IO.File]::WriteAllBytes($certPath, $pfx_cert_byte)
|
||||
echo "WINDOWS_PFX_FILE=$certPath" >> $env:GITHUB_ENV
|
||||
echo "WINDOWS_PFX_PASSWORD=$env:PFX_PASSWORD" >> $env:GITHUB_ENV
|
||||
shell: 'pwsh'
|
||||
|
||||
- name: 'Build Binary'
|
||||
run: 'npm run build:binary'
|
||||
|
||||
- name: 'Build Core Package'
|
||||
run: 'npm run build -w @google/gemini-cli-core'
|
||||
|
||||
- name: 'Verify Output Exists'
|
||||
run: |
|
||||
if [ -f "dist/${{ matrix.platform_name }}/gemini" ]; then
|
||||
echo "Binary found at dist/${{ matrix.platform_name }}/gemini"
|
||||
elif [ -f "dist/${{ matrix.platform_name }}/gemini.exe" ]; then
|
||||
echo "Binary found at dist/${{ matrix.platform_name }}/gemini.exe"
|
||||
else
|
||||
echo "Error: Binary not found in dist/${{ matrix.platform_name }}/"
|
||||
ls -R dist/
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 'Smoke Test Binary'
|
||||
run: |
|
||||
echo "Running binary smoke test..."
|
||||
if [ -f "dist/${{ matrix.platform_name }}/gemini.exe" ]; then
|
||||
"./dist/${{ matrix.platform_name }}/gemini.exe" --version
|
||||
else
|
||||
"./dist/${{ matrix.platform_name }}/gemini" --version
|
||||
fi
|
||||
|
||||
- name: 'Run Integration Tests'
|
||||
if: "github.event_name != 'pull_request'"
|
||||
env:
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
run: |
|
||||
echo "Running integration tests with binary..."
|
||||
if [[ "${{ matrix.os }}" == 'windows-latest' ]]; then
|
||||
BINARY_PATH="$(cygpath -m "$(pwd)/dist/${{ matrix.platform_name }}/gemini.exe")"
|
||||
else
|
||||
BINARY_PATH="$(pwd)/dist/${{ matrix.platform_name }}/gemini"
|
||||
fi
|
||||
echo "Using binary at $BINARY_PATH"
|
||||
export INTEGRATION_TEST_GEMINI_BINARY_PATH="$BINARY_PATH"
|
||||
npm run test:integration:sandbox:none -- --testTimeout=600000
|
||||
|
||||
- name: 'Upload Artifact'
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'gemini-cli-${{ matrix.platform_name }}'
|
||||
path: 'dist/${{ matrix.platform_name }}/'
|
||||
retention-days: 5
|
||||
@@ -15,7 +15,6 @@ on:
|
||||
|
||||
jobs:
|
||||
save_repo_name:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- name: 'Save Repo name'
|
||||
@@ -24,15 +23,14 @@ jobs:
|
||||
HEAD_SHA: '${{ github.event.inputs.head_sha || github.event.pull_request.head.sha }}'
|
||||
run: |
|
||||
mkdir -p ./pr
|
||||
echo "${REPO_NAME}" > ./pr/repo_name
|
||||
echo "${HEAD_SHA}" > ./pr/head_sha
|
||||
echo '${{ env.REPO_NAME }}' > ./pr/repo_name
|
||||
echo '${{ env.HEAD_SHA }}' > ./pr/head_sha
|
||||
- uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'repo_name'
|
||||
path: 'pr/'
|
||||
trigger_e2e:
|
||||
name: 'Trigger e2e'
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'gemini-cli-ubuntu-16-core'
|
||||
steps:
|
||||
- id: 'trigger-e2e'
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
name: 'Unassign Inactive Issue Assignees'
|
||||
|
||||
# This workflow runs daily and scans every open "help wanted" issue that has
|
||||
# one or more assignees. For each assignee it checks whether they have a
|
||||
# non-draft pull request (open and ready for review, or already merged) that
|
||||
# is linked to the issue. Draft PRs are intentionally excluded so that
|
||||
# contributors cannot reset the check by opening a no-op PR. If no
|
||||
# qualifying PR is found within 7 days of assignment the assignee is
|
||||
# automatically removed and a friendly comment is posted so that other
|
||||
# contributors can pick up the work.
|
||||
# Maintainers, org members, and collaborators (anyone with write access or
|
||||
# above) are always exempted and will never be auto-unassigned.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 9 * * *' # Every day at 09:00 UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Run in dry-run mode (no changes will be applied)'
|
||||
required: false
|
||||
default: false
|
||||
type: 'boolean'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
unassign-inactive-assignees:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
issues: 'write'
|
||||
|
||||
steps:
|
||||
- name: 'Generate GitHub App Token'
|
||||
id: 'generate_token'
|
||||
uses: 'actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349' # ratchet:actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: '${{ secrets.APP_ID }}'
|
||||
private-key: '${{ secrets.PRIVATE_KEY }}'
|
||||
|
||||
- name: 'Unassign inactive assignees'
|
||||
uses: 'actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b' # ratchet:actions/github-script@v7
|
||||
env:
|
||||
DRY_RUN: '${{ inputs.dry_run }}'
|
||||
with:
|
||||
github-token: '${{ steps.generate_token.outputs.token }}'
|
||||
script: |
|
||||
const dryRun = process.env.DRY_RUN === 'true';
|
||||
if (dryRun) {
|
||||
core.info('DRY RUN MODE ENABLED: No changes will be applied.');
|
||||
}
|
||||
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const GRACE_PERIOD_DAYS = 7;
|
||||
const now = new Date();
|
||||
|
||||
let maintainerLogins = new Set();
|
||||
const teams = ['gemini-cli-maintainers', 'gemini-cli-askmode-approvers', 'gemini-cli-docs'];
|
||||
|
||||
for (const team_slug of teams) {
|
||||
try {
|
||||
const members = await github.paginate(github.rest.teams.listMembersInOrg, {
|
||||
org: owner,
|
||||
team_slug,
|
||||
});
|
||||
for (const m of members) maintainerLogins.add(m.login.toLowerCase());
|
||||
core.info(`Fetched ${members.length} members from team ${team_slug}.`);
|
||||
} catch (e) {
|
||||
core.warning(`Could not fetch team ${team_slug}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const isGooglerCache = new Map();
|
||||
const isGoogler = async (login) => {
|
||||
if (isGooglerCache.has(login)) return isGooglerCache.get(login);
|
||||
try {
|
||||
for (const org of ['googlers', 'google']) {
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({ org, username: login });
|
||||
isGooglerCache.set(login, true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(`Could not check org membership for ${login}: ${e.message}`);
|
||||
}
|
||||
isGooglerCache.set(login, false);
|
||||
return false;
|
||||
};
|
||||
|
||||
const permissionCache = new Map();
|
||||
const isPrivilegedUser = async (login) => {
|
||||
if (maintainerLogins.has(login.toLowerCase())) return true;
|
||||
|
||||
if (permissionCache.has(login)) return permissionCache.get(login);
|
||||
|
||||
try {
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner,
|
||||
repo,
|
||||
username: login,
|
||||
});
|
||||
const privileged = ['admin', 'maintain', 'write', 'triage'].includes(data.permission);
|
||||
permissionCache.set(login, privileged);
|
||||
if (privileged) {
|
||||
core.info(` @${login} is a repo collaborator (${data.permission}) — exempt.`);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.status !== 404) {
|
||||
core.warning(`Could not check permission for ${login}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const googler = await isGoogler(login);
|
||||
permissionCache.set(login, googler);
|
||||
return googler;
|
||||
};
|
||||
|
||||
core.info('Fetching open "help wanted" issues with assignees...');
|
||||
|
||||
const issues = await github.paginate(github.rest.issues.listForRepo, {
|
||||
owner,
|
||||
repo,
|
||||
state: 'open',
|
||||
labels: 'help wanted',
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const assignedIssues = issues.filter(
|
||||
(issue) => !issue.pull_request && issue.assignees && issue.assignees.length > 0
|
||||
);
|
||||
|
||||
core.info(`Found ${assignedIssues.length} assigned "help wanted" issues.`);
|
||||
|
||||
let totalUnassigned = 0;
|
||||
|
||||
let timelineEvents = [];
|
||||
try {
|
||||
timelineEvents = await github.paginate(github.rest.issues.listEventsForTimeline, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
per_page: 100,
|
||||
mediaType: { previews: ['mockingbird'] },
|
||||
});
|
||||
} catch (err) {
|
||||
core.warning(`Could not fetch timeline for issue #${issue.number}: ${err.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const assignedAtMap = new Map();
|
||||
|
||||
for (const event of timelineEvents) {
|
||||
if (event.event === 'assigned' && event.assignee) {
|
||||
const login = event.assignee.login.toLowerCase();
|
||||
const at = new Date(event.created_at);
|
||||
assignedAtMap.set(login, at);
|
||||
} else if (event.event === 'unassigned' && event.assignee) {
|
||||
assignedAtMap.delete(event.assignee.login.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
const linkedPRAuthorSet = new Set();
|
||||
const seenPRKeys = new Set();
|
||||
|
||||
for (const event of timelineEvents) {
|
||||
if (
|
||||
event.event !== 'cross-referenced' ||
|
||||
!event.source ||
|
||||
event.source.type !== 'pull_request' ||
|
||||
!event.source.issue ||
|
||||
!event.source.issue.user ||
|
||||
!event.source.issue.number ||
|
||||
!event.source.issue.repository
|
||||
) continue;
|
||||
|
||||
const prOwner = event.source.issue.repository.owner.login;
|
||||
const prRepo = event.source.issue.repository.name;
|
||||
const prNumber = event.source.issue.number;
|
||||
const prAuthor = event.source.issue.user.login.toLowerCase();
|
||||
const prKey = `${prOwner}/${prRepo}#${prNumber}`;
|
||||
|
||||
if (seenPRKeys.has(prKey)) continue;
|
||||
seenPRKeys.add(prKey);
|
||||
|
||||
try {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: prOwner,
|
||||
repo: prRepo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
const isReady = (pr.state === 'open' && !pr.draft) ||
|
||||
(pr.state === 'closed' && pr.merged_at !== null);
|
||||
|
||||
core.info(
|
||||
` PR ${prKey} by @${prAuthor}: ` +
|
||||
`state=${pr.state}, draft=${pr.draft}, merged=${!!pr.merged_at} → ` +
|
||||
(isReady ? 'qualifies' : 'does NOT qualify (draft or closed without merge)')
|
||||
);
|
||||
|
||||
if (isReady) linkedPRAuthorSet.add(prAuthor);
|
||||
} catch (err) {
|
||||
core.warning(`Could not fetch PR ${prKey}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const assigneesToRemove = [];
|
||||
|
||||
for (const assignee of issue.assignees) {
|
||||
const login = assignee.login.toLowerCase();
|
||||
|
||||
if (await isPrivilegedUser(assignee.login)) {
|
||||
core.info(` @${assignee.login}: privileged user — skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const assignedAt = assignedAtMap.get(login);
|
||||
|
||||
if (!assignedAt) {
|
||||
core.warning(
|
||||
`No 'assigned' event found for @${login} on issue #${issue.number}; ` +
|
||||
`falling back to issue creation date (${issue.created_at}).`
|
||||
);
|
||||
assignedAtMap.set(login, new Date(issue.created_at));
|
||||
}
|
||||
const resolvedAssignedAt = assignedAtMap.get(login);
|
||||
|
||||
const daysSinceAssignment = (now - resolvedAssignedAt) / (1000 * 60 * 60 * 24);
|
||||
|
||||
core.info(
|
||||
` @${login}: assigned ${daysSinceAssignment.toFixed(1)} day(s) ago, ` +
|
||||
`ready-for-review PR: ${linkedPRAuthorSet.has(login) ? 'yes' : 'no'}`
|
||||
);
|
||||
|
||||
if (daysSinceAssignment < GRACE_PERIOD_DAYS) {
|
||||
core.info(` → within grace period, skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (linkedPRAuthorSet.has(login)) {
|
||||
core.info(` → ready-for-review PR found, keeping assignment.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
core.info(` → no ready-for-review PR after ${GRACE_PERIOD_DAYS} days, will unassign.`);
|
||||
assigneesToRemove.push(assignee.login);
|
||||
}
|
||||
|
||||
if (assigneesToRemove.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
try {
|
||||
await github.rest.issues.removeAssignees({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
assignees: assigneesToRemove,
|
||||
});
|
||||
} catch (err) {
|
||||
core.warning(
|
||||
`Failed to unassign ${assigneesToRemove.join(', ')} from issue #${issue.number}: ${err.message}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const mentionList = assigneesToRemove.map((l) => `@${l}`).join(', ');
|
||||
const commentBody =
|
||||
`👋 ${mentionList} — it has been more than ${GRACE_PERIOD_DAYS} days since ` +
|
||||
`you were assigned to this issue and we could not find a pull request ` +
|
||||
`ready for review.\n\n` +
|
||||
`To keep the backlog moving and ensure issues stay accessible to all ` +
|
||||
`contributors, we require a PR that is open and ready for review (not a ` +
|
||||
`draft) within ${GRACE_PERIOD_DAYS} days of assignment.\n\n` +
|
||||
`We are automatically unassigning you so that other contributors can pick ` +
|
||||
`this up. If you are still actively working on this, please:\n` +
|
||||
`1. Re-assign yourself by commenting \`/assign\`.\n` +
|
||||
`2. Open a PR (not a draft) linked to this issue (e.g. \`Fixes #${issue.number}\`) ` +
|
||||
`within ${GRACE_PERIOD_DAYS} days so the automation knows real progress is being made.\n\n` +
|
||||
`Thank you for your contribution — we hope to see a PR from you soon! 🙏`;
|
||||
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
body: commentBody,
|
||||
});
|
||||
} catch (err) {
|
||||
core.warning(
|
||||
`Failed to post comment on issue #${issue.number}: ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
totalUnassigned += assigneesToRemove.length;
|
||||
core.info(
|
||||
` ${dryRun ? '[DRY RUN] Would have unassigned' : 'Unassigned'}: ${assigneesToRemove.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
core.info(`\nDone. Total assignees ${dryRun ? 'that would be' : ''} unassigned: ${totalUnassigned}`);
|
||||
@@ -28,7 +28,6 @@ on:
|
||||
|
||||
jobs:
|
||||
verify-release:
|
||||
if: "github.repository == 'google-gemini/gemini-cli'"
|
||||
environment: "${{ github.event.inputs.environment || 'prod' }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -62,5 +62,3 @@ gemini-debug.log
|
||||
.gemini-clipboard/
|
||||
.eslintcache
|
||||
evals/logs/
|
||||
|
||||
temp_agents/
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
|
||||
@@ -60,54 +60,26 @@ All submissions, including submissions by project members, require review. We
|
||||
use [GitHub pull requests](https://docs.github.com/articles/about-pull-requests)
|
||||
for this purpose.
|
||||
|
||||
To assist with the review process, we provide an automated review tool that
|
||||
helps detect common anti-patterns, testing issues, and other best practices that
|
||||
are easy to miss.
|
||||
If your pull request involves changes to `packages/cli` (the frontend), we
|
||||
recommend running our automated frontend review tool. **Note: This tool is
|
||||
currently experimental.** It helps detect common React anti-patterns, testing
|
||||
issues, and other frontend-specific best practices that are easy to miss.
|
||||
|
||||
#### Using the automated review tool
|
||||
To run the review tool, enter the following command from within Gemini CLI:
|
||||
|
||||
You can run the review tool in two ways:
|
||||
```text
|
||||
/review-frontend <PR_NUMBER>
|
||||
```
|
||||
|
||||
1. **Using the helper script (Recommended):** We provide a script that
|
||||
automatically handles checking out the PR into a separate worktree,
|
||||
installing dependencies, building the project, and launching the review
|
||||
tool.
|
||||
Replace `<PR_NUMBER>` with your pull request number. Authors are encouraged to
|
||||
run this on their own PRs for self-review, and reviewers should use it to
|
||||
augment their manual review process.
|
||||
|
||||
```bash
|
||||
./scripts/review.sh <PR_NUMBER> [model]
|
||||
```
|
||||
### Self assigning issues
|
||||
|
||||
**Warning:** If you run `scripts/review.sh`, you must have first verified
|
||||
that the code for the PR being reviewed is safe to run and does not contain
|
||||
data exfiltration attacks.
|
||||
|
||||
**Authors are strongly encouraged to run this script on their own PRs**
|
||||
immediately after creation. This allows you to catch and fix simple issues
|
||||
locally before a maintainer performs a full review.
|
||||
|
||||
**Note on Models:** By default, the script uses the latest Pro model
|
||||
(`gemini-3.1-pro-preview`). If you do not have enough Pro quota, you can run
|
||||
it with the latest Flash model instead:
|
||||
`./scripts/review.sh <PR_NUMBER> gemini-3-flash-preview`.
|
||||
|
||||
2. **Manually from within Gemini CLI:** If you already have the PR checked out
|
||||
and built, you can run the tool directly from the CLI prompt:
|
||||
|
||||
```text
|
||||
/review-frontend <PR_NUMBER>
|
||||
```
|
||||
|
||||
Replace `<PR_NUMBER>` with your pull request number. Reviewers should use this
|
||||
tool to augment, not replace, their manual review process.
|
||||
|
||||
### Self-assigning and unassigning issues
|
||||
|
||||
To assign an issue to yourself, simply add a comment with the text `/assign`. To
|
||||
unassign yourself from an issue, add a comment with the text `/unassign`.
|
||||
|
||||
The comment must contain only that text and nothing else. These commands will
|
||||
assign or unassign the issue as requested, provided the conditions are met
|
||||
(e.g., an issue must be unassigned to be assigned).
|
||||
To assign an issue to yourself, simply add a comment with the text `/assign`.
|
||||
The comment must contain only that text and nothing else. This command will
|
||||
assign the issue to you, provided it is not already assigned.
|
||||
|
||||
Please note that you can have a maximum of 3 issues assigned to you at any given
|
||||
time.
|
||||
@@ -292,8 +264,7 @@ npm run test:e2e
|
||||
```
|
||||
|
||||
For more detailed information on the integration testing framework, please see
|
||||
the
|
||||
[Integration Tests documentation](https://geminicli.com/docs/integration-tests).
|
||||
the [Integration Tests documentation](/docs/integration-tests.md).
|
||||
|
||||
### Linting and preflight checks
|
||||
|
||||
@@ -323,8 +294,8 @@ fi
|
||||
|
||||
#### Formatting
|
||||
|
||||
To separately format the code in this project, run the following command from
|
||||
the root directory:
|
||||
To separately format the code in this project by running the following command
|
||||
from the root directory:
|
||||
|
||||
```bash
|
||||
npm run format
|
||||
@@ -354,6 +325,21 @@ npm run lint
|
||||
- **Imports:** Pay special attention to import paths. The project uses ESLint to
|
||||
enforce restrictions on relative imports between packages.
|
||||
|
||||
### Project structure
|
||||
|
||||
- `packages/`: Contains the individual sub-packages of the project.
|
||||
- `a2a-server`: A2A server implementation for the Gemini CLI. (Experimental)
|
||||
- `cli/`: The command-line interface.
|
||||
- `core/`: The core backend logic for the Gemini CLI.
|
||||
- `test-utils` Utilities for creating and cleaning temporary file systems for
|
||||
testing.
|
||||
- `vscode-ide-companion/`: The Gemini CLI Companion extension pairs with
|
||||
Gemini CLI.
|
||||
- `docs/`: Contains all project documentation.
|
||||
- `scripts/`: Utility scripts for building, testing, and development tasks.
|
||||
|
||||
For more detailed architecture, see `docs/architecture.md`.
|
||||
|
||||
### Debugging
|
||||
|
||||
#### VS Code
|
||||
@@ -507,9 +493,8 @@ code.
|
||||
|
||||
### Documentation structure
|
||||
|
||||
Our documentation is organized using
|
||||
[sidebar.json](https://github.com/google-gemini/gemini-cli/blob/main/docs/sidebar.json)
|
||||
as the table of contents. When adding new documentation:
|
||||
Our documentation is organized using [sidebar.json](/docs/sidebar.json) as the
|
||||
table of contents. When adding new documentation:
|
||||
|
||||
1. Create your markdown file **in the appropriate directory** under `/docs`.
|
||||
2. Add an entry to `sidebar.json` in the relevant section.
|
||||
@@ -560,7 +545,7 @@ Before submitting your documentation pull request, please:
|
||||
|
||||
If you have questions about contributing documentation:
|
||||
|
||||
- Check our [FAQ](https://geminicli.com/docs/resources/faq).
|
||||
- Check our [FAQ](/docs/resources/faq.md).
|
||||
- Review existing documentation for examples.
|
||||
- Open [an issue](https://github.com/google-gemini/gemini-cli/issues) to discuss
|
||||
your proposed changes.
|
||||
|
||||
@@ -22,10 +22,9 @@ powerful tool for developers.
|
||||
rendering.
|
||||
- `packages/core`: Backend logic, Gemini API orchestration, prompt
|
||||
construction, and tool execution.
|
||||
- `packages/core/src/tools/`: Built-in tools for file system, shell, and web
|
||||
operations.
|
||||
- `packages/a2a-server`: Experimental Agent-to-Agent server.
|
||||
- `packages/sdk`: Programmatic SDK for embedding Gemini CLI capabilities.
|
||||
- `packages/devtools`: Integrated developer tools (Network/Console inspector).
|
||||
- `packages/test-utils`: Shared test utilities and test rig.
|
||||
- `packages/vscode-ide-companion`: VS Code extension pairing with the CLI.
|
||||
|
||||
## Building and Running
|
||||
@@ -59,6 +58,10 @@ powerful tool for developers.
|
||||
|
||||
## Development Conventions
|
||||
|
||||
- **Legacy Snippets:** `packages/core/src/prompts/snippets.legacy.ts` is a
|
||||
snapshot of an older system prompt. Avoid changing the prompting verbiage to
|
||||
preserve its historical behavior; however, structural changes to ensure
|
||||
compilation or simplify the code are permitted.
|
||||
- **Contributions:** Follow the process outlined in `CONTRIBUTING.md`. Requires
|
||||
signing the Google CLA.
|
||||
- **Pull Requests:** Keep PRs small, focused, and linked to an existing issue.
|
||||
@@ -66,6 +69,8 @@ powerful tool for developers.
|
||||
`gh` CLI.
|
||||
- **Commit Messages:** Follow the
|
||||
[Conventional Commits](https://www.conventionalcommits.org/) standard.
|
||||
- **Coding Style:** Adhere to existing patterns in `packages/cli` (React/Ink)
|
||||
and `packages/core` (Backend logic).
|
||||
- **Imports:** Use specific imports and avoid restricted relative imports
|
||||
between packages (enforced by ESLint).
|
||||
- **License Headers:** For all new source code files (`.ts`, `.tsx`, `.js`),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[](https://github.com/google-gemini/gemini-cli/blob/main/LICENSE)
|
||||
[](https://codewiki.google/github.com/google-gemini/gemini-cli?utm_source=badge&utm_medium=github&utm_campaign=github.com/google-gemini/gemini-cli)
|
||||
|
||||

|
||||

|
||||
|
||||
Gemini CLI is an open-source AI agent that brings the power of Gemini directly
|
||||
into your terminal. It provides lightweight access to Gemini, giving you the
|
||||
@@ -30,7 +30,7 @@ Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).
|
||||
## 📦 Installation
|
||||
|
||||
See
|
||||
[Gemini CLI installation, execution, and releases](https://www.geminicli.com/docs/get-started/installation)
|
||||
[Gemini CLI installation, execution, and releases](./docs/get-started/installation.md)
|
||||
for recommended system specifications and a detailed installation guide.
|
||||
|
||||
### Quick Install
|
||||
@@ -71,13 +71,13 @@ conda activate gemini_env
|
||||
npm install -g @google/gemini-cli
|
||||
```
|
||||
|
||||
## Release Channels
|
||||
## Release Cadence and Tags
|
||||
|
||||
See [Releases](https://www.geminicli.com/docs/changelogs) for more details.
|
||||
See [Releases](./docs/releases.md) for more details.
|
||||
|
||||
### Preview
|
||||
|
||||
New preview releases will be published each week at UTC 23:59 on Tuesdays. These
|
||||
New preview releases will be published each week at UTC 2359 on Tuesdays. These
|
||||
releases will not have been fully vetted and may contain regressions or other
|
||||
outstanding issues. Please help us test and install with `preview` tag.
|
||||
|
||||
@@ -87,7 +87,7 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
### Stable
|
||||
|
||||
- New stable releases will be published each week at UTC 20:00 on Tuesdays, this
|
||||
- New stable releases will be published each week at UTC 2000 on Tuesdays, this
|
||||
will be the full promotion of last week's `preview` release + any bug fixes
|
||||
and validations. Use `latest` tag.
|
||||
|
||||
@@ -97,7 +97,7 @@ npm install -g @google/gemini-cli@latest
|
||||
|
||||
### Nightly
|
||||
|
||||
- New releases will be published each day at UTC 00:00. This will be all changes
|
||||
- New releases will be published each day at UTC 0000. This will be all changes
|
||||
from the main branch as represented at time of release. It should be assumed
|
||||
there are pending validations and issues. Use `nightly` tag.
|
||||
|
||||
@@ -147,7 +147,7 @@ Integrate Gemini CLI directly into your GitHub workflows with
|
||||
|
||||
Choose the authentication method that best fits your needs:
|
||||
|
||||
### Option 1: Sign in with Google (OAuth login using your Google Account)
|
||||
### Option 1: Login with Google (OAuth login using your Google Account)
|
||||
|
||||
**✨ Best for:** Individual developers as well as anyone who has a Gemini Code
|
||||
Assist License. (see
|
||||
@@ -161,7 +161,7 @@ for details)
|
||||
- **No API key management** - just sign in with your Google account
|
||||
- **Automatic updates** to latest models
|
||||
|
||||
#### Start Gemini CLI, then choose _Sign in with Google_ and follow the browser authentication flow when prompted
|
||||
#### Start Gemini CLI, then choose _Login with Google_ and follow the browser authentication flow when prompted
|
||||
|
||||
```bash
|
||||
gemini
|
||||
@@ -209,7 +209,7 @@ gemini
|
||||
```
|
||||
|
||||
For Google Workspace accounts and other authentication methods, see the
|
||||
[authentication guide](https://www.geminicli.com/docs/get-started/authentication).
|
||||
[authentication guide](./docs/get-started/authentication.md).
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
@@ -278,64 +278,60 @@ gemini
|
||||
|
||||
### Getting Started
|
||||
|
||||
- [**Quickstart Guide**](https://www.geminicli.com/docs/get-started) - Get up
|
||||
and running quickly.
|
||||
- [**Authentication Setup**](https://www.geminicli.com/docs/get-started/authentication) -
|
||||
Detailed auth configuration.
|
||||
- [**Configuration Guide**](https://www.geminicli.com/docs/reference/configuration) -
|
||||
Settings and customization.
|
||||
- [**Keyboard Shortcuts**](https://www.geminicli.com/docs/reference/keyboard-shortcuts) -
|
||||
Productivity tips.
|
||||
- [**Quickstart Guide**](./docs/get-started/index.md) - Get up and running
|
||||
quickly.
|
||||
- [**Authentication Setup**](./docs/get-started/authentication.md) - Detailed
|
||||
auth configuration.
|
||||
- [**Configuration Guide**](./docs/get-started/configuration.md) - Settings and
|
||||
customization.
|
||||
- [**Keyboard Shortcuts**](./docs/cli/keyboard-shortcuts.md) - Productivity
|
||||
tips.
|
||||
|
||||
### Core Features
|
||||
|
||||
- [**Commands Reference**](https://www.geminicli.com/docs/reference/commands) -
|
||||
All slash commands (`/help`, `/chat`, etc).
|
||||
- [**Custom Commands**](https://www.geminicli.com/docs/cli/custom-commands) -
|
||||
Create your own reusable commands.
|
||||
- [**Context Files (GEMINI.md)**](https://www.geminicli.com/docs/cli/gemini-md) -
|
||||
Provide persistent context to Gemini CLI.
|
||||
- [**Checkpointing**](https://www.geminicli.com/docs/cli/checkpointing) - Save
|
||||
and resume conversations.
|
||||
- [**Token Caching**](https://www.geminicli.com/docs/cli/token-caching) -
|
||||
Optimize token usage.
|
||||
- [**Commands Reference**](./docs/cli/commands.md) - All slash commands
|
||||
(`/help`, `/chat`, etc).
|
||||
- [**Custom Commands**](./docs/cli/custom-commands.md) - Create your own
|
||||
reusable commands.
|
||||
- [**Context Files (GEMINI.md)**](./docs/cli/gemini-md.md) - Provide persistent
|
||||
context to Gemini CLI.
|
||||
- [**Checkpointing**](./docs/cli/checkpointing.md) - Save and resume
|
||||
conversations.
|
||||
- [**Token Caching**](./docs/cli/token-caching.md) - Optimize token usage.
|
||||
|
||||
### Tools & Extensions
|
||||
|
||||
- [**Built-in Tools Overview**](https://www.geminicli.com/docs/reference/tools)
|
||||
- [File System Operations](https://www.geminicli.com/docs/tools/file-system)
|
||||
- [Shell Commands](https://www.geminicli.com/docs/tools/shell)
|
||||
- [Web Fetch & Search](https://www.geminicli.com/docs/tools/web-fetch)
|
||||
- [**MCP Server Integration**](https://www.geminicli.com/docs/tools/mcp-server) -
|
||||
Extend with custom tools.
|
||||
- [**Custom Extensions**](https://geminicli.com/docs/extensions/writing-extensions) -
|
||||
Build and share your own commands.
|
||||
- [**Built-in Tools Overview**](./docs/tools/index.md)
|
||||
- [File System Operations](./docs/tools/file-system.md)
|
||||
- [Shell Commands](./docs/tools/shell.md)
|
||||
- [Web Fetch & Search](./docs/tools/web-fetch.md)
|
||||
- [**MCP Server Integration**](./docs/tools/mcp-server.md) - Extend with custom
|
||||
tools.
|
||||
- [**Custom Extensions**](./docs/extensions/index.md) - Build and share your own
|
||||
commands.
|
||||
|
||||
### Advanced Topics
|
||||
|
||||
- [**Headless Mode (Scripting)**](https://www.geminicli.com/docs/cli/headless) -
|
||||
Use Gemini CLI in automated workflows.
|
||||
- [**IDE Integration**](https://www.geminicli.com/docs/ide-integration) - VS
|
||||
Code companion.
|
||||
- [**Sandboxing & Security**](https://www.geminicli.com/docs/cli/sandbox) - Safe
|
||||
execution environments.
|
||||
- [**Trusted Folders**](https://www.geminicli.com/docs/cli/trusted-folders) -
|
||||
Control execution policies by folder.
|
||||
- [**Enterprise Guide**](https://www.geminicli.com/docs/cli/enterprise) - Deploy
|
||||
and manage in a corporate environment.
|
||||
- [**Telemetry & Monitoring**](https://www.geminicli.com/docs/cli/telemetry) -
|
||||
Usage tracking.
|
||||
- [**Tools reference**](https://www.geminicli.com/docs/reference/tools) -
|
||||
Built-in tools overview.
|
||||
- [**Local development**](https://www.geminicli.com/docs/local-development) -
|
||||
Local development tooling.
|
||||
- [**Headless Mode (Scripting)**](./docs/cli/headless.md) - Use Gemini CLI in
|
||||
automated workflows.
|
||||
- [**Architecture Overview**](./docs/architecture.md) - How Gemini CLI works.
|
||||
- [**IDE Integration**](./docs/ide-integration/index.md) - VS Code companion.
|
||||
- [**Sandboxing & Security**](./docs/cli/sandbox.md) - Safe execution
|
||||
environments.
|
||||
- [**Trusted Folders**](./docs/cli/trusted-folders.md) - Control execution
|
||||
policies by folder.
|
||||
- [**Enterprise Guide**](./docs/cli/enterprise.md) - Deploy and manage in a
|
||||
corporate environment.
|
||||
- [**Telemetry & Monitoring**](./docs/cli/telemetry.md) - Usage tracking.
|
||||
- [**Tools API Development**](./docs/core/tools-api.md) - Create custom tools.
|
||||
- [**Local development**](./docs/local-development.md) - Local development
|
||||
tooling.
|
||||
|
||||
### Troubleshooting & Support
|
||||
|
||||
- [**Troubleshooting Guide**](https://www.geminicli.com/docs/resources/troubleshooting) -
|
||||
Common issues and solutions.
|
||||
- [**FAQ**](https://www.geminicli.com/docs/resources/faq) - Frequently asked
|
||||
questions.
|
||||
- [**Troubleshooting Guide**](./docs/troubleshooting.md) - Common issues and
|
||||
solutions.
|
||||
- [**FAQ**](./docs/faq.md) - Frequently asked questions.
|
||||
- Use `/bug` command to report issues directly from the CLI.
|
||||
|
||||
### Using MCP Servers
|
||||
@@ -349,9 +345,8 @@ custom tools:
|
||||
> @database Run a query to find inactive users
|
||||
```
|
||||
|
||||
See the
|
||||
[MCP Server Integration guide](https://www.geminicli.com/docs/tools/mcp-server)
|
||||
for setup instructions.
|
||||
See the [MCP Server Integration guide](./docs/tools/mcp-server.md) for setup
|
||||
instructions.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
@@ -372,8 +367,7 @@ for planned features and priorities.
|
||||
## 📖 Resources
|
||||
|
||||
- **[Official Roadmap](./ROADMAP.md)** - See what's coming next.
|
||||
- **[Changelog](https://www.geminicli.com/docs/changelogs)** - See recent
|
||||
notable updates.
|
||||
- **[Changelog](./docs/changelogs/index.md)** - See recent notable updates.
|
||||
- **[NPM Package](https://www.npmjs.com/package/@google/gemini-cli)** - Package
|
||||
registry.
|
||||
- **[GitHub Issues](https://github.com/google-gemini/gemini-cli/issues)** -
|
||||
@@ -383,14 +377,12 @@ for planned features and priorities.
|
||||
|
||||
### Uninstall
|
||||
|
||||
See the [Uninstall Guide](https://www.geminicli.com/docs/resources/uninstall)
|
||||
for removal instructions.
|
||||
See the [Uninstall Guide](docs/cli/uninstall.md) for removal instructions.
|
||||
|
||||
## 📄 Legal
|
||||
|
||||
- **License**: [Apache License 2.0](LICENSE)
|
||||
- **Terms of Service**:
|
||||
[Terms & Privacy](https://www.geminicli.com/docs/resources/tos-privacy)
|
||||
- **Terms of Service**: [Terms & Privacy](./docs/resources/tos-privacy.md)
|
||||
- **Security**: [Security Policy](SECURITY.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -1,529 +0,0 @@
|
||||
# ADK-TS Alignment Pass
|
||||
|
||||
Every interface in our outline must map cleanly to ADK-TS. This document
|
||||
verifies that mapping field-by-field, identifies gaps, and confirms
|
||||
HITL/plugin/transfer patterns work.
|
||||
|
||||
Source: ADK-TS v0.4.0 at `/Users/adamfweidman/Desktop/adk-int/adk-js/core/src/`
|
||||
|
||||
---
|
||||
|
||||
## 1. AgentDescriptor ↔ ADK Agent Hierarchy
|
||||
|
||||
### Field-by-field mapping
|
||||
|
||||
| AgentDescriptor field | ADK-TS source | Notes |
|
||||
| ---------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `name` | `BaseAgent.name` | Direct. ADK validates it's a valid JS identifier. |
|
||||
| `displayName` | — | ADK doesn't have this. No conflict. |
|
||||
| `description` | `BaseAgent.description` (optional in ADK) | Direct. Used for model routing in AgentTool. |
|
||||
| `executor` | — | New concept. ADK agents are always 'adk'. Adapter sets this. |
|
||||
| `inputSchema` | `LlmAgent.inputSchema` (Zod or JSON Schema) | Direct. ADK's AgentTool uses this for tool parameter generation. |
|
||||
| `outputSchema` | `LlmAgent.outputSchema` (Zod or JSON Schema) | Direct. ADK uses for structured output + AgentTool response. |
|
||||
| `capabilities` | — | New concept. Adapter infers from agent type: LlmAgent gets `['elicitation', 'streaming', 'host_tool_execution']`, LoopAgent gets `['composition']`, etc. |
|
||||
| `ownTools` | `LlmAgent.tools: ToolUnion[]` | Maps via ToolDescriptor adapter. ADK tools have `name`, `description`, `_getDeclaration()` which returns JSON Schema. |
|
||||
| `requiredTools` | — | New concept. ADK agents don't declare required host tools. Adapter can infer from tool references. |
|
||||
| `subAgents` | `BaseAgent.subAgents: BaseAgent[]` | Recursive. Each sub-agent becomes a nested AgentDescriptor. |
|
||||
| `constraints.maxTurns` | `RunConfig.maxLlmCalls` (default 500) | Maps, though semantics differ slightly (LLM calls vs turns). |
|
||||
| `constraints.maxTimeMinutes` | — | ADK doesn't have time limits. No conflict — host enforces. |
|
||||
| `constraints.maxBudgetUsd` | — | ADK doesn't have budget. No conflict — host enforces. |
|
||||
| `metadata` | — | New concept. Adapter can populate from agent registration context. |
|
||||
|
||||
### ADK-specific fields NOT in AgentDescriptor
|
||||
|
||||
| ADK field | Where it lives | Our approach |
|
||||
| ----------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `instruction` / `globalInstruction` | LlmAgent | Executor-internal. Not in descriptor (it's runtime config, not identity). |
|
||||
| `model` | LlmAgent | Goes in ExecutionOptions.model or executor-internal config. |
|
||||
| `generateContentConfig` | LlmAgent | Executor-internal. |
|
||||
| `disallowTransferToParent/Peers` | LlmAgent | Could be `constraints` or `_meta`. Transfer policy is host-enforced. |
|
||||
| `includeContents` | LlmAgent | Executor-internal (context management). |
|
||||
| `outputKey` | LlmAgent | Executor-internal (state management). |
|
||||
| `beforeModelCallback`, etc. | LlmAgent | Executor-internal. These are ADK's callback system — our LifecycleInterceptor is the interface equivalent. |
|
||||
|
||||
### Verdict: CLEAN MAPPING
|
||||
|
||||
AgentDescriptor captures everything needed to describe an ADK agent externally.
|
||||
ADK-specific runtime config (instruction, model, callbacks) stays inside the
|
||||
executor — exactly right for the descriptor/executor separation.
|
||||
|
||||
**Key ADK pattern preserved:** AgentTool wraps an agent as a tool using
|
||||
`inputSchema` for parameters and `description` for the tool description. Our
|
||||
AgentDescriptor has both, so SubagentTool can do the same thing.
|
||||
|
||||
---
|
||||
|
||||
## 2. AgentSession ↔ ADK Runner
|
||||
|
||||
### Method mapping
|
||||
|
||||
| AgentSession method | ADK-TS equivalent | How adapter works |
|
||||
| ----------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `stream(data, options)` | `Runner.runAsync({ userId, sessionId, newMessage, runConfig })` | Adapter creates/loads session, maps data+options → runAsync params, wraps Event generator → AgentEvent generator. Each `stream()` call triggers a new `runAsync()`. |
|
||||
| `update(config)` | No direct equivalent | ADK doesn't support mid-stream config changes. Adapter queues updates for next `runAsync()` call. |
|
||||
| `steer(data)` | No direct equivalent | ADK doesn't support mid-stream intervention. Adapter can queue for next invocation or ignore. |
|
||||
| `abort()` | No direct equivalent | ADK uses `invocationContext.endInvocation = true`. Adapter sets this flag. Could also use AbortController. |
|
||||
|
||||
### ExecutionRequest → Runner.runAsync mapping
|
||||
|
||||
| ExecutionRequest field | ADK mapping |
|
||||
| --------------------------- | ---------------------------------------------------------------- |
|
||||
| `descriptor` | Used to find/create the BaseAgent instance |
|
||||
| `input` | → `newMessage: Content` (converted from ContentPart[] → Content) |
|
||||
| `sessionRef` | → `sessionId` (string) or creates session from SessionSnapshot |
|
||||
| `forkSession` | Adapter clones session before running |
|
||||
| `options.tools` | → merged into agent's `tools` config |
|
||||
| `options.model` | → `LlmAgent.model` override |
|
||||
| `options.hostToolExecution` | → `RunConfig.pauseOnToolCalls: true` |
|
||||
| `options.streaming` | → `RunConfig.streamingMode` |
|
||||
| `options.permissionMode` | → SecurityPlugin config |
|
||||
| `signal` | → wired to `invocationContext.endInvocation` |
|
||||
|
||||
### HITL: How pauseOnToolCalls works end-to-end
|
||||
|
||||
This is the critical path. Here's the full flow:
|
||||
|
||||
```
|
||||
1. LLM returns tool call (FunctionCall in Event)
|
||||
2. ADK checks RunConfig.pauseOnToolCalls === true
|
||||
3. ADK sets invocationContext.endInvocation = true
|
||||
4. ADK yields the Event (with FunctionCall) and stops
|
||||
5. Runner.runAsync() generator completes
|
||||
|
||||
--- OUR INTERFACE BOUNDARY ---
|
||||
|
||||
6. Adapter translates ADK Event → ToolRequestEvent
|
||||
7. Host receives ToolRequestEvent from session.stream() generator
|
||||
8. Host runs policy check (PolicyEvaluator.evaluate())
|
||||
9. Host fires hooks (LifecycleInterceptor.fire('before_tool', ...))
|
||||
10. If policy allows → Host executes tool → gets ToolResultData
|
||||
11. Host calls session.stream({ kind: 'tool_result', ... }) to get next stream
|
||||
|
||||
--- BACK INTO ADK ---
|
||||
|
||||
12. Adapter receives tool result
|
||||
13. Adapter creates FunctionResponse Content
|
||||
14. Adapter calls Runner.runAsync() again with FunctionResponse as newMessage
|
||||
15. ADK loads session (has prior tool call event)
|
||||
16. ADK resumes agent with tool response
|
||||
17. Loop continues from step 1
|
||||
```
|
||||
|
||||
**Why this works:** ADK's `pauseOnToolCalls` was designed exactly for this
|
||||
pattern — external tool execution by a host. The adapter translates between
|
||||
ADK's "end invocation + resume with FunctionResponse" pattern and our
|
||||
"ToolRequestEvent + send(tool_result)" pattern.
|
||||
|
||||
**Key insight:** Each `session.stream()` call triggers a new `Runner.runAsync()`
|
||||
call. This means each ADK "invocation" maps to one `stream()` call. The session
|
||||
persists state across invocations. Mid-stream `update()` and `steer()` calls are
|
||||
queued for the next invocation since ADK doesn't support mid-turn changes.
|
||||
|
||||
### HITL: ToolConfirmation flow
|
||||
|
||||
ADK also has a separate ToolConfirmation pattern (via
|
||||
`context.requestConfirmation()`):
|
||||
|
||||
```
|
||||
1. beforeToolCallback calls context.requestConfirmation({ hint: '...' })
|
||||
2. This sets eventActions.requestedToolConfirmations[functionCallId]
|
||||
3. ADK yields event with requestedToolConfirmations populated
|
||||
4. Runner completes (invocation ends)
|
||||
|
||||
--- OUR INTERFACE BOUNDARY ---
|
||||
|
||||
5. Adapter sees requestedToolConfirmations in event
|
||||
6. Adapter translates → ElicitationRequest { kind: 'tool_confirmation', ... }
|
||||
7. Host renders confirmation UI
|
||||
8. User responds → ElicitationResponse { action: 'accept' | 'decline' }
|
||||
|
||||
--- BACK INTO ADK ---
|
||||
|
||||
9. Adapter receives elicitation response
|
||||
10. If accepted: Adapter creates FunctionResponse with confirmed=true
|
||||
11. Calls Runner.runAsync() with FunctionResponse
|
||||
12. ADK's SecurityPlugin or callback reads confirmation from session
|
||||
13. Tool executes
|
||||
```
|
||||
|
||||
**Maps to our ElicitationRequest:** ADK's `ToolConfirmation.hint` →
|
||||
`ElicitationRequest.message`. ADK's `ToolConfirmation.payload` →
|
||||
`ElicitationRequest.context`. The `kind: 'tool_confirmation'` is the
|
||||
discriminator.
|
||||
|
||||
### HITL: Auth request flow
|
||||
|
||||
```
|
||||
1. Tool or callback calls context.requestCredential(authConfig)
|
||||
2. Sets eventActions.requestedAuthConfigs[functionCallId]
|
||||
3. Event yields, invocation ends
|
||||
|
||||
--- OUR INTERFACE BOUNDARY ---
|
||||
|
||||
4. Adapter sees requestedAuthConfigs
|
||||
5. Translates → ElicitationRequest { kind: 'auth_required', context: authConfig }
|
||||
6. User provides credentials
|
||||
7. ElicitationResponse { action: 'accept', content: { credential: ... } }
|
||||
|
||||
--- BACK INTO ADK ---
|
||||
|
||||
8. Adapter stores credential via CredentialService
|
||||
9. Calls Runner.runAsync() again
|
||||
10. Tool calls context.getAuthResponse() → gets credential
|
||||
```
|
||||
|
||||
**Maps to our ElicitationRequest:** ADK's auth pattern is just another
|
||||
elicitation kind. This validates our generic elicitation design — it handles
|
||||
tool confirmation, auth, and any future interaction type.
|
||||
|
||||
---
|
||||
|
||||
## 3. AgentEvent ↔ ADK Event
|
||||
|
||||
### Event type mapping
|
||||
|
||||
| Our AgentEvent | ADK Event pattern | Adapter translation |
|
||||
| --------------------- | --------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `InitializeEvent` | First event from Runner.runAsync() | Adapter emits on first stream() call |
|
||||
| `SessionUpdateEvent` | `eventActions.stateDelta` | Adapter emits when stateDelta is non-empty |
|
||||
| `MessageEvent` | `event.content` with text Parts | Filter text/thought parts from Content |
|
||||
| `ToolRequestEvent` | `getFunctionCalls(event)` returns FunctionCall[] | Each FunctionCall → one ToolRequestEvent |
|
||||
| `ToolUpdateEvent` | `event.longRunningToolIds` | Adapter emits progress for long-running tools |
|
||||
| `ToolResponseEvent` | `getFunctionResponses(event)` returns FunctionResponse[] | Each FunctionResponse → one ToolResponseEvent |
|
||||
| `ElicitationRequest` | `eventActions.requestedToolConfirmations` or `requestedAuthConfigs` | Map to generic elicitation |
|
||||
| `ElicitationResponse` | User input → FunctionResponse in next runAsync call | Reverse of above |
|
||||
| `UsageEvent` | `event.usageMetadata` (GenerateContentResponseUsageMetadata) | Map token counts |
|
||||
| `ErrorEvent` | `event.errorCode` + `event.errorMessage` | Map error fields |
|
||||
| `stream_end` | `isFinalResponse(event)`, `eventActions.transferToAgent`, `eventActions.escalate` | Derive `stream_end` reason from ADK signals |
|
||||
| `CustomEvent` | `event.customMetadata` | Pass through |
|
||||
|
||||
### ADK EventActions → Our events
|
||||
|
||||
| EventActions field | Our event | Notes |
|
||||
| ---------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `stateDelta` | SessionUpdate or embedded in other events | Delta state is a core ADK pattern |
|
||||
| `artifactDelta` | `CustomEvent { kind: 'artifact_delta' }` | Artifacts not in our core events |
|
||||
| `transferToAgent` | Tool call (`transfer_to_agent`) + `stream_end` `reason: 'completed'` | Handoff is a tool call. Host intercepts the tool request, mediates the handoff, originating agent completes. |
|
||||
| `escalate` | `stream_end` `reason: 'completed'` with `data: { escalateReason: '...' }` | LoopAgent exit signal. ADK's escalate = "I'm done, pass control back up" |
|
||||
| `requestedToolConfirmations` | `ElicitationRequest { kind: 'tool_confirmation' }` | Per function call ID |
|
||||
| `requestedAuthConfigs` | `ElicitationRequest { kind: 'auth_required' }` | Per function call ID |
|
||||
| `skipSummarization` | `_meta: { skipSummarization: true }` | ADK-specific, goes in metadata |
|
||||
|
||||
### AgentEventBase mapping
|
||||
|
||||
| AgentEventBase field | ADK Event field | Notes |
|
||||
| -------------------- | ---------------------------------------- | ------------------------------------------------- |
|
||||
| `id` | `event.id` | Direct |
|
||||
| `timestamp` | `event.timestamp` (number) | Convert to ISO 8601 string |
|
||||
| `type` | Derived from content analysis | ADK doesn't have event types — adapter classifies |
|
||||
| `agentId` | `event.author` (agent name) or context | **New field** — which agent emitted this event |
|
||||
| `threadId` | `event.branch` (e.g., "agent_1.agent_2") | Direct mapping |
|
||||
| `source` | `event.author` ("user" or agent name) | Direct |
|
||||
| `_meta` | `event.customMetadata` | Direct |
|
||||
|
||||
### Verdict: CLEAN MAPPING
|
||||
|
||||
Every ADK event pattern maps to our event types. The adapter classifies ADK's
|
||||
untyped events into our typed event taxonomy. Key insight: ADK events are richer
|
||||
(they carry EventActions, function calls, auth requests all in one event), so
|
||||
the adapter may fan out one ADK Event into multiple AgentEvents (e.g., one
|
||||
Message + one ToolRequest + one ElicitationRequest). The new `agentId` field
|
||||
maps directly from ADK's `event.author`.
|
||||
|
||||
---
|
||||
|
||||
## 4. ToolContract ↔ ADK Tool System
|
||||
|
||||
### ToolDescriptor ↔ BaseTool
|
||||
|
||||
| ToolDescriptor field | ADK source | Notes |
|
||||
| ------------------------- | ------------------------------------------------------------- | --------------------------------- |
|
||||
| `name` | `BaseTool.name` | Direct |
|
||||
| `displayName` | — | ADK doesn't have this |
|
||||
| `description` | `BaseTool.description` | Direct |
|
||||
| `parametersSchema` | `BaseTool._getDeclaration()` → FunctionDeclaration.parameters | JSON Schema from declaration |
|
||||
| `annotations.readOnly` | Inferred from tool type | FunctionTool with no side effects |
|
||||
| `annotations.longRunning` | `BaseTool.isLongRunning` | Direct |
|
||||
|
||||
### ToolCallRequest ↔ FunctionCall
|
||||
|
||||
| ToolCallRequest | ADK FunctionCall | Notes |
|
||||
| --------------- | ------------------- | ------ |
|
||||
| `requestId` | `functionCall.id` | Direct |
|
||||
| `name` | `functionCall.name` | Direct |
|
||||
| `args` | `functionCall.args` | Direct |
|
||||
|
||||
### ToolResultData ↔ FunctionResponse + tool return
|
||||
|
||||
| ToolResultData | ADK | Notes |
|
||||
| ---------------- | ------------------------------ | ------------------------------------------------ |
|
||||
| `llmContent` | `FunctionResponse.response` | Adapter wraps into ContentPart[] |
|
||||
| `displayContent` | — | ADK doesn't separate display from model content |
|
||||
| `isError` | Error thrown from `runAsync()` | Adapter catches and sets flag |
|
||||
| `tailCalls` | — | ADK doesn't have tail calls (gemini-cli concept) |
|
||||
|
||||
### AgentTool pattern
|
||||
|
||||
ADK's `AgentTool` wraps a `BaseAgent` as a `BaseTool`:
|
||||
|
||||
- Uses `agent.inputSchema` for tool parameters
|
||||
- Uses `agent.description` for tool description
|
||||
- Creates internal Runner with isolated session
|
||||
- Returns agent output as tool result
|
||||
- Merges state deltas back to parent
|
||||
|
||||
**Our equivalent:** `SubagentTool` wraps `AgentDescriptor` as a tool:
|
||||
|
||||
- Uses `descriptor.inputSchema` for tool parameters
|
||||
- Uses `descriptor.description` for tool description
|
||||
- Creates executor via `SessionFactory.create(descriptor, context)`
|
||||
- Returns execution result as tool result
|
||||
|
||||
**Mapping is 1:1.** The only difference is ADK does it with concrete agent
|
||||
instances; we do it with descriptors + factory.
|
||||
|
||||
---
|
||||
|
||||
## 5. LifecycleInterceptor ↔ ADK Plugin System
|
||||
|
||||
### Hook point mapping
|
||||
|
||||
| Our hook point string | ADK Plugin callback | Mapping |
|
||||
| --------------------- | ----------------------- | ------------------------------------------ |
|
||||
| `'before_agent'` | `beforeAgentCallback` | `payload: { agent, context }` |
|
||||
| `'after_agent'` | `afterAgentCallback` | `payload: { agent, context }` |
|
||||
| `'before_model'` | `beforeModelCallback` | `payload: { context, llmRequest }` |
|
||||
| `'after_model'` | `afterModelCallback` | `payload: { context, llmResponse }` |
|
||||
| `'before_tool'` | `beforeToolCallback` | `payload: { tool, args, context }` |
|
||||
| `'after_tool'` | `afterToolCallback` | `payload: { tool, args, context, result }` |
|
||||
| `'on_event'` | `onEventCallback` | `payload: { event }` |
|
||||
| `'on_user_message'` | `onUserMessageCallback` | `payload: { userMessage }` |
|
||||
| `'before_run'` | `beforeRunCallback` | `payload: { context }` |
|
||||
| `'after_run'` | `afterRunCallback` | `payload: { context }` |
|
||||
| `'on_model_error'` | `onModelErrorCallback` | `payload: { request, error }` |
|
||||
| `'on_tool_error'` | `onToolErrorCallback` | `payload: { tool, args, error }` |
|
||||
|
||||
### HookResult ↔ ADK callback return
|
||||
|
||||
| HookResult field | ADK pattern | Notes |
|
||||
| ------------------- | ----------------------------------------------- | ----------------------------------- |
|
||||
| `action: 'proceed'` | Return `undefined` | Plugin returns nothing → continue |
|
||||
| `action: 'block'` | Return `Content` (for agent/model) or throw | Non-undefined return short-circuits |
|
||||
| `modifications` | Return modified `LlmRequest`/`LlmResponse`/args | Plugin returns modified version |
|
||||
|
||||
### ADK's early-exit pattern
|
||||
|
||||
ADK plugins use "first non-undefined return wins":
|
||||
|
||||
- `beforeModelCallback` returns `LlmResponse` → skips LLM call entirely (cache
|
||||
hit)
|
||||
- `beforeToolCallback` returns modified `args` → tool runs with new args
|
||||
- `beforeAgentCallback` returns `Content` → skips agent run entirely
|
||||
|
||||
Our `HookResult.modifications` carries the same data. The `action: 'block'` +
|
||||
return value pattern maps cleanly.
|
||||
|
||||
### gemini-cli hooks NOT in ADK
|
||||
|
||||
| gemini-cli hook | ADK equivalent | Notes |
|
||||
| --------------------- | ------------------------------------ | ------------------------------------------------------------- |
|
||||
| `BeforeToolSelection` | — | ADK doesn't let you modify which tools are available mid-turn |
|
||||
| `Notification` | — | ADK doesn't have notification hooks |
|
||||
| `SessionStart` | `onUserMessageCallback` (first call) | Close enough |
|
||||
| `SessionEnd` | `afterRunCallback` | Close enough |
|
||||
| `PreCompress` | — | ADK doesn't have context compression hooks |
|
||||
|
||||
These gaps are fine — they're gemini-cli-specific hook points. Our generic
|
||||
`fire(hookPoint, payload)` handles them because the hook point is an open
|
||||
string. ADK executors simply don't fire these hook points, and
|
||||
`supportedHookPoints()` reflects that.
|
||||
|
||||
---
|
||||
|
||||
## 6. PolicyEvaluator ↔ ADK SecurityPlugin
|
||||
|
||||
### ADK SecurityPlugin
|
||||
|
||||
```typescript
|
||||
class SecurityPlugin extends BasePlugin {
|
||||
policyEngine: BasePolicyEngine;
|
||||
|
||||
// In beforeToolCallback:
|
||||
async beforeToolCallback({ tool, args, context }) {
|
||||
const outcome = await this.policyEngine.evaluate(tool.name, args);
|
||||
switch (outcome) {
|
||||
case PolicyOutcome.DENY:
|
||||
throw error;
|
||||
case PolicyOutcome.CONFIRM:
|
||||
context.requestConfirmation({ hint });
|
||||
case PolicyOutcome.ALLOW:
|
||||
return undefined; // proceed
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Mapping
|
||||
|
||||
| Our PolicyEvaluator | ADK SecurityPlugin | Notes |
|
||||
| ------------------------- | --------------------------------------------------------- | ------------------------------------------ |
|
||||
| `evaluate(request)` | `policyEngine.evaluate(toolName, args)` | ADK is simpler — tool name + args only |
|
||||
| `PolicyDecision.allow` | `PolicyOutcome.ALLOW` | Direct |
|
||||
| `PolicyDecision.deny` | `PolicyOutcome.DENY` | Direct |
|
||||
| `PolicyDecision.ask_user` | `PolicyOutcome.CONFIRM` → `context.requestConfirmation()` | ADK chains to ToolConfirmation |
|
||||
| `getExcluded()` | — | ADK doesn't pre-filter tools |
|
||||
| `request.principal` | — | ADK doesn't track who's calling |
|
||||
| `request.principalPath` | Could use `context.agentName` + branch | For hierarchical policy |
|
||||
| `request.context` | — | Our extension point for host-specific data |
|
||||
|
||||
### How ADK policy maps when host controls execution
|
||||
|
||||
With `pauseOnToolCalls: true`, the flow is:
|
||||
|
||||
1. ADK yields tool call → adapter converts to ToolRequestEvent
|
||||
2. **Host** runs PolicyEvaluator.evaluate() — NOT ADK's SecurityPlugin
|
||||
3. Host decides allow/deny/ask_user
|
||||
4. If allowed, host executes tool and sends result via `session.stream()`
|
||||
|
||||
This means **ADK's SecurityPlugin is bypassed when the host controls tool
|
||||
execution** — which is correct! The host's PolicyEvaluator is the authority.
|
||||
ADK's SecurityPlugin only matters when ADK executes tools internally
|
||||
(`pauseOnToolCalls: false`).
|
||||
|
||||
---
|
||||
|
||||
## 7. SessionContract ↔ ADK Session
|
||||
|
||||
### Session mapping
|
||||
|
||||
| Our SessionHandle | ADK Session | Notes |
|
||||
| ----------------- | ---------------------------------------- | ----------------------------------------- |
|
||||
| `id` | `Session.id` | Direct |
|
||||
| `agentName` | `Session.appName` | ADK uses appName, not agent name |
|
||||
| `events` | `Session.events: Event[]` | Direct (but ADK Events → our AgentEvents) |
|
||||
| `state` | `Session.state: Record<string, unknown>` | Direct |
|
||||
| `lastUpdateTime` | `Session.lastUpdateTime` | Direct |
|
||||
|
||||
### SessionProvider ↔ BaseSessionService
|
||||
|
||||
| Our SessionProvider | ADK BaseSessionService | Notes |
|
||||
| ----------------------------- | ----------------------------------------------- | -------------------------- |
|
||||
| `create(agentName, metadata)` | `createSession({ appName, userId })` | ADK requires userId |
|
||||
| `load(sessionId)` | `getSession({ appName, userId, sessionId })` | ADK requires all three IDs |
|
||||
| `list(agentName)` | `listSessions({ appName, userId })` | ADK scopes by userId |
|
||||
| `delete(sessionId)` | `deleteSession({ appName, userId, sessionId })` | Same pattern |
|
||||
|
||||
### Gap: ADK requires userId
|
||||
|
||||
ADK sessions are scoped by `(appName, userId, sessionId)`. Our interface uses
|
||||
just `sessionId`. The adapter can embed userId in the session metadata or derive
|
||||
it from HostContext.
|
||||
|
||||
### State prefixes (ADK-specific)
|
||||
|
||||
ADK uses prefixed state keys:
|
||||
|
||||
- `app:` — app-scoped, persisted
|
||||
- `user:` — user-scoped, persisted
|
||||
- `temp:` — temporary, stripped before persistence
|
||||
|
||||
Our `SessionHandle.state` is a flat `Record<string, unknown>`. The adapter
|
||||
preserves prefixes as-is — they're just string keys. No conflict.
|
||||
|
||||
---
|
||||
|
||||
## 8. ContentPart ↔ ADK Content/Part
|
||||
|
||||
### ADK uses Google GenAI types
|
||||
|
||||
ADK's `Content` and `Part` come from `@google/genai`:
|
||||
|
||||
```typescript
|
||||
interface Content {
|
||||
role?: string; // 'user' | 'model'
|
||||
parts: Part[];
|
||||
}
|
||||
|
||||
type Part = TextPart | InlineDataPart | FunctionCallPart | FunctionResponsePart | ...
|
||||
```
|
||||
|
||||
### Mapping
|
||||
|
||||
| Our ContentPart | ADK/GenAI Part | Notes |
|
||||
| --------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------ |
|
||||
| `{ type: 'text', text }` | `{ text: string }` | Direct |
|
||||
| `{ type: 'thought', thought }` | `{ thought: true, text: string }` | ADK uses `thought` boolean flag on TextPart |
|
||||
| `{ type: 'media', mimeType, data }` | `{ inlineData: { mimeType, data } }` | Restructure |
|
||||
| `{ type: 'reference', text, uri }` | `{ fileData: { fileUri, mimeType } }` | Map fileData → reference |
|
||||
| `{ type: 'refusal', text }` | — | Not in ADK/GenAI. Adapter would map from finishReason. |
|
||||
| `{ type: 'function_call', name, args, id }` | `{ functionCall: { name, args, id } }` | Unwrap |
|
||||
| `{ type: 'function_response', name, response, id }` | `{ functionResponse: { name, response, id } }` | Unwrap |
|
||||
|
||||
### Verdict: CLEAN MAPPING
|
||||
|
||||
The adapter converts between our flat discriminated union and ADK's nested Part
|
||||
structure. No information loss in either direction.
|
||||
|
||||
---
|
||||
|
||||
## 9. Composition ↔ ADK Agent Patterns
|
||||
|
||||
| Our CompositionConfig.pattern | ADK Agent type | Notes |
|
||||
| ----------------------------- | -------------------------------------- | ------------------------------------------------ |
|
||||
| `'hierarchical'` | Any agent with `subAgents` | Default — parent calls sub-agents as tools |
|
||||
| `'sequential'` | `SequentialAgent` | Runs children in order |
|
||||
| `'parallel'` | `ParallelAgent` | Runs children concurrently, branch isolation |
|
||||
| `'loop'` | `LoopAgent` | Repeats children until escalate or maxIterations |
|
||||
| `'transfer'` | LlmAgent with `transfer_to_agent` tool | Peer-to-peer handoff |
|
||||
|
||||
### Branch isolation
|
||||
|
||||
ADK's `ParallelAgent` gives each child an isolated `branch` context:
|
||||
|
||||
- Children don't see peer events
|
||||
- Each gets unique branch path: `"parent.child_0"`, `"parent.child_1"`
|
||||
- Results merged after all complete
|
||||
|
||||
Maps to our `threadId` — each parallel branch gets a unique threadId. Events
|
||||
from different branches are interleaved by the host.
|
||||
|
||||
---
|
||||
|
||||
## 10. Summary: Gaps and Resolutions
|
||||
|
||||
### No gaps blocking ADK integration:
|
||||
|
||||
| Concern | Status | Resolution |
|
||||
| ----------------------- | --------- | ------------------------------------------------------------------------- |
|
||||
| pauseOnToolCalls HITL | **Works** | Adapter maps to stream() cycle (§2) |
|
||||
| ToolConfirmation | **Works** | Maps to ElicitationRequest (§2) |
|
||||
| Auth requests | **Works** | Maps to ElicitationRequest (§2) |
|
||||
| Plugin hooks (12 types) | **Works** | Maps to LifecycleInterceptor.fire() (§5) |
|
||||
| Agent transfers | **Works** | Tool call (`transfer_to_agent`) + `stream_end` `reason: 'completed'` (§3) |
|
||||
| State delta pattern | **Works** | SessionUpdateEvent or \_meta (§3) |
|
||||
| Branch isolation | **Works** | threadId mapping (§9) |
|
||||
| AgentTool pattern | **Works** | SubagentTool with descriptor + factory (§4) |
|
||||
| Session management | **Works** | Adapter maps userId into session (§7) |
|
||||
|
||||
### Minor adapter complexity:
|
||||
|
||||
1. **Event fan-out:** One ADK Event may become multiple AgentEvents (message +
|
||||
tool call + elicitation). Adapter logic needed but straightforward.
|
||||
2. **userId scoping:** ADK sessions require userId; our interface doesn't.
|
||||
Adapter derives from HostContext.
|
||||
3. **Timestamp format:** ADK uses `number` (epoch ms); we use ISO 8601 string.
|
||||
Simple conversion.
|
||||
4. **Content structure:** ADK uses nested Part types; we use flat discriminated
|
||||
union. Adapter converts bidirectionally.
|
||||
|
||||
### ADK features our interface supports that gemini-cli doesn't have yet:
|
||||
|
||||
- `LoopAgent` / `ParallelAgent` / `SequentialAgent` composition → our
|
||||
CompositionConfig
|
||||
- `eventActions.stateDelta` → our SessionUpdateEvent
|
||||
- `eventActions.transferToAgent` → tool call (`transfer_to_agent`) +
|
||||
`stream_end` `reason: 'completed'`
|
||||
- `eventActions.escalate` → `stream_end` `reason: 'completed'` with
|
||||
`data: { escalateReason }`
|
||||
- Long-running tools → our ToolUpdateEvent
|
||||
- Auth credential flow → our ElicitationRequest with kind: 'auth_required'
|
||||
@@ -1,274 +0,0 @@
|
||||
# ADK-TS (Agent Development Kit - TypeScript) Architecture Notes
|
||||
|
||||
## Package: `@google/adk` v0.4.0
|
||||
|
||||
**Location:** `/Users/adamfweidman/Desktop/adk-int/adk-js/core/`
|
||||
|
||||
## Agent Hierarchy
|
||||
|
||||
```
|
||||
BaseAgent (abstract)
|
||||
├── LlmAgent - Model-driven agent with tools (the main one)
|
||||
├── LoopAgent - Runs sub-agents in a loop (maxIterations, escalate to exit)
|
||||
├── ParallelAgent - Runs sub-agents concurrently (isolated branches)
|
||||
└── SequentialAgent - Runs sub-agents sequentially
|
||||
```
|
||||
|
||||
### BaseAgent Config
|
||||
|
||||
- `name: string` - Unique identifier (must be valid JS identifier)
|
||||
- `description?: string` - One-line capability for model routing
|
||||
- `parentAgent?: BaseAgent` - Parent in agent tree
|
||||
- `subAgents?: BaseAgent[]` - Child agents
|
||||
- `beforeAgentCallback / afterAgentCallback` - Pre/post execution hooks
|
||||
|
||||
### LlmAgent Config (extends BaseAgent)
|
||||
|
||||
- `model?: string | BaseLlm` - LLM to use
|
||||
- `instruction?: string | InstructionProvider` - Agent-specific instructions
|
||||
- `globalInstruction?: string | InstructionProvider` - Tree-wide (root only)
|
||||
- `tools?: ToolUnion[]` - Available tools
|
||||
- `generateContentConfig?: GenerateContentConfig` - LLM params
|
||||
- `disallowTransferToParent / disallowTransferToPeers` - Transfer controls
|
||||
- `includeContents?: 'default' | 'none'` - Context history inclusion
|
||||
- `inputSchema / outputSchema` - Validation schemas
|
||||
- `outputKey?: string` - Session state key for output storage
|
||||
- `beforeModelCallback / afterModelCallback` - LLM hooks
|
||||
- `beforeToolCallback / afterToolCallback` - Tool hooks
|
||||
- `requestProcessors / responseProcessors` - LLM request/response processors
|
||||
- `codeExecutor?: BaseCodeExecutor`
|
||||
|
||||
## Event System
|
||||
|
||||
### Event Interface
|
||||
|
||||
```typescript
|
||||
interface Event extends LlmResponse {
|
||||
id: string;
|
||||
invocationId: string;
|
||||
author?: string; // "user" or agent name
|
||||
actions: EventActions; // State/artifact/auth/transfer operations
|
||||
longRunningToolIds?: string[];
|
||||
branch?: string; // Hierarchical agent path
|
||||
timestamp: number;
|
||||
content?: Content;
|
||||
partial?: boolean; // Streaming indicator
|
||||
}
|
||||
```
|
||||
|
||||
### EventActions
|
||||
|
||||
```typescript
|
||||
interface EventActions {
|
||||
skipSummarization?: boolean;
|
||||
stateDelta: Record<string, unknown>;
|
||||
artifactDelta: Record<string, number>;
|
||||
transferToAgent?: string;
|
||||
escalate?: boolean;
|
||||
requestedAuthConfigs: Record<string, AuthConfig>;
|
||||
requestedToolConfirmations: Record<string, ToolConfirmation>;
|
||||
}
|
||||
```
|
||||
|
||||
### Structured Events (utility layer)
|
||||
|
||||
Converts raw Event to discriminated union:
|
||||
|
||||
```
|
||||
EventType: THOUGHT | CONTENT | TOOL_CALL | TOOL_RESULT | CALL_CODE |
|
||||
CODE_RESULT | ERROR | ACTIVITY | TOOL_CONFIRMATION | FINISHED
|
||||
```
|
||||
|
||||
## Tool System
|
||||
|
||||
### BaseTool (abstract)
|
||||
|
||||
- `name, description, isLongRunning`
|
||||
- `_getDeclaration(): FunctionDeclaration` - OpenAPI schema for LLM
|
||||
- `runAsync(request): Promise<unknown>` - Execute tool
|
||||
- `processLlmRequest(request): Promise<void>` - Preprocessing
|
||||
|
||||
### Concrete Tool Types
|
||||
|
||||
1. **FunctionTool** - Generic typed tools (Zod schema support)
|
||||
2. **AgentTool** - Wrap agents as tools (for hierarchical composition)
|
||||
3. **MCPTool** - Model Context Protocol server tools
|
||||
4. **GoogleSearchTool** - Built-in web search
|
||||
5. **ExitLoopTool** - Signal loop exit
|
||||
6. **LongRunningFunctionTool** - Async long-running operations
|
||||
|
||||
### BaseToolset
|
||||
|
||||
- Filter tools by predicate or string list
|
||||
- `getTools(context)`, `close()`, `isToolSelected()`
|
||||
- **MCPToolset** - Toolset for MCP server connections
|
||||
|
||||
## Session Management
|
||||
|
||||
### Session Interface
|
||||
|
||||
```typescript
|
||||
interface Session {
|
||||
id: string;
|
||||
appName: string;
|
||||
userId: string;
|
||||
state: Record<string, unknown>; // Mutable key-value store
|
||||
events: Event[]; // Complete conversation history
|
||||
lastUpdateTime: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Session Services
|
||||
|
||||
- `BaseSessionService` (abstract) - createSession, getSession, listSessions,
|
||||
deleteSession, appendEvent
|
||||
- `InMemorySessionService` - In-process storage
|
||||
- `DatabaseSessionService` - Mikro-ORM backed (SQL)
|
||||
|
||||
### State Management
|
||||
|
||||
- `State` class wraps base state + delta
|
||||
- `get()` returns from delta if present, else base
|
||||
- `set()` updates delta only
|
||||
- `hasDelta()` checks if changes made
|
||||
|
||||
## Human-in-the-Loop (HITL)
|
||||
|
||||
### Tool Confirmation
|
||||
|
||||
```typescript
|
||||
class ToolConfirmation {
|
||||
hint?: string; // Guidance for user
|
||||
confirmed: boolean; // User approval
|
||||
payload?: unknown; // Additional context
|
||||
}
|
||||
```
|
||||
|
||||
### Security Plugin
|
||||
|
||||
- `beforeToolCallback` - Evaluates policy before tool execution
|
||||
- `BasePolicyEngine` interface with `evaluate()` method
|
||||
- `PolicyOutcome`: DENY | CONFIRM | ALLOW
|
||||
|
||||
### Auth Requests
|
||||
|
||||
- `context.requestCredential(authConfig)` - Request auth from user
|
||||
- `context.getAuthResponse(authConfig)` - Check for auth response
|
||||
- Sets `eventActions.requestedAuthConfigs[functionCallId]`
|
||||
|
||||
## Multi-Agent Patterns
|
||||
|
||||
### Agent Transfer
|
||||
|
||||
- LlmAgent injects `transfer_to_agent(agentName)` tool
|
||||
- Sets `eventActions.transferToAgent = targetAgentName`
|
||||
- Runner resolves target and continues
|
||||
- Can transfer to: sub-agents, parent (if not disabled), peers (if not disabled)
|
||||
|
||||
### Parallel Agent
|
||||
|
||||
- Runs all subAgents concurrently
|
||||
- Isolates each via `branch` context
|
||||
- Sub-agents don't see peer history
|
||||
- Merges event streams with fair ordering
|
||||
|
||||
### Loop Agent
|
||||
|
||||
- Repeatedly runs subAgents
|
||||
- `maxIterations` caps loop count
|
||||
- Exits on `event.actions.escalate === true`
|
||||
|
||||
## Plugin System
|
||||
|
||||
### BasePlugin Lifecycle Hooks (14 hooks!)
|
||||
|
||||
- `onUserMessageCallback` - Preprocess user messages
|
||||
- `beforeRunCallback` - Before agent run (can short-circuit)
|
||||
- `onEventCallback` - Per-event (can modify events)
|
||||
- `afterRunCallback` - Final cleanup
|
||||
- `beforeAgentCallback / afterAgentCallback` - Agent lifecycle
|
||||
- `beforeModelCallback / afterModelCallback` - LLM lifecycle
|
||||
- `onModelErrorCallback` - Model error handling
|
||||
- `beforeToolCallback / afterToolCallback` - Tool lifecycle
|
||||
- `onToolErrorCallback` - Tool error handling
|
||||
|
||||
### Built-in Plugins
|
||||
|
||||
- **LoggingPlugin** - Debug logging
|
||||
- **SecurityPlugin** - Policy enforcement + tool confirmation
|
||||
- **PluginManager** - Plugin orchestration
|
||||
|
||||
## Runner
|
||||
|
||||
### Runner Config
|
||||
|
||||
```typescript
|
||||
interface RunnerConfig {
|
||||
appName: string;
|
||||
agent: BaseAgent; // Root agent
|
||||
plugins?: BasePlugin[];
|
||||
artifactService?: BaseArtifactService;
|
||||
sessionService: BaseSessionService; // Required
|
||||
memoryService?: BaseMemoryService;
|
||||
credentialService?: BaseCredentialService;
|
||||
}
|
||||
```
|
||||
|
||||
### RunConfig (per-run options)
|
||||
|
||||
```typescript
|
||||
interface RunConfig {
|
||||
speechConfig?: SpeechConfig;
|
||||
responseModalities?: Modality[];
|
||||
maxLlmCalls?: number; // Default 500
|
||||
pauseOnToolCalls?: boolean; // Client-side tool execution
|
||||
streamingMode?: StreamingMode; // NONE | SSE | BIDI
|
||||
// ... audio/live configs
|
||||
}
|
||||
```
|
||||
|
||||
### Execution Pipeline
|
||||
|
||||
1. Load or create session
|
||||
2. Create InvocationContext
|
||||
3. Run pluginManager.runOnUserMessageCallback()
|
||||
4. Append user message to session
|
||||
5. Run agent.runAsync(invocationContext) → yields events
|
||||
6. For each non-partial event: append to session
|
||||
7. Run pluginManager.runOnEventCallback()
|
||||
8. Run pluginManager.runAfterRunCallback()
|
||||
|
||||
## Model Layer
|
||||
|
||||
### BaseLlm (abstract)
|
||||
|
||||
- `generateContentAsync(llmRequest, stream?): AsyncGenerator<LlmResponse>`
|
||||
- `connect(llmRequest): Promise<BaseLlmConnection>` - For live/streaming
|
||||
|
||||
### Implementations
|
||||
|
||||
- `Gemini` - Google Gemini API
|
||||
- `ApigeeLlm` - Apigee-wrapped models
|
||||
- `LLMRegistry` - Static registry for model lookup
|
||||
|
||||
## Service Adapters (all abstract base + implementations)
|
||||
|
||||
| Service | Implementations |
|
||||
| --------------------- | ------------------------------ |
|
||||
| BaseSessionService | InMemory, Database (Mikro-ORM) |
|
||||
| BaseArtifactService | InMemory, File, GCS |
|
||||
| BaseMemoryService | InMemory |
|
||||
| BaseCredentialService | InMemory |
|
||||
| BaseCodeExecutor | BuiltIn |
|
||||
|
||||
## Design Patterns
|
||||
|
||||
1. **Symbol-based type guards** - Every class uses `Symbol.for()` + `isXxx()`
|
||||
2. **Abstract base classes** - Service interfaces via abstract classes
|
||||
3. **Async generators** - All agent execution yields events
|
||||
4. **Context objects** - Rich context passed to callbacks/tools
|
||||
5. **Delta state** - Session state + event action deltas
|
||||
6. **Plugin middleware** - 14 hooks at multiple execution points
|
||||
7. **Tree-based hierarchy** - Parent-child agents with root traversal
|
||||
8. **Branch isolation** - Parallel agents use branch paths
|
||||
9. **Callback chains** - Multiple callbacks per stage with early termination
|
||||
@@ -1,587 +0,0 @@
|
||||
# Cross-SDK Comparison: Events, Agents, and Interface Superset
|
||||
|
||||
## 1. AgentEvents: Our Outline vs Michael's
|
||||
|
||||
Our outline and Michael's `Gemini CLI Agents.txt` are **nearly identical** in
|
||||
event taxonomy. The only difference is we added a `stream_end` event type:
|
||||
|
||||
| # | Michael's Events | Our Outline | Delta |
|
||||
| --- | ---------------------- | --------------------- | ------------------------------------------------------------------------------- |
|
||||
| 1 | `initialize` | `InitializeEvent` | Same |
|
||||
| 2 | `session_update` | `SessionUpdateEvent` | Same |
|
||||
| 3 | `message` | `MessageEvent` | Same — streaming handled by AsyncGenerator |
|
||||
| 4 | `tool_request` | `ToolRequestEvent` | Same |
|
||||
| 5 | `tool_update` | `ToolUpdateEvent` | Same |
|
||||
| 6 | `tool_response` | `ToolResponseEvent` | Same |
|
||||
| 7 | `elicitation_request` | `ElicitationRequest` | Same |
|
||||
| 8 | `elicitation_response` | `ElicitationResponse` | Same |
|
||||
| 9 | `usage` | `UsageEvent` | Same |
|
||||
| 10 | `error` | `ErrorEvent` | Same |
|
||||
| 11 | `custom` | `CustomEvent` | Same |
|
||||
| 12 | — | **StreamEnd** | **Added**: completed, failed, aborted, max_turns, max_budget, max_time, refusal |
|
||||
|
||||
### Minor structural differences:
|
||||
|
||||
| Aspect | Michael | Our Outline |
|
||||
| ---------------------- | --------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| **Base type** | `AgentEventCommon` with `type: string` (fully open) | `AgentEventBase` with `type: AgentEventType` (`'known' \| (string & {})`) |
|
||||
| **Agent ID** | — | `agentId` on event base (which agent emitted this event) |
|
||||
| **Event map** | Generic `interface AgentEvents` + mapped type | Same — adopted Michael's pattern for declaration merging extensibility |
|
||||
| **ContentPart.\_meta** | Required (`_meta: Record<string, unknown>`) | Optional (`_meta?: Record<string, unknown>`) |
|
||||
| **ErrorData.status** | Google RPC codes (`'RESOURCE_EXHAUSTED' \| '...'`) | Open string (per our generic philosophy) |
|
||||
| **Message.role** | `'user' \| 'agent' \| 'developer'` | Same |
|
||||
| **Stream end** | Only `initialize` | `stream_end` with `reason` field + open `data` bag |
|
||||
| **Handoff** | Not covered | Tool call (`transfer_to_agent`) — no dedicated event |
|
||||
| **Pausing** | Implicit (elicitation/tool events) | Same — no explicit pause/resume events |
|
||||
|
||||
### Design decisions adopted from Michael
|
||||
|
||||
1. **`interface AgentEvents` + mapped type** — Michael's pattern enables
|
||||
declaration merging, letting any module add new event types without modifying
|
||||
the base definition. Strictly better than an explicit union type.
|
||||
2. **`_meta` on ContentPart** — More extensible. We adopted it (as optional).
|
||||
3. **Implicit pausing** — No separate pause/resume events. When the agent emits
|
||||
an `elicitation_request` or `tool_request`, the stream naturally pauses. The
|
||||
host calls `stream()` to resume.
|
||||
|
||||
---
|
||||
|
||||
## 2. Claude Agent SDK — Key Interfaces
|
||||
|
||||
Source: `@anthropic-ai/claude-agent-sdk`
|
||||
|
||||
### Agent Execution Model
|
||||
|
||||
```typescript
|
||||
// Entry point — not an interface, a function
|
||||
function query({
|
||||
prompt: string | AsyncIterable<SDKUserMessage>,
|
||||
options?: Options
|
||||
}): Query // extends AsyncGenerator<SDKMessage, void>
|
||||
```
|
||||
|
||||
### Message Types (Event Stream)
|
||||
|
||||
```typescript
|
||||
type SDKMessage =
|
||||
| SystemMessage // subtype: "init" | "compact_boundary"
|
||||
| AssistantMessage // Claude's response with tool calls
|
||||
| UserMessage // Tool results fed back
|
||||
| StreamEvent // Raw API stream events (opt-in)
|
||||
| ResultMessage // Final: success | error_max_turns | error_max_budget_usd | error_during_execution
|
||||
| CompactBoundaryMessage; // Context compaction marker
|
||||
```
|
||||
|
||||
### Tool Approval (HITL)
|
||||
|
||||
```typescript
|
||||
canUseTool: async (toolName: string, input: Record<string, any>) =>
|
||||
Promise<
|
||||
| { behavior: 'allow'; updatedInput: Record<string, any> }
|
||||
| { behavior: 'deny'; message: string }
|
||||
>;
|
||||
```
|
||||
|
||||
### Subagent Definition
|
||||
|
||||
```typescript
|
||||
interface AgentDefinition {
|
||||
description: string; // When to invoke
|
||||
prompt: string; // System prompt
|
||||
tools?: string[]; // Available tools (defaults to all)
|
||||
model?: 'sonnet' | 'opus' | 'haiku' | 'inherit';
|
||||
}
|
||||
```
|
||||
|
||||
### Session Management
|
||||
|
||||
```typescript
|
||||
interface Options {
|
||||
continue?: boolean; // Resume most recent session
|
||||
resume?: string; // Resume by session ID
|
||||
forkSession?: boolean; // Branch from resume point
|
||||
persistSession?: boolean; // Default: true
|
||||
maxTurns?: number;
|
||||
maxBudgetUsd?: number; // Spend limit
|
||||
permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions';
|
||||
structuredOutput?: { type: "json_schema", ... };
|
||||
}
|
||||
```
|
||||
|
||||
### Result (Termination)
|
||||
|
||||
```typescript
|
||||
interface SDKResultMessage {
|
||||
type: 'result';
|
||||
subtype:
|
||||
| 'success'
|
||||
| 'error_max_turns'
|
||||
| 'error_max_budget_usd'
|
||||
| 'error_during_execution'
|
||||
| 'error_max_structured_output_retries';
|
||||
result?: string;
|
||||
total_cost_usd: number;
|
||||
usage: { input_tokens: number; output_tokens: number };
|
||||
num_turns: number;
|
||||
session_id: string;
|
||||
stop_reason: string | null; // "end_turn", "max_tokens", "refusal"
|
||||
}
|
||||
```
|
||||
|
||||
### V2 Preview (Simpler API)
|
||||
|
||||
```typescript
|
||||
await using session = unstable_v2_createSession({ model: "..." });
|
||||
await session.send("Hello!");
|
||||
for await (const msg of session.stream()) { ... }
|
||||
await session.send("Follow-up");
|
||||
for await (const msg of session.stream()) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. OpenAI Codex SDK / Responses API — Key Interfaces
|
||||
|
||||
### Codex SDK (TypeScript)
|
||||
|
||||
```typescript
|
||||
// Client
|
||||
const codex = new Codex({ env?, config? });
|
||||
const thread = codex.startThread({ workingDirectory?, skipGitRepoCheck? });
|
||||
const thread = codex.resumeThread(threadId);
|
||||
|
||||
// Execution
|
||||
const turn = await thread.run(prompt: string | InputEntry[], options?);
|
||||
const { events } = await thread.runStreamed(prompt);
|
||||
|
||||
// Streaming
|
||||
for await (const event of events) {
|
||||
switch (event.type) {
|
||||
case "item.completed": // event.item
|
||||
case "turn.completed": // event.usage
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Responses API Streaming Events (53 types)
|
||||
|
||||
Organized hierarchically:
|
||||
|
||||
**Response Lifecycle (7):**
|
||||
|
||||
- `response.queued`, `response.created`, `response.in_progress`
|
||||
- `response.completed`, `response.incomplete`, `response.failed`
|
||||
- `error`
|
||||
|
||||
**Content Streaming (8):**
|
||||
|
||||
- `response.output_item.added`, `response.output_item.done`
|
||||
- `response.content_part.added`, `response.content_part.done`
|
||||
- `response.output_text.delta`, `response.output_text.done`
|
||||
- `response.refusal.delta`, `response.refusal.done`
|
||||
|
||||
**Reasoning (6):**
|
||||
|
||||
- `response.reasoning_text.delta`, `response.reasoning_text.done`
|
||||
- `response.reasoning_summary_part.added`,
|
||||
`response.reasoning_summary_part.done`
|
||||
- `response.reasoning_summary_text.delta`,
|
||||
`response.reasoning_summary_text.done`
|
||||
|
||||
**Function Calls (2):**
|
||||
|
||||
- `response.function_call_arguments.delta`,
|
||||
`response.function_call_arguments.done`
|
||||
|
||||
**MCP (8):**
|
||||
|
||||
- `response.mcp_call_arguments.delta`, `response.mcp_call_arguments.done`
|
||||
- `response.mcp_call.in_progress`, `response.mcp_call.completed`,
|
||||
`response.mcp_call.failed`
|
||||
- `response.mcp_list_tools.in_progress`, `response.mcp_list_tools.completed`,
|
||||
`response.mcp_list_tools.failed`
|
||||
|
||||
**Built-in Tools (15):**
|
||||
|
||||
- File search: `in_progress`, `searching`, `completed`
|
||||
- Web search: `in_progress`, `searching`, `completed`
|
||||
- Code interpreter: `in_progress`, `interpreting`, `code.delta`, `code.done`,
|
||||
`completed`
|
||||
- Image gen: `in_progress`, `generating`, `partial_image`, `completed`
|
||||
|
||||
**Audio (4):**
|
||||
|
||||
- `response.audio.delta`, `response.audio.done`
|
||||
- `response.audio.transcript.delta`, `response.audio.transcript.done`
|
||||
|
||||
**Annotations (1):**
|
||||
|
||||
- `response.output_text.annotation.added`
|
||||
|
||||
### OpenAI Agents SDK (higher-level)
|
||||
|
||||
```python
|
||||
# Python-first, but patterns apply
|
||||
class RunItemStreamEvent:
|
||||
name: Literal[
|
||||
"message_output_created",
|
||||
"handoff_requested",
|
||||
"handoff_occurred",
|
||||
"tool_called",
|
||||
"tool_output",
|
||||
"tool_search_called",
|
||||
"tool_search_output_created",
|
||||
"reasoning_item_created",
|
||||
"mcp_approval_requested",
|
||||
"mcp_approval_response",
|
||||
"mcp_list_tools",
|
||||
]
|
||||
|
||||
class AgentUpdatedStreamEvent:
|
||||
# Fires when current agent changes (handoff)
|
||||
new_agent: Agent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Superset Analysis — What Changes Our Interfaces?
|
||||
|
||||
### Concepts Present in ALL Systems
|
||||
|
||||
| Concept | gemini-cli | ADK-TS | Claude SDK | Codex/OpenAI | Our Interfaces |
|
||||
| --------------------- | ---------- | ------ | ------------- | -------------- | ----------------------- |
|
||||
| Text streaming | ✅ | ✅ | ✅ | ✅ | ✅ MessageEvent |
|
||||
| Tool request/response | ✅ | ✅ | ✅ | ✅ | ✅ ToolRequest/Response |
|
||||
| Thinking/reasoning | ✅ | ✅ | ✅ (thinking) | ✅ (reasoning) | ✅ ContentPart.thought |
|
||||
| Error events | ✅ | ✅ | ✅ | ✅ | ✅ ErrorEvent |
|
||||
| Token usage | ✅ | ✅ | ✅ | ✅ | ✅ UsageEvent |
|
||||
| Tool progress | ✅ | ✅ | — | ✅ | ✅ ToolUpdateEvent |
|
||||
| Session resume | ✅ | ✅ | ✅ | ✅ | ✅ sessionRef |
|
||||
| Subagents | ✅ | ✅ | ✅ | — | ✅ threadId |
|
||||
| Abort/cancel | ✅ | ✅ | ✅ | ✅ | ✅ abort() |
|
||||
| Metadata escape hatch | — | ✅ | — | — | ✅ \_meta |
|
||||
|
||||
### NEW Concepts From Claude/Codex That We Should Incorporate
|
||||
|
||||
#### 4.1 Structured Stream End Reasons (HIGH PRIORITY)
|
||||
|
||||
**What:** Claude SDK has typed termination:
|
||||
`success | error_max_turns | error_max_budget_usd | error_during_execution`.
|
||||
OpenAI has `completed | incomplete | failed`.
|
||||
|
||||
**Why it matters:** We need a `stream_end` event that captures why the stream
|
||||
ended — the one signal not covered by other event types.
|
||||
|
||||
**Final design — `stream_end` with `reason` + open `data` bag:**
|
||||
|
||||
```typescript
|
||||
type StreamEndReason =
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'aborted'
|
||||
| 'max_turns'
|
||||
| 'max_budget'
|
||||
| 'max_time'
|
||||
| 'refusal'
|
||||
| (string & {});
|
||||
|
||||
interface StreamEnd {
|
||||
reason: StreamEndReason;
|
||||
data?: Record<string, unknown>; // { result?, cost?, usage?, numTurns?, error?, ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Design rationale:**
|
||||
|
||||
- Start is covered by `initialize`. Pausing is implicit (elicitation/tool
|
||||
request events). Handoff is a tool call (`transfer_to_agent`).
|
||||
- End-of-stream details go in `data` as an open bag, not fixed fields.
|
||||
|
||||
#### 4.2 Budget Constraints (MEDIUM PRIORITY)
|
||||
|
||||
**What:** Claude SDK has `maxBudgetUsd`. Neither gemini-cli nor ADK has this
|
||||
today.
|
||||
|
||||
**Why it matters:** Cost control is critical for production deployments.
|
||||
|
||||
**Proposed change to AgentConstraints:**
|
||||
|
||||
```typescript
|
||||
interface AgentConstraints {
|
||||
maxTurns?: number;
|
||||
maxTimeMinutes?: number;
|
||||
maxLlmCalls?: number;
|
||||
maxBudgetUsd?: number; // NEW: from Claude SDK
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3 Session Forking (MEDIUM PRIORITY)
|
||||
|
||||
**What:** Claude SDK supports `forkSession: boolean` — branch from a resume
|
||||
point to explore alternatives.
|
||||
|
||||
**Why it matters:** Enables "what if" exploration without destroying history.
|
||||
Useful for plan mode.
|
||||
|
||||
**Proposed change to ExecutionRequest:**
|
||||
|
||||
```typescript
|
||||
interface ExecutionRequest {
|
||||
// ... existing fields ...
|
||||
sessionRef?: string | SessionSnapshot;
|
||||
forkSession?: boolean; // NEW: branch from sessionRef instead of continuing
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.4 Permission Modes on Execution (MEDIUM PRIORITY)
|
||||
|
||||
**What:** Claude has 5 permission modes:
|
||||
`default | acceptEdits | plan | dontAsk | bypassPermissions`. gemini-cli has 4
|
||||
approval modes: `default | autoEdit | yolo | plan`.
|
||||
|
||||
**Why it matters:** Both systems have this concept. It should be in
|
||||
ExecutionOptions, not hard-coded.
|
||||
|
||||
**Proposed change to ExecutionOptions:**
|
||||
|
||||
```typescript
|
||||
interface ExecutionOptions {
|
||||
// ... existing fields ...
|
||||
permissionMode?: string; // Open string. Conventions: 'default' | 'auto_edit' | 'autonomous' | 'plan' | string
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.5 Agent Handoff (MEDIUM PRIORITY)
|
||||
|
||||
**What:** OpenAI Agents SDK has explicit `handoff_requested` /
|
||||
`handoff_occurred` events plus `AgentUpdatedStreamEvent`. ADK has
|
||||
`transfer_to_agent` tool + `eventActions.transferToAgent`. Claude SDK has
|
||||
subagent invocation via Agent tool.
|
||||
|
||||
**Why it matters:** When agent A delegates to agent B, the host/UI needs to
|
||||
know.
|
||||
|
||||
**Design decision: Handoff is a tool call, not a separate event type.**
|
||||
|
||||
The agent calls `transfer_to_agent` as a tool (ToolRequest event). The host
|
||||
intercepts this tool call (since host controls tool execution), looks up the
|
||||
target agent, creates a new executor via the factory, and mediates the handoff.
|
||||
The originating agent's stream ends with `stream_end` reason `'completed'`.
|
||||
|
||||
```typescript
|
||||
// 1. Agent emits tool request:
|
||||
{ type: 'tool_request', name: 'transfer_to_agent', args: { target: 'coder', reason: '...' } }
|
||||
|
||||
// 2. Host mediates handoff, originating agent completes:
|
||||
{ type: 'stream_end', reason: 'completed', agentId: 'planner', data: { handoffTarget: 'coder' } }
|
||||
```
|
||||
|
||||
This avoids duplicating routing logic between stream_end events and tool calls.
|
||||
Matches ADK's `transfer_to_agent` tool pattern.
|
||||
|
||||
#### 4.6 Refusal as Distinct Signal (LOW PRIORITY)
|
||||
|
||||
**What:** OpenAI has explicit `response.refusal.delta/done` events. Claude has
|
||||
`stop_reason: "refusal"`.
|
||||
|
||||
**Why it matters:** Model refusals are operationally important (safety, policy).
|
||||
|
||||
**Proposed:** No new event type. Handle via `MessageEvent` with a `refusal`
|
||||
content part type, or via `ErrorEvent` with specific error code. ContentPart can
|
||||
be extended:
|
||||
|
||||
```typescript
|
||||
| { type: 'refusal'; text: string }
|
||||
```
|
||||
|
||||
#### 4.7 Content Annotations (LOW PRIORITY)
|
||||
|
||||
**What:** OpenAI has `response.output_text.annotation.added` for citations, file
|
||||
paths.
|
||||
|
||||
**Why it matters:** Citations and source attribution are increasingly important.
|
||||
|
||||
**Proposed:** Michael's `reference` ContentPart already covers this. No change
|
||||
needed — `reference` with `uri` and `text` handles citations.
|
||||
|
||||
#### 4.8 Context Compaction Events (LOW PRIORITY)
|
||||
|
||||
**What:** Claude SDK has `CompactBoundaryMessage` marking when context was
|
||||
compressed.
|
||||
|
||||
**Why it matters:** For long sessions, knowing when context was compressed helps
|
||||
with debugging and UI.
|
||||
|
||||
**Proposed:** `CustomEvent` with `kind: 'compact_boundary'`. No new event type
|
||||
needed.
|
||||
|
||||
#### 4.9 Structured Output Schema (ALREADY COVERED)
|
||||
|
||||
**What:** Both Claude (`structuredOutput`) and OpenAI support JSON Schema output
|
||||
constraints.
|
||||
|
||||
**Status:** Already covered by `AgentDescriptor.outputSchema: JsonSchema`. No
|
||||
change needed.
|
||||
|
||||
### Concepts We DON'T Need to Adopt
|
||||
|
||||
| Concept | Why Skip |
|
||||
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| OpenAI's 53 granular streaming events | Too coupled to Responses API internals. Our `ToolUpdateEvent` + `MessageEvent` via AsyncGenerator abstracts over this. |
|
||||
| OpenAI's per-tool-type events (file_search, web_search, code_interpreter) | Tool-specific progress belongs in `ToolUpdateEvent.data`, not in the event taxonomy. |
|
||||
| Audio/Image streaming events | Handle via `ToolUpdateEvent` with media ContentParts. When needed, add as ContentPart types, not event types. |
|
||||
| Claude's raw `StreamEvent` wrapper | Implementation detail of the Claude API client. Our adapters consume these internally. |
|
||||
| MCP-specific events (mcp_call, mcp_list_tools) | MCP tools are just tools. Use generic `ToolRequestEvent/ToolResponseEvent`. MCP approval is an `ElicitationRequest`. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Updated Event Type Comparison (Full Superset)
|
||||
|
||||
| # | Event Type | Michael | Our Outline | Claude SDK | OpenAI | Verdict |
|
||||
| --- | -------------------- | ------- | ----------- | ----------------------------- | --------------------------------- | ---------------------------------------------- |
|
||||
| 1 | Initialize | ✅ | ✅ | SystemMessage(init) | — | **Keep** |
|
||||
| 2 | Session Update | ✅ | ✅ | — | — | **Keep** |
|
||||
| 3 | Message | ✅ | ✅ | AssistantMessage | output_text.delta/done | **Keep** |
|
||||
| 4 | Tool Request | ✅ | ✅ | AssistantMessage.tool_use | function_call_arguments | **Keep** |
|
||||
| 5 | Tool Update | ✅ | ✅ | — | per-tool progress events | **Keep** |
|
||||
| 6 | Tool Response | ✅ | ✅ | UserMessage | — | **Keep** |
|
||||
| 7 | Elicitation Request | ✅ | ✅ | canUseTool callback | mcp_approval_requested | **Keep** |
|
||||
| 8 | Elicitation Response | ✅ | ✅ | canUseTool return | mcp_approval_response | **Keep** |
|
||||
| 9 | Usage | ✅ | ✅ | ResultMessage.usage | response.completed | **Keep** |
|
||||
| 10 | Error | ✅ | ✅ | ResultMessage(error\_\*) | response.failed | **Keep** |
|
||||
| 11 | Custom | ✅ | ✅ | — | — | **Keep** |
|
||||
| 12 | StreamEnd | — | ✅ | ResultMessage + SystemMessage | response.created/completed/failed | **Keep — `stream_end` with `reason` + `data`** |
|
||||
|
||||
**Result: Our 12 event types are the right abstraction level.** Claude and
|
||||
OpenAI validate every category. The granularity differences (OpenAI's 53 vs
|
||||
our 12) are implementation details that adapters handle internally. `stream_end`
|
||||
uses a single `reason` field with an open `data` bag. Handoff is a tool call.
|
||||
Pausing is implicit.
|
||||
|
||||
---
|
||||
|
||||
## 6. Updated ContentPart Types (Superset)
|
||||
|
||||
```typescript
|
||||
type ContentPart = (
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'thought'; thought: string; thoughtSignature?: string }
|
||||
| { type: 'media'; data?: string; uri?: string; mimeType?: string }
|
||||
| {
|
||||
type: 'reference';
|
||||
text: string;
|
||||
data?: string;
|
||||
uri?: string;
|
||||
mimeType?: string;
|
||||
}
|
||||
| { type: 'refusal'; text: string } // NEW: from OpenAI
|
||||
) &
|
||||
// Future: type: string for unknown types from new SDKs
|
||||
{ _meta?: Record<string, unknown> };
|
||||
```
|
||||
|
||||
Adding `refusal` as a ContentPart type (rather than a new event) keeps the event
|
||||
taxonomy stable while supporting model refusals from both Claude and OpenAI.
|
||||
|
||||
---
|
||||
|
||||
## 7. Key Architectural Patterns Across SDKs
|
||||
|
||||
### Pattern: Execution Entry Points
|
||||
|
||||
| SDK | Entry Point | Multi-turn Pattern |
|
||||
| ----------- | ------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| Michael | `agent.send(trajectory, data)` / `session.send()` + `session.update()` | Same method / three-method session |
|
||||
| Our Outline | `session.stream(data)` + `session.update(config)` + `session.steer(data)` | Four-method session (stream/update/steer/abort) |
|
||||
| Claude SDK | `query({ prompt, options })` | New `query()` call with `resume: sessionId` |
|
||||
| Claude V2 | `session.send()` + `session.stream()` | Separate send/stream |
|
||||
| Codex SDK | `thread.run(prompt)` / `thread.runStreamed(prompt)` | Same thread object |
|
||||
|
||||
**Observation:** Claude V2 and Codex both use a stateful session/thread object
|
||||
with send+stream. Michael uses a single `send()` method. Our `stream()` method
|
||||
is the unified version — the first call starts, subsequent calls continue (like
|
||||
ADK's `runAsync()`).
|
||||
|
||||
### Pattern: Tool Approval
|
||||
|
||||
| SDK | Pattern | Sync/Async |
|
||||
| ----------- | -------------------------------------------------------- | ------------------------ |
|
||||
| gemini-cli | PolicyEngine + ConfirmationBus | Async (message bus) |
|
||||
| ADK-TS | SecurityPlugin.policyCheck() | Async (plugin callback) |
|
||||
| Claude SDK | `canUseTool()` callback | Async (callback) |
|
||||
| OpenAI | `mcp_approval_requested` event | Event-based |
|
||||
| Our Outline | `ElicitationRequest` event + `PolicyEvaluator` interface | Both (event + interface) |
|
||||
|
||||
**Observation:** Our approach covers both patterns — the `ElicitationRequest`
|
||||
event for event-based approval (like OpenAI), and the `PolicyEvaluator`
|
||||
interface for synchronous policy checks (like gemini-cli/ADK/Claude). This is
|
||||
the right superset.
|
||||
|
||||
### Pattern: Subagent Definition
|
||||
|
||||
| SDK | Pattern | Key Fields |
|
||||
| ------------- | -------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| gemini-cli | `AgentDefinition` (local/remote) | name, description, kind, tools, model |
|
||||
| ADK-TS | `BaseAgentConfig` | name, description, subAgents, tools |
|
||||
| Claude SDK | `AgentDefinition` | description, prompt, tools, model |
|
||||
| OpenAI Agents | `Agent` class | name, instructions, tools, handoffs, model |
|
||||
| Our Outline | `AgentDescriptor` | name, description, executor, inputSchema, capabilities, ownTools, requiredTools, subAgents |
|
||||
|
||||
**Observation:** Our `AgentDescriptor` is the most complete. Claude's `prompt`
|
||||
field and OpenAI's `instructions` are executor-level concerns (system prompt),
|
||||
not descriptor-level. The descriptor declares identity; the executor uses the
|
||||
prompt. This separation is correct.
|
||||
|
||||
One gap: **handoffs**. OpenAI Agents has an explicit `handoffs` field listing
|
||||
which agents can be delegated to. Our `subAgents` field serves the same purpose
|
||||
but the naming implies hierarchy rather than peer delegation. Consider whether
|
||||
`subAgents` should be renamed to `delegateAgents` or kept as-is with
|
||||
documentation clarifying it covers both hierarchical and peer delegation.
|
||||
|
||||
---
|
||||
|
||||
## 8. Concrete Changes to outline.md
|
||||
|
||||
Based on this analysis, the following changes should be made:
|
||||
|
||||
### Applied (validated by multiple SDKs):
|
||||
|
||||
1. ✅ **`type: AgentEventType`** with known values + `(string & {})`
|
||||
(autocomplete + extensibility)
|
||||
2. ✅ **`interface AgentEvents` + mapped type** (adopted from Michael for
|
||||
declaration merging)
|
||||
3. ✅ **`agentId` on event base** (which agent emitted this event)
|
||||
4. ✅ **`_meta` on ContentPart** (aligned with Michael)
|
||||
5. ✅ **`stream_end` event** — signals why the stream ended, with `reason`
|
||||
field + open `data` bag
|
||||
6. ✅ **Handoff as tool call** — `transfer_to_agent` tool, not a separate event
|
||||
7. ✅ **`maxBudgetUsd` in AgentConstraints** (Claude SDK, increasingly standard)
|
||||
8. ✅ **`refusal` ContentPart type** (both Claude and OpenAI surface refusals)
|
||||
9. ✅ **`forkSession` in ExecutionRequest** (Claude SDK, valuable for
|
||||
exploration)
|
||||
10. ✅ **`permissionMode` in ExecutionOptions** (both gemini-cli and Claude SDK)
|
||||
11. ✅ **`cost` field on Usage** (Claude SDK tracks total_cost_usd)
|
||||
|
||||
### Correctly abstracted (no change needed):
|
||||
|
||||
- Event taxonomy (12 types) — validated as right abstraction level
|
||||
- `AgentDescriptor` shape — most complete across all SDKs
|
||||
- `AgentSession.stream/update/steer/abort` — covers all SDK patterns
|
||||
- ToolUpdate — correctly abstracts over OpenAI's 15+ tool-specific progress
|
||||
events
|
||||
- `ElicitationRequest/Response` — covers both callback and event patterns
|
||||
- `ContentPart` types — text/thought/media/reference/refusal
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [Claude Agent SDK TypeScript Reference](https://platform.claude.com/docs/en/agent-sdk/typescript)
|
||||
- [Claude Agent SDK Streaming](https://platform.claude.com/docs/en/agent-sdk/streaming-output)
|
||||
- [Claude Agent SDK Sessions](https://platform.claude.com/docs/en/agent-sdk/sessions)
|
||||
- [Claude Agent SDK Subagents](https://platform.claude.com/docs/en/agent-sdk/subagents)
|
||||
- [OpenAI Codex SDK TypeScript](https://github.com/openai/codex/tree/main/sdk/typescript)
|
||||
- [OpenAI Codex SDK Docs](https://developers.openai.com/codex/sdk/)
|
||||
- [OpenAI Responses API Streaming Events](https://developers.openai.com/api/reference/resources/responses/streaming-events/)
|
||||
- [OpenAI Agents SDK Streaming](https://openai.github.io/openai-agents-python/streaming/)
|
||||
- [Responses API Streaming Guide (Community)](https://community.openai.com/t/responses-api-streaming-the-simple-guide-to-events/1363122)
|
||||
@@ -1,259 +0,0 @@
|
||||
# Gemini CLI Architecture Notes
|
||||
|
||||
## Project Structure
|
||||
|
||||
**Monorepo packages:**
|
||||
|
||||
- `packages/core/` - Main execution engine (the big one)
|
||||
- `packages/cli/` - CLI frontend
|
||||
- `packages/sdk/` - SDK for extensions
|
||||
- `packages/a2a-server/` - Agent-to-agent server
|
||||
- `packages/devtools/` - Dev utilities
|
||||
- `packages/vscode-ide-companion/` - VS Code extension
|
||||
|
||||
## Core Execution Loop
|
||||
|
||||
### GeminiClient (`core/src/core/client.ts` ~38KB)
|
||||
|
||||
- **Primary orchestrator** for user interactions
|
||||
- Manages session lifecycle, message routing, model selection
|
||||
- Coordinates hooks, context management, error recovery
|
||||
- Enforces `MAX_TURNS = 100` per session
|
||||
- Tracks `currentSequenceModel` for multi-turn stickiness
|
||||
- Handles history compression when context grows
|
||||
|
||||
### GeminiChat (`core/src/core/geminiChat.ts` ~34KB)
|
||||
|
||||
- Bidirectional LLM communication
|
||||
- Maintains `history[]` alternating user/model turns
|
||||
- Retry logic: max 2 attempts, 500ms delay for invalid responses
|
||||
- Fires `BeforeModel` and `AfterModel` hooks
|
||||
- Integrates ChatRecordingService for persistence
|
||||
|
||||
### Scheduler (`core/src/scheduler/scheduler.ts` ~23KB)
|
||||
|
||||
- **Three-phase event-driven**: Ingestion → Processing → Completion
|
||||
- Tool call state machine:
|
||||
`Validating → AwaitingApproval → Scheduled → Executing → Terminal`
|
||||
- Terminal states: `Success`, `Error`, `Cancelled`
|
||||
- Parallel execution for read-only and agent-type tools
|
||||
- Yields to event loop for user approval
|
||||
- Publishes state changes via MessageBus
|
||||
|
||||
### CoreToolScheduler (`core/src/core/coreToolScheduler.ts` ~38KB)
|
||||
|
||||
- Sequential, queue-based tool processing
|
||||
- Validates policy via PolicyEngine
|
||||
- Confirmation handling via ToolModificationHandler (editor integration)
|
||||
- Uses MessageBus for async confirmation responses
|
||||
|
||||
## Tool System
|
||||
|
||||
### DeclarativeTool Pattern
|
||||
|
||||
- **Separation of concerns**: build() → validate → createInvocation() →
|
||||
execute()
|
||||
- `ToolBuilder` defines metadata (name, displayName, description, kind) + schema
|
||||
via `getSchema()`
|
||||
- `ToolInvocation` has: `getDescription()`, `toolLocations()`,
|
||||
`shouldConfirmExecute()`, `execute()`
|
||||
- `ToolResult` contains: `llmContent` (for LLM), `returnDisplay` (for UI), error
|
||||
details, tail calls
|
||||
|
||||
### BaseToolInvocation
|
||||
|
||||
- Abstract base with MessageBus integration for policy/confirmation
|
||||
- Three decision paths: ALLOW, DENY, ASK_USER via `getMessageBusDecision()`
|
||||
|
||||
### ToolRegistry (`core/src/tools/tool-registry.ts`)
|
||||
|
||||
- Registers tools via `registerTool()`
|
||||
- MCP tools with fully qualified names: `mcp_serverName_toolName`
|
||||
- Priority sorting: built-in → discovered → MCP (by server name)
|
||||
- Filters by active status based on configuration
|
||||
|
||||
### Confirmation System
|
||||
|
||||
- `ToolCallConfirmationDetails` union: edit, execute, MCP, info, ask_user,
|
||||
exit_plan_mode
|
||||
- `ToolConfirmationOutcome` enum: ProceedOnce, ProceedAlways, etc.
|
||||
- Async confirmation via MessageBus pub/sub
|
||||
|
||||
## Hooks System
|
||||
|
||||
### Hook Types (11 hook points)
|
||||
|
||||
| Hook | Trigger | Key Capability |
|
||||
| --------------------- | ----------------------- | --------------------------------- |
|
||||
| `BeforeTool` | Before tool execution | Modify tool_input |
|
||||
| `AfterTool` | After tool completion | Context injection, tail calls |
|
||||
| `BeforeAgent` | Before agent prompt | Additional context |
|
||||
| `AfterAgent` | After agent response | Clear context flag |
|
||||
| `BeforeModel` | Before LLM request | Modify request or inject response |
|
||||
| `AfterModel` | After LLM response | Modify response |
|
||||
| `BeforeToolSelection` | Before tool selection | Modify toolConfig |
|
||||
| `Notification` | When notifications fire | Suppress/modify message |
|
||||
| `SessionStart` | Session begins | Additional context |
|
||||
| `SessionEnd` | Session terminates | Cleanup |
|
||||
| `PreCompress` | Before compression | Suppress/modify |
|
||||
|
||||
### Hook Output Fields (common to all hooks)
|
||||
|
||||
- `continue` - Whether execution proceeds
|
||||
- `stopReason` - Reason to halt
|
||||
- `suppressOutput` - Hide from user
|
||||
- `systemMessage` - Add to system context
|
||||
- `decision` - ask/block/deny/approve/allow
|
||||
|
||||
### Hook System Components
|
||||
|
||||
- `HookSystem` - Main coordinator
|
||||
- `HookRegistry` - Stores/manages configurations
|
||||
- `HookRunner` - Executes registered hooks
|
||||
- `HookAggregator` - Combines multiple hook results
|
||||
- `HookPlanner` - Determines execution order
|
||||
- `HookEventHandler` - Orchestrates event firing
|
||||
- `HookTranslator` - Converts between formats
|
||||
|
||||
## Policy Engine
|
||||
|
||||
### Rule Structure
|
||||
|
||||
```
|
||||
PolicyRule {
|
||||
toolName: string; // wildcards supported
|
||||
decision: PolicyDecision; // ALLOW | DENY | ASK_USER
|
||||
priority: number;
|
||||
argsPattern?: RegExp; // conditional on args
|
||||
mcpName?: string;
|
||||
source: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Tier Hierarchy (lowest → highest priority)
|
||||
|
||||
1. Default (1) - Core built-in policies
|
||||
2. Extension (2) - Extension contributions
|
||||
3. Workspace (3) - Project-scoped (.gemini/)
|
||||
4. User (4) - User-provided (~/.gemini/)
|
||||
5. Admin (5) - System-level policies
|
||||
|
||||
### Dynamic Rule Priorities (within User Tier)
|
||||
|
||||
- 4.9 - MCP_EXCLUDED (persistent server blocks)
|
||||
- 4.4 - EXCLUDE_TOOLS_FLAG (CLI exclusions)
|
||||
- 4.3 - ALLOWED_TOOLS_FLAG (CLI allows)
|
||||
- 4.2 - TRUSTED_MCP_SERVER
|
||||
- 4.1 - ALLOWED_MCP_SERVER
|
||||
- 3.95 - ALWAYS_ALLOW (interactive selections)
|
||||
|
||||
### Security Constraint
|
||||
|
||||
- Extensions CANNOT contribute ALLOW rules or YOLO mode
|
||||
|
||||
## Agent System
|
||||
|
||||
### Agent Registry (`core/src/agents/registry.ts`)
|
||||
|
||||
Discovery sources:
|
||||
|
||||
1. Built-in: CodebaseInvestigator, CliHelp, Generalist, Browser
|
||||
2. User-level: `~/.gemini/agents/`
|
||||
3. Project-level: `.gemini/agents/` (requires folder trust)
|
||||
4. Extension-based: From active extensions
|
||||
|
||||
### LocalAgentExecutor (`core/src/agents/local-executor.ts`)
|
||||
|
||||
- Prompt processing: input augmentation → template expansion → system prompt
|
||||
construction
|
||||
- Uses GeminiChat for accumulating conversation
|
||||
- ChatCompressionService for history management
|
||||
- Turn loop: invoke model → extract function calls → check auth → append results
|
||||
- Termination: complete_task tool, max turns, timeout
|
||||
|
||||
### SubagentTool (`core/src/agents/subagent-tool.ts`)
|
||||
|
||||
- Extends BaseDeclarativeTool - agents invoked like standard tools
|
||||
- Read-only status checking, user hint propagation
|
||||
- Execution: validate → optional confirmation → parameter enrichment →
|
||||
SubagentToolWrapper
|
||||
|
||||
### Remote Agents
|
||||
|
||||
- A2A client manager for agent-to-agent protocol
|
||||
- Remote invocation for external agents
|
||||
- Agent acknowledgement system (security for project agents)
|
||||
|
||||
## Model System
|
||||
|
||||
### ModelConfigService
|
||||
|
||||
- **Hierarchical alias system**: children override parents
|
||||
- Resolution: alias chain → level assignment → apply overrides
|
||||
- Deep merging with array override capability
|
||||
- Fallback to `chat-base` alias for unknown models
|
||||
|
||||
### ModelRouterService
|
||||
|
||||
Sequential strategy pattern:
|
||||
|
||||
1. Fallback & Override
|
||||
2. Approval Mode Strategy
|
||||
3. Gemma Classifier (if enabled)
|
||||
4. Generic Classifier
|
||||
5. Numerical Classifier
|
||||
6. Default Strategy
|
||||
|
||||
### ModelAvailabilityService
|
||||
|
||||
Health states:
|
||||
|
||||
- **Terminal** - permanently unavailable
|
||||
- **Sticky Retry** - failed once, can retry once per turn
|
||||
- **Healthy** - no issues
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Purpose |
|
||||
| --------------------------- | --------------------------------------- |
|
||||
| ChatRecordingService | Session persistence (JSON files) |
|
||||
| ChatCompressionService | History summarization for token budgets |
|
||||
| ModelConfigService | Hierarchical model config with aliases |
|
||||
| ModelAvailabilityService | Model health tracking |
|
||||
| ModelRouterService | Model selection via strategies |
|
||||
| FolderTrustDiscoveryService | Workspace security scanning |
|
||||
| KeychainService | Credential storage |
|
||||
| LoopDetectionService | Detect repetitive agent loops |
|
||||
|
||||
## UI + Core Separation
|
||||
|
||||
### IDE Client (`core/src/ide/ide-client.ts`)
|
||||
|
||||
- Singleton managing CLI ↔ IDE communication via MCP
|
||||
- **Outbound** (CLI → IDE): `openDiff`, `closeDiff`
|
||||
- **Inbound** (IDE → CLI): `ide/contextUpdate`, `ide/diffAccepted`,
|
||||
`ide/diffRejected`
|
||||
|
||||
### Event Contract
|
||||
|
||||
```typescript
|
||||
interface IdeContextNotification {
|
||||
method: 'ide/contextUpdate';
|
||||
params: { workspaceState: { openFiles: string[]; isTrusted: boolean } };
|
||||
}
|
||||
```
|
||||
|
||||
### Confirmation Bus
|
||||
|
||||
- `TOOL_CONFIRMATION_REQUEST` / `TOOL_CONFIRMATION_RESPONSE`
|
||||
- Detail types: edit, execute, MCP, info, ask_user, exit_plan_mode
|
||||
- Async pub/sub via MessageBus
|
||||
|
||||
## Configuration (`core/src/config/config.ts` ~95KB!)
|
||||
|
||||
- Tool config: core tools, allowed/excluded, MCP servers
|
||||
- File filtering: git ignore, fuzzy search, max counts, timeouts
|
||||
- Approval modes: policy engine config
|
||||
- Experiments: feature flags (GEMINI_3_1_PRO_LAUNCHED, ENABLE_ADMIN_CONTROLS,
|
||||
etc.)
|
||||
- FolderTrust: discovery scans for commands, skills, settings, MCP, hooks
|
||||
@@ -1,296 +0,0 @@
|
||||
# Deep Dive: Key Gemini-CLI Systems
|
||||
|
||||
## Hooks System (Complete)
|
||||
|
||||
### 11 Hook Points
|
||||
|
||||
| Hook | Input | Key Output Capabilities |
|
||||
| ------------------- | -------------------------------------- | --------------------------------------------- |
|
||||
| BeforeTool | toolName, toolInput, mcpContext | Modify tool_input, block/allow, systemMessage |
|
||||
| AfterTool | toolName, toolInput, toolResponse | additionalContext, tailToolCallRequest |
|
||||
| BeforeAgent | prompt | Additional context |
|
||||
| AfterAgent | prompt, response, stopHookActive | Clear context |
|
||||
| BeforeModel | llmRequest (GenerateContentParameters) | Modify llm_request OR inject llm_response |
|
||||
| AfterModel | llmRequest, llmResponse | Modify llm_response |
|
||||
| BeforeToolSelection | llmRequest | Modify toolConfig (function list, mode) |
|
||||
| Notification | type, message, details | Suppress/modify |
|
||||
| SessionStart | source (Startup/Resume/Clear) | Additional context |
|
||||
| SessionEnd | reason (Exit/Clear/Logout/etc) | Cleanup |
|
||||
| PreCompress | trigger (Manual/Auto) | Suppress/modify |
|
||||
|
||||
### Hook Configuration Types
|
||||
|
||||
- **Runtime hooks** (HookType.Runtime): JS/TS functions, registered
|
||||
programmatically
|
||||
- **Command hooks** (HookType.Command): External shell commands with JSON I/O
|
||||
|
||||
### Exit Code Semantics (Command Hooks)
|
||||
|
||||
- 0 = Success (allowed with system message)
|
||||
- 1 = Non-blocking error (warning, continues)
|
||||
- 2+ = Blocking failure (denied, stderr as reason)
|
||||
|
||||
### Hook Decision Values
|
||||
|
||||
`'ask' | 'block' | 'deny' | 'approve' | 'allow' | undefined`
|
||||
|
||||
### Execution Strategies
|
||||
|
||||
- **Parallel** (default): Promise.all(), independent
|
||||
- **Sequential** (opt-in per hook): Chained, output→input cascading
|
||||
|
||||
### Aggregation
|
||||
|
||||
- Blocking decisions: OR logic (any block → all block)
|
||||
- Field replacement: later overrides earlier
|
||||
- Tool selection: union of allowed functions, mode precedence NONE > ANY > AUTO
|
||||
|
||||
### Trust Model
|
||||
|
||||
- Project hooks require folder trust verification
|
||||
- TrustedHooksManager at `~/.gemini/trusted-hooks.json`
|
||||
- Environment sanitized for command hooks (sensitive vars removed)
|
||||
- `GEMINI_PROJECT_DIR` injected
|
||||
|
||||
### Key Insight for Abstraction
|
||||
|
||||
Hooks fire inside gemini-cli's execution loop. When ADK controls the model:
|
||||
|
||||
- BeforeModel/AfterModel still fire because AdkGeminiModel wraps GeminiChat
|
||||
- BeforeTool/AfterTool still fire because AdkToolAdapter wraps DeclarativeTool
|
||||
- This is dewitt's solution: adapters preserve hook injection points
|
||||
|
||||
**For OpenRouter or opaque agents, hooks CANNOT fire unless the agent delegates
|
||||
model/tool calls back to gemini-cli.**
|
||||
|
||||
---
|
||||
|
||||
## Policy Engine (Complete)
|
||||
|
||||
### TOML Rule Format
|
||||
|
||||
```toml
|
||||
[[rules]]
|
||||
decision = "allow" | "deny" | "ask_user"
|
||||
priority = 0-999
|
||||
toolName = "tool_name" # wildcards: *, mcp_*, mcp_server_*
|
||||
mcpName = "server_name" # MCP server filter
|
||||
argsPattern = "regex" # matches JSON-stringified args
|
||||
commandPrefix = "cmd" # shell command prefix match
|
||||
commandRegex = "regex" # shell command regex (mutually exclusive with prefix)
|
||||
modes = ["default", "autoEdit", "yolo", "plan"]
|
||||
annotations = ["read-only", "experimental"]
|
||||
allowRedirection = true # for shell commands
|
||||
allowMessage = "..." # user-facing message on allow
|
||||
denyMessage = "..." # user-facing message on deny
|
||||
```
|
||||
|
||||
### 5-Tier Priority System
|
||||
|
||||
- Tier 5 (Admin): 5.000-5.999
|
||||
- Tier 4 (User): 4.000-4.999
|
||||
- Tier 3 (Workspace): 3.000-3.999
|
||||
- Tier 2 (Extension): 2.000-2.999
|
||||
- Tier 1 (Default): 1.000-1.999
|
||||
|
||||
Formula: `tier + (priority / 1000)`
|
||||
|
||||
### 4 Approval Modes
|
||||
|
||||
1. **default** — ASK_USER decisions prompt user
|
||||
2. **autoEdit** — File writes auto-approved with safety checking (conseca)
|
||||
3. **yolo** — All auto-approved except explicit ask_user rules
|
||||
4. **plan** — Read-only, blocks modifications, allows planning docs
|
||||
|
||||
### Shell Command Safety
|
||||
|
||||
- Parses multi-command sequences (&&, ;, ||)
|
||||
- Detects injection: $(...), `...`, <(...), >(...), --flag=$(...)
|
||||
- Each subcommand evaluated independently
|
||||
- DENY overrides everything; ASK_USER escalates; ALLOW only if all pass
|
||||
- Redirections (>) downgrade ALLOW → ASK_USER unless allowRedirection=true
|
||||
|
||||
### Security Constraints
|
||||
|
||||
- Extensions cannot contribute ALLOW rules or YOLO mode
|
||||
- Regex patterns validated for ReDoS
|
||||
- Tool name typos detected via Levenshtein distance ≤3
|
||||
- Policy file integrity: SHA-256 hash checking
|
||||
|
||||
### Key Insight for Abstraction
|
||||
|
||||
Policy is evaluated at the tool execution boundary. For the interface layer:
|
||||
|
||||
- If CLI controls tool execution → policy naturally applies
|
||||
- If agent controls tool execution internally → policy bypassed (danger!)
|
||||
- This reinforces the `pauseOnToolCalls: true` approach for ADK
|
||||
- Need a `PolicyEvaluator` interface that any executor can call
|
||||
|
||||
---
|
||||
|
||||
## Tool System (Complete)
|
||||
|
||||
### Core Abstraction Chain
|
||||
|
||||
```
|
||||
ToolBuilder (metadata + schema)
|
||||
→ build(params) validates → ToolInvocation (ready to execute)
|
||||
→ shouldConfirmExecute() → execute(signal) → ToolResult
|
||||
```
|
||||
|
||||
### DeclarativeTool Pattern
|
||||
|
||||
- `build(params)` — Validate and create invocation
|
||||
- `buildAndExecute(params)` — One-step convenience
|
||||
- `validateBuildAndExecute(params)` — Non-throwing variant
|
||||
|
||||
### BaseToolInvocation
|
||||
|
||||
- Message bus integration for policy decisions
|
||||
- Three decision paths: ALLOW → execute, DENY → reject, ASK_USER → confirm
|
||||
|
||||
### ToolResult Structure
|
||||
|
||||
- `llmContent` — For LLM conversation history
|
||||
- `returnDisplay` — For UI presentation
|
||||
- `displayContent` — Additional display formatting
|
||||
- `errorDetails` — Optional error info
|
||||
- `result` — Structured data payload
|
||||
- `tailCall` — Optional chaining requests
|
||||
|
||||
### Confirmation System (6 types)
|
||||
|
||||
1. **edit** — File modification with diff
|
||||
2. **execute** — Command execution
|
||||
3. **mcp** — MCP tool with allowlist mgmt
|
||||
4. **info** — Information-only
|
||||
5. **ask_user** — General user approval
|
||||
6. **exit_plan_mode** — Plan exit notification
|
||||
|
||||
### Confirmation Outcomes (7 values)
|
||||
|
||||
ProceedOnce, ProceedAlways, ProceedAlwaysAndSave, ProceedAlwaysServer,
|
||||
ProceedAlwaysTool, ModifyWithEditor, Cancel
|
||||
|
||||
### Tool Kinds
|
||||
|
||||
- **Mutator**: Edit, Delete, Move, Execute
|
||||
- **Read-Only**: Read, Search, Fetch
|
||||
- **Other**: Think, Agent, Communicate, Plan, SwitchMode, Other
|
||||
|
||||
### MCP Tools
|
||||
|
||||
- Naming: `mcp_<server>_<toolname>` (64-char limit)
|
||||
- Schema validation via LenientJsonSchemaValidator
|
||||
- Response types: McpTextBlock, McpMediaBlock, McpResourceBlock,
|
||||
McpResourceLinkBlock
|
||||
- Transform to GenAI Parts format
|
||||
|
||||
### Error Types (20+)
|
||||
|
||||
- **Recoverable**: INVALID_TOOL_PARAMS, FILE_NOT_FOUND,
|
||||
EDIT_NO_OCCURRENCE_FOUND, SHELL_TIMEOUT, MCP_TOOL_ERROR...
|
||||
- **Fatal**: NO_SPACE_LEFT (only one!)
|
||||
|
||||
### ModifiableTool
|
||||
|
||||
- Extends DeclarativeTool with external editor support
|
||||
- `getModifyContext()` → temp files → editor opens → `getUpdatedParams()` → diff
|
||||
|
||||
---
|
||||
|
||||
## Execution Loop (Complete)
|
||||
|
||||
### LocalAgentExecutor Flow
|
||||
|
||||
1. Collect user hints, setup deadline timer
|
||||
2. **Turn loop**: executeTurn() repeatedly until completion
|
||||
3. Per-turn: compress chat → callModel() → processFunctionCalls()
|
||||
4. On limit hit: executeFinalWarningTurn() with 60s grace period
|
||||
5. Return OutputObject { result, terminate_reason }
|
||||
|
||||
### AgentTerminateMode
|
||||
|
||||
GOAL | TIMEOUT | MAX_TURNS | ABORTED | ERROR | ERROR_NO_COMPLETE_TASK_CALL
|
||||
|
||||
### SubagentTool Architecture
|
||||
|
||||
```
|
||||
Parent Agent
|
||||
└─ SubagentTool (wraps AgentDefinition as DeclarativeTool)
|
||||
└─ SubagentToolWrapper (routes by agent kind)
|
||||
├─ LocalSubagentInvocation → LocalAgentExecutor
|
||||
├─ RemoteAgentInvocation → A2AClientManager
|
||||
└─ BrowserAgentInvocation
|
||||
```
|
||||
|
||||
### Agent Types
|
||||
|
||||
- `LocalAgentDefinition` — kind: 'local', has promptConfig, modelConfig,
|
||||
runConfig, toolConfig
|
||||
- `RemoteAgentDefinition` — kind: 'remote', has agentCardUrl, auth config
|
||||
|
||||
### Key Defaults
|
||||
|
||||
- DEFAULT_MAX_TURNS = 15
|
||||
- DEFAULT_MAX_TIME_MINUTES = 5
|
||||
- A2A_TIMEOUT = 1800000 (30 min for remote agents)
|
||||
|
||||
---
|
||||
|
||||
## Services/Config (Complete)
|
||||
|
||||
### ModelConfigService
|
||||
|
||||
- **Alias chains**: Inheritance with `extends`, merged root-to-leaf
|
||||
- **Overrides**: Contextual (model, scope, retry, isChatModel), sorted by
|
||||
specificity
|
||||
- **Runtime registration**: Dynamic aliases and overrides
|
||||
- **Deep merge**: Objects merged, arrays replaced entirely
|
||||
|
||||
### ModelRouterService (Strategy Chain)
|
||||
|
||||
1. Fallback & Override → 2. Approval Mode → 3. Gemma Classifier → 4. Generic
|
||||
Classifier → 5. Numerical Classifier → 6. Default
|
||||
|
||||
### ModelAvailabilityService
|
||||
|
||||
- Terminal (permanent), Sticky_retry (one retry per turn), Healthy
|
||||
- `selectFirstAvailable()` iterates fallback chain
|
||||
- `resetTurn()` at turn boundaries enables fresh retries
|
||||
|
||||
### Config (~95KB!)
|
||||
|
||||
Central dependency injection. Initializes: ModelAvailabilityService →
|
||||
ModelConfigService → FolderTrustDiscoveryService → PolicyEngine →
|
||||
FileDiscoveryService → GitService → ToolRegistry → MCP → GeminiClient →
|
||||
HookSystem
|
||||
|
||||
### CoreEventEmitter (UI Events)
|
||||
|
||||
Event types: UserFeedback, ModelChanged, ConsoleLog, Output, RetryAttempt,
|
||||
ConsentRequest, McpProgress, Hook, QuotaChanged
|
||||
|
||||
Backlog buffering (max 10,000) with head-pointer eviction and auto-compaction.
|
||||
|
||||
### Scheduler Types
|
||||
|
||||
```typescript
|
||||
ToolCallRequestInfo {
|
||||
callId, name, args, originalRequestName,
|
||||
isClientInitiated, prompt_id, checkpoint, traceId,
|
||||
parentCallId, schedulerId
|
||||
}
|
||||
ToolCallResponseInfo {
|
||||
callId, responseParts, resultDisplay, error, errorType,
|
||||
outputFile, contentLength, data
|
||||
}
|
||||
CoreToolCallStatus: Validating → AwaitingApproval → Scheduled → Executing → Success|Error|Cancelled
|
||||
```
|
||||
|
||||
### FolderTrust
|
||||
|
||||
Scans: commands (.toml), skills (SKILL.md), settings.json, MCP servers, hooks
|
||||
Security warnings: auto-approved tools, autonomous agents, disabled trust,
|
||||
disabled sandbox Pattern: discovery → review → execution (no code runs during
|
||||
scan)
|
||||
@@ -1,349 +0,0 @@
|
||||
# Interface Priority Analysis & Open Questions
|
||||
|
||||
## The Big Picture
|
||||
|
||||
We're defining **framework-agnostic interfaces** that allow gemini-cli to:
|
||||
|
||||
1. Keep its existing execution loop working unchanged (Legacy path)
|
||||
2. Swap in ADK as an alternative runtime via config flag
|
||||
3. Eventually support OpenRouter or other agent backends
|
||||
4. Maintain all existing CLI behavior: hooks, policies, confirmations, UI events
|
||||
|
||||
## Proposed Interface Layers (Priority Order)
|
||||
|
||||
---
|
||||
|
||||
### P0 (Critical Path - Must Define First)
|
||||
|
||||
#### 1. AgentEvent / Event Stream Contract
|
||||
|
||||
**Why first:** Everything else consumes or produces these events. The UI renders
|
||||
them. The hooks intercept them. The adapters translate to/from them.
|
||||
|
||||
**Key decision:** Merge Dewitt's simpler model with Coworker's richer model?
|
||||
|
||||
**Recommendation:** Coworker's approach is more complete. Key additions:
|
||||
|
||||
- `threadId` for sub-agent tracking (AG-UI has `parentRunId`)
|
||||
- `tool_update` for progress on long-running tools
|
||||
- `elicitation_request/response` as first-class (not just tool_confirmation)
|
||||
- `usage` event for token tracking
|
||||
- `_meta` escape hatch (matches AG-UI's extensibility philosophy)
|
||||
- `initialize` event (matches AG-UI's RunStarted)
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- Do we need AG-UI's start/content/end triple pattern for streaming? Or is
|
||||
yielding partial events sufficient?
|
||||
- How do ContentPart types map to existing gemini-cli Part types?
|
||||
- Should events carry a `source` field? (useful for hook attribution)
|
||||
|
||||
#### 2. Agent Interface
|
||||
|
||||
**Why second:** This is the primary abstraction that LocalAgentExecutor, ADK
|
||||
adapters, and future OpenRouter adapters all implement.
|
||||
|
||||
**Key decision:** Dewitt's `runAsync/runEphemeral` vs Coworker's
|
||||
`send(Trajectory|string)`
|
||||
|
||||
**Recommendation:** Hybrid approach:
|
||||
|
||||
- Dewitt's `runAsync/runEphemeral` split is ADK-aligned and cleaner for the
|
||||
factory pattern
|
||||
- BUT add Coworker's elicitation support via AgentSend union type
|
||||
- The Trajectory concept is powerful but may be too opinionated for Phase 2
|
||||
|
||||
```
|
||||
Agent<TInput, TOutput>
|
||||
name: string
|
||||
description: string
|
||||
runAsync(input, options) → AsyncGenerator<AgentEvent, TOutput>
|
||||
runEphemeral(input, options) → AsyncGenerator<AgentEvent, TOutput>
|
||||
```
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- Should Agent also support `send()` for mid-stream interactions (elicitations)?
|
||||
- How does AbortSignal propagate through the adapter boundary?
|
||||
- Do we need a `capabilities` field (supports elicitation? supports HITL? etc.)?
|
||||
|
||||
#### 3. Tool Execution Contract
|
||||
|
||||
**Why third:** Tools are the primary action mechanism. Both the policy engine
|
||||
and hooks system wrap tool execution.
|
||||
|
||||
**What needs abstracting:**
|
||||
|
||||
- Tool declaration (name, schema) — already somewhat generic via JSON Schema
|
||||
- Tool execution (args → result)
|
||||
- Tool confirmation flow (ASK_USER → user decision → proceed/deny)
|
||||
- Tool result shape (llmContent + displayContent + error + tailCalls)
|
||||
|
||||
**Key decision:** Keep DeclarativeTool pattern or flatten to a simpler
|
||||
interface?
|
||||
|
||||
**Recommendation:** Define a minimal `ToolExecutor` interface:
|
||||
|
||||
```
|
||||
ToolExecutor {
|
||||
name: string
|
||||
description: string
|
||||
schema: JSONSchema
|
||||
execute(args, context): Promise<ToolResult>
|
||||
requiresConfirmation?(args, context): Promise<boolean>
|
||||
}
|
||||
```
|
||||
|
||||
DeclarativeTool remains the concrete implementation. ADK's BaseTool adapts to
|
||||
this.
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- How do MCP tools fit? They already have their own protocol.
|
||||
- Tool annotations (destructive hints) — should these be in the interface?
|
||||
- Long-running tools need progress reporting — how does this interact with
|
||||
tool_update events?
|
||||
|
||||
---
|
||||
|
||||
### P1 (Important - Define After P0)
|
||||
|
||||
#### 4. Policy / Permission Interface
|
||||
|
||||
**Why important:** Every tool call goes through policy. External agents need
|
||||
policy enforcement too.
|
||||
|
||||
**Current state:** gemini-cli has a sophisticated TOML-based policy engine with
|
||||
tiered priorities. ADK-TS has a simpler SecurityPlugin with PolicyOutcome
|
||||
(DENY/CONFIRM/ALLOW).
|
||||
|
||||
**What needs abstracting:**
|
||||
|
||||
```
|
||||
PolicyEngine {
|
||||
evaluate(toolName, args, context): PolicyDecision // ALLOW | DENY | ASK_USER
|
||||
getExcludedTools(): string[] // Tools statically denied
|
||||
}
|
||||
```
|
||||
|
||||
**Key decision:** Do external agents (OpenRouter, etc.) get the same policy
|
||||
enforcement?
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- If an ADK agent calls a tool internally, does gemini-cli's policy apply?
|
||||
- With `pauseOnToolCalls: true` in ADK, the CLI controls execution — but what
|
||||
about headless mode?
|
||||
- How do agent-level policies work? (allow/deny entire agents, not just tools)
|
||||
- Should policy be a middleware (AG-UI pattern) or a callback (ADK plugin
|
||||
pattern)?
|
||||
|
||||
#### 5. Hooks Interface
|
||||
|
||||
**Why important:** Hooks are a major gemini-cli feature. They need to work
|
||||
regardless of which agent backend runs.
|
||||
|
||||
**Current state:** 11 hook types firing at specific lifecycle points.
|
||||
|
||||
**What needs abstracting:**
|
||||
|
||||
- Hook lifecycle must be backend-agnostic
|
||||
- BeforeModel/AfterModel hooks need to work even when ADK controls the model
|
||||
- BeforeTool/AfterTool hooks need to intercept regardless of who executes the
|
||||
tool
|
||||
|
||||
**Key challenge:** When ADK runs the model internally, gemini-cli hooks can't
|
||||
easily intercept. **Dewitt's solution:** ADK uses gemini-cli's model via
|
||||
AdkGeminiModel adapter — hooks fire inside GeminiChat.
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- If OpenRouter runs the model, how do BeforeModel/AfterModel hooks work?
|
||||
- Do we need a "model steering" abstraction (injecting context mid-stream)?
|
||||
- Can hooks be expressed as AG-UI middleware? (intercept event stream)
|
||||
|
||||
#### 6. Model / LLM Interface
|
||||
|
||||
**Why important:** Model abstraction enables swapping LLM providers.
|
||||
|
||||
**Dewitt's approach:** Exposes Model interface, ADK uses it via AdkGeminiModel
|
||||
adapter. **Coworker's approach:** Model is internal to Agent (no separate Model
|
||||
interface).
|
||||
|
||||
**Recommendation:** Keep Dewitt's separate Model interface BUT make it
|
||||
provider-agnostic:
|
||||
|
||||
- Remove `@google/genai` types from the interface signature
|
||||
- Define generic Message/Content types
|
||||
- Model interface is an implementation detail, not part of the Agent contract
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- Can we define a truly provider-agnostic Model interface?
|
||||
- Or is the Model always tied to the agent backend? (ADK uses Gemini, OpenRouter
|
||||
uses whatever)
|
||||
- Model routing (choosing which model) — is this a concern of the Model
|
||||
interface or a separate service?
|
||||
|
||||
---
|
||||
|
||||
### P2 (Important but Can Follow)
|
||||
|
||||
#### 7. Session / State Interface
|
||||
|
||||
**Current state:** gemini-cli uses ChatRecordingService (JSON files). ADK uses
|
||||
Session with BaseSessionService.
|
||||
|
||||
**What needs abstracting:**
|
||||
|
||||
- Session creation/retrieval
|
||||
- State persistence across turns
|
||||
- History/trajectory management
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- Does the trajectory (coworker's concept) replace gemini-cli's chat recording?
|
||||
- Should session state be shared between gemini-cli and the agent backend?
|
||||
|
||||
#### 8. Elicitation / User Interaction Interface
|
||||
|
||||
**What it covers:** Model fallback dialogs, tool confirmations, Ctrl+B
|
||||
interrupts, user questions
|
||||
|
||||
**Current state:** gemini-cli uses ConfirmationBus + MessageBus. AG-UI uses
|
||||
frontend tools.
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- Is elicitation just a special case of tool calls (AG-UI approach)?
|
||||
- Or is it a first-class event type (coworker's approach)?
|
||||
- How does Ctrl+B (cancel/interrupt) propagate through the agent boundary?
|
||||
|
||||
#### 9. Configuration / Capability Discovery
|
||||
|
||||
**What it covers:** Feature flags, experiment settings, agent capabilities
|
||||
|
||||
**Open questions:**
|
||||
|
||||
- How does an external agent declare its capabilities?
|
||||
- Does OpenRouter support HITL? Elicitation? Tool confirmation? Each agent may
|
||||
differ.
|
||||
- Need a `capabilities` negotiation at connection time?
|
||||
|
||||
---
|
||||
|
||||
### P3 (Future / Can Defer)
|
||||
|
||||
#### 10. A2UI / Rich UI Interface
|
||||
|
||||
- Declarative UI generation from agents
|
||||
- Not critical for Phase 2 but important for differentiation
|
||||
|
||||
#### 11. Memory / Artifact Interface
|
||||
|
||||
- ADK has memory/artifact services
|
||||
- gemini-cli has ChatRecordingService + memory tools
|
||||
- Can standardize later
|
||||
|
||||
#### 12. Telemetry / Observability Interface
|
||||
|
||||
- Both systems have telemetry
|
||||
- Can standardize later
|
||||
|
||||
---
|
||||
|
||||
## Critical Open Questions (Need Team Discussion)
|
||||
|
||||
### 1. OpenRouter Integration Model
|
||||
|
||||
**Question:** When OpenRouter (or any external agent) is used, what does the
|
||||
integration look like?
|
||||
|
||||
**Option A: Full Agent Interface** — OpenRouter implements the Agent interface
|
||||
directly
|
||||
|
||||
- Pro: Clean, uniform
|
||||
- Con: OpenRouter doesn't support HITL, hooks, policies natively
|
||||
|
||||
**Option B: ACP Shim** — Agent Communication Protocol between CLI and external
|
||||
agents
|
||||
|
||||
- Pro: Standards-based
|
||||
- Con: Additional protocol layer, may be premature
|
||||
|
||||
**Option C: Model-only Integration** — OpenRouter is just an alternative Model,
|
||||
not Agent
|
||||
|
||||
- Pro: Simpler, leverages existing agent loop
|
||||
- Con: Doesn't support OpenRouter-specific features
|
||||
|
||||
**Recommendation:** Start with Option C (model-only). OpenRouter provides an LLM
|
||||
endpoint. Gemini-cli's own agent loop handles tools, policies, hooks. This means
|
||||
defining a provider-agnostic Model interface is the key enabler.
|
||||
|
||||
### 2. Tool Execution: Client-side vs Agent-side
|
||||
|
||||
**Question:** Who executes tools — the CLI or the agent backend?
|
||||
|
||||
**Option A: Always client-side** (CLI executes, agent suspends)
|
||||
|
||||
- ADK: `pauseOnToolCalls: true`
|
||||
- Pro: CLI maintains control, policies enforced, hooks fire
|
||||
- Con: Higher latency, more round-trips
|
||||
|
||||
**Option B: Agent-side execution** (agent runs tools internally)
|
||||
|
||||
- Pro: Faster, simpler
|
||||
- Con: Bypasses CLI policies, hooks, confirmations
|
||||
|
||||
**Option C: Configurable** — CLI decides per-tool or per-agent
|
||||
|
||||
- Pro: Flexible
|
||||
- Con: Complex
|
||||
|
||||
**Recommendation:** Option A for safety-critical CLI use case. Option B only for
|
||||
trusted/sandboxed sub-agents.
|
||||
|
||||
### 3. Model Steering (Hooks that inject context mid-stream)
|
||||
|
||||
**Question:** How do user-local hooks (like injecting project context) work with
|
||||
external agents?
|
||||
|
||||
**Answer:** They can only work if:
|
||||
|
||||
- The CLI controls the model (via Model interface adapter) — then BeforeModel
|
||||
hook injects context
|
||||
- OR the agent supports a "system instruction update" mechanism
|
||||
|
||||
For OpenRouter: model steering works because CLI controls the model call. For
|
||||
ADK: model steering works because AdkGeminiModel wraps GeminiChat. For fully
|
||||
opaque agents: model steering **cannot work** — this is a known limitation.
|
||||
|
||||
### 4. Elicitation Flow
|
||||
|
||||
**Question:** When the agent needs user input (model fallback, clarification),
|
||||
how does it work?
|
||||
|
||||
**For CLI-controlled agents:** Agent yields an elicitation_request event → CLI
|
||||
renders prompt → user responds → CLI sends response back via session.stream({
|
||||
kind: 'elicitation_response', ... }) to resume
|
||||
|
||||
**For external agents:** Agent uses A2A protocol or similar to send elicitation
|
||||
→ CLI bridges the request to user → response sent back via protocol
|
||||
|
||||
**Key insight:** Elicitation is fundamentally about the agent SUSPENDING and
|
||||
waiting for user input. ADK already supports this via `pauseOnToolCalls`. Can we
|
||||
generalize to `pauseOnElicitation`?
|
||||
|
||||
### 5. Sub-agent Identity and Policies
|
||||
|
||||
**Question:** When a sub-agent spawns, does it inherit parent policies? Get its
|
||||
own?
|
||||
|
||||
**Current gemini-cli behavior:** Sub-agents registered as tools, go through same
|
||||
policy engine. **ADK behavior:** Sub-agents are child nodes in agent tree, get
|
||||
parent's plugins.
|
||||
|
||||
**Recommendation:** Sub-agents inherit parent policy context. Additional
|
||||
restrictions can be layered (e.g., sub-agent X cannot use shell tool). This is
|
||||
already how gemini-cli works.
|
||||
@@ -1,438 +0,0 @@
|
||||
# Architectural Design: Gemini CLI to ADK Migration
|
||||
|
||||
| Authors: [Adam Weidman](mailto:adamfweidman@google.com) Contributors: Reviewers: *See section [Status of this document](#status-of-this-document).* | Status: Draft Last revised: Apr 7, 2026 Visibility: Confidential |
|
||||
| :--- | :--- |
|
||||
|
||||
---
|
||||
|
||||
# Goal
|
||||
|
||||
To migrate the Gemini CLI backend execution engine from its legacy fragmented loop structure to the Agent Development Kit (ADK). This migration will unify how agents and subagents are orchestrated, simplify state persistence, and expose a standard `AgentSession` interface for the CLI, future SDK surfaces, and subagent execution.
|
||||
|
||||
---
|
||||
|
||||
# Context
|
||||
|
||||
Over time, Gemini CLI has accumulated complex runtime behaviors: multi-tier tool scheduling, policy-driven approvals, payload masking, dynamic routing, and fine-grained telemetry. Integrating these with ADK requires a clean boundary that preserves Gemini CLI semantics without forking ADK core behavior.
|
||||
|
||||
The key migration boundary is:
|
||||
|
||||
- ADK runtime semantics -> Gemini CLI `AgentProtocol` / `AgentSession`
|
||||
- ADK `Event` stream -> Gemini CLI `AgentEvent` stream
|
||||
|
||||
That boundary, not the model wrapper alone, is the architectural center of this design.
|
||||
|
||||
---
|
||||
|
||||
# Current State and Proposed Mappings
|
||||
|
||||
The following analysis maps existing Gemini CLI components onto ADK capabilities, citing both repositories (`gemini-cli` and `adk-js`).
|
||||
|
||||
## Core Runtime Architecture
|
||||
|
||||
The migration uses one shared ADK-backed runtime core. Every orchestrated agent, including subagents, is exposed through the same external session contract:
|
||||
|
||||
- **Top-level CLI agent** -> `AgentSession`
|
||||
- **Future SDK entry point** -> `AgentSession`
|
||||
- **Subagent execution** -> `AgentSession`
|
||||
|
||||
The runtime owns:
|
||||
|
||||
- ADK runner/session lifecycle
|
||||
- tool execution
|
||||
- policy integration
|
||||
- routing, availability, compaction, and masking hooks
|
||||
- persistence integration
|
||||
|
||||
The adapters own:
|
||||
|
||||
- `streamId` timing guarantees
|
||||
- replay / reattach behavior
|
||||
- translation from ADK `Event` to Gemini CLI `AgentEvent`
|
||||
- top-level versus subagent event projection
|
||||
- projection of a child `AgentSession` into parent-facing tool or thread events when a subagent is embedded inside another agent
|
||||
|
||||
The minimum shape is:
|
||||
|
||||
```typescript
|
||||
interface PipelineServices {
|
||||
run(
|
||||
request: LlmRequest,
|
||||
baseModel: BaseLlm,
|
||||
stream: boolean,
|
||||
): AsyncGenerator<LlmResponse, void>;
|
||||
connect(
|
||||
request: LlmRequest,
|
||||
baseModel: BaseLlm,
|
||||
): Promise<BaseLlmConnection>;
|
||||
}
|
||||
|
||||
class GcliAgentModel extends BaseLlm {
|
||||
constructor(
|
||||
private baseModel: BaseLlm,
|
||||
private pipeline: PipelineServices,
|
||||
) {
|
||||
super({model: 'gcli-consolidated'});
|
||||
}
|
||||
|
||||
async *generateContentAsync(
|
||||
request: LlmRequest,
|
||||
stream = false,
|
||||
): AsyncGenerator<LlmResponse, void> {
|
||||
yield* this.pipeline.run(request, this.baseModel, stream);
|
||||
}
|
||||
|
||||
async connect(request: LlmRequest): Promise<BaseLlmConnection> {
|
||||
return this.pipeline.connect(request, this.baseModel);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Design rule:
|
||||
|
||||
- request mutation stays in the pipeline
|
||||
- all orchestrated agents expose the same `AgentSession` contract
|
||||
- session lifecycle, replay, approvals, and subagent projection stay in the runtime/adapters
|
||||
|
||||
`AdkAgentService` is the composition root for this architecture. It creates and resumes both top-level and subagent `AgentSession`s, builds scoped registries and message-bus instances, wires policy and approval bridges, and embeds child sessions through projection adapters rather than through a separate subagent runtime. See Appendix A for the intended initialization shape.
|
||||
|
||||
Composition rule:
|
||||
|
||||
- stateful tools are cloned or reinstantiated per session when needed; stateless tools may be shared
|
||||
- MCP discovery is shared at the manager layer but registered into session-local tool, prompt, and resource registries
|
||||
- `AgentLoopContext` is decomposed into pipeline config, tool/subagent config, session services, callback bridges, and UI projection rather than passed through as a single runtime object
|
||||
- file persistence remains Gemini CLI-owned through `GcliFileSessionService extends BaseSessionService`
|
||||
|
||||
Persistence rule:
|
||||
|
||||
- persisted history is an append-only event log plus derived app/user/session state
|
||||
- the session service provides atomic append, crash-safe recovery, and single-writer enforcement
|
||||
- rewind truncates the event log, recomputes derived state, and invalidates confirmation/resumption state past the rewind point
|
||||
|
||||
## 3.1 Authentication Flexibility
|
||||
|
||||
The CLI resolves distinct authentication flows (OAuth, ADC, Compute metadata) using standard Google libraries.
|
||||
|
||||
* **Current State:** Resolved in `packages/core/src/code_assist/oauth2.ts` based on `AuthType`.
|
||||
* OAuth (`LOGIN_WITH_GOOGLE`)
|
||||
* Compute Metadata Server (`COMPUTE_ADC`)
|
||||
* **Constraint:** Standard `Gemini` construction in `adk-js/core/src/models/google_llm.ts` still selects backend from constructor-level `apiKey` or Vertex config. It does **not** natively accept Gemini CLI's `AuthClient`-driven auth shape.
|
||||
* **Proposed ADK Mapping:** Phase 1 keeps auth in `GcliAgentModel`. The pipeline resolves refreshed credentials and injects them through `request.config.httpOptions.headers` for unary requests and `request.liveConnectConfig.httpOptions.headers` for live connections.
|
||||
* **Design position:** This is a **bridge**, not native ADK auth parity. Long-term cleanup is either:
|
||||
* a `CodeAssistLlm extends BaseLlm`, or
|
||||
* an upstream ADK auth-provider abstraction.
|
||||
|
||||
```typescript
|
||||
request.config ??= {};
|
||||
request.config.httpOptions ??= {};
|
||||
request.config.httpOptions.headers = {
|
||||
...request.config.httpOptions.headers,
|
||||
Authorization: `Bearer ${await auth.getAccessToken()}`,
|
||||
};
|
||||
```
|
||||
|
||||
## 3.2 Model Steering and Mid-Stream Injection
|
||||
|
||||
User interjections (hints) course-correct the loop mid-turn.
|
||||
|
||||
* **Current State:** Steering today is tied to the legacy loop and injection services.
|
||||
* **Proposed ADK Mapping (Next-Step Steering):** Supported in Phase 1. `beforeModelCallback` can read queued hints and mutate the next outbound request.
|
||||
* **Proposed ADK Mapping (True Real-Time Interrupt):** Still blocked. TypeScript ADK live runtime is not complete yet, and input-stream semantics are not a stable dependency.
|
||||
* **Phase 1 behavior:** user interjections are queued and prepended to the next model request at tool or turn boundaries. True in-place turn interruption remains out of scope.
|
||||
|
||||
## 3.3 State Management and Token Compaction
|
||||
|
||||
The CLI truncates large tool responses and summarizes older history to protect token budgets.
|
||||
|
||||
* **Current State:** `ChatCompressionService` in `packages/core/src/context/chatCompressionService.ts` implements reverse token budgeting and a two-phase verification loop.
|
||||
* **Proposed ADK Mapping:** Compaction remains a Gemini CLI-owned history processor invoked by the request pipeline.
|
||||
* **Design position:** Phase 1 does **not** force this onto ADK `BaseContextCompactor`. Gemini CLI compression is currently an outbound-history projection with truncation plus summary/verification sub-calls, while ADK's native compactor path is event-log-centric and mutates session history.
|
||||
* **Design choice:** In Phase 1, compaction mutates **outgoing request history only**. The persisted session event log remains canonical.
|
||||
* **Utility calls:** Compaction sub-calls use the same routing, auth, and availability pipeline as primary model calls.
|
||||
|
||||
## 3.4 Model Configuration and Hierarchical Overrides
|
||||
|
||||
Dynamic aliasing (for example, temperature scoped to specific sub-commands).
|
||||
|
||||
* **Current State:** Managed by `ModelConfigService`.
|
||||
* **Proposed ADK Mapping:** Resolution stays **request-scoped**, not session-init-scoped.
|
||||
* **Design choice:** The session stores requested model and override state. The runtime resolves the concrete model and temperature on each request, including retries, subagents, and utility calls.
|
||||
|
||||
## 3.5 Universal Policy Enforcement (TOML Rules)
|
||||
|
||||
Tiered workspace restrictions (for example, read-only tools in untrusted folders).
|
||||
|
||||
* **Current State:** Intercepted at tool scheduling time in legacy scheduler loops.
|
||||
* **Proposed ADK Mapping (Container):** Standardize on ADK `SecurityPlugin`.
|
||||
* **Proposed ADK Mapping (Decision Brain):** Implement `GcliPolicyEngineAdapter implements BasePolicyEngine`.
|
||||
* **Richer context:** The adapter is fed session/mode/subagent/MCP metadata from the runtime so current Gemini CLI policy semantics are preserved as closely as possible.
|
||||
* **Phase 1 suspension flow:** current tool approvals use existing Gemini CLI callbacks. This is intentionally **non-native** and **non-resumable across process death**.
|
||||
* **Long-term target:** move the same policy decisions onto native ADK confirmation/resumption semantics once the broader live/elicitation surface is ready.
|
||||
|
||||
```typescript
|
||||
interface GcliPolicyBridge {
|
||||
evaluate(context: ToolCallPolicyContext): Promise<PolicyCheckResult>;
|
||||
}
|
||||
|
||||
export class GcliPolicyEngineAdapter implements BasePolicyEngine {
|
||||
constructor(private readonly policyBridge: GcliPolicyBridge) {}
|
||||
|
||||
async evaluate(context: ToolCallPolicyContext): Promise<PolicyCheckResult> {
|
||||
return this.policyBridge.evaluate(context);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3.6 Telemetry and Observability (Clearcut Tracking)
|
||||
|
||||
Hardware metrics, token counts, and step durations.
|
||||
|
||||
* **Current State:** `ClearcutLogger` reads system metrics and relies on deep scheduler hooks for latency accounting.
|
||||
* **Proposed ADK Mapping:** Use a combination of:
|
||||
* passive event-stream observation, and
|
||||
* explicit runtime instrumentation where passive ADK events are insufficient.
|
||||
* **Correlation rule:** tool timing is keyed by `functionCall.id`, **not** by `event.id`.
|
||||
* **Design position:** Passive stream interception alone is not assumed to provide full parity.
|
||||
|
||||
## 3.7 Dynamic Model Routing and Configurability
|
||||
|
||||
Banning a model mid-turn, auto-routing via classifiers, and falling back dynamically without reinitializing the session.
|
||||
|
||||
* **Current State:** Managed by `ModelRouterService` and a chain of `RoutingStrategy` implementations which require the full `RoutingContext` (`history`, `request`, `AbortSignal`).
|
||||
* **Proposed ADK Mapping:** Mostly implementable now, but not “100% possible today” without bridge logic.
|
||||
* **Design choice:** The runtime constructs a proper `RoutingContext` from:
|
||||
* canonical session history
|
||||
* the pending user request
|
||||
* requested model
|
||||
* abort signal
|
||||
* **Execution point:** routing remains request-scoped and runs before final dispatch.
|
||||
* **Model banning:** treated as routing/fallback selection, not as a synthetic terminal model error.
|
||||
|
||||
## 3.8 Fallbacks and Availability Management
|
||||
|
||||
Ensuring availability by retrying or switching models when rate limits (429s) or terminal faults occur.
|
||||
|
||||
* **Current State:** Managed by `ModelAvailabilityService` and `ModelPool`.
|
||||
* **Proposed ADK Mapping (Preflight):** availability and fallback selection run before dispatch and before routing commits to a concrete model.
|
||||
* **Proposed ADK Mapping (Post-failure):** handled separately by the runtime. Availability state mutation and retry decisions are not treated as pure request preprocessing.
|
||||
* **Global Application:** utility calls use the same availability/fallback services as primary model calls.
|
||||
* **Retry safety rule:** automatic full-turn replay is allowed only before any side-effecting tool has executed. After side effects, the runtime surfaces the failure and requires explicit user action.
|
||||
* **Phase 1 transition path:** approvals and fallback prompts continue to use existing Gemini CLI callbacks. The doc treats this as an internal bridge, not as an existing standard ADK elicitation API.
|
||||
|
||||
## 3.9 State-Driven Mode Switching (Plan Mode)
|
||||
|
||||
Dynamically shifting system prompts and active tools when users switch interaction tiers (for example, Chat Mode to Plan Mode).
|
||||
|
||||
* **Current State:** Toggled via `/plan`, which changes `ApprovalMode` and related legacy scheduler behavior.
|
||||
* **Proposed ADK Mapping (Dynamic Prompt):** Use `InstructionProvider` in `LlmAgentConfig.instruction`.
|
||||
* **Proposed ADK Mapping (Dynamic Tooling):** Use a custom `BaseToolset` whose filter is derived from session mode.
|
||||
* **Single source of truth:** mode is session/runtime state derived from current approval mode; prompt, toolset, policy, routing, and UI all consume the same state.
|
||||
|
||||
```typescript
|
||||
export class GcliModeAwareToolset extends BaseToolset {
|
||||
constructor(
|
||||
private readonly chatTools: BaseTool[],
|
||||
private readonly planTools: BaseTool[],
|
||||
) {
|
||||
super(() => true);
|
||||
}
|
||||
|
||||
async getTools(context?: ReadonlyContext): Promise<BaseTool[]> {
|
||||
const isPlan = context?.state.get('plan_mode') === true;
|
||||
return isPlan ? this.planTools : this.chatTools;
|
||||
}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
}
|
||||
```
|
||||
|
||||
## 3.10 Tool Output Masking
|
||||
|
||||
Managing context window efficiency by offloading bulky tool outputs (for example, shell logs and large file reads) to files.
|
||||
|
||||
* **Current State:** `ToolOutputMaskingService` in `packages/core/src/context/toolOutputMaskingService.ts`.
|
||||
* **Proposed ADK Mapping:** masking runs on **outgoing request history only**, after compaction.
|
||||
* **Design choice:** persisted session history remains the canonical event log. Masking artifacts are session-scoped files referenced from the outbound request projection.
|
||||
|
||||
---
|
||||
|
||||
# 5. Known Gaps in ADK (Gating Blockers)
|
||||
|
||||
This section highlights existing gaps in standard ADK that prevent a seamless cutover without bridge logic or upstream changes.
|
||||
|
||||
## 5.1 Real-Time User Message Injections (Aborted Turns)
|
||||
|
||||
While next-step steering is possible today using `beforeModelCallback`, true real-time interruption requires:
|
||||
|
||||
- stable input-stream support, and
|
||||
- a complete TypeScript live runtime
|
||||
|
||||
Until then, the supported behavior is queued next-step steering at model boundaries, not mid-stream interruption.
|
||||
|
||||
## 5.2 Conversation Rewind and State Reversal
|
||||
|
||||
Translating manual trajectory drops to ADK runtime state is cumbersome. While Python ADK supports rollback, TypeScript ADK does not yet support it natively.
|
||||
|
||||
* **Resolution Strategy:** Gemini CLI implements rewind in `GcliFileSessionService`, but **not** as a shallow JSON edit. Rewind truncates the event log, recomputes derived state, and invalidates any confirmation/resumption state past the rewind point.
|
||||
|
||||
## 5.3 Phase 1 Non-Goals
|
||||
|
||||
To keep the migration surface bounded, Phase 1 intentionally excludes:
|
||||
|
||||
- non-interactive surfacing of `elicitation_request` / `elicitation_response`
|
||||
- true live/bidi interruption semantics
|
||||
- native ADK confirmation/resumption parity for approvals and fallback prompts
|
||||
|
||||
---
|
||||
|
||||
# Long-Term Vision: Unification of Agents and Subagents
|
||||
|
||||
The long-term vision is that subagents and the primary agent share:
|
||||
|
||||
- the same runtime core
|
||||
- the same tool definitions
|
||||
- the same policy constraints
|
||||
- the same configuration schemas
|
||||
- the same orchestration contract: `AgentSession`
|
||||
|
||||
What may differ is the **embedding adapter**:
|
||||
|
||||
- standalone/top-level agent -> consumed directly as `AgentSession`
|
||||
- subagent embedded by a parent agent -> projected from its `AgentSession` into parent-facing tool or child-thread events
|
||||
|
||||
For SDK-first unification, Gemini CLI orchestration targets `AgentSession`, not a specific ADK tool abstraction. When a subagent must be exposed to a parent model as a tool, Gemini CLI wraps that child `AgentSession` with a `FunctionTool` or custom `BaseTool` projection. `AgentTool` remains the native ADK nested-agent option if we later want full ADK-native nested-agent semantics.
|
||||
|
||||
---
|
||||
|
||||
# Migration Sequence and Unification Checklist
|
||||
|
||||
The migration remains non-sequential, but each phase has an explicit success condition:
|
||||
|
||||
- [ ] **Non-interactive session parity** `#22699`
|
||||
output/error parity, basic replay correctness, feature-flagged rollout
|
||||
- [ ] **Interactive session parity** `#22701`
|
||||
projected event parity, approval bridge wiring, no live-interrupt claim
|
||||
- [ ] **Subagent adapter parity** `#22700`
|
||||
tool isolation, activity projection, policy/routing parity for child runs
|
||||
- [ ] **ADK session conformance** `#22974`
|
||||
`AgentSession` timing/replay guarantees preserved by the adapter
|
||||
- [ ] **Skills parity** `#22966`
|
||||
skills work through the shared runtime without special legacy paths
|
||||
- [ ] **Policy and confirmation parity** `#22964`
|
||||
phase-1 callback bridge stable; native confirmation migration scoped separately
|
||||
- [ ] **Compaction quality parity** `#22979`
|
||||
comparable summarization and truncation quality to current behavior
|
||||
|
||||
---
|
||||
|
||||
# Appendix A: Initialization Sketch
|
||||
|
||||
The main agent and subagents share the same composition root. The difference is whether the resulting `AgentSession` is consumed directly or projected into a parent session.
|
||||
|
||||
```typescript
|
||||
shared = {
|
||||
baseModel,
|
||||
modelConfigService,
|
||||
availabilityService,
|
||||
router,
|
||||
authService,
|
||||
policyEngine,
|
||||
mcpClientManager,
|
||||
skillCatalog,
|
||||
baseToolCatalog,
|
||||
sessionService,
|
||||
}
|
||||
|
||||
agentService = new AdkAgentService(shared)
|
||||
|
||||
function createSession(definition, parentSessionId?) {
|
||||
sessionRecord = sessionService.create({ definition, parentSessionId })
|
||||
registries = buildScopedRegistries(definition, parentSessionId)
|
||||
pipeline = createPipeline({
|
||||
sessionId: sessionRecord.id,
|
||||
agentId: definition.name,
|
||||
parentSessionId,
|
||||
})
|
||||
|
||||
model = new GcliAgentModel(baseModel, pipeline)
|
||||
runtime = createAdkRuntime({
|
||||
definition,
|
||||
model,
|
||||
sessionRecord,
|
||||
registries,
|
||||
policyEngine,
|
||||
})
|
||||
|
||||
return new AgentSession(new AdkAgentProtocolAdapter(runtime))
|
||||
}
|
||||
|
||||
function createSubagentTool(definition) {
|
||||
return new FunctionTool(async (input, toolContext) => {
|
||||
child = createSession(definition, toolContext.sessionId)
|
||||
stream = await child.send(userMessage(input))
|
||||
|
||||
for await (event of child.stream(stream)) {
|
||||
projectChildEventToParent(toolContext, event)
|
||||
}
|
||||
|
||||
return collectFinalText()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Child session projection shape:
|
||||
|
||||
```typescript
|
||||
emit(tool_request({ requestId, name: definition.name, args: input }))
|
||||
|
||||
for await (event of childSession.stream({ streamId })) {
|
||||
if (event.type === 'message' || event.type === 'tool_update') {
|
||||
emit(
|
||||
tool_update({
|
||||
requestId,
|
||||
content: projectDisplayContent(event),
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (event.type === 'error') {
|
||||
emit(
|
||||
tool_response({
|
||||
requestId,
|
||||
name: definition.name,
|
||||
isError: true,
|
||||
content: projectErrorContent(event),
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === 'agent_end') {
|
||||
emit(
|
||||
tool_response({
|
||||
requestId,
|
||||
name: definition.name,
|
||||
content: collectFinalChildResult(),
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Only the final `tool_response` is returned to the parent model. `tool_update` remains a progress/UI projection surface.
|
||||
|
||||
Service lifetime split:
|
||||
|
||||
- shared across sessions: model config, availability, routing, auth, policy engine, MCP manager, skill catalog
|
||||
- scoped per agent/session: `AgentSession`, pipeline instance, tool/prompt/resource registries, derived message bus
|
||||
- scoped per invocation: shell process, MCP request, confirmation continuation, tool progress updates
|
||||
|
||||
---
|
||||
|
||||
# Status of this document approvals table {#status-of-this-document}
|
||||
|
||||
| #begin-approvals-addon-section See [go/g3a-approvals](http://goto.google.com/g3a-approvals) for instructions on adding reviewers. |
|
||||
| :---: |
|
||||
.
|
||||
@@ -1,554 +0,0 @@
|
||||
# Unified ADK CLI Design Review
|
||||
|
||||
Date: 2026-04-06
|
||||
|
||||
> **Rollout:** All ADK migration work is feature-gated behind `experimental.adk` flags. No behavioral changes ship without an explicit opt-in. This applies to both non-interactive and interactive flows.
|
||||
|
||||
Inputs merged:
|
||||
- `claude-adk-cli-design-review.md` (adversarial code-level review)
|
||||
- `codex-adk-cli-design-review.md`
|
||||
- Follow-up review discussion on subagent architecture, session semantics, and `BaseContextCompactor`
|
||||
|
||||
Scope reviewed:
|
||||
- `gemini-cli/docs/adk-replat/adk_migration_design_doc.md`
|
||||
- `gemini-cli` `AgentProtocol` / `AgentSession` / interactive migration work
|
||||
- `adk-js` runner / session / tool / security / model architecture
|
||||
- PR context for interactive migration, including `google-gemini/gemini-cli#24297`
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The design is directionally sound and the migration goal is correct. However, the document is not yet principal-review ready due to:
|
||||
|
||||
1. **Four non-compilable code samples** (C1-C4) that will immediately erode reviewer confidence
|
||||
2. **An under-specified translation boundary** — the ADK→AgentProtocol mapping is the real architecture, but the doc treats it as an afterthought
|
||||
3. **Overclaimed parity** in routing, telemetry, and approvals
|
||||
4. **Missing phased plan** with clear milestones and non-goals
|
||||
|
||||
The central architectural insight is right: the migration boundary is:
|
||||
|
||||
- `adk-js Event -> Gemini CLI AgentEvent`
|
||||
- `adk-js session/runtime semantics -> AgentProtocol / AgentSession semantics`
|
||||
|
||||
That boundary carries stream lifecycle, replay/resume, event projection, approval correlation, and persistence correctness. Until the translation architecture is explicit, parity claims remain unsupported.
|
||||
|
||||
## Compilation Blockers (Must Fix First)
|
||||
|
||||
These are binary errors — the code samples in the design doc will not compile against current ADK types.
|
||||
|
||||
### C1. `LlmRequest` has no `headers` field
|
||||
|
||||
`adk-js/core/src/models/llm_request.ts` defines: `model?`, `contents`, `config?`, `liveConnectConfig`, `toolsDict`. The design doc's `request.headers = {...}` will silently fail or not compile. **Fix:** use `llmRequest.config.httpOptions.headers` or pass headers at `Gemini` constructor time.
|
||||
|
||||
### C2. `BaseLlm.connect()` is abstract and unimplemented
|
||||
|
||||
`adk-js/core/src/models/base_llm.ts:67-78` has TWO abstract methods: `generateContentAsync` and `connect()`. `GcliAgentModel` must implement both. **Fix:** add `connect()` — stub with `throw new Error('not supported')` if live connections are out of scope.
|
||||
|
||||
### C3. `BaseToolset` constructor signature is wrong
|
||||
|
||||
`adk-js/core/src/tools/base_toolset.ts:46-49`: `constructor(readonly toolFilter: ToolPredicate | string[], readonly prefix?: string)`. The design doc's `super([])` passes an empty array as `toolFilter`, which makes `isToolSelected` return false for all tools — a silent bug. Also `close()` is abstract and must be implemented. **Fix:** match the actual signature.
|
||||
|
||||
### C4. `FunctionalTool` does not exist
|
||||
|
||||
The design doc references `FunctionalTool` for subagent wrapping. This class does not exist in adk-js. The correct classes are `FunctionTool` (wraps a plain function) and `AgentTool` (wraps an agent with isolated runner). See subagent architecture section below for the recommended approach.
|
||||
|
||||
---
|
||||
|
||||
## High-Risk Findings
|
||||
|
||||
### 1. The actual migration boundary is under-specified
|
||||
|
||||
Current `AgentProtocol` requires `send()` timing guarantees, replay/reattach semantics, `streamId`, and optional `threadId` for subagent threads in `gemini-cli/packages/core/src/agent/types.ts` and `gemini-cli/packages/core/src/agent/agent-session.ts`.
|
||||
|
||||
ADK emits a different event model in `adk-js/core/src/events/event.ts`.
|
||||
|
||||
The doc needs a first-class architecture section for:
|
||||
- `AdkSessionRuntime`
|
||||
- `AdkEventTranslator`
|
||||
- event buffering / replay
|
||||
- `threadId` mapping
|
||||
- `tool_update` mapping
|
||||
- `agent_start` / `agent_end` ownership
|
||||
|
||||
Without that, the rest of the design sits on an unstated core contract.
|
||||
|
||||
### 2. The consolidated decorator needs two concerns extracted
|
||||
|
||||
The `GcliAgentModel.generateContentAsync` pipeline is a reasonable orchestrator pattern — each concern is a separate injected service call, not a monolithic class. However, two concerns don't belong in the model layer:
|
||||
|
||||
- **Quota/availability prompting** — involves interactive UI suspension, not request mutation. Should use `beforeModelCallback` on `LlmAgent`.
|
||||
- **Policy/approval control flow** — involves blocking for user input. Should use `beforeToolCallback` or the `ApprovalBridge` (see architecture section).
|
||||
|
||||
The remaining concerns (auth header injection, routing/model rewrite, compaction, masking) are legitimate request-preprocessing and can stay in the model pipeline.
|
||||
|
||||
### 3. The design frequently conflates “possible with custom logic” with “supported by current ADK architecture”
|
||||
|
||||
This is one of the main wording risks.
|
||||
|
||||
Several statements currently read like native ADK parity when the real meaning is:
|
||||
- possible with substantial Gemini CLI-owned bridge logic
|
||||
- possible by bypassing native ADK facilities
|
||||
- possible only for phase 1 with intentionally degraded semantics
|
||||
|
||||
That distinction needs to be explicit everywhere the doc uses strong language like “validated” or “100% possible today.”
|
||||
|
||||
### 4. Auth architecture is deeper than header injection — Coast Assist requires a custom transport
|
||||
|
||||
The auth section has two distinct problems:
|
||||
|
||||
**Problem A: API surface mismatch (C1).** `LlmRequest` has no `headers` field. Per-request headers are possible via `llmRequest.config.httpOptions.headers`, and `Gemini` constructor accepts a `headers` param (`google_llm.ts:57,79`). This is fixable.
|
||||
|
||||
**Problem B: Coast Assist / `LOGIN_WITH_GOOGLE` uses a different backend entirely.** The current `CodeAssistServer` (`packages/core/src/code_assist/server.ts:407`) does NOT call the standard Gemini API. It uses `google-auth-library`'s `AuthClient.request()` to talk to a Code Assist backend endpoint. The OAuth flow (`packages/core/src/code_assist/oauth2.ts`) handles browser launch (L305), local callback server (L492-615), token exchange (L546-550), credential caching (L739-750), and automatic token refresh via `OAuth2Client`.
|
||||
|
||||
This means `GcliAgentModel` cannot wrap a standard `Gemini` BaseLlm and just inject headers. For Coast Assist auth, `GcliAgentModel` must **extend `BaseLlm` directly** and implement its own HTTP transport using `AuthClient.request()`, translating between `LlmRequest`/`LlmResponse` and the Code Assist protocol.
|
||||
|
||||
What works:
|
||||
- The OAuth dance itself (browser launch, token caching, refresh) runs before the agent session starts — no blocking inside `generateContentAsync`
|
||||
- `OAuth2Client.getAccessToken()` handles mid-session token refresh transparently
|
||||
- Per-request header injection works for API-key-based auth via `httpOptions.headers`
|
||||
|
||||
What the design doc must change:
|
||||
- Stop assuming `Gemini` as the inner model — `GcliAgentModel extends BaseLlm` directly
|
||||
- Define two transport paths: standard Gemini API (API key) and Code Assist backend (OAuth)
|
||||
- The dummy-key workaround is irrelevant for Coast Assist — you never call `GoogleGenAI.models.generateContent`
|
||||
|
||||
### 5. Approval / elicitation bridging is acceptable only as a temporary non-native bridge
|
||||
|
||||
The design’s callback-based approval approach is pragmatic for phase 1, but it bypasses native ADK confirmation semantics.
|
||||
|
||||
`adk-js/core/src/plugins/security_plugin.ts` and `adk-js/core/src/agents/processors/request_confirmation_llm_request_processor.ts` show that ADK’s model is:
|
||||
- `ALLOW | DENY | CONFIRM`
|
||||
- persisted confirmation state
|
||||
- later resumption from history
|
||||
|
||||
Blocking in callbacks may be acceptable for the first rollout, but the doc must say clearly:
|
||||
- local-only bridge
|
||||
- not resumable across process death
|
||||
- not long-term SDK behavior
|
||||
- intentionally non-native pending future elicitation/bidi work
|
||||
|
||||
### 6. Rewind is deeper than file truncation
|
||||
|
||||
The doc treats rewind as storage truncation. The storage part is straightforward — `GcliFileSessionService` wraps the existing `ChatRecordingService` and implements 4 abstract methods (`createSession`, `getSession`, `listSessions`, `deleteSession`). DB-level concerns (locking, stale-writer detection, `PESSIMISTIC_WRITE`) do not apply to a single-user CLI.
|
||||
|
||||
However, rewind itself is not just storage:
|
||||
- `adk-js/core/src/agents/processors/request_confirmation_llm_request_processor.ts` scans event history to reconstruct pending confirmations and resume tool execution
|
||||
- `adk-js/core/src/runner/runner.ts:447` has a TODO acknowledging the event log is used as a transaction log
|
||||
- Truncating events without rolling back derived state (pending confirmations, app/user state prefixes) will break resumption
|
||||
|
||||
The doc should define rewind as: event-log truncation + session state recomputation + confirmation/auth state rollback.
|
||||
|
||||
Note: `BaseSessionService.appendEvent` handles state merging with `app:`, `user:`, `temp:` prefixed keys automatically — this is inherited for free.
|
||||
|
||||
### 7. Routing, telemetry, and plan mode are all overclaimed
|
||||
|
||||
Routing:
|
||||
- current routing requires a richer `RoutingContext`
|
||||
- `contents.slice(0, -1)` / `contents.pop()` is not a sufficient or type-accurate mapping
|
||||
- model banning via simulated error is not the same as model routing
|
||||
|
||||
Telemetry:
|
||||
- **The stream-interceptor approach in section 3.6 is wrong.** The correct mechanism is ADK's `BasePlugin` system.
|
||||
- `event.id` is not a safe per-tool correlation key — use `functionCall.id` via `beforeToolCallback`/`afterToolCallback`
|
||||
- ADK can batch multiple function calls or responses in one event — stream interception cannot distinguish them, but plugin hooks fire per-tool with distinct `functionCallId`
|
||||
- **Correction:** Token usage IS available on ADK events. `llm_agent.ts:831-834` spreads `LlmResponse` (including `usageMetadata`) into the event via `createEvent({...modelResponseEvent, ...llmResponse})`. Additionally, `afterModelCallback` receives the raw `LlmResponse` with `usageMetadata` directly — providing a second capture point.
|
||||
- HTTP-level metrics (status code, request duration) are NOT on ADK events — must be captured in the `GcliAgentModel` wrapper
|
||||
- The sample APIs in the design do not match the current `ClearcutLogger` taxonomy (~195 distinct metadata keys)
|
||||
|
||||
**Recommended telemetry architecture:**
|
||||
|
||||
| Metric Category | Capture Mechanism | ADK Hook |
|
||||
|---|---|---|
|
||||
| Token counts (input, output, cached, thinking) | `afterModelCallback` → `LlmResponse.usageMetadata` | `BasePlugin.afterModelCallback` |
|
||||
| Per-tool timing | Timer keyed on `functionCallId` | `BasePlugin.beforeToolCallback` / `afterToolCallback` |
|
||||
| Agent lifecycle | Start/end tracking | `BasePlugin.beforeAgentCallback` / `afterAgentCallback` |
|
||||
| Model errors | Error classification | `BasePlugin.onModelErrorCallback` |
|
||||
| HTTP status, API duration | Captured in model wrapper | `GcliAgentModel.generateContentAsync` |
|
||||
| Routing decisions, latency | Captured in model or beforeModelCallback | `beforeModelCallback` / model wrapper |
|
||||
| Context token breakdowns (system, tools, history) | Computed in request pipeline | Model wrapper |
|
||||
| Tool approval decisions | Injected from approval layer | `ApprovalBridge` |
|
||||
| Session config, compression, rewind, IDE, extensions, billing, slash commands, hooks, plan execution, onboarding | **Unchanged** — stays in current call sites | Existing gemini-cli code |
|
||||
|
||||
~30% of Clearcut metrics map to ADK plugin hooks. ~70% remain in their current call sites unchanged.
|
||||
|
||||
Plan mode:
|
||||
- current behavior is broader than prompt + tool filtering
|
||||
- policy, approval mode, routing, and config refresh are all part of the behavior
|
||||
|
||||
### 8. Retry safety around side effects (note)
|
||||
|
||||
The “full turn reset” proposal could duplicate side effects (file writes, shell commands, MCP mutations) if the turn already executed side-effecting tools before the stream failure. This is an ADK-wide concern, not specific to this migration. A brief note acknowledging this limitation is sufficient — a full retry safety taxonomy is out of scope for this design.
|
||||
|
||||
### 9. Design ignores ADK's native `BaseContextCompactor`
|
||||
|
||||
ADK already has a compaction extension point: `BaseContextCompactor` (`adk-js/core/src/context/base_context_compactor.ts`) with `shouldCompact(invocationContext)` + `compact(invocationContext)`. It's wired into the request processor pipeline via `ContextCompactorRequestProcessor` at `llm_agent.ts:407-420`.
|
||||
|
||||
The design doc proposes running compaction inside `GcliAgentModel.generateContentAsync` instead. This works but bypasses the native slot. Recommendation: implement `BaseContextCompactor`, delegate to the existing `ChatCompressionService` internally. ADK provides the trigger point; your service provides the logic. Note that `BaseContextCompactor` returns void — failure states (COMPRESSED, NOOP, CONTENT_TRUNCATED, etc.) must be communicated via session state or custom events.
|
||||
|
||||
### 10. Existing `isStructuredError` type-guard bug
|
||||
|
||||
`gemini-cli/packages/core/src/agent/event-translator.ts:431-438`: `isStructuredError()` checks only `typeof error === 'object' && 'message' in error && typeof error.message === 'string'`. Since every `Error` instance has `message: string`, plain `Error` objects pass this guard. In `mapError` (lines 390-429), the structured branch runs before `instanceof Error`, so plain errors get incorrect HTTP→gRPC status mapping. This should be fixed before the translator becomes the long-term session boundary. **Fix:** add `'status' in error` to the guard, or check `instanceof Error` first.
|
||||
|
||||
### 11. The review should distinguish architecture defects from rollout-status evidence
|
||||
|
||||
The interactive PR findings matter:
|
||||
- stacked-branch dependency
|
||||
- hook-order risk in `#24297`
|
||||
- `_meta.legacyState` leakage into protocol events
|
||||
|
||||
But they are secondary to the core design defect, which is the missing runtime/translation architecture.
|
||||
|
||||
These findings should stay in the review, but as credibility and rollout-risk evidence rather than the centerpiece.
|
||||
|
||||
## Recommended Architecture
|
||||
|
||||
This is the cleanest merged recommendation from both reviews plus follow-up discussion.
|
||||
|
||||
### Core principle
|
||||
|
||||
Use one shared ADK-based execution/runtime core, then put different adapters on top of it.
|
||||
|
||||
Do not make `AgentSession` the innermost engine.
|
||||
|
||||
### Recommended split
|
||||
|
||||
1. `AdkRuntimeCore`
|
||||
- owns the ADK runner loop
|
||||
- owns tool execution, policy integration, routing integration, masking, compaction hooks, and persistence hooks
|
||||
- emits runtime-level activity/events
|
||||
|
||||
2. `TopLevelSessionAdapter`
|
||||
- exposes the runtime as `AgentProtocol` / `AgentSession`
|
||||
- owns replay, reattach, `streamId`, `threadId`, `agent_start` / `agent_end`, and top-level event projection
|
||||
|
||||
3. `SubagentTools` (custom `BaseTool` implementations)
|
||||
- subagents invoke the same shared runtime core as the main agent
|
||||
- the only difference is the type mapping at the boundary (projecting child activity into parent-facing `tool_update` / `tool_response` or `threadId`-scoped events)
|
||||
- `AgentTool` is explicitly rejected — it creates a fully isolated runner/session, which conflicts with the shared-core goal
|
||||
- custom `BaseTool` wrappers allow subagents to share policy, routing, tool definitions, and config with the parent while keeping the door open for future resumability and richer state sharing
|
||||
|
||||
4. `ApprovalBridge`
|
||||
- phase-1 callback bridge for approvals / elicitation using existing callbacks
|
||||
- clearly documented as temporary and non-native
|
||||
- eventual migration to full elicitation bidi format
|
||||
|
||||
5. `GcliFileSessionService`
|
||||
- file-backed persistence wrapping existing `ChatRecordingService`
|
||||
- implements 4 abstract methods from `BaseSessionService`
|
||||
- inherits state-merging (app/user/temp prefixes) for free
|
||||
- single-writer model — concurrent runs per session are forbidden
|
||||
|
||||
### Subagent architecture decision
|
||||
|
||||
`AgentTool` is not the right abstraction for this migration. It creates an isolated runner with its own `InMemorySessionService`, meaning:
|
||||
- state is NOT shared with the parent
|
||||
- events are NOT projected into the parent stream
|
||||
- policy, routing, and config are NOT inherited
|
||||
|
||||
The core requirement is that subagents use the same core loop functionality as the main agent — the only difference is how results are mapped back to the parent. Custom `BaseTool` implementations that invoke the shared `AdkRuntimeCore` achieve this. This also positions subagents for future complexity (resumability, richer state sharing, MCP-scoped tool sets) without fighting AgentTool's isolation model.
|
||||
|
||||
### Session conclusion
|
||||
|
||||
Main agent and subagents can share the same underlying runtime core.
|
||||
|
||||
But they should not necessarily share the exact same public adapter.
|
||||
|
||||
The right formulation is:
|
||||
- same core runtime
|
||||
- different event/session projections depending on embedding context
|
||||
|
||||
## Section-by-Section Unified Review
|
||||
|
||||
### 3.1 Authentication Flexibility
|
||||
|
||||
Assessment: implementable, but the design doc’s approach is wrong for Coast Assist.
|
||||
|
||||
Keep:
|
||||
- ADK `Gemini` does not natively accept the CLI’s auth shape
|
||||
|
||||
Change:
|
||||
- `GcliAgentModel` must extend `BaseLlm` directly, not wrap `Gemini` — Coast Assist uses `AuthClient.request()` against a non-standard backend, not `GoogleGenAI`
|
||||
- fix `request.headers` to `llmRequest.config.httpOptions.headers` (C1)
|
||||
- define two transport paths: API-key (standard Gemini) and OAuth (Code Assist)
|
||||
- token refresh is transparent via `OAuth2Client` — this works as-is
|
||||
- browser-based OAuth runs before agent session starts — no blocking concern
|
||||
|
||||
### 3.2 Model Steering and Mid-Stream Injection
|
||||
|
||||
Assessment: next-turn steering is plausible now; true live interruption is still blocked.
|
||||
|
||||
Required change:
|
||||
- split “next-step steering” from “true mid-turn interruption”
|
||||
- define temporary user-visible behavior until live input-stream support exists
|
||||
|
||||
### 3.3 State Management and Token Compaction
|
||||
|
||||
Assessment: plausible, but too wrapper-centric.
|
||||
|
||||
Required change:
|
||||
- specify whether compaction operates on persisted history, outgoing request history, or both
|
||||
- define recursion guards and utility-call isolation
|
||||
- define artifact ownership across sessions and subagents
|
||||
|
||||
### 3.4 Model Configuration and Hierarchical Overrides
|
||||
|
||||
Assessment: under-specified.
|
||||
|
||||
Required change:
|
||||
- model config resolution should remain request-scoped
|
||||
- preserve scoped overrides and retry-aware behavior
|
||||
- explain subagent and utility-call override behavior
|
||||
|
||||
### 3.5 Universal Policy Enforcement
|
||||
|
||||
Assessment: acceptable as a bridge, not as full parity.
|
||||
|
||||
Required change:
|
||||
- explicitly document reduced phase-1 semantics
|
||||
- list policy inputs lost unless extra context plumbing is added
|
||||
- separate short-term callback bridge from long-term native confirmation flow
|
||||
|
||||
### 3.6 Telemetry and Observability (Clearcut)
|
||||
|
||||
Assessment: fully implementable, but the stream-interceptor approach must be replaced with a `BasePlugin`.
|
||||
|
||||
Required change:
|
||||
- replace stream interception with `ClearcutTelemetryPlugin extends BasePlugin`
|
||||
- use `afterModelCallback` for token counts (`LlmResponse.usageMetadata` is available)
|
||||
- use `beforeToolCallback`/`afterToolCallback` with `functionCallId` for per-tool timing
|
||||
- capture HTTP-level metrics (status code, duration) in `GcliAgentModel` wrapper
|
||||
- ~70% of Clearcut metrics stay in their current call sites unchanged — document which
|
||||
- replace fake API examples with a telemetry parity matrix showing capture mechanism per metric
|
||||
|
||||
### 3.7 Dynamic Model Routing and Configurability
|
||||
|
||||
Assessment: partly feasible, materially overstated.
|
||||
|
||||
Required change:
|
||||
- remove “100% possible today”
|
||||
- separate alias rewrite, fallback selection, classifier routing, and banning/rejection behavior
|
||||
- define a real `RoutingContextBuilder`
|
||||
|
||||
### 3.8 Fallbacks and Availability Management
|
||||
|
||||
Assessment: incomplete.
|
||||
|
||||
Required change:
|
||||
- split preflight fallback from post-failure transition behavior
|
||||
- define retry safety
|
||||
- define main-call vs utility-call fallback ownership
|
||||
|
||||
### 3.9 State-Driven Mode Switching
|
||||
|
||||
Assessment: too narrow.
|
||||
|
||||
Required change:
|
||||
- define a single mode state source of truth
|
||||
- enumerate all consumers: prompt, toolset, policy, router, UI
|
||||
|
||||
### 3.10 Tool Output Masking
|
||||
|
||||
Assessment: one of the stronger sections.
|
||||
|
||||
Required change:
|
||||
- define whether masking mutates persisted history or only outgoing request history
|
||||
- define ordering and idempotence relative to compaction
|
||||
- define file/artifact lifecycle
|
||||
|
||||
### 4. SDK Facade / Stateful Orchestration
|
||||
|
||||
Assessment: this should become the architectural center of the document.
|
||||
|
||||
Required change:
|
||||
- expand ownership boundaries
|
||||
- describe the shared runtime plus adapter model
|
||||
- document temporary approval/elicitation limitations explicitly
|
||||
|
||||
### 4.1 Hybrid Tool Instantiation
|
||||
|
||||
Assessment: directionally good.
|
||||
|
||||
Required change:
|
||||
- include message bus derivation
|
||||
- include MCP discovery scoping
|
||||
- include recursion prevention and tool isolation details
|
||||
|
||||
### 4.2 Custom File-Based Persistence
|
||||
|
||||
Assessment: straightforward for storage, but rewind semantics need work.
|
||||
|
||||
The storage layer itself is low-risk — `GcliFileSessionService` wraps existing `ChatRecordingService` and implements 4 abstract methods. DB concerns (locking, stale-writer, crash recovery) don't apply to a single-user CLI.
|
||||
|
||||
Required change:
|
||||
- define rewind as event-log truncation + state recomputation (not just file truncation)
|
||||
- state that concurrent runs per session are forbidden (single-writer)
|
||||
- define how app/user/temp state prefixes map to existing workspace JSON
|
||||
|
||||
### 4.3 Decomposing `AgentLoopContext`
|
||||
|
||||
Assessment: right instinct, incomplete decomposition.
|
||||
|
||||
Required change:
|
||||
- account for message bus / confirmation routing
|
||||
- account for injection queues
|
||||
- account for prompt/resource registries and MCP scoping
|
||||
|
||||
### 5.1 Real-Time User Message Injections
|
||||
|
||||
Assessment: blocker analysis is incomplete.
|
||||
|
||||
Required change:
|
||||
- mention TS live runtime gaps directly
|
||||
- describe temporary UX until true interruption exists
|
||||
|
||||
### 5.2 Conversation Rewind and State Reversal
|
||||
|
||||
Assessment: too shallow today.
|
||||
|
||||
Required change:
|
||||
- define rewind as event-log truncation plus state rollback semantics
|
||||
- define confirmation/auth rollback semantics
|
||||
- define artifact/version rollback expectations
|
||||
|
||||
### 5.3 Concurrent Sessions
|
||||
|
||||
Assessment: a single-user CLI does not need DB-level locking.
|
||||
|
||||
Required change:
|
||||
- state that concurrent runs per session are forbidden (single-writer model)
|
||||
- this is sufficient for a CLI — no further locking design needed
|
||||
|
||||
## Corrections to Earlier Reviews
|
||||
|
||||
### Auth is implementable, not blocked
|
||||
API-key auth works via `httpOptions.headers`. Coast Assist / `LOGIN_WITH_GOOGLE` requires `GcliAgentModel` to extend `BaseLlm` directly with its own transport (using `AuthClient.request()`), not wrap `Gemini`. The OAuth flow, token refresh, and credential caching all work as-is — the issue was the design doc's assumption about the inner model, not auth capability.
|
||||
|
||||
### Persistence is simpler than initially claimed
|
||||
DB-level concerns (PESSIMISTIC_WRITE, stale-writer detection, concurrent append policy) come from `DatabaseSessionService` and don't apply. The file-backed service wraps existing `ChatRecordingService`. Only rewind semantics need deeper design.
|
||||
|
||||
### Interactive PR findings are rollout-risk evidence, not architecture defects
|
||||
The branch-stack dependency, hook-order risk, and `_meta.legacyState` leakage in PR #24297 are real but secondary to the core architecture gaps above.
|
||||
|
||||
## What To Change Before Principal Review
|
||||
|
||||
1. **Fix the 4 compilation blockers** (C1-C4) — these will be the first thing reviewers check
|
||||
2. **Add an explicit ADK→AgentProtocol translation architecture section** — this is the real design center
|
||||
3. **Add the shared-core subagent architecture** — custom `BaseTool` over shared runtime, not `AgentTool`
|
||||
4. **Use `BaseContextCompactor`** for compaction instead of embedding it in the model layer
|
||||
5. **Replace “100% possible today” and similar language** with explicit labels: native / bridgeable / blocked
|
||||
6. **Add a parity matrix** (feature | ADK mechanism | status | bridge workaround | long-term target)
|
||||
|
||||
## Phased Migration Plan
|
||||
|
||||
### Phase 0: Foundation (current state → near-term)
|
||||
**Goal:** Establish the shared runtime core and translation layer.
|
||||
|
||||
- [ ] Fix compilation blockers (C1-C4) in design doc code samples
|
||||
- [ ] Implement `AdkRuntimeCore` wrapping ADK `Runner` + `LlmAgent`
|
||||
- [ ] Implement `AdkEventTranslator` (ADK `Event` → gemini-cli `AgentEvent`)
|
||||
- [ ] Implement `GcliAgentModel extends BaseLlm` with dual transport (API-key via `httpOptions.headers`, Coast Assist via `AuthClient.request()`)
|
||||
- [ ] Implement `GcliFileSessionService extends BaseSessionService` wrapping `ChatRecordingService`
|
||||
- [ ] Implement `GcliContextCompactor implements BaseContextCompactor` wrapping `ChatCompressionService`
|
||||
- [ ] Wire behind `experimental.adk` feature gate
|
||||
|
||||
**Non-goals for Phase 0:**
|
||||
- No interactive flow changes
|
||||
- No subagent support
|
||||
- No live/bidi connections
|
||||
- No resumable approvals
|
||||
|
||||
**Exit criteria:** Non-interactive flow produces identical output behind feature gate.
|
||||
|
||||
### Phase 1: Non-Interactive Parity
|
||||
**Goal:** Feature-gated non-interactive flow matches legacy behavior.
|
||||
|
||||
- [ ] Tool output masking via `BaseLlmRequestProcessor`
|
||||
- [ ] Model routing via `beforeModelCallback` + `RoutingContextBuilder` adapter
|
||||
- [ ] Policy enforcement via `beforeToolCallback` using existing callbacks (temporary bridge)
|
||||
- [ ] Quota/availability handling via `beforeModelCallback`
|
||||
- [ ] `ClearcutTelemetryPlugin extends BasePlugin` for token counts (`afterModelCallback`), per-tool timing (`beforeToolCallback`/`afterToolCallback` with `functionCallId`), agent lifecycle, model errors
|
||||
- [ ] HTTP-level telemetry (status code, duration) captured in `GcliAgentModel` wrapper
|
||||
- [ ] Verify ~70% of existing Clearcut call sites work unchanged
|
||||
|
||||
**Non-goals for Phase 1:**
|
||||
- No interactive UI changes
|
||||
- No plan mode switching
|
||||
- No subagents
|
||||
- Approvals are callback-based, not resumable
|
||||
|
||||
**Exit criteria:** Non-interactive tests pass with `experimental.adk.enabled = true`. Legacy path remains default.
|
||||
|
||||
### Phase 2: Interactive Flow Migration
|
||||
**Goal:** Interactive flow uses the same ADK runtime behind `TopLevelSessionAdapter`.
|
||||
|
||||
- [ ] `TopLevelSessionAdapter` exposing runtime as `AgentProtocol` / `AgentSession`
|
||||
- [ ] Stream lifecycle (`agent_start` / `agent_end` / `streamId` ownership)
|
||||
- [ ] Replay/reattach semantics matching current `AgentSession.stream()` behavior
|
||||
- [ ] Plan mode via dynamic `BaseToolset` + `InstructionProvider` + mode state
|
||||
- [ ] `ApprovalBridge` for tool confirmations using existing UI callbacks
|
||||
- [ ] Elicitation via existing callbacks (not yet bidi)
|
||||
- [ ] `_meta.legacyState` bridge for UI rendering (documented as temporary)
|
||||
|
||||
**Non-goals for Phase 2:**
|
||||
- No live/bidi parity (`runLiveImpl` is still a stub in ADK TS)
|
||||
- No true mid-turn interruption (steering limited to once-per-LLM-call)
|
||||
- No resumable approvals across process death
|
||||
- No concurrent multi-stream sessions
|
||||
|
||||
**Exit criteria:** Interactive flow works behind feature gate. Legacy path remains default.
|
||||
|
||||
### Phase 3: Subagents and SDK Readiness
|
||||
**Goal:** Subagents share the core runtime. The architecture is SDK-reusable.
|
||||
|
||||
- [ ] Custom `BaseTool` subagent wrappers over shared `AdkRuntimeCore`
|
||||
- [ ] `threadId`-scoped event projection for child activity
|
||||
- [ ] Shared policy, routing, and config inheritance
|
||||
- [ ] Remove `_meta.legacyState` — replace with proper event/adapter separation
|
||||
- [ ] Migrate approval bridge to native ADK elicitation/bidi (when available)
|
||||
- [ ] Define SDK public API surface
|
||||
|
||||
**Non-goals for Phase 3:**
|
||||
- Full ADK Dev UI compatibility
|
||||
- Agent-to-agent transfer via ADK's native mechanism
|
||||
|
||||
**Exit criteria:** Subagent invocation works. Architecture is documented for SDK consumers.
|
||||
|
||||
## Positive Signals
|
||||
|
||||
- Feature-gating behind `experimental.adk` is the right rollout pattern
|
||||
- Tool output masking maps cleanly to request-preprocessing
|
||||
- Dynamic toolset + `InstructionProvider` for plan mode is a good ADK-native fit
|
||||
- The phased migration checklist with tracking issues shows good engineering discipline
|
||||
- The doc correctly identifies 3 known ADK gaps (sections 5.1-5.3)
|
||||
- `AgentProtocol` / `AgentSession` is the right consumer-facing boundary
|
||||
- The policy adapter pattern is directionally sound even with reduced phase-1 semantics
|
||||
|
||||
## Concern Decomposition
|
||||
|
||||
| Concern | Level | ADK Mechanism |
|
||||
|---|---|---|
|
||||
| Auth (API key) | Transport | `BaseLlm` constructor / `httpOptions.headers` |
|
||||
| Auth (Coast Assist) | Transport | `BaseLlm` direct — custom transport via `AuthClient.request()` |
|
||||
| Model rewrite | Transport | `BaseLlm.generateContentAsync` model override |
|
||||
| Model routing | Agent | `beforeModelCallback` + `RoutingContextBuilder` |
|
||||
| Token compaction | Agent | `BaseContextCompactor` (native ADK) |
|
||||
| Quota/availability | Agent | `beforeModelCallback` |
|
||||
| Tool masking | Agent | `BaseLlmRequestProcessor` |
|
||||
| Tool confirmations | Agent+Consumer | `beforeToolCallback` + existing callbacks (phase 1) |
|
||||
| Telemetry (lifecycle) | Agent | `ClearcutTelemetryPlugin` — `afterModelCallback`, `beforeToolCallback`/`afterToolCallback` |
|
||||
| Telemetry (HTTP) | Transport | `GcliAgentModel` wrapper (status code, duration) |
|
||||
| Telemetry (CLI) | Consumer | Existing call sites (~70% of Clearcut metrics, unchanged) |
|
||||
| UI rendering | Consumer | Event subscriber / adapter |
|
||||
| Subagent invocation | Agent | Custom `BaseTool` over shared runtime core |
|
||||
|
||||
## Final Verdict
|
||||
|
||||
The design is architecturally correct in its goal. To be principal-review ready:
|
||||
|
||||
1. Fix the 4 compilation blockers
|
||||
2. Add the ADK→AgentProtocol translation architecture as the design center
|
||||
3. Adopt native `BaseContextCompactor` instead of model-layer compaction
|
||||
4. Use custom `BaseTool` subagents over shared core (not `AgentTool`)
|
||||
5. Downgrade parity claims to match reality
|
||||
6. Add the phased plan with explicit non-goals per phase
|
||||
|
||||
The migration path is: shared runtime core → translation layer → adapters (top-level session, subagent tools, approval bridge). Each phase ships independently behind the feature gate.
|
||||
@@ -106,67 +106,6 @@ organization.
|
||||
ensures users maintain final control over which permitted servers are actually
|
||||
active in their environment.
|
||||
|
||||
#### Required MCP Servers (preview)
|
||||
|
||||
**Default**: empty
|
||||
|
||||
Allows administrators to define MCP servers that are **always injected** into
|
||||
the user's environment. Unlike the allowlist (which filters user-configured
|
||||
servers), required servers are automatically added regardless of the user's
|
||||
local configuration.
|
||||
|
||||
**Required Servers Format:**
|
||||
|
||||
```json
|
||||
{
|
||||
"requiredMcpServers": {
|
||||
"corp-compliance-tool": {
|
||||
"url": "https://mcp.corp/compliance",
|
||||
"type": "http",
|
||||
"trust": true,
|
||||
"description": "Corporate compliance tool"
|
||||
},
|
||||
"internal-registry": {
|
||||
"url": "https://registry.corp/mcp",
|
||||
"type": "sse",
|
||||
"authProviderType": "google_credentials",
|
||||
"oauth": {
|
||||
"scopes": ["https://www.googleapis.com/auth/scope"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Supported Fields:**
|
||||
|
||||
- `url`: (Required) The full URL of the MCP server endpoint.
|
||||
- `type`: (Required) The connection type (`sse` or `http`).
|
||||
- `trust`: (Optional) If set to `true`, tool execution will not require user
|
||||
approval. Defaults to `true` for required servers.
|
||||
- `description`: (Optional) Human-readable description of the server.
|
||||
- `authProviderType`: (Optional) Authentication provider (`dynamic_discovery`,
|
||||
`google_credentials`, or `service_account_impersonation`).
|
||||
- `oauth`: (Optional) OAuth configuration including `scopes`, `clientId`, and
|
||||
`clientSecret`.
|
||||
- `targetAudience`: (Optional) OAuth target audience for service-to-service
|
||||
auth.
|
||||
- `targetServiceAccount`: (Optional) Service account email to impersonate.
|
||||
- `headers`: (Optional) Additional HTTP headers to send with requests.
|
||||
- `includeTools` / `excludeTools`: (Optional) Tool filtering lists.
|
||||
- `timeout`: (Optional) Timeout in milliseconds for MCP requests.
|
||||
|
||||
**Client Enforcement Logic:**
|
||||
|
||||
- Required servers are injected **after** allowlist filtering, so they are
|
||||
always available even if the allowlist is active.
|
||||
- If a required server has the **same name** as a locally configured server, the
|
||||
admin configuration **completely overrides** the local one.
|
||||
- Required servers only support remote transports (`sse`, `http`). Local
|
||||
execution fields (`command`, `args`, `env`, `cwd`) are not supported.
|
||||
- Required servers can coexist with allowlisted servers — both features work
|
||||
independently.
|
||||
|
||||
### Unmanaged Capabilities
|
||||
|
||||
**Enabled/Disabled** | Default: disabled
|
||||
|
||||
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 126 KiB |