gemcode 0.3.107__tar.gz → 0.3.109__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.
Files changed (165) hide show
  1. {gemcode-0.3.107/src/gemcode.egg-info → gemcode-0.3.109}/PKG-INFO +1 -1
  2. {gemcode-0.3.107 → gemcode-0.3.109}/pyproject.toml +1 -1
  3. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/agent.py +83 -0
  4. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/invoke.py +4 -9
  5. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/model_errors.py +18 -0
  6. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/repl_commands.py +3 -0
  7. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/repl_slash.py +91 -0
  8. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/session_runtime.py +57 -0
  9. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tool_prompt_manifest.py +26 -0
  10. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tool_registry.py +3 -1
  11. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tui/scrollback.py +59 -28
  12. {gemcode-0.3.107 → gemcode-0.3.109/src/gemcode.egg-info}/PKG-INFO +1 -1
  13. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode.egg-info/SOURCES.txt +1 -0
  14. gemcode-0.3.109/tests/test_agent_instruction.py +76 -0
  15. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_model_errors.py +17 -1
  16. gemcode-0.3.109/tests/test_session_runtime_cache.py +39 -0
  17. gemcode-0.3.107/tests/test_agent_instruction.py +0 -22
  18. {gemcode-0.3.107 → gemcode-0.3.109}/LICENSE +0 -0
  19. {gemcode-0.3.107 → gemcode-0.3.109}/MANIFEST.in +0 -0
  20. {gemcode-0.3.107 → gemcode-0.3.109}/README.md +0 -0
  21. {gemcode-0.3.107 → gemcode-0.3.109}/setup.cfg +0 -0
  22. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/__init__.py +0 -0
  23. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/__main__.py +0 -0
  24. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/audit.py +0 -0
  25. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/autocompact.py +0 -0
  26. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/autotune.py +0 -0
  27. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/callbacks.py +0 -0
  28. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/capability_routing.py +0 -0
  29. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/checkpoints.py +0 -0
  30. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/cli.py +0 -0
  31. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/compaction.py +0 -0
  32. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/computer_use/__init__.py +0 -0
  33. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/computer_use/browser_computer.py +0 -0
  34. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/config.py +0 -0
  35. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/context_budget.py +0 -0
  36. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/context_warning.py +0 -0
  37. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/credentials.py +0 -0
  38. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/curated_memory.py +0 -0
  39. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/dynamic_policy.py +0 -0
  40. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/evals/harness.py +0 -0
  41. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/hitl_session.py +0 -0
  42. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/hooks.py +0 -0
  43. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/ide_protocol.py +0 -0
  44. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/ide_stdio.py +0 -0
  45. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/intent_classifier.py +0 -0
  46. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/interactions.py +0 -0
  47. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/kaira_client.py +0 -0
  48. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/kaira_daemon.py +0 -0
  49. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/kaira_ipc.py +0 -0
  50. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/kaira_job_store.py +0 -0
  51. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/learning.py +0 -0
  52. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/limits.py +0 -0
  53. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/live_audio_engine.py +0 -0
  54. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/logging_config.py +0 -0
  55. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/mcp_loader.py +0 -0
  56. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/memory/__init__.py +0 -0
  57. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/memory/embedding_memory_service.py +0 -0
  58. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/memory/file_memory_service.py +0 -0
  59. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/modality_tools.py +0 -0
  60. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/model_routing.py +0 -0
  61. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/multimodal_input.py +0 -0
  62. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/openapi_loader.py +0 -0
  63. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/org.py +0 -0
  64. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/output_styles.py +0 -0
  65. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/paths.py +0 -0
  66. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/permissions.py +0 -0
  67. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/plugins/__init__.py +0 -0
  68. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  69. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  70. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/policy_profile.py +0 -0
  71. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/pricing.py +0 -0
  72. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/prompt_suggestions.py +0 -0
  73. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/query/__init__.py +0 -0
  74. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/query/config.py +0 -0
  75. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/query/deps.py +0 -0
  76. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/query/engine.py +0 -0
  77. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/query/stop_hooks.py +0 -0
  78. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/query/token_budget.py +0 -0
  79. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/query/transitions.py +0 -0
  80. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/query_sanitizer.py +0 -0
  81. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/refine.py +0 -0
  82. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/review_agent.py +0 -0
  83. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/rules.py +0 -0
  84. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/session_store.py +0 -0
  85. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/session_summariser.py +0 -0
  86. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/skills.py +0 -0
  87. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/slash_commands.py +0 -0
  88. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/thinking.py +0 -0
  89. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tool_result_store.py +0 -0
  90. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/__init__.py +0 -0
  91. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/bash.py +0 -0
  92. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/browser.py +0 -0
  93. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/compress_memory.py +0 -0
  94. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/curated_memory.py +0 -0
  95. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/edit.py +0 -0
  96. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/filesystem.py +0 -0
  97. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/notebook.py +0 -0
  98. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/notes.py +0 -0
  99. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/org_tools.py +0 -0
  100. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/repo_map.py +0 -0
  101. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/search.py +0 -0
  102. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/shell.py +0 -0
  103. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/shell_gate.py +0 -0
  104. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/skills.py +0 -0
  105. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/subtask.py +0 -0
  106. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/tasks.py +0 -0
  107. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/think.py +0 -0
  108. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/todo.py +0 -0
  109. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/user_choice.py +0 -0
  110. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/veomem_tools.py +0 -0
  111. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/web.py +0 -0
  112. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools/web_search.py +0 -0
  113. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tools_inspector.py +0 -0
  114. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/trust.py +0 -0
  115. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tui/input_handler.py +0 -0
  116. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tui/spinner.py +0 -0
  117. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tui/welcome_banner.py +0 -0
  118. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/tui/welcome_rich.py +0 -0
  119. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/veomem_bridge.py +0 -0
  120. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/version.py +0 -0
  121. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/vertex.py +0 -0
  122. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/wal.py +0 -0
  123. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/web/__init__.py +0 -0
  124. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/web/sse_adapter.py +0 -0
  125. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/web/terminal_repl.py +0 -0
  126. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/web/web_sse_compat.py +0 -0
  127. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode/workspace_hints.py +0 -0
  128. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode.egg-info/dependency_links.txt +0 -0
  129. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode.egg-info/entry_points.txt +0 -0
  130. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode.egg-info/requires.txt +0 -0
  131. {gemcode-0.3.107 → gemcode-0.3.109}/src/gemcode.egg-info/top_level.txt +0 -0
  132. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_add_dir.py +0 -0
  133. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_autocompact.py +0 -0
  134. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_capability_routing.py +0 -0
  135. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_checkpoint_diff_command.py +0 -0
  136. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_cli_init.py +0 -0
  137. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_compress_memory_tool.py +0 -0
  138. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_computer_use_permissions.py +0 -0
  139. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_context_budget.py +0 -0
  140. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_context_warning.py +0 -0
  141. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_credentials.py +0 -0
  142. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_eval_harness_layout.py +0 -0
  143. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_ide_stdio_attachments.py +0 -0
  144. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_interactive_permission_ask.py +0 -0
  145. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_kaira_scheduler.py +0 -0
  146. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_modality_tools.py +0 -0
  147. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_model_error_retry.py +0 -0
  148. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_model_routing.py +0 -0
  149. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_multimodal_input.py +0 -0
  150. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_output_styles_and_rules.py +0 -0
  151. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_paths.py +0 -0
  152. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_permissions.py +0 -0
  153. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_prompt_suggestions.py +0 -0
  154. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_repl_commands.py +0 -0
  155. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_repl_slash.py +0 -0
  156. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_skills.py +0 -0
  157. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_slash_commands.py +0 -0
  158. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_slash_completion_registry.py +0 -0
  159. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_thinking_config.py +0 -0
  160. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_token_budget.py +0 -0
  161. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_tool_context_circulation.py +0 -0
  162. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_tools.py +0 -0
  163. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_tools_inspector.py +0 -0
  164. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_web_sse_adapter.py +0 -0
  165. {gemcode-0.3.107 → gemcode-0.3.109}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.107
3
+ Version: 0.3.109
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.107"
7
+ version = "0.3.109"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -76,6 +76,8 @@ def build_global_instruction() -> str:
76
76
  "You are GemCode, an expert software engineering agent powered by Google Gemini. "
77
77
  "Think deeply about what the person actually wants before you do anything. "
78
78
  "Use exactly as many tools as the task genuinely requires — no more. "
79
+ "When routing or capabilities change between turns, still prefer minimal tools, "
80
+ "repo-grounded evidence, and verification before claiming done. "
79
81
  "Act fully and autonomously when action is needed. "
80
82
  "Always use read-only tools before shell or write tools. "
81
83
  "Never create CLAUDE.md or AGENTS.md; use GEMINI.md for project instructions."
@@ -353,6 +355,79 @@ def _build_runtime_facts(cfg: GemCodeConfig) -> str:
353
355
  - **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}{veomem_section}"""
354
356
 
355
357
 
358
+ def _build_calibration_section(cfg: GemCodeConfig) -> str:
359
+ """
360
+ Meta-instruction for "smart" behavior under dynamic routing (auto model/capability mode,
361
+ orchestration tools, etc.). Kept compact to limit prompt bloat.
362
+ """
363
+ mm = (getattr(cfg, "model_mode", "") or "").strip().lower()
364
+ cm = (getattr(cfg, "capability_mode", "") or "").strip().lower()
365
+ bits: list[str] = []
366
+ if mm == "auto":
367
+ bits.append("`model_mode=auto` (model may shift per turn for speed vs depth)")
368
+ if cm == "auto":
369
+ bits.append("`capability_mode=auto` (deep research / extras may attach per turn)")
370
+ session_note = ""
371
+ if bits:
372
+ session_note = (
373
+ "\n**This session uses dynamic routing:** "
374
+ + " · ".join(bits)
375
+ + ". Defaults are **hints** — override with judgment when the task clearly needs more or less depth.\n"
376
+ )
377
+
378
+ return f"""## Calibration and dynamic routing (all modes)
379
+
380
+ Infer **intent → depth → tools** without fixed buckets (requests vary):
381
+
382
+ | Stance | Move |
383
+ | --- | --- |
384
+ | **Explain / review** | Read-only recon first; cite **paths**; avoid unrelated edits. |
385
+ | **Implement / fix** | Recon → plan (`todo_write` when 3+ steps) → smallest change → **verify** (tests, lint, build slice, or read-back). |
386
+ | **Debug** | One hypothesis per iteration; change one variable between tries; never repeat the same failing command verbatim. |
387
+ | **External facts** | Use web/research tools when the answer is outside the repo (docs, APIs, CVEs). Prefer **repo files** for how *this* codebase behaves. |
388
+
389
+ **Evidence:** tie non-obvious claims about this workspace to **files or command output** you actually saw.
390
+
391
+ **Orchestration:** use `spawn_subtasks`, org delegation, or background jobs only when work is **parallel** or **role-split**. Merge into **one** answer with a single recommendation; skip fan-out for small linear tasks.
392
+
393
+ **Anti-patterns:** tool spam; “done” without verification on risky edits; searching the web when the source file is already in-tree; repeating identical failures.{session_note}"""
394
+
395
+
396
+ def _engineering_discipline_instruction_enabled() -> bool:
397
+ """Extra prompt section: cautious change quality. Opt out with GEMCODE_ENGINEERING_DISCIPLINE=0."""
398
+ import os
399
+
400
+ v = os.environ.get("GEMCODE_ENGINEERING_DISCIPLINE", "1").strip().lower()
401
+ return v not in ("0", "false", "no", "off")
402
+
403
+
404
+ def _build_engineering_discipline_section(cfg: GemCodeConfig) -> str:
405
+ """Prompt block for minimal, evidence-grounded edits; trivial fixes need not dwell on it."""
406
+ _ = cfg # reserved for future project-scoped tuning
407
+ return """## Engineering discipline (change quality)
408
+
409
+ **Tradeoff:** biases toward careful, minimal diffs over speed. For trivial edits, use judgment.
410
+
411
+ ### Ambiguity
412
+ - Briefly state **what you understood** before substantial implementation. If several readings fit, outline them or ask **one** precise question — do not silently pick and run.
413
+ - Prefer **evidence from this repo** (reads, grep, tests) over guessed APIs, paths, or behaviour.
414
+
415
+ ### Scope and simplicity
416
+ - Deliver **what was asked**, not a roadmap of extras. Avoid speculative features, abstraction layers, or configurability “for later” unless the user requested flexibility.
417
+ - Prefer the **smallest** correct change. Expand structure only when complexity is already present or clearly required.
418
+
419
+ ### Surgical edits
420
+ - Change **only** what is necessary for the outcome; match surrounding **style and patterns** unless project docs say otherwise.
421
+ - Do not refactor, rename, or reformat unrelated code in the same pass. Note worthwhile cleanups separately if helpful.
422
+ - Remove **orphans your edit introduced** (e.g. unused imports from your change). Leave pre-existing dead code unless the user asks to remove it.
423
+
424
+ ### When to call it done
425
+ - Turn fuzzy requests into **checkable** outcomes where it matters (e.g. bug → reproduce → fix → same checks green).
426
+ - After material edits, run the **cheapest** falsifying step you can: targeted test, lint, build, or re-read the critical path — not guess-and-hope.
427
+
428
+ """
429
+
430
+
356
431
  def _build_memory_section(cfg: GemCodeConfig) -> str:
357
432
  """Injected when enable_memory=True so the agent understands and uses memory."""
358
433
  mem_path = cfg.project_root / ".gemcode" / "memories.jsonl"
@@ -559,6 +634,12 @@ def build_instruction(cfg: GemCodeConfig) -> str:
559
634
  "on",
560
635
  )
561
636
 
637
+ discipline_block = (
638
+ _build_engineering_discipline_section(cfg)
639
+ if _engineering_discipline_instruction_enabled()
640
+ else ""
641
+ )
642
+
562
643
  base = f"""You are GemCode, an expert software engineering agent powered by Google Gemini.
