gemcode 0.4.13__tar.gz → 0.4.15__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.13/src/gemcode.egg-info → gemcode-0.4.15}/PKG-INFO +2 -2
- {gemcode-0.4.13 → gemcode-0.4.15}/README.md +1 -1
- {gemcode-0.4.13 → gemcode-0.4.15}/pyproject.toml +1 -1
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/agent_habits.py +5 -3
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/agent_mesh.py +183 -64
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/fleet_reports.py +57 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/repl_commands.py +1 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/repl_slash.py +55 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tui/scrollback.py +79 -0
- {gemcode-0.4.13 → gemcode-0.4.15/src/gemcode.egg-info}/PKG-INFO +2 -2
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_agent_mesh.py +86 -35
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_fleet_reports.py +22 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/LICENSE +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/MANIFEST.in +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/setup.cfg +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/__init__.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/__main__.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/a2a_bridge.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/agent.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/agent_intelligence.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/agent_triggers.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/audit.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/automations.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/autotune.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/cli.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/codebase_awareness.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/compaction.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/config.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/credentials.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/delegation_learning.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/event_bus.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/hooks.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/interactions.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/invoke.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/kaira_client.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/kaira_ipc.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/kaira_job_store.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/learning.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/limits.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/org.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/paths.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/permissions.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/pricing.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/config.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/refine.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/rules.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/self_healing.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/session_store.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/skills.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/thinking.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tool_synthesis.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/automations_tools.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/org_tools.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/user_choice.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/trust.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/version.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/vertex.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/wal.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/web/sse_adapter.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode.egg-info/SOURCES.txt +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_add_dir.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_agent_habits.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_autocompact.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_automations.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_capability_routing.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_cli_init.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_context_budget.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_context_warning.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_credentials.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_event_bus.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_kaira_ipc_paths.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_modality_tools.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_model_errors.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_model_routing.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_paths.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_permissions.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_repl_commands.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_repl_slash.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_session_runtime_cache.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_skills.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_slash_commands.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_thinking_config.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_token_budget.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_tools.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.4.13 → gemcode-0.4.15}/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.15
|
|
4
4
|
Summary: Local-first coding agent on Google Gemini + ADK
|
|
5
5
|
Author: GemCode Contributors
|
|
6
6
|
License: Apache License
|
|
@@ -398,7 +398,7 @@ 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.
|
|
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
402
|
|
|
403
403
|
Docs:
|
|
404
404
|
- [`../docs/orchestration.md`](../docs/orchestration.md)
|
|
@@ -205,7 +205,7 @@ 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.
|
|
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
209
|
|
|
210
210
|
Docs:
|
|
211
211
|
- [`../docs/orchestration.md`](../docs/orchestration.md)
|
|
@@ -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").
|
|
@@ -16,8 +16,10 @@ Slash **`/agent assign`** / **`trigger`** publish `org.assign` over IPC when the
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import asyncio
|
|
19
|
+
import concurrent.futures
|
|
19
20
|
import copy
|
|
20
21
|
import os
|
|
22
|
+
import threading
|
|
21
23
|
import time
|
|
22
24
|
import uuid
|
|
23
25
|
from dataclasses import dataclass, field
|
|
@@ -42,6 +44,8 @@ class AgentJob:
|
|
|
42
44
|
result: str = ""
|
|
43
45
|
error: str = ""
|
|
44
46
|
created_ms: int = field(default_factory=lambda: int(time.time() * 1000))
|
|
47
|
+
# When set, completed with this AgentJob from the mesh thread (cross-thread wait).
|
|
48
|
+
completion_future: concurrent.futures.Future | None = None
|
|
45
49
|
|
|
46
50
|
|
|
47
51
|
def _apply_mesh_worker_unattended_policy(cfg: GemCodeConfig) -> None:
|
|
@@ -80,6 +84,11 @@ class AgentMesh:
|
|
|
80
84
|
self._sem = asyncio.Semaphore(self.max_concurrency)
|
|
81
85
|
self._running: dict[str, asyncio.Task] = {}
|
|
82
86
|
self._completed: list[AgentJob] = []
|
|
87
|
+
self._completed_lock = threading.Lock()
|
|
88
|
+
self._enqueue_lock = threading.Lock()
|
|
89
|
+
self._bg_thread_ident: int | None = None
|
|
90
|
+
# >0 while executing a mesh job on the background loop (nested delegate avoids deadlock).
|
|
91
|
+
self._mesh_job_depth: int = 0
|
|
83
92
|
self._scheduler_task: asyncio.Task | None = None
|
|
84
93
|
self._stop: asyncio.Event | None = None # Created in background loop
|
|
85
94
|
self._bg_thread: "threading.Thread | None" = None
|
|
@@ -172,6 +181,7 @@ class AgentMesh:
|
|
|
172
181
|
"""Background thread entry: create a new event loop and run the scheduler."""
|
|
173
182
|
self._bg_loop = asyncio.new_event_loop()
|
|
174
183
|
asyncio.set_event_loop(self._bg_loop)
|
|
184
|
+
self._bg_thread_ident = threading.current_thread().ident
|
|
175
185
|
try:
|
|
176
186
|
self._bg_loop.run_until_complete(self._bg_main())
|
|
177
187
|
except Exception:
|
|
@@ -190,7 +200,9 @@ class AgentMesh:
|
|
|
190
200
|
self._stop = asyncio.Event() # Create in the correct loop
|
|
191
201
|
|
|
192
202
|
# Start all sub-systems in this loop
|
|
193
|
-
|
|
203
|
+
# Tests can set PYTEST_GEMCODE_MESH_SCHEDULER=0 to inspect the queue without workers consuming it.
|
|
204
|
+
if os.environ.get("PYTEST_GEMCODE_MESH_SCHEDULER", "").strip() != "0":
|
|
205
|
+
self._scheduler_task = asyncio.create_task(self._scheduler_loop())
|
|
194
206
|
|
|
195
207
|
if self._trigger_engine is not None:
|
|
196
208
|
self._trigger_engine.start()
|
|
@@ -217,6 +229,40 @@ class AgentMesh:
|
|
|
217
229
|
except Exception:
|
|
218
230
|
pass
|
|
219
231
|
|
|
232
|
+
def _is_on_mesh_thread(self) -> bool:
|
|
233
|
+
ident = threading.current_thread().ident
|
|
234
|
+
return self._bg_thread_ident is not None and ident == self._bg_thread_ident
|
|
235
|
+
|
|
236
|
+
def _wait_bg_loop_ready(self, timeout_s: float = 5.0) -> bool:
|
|
237
|
+
"""Wait until the background thread has assigned ``_bg_loop``."""
|
|
238
|
+
deadline = time.time() + timeout_s
|
|
239
|
+
while time.time() < deadline:
|
|
240
|
+
if self._bg_loop is not None:
|
|
241
|
+
return True
|
|
242
|
+
if self._bg_thread is not None and not self._bg_thread.is_alive():
|
|
243
|
+
return False
|
|
244
|
+
time.sleep(0.001)
|
|
245
|
+
return self._bg_loop is not None
|
|
246
|
+
|
|
247
|
+
def wait_for_pending_enqueues(self, timeout: float = 5.0) -> None:
|
|
248
|
+
"""
|
|
249
|
+
Block until callbacks scheduled with ``call_soon_threadsafe`` (for enqueues
|
|
250
|
+
from other threads) have run on the mesh loop. Useful in tests.
|
|
251
|
+
"""
|
|
252
|
+
if self._bg_loop is None:
|
|
253
|
+
return
|
|
254
|
+
fut: concurrent.futures.Future[None] = concurrent.futures.Future()
|
|
255
|
+
|
|
256
|
+
def _poke() -> None:
|
|
257
|
+
if not fut.done():
|
|
258
|
+
try:
|
|
259
|
+
fut.set_result(None)
|
|
260
|
+
except Exception:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
self._bg_loop.call_soon_threadsafe(_poke)
|
|
264
|
+
fut.result(timeout=timeout)
|
|
265
|
+
|
|
220
266
|
def enqueue(
|
|
221
267
|
self,
|
|
222
268
|
*,
|
|
@@ -225,10 +271,13 @@ class AgentMesh:
|
|
|
225
271
|
session_id: str = "",
|
|
226
272
|
member_name: str = "",
|
|
227
273
|
meta: dict[str, Any] | None = None,
|
|
274
|
+
completion_future: concurrent.futures.Future | None = None,
|
|
228
275
|
) -> str:
|
|
229
|
-
"""Enqueue a job and return its job_id.
|
|
276
|
+
"""Enqueue a job and return its job_id. Safe to call from any thread."""
|
|
230
277
|
job_id = f"mesh_{uuid.uuid4().hex[:10]}"
|
|
231
|
-
self.
|
|
278
|
+
with self._enqueue_lock:
|
|
279
|
+
self._seq += 1
|
|
280
|
+
seq = self._seq
|
|
232
281
|
job = AgentJob(
|
|
233
282
|
job_id=job_id,
|
|
234
283
|
prompt=prompt,
|
|
@@ -236,22 +285,45 @@ class AgentMesh:
|
|
|
236
285
|
session_id=session_id or str(uuid.uuid4()),
|
|
237
286
|
member_name=member_name,
|
|
238
287
|
meta=meta or {},
|
|
288
|
+
completion_future=completion_future,
|
|
239
289
|
)
|
|
240
|
-
|
|
241
|
-
self._queue.put_nowait((-priority, self._seq, job))
|
|
290
|
+
item = (-priority, seq, job)
|
|
242
291
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
292
|
+
def do_put() -> None:
|
|
293
|
+
try:
|
|
294
|
+
self._queue.put_nowait(item)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
fut = job.completion_future
|
|
297
|
+
if fut is not None and not fut.done():
|
|
298
|
+
try:
|
|
299
|
+
fut.set_exception(e)
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
return
|
|
303
|
+
self._bus.publish_sync(BusMessage(
|
|
304
|
+
topic="job.queued",
|
|
305
|
+
from_addr="mesh",
|
|
306
|
+
to_addr="manager",
|
|
307
|
+
payload={"job_id": job_id, "member": member_name, "priority": priority},
|
|
308
|
+
))
|
|
250
309
|
|
|
251
|
-
# Auto-start the background thread if not running
|
|
252
310
|
if self._bg_thread is None or not self._bg_thread.is_alive():
|
|
253
311
|
self.start()
|
|
254
312
|
|
|
313
|
+
if not self._wait_bg_loop_ready():
|
|
314
|
+
fut = job.completion_future
|
|
315
|
+
if fut is not None and not fut.done():
|
|
316
|
+
try:
|
|
317
|
+
fut.set_exception(RuntimeError("mesh background loop failed to start"))
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
320
|
+
return job_id
|
|
321
|
+
|
|
322
|
+
if self._is_on_mesh_thread():
|
|
323
|
+
do_put()
|
|
324
|
+
else:
|
|
325
|
+
self._bg_loop.call_soon_threadsafe(do_put)
|
|
326
|
+
|
|
255
327
|
return job_id
|
|
256
328
|
|
|
257
329
|
async def delegate_to_member(
|
|
@@ -303,39 +375,60 @@ class AgentMesh:
|
|
|
303
375
|
if context:
|
|
304
376
|
full_prompt += "\n\nContext:\n" + context
|
|
305
377
|
|
|
378
|
+
org_meta = {
|
|
379
|
+
"org": {
|
|
380
|
+
"member": m.to_dict() if hasattr(m, "to_dict") else {},
|
|
381
|
+
"task": task,
|
|
382
|
+
"context": context,
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if not wait:
|
|
387
|
+
job_id = self.enqueue(
|
|
388
|
+
prompt=full_prompt,
|
|
389
|
+
priority=priority,
|
|
390
|
+
session_id="",
|
|
391
|
+
member_name=m.name,
|
|
392
|
+
meta=org_meta,
|
|
393
|
+
)
|
|
394
|
+
return {"ok": True, "job_id": job_id, "delegated_to": m.name, "async": True}
|
|
395
|
+
|
|
396
|
+
# Nested wait=True from inside a running mesh job would deadlock the scheduler
|
|
397
|
+
# (parent holds a concurrency slot while the child waits for another slot).
|
|
398
|
+
if self._mesh_job_depth > 0:
|
|
399
|
+
job_id = f"mesh_{uuid.uuid4().hex[:10]}"
|
|
400
|
+
with self._enqueue_lock:
|
|
401
|
+
self._seq += 1
|
|
402
|
+
inline_job = AgentJob(
|
|
403
|
+
job_id=job_id,
|
|
404
|
+
prompt=full_prompt,
|
|
405
|
+
priority=priority,
|
|
406
|
+
session_id="",
|
|
407
|
+
member_name=m.name,
|
|
408
|
+
meta=org_meta,
|
|
409
|
+
)
|
|
410
|
+
await self._run_job_inner(inline_job)
|
|
411
|
+
if inline_job.status == "finished":
|
|
412
|
+
return {"ok": True, "job_id": job_id, "result": inline_job.result}
|
|
413
|
+
return {"ok": False, "job_id": job_id, "error": inline_job.error}
|
|
414
|
+
|
|
415
|
+
loop = asyncio.get_running_loop()
|
|
416
|
+
fut: concurrent.futures.Future[AgentJob] = concurrent.futures.Future()
|
|
306
417
|
job_id = self.enqueue(
|
|
307
418
|
prompt=full_prompt,
|
|
308
419
|
priority=priority,
|
|
309
420
|
session_id="",
|
|
310
421
|
member_name=m.name,
|
|
311
|
-
meta=
|
|
312
|
-
|
|
313
|
-
"member": m.to_dict() if hasattr(m, "to_dict") else {},
|
|
314
|
-
"task": task,
|
|
315
|
-
"context": context,
|
|
316
|
-
}
|
|
317
|
-
},
|
|
422
|
+
meta=org_meta,
|
|
423
|
+
completion_future=fut,
|
|
318
424
|
)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
async def _wait_for_job(self, job_id: str, timeout: float = 300.0) -> dict[str, Any]:
|
|
328
|
-
"""Wait for a specific job to complete."""
|
|
329
|
-
deadline = time.time() + timeout
|
|
330
|
-
while time.time() < deadline:
|
|
331
|
-
for job in self._completed:
|
|
332
|
-
if job.job_id == job_id:
|
|
333
|
-
if job.status == "finished":
|
|
334
|
-
return {"ok": True, "job_id": job_id, "result": job.result}
|
|
335
|
-
else:
|
|
336
|
-
return {"ok": False, "job_id": job_id, "error": job.error}
|
|
337
|
-
await asyncio.sleep(0.1)
|
|
338
|
-
return {"ok": False, "job_id": job_id, "error": "timeout"}
|
|
425
|
+
try:
|
|
426
|
+
job = await asyncio.wait_for(asyncio.wrap_future(fut, loop=loop), timeout=300.0)
|
|
427
|
+
except asyncio.TimeoutError:
|
|
428
|
+
return {"ok": False, "job_id": job_id, "error": "timeout"}
|
|
429
|
+
if job.status == "finished":
|
|
430
|
+
return {"ok": True, "job_id": job_id, "result": job.result}
|
|
431
|
+
return {"ok": False, "job_id": job_id, "error": job.error}
|
|
339
432
|
|
|
340
433
|
async def _handle_org_assign(self, msg: BusMessage) -> None:
|
|
341
434
|
"""Handle org.assign bus messages (A2A-style delegation)."""
|
|
@@ -373,6 +466,14 @@ class AgentMesh:
|
|
|
373
466
|
|
|
374
467
|
async def _run_job(self, job: AgentJob) -> None:
|
|
375
468
|
"""Execute a single job using a fresh ADK Runner."""
|
|
469
|
+
self._mesh_job_depth += 1
|
|
470
|
+
try:
|
|
471
|
+
await self._run_job_inner(job)
|
|
472
|
+
finally:
|
|
473
|
+
self._mesh_job_depth -= 1
|
|
474
|
+
|
|
475
|
+
async def _run_job_inner(self, job: AgentJob) -> None:
|
|
476
|
+
"""Full job lifecycle (shared by the scheduler and nested inline delegation)."""
|
|
376
477
|
job.status = "running"
|
|
377
478
|
start_ms = int(time.time() * 1000)
|
|
378
479
|
|
|
@@ -391,18 +492,22 @@ class AgentMesh:
|
|
|
391
492
|
duration_ms = int(time.time() * 1000) - start_ms
|
|
392
493
|
|
|
393
494
|
# Publish completion
|
|
495
|
+
_jr_finished: dict[str, Any] = {
|
|
496
|
+
"job_id": job.job_id,
|
|
497
|
+
"session_id": job.session_id,
|
|
498
|
+
"status": "finished",
|
|
499
|
+
"member": job.member_name,
|
|
500
|
+
"report": result_text[:8000],
|
|
501
|
+
"duration_ms": duration_ms,
|
|
502
|
+
}
|
|
503
|
+
_hm0 = job.meta.get("habit") if isinstance(job.meta, dict) else None
|
|
504
|
+
if isinstance(_hm0, dict):
|
|
505
|
+
_jr_finished["habit"] = _hm0
|
|
394
506
|
await self._bus.publish(BusMessage(
|
|
395
507
|
topic="job.report",
|
|
396
508
|
from_addr=job.member_name or "mesh",
|
|
397
509
|
to_addr="manager",
|
|
398
|
-
payload=
|
|
399
|
-
"job_id": job.job_id,
|
|
400
|
-
"session_id": job.session_id,
|
|
401
|
-
"status": "finished",
|
|
402
|
-
"member": job.member_name,
|
|
403
|
-
"report": result_text[:8000],
|
|
404
|
-
"duration_ms": duration_ms,
|
|
405
|
-
},
|
|
510
|
+
payload=_jr_finished,
|
|
406
511
|
))
|
|
407
512
|
|
|
408
513
|
# Also publish org.report if this was an org delegation
|
|
@@ -459,17 +564,21 @@ class AgentMesh:
|
|
|
459
564
|
job.status = "failed"
|
|
460
565
|
job.error = f"{type(e).__name__}: {e}"
|
|
461
566
|
|
|
567
|
+
_jr_failed: dict[str, Any] = {
|
|
568
|
+
"job_id": job.job_id,
|
|
569
|
+
"session_id": job.session_id,
|
|
570
|
+
"status": "failed",
|
|
571
|
+
"member": job.member_name,
|
|
572
|
+
"error": job.error,
|
|
573
|
+
}
|
|
574
|
+
_hm1 = job.meta.get("habit") if isinstance(job.meta, dict) else None
|
|
575
|
+
if isinstance(_hm1, dict):
|
|
576
|
+
_jr_failed["habit"] = _hm1
|
|
462
577
|
await self._bus.publish(BusMessage(
|
|
463
578
|
topic="job.report",
|
|
464
579
|
from_addr=job.member_name or "mesh",
|
|
465
580
|
to_addr="manager",
|
|
466
|
-
payload=
|
|
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
|
-
},
|
|
581
|
+
payload=_jr_failed,
|
|
473
582
|
))
|
|
474
583
|
|
|
475
584
|
# Persist failure to fleet reports
|
|
@@ -491,10 +600,17 @@ class AgentMesh:
|
|
|
491
600
|
pass
|
|
492
601
|
|
|
493
602
|
finally:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
603
|
+
cf = job.completion_future
|
|
604
|
+
if cf is not None and not cf.done():
|
|
605
|
+
try:
|
|
606
|
+
cf.set_result(job)
|
|
607
|
+
except Exception:
|
|
608
|
+
pass
|
|
609
|
+
with self._completed_lock:
|
|
610
|
+
self._completed.append(job)
|
|
611
|
+
# Keep completed list bounded
|
|
612
|
+
if len(self._completed) > 200:
|
|
613
|
+
self._completed = self._completed[-100:]
|
|
498
614
|
|
|
499
615
|
async def _execute_agent_turn(self, job: AgentJob) -> str:
|
|
500
616
|
"""
|
|
@@ -732,15 +848,18 @@ class AgentMesh:
|
|
|
732
848
|
|
|
733
849
|
def status(self) -> dict[str, Any]:
|
|
734
850
|
"""Get mesh status for debugging/display."""
|
|
851
|
+
with self._completed_lock:
|
|
852
|
+
recent = [
|
|
853
|
+
{"job_id": j.job_id, "member": j.member_name, "status": j.status}
|
|
854
|
+
for j in self._completed[-10:]
|
|
855
|
+
]
|
|
856
|
+
n_completed = len(self._completed)
|
|
735
857
|
return {
|
|
736
858
|
"running_jobs": len(self._running),
|
|
737
859
|
"queued_jobs": self._queue.qsize(),
|
|
738
|
-
"completed_jobs":
|
|
860
|
+
"completed_jobs": n_completed,
|
|
739
861
|
"max_concurrency": self.max_concurrency,
|
|
740
|
-
"recent_completed":
|
|
741
|
-
{"job_id": j.job_id, "member": j.member_name, "status": j.status}
|
|
742
|
-
for j in self._completed[-10:]
|
|
743
|
-
],
|
|
862
|
+
"recent_completed": recent,
|
|
744
863
|
}
|
|
745
864
|
|
|
746
865
|
|
|
@@ -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.15
|
|
4
4
|
Summary: Local-first coding agent on Google Gemini + ADK
|
|
5
5
|
Author: GemCode Contributors
|
|
6
6
|
License: Apache License
|
|
@@ -398,7 +398,7 @@ 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.
|
|
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
402
|
|
|
403
403
|
Docs:
|
|
404
404
|
- [`../docs/orchestration.md`](../docs/orchestration.md)
|