gemcode 0.3.26__py3-none-any.whl → 0.3.28__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 CHANGED
@@ -58,17 +58,93 @@ def _load_gemini_md(project_root: Path) -> str:
58
58
 
59
59
  def _build_runtime_facts(cfg: GemCodeConfig) -> str:
60
60
  """
61
- Injected every session so the model does not hallucinate deployment, permissions,
62
- or "how to switch Pro" the way a product-agnostic base prompt would.
61
+ Injected every session so the model is fully self-aware of its own capabilities,
62
+ limits, and the environment not just generic defaults.
63
63
  """
64
64
  root = cfg.project_root.resolve()
65
65
  model = (getattr(cfg, "model", None) or "").strip() or "(default)"
66
+
67
+ # ── Active capabilities ──────────────────────────────────────────────────
68
+ caps: list[str] = []
69
+ if getattr(cfg, "enable_deep_research", False):
70
+ dr_extras = " + google_maps_grounding" if getattr(cfg, "enable_maps_grounding", False) else ""
71
+ caps.append(f"deep_research ON (tools: google_search, url_context{dr_extras})")
72
+ if getattr(cfg, "enable_embeddings", False):
73
+ caps.append(f"embeddings ON (tool: semantic_search_files, model: {getattr(cfg, 'embeddings_model', 'default')})")
74
+ if getattr(cfg, "enable_memory", False):
75
+ mem_path = root / ".gemcode" / "memories.jsonl"
76
+ mem_kind = "embedding-backed" if getattr(cfg, "enable_embeddings", False) else "keyword-backed"
77
+ caps.append(f"memory ON ({mem_kind}, stored at {mem_path}; ADK preload_memory injects relevant memories before each turn)")
78
+ if getattr(cfg, "enable_computer_use", False):
79
+ caps.append("computer_use ON (tools: navigate, click_at, type_text_at, browser_screenshot, browser_find_element, etc.)")
80
+ if not caps:
81
+ caps.append("none enabled (use /research on, /embeddings on, /memory on, /computer on to enable)")
82
+ caps_text = "\n".join(f" - {c}" for c in caps)
83
+
84
+ # ── Limits ───────────────────────────────────────────────────────────────
85
+ max_calls = getattr(cfg, "max_llm_calls", 256) or 256
86
+ token_budget = getattr(cfg, "token_budget", None)
87
+ max_session_tokens = getattr(cfg, "max_session_tokens", None)
88
+ budget_line = f"{max_calls} model↔tool iterations per user message"
89
+ if token_budget:
90
+ budget_line += f" · token_budget={token_budget:,} per turn"
91
+ if max_session_tokens:
92
+ budget_line += f" · max_session_tokens={max_session_tokens:,}"
93
+
94
+ # ── Kairos ────────────────────────────────────────────────────────────────
95
+ # The user can run `gemcode kairos -C <project>` in a separate terminal to
96
+ # launch a long-lived scheduler. Jobs submitted to it run concurrently with
97
+ # the current session. This is useful for background / parallel heavy work.
98
+ kairos_section = (
99
+ "- **Kairos background scheduler** — `gemcode kairos -C <project>` launches a "
100
+ "long-lived daemon that reads prompts from stdin and runs each as an isolated job "
101
+ "(up to N concurrently). Each job gets `kairos_sleep_ms(ms)` and "
102
+ "`kairos_enqueue_prompt(prompt, priority, session_id)` tools so the model can "
103
+ "schedule follow-up work itself. Useful for: bulk file processing, repeated "
104
+ "polling loops, parallelising large independent tasks. "
105
+ "Tell the user to open a second terminal and run `gemcode kairos` if a task "
106
+ "would benefit from background parallelism."
107
+ )
108
+
66
109
  return f"""## Runtime facts (authoritative for this session)
67
110
  - **Project root** — every filesystem tool path is relative to: `{root}`
68
- - **Model id in use:** `{model}`. In this TUI/REPL you can override it for subsequent turns with `/model use <id>` (use `/model list` to browse IDs). For a full restart you can still use `--model <id>` or env `GEMCODE_MODEL`.
69
- - **UI banner** phrases such as "GemCode Pro" are **terminal marketing**, not a separate API tier or model you enable from chat.
70
- - **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.
71
- - **Working in subfolders** — use tools: e.g. `list_directory("Desktop")`, `glob_files("**/query.ts")`, `read_file("testing/ai-edtech-app/src/app/page.tsx")`, or `run_command` with `cwd_subdir`. Never claim the sandbox cannot reach a subpath unless a tool returned an explicit error."""
111
+ - **Model id in use:** `{model}`. Override mid-session with `/model use <id>` or `/mode fast|balanced|quality|auto`.
112
+ - **Execution budget:** {budget_line}.
113
+ - **Active capabilities:**
114
+ {caps_text}
115
+ - **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.
116
+ - **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.
117
+ - **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.
118
+ {kairos_section}
119
+ - **UI banner** phrases like "GemCode Pro" are terminal marketing, not a separate API tier.
120
+ - **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.
121
+ - **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."""
122
+
123
+
124
+ def _build_memory_section(cfg: GemCodeConfig) -> str:
125
+ """Injected when enable_memory=True so the agent understands and uses memory."""
126
+ mem_path = cfg.project_root / ".gemcode" / "memories.jsonl"
127
+ kind = "embedding-based (semantic cosine similarity)" if getattr(cfg, "enable_embeddings", False) else "keyword-based"
128
+ return f"""
129
+ ## Persistent Memory System
130
+ Memory is **ON** ({kind}). Stored at: `{mem_path}`
131
+
132
+ ### How it works
133
+ - Before each turn, ADK automatically searches the memory store for relevant past facts and injects them as context — you do not need to call a tool to load them.
134
+ - After each turn, the session is automatically added to memory by the post-turn plugin.
135
+ - The memory file persists across sessions (JSONL, one entry per session).
136
+
137
+ ### What to do with memory
138
+ - **Reference it naturally** — if the injected context mentions past facts ("last time the user's API key was X", "the user prefers TypeScript"), treat that as trusted context.
139
+ - **Update it proactively** — if the user tells you important facts about their project, preferences, or recurring patterns, note them in your response: "I'll remember that for future sessions."
140
+ - **Don't re-explain already-known context** — if the memory already contains the project structure or preferences, skip the discovery step and act on what's known.
141
+ - **Memory is scoped to this project root** — `{cfg.project_root}`. Different project roots have separate memories.
142
+
143
+ ### When memory helps most
144
+ - Long-running projects (user preferences, patterns, recurring tasks)
145
+ - Multi-session workflows (continuing work from a previous day)
146
+ - Team conventions stored once and reused automatically
147
+ """
72
148
 
73
149
 
74
150
  def _build_computer_use_section(cfg: GemCodeConfig) -> str:
@@ -378,10 +454,13 @@ For tasks where quality matters:
378
454
  ## Workspace scope
379
455
  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`."""