563
644
  You run locally via the GemCode CLI. You are the same agent the user launched — not a hosted portal.
564
645
 
@@ -610,6 +691,8 @@ You have native deep thinking capability — use it actively:
610
691
  - Prefer **small, targeted tool outputs** by default (saves context, improves accuracy).
611
692
  - If a tool output was **offloaded** (you see a `tool_result:<sha>` reference), and you need details, call `load_tool_result(ref)` and extract only the relevant slice.
612
693
 
694
+ {_build_calibration_section(cfg)}
695
+ {discipline_block}
613
696
  ## Tool selection guide (only when needed)
614
697
 
615
698
  Keep tool usage minimal. Prefer short, targeted calls and keep tool outputs small.
@@ -18,11 +18,6 @@ from google.adk.runners import Runner
18
18
  from google.genai import types
19
19
 
20
20
 
21
- # Delays (seconds) between successive transient-error retries: 2s, 5s, 12s.
22
- # Three retries = up to ~19 seconds of total wait before giving up.
23
- _TRANSIENT_RETRY_DELAYS = [2.0, 5.0, 12.0]
24
-
25
-
26
21
  _HITL_PROMPT_LOCK = Lock()
27
22
 
28
23
  async def _maybe_enqueue_kaira_autopilot(*, cfg: "GemCodeConfig", session_id: str) -> None:
