gemcode 0.4.14__tar.gz → 0.4.16__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.16}/PKG-INFO +1 -1
- {gemcode-0.4.14 → gemcode-0.4.16}/pyproject.toml +1 -1
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/agent_mesh.py +194 -49
- {gemcode-0.4.14 → gemcode-0.4.16/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_agent_mesh.py +86 -35
- {gemcode-0.4.14 → gemcode-0.4.16}/LICENSE +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/MANIFEST.in +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/README.md +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/setup.cfg +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/__main__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/a2a_bridge.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/agent.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/agent_habits.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/agent_intelligence.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/agent_triggers.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/audit.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/automations.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/autotune.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/checkpoints.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/cli.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/codebase_awareness.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/compaction.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/config.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/credentials.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/curated_memory.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/delegation_learning.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/dynamic_policy.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/evals/harness.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/event_bus.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/fleet_reports.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/hooks.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/ide_protocol.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/ide_stdio.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/intent_classifier.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/interactions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/invoke.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/kaira_client.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/kaira_daemon.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/kaira_ipc.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/kaira_job_store.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/learning.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/limits.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/multimodal_input.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/org.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/output_styles.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/paths.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/permissions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/policy_profile.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/pricing.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/config.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/query_sanitizer.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/refine.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/rules.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/self_healing.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/session_store.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/session_summariser.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/skills.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/thinking.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tool_result_store.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tool_synthesis.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/automations_tools.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/compress_memory.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/curated_memory.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/notebook.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/notes.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/org_tools.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/repo_map.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/skills.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/tasks.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/user_choice.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/veomem_tools.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools/web_search.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/trust.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tui/scrollback.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/veomem_bridge.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/version.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/vertex.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/wal.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/web/sse_adapter.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/web/web_sse_compat.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode.egg-info/SOURCES.txt +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_add_dir.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_agent_habits.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_autocompact.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_automations.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_capability_routing.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_checkpoint_diff_command.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_cli_init.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_compress_memory_tool.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_context_budget.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_context_warning.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_credentials.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_eval_harness_layout.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_event_bus.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_fleet_reports.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_ide_stdio_attachments.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_kaira_ipc_paths.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_kaira_scheduler.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_modality_tools.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_model_errors.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_model_routing.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_multimodal_input.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_output_styles_and_rules.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_paths.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_permissions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_repl_commands.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_repl_slash.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_session_runtime_cache.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_skills.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_slash_commands.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_slash_completion_registry.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_thinking_config.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_token_budget.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_tools.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/tests/test_web_sse_adapter.py +0 -0
- {gemcode-0.4.14 → gemcode-0.4.16}/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,63 @@ 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
|
+
# Only use the inline path on the mesh thread: the manager runs on a different
|
|
399
|
+
# event loop; _mesh_job_depth can be >0 while a habit runs, but inline
|
|
400
|
+
# _run_job_inner must not run there (asyncio locks / ADK are mesh-loop-local).
|
|
401
|
+
if self._is_on_mesh_thread() and self._mesh_job_depth > 0:
|
|
402
|
+
job_id = f"mesh_{uuid.uuid4().hex[:10]}"
|
|
403
|
+
with self._enqueue_lock:
|
|
404
|
+
self._seq += 1
|
|
405
|
+
inline_job = AgentJob(
|
|
406
|
+
job_id=job_id,
|
|
407
|
+
prompt=full_prompt,
|
|
408
|
+
priority=priority,
|
|
409
|
+
session_id="",
|
|
410
|
+
member_name=m.name,
|
|
411
|
+
meta=org_meta,
|
|
412
|
+
)
|
|
413
|
+
await self._run_job_inner(inline_job)
|
|
414
|
+
if inline_job.status == "finished":
|
|
415
|
+
return {"ok": True, "job_id": job_id, "result": inline_job.result}
|
|
416
|
+
return {"ok": False, "job_id": job_id, "error": inline_job.error}
|
|
417
|
+
|
|
418
|
+
loop = asyncio.get_running_loop()
|
|
419
|
+
fut: concurrent.futures.Future[AgentJob] = concurrent.futures.Future()
|
|
306
420
|
job_id = self.enqueue(
|
|
307
421
|
prompt=full_prompt,
|
|
308
422
|
priority=priority,
|
|
309
423
|
session_id="",
|
|
310
424
|
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
|
-
},
|
|
425
|
+
meta=org_meta,
|
|
426
|
+
completion_future=fut,
|
|
318
427
|
)
|
|
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"}
|
|
428
|
+
try:
|
|
429
|
+
job = await asyncio.wait_for(asyncio.wrap_future(fut, loop=loop), timeout=300.0)
|
|
430
|
+
except asyncio.TimeoutError:
|
|
431
|
+
return {"ok": False, "job_id": job_id, "error": "timeout"}
|
|
432
|
+
if job.status == "finished":
|
|
433
|
+
return {"ok": True, "job_id": job_id, "result": job.result}
|
|
434
|
+
return {"ok": False, "job_id": job_id, "error": job.error}
|
|
339
435
|
|
|
340
436
|
async def _handle_org_assign(self, msg: BusMessage) -> None:
|
|
341
437
|
"""Handle org.assign bus messages (A2A-style delegation)."""
|
|
@@ -373,6 +469,14 @@ class AgentMesh:
|
|
|
373
469
|
|
|
374
470
|
async def _run_job(self, job: AgentJob) -> None:
|
|
375
471
|
"""Execute a single job using a fresh ADK Runner."""
|
|
472
|
+
self._mesh_job_depth += 1
|
|
473
|
+
try:
|
|
474
|
+
await self._run_job_inner(job)
|
|
475
|
+
finally:
|
|
476
|
+
self._mesh_job_depth -= 1
|
|
477
|
+
|
|
478
|
+
async def _run_job_inner(self, job: AgentJob) -> None:
|
|
479
|
+
"""Full job lifecycle (shared by the scheduler and nested inline delegation)."""
|
|
376
480
|
job.status = "running"
|
|
377
481
|
start_ms = int(time.time() * 1000)
|
|
378
482
|
|
|
@@ -499,10 +603,17 @@ class AgentMesh:
|
|
|
499
603
|
pass
|
|
500
604
|
|
|
501
605
|
finally:
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
606
|
+
cf = job.completion_future
|
|
607
|
+
if cf is not None and not cf.done():
|
|
608
|
+
try:
|
|
609
|
+
cf.set_result(job)
|
|
610
|
+
except Exception:
|
|
611
|
+
pass
|
|
612
|
+
with self._completed_lock:
|
|
613
|
+
self._completed.append(job)
|
|
614
|
+
# Keep completed list bounded
|
|
615
|
+
if len(self._completed) > 200:
|
|
616
|
+
self._completed = self._completed[-100:]
|
|
506
617
|
|
|
507
618
|
async def _execute_agent_turn(self, job: AgentJob) -> str:
|
|
508
619
|
"""
|
|
@@ -740,15 +851,18 @@ class AgentMesh:
|
|
|
740
851
|
|
|
741
852
|
def status(self) -> dict[str, Any]:
|
|
742
853
|
"""Get mesh status for debugging/display."""
|
|
854
|
+
with self._completed_lock:
|
|
855
|
+
recent = [
|
|
856
|
+
{"job_id": j.job_id, "member": j.member_name, "status": j.status}
|
|
857
|
+
for j in self._completed[-10:]
|
|
858
|
+
]
|
|
859
|
+
n_completed = len(self._completed)
|
|
743
860
|
return {
|
|
744
861
|
"running_jobs": len(self._running),
|
|
745
862
|
"queued_jobs": self._queue.qsize(),
|
|
746
|
-
"completed_jobs":
|
|
863
|
+
"completed_jobs": n_completed,
|
|
747
864
|
"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
|
-
],
|
|
865
|
+
"recent_completed": recent,
|
|
752
866
|
}
|
|
753
867
|
|
|
754
868
|
|
|
@@ -757,12 +871,41 @@ class AgentMesh:
|
|
|
757
871
|
_global_mesh: AgentMesh | None = None
|
|
758
872
|
|
|
759
873
|
|
|
874
|
+
def _sync_mesh_cfg_project_root(mesh: AgentMesh, cfg: GemCodeConfig) -> None:
|
|
875
|
+
"""
|
|
876
|
+
Keep the singleton aligned with the active manager ``cfg.project_root``.
|
|
877
|
+
|
|
878
|
+
If the mesh was created from a different cwd than a later ``ensure_mesh`` /
|
|
879
|
+
``get_mesh`` (e.g. ``-C``), habits/triggers/fleet would otherwise read the
|
|
880
|
+
wrong ``.gemcode/`` tree while the UI used another.
|
|
881
|
+
"""
|
|
882
|
+
try:
|
|
883
|
+
cur = Path(mesh.cfg.project_root).resolve()
|
|
884
|
+
nxt = Path(cfg.project_root).resolve()
|
|
885
|
+
if cur == nxt:
|
|
886
|
+
return
|
|
887
|
+
mesh.cfg.project_root = cfg.project_root
|
|
888
|
+
if mesh._trigger_engine is not None:
|
|
889
|
+
mesh._trigger_engine.cfg = mesh.cfg
|
|
890
|
+
mesh._trigger_engine.reload()
|
|
891
|
+
if mesh._habit_scheduler is not None:
|
|
892
|
+
mesh._habit_scheduler.cfg = mesh.cfg
|
|
893
|
+
if mesh._learner is not None:
|
|
894
|
+
mesh._learner.cfg = mesh.cfg
|
|
895
|
+
if mesh._self_healing is not None:
|
|
896
|
+
mesh._self_healing.cfg = mesh.cfg
|
|
897
|
+
except Exception:
|
|
898
|
+
pass
|
|
899
|
+
|
|
900
|
+
|
|
760
901
|
def get_mesh(cfg: GemCodeConfig | None = None) -> AgentMesh | None:
|
|
761
902
|
"""Get or create the global agent mesh."""
|
|
762
903
|
global _global_mesh
|
|
763
904
|
if _global_mesh is None and cfg is not None:
|
|
764
905
|
concurrency = int(os.environ.get("GEMCODE_MESH_CONCURRENCY", "3"))
|
|
765
906
|
_global_mesh = AgentMesh(cfg, max_concurrency=concurrency)
|
|
907
|
+
elif _global_mesh is not None and cfg is not None:
|
|
908
|
+
_sync_mesh_cfg_project_root(_global_mesh, cfg)
|
|
766
909
|
return _global_mesh
|
|
767
910
|
|
|
768
911
|
|
|
@@ -772,6 +915,8 @@ def ensure_mesh(cfg: GemCodeConfig) -> AgentMesh:
|
|
|
772
915
|
if _global_mesh is None:
|
|
773
916
|
concurrency = int(os.environ.get("GEMCODE_MESH_CONCURRENCY", "3"))
|
|
774
917
|
_global_mesh = AgentMesh(cfg, max_concurrency=concurrency)
|
|
918
|
+
else:
|
|
919
|
+
_sync_mesh_cfg_project_root(_global_mesh, cfg)
|
|
775
920
|
return _global_mesh
|
|
776
921
|
|
|
777
922
|
|
|
@@ -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
|