380
456
 
381
- # Inject computer use strategy when the browser is enabled.
457
+ # Inject capability-specific strategy sections only when those caps are on.
382
458
  if getattr(cfg, "enable_computer_use", False):
383
459
  base = f"{base}\n\n{_build_computer_use_section(cfg)}"
384
460
 
461
+ if getattr(cfg, "enable_memory", False):
462
+ base = f"{base}\n\n{_build_memory_section(cfg)}"
463
+
385
464
  tool_manifest = build_tool_manifest(cfg)
386
465
  if tool_manifest:
387
466
  base = f"{base}\n\n{tool_manifest}"
@@ -409,7 +488,7 @@ def build_root_agent(
409
488
  if _tools is not None:
410
489
  tools = list(_tools)
411
490
  else:
412
- tools = build_function_tools(cfg)
491
+ tools = build_function_tools(cfg)
413
492
  if getattr(cfg, "enable_memory", False):
414
493
  # ADK preload_memory injects retrieved memories into the next llm_request.
415
494
  from google.adk.tools import preload_memory
gemcode/repl_commands.py CHANGED
@@ -215,4 +215,5 @@ def slash_help_lines() -> list[str]:
215
215
  " Other:",
216
216
  " /permissions Show permission / HITL settings",
217
217
  " /hooks Show post-turn hook configuration",
218
+ " /kairos How to launch the background parallel job scheduler",
218
219
  ]
