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.
- {gemcode-0.3.108/src/gemcode.egg-info → gemcode-0.3.110}/PKG-INFO +1 -1
- {gemcode-0.3.108 → gemcode-0.3.110}/pyproject.toml +1 -1
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/kaira_daemon.py +55 -5
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/repl_commands.py +3 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/repl_slash.py +91 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/session_runtime.py +60 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tui/scrollback.py +56 -0
- {gemcode-0.3.108 → gemcode-0.3.110/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode.egg-info/SOURCES.txt +1 -0
- gemcode-0.3.110/tests/test_session_runtime_cache.py +39 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/LICENSE +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/MANIFEST.in +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/README.md +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/setup.cfg +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/agent.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/autotune.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/cli.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/config.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/hooks.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/kaira_client.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/kaira_ipc.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/kaira_job_store.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/learning.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/org.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/pricing.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/refine.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/rules.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/session_store.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/skills.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/org_tools.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/user_choice.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/version.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/wal.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/web/sse_adapter.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_add_dir.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_credentials.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_paths.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_permissions.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_skills.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_tools.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.3.108 → gemcode-0.3.110}/tests/test_workspace_hints.py +0 -0
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|