gemcode 0.3.75__py3-none-any.whl → 0.3.77__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/agent.py +51 -18
- gemcode/autocompact.py +2 -2
- gemcode/autotune.py +76 -0
- gemcode/callbacks.py +6 -6
- gemcode/capability_routing.py +2 -2
- gemcode/checkpoints.py +144 -0
- gemcode/cli.py +63 -28
- gemcode/config.py +21 -4
- gemcode/context_budget.py +2 -2
- gemcode/context_warning.py +6 -6
- gemcode/credentials.py +1 -1
- gemcode/curated_memory.py +110 -0
- gemcode/evals/harness.py +126 -0
- gemcode/hooks.py +1 -1
- gemcode/ide_protocol.py +14 -1
- gemcode/ide_stdio.py +52 -10
- gemcode/invoke.py +1 -1
- gemcode/{kairos_daemon.py → kaira_daemon.py} +19 -19
- gemcode/learning.py +122 -0
- gemcode/limits.py +1 -1
- gemcode/mcp_loader.py +1 -1
- gemcode/modality_tools.py +1 -1
- gemcode/model_routing.py +1 -1
- gemcode/output_styles.py +78 -0
- gemcode/paths.py +60 -0
- gemcode/permissions.py +5 -2
- gemcode/plugins/terminal_hooks_plugin.py +13 -1
- gemcode/plugins/tool_recovery_plugin.py +2 -2
- gemcode/prompt_suggestions.py +2 -2
- gemcode/query/__init__.py +1 -1
- gemcode/query/config.py +1 -1
- gemcode/query/deps.py +1 -1
- gemcode/query/engine.py +1 -1
- gemcode/query/stop_hooks.py +1 -1
- gemcode/query/token_budget.py +2 -2
- gemcode/query/transitions.py +1 -1
- gemcode/repl_commands.py +13 -3
- gemcode/repl_slash.py +513 -9
- gemcode/rules.py +115 -0
- gemcode/session_runtime.py +10 -1
- gemcode/skills.py +299 -0
- gemcode/slash_commands.py +1 -1
- gemcode/thinking.py +12 -17
- gemcode/tool_prompt_manifest.py +1 -1
- gemcode/tool_registry.py +2 -2
- gemcode/tool_result_store.py +1 -1
- gemcode/tools/__init__.py +26 -0
- gemcode/tools/bash.py +1 -1
- gemcode/tools/curated_memory.py +34 -0
- gemcode/tools/edit.py +65 -2
- gemcode/tools/filesystem.py +65 -13
- gemcode/tools/notebook.py +2 -2
- gemcode/tools/notes.py +2 -3
- gemcode/tools/repo_map.py +11 -0
- gemcode/tools/search.py +63 -19
- gemcode/tools/shell.py +2 -1
- gemcode/tools/skills.py +61 -0
- gemcode/tools/subtask.py +1 -1
- gemcode/tools/tasks.py +2 -2
- gemcode/tools/think.py +1 -2
- gemcode/tools/todo.py +1 -1
- gemcode/tools/web.py +1 -1
- gemcode/tools/web_search.py +1 -1
- gemcode/tools_inspector.py +1 -1
- gemcode/tui/input_handler.py +8 -3
- gemcode/tui/scrollback.py +3 -3
- gemcode/tui/spinner.py +8 -8
- gemcode/tui/welcome_rich.py +1 -1
- gemcode/web/{claude_sse_adapter.py → sse_adapter.py} +26 -61
- gemcode/web/terminal_repl.py +3 -3
- gemcode/web/web_sse_compat.py +24 -0
- gemcode-0.3.77.dist-info/METADATA +689 -0
- gemcode-0.3.77.dist-info/RECORD +108 -0
- gemcode-0.3.75.dist-info/METADATA +0 -510
- gemcode-0.3.75.dist-info/RECORD +0 -97
- {gemcode-0.3.75.dist-info → gemcode-0.3.77.dist-info}/WHEEL +0 -0
- {gemcode-0.3.75.dist-info → gemcode-0.3.77.dist-info}/entry_points.txt +0 -0
- {gemcode-0.3.75.dist-info → gemcode-0.3.77.dist-info}/licenses/LICENSE +0 -0
- {gemcode-0.3.75.dist-info → gemcode-0.3.77.dist-info}/top_level.txt +0 -0
gemcode/agent.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Root LlmAgent definition (
|
|
2
|
+
Root LlmAgent definition (agent config + tool list, analogous to a tools registry + prompts).
|
|
3
3
|
|
|
4
4
|
See `session_runtime.py` for Runner/session wiring (outer layer).
|
|
5
5
|
See `tool_registry.py` for tool categories (read vs mutating vs shell).
|
|
@@ -27,6 +27,9 @@ from gemcode.limits import make_before_model_limits_callback, make_before_model_
|
|
|
27
27
|
from gemcode.thinking import build_thinking_config
|
|
28
28
|
from gemcode.tools import build_function_tools
|
|
29
29
|
from gemcode.tool_prompt_manifest import build_tool_manifest
|
|
30
|
+
from gemcode.skills import build_skill_manifest_text
|
|
31
|
+
from gemcode.output_styles import build_output_style_section
|
|
32
|
+
from gemcode.rules import build_rules_section
|
|
30
33
|
|
|
31
34
|
|
|
32
35
|
def build_global_instruction() -> str:
|
|
@@ -61,7 +64,7 @@ def _chain_before_model_callbacks(*callbacks):
|
|
|
61
64
|
|
|
62
65
|
def _load_gemini_md(project_root: Path) -> str:
|
|
63
66
|
"""
|
|
64
|
-
Load GEMINI.md / .gemcode/NOTES.md from a
|
|
67
|
+
Load GEMINI.md / .gemcode/NOTES.md from a interactive CLI–style hierarchy.
|
|
65
68
|
|
|
66
69
|
Priority (later entries override earlier ones, all are concatenated):
|
|
67
70
|
1. ~/.gemcode/GEMINI.md — user-global instructions (all projects)
|
|
@@ -86,7 +89,7 @@ def _load_gemini_md(project_root: Path) -> str:
|
|
|
86
89
|
return ""
|
|
87
90
|
try:
|
|
88
91
|
raw = p.read_text(encoding="utf-8", errors="replace")[:_FILE_CAP]
|
|
89
|
-
# Strip HTML comments (
|
|
92
|
+
# Strip HTML comments (saves tokens)
|
|
90
93
|
return _COMMENT_RE.sub("", raw).strip()
|
|
91
94
|
except OSError:
|
|
92
95
|
return ""
|
|
@@ -137,7 +140,7 @@ def _get_git_context(root) -> str:
|
|
|
137
140
|
"""
|
|
138
141
|
Run a quick git snapshot at session start — branch, recent commits, diff-stat.
|
|
139
142
|
Returns a formatted string or empty string if not a git repo.
|
|
140
|
-
Mirrors
|
|
143
|
+
Mirrors Reference UI getGitStatus() pattern.
|
|
141
144
|
"""
|
|
142
145
|
import subprocess
|
|
143
146
|
import shutil
|
|
@@ -229,18 +232,18 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
229
232
|
if max_session_tokens:
|
|
230
233
|
budget_line += f" · max_session_tokens={max_session_tokens:,}"
|
|
231
234
|
|
|
232
|
-
# ──
|
|
233
|
-
# The user can run `gemcode
|
|
235
|
+
# ── Kaira ────────────────────────────────────────────────────────────────
|
|
236
|
+
# The user can run `gemcode kaira -C <project>` in a separate terminal to
|
|
234
237
|
# launch a long-lived scheduler. Jobs submitted to it run concurrently with
|
|
235
238
|
# the current session. This is useful for background / parallel heavy work.
|
|
236
|
-
|
|
237
|
-
"- **
|
|
239
|
+
kaira_section = (
|
|
240
|
+
"- **Kaira background scheduler** — `gemcode kaira -C <project>` launches a "
|
|
238
241
|
"long-lived daemon that reads prompts from stdin and runs each as an isolated job "
|
|
239
|
-
"(up to N concurrently). Each job gets `
|
|
240
|
-
"`
|
|
242
|
+
"(up to N concurrently). Each job gets `kaira_sleep_ms(ms)` and "
|
|
243
|
+
"`kaira_enqueue_prompt(prompt, priority, session_id)` tools so the model can "
|
|
241
244
|
"schedule follow-up work itself. Useful for: bulk file processing, repeated "
|
|
242
245
|
"polling loops, parallelising large independent tasks. "
|
|
243
|
-
"Tell the user to open a second terminal and run `gemcode
|
|
246
|
+
"Tell the user to open a second terminal and run `gemcode kaira` if a task "
|
|
244
247
|
"would benefit from background parallelism."
|
|
245
248
|
)
|
|
246
249
|
|
|
@@ -248,6 +251,19 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
248
251
|
git_ctx = _get_git_context(root)
|
|
249
252
|
git_section = f"\n\n## Git context (snapshot at session start)\n{git_ctx}" if git_ctx else ""
|
|
250
253
|
|
|
254
|
+
# ── Curated memory (safe-to-inject) ───────────────────────────────────────
|
|
255
|
+
curated_section = ""
|
|
256
|
+
try:
|
|
257
|
+
snap = getattr(cfg, "_curated_memory_snapshot", None)
|
|
258
|
+
if isinstance(snap, dict) and (snap.get("text") or "").strip():
|
|
259
|
+
curated_section = (
|
|
260
|
+
"\n\n## Curated memory (safe, persistent)\n"
|
|
261
|
+
"This is small, curated memory that should be treated as durable project/user facts.\n"
|
|
262
|
+
f"{snap.get('text')}\n"
|
|
263
|
+
)
|
|
264
|
+
except Exception:
|
|
265
|
+
curated_section = ""
|
|
266
|
+
|
|
251
267
|
return f"""## Runtime facts (authoritative for this session)
|
|
252
268
|
- **Today's date:** {today}
|
|
253
269
|
- **Project root** — every filesystem tool path is relative to: `{root}`
|
|
@@ -258,10 +274,10 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
258
274
|
- **Capability routing** (`capability_mode={getattr(cfg, 'capability_mode', 'auto')}`): in `auto` mode, GemCode automatically enables deep_research when it detects research-intent keywords in your prompt each turn. You can also type `/research on`, `/embeddings on`, `/memory on`, `/computer on` at the prompt.
|
|
259
275
|
- **Your tool palette can grow mid-session:** if the user enables a capability via a slash command, the runner rebuilds and you get new tools on the next turn.
|
|
260
276
|
- **Memory system:** when `memory ON`, ADK automatically searches `.gemcode/memories.jsonl` and injects relevant past context before each turn. Facts the user tells you in one session can appear in future sessions. You do not need to manage memory explicitly — it is loaded automatically.
|
|
261
|
-
{
|
|
277
|
+
{kaira_section}
|
|
262
278
|
- **UI banner** phrases like "GemCode Pro" are terminal marketing, not a separate API tier.
|
|
263
279
|
- **Env toggles** (`GEMCODE_ENABLE_COMPUTER_USE`, `GEMCODE_MODEL`, etc.) affect only the OS process that launched gemcode. Pasting `VAR=1` in chat does NOT reconfigure a running session—tell the user to export in their shell, use project `.env`, or restart the CLI.
|
|
264
|
-
- **Working in subfolders** — call `list_directory("Desktop")`, `glob_files("**/query.ts")`, `read_file("testing/ai-edtech-app/src/app/page.tsx")` directly. Never claim access is blocked unless a tool returned an explicit error.{git_section}"""
|
|
280
|
+
- **Working in subfolders** — call `list_directory(\"Desktop\")`, `glob_files(\"**/query.ts\")`, `read_file(\"testing/ai-edtech-app/src/app/page.tsx\")` directly. Never claim access is blocked unless a tool returned an explicit error.{git_section}{curated_section}"""
|
|
265
281
|
|
|
266
282
|
|
|
267
283
|
def _build_memory_section(cfg: GemCodeConfig) -> str:
|
|
@@ -664,7 +680,7 @@ Concrete patterns:
|
|
|
664
680
|
- Grepping different patterns → multiple `grep_content` in one response
|
|
665
681
|
- `list_directory` + `glob_files` → both at once
|
|
666
682
|
|
|
667
|
-
**Parallel sub-agent exploration (
|
|
683
|
+
**Parallel sub-agent exploration (reference terminal UI pattern):**
|
|
668
684
|
When a task requires understanding several subsystems before acting:
|
|
669
685
|
1. Spawn parallel `run_subtask` workers, one per subsystem
|
|
670
686
|
2. Wait for all results to return in the same turn
|
|
@@ -828,7 +844,7 @@ Use `gh pr create` via `bash`. When asked to create a PR:
|
|
|
828
844
|
All file tools use paths **relative to the project root** (where GemCode was started). The root may be the home folder — subfolders like `Desktop`, `Desktop/code`, `Documents` are inside the sandbox. Call `list_directory("Desktop")` or `glob_files("**/*name*.ts")` instead of assuming access is blocked. Only treat access as denied when a tool returns an explicit `error`.
|
|
829
845
|
|
|
830
846
|
## Agent notes (.gemcode/notes.md)
|
|
831
|
-
You have two tools to persist project insights across sessions
|
|
847
|
+
You have two tools to persist project insights across sessions (auto-memory style):
|
|
832
848
|
|
|
833
849
|
- **`append_project_note(note)`** — write a note to `.gemcode/notes.md`. Use this proactively when you discover something worth remembering:
|
|
834
850
|
- Build/test/lint commands you discover ("Build: `npm run build` — requires Node 20")
|
|
@@ -840,7 +856,12 @@ You have two tools to persist project insights across sessions, like Claude Code
|
|
|
840
856
|
Call this **immediately** when you discover something useful — not just at the end of tasks.
|
|
841
857
|
Notes are loaded at session start so future sessions inherit this knowledge.
|
|
842
858
|
|
|
843
|
-
- **`read_project_notes()`** — read current notes **only when starting a real engineering task** (editing, debugging, building). Do NOT call this for greetings or general questions. If notes exist and you're about to work on a task, read them once to avoid re-discovering known information.
|
|
859
|
+
- **`read_project_notes()`** — read current notes **only when starting a real engineering task** (editing, debugging, building). Do NOT call this for greetings or general questions. If notes exist and you're about to work on a task, read them once to avoid re-discovering known information.
|
|
860
|
+
|
|
861
|
+
## Do not create vendor-specific instruction files
|
|
862
|
+
- Do NOT create or modify `CLAUDE.md` or `AGENTS.md`. GemCode does not use these.
|
|
863
|
+
- If project instructions are needed and the user asked for it, use `GEMINI.md` (repo root).
|
|
864
|
+
"""
|
|
844
865
|
|
|
845
866
|
# Inject capability-specific strategy sections only when those caps are on.
|
|
846
867
|
if getattr(cfg, "enable_computer_use", False):
|
|
@@ -855,6 +876,18 @@ You have two tools to persist project insights across sessions, like Claude Code
|
|
|
855
876
|
tool_manifest = build_tool_manifest(cfg)
|
|
856
877
|
if tool_manifest:
|
|
857
878
|
base = f"{base}\n\n{tool_manifest}"
|
|
879
|
+
# Output style: small, user-selected formatting layer.
|
|
880
|
+
style_section = build_output_style_section(cfg.project_root, getattr(cfg, "output_style", None))
|
|
881
|
+
if style_section:
|
|
882
|
+
base = f"{base}\n\n{style_section}"
|
|
883
|
+
# Rules: project conventions (path-gated based on files the agent/user touched this session).
|
|
884
|
+
touched = sorted(getattr(cfg, "_touched_paths", set()) or set())
|
|
885
|
+
rules_section = build_rules_section(cfg.project_root, touched_paths=touched or None)
|
|
886
|
+
if rules_section:
|
|
887
|
+
base = f"{base}\n\n{rules_section}"
|
|
888
|
+
skill_manifest = build_skill_manifest_text(cfg.project_root)
|
|
889
|
+
if skill_manifest:
|
|
890
|
+
base = f"{base}\n\n{skill_manifest}"
|
|
858
891
|
extra = _load_gemini_md(cfg.project_root)
|
|
859
892
|
if extra.strip():
|
|
860
893
|
return f"{base}\n\n## Project instructions (GEMINI.md)\n{extra}"
|
|
@@ -903,7 +936,7 @@ def build_root_agent(
|
|
|
903
936
|
except Exception:
|
|
904
937
|
pass
|
|
905
938
|
|
|
906
|
-
# Agent auto-notes: write project insights to .gemcode/notes.md (
|
|
939
|
+
# Agent auto-notes: write project insights to .gemcode/notes.md (project notes file)
|
|
907
940
|
try:
|
|
908
941
|
from gemcode.tools.notes import build_notes_tools
|
|
909
942
|
notes_tools = build_notes_tools(cfg.project_root)
|
|
@@ -931,7 +964,7 @@ def build_root_agent(
|
|
|
931
964
|
if before_model is not None:
|
|
932
965
|
cb_kwargs["before_model_callback"] = before_model
|
|
933
966
|
|
|
934
|
-
#
|
|
967
|
+
# familiar thinking: enabled by default (Gemini dynamic), but allow
|
|
935
968
|
# explicit overrides for disable/budgets/levels.
|
|
936
969
|
gen_cfg = None
|
|
937
970
|
thinking_cfg = build_thinking_config(cfg)
|
gemcode/autocompact.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
interactive CLI–style autocompact for ADK/Gemini.
|
|
3
3
|
|
|
4
4
|
GemCode already has:
|
|
5
5
|
- bounded tool output (after_tool truncation)
|
|
@@ -41,7 +41,7 @@ def _autocompact_enabled(cfg: GemCodeConfig) -> bool:
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def _autocompact_threshold_chars(cfg: GemCodeConfig) -> int:
|
|
44
|
-
#
|
|
44
|
+
# uses token windows; we use a character proxy budget since
|
|
45
45
|
# Gemini tokenizers vary and ADK does not expose a cheap exact counter.
|
|
46
46
|
max_chars = int(getattr(cfg, "max_context_chars", 0) or 0)
|
|
47
47
|
if max_chars <= 0:
|
gemcode/autotune.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from gemcode.evals.harness import run_eval_suite, write_eval_record
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _sh(cmd: list[str], *, cwd: Path) -> tuple[int, str]:
|
|
12
|
+
p = subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True)
|
|
13
|
+
out = (p.stdout or "") + (p.stderr or "")
|
|
14
|
+
return int(p.returncode), out
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _git_head_sha(repo: Path) -> str | None:
|
|
18
|
+
rc, out = _sh(["git", "rev-parse", "HEAD"], cwd=repo)
|
|
19
|
+
if rc != 0:
|
|
20
|
+
return None
|
|
21
|
+
return (out or "").strip().splitlines()[-1] if (out or "").strip() else None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _git_branch(repo: Path) -> str | None:
|
|
25
|
+
rc, out = _sh(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=repo)
|
|
26
|
+
if rc != 0:
|
|
27
|
+
return None
|
|
28
|
+
return (out or "").strip().splitlines()[-1] if (out or "").strip() else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def init_autotune(*, project_root: Path, tag: str) -> dict[str, Any]:
|
|
32
|
+
"""
|
|
33
|
+
AutoResearch-style setup:
|
|
34
|
+
- create branch autotune/<tag> (if not exists)
|
|
35
|
+
- create results ledger under .gemcode/evals/
|
|
36
|
+
"""
|
|
37
|
+
repo = project_root
|
|
38
|
+
if not (repo / ".git").exists():
|
|
39
|
+
return {"error": "not_a_git_repo"}
|
|
40
|
+
branch = f"autotune/{tag}"
|
|
41
|
+
rc, out = _sh(["git", "rev-parse", "--verify", branch], cwd=repo)
|
|
42
|
+
if rc == 0:
|
|
43
|
+
return {"status": "exists", "branch": branch}
|
|
44
|
+
rc2, out2 = _sh(["git", "checkout", "-b", branch], cwd=repo)
|
|
45
|
+
if rc2 != 0:
|
|
46
|
+
return {"error": "branch_create_failed", "output": out2[-1200:]}
|
|
47
|
+
return {"status": "created", "branch": branch}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_autotune_eval(*, project_root: Path, include_llm: bool, model: str | None = None) -> dict[str, Any]:
|
|
51
|
+
"""
|
|
52
|
+
Run eval suite and persist last result to .gemcode/evals/last_eval.json.
|
|
53
|
+
"""
|
|
54
|
+
res = run_eval_suite(project_root=project_root, include_llm=include_llm, model=model)
|
|
55
|
+
meta = {
|
|
56
|
+
"ts": time.time(),
|
|
57
|
+
"git_sha": _git_head_sha(project_root),
|
|
58
|
+
"git_branch": _git_branch(project_root),
|
|
59
|
+
}
|
|
60
|
+
p = write_eval_record(project_root, {**meta, **res})
|
|
61
|
+
res["record_path"] = str(p)
|
|
62
|
+
|
|
63
|
+
# Append ledger line (untracked; .gemcode/ is gitignored)
|
|
64
|
+
try:
|
|
65
|
+
ledger = project_root / ".gemcode" / "evals" / "autotune_ledger.jsonl"
|
|
66
|
+
ledger.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
import json
|
|
68
|
+
|
|
69
|
+
ledger.write_text("", encoding="utf-8") if not ledger.exists() else None
|
|
70
|
+
with ledger.open("a", encoding="utf-8") as f:
|
|
71
|
+
f.write(json.dumps({**meta, **res}, ensure_ascii=False) + "\n")
|
|
72
|
+
res["ledger_path"] = str(ledger)
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
return res
|
|
76
|
+
|
gemcode/callbacks.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
ADK callbacks: permissions, audit, tool failure circuit breaker, usage logging.
|
|
3
3
|
|
|
4
|
-
Maps to
|
|
4
|
+
Maps to patterns:
|
|
5
5
|
- before_tool / after_tool ≈ permission gates + telemetry around tool execution
|
|
6
6
|
- after_model ≈ cost / usage hooks (see cost-tracker.ts role)
|
|
7
7
|
- Session state for streak counters ≈ autoCompact failure tracking (MVP: tool errors)
|
|
@@ -60,7 +60,7 @@ def _truthy_env(name: str, *, default: bool = False) -> bool:
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def _maybe_tool_summary_enabled() -> bool:
|
|
63
|
-
# Mirrors
|
|
63
|
+
# Mirrors optional "emit tool use summaries" gating.
|
|
64
64
|
return _truthy_env("GEMCODE_EMIT_TOOL_USE_SUMMARIES", default=False)
|
|
65
65
|
|
|
66
66
|
|
|
@@ -344,7 +344,7 @@ def make_before_tool_callback(cfg: GemCodeConfig):
|
|
|
344
344
|
|
|
345
345
|
|
|
346
346
|
def make_after_tool_callback(cfg: GemCodeConfig):
|
|
347
|
-
"""Track consecutive tool failures in session state (
|
|
347
|
+
"""Track consecutive tool failures in session state (conventional circuit breaker)."""
|
|
348
348
|
|
|
349
349
|
def after_tool(
|
|
350
350
|
tool: BaseTool,
|
|
@@ -548,7 +548,7 @@ def make_after_tool_callback(cfg: GemCodeConfig):
|
|
|
548
548
|
summary[k] = v
|
|
549
549
|
append_audit(cfg.project_root, summary)
|
|
550
550
|
# Also print a concise, user-visible summary in CLI contexts.
|
|
551
|
-
# (
|
|
551
|
+
# (renders tool cards; this is the lightweight equivalent.)
|
|
552
552
|
try:
|
|
553
553
|
# Full-screen TUIs get corrupted by stray stderr prints.
|
|
554
554
|
if _truthy_env("GEMCODE_TUI_ACTIVE", default=False):
|
|
@@ -621,7 +621,7 @@ def make_after_model_callback(cfg: GemCodeConfig):
|
|
|
621
621
|
|
|
622
622
|
# ── Expose live token stats to the TUI ───────────────────────────────────
|
|
623
623
|
# The TUI reads cfg._last_turn_stats after each turn to display token counts
|
|
624
|
-
# and estimated cost in the footer (like
|
|
624
|
+
# and estimated cost in the footer (like Reference UI spinner token display).
|
|
625
625
|
try:
|
|
626
626
|
in_tok = d.get("prompt_token_count", 0) or 0
|
|
627
627
|
out_tok = d.get("candidates_token_count", 0) or 0
|
|
@@ -762,7 +762,7 @@ def make_after_model_callback(cfg: GemCodeConfig):
|
|
|
762
762
|
|
|
763
763
|
|
|
764
764
|
def make_on_tool_error_callback(cfg: GemCodeConfig):
|
|
765
|
-
"""Turn tool exceptions into structured tool results (
|
|
765
|
+
"""Turn tool exceptions into structured tool results (familiar is_error)."""
|
|
766
766
|
|
|
767
767
|
async def on_tool_error(
|
|
768
768
|
*, tool: BaseTool, args: dict[str, Any], tool_context, error: Exception
|
gemcode/capability_routing.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Capability-based routing (
|
|
2
|
+
Capability-based routing (style conceptually).
|
|
3
3
|
|
|
4
4
|
This layer decides which *capabilities* to enable (deep research tools,
|
|
5
|
-
embeddings retrieval, computer-use tools) and leaves the existing
|
|
5
|
+
embeddings retrieval, computer-use tools) and leaves the existing familiar
|
|
6
6
|
outer/inner loops intact.
|
|
7
7
|
|
|
8
8
|
It is intentionally conservative:
|
gemcode/checkpoints.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hermes-style checkpoints for GemCode.
|
|
3
|
+
|
|
4
|
+
Goal: make file mutations reversible with an explicit, local checkpoint log.
|
|
5
|
+
|
|
6
|
+
Storage:
|
|
7
|
+
<project>/.gemcode/checkpoints/<checkpoint_id>/manifest.json
|
|
8
|
+
<project>/.gemcode/checkpoints/<checkpoint_id>/files/<path>
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _now_ms() -> int:
|
|
21
|
+
return int(time.time() * 1000)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _checkpoints_dir(project_root: Path) -> Path:
|
|
25
|
+
return project_root / ".gemcode" / "checkpoints"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _safe_rel(project_root: Path, p: Path) -> str:
|
|
29
|
+
return str(p.resolve().relative_to(project_root.resolve()))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class CheckpointFile:
|
|
34
|
+
path: str
|
|
35
|
+
existed: bool
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Checkpoint:
|
|
40
|
+
id: str
|
|
41
|
+
ts_ms: int
|
|
42
|
+
op: str
|
|
43
|
+
files: list[CheckpointFile]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_checkpoint(
|
|
47
|
+
*,
|
|
48
|
+
project_root: Path,
|
|
49
|
+
op: str,
|
|
50
|
+
file_snapshots: list[tuple[Path, bool]],
|
|
51
|
+
) -> Checkpoint:
|
|
52
|
+
"""
|
|
53
|
+
Create a checkpoint capturing the *previous* contents of the provided files.
|
|
54
|
+
|
|
55
|
+
file_snapshots entries are (absolute_path, existed_bool).
|
|
56
|
+
"""
|
|
57
|
+
ts = _now_ms()
|
|
58
|
+
cid = f"cp_{ts}"
|
|
59
|
+
base = _checkpoints_dir(project_root) / cid
|
|
60
|
+
files_dir = base / "files"
|
|
61
|
+
files_dir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
out_files: list[CheckpointFile] = []
|
|
63
|
+
|
|
64
|
+
for abs_path, existed in file_snapshots:
|
|
65
|
+
try:
|
|
66
|
+
rel = _safe_rel(project_root, abs_path)
|
|
67
|
+
except Exception:
|
|
68
|
+
continue
|
|
69
|
+
out_files.append(CheckpointFile(path=rel, existed=bool(existed)))
|
|
70
|
+
if existed and abs_path.is_file():
|
|
71
|
+
target = files_dir / rel
|
|
72
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
target.write_bytes(abs_path.read_bytes())
|
|
74
|
+
|
|
75
|
+
manifest = {
|
|
76
|
+
"id": cid,
|
|
77
|
+
"ts_ms": ts,
|
|
78
|
+
"op": op,
|
|
79
|
+
"files": [{"path": f.path, "existed": f.existed} for f in out_files],
|
|
80
|
+
}
|
|
81
|
+
(base / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
82
|
+
return Checkpoint(id=cid, ts_ms=ts, op=op, files=out_files)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def list_checkpoints(project_root: Path, limit: int = 20) -> list[dict[str, Any]]:
|
|
86
|
+
d = _checkpoints_dir(project_root)
|
|
87
|
+
if not d.is_dir():
|
|
88
|
+
return []
|
|
89
|
+
cps = []
|
|
90
|
+
for p in sorted(d.iterdir(), key=lambda x: x.name, reverse=True):
|
|
91
|
+
m = p / "manifest.json"
|
|
92
|
+
if not m.is_file():
|
|
93
|
+
continue
|
|
94
|
+
try:
|
|
95
|
+
obj = json.loads(m.read_text(encoding="utf-8"))
|
|
96
|
+
cps.append(obj)
|
|
97
|
+
except Exception:
|
|
98
|
+
continue
|
|
99
|
+
if len(cps) >= max(1, int(limit)):
|
|
100
|
+
break
|
|
101
|
+
return cps
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def undo_checkpoint(project_root: Path, checkpoint_id: str | None = None) -> dict[str, Any]:
|
|
105
|
+
d = _checkpoints_dir(project_root)
|
|
106
|
+
if not d.is_dir():
|
|
107
|
+
return {"error": "no_checkpoints"}
|
|
108
|
+
if checkpoint_id:
|
|
109
|
+
base = d / checkpoint_id
|
|
110
|
+
else:
|
|
111
|
+
# newest
|
|
112
|
+
items = [p for p in d.iterdir() if p.is_dir()]
|
|
113
|
+
if not items:
|
|
114
|
+
return {"error": "no_checkpoints"}
|
|
115
|
+
base = sorted(items, key=lambda x: x.name, reverse=True)[0]
|
|
116
|
+
manifest_path = base / "manifest.json"
|
|
117
|
+
if not manifest_path.is_file():
|
|
118
|
+
return {"error": "checkpoint_missing_manifest"}
|
|
119
|
+
try:
|
|
120
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
121
|
+
except Exception as e:
|
|
122
|
+
return {"error": f"checkpoint_manifest_invalid:{e}"}
|
|
123
|
+
files_dir = base / "files"
|
|
124
|
+
restored = []
|
|
125
|
+
for f in manifest.get("files") or []:
|
|
126
|
+
try:
|
|
127
|
+
rel = str(f.get("path") or "")
|
|
128
|
+
existed = bool(f.get("existed"))
|
|
129
|
+
abs_path = (project_root / rel).resolve()
|
|
130
|
+
if existed:
|
|
131
|
+
src = files_dir / rel
|
|
132
|
+
if src.is_file():
|
|
133
|
+
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
abs_path.write_bytes(src.read_bytes())
|
|
135
|
+
restored.append(rel)
|
|
136
|
+
else:
|
|
137
|
+
# File did not exist previously; remove it if it exists now.
|
|
138
|
+
if abs_path.is_file():
|
|
139
|
+
abs_path.unlink()
|
|
140
|
+
restored.append(rel)
|
|
141
|
+
except Exception:
|
|
142
|
+
continue
|
|
143
|
+
return {"checkpoint_id": manifest.get("id") or base.name, "restored": restored}
|
|
144
|
+
|