@@ -423,14 +418,14 @@ async def run_turn(
423
418
  next_message=current_message, do_reset=do_reset
424
419
  )
425
420
  except Exception as _exc:
426
- from gemcode.model_errors import is_transient_error
427
- if is_transient_error(_exc) and transient_attempts < len(_TRANSIENT_RETRY_DELAYS):
428
- delay = _TRANSIENT_RETRY_DELAYS[transient_attempts]
421
+ from gemcode.model_errors import API_TRANSIENT_RETRY_DELAYS_SEC, is_transient_error
422
+ if is_transient_error(_exc) and transient_attempts < len(API_TRANSIENT_RETRY_DELAYS_SEC):
423
+ delay = API_TRANSIENT_RETRY_DELAYS_SEC[transient_attempts]
429
424
  transient_attempts += 1
430
425
  _tui_active = os.environ.get("GEMCODE_TUI_ACTIVE", "0").lower() in ("1", "true", "yes", "on")
431
426
  _msg = (
432
427
  f"\n[gemcode] Transient API error ({type(_exc).__name__}). "
433
- f"Retrying in {delay:.0f}s (attempt {transient_attempts}/{len(_TRANSIENT_RETRY_DELAYS)})...\n"
428
+ f"Retrying in {delay:.0f}s (attempt {transient_attempts}/{len(API_TRANSIENT_RETRY_DELAYS_SEC)})...\n"
434
429
  )
435
430
  print(_msg, file=sys.stderr)
436
431
  # Surface retry notice in TUI if available.
@@ -4,6 +4,9 @@ from __future__ import annotations
4
4
 
5
5
  import re
6
6
 
7
+ # Seconds between retries for transient API failures (shared by invoke.py and TUI).
8
+ API_TRANSIENT_RETRY_DELAYS_SEC: tuple[float, ...] = (2.0, 5.0, 12.0)
9
+
7
10
 
8
11
  def is_transient_error(error: Exception) -> bool:
