gemcode 0.3.15__tar.gz → 0.3.16__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gemcode-0.3.15/src/gemcode.egg-info → gemcode-0.3.16}/PKG-INFO +1 -1
- {gemcode-0.3.15 → gemcode-0.3.16}/pyproject.toml +1 -1
- gemcode-0.3.16/src/gemcode/agent.py +286 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/config.py +79 -3
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tool_registry.py +4 -2
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools/__init__.py +9 -2
- gemcode-0.3.16/src/gemcode/tools/bash.py +127 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools/filesystem.py +67 -5
- gemcode-0.3.16/src/gemcode/tools/search.py +142 -0
- gemcode-0.3.16/src/gemcode/tools/web.py +133 -0
- {gemcode-0.3.15 → gemcode-0.3.16/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode.egg-info/SOURCES.txt +2 -0
- gemcode-0.3.15/src/gemcode/agent.py +0 -199
- gemcode-0.3.15/src/gemcode/tools/search.py +0 -85
- {gemcode-0.3.15 → gemcode-0.3.16}/LICENSE +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/MANIFEST.in +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/README.md +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/setup.cfg +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/cli.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/version.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_credentials.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_paths.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_permissions.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_tools.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.15 → gemcode-0.3.16}/tests/test_workspace_hints.py +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Root LlmAgent definition (Claude Code: agent config + tool list, analogous to tools.ts + prompts).
|
|
3
|
+
|
|
4
|
+
See `session_runtime.py` for Runner/session wiring (outer layer).
|
|
5
|
+
See `tool_registry.py` for tool categories (read vs mutating vs shell).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from google.adk.agents.llm_agent import LlmAgent
|
|
14
|
+
|
|
15
|
+
from gemcode.autocompact import make_before_model_autocompact_callback
|
|
16
|
+
from gemcode.callbacks import (
|
|
17
|
+
make_after_model_callback,
|
|
18
|
+
make_after_tool_callback,
|
|
19
|
+
make_before_tool_callback,
|
|
20
|
+
make_on_model_error_callback,
|
|
21
|
+
make_on_tool_error_callback,
|
|
22
|
+
)
|
|
23
|
+
from gemcode.compaction import make_before_model_callback
|
|
24
|
+
from gemcode.config import GemCodeConfig
|
|
25
|
+
from gemcode.context_budget import make_before_model_context_shrink_callback
|
|
26
|
+
from gemcode.limits import make_before_model_limits_callback, make_before_model_token_budget_callback
|
|
27
|
+
from gemcode.thinking import build_thinking_config
|
|
28
|
+
from gemcode.tools import build_function_tools
|
|
29
|
+
from gemcode.tool_prompt_manifest import build_tool_manifest
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _chain_before_model_callbacks(*callbacks):
|
|
33
|
+
cbs = [c for c in callbacks if c is not None]
|
|
34
|
+
if not cbs:
|
|
35
|
+
return None
|
|
36
|
+
if len(cbs) == 1:
|
|
37
|
+
return cbs[0]
|
|
38
|
+
|
|
39
|
+
async def chained(callback_context, llm_request):
|
|
40
|
+
for cb in cbs:
|
|
41
|
+
out = cb(callback_context, llm_request)
|
|
42
|
+
if inspect.isawaitable(out):
|
|
43
|
+
out = await out
|
|
44
|
+
if out is not None:
|
|
45
|
+
return out
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
return chained
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _load_gemini_md(project_root: Path) -> str:
|
|
52
|
+
for name in ("GEMINI.md", "gemini.md"):
|
|
53
|
+
p = project_root / name
|
|
54
|
+
if p.is_file():
|
|
55
|
+
return p.read_text(encoding="utf-8", errors="replace")[:50_000]
|
|
56
|
+
return ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Injected every session so the model does not hallucinate deployment, permissions,
|
|
62
|
+
or "how to switch Pro" the way a product-agnostic base prompt would.
|
|
63
|
+
"""
|
|
64
|
+
root = cfg.project_root.resolve()
|
|
65
|
+
model = (getattr(cfg, "model", None) or "").strip() or "(default)"
|
|
66
|
+
return f"""## Runtime facts (authoritative for this session)
|
|
67
|
+
- **Project root** — every filesystem tool path is relative to: `{root}`
|
|
68
|
+
- **Model id in use:** `{model}`. In this TUI/REPL you can override it for subsequent turns with `/model use <id>` (use `/model list` to browse IDs). For a full restart you can still use `--model <id>` or env `GEMCODE_MODEL`.
|
|
69
|
+
- **UI banner** phrases such as "GemCode Pro" are **terminal marketing**, not a separate API tier or model you enable from chat.
|
|
70
|
+
- **Env toggles** (`GEMCODE_ENABLE_COMPUTER_USE`, `GEMCODE_MODEL`, etc.) affect only the **OS process** that launched `gemcode`. Pasting `VAR=1` in chat does **not** reconfigure a running session—tell the user to export in their shell, use project `.env`, or restart the CLI.
|
|
71
|
+
- **Working in subfolders** — use tools: e.g. `list_directory("Desktop")`, `glob_files("**/query.ts")`, `read_file("testing/ai-edtech-app/src/app/page.tsx")`, or `run_command` with `cwd_subdir`. Never claim the sandbox cannot reach a subpath unless a tool returned an explicit error."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_instruction(cfg: GemCodeConfig) -> str:
|
|
75
|
+
base = f"""You are GemCode, an expert software engineering agent powered by Google Gemini.
|
|
76
|
+
You run locally via the GemCode CLI. You are the same agent the user launched — not a hosted portal.
|
|
77
|
+
|
|
78
|
+
{_build_runtime_facts(cfg)}
|
|
79
|
+
|
|
80
|
+
## Core identity and approach
|
|
81
|
+
You are a senior engineer who *acts*, not just advises. When given a task:
|
|
82
|
+
1. **Orient** — understand the repo structure, find the relevant files.
|
|
83
|
+
2. **Plan** — for complex tasks, call `todo_write` upfront to map out the work.
|
|
84
|
+
3. **Execute** — make the changes, run the checks, iterate.
|
|
85
|
+
4. **Verify** — confirm the result is correct before reporting done.
|
|
86
|
+
|
|
87
|
+
Never stop mid-task just because the first tool call succeeded. Keep going until the full task is complete or you hit a genuine blocker.
|
|
88
|
+
|
|
89
|
+
## Thinking through hard problems
|
|
90
|
+
You have native deep thinking capability — use it actively:
|
|
91
|
+
- **Before** starting a complex refactor or architectural change, think: what are the dependencies? what could break? what is the minimal safe change?
|
|
92
|
+
- **When debugging**: trace the execution path mentally before acting. Form a hypothesis, then verify with tools.
|
|
93
|
+
- **When stuck after 2 attempts**: stop and reconsider your assumptions rather than repeating the same approach.
|
|
94
|
+
- **For trade-off decisions** (which library, which pattern, which approach): reason through the pros/cons given this specific codebase.
|
|
95
|
+
|
|
96
|
+
## Interpreting requests
|
|
97
|
+
- Treat every message as a software engineering task unless clearly otherwise.
|
|
98
|
+
- If vague ("fix it", "the config", "rename that"), **infer from the repo**: search, read, then act. Do not give abstract advice when concrete files exist.
|
|
99
|
+
- If the user refers to symbols or behaviors, **find them** with glob/grep/list — never ask them to paste paths you can discover yourself.
|
|
100
|
+
- **Never propose edits to files you haven't read.** Read first, then edit.
|
|
101
|
+
- When something fails, diagnose (re-read the error, check assumptions) before switching strategy. Do not repeat the same failed call.
|
|
102
|
+
|
|
103
|
+
## Tool selection guide
|
|
104
|
+
|
|
105
|
+
### Shell execution (critical — use these for real work)
|
|
106
|
+
- **`bash`** — use for all shell workflows that need pipelines, redirects, or shell features:
|
|
107
|
+
- `bash("git log --oneline -20")` — git history
|
|
108
|
+
- `bash("git diff HEAD~1 -- src/api/")` — targeted diff
|
|
109
|
+
- `bash("git status && git diff --stat")` — repo state
|
|
110
|
+
- `bash("find . -name '*.py' | xargs grep -l 'SomeClass' | head -20")` — cross-file search
|
|
111
|
+
- `bash("npm run build 2>&1 | tail -50")` — build output (stderr + stdout combined)
|
|
112
|
+
- `bash("pytest tests/ -x -q --tb=short 2>&1 | head -150")` — test run
|
|
113
|
+
- `bash("cat package.json | python3 -m json.tool")` — parse JSON
|
|
114
|
+
- `bash("ls -la src/ | grep -E '\\.(ts|tsx)$'")` — filtered listing
|
|
115
|
+
- `bash("wc -l $(find . -name '*.py') | sort -n | tail -20")` — largest files
|
|
116
|
+
- For **dev servers**: `bash("npm run dev", background=True, cwd_subdir="frontend")`
|
|
117
|
+
- For **subfolders**: `bash("cargo build --release", cwd_subdir="backend")`
|
|
118
|
+
|
|
119
|
+
- **`run_command`** — simple single-executable calls without shell features:
|
|
120
|
+
- `run_command("npm", args=["install", "--legacy-peer-deps"])` — clean npm install
|
|
121
|
+
- `run_command("python3", args=["-m", "pytest", "--version"])` — version check
|
|
122
|
+
- Use `extra_env_keys`/`extra_env_values` for non-interactive scaffolding tools.
|
|
123
|
+
|
|
124
|
+
### File operations
|
|
125
|
+
- **`read_file`** — read code/config. Use `start_line`/`end_line` for large files:
|
|
126
|
+
- `read_file("src/server.py", start_line=100, end_line=200)` — read a section
|
|
127
|
+
- `read_file("long_file.py", start_line=500)` — from line 500 to end
|
|
128
|
+
- Always read before editing.
|
|
129
|
+
|
|
130
|
+
- **`grep_content`** — search with regex. Use `context_lines` to see surrounding code:
|
|
131
|
+
- `grep_content("def authenticate", "**/*.py", context_lines=4)` — function + context
|
|
132
|
+
- `grep_content("TODO|FIXME|HACK", "**/*.ts")` — multiple patterns (regex alternation)
|
|
133
|
+
- `grep_content("import React", "**/*.tsx", case_sensitive=False)` — case-insensitive
|
|
134
|
+
- `grep_content("class.*Error", "**/*.py", context_lines=2)` — error classes
|
|
135
|
+
|
|
136
|
+
- **`glob_files`** — find files by name pattern:
|
|
137
|
+
- `glob_files("**/*.test.ts")`, `glob_files("**/config*.json")`, `glob_files("src/**/*.py")`
|
|
138
|
+
|
|
139
|
+
- **`list_directory`** — explore directory structure:
|
|
140
|
+
- `list_directory(".")`, `list_directory("src/api")`, `list_directory("Desktop")`
|
|
141
|
+
|
|
142
|
+
- **`write_file`** — create or overwrite files. Read first if the file exists.
|
|
143
|
+
- **`search_replace`** — targeted in-place edits. Provide enough context in `old_string` to be unique.
|
|
144
|
+
- **`move_file`** — rename or reorganize files/directories within the project.
|
|
145
|
+
- **`delete_file`** — remove a single file.
|
|
146
|
+
|
|
147
|
+
### Research and documentation
|
|
148
|
+
- **`web_fetch`** — fetch docs, APIs, changelogs, READMEs from the web:
|
|
149
|
+
- `web_fetch("https://docs.python.org/3/library/asyncio.html")` — official docs
|
|
150
|
+
- `web_fetch("https://api.github.com/repos/owner/repo/releases/latest")` — API data
|
|
151
|
+
- `web_fetch("https://registry.npmjs.org/react/latest")` — npm package info
|
|
152
|
+
- Use when you need to look up an API, check the latest version, or read documentation.
|
|
153
|
+
|
|
154
|
+
### Planning
|
|
155
|
+
- **`todo_write`** — track work items. Use for any task with 3+ steps.
|
|
156
|
+
- Create at task start, mark completed as you finish, merge updates.
|
|
157
|
+
|
|
158
|
+
## Multi-step task execution
|
|
159
|
+
One user message = many model↔tool rounds (up to 256 LLM calls by default). This is intentional — you are expected to do complete tasks autonomously.
|
|
160
|
+
|
|
161
|
+
**Standard workflow for complex tasks:**
|
|
162
|
+
1. `todo_write` — plan the work items
|
|
163
|
+
2. Explore — `bash("find . ...")` or `list_directory` or `glob_files` to understand structure
|
|
164
|
+
3. Read — `read_file` with line ranges on large files; `grep_content` for symbol search
|
|
165
|
+
4. Edit — `write_file` / `search_replace` for changes
|
|
166
|
+
5. Verify — `bash("pytest ...")` or `bash("npm run build ...")` or `bash("git diff")`
|
|
167
|
+
6. Fix — iterate on failures, re-verify
|
|
168
|
+
7. Update todos — mark done as you go
|
|
169
|
+
|
|
170
|
+
**Do not stop after step 2 or 3** — complete the full task.
|
|
171
|
+
|
|
172
|
+
## Parallelism
|
|
173
|
+
Issue independent tool calls in the same turn when outputs don't depend on each other:
|
|
174
|
+
- Reading multiple files simultaneously ✓
|
|
175
|
+
- Grepping for different patterns at once ✓
|
|
176
|
+
- `list_directory` + `glob_files` in parallel ✓
|
|
177
|
+
Sequential: when step B needs step A's result.
|
|
178
|
+
|
|
179
|
+
## Error recovery
|
|
180
|
+
- **Test/build failures**: read the full error, identify the exact line, fix, re-run.
|
|
181
|
+
- **Tool errors**: diagnose why it failed before retrying — don't repeat the exact same call.
|
|
182
|
+
- **After 2 failed attempts on the same problem**: stop and explain the blocker clearly.
|
|
183
|
+
- **Unexpected file content**: re-read the actual file rather than assuming your mental model is correct.
|
|
184
|
+
|
|
185
|
+
## Risk and permissions
|
|
186
|
+
- State destructive operations clearly before doing them (deletes, force-push, data truncation).
|
|
187
|
+
- For `bash` commands that could be destructive (`rm -rf`, `git push --force`), confirm with the user first.
|
|
188
|
+
- If a tool is denied, adjust the plan — don't retry the same gated call.
|
|
189
|
+
|
|
190
|
+
## Communication
|
|
191
|
+
- One short line before the first tool call in a turn (e.g. "Reading the auth module and checking the test suite...").
|
|
192
|
+
- Summarize tool results in plain language — the user doesn't see raw tool internals.
|
|
193
|
+
- After completing a task: clear summary of what changed, where, and why.
|
|
194
|
+
- If the user pastes UI copy / noise / error output, extract the real intent and act on source files.
|
|
195
|
+
- Prefer small, testable, accurate changes over broad rewrites.
|
|
196
|
+
|
|
197
|
+
## Workspace scope
|
|
198
|
+
All file tools use paths **relative to the project root** (where GemCode was started). The root may be the home folder — subfolders like `Desktop`, `Desktop/code`, `Documents` are inside the sandbox. Call `list_directory("Desktop")` or `glob_files("**/*name*.ts")` instead of assuming access is blocked. Only treat access as denied when a tool returns an explicit `error`."""
|
|
199
|
+
|
|
200
|
+
tool_manifest = build_tool_manifest(cfg)
|
|
201
|
+
|
|
202
|
+
if tool_manifest:
|
|
203
|
+
base = f"{base}\n\n{tool_manifest}"
|
|
204
|
+
extra = _load_gemini_md(cfg.project_root)
|
|
205
|
+
if extra.strip():
|
|
206
|
+
return f"{base}\n\n## Project instructions (GEMINI.md)\n{extra}"
|
|
207
|
+
return base
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def build_root_agent(cfg: GemCodeConfig, extra_tools: list | None = None) -> LlmAgent:
|
|
211
|
+
"""Create the root LlmAgent with tools and callbacks (no Runner)."""
|
|
212
|
+
tools = build_function_tools(cfg)
|
|
213
|
+
if getattr(cfg, "enable_memory", False):
|
|
214
|
+
# ADK preload_memory injects retrieved memories into the next llm_request.
|
|
215
|
+
from google.adk.tools import preload_memory
|
|
216
|
+
|
|
217
|
+
tools = [preload_memory, *tools]
|
|
218
|
+
if extra_tools:
|
|
219
|
+
tools = [*tools, *extra_tools]
|
|
220
|
+
|
|
221
|
+
before_model = _chain_before_model_callbacks(
|
|
222
|
+
make_before_model_autocompact_callback(cfg),
|
|
223
|
+
make_before_model_context_shrink_callback(cfg),
|
|
224
|
+
make_before_model_callback(cfg),
|
|
225
|
+
make_before_model_limits_callback(cfg),
|
|
226
|
+
make_before_model_token_budget_callback(cfg),
|
|
227
|
+
)
|
|
228
|
+
cb_kwargs: dict = {
|
|
229
|
+
"before_tool_callback": make_before_tool_callback(cfg),
|
|
230
|
+
"after_tool_callback": make_after_tool_callback(cfg),
|
|
231
|
+
"after_model_callback": make_after_model_callback(cfg),
|
|
232
|
+
"on_tool_error_callback": make_on_tool_error_callback(cfg),
|
|
233
|
+
"on_model_error_callback": make_on_model_error_callback(cfg),
|
|
234
|
+
}
|
|
235
|
+
if before_model is not None:
|
|
236
|
+
cb_kwargs["before_model_callback"] = before_model
|
|
237
|
+
|
|
238
|
+
# Claude-like thinking: enabled by default (Gemini dynamic), but allow
|
|
239
|
+
# explicit overrides for disable/budgets/levels.
|
|
240
|
+
gen_cfg = None
|
|
241
|
+
thinking_cfg = build_thinking_config(cfg)
|
|
242
|
+
tool_cfg = None
|
|
243
|
+
model_id = getattr(cfg, "model", "") or ""
|
|
244
|
+
is_gemini_3 = "gemini-3" in model_id.lower()
|
|
245
|
+
comb_mode = (getattr(cfg, "tool_combination_mode", None) or "deep_research").lower()
|
|
246
|
+
enable_for_run = False
|
|
247
|
+
if comb_mode in ("auto", "deep_research"):
|
|
248
|
+
enable_for_run = bool(getattr(cfg, "enable_deep_research", False))
|
|
249
|
+
elif comb_mode == "always":
|
|
250
|
+
enable_for_run = True
|
|
251
|
+
elif comb_mode == "never":
|
|
252
|
+
enable_for_run = False
|
|
253
|
+
else:
|
|
254
|
+
# Unknown values: stay conservative.
|
|
255
|
+
enable_for_run = bool(getattr(cfg, "enable_deep_research", False))
|
|
256
|
+
|
|
257
|
+
if enable_for_run and is_gemini_3:
|
|
258
|
+
from google.genai import types
|
|
259
|
+
|
|
260
|
+
# Gemini "tool context circulation" enables built-in tools results to
|
|
261
|
+
# be combined with your client-side function tools in the same workflow.
|
|
262
|
+
tool_cfg = types.ToolConfig(include_server_side_tool_invocations=True)
|
|
263
|
+
|
|
264
|
+
if thinking_cfg is not None or tool_cfg is not None:
|
|
265
|
+
from google.genai import types
|
|
266
|
+
|
|
267
|
+
gen_cfg = types.GenerateContentConfig(
|
|
268
|
+
thinking_config=thinking_cfg,
|
|
269
|
+
tool_config=tool_cfg,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return LlmAgent(
|
|
273
|
+
model=cfg.model,
|
|
274
|
+
name="gemcode",
|
|
275
|
+
instruction=build_instruction(cfg),
|
|
276
|
+
tools=tools,
|
|
277
|
+
generate_content_config=gen_cfg,
|
|
278
|
+
**cb_kwargs,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None):
|
|
283
|
+
"""Backward-compatible: prefer `gemcode.session_runtime.create_runner`."""
|
|
284
|
+
from gemcode.session_runtime import create_runner as _cr
|
|
285
|
+
|
|
286
|
+
return _cr(cfg, extra_tools=extra_tools)
|
|
@@ -259,18 +259,94 @@ class GemCodeConfig:
|
|
|
259
259
|
else:
|
|
260
260
|
self.allow_commands = frozenset(
|
|
261
261
|
(
|
|
262
|
+
# Python
|
|
262
263
|
"pytest",
|
|
263
264
|
"python3",
|
|
264
265
|
"python",
|
|
265
266
|
"pip",
|
|
266
267
|
"pip3",
|
|
267
|
-
"npm",
|
|
268
|
-
"npx",
|
|
269
|
-
"git",
|
|
270
268
|
"ruff",
|
|
271
269
|
"uv",
|
|
270
|
+
"mypy",
|
|
271
|
+
"black",
|
|
272
|
+
"isort",
|
|
273
|
+
"flake8",
|
|
274
|
+
"pylint",
|
|
275
|
+
# Node / JS
|
|
276
|
+
"npm",
|
|
277
|
+
"npx",
|
|
278
|
+
"node",
|
|
279
|
+
"pnpm",
|
|
280
|
+
"yarn",
|
|
281
|
+
"bun",
|
|
282
|
+
"bunx",
|
|
283
|
+
"tsc",
|
|
284
|
+
"eslint",
|
|
285
|
+
"prettier",
|
|
286
|
+
"vite",
|
|
287
|
+
"next",
|
|
288
|
+
"turbo",
|
|
289
|
+
# Systems
|
|
272
290
|
"cargo",
|
|
273
291
|
"go",
|
|
292
|
+
"rustc",
|
|
293
|
+
"make",
|
|
294
|
+
"cmake",
|
|
295
|
+
"gradle",
|
|
296
|
+
"mvn",
|
|
297
|
+
"java",
|
|
298
|
+
"javac",
|
|
299
|
+
"dotnet",
|
|
300
|
+
# Version control
|
|
301
|
+
"git",
|
|
302
|
+
"gh",
|
|
303
|
+
# Shell utilities (safe read/inspect)
|
|
304
|
+
"cat",
|
|
305
|
+
"ls",
|
|
306
|
+
"echo",
|
|
307
|
+
"find",
|
|
308
|
+
"head",
|
|
309
|
+
"tail",
|
|
310
|
+
"wc",
|
|
311
|
+
"sort",
|
|
312
|
+
"uniq",
|
|
313
|
+
"cut",
|
|
314
|
+
"awk",
|
|
315
|
+
"sed",
|
|
316
|
+
"grep",
|
|
317
|
+
"rg",
|
|
318
|
+
"ag",
|
|
319
|
+
# File management
|
|
320
|
+
"mkdir",
|
|
321
|
+
"cp",
|
|
322
|
+
"mv",
|
|
323
|
+
"touch",
|
|
324
|
+
"chmod",
|
|
325
|
+
"chown",
|
|
326
|
+
# Networking (read)
|
|
327
|
+
"curl",
|
|
328
|
+
"wget",
|
|
329
|
+
# Containers
|
|
330
|
+
"docker",
|
|
331
|
+
"docker-compose",
|
|
332
|
+
"kubectl",
|
|
333
|
+
# Database
|
|
334
|
+
"psql",
|
|
335
|
+
"mysql",
|
|
336
|
+
"sqlite3",
|
|
337
|
+
# Misc
|
|
338
|
+
"jq",
|
|
339
|
+
"yq",
|
|
340
|
+
"which",
|
|
341
|
+
"env",
|
|
342
|
+
"printenv",
|
|
343
|
+
"date",
|
|
344
|
+
"pwd",
|
|
345
|
+
"diff",
|
|
346
|
+
"patch",
|
|
347
|
+
"zip",
|
|
348
|
+
"unzip",
|
|
349
|
+
"tar",
|
|
274
350
|
)
|
|
275
351
|
)
|
|
276
352
|
|
|
@@ -18,6 +18,7 @@ READ_ONLY_TOOLS: frozenset[str] = frozenset(
|
|
|
18
18
|
"list_directory",
|
|
19
19
|
"glob_files",
|
|
20
20
|
"grep_content",
|
|
21
|
+
"web_fetch",
|
|
21
22
|
}
|
|
22
23
|
)
|
|
23
24
|
|
|
@@ -27,11 +28,12 @@ MUTATING_TOOLS: frozenset[str] = frozenset(
|
|
|
27
28
|
"write_file",
|
|
28
29
|
"search_replace",
|
|
29
30
|
"delete_file",
|
|
31
|
+
"move_file",
|
|
30
32
|
}
|
|
31
33
|
)
|
|
32
34
|
|
|
33
|
-
# Subprocess (allowlist
|
|
34
|
-
SHELL_TOOLS: frozenset[str] = frozenset({"run_command"})
|
|
35
|
+
# Subprocess (allowlist or bash -c; require --yes / not strict)
|
|
36
|
+
SHELL_TOOLS: frozenset[str] = frozenset({"run_command", "bash"})
|
|
35
37
|
|
|
36
38
|
# Session planning only (no disk / shell; no extra permission)
|
|
37
39
|
PLANNING_TOOLS: frozenset[str] = frozenset({"todo_write"})
|
|
@@ -3,27 +3,34 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from gemcode.config import GemCodeConfig
|
|
6
|
+
from gemcode.tools.bash import make_bash_tool
|
|
6
7
|
from gemcode.tools.edit import make_edit_tools
|
|
7
8
|
from gemcode.tools.filesystem import make_filesystem_tools
|
|
8
9
|
from gemcode.tools.search import make_grep_tool
|
|
9
10
|
from gemcode.tools.shell import make_run_command
|
|
10
11
|
from gemcode.tools.todo import make_todo_tool
|
|
12
|
+
from gemcode.tools.web import make_web_fetch_tool
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def build_function_tools(cfg: GemCodeConfig) -> list:
|
|
14
|
-
read_file, list_directory, glob_files, delete_file = make_filesystem_tools(cfg)
|
|
16
|
+
read_file, list_directory, glob_files, delete_file, move_file = make_filesystem_tools(cfg)
|
|
15
17
|
grep_content = make_grep_tool(cfg)
|
|
16
18
|
run_command = make_run_command(cfg)
|
|
19
|
+
bash = make_bash_tool(cfg)
|
|
17
20
|
write_file, search_replace = make_edit_tools(cfg)
|
|
18
21
|
todo_write = make_todo_tool(cfg)
|
|
22
|
+
web_fetch = make_web_fetch_tool()
|
|
19
23
|
return [
|
|
20
24
|
todo_write,
|
|
21
25
|
read_file,
|
|
22
26
|
list_directory,
|
|
23
27
|
glob_files,
|
|
24
|
-
delete_file,
|
|
25
28
|
grep_content,
|
|
29
|
+
bash,
|
|
26
30
|
run_command,
|
|
27
31
|
write_file,
|
|
28
32
|
search_replace,
|
|
33
|
+
move_file,
|
|
34
|
+
delete_file,
|
|
35
|
+
web_fetch,
|
|
29
36
|
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Arbitrary shell execution via bash -c.
|
|
3
|
+
|
|
4
|
+
Analogous to OpenClaude's BashTool — allows pipelines, redirects, multi-step
|
|
5
|
+
shell workflows that run_command (basename-only) cannot express.
|
|
6
|
+
|
|
7
|
+
Security model: same gating as run_command (SHELL_TOOLS category), requires
|
|
8
|
+
--yes / interactive approval. Timeout-bounded; no TTY for child processes.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from gemcode.config import GemCodeConfig
|
|
19
|
+
from gemcode.paths import PathEscapeError, resolve_under_root
|
|
20
|
+
from gemcode.trust import is_trusted_root
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def make_bash_tool(cfg: GemCodeConfig):
|
|
24
|
+
root = cfg.project_root
|
|
25
|
+
trusted = is_trusted_root(root)
|
|
26
|
+
|
|
27
|
+
def bash(
|
|
28
|
+
command: str,
|
|
29
|
+
timeout_seconds: int = 120,
|
|
30
|
+
cwd_subdir: str = ".",
|
|
31
|
+
background: bool = False,
|
|
32
|
+
) -> dict:
|
|
33
|
+
"""
|
|
34
|
+
Run an arbitrary shell command via bash. Supports pipelines, redirects,
|
|
35
|
+
subshells, and multi-step workflows that run_command cannot express.
|
|
36
|
+
|
|
37
|
+
Use this for:
|
|
38
|
+
- Git operations: bash("git log --oneline -20")
|
|
39
|
+
- Pipelines: bash("cat package.json | python3 -m json.tool")
|
|
40
|
+
- Finding files: bash("find . -name '*.py' -newer setup.py | head -20")
|
|
41
|
+
- Complex builds: bash("cd frontend && npm ci && npm run build")
|
|
42
|
+
- Inspecting output: bash("ls -la | grep '.py'")
|
|
43
|
+
- Running tests with flags: bash("pytest -x -q --tb=short 2>&1 | head -100")
|
|
44
|
+
|
|
45
|
+
For long-running servers use background=True. cwd_subdir sets working
|
|
46
|
+
directory relative to the project root.
|
|
47
|
+
|
|
48
|
+
IMPORTANT: This runs real shell commands. Be precise and avoid destructive
|
|
49
|
+
operations (rm -rf, force-push, etc.) without explicit user approval.
|
|
50
|
+
"""
|
|
51
|
+
if not trusted:
|
|
52
|
+
return {"error": "Project folder is not trusted. Re-run GemCode and approve folder trust."}
|
|
53
|
+
|
|
54
|
+
if not command or not command.strip():
|
|
55
|
+
return {"error": "command must not be empty"}
|
|
56
|
+
|
|
57
|
+
if timeout_seconds < 1:
|
|
58
|
+
timeout_seconds = 1
|
|
59
|
+
if timeout_seconds > 600:
|
|
60
|
+
timeout_seconds = 600
|
|
61
|
+
|
|
62
|
+
bash_exe = shutil.which("bash")
|
|
63
|
+
if not bash_exe:
|
|
64
|
+
return {"error": "bash not found on PATH"}
|
|
65
|
+
|
|
66
|
+
if not (cwd_subdir or "").strip():
|
|
67
|
+
cwd_subdir = "."
|
|
68
|
+
try:
|
|
69
|
+
exec_cwd = resolve_under_root(root, cwd_subdir)
|
|
70
|
+
except PathEscapeError as e:
|
|
71
|
+
return {"error": str(e)}
|
|
72
|
+
if not exec_cwd.is_dir():
|
|
73
|
+
return {
|
|
74
|
+
"error": (
|
|
75
|
+
f"cwd_subdir={cwd_subdir!r} is not an existing directory under the project. "
|
|
76
|
+
"Create it first or fix the path."
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
env = {**os.environ}
|
|
81
|
+
|
|
82
|
+
if background:
|
|
83
|
+
try:
|
|
84
|
+
proc = subprocess.Popen(
|
|
85
|
+
[bash_exe, "-c", command],
|
|
86
|
+
cwd=str(exec_cwd),
|
|
87
|
+
env=env,
|
|
88
|
+
stdout=subprocess.DEVNULL,
|
|
89
|
+
stderr=subprocess.DEVNULL,
|
|
90
|
+
start_new_session=True,
|
|
91
|
+
)
|
|
92
|
+
except OSError as e:
|
|
93
|
+
return {"error": f"Failed to start background process: {e}"}
|
|
94
|
+
return {
|
|
95
|
+
"command": command,
|
|
96
|
+
"cwd": str(exec_cwd.relative_to(root)) if exec_cwd != root else ".",
|
|
97
|
+
"background": True,
|
|
98
|
+
"pid": proc.pid,
|
|
99
|
+
"note": "Process started in the background. Stop it with kill from the OS when done.",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
proc = subprocess.run(
|
|
104
|
+
[bash_exe, "-c", command],
|
|
105
|
+
cwd=str(exec_cwd),
|
|
106
|
+
capture_output=True,
|
|
107
|
+
text=True,
|
|
108
|
+
timeout=timeout_seconds,
|
|
109
|
+
env=env,
|
|
110
|
+
check=False,
|
|
111
|
+
)
|
|
112
|
+
stdout = proc.stdout[:80_000]
|
|
113
|
+
stderr = proc.stderr[:20_000]
|
|
114
|
+
result: dict = {
|
|
115
|
+
"command": command,
|
|
116
|
+
"cwd": str(exec_cwd.relative_to(root)) if exec_cwd != root else ".",
|
|
117
|
+
"exit_code": proc.returncode,
|
|
118
|
+
"stdout": stdout,
|
|
119
|
+
"stderr": stderr,
|
|
120
|
+
}
|
|
121
|
+
if proc.returncode != 0 and not stdout and not stderr:
|
|
122
|
+
result["note"] = f"Command exited {proc.returncode} with no output."
|
|
123
|
+
return result
|
|
124
|
+
except subprocess.TimeoutExpired:
|
|
125
|
+
return {"error": f"Timeout after {timeout_seconds}s", "command": command}
|
|
126
|
+
|
|
127
|
+
return bash
|