gemcode/repl_slash.py CHANGED
@@ -318,6 +318,45 @@ async def process_repl_slash(
318
318
  if name in ("exit", "quit"):
319
319
  return ReplSlashResult(exit_repl=True)
320
320
 
321
+ # ── /kairos ───────────────────────────────────────────────────────────────
322
+ if name == "kairos":
323
+ args_s = (sc.args or "").strip()
324
+ out("Kairos — background parallel job scheduler")
325
+ out()
326
+ out("What it does:")
327
+ out(" Runs a long-lived daemon that accepts prompts on stdin and executes")
328
+ out(" each as an isolated GemCode agent job. Jobs run concurrently (up to")
329
+ out(" --concurrency N, default 2) in a priority queue.")
330
+ out()
331
+ out("How to launch (in a separate terminal):")
332
+ out(f" gemcode kairos -C {cfg.project_root}")
333
+ out(" gemcode kairos -C <project> --concurrency 4 --yes")
334
+ out(" gemcode kairos -C <project> --model gemini-2.5-pro --deep-research")
335
+ out()
336
+ out("Options:")
337
+ out(" --concurrency N Max parallel jobs (default: 2)")
338
+ out(" --default-priority N Priority for stdin jobs (higher = runs first, default: 0)")
339
+ out(" --yes Auto-approve mutations (like gemcode --yes)")
340
+ out(" --model <id> Override model for all jobs")
341
+ out(" --model-mode <mode> fast|balanced|quality|auto")
342
+ out(" --deep-research Enable Google Search + URL Context for all jobs")
343
+ out(" --embeddings Enable semantic file search for all jobs")
344
+ out(" --max-llm-calls N Cap model↔tool iterations per job")
345
+ out(" --session <uuid> Share session history with a running gemcode session")
346
+ out()
347
+ out("Tools available to jobs (in addition to normal GemCode tools):")
348
+ out(" kairos_sleep_ms(duration_ms) — pause this job without blocking others")
349
+ out(" kairos_enqueue_prompt(prompt, priority, session_id)")
350
+ out(" — the model can queue MORE jobs itself")
351
+ out()
352
+ out("Use cases:")
353
+ out(" - Process N files in parallel: each file → one job")
354
+ out(" - Polling loop: job sleeps, re-enqueues itself with kairos_enqueue_prompt")
355
+ out(" - Bulk code generation, test runs, or research across many documents")
356
+ out(" - Background work while you continue chatting in this session")
357
+ out()
358
+ return ReplSlashResult(skip_model_turn=True)
359
+
321
360
  # ── /computer ────────────────────────────────────────────────────────────
322
361
  if name in ("computer", "browser"):
323
362
  args_s = (sc.args or "").strip().lower()
@@ -49,7 +49,7 @@ def build_tool_manifest(cfg: GemCodeConfig) -> str | None:
49
49
  if not enabled:
50
50
  return None
51
51
 
52
- max_chars = int(os.environ.get("GEMCODE_TOOL_SYSTEM_PROMPT_MAX_CHARS", "3200"))
52
+ max_chars = int(os.environ.get("GEMCODE_TOOL_SYSTEM_PROMPT_MAX_CHARS", "6000"))
53
53
 
54
54
  permission_mode = getattr(cfg, "permission_mode", "default")
55
55
  yes_to_all = bool(getattr(cfg, "yes_to_all", False))
@@ -95,47 +95,70 @@ def build_tool_manifest(cfg: GemCodeConfig) -> str | None:
95
95
  " If neither --yes nor inline HITL is available, ask the user to re-run with `--yes` when mutations are required."
96
96
  )
97
97
 
98
+ memory_on = bool(getattr(cfg, "enable_memory", False))
99
+
98
100
  manifest = f"""## Tool system (GemCode)
99
101
 
100
- Model behavior:
101
- - Issue **multiple tool calls in one assistant step** when calls are independent (e.g. several `read_file`/`glob_files`/`grep_content` in parallel). Use a single tool call when the next action depends on the previous result.
102
- - **Reason end-to-end:** you decide which tools to call and in what order—the user expects autonomous execution within policy, not a questionnaire.
103
-
104
- Permission policy:
105
- - permission_mode={permission_mode}
106
- - user_confirmation_provided(--yes)={yes_to_all}
107
- - user_in_run_hitl_prompt_enabled={interactive_ask_on}
108
- - session_sticky_hitl={sticky_hitl}: when true, after the user approves **one** tool in this session, further mutating/shell tools may run without re-prompting (set GEMCODE_HITL_STICKY_SESSION=0 to prompt every time).
109
-
110
- You may call tools as follows:
111
- - Session planning / reasoning: {_fmt_list(planning)}.
112
- - `todo_write`: in-memory task list (no disk writes). Use for non-trivial multi-step work; merge=true to upsert by id.
113
- - `think`: private scratchpad — write reasoning/analysis before risky or complex actions. Not shown to user.
114
- - `run_subtask(task, context)`: spawn an isolated sub-agent to handle context-heavy work in parallel or in sequence. Sub-agent inherits your permissions and returns its final text as `result`. Exclude run_subtask from sub-agent by design (no recursion).
115
- - Read-only tools: {_fmt_list(read_only)}.
116
- Use these proactively to locate files and code before asking the user for paths.
117
- - Mutating tools (WRITE/EDIT/DELETE): {_fmt_list(mutating)}.
118
- Only call if user_confirmation_provided(--yes) is true OR user_in_run_hitl_prompt_enabled is true.{mutating_policy_extra}
119
- - Shell tool (run_command): {_fmt_list(shell)}.
120
- Prefer allowlisted commands (GEMCODE_ALLOW_COMMANDS). Allowed={allow_cmds_str or "<none>"}.
121
- If the user approves a run_command in the session prompt, that specific invocation may run even when the executable is not on the default list.
122
- Only call if user_confirmation_provided(--yes) is true OR user_in_run_hitl_prompt_enabled is true.
123
- Notes:
124
- - Prefer `python -m pip ...` (or `python3 -m pip ...`) so installs stay in the active virtualenv.
125
- - Do not assume sudo/system package manager access.
126
- - `run_command` supports `cwd_subdir` (relative path under the project) instead of `cd`/`bash`; use parallel `extra_env_keys` / `extra_env_values` (e.g. ["CI"] and ["1"]) for non-interactive installers; use `background=true` for long-running dev servers.
127
-
128
- Optional capability tools:
129
- - Deep research built-ins are {'ON' if deep_research_on else 'OFF'}.
130
- Active built-ins: google_search, url_context{', google_maps' if deep_research_on and maps_grounding_on else ''}.
131
- {gemini3_combination_contract}
132
- - Embeddings semantic retrieval is {'ON' if embeddings_on else 'OFF'}:
133
- semantic_search_files.
134
- - Computer use is {'ON' if computer_on else 'OFF'}:
135
- browser automation actions via Computer Use toolset.
136
- Only call if permission_mode != strict AND (user_confirmation_provided(--yes) is true OR user_in_run_hitl_prompt_enabled is true).
137
-
138
- If a tool call is rejected by policy, do NOT retry the same mutation without the required user confirmation.
102
+ ### Execution model
103
+ - Issue **multiple independent tool calls in one step** (parallel reads, parallel grep, parallel run_subtask). Use sequential calls only when step B needs step A's result.
104
+ - **Reason end-to-end autonomously.** The user expects complete tasks, not a questionnaire. Use `think` before complex actions, `todo_write` to track multi-step work.
105
+ - **Never stop after the first tool call succeeds.** Keep going until the full task is done or you hit a genuine blocker.
106
+
107
+ ### Permission policy
108
+ | Setting | Value |
109
+ |---------|-------|
110
+ | permission_mode | {permission_mode} |
111
+ | --yes (auto-approve mutations) | {yes_to_all} |
112
+ | interactive HITL prompt | {interactive_ask_on} |
113
+ | sticky HITL (approve once → whole session) | {sticky_hitl} |
114
+
115
+ {mutating_policy_extra.strip()}
116
+
117
+ ### Planning & reasoning tools: {_fmt_list(planning)}
118
+ - **`todo_write(todos, merge)`** in-memory task tracker.
119
+ - `todos`: list of objects with `id` (str), `content` (str), `status` (one of: `pending`, `in_progress`, `completed`, `cancelled`).
120
+ - `merge=false` (default): replace the entire list. `merge=true`: upsert by id (update existing, add new, leave others unchanged).
121
+ - Workflow: create with status=pending at task start → update to in_progress when you begin → completed when done.
122
+ - Use for ANY task with 3+ distinct steps.
123
+ - **`think(thought)`** private reasoning scratchpad. Not shown to user. Use before: complex refactors, debugging hypotheses, destructive actions, architecture decisions.
124
+ - **`run_subtask(task, context)`** spawn an isolated sub-agent with a fresh context window.
125
+ - Inherits your permissions and tool set (except run_subtask itself — no recursion).
126
+ - Returns the sub-agent's final text as `result`.
127
+ - Use for: context-heavy exploration (reading 50+ files), parallel investigation of independent subsystems, verification passes after you finish work.
128
+ - Always give the sub-agent enough context to work independently; end the task with "Summarise your findings clearly."
129
+
130
+ ### Read-only tools (no permission needed): {_fmt_list(read_only)}
131
+ Use proactively. Never use bash/run_command just to list or read files — these are instant and require zero approval.
132
+
133
+ ### Mutating tools (WRITE/EDIT/DELETE): {_fmt_list(mutating)}
134
+ Require: `--yes` OR inline HITL approval.
135
+ - `write_file(path, content)` — create or overwrite. Always `read_file` first if the file exists.
136
+ - `search_replace(path, old_string, new_string)` targeted in-place edit. `old_string` must be unique in the file; include 3+ lines of context.
137
+ - `delete_file(path)`, `move_file(src, dest)` destructive; think before calling.
138
+
139
+ ### Shell tools: {_fmt_list(shell)}
140
+ Require: `--yes` OR inline HITL approval. Allowed commands: {allow_cmds_str or "(default allowlist)"}.
141
+ - `bash(command, timeout_seconds, cwd_subdir, background)` — full shell with pipes, redirects.
142
+ - `background=true` for dev servers, watchers, long builds.
143
+ - Use `2>&1 | tail -N` to cap verbose output.
144
+ - `run_command(command, args, cwd_subdir, background, extra_env_keys, extra_env_values)` — single-executable without shell features.
145
+ - Use `extra_env_keys`/`extra_env_values` for non-interactive environment injection.
146
+ - Prefer `python -m pip` over `pip` to stay in the active virtualenv.
147
+
148
+ ### Optional capability tools
149
+ | Capability | Status | Tools available |
150
+ |-----------|--------|----------------|
151
+ | Deep research | {'**ON**' if deep_research_on else 'off'} | {('google_search, url_context' + (', google_maps_grounding' if maps_grounding_on else '')) if deep_research_on else '—'} |
152
+ | Embeddings / semantic search | {'**ON**' if embeddings_on else 'off'} | {'semantic_search_files' if embeddings_on else '—'} |
153
+ | Persistent memory | {'**ON**' if memory_on else 'off'} | {'preload_memory (auto-injected), memory loaded from .gemcode/memories.jsonl' if memory_on else '— (enable with /memory on)'} |
154
+ | Browser / computer use | {'**ON**' if computer_on else 'off'} | {'navigate, click_at, type_text_at, browser_screenshot, browser_find_element, scroll_at, key_combination, ...' if computer_on else '— (enable with /computer on)'} |
155
+
156
+ {gemini3_combination_contract}
157
+
158
+ Enable any capability mid-session with: `/research on`, `/embeddings on`, `/memory on`, `/computer on`.
159
+
160
+ ### Policy
161
+ If a tool call is rejected, do NOT retry the same mutation without the required user confirmation. Adjust your plan and report clearly.
139
162
  """
