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.
Files changed (58) hide show
  1. gemcode/__init__.py +3 -0
  2. gemcode/__main__.py +3 -0
  3. gemcode/agent.py +146 -0
  4. gemcode/audit.py +16 -0
  5. gemcode/callbacks.py +473 -0
  6. gemcode/capability_routing.py +137 -0
  7. gemcode/cli.py +658 -0
  8. gemcode/compaction.py +35 -0
  9. gemcode/computer_use/__init__.py +0 -0
  10. gemcode/computer_use/browser_computer.py +275 -0
  11. gemcode/config.py +247 -0
  12. gemcode/interactions.py +15 -0
  13. gemcode/invoke.py +151 -0
  14. gemcode/kairos_daemon.py +221 -0
  15. gemcode/limits.py +83 -0
  16. gemcode/live_audio_engine.py +124 -0
  17. gemcode/mcp_loader.py +57 -0
  18. gemcode/memory/__init__.py +0 -0
  19. gemcode/memory/embedding_memory_service.py +292 -0
  20. gemcode/memory/file_memory_service.py +176 -0
  21. gemcode/modality_tools.py +216 -0
  22. gemcode/model_routing.py +179 -0
  23. gemcode/paths.py +29 -0
  24. gemcode/permissions.py +5 -0
  25. gemcode/plugins/__init__.py +0 -0
  26. gemcode/plugins/terminal_hooks_plugin.py +168 -0
  27. gemcode/plugins/tool_recovery_plugin.py +135 -0
  28. gemcode/prompt_suggestions.py +80 -0
  29. gemcode/query/__init__.py +36 -0
  30. gemcode/query/config.py +35 -0
  31. gemcode/query/deps.py +20 -0
  32. gemcode/query/engine.py +55 -0
  33. gemcode/query/stop_hooks.py +63 -0
  34. gemcode/query/token_budget.py +109 -0
  35. gemcode/query/transitions.py +41 -0
  36. gemcode/session_runtime.py +81 -0
  37. gemcode/thinking.py +136 -0
  38. gemcode/tool_prompt_manifest.py +118 -0
  39. gemcode/tool_registry.py +50 -0
  40. gemcode/tools/__init__.py +25 -0
  41. gemcode/tools/edit.py +53 -0
  42. gemcode/tools/filesystem.py +73 -0
  43. gemcode/tools/search.py +85 -0
  44. gemcode/tools/shell.py +73 -0
  45. gemcode/tools_inspector.py +132 -0
  46. gemcode/trust.py +54 -0
  47. gemcode/tui/app.py +697 -0
  48. gemcode/tui/scrollback.py +312 -0
  49. gemcode/vertex.py +22 -0
  50. gemcode/web/__init__.py +2 -0
  51. gemcode/web/claude_sse_adapter.py +282 -0
  52. gemcode/web/terminal_repl.py +147 -0
  53. gemcode-0.2.2.dist-info/METADATA +440 -0
  54. gemcode-0.2.2.dist-info/RECORD +58 -0
  55. gemcode-0.2.2.dist-info/WHEEL +5 -0
  56. gemcode-0.2.2.dist-info/entry_points.txt +2 -0
  57. gemcode-0.2.2.dist-info/licenses/LICENSE +151 -0
  58. gemcode-0.2.2.dist-info/top_level.txt +1 -0
@@ -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
+
@@ -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
+ ]