gemcode 0.3.108__tar.gz → 0.3.110__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 (164) hide show
  1. {gemcode-0.3.108/src/gemcode.egg-info → gemcode-0.3.110}/PKG-INFO +1 -1
  2. {gemcode-0.3.108 → gemcode-0.3.110}/pyproject.toml +1 -1
  3. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/kaira_daemon.py +55 -5
  4. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/repl_commands.py +3 -0
  5. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/repl_slash.py +91 -0
  6. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/session_runtime.py +60 -0
  7. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tui/scrollback.py +56 -0
  8. {gemcode-0.3.108 → gemcode-0.3.110/src/gemcode.egg-info}/PKG-INFO +1 -1
  9. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode.egg-info/SOURCES.txt +1 -0
  10. gemcode-0.3.110/tests/test_session_runtime_cache.py +39 -0
  11. {gemcode-0.3.108 → gemcode-0.3.110}/LICENSE +0 -0
  12. {gemcode-0.3.108 → gemcode-0.3.110}/MANIFEST.in +0 -0
  13. {gemcode-0.3.108 → gemcode-0.3.110}/README.md +0 -0
  14. {gemcode-0.3.108 → gemcode-0.3.110}/setup.cfg +0 -0
  15. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/__init__.py +0 -0
  16. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/__main__.py +0 -0
  17. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/agent.py +0 -0
  18. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/audit.py +0 -0
  19. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/autocompact.py +0 -0
  20. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/autotune.py +0 -0
  21. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/callbacks.py +0 -0
  22. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/capability_routing.py +0 -0
  23. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/checkpoints.py +0 -0
  24. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/cli.py +0 -0
  25. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/compaction.py +0 -0
  26. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/computer_use/__init__.py +0 -0
  27. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/computer_use/browser_computer.py +0 -0
  28. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/config.py +0 -0
  29. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/context_budget.py +0 -0
  30. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/context_warning.py +0 -0
  31. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/credentials.py +0 -0
  32. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/curated_memory.py +0 -0
  33. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/dynamic_policy.py +0 -0
  34. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/evals/harness.py +0 -0
  35. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/hitl_session.py +0 -0
  36. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/hooks.py +0 -0
  37. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/ide_protocol.py +0 -0
  38. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/ide_stdio.py +0 -0
  39. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/intent_classifier.py +0 -0
  40. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/interactions.py +0 -0
  41. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/invoke.py +0 -0
  42. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/kaira_client.py +0 -0
  43. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/kaira_ipc.py +0 -0
  44. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/kaira_job_store.py +0 -0
  45. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/learning.py +0 -0
  46. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/limits.py +0 -0
  47. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/live_audio_engine.py +0 -0
  48. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/logging_config.py +0 -0
  49. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/mcp_loader.py +0 -0
  50. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/memory/__init__.py +0 -0
  51. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/memory/embedding_memory_service.py +0 -0
  52. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/memory/file_memory_service.py +0 -0
  53. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/modality_tools.py +0 -0
  54. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/model_errors.py +0 -0
  55. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/model_routing.py +0 -0
  56. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/multimodal_input.py +0 -0
  57. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/openapi_loader.py +0 -0
  58. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/org.py +0 -0
  59. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/output_styles.py +0 -0
  60. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/paths.py +0 -0
  61. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/permissions.py +0 -0
  62. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/plugins/__init__.py +0 -0
  63. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  64. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  65. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/policy_profile.py +0 -0
  66. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/pricing.py +0 -0
  67. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/prompt_suggestions.py +0 -0
  68. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/__init__.py +0 -0
  69. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/config.py +0 -0
  70. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/deps.py +0 -0
  71. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/engine.py +0 -0
  72. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/stop_hooks.py +0 -0
  73. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/token_budget.py +0 -0
  74. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/transitions.py +0 -0
  75. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query_sanitizer.py +0 -0
  76. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/refine.py +0 -0
  77. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/review_agent.py +0 -0
  78. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/rules.py +0 -0
  79. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/session_store.py +0 -0
  80. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/session_summariser.py +0 -0
  81. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/skills.py +0 -0
  82. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/slash_commands.py +0 -0
  83. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/thinking.py +0 -0
  84. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tool_prompt_manifest.py +0 -0
  85. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tool_registry.py +0 -0
  86. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tool_result_store.py +0 -0
  87. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/__init__.py +0 -0
  88. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/bash.py +0 -0
  89. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/browser.py +0 -0
  90. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/compress_memory.py +0 -0
  91. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/curated_memory.py +0 -0
  92. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/edit.py +0 -0
  93. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/filesystem.py +0 -0
  94. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/notebook.py +0 -0
  95. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/notes.py +0 -0
  96. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/org_tools.py +0 -0
  97. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/repo_map.py +0 -0
  98. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/search.py +0 -0
  99. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/shell.py +0 -0
  100. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/shell_gate.py +0 -0
  101. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/skills.py +0 -0
  102. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/subtask.py +0 -0
  103. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/tasks.py +0 -0
  104. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/think.py +0 -0
  105. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/todo.py +0 -0
  106. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/user_choice.py +0 -0
  107. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/veomem_tools.py +0 -0
  108. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/web.py +0 -0
  109. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/web_search.py +0 -0
  110. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools_inspector.py +0 -0
  111. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/trust.py +0 -0
  112. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tui/input_handler.py +0 -0
  113. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tui/spinner.py +0 -0
  114. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tui/welcome_banner.py +0 -0
  115. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tui/welcome_rich.py +0 -0
  116. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/veomem_bridge.py +0 -0
  117. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/version.py +0 -0
  118. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/vertex.py +0 -0
  119. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/wal.py +0 -0
  120. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/web/__init__.py +0 -0
  121. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/web/sse_adapter.py +0 -0
  122. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/web/terminal_repl.py +0 -0
  123. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/web/web_sse_compat.py +0 -0
  124. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/workspace_hints.py +0 -0
  125. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode.egg-info/dependency_links.txt +0 -0
  126. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode.egg-info/entry_points.txt +0 -0
  127. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode.egg-info/requires.txt +0 -0
  128. {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode.egg-info/top_level.txt +0 -0
  129. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_add_dir.py +0 -0
  130. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_agent_instruction.py +0 -0
  131. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_autocompact.py +0 -0
  132. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_capability_routing.py +0 -0
  133. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_checkpoint_diff_command.py +0 -0
  134. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_cli_init.py +0 -0
  135. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_compress_memory_tool.py +0 -0
  136. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_computer_use_permissions.py +0 -0
  137. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_context_budget.py +0 -0
  138. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_context_warning.py +0 -0
  139. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_credentials.py +0 -0
  140. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_eval_harness_layout.py +0 -0
  141. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_ide_stdio_attachments.py +0 -0
  142. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_interactive_permission_ask.py +0 -0
  143. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_kaira_scheduler.py +0 -0
  144. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_modality_tools.py +0 -0
  145. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_model_error_retry.py +0 -0
  146. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_model_errors.py +0 -0
  147. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_model_routing.py +0 -0
  148. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_multimodal_input.py +0 -0
  149. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_output_styles_and_rules.py +0 -0
  150. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_paths.py +0 -0
  151. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_permissions.py +0 -0
  152. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_prompt_suggestions.py +0 -0
  153. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_repl_commands.py +0 -0
  154. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_repl_slash.py +0 -0
  155. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_skills.py +0 -0
  156. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_slash_commands.py +0 -0
  157. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_slash_completion_registry.py +0 -0
  158. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_thinking_config.py +0 -0
  159. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_token_budget.py +0 -0
  160. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_tool_context_circulation.py +0 -0
  161. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_tools.py +0 -0
  162. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_tools_inspector.py +0 -0
  163. {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_web_sse_adapter.py +0 -0
  164. {gemcode-0.3.108 → gemcode-0.3.110}/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.108
3
+ Version: 0.3.110
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.108"
7
+ version = "0.3.110"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -94,6 +94,22 @@ def _fmt_tool_result(resp: object) -> str:
94
94
  return ""
95
95
 
96
96
 
97
+ def _should_stream_to_terminal() -> bool:
98
+ """Stream live job output to the local terminal when interactive."""
99
+ try:
100
+ return bool(hasattr(sys.stdin, "isatty") and sys.stdin.isatty())
101
+ except Exception:
102
+ return False
103
+
104
+
105
+ def _stream_print(s: str) -> None:
106
+ try:
107
+ sys.stdout.write(s)
108
+ sys.stdout.flush()
109
+ except Exception:
110
+ pass
111
+
112
+
97
113
  async def _broadcast_text_delta(
98
114
  *,
99
115
  ipc: KairaIpcServer,
@@ -402,6 +418,9 @@ class KairaDaemon:
402
418
  async def _stream_one_message(*, current_message: types.Content) -> tuple[list, str]:
403
419
  emitted_text = ""
404
420
  events: list = []
421
+ stream_live = _should_stream_to_terminal()
422
+ if stream_live:
423
+ _stream_print(f"\n[kaira {job.job_id}] started\n")
405
424
  async for ev in runner.run_async(
406
425
  user_id=self.user_id,
407
426
  session_id=job.session_id,
@@ -409,6 +428,28 @@ class KairaDaemon:
409
428
  **({"run_config": run_config} if run_config is not None else {}),
410
429
  ):
411
430
  events.append(ev)
431
+ # Live terminal streaming (independent of IPC).
432
+ if stream_live:
433
+ try:
434
+ from gemcode.web.sse_adapter import extract_text_from_event
435
+
436
+ txt_live = extract_text_from_event(ev)
437
+ if txt_live:
438
+ if txt_live.startswith(emitted_text):
439
+ delta_live = txt_live[len(emitted_text) :]
440
+ else:
441
+ # Fallback: find common prefix.
442
+ common = 0
443
+ max_common = min(len(txt_live), len(emitted_text))
444
+ while common < max_common and txt_live[common] == emitted_text[common]:
445
+ common += 1
446
+ delta_live = txt_live[common:]
447
+ if delta_live:
448
+ _stream_print(delta_live)
449
+ emitted_text = txt_live
450
+ except Exception:
451
+ pass
452
+
412
453
  if self._ipc is None:
413
454
  continue
414
455
 
@@ -460,7 +501,7 @@ class KairaDaemon:
460
501
  except Exception:
461
502
  pass
462
503
 
463
- # Text deltas
504
+ # Text deltas (IPC subscribers)
464
505
  try:
465
506
  from gemcode.web.sse_adapter import extract_text_from_event
466
507
 
@@ -602,8 +643,12 @@ class KairaDaemon:
602
643
  session_id=session_id,
603
644
  )
604
645
 
605
- async def run_forever(self, *, session_id: str) -> None:
606
- """Start the scheduler and keep running until stdin EOF/quit."""
646
+ async def run_forever(self, *, session_id: str, enable_stdin: bool = True) -> None:
647
+ """Start the scheduler and keep running until stopped.
648
+
649
+ When enable_stdin=False, Kaira runs headless (IPC-only) and does not read
650
+ from stdin. This mode is used when embedding Kaira inside the GemCode TUI.
651
+ """
607
652
 
608
653
  # Start IPC server for two-way control + event streaming.
609
654
  try:
@@ -623,11 +668,16 @@ class KairaDaemon:
623
668
  print(f"[kaira] ipc disabled: {e}", file=sys.stderr, flush=True)
624
669
 
625
670
  scheduler_task = asyncio.create_task(self._scheduler_loop())
626
- stdin_task = asyncio.create_task(self._stdin_loop(session_id=session_id))
671
+ stdin_task = None
672
+ if enable_stdin:
673
+ stdin_task = asyncio.create_task(self._stdin_loop(session_id=session_id))
627
674
 
628
675
  # Wait for either scheduler to stop (shouldn't happen) or stdin loop to end.
676
+ wait_set = {scheduler_task}
677
+ if stdin_task is not None:
678
+ wait_set.add(stdin_task)
629
679
  done, pending = await asyncio.wait(
630
- {scheduler_task, stdin_task},
680
+ wait_set,
631
681
  return_when=asyncio.FIRST_COMPLETED,
632
682
  )
633
683
  for p in pending:
@@ -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
  # ---------------------------------------------------------------------------
@@ -417,9 +474,12 @@ def create_runner(cfg: GemCodeConfig, extra_tools: list | None = None) -> Runner
417
474
  import sys
418
475
 
419
476
  prompt_afc = os.environ.get("GEMCODE_AFC_PROMPT", "1").strip().lower() in ("1", "true", "yes", "on")
477
+ afc_default = (os.environ.get("GEMCODE_AFC_DEFAULT") or "").strip().lower()
420
478
  if prompt_afc and hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
421
479
  tools_list = list(merged_extra_tools or [])
422
480
  noncallable = [t for t in tools_list if not callable(t)]
481
+ if noncallable and getattr(cfg, "_afc_choice", None) not in ("all", "callables") and afc_default in ("all", "callables"):
482
+ object.__setattr__(cfg, "_afc_choice", afc_default)
423
483
  if noncallable and getattr(cfg, "_afc_choice", None) not in ("all", "callables"):
424
484
  print(
425
485
  "\n[gemcode] AFC compatibility\n"
@@ -279,6 +279,50 @@ async def run_gemcode_scrollback_tui(
279
279
  get_cfg=lambda: cfg,
280
280
  )
281
281
 
282
+ # ── Optional: embed Kaira daemon in this TUI ─────────────────────────────
283
+ # This enables continuous background automation without requiring a second
284
+ # terminal. The TUI will also auto-subscribe to IPC events (below) so job
285
+ # output appears inline.
286
+ _embedded_kaira_task: list = [None] # asyncio.Task | None
287
+
288
+ def _embed_kaira_enabled() -> bool:
289
+ return os.environ.get("GEMCODE_TUI_WITH_KAIRA", "0").strip().lower() in (
290
+ "1",
291
+ "true",
292
+ "yes",
293
+ "on",
294
+ )
295
+
296
+ async def _start_embedded_kaira() -> None:
297
+ if not _embed_kaira_enabled():
298
+ return
299
+ # Avoid starting if a socket already exists (external daemon running).
300
+ sock = os.environ.get("GEMCODE_KAIRA_SOCKET") or str(cfg.project_root / _KAIRA_SOCKET_DEFAULT)
301
+ try:
302
+ from pathlib import Path as _Path
303
+
304
+ if _Path(sock).exists():
305
+ return
306
+ except Exception:
307
+ pass
308
+ try:
309
+ from gemcode.kaira_daemon import KairaDaemon
310
+
311
+ # Use this TUI's session id as the default session for stdin-less jobs.
312
+ daemon = KairaDaemon(cfg=cfg, concurrency=2, default_priority=0)
313
+ # Run headless: IPC-only. Jobs are enqueued via IPC (monitor scripts, org tools, etc.).
314
+ await daemon.run_forever(session_id=session_id, enable_stdin=False)
315
+ except asyncio.CancelledError:
316
+ return
317
+ except Exception:
318
+ return
319
+
320
+ try:
321
+ if _embed_kaira_enabled():
322
+ _embedded_kaira_task[0] = asyncio.create_task(_start_embedded_kaira())
323
+ except Exception:
324
+ _embedded_kaira_task[0] = None
325
+
282
326
  # ── Kaira auto-connect (IPC subscribe) ───────────────────────────────────
283
327
  # If a Kaira daemon is running for this project, subscribe to its updates and
284
328
  # surface them in the same terminal UI. Also handle permission requests by
@@ -765,6 +809,12 @@ async def run_gemcode_scrollback_tui(
765
809
  t.cancel()
766
810
  except Exception:
767
811
  pass
812
+ try:
813
+ t2 = _embedded_kaira_task[0]
814
+ if t2 is not None:
815
+ t2.cancel()
816
+ except Exception:
817
+ pass
768
818
  try:
769
819
  from gemcode.hooks import run_session_stop_hook
770
820
  run_session_stop_hook(cfg.project_root, model=getattr(cfg, "model", "") or "")
@@ -781,6 +831,12 @@ async def run_gemcode_scrollback_tui(
781
831
  t.cancel()
782
832
  except Exception:
783
833
  pass
834
+ try:
835
+ t2 = _embedded_kaira_task[0]
836
+ if t2 is not None:
837
+ t2.cancel()
838
+ except Exception:
839
+ pass
784
840
  from gemcode.hooks import run_session_stop_hook
785
841
  run_session_stop_hook(cfg.project_root, model=getattr(cfg, "model", "") or "")
786
842
  except Exception:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.108
3
+ Version: 0.3.110
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,39 @@
1
+ """Tests for Gemini context-cache cleanup heuristics (session_runtime)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from gemcode.session_runtime import _gemini_cache_delete_already_gone
8
+
9
+
10
+ def test_cache_delete_harmless_user_reported_403() -> None:
11
+ pytest.importorskip("google.genai.errors")
12
+ from google.genai.errors import ClientError
13
+
14
+ exc = ClientError(
15
+ 403,
16
+ {"error": {"message": "CachedContent not found (or permission denied)", "status": "PERMISSION_DENIED"}},
17
+ None,
18
+ )
19
+ assert _gemini_cache_delete_already_gone(exc) is True
20
+
21
+
22
+ def test_cache_delete_not_harmless_other_403() -> None:
23
+ pytest.importorskip("google.genai.errors")
24
+ from google.genai.errors import ClientError
25
+
26
+ exc = ClientError(
27
+ 403,
28
+ {"error": {"message": "Permission denied on billing account", "status": "PERMISSION_DENIED"}},
29
+ None,
30
+ )
31
+ assert _gemini_cache_delete_already_gone(exc) is False
32
+
33
+
34
+ def test_cache_delete_harmless_404() -> None:
35
+ pytest.importorskip("google.genai.errors")
36
+ from google.genai.errors import ClientError
37
+
38
+ exc = ClientError(404, {"error": {"message": "Not found", "status": "NOT_FOUND"}}, None)
39
+ assert _gemini_cache_delete_already_gone(exc) is True
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes