gemcode 0.2.2__py3-none-any.whl
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/__init__.py +3 -0
- gemcode/__main__.py +3 -0
- gemcode/agent.py +146 -0
- gemcode/audit.py +16 -0
- gemcode/callbacks.py +473 -0
- gemcode/capability_routing.py +137 -0
- gemcode/cli.py +658 -0
- gemcode/compaction.py +35 -0
- gemcode/computer_use/__init__.py +0 -0
- gemcode/computer_use/browser_computer.py +275 -0
- gemcode/config.py +247 -0
- gemcode/interactions.py +15 -0
- gemcode/invoke.py +151 -0
- gemcode/kairos_daemon.py +221 -0
- gemcode/limits.py +83 -0
- gemcode/live_audio_engine.py +124 -0
- gemcode/mcp_loader.py +57 -0
- gemcode/memory/__init__.py +0 -0
- gemcode/memory/embedding_memory_service.py +292 -0
- gemcode/memory/file_memory_service.py +176 -0
- gemcode/modality_tools.py +216 -0
- gemcode/model_routing.py +179 -0
- gemcode/paths.py +29 -0
- gemcode/permissions.py +5 -0
- gemcode/plugins/__init__.py +0 -0
- gemcode/plugins/terminal_hooks_plugin.py +168 -0
- gemcode/plugins/tool_recovery_plugin.py +135 -0
- gemcode/prompt_suggestions.py +80 -0
- gemcode/query/__init__.py +36 -0
- gemcode/query/config.py +35 -0
- gemcode/query/deps.py +20 -0
- gemcode/query/engine.py +55 -0
- gemcode/query/stop_hooks.py +63 -0
- gemcode/query/token_budget.py +109 -0
- gemcode/query/transitions.py +41 -0
- gemcode/session_runtime.py +81 -0
- gemcode/thinking.py +136 -0
- gemcode/tool_prompt_manifest.py +118 -0
- gemcode/tool_registry.py +50 -0
- gemcode/tools/__init__.py +25 -0
- gemcode/tools/edit.py +53 -0
- gemcode/tools/filesystem.py +73 -0
- gemcode/tools/search.py +85 -0
- gemcode/tools/shell.py +73 -0
- gemcode/tools_inspector.py +132 -0
- gemcode/trust.py +54 -0
- gemcode/tui/app.py +697 -0
- gemcode/tui/scrollback.py +312 -0
- gemcode/vertex.py +22 -0
- gemcode/web/__init__.py +2 -0
- gemcode/web/claude_sse_adapter.py +282 -0
- gemcode/web/terminal_repl.py +147 -0
- gemcode-0.2.2.dist-info/METADATA +440 -0
- gemcode-0.2.2.dist-info/RECORD +58 -0
- gemcode-0.2.2.dist-info/WHEEL +5 -0
- gemcode-0.2.2.dist-info/entry_points.txt +2 -0
- gemcode-0.2.2.dist-info/licenses/LICENSE +151 -0
- gemcode-0.2.2.dist-info/top_level.txt +1 -0
gemcode/query/engine.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Outer session engine (cf. claude-code `QueryEngine.ts`).
|
|
3
|
+
|
|
4
|
+
Owns config + runner + one `submit_message` path per user turn.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from google.adk.runners import Runner
|
|
13
|
+
|
|
14
|
+
from gemcode.config import GemCodeConfig
|
|
15
|
+
from gemcode.invoke import run_turn
|
|
16
|
+
from gemcode.session_runtime import create_runner
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GemCodeQueryEngine:
|
|
20
|
+
"""One engine per workspace session; reuse runner for multiple turns."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
cfg: GemCodeConfig,
|
|
25
|
+
*,
|
|
26
|
+
extra_tools: list | None = None,
|
|
27
|
+
runner_factory: Callable[[GemCodeConfig, list | None], Runner] | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self.cfg = cfg
|
|
30
|
+
self._extra_tools = extra_tools
|
|
31
|
+
self._runner_factory = runner_factory or create_runner
|
|
32
|
+
self._runner: Runner | None = None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def runner(self) -> Runner:
|
|
36
|
+
if self._runner is None:
|
|
37
|
+
self._runner = self._runner_factory(self.cfg, self._extra_tools)
|
|
38
|
+
return self._runner
|
|
39
|
+
|
|
40
|
+
async def submit_message(
|
|
41
|
+
self,
|
|
42
|
+
prompt: str,
|
|
43
|
+
*,
|
|
44
|
+
user_id: str = "local",
|
|
45
|
+
session_id: str,
|
|
46
|
+
) -> list[Any]:
|
|
47
|
+
"""Run one user message; returns collected ADK events."""
|
|
48
|
+
return await run_turn(
|
|
49
|
+
self.runner,
|
|
50
|
+
user_id=user_id,
|
|
51
|
+
session_id=session_id,
|
|
52
|
+
prompt=prompt,
|
|
53
|
+
max_llm_calls=self.cfg.max_llm_calls,
|
|
54
|
+
cfg=self.cfg,
|
|
55
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Post-turn hooks (subset of claude-code `query/stopHooks.ts`).
|
|
3
|
+
|
|
4
|
+
Runs after a user message finishes streaming through the agent. Optional:
|
|
5
|
+
- `GEMCODE_POST_TURN_HOOK` — path to an executable
|
|
6
|
+
- `.gemcode/hooks/post_turn` — if executable exists (and env not set)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import stat
|
|
13
|
+
import subprocess
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from gemcode.audit import append_audit
|
|
17
|
+
from gemcode.config import GemCodeConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_executable(p: Path) -> bool:
|
|
21
|
+
try:
|
|
22
|
+
return p.is_file() and (p.stat().st_mode & stat.S_IXUSR)
|
|
23
|
+
except OSError:
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_post_turn_hooks(
|
|
28
|
+
cfg: GemCodeConfig,
|
|
29
|
+
*,
|
|
30
|
+
session_id: str,
|
|
31
|
+
user_id: str = "local",
|
|
32
|
+
timeout_sec: float = 120.0,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Fire-and-forget-safe: catches errors, logs to audit."""
|
|
35
|
+
hook = os.environ.get("GEMCODE_POST_TURN_HOOK")
|
|
36
|
+
if not hook:
|
|
37
|
+
candidate = cfg.project_root / ".gemcode" / "hooks" / "post_turn"
|
|
38
|
+
if _is_executable(candidate):
|
|
39
|
+
hook = str(candidate)
|
|
40
|
+
if not hook:
|
|
41
|
+
return
|
|
42
|
+
path = Path(hook)
|
|
43
|
+
if not path.is_file():
|
|
44
|
+
append_audit(cfg.project_root, {"hook": "post_turn", "error": "missing_file", "path": hook})
|
|
45
|
+
return
|
|
46
|
+
env = os.environ.copy()
|
|
47
|
+
env["GEMCODE_PROJECT_ROOT"] = str(cfg.project_root)
|
|
48
|
+
env["GEMCODE_SESSION_ID"] = session_id
|
|
49
|
+
env["GEMCODE_USER_ID"] = user_id
|
|
50
|
+
try:
|
|
51
|
+
subprocess.run(
|
|
52
|
+
[str(path)],
|
|
53
|
+
cwd=str(cfg.project_root),
|
|
54
|
+
env=env,
|
|
55
|
+
timeout=timeout_sec,
|
|
56
|
+
capture_output=True,
|
|
57
|
+
check=False,
|
|
58
|
+
)
|
|
59
|
+
append_audit(cfg.project_root, {"hook": "post_turn", "path": hook, "ok": True})
|
|
60
|
+
except subprocess.TimeoutExpired:
|
|
61
|
+
append_audit(cfg.project_root, {"hook": "post_turn", "path": hook, "error": "timeout"})
|
|
62
|
+
except OSError as e:
|
|
63
|
+
append_audit(cfg.project_root, {"hook": "post_turn", "path": hook, "error": str(e)})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Per-turn token budget continuation (cf. claude-code `query/tokenBudget.ts`).
|
|
3
|
+
|
|
4
|
+
Used with a *single* agent (no sub-agent id): decide whether to inject a
|
|
5
|
+
continuation nudge vs stop when cumulative turn tokens approach `budget`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
COMPLETION_THRESHOLD = 0.9
|
|
14
|
+
DIMINISHING_THRESHOLD = 500
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BudgetTracker:
|
|
19
|
+
continuation_count: int = 0
|
|
20
|
+
last_delta_tokens: int = 0
|
|
21
|
+
last_global_turn_tokens: int = 0
|
|
22
|
+
started_at_ms: int = 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_budget_tracker() -> BudgetTracker:
|
|
26
|
+
import time
|
|
27
|
+
|
|
28
|
+
return BudgetTracker(started_at_ms=int(time.time() * 1000))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class _ContinueDecision:
|
|
33
|
+
action: Literal["continue"]
|
|
34
|
+
nudge_message: str
|
|
35
|
+
continuation_count: int
|
|
36
|
+
pct: int
|
|
37
|
+
turn_tokens: int
|
|
38
|
+
budget: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class _StopDecision:
|
|
43
|
+
action: Literal["stop"]
|
|
44
|
+
completion_event: dict | None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
TokenBudgetDecision = _ContinueDecision | _StopDecision
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_budget_continuation_message(pct: int, turn_tokens: int, budget: int) -> str:
|
|
51
|
+
def fmt(n: int) -> str:
|
|
52
|
+
return f"{n:,}"
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
f"Stopped at {pct}% of token target ({fmt(turn_tokens)} / {fmt(budget)}). "
|
|
56
|
+
"Keep working — do not summarize."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def check_token_budget(
|
|
61
|
+
tracker: BudgetTracker,
|
|
62
|
+
agent_id: str | None,
|
|
63
|
+
budget: int | None,
|
|
64
|
+
global_turn_tokens: int,
|
|
65
|
+
) -> TokenBudgetDecision:
|
|
66
|
+
"""Same control flow as Claude Code `checkTokenBudget`."""
|
|
67
|
+
if agent_id or budget is None or budget <= 0:
|
|
68
|
+
return _StopDecision(action="stop", completion_event=None)
|
|
69
|
+
|
|
70
|
+
turn_tokens = global_turn_tokens
|
|
71
|
+
pct = min(100, round((turn_tokens / budget) * 100))
|
|
72
|
+
delta_since_last = global_turn_tokens - tracker.last_global_turn_tokens
|
|
73
|
+
|
|
74
|
+
is_diminishing = (
|
|
75
|
+
tracker.continuation_count >= 3
|
|
76
|
+
and delta_since_last < DIMINISHING_THRESHOLD
|
|
77
|
+
and tracker.last_delta_tokens < DIMINISHING_THRESHOLD
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if not is_diminishing and turn_tokens < budget * COMPLETION_THRESHOLD:
|
|
81
|
+
tracker.continuation_count += 1
|
|
82
|
+
tracker.last_delta_tokens = delta_since_last
|
|
83
|
+
tracker.last_global_turn_tokens = global_turn_tokens
|
|
84
|
+
return _ContinueDecision(
|
|
85
|
+
action="continue",
|
|
86
|
+
nudge_message=get_budget_continuation_message(pct, turn_tokens, budget),
|
|
87
|
+
continuation_count=tracker.continuation_count,
|
|
88
|
+
pct=pct,
|
|
89
|
+
turn_tokens=turn_tokens,
|
|
90
|
+
budget=budget,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if is_diminishing or tracker.continuation_count > 0:
|
|
94
|
+
import time
|
|
95
|
+
|
|
96
|
+
duration_ms = int(time.time() * 1000) - tracker.started_at_ms
|
|
97
|
+
return _StopDecision(
|
|
98
|
+
action="stop",
|
|
99
|
+
completion_event={
|
|
100
|
+
"continuation_count": tracker.continuation_count,
|
|
101
|
+
"pct": pct,
|
|
102
|
+
"turn_tokens": turn_tokens,
|
|
103
|
+
"budget": budget,
|
|
104
|
+
"diminishing_returns": is_diminishing,
|
|
105
|
+
"duration_ms": duration_ms,
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return _StopDecision(action="stop", completion_event=None)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transition types for the model↔tool loop (cf. claude-code `query/transitions.ts`).
|
|
3
|
+
|
|
4
|
+
Terminal: why a turn or invocation ended.
|
|
5
|
+
Continue: why another model iteration was scheduled (conceptual; ADK handles scheduling).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Terminal:
|
|
16
|
+
"""The loop exited."""
|
|
17
|
+
|
|
18
|
+
reason: Literal[
|
|
19
|
+
"completed",
|
|
20
|
+
"blocking_limit",
|
|
21
|
+
"model_error",
|
|
22
|
+
"aborted",
|
|
23
|
+
"prompt_too_long",
|
|
24
|
+
"stop_hook_prevented",
|
|
25
|
+
"max_llm_calls",
|
|
26
|
+
"session_token_limit",
|
|
27
|
+
"tool_circuit_breaker",
|
|
28
|
+
] | str
|
|
29
|
+
error: object | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Continue:
|
|
34
|
+
"""Another iteration would run (documentation / logging; ADK runs internally)."""
|
|
35
|
+
|
|
36
|
+
reason: Literal[
|
|
37
|
+
"tool_use",
|
|
38
|
+
"reactive_compact_retry",
|
|
39
|
+
"token_budget_continuation",
|
|
40
|
+
"queued_command",
|
|
41
|
+
] | str
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session runtime (Claude Code: outer engine ≈ QueryEngine + session store).
|
|
3
|
+
|
|
4
|
+
- **SqliteSessionService**: durable session + events (like transcript persistence).
|
|
5
|
+
- **Runner**: wires the root agent to the session, equivalent to “submit query → stream events”.
|
|
6
|
+
|
|
7
|
+
The inner turn loop (model ↔ tools) is implemented inside ADK (analogous to `query.ts` +
|
|
8
|
+
StreamingToolExecutor + runTools orchestration). See `gemcode.query` for transition types,
|
|
9
|
+
token budget helpers, and `GemCodeQueryEngine`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from google.adk.runners import Runner
|
|
18
|
+
from google.adk.sessions.sqlite_session_service import SqliteSessionService
|
|
19
|
+
|
|
20
|
+
from gemcode.agent import build_root_agent
|
|
21
|
+
from gemcode.config import GemCodeConfig
|
|
22
|
+
from gemcode.modality_tools import build_extra_tools as build_modality_extra_tools
|
|
23
|
+
from gemcode.memory.embedding_memory_service import EmbeddingFileMemoryService
|
|
24
|
+
from gemcode.memory.file_memory_service import FileMemoryService
|
|
25
|
+
from gemcode.plugins.terminal_hooks_plugin import GemCodeTerminalHooksPlugin
|
|
26
|
+
from gemcode.plugins.tool_recovery_plugin import GemCodeReflectAndRetryToolPlugin
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def session_db_path(cfg: GemCodeConfig) -> Path:
|
|
30
|
+
return cfg.project_root / ".gemcode" / "sessions.sqlite"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None) -> Runner:
|
|
34
|
+
"""Construct Runner + SQLite session service + root LlmAgent."""
|
|
35
|
+
modality_tools = build_modality_extra_tools(cfg)
|
|
36
|
+
merged_extra_tools: list | None
|
|
37
|
+
if extra_tools:
|
|
38
|
+
merged_extra_tools = [*extra_tools, *modality_tools] if modality_tools else list(extra_tools)
|
|
39
|
+
else:
|
|
40
|
+
merged_extra_tools = modality_tools or None
|
|
41
|
+
|
|
42
|
+
# Computer-use: ADK ComputerUseToolset backed by our Playwright BrowserComputer.
|
|
43
|
+
if getattr(cfg, "enable_computer_use", False):
|
|
44
|
+
headless_env = os.environ.get("GEMCODE_COMPUTER_HEADLESS", "1").lower()
|
|
45
|
+
headless = headless_env in ("1", "true", "yes", "on")
|
|
46
|
+
from gemcode.computer_use.browser_computer import BrowserComputer
|
|
47
|
+
from google.adk.tools.computer_use.computer_use_toolset import ComputerUseToolset
|
|
48
|
+
|
|
49
|
+
computer = BrowserComputer(headless=headless)
|
|
50
|
+
computer_toolset = ComputerUseToolset(computer=computer)
|
|
51
|
+
merged_extra_tools = list(merged_extra_tools or [])
|
|
52
|
+
merged_extra_tools.append(computer_toolset)
|
|
53
|
+
|
|
54
|
+
agent = build_root_agent(cfg, extra_tools=merged_extra_tools)
|
|
55
|
+
db = session_db_path(cfg)
|
|
56
|
+
db.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
session_service = SqliteSessionService(str(db))
|
|
58
|
+
|
|
59
|
+
plugins = [GemCodeTerminalHooksPlugin(cfg)]
|
|
60
|
+
# Place recovery plugin before terminal hooks so it can influence tool results
|
|
61
|
+
# during the invocation.
|
|
62
|
+
if True:
|
|
63
|
+
plugins.insert(0, GemCodeReflectAndRetryToolPlugin(cfg))
|
|
64
|
+
memory_service = None
|
|
65
|
+
if getattr(cfg, "enable_memory", False):
|
|
66
|
+
mem_path = cfg.project_root / ".gemcode" / "memories.jsonl"
|
|
67
|
+
if getattr(cfg, "enable_embeddings", False):
|
|
68
|
+
memory_service = EmbeddingFileMemoryService(
|
|
69
|
+
mem_path, embeddings_model=getattr(cfg, "embeddings_model", None)
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
memory_service = FileMemoryService(mem_path)
|
|
73
|
+
|
|
74
|
+
return Runner(
|
|
75
|
+
app_name="gemcode",
|
|
76
|
+
agent=agent,
|
|
77
|
+
session_service=session_service,
|
|
78
|
+
plugins=plugins,
|
|
79
|
+
memory_service=memory_service,
|
|
80
|
+
auto_create_session=True,
|
|
81
|
+
)
|
gemcode/thinking.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gemini thinking configuration helper.
|
|
3
|
+
|
|
4
|
+
Conceptual mapping to Claude Code:
|
|
5
|
+
- thinking is enabled by default (Gemini adaptive/dynamic)
|
|
6
|
+
- only when user explicitly disables or sets a budget/level we override
|
|
7
|
+
- Gemini 3 uses `thinkingLevel`; Gemini 2.5 uses `thinkingBudget`
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from google.genai import types
|
|
15
|
+
|
|
16
|
+
from gemcode.config import GemCodeConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_gemini_2_5_series(model_id: str) -> bool:
|
|
20
|
+
return "2.5" in (model_id or "")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_gemini_3_pro(model_id: str) -> bool:
|
|
24
|
+
# Gemini 3.1 Pro preview doesn't support `thinkingLevel=minimal`.
|
|
25
|
+
return "3.1-pro" in (model_id or "") or "3-pro" in (model_id or "")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_thinking_config(cfg: GemCodeConfig) -> Optional[types.ThinkingConfig]:
|
|
29
|
+
"""
|
|
30
|
+
Returns a `types.ThinkingConfig` to pass to ADK's LlmAgent.generate_content_config.
|
|
31
|
+
|
|
32
|
+
If `None` is returned, GemCode lets Gemini use its default dynamic thinking
|
|
33
|
+
behavior (Claude-like adaptive default).
|
|
34
|
+
"""
|
|
35
|
+
model_id = getattr(cfg, "model", "") or ""
|
|
36
|
+
is_25 = _is_gemini_2_5_series(model_id)
|
|
37
|
+
|
|
38
|
+
# Claude-like disable semantics:
|
|
39
|
+
# - Gemini 3 can't fully disable, so approximate with `minimal`.
|
|
40
|
+
# - Gemini 2.5 can be disabled by setting thinkingBudget=0 (if supported).
|
|
41
|
+
disable = bool(getattr(cfg, "disable_thinking", False))
|
|
42
|
+
|
|
43
|
+
include = bool(getattr(cfg, "include_thought_summaries", False))
|
|
44
|
+
thinking_level = getattr(cfg, "thinking_level", None)
|
|
45
|
+
thinking_budget = getattr(cfg, "thinking_budget", None)
|
|
46
|
+
|
|
47
|
+
if disable:
|
|
48
|
+
if is_25:
|
|
49
|
+
# 2.5 Pro doesn't support fully disabling thinking.
|
|
50
|
+
if "2.5-pro" in model_id:
|
|
51
|
+
return types.ThinkingConfig(
|
|
52
|
+
thinking_budget=512,
|
|
53
|
+
include_thoughts=include or None,
|
|
54
|
+
)
|
|
55
|
+
if "flash-lite" in model_id:
|
|
56
|
+
return types.ThinkingConfig(
|
|
57
|
+
thinking_budget=0,
|
|
58
|
+
include_thoughts=include or None,
|
|
59
|
+
)
|
|
60
|
+
return types.ThinkingConfig(
|
|
61
|
+
thinking_budget=0,
|
|
62
|
+
include_thoughts=include or None,
|
|
63
|
+
)
|
|
64
|
+
# Gemini 3: minimal is the closest available "disable" knob.
|
|
65
|
+
if _is_gemini_3_pro(model_id):
|
|
66
|
+
return types.ThinkingConfig(
|
|
67
|
+
thinking_level="low",
|
|
68
|
+
include_thoughts=include or None,
|
|
69
|
+
)
|
|
70
|
+
return types.ThinkingConfig(
|
|
71
|
+
thinking_level="minimal",
|
|
72
|
+
include_thoughts=include or None,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Explicit user overrides take precedence.
|
|
76
|
+
if thinking_level is not None and not is_25:
|
|
77
|
+
level = str(thinking_level)
|
|
78
|
+
if _is_gemini_3_pro(model_id) and level == "minimal":
|
|
79
|
+
level = "low"
|
|
80
|
+
return types.ThinkingConfig(
|
|
81
|
+
thinking_level=level,
|
|
82
|
+
include_thoughts=include or None,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if thinking_budget is not None and is_25:
|
|
86
|
+
return types.ThinkingConfig(
|
|
87
|
+
thinking_budget=int(thinking_budget),
|
|
88
|
+
include_thoughts=include or None,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# If the user only wants thought summaries, we can set include_thoughts
|
|
92
|
+
# without forcing a budget/level.
|
|
93
|
+
if include:
|
|
94
|
+
return types.ThinkingConfig(include_thoughts=True)
|
|
95
|
+
|
|
96
|
+
# Otherwise: Claude-like auto mapping based on model_mode.
|
|
97
|
+
mode = (getattr(cfg, "model_mode", "auto") or "auto").lower()
|
|
98
|
+
if mode == "auto":
|
|
99
|
+
# Let Gemini choose its dynamic/adaptive thinking.
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
if not is_25:
|
|
103
|
+
# Gemini 3 thinkingLevel mapping.
|
|
104
|
+
if mode == "fast":
|
|
105
|
+
return types.ThinkingConfig(
|
|
106
|
+
thinking_level="low" if _is_gemini_3_pro(model_id) else "minimal",
|
|
107
|
+
include_thoughts=None,
|
|
108
|
+
)
|
|
109
|
+
if mode == "balanced":
|
|
110
|
+
return types.ThinkingConfig(
|
|
111
|
+
thinking_level="medium",
|
|
112
|
+
include_thoughts=None,
|
|
113
|
+
)
|
|
114
|
+
# quality
|
|
115
|
+
return types.ThinkingConfig(
|
|
116
|
+
thinking_level="high",
|
|
117
|
+
include_thoughts=None,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Gemini 2.5 thinkingBudget mapping.
|
|
121
|
+
if mode == "fast":
|
|
122
|
+
if "2.5-pro" in model_id:
|
|
123
|
+
return types.ThinkingConfig(thinking_budget=512, include_thoughts=None)
|
|
124
|
+
# flash/flash-preview/flash-lite
|
|
125
|
+
return types.ThinkingConfig(
|
|
126
|
+
thinking_budget=0 if "flash-lite" in model_id else 0, include_thoughts=None
|
|
127
|
+
)
|
|
128
|
+
if mode == "balanced":
|
|
129
|
+
if "2.5-pro" in model_id:
|
|
130
|
+
return types.ThinkingConfig(thinking_budget=4096, include_thoughts=None)
|
|
131
|
+
return types.ThinkingConfig(thinking_budget=1024, include_thoughts=None)
|
|
132
|
+
|
|
133
|
+
# quality
|
|
134
|
+
# For 2.5 Pro this remains dynamic because disable isn't supported.
|
|
135
|
+
return types.ThinkingConfig(thinking_budget=-1, include_thoughts=None)
|
|
136
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool system prompt manifest.
|
|
3
|
+
|
|
4
|
+
Claude Code's "tool system" approach is largely prompt-driven: it provides the
|
|
5
|
+
model a clear, consistent contract about what tools exist, what categories they
|
|
6
|
+
fall into, and what the model is allowed to do with each tool.
|
|
7
|
+
|
|
8
|
+
GemCode primarily enforces policy via ADK callbacks, but adding a short,
|
|
9
|
+
deterministic tool manifest to the model instruction improves behavior and
|
|
10
|
+
reduces tool-call mistakes.
|
|
11
|
+
|
|
12
|
+
This module builds a compact manifest string from GemCodeConfig.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from typing import Iterable
|
|
19
|
+
|
|
20
|
+
from gemcode.config import GemCodeConfig
|
|
21
|
+
from gemcode.tool_registry import MUTATING_TOOLS, READ_ONLY_TOOLS, SHELL_TOOLS
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _truthy_env(name: str, *, default: bool = False) -> bool:
|
|
25
|
+
v = os.environ.get(name)
|
|
26
|
+
if v is None:
|
|
27
|
+
return default
|
|
28
|
+
return v.lower() in ("1", "true", "yes", "on")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _cap(s: str, *, max_chars: int) -> str:
|
|
32
|
+
if len(s) <= max_chars:
|
|
33
|
+
return s
|
|
34
|
+
return s[: max_chars - 3] + "..."
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _fmt_list(items: Iterable[str]) -> str:
|
|
38
|
+
xs = [x for x in items if x]
|
|
39
|
+
xs.sort(key=lambda a: a.lower())
|
|
40
|
+
return ", ".join(xs)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_tool_manifest(cfg: GemCodeConfig) -> str | None:
|
|
44
|
+
"""
|
|
45
|
+
Returns a compact "tool system" section to append to the LLM instruction,
|
|
46
|
+
or None if disabled.
|
|
47
|
+
"""
|
|
48
|
+
enabled = _truthy_env("GEMCODE_ENABLE_TOOL_SYSTEM_PROMPT", default=True)
|
|
49
|
+
if not enabled:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
max_chars = int(os.environ.get("GEMCODE_TOOL_SYSTEM_PROMPT_MAX_CHARS", "2400"))
|
|
53
|
+
|
|
54
|
+
permission_mode = getattr(cfg, "permission_mode", "default")
|
|
55
|
+
yes_to_all = bool(getattr(cfg, "yes_to_all", False))
|
|
56
|
+
interactive_ask_on = bool(getattr(cfg, "interactive_permission_ask", False))
|
|
57
|
+
|
|
58
|
+
# Core custom tools.
|
|
59
|
+
read_only = sorted(READ_ONLY_TOOLS)
|
|
60
|
+
mutating = sorted(MUTATING_TOOLS)
|
|
61
|
+
shell = sorted(SHELL_TOOLS)
|
|
62
|
+
|
|
63
|
+
# Deep research built-ins are not always safe to combine; we only describe
|
|
64
|
+
# what's actually enabled by config.
|
|
65
|
+
deep_research_on = bool(getattr(cfg, "enable_deep_research", False))
|
|
66
|
+
embeddings_on = bool(getattr(cfg, "enable_embeddings", False))
|
|
67
|
+
computer_on = bool(getattr(cfg, "enable_computer_use", False))
|
|
68
|
+
|
|
69
|
+
maps_grounding_on = bool(getattr(cfg, "enable_maps_grounding", False))
|
|
70
|
+
tool_comb_mode = getattr(cfg, "tool_combination_mode", None) or "deep_research"
|
|
71
|
+
|
|
72
|
+
allow_cmds = getattr(cfg, "allow_commands", None)
|
|
73
|
+
allow_cmds_str = ""
|
|
74
|
+
if allow_cmds:
|
|
75
|
+
allow_cmds_str = _fmt_list(list(allow_cmds))
|
|
76
|
+
|
|
77
|
+
# Provide Gemini 3 tool-context circulation contract (best-effort).
|
|
78
|
+
# We can't guarantee exact tool combination semantics, but the model can
|
|
79
|
+
# align expectations with GemCode's behavior.
|
|
80
|
+
gemini3_combination_contract = (
|
|
81
|
+
f"tool-context-circulation mode is {tool_comb_mode}. "
|
|
82
|
+
"When enabled, built-in tool results may appear in context for subsequent function tool calls."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
manifest = f"""## Tool system (GemCode)
|
|
86
|
+
|
|
87
|
+
Permission policy:
|
|
88
|
+
- permission_mode={permission_mode}
|
|
89
|
+
- user_confirmation_provided(--yes)={yes_to_all}
|
|
90
|
+
- user_in_run_hitl_prompt_enabled={interactive_ask_on}
|
|
91
|
+
|
|
92
|
+
You may call tools as follows:
|
|
93
|
+
- Read-only tools: {_fmt_list(read_only)}
|
|
94
|
+
- Mutating tools (WRITE/EDIT): {_fmt_list(mutating)}.
|
|
95
|
+
Only call if user_confirmation_provided(--yes) is true OR user_in_run_hitl_prompt_enabled is true.
|
|
96
|
+
If neither is true, you must ask the user to re-run with --yes.
|
|
97
|
+
- Shell tool (run_command): {_fmt_list(shell)}.
|
|
98
|
+
Only use commands from GEMCODE_ALLOW_COMMANDS. Allowed={allow_cmds_str or "<none>"}.
|
|
99
|
+
Only call if user_confirmation_provided(--yes) is true OR user_in_run_hitl_prompt_enabled is true.
|
|
100
|
+
Notes:
|
|
101
|
+
- Prefer `python -m pip ...` (or `python3 -m pip ...`) so installs stay in the active virtualenv.
|
|
102
|
+
- Do not assume sudo/system package manager access.
|
|
103
|
+
|
|
104
|
+
Optional capability tools:
|
|
105
|
+
- Deep research built-ins are {'ON' if deep_research_on else 'OFF'}.
|
|
106
|
+
Active built-ins: google_search, url_context{', google_maps' if deep_research_on and maps_grounding_on else ''}.
|
|
107
|
+
{gemini3_combination_contract}
|
|
108
|
+
- Embeddings semantic retrieval is {'ON' if embeddings_on else 'OFF'}:
|
|
109
|
+
semantic_search_files.
|
|
110
|
+
- Computer use is {'ON' if computer_on else 'OFF'}:
|
|
111
|
+
browser automation actions via Computer Use toolset.
|
|
112
|
+
Only call if permission_mode != strict AND (user_confirmation_provided(--yes) is true OR user_in_run_hitl_prompt_enabled is true).
|
|
113
|
+
|
|
114
|
+
If a tool call is rejected by policy, do NOT retry the same mutation without the required user confirmation.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
return _cap(manifest, max_chars=max_chars)
|
|
118
|
+
|
gemcode/tool_registry.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool catalog and concurrency class (Claude Code–style partitioning, clean-room).
|
|
3
|
+
|
|
4
|
+
In Claude Code, `tools.ts` registers tools and `toolOrchestration.ts` runs
|
|
5
|
+
concurrency-safe batches in parallel and serializes mutating work. Here we
|
|
6
|
+
classify tools so permissions and docs stay aligned; ADK executes calls as the
|
|
7
|
+
model emits them, but we still enforce policy in order (before_tool_callback).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
# Mirrors "safe read" tools — can be interleaved / parallel-friendly in principle.
|
|
15
|
+
READ_ONLY_TOOLS: frozenset[str] = frozenset(
|
|
16
|
+
{
|
|
17
|
+
"read_file",
|
|
18
|
+
"list_directory",
|
|
19
|
+
"glob_files",
|
|
20
|
+
"grep_content",
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Disk mutations (require --yes / not strict)
|
|
25
|
+
MUTATING_TOOLS: frozenset[str] = frozenset(
|
|
26
|
+
{
|
|
27
|
+
"write_file",
|
|
28
|
+
"search_replace",
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Subprocess (allowlist enforced inside tool)
|
|
33
|
+
SHELL_TOOLS: frozenset[str] = frozenset({"run_command"})
|
|
34
|
+
|
|
35
|
+
ToolConcurrency = Literal["parallel_safe", "serial_mutating", "shell"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def concurrency_class(tool_name: str) -> ToolConcurrency:
|
|
39
|
+
if tool_name in READ_ONLY_TOOLS:
|
|
40
|
+
return "parallel_safe"
|
|
41
|
+
if tool_name in MUTATING_TOOLS:
|
|
42
|
+
return "serial_mutating"
|
|
43
|
+
if tool_name in SHELL_TOOLS:
|
|
44
|
+
return "shell"
|
|
45
|
+
# MCP and unknown tools: treat as serial / cautious
|
|
46
|
+
return "serial_mutating"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_mutating(tool_name: str) -> bool:
|
|
50
|
+
return tool_name in MUTATING_TOOLS
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Build function tools for the LlmAgent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from gemcode.config import GemCodeConfig
|
|
6
|
+
from gemcode.tools.edit import make_edit_tools
|
|
7
|
+
from gemcode.tools.filesystem import make_filesystem_tools
|
|
8
|
+
from gemcode.tools.search import make_grep_tool
|
|
9
|
+
from gemcode.tools.shell import make_run_command
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_function_tools(cfg: GemCodeConfig) -> list:
|
|
13
|
+
read_file, list_directory, glob_files = make_filesystem_tools(cfg)
|
|
14
|
+
grep_content = make_grep_tool(cfg)
|
|
15
|
+
run_command = make_run_command(cfg)
|
|
16
|
+
write_file, search_replace = make_edit_tools(cfg)
|
|
17
|
+
return [
|
|
18
|
+
read_file,
|
|
19
|
+
list_directory,
|
|
20
|
+
glob_files,
|
|
21
|
+
grep_content,
|
|
22
|
+
run_command,
|
|
23
|
+
write_file,
|
|
24
|
+
search_replace,
|
|
25
|
+
]
|