gemcode 0.2.2__tar.gz → 0.3.0__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.2.2/src/gemcode.egg-info → gemcode-0.3.0}/PKG-INFO +1 -1
- {gemcode-0.2.2 → gemcode-0.3.0}/pyproject.toml +1 -1
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/agent.py +33 -2
- gemcode-0.3.0/src/gemcode/autocompact.py +210 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/callbacks.py +118 -11
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/cli.py +143 -18
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/compaction.py +6 -1
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/config.py +51 -1
- gemcode-0.3.0/src/gemcode/context_budget.py +342 -0
- gemcode-0.3.0/src/gemcode/context_warning.py +126 -0
- gemcode-0.3.0/src/gemcode/credentials.py +49 -0
- gemcode-0.3.0/src/gemcode/hitl_session.py +5 -0
- gemcode-0.3.0/src/gemcode/invoke.py +237 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/limits.py +5 -5
- gemcode-0.3.0/src/gemcode/model_errors.py +87 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/prompt_suggestions.py +4 -2
- gemcode-0.3.0/src/gemcode/repl_commands.py +178 -0
- gemcode-0.3.0/src/gemcode/repl_slash.py +218 -0
- gemcode-0.3.0/src/gemcode/slash_commands.py +34 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/tool_prompt_manifest.py +28 -7
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/tool_registry.py +5 -1
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/tools/__init__.py +5 -1
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/tools/filesystem.py +14 -1
- gemcode-0.3.0/src/gemcode/tools/shell.py +159 -0
- gemcode-0.3.0/src/gemcode/tools/shell_gate.py +28 -0
- gemcode-0.3.0/src/gemcode/tools/todo.py +96 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/tui/app.py +46 -2
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/tui/scrollback.py +25 -2
- {gemcode-0.2.2 → gemcode-0.3.0/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode.egg-info/SOURCES.txt +20 -0
- gemcode-0.3.0/tests/test_autocompact.py +71 -0
- gemcode-0.3.0/tests/test_context_budget.py +60 -0
- gemcode-0.3.0/tests/test_context_warning.py +42 -0
- gemcode-0.3.0/tests/test_credentials.py +41 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_interactive_permission_ask.py +21 -0
- gemcode-0.3.0/tests/test_model_error_retry.py +55 -0
- gemcode-0.3.0/tests/test_model_errors.py +28 -0
- gemcode-0.3.0/tests/test_repl_commands.py +52 -0
- gemcode-0.3.0/tests/test_repl_slash.py +86 -0
- gemcode-0.3.0/tests/test_slash_commands.py +25 -0
- gemcode-0.3.0/tests/test_tools.py +139 -0
- gemcode-0.2.2/src/gemcode/invoke.py +0 -151
- gemcode-0.2.2/src/gemcode/tools/shell.py +0 -73
- gemcode-0.2.2/tests/test_tools.py +0 -22
- {gemcode-0.2.2 → gemcode-0.3.0}/LICENSE +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/MANIFEST.in +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/README.md +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/setup.cfg +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/__init__.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/__main__.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/audit.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/interactions.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/paths.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/permissions.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/query/config.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/thinking.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/trust.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/vertex.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_capability_routing.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_modality_tools.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_model_routing.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_paths.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_permissions.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_thinking_config.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_token_budget.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.2.2 → gemcode-0.3.0}/tests/test_tools_inspector.py +0 -0
|
@@ -12,6 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
|
|
13
13
|
from google.adk.agents.llm_agent import LlmAgent
|
|
14
14
|
|
|
15
|
+
from gemcode.autocompact import make_before_model_autocompact_callback
|
|
15
16
|
from gemcode.callbacks import (
|
|
16
17
|
make_after_model_callback,
|
|
17
18
|
make_after_tool_callback,
|
|
@@ -21,6 +22,7 @@ from gemcode.callbacks import (
|
|
|
21
22
|
)
|
|
22
23
|
from gemcode.compaction import make_before_model_callback
|
|
23
24
|
from gemcode.config import GemCodeConfig
|
|
25
|
+
from gemcode.context_budget import make_before_model_context_shrink_callback
|
|
24
26
|
from gemcode.limits import make_before_model_limits_callback, make_before_model_token_budget_callback
|
|
25
27
|
from gemcode.thinking import build_thinking_config
|
|
26
28
|
from gemcode.tools import build_function_tools
|
|
@@ -55,9 +57,36 @@ def _load_gemini_md(project_root: Path) -> str:
|
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
def build_instruction(cfg: GemCodeConfig) -> str:
|
|
60
|
+
# Layered instructions mirror the *structure* of mature coding agents (scope,
|
|
61
|
+
# task interpretation, tool choice, parallelism, risk)—not proprietary text.
|
|
58
62
|
base = """You are GemCode, an expert software engineering agent.
|
|
59
|
-
You
|
|
60
|
-
|
|
63
|
+
You operate only inside the user's project directory (current working directory).
|
|
64
|
+
|
|
65
|
+
## How to interpret requests
|
|
66
|
+
- Treat every message as a **software engineering** task in this repo unless the user clearly wants something else. If the instruction is vague ("fix it", "rename that", "the config", "see codebase"), **infer intent from the repository**: search, read, then act—do not answer with abstract advice when concrete files exist.
|
|
67
|
+
- If the user refers to symbols, filenames, or behaviors, **locate them in the tree** (glob/grep/list) instead of asking them to paste paths. Only ask a clarifying question when multiple plausible targets exist **and** choosing wrongly would be harmful.
|
|
68
|
+
- **Do not propose edits to files you have not read** (or have not inspected via grep/list with enough context). Understand what is there before you change it.
|
|
69
|
+
- When something fails, **diagnose** (read the error, re-check assumptions) before switching strategies; do not repeat the exact same failed tool call.
|
|
70
|
+
|
|
71
|
+
## Using tools (decisive and efficient)
|
|
72
|
+
- **Multi-step work:** call `todo_write` to track tasks (merge updates by id). Mark items completed as you finish—helps you stay organized like a senior engineer.
|
|
73
|
+
- **Prefer dedicated tools over the shell** for this workspace: `read_file`, `list_directory`, `glob_files`, `grep_content`, `write_file`, `search_replace`, `delete_file`. Use `run_command` for builds, tests, package managers, git, and other true shell workflows.
|
|
74
|
+
- **`run_command` rules (critical):**
|
|
75
|
+
- `command` must be a **single executable basename** (e.g. `npm`, `npx`, `mkdir`) — **not** `bash`, `sh`, or `cd foo && ...`.
|
|
76
|
+
- Pass argv as `args` (list). To run a command **inside** a subfolder (e.g. Next app in `testing/`), set **`cwd_subdir`** to that relative path (e.g. `"testing"`) and run `npm run dev` there — **never** simulate `cd` with `bash`.
|
|
77
|
+
- **Scaffolding** (`create-next-app`, etc.): many CLIs require non-interactive mode — pass **`extra_env`** like `{"CI": "1"}` and/or flags supported by that tool (`--yes` where documented).
|
|
78
|
+
- **Dev servers** (`npm run dev`, `vite`, etc.) run until stopped: use **`background=True`** so the process detaches; otherwise the tool may time out. You cannot open a *new OS terminal window* from here—background start is the supported way to keep running.
|
|
79
|
+
- **Parallelize:** when you need several **independent** reads or searches (no output from one is required to form the next call), issue them together in one turn so the user gets answers faster. When step B depends on step A's result, run **sequentially**.
|
|
80
|
+
- **Deletion:** use `delete_file` for a single file under the project root; reserve `rm` via `run_command` for unusual cases.
|
|
81
|
+
- **Autonomy:** explore with `list_directory` ("."), `glob_files` (e.g. `**/*.md`, `**/*keyword*`), and `grep_content` before asking "which file?". Prefer widening your search over interrogating the user.
|
|
82
|
+
|
|
83
|
+
## Risk and permissions
|
|
84
|
+
- Destructive or irreversible actions (deletes, force pushes, anything that wipes data) deserve a clear, honest description; the runtime may require explicit user approval. If the session uses **inline** approval, wait for it—do not instruct the user to "re-run with --yes" unless that is actually required by the environment.
|
|
85
|
+
- If a tool call is denied, **do not** immediately retry the identical call; adjust the plan or explain the blocker.
|
|
86
|
+
|
|
87
|
+
## Communication
|
|
88
|
+
- Before the first tool call in a turn, give a **short** line on what you are about to do. Assume the user does not see raw tool internals—summarize outcomes in plain language.
|
|
89
|
+
- Prefer small, testable edits and accurate reporting over breadth."""
|
|
61
90
|
|
|
62
91
|
tool_manifest = build_tool_manifest(cfg)
|
|
63
92
|
|
|
@@ -81,6 +110,8 @@ def build_root_agent(cfg: GemCodeConfig, extra_tools: list | None = None) -> Llm
|
|
|
81
110
|
tools = [*tools, *extra_tools]
|
|
82
111
|
|
|
83
112
|
before_model = _chain_before_model_callbacks(
|
|
113
|
+
make_before_model_autocompact_callback(cfg),
|
|
114
|
+
make_before_model_context_shrink_callback(cfg),
|
|
84
115
|
make_before_model_callback(cfg),
|
|
85
116
|
make_before_model_limits_callback(cfg),
|
|
86
117
|
make_before_model_token_budget_callback(cfg),
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude Code–style autocompact for ADK/Gemini.
|
|
3
|
+
|
|
4
|
+
GemCode already has:
|
|
5
|
+
- bounded tool output (after_tool truncation)
|
|
6
|
+
- soft context shrink (before_model trimming/clearing)
|
|
7
|
+
|
|
8
|
+
This module adds:
|
|
9
|
+
- threshold-based autocompact: when context is near the ceiling, summarize older
|
|
10
|
+
conversation into a compact "memory" message and keep only the tail turns.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from google.genai import Client
|
|
19
|
+
from google.genai import types
|
|
20
|
+
|
|
21
|
+
from gemcode.config import GemCodeConfig
|
|
22
|
+
from gemcode.context_budget import estimate_contents_text_chars
|
|
23
|
+
|
|
24
|
+
_AC_STATE_KEY = "gemcode:autocompact"
|
|
25
|
+
_AC_FAILURES_KEY = "gemcode:autocompact_failures"
|
|
26
|
+
_AC_LAST_SUMMARY_KEY = "gemcode:autocompact_last_summary"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _truthy_env(name: str, *, default: bool = False) -> bool:
|
|
30
|
+
v = os.environ.get(name)
|
|
31
|
+
if v is None:
|
|
32
|
+
return default
|
|
33
|
+
return v.lower() in ("1", "true", "yes", "on")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _autocompact_enabled(cfg: GemCodeConfig) -> bool:
|
|
37
|
+
# Default on to match "it knows what to do and when".
|
|
38
|
+
if os.environ.get("GEMCODE_AUTOCOMPACT") is not None:
|
|
39
|
+
return _truthy_env("GEMCODE_AUTOCOMPACT", default=True)
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _autocompact_threshold_chars(cfg: GemCodeConfig) -> int:
|
|
44
|
+
# Claude Code uses token windows; we use a character proxy budget since
|
|
45
|
+
# Gemini tokenizers vary and ADK does not expose a cheap exact counter.
|
|
46
|
+
max_chars = int(getattr(cfg, "max_context_chars", 0) or 0)
|
|
47
|
+
if max_chars <= 0:
|
|
48
|
+
return 0
|
|
49
|
+
buffer_chars = int(os.environ.get("GEMCODE_AUTOCOMPACT_BUFFER_CHARS", "60000"))
|
|
50
|
+
return max(50_000, max_chars - max(10_000, buffer_chars))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _max_failures() -> int:
|
|
54
|
+
return int(os.environ.get("GEMCODE_AUTOCOMPACT_MAX_FAILURES", "3"))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _tail_keep_contents(cfg: GemCodeConfig) -> int:
|
|
58
|
+
return int(os.environ.get("GEMCODE_AUTOCOMPACT_KEEP_CONTENT_ITEMS", "18"))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _summary_model(cfg: GemCodeConfig) -> str:
|
|
62
|
+
return os.environ.get("GEMCODE_AUTOCOMPACT_MODEL", getattr(cfg, "model", ""))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _build_summary_prompt(contents: Any) -> str:
|
|
66
|
+
# Safe, bounded textualization for summarization. We do not try to serialize
|
|
67
|
+
# structured tool blocks fully; the pre-model context shrink already clears
|
|
68
|
+
# most large payloads under pressure.
|
|
69
|
+
lines: list[str] = []
|
|
70
|
+
for c in contents or []:
|
|
71
|
+
role = getattr(c, "role", "unknown")
|
|
72
|
+
parts = getattr(c, "parts", None) or []
|
|
73
|
+
texts: list[str] = []
|
|
74
|
+
for p in parts:
|
|
75
|
+
t = getattr(p, "text", None)
|
|
76
|
+
if isinstance(t, str) and t.strip():
|
|
77
|
+
texts.append(t.strip())
|
|
78
|
+
if not texts:
|
|
79
|
+
continue
|
|
80
|
+
joined = "\n".join(texts)
|
|
81
|
+
# Bound per content item to avoid PTL inside the compact call itself.
|
|
82
|
+
if len(joined) > 20_000:
|
|
83
|
+
joined = joined[:20_000] + "\n… [truncated for autocompact]"
|
|
84
|
+
lines.append(f"{role.upper()}:\n{joined}")
|
|
85
|
+
transcript = "\n\n".join(lines)
|
|
86
|
+
if len(transcript) > 180_000:
|
|
87
|
+
transcript = transcript[:180_000] + "\n… [older transcript truncated for autocompact]"
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
"You are GemCode. Summarize the conversation so far into a compact, actionable memory.\n"
|
|
91
|
+
"Requirements:\n"
|
|
92
|
+
"- Preserve key decisions, constraints, and current plan.\n"
|
|
93
|
+
"- Preserve important file paths, commands, and errors.\n"
|
|
94
|
+
"- Keep it concise but information-dense.\n"
|
|
95
|
+
"- Do NOT include tool call JSON; paraphrase.\n\n"
|
|
96
|
+
"Conversation:\n"
|
|
97
|
+
f"{transcript}\n"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _summarize_via_genai(cfg: GemCodeConfig, prompt: str) -> str:
|
|
102
|
+
api_key = os.environ.get("GOOGLE_API_KEY")
|
|
103
|
+
if not api_key:
|
|
104
|
+
raise RuntimeError("GOOGLE_API_KEY not set (required for autocompact summary call)")
|
|
105
|
+
client = Client(api_key=api_key)
|
|
106
|
+
model = _summary_model(cfg) or getattr(cfg, "model", "")
|
|
107
|
+
resp = client.models.generate_content(
|
|
108
|
+
model=model,
|
|
109
|
+
contents=[types.Content(role="user", parts=[types.Part(text=prompt)])],
|
|
110
|
+
config=types.GenerateContentConfig(temperature=0.2),
|
|
111
|
+
)
|
|
112
|
+
out_parts: list[str] = []
|
|
113
|
+
try:
|
|
114
|
+
if resp.candidates:
|
|
115
|
+
c0 = resp.candidates[0]
|
|
116
|
+
content = getattr(c0, "content", None)
|
|
117
|
+
for p in getattr(content, "parts", None) or []:
|
|
118
|
+
t = getattr(p, "text", None)
|
|
119
|
+
if isinstance(t, str) and t:
|
|
120
|
+
out_parts.append(t)
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
text = "".join(out_parts).strip()
|
|
124
|
+
if not text:
|
|
125
|
+
raise RuntimeError("autocompact summary call returned empty text")
|
|
126
|
+
# Hard bound
|
|
127
|
+
return text[:80_000]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def make_before_model_autocompact_callback(cfg: GemCodeConfig):
|
|
131
|
+
if not _autocompact_enabled(cfg):
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
async def before_model(callback_context, llm_request):
|
|
135
|
+
try:
|
|
136
|
+
contents = getattr(llm_request, "contents", None) or []
|
|
137
|
+
except Exception:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
threshold = _autocompact_threshold_chars(cfg)
|
|
141
|
+
if threshold <= 0:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
used = estimate_contents_text_chars(contents)
|
|
145
|
+
force = os.environ.get("GEMCODE_AUTOCOMPACT_FORCE", "").lower() in (
|
|
146
|
+
"1",
|
|
147
|
+
"true",
|
|
148
|
+
"yes",
|
|
149
|
+
"on",
|
|
150
|
+
)
|
|
151
|
+
if not force and used < threshold:
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
st = getattr(callback_context, "state", None) or {}
|
|
155
|
+
failures = int(st.get(_AC_FAILURES_KEY, 0) or 0)
|
|
156
|
+
if failures >= _max_failures():
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
# Build summary from the "older" prefix; keep tail untouched.
|
|
160
|
+
#
|
|
161
|
+
# Keep a reasonable tail by default, but allow compaction even in short
|
|
162
|
+
# conversations that become huge due to tool payloads.
|
|
163
|
+
requested_keep = max(4, _tail_keep_contents(cfg))
|
|
164
|
+
# Need at least 2 items in the summarize slice to be worth it:
|
|
165
|
+
# [first] + [summary] + [tail...]
|
|
166
|
+
max_keep_for_summarize = max(2, len(contents) - 2)
|
|
167
|
+
keep_n = min(requested_keep, max_keep_for_summarize)
|
|
168
|
+
keep_first = 1 if contents else 0
|
|
169
|
+
tail = contents[-keep_n:] if len(contents) > keep_n else list(contents)
|
|
170
|
+
prefix = []
|
|
171
|
+
if keep_first:
|
|
172
|
+
prefix = contents[:1]
|
|
173
|
+
summarize_slice = contents[1:-keep_n] if len(contents) > (1 + keep_n) else []
|
|
174
|
+
else:
|
|
175
|
+
summarize_slice = contents[:-keep_n] if len(contents) > keep_n else []
|
|
176
|
+
|
|
177
|
+
if not summarize_slice:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
prompt = _build_summary_prompt(summarize_slice)
|
|
182
|
+
summary_text = _summarize_via_genai(cfg, prompt)
|
|
183
|
+
except Exception:
|
|
184
|
+
st[_AC_FAILURES_KEY] = failures + 1
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
st[_AC_FAILURES_KEY] = 0
|
|
188
|
+
st[_AC_LAST_SUMMARY_KEY] = summary_text
|
|
189
|
+
st[_AC_STATE_KEY] = True
|
|
190
|
+
|
|
191
|
+
summary_msg = types.Content(
|
|
192
|
+
role="user",
|
|
193
|
+
parts=[
|
|
194
|
+
types.Part(
|
|
195
|
+
text=(
|
|
196
|
+
"Conversation summary (autocompacted):\n"
|
|
197
|
+
f"{summary_text}\n"
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
llm_request.contents = [*prefix, summary_msg, *tail]
|
|
204
|
+
# One-shot force flag.
|
|
205
|
+
if force:
|
|
206
|
+
os.environ.pop("GEMCODE_AUTOCOMPACT_FORCE", None)
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
return before_model
|
|
210
|
+
|
|
@@ -11,15 +11,21 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import os
|
|
13
13
|
import sys
|
|
14
|
+
from pathlib import Path
|
|
14
15
|
from typing import Any
|
|
15
16
|
|
|
16
17
|
from google.adk.tools.base_tool import BaseTool
|
|
17
18
|
|
|
18
19
|
from gemcode.audit import append_audit
|
|
19
20
|
from gemcode.config import GemCodeConfig
|
|
21
|
+
from gemcode.context_budget import truncate_tool_result_dict
|
|
22
|
+
from gemcode.context_warning import calculate_context_warning_state, worst_alert_level
|
|
20
23
|
from gemcode.limits import SESSION_TOTAL_TOKENS_KEY
|
|
21
24
|
from gemcode.query.token_budget import BudgetTracker, check_token_budget, create_budget_tracker
|
|
25
|
+
from gemcode.hitl_session import HITL_STICKY_SESSION_KEY
|
|
26
|
+
from gemcode.model_errors import format_model_error_for_user
|
|
22
27
|
from gemcode.tool_registry import MUTATING_TOOLS, SHELL_TOOLS
|
|
28
|
+
from gemcode.tools.shell_gate import arm_confirmed_shell_basename
|
|
23
29
|
|
|
24
30
|
_STATE_FAILURE_KEY = "gemcode:consecutive_tool_failures"
|
|
25
31
|
TERMINAL_REASON_KEY = "gemcode:terminal_reason"
|
|
@@ -33,6 +39,10 @@ _BT_CC = "gemcode:bt_cc"
|
|
|
33
39
|
_BT_LD = "gemcode:bt_ld"
|
|
34
40
|
_BT_LG = "gemcode:bt_lg"
|
|
35
41
|
_BT_T0 = "gemcode:bt_t0"
|
|
42
|
+
_CTX_WARN_LEVEL_NOTIFIED = "gemcode:ctx_warn_level_notified"
|
|
43
|
+
_LAST_PROMPT_TOKENS = "gemcode:last_prompt_tokens"
|
|
44
|
+
_LAST_CONTEXT_PCT = "gemcode:last_context_percent_left"
|
|
45
|
+
_LAST_CONTEXT_LEVEL = "gemcode:last_context_alert_level"
|
|
36
46
|
|
|
37
47
|
def _truthy_env(name: str, *, default: bool = False) -> bool:
|
|
38
48
|
v = os.environ.get(name)
|
|
@@ -57,6 +67,12 @@ def _max_consecutive_failures() -> int:
|
|
|
57
67
|
return int(os.environ.get("GEMCODE_MAX_CONSECUTIVE_TOOL_FAILURES", "8"))
|
|
58
68
|
|
|
59
69
|
|
|
70
|
+
def _arm_shell_from_args(args: dict[str, Any]) -> None:
|
|
71
|
+
cmd = args.get("command")
|
|
72
|
+
if isinstance(cmd, str) and cmd.strip():
|
|
73
|
+
arm_confirmed_shell_basename(Path(cmd.strip()).name)
|
|
74
|
+
|
|
75
|
+
|
|
60
76
|
def _is_computer_use_tool(tool: BaseTool) -> bool:
|
|
61
77
|
"""
|
|
62
78
|
Detect ADK ComputerUseTool instances without enumerating every method name.
|
|
@@ -76,6 +92,25 @@ def _is_computer_use_tool(tool: BaseTool) -> bool:
|
|
|
76
92
|
def make_before_tool_callback(cfg: GemCodeConfig):
|
|
77
93
|
"""Permission gate + circuit breaker (open after too many tool errors in a row)."""
|
|
78
94
|
|
|
95
|
+
def _hitl_sticky_enabled(tool_context) -> bool:
|
|
96
|
+
try:
|
|
97
|
+
return bool(
|
|
98
|
+
getattr(cfg, "interactive_hitl_sticky_session", False)
|
|
99
|
+
and tool_context is not None
|
|
100
|
+
and tool_context.state.get(HITL_STICKY_SESSION_KEY)
|
|
101
|
+
)
|
|
102
|
+
except Exception:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def _hitl_mark_session_approved(tool_context) -> None:
|
|
106
|
+
if not getattr(cfg, "interactive_hitl_sticky_session", False):
|
|
107
|
+
return
|
|
108
|
+
try:
|
|
109
|
+
if tool_context is not None:
|
|
110
|
+
tool_context.state[HITL_STICKY_SESSION_KEY] = True
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
79
114
|
def _tool_confirmation_state(tool_context) -> bool | None:
|
|
80
115
|
"""
|
|
81
116
|
Returns:
|
|
@@ -145,13 +180,17 @@ def make_before_tool_callback(cfg: GemCodeConfig):
|
|
|
145
180
|
# In-run HITL: request ADK tool confirmation and pause execution until
|
|
146
181
|
# the user approves in the current terminal session.
|
|
147
182
|
if getattr(cfg, "interactive_permission_ask", False):
|
|
183
|
+
# After one approval this ADK session, optional skip (see GEMCODE_HITL_STICKY_SESSION).
|
|
184
|
+
if _hitl_sticky_enabled(tool_context):
|
|
185
|
+
return None
|
|
148
186
|
tc_state = _tool_confirmation_state(tool_context)
|
|
149
187
|
if tc_state is True:
|
|
188
|
+
_hitl_mark_session_approved(tool_context)
|
|
150
189
|
return None
|
|
151
190
|
if tc_state is False:
|
|
152
191
|
return {
|
|
153
|
-
|
|
154
|
-
|
|
192
|
+
"error": "This tool call was rejected.",
|
|
193
|
+
"error_kind": _ERROR_KIND_PERMISSION_DENIED,
|
|
155
194
|
}
|
|
156
195
|
if tool_context is not None and hasattr(
|
|
157
196
|
tool_context, "request_confirmation"
|
|
@@ -162,7 +201,7 @@ def make_before_tool_callback(cfg: GemCodeConfig):
|
|
|
162
201
|
)
|
|
163
202
|
else:
|
|
164
203
|
tool_context.request_confirmation(
|
|
165
|
-
hint="Approve to apply the requested
|
|
204
|
+
hint=f"Approve to apply the requested mutation ({name})."
|
|
166
205
|
)
|
|
167
206
|
return {
|
|
168
207
|
"error": "This tool call requires confirmation.",
|
|
@@ -192,8 +231,12 @@ def make_before_tool_callback(cfg: GemCodeConfig):
|
|
|
192
231
|
}
|
|
193
232
|
if not cfg.yes_to_all:
|
|
194
233
|
if getattr(cfg, "interactive_permission_ask", False):
|
|
234
|
+
if _hitl_sticky_enabled(tool_context):
|
|
235
|
+
return None
|
|
195
236
|
tc_state = _tool_confirmation_state(tool_context)
|
|
196
237
|
if tc_state is True:
|
|
238
|
+
_hitl_mark_session_approved(tool_context)
|
|
239
|
+
_arm_shell_from_args(args)
|
|
197
240
|
return None
|
|
198
241
|
if tc_state is False:
|
|
199
242
|
return {
|
|
@@ -229,13 +272,21 @@ def make_after_tool_callback(cfg: GemCodeConfig):
|
|
|
229
272
|
tool_context,
|
|
230
273
|
tool_response: dict,
|
|
231
274
|
) -> dict | None:
|
|
275
|
+
truncated = False
|
|
276
|
+
if isinstance(tool_response, dict) and getattr(cfg, "tool_result_max_chars", 0) > 0:
|
|
277
|
+
new_d, did = truncate_tool_result_dict(
|
|
278
|
+
tool_response, int(cfg.tool_result_max_chars)
|
|
279
|
+
)
|
|
280
|
+
if did:
|
|
281
|
+
tool_response = new_d
|
|
282
|
+
truncated = True
|
|
232
283
|
name = getattr(tool, "name", None) or ""
|
|
233
284
|
if tool_context is None:
|
|
234
|
-
return None
|
|
285
|
+
return tool_response if truncated else None
|
|
235
286
|
try:
|
|
236
287
|
st = tool_context.state
|
|
237
288
|
except Exception:
|
|
238
|
-
return None
|
|
289
|
+
return tool_response if truncated else None
|
|
239
290
|
err = isinstance(tool_response, dict) and tool_response.get("error")
|
|
240
291
|
err_kind = (
|
|
241
292
|
isinstance(tool_response, dict) and tool_response.get("error_kind")
|
|
@@ -292,7 +343,7 @@ def make_after_tool_callback(cfg: GemCodeConfig):
|
|
|
292
343
|
try:
|
|
293
344
|
# Full-screen TUIs get corrupted by stray stderr prints.
|
|
294
345
|
if _truthy_env("GEMCODE_TUI_ACTIVE", default=False):
|
|
295
|
-
return None
|
|
346
|
+
return tool_response if truncated else None
|
|
296
347
|
ok = bool(summary.get("ok"))
|
|
297
348
|
prefix = "[tool ok]" if ok else "[tool err]"
|
|
298
349
|
details = ""
|
|
@@ -305,6 +356,8 @@ def make_after_tool_callback(cfg: GemCodeConfig):
|
|
|
305
356
|
print(f"{prefix} {name}{details}", file=sys.stderr)
|
|
306
357
|
except Exception:
|
|
307
358
|
pass
|
|
359
|
+
if truncated:
|
|
360
|
+
return tool_response
|
|
308
361
|
return None
|
|
309
362
|
|
|
310
363
|
return after_tool
|
|
@@ -354,6 +407,54 @@ def make_after_model_callback(cfg: GemCodeConfig):
|
|
|
354
407
|
if d:
|
|
355
408
|
append_audit(cfg.project_root, {"phase": "model_usage", **d})
|
|
356
409
|
|
|
410
|
+
pt = d.get("prompt_token_count")
|
|
411
|
+
if isinstance(pt, int) and pt >= 0:
|
|
412
|
+
try:
|
|
413
|
+
model_id = getattr(cfg, "model", "") or ""
|
|
414
|
+
cw = calculate_context_warning_state(
|
|
415
|
+
prompt_token_count=pt, model=model_id, cfg=cfg
|
|
416
|
+
)
|
|
417
|
+
level = worst_alert_level(cw)
|
|
418
|
+
st[_LAST_PROMPT_TOKENS] = pt
|
|
419
|
+
st[_LAST_CONTEXT_PCT] = cw.get("percent_left")
|
|
420
|
+
st[_LAST_CONTEXT_LEVEL] = level
|
|
421
|
+
append_audit(
|
|
422
|
+
cfg.project_root,
|
|
423
|
+
{
|
|
424
|
+
"phase": "context_warning",
|
|
425
|
+
"prompt_token_count": pt,
|
|
426
|
+
"percent_left": cw.get("percent_left"),
|
|
427
|
+
"level": level,
|
|
428
|
+
"is_above_warning_threshold": cw.get("is_above_warning_threshold"),
|
|
429
|
+
"is_above_error_threshold": cw.get("is_above_error_threshold"),
|
|
430
|
+
"is_above_auto_compact_threshold": cw.get(
|
|
431
|
+
"is_above_auto_compact_threshold"
|
|
432
|
+
),
|
|
433
|
+
"is_at_blocking_limit": cw.get("is_at_blocking_limit"),
|
|
434
|
+
},
|
|
435
|
+
)
|
|
436
|
+
prev = int(st.get(_CTX_WARN_LEVEL_NOTIFIED, 0) or 0)
|
|
437
|
+
if level < prev:
|
|
438
|
+
st[_CTX_WARN_LEVEL_NOTIFIED] = level
|
|
439
|
+
prev = level
|
|
440
|
+
if (
|
|
441
|
+
level > prev
|
|
442
|
+
and not _truthy_env("GEMCODE_TUI_ACTIVE", default=False)
|
|
443
|
+
and os.environ.get("GEMCODE_CONTEXT_WARNINGS", "1").lower()
|
|
444
|
+
not in ("0", "false", "no", "off")
|
|
445
|
+
):
|
|
446
|
+
labels = ("ok", "warning", "error", "blocking")
|
|
447
|
+
label = labels[min(level, 3)]
|
|
448
|
+
msg = (
|
|
449
|
+
f"[gemcode context] ~{cw.get('percent_left')}% context left "
|
|
450
|
+
f"(prompt_tokens≈{pt}; {label}). "
|
|
451
|
+
"Use /compact or start a new session if you hit limits."
|
|
452
|
+
)
|
|
453
|
+
print(msg, file=sys.stderr)
|
|
454
|
+
st[_CTX_WARN_LEVEL_NOTIFIED] = level
|
|
455
|
+
except Exception:
|
|
456
|
+
pass
|
|
457
|
+
|
|
357
458
|
total_this = d.get("total_token_count")
|
|
358
459
|
if isinstance(total_this, int) and total_this >= 0:
|
|
359
460
|
prev_total = int(st.get(SESSION_TOTAL_TOKENS_KEY, 0) or 0)
|
|
@@ -447,11 +548,17 @@ def make_on_model_error_callback(cfg: GemCodeConfig):
|
|
|
447
548
|
append_audit(
|
|
448
549
|
cfg.project_root,
|
|
449
550
|
{
|
|
450
|
-
|
|
451
|
-
|
|
551
|
+
"phase": "model_exception",
|
|
552
|
+
"error": f"{type(error).__name__}: {error}",
|
|
452
553
|
},
|
|
453
554
|
)
|
|
454
|
-
|
|
555
|
+
if _truthy_env("GEMCODE_VERBOSE_MODEL_ERRORS", default=False):
|
|
556
|
+
import traceback
|
|
557
|
+
|
|
558
|
+
traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)
|
|
559
|
+
|
|
560
|
+
user_text = format_model_error_for_user(error)
|
|
561
|
+
# Scrollback/TUI already prints "GemCode:" before assistant text — avoid "GemCode: GemCode:".
|
|
455
562
|
from google.adk.models.llm_response import LlmResponse
|
|
456
563
|
from google.genai import types
|
|
457
564
|
|
|
@@ -461,8 +568,8 @@ def make_on_model_error_callback(cfg: GemCodeConfig):
|
|
|
461
568
|
parts=[
|
|
462
569
|
types.Part(
|
|
463
570
|
text=(
|
|
464
|
-
"
|
|
465
|
-
"
|
|
571
|
+
f"{user_text} "
|
|
572
|
+
"You can re-run, shorten the message, or start a fresh session."
|
|
466
573
|
)
|
|
467
574
|
)
|
|
468
575
|
],
|