140
163
 
141
164
  return _cap(manifest, max_chars=max_chars)
gemcode/tools/__init__.py CHANGED
@@ -14,6 +14,22 @@ from gemcode.tools.todo import make_todo_tool
14
14
  from gemcode.tools.web import make_web_fetch_tool
15
15
 
16
16
 
17
+ def _wrap_long_running(fn):
18
+ """
19
+ Wrap a function tool with ADK's LongRunningFunctionTool so that long-running
20
+ operations (npm install, cargo build, pytest, etc.) can run beyond the normal
21
+ streaming timeout and yield intermediate updates.
22
+
23
+ Falls back gracefully to the plain function if google-adk does not support
24
+ LongRunningFunctionTool in the installed version.
25
+ """
26
+ try:
27
+ from google.adk.tools import LongRunningFunctionTool
28
+ return LongRunningFunctionTool(fn)
29
+ except Exception:
30
+ return fn
31
+
32
+
17
33
  def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) -> list:
18
34
  read_file, list_directory, glob_files, delete_file, move_file = make_filesystem_tools(cfg)
19
35
  grep_content = make_grep_tool(cfg)
@@ -24,6 +40,12 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
24
40
  think = make_think_tool()
25
41
  web_fetch = make_web_fetch_tool()
26
42
 
