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.
- {gemcode-0.4.14/src/gemcode.egg-info → gemcode-0.4.15}/PKG-INFO +1 -1
- {gemcode-0.4.14 → gemcode-0.4.15}/pyproject.toml +1 -1
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/agent_mesh.py +160 -49
- {gemcode-0.4.14 → gemcode-0.4.15/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_agent_mesh.py +86 -35
- {gemcode-0.4.14 → gemcode-0.4.15}/LICENSE +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/MANIFEST.in +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/README.md +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/setup.cfg +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/__main__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/a2a_bridge.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/agent.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/agent_habits.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/agent_intelligence.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/agent_triggers.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/audit.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/automations.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/autotune.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/cli.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/codebase_awareness.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/compaction.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/config.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/credentials.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/delegation_learning.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/event_bus.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/fleet_reports.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/hooks.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/interactions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/invoke.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/kaira_client.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/kaira_ipc.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/kaira_job_store.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/learning.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/limits.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/org.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/paths.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/permissions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/pricing.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/config.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/refine.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/rules.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/self_healing.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/session_store.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/skills.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/thinking.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tool_synthesis.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/automations_tools.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/org_tools.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/user_choice.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/trust.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/version.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/vertex.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/wal.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/web/sse_adapter.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode.egg-info/SOURCES.txt +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_add_dir.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_agent_habits.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_autocompact.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_automations.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_capability_routing.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_cli_init.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_context_budget.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_context_warning.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_credentials.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_event_bus.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_fleet_reports.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_kaira_ipc_paths.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_modality_tools.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_model_errors.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_model_routing.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_paths.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_permissions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_repl_commands.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_repl_slash.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_session_runtime_cache.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_skills.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_slash_commands.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_thinking_config.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_token_budget.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_tools.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.15}/tests/test_workspace_hints.py +0 -0
|
@@ -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
|
|
|
@@ -499,10 +600,17 @@ class AgentMesh:
|
|
|
499
600
|
pass
|
|
500
601
|
|
|
501
602
|
finally:
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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":
|
|
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
|
|
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
130
|
+
bus.subscribe(topic="job.queued", callback=capture)
|
|
94
131
|
mesh.enqueue(prompt="hello", priority=0, member_name="test")
|
|
95
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|