gemcode 0.4.6__tar.gz → 0.4.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. {gemcode-0.4.6/src/gemcode.egg-info → gemcode-0.4.9}/PKG-INFO +1 -1
  2. {gemcode-0.4.6 → gemcode-0.4.9}/pyproject.toml +1 -1
  3. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/agent_mesh.py +97 -17
  4. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/fleet_reports.py +11 -0
  5. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/invoke.py +1 -4
  6. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/org_tools.py +17 -113
  7. {gemcode-0.4.6 → gemcode-0.4.9/src/gemcode.egg-info}/PKG-INFO +1 -1
  8. {gemcode-0.4.6 → gemcode-0.4.9}/LICENSE +0 -0
  9. {gemcode-0.4.6 → gemcode-0.4.9}/MANIFEST.in +0 -0
  10. {gemcode-0.4.6 → gemcode-0.4.9}/README.md +0 -0
  11. {gemcode-0.4.6 → gemcode-0.4.9}/setup.cfg +0 -0
  12. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/__init__.py +0 -0
  13. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/__main__.py +0 -0
  14. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/a2a_bridge.py +0 -0
  15. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/agent.py +0 -0
  16. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/agent_habits.py +0 -0
  17. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/agent_intelligence.py +0 -0
  18. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/agent_triggers.py +0 -0
  19. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/audit.py +0 -0
  20. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/autocompact.py +0 -0
  21. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/automations.py +0 -0
  22. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/autotune.py +0 -0
  23. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/callbacks.py +0 -0
  24. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/capability_routing.py +0 -0
  25. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/checkpoints.py +0 -0
  26. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/cli.py +0 -0
  27. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/codebase_awareness.py +0 -0
  28. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/compaction.py +0 -0
  29. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/computer_use/__init__.py +0 -0
  30. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/computer_use/browser_computer.py +0 -0
  31. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/config.py +0 -0
  32. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/context_budget.py +0 -0
  33. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/context_warning.py +0 -0
  34. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/credentials.py +0 -0
  35. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/curated_memory.py +0 -0
  36. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/delegation_learning.py +0 -0
  37. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/dynamic_policy.py +0 -0
  38. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/evals/harness.py +0 -0
  39. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/event_bus.py +0 -0
  40. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/hitl_session.py +0 -0
  41. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/hooks.py +0 -0
  42. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/ide_protocol.py +0 -0
  43. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/ide_stdio.py +0 -0
  44. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/intent_classifier.py +0 -0
  45. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/interactions.py +0 -0
  46. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/kaira_client.py +0 -0
  47. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/kaira_daemon.py +0 -0
  48. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/kaira_ipc.py +0 -0
  49. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/kaira_job_store.py +0 -0
  50. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/learning.py +0 -0
  51. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/limits.py +0 -0
  52. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/live_audio_engine.py +0 -0
  53. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/logging_config.py +0 -0
  54. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/mcp_loader.py +0 -0
  55. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/memory/__init__.py +0 -0
  56. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/memory/embedding_memory_service.py +0 -0
  57. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/memory/file_memory_service.py +0 -0
  58. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/modality_tools.py +0 -0
  59. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/model_errors.py +0 -0
  60. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/model_routing.py +0 -0
  61. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/multimodal_input.py +0 -0
  62. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/openapi_loader.py +0 -0
  63. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/org.py +0 -0
  64. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/output_styles.py +0 -0
  65. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/paths.py +0 -0
  66. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/permissions.py +0 -0
  67. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/plugins/__init__.py +0 -0
  68. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  69. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  70. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/policy_profile.py +0 -0
  71. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/pricing.py +0 -0
  72. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/prompt_suggestions.py +0 -0
  73. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/query/__init__.py +0 -0
  74. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/query/config.py +0 -0
  75. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/query/deps.py +0 -0
  76. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/query/engine.py +0 -0
  77. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/query/stop_hooks.py +0 -0
  78. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/query/token_budget.py +0 -0
  79. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/query/transitions.py +0 -0
  80. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/query_sanitizer.py +0 -0
  81. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/refine.py +0 -0
  82. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/repl_commands.py +0 -0
  83. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/repl_slash.py +0 -0
  84. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/review_agent.py +0 -0
  85. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/rules.py +0 -0
  86. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/self_healing.py +0 -0
  87. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/session_runtime.py +0 -0
  88. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/session_store.py +0 -0
  89. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/session_summariser.py +0 -0
  90. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/skills.py +0 -0
  91. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/slash_commands.py +0 -0
  92. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/thinking.py +0 -0
  93. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tool_prompt_manifest.py +0 -0
  94. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tool_registry.py +0 -0
  95. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tool_result_store.py +0 -0
  96. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tool_synthesis.py +0 -0
  97. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/__init__.py +0 -0
  98. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/automations_tools.py +0 -0
  99. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/bash.py +0 -0
  100. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/browser.py +0 -0
  101. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/compress_memory.py +0 -0
  102. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/curated_memory.py +0 -0
  103. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/edit.py +0 -0
  104. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/filesystem.py +0 -0
  105. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/notebook.py +0 -0
  106. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/notes.py +0 -0
  107. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/repo_map.py +0 -0
  108. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/search.py +0 -0
  109. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/shell.py +0 -0
  110. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/shell_gate.py +0 -0
  111. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/skills.py +0 -0
  112. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/subtask.py +0 -0
  113. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/tasks.py +0 -0
  114. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/think.py +0 -0
  115. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/todo.py +0 -0
  116. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/user_choice.py +0 -0
  117. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/veomem_tools.py +0 -0
  118. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/web.py +0 -0
  119. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools/web_search.py +0 -0
  120. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tools_inspector.py +0 -0
  121. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/trust.py +0 -0
  122. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tui/input_handler.py +0 -0
  123. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tui/scrollback.py +0 -0
  124. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tui/spinner.py +0 -0
  125. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tui/welcome_banner.py +0 -0
  126. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/tui/welcome_rich.py +0 -0
  127. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/veomem_bridge.py +0 -0
  128. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/version.py +0 -0
  129. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/vertex.py +0 -0
  130. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/wal.py +0 -0
  131. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/web/__init__.py +0 -0
  132. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/web/sse_adapter.py +0 -0
  133. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/web/terminal_repl.py +0 -0
  134. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/web/web_sse_compat.py +0 -0
  135. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode/workspace_hints.py +0 -0
  136. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode.egg-info/SOURCES.txt +0 -0
  137. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode.egg-info/dependency_links.txt +0 -0
  138. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode.egg-info/entry_points.txt +0 -0
  139. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode.egg-info/requires.txt +0 -0
  140. {gemcode-0.4.6 → gemcode-0.4.9}/src/gemcode.egg-info/top_level.txt +0 -0
  141. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_add_dir.py +0 -0
  142. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_agent_habits.py +0 -0
  143. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_agent_instruction.py +0 -0
  144. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_agent_mesh.py +0 -0
  145. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_autocompact.py +0 -0
  146. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_automations.py +0 -0
  147. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_capability_routing.py +0 -0
  148. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_checkpoint_diff_command.py +0 -0
  149. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_cli_init.py +0 -0
  150. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_compress_memory_tool.py +0 -0
  151. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_computer_use_permissions.py +0 -0
  152. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_context_budget.py +0 -0
  153. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_context_warning.py +0 -0
  154. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_credentials.py +0 -0
  155. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_eval_harness_layout.py +0 -0
  156. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_event_bus.py +0 -0
  157. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_fleet_reports.py +0 -0
  158. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_ide_stdio_attachments.py +0 -0
  159. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_interactive_permission_ask.py +0 -0
  160. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_kaira_ipc_paths.py +0 -0
  161. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_kaira_scheduler.py +0 -0
  162. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_modality_tools.py +0 -0
  163. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_model_error_retry.py +0 -0
  164. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_model_errors.py +0 -0
  165. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_model_routing.py +0 -0
  166. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_multimodal_input.py +0 -0
  167. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_output_styles_and_rules.py +0 -0
  168. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_paths.py +0 -0
  169. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_permissions.py +0 -0
  170. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_prompt_suggestions.py +0 -0
  171. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_repl_commands.py +0 -0
  172. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_repl_slash.py +0 -0
  173. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_session_runtime_cache.py +0 -0
  174. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_skills.py +0 -0
  175. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_slash_commands.py +0 -0
  176. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_slash_completion_registry.py +0 -0
  177. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_thinking_config.py +0 -0
  178. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_token_budget.py +0 -0
  179. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_tool_context_circulation.py +0 -0
  180. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_tools.py +0 -0
  181. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_tools_inspector.py +0 -0
  182. {gemcode-0.4.6 → gemcode-0.4.9}/tests/test_web_sse_adapter.py +0 -0
  183. {gemcode-0.4.6 → gemcode-0.4.9}/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.6