9
12
  """Return True for HTTP 503 / 429 and similar transient API errors that are safe to retry.
@@ -29,6 +32,21 @@ def is_transient_error(error: Exception) -> bool:
29
32
  return True
30
33
 
31
34
  msg = str(error)
35
+ ml = msg.lower()
36
+
37
+ # httpx / google-genai often raise ``ServerError`` for HTTP 500 without ``APIError.code``.
38
+ if et == "ServerError" or "ServerError" in et:
39
+ if any(code in msg for code in ("500", "502", "503", "504")):
40
+ return True
41
+
42
+ # Gemini REST body shape: ``'code': 500`` / ``"INTERNAL"`` / "Internal error encountered."
43
+ if ("'code': 500" in msg or '"code": 500' in msg or re.search(r"\b500\b", msg)) and any(
44
+ p in ml for p in ("internal", "unavailable", "try again", "deadline", "timeout", "backend")
45
+ ):
46
+ return True
47
+ if "internal error encountered" in ml:
48
+ return True
49
+
32
50
  # Match the specific phrases Gemini uses in 503 responses
33
51
  if "503" in msg and any(p in msg for p in ("high demand", "service unavailable", "overloaded")):
34
52
  return True
@@ -378,6 +378,9 @@ def slash_help_lines() -> list[str]:
378
378
  " /audit [N] Tail of .gemcode/audit.log (default 40 lines)",
379
379
  " /tools List tool inventory for this config",
380
380
  " /tools smoke Declaration compile check only (failures listed)",
381
+ " /mcp MCP status (reads .gemcode/mcp.json; shows loaded toolsets)",
382
+ " /mcp list List configured MCP servers",
383
+ " /mcp reload Rebuild runner to reload MCP toolsets",
381
384
  " /eval [llm] Run tools_smoke (+ pytest if tests/ exist); optional LLM goldens",
382
385
  " /autotune init <tag> Git branch autotune/<tag> for experiment tracking",
383
386
  " /autotune eval [llm] Eval + append .gemcode/evals/autotune_ledger.jsonl",
@@ -1245,6 +1245,97 @@ async def process_repl_slash(
1245
1245
  out()
1246
1246
  return ReplSlashResult(skip_model_turn=True)
1247
1247
 
1248
+ # ── /mcp (Model Context Protocol toolsets) ────────────────────────────────
1249
+ if name == "mcp":
1250
+ args_m = (sc.args or "").strip()
1251
+ sub = (args_m.split()[0].strip().lower() if args_m else "status")
1252
+ mcp_path = cfg.project_root / ".gemcode" / "mcp.json"
1253
+
1254
+ if sub in ("help", "?"):
1255
+ out("Usage:")
1256
+ out(" /mcp Show MCP config + loaded toolsets (same as /mcp status)")
1257
+ out(" /mcp status Show MCP config + loaded toolsets")
1258
+ out(" /mcp list List configured servers from .gemcode/mcp.json")
1259
+ out(" /mcp reload Rebuild runner to reload MCP toolsets from disk")
1260
+ out()
1261
+ out("Config:")
1262
+ out(f" {mcp_path}")
1263
+ out()
1264
+ return ReplSlashResult(skip_model_turn=True)
1265
+
1266
+ if sub in ("reload", "refresh"):
1267
+ out("MCP: runner will rebuild on the next turn (reload .gemcode/mcp.json).")
1268
+ out()
1269
+ return ReplSlashResult(skip_model_turn=True, force_rebuild_runner=True)
1270
+
1271
+ # Read config if present.
1272
+ servers: list[dict] = []
1273
+ parse_error: str | None = None
1274
+ if mcp_path.is_file():
1275
+ try:
1276
+ import json
1277
+
1278
+ data = json.loads(mcp_path.read_text(encoding="utf-8"))
1279
+ servers = list(data.get("servers") or [])
1280
+ except Exception as e:
1281
+ parse_error = str(e)
1282
+
1283
+ # Inspect currently loaded toolsets (best-effort; depends on how caller wired extra_tools).
1284
+ loaded_prefixes: list[str] = []
1285
+ loaded_count = 0
1286
+ try:
1287
+ from google.adk.tools.mcp_tool.mcp_toolset import McpToolset # type: ignore
1288
+
1289
+ for t in list(extra_tools or []):
1290
+ if isinstance(t, McpToolset):
1291
+ loaded_count += 1
1292
+ try:
1293
+ p = getattr(t, "tool_name_prefix", None)
1294
+ if isinstance(p, str) and p and p not in loaded_prefixes:
1295
+ loaded_prefixes.append(p)
1296
+ except Exception:
1297
+ pass
1298
+ except Exception:
1299
+ # MCP extras not installed or ADK missing MCP toolset types.
1300
+ pass
1301
+
1302
+ if sub in ("list", "ls"):
1303
+ out(f"mcp.json: {mcp_path} ({'exists' if mcp_path.is_file() else 'missing'})")
1304
+ if parse_error:
1305
+ out(f"error: {parse_error}")
1306
+ out()
1307
+ return ReplSlashResult(skip_model_turn=True)
1308
+ if not servers:
1309
+ out("(no servers configured)")
1310
+ out()
1311
+ return ReplSlashResult(skip_model_turn=True)
1312
+ out("Servers:")
1313
+ for s in servers[:200]:
1314
+ try:
1315
+ nm = (s.get("name") or "mcp").strip()
1316
+ kind = "stdio" if "stdio" in s else ("http" if "http" in s else ("sse" if "sse" in s else "?"))
1317
+ out(f" - {nm} ({kind})")
1318
+ except Exception:
1319
+ continue
1320
+ if len(servers) > 200:
1321
+ out(f" … (+{len(servers) - 200} more)")
1322
+ out()
1323
+ return ReplSlashResult(skip_model_turn=True)
1324
+
1325
+ # Default: status.
1326
+ out("MCP:")
1327
+ out(f" mcp.json: {mcp_path} ({'exists' if mcp_path.is_file() else 'missing'})")
1328
+ if parse_error:
1329
+ out(f" parse_error: {parse_error}")
1330
+ out(f" configured_servers: {len(servers)}")
1331
+ suffix = f" (prefixes: {', '.join(sorted(loaded_prefixes))})" if loaded_prefixes else ""
1332
+ out(f" loaded_toolsets: {loaded_count}{suffix}")
1333
+ out()
1334
+ if not mcp_path.is_file():
1335
+ out("Tip: create .gemcode/mcp.json to enable MCP toolsets for this project.")
1336
+ out()
1337
+ return ReplSlashResult(skip_model_turn=True)
1338
+
1248
1339
  if name == "tools":
1249
1340
  args_t = (sc.args or "").strip().lower()
1250
1341
  if args_t in ("smoke", "decl", "declarations"):
@@ -31,6 +31,63 @@ from gemcode.plugins.terminal_hooks_plugin import GemCodeTerminalHooksPlugin
31
31
  from gemcode.plugins.tool_recovery_plugin import GemCodeReflectAndRetryToolPlugin
32
32
 
33
33
 
34
+ # ---------------------------------------------------------------------------
35
+ # ADK: Gemini context cache — quiet "stale delete" failures
36
+ # ---------------------------------------------------------------------------
37
+
38
+
39
+ def _gemini_cache_delete_already_gone(exc: BaseException) -> bool:
40
+ """True when delete failed only because the cache entry is already gone.
41
+
42
+ The Gemini API often returns ``403 PERMISSION_DENIED`` with a body like
43
+ ``CachedContent not found`` after TTL expiry or server-side eviction. ADK's
44
+ default cleanup logs that as WARNING every time — noisy and usually harmless.
45
+ """
46
+ msg = str(exc).lower()
47
+ if "cachedcontent not found" in msg:
48
+ return True
49
+ if "not found" in msg and "cached" in msg:
50
+ return True
51
+ code = getattr(exc, "code", None)
52
+ if code == 404:
53
+ return True
54
+ status = (getattr(exc, "status", None) or "").upper()
55
+ if code == 403 and status == "PERMISSION_DENIED" and ("cachedcontent" in msg or "cached content" in msg):
56
+ return True
57
+ return False
58
+
59
+
60
+ def _patch_gemini_adk_cache_cleanup() -> None:
61
+ """Downgrade benign cache-delete failures to DEBUG (see ``_gemini_cache_delete_already_gone``)."""
62
+ try:
63
+ from google.adk.models import gemini_context_cache_manager as gccm
64
+ except Exception:
65
+ return
66
+ if getattr(gccm.GeminiContextCacheManager, "_gemcode_cleanup_patch", False):
67
+ return
68
+
69
+ async def _cleanup(self, cache_name: str) -> None:
70
+ gccm.logger.debug("Attempting to delete cache: %s", cache_name)
71
+ try:
72
+ await self.genai_client.aio.caches.delete(name=cache_name)
73
+ gccm.logger.info("Cache cleaned up: %s", cache_name)
74
+ except BaseException as e:
75
+ if _gemini_cache_delete_already_gone(e):
76
+ gccm.logger.debug(
77
+ "Cache delete no-op (already expired or gone): %s — %s",
78
+ cache_name,
79
+ e,
80
+ )
81
+ return
82
+ gccm.logger.warning("Failed to cleanup cache %s: %s", cache_name, e)
83
+
84
+ gccm.GeminiContextCacheManager.cleanup_cache = _cleanup # type: ignore[method-assign]
85
+ gccm.GeminiContextCacheManager._gemcode_cleanup_patch = True
86
+
87
+
88
+ _patch_gemini_adk_cache_cleanup()
89
+
90
+
34
91
  # ---------------------------------------------------------------------------
35
92
  # ADK App-level feature helpers
36
93
  # ---------------------------------------------------------------------------
@@ -105,6 +105,30 @@ def build_tool_manifest(cfg: GemCodeConfig) -> str | None:
105
105
 
106
106
  memory_on = bool(getattr(cfg, "enable_memory", False))
107
107
 
108
+ mm = (getattr(cfg, "model_mode", "") or "").strip().lower()
109
+ cm = (getattr(cfg, "capability_mode", "") or "").strip().lower()
110
+ auto_routing_note = ""
111
+ if mm == "auto" or cm == "auto":
112
+ auto_routing_note = (
113
+ f"\n- **Dynamic routing:** `model_mode={mm}`, `capability_mode={cm}` — per-turn defaults are **hints**; "
114
+ "still pick the smallest depth/toolset that fits the ask.\n"
115
+ )
116
+
117
+ calibration_manifest = f"""### Calibration (aligned with main instruction)
118
+ - **Repo vs external:** ground claims about *this repo* with reads/grep/tests; use research/web for **external** docs, APIs, or facts outside the tree.
119
+ - **Orchestration:** use `run_subtask` / `spawn_subtasks` only for **parallel independent** work or explicit verification; merge into **one** answer; skip fan-out for trivial linear tasks.
120
+ - **Finish line:** verify risky edits (test/lint/read-back) before declaring done.{auto_routing_note}"""
121
+
122
+ _disc = os.environ.get("GEMCODE_ENGINEERING_DISCIPLINE", "1").strip().lower()
123
+ discipline_manifest = ""
124
+ if _disc not in ("0", "false", "no", "off"):
125
+ discipline_manifest = """
126
+
127
+ ### Engineering discipline (aligned with main instruction)
128
+ - **Ambiguity:** state what you understood or ask **one** precise question; prefer repo evidence over guesses.
129
+ - **Scope:** deliver the ask with the **smallest** adequate change; skip speculative extras.
130
+ - **Surgical:** touch only what you must; match local style; clean up orphans **your** edit introduced."""
131
+
108
132
  manifest = f"""## Tool system (GemCode)