43
+ # bash and run_command are the most common long-running tools (builds, tests,
44
+ # installs). Wrap them with LongRunningFunctionTool so ADK can handle slow
45
+ # processes without hitting streaming timeouts.
46
+ bash_tool = _wrap_long_running(bash)
47
+ run_command_tool = _wrap_long_running(run_command)
48
+
27
49
  tools = [
28
50
  todo_write,
29
51
  think,
@@ -31,8 +53,8 @@ def build_function_tools(cfg: GemCodeConfig, *, include_subtask: bool = True) ->
31
53
  list_directory,
32
54
  glob_files,
33
55
  grep_content,
34
- bash,
35
- run_command,
56
+ bash_tool,
57
+ run_command_tool,
36
58
  write_file,
37
59
  search_replace,
38
60
  move_file,
@@ -55,6 +55,7 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
55
55
  ("memory", "Toggle persistent memory · /memory on|off"),
56
56
  ("budget", "Set per-turn token budget · /budget <N> · /budget off"),
57
57
  ("limits", "Show/set execution limits (max_llm_calls, context, etc.)"),
58
+ ("kairos", "Background parallel job scheduler — how to run gemcode kairos"),
58
59
  ("tools", "List all tools and their permission categories"),
59
60
  ("config", "Show full active configuration (all fields)"),
60
61
  ("permissions", "Show current permission mode (default / strict / yes)"),
gemcode/tui/scrollback.py CHANGED
@@ -252,15 +252,26 @@ async def run_gemcode_scrollback_tui(
252
252
  REQUEST_CONFIRMATION_FC = _ADK_REQUEST_CONFIRMATION
253
253
 
254
254
  def _get_confirmation_fcs(events: list) -> list[types.FunctionCall]:
255
- out: list[types.FunctionCall] = []
256
- for ev in events:
255
+ """Return confirmation FCs from the LAST event that contains them.
256
+
257
+ The ADK runner expects function responses only for the function calls
258
+ in the most recent event. Sending responses for FCs from earlier events
259
+ in the same batch raises:
260
+ ValueError: Last response event should only contain the responses for
261
+ the function calls in the same function call event.
262
+ So we scan backwards and return only the first (= last) match.
263
+ """
264
+ for ev in reversed(events):
257
265
  try:
258
- for fc in ev.get_function_calls() or []:
259
- if getattr(fc, "name", None) == _ADK_REQUEST_CONFIRMATION:
260
- out.append(fc)
266
+ fcs = [
267
+ fc for fc in (ev.get_function_calls() or [])
268
+ if getattr(fc, "name", None) == _ADK_REQUEST_CONFIRMATION
269
+ ]
270
+ if fcs:
271
+ return fcs
261
272
  except Exception:
262
273
  continue
263
- return out
274
+ return []
264
275
 
265
276
  def _extract_tool_and_hint(fc: types.FunctionCall) -> tuple[str, str]:
266
277
  tool_name = "unknown_tool"
@@ -527,26 +538,40 @@ async def run_gemcode_scrollback_tui(
527
538
  # as different phases of the turn complete.
528
539
  _start_anim("Thinking\u2026")
529
540
 
530
- async for ev in runner.run_async(**kwargs):
531
- events.append(ev)
532
- _render_tool_calls(ev)
533
- _render_tool_results(ev)
534
- try:
535
- if not ev.content or not ev.content.parts:
536
- continue
537
- if not getattr(ev, "author", None) or ev.author == "user":
538
- continue
539
- for part in ev.content.parts:
540
- delta = getattr(part, "text", None)
541
- if not delta:
541
+ try:
542
+ async for ev in runner.run_async(**kwargs):
543
+ events.append(ev)
544
+ _render_tool_calls(ev)
545
+ _render_tool_results(ev)
546
+ try:
547
+ if not ev.content or not ev.content.parts:
542
548
  continue
543
- assistant_wrote_text = True
544
- if getattr(part, "thought", None):
545
- buffered_thought.append(delta)
546
- else:
547
- buffered_final.append(delta)
548
- except Exception:
549
- continue
549
+ if not getattr(ev, "author", None) or ev.author == "user":
550
+ continue
551
+ for part in ev.content.parts:
552
+ delta = getattr(part, "text", None)
553
+ if not delta:
554
+ continue
555
+ assistant_wrote_text = True
556
+ if getattr(part, "thought", None):
557
+ buffered_thought.append(delta)
558
+ else:
559
+ buffered_final.append(delta)
560
+ except Exception:
561
+ continue
562
+ except Exception as _turn_err:
563
+ # Catch runner errors (e.g. ADK ValueError from mismatched function
564
+ # response IDs) so a single bad turn doesn't crash the whole TUI.
565
+ _stop_anim()
566
+ print(
567
+ f"\n {ansi.blue_warn}[gemcode] turn error: "
568
+ f"{type(_turn_err).__name__}: {_turn_err}{ansi.reset}"
569
+ )
570
+ print(
571
+ f" {ansi.dim}The agent encountered an internal error on this turn. "
572
+ f"Please send your message again.{ansi.reset}\n"
573
+ )
574
+ break
550
575
 
551
576
  _stop_anim() # ensure spinner is gone before printing the response
552
577
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.26
3
+ Version: 0.3.28
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -1,6 +1,6 @@
1
1
  gemcode/__init__.py,sha256=l0DCRYqK7KM7Fb7u49fqh-5_SlpeIL7r3LjMeJWMgSg,112
2
2
  gemcode/__main__.py,sha256=EX2s1hxq2Yvli_-tnBN3w5Qv4bOjsBBbjyISF0pDIQw,37
3
- gemcode/agent.py,sha256=_mQDjQ_f1W8Frw1AhvvVefoyO73EEBWsFxgWQ7XT-5o,24598
3
+ gemcode/agent.py,sha256=etP0evf7_QSzxY9RqTvc3peVJXh6tcwBmcGqqfb2VDQ,29990
4
4
  gemcode/audit.py,sha256=bh9uhXaeh8wqxqoZtz3ZAowd8Ndk1ss-mw9993Vlrgo,469
5
5
  gemcode/autocompact.py,sha256=77h5tgFzJ2rjrhlCL2oIc28IHwLbP4Pqlo7cSNgDwiA,6727
6
6
  gemcode/callbacks.py,sha256=dxZhHsJDE-16nJ0OGe4udPkWRTOCVfSi2gBSR7MUWOM,20465
@@ -25,12 +25,12 @@ gemcode/model_routing.py,sha256=Q42HZtXQa6rao2O2vYMHxohrTgD-wq4t7qGxU4_38Jg,4881
25
25
  gemcode/paths.py,sha256=U6cEH9jfIcSc4NO8Ke0jniZSiJTfCIJPvSMue3hR0ZU,768
26
26
  gemcode/permissions.py,sha256=9FmwTP_LLflahxS9KXvY3VPSra4e-h3OariOqnWIKDI,177
27
27
  gemcode/prompt_suggestions.py,sha256=h-W_9LlfagS91PyoMEjEjsCqoG4XmIh3QBypA59HyGw,2553
28
- gemcode/repl_commands.py,sha256=6R8_MKNcPmUQgythIRGMRzXk5Z7WxnQihACX4TecNww,8185
29
- gemcode/repl_slash.py,sha256=l_xi9Ysjp0JScNOpXCquskJ0JHQNGt2q6CHS2VbPrik,29233
28
+ gemcode/repl_commands.py,sha256=yYd1aXaY9wZzoiWNMlVhtFPK1KO2_tX-FUTTXrfHuhE,8270
29
+ gemcode/repl_slash.py,sha256=epkMgUcjfY6u4XOpr_oGoN3UDIFTyhsGSmHvl46s590,31498
30
30
  gemcode/session_runtime.py,sha256=iasFwXJmrqbutNnZmSCbWmDerWiBf32_eloi3SKKu6c,4081
31
31
  gemcode/slash_commands.py,sha256=Qylzsj1notk0xN_hvd3CR4HD8g-l99UENDMcg1pKeBA,794
32
32
  gemcode/thinking.py,sha256=RanBf_x9fKv1o4DNyNXPLfOdn2xT0KybJb65nYgmMEE,4885
33
- gemcode/tool_prompt_manifest.py,sha256=S1Q3p4GaOqywGInDDrwe7_pQH_1KmjcF9vidg8HLj-E,6655
33
+ gemcode/tool_prompt_manifest.py,sha256=6Fy5vrzgMX-0SIOeenXe8I41RmKPwIoz0qMp9KcLDRM,7976
34
34
  gemcode/tool_registry.py,sha256=ifqxtr2uLwEUwnJLKYLza_tIz-paaZediJa75y9MiyA,1795
35
35
  gemcode/tools_inspector.py,sha256=okmu4PDYAQQ7nthDvuzSHmy2zArFTG4ftIPRadzLnxA,4100
36
36
  gemcode/trust.py,sha256=fxe57Xg6aL_KU24bQDUtD-rXjsNpaq7g-eQTInZnudE,1336
@@ -52,7 +52,7 @@ gemcode/query/engine.py,sha256=GPuvUgTRpWmyA39I_3ayVEADlHVFhPrC2FGW_yKs2yw,1420
52
52
  gemcode/query/stop_hooks.py,sha256=jaXMN2OptwHeGXF8o630zIVr62T8jVg-eIyREG0GyxA,1847
53
53
  gemcode/query/token_budget.py,sha256=JTq2TrGkFY5t5KOs-P9XQPqahyjcdTzN3wctZ1JxFV0,2973
54
54
  gemcode/query/transitions.py,sha256=vJ77cv4cFwdvsxyGr7MxXz6uGVB5IDqOClqR1MkWvqw,933
55
- gemcode/tools/__init__.py,sha256=wFJMozUoikxdnwcZnry49-RxCU-nMgm0zA0v4mE-ooc,1396
55
+ gemcode/tools/__init__.py,sha256=jea95Ds3BwUaiDNVGesBXDKvveIHbdTNpk4yKVWcPVA,2226
56
56
  gemcode/tools/bash.py,sha256=n0BH-6mGl0MbklCzgjm6bPh14XO7zR0qlqVx_SM0ZyI,4547
57
57
  gemcode/tools/browser.py,sha256=StWRttiyGkR4qaG5urRviJgdoj2hiFB2OuHPItaVzJY,7250
58
58
  gemcode/tools/edit.py,sha256=Q6ALUCHQV37n0j6XQd2luCaDm0fIavho3faDisOqtuU,1716
@@ -64,17 +64,17 @@ gemcode/tools/subtask.py,sha256=ur_6jnwnNzHSBtW96mB2N2A6zmALpydSV24Nkx0T6UQ,5683
64
64
  gemcode/tools/think.py,sha256=WrNATR-bi97aLkbSsOFOYYAGxbzihe9AnPDZfw3z5-Q,1704
65
65
  gemcode/tools/todo.py,sha256=d9aXiyT04r1RFZIk6qdVif17-_Oc3oi4ymDnsPBRg68,3143
66
66
  gemcode/tools/web.py,sha256=ULg1e3inG4FjPSUCYI8dVBzTrcCHINNRo76SIU9qw-A,4489
67
- gemcode/tui/input_handler.py,sha256=rCSzV5Ucc-nZTBbY8KKTDS9einNrgMzvI_NGcMls9e8,8147
68
- gemcode/tui/scrollback.py,sha256=f14MSfDM4dryDWWG3vfTuzkV4mJfFbz2Yd7gF9S4pXs,23455
67
+ gemcode/tui/input_handler.py,sha256=nQEltM2U0Ks2GW_8KYyoww-S7x7Xjspig59r6yi_Pyo,8235
68
+ gemcode/tui/scrollback.py,sha256=zc91bA1jPQdluwy5eUFX3-lElYhaaSBIyOKQ0bj-zbA,24521
69
69
  gemcode/tui/spinner.py,sha256=AJrApG5od-Sh40-5uWcNM9RHb5ax7gr-NbgAZmTbIYY,4848
70
70
  gemcode/tui/welcome_banner.py,sha256=aocl1lnoyLIM6RN4f65g3i0wRA71RqUlgPrGsXeVLW4,4387
71
71
  gemcode/tui/welcome_rich.py,sha256=8FEZzLXrzqly5JWiDgV9ooRV1LNXDk-CXV1a7K6ua-U,4048
72
72
  gemcode/web/__init__.py,sha256=EysmUAWs6g-lmMk4VFljKfaHVrEgb_FiIzwQmBdORJc,40
73
73
  gemcode/web/claude_sse_adapter.py,sha256=HcNp0Lh4DdBZBLOpstsqa-VzfqAUrRngZ6FSuJ-mIMg,8609
74
74
  gemcode/web/terminal_repl.py,sha256=k2irvFGbCY8gDm_pbirR7b_cakaeafcctoTIvnJkVXk,3902
75
- gemcode-0.3.26.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
76
- gemcode-0.3.26.dist-info/METADATA,sha256=gvuLPIdccTr2APbK1N6Zjp9MwO9Uw7k6sS6C71ckrXU,23695
77
- gemcode-0.3.26.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
78
- gemcode-0.3.26.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
79
- gemcode-0.3.26.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
80
- gemcode-0.3.26.dist-info/RECORD,,
75
+ gemcode-0.3.28.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
76
+ gemcode-0.3.28.dist-info/METADATA,sha256=iwoh3k_b5G6pb68A58SBvkcfcobd9xok_br31HvZlT0,23695
77
+ gemcode-0.3.28.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
78
+ gemcode-0.3.28.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
79
+ gemcode-0.3.28.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
80
+ gemcode-0.3.28.dist-info/RECORD,,