gemcode 0.4.14__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.
Files changed (183) hide show
  1. {gemcode-0.4.14/src/gemcode.egg-info → gemcode-0.4.15}/PKG-INFO +1 -1
  2. {gemcode-0.4.14 → gemcode-0.4.15}/pyproject.toml +1 -1
  3. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/agent_mesh.py +160 -49
  4. {gemcode-0.4.14 → gemcode-0.4.15/src/gemcode.egg-info}/PKG-INFO +1 -1
  5. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_agent_mesh.py +86 -35
  6. {gemcode-0.4.14 → gemcode-0.4.15}/LICENSE +0 -0
  7. {gemcode-0.4.14 → gemcode-0.4.15}/MANIFEST.in +0 -0
  8. {gemcode-0.4.14 → gemcode-0.4.15}/README.md +0 -0
  9. {gemcode-0.4.14 → gemcode-0.4.15}/setup.cfg +0 -0
  10. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/__init__.py +0 -0
  11. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/__main__.py +0 -0
  12. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/a2a_bridge.py +0 -0
  13. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/agent.py +0 -0
  14. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/agent_habits.py +0 -0
  15. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/agent_intelligence.py +0 -0
  16. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/agent_triggers.py +0 -0
  17. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/audit.py +0 -0
  18. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/autocompact.py +0 -0
  19. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/automations.py +0 -0
  20. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/autotune.py +0 -0
  21. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/callbacks.py +0 -0
  22. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/capability_routing.py +0 -0
  23. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/checkpoints.py +0 -0
  24. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/cli.py +0 -0
  25. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/codebase_awareness.py +0 -0
  26. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/compaction.py +0 -0
  27. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/computer_use/__init__.py +0 -0
  28. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/computer_use/browser_computer.py +0 -0
  29. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/config.py +0 -0
  30. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/context_budget.py +0 -0
  31. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/context_warning.py +0 -0
  32. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/credentials.py +0 -0
  33. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/curated_memory.py +0 -0
  34. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/delegation_learning.py +0 -0
  35. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/dynamic_policy.py +0 -0
  36. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/evals/harness.py +0 -0
  37. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/event_bus.py +0 -0
  38. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/fleet_reports.py +0 -0
  39. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/hitl_session.py +0 -0
  40. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/hooks.py +0 -0
  41. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/ide_protocol.py +0 -0
  42. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/ide_stdio.py +0 -0
  43. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/intent_classifier.py +0 -0
  44. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/interactions.py +0 -0
  45. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/invoke.py +0 -0
  46. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/kaira_client.py +0 -0
  47. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/kaira_daemon.py +0 -0
  48. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/kaira_ipc.py +0 -0
  49. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/kaira_job_store.py +0 -0
  50. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/learning.py +0 -0
  51. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/limits.py +0 -0
  52. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/live_audio_engine.py +0 -0
  53. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/logging_config.py +0 -0
  54. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/mcp_loader.py +0 -0
  55. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/memory/__init__.py +0 -0
  56. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/memory/embedding_memory_service.py +0 -0
  57. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/memory/file_memory_service.py +0 -0
  58. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/modality_tools.py +0 -0
  59. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/model_errors.py +0 -0
  60. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/model_routing.py +0 -0
  61. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/multimodal_input.py +0 -0
  62. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/openapi_loader.py +0 -0
  63. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/org.py +0 -0
  64. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/output_styles.py +0 -0
  65. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/paths.py +0 -0
  66. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/permissions.py +0 -0
  67. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/plugins/__init__.py +0 -0
  68. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  69. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  70. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/policy_profile.py +0 -0
  71. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/pricing.py +0 -0
  72. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/prompt_suggestions.py +0 -0
  73. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/__init__.py +0 -0
  74. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/config.py +0 -0
  75. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/deps.py +0 -0
  76. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/engine.py +0 -0
  77. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/stop_hooks.py +0 -0
  78. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/token_budget.py +0 -0
  79. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/transitions.py +0 -0
  80. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query_sanitizer.py +0 -0
  81. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/refine.py +0 -0
  82. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/repl_commands.py +0 -0
  83. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/repl_slash.py +0 -0
  84. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/review_agent.py +0 -0
  85. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/rules.py +0 -0
  86. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/self_healing.py +0 -0
  87. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/session_runtime.py +0 -0
  88. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/session_store.py +0 -0
  89. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/session_summariser.py +0 -0
  90. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/skills.py +0 -0
  91. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/slash_commands.py +0 -0
  92. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/thinking.py +0 -0
  93. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tool_prompt_manifest.py +0 -0
  94. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tool_registry.py +0 -0
  95. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tool_result_store.py +0 -0
  96. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tool_synthesis.py +0 -0
  97. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/__init__.py +0 -0
  98. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/automations_tools.py +0 -0
  99. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/bash.py +0 -0
  100. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/browser.py +0 -0
  101. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/compress_memory.py +0 -0
  102. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/curated_memory.py +0 -0
  103. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/edit.py +0 -0
  104. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/filesystem.py +0 -0
  105. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/notebook.py +0 -0
  106. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/notes.py +0 -0
  107. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/org_tools.py +0 -0
  108. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/repo_map.py +0 -0
  109. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/search.py +0 -0
  110. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/shell.py +0 -0
  111. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/shell_gate.py +0 -0
  112. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/skills.py +0 -0
  113. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/subtask.py +0 -0
  114. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/tasks.py +0 -0
  115. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/think.py +0 -0
  116. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/todo.py +0 -0
  117. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/user_choice.py +0 -0
  118. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/veomem_tools.py +0 -0
  119. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/web.py +0 -0
  120. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/web_search.py +0 -0
  121. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools_inspector.py +0 -0
  122. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/trust.py +0 -0
  123. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tui/input_handler.py +0 -0
  124. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tui/scrollback.py +0 -0
  125. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tui/spinner.py +0 -0
  126. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tui/welcome_banner.py +0 -0
  127. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tui/welcome_rich.py +0 -0
  128. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/veomem_bridge.py +0 -0
  129. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/version.py +0 -0
  130. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/vertex.py +0 -0
  131. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/wal.py +0 -0
  132. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/web/__init__.py +0 -0
  133. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/web/sse_adapter.py +0 -0
  134. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/web/terminal_repl.py +0 -0
  135. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/web/web_sse_compat.py +0 -0
  136. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/workspace_hints.py +0 -0
  137. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode.egg-info/SOURCES.txt +0 -0
  138. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode.egg-info/dependency_links.txt +0 -0
  139. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode.egg-info/entry_points.txt +0 -0
  140. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode.egg-info/requires.txt +0 -0
  141. {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode.egg-info/top_level.txt +0 -0
  142. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_add_dir.py +0 -0
  143. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_agent_habits.py +0 -0
  144. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_agent_instruction.py +0 -0
  145. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_autocompact.py +0 -0
  146. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_automations.py +0 -0
  147. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_capability_routing.py +0 -0
  148. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_checkpoint_diff_command.py +0 -0
  149. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_cli_init.py +0 -0
  150. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_compress_memory_tool.py +0 -0
  151. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_computer_use_permissions.py +0 -0
  152. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_context_budget.py +0 -0
  153. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_context_warning.py +0 -0
  154. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_credentials.py +0 -0
  155. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_eval_harness_layout.py +0 -0
  156. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_event_bus.py +0 -0
  157. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_fleet_reports.py +0 -0
  158. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_ide_stdio_attachments.py +0 -0
  159. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_interactive_permission_ask.py +0 -0
  160. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_kaira_ipc_paths.py +0 -0
  161. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_kaira_scheduler.py +0 -0
  162. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_modality_tools.py +0 -0
  163. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_model_error_retry.py +0 -0
  164. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_model_errors.py +0 -0
  165. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_model_routing.py +0 -0
  166. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_multimodal_input.py +0 -0
  167. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_output_styles_and_rules.py +0 -0
  168. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_paths.py +0 -0
  169. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_permissions.py +0 -0
  170. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_prompt_suggestions.py +0 -0
  171. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_repl_commands.py +0 -0
  172. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_repl_slash.py +0 -0
  173. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_session_runtime_cache.py +0 -0
  174. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_skills.py +0 -0
  175. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_slash_commands.py +0 -0
  176. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_slash_completion_registry.py +0 -0
  177. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_thinking_config.py +0 -0
  178. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_token_budget.py +0 -0
  179. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_tool_context_circulation.py +0 -0
  180. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_tools.py +0 -0
  181. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_tools_inspector.py +0 -0
  182. {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_web_sse_adapter.py +0 -0
  183. {gemcode-0.4.14 → 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.14
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.4.14"
7
+ version = "0.4.15"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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
- self._scheduler_task = asyncio.create_task(self._scheduler_loop())
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. Thread-safe."""
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._seq += 1
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
- # Higher priority = runs first (negate for min-heap)
241
- self._queue.put_nowait((-priority, self._seq, job))
290
+ item = (-priority, seq, job)
242
291
 
243
- # Publish queued event
244
- self._bus.publish_sync(BusMessage(
245
- topic="job.queued",
246
- from_addr="mesh",
247
- to_addr="manager",
248
- payload={"job_id": job_id, "member": member_name, "priority": priority},
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
- "org": {
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
- if not wait:
321
- return {"ok": True, "job_id": job_id, "delegated_to": m.name, "async": True}
322
-
323
- # Wait for completion
324
- result = await self._wait_for_job(job_id, timeout=300.0)
325
- return result
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
 
@@ -499,10 +600,17 @@ class AgentMesh:
499
600
  pass
500
601
 
501
602
  finally:
502
- self._completed.append(job)
503
- # Keep completed list bounded
504
- if len(self._completed) > 200:
505
- self._completed = self._completed[-100:]
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:]
506
614
 
507
615
  async def _execute_agent_turn(self, job: AgentJob) -> str:
508
616
  """
@@ -740,15 +848,18 @@ class AgentMesh:
740
848
 
741
849
  def status(self) -> dict[str, Any]:
742
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)
743
857
  return {
744
858
  "running_jobs": len(self._running),
745
859
  "queued_jobs": self._queue.qsize(),
746
- "completed_jobs": len(self._completed),
860
+ "completed_jobs": n_completed,
747
861
  "max_concurrency": self.max_concurrency,
748
- "recent_completed": [
749
- {"job_id": j.job_id, "member": j.member_name, "status": j.status}
750
- for j in self._completed[-10:]
751
- ],
862
+ "recent_completed": recent,
752
863
  }
753
864
 
754
865
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.4.14
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
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import os
7
+ import threading
8
+ import time
7
9
  from pathlib import Path
8
10
 
9
11
  import pytest
@@ -63,22 +65,52 @@ def test_mesh_singleton(tmp_path: Path) -> None:
63
65
 
64
66
 
65
67
  def test_mesh_enqueue(tmp_path: Path) -> None:
66
- cfg = GemCodeConfig(project_root=tmp_path)
67
- mesh = AgentMesh(cfg, max_concurrency=2)
68
- job_id = mesh.enqueue(prompt="test task", priority=5, member_name="worker")
69
- assert job_id.startswith("mesh_")
70
- assert mesh._queue.qsize() == 1
68
+ old_s = os.environ.get("PYTEST_GEMCODE_MESH_SCHEDULER")
69
+ old_h = os.environ.get("GEMCODE_AGENT_HABITS")
70
+ os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = "0"
71
+ os.environ["GEMCODE_AGENT_HABITS"] = "0"
72
+ try:
73
+ cfg = GemCodeConfig(project_root=tmp_path)
74
+ mesh = AgentMesh(cfg, max_concurrency=2)
75
+ job_id = mesh.enqueue(prompt="test task", priority=5, member_name="worker")
76
+ assert job_id.startswith("mesh_")
77
+ mesh.wait_for_pending_enqueues()
78
+ assert mesh._queue.qsize() == 1
79
+ finally:
80
+ if old_s is None:
81
+ os.environ.pop("PYTEST_GEMCODE_MESH_SCHEDULER", None)
82
+ else:
83
+ os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = old_s
84
+ if old_h is None:
85
+ os.environ.pop("GEMCODE_AGENT_HABITS", None)
86
+ else:
87
+ os.environ["GEMCODE_AGENT_HABITS"] = old_h
71
88
 
72
89
 
73
90
  def test_mesh_status(tmp_path: Path) -> None:
74
- cfg = GemCodeConfig(project_root=tmp_path)
75
- mesh = AgentMesh(cfg, max_concurrency=2)
76
- mesh.enqueue(prompt="task1", priority=1, member_name="a")
77
- mesh.enqueue(prompt="task2", priority=2, member_name="b")
78
- status = mesh.status()
79
- assert status["queued_jobs"] == 2
80
- assert status["running_jobs"] == 0
81
- assert status["max_concurrency"] == 2
91
+ old_s = os.environ.get("PYTEST_GEMCODE_MESH_SCHEDULER")
92
+ old_h = os.environ.get("GEMCODE_AGENT_HABITS")
93
+ os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = "0"
94
+ os.environ["GEMCODE_AGENT_HABITS"] = "0"
95
+ try:
96
+ cfg = GemCodeConfig(project_root=tmp_path)
97
+ mesh = AgentMesh(cfg, max_concurrency=2)
98
+ mesh.enqueue(prompt="task1", priority=1, member_name="a")
99
+ mesh.enqueue(prompt="task2", priority=2, member_name="b")
100
+ mesh.wait_for_pending_enqueues()
101
+ status = mesh.status()
102
+ assert status["queued_jobs"] == 2
103
+ assert status["running_jobs"] == 0
104
+ assert status["max_concurrency"] == 2
105
+ finally:
106
+ if old_s is None:
107
+ os.environ.pop("PYTEST_GEMCODE_MESH_SCHEDULER", None)
108
+ else:
109
+ os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = old_s
110
+ if old_h is None:
111
+ os.environ.pop("GEMCODE_AGENT_HABITS", None)
112
+ else:
113
+ os.environ["GEMCODE_AGENT_HABITS"] = old_h
82
114
 
83
115
 
84
116
  def test_mesh_bus_integration(tmp_path: Path) -> None:
@@ -88,17 +120,21 @@ def test_mesh_bus_integration(tmp_path: Path) -> None:
88
120
  bus = get_bus()
89
121
 
90
122
  received: list[BusMessage] = []
123
+ got = threading.Event()
124
+
125
+ async def capture(msg: BusMessage) -> None:
126
+ received.append(msg)
127
+ got.set()
91
128
 
92
129
  async def run():
93
- sub = bus.subscribe(topic="job.queued")
130
+ bus.subscribe(topic="job.queued", callback=capture)
94
131
  mesh.enqueue(prompt="hello", priority=0, member_name="test")
95
- # Give the sync publish a moment to schedule
96
- await asyncio.sleep(0.1)
97
- msg = await sub.get(timeout=1.0)
98
- if msg:
99
- received.append(msg)
132
+ mesh.wait_for_pending_enqueues()
100
133
 
101
134
  asyncio.run(run())
135
+ # Mesh loop processes publish asynchronously on a background thread.
136
+ assert got.wait(timeout=3.0), "timed out waiting for job.queued on bus"
137
+ time.sleep(0.01)
102
138
  assert len(received) == 1
103
139
  assert received[0].payload["member"] == "test"
104
140
 
@@ -139,19 +175,34 @@ def test_apply_mesh_worker_unattended_off_inherits_manager(tmp_path: Path) -> No
139
175
 
140
176
  def test_mesh_priority_ordering(tmp_path: Path) -> None:
141
177
  """Higher priority jobs should be dequeued first."""
142
- cfg = GemCodeConfig(project_root=tmp_path)
143
- mesh = AgentMesh(cfg, max_concurrency=1)
144
-
145
- mesh.enqueue(prompt="low", priority=1, member_name="a")
146
- mesh.enqueue(prompt="high", priority=10, member_name="b")
147
- mesh.enqueue(prompt="mid", priority=5, member_name="c")
148
-
149
- # Drain the queue manually to check ordering
150
- items = []
151
- while not mesh._queue.empty():
152
- neg_pri, seq, job = mesh._queue.get_nowait()
153
- items.append((job.prompt, job.priority))
154
-
155
- assert items[0] == ("high", 10)
156
- assert items[1] == ("mid", 5)
157
- assert items[2] == ("low", 1)
178
+ old_s = os.environ.get("PYTEST_GEMCODE_MESH_SCHEDULER")
179
+ old_h = os.environ.get("GEMCODE_AGENT_HABITS")
180
+ os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = "0"
181
+ os.environ["GEMCODE_AGENT_HABITS"] = "0"
182
+ try:
183
+ cfg = GemCodeConfig(project_root=tmp_path)
184
+ mesh = AgentMesh(cfg, max_concurrency=1)
185
+
186
+ mesh.enqueue(prompt="low", priority=1, member_name="a")
187
+ mesh.enqueue(prompt="high", priority=10, member_name="b")
188
+ mesh.enqueue(prompt="mid", priority=5, member_name="c")
189
+ mesh.wait_for_pending_enqueues()
190
+
191
+ # Drain the queue manually to check ordering
192
+ items = []
193
+ while not mesh._queue.empty():
194
+ neg_pri, seq, job = mesh._queue.get_nowait()
195
+ items.append((job.prompt, job.priority))
196
+
197
+ assert items[0] == ("high", 10)
198
+ assert items[1] == ("mid", 5)
199
+ assert items[2] == ("low", 1)
200
+ finally:
201
+ if old_s is None:
202
+ os.environ.pop("PYTEST_GEMCODE_MESH_SCHEDULER", None)
203
+ else:
204
+ os.environ["PYTEST_GEMCODE_MESH_SCHEDULER"] = old_s
205
+ if old_h is None:
206
+ os.environ.pop("GEMCODE_AGENT_HABITS", None)
207
+ else:
208
+ os.environ["GEMCODE_AGENT_HABITS"] = old_h
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