gemcode 0.3.76__py3-none-any.whl → 0.3.78__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 +58 -17
- gemcode/autocompact.py +2 -2
- gemcode/autotune.py +17 -3
- gemcode/callbacks.py +6 -6
- gemcode/capability_routing.py +2 -2
- gemcode/cli.py +45 -28
- gemcode/config.py +10 -4
- gemcode/context_budget.py +2 -2
- gemcode/context_warning.py +6 -6
- gemcode/credentials.py +1 -1
- gemcode/evals/harness.py +74 -20
- gemcode/hooks.py +1 -1
- gemcode/invoke.py +1 -1
- gemcode/{kairos_daemon.py → kaira_daemon.py} +19 -19
- gemcode/limits.py +1 -1
- gemcode/mcp_loader.py +1 -1
- gemcode/modality_tools.py +1 -1
- gemcode/model_routing.py +1 -1
- gemcode/permissions.py +2 -2
- gemcode/plugins/terminal_hooks_plugin.py +1 -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 +134 -4
- gemcode/repl_slash.py +335 -12
- gemcode/session_runtime.py +1 -1
- gemcode/skills.py +1 -1
- gemcode/slash_commands.py +1 -1
- gemcode/thinking.py +4 -4
- gemcode/tool_prompt_manifest.py +1 -1
- gemcode/tool_registry.py +2 -2
- gemcode/tool_result_store.py +1 -1
- gemcode/tools/bash.py +1 -1
- gemcode/tools/edit.py +3 -2
- gemcode/tools/notebook.py +2 -2
- gemcode/tools/notes.py +2 -3
- 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/trust.py +5 -0
- gemcode/tui/input_handler.py +8 -42
- gemcode/tui/scrollback.py +4 -3
- gemcode/tui/spinner.py +8 -8
- gemcode/tui/welcome_rich.py +1 -1
- gemcode/web/{claude_sse_adapter.py → web_sse_compat.py} +2 -5
- gemcode-0.3.78.dist-info/METADATA +778 -0
- gemcode-0.3.78.dist-info/RECORD +108 -0
- gemcode-0.3.76.dist-info/METADATA +0 -523
- gemcode-0.3.76.dist-info/RECORD +0 -108
- {gemcode-0.3.76.dist-info → gemcode-0.3.78.dist-info}/WHEEL +0 -0
- {gemcode-0.3.76.dist-info → gemcode-0.3.78.dist-info}/entry_points.txt +0 -0
- {gemcode-0.3.76.dist-info → gemcode-0.3.78.dist-info}/licenses/LICENSE +0 -0
- {gemcode-0.3.76.dist-info → gemcode-0.3.78.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,11 +27,49 @@ 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
|
|
30
|
+
from gemcode.skills import (
|
|
31
|
+
build_skill_manifest_text,
|
|
32
|
+
expand_skill_text,
|
|
33
|
+
list_supporting_files,
|
|
34
|
+
load_skill,
|
|
35
|
+
)
|
|
31
36
|
from gemcode.output_styles import build_output_style_section
|
|
32
37
|
from gemcode.rules import build_rules_section
|
|
33
38
|
|
|
34
39
|
|
|
40
|
+
def _build_session_loaded_skills_section(cfg: GemCodeConfig) -> str:
|
|
41
|
+
"""Full bodies for GemSkills the user loaded with /gemskill (session-scoped)."""
|
|
42
|
+
names = list(getattr(cfg, "session_loaded_skill_names", None) or [])
|
|
43
|
+
if not names:
|
|
44
|
+
return ""
|
|
45
|
+
sid = getattr(cfg, "session_skill_expand_session_id", None) or ""
|
|
46
|
+
chunks: list[str] = []
|
|
47
|
+
seen: set[str] = set()
|
|
48
|
+
for raw in names:
|
|
49
|
+
sk_name = (raw or "").strip().lower()
|
|
50
|
+
if not sk_name or sk_name in seen:
|
|
51
|
+
continue
|
|
52
|
+
seen.add(sk_name)
|
|
53
|
+
s = load_skill(cfg.project_root, sk_name)
|
|
54
|
+
if s is None:
|
|
55
|
+
continue
|
|
56
|
+
expanded = expand_skill_text(s, arguments="", session_id=sid)
|
|
57
|
+
files = list_supporting_files(s)
|
|
58
|
+
head = f"### GemSkill: `/{s.meta.name}` (loaded for this session)\n\n"
|
|
59
|
+
chunk = head + expanded
|
|
60
|
+
if files:
|
|
61
|
+
chunk += f"\n\nSupporting files: {', '.join(files)}"
|
|
62
|
+
chunks.append(chunk)
|
|
63
|
+
if not chunks:
|
|
64
|
+
return ""
|
|
65
|
+
return (
|
|
66
|
+
"## Loaded GemSkills (this session)\n"
|
|
67
|
+
"The user explicitly loaded these skills with `/gemskill`. Follow their workflows "
|
|
68
|
+
"when the task matches their purpose; do not force them on unrelated requests.\n\n"
|
|
69
|
+
+ "\n\n---\n\n".join(chunks)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
35
73
|
def build_global_instruction() -> str:
|
|
36
74
|
"""Global instruction applied to the entire agent tree (via ADK plugin)."""
|
|
37
75
|
return (
|
|
@@ -64,7 +102,7 @@ def _chain_before_model_callbacks(*callbacks):
|
|
|
64
102
|
|
|
65
103
|
def _load_gemini_md(project_root: Path) -> str:
|
|
66
104
|
"""
|
|
67
|
-
Load GEMINI.md / .gemcode/NOTES.md from a
|
|
105
|
+
Load GEMINI.md / .gemcode/NOTES.md from a interactive CLI–style hierarchy.
|
|
68
106
|
|
|
69
107
|
Priority (later entries override earlier ones, all are concatenated):
|
|
70
108
|
1. ~/.gemcode/GEMINI.md — user-global instructions (all projects)
|
|
@@ -89,7 +127,7 @@ def _load_gemini_md(project_root: Path) -> str:
|
|
|
89
127
|
return ""
|
|
90
128
|
try:
|
|
91
129
|
raw = p.read_text(encoding="utf-8", errors="replace")[:_FILE_CAP]
|
|
92
|
-
# Strip HTML comments (
|
|
130
|
+
# Strip HTML comments (saves tokens)
|
|
93
131
|
return _COMMENT_RE.sub("", raw).strip()
|
|
94
132
|
except OSError:
|
|
95
133
|
return ""
|
|
@@ -140,7 +178,7 @@ def _get_git_context(root) -> str:
|
|
|
140
178
|
"""
|
|
141
179
|
Run a quick git snapshot at session start — branch, recent commits, diff-stat.
|
|
142
180
|
Returns a formatted string or empty string if not a git repo.
|
|
143
|
-
Mirrors
|
|
181
|
+
Mirrors Reference UI getGitStatus() pattern.
|
|
144
182
|
"""
|
|
145
183
|
import subprocess
|
|
146
184
|
import shutil
|
|
@@ -232,18 +270,18 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
232
270
|
if max_session_tokens:
|
|
233
271
|
budget_line += f" · max_session_tokens={max_session_tokens:,}"
|
|
234
272
|
|
|
235
|
-
# ──
|
|
236
|
-
# The user can run `gemcode
|
|
273
|
+
# ── Kaira ────────────────────────────────────────────────────────────────
|
|
274
|
+
# The user can run `gemcode kaira -C <project>` in a separate terminal to
|
|
237
275
|
# launch a long-lived scheduler. Jobs submitted to it run concurrently with
|
|
238
276
|
# the current session. This is useful for background / parallel heavy work.
|
|
239
|
-
|
|
240
|
-
"- **
|
|
277
|
+
kaira_section = (
|
|
278
|
+
"- **Kaira background scheduler** — `gemcode kaira -C <project>` launches a "
|
|
241
279
|
"long-lived daemon that reads prompts from stdin and runs each as an isolated job "
|
|
242
|
-
"(up to N concurrently). Each job gets `
|
|
243
|
-
"`
|
|
280
|
+
"(up to N concurrently). Each job gets `kaira_sleep_ms(ms)` and "
|
|
281
|
+
"`kaira_enqueue_prompt(prompt, priority, session_id)` tools so the model can "
|
|
244
282
|
"schedule follow-up work itself. Useful for: bulk file processing, repeated "
|
|
245
283
|
"polling loops, parallelising large independent tasks. "
|
|
246
|
-
"Tell the user to open a second terminal and run `gemcode
|
|
284
|
+
"Tell the user to open a second terminal and run `gemcode kaira` if a task "
|
|
247
285
|
"would benefit from background parallelism."
|
|
248
286
|
)
|
|
249
287
|
|
|
@@ -274,7 +312,7 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
|
|
|
274
312
|
- **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.
|
|
275
313
|
- **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.
|
|
276
314
|
- **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.
|
|
277
|
-
{
|
|
315
|
+
{kaira_section}
|
|
278
316
|
- **UI banner** phrases like "GemCode Pro" are terminal marketing, not a separate API tier.
|
|
279
317
|
- **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.
|
|
280
318
|
- **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}"""
|
|
@@ -680,7 +718,7 @@ Concrete patterns:
|
|
|
680
718
|
- Grepping different patterns → multiple `grep_content` in one response
|
|
681
719
|
- `list_directory` + `glob_files` → both at once
|
|
682
720
|
|
|
683
|
-
**Parallel sub-agent exploration (
|
|
721
|
+
**Parallel sub-agent exploration (reference terminal UI pattern):**
|
|
684
722
|
When a task requires understanding several subsystems before acting:
|
|
685
723
|
1. Spawn parallel `run_subtask` workers, one per subsystem
|
|
686
724
|
2. Wait for all results to return in the same turn
|
|
@@ -844,7 +882,7 @@ Use `gh pr create` via `bash`. When asked to create a PR:
|
|
|
844
882
|
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`.
|
|
845
883
|
|
|
846
884
|
## Agent notes (.gemcode/notes.md)
|
|
847
|
-
You have two tools to persist project insights across sessions
|
|
885
|
+
You have two tools to persist project insights across sessions (auto-memory style):
|
|
848
886
|
|
|
849
887
|
- **`append_project_note(note)`** — write a note to `.gemcode/notes.md`. Use this proactively when you discover something worth remembering:
|
|
850
888
|
- Build/test/lint commands you discover ("Build: `npm run build` — requires Node 20")
|
|
@@ -888,6 +926,9 @@ You have two tools to persist project insights across sessions, like Claude Code
|
|
|
888
926
|
skill_manifest = build_skill_manifest_text(cfg.project_root)
|
|
889
927
|
if skill_manifest:
|
|
890
928
|
base = f"{base}\n\n{skill_manifest}"
|
|
929
|
+
loaded_skills = _build_session_loaded_skills_section(cfg)
|
|
930
|
+
if loaded_skills:
|
|
931
|
+
base = f"{base}\n\n{loaded_skills}"
|
|
891
932
|
extra = _load_gemini_md(cfg.project_root)
|
|
892
933
|
if extra.strip():
|
|
893
934
|
return f"{base}\n\n## Project instructions (GEMINI.md)\n{extra}"
|
|
@@ -936,7 +977,7 @@ def build_root_agent(
|
|
|
936
977
|
except Exception:
|
|
937
978
|
pass
|
|
938
979
|
|
|
939
|
-
# Agent auto-notes: write project insights to .gemcode/notes.md (
|
|
980
|
+
# Agent auto-notes: write project insights to .gemcode/notes.md (project notes file)
|
|
940
981
|
try:
|
|
941
982
|
from gemcode.tools.notes import build_notes_tools
|
|
942
983
|
notes_tools = build_notes_tools(cfg.project_root)
|
|
@@ -964,7 +1005,7 @@ def build_root_agent(
|
|
|
964
1005
|
if before_model is not None:
|
|
965
1006
|
cb_kwargs["before_model_callback"] = before_model
|
|
966
1007
|
|
|
967
|
-
#
|
|
1008
|
+
# familiar thinking: enabled by default (Gemini dynamic), but allow
|
|
968
1009
|
# explicit overrides for disable/budgets/levels.
|
|
969
1010
|
gen_cfg = None
|
|
970
1011
|
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
CHANGED
|
@@ -3,8 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
import subprocess
|
|
4
4
|
import time
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any, Iterable
|
|
7
7
|
|
|
8
|
+
from gemcode.config import GemCodeConfig
|
|
8
9
|
from gemcode.evals.harness import run_eval_suite, write_eval_record
|
|
9
10
|
|
|
10
11
|
|
|
@@ -47,11 +48,24 @@ def init_autotune(*, project_root: Path, tag: str) -> dict[str, Any]:
|
|
|
47
48
|
return {"status": "created", "branch": branch}
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
def run_autotune_eval(
|
|
51
|
+
def run_autotune_eval(
|
|
52
|
+
*,
|
|
53
|
+
project_root: Path,
|
|
54
|
+
include_llm: bool,
|
|
55
|
+
model: str | None = None,
|
|
56
|
+
session_cfg: GemCodeConfig | None = None,
|
|
57
|
+
extra_tools: Iterable[Any] | None = None,
|
|
58
|
+
) -> dict[str, Any]:
|
|
51
59
|
"""
|
|
52
60
|
Run eval suite and persist last result to .gemcode/evals/last_eval.json.
|
|
53
61
|
"""
|
|
54
|
-
res = run_eval_suite(
|
|
62
|
+
res = run_eval_suite(
|
|
63
|
+
project_root=project_root,
|
|
64
|
+
include_llm=include_llm,
|
|
65
|
+
model=model,
|
|
66
|
+
session_cfg=session_cfg,
|
|
67
|
+
extra_tools=extra_tools,
|
|
68
|
+
)
|
|
55
69
|
meta = {
|
|
56
70
|
"ts": time.time(),
|
|
57
71
|
"git_sha": _git_head_sha(project_root),
|
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/cli.py
CHANGED
|
@@ -37,7 +37,7 @@ def _events_to_text(events) -> str:
|
|
|
37
37
|
|
|
38
38
|
def _maybe_prompt_trust(cfg: GemCodeConfig) -> None:
|
|
39
39
|
"""
|
|
40
|
-
|
|
40
|
+
interactive CLI–style workspace trust prompt.
|
|
41
41
|
|
|
42
42
|
On first use in a project root, ask the user to trust the folder so file,
|
|
43
43
|
shell, and git tools can run. If not trusted, we exit before any tool runs.
|
|
@@ -70,7 +70,7 @@ def _maybe_prompt_trust(cfg: GemCodeConfig) -> None:
|
|
|
70
70
|
|
|
71
71
|
def _maybe_prompt_google_api_key() -> None:
|
|
72
72
|
"""
|
|
73
|
-
One-time interactive prompt for ``GOOGLE_API_KEY`` (like ``
|
|
73
|
+
One-time interactive prompt for ``GOOGLE_API_KEY`` (like ``gemcode login`` / first-run).
|
|
74
74
|
|
|
75
75
|
Skipped when the key is already set, stdin is not a TTY, or
|
|
76
76
|
``GEMCODE_NO_LOGIN_PROMPT=1``.
|
|
@@ -125,7 +125,7 @@ def _initialize_gemcode_project(cfg: GemCodeConfig) -> None:
|
|
|
125
125
|
Create ``<project>/.gemcode/`` and print a short banner the first time it appears.
|
|
126
126
|
|
|
127
127
|
Runs after workspace trust + API key are satisfied so a bare ``gemcode`` REPL
|
|
128
|
-
feels like a guided first-run (
|
|
128
|
+
feels like a guided first-run (interactive CLI–style).
|
|
129
129
|
"""
|
|
130
130
|
root = cfg.project_root.resolve()
|
|
131
131
|
gem_dir = root / ".gemcode"
|
|
@@ -174,7 +174,7 @@ async def _run_prompt(
|
|
|
174
174
|
|
|
175
175
|
async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> None:
|
|
176
176
|
"""
|
|
177
|
-
Interactive REPL mode (
|
|
177
|
+
Interactive REPL mode (multi-turn REPL): keep the session open for multiple turns.
|
|
178
178
|
"""
|
|
179
179
|
load_cli_environment()
|
|
180
180
|
_maybe_prompt_trust(cfg)
|
|
@@ -245,6 +245,13 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
245
245
|
file=sys.stderr,
|
|
246
246
|
)
|
|
247
247
|
|
|
248
|
+
try:
|
|
249
|
+
from gemcode.repl_commands import install_readline_slash_completion
|
|
250
|
+
|
|
251
|
+
install_readline_slash_completion()
|
|
252
|
+
except Exception:
|
|
253
|
+
pass
|
|
254
|
+
|
|
248
255
|
print(
|
|
249
256
|
"GemCode CLI is running. Type your prompt and press Enter. (Ctrl+D to exit)",
|
|
250
257
|
file=sys.stderr,
|
|
@@ -261,6 +268,7 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
261
268
|
if prompt_text in (":q", "quit", "exit", "/exit"):
|
|
262
269
|
break
|
|
263
270
|
|
|
271
|
+
cfg.session_skill_expand_session_id = session_id
|
|
264
272
|
slash = await process_repl_slash(
|
|
265
273
|
cfg=cfg,
|
|
266
274
|
runner=runner,
|
|
@@ -273,7 +281,16 @@ async def _run_repl(cfg: GemCodeConfig, session_id: str, *, use_mcp: bool) -> No
|
|
|
273
281
|
break
|
|
274
282
|
if slash.new_session_id is not None:
|
|
275
283
|
session_id = slash.new_session_id
|
|
284
|
+
cfg.session_skill_expand_session_id = session_id
|
|
276
285
|
if slash.skip_model_turn:
|
|
286
|
+
if slash.force_rebuild_runner:
|
|
287
|
+
try:
|
|
288
|
+
_c = runner.close()
|
|
289
|
+
if asyncio.iscoroutine(_c):
|
|
290
|
+
await _c
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
293
|
+
runner = create_runner(cfg, extra_tools=None)
|
|
277
294
|
continue
|
|
278
295
|
prompt_text = slash.model_prompt or prompt_text
|
|
279
296
|
|
|
@@ -356,7 +373,7 @@ def main() -> None:
|
|
|
356
373
|
return
|
|
357
374
|
raise SystemExit("Usage: gemcode ide --stdio")
|
|
358
375
|
|
|
359
|
-
# Persist or rotate API key (
|
|
376
|
+
# Persist or rotate API key (interactive CLI–style `gemcode login`).
|
|
360
377
|
if len(sys.argv) > 1 and sys.argv[1] == "login":
|
|
361
378
|
load_cli_environment()
|
|
362
379
|
if not (hasattr(sys.stdin, "isatty") and sys.stdin.isatty()):
|
|
@@ -598,78 +615,78 @@ def main() -> None:
|
|
|
598
615
|
print(f"\n[gemcode live-audio] session_id={session_id}", file=sys.stderr)
|
|
599
616
|
return
|
|
600
617
|
|
|
601
|
-
#
|
|
602
|
-
if len(sys.argv) > 1 and sys.argv[1] == "
|
|
603
|
-
|
|
604
|
-
prog="gemcode
|
|
605
|
-
description="
|
|
618
|
+
# Kaira proactive scheduler daemon.
|
|
619
|
+
if len(sys.argv) > 1 and sys.argv[1] == "kaira":
|
|
620
|
+
kaira_parser = argparse.ArgumentParser(
|
|
621
|
+
prog="gemcode kaira",
|
|
622
|
+
description="Background proactive scheduler daemon (stdin -> queued jobs).",
|
|
606
623
|
)
|
|
607
|
-
|
|
624
|
+
kaira_parser.add_argument(
|
|
608
625
|
"-C",
|
|
609
626
|
"--directory",
|
|
610
627
|
type=Path,
|
|
611
628
|
default=Path.cwd(),
|
|
612
629
|
help="Project root",
|
|
613
630
|
)
|
|
614
|
-
|
|
631
|
+
kaira_parser.add_argument(
|
|
615
632
|
"--session",
|
|
616
633
|
default=None,
|
|
617
634
|
help="Session id for SQLite-backed history (optional; defaults to a new uuid).",
|
|
618
635
|
)
|
|
619
|
-
|
|
636
|
+
kaira_parser.add_argument(
|
|
620
637
|
"--concurrency",
|
|
621
638
|
type=int,
|
|
622
639
|
default=2,
|
|
623
640
|
help="Max number of concurrent queued jobs.",
|
|
624
641
|
)
|
|
625
|
-
|
|
642
|
+
kaira_parser.add_argument(
|
|
626
643
|
"--default-priority",
|
|
627
644
|
type=int,
|
|
628
645
|
default=0,
|
|
629
646
|
help="Priority used for stdin-enqueued jobs.",
|
|
630
647
|
)
|
|
631
|
-
|
|
648
|
+
kaira_parser.add_argument(
|
|
632
649
|
"--yes",
|
|
633
650
|
action="store_true",
|
|
634
651
|
help="Allow write_file / search_replace (disables interactive HITL prompts).",
|
|
635
652
|
)
|
|
636
|
-
|
|
653
|
+
kaira_parser.add_argument(
|
|
637
654
|
"--interactive-ask",
|
|
638
655
|
action="store_true",
|
|
639
656
|
help="Prompt in-run for mutating tool confirmations (HITL).",
|
|
640
657
|
)
|
|
641
|
-
|
|
642
|
-
|
|
658
|
+
kaira_parser.add_argument("--model", default=None, help="Override GEMCODE_MODEL")
|
|
659
|
+
kaira_parser.add_argument(
|
|
643
660
|
"--model-mode",
|
|
644
661
|
default=None,
|
|
645
662
|
help="Model mode: auto|fast|balanced|quality (overrides GEMCODE_MODEL_MODE).",
|
|
646
663
|
)
|
|
647
|
-
|
|
664
|
+
kaira_parser.add_argument(
|
|
648
665
|
"--deep-research",
|
|
649
666
|
action="store_true",
|
|
650
667
|
help="Enable deep research tools + routing.",
|
|
651
668
|
)
|
|
652
|
-
|
|
669
|
+
kaira_parser.add_argument(
|
|
653
670
|
"--maps-grounding",
|
|
654
671
|
action="store_true",
|
|
655
672
|
help="Opt-in to Google Maps grounding tool inside deep-research.",
|
|
656
673
|
)
|
|
657
|
-
|
|
674
|
+
kaira_parser.add_argument(
|
|
658
675
|
"--embeddings",
|
|
659
676
|
action="store_true",
|
|
660
677
|
help="Enable embeddings-based semantic retrieval.",
|
|
661
678
|
)
|
|
662
|
-
|
|
679
|
+
kaira_parser.add_argument(
|
|
663
680
|
"--capability-mode",
|
|
664
681
|
default=None,
|
|
665
682
|
help="Capability routing: auto|research|embeddings|computer|audio|all (enables tools and routes models).",
|
|
666
683
|
)
|
|
667
|
-
|
|
684
|
+
kaira_parser.add_argument(
|
|
668
685
|
"--tool-combination-mode",
|
|
669
686
|
default=None,
|
|
670
687
|
help="Gemini 3 tool context circulation: deep_research|always|never|auto",
|
|
671
688
|
)
|
|
672
|
-
|
|
689
|
+
kaira_parser.add_argument(
|
|
673
690
|
"--max-llm-calls",
|
|
674
691
|
type=int,
|
|
675
692
|
default=None,
|
|
@@ -677,7 +694,7 @@ def main() -> None:
|
|
|
677
694
|
help="Cap model↔tool iterations for each job message (ADK RunConfig.max_llm_calls).",
|
|
678
695
|
)
|
|
679
696
|
|
|
680
|
-
args =
|
|
697
|
+
args = kaira_parser.parse_args(sys.argv[2:])
|
|
681
698
|
load_cli_environment()
|
|
682
699
|
|
|
683
700
|
cfg = GemCodeConfig(project_root=args.directory)
|
|
@@ -713,15 +730,15 @@ def main() -> None:
|
|
|
713
730
|
require_google_api_key()
|
|
714
731
|
|
|
715
732
|
session_id = args.session or str(uuid.uuid4())
|
|
716
|
-
from gemcode.
|
|
733
|
+
from gemcode.kaira_daemon import KairaDaemon
|
|
717
734
|
|
|
718
|
-
daemon =
|
|
735
|
+
daemon = KairaDaemon(
|
|
719
736
|
cfg=cfg,
|
|
720
737
|
concurrency=args.concurrency,
|
|
721
738
|
default_priority=args.default_priority,
|
|
722
739
|
)
|
|
723
740
|
asyncio.run(daemon.run_forever(session_id=session_id))
|
|
724
|
-
print(f"\n[gemcode
|
|
741
|
+
print(f"\n[gemcode kaira] session_id={session_id}", file=sys.stderr)
|
|
725
742
|
return
|
|
726
743
|
|
|
727
744
|
parser = argparse.ArgumentParser(prog="gemcode", description="Gemini + ADK coding agent")
|
gemcode/config.py
CHANGED
|
@@ -49,7 +49,7 @@ def _truthy_env(name: str, *, default: bool = False) -> bool:
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def token_budget_invocation_reset() -> dict:
|
|
52
|
-
"""Reset per-user-message token budget tracker
|
|
52
|
+
"""Reset per-user-message token budget tracker when starting a new user turn."""
|
|
53
53
|
import time
|
|
54
54
|
|
|
55
55
|
t = int(time.time() * 1000)
|
|
@@ -212,6 +212,12 @@ class GemCodeConfig:
|
|
|
212
212
|
default_factory=lambda: os.environ.get("GEMCODE_OUTPUT_STYLE") or None
|
|
213
213
|
)
|
|
214
214
|
|
|
215
|
+
# GemSkills explicitly loaded via /gemskill — full bodies injected into the
|
|
216
|
+
# system instruction until cleared or the session is reset/resumed.
|
|
217
|
+
session_loaded_skill_names: list[str] = field(default_factory=list)
|
|
218
|
+
# Substitutes ${GEMCODE_SESSION_ID} when expanding loaded skills for prompts.
|
|
219
|
+
session_skill_expand_session_id: str | None = None
|
|
220
|
+
|
|
215
221
|
# Modality toggles (tool injection + routing).
|
|
216
222
|
enable_deep_research: bool = field(
|
|
217
223
|
default_factory=lambda: _truthy_env("GEMCODE_ENABLE_DEEP_RESEARCH", default=False)
|
|
@@ -285,9 +291,9 @@ class GemCodeConfig:
|
|
|
285
291
|
# role-based routing from overriding their selection.
|
|
286
292
|
model_overridden: bool = False
|
|
287
293
|
|
|
288
|
-
# Gemini thinking controls (
|
|
294
|
+
# Gemini thinking controls (familiar intent, Gemini-specific knobs).
|
|
289
295
|
#
|
|
290
|
-
#
|
|
296
|
+
# enables thinking by default and only forces disable/budgets
|
|
291
297
|
# when explicitly configured. We match that by returning "None" unless the
|
|
292
298
|
# user asks for explicit overrides below.
|
|
293
299
|
#
|
|
@@ -333,7 +339,7 @@ class GemCodeConfig:
|
|
|
333
339
|
|
|
334
340
|
# Plan mode: when ON, the agent explicitly writes out a numbered plan
|
|
335
341
|
# BEFORE executing any tools, then checks the plan before reporting done.
|
|
336
|
-
# Like
|
|
342
|
+
# Like Reference UI EnterPlanMode — great for complex, multi-file tasks.
|
|
337
343
|
# Toggle at runtime with /plan on|off.
|
|
338
344
|
plan_mode: bool = field(
|
|
339
345
|
default_factory=lambda: _truthy_env("GEMCODE_PLAN_MODE", default=False)
|
gemcode/context_budget.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Bounded tool output and soft prompt-size limits (
|
|
2
|
+
Bounded tool output and soft prompt-size limits (interactive CLI–style context hygiene).
|
|
3
3
|
|
|
4
4
|
- Truncate oversized tool result dicts before they enter history (`after_tool`).
|
|
5
5
|
- Before each LLM call, trim oldest text parts until estimated char total is under
|
|
@@ -169,7 +169,7 @@ def shrink_contents_text_inplace(contents: Any, max_total_chars: int) -> bool:
|
|
|
169
169
|
# Choose a max string size that should reduce at least some of the excess.
|
|
170
170
|
if excess >= max_len:
|
|
171
171
|
# Extreme overage: clearing oldest tool payload is closer to
|
|
172
|
-
#
|
|
172
|
+
# microcompact behavior and guarantees progress.
|
|
173
173
|
cleared = {"[Old tool result content cleared]": True}
|
|
174
174
|
if getattr(part, "tool_response", None) is not None:
|
|
175
175
|
part.tool_response.response = cleared
|
gemcode/context_warning.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Context pressure signals for autocompact and warnings (token-based thresholds).
|
|
3
3
|
|
|
4
4
|
Uses API `prompt_token_count` when available; thresholds are token-based with
|
|
5
|
-
env overrides. Mirrors
|
|
5
|
+
env overrides. Mirrors common buffer constants where practical.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -11,7 +11,7 @@ import os
|
|
|
11
11
|
|
|
12
12
|
from gemcode.config import GemCodeConfig
|
|
13
13
|
|
|
14
|
-
#
|
|
14
|
+
# Reference auto-compact defaults
|
|
15
15
|
AUTOCOMPACT_BUFFER_TOKENS = 13_000
|
|
16
16
|
WARNING_THRESHOLD_BUFFER_TOKENS = 20_000
|
|
17
17
|
ERROR_THRESHOLD_BUFFER_TOKENS = 20_000
|
|
@@ -40,12 +40,12 @@ def get_effective_context_window_tokens(model: str) -> int:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def get_reserved_summary_tokens(model: str) -> int:
|
|
43
|
-
"""
|
|
43
|
+
"""Reserve headroom for compaction summary output; we use a small cap."""
|
|
44
44
|
return min(_opt_int("GEMCODE_AUTOCOMPACT_RESERVED_OUTPUT_TOKENS", 20_000), 20_000)
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def get_effective_context_window_size_tokens(model: str) -> int:
|
|
48
|
-
"""`effectiveContextWindow` ≈ window minus reserved output
|
|
48
|
+
"""`effectiveContextWindow` ≈ window minus reserved output for summaries."""
|
|
49
49
|
w = get_effective_context_window_tokens(model)
|
|
50
50
|
return max(10_000, w - get_reserved_summary_tokens(model))
|
|
51
51
|
|
|
@@ -70,7 +70,7 @@ def calculate_context_warning_state(
|
|
|
70
70
|
cfg: GemCodeConfig | None = None,
|
|
71
71
|
) -> dict[str, object]:
|
|
72
72
|
"""
|
|
73
|
-
Returns keys aligned with
|
|
73
|
+
Returns keys aligned with typical token-warning state helpers:
|
|
74
74
|
- percent_left
|
|
75
75
|
- is_above_warning_threshold
|
|
76
76
|
- is_above_error_threshold
|
gemcode/credentials.py
CHANGED