3
+ Version: 0.4.9
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.4.6"
7
+ version = "0.4.9"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -66,7 +66,11 @@ class AgentMesh:
66
66
  self._running: dict[str, asyncio.Task] = {}
67
67
  self._completed: list[AgentJob] = []
68
68
  self._scheduler_task: asyncio.Task | None = None
69
- self._stop = asyncio.Event()
69
+ self._stop: asyncio.Event | None = None # Created in background loop
70
+ self._bg_thread: "threading.Thread | None" = None
71
+ self._bg_loop: asyncio.AbstractEventLoop | None = None
72
+ # _stop is created lazily in the background loop
73
+ self._stop_flag = False
70
74
 
71
75
  # Subscribe to org.assign messages on the bus
72
76
  self._bus.subscribe(
@@ -112,25 +116,69 @@ class AgentMesh:
112
116
  return self._bus
113
117
 
114
118
  def start(self) -> None:
115
- """Start the background scheduler loop and trigger engine."""
116
- if self._scheduler_task is None or self._scheduler_task.done():
117
- self._scheduler_task = asyncio.create_task(self._scheduler_loop())
118
- # Start self-triggering agents
119
+ """Start the mesh in a dedicated background thread with its own event loop.
120
+
121
+ This is critical: the TUI's event loop is blocked on user input most of the
122
+ time. If we run mesh jobs on the same loop, they can only execute during the
123
+ brief moments prompt_toolkit yields control — not enough for a full agent turn.
124
+
125
+ By running in a separate thread, mesh jobs execute independently. Results are
126
+ written to fleet_reports.jsonl and picked up on the next user turn, or printed
127
+ to the terminal via the notification callback.
128
+ """
129
+ if self._bg_thread is not None and self._bg_thread.is_alive():
130
+ return # Already running
131
+
132
+ import threading
133
+
134
+ def _run_bg_loop():
135
+ """Background thread entry: create a new event loop and run the scheduler."""
136
+ self._bg_loop = asyncio.new_event_loop()
137
+ asyncio.set_event_loop(self._bg_loop)
138
+ try:
139
+ self._bg_loop.run_until_complete(self._bg_main())
140
+ except Exception:
141
+ pass
142
+ finally:
143
+ try:
144
+ self._bg_loop.close()
145
+ except Exception:
146
+ pass
147
+
148
+ self._bg_thread = threading.Thread(target=_run_bg_loop, daemon=True, name="gemcode-mesh")
149
+ self._bg_thread.start()
150
+
151
+ async def _bg_main(self) -> None:
152
+ """Main coroutine for the background event loop."""
153
+ self._stop = asyncio.Event() # Create in the correct loop
154
+
155
+ # Start all sub-systems in this loop
156
+ self._scheduler_task = asyncio.create_task(self._scheduler_loop())
157
+
119
158
  if self._trigger_engine is not None:
120
159
  self._trigger_engine.start()
121
- # Start habit scheduler (cron/interval recurring tasks)
122
160
  if self._habit_scheduler is not None:
123
161
  self._habit_scheduler.start()
124
162
 
163
+ # Wait until stopped
164
+ await self._stop.wait()
165
+
125
166
  def stop(self) -> None:
126
167
  """Stop the scheduler and trigger engine."""
127
- self._stop.set()
168
+ if self._stop is not None:
169
+ self._stop.set()
128
170
  if self._scheduler_task and not self._scheduler_task.done():
129
171
  self._scheduler_task.cancel()
130
172
  if self._trigger_engine is not None:
131
173
  self._trigger_engine.stop()
132
174
  if self._habit_scheduler is not None:
133
175
  self._habit_scheduler.stop()
176
+ # Signal the background loop to exit
177
+ if self._bg_loop is not None:
178
+ try:
179
+ self._bg_loop.call_soon_threadsafe(self._bg_loop.stop)
180
+ except Exception:
181
+ pass
134
182
 
135
183
  def enqueue(
136
184
  self,
@@ -141,7 +189,7 @@ class AgentMesh:
141
189
  member_name: str = "",
142
190
  meta: dict[str, Any] | None = None,
143
191
  ) -> str:
144
- """Enqueue a job and return its job_id."""
192
+ """Enqueue a job and return its job_id. Thread-safe."""
145
193
  job_id = f"mesh_{uuid.uuid4().hex[:10]}"
146
194
  self._seq += 1
147
195
  job = AgentJob(
@@ -163,13 +211,9 @@ class AgentMesh:
163
211
  payload={"job_id": job_id, "member": member_name, "priority": priority},
164
212
  ))
