gemcode 0.4.12__tar.gz → 0.4.14__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.4.12/src/gemcode.egg-info → gemcode-0.4.14}/PKG-INFO +3 -1
- {gemcode-0.4.12 → gemcode-0.4.14}/README.md +2 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/pyproject.toml +1 -1
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/agent_habits.py +5 -3
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/agent_mesh.py +110 -60
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/fleet_reports.py +57 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/repl_commands.py +1 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/repl_slash.py +55 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tui/scrollback.py +79 -0
- {gemcode-0.4.12 → gemcode-0.4.14/src/gemcode.egg-info}/PKG-INFO +3 -1
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_agent_mesh.py +62 -1
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_fleet_reports.py +22 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/LICENSE +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/MANIFEST.in +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/setup.cfg +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/__init__.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/__main__.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/a2a_bridge.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/agent.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/agent_intelligence.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/agent_triggers.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/audit.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/automations.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/autotune.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/cli.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/codebase_awareness.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/compaction.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/config.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/credentials.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/delegation_learning.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/event_bus.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/hooks.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/interactions.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/invoke.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/kaira_client.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/kaira_ipc.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/kaira_job_store.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/learning.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/limits.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/org.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/paths.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/permissions.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/pricing.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/config.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/refine.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/rules.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/self_healing.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/session_store.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/skills.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/thinking.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tool_synthesis.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/automations_tools.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/org_tools.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/user_choice.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/trust.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/version.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/vertex.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/wal.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/web/sse_adapter.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode.egg-info/SOURCES.txt +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_add_dir.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_agent_habits.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_autocompact.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_automations.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_capability_routing.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_cli_init.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_context_budget.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_context_warning.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_credentials.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_event_bus.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_kaira_ipc_paths.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_modality_tools.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_model_errors.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_model_routing.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_paths.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_permissions.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_repl_commands.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_repl_slash.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_session_runtime_cache.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_skills.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_slash_commands.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_thinking_config.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_token_budget.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_tools.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.4.12 → gemcode-0.4.14}/tests/test_workspace_hints.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gemcode
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.14
|
|
4
4
|
Summary: Local-first coding agent on Google Gemini + ADK
|
|
5
5
|
Author: GemCode Contributors
|
|
6
6
|
License: Apache License
|
|
@@ -398,6 +398,8 @@ The LLM calls `transfer_to_agent(agent_name='verifier')` → ADK routes natively
|
|
|
398
398
|
|
|
399
399
|
For background work: `org_delegate("kaira", "run tests")` → mesh runs kaira as a full GemCode session → result flows back via fleet reports.
|
|
400
400
|
|
|
401
|
+
**Mesh / habits reliability:** overlapping jobs for the **same** org member serialize writes to that agent’s durable SQLite session so ADK does not raise “stale session” errors. Mesh workers default to **unattended tool approval** (env **`GEMCODE_MESH_WORKER_UNATTENDED`**, on by default) so background shell / delegation / writes do not block the main TUI on HITL. Fleet auto-continue digests the inbox after **assistant** turns; while idle at ❯, use **`/fleet`** / **`/fleet show`** or any message — see **`GEMCODE_FLEET_TUI_NOTIFY`** in [`../docs/configuration.md`](../docs/configuration.md). See [`../docs/orchestration.md`](../docs/orchestration.md).
|
|
402
|
+
|
|
401
403
|
Docs:
|
|
402
404
|
- [`../docs/orchestration.md`](../docs/orchestration.md)
|
|
403
405
|
|
|
@@ -205,6 +205,8 @@ The LLM calls `transfer_to_agent(agent_name='verifier')` → ADK routes natively
|
|
|
205
205
|
|
|
206
206
|
For background work: `org_delegate("kaira", "run tests")` → mesh runs kaira as a full GemCode session → result flows back via fleet reports.
|
|
207
207
|
|
|
208
|
+
**Mesh / habits reliability:** overlapping jobs for the **same** org member serialize writes to that agent’s durable SQLite session so ADK does not raise “stale session” errors. Mesh workers default to **unattended tool approval** (env **`GEMCODE_MESH_WORKER_UNATTENDED`**, on by default) so background shell / delegation / writes do not block the main TUI on HITL. Fleet auto-continue digests the inbox after **assistant** turns; while idle at ❯, use **`/fleet`** / **`/fleet show`** or any message — see **`GEMCODE_FLEET_TUI_NOTIFY`** in [`../docs/configuration.md`](../docs/configuration.md). See [`../docs/orchestration.md`](../docs/orchestration.md).
|
|
209
|
+
|
|
208
210
|
Docs:
|
|
209
211
|
- [`../docs/orchestration.md`](../docs/orchestration.md)
|
|
210
212
|
|
|
@@ -324,9 +324,11 @@ def make_habits_tools(cfg: GemCodeConfig) -> list:
|
|
|
324
324
|
They fire as long as GemCode is open (REPL/TUI session).
|
|
325
325
|
|
|
326
326
|
Results go to the fleet inbox (.gemcode/fleet_reports.jsonl). Fleet auto-continue
|
|
327
|
-
(GEMCODE_FLEET_REPORTS_AUTO_CONTINUE, default on)
|
|
328
|
-
reply
|
|
329
|
-
|
|
327
|
+
(GEMCODE_FLEET_REPORTS_AUTO_CONTINUE, default on) runs digest turns **after each assistant
|
|
328
|
+
reply** when the inbox still has entries — it does **not** wake the model while the TUI is
|
|
329
|
+
idle at the prompt. While waiting at ❯, use **`/fleet`** (digest) or **`/fleet show`** (peek),
|
|
330
|
+
or send any message; the TUI also prints a throttled hint when mesh jobs finish
|
|
331
|
+
(GEMCODE_FLEET_TUI_NOTIFY).
|
|
330
332
|
|
|
331
333
|
Args:
|
|
332
334
|
name: Unique name for this habit (e.g., "test-watch", "nightly-audit").
|
|
@@ -24,7 +24,7 @@ from dataclasses import dataclass, field
|
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
from typing import Any
|
|
26
26
|
|
|
27
|
-
from gemcode.config import GemCodeConfig
|
|
27
|
+
from gemcode.config import GemCodeConfig, _truthy_env
|
|
28
28
|
from gemcode.event_bus import BusMessage, EventBus, get_bus
|
|
29
29
|
from gemcode.org import OrgMember, find_member, list_members, resolve_fleet_root
|
|
30
30
|
|
|
@@ -44,6 +44,21 @@ class AgentJob:
|
|
|
44
44
|
created_ms: int = field(default_factory=lambda: int(time.time() * 1000))
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def _apply_mesh_worker_unattended_policy(cfg: GemCodeConfig) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Mesh jobs run on a background thread: there is no human at a separate keyboard.
|
|
50
|
+
When enabled (default), treat the worker like ``--yes`` and disable in-run HITL so
|
|
51
|
+
shell / org_delegate / write tools from habits and workers do not block the main TUI.
|
|
52
|
+
|
|
53
|
+
Opt out: ``GEMCODE_MESH_WORKER_UNATTENDED=0`` (mesh workers then inherit the
|
|
54
|
+
manager's ``yes_to_all`` / ``interactive_permission_ask`` as before).
|
|
55
|
+
"""
|
|
56
|
+
if not _truthy_env("GEMCODE_MESH_WORKER_UNATTENDED", default=True):
|
|
57
|
+
return
|
|
58
|
+
cfg.yes_to_all = True
|
|
59
|
+
cfg.interactive_permission_ask = False
|
|
60
|
+
|
|
61
|
+
|
|
47
62
|
class AgentMesh:
|
|
48
63
|
"""
|
|
49
64
|
In-process agent orchestration mesh.
|
|
@@ -72,6 +87,12 @@ class AgentMesh:
|
|
|
72
87
|
# _stop is created lazily in the background loop
|
|
73
88
|
self._stop_flag = False
|
|
74
89
|
|
|
90
|
+
# Serialize ADK SqliteSessionService writes per (db, user_id, session_id).
|
|
91
|
+
# Concurrent mesh jobs for the same agent share one session row; interleaved
|
|
92
|
+
# append_event calls hit optimistic-lock checks and raise "stale session".
|
|
93
|
+
self._mesh_sqlite_session_locks: dict[str, asyncio.Lock] = {}
|
|
94
|
+
self._mesh_sqlite_session_locks_guard = asyncio.Lock()
|
|
95
|
+
|
|
75
96
|
# Subscribe to org.assign messages on the bus
|
|
76
97
|
self._bus.subscribe(
|
|
77
98
|
topic="org.assign",
|
|
@@ -115,6 +136,22 @@ class AgentMesh:
|
|
|
115
136
|
def bus(self) -> EventBus:
|
|
116
137
|
return self._bus
|
|
117
138
|
|
|
139
|
+
async def _lock_sqlite_session(
|
|
140
|
+
self,
|
|
141
|
+
*,
|
|
142
|
+
db_path: Path,
|
|
143
|
+
user_id: str,
|
|
144
|
+
session_id: str,
|
|
145
|
+
) -> asyncio.Lock:
|
|
146
|
+
"""Return the asyncio lock for this ADK SQLite session (create if needed)."""
|
|
147
|
+
key = f"{db_path.resolve()}\0{user_id}\0{session_id}"
|
|
148
|
+
async with self._mesh_sqlite_session_locks_guard:
|
|
149
|
+
lock = self._mesh_sqlite_session_locks.get(key)
|
|
150
|
+
if lock is None:
|
|
151
|
+
lock = asyncio.Lock()
|
|
152
|
+
self._mesh_sqlite_session_locks[key] = lock
|
|
153
|
+
return lock
|
|
154
|
+
|
|
118
155
|
def start(self) -> None:
|
|
119
156
|
"""Start the mesh in a dedicated background thread with its own event loop.
|
|
120
157
|
|
|
@@ -354,18 +391,22 @@ class AgentMesh:
|
|
|
354
391
|
duration_ms = int(time.time() * 1000) - start_ms
|
|
355
392
|
|
|
356
393
|
# Publish completion
|
|
394
|
+
_jr_finished: dict[str, Any] = {
|
|
395
|
+
"job_id": job.job_id,
|
|
396
|
+
"session_id": job.session_id,
|
|
397
|
+
"status": "finished",
|
|
398
|
+
"member": job.member_name,
|
|
399
|
+
"report": result_text[:8000],
|
|
400
|
+
"duration_ms": duration_ms,
|
|
401
|
+
}
|
|
402
|
+
_hm0 = job.meta.get("habit") if isinstance(job.meta, dict) else None
|
|
403
|
+
if isinstance(_hm0, dict):
|
|
404
|
+
_jr_finished["habit"] = _hm0
|
|
357
405
|
await self._bus.publish(BusMessage(
|
|
358
406
|
topic="job.report",
|
|
359
407
|
from_addr=job.member_name or "mesh",
|
|
360
408
|
to_addr="manager",
|
|
361
|
-
payload=
|
|
362
|
-
"job_id": job.job_id,
|
|
363
|
-
"session_id": job.session_id,
|
|
364
|
-
"status": "finished",
|
|
365
|
-
"member": job.member_name,
|
|
366
|
-
"report": result_text[:8000],
|
|
367
|
-
"duration_ms": duration_ms,
|
|
368
|
-
},
|
|
409
|
+
payload=_jr_finished,
|
|
369
410
|
))
|
|
370
411
|
|
|
371
412
|
# Also publish org.report if this was an org delegation
|
|
@@ -422,17 +463,21 @@ class AgentMesh:
|
|
|
422
463
|
job.status = "failed"
|
|
423
464
|
job.error = f"{type(e).__name__}: {e}"
|
|
424
465
|
|
|
466
|
+
_jr_failed: dict[str, Any] = {
|
|
467
|
+
"job_id": job.job_id,
|
|
468
|
+
"session_id": job.session_id,
|
|
469
|
+
"status": "failed",
|
|
470
|
+
"member": job.member_name,
|
|
471
|
+
"error": job.error,
|
|
472
|
+
}
|
|
473
|
+
_hm1 = job.meta.get("habit") if isinstance(job.meta, dict) else None
|
|
474
|
+
if isinstance(_hm1, dict):
|
|
475
|
+
_jr_failed["habit"] = _hm1
|
|
425
476
|
await self._bus.publish(BusMessage(
|
|
426
477
|
topic="job.report",
|
|
427
478
|
from_addr=job.member_name or "mesh",
|
|
428
479
|
to_addr="manager",
|
|
429
|
-
payload=
|
|
430
|
-
"job_id": job.job_id,
|
|
431
|
-
"session_id": job.session_id,
|
|
432
|
-
"status": "failed",
|
|
433
|
-
"member": job.member_name,
|
|
434
|
-
"error": job.error,
|
|
435
|
-
},
|
|
480
|
+
payload=_jr_failed,
|
|
436
481
|
))
|
|
437
482
|
|
|
438
483
|
# Persist failure to fleet reports
|
|
@@ -475,14 +520,17 @@ class AgentMesh:
|
|
|
475
520
|
This means agents build up context over time — they remember past tasks,
|
|
476
521
|
learn from their history, and maintain their own notes.
|
|
477
522
|
"""
|
|
523
|
+
import hashlib
|
|
524
|
+
|
|
478
525
|
from gemcode.invoke import run_turn
|
|
479
|
-
from gemcode.session_runtime import create_runner
|
|
526
|
+
from gemcode.session_runtime import create_runner, session_db_path
|
|
480
527
|
from gemcode.capability_routing import apply_capability_routing
|
|
481
528
|
from gemcode.model_routing import pick_effective_model
|
|
482
529
|
|
|
483
530
|
# Resolve the agent's workspace as their project root
|
|
484
531
|
# This gives them their own .gemcode/ directory, sessions, memory, etc.
|
|
485
532
|
agent_cfg = copy.deepcopy(self.cfg)
|
|
533
|
+
_apply_mesh_worker_unattended_policy(agent_cfg)
|
|
486
534
|
fleet_root = resolve_fleet_root(self.cfg.project_root)
|
|
487
535
|
|
|
488
536
|
if job.member_name:
|
|
@@ -507,54 +555,56 @@ class AgentMesh:
|
|
|
507
555
|
parent_tools = self._build_parent_access_tools(fleet_root)
|
|
508
556
|
all_extra = (mesh_tools or []) + (parent_tools or [])
|
|
509
557
|
|
|
510
|
-
#
|
|
511
|
-
|
|
512
|
-
|
|
558
|
+
# Stable session ID per agent (same id across jobs = durable history).
|
|
559
|
+
stable_session_id = (job.session_id or "").strip()
|
|
560
|
+
if job.member_name:
|
|
561
|
+
stable_session_id = f"agent_{hashlib.sha256(job.member_name.encode()).hexdigest()[:12]}"
|
|
562
|
+
mesh_user_id = job.member_name or "mesh"
|
|
513
563
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
stable_session_id = job.session_id
|
|
518
|
-
if job.member_name:
|
|
519
|
-
# Stable session = agent name hash (persists across jobs)
|
|
520
|
-
import hashlib
|
|
521
|
-
stable_session_id = f"agent_{hashlib.sha256(job.member_name.encode()).hexdigest()[:12]}"
|
|
522
|
-
|
|
523
|
-
# Execute the turn with full power
|
|
524
|
-
max_calls = min(int(self.cfg.max_llm_calls or 128), 128)
|
|
525
|
-
events = await run_turn(
|
|
526
|
-
runner,
|
|
527
|
-
user_id=job.member_name or "mesh",
|
|
564
|
+
sqlite_lock = await self._lock_sqlite_session(
|
|
565
|
+
db_path=session_db_path(agent_cfg),
|
|
566
|
+
user_id=mesh_user_id,
|
|
528
567
|
session_id=stable_session_id,
|
|
529
|
-
|
|
530
|
-
max_llm_calls=max_calls,
|
|
531
|
-
cfg=agent_cfg,
|
|
532
|
-
consume_fleet_reports=False,
|
|
533
|
-
)
|
|
568
|
+
)
|
|
534
569
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if not ev.content or not ev.content.parts:
|
|
540
|
-
continue
|
|
541
|
-
if getattr(ev, "author", None) == "user":
|
|
542
|
-
continue
|
|
543
|
-
for part in ev.content.parts:
|
|
544
|
-
t = getattr(part, "text", None)
|
|
545
|
-
is_thought = getattr(part, "thought", None)
|
|
546
|
-
if isinstance(t, str) and t.strip() and not is_thought:
|
|
547
|
-
parts.append(t)
|
|
548
|
-
except Exception:
|
|
549
|
-
continue
|
|
570
|
+
async with sqlite_lock:
|
|
571
|
+
# One runner + turn at a time per session row — avoids ADK "stale session"
|
|
572
|
+
# when habits or overlapping delegations write events concurrently.
|
|
573
|
+
runner = create_runner(agent_cfg, extra_tools=all_extra or None)
|
|
550
574
|
|
|
551
|
-
return "".join(parts).strip() or "(no output)"
|
|
552
|
-
finally:
|
|
553
|
-
# Clean up the runner
|
|
554
575
|
try:
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
576
|
+
max_calls = min(int(self.cfg.max_llm_calls or 128), 128)
|
|
577
|
+
events = await run_turn(
|
|
578
|
+
runner,
|
|
579
|
+
user_id=mesh_user_id,
|
|
580
|
+
session_id=stable_session_id,
|
|
581
|
+
prompt=job.prompt,
|
|
582
|
+
max_llm_calls=max_calls,
|
|
583
|
+
cfg=agent_cfg,
|
|
584
|
+
consume_fleet_reports=False,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
parts: list[str] = []
|
|
588
|
+
for ev in events:
|
|
589
|
+
try:
|
|
590
|
+
if not ev.content or not ev.content.parts:
|
|
591
|
+
continue
|
|
592
|
+
if getattr(ev, "author", None) == "user":
|
|
593
|
+
continue
|
|
594
|
+
for part in ev.content.parts:
|
|
595
|
+
t = getattr(part, "text", None)
|
|
596
|
+
is_thought = getattr(part, "thought", None)
|
|
597
|
+
if isinstance(t, str) and t.strip() and not is_thought:
|
|
598
|
+
parts.append(t)
|
|
599
|
+
except Exception:
|
|
600
|
+
continue
|
|
601
|
+
|
|
602
|
+
return "".join(parts).strip() or "(no output)"
|
|
603
|
+
finally:
|
|
604
|
+
try:
|
|
605
|
+
await runner.close()
|
|
606
|
+
except Exception:
|
|
607
|
+
pass
|
|
558
608
|
|
|
559
609
|
def _build_parent_access_tools(self, fleet_root: Path) -> list:
|
|
560
610
|
"""
|
|
@@ -366,3 +366,60 @@ def drain_for_prompt(project_root: Path, *, max_chars: int | None = None) -> str
|
|
|
366
366
|
"increase GEMCODE_FLEET_REPORTS_MAX_CHARS to drain more per turn)"
|
|
367
367
|
)
|
|
368
368
|
return header + body
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def preview_fleet_inbox(project_root: Path, *, max_chars: int | None = None) -> str:
|
|
372
|
+
"""
|
|
373
|
+
Format pending fleet inbox lines without draining the file (for ``/fleet show``).
|
|
374
|
+
"""
|
|
375
|
+
if max_chars is None:
|
|
376
|
+
try:
|
|
377
|
+
max_chars = int(os.environ.get("GEMCODE_FLEET_REPORTS_MAX_CHARS", "14000"))
|
|
378
|
+
except Exception:
|
|
379
|
+
max_chars = 14_000
|
|
380
|
+
try:
|
|
381
|
+
from gemcode.org import resolve_fleet_root
|
|
382
|
+
|
|
383
|
+
fleet_root = resolve_fleet_root(project_root)
|
|
384
|
+
except Exception:
|
|
385
|
+
fleet_root = project_root
|
|
386
|
+
p = _fleet_reports_path(fleet_root)
|
|
387
|
+
if not p.is_file():
|
|
388
|
+
return "(no `.gemcode/fleet_reports.jsonl` yet — background jobs append here when they finish)"
|
|
389
|
+
try:
|
|
390
|
+
raw = p.read_text(encoding="utf-8", errors="replace")
|
|
391
|
+
except Exception:
|
|
392
|
+
return "(could not read fleet_reports.jsonl)"
|
|
393
|
+
if not raw.strip():
|
|
394
|
+
return "(fleet inbox is empty)"
|
|
395
|
+
|
|
396
|
+
lines_in = [ln.strip() for ln in raw.splitlines() if ln.strip()]
|
|
397
|
+
blocks: list[str] = []
|
|
398
|
+
total = 0
|
|
399
|
+
truncated = False
|
|
400
|
+
for line in lines_in:
|
|
401
|
+
try:
|
|
402
|
+
rec = json.loads(line)
|
|
403
|
+
except Exception:
|
|
404
|
+
continue
|
|
405
|
+
if not isinstance(rec, dict):
|
|
406
|
+
continue
|
|
407
|
+
b = _format_record(rec)
|
|
408
|
+
if not b:
|
|
409
|
+
continue
|
|
410
|
+
need = len(b) + 2
|
|
411
|
+
if total + need > max_chars:
|
|
412
|
+
truncated = True
|
|
413
|
+
break
|
|
414
|
+
blocks.append(b)
|
|
415
|
+
total += need
|
|
416
|
+
|
|
417
|
+
if not blocks:
|
|
418
|
+
return "(fleet inbox has no readable entries)"
|
|
419
|
+
header = "Fleet / agent reports (preview — inbox not cleared):\n\n"
|
|
420
|
+
body = "\n\n".join(blocks)
|
|
421
|
+
if truncated:
|
|
422
|
+
body += (
|
|
423
|
+
"\n\n… (truncated; increase GEMCODE_FLEET_REPORTS_MAX_CHARS or run /fleet show after /fleet digest)"
|
|
424
|
+
)
|
|
425
|
+
return header + body
|
|
@@ -236,6 +236,7 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
|
|
|
236
236
|
("runtime", "Fleet socket status · gemcode runtime · attach/connect"),
|
|
237
237
|
("bus", "Runtime bus — send/publish lightweight messages over IPC"),
|
|
238
238
|
("inbox", "Bus inbox filters for this UI (to/topics)"),
|
|
239
|
+
("fleet", "Fleet inbox — /fleet show | digest (habits / mesh reports)"),
|
|
239
240
|
("agent", "Create/manage a child agent workspace (folder + registry)"),
|
|
240
241
|
# NOTE: /org and /delegate are deprecated aliases; keep working but do not list.
|
|
241
242
|
("limits", "Execution limits (calls, context, …)"),
|
|
@@ -91,6 +91,61 @@ async def process_repl_slash(
|
|
|
91
91
|
out()
|
|
92
92
|
return ReplSlashResult(skip_model_turn=True)
|
|
93
93
|
|
|
94
|
+
# ── /fleet (fleet_reports.jsonl — habits / mesh without waiting for a model turn) ─
|
|
95
|
+
if name in ("fleet", "fleet_reports", "reports"):
|
|
96
|
+
from gemcode.fleet_reports import (
|
|
97
|
+
fleet_digest_prompt,
|
|
98
|
+
has_pending_fleet_reports,
|
|
99
|
+
inject_enabled,
|
|
100
|
+
preview_fleet_inbox,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
object.__setattr__(cfg, "_fleet_auto_chain", 0)
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
sub = (sc.args or "").strip()
|
|
109
|
+
sub_l = sub.lower()
|
|
110
|
+
first = sub_l.split()[0] if sub_l else ""
|
|
111
|
+
|
|
112
|
+
if sub_l in ("help", "?") or (not sub_l and not has_pending_fleet_reports(cfg.project_root)):
|
|
113
|
+
out("Fleet inbox (`.gemcode/fleet_reports.jsonl` at fleet root):")
|
|
114
|
+
out(" /fleet If reports are pending: run a digest turn (drains inbox into the model).")
|
|
115
|
+
out(" /fleet digest Same, when you want to force digest.")
|
|
116
|
+
out(" /fleet show Print pending lines without draining (peek only).")
|
|
117
|
+
out("While the TUI is idle at ❯, auto-continue only runs after an assistant reply;")
|
|
118
|
+
out("mesh/habit completions still land in the inbox — use /fleet or any message to drain.")
|
|
119
|
+
if not inject_enabled():
|
|
120
|
+
out(" (GEMCODE_FLEET_REPORTS_INJECT=0 — inbox is not written.)")
|
|
121
|
+
out()
|
|
122
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
123
|
+
|
|
124
|
+
if sub_l in ("show", "peek", "cat") or first in ("show", "peek", "cat"):
|
|
125
|
+
out(preview_fleet_inbox(cfg.project_root))
|
|
126
|
+
out()
|
|
127
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
128
|
+
|
|
129
|
+
if sub_l in ("digest", "summarize", "sum") or first in ("digest", "summarize", "sum"):
|
|
130
|
+
if not has_pending_fleet_reports(cfg.project_root):
|
|
131
|
+
out("[fleet] no pending reports in inbox.")
|
|
132
|
+
out()
|
|
133
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
134
|
+
return ReplSlashResult(model_prompt=fleet_digest_prompt())
|
|
135
|
+
|
|
136
|
+
if not sub_l:
|
|
137
|
+
if has_pending_fleet_reports(cfg.project_root):
|
|
138
|
+
return ReplSlashResult(model_prompt=fleet_digest_prompt())
|
|
139
|
+
out("[fleet] no pending reports. Try `/fleet help`.")
|
|
140
|
+
out()
|
|
141
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
142
|
+
|
|
143
|
+
if not has_pending_fleet_reports(cfg.project_root):
|
|
144
|
+
out("[fleet] no pending reports in inbox.")
|
|
145
|
+
out()
|
|
146
|
+
return ReplSlashResult(skip_model_turn=True)
|
|
147
|
+
return ReplSlashResult(model_prompt=fleet_digest_prompt())
|
|
148
|
+
|
|
94
149
|
# ── /attach (queue files for the next user message: PDF, images, audio, …) ─
|
|
95
150
|
if name in ("attach", "file", "image", "img"):
|
|
96
151
|
raw_i = (sc.args or "").strip()
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
|
+
import time
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
|
|
9
10
|
from google.adk.agents.run_config import RunConfig
|
|
@@ -278,6 +279,84 @@ async def run_gemcode_scrollback_tui(
|
|
|
278
279
|
get_cfg=lambda: cfg,
|
|
279
280
|
)
|
|
280
281
|
|
|
282
|
+
# When mesh/habit jobs finish, fleet_reports.jsonl updates while we are idle at ❯.
|
|
283
|
+
# Auto-continue only runs after an *assistant* turn, so print a throttled hint.
|
|
284
|
+
_tui_loop = asyncio.get_running_loop()
|
|
285
|
+
_last_fleet_notify_s = [0.0]
|
|
286
|
+
|
|
287
|
+
def _on_mesh_job_report_notify(msg: object) -> None:
|
|
288
|
+
if os.environ.get("GEMCODE_FLEET_TUI_NOTIFY", "1").strip().lower() in (
|
|
289
|
+
"0", "false", "no", "off",
|
|
290
|
+
):
|
|
291
|
+
return
|
|
292
|
+
try:
|
|
293
|
+
from gemcode.event_bus import BusMessage
|
|
294
|
+
|
|
295
|
+
if not isinstance(msg, BusMessage) or msg.topic != "job.report":
|
|
296
|
+
return
|
|
297
|
+
pl = msg.payload if isinstance(msg.payload, dict) else {}
|
|
298
|
+
if str(pl.get("status") or "").strip().lower() not in ("finished", "failed"):
|
|
299
|
+
return
|
|
300
|
+
except Exception:
|
|
301
|
+
return
|
|
302
|
+
try:
|
|
303
|
+
min_gap = float(os.environ.get("GEMCODE_FLEET_TUI_NOTIFY_MIN_S", "8") or "8")
|
|
304
|
+
except Exception:
|
|
305
|
+
min_gap = 8.0
|
|
306
|
+
now = time.time()
|
|
307
|
+
if now - _last_fleet_notify_s[0] < min_gap:
|
|
308
|
+
return
|
|
309
|
+
_last_fleet_notify_s[0] = now
|
|
310
|
+
|
|
311
|
+
def _print_hint() -> None:
|
|
312
|
+
try:
|
|
313
|
+
mem = ""
|
|
314
|
+
if isinstance(msg.payload, dict):
|
|
315
|
+
mem = str(msg.payload.get("member") or "").strip()
|
|
316
|
+
habit = ""
|
|
317
|
+
if isinstance(msg.payload, dict):
|
|
318
|
+
hm = msg.payload.get("habit")
|
|
319
|
+
if isinstance(hm, dict):
|
|
320
|
+
habit = str(hm.get("name") or "").strip()
|
|
321
|
+
bits = []
|
|
322
|
+
if mem:
|
|
323
|
+
bits.append(mem)
|
|
324
|
+
if habit:
|
|
325
|
+
bits.append(f"habit:{habit}")
|
|
326
|
+
extra = f" ({' · '.join(bits)})" if bits else ""
|
|
327
|
+
line = (
|
|
328
|
+
f"{ansi.dim}[gemcode] Background job finished{extra} — "
|
|
329
|
+
f"/fleet to summarize inbox (any message also drains).{ansi.reset}"
|
|
330
|
+
)
|
|
331
|
+
if input_handler.is_interactive():
|
|
332
|
+
try:
|
|
333
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
334
|
+
|
|
335
|
+
with patch_stdout():
|
|
336
|
+
print(line, flush=True)
|
|
337
|
+
except Exception:
|
|
338
|
+
print(line, flush=True)
|
|
339
|
+
else:
|
|
340
|
+
print(line, flush=True)
|
|
341
|
+
except Exception:
|
|
342
|
+
pass
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
_tui_loop.call_soon_threadsafe(_print_hint)
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
from gemcode.event_bus import get_bus
|
|
351
|
+
|
|
352
|
+
get_bus().subscribe(
|
|
353
|
+
topic="job.report",
|
|
354
|
+
to_addr="manager",
|
|
355
|
+
callback=_on_mesh_job_report_notify,
|
|
356
|
+
)
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
|
|
281
360
|
# ── Optional: embed Kaira daemon in this TUI ─────────────────────────────
|
|
282
361
|
# This enables continuous background automation without requiring a second
|
|
283
362
|
# terminal. The TUI will also auto-subscribe to IPC events (below) so job
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gemcode
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.14
|
|
4
4
|
Summary: Local-first coding agent on Google Gemini + ADK
|
|
5
5
|
Author: GemCode Contributors
|
|
6
6
|
License: Apache License
|
|
@@ -398,6 +398,8 @@ The LLM calls `transfer_to_agent(agent_name='verifier')` → ADK routes natively
|
|
|
398
398
|
|
|
399
399
|
For background work: `org_delegate("kaira", "run tests")` → mesh runs kaira as a full GemCode session → result flows back via fleet reports.
|
|
400
400
|
|
|
401
|
+
**Mesh / habits reliability:** overlapping jobs for the **same** org member serialize writes to that agent’s durable SQLite session so ADK does not raise “stale session” errors. Mesh workers default to **unattended tool approval** (env **`GEMCODE_MESH_WORKER_UNATTENDED`**, on by default) so background shell / delegation / writes do not block the main TUI on HITL. Fleet auto-continue digests the inbox after **assistant** turns; while idle at ❯, use **`/fleet`** / **`/fleet show`** or any message — see **`GEMCODE_FLEET_TUI_NOTIFY`** in [`../docs/configuration.md`](../docs/configuration.md). See [`../docs/orchestration.md`](../docs/orchestration.md).
|
|
402
|
+
|
|
401
403
|
Docs:
|
|
402
404
|
- [`../docs/orchestration.md`](../docs/orchestration.md)
|
|
403
405
|
|
|
@@ -3,11 +3,18 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import os
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
9
10
|
|
|
10
|
-
from gemcode.agent_mesh import
|
|
11
|
+
from gemcode.agent_mesh import (
|
|
12
|
+
AgentMesh,
|
|
13
|
+
_apply_mesh_worker_unattended_policy,
|
|
14
|
+
ensure_mesh,
|
|
15
|
+
get_mesh,
|
|
16
|
+
reset_mesh,
|
|
17
|
+
)
|
|
11
18
|
from gemcode.config import GemCodeConfig
|
|
12
19
|
from gemcode.event_bus import BusMessage, get_bus, reset_bus
|
|
13
20
|
|
|
@@ -21,6 +28,26 @@ def _reset():
|
|
|
21
28
|
reset_bus()
|
|
22
29
|
|
|
23
30
|
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
async def test_lock_sqlite_session_same_key_reuses_lock(tmp_path: Path) -> None:
|
|
33
|
+
cfg = GemCodeConfig(project_root=tmp_path)
|
|
34
|
+
mesh = AgentMesh(cfg)
|
|
35
|
+
db = tmp_path / ".gemcode" / "sessions.sqlite"
|
|
36
|
+
a = await mesh._lock_sqlite_session(db_path=db, user_id="agent_x", session_id="sess1")
|
|
37
|
+
b = await mesh._lock_sqlite_session(db_path=db, user_id="agent_x", session_id="sess1")
|
|
38
|
+
assert a is b
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.asyncio
|
|
42
|
+
async def test_lock_sqlite_session_different_session_distinct_locks(tmp_path: Path) -> None:
|
|
43
|
+
cfg = GemCodeConfig(project_root=tmp_path)
|
|
44
|
+
mesh = AgentMesh(cfg)
|
|
45
|
+
db = tmp_path / ".gemcode" / "sessions.sqlite"
|
|
46
|
+
a = await mesh._lock_sqlite_session(db_path=db, user_id="agent_x", session_id="sess1")
|
|
47
|
+
b = await mesh._lock_sqlite_session(db_path=db, user_id="agent_x", session_id="sess2")
|
|
48
|
+
assert a is not b
|
|
49
|
+
|
|
50
|
+
|
|
24
51
|
def test_mesh_creation(tmp_path: Path) -> None:
|
|
25
52
|
cfg = GemCodeConfig(project_root=tmp_path)
|
|
26
53
|
mesh = ensure_mesh(cfg)
|
|
@@ -76,6 +103,40 @@ def test_mesh_bus_integration(tmp_path: Path) -> None:
|
|
|
76
103
|
assert received[0].payload["member"] == "test"
|
|
77
104
|
|
|
78
105
|
|
|
106
|
+
def test_apply_mesh_worker_unattended_default_on(tmp_path: Path) -> None:
|
|
107
|
+
cfg = GemCodeConfig(project_root=tmp_path)
|
|
108
|
+
cfg.yes_to_all = False
|
|
109
|
+
cfg.interactive_permission_ask = True
|
|
110
|
+
old = os.environ.get("GEMCODE_MESH_WORKER_UNATTENDED")
|
|
111
|
+
try:
|
|
112
|
+
os.environ.pop("GEMCODE_MESH_WORKER_UNATTENDED", None)
|
|
113
|
+
_apply_mesh_worker_unattended_policy(cfg)
|
|
114
|
+
finally:
|
|
115
|
+
if old is None:
|
|
116
|
+
os.environ.pop("GEMCODE_MESH_WORKER_UNATTENDED", None)
|
|
117
|
+
else:
|
|
118
|
+
os.environ["GEMCODE_MESH_WORKER_UNATTENDED"] = old
|
|
119
|
+
assert cfg.yes_to_all is True
|
|
120
|
+
assert cfg.interactive_permission_ask is False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_apply_mesh_worker_unattended_off_inherits_manager(tmp_path: Path) -> None:
|
|
124
|
+
cfg = GemCodeConfig(project_root=tmp_path)
|
|
125
|
+
cfg.yes_to_all = False
|
|
126
|
+
cfg.interactive_permission_ask = True
|
|
127
|
+
old = os.environ.get("GEMCODE_MESH_WORKER_UNATTENDED")
|
|
128
|
+
try:
|
|
129
|
+
os.environ["GEMCODE_MESH_WORKER_UNATTENDED"] = "0"
|
|
130
|
+
_apply_mesh_worker_unattended_policy(cfg)
|
|
131
|
+
finally:
|
|
132
|
+
if old is None:
|
|
133
|
+
os.environ.pop("GEMCODE_MESH_WORKER_UNATTENDED", None)
|
|
134
|
+
else:
|
|
135
|
+
os.environ["GEMCODE_MESH_WORKER_UNATTENDED"] = old
|
|
136
|
+
assert cfg.yes_to_all is False
|
|
137
|
+
assert cfg.interactive_permission_ask is True
|
|
138
|
+
|
|
139
|
+
|
|
79
140
|
def test_mesh_priority_ordering(tmp_path: Path) -> None:
|
|
80
141
|
"""Higher priority jobs should be dequeued first."""
|
|
81
142
|
cfg = GemCodeConfig(project_root=tmp_path)
|
|
@@ -12,6 +12,7 @@ from gemcode.fleet_reports import (
|
|
|
12
12
|
has_pending_fleet_reports,
|
|
13
13
|
inject_enabled,
|
|
14
14
|
maybe_append_org_report,
|
|
15
|
+
preview_fleet_inbox,
|
|
15
16
|
)
|
|
16
17
|
from gemcode.org import resolve_fleet_root
|
|
17
18
|
|
|
@@ -98,3 +99,24 @@ def test_inject_disabled_skips_append(tmp_path: Path, monkeypatch: pytest.Monkey
|
|
|
98
99
|
append_fleet_report(tmp_path, topic="org.report", payload={"status": "finished"})
|
|
99
100
|
p = tmp_path / ".gemcode" / "fleet_reports.jsonl"
|
|
100
101
|
assert not p.exists()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_preview_fleet_inbox_does_not_clear(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
105
|
+
monkeypatch.setenv("GEMCODE_FLEET_REPORTS_INJECT", "1")
|
|
106
|
+
(tmp_path / ".gemcode").mkdir()
|
|
107
|
+
maybe_append_org_report(
|
|
108
|
+
tmp_path,
|
|
109
|
+
{
|
|
110
|
+
"status": "finished",
|
|
111
|
+
"job_id": "j-preview",
|
|
112
|
+
"task": "t",
|
|
113
|
+
"member": {"name": "w"},
|
|
114
|
+
"result": {"report": "hello"},
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
p = tmp_path / ".gemcode" / "fleet_reports.jsonl"
|
|
118
|
+
assert p.read_text(encoding="utf-8").strip()
|
|
119
|
+
prev = preview_fleet_inbox(tmp_path)
|
|
120
|
+
assert "preview" in prev.lower() or "Fleet / agent reports" in prev
|
|
121
|
+
assert "hello" in prev
|
|
122
|
+
assert p.read_text(encoding="utf-8").strip()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|