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,179 @@
1
+ """
2
+ Model routing for GemCode.
3
+
4
+ Goal: match Claude-style "multi modes" where users can select a model mode
5
+ explicitly, or GemCode can choose a best-fit model automatically (no extra
6
+ model call; heuristic only).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from typing import Literal
13
+
14
+ from gemcode.config import GemCodeConfig
15
+
16
+ ModelMode = Literal["auto", "fast", "balanced", "quality"]
17
+ FamilyMode = Literal["auto", "primary", "alt"]
18
+
19
+
20
+ def _contains_any(haystack: str, needles: list[str]) -> bool:
21
+ h = haystack.lower()
22
+ return any(n in h for n in needles)
23
+
24
+
25
+ def pick_effective_model(cfg: GemCodeConfig, prompt: str) -> str:
26
+ """
27
+ Returns the effective model id to use for this run.
28
+
29
+ Heuristic rules (cheap, deterministic):
30
+ - quality for architecture/design/refactor/complex trade-offs
31
+ - fast for small edits, tool-driven tasks, or quick debugging
32
+ - balanced as the default otherwise
33
+ """
34
+ mode_norm = (getattr(cfg, "model_mode", "fast") or "fast").lower()
35
+
36
+ # If the user explicitly picked a model id, honor it.
37
+ if getattr(cfg, "model_overridden", False):
38
+ return cfg.model
39
+
40
+ # Optional deep research routing: when user asks explicitly (flag) or when
41
+ # `model_mode=auto` and the prompt requests research-like output.
42
+ deep_research_triggers = [
43
+ "deep research",
44
+ "deep-dive",
45
+ "research",
46
+ "sources",
47
+ "citations",
48
+ "grounded",
49
+ "investigate",
50
+ "literature",
51
+ "benchmark",
52
+ ]
53
+ prompt_norm = re.sub(r"\s+", " ", prompt or "").strip().lower()
54
+ if getattr(cfg, "enable_deep_research", False):
55
+ return getattr(cfg, "model_deep_research", None) or cfg.model
56
+ if mode_norm == "auto" and _contains_any(prompt_norm, deep_research_triggers):
57
+ return getattr(cfg, "model_deep_research", None) or cfg.model
58
+
59
+ # Capability precedence: computer-use model selection.
60
+ # (Deep research already handled above.)
61
+ if getattr(cfg, "enable_audio", False):
62
+ return getattr(cfg, "model_audio_live", None) or cfg.model
63
+ if getattr(cfg, "enable_computer_use", False):
64
+ return getattr(cfg, "model_computer_use", None) or cfg.model
65
+
66
+ primary_fast = cfg.model
67
+ primary_quality = getattr(cfg, "model_quality", None) or primary_fast
68
+ primary_balanced = getattr(cfg, "model_balanced", None) or primary_fast
69
+
70
+ alt_fast = getattr(cfg, "model_alt", None) or primary_fast
71
+ alt_quality = getattr(cfg, "model_alt_quality", None) or primary_quality
72
+ alt_balanced = getattr(cfg, "model_alt_balanced", None) or primary_balanced
73
+
74
+ if mode_norm not in ("auto", "fast", "balanced", "quality"):
75
+ return primary_fast
76
+
77
+ def decide_base_mode() -> Literal["fast", "balanced", "quality"]:
78
+ if mode_norm == "fast":
79
+ return "fast"
80
+ if mode_norm == "balanced":
81
+ return "balanced"
82
+ if mode_norm == "quality":
83
+ return "quality"
84
+
85
+ # auto mode: choose base mode using prompt heuristics.
86
+ p_norm = re.sub(r"\s+", " ", prompt or "").strip()
87
+ plen = len(p_norm)
88
+
89
+ quality_triggers = [
90
+ "architecture",
91
+ "design",
92
+ "system",
93
+ "refactor",
94
+ "trade",
95
+ "complex",
96
+ "scal",
97
+ "performance",
98
+ "profil",
99
+ "migration",
100
+ "schema",
101
+ "how would you",
102
+ "deep dive",
103
+ ]
104
+ fast_triggers = [
105
+ "fix",
106
+ "bug",
107
+ "error",
108
+ "tests",
109
+ "pytest",
110
+ "debug",
111
+ "failing",
112
+ "lint",
113
+ "format",
114
+ "quick",
115
+ "small change",
116
+ ]
117
+
118
+ if (
119
+ plen > 2_000
120
+ and _contains_any(p_norm, ["design", "architecture", "refactor", "trade"])
121
+ ):
122
+ return "quality"
123
+
124
+ if _contains_any(p_norm, quality_triggers):
125
+ return "quality"
126
+
127
+ if _contains_any(p_norm, fast_triggers):
128
+ return "fast"
129
+
130
+ # If prompt is long but not explicitly complex, balanced tends to be safer.
131
+ if plen > 4_000:
132
+ return "balanced"
133
+
134
+ return "balanced"
135
+
136
+ base_mode = decide_base_mode()
137
+
138
+ # Decide model family (primary vs 2.5-alt).
139
+ fam = (getattr(cfg, "model_family_mode", "auto") or "auto").lower()
140
+ if fam not in ("auto", "primary", "alt"):
141
+ fam = "auto"
142
+
143
+ p_norm2 = re.sub(r"\s+", " ", prompt or "").strip()
144
+ # Reuse quality triggers: complex prompts get primary (3.x); simpler prompts
145
+ # prefer alt (2.5) by default in `auto` family mode.
146
+ complex_triggers = [
147
+ "architecture",
148
+ "design",
149
+ "system",
150
+ "refactor",
151
+ "trade",
152
+ "complex",
153
+ "performance",
154
+ "migration",
155
+ "schema",
156
+ "deep dive",
157
+ ]
158
+
159
+ choose_primary: bool
160
+ if fam == "primary":
161
+ choose_primary = True
162
+ elif fam == "alt":
163
+ choose_primary = False
164
+ else:
165
+ choose_primary = _contains_any(p_norm2, complex_triggers)
166
+
167
+ if choose_primary:
168
+ if base_mode == "fast":
169
+ return primary_fast
170
+ if base_mode == "balanced":
171
+ return primary_balanced
172
+ return primary_quality
173
+
174
+ if base_mode == "fast":
175
+ return alt_fast
176
+ if base_mode == "balanced":
177
+ return alt_balanced
178
+ return alt_quality
179
+
gemcode/paths.py ADDED
@@ -0,0 +1,29 @@
1
+ """Safe path resolution under a project root."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ class PathEscapeError(ValueError):
9
+ """Resolved path would leave the project sandbox."""
10
+
11
+
12
+ def resolve_under_root(project_root: Path, rel: str) -> Path:
13
+ """
14
+ Resolve a user-relative path against project_root.
15
+
16
+ Rejects absolute paths that escape the root (symlink traversal is still a
17
+ concern for production; MVP uses realpath check).
18
+ """
19
+ root = project_root.resolve()
20
+ raw = Path(rel)
21
+ if raw.is_absolute():
22
+ candidate = raw.resolve()
23
+ else:
24
+ candidate = (root / raw).resolve()
25
+ try:
26
+ candidate.relative_to(root)
27
+ except ValueError as e:
28
+ raise PathEscapeError(f"Path outside project root: {rel!r}") from e
29
+ return candidate
gemcode/permissions.py ADDED
@@ -0,0 +1,5 @@
1
+ """Backward-compatible re-exports. Prefer `gemcode.callbacks` for new code."""
2
+
3
+ from gemcode.callbacks import make_before_tool_callback
4
+
5
+ __all__ = ["make_before_tool_callback"]
File without changes
@@ -0,0 +1,168 @@
1
+ """
2
+ ADK plugin: complements Claude-like stopHooks with GemCode terminal reasons,
3
+ optional memory ingestion, and post-turn hook execution.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ import os
11
+
12
+ from google.adk.plugins.base_plugin import BasePlugin
13
+
14
+ from google.adk.models.google_llm import Gemini
15
+ from google.adk.models.llm_request import LlmRequest
16
+ from google.genai import types
17
+
18
+ from gemcode.config import GemCodeConfig
19
+ from gemcode.query.stop_hooks import run_post_turn_hooks
20
+ from gemcode.audit import append_audit
21
+ from gemcode.prompt_suggestions import build_prompt_suggestion
22
+
23
+
24
+ class GemCodeTerminalHooksPlugin(BasePlugin):
25
+ def __init__(self, cfg: GemCodeConfig):
26
+ super().__init__(name="gemcode_terminal_hooks")
27
+ self.cfg = cfg
28
+
29
+ def _use_interactions_for_prompt_suggestions(self) -> bool:
30
+ v = os.environ.get("GEMCODE_PROMPT_SUGGESTIONS_USE_INTERACTIONS", "1")
31
+ return v.lower() in ("1", "true", "yes", "on")
32
+
33
+ def _find_previous_interaction_id(
34
+ self, *, callback_context: Any, agent_name: str
35
+ ) -> str | None:
36
+ # Interactions chaining uses `previous_interaction_id` extracted from the
37
+ # most recent model response event for the same agent.
38
+ try:
39
+ events = callback_context.session.events or []
40
+ except Exception:
41
+ return None
42
+ for ev in reversed(events):
43
+ if getattr(ev, "author", None) == agent_name and getattr(
44
+ ev, "interaction_id", None
45
+ ):
46
+ return ev.interaction_id
47
+ return None
48
+
49
+ async def _suggest_via_interactions(
50
+ self,
51
+ *,
52
+ terminal_reason: str,
53
+ callback_context: Any,
54
+ agent: Any,
55
+ heuristic: str,
56
+ ) -> str | None:
57
+ if not self._use_interactions_for_prompt_suggestions():
58
+ return None
59
+
60
+ try:
61
+ previous_interaction_id = self._find_previous_interaction_id(
62
+ callback_context=callback_context,
63
+ agent_name=getattr(agent, "name", "gemcode"),
64
+ )
65
+
66
+ prompt = (
67
+ "You are GemCode. Provide the best next-step guidance for the user. "
68
+ "Terminal reason: {reason}. "
69
+ "Write a single short sentence (<= 220 chars). "
70
+ "If it involves policy/actions, reference exact flags like `--yes` or `--session`.\n\n"
71
+ "Heuristic suggestion (may be imperfect): {heuristic}"
72
+ ).format(reason=terminal_reason, heuristic=heuristic)
73
+
74
+ llm = Gemini(model=self.cfg.model, use_interactions_api=True)
75
+ req = LlmRequest(
76
+ model=self.cfg.model,
77
+ contents=[
78
+ types.Content(
79
+ role="user",
80
+ parts=[types.Part(text=prompt)],
81
+ )
82
+ ],
83
+ config=types.GenerateContentConfig(),
84
+ )
85
+ if previous_interaction_id:
86
+ req.previous_interaction_id = previous_interaction_id
87
+
88
+ async for resp in llm.generate_content_async(req, stream=False):
89
+ if resp.content and resp.content.parts:
90
+ texts = [
91
+ getattr(p, "text", None)
92
+ for p in resp.content.parts
93
+ if getattr(p, "text", None)
94
+ ]
95
+ if texts:
96
+ out = "".join(texts).strip()
97
+ if out:
98
+ return out
99
+ return None
100
+ except Exception as e:
101
+ append_audit(
102
+ self.cfg.project_root,
103
+ {
104
+ "phase": "prompt_suggestion_interactions",
105
+ "ok": False,
106
+ "error": str(e),
107
+ "terminal_reason": terminal_reason,
108
+ },
109
+ )
110
+ return None
111
+
112
+ async def after_agent_callback(self, *, agent: Any, callback_context: Any):
113
+ # callback_context is an ADK Context (mutable state + helper methods).
114
+ state = callback_context.state
115
+ terminal_reason = state.get("gemcode:terminal_reason", None)
116
+ if not terminal_reason:
117
+ terminal_reason = "completed"
118
+
119
+ append_audit(self.cfg.project_root, {"phase": "terminal", "reason": terminal_reason})
120
+
121
+ heuristic = build_prompt_suggestion(
122
+ self.cfg, terminal_reason=terminal_reason
123
+ )
124
+ if heuristic:
125
+ suggestion = heuristic
126
+ suggestion_via_interactions = await self._suggest_via_interactions(
127
+ terminal_reason=terminal_reason,
128
+ callback_context=callback_context,
129
+ agent=agent,
130
+ heuristic=heuristic,
131
+ )
132
+ if suggestion_via_interactions:
133
+ suggestion = suggestion_via_interactions
134
+
135
+ append_audit(
136
+ self.cfg.project_root,
137
+ {
138
+ "phase": "prompt_suggestion",
139
+ "terminal_reason": terminal_reason,
140
+ "suggestion": suggestion,
141
+ },
142
+ )
143
+
144
+ if getattr(self.cfg, "enable_memory", False):
145
+ try:
146
+ await callback_context.add_session_to_memory()
147
+ append_audit(self.cfg.project_root, {"phase": "memory_ingest", "ok": True})
148
+ except Exception as e:
149
+ append_audit(
150
+ self.cfg.project_root,
151
+ {"phase": "memory_ingest", "ok": False, "error": str(e)},
152
+ )
153
+
154
+ # Execute stopHooks-like script hook at the end of the invocation.
155
+ try:
156
+ run_post_turn_hooks(
157
+ self.cfg,
158
+ session_id=callback_context.session.id,
159
+ user_id=callback_context.user_id,
160
+ )
161
+ except Exception as e:
162
+ append_audit(
163
+ self.cfg.project_root,
164
+ {"phase": "post_turn_hook", "ok": False, "error": str(e)},
165
+ )
166
+
167
+ return None
168
+
@@ -0,0 +1,135 @@
1
+ """
2
+ Claude-like recovery-loop for GemCode tool failures.
3
+
4
+ We complement ADK's `ReflectAndRetryToolPlugin` by treating our tool result
5
+ dicts like `{"error": "...", "error_kind": "..."}` as retryable tool failures
6
+ so the model gets reflection guidance and can try a corrected approach.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from typing import Any, Optional
13
+
14
+ from google.adk.plugins.reflect_retry_tool_plugin import (
15
+ ReflectAndRetryToolPlugin,
16
+ TrackingScope,
17
+ )
18
+ from google.adk.tools.base_tool import BaseTool
19
+ from google.adk.tools.tool_context import ToolContext
20
+
21
+ from gemcode.audit import append_audit
22
+ from gemcode.config import GemCodeConfig
23
+
24
+
25
+ _STATE_FAILURE_KEY = "gemcode:consecutive_tool_failures"
26
+ _TERMINAL_REASON_KEY = "gemcode:terminal_reason"
27
+
28
+ _ERROR_KIND_PERMISSION_DENIED = "permission_denied"
29
+ _ERROR_KIND_PERMISSION_BLOCK = "permission_block"
30
+ _ERROR_KIND_CIRCUIT_BREAKER = "circuit_breaker"
31
+
32
+
33
+ class GemCodeReflectAndRetryToolPlugin(ReflectAndRetryToolPlugin):
34
+ """Retry tool failures with reflection guidance (Claude recovery-loop)."""
35
+
36
+ def __init__(self, cfg: GemCodeConfig):
37
+ self.cfg = cfg
38
+
39
+ enabled = os.environ.get("GEMCODE_ENABLE_TOOL_RECOVERY_RETRY", "1").lower()
40
+ if enabled not in ("1", "true", "yes", "on"):
41
+ # Still construct; we just set max_retries=0 so it becomes inert.
42
+ max_retries = 0
43
+ else:
44
+ max_retries = int(os.environ.get("GEMCODE_TOOL_REFLECT_MAX_RETRIES", "1"))
45
+
46
+ super().__init__(
47
+ max_retries=max_retries,
48
+ throw_exception_if_retry_exceeded=False,
49
+ tracking_scope=TrackingScope.INVOCATION,
50
+ )
51
+
52
+ async def extract_error_from_result(
53
+ self,
54
+ *,
55
+ tool: BaseTool,
56
+ tool_args: dict[str, Any],
57
+ tool_context: ToolContext,
58
+ result: Any,
59
+ ) -> Optional[Exception]:
60
+ """
61
+ Treat `{ "error": ... }` tool results as retryable failures.
62
+
63
+ Important: skip policy rejections (permission denials / circuit breaker)
64
+ so we don't waste retries on user-actionable gating.
65
+ """
66
+ if not isinstance(result, dict):
67
+ return None
68
+ if "error" not in result:
69
+ return None
70
+
71
+ err_kind = result.get("error_kind")
72
+ if err_kind in (
73
+ _ERROR_KIND_PERMISSION_DENIED,
74
+ _ERROR_KIND_PERMISSION_BLOCK,
75
+ _ERROR_KIND_CIRCUIT_BREAKER,
76
+ ):
77
+ return None
78
+
79
+ # Update GemCode streak/terminal state since canonical agent callbacks
80
+ # are likely short-circuited when this plugin returns a reflection.
81
+ try:
82
+ st = tool_context.state
83
+ st[_STATE_FAILURE_KEY] = st.get(_STATE_FAILURE_KEY, 0) + 1
84
+ if not st.get(_TERMINAL_REASON_KEY):
85
+ st[_TERMINAL_REASON_KEY] = "tool_retryable_error"
86
+ except Exception:
87
+ pass
88
+
89
+ err = result.get("error")
90
+ err_text = err if isinstance(err, str) else str(err)
91
+ append_audit(
92
+ self.cfg.project_root,
93
+ {
94
+ "phase": "tool_recovery_retry",
95
+ "tool": tool.name,
96
+ "error_kind": err_kind,
97
+ "error": err_text,
98
+ },
99
+ )
100
+
101
+ return Exception(err_text)
102
+
103
+ async def on_tool_error_callback(
104
+ self,
105
+ *,
106
+ tool: BaseTool,
107
+ tool_args: dict[str, Any],
108
+ tool_context: ToolContext,
109
+ error: Exception,
110
+ ) -> Optional[dict[str, Any]]:
111
+ """Ensure GemCode streak/terminal state is updated on exceptions."""
112
+ try:
113
+ st = tool_context.state
114
+ st[_STATE_FAILURE_KEY] = st.get(_STATE_FAILURE_KEY, 0) + 1
115
+ if not st.get(_TERMINAL_REASON_KEY):
116
+ st[_TERMINAL_REASON_KEY] = "tool_exception"
117
+ except Exception:
118
+ pass
119
+
120
+ append_audit(
121
+ self.cfg.project_root,
122
+ {
123
+ "phase": "tool_recovery_exception",
124
+ "tool": tool.name,
125
+ "error": f"{type(error).__name__}: {error}",
126
+ },
127
+ )
128
+
129
+ return await super().on_tool_error_callback(
130
+ tool=tool,
131
+ tool_args=tool_args,
132
+ tool_context=tool_context,
133
+ error=error,
134
+ )
135
+
@@ -0,0 +1,80 @@
1
+ """
2
+ Heuristic next-step guidance (Claude stopHooks-style).
3
+
4
+ We can't perfectly replicate Claude's UI-level "next suggestion job" without
5
+ extra model calls, but we can produce reliable, deterministic guidance from
6
+ GemCode's recorded `terminal_reason`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+
13
+ from gemcode.config import GemCodeConfig
14
+
15
+
16
+ def _truthy_env(name: str, *, default: bool = False) -> bool:
17
+ v = os.environ.get(name)
18
+ if v is None:
19
+ return default
20
+ return v.lower() in ("1", "true", "yes", "on")
21
+
22
+
23
+ def build_prompt_suggestion(
24
+ cfg: GemCodeConfig, *, terminal_reason: str
25
+ ) -> str | None:
26
+ if not _truthy_env("GEMCODE_ENABLE_PROMPT_SUGGESTIONS", default=True):
27
+ return None
28
+
29
+ r = terminal_reason
30
+ if r in ("completed", ""):
31
+ return None
32
+
33
+ if r == "permission_denied":
34
+ return (
35
+ "Some actions were blocked by policy. Re-run with `--yes` (or add the "
36
+ "needed command to `GEMCODE_ALLOW_COMMANDS` when in strict mode), then "
37
+ "try again with the same request."
38
+ )
39
+
40
+ if r == "tool_circuit_breaker":
41
+ return (
42
+ "Tool execution is being halted by the circuit breaker. Start a new "
43
+ "session (`--session <new_id>`), then either fix the failing tool "
44
+ "inputs or increase `GEMCODE_MAX_CONSECUTIVE_TOOL_FAILURES`."
45
+ )
46
+
47
+ if r == "session_token_limit":
48
+ return (
49
+ "This session exceeded the token ceiling. Start a new session (new "
50
+ "`--session` id) or raise `GEMCODE_MAX_SESSION_TOKENS`, then re-run "
51
+ "the request."
52
+ )
53
+
54
+ if r == "token_budget_stop":
55
+ return (
56
+ "Per-turn token budget was exhausted. Re-run the request (or split it "
57
+ "into smaller steps). If you want more room, increase "
58
+ "`GEMCODE_TOKEN_BUDGET` or reduce the prompt/context."
59
+ )
60
+
61
+ if r in ("tool_exception", "tool_retryable_error"):
62
+ return (
63
+ "A tool raised an exception. Check `.gemcode/audit.log` for the tool "
64
+ "error details, then re-run with corrected inputs or fewer/shorter "
65
+ "arguments."
66
+ )
67
+
68
+ if r == "model_error":
69
+ return (
70
+ "The model call failed. Try again, reduce prompt size, or switch to a "
71
+ "different model via `--model`."
72
+ )
73
+
74
+ # Generic fallback.
75
+ return (
76
+ f"The run ended with terminal reason `{terminal_reason}`. Check "
77
+ f"`.gemcode/audit.log`, then re-run with a narrower request or after "
78
+ f"adjusting limits/policy flags."
79
+ )
80
+
@@ -0,0 +1,36 @@
1
+ """
2
+ Query-layer types and helpers (clean-room analogue of Claude Code `src/query/*`).
3
+
4
+ - `transitions` — terminal vs continue reasons for the model↔tool loop.
5
+ - `config` — immutable gate snapshot per run (env/session).
6
+ - `token_budget` — continuation/stop decisions vs a per-turn token budget.
7
+ - `deps` — injectable dependencies for tests.
8
+ - `stop_hooks` — optional post-turn subprocess hooks.
9
+ - `engine` — `GemCodeQueryEngine` facade (outer session + submit message).
10
+
11
+ The ADK `Runner` still executes the inner loop; these modules document parity and
12
+ host logic that maps to `query.ts` / `QueryEngine.ts` responsibilities.
13
+ """
14
+
15
+ from gemcode.query.config import QueryGates, build_query_gates
16
+ from gemcode.query.token_budget import (
17
+ BudgetTracker,
18
+ TokenBudgetDecision,
19
+ check_token_budget,
20
+ get_budget_continuation_message,
21
+ )
22
+ from gemcode.query.transitions import Continue, Terminal
23
+
24
+ # Note: import `GemCodeQueryEngine` from `gemcode.query.engine` to avoid import cycles
25
+ # (engine pulls session_runtime → agent → callbacks → query.token_budget).
26
+
27
+ __all__ = [
28
+ "BudgetTracker",
29
+ "Continue",
30
+ "QueryGates",
31
+ "Terminal",
32
+ "TokenBudgetDecision",
33
+ "build_query_gates",
34
+ "check_token_budget",
35
+ "get_budget_continuation_message",
36
+ ]
@@ -0,0 +1,35 @@
1
+ """
2
+ Immutable gate snapshot at query entry (cf. `query/config.ts`).
3
+
4
+ Feature flags in Claude use `feature()` for tree-shaking; we use env + this struct.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from dataclasses import dataclass
11
+
12
+
13
+ def _truthy(name: str, default: bool = False) -> bool:
14
+ v = os.environ.get(name)
15
+ if v is None:
16
+ return default
17
+ return v.lower() in ("1", "true", "yes", "on")
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class QueryGates:
22
+ """Runtime gates (env), snapshotted once per invocation."""
23
+
24
+ emit_tool_use_summaries: bool
25
+ fast_mode_enabled: bool
26
+ streaming_tool_execution: bool
27
+
28
+
29
+ def build_query_gates() -> QueryGates:
30
+ """Snapshot env-driven gates (no network)."""
31
+ return QueryGates(
32
+ emit_tool_use_summaries=_truthy("GEMCODE_EMIT_TOOL_USE_SUMMARIES"),
33
+ fast_mode_enabled=not _truthy("GEMCODE_DISABLE_FAST_MODE"),
34
+ streaming_tool_execution=_truthy("GEMCODE_STREAMING_TOOL_EXEC", default=True),
35
+ )
gemcode/query/deps.py ADDED
@@ -0,0 +1,20 @@
1
+ """
2
+ Injectable dependencies (cf. claude-code `query/deps.ts`).
3
+
4
+ Kept minimal: tests can replace `uuid` without patching modules.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import uuid as uuid_mod
10
+ from dataclasses import dataclass
11
+ from typing import Callable
12
+
13
+
14
+ @dataclass
15
+ class QueryDeps:
16
+ uuid: Callable[[], str]
17
+
18
+
19
+ def production_deps() -> QueryDeps:
20
+ return QueryDeps(uuid=lambda: str(uuid_mod.uuid4()))