109
133
 
110
134
  ### Execution model
@@ -112,6 +136,8 @@ def build_tool_manifest(cfg: GemCodeConfig) -> str | None:
112
136
  - **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.
113
137
  - **Never stop after the first tool call succeeds.** Keep going until the full task is done or you hit a genuine blocker.
114
138
 
139
+ {calibration_manifest}{discipline_manifest}
140
+
115
141
  ### Permission policy
116
142
  | Setting | Value |
117
143
  |---------|-------|
@@ -38,7 +38,9 @@ SHELL_TOOLS: frozenset[str] = frozenset({"run_command", "bash"})
38
38
  # Session planning only (no disk / shell; no extra permission)
39
39
  # think — in-context reasoning scratchpad (no-op, no side effects)
40
40
  # run_subtask — spawns a sub-agent; inherits parent permission settings
41
- PLANNING_TOOLS: frozenset[str] = frozenset({"todo_write", "think", "run_subtask"})
41
+ PLANNING_TOOLS: frozenset[str] = frozenset(
42
+ {"todo_write", "think", "run_subtask", "spawn_subtasks"}
43
+ )
42
44
 
43
45
  ToolConcurrency = Literal["parallel_safe", "serial_mutating", "shell"]
44
46
 
@@ -16,6 +16,7 @@ from gemcode.multimodal_input import build_user_content
16
16
 
17
17
  from gemcode.capability_routing import apply_capability_routing
18
18
  from gemcode.config import load_cli_environment
19
+ from gemcode.model_errors import API_TRANSIENT_RETRY_DELAYS_SEC, format_model_error_for_user, is_transient_error
19
20
  from gemcode.model_routing import pick_effective_model
20
21
  from gemcode.repl_slash import process_repl_slash
21
22
  from gemcode.tui.input_handler import GemCodeInputHandler
@@ -915,40 +916,70 @@ async def run_gemcode_scrollback_tui(
915
916
  # as different phases of the turn complete.
916
917
  _start_anim("Thinking\u2026")
917
918
 
918
- try:
919
- async for ev in runner.run_async(**kwargs):
920
- events.append(ev)
921
- _render_tool_calls(ev)
922
- _render_tool_results(ev)
923
- try:
924
- if not ev.content or not ev.content.parts:
925
- continue
926
- # Only skip user turns. ADK often omits `author` on model events — do NOT
927
- # skip those or the assistant text never renders (blank reply, ↓0 tokens).
928
- if getattr(ev, "author", None) == "user":
929
- continue
930
- for part in ev.content.parts:
931
- delta = getattr(part, "text", None)
932
- if not delta:
919
+ transient_attempts = 0
920
+ stream_exc: Exception | None = None
921
+ while True:
922
+ stream_exc = None
923
+ try:
924
+ async for ev in runner.run_async(**kwargs):
925
+ events.append(ev)
926
+ _render_tool_calls(ev)
927
+ _render_tool_results(ev)
928
+ try:
929
+ if not ev.content or not ev.content.parts:
933
930
  continue
934
- assistant_wrote_text = True
935
- if getattr(part, "thought", None):
936
- buffered_thought.append(delta)
937
- else:
938
- buffered_final.append(delta)
939
- except Exception:
931
+ # Only skip user turns. ADK often omits `author` on model events — do NOT
932
+ # skip those or the assistant text never renders (blank reply, ↓0 tokens).
933
+ if getattr(ev, "author", None) == "user":
934
+ continue
935
+ for part in ev.content.parts:
936
+ delta = getattr(part, "text", None)
937
+ if not delta:
938
+ continue
939
+ assistant_wrote_text = True
940
+ if getattr(part, "thought", None):
941
+ buffered_thought.append(delta)
942
+ else:
943
+ buffered_final.append(delta)
944
+ except Exception:
945
+ continue
946
+ break
947
+ except Exception as _turn_err:
948
+ if is_transient_error(_turn_err) and transient_attempts < len(
949
+ API_TRANSIENT_RETRY_DELAYS_SEC
950
+ ):
951
+ _stop_anim()
952
+ delay = API_TRANSIENT_RETRY_DELAYS_SEC[transient_attempts]
953
+ transient_attempts += 1
954
+ print(
955
+ f"\n {ansi.dim}[gemcode] Transient API error ({type(_turn_err).__name__}). "
956
+ f"Retrying in {delay:.0f}s ({transient_attempts}/"
957
+ f"{len(API_TRANSIENT_RETRY_DELAYS_SEC)})…{ansi.reset}\n",
958
+ flush=True,
959
+ )
960
+ await asyncio.sleep(delay)
961
+ events.clear()
962
+ assistant_wrote_text = False
963
+ buffered_thought.clear()
964
+ buffered_final.clear()
965
+ last_tool_error = None
966
+ _start_anim("Retrying\u2026")
940
967
  continue
941
- except Exception as _turn_err:
968
+ stream_exc = _turn_err
969
+ break
970
+
971
+ if stream_exc is not None:
942
972
  # Catch runner errors (e.g. ADK ValueError from mismatched function
943
973
  # response IDs) so a single bad turn doesn't crash the whole TUI.
944
974
  _stop_anim()
975
+ try:
976
+ hint = format_model_error_for_user(stream_exc)
977
+ except Exception:
978
+ hint = f"{type(stream_exc).__name__}: {stream_exc}"
979
+ print(f"\n {ansi.blue_warn}[gemcode] turn error: {hint}{ansi.reset}")
945
980
  print(
946
- f"\n {ansi.blue_warn}[gemcode] turn error: "
947
- f"{type(_turn_err).__name__}: {_turn_err}{ansi.reset}"
948
- )
949
- print(
950
- f" {ansi.dim}The agent encountered an internal error on this turn. "
951
- f"Please send your message again.{ansi.reset}\n"
981
+ f" {ansi.dim}If this was a temporary Google API issue, send your message again. "
982
+ f"Otherwise try /compact, a shorter prompt, or /model to switch model.{ansi.reset}\n"
952
983
  )
953
984
  break
954
985
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.107
3
+ Version: 0.3.109
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -149,6 +149,7 @@ tests/test_permissions.py
149
149
  tests/test_prompt_suggestions.py
150
150
  tests/test_repl_commands.py
151
151
  tests/test_repl_slash.py
152
+ tests/test_session_runtime_cache.py
152
153
  tests/test_skills.py
153
154
  tests/test_slash_commands.py
154
155
  tests/test_slash_completion_registry.py
@@ -0,0 +1,76 @@
1
+ from pathlib import Path
2
+
3
+ from gemcode.agent import build_instruction
4
+ from gemcode.config import GemCodeConfig
5
+ from gemcode.tool_prompt_manifest import build_tool_manifest
6
+
7
+
8
+ def test_instruction_includes_runtime_facts(tmp_path: Path) -> None:
9
+ cfg = GemCodeConfig(project_root=tmp_path, model="gemini-2.5-flash")
10
+ text = build_instruction(cfg)
11
+ assert str(tmp_path.resolve()) in text
12
+ assert "gemini-2.5-flash" in text
13
+ assert "GEMCODE_MODEL" in text
14
+
15
+
16
+ def test_instruction_includes_calibration_section(tmp_path: Path) -> None:
17
+ cfg = GemCodeConfig(project_root=tmp_path, model="gemini-2.5-flash")
18
+ text = build_instruction(cfg)
19
+ assert "Calibration and dynamic routing" in text
20
+ assert "Orchestration" in text
21
+
22
+
23
+ def test_instruction_includes_engineering_discipline_section(tmp_path: Path) -> None:
24
+ cfg = GemCodeConfig(project_root=tmp_path, model="gemini-2.5-flash")
25
+ text = build_instruction(cfg)
26
+ assert "Engineering discipline (change quality)" in text
27
+
28
+
29
+ def test_instruction_omits_engineering_discipline_when_disabled(tmp_path: Path, monkeypatch) -> None:
30
+ monkeypatch.setenv("GEMCODE_ENGINEERING_DISCIPLINE", "0")
31
+ cfg = GemCodeConfig(project_root=tmp_path, model="gemini-2.5-flash")
32
+ text = build_instruction(cfg)
33
+ assert "Engineering discipline (change quality)" not in text
34
+
35
+
36
+ def test_tool_manifest_includes_engineering_discipline_aligned_block(tmp_path: Path) -> None:
37
+ cfg = GemCodeConfig(project_root=tmp_path, model="gemini-2.5-flash")
38
+ m = build_tool_manifest(cfg)
39
+ assert m is not None
40
+ assert "Engineering discipline (aligned with main instruction)" in m
41
+
42
+
43
+ def test_tool_manifest_omits_engineering_discipline_when_disabled(tmp_path: Path, monkeypatch) -> None:
44
+ monkeypatch.setenv("GEMCODE_ENGINEERING_DISCIPLINE", "0")
45
+ cfg = GemCodeConfig(project_root=tmp_path, model="gemini-2.5-flash")
46
+ m = build_tool_manifest(cfg)
47
+ assert m is not None
48
+ assert "Engineering discipline (aligned with main instruction)" not in m
49
+
50
+
51
+ def test_tool_manifest_includes_calibration_aligned_block(tmp_path: Path) -> None:
52
+ cfg = GemCodeConfig(project_root=tmp_path, model="gemini-2.5-flash")
53
+ m = build_tool_manifest(cfg)
54
+ assert m is not None
55
+ assert "Calibration (aligned with main instruction)" in m
56
+ assert "spawn_subtasks" in m
57
+
58
+
59
+ def test_instruction_notes_auto_routing_when_configured(tmp_path: Path) -> None:
60
+ cfg = GemCodeConfig(project_root=tmp_path, model="gemini-2.5-flash")
61
+ cfg.model_mode = "auto"
62
+ cfg.capability_mode = "auto"
63
+ text = build_instruction(cfg)
64
+ assert "dynamic routing" in text
65
+ assert "model_mode=auto" in text
66
+ assert "capability_mode=auto" in text
67
+
68
+
69
+ def test_instruction_includes_veomem_tool_flow_when_recall_present(tmp_path: Path) -> None:
70
+ cfg = GemCodeConfig(project_root=tmp_path, model="gemini-2.5-flash")
71
+ object.__setattr__(cfg, "_veomem_wakeup_text", "<veomem-context>hello</veomem-context>")
72
+ text = build_instruction(cfg)
73
+ assert "VeoMem recall" in text
74
+ assert "veomem_search(query=...)" in text
75
+ assert "veomem_timeline(id=...)" in text
76
+ assert "veomem_get_observations(ids=...)" in text
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import pytest
6
6
 
7
- from gemcode.model_errors import format_model_error_for_user
7
+ from gemcode.model_errors import format_model_error_for_user, is_transient_error
8
8
 
9
9
 
10
10
  def test_format_generic_exception() -> None:
@@ -13,6 +13,22 @@ def test_format_generic_exception() -> None:
13
13
  assert "something broke" in msg
14
14
 
15
15
 
16
+ def test_is_transient_server_error_500_internal() -> None:
17
+ """Gemini often surfaces 500 INTERNAL as httpx ServerError, not genai APIError."""
18
+ msg = (
19
+ "ServerError: 500 INTERNAL. {'error': {'code': 500, 'message': "
20
+ "'Internal error encountered.', 'status': 'INTERNAL'}}"
21
+ )
22
+ assert is_transient_error(RuntimeError(msg))
23
+
24
+
25
+ def test_is_transient_server_error_classname() -> None:
26
+ class ServerError(Exception):
27
+ pass
28
+
29
+ assert is_transient_error(ServerError("500 something"))
30
+
31
+
16
32
  def test_format_genai_client_error() -> None:
17
33
  try:
18
34
  from google.genai import errors as genai_errors