165
213
 
166
- # Auto-start scheduler if not running
167
- try:
168
- loop = asyncio.get_running_loop()
169
- if self._scheduler_task is None or self._scheduler_task.done():
170
- self._scheduler_task = loop.create_task(self._scheduler_loop())
171
- except RuntimeError:
172
- pass
214
+ # Auto-start the background thread if not running
215
+ if self._bg_thread is None or not self._bg_thread.is_alive():
216
+ self.start()
173
217
 
174
218
  return job_id
175
219
 
@@ -267,7 +311,7 @@ class AgentMesh:
267
311
 
268
312
  async def _scheduler_loop(self) -> None:
269
313
  """Continuously dequeue and run jobs."""
270
- while not self._stop.is_set():
314
+ while self._stop is None or not self._stop.is_set():
271
315
  try:
272
316
  await self._sem.acquire()
273
317
  try:
@@ -590,11 +634,47 @@ class AgentMesh:
590
634
  )
591
635
  return {"ok": True, "job_id": job_id}
592
636
 
637
+ async def agent_dm(to: str, message: str) -> dict:
638
+ """Send a direct message to another agent via the event bus."""
639
+ from_name = job.member_name or 'anonymous'
640
+ await mesh._bus.publish(BusMessage(
641
+ topic='agent.dm', from_addr=from_name, to_addr=to,
642
+ payload={'from': from_name, 'to': to, 'message': message},
643
+ ))
644
+ try:
645
+ from gemcode.fleet_reports import append_fleet_report
646
+ fleet_root = resolve_fleet_root(mesh.cfg.project_root)
647
+ append_fleet_report(fleet_root, topic='agent.dm', payload={
648
+ 'from': from_name, 'to': to, 'message': message[:4000],
649
+ })
650
+ except Exception:
651
+ pass
652
+ return {'ok': True, 'sent_to': to}
653
+
654
+ async def agent_broadcast(message: str) -> dict:
655
+ """Broadcast a message to all agents and the manager."""
656
+ from_name = job.member_name or 'anonymous'
657
+ await mesh._bus.publish(BusMessage(
658
+ topic='agent.broadcast', from_addr=from_name, to_addr='',
659
+ payload={'from': from_name, 'message': message},
660
+ ))
661
+ try:
662
+ from gemcode.fleet_reports import append_fleet_report
663
+ fleet_root = resolve_fleet_root(mesh.cfg.project_root)
664
+ append_fleet_report(fleet_root, topic='agent.broadcast', payload={
665
+ 'from': from_name, 'message': message[:4000],
666
+ })
667
+ except Exception:
668
+ pass
669
+ return {'ok': True, 'broadcast_from': from_name}
670
+
593
671
  mesh_delegate.__name__ = "mesh_delegate"
594
672
  mesh_report.__name__ = "mesh_report"
595
673
  mesh_enqueue.__name__ = "mesh_enqueue"
674
+ agent_dm.__name__ = "agent_dm"
675
+ agent_broadcast.__name__ = "agent_broadcast"
596
676
 
597
- return [mesh_delegate, mesh_report, mesh_enqueue]
677
+ return [mesh_delegate, mesh_report, mesh_enqueue, agent_dm, agent_broadcast]
598
678
 
599
679
  # ── Status / Introspection ──────────────────────────────────────────────
600
680
 
@@ -273,6 +273,17 @@ def _format_record(rec: dict[str, Any]) -> str:
273
273
  elif isinstance(res, str) and res.strip():
274
274
  lines.append(f" result: {res[:8000]}")
275
275
 
276
+ elif topic == "agent.dm":
277
+ from_name = str(payload.get("from") or "")
278
+ to_name = str(payload.get("to") or "")
279
+ msg = str(payload.get("message") or "").strip()
280
+ lines.append(f"[agent.dm] {from_name} → {to_name}: {msg[:4000]}")
281
+
282
+ elif topic == "agent.broadcast":
283
+ from_name = str(payload.get("from") or "")
284
+ msg = str(payload.get("message") or "").strip()
285
+ lines.append(f"[agent.broadcast] {from_name}: {msg[:4000]}")
286
+
276
287
  return "\n".join(lines)
277
288
 
278
289
 
@@ -111,10 +111,7 @@ async def run_turn(
111
111
  try:
112
112
  from gemcode.agent_mesh import ensure_mesh
113
113
  mesh = ensure_mesh(cfg)
114
- try:
115
- mesh.start()
116
- except Exception:
117
- pass
114
+ mesh.start() # Starts background thread if not already running
118
115
  except Exception:
119
116
  pass
120
117
 
@@ -152,34 +152,7 @@ def make_org_tools(cfg: GemCodeConfig) -> list:
152
152
  pass
153
153
 
154
154
  # ── Also try IPC if daemon is running (bonus, not required) ───────────
155
- if not _bus_enabled():
156
- return
157
-
158
- from gemcode.kaira_ipc import fleet_manager_ipc_path
159
-
160
- sock = str(fleet_manager_ipc_path(fleet_root))
161
- try:
162
- if not Path(sock).exists():
163
- return # No daemon, but that's fine — bus + fleet reports already handled it
164
- except Exception:
165
- return
166
-
167
- try:
168
- from gemcode.kaira_client import KairaIpcClient
169
-
170
- c = await KairaIpcClient.connect(socket_path=str(sock))
171
- try:
172
- for to_addr in chain:
173
- await c.publish(
174
- topic="org.report",
175
- to=str(to_addr or "manager"),
176
- from_addr=from_addr,
177
- payload=payload,
178
- )
179
- finally:
180
- await c.close()
181
- except Exception:
182
- pass
155
+ # Removed: Kaira IPC is no longer needed. The bus + fleet reports handle everything.
183
156
 
184
157
  def org_list() -> dict:
185
158
  """List available org members (workers)."""
@@ -219,7 +192,7 @@ def make_org_tools(cfg: GemCodeConfig) -> list:
219
192
  return {"ok": True, "tree": org_tree(root)}
220
193
 
221
194
  async def org_delegate(member: str, task: str, context: str = "") -> dict:
222
- """Delegate a task to an org member (Kaira worker or subagent)."""
195
+ """Delegate a task to an org member. Uses the mesh — no daemon required."""
223
196
  m = find_member(root, member)
224
197
  if m is None:
225
198
  return {"ok": False, "error": f"unknown member: {member}"}
@@ -229,109 +202,40 @@ def make_org_tools(cfg: GemCodeConfig) -> list:
229
202
  if not task:
230
203
  return {"ok": False, "error": "missing task"}
231
204
 
232
- # ── Strategy 1: In-process Agent Mesh (always available) ──────────────
233
- # The mesh runs real ADK agents in-process with their own sessions.
234
- # This is the PRIMARY path — no daemon required.
205
+ # Primary path: Agent Mesh (always available, runs in background thread)
235
206
  mesh = _get_mesh(cfg)
236
207
  if mesh is not None:
237
208
  try:
238
- # For kaira_workers, run async (non-blocking) by default
239
- # For subagents, run sync (blocking) to return result immediately
240
209
  wait = (m.kind != "kaira_worker")
241
210
  result = await mesh.delegate_to_member(
242
- member=m,
243
- task=task,
244
- context=ctx,
245
- priority=0,
246
- wait=wait,
211
+ member=m, task=task, context=ctx, priority=0, wait=wait,
247
212
  )
248
213
  if result.get("ok"):
249
214
  return {"ok": True, "delegated_to": m.to_dict(), **result}
250
- # If mesh delegation failed, fall through to other strategies
251
215
  except Exception:
252
216
  pass
253
217
 
254
- # ── Strategy 2: Kaira Daemon IPC (if running) ─────────────────────────
255
- if m.kind == "kaira_worker":
256
- try:
257
- from gemcode.kaira_client import KairaIpcClient
258
- fleet_root = resolve_fleet_root(getattr(cfg, "project_root", Path.cwd()))
259
- from gemcode.kaira_ipc import fleet_manager_ipc_path
260
-
261
- sock_s = str(fleet_manager_ipc_path(fleet_root))
262
- if Path(sock_s).exists():
263
- client = await KairaIpcClient.connect(socket_path=sock_s)
264
- try:
265
- session_id = str(getattr(cfg, "_active_session_id", "") or "")
266
- notify_chain = _ancestor_addresses_for(m)
267
- header = (
268
- f"You are {m.name} ({m.title}).\n"
269
- f"Role description: {m.description or '(none)'}\n\n"
270
- "Do the assigned task. Keep outputs concise and actionable.\n"
271
- )
272
- prompt = header + "\nTask:\n" + task
273
- if ctx:
274
- prompt += "\n\nContext:\n" + ctx
275
- meta = {
276
- "org": {
277
- "member": (m.to_dict() if hasattr(m, "to_dict") else {}),
278
- "capabilities": {
279
- "kind": getattr(m, "kind", ""),
280
- "address": getattr(m, "address", "") or getattr(m, "name", ""),
281
- "workspace_rel": getattr(m, "workspace_rel", "") or "",
282
- "reports_to": getattr(m, "reports_to", "") or "",
283
- },
284
- "task": task,
285
- "context": ctx,
286
- "notify_chain": notify_chain,
287
- }
288
- }
289
- res = await client.request(
290
- action="enqueue",
291
- prompt=prompt,
292
- priority=0,
293
- session_id=session_id,
294
- meta=meta,
295
- )
296
- if res.get("ok"):
297
- job_id = str(res.get("job_id") or "")
298
- await _publish_org_report(
299
- m=m, status="delegated", task=task, context=ctx, job_id=job_id,
300
- result={"kind": "kaira_worker", "job_id": job_id},
301
- )
302
- return {"ok": True, "delegated_to": m.to_dict(), "job_id": job_id}
303
- finally:
304
- await client.close()
305
- except Exception:
306
- pass
307
-
308
- # ── Strategy 3: In-process subtask fallback ───────────────────────────
309
- header = (
310
- f"You are {m.name} ({m.title}).\n"
311
- f"Role description: {m.description or '(none)'}\n\n"
312
- "Before acting, load and follow your role skill if available.\n"
313
- f"- If a GemSkill exists: call load_skill(\"{m.skill_name or 'member-' + m.name.lower()}\")\n\n"
314
- "Do the assigned task. Keep outputs concise and actionable.\n"
315
- )
316
- prompt = header + "\nTask:\n" + task
317
- if ctx:
318
- prompt += "\n\nContext:\n" + ctx
319
-
218
+ # Fallback: in-process subtask (blocking but guaranteed to work)
320
219
  try:
321
220
  from gemcode.tools.subtask import make_run_subtask_tool
322
221
 
222
+ header = (
223
+ f"You are {m.name} ({m.title}).\n"
224
+ f"Role: {m.description or '(none)'}\n\n"
225
+ "Do the assigned task. Keep outputs concise and actionable.\n"
226
+ )
227
+ prompt = header + "\nTask:\n" + task
228
+ if ctx:
229
+ prompt += "\n\nContext:\n" + ctx
230
+
323
231
  run_subtask = make_run_subtask_tool(cfg)
324
232
  out = await run_subtask(prompt, "")
325
233
  result = out.get("result") if isinstance(out, dict) else out
326
- await _publish_org_report(
327
- m=m, status="finished", task=task, context=ctx, result=result,
328
- )
234
+ await _publish_org_report(m=m, status="finished", task=task, context=ctx, result=result)
329
235
  return {"ok": True, "delegated_to": m.to_dict(), "result": result}
330
236
  except Exception as e:
331
- await _publish_org_report(
332
- m=m, status="failed", task=task, context=ctx,
333
- error=f"all_strategies_failed: {type(e).__name__}: {e}",
334
- )
237
+ await _publish_org_report(m=m, status="failed", task=task, context=ctx,
238
+ error=f"delegation_failed: {type(e).__name__}: {e}")
335
239
  return {"ok": False, "error": f"delegation_failed: {type(e).__name__}: {e}"}
336
240
 
337
241
  async def org_spawn(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.6
3
+ Version: 0.4.9
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
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