aindy-runtime 1.1.0__py3-none-any.whl

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 (342) hide show
  1. AINDY/_version.py +1 -0
  2. AINDY/agents/__init__.py +1 -0
  3. AINDY/agents/agent_coordinator.py +464 -0
  4. AINDY/agents/agent_event_service.py +163 -0
  5. AINDY/agents/agent_message_bus.py +199 -0
  6. AINDY/agents/agent_runtime/__init__.py +83 -0
  7. AINDY/agents/agent_runtime/approvals.py +149 -0
  8. AINDY/agents/agent_runtime/creation.py +224 -0
  9. AINDY/agents/agent_runtime/execution.py +299 -0
  10. AINDY/agents/agent_runtime/planner_backends.py +205 -0
  11. AINDY/agents/agent_runtime/planning.py +232 -0
  12. AINDY/agents/agent_runtime/presentation.py +150 -0
  13. AINDY/agents/agent_runtime/replay.py +76 -0
  14. AINDY/agents/agent_runtime/shared.py +97 -0
  15. AINDY/agents/agent_runtime.py +7 -0
  16. AINDY/agents/agent_tools.py +26 -0
  17. AINDY/agents/autonomous_controller.py +219 -0
  18. AINDY/agents/capability_service.py +602 -0
  19. AINDY/agents/dead_letter_service.py +114 -0
  20. AINDY/agents/runtime_api.py +338 -0
  21. AINDY/agents/runtime_guardrails.py +293 -0
  22. AINDY/agents/stuck_run_service.py +397 -0
  23. AINDY/agents/stuck_run_watchdog.py +119 -0
  24. AINDY/agents/tool_registry.py +188 -0
  25. AINDY/agents/tool_syscalls.py +29 -0
  26. AINDY/apscheduler/__init__.py +1 -0
  27. AINDY/apscheduler/schedulers/__init__.py +1 -0
  28. AINDY/apscheduler/schedulers/background.py +51 -0
  29. AINDY/apscheduler/triggers/__init__.py +1 -0
  30. AINDY/apscheduler/triggers/cron.py +20 -0
  31. AINDY/apscheduler/triggers/interval.py +6 -0
  32. AINDY/auth/__init__.py +2 -0
  33. AINDY/auth/api_key_auth.py +22 -0
  34. AINDY/cli.py +605 -0
  35. AINDY/config.py +407 -0
  36. AINDY/core/__init__.py +1 -0
  37. AINDY/core/distributed_queue.py +1237 -0
  38. AINDY/core/execution_dispatcher.py +582 -0
  39. AINDY/core/execution_envelope.py +118 -0
  40. AINDY/core/execution_gate.py +572 -0
  41. AINDY/core/execution_guard.py +110 -0
  42. AINDY/core/execution_helper.py +81 -0
  43. AINDY/core/execution_pipeline/__init__.py +23 -0
  44. AINDY/core/execution_pipeline/context.py +98 -0
  45. AINDY/core/execution_pipeline/pipeline.py +358 -0
  46. AINDY/core/execution_pipeline/resources.py +163 -0
  47. AINDY/core/execution_pipeline/runtime_state.py +121 -0
  48. AINDY/core/execution_pipeline/shared.py +26 -0
  49. AINDY/core/execution_pipeline/signals.py +237 -0
  50. AINDY/core/execution_pipeline/waits.py +91 -0
  51. AINDY/core/execution_record_service.py +88 -0
  52. AINDY/core/execution_service.py +84 -0
  53. AINDY/core/execution_signal_helper.py +135 -0
  54. AINDY/core/execution_unit_service.py +476 -0
  55. AINDY/core/flow_run_rehydration.py +396 -0
  56. AINDY/core/observability_events.py +164 -0
  57. AINDY/core/request_metric_writer.py +155 -0
  58. AINDY/core/response_adapter.py +70 -0
  59. AINDY/core/resume_watchdog.py +134 -0
  60. AINDY/core/retry_policy.py +265 -0
  61. AINDY/core/route_execution_guard.py +249 -0
  62. AINDY/core/router_guard.py +223 -0
  63. AINDY/core/system_event_service.py +626 -0
  64. AINDY/core/system_event_types.py +55 -0
  65. AINDY/core/wait_condition.py +179 -0
  66. AINDY/core/wait_rehydration.py +309 -0
  67. AINDY/db/__init__.py +7 -0
  68. AINDY/db/base.py +5 -0
  69. AINDY/db/create_all.py +15 -0
  70. AINDY/db/dao/__init__.py +153 -0
  71. AINDY/db/dao/memory_node_dao.py +1716 -0
  72. AINDY/db/dao/memory_trace_dao.py +161 -0
  73. AINDY/db/database.py +192 -0
  74. AINDY/db/model_registry.py +73 -0
  75. AINDY/db/models/__init__.py +70 -0
  76. AINDY/db/models/agent.py +54 -0
  77. AINDY/db/models/agent_event.py +52 -0
  78. AINDY/db/models/agent_registry.py +23 -0
  79. AINDY/db/models/agent_run.py +137 -0
  80. AINDY/db/models/api_key.py +84 -0
  81. AINDY/db/models/background_task_lease.py +17 -0
  82. AINDY/db/models/capability.py +80 -0
  83. AINDY/db/models/dynamic_flow.py +55 -0
  84. AINDY/db/models/dynamic_node.py +56 -0
  85. AINDY/db/models/effect_record.py +89 -0
  86. AINDY/db/models/event_edge.py +46 -0
  87. AINDY/db/models/execution_unit.py +178 -0
  88. AINDY/db/models/flow_run.py +94 -0
  89. AINDY/db/models/job_log.py +45 -0
  90. AINDY/db/models/memory_metrics.py +20 -0
  91. AINDY/db/models/memory_node_history.py +44 -0
  92. AINDY/db/models/memory_trace.py +22 -0
  93. AINDY/db/models/memory_trace_node.py +22 -0
  94. AINDY/db/models/nodus_scheduled_job.py +74 -0
  95. AINDY/db/models/nodus_trace_event.py +74 -0
  96. AINDY/db/models/request_metric.py +20 -0
  97. AINDY/db/models/system_event.py +33 -0
  98. AINDY/db/models/system_health_log.py +13 -0
  99. AINDY/db/models/system_state_snapshot.py +23 -0
  100. AINDY/db/models/user.py +26 -0
  101. AINDY/db/models/user_identity.py +69 -0
  102. AINDY/db/models/waiting_flow_run.py +33 -0
  103. AINDY/db/models/webhook_subscription.py +51 -0
  104. AINDY/db/mongo_setup.py +179 -0
  105. AINDY/db/schema_contract.py +628 -0
  106. AINDY/db/schema_ops.py +110 -0
  107. AINDY/deepseek_config.json +27 -0
  108. AINDY/exception_handlers.py +208 -0
  109. AINDY/kernel/__init__.py +171 -0
  110. AINDY/kernel/circuit_breaker.py +177 -0
  111. AINDY/kernel/condition_codes.py +154 -0
  112. AINDY/kernel/errors.py +5 -0
  113. AINDY/kernel/event_bus.py +557 -0
  114. AINDY/kernel/redis_wait_registry.py +115 -0
  115. AINDY/kernel/resource_manager.py +911 -0
  116. AINDY/kernel/resume_spec.py +59 -0
  117. AINDY/kernel/scheduler/__init__.py +32 -0
  118. AINDY/kernel/scheduler/common.py +81 -0
  119. AINDY/kernel/scheduler/core.py +141 -0
  120. AINDY/kernel/scheduler/cross_instance.py +147 -0
  121. AINDY/kernel/scheduler/dispatch.py +85 -0
  122. AINDY/kernel/scheduler/engine.py +32 -0
  123. AINDY/kernel/scheduler/persistence.py +100 -0
  124. AINDY/kernel/scheduler/recovery.py +122 -0
  125. AINDY/kernel/scheduler/waits.py +211 -0
  126. AINDY/kernel/scheduler_engine.py +2 -0
  127. AINDY/kernel/syscall_dispatcher.py +902 -0
  128. AINDY/kernel/syscall_handlers.py +21 -0
  129. AINDY/kernel/syscall_registry.py +1313 -0
  130. AINDY/kernel/syscall_versioning.py +250 -0
  131. AINDY/kernel/tenant_context.py +171 -0
  132. AINDY/main.py +95 -0
  133. AINDY/memory/__init__.py +26 -0
  134. AINDY/memory/bridge.py +263 -0
  135. AINDY/memory/embedding_jobs.py +194 -0
  136. AINDY/memory/embedding_service.py +185 -0
  137. AINDY/memory/ingest_queue.py +190 -0
  138. AINDY/memory/memory_address_space.py +358 -0
  139. AINDY/memory/memory_capture_engine.py +562 -0
  140. AINDY/memory/memory_helpers.py +137 -0
  141. AINDY/memory/memory_ingest_service.py +221 -0
  142. AINDY/memory/memory_persistence.py +248 -0
  143. AINDY/memory/memory_scoring_service.py +181 -0
  144. AINDY/memory/nodus_memory_bridge.py +395 -0
  145. AINDY/middleware.py +185 -0
  146. AINDY/nodus/__init__.py +28 -0
  147. AINDY/nodus/runtime/__init__.py +1 -0
  148. AINDY/nodus/runtime/aindy_runtime.py +14 -0
  149. AINDY/nodus/runtime/embedding.py +4 -0
  150. AINDY/nodus/runtime/memory_bridge.py +265 -0
  151. AINDY/nodus/stdlib/.nodus/deps.json +8 -0
  152. AINDY/platform/dist/assets/AgentApprovalInbox-JvpJhWo-.js +1 -0
  153. AINDY/platform/dist/assets/AgentApprovalInbox-xp4dnTLc.js +1 -0
  154. AINDY/platform/dist/assets/AgentConsole-BwJhlQMZ.js +1 -0
  155. AINDY/platform/dist/assets/AgentConsole-DmK_D2nk.js +1 -0
  156. AINDY/platform/dist/assets/AgentRegistry-DipCGz5Q.js +1 -0
  157. AINDY/platform/dist/assets/AgentRegistry-DosperjU.js +1 -0
  158. AINDY/platform/dist/assets/ExecutionConsole-BpMjUGGt.js +1 -0
  159. AINDY/platform/dist/assets/ExecutionConsole-DviutjyX.js +1 -0
  160. AINDY/platform/dist/assets/FlowEngineConsole-BwPKPq6f.js +3 -0
  161. AINDY/platform/dist/assets/FlowEngineConsole-DgNp6YSR.js +3 -0
  162. AINDY/platform/dist/assets/HealthDashboard-BipaBrfK.js +1 -0
  163. AINDY/platform/dist/assets/HealthDashboard-lJV4ohCV.js +1 -0
  164. AINDY/platform/dist/assets/ObservabilityDashboard-Cjue5l6w.js +68 -0
  165. AINDY/platform/dist/assets/ObservabilityDashboard-dzZSTNIx.js +68 -0
  166. AINDY/platform/dist/assets/RippleTraceViewer-BOf-8HOL.js +1 -0
  167. AINDY/platform/dist/assets/RippleTraceViewer-DmGwtwnz.js +1 -0
  168. AINDY/platform/dist/assets/SurfacePrimitives-ClHsSZxI.js +1 -0
  169. AINDY/platform/dist/assets/SurfacePrimitives-CsnK-Fe4.js +1 -0
  170. AINDY/platform/dist/assets/agent-BQJyJP8K.js +1 -0
  171. AINDY/platform/dist/assets/agent-D2xJbeWj.js +1 -0
  172. AINDY/platform/dist/assets/index-BljliAfI.css +1 -0
  173. AINDY/platform/dist/assets/index-CI4gqdMf.js +77 -0
  174. AINDY/platform/dist/assets/index-DFmlbApI.css +1 -0
  175. AINDY/platform/dist/assets/index-lajStSSe.js +77 -0
  176. AINDY/platform/dist/assets/operator-Cpajhmrr.js +1 -0
  177. AINDY/platform/dist/assets/operator-DY3yPK_P.js +1 -0
  178. AINDY/platform/dist/index.html +13 -0
  179. AINDY/platform_layer/__init__.py +39 -0
  180. AINDY/platform_layer/agent_plugin_contracts.py +50 -0
  181. AINDY/platform_layer/api_key_service.py +162 -0
  182. AINDY/platform_layer/app_runtime.py +19 -0
  183. AINDY/platform_layer/async_execution_context.py +22 -0
  184. AINDY/platform_layer/async_job_service.py +1321 -0
  185. AINDY/platform_layer/bootstrap_contract.py +144 -0
  186. AINDY/platform_layer/bootstrap_graph.py +82 -0
  187. AINDY/platform_layer/cache_backend.py +23 -0
  188. AINDY/platform_layer/deepseek_client.py +228 -0
  189. AINDY/platform_layer/deployment_contract.py +1193 -0
  190. AINDY/platform_layer/domain_health.py +59 -0
  191. AINDY/platform_layer/event_service.py +712 -0
  192. AINDY/platform_layer/event_trace_service.py +291 -0
  193. AINDY/platform_layer/extension_abi.py +288 -0
  194. AINDY/platform_layer/extension_boundary.py +72 -0
  195. AINDY/platform_layer/extension_capabilities.py +225 -0
  196. AINDY/platform_layer/extension_execution_model.py +374 -0
  197. AINDY/platform_layer/extension_policy.py +255 -0
  198. AINDY/platform_layer/extension_provenance.py +418 -0
  199. AINDY/platform_layer/extension_provenance_inventory.py +99 -0
  200. AINDY/platform_layer/extension_runtime_api.py +180 -0
  201. AINDY/platform_layer/extension_runtime_inventory.py +180 -0
  202. AINDY/platform_layer/extension_worker.py +1299 -0
  203. AINDY/platform_layer/external_call_service.py +123 -0
  204. AINDY/platform_layer/health_service.py +924 -0
  205. AINDY/platform_layer/kernel_proc_reader.py +231 -0
  206. AINDY/platform_layer/llm_client.py +105 -0
  207. AINDY/platform_layer/log_config.py +127 -0
  208. AINDY/platform_layer/memory_runtime.py +69 -0
  209. AINDY/platform_layer/metrics.py +340 -0
  210. AINDY/platform_layer/node_registry.py +812 -0
  211. AINDY/platform_layer/nodus_script_store.py +92 -0
  212. AINDY/platform_layer/openai_client.py +319 -0
  213. AINDY/platform_layer/otel.py +98 -0
  214. AINDY/platform_layer/platform_loader.py +255 -0
  215. AINDY/platform_layer/plugin_artifacts.py +164 -0
  216. AINDY/platform_layer/plugin_host.py +1185 -0
  217. AINDY/platform_layer/public_contract.py +417 -0
  218. AINDY/platform_layer/rate_limiter.py +73 -0
  219. AINDY/platform_layer/recovery_jobs.py +279 -0
  220. AINDY/platform_layer/registry.py +1875 -0
  221. AINDY/platform_layer/registry_contracts.py +476 -0
  222. AINDY/platform_layer/response_adapters.py +98 -0
  223. AINDY/platform_layer/runtime_agent_defaults.py +211 -0
  224. AINDY/platform_layer/runtime_callback_host.py +97 -0
  225. AINDY/platform_layer/runtime_callback_worker.py +97 -0
  226. AINDY/platform_layer/runtime_compatibility.py +37 -0
  227. AINDY/platform_layer/sandbox_certification.py +920 -0
  228. AINDY/platform_layer/sandbox_runner.py +2437 -0
  229. AINDY/platform_layer/scheduler_service.py +807 -0
  230. AINDY/platform_layer/system_state_service.py +280 -0
  231. AINDY/platform_layer/trace_context.py +107 -0
  232. AINDY/platform_layer/user_ids.py +31 -0
  233. AINDY/platform_layer/watcher_contract.py +24 -0
  234. AINDY/platform_layer/watcher_service.py +80 -0
  235. AINDY/plugins/nodes/__init__.py +13 -0
  236. AINDY/routes/__init__.py +52 -0
  237. AINDY/routes/agent_router.py +366 -0
  238. AINDY/routes/auth_router.py +178 -0
  239. AINDY/routes/coordination_router.py +443 -0
  240. AINDY/routes/db_verify_router.py +35 -0
  241. AINDY/routes/flow_router.py +200 -0
  242. AINDY/routes/health_router.py +659 -0
  243. AINDY/routes/memory_metrics_router.py +101 -0
  244. AINDY/routes/memory_router.py +737 -0
  245. AINDY/routes/memory_trace_router.py +176 -0
  246. AINDY/routes/observability_router.py +416 -0
  247. AINDY/routes/platform/__init__.py +73 -0
  248. AINDY/routes/platform/admin_router.py +63 -0
  249. AINDY/routes/platform/flows_router.py +167 -0
  250. AINDY/routes/platform/keys_router.py +116 -0
  251. AINDY/routes/platform/nodes_router.py +105 -0
  252. AINDY/routes/platform/nodus_flow_router.py +84 -0
  253. AINDY/routes/platform/nodus_router.py +161 -0
  254. AINDY/routes/platform/nodus_schedule_router.py +97 -0
  255. AINDY/routes/platform/nodus_shared.py +151 -0
  256. AINDY/routes/platform/platform_ops_router.py +257 -0
  257. AINDY/routes/platform/queue_router.py +134 -0
  258. AINDY/routes/platform/schemas.py +146 -0
  259. AINDY/routes/platform/webhooks_router.py +108 -0
  260. AINDY/routes/platform_router.py +121 -0
  261. AINDY/routes/version_router.py +92 -0
  262. AINDY/routes/watcher_router.py +208 -0
  263. AINDY/routing.py +80 -0
  264. AINDY/runtime/__init__.py +227 -0
  265. AINDY/runtime/execution_loop.py +1 -0
  266. AINDY/runtime/execution_registry.py +37 -0
  267. AINDY/runtime/flow_definitions.py +26 -0
  268. AINDY/runtime/flow_definitions_engine.py +181 -0
  269. AINDY/runtime/flow_definitions_extended.py +86 -0
  270. AINDY/runtime/flow_definitions_memory.py +462 -0
  271. AINDY/runtime/flow_definitions_observability.py +204 -0
  272. AINDY/runtime/flow_engine/__init__.py +53 -0
  273. AINDY/runtime/flow_engine/entrypoints.py +167 -0
  274. AINDY/runtime/flow_engine/event_router.py +94 -0
  275. AINDY/runtime/flow_engine/node_executor.py +60 -0
  276. AINDY/runtime/flow_engine/registry.py +38 -0
  277. AINDY/runtime/flow_engine/runner.py +396 -0
  278. AINDY/runtime/flow_engine/runner_completion.py +204 -0
  279. AINDY/runtime/flow_engine/runner_failure.py +106 -0
  280. AINDY/runtime/flow_engine/runner_steps.py +370 -0
  281. AINDY/runtime/flow_engine/serialization.py +173 -0
  282. AINDY/runtime/flow_engine/shared.py +37 -0
  283. AINDY/runtime/flow_helpers.py +20 -0
  284. AINDY/runtime/flow_registry.py +319 -0
  285. AINDY/runtime/memory/__init__.py +19 -0
  286. AINDY/runtime/memory/context_builder.py +35 -0
  287. AINDY/runtime/memory/filters.py +62 -0
  288. AINDY/runtime/memory/memory_feedback.py +64 -0
  289. AINDY/runtime/memory/memory_learning.py +126 -0
  290. AINDY/runtime/memory/memory_metrics.py +151 -0
  291. AINDY/runtime/memory/metrics_store.py +143 -0
  292. AINDY/runtime/memory/native_scorer.py +166 -0
  293. AINDY/runtime/memory/orchestrator.py +194 -0
  294. AINDY/runtime/memory/query_expander.py +19 -0
  295. AINDY/runtime/memory/scorer.py +162 -0
  296. AINDY/runtime/memory/strategies.py +51 -0
  297. AINDY/runtime/memory/types.py +61 -0
  298. AINDY/runtime/memory_loop.py +228 -0
  299. AINDY/runtime/nodus_adapter.py +1002 -0
  300. AINDY/runtime/nodus_builtins.py +530 -0
  301. AINDY/runtime/nodus_execution_service.py +707 -0
  302. AINDY/runtime/nodus_flow_compiler.py +269 -0
  303. AINDY/runtime/nodus_runtime_adapter.py +459 -0
  304. AINDY/runtime/nodus_schedule_service.py +478 -0
  305. AINDY/runtime/nodus_security.py +180 -0
  306. AINDY/runtime/nodus_trace_service.py +151 -0
  307. AINDY/runtime/nodus_worker.py +272 -0
  308. AINDY/runtime_only.py +325 -0
  309. AINDY/runtime_plugins.json +10 -0
  310. AINDY/schemas/__init__.py +2 -0
  311. AINDY/schemas/auth_schemas.py +17 -0
  312. AINDY/services/auth_service.py +489 -0
  313. AINDY/spa_fallback.py +153 -0
  314. AINDY/startup.py +1542 -0
  315. AINDY/system_manifest.json +22 -0
  316. AINDY/utils/__init__.py +36 -0
  317. AINDY/utils/normalize_encoding.py +18 -0
  318. AINDY/utils/sanitize_text.py +41 -0
  319. AINDY/utils/text_constraints.py +66 -0
  320. AINDY/utils/uuid_utils.py +17 -0
  321. AINDY/version.json +6 -0
  322. AINDY/watcher/__init__.py +5 -0
  323. AINDY/watcher/classifier.py +292 -0
  324. AINDY/watcher/config.py +116 -0
  325. AINDY/watcher/constants.py +33 -0
  326. AINDY/watcher/session_tracker.py +338 -0
  327. AINDY/watcher/signal_emitter.py +208 -0
  328. AINDY/watcher/watcher.py +152 -0
  329. AINDY/watcher/window_detector.py +251 -0
  330. AINDY/worker/__init__.py +112 -0
  331. AINDY/worker/__main__.py +85 -0
  332. AINDY/worker/health_server.py +221 -0
  333. AINDY/worker/memory_ingest_worker.py +60 -0
  334. AINDY/worker/metric_writer_worker.py +58 -0
  335. AINDY/worker/worker_loop.py +897 -0
  336. AINDY/worker.py +106 -0
  337. aindy_runtime-1.1.0.dist-info/METADATA +539 -0
  338. aindy_runtime-1.1.0.dist-info/RECORD +342 -0
  339. aindy_runtime-1.1.0.dist-info/WHEEL +5 -0
  340. aindy_runtime-1.1.0.dist-info/entry_points.txt +2 -0
  341. aindy_runtime-1.1.0.dist-info/licenses/LICENSE +21 -0
  342. aindy_runtime-1.1.0.dist-info/top_level.txt +1 -0
AINDY/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.1.0"
@@ -0,0 +1 @@
1
+ # agents -- AINDY agentic execution layer
@@ -0,0 +1,464 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timedelta, timezone
4
+ from typing import Any
5
+
6
+ from AINDY.db.models.agent_registry import AgentRegistry
7
+ from AINDY.db.models.system_event import SystemEvent
8
+ from AINDY.agents.agent_message_bus import publish_operation_request
9
+ from AINDY.agents.runtime_guardrails import AgentRuntimeGuardrailViolation, enforce_delegation_guardrails
10
+ from AINDY.platform_layer.registry import get_agent_ranking_strategy
11
+ from AINDY.utils.uuid_utils import normalize_uuid
12
+
13
+
14
+ STALE_AGENT_MINUTES = 10
15
+
16
+
17
+ def register_or_update_agent(
18
+ db,
19
+ *,
20
+ agent_id: str,
21
+ capabilities: list[str] | None = None,
22
+ current_state: dict[str, Any] | None = None,
23
+ load: float = 0.0,
24
+ health_status: str = "healthy",
25
+ ) -> dict[str, Any]:
26
+ normalized_agent_id = normalize_uuid(agent_id)
27
+ row = db.query(AgentRegistry).filter(AgentRegistry.agent_id == normalized_agent_id).first()
28
+ if row is None:
29
+ row = AgentRegistry(agent_id=normalized_agent_id)
30
+ db.add(row)
31
+ row.capabilities = capabilities or row.capabilities or []
32
+ row.current_state = current_state or row.current_state or {}
33
+ row.load = max(0.0, min(1.0, float(load or 0.0)))
34
+ row.health_status = health_status or "healthy"
35
+ row.last_seen = datetime.now(timezone.utc)
36
+ db.commit()
37
+ db.refresh(row)
38
+ return serialize_agent_registry(row)
39
+
40
+
41
+ def list_agents(db, *, include_stale: bool = False) -> list[dict[str, Any]]:
42
+ query = db.query(AgentRegistry).order_by(AgentRegistry.last_seen.desc())
43
+ rows = query.all()
44
+ now = datetime.now(timezone.utc)
45
+ results = []
46
+ for row in rows:
47
+ serialized = serialize_agent_registry(row)
48
+ serialized["stale"] = _is_stale(row.last_seen, now)
49
+ if include_stale or not serialized["stale"]:
50
+ results.append(serialized)
51
+ return results
52
+
53
+
54
+ def get_agent_status(db) -> dict[str, Any]:
55
+ agents = list_agents(db, include_stale=True)
56
+ healthy = sum(1 for agent in agents if agent["health_status"] == "healthy" and not agent["stale"])
57
+ degraded = sum(1 for agent in agents if agent["health_status"] == "degraded" and not agent["stale"])
58
+ critical = sum(1 for agent in agents if agent["health_status"] == "critical" or agent["stale"])
59
+ return {
60
+ "total_agents": len(agents),
61
+ "healthy_agents": healthy,
62
+ "degraded_agents": degraded,
63
+ "critical_agents": critical,
64
+ "agents": agents,
65
+ }
66
+
67
+
68
+ def assign_operation(
69
+ db,
70
+ operation: dict[str, Any],
71
+ *,
72
+ user_id: str | None = None,
73
+ trace_id: str | None = None,
74
+ sender_agent_id: str | None = None,
75
+ ) -> dict[str, Any] | None:
76
+ candidates = _rank_candidate_agents(db, operation, user_id=user_id)
77
+ if not candidates:
78
+ return None
79
+ best = candidates[0]
80
+ if sender_agent_id:
81
+ publish_operation_request(
82
+ db=db,
83
+ sender_agent_id=sender_agent_id,
84
+ recipient_agent_id=best["agent_id"],
85
+ operation=operation,
86
+ user_id=user_id,
87
+ trace_id=trace_id,
88
+ )
89
+ return best
90
+
91
+
92
+ def broadcast_operation(
93
+ db,
94
+ operation: dict[str, Any],
95
+ *,
96
+ user_id: str | None = None,
97
+ trace_id: str | None = None,
98
+ sender_agent_id: str | None = None,
99
+ limit: int = 5,
100
+ ) -> list[dict[str, Any]]:
101
+ ranked = _rank_candidate_agents(db, operation, user_id=user_id)[:limit]
102
+ if sender_agent_id:
103
+ for candidate in ranked:
104
+ publish_operation_request(
105
+ db=db,
106
+ sender_agent_id=sender_agent_id,
107
+ recipient_agent_id=candidate["agent_id"],
108
+ operation=operation,
109
+ user_id=user_id,
110
+ trace_id=trace_id,
111
+ )
112
+ return ranked
113
+
114
+
115
+ def decide_execution_mode(
116
+ db,
117
+ *,
118
+ local_agent_id: str | None,
119
+ operation: dict[str, Any],
120
+ user_id: str | None = None,
121
+ ) -> dict[str, Any]:
122
+ ranked = _rank_candidate_agents(db, operation, user_id=user_id)
123
+ if not ranked:
124
+ return {"mode": "local", "selected_agent": None, "candidates": []}
125
+
126
+ best = resolve_conflict(ranked)
127
+ if local_agent_id and str(best["agent_id"]) == str(local_agent_id):
128
+ return {"mode": "local", "selected_agent": best, "candidates": ranked[:3]}
129
+ if len(ranked) >= 2 and abs(ranked[0]["coordination_score"] - ranked[1]["coordination_score"]) <= 0.05:
130
+ return {"mode": "collaborate", "selected_agent": best, "candidates": ranked[:3]}
131
+ return {"mode": "delegate", "selected_agent": best, "candidates": ranked[:3]}
132
+
133
+
134
+ def coordination_graph(db, *, user_id: str | None = None, limit: int = 100) -> dict[str, Any]:
135
+ query = (
136
+ db.query(SystemEvent)
137
+ .filter(SystemEvent.agent_id.isnot(None))
138
+ .order_by(SystemEvent.timestamp.desc())
139
+ .limit(limit)
140
+ )
141
+ if user_id:
142
+ query = query.filter(SystemEvent.user_id == normalize_uuid(user_id))
143
+ rows = query.all()
144
+ nodes = {}
145
+ edges = []
146
+ for row in rows:
147
+ agent_key = str(row.agent_id)
148
+ nodes[agent_key] = {
149
+ "id": agent_key,
150
+ "health_status": None,
151
+ "load": None,
152
+ }
153
+ payload = row.payload or {}
154
+ recipient = payload.get("recipient_agent_id")
155
+ if recipient:
156
+ edges.append(
157
+ {
158
+ "source": agent_key,
159
+ "target": str(recipient),
160
+ "event_type": row.type,
161
+ "trace_id": row.trace_id,
162
+ "timestamp": row.timestamp.isoformat() if row.timestamp else None,
163
+ }
164
+ )
165
+ for agent in list_agents(db, include_stale=True):
166
+ nodes[str(agent["agent_id"])] = {
167
+ "id": str(agent["agent_id"]),
168
+ "health_status": agent["health_status"],
169
+ "load": agent["load"],
170
+ }
171
+ return {"nodes": list(nodes.values()), "edges": edges}
172
+
173
+
174
+ def _rank_candidate_agents(db, operation: dict[str, Any], *, user_id: str | None = None) -> list[dict[str, Any]]:
175
+ operation_capabilities = set(operation.get("required_capabilities") or operation.get("capabilities") or [])
176
+ candidates = [
177
+ _enrich_candidate_for_coordination(db, row, operation_capabilities=operation_capabilities, user_id=user_id)
178
+ for row in list_agents(db, include_stale=False)
179
+ ]
180
+ context = {
181
+ "db": db,
182
+ "operation": operation,
183
+ "user_id": user_id,
184
+ "required_capabilities": sorted(operation_capabilities),
185
+ }
186
+
187
+ ranking_strategy = get_agent_ranking_strategy()
188
+ if ranking_strategy is not None:
189
+ ranked = ranking_strategy(candidates, context)
190
+ if isinstance(ranked, list):
191
+ return ranked
192
+
193
+ ranked = list(candidates)
194
+ ranked.sort(key=lambda item: item["coordination_score"], reverse=True)
195
+ return ranked
196
+
197
+
198
+ def _enrich_candidate_for_coordination(
199
+ db,
200
+ row: dict[str, Any],
201
+ *,
202
+ operation_capabilities: set[str],
203
+ user_id: str | None = None,
204
+ ) -> dict[str, Any]:
205
+ capability_overlap = 0.0
206
+ capabilities = set(row.get("capabilities") or [])
207
+ if operation_capabilities:
208
+ capability_overlap = len(operation_capabilities & capabilities) / max(1, len(operation_capabilities))
209
+ elif capabilities:
210
+ capability_overlap = 0.5
211
+
212
+ past_performance = _agent_performance_score(db, row["agent_id"], user_id=user_id)
213
+ score = (
214
+ capability_overlap * 0.45
215
+ + (1.0 - float(row.get("load") or 0.0)) * 0.20
216
+ + past_performance * 0.20
217
+ + (0.15 if row.get("health_status") == "healthy" else 0.05 if row.get("health_status") == "degraded" else 0.0)
218
+ )
219
+ enriched = dict(row)
220
+ enriched["coordination_score"] = round(max(0.0, min(1.0, score)), 4)
221
+ enriched["capability_overlap"] = round(capability_overlap, 4)
222
+ enriched["past_performance"] = round(past_performance, 4)
223
+ return enriched
224
+
225
+
226
+ def resolve_conflict(candidates: list[dict[str, Any]]) -> dict[str, Any]:
227
+ if not candidates:
228
+ raise ValueError("resolve_conflict requires at least one candidate")
229
+ if len(candidates) == 1:
230
+ return candidates[0]
231
+
232
+ top_score = float(candidates[0].get("coordination_score") or 0.0)
233
+ tied = [
234
+ candidate for candidate in candidates
235
+ if abs(float(candidate.get("coordination_score") or 0.0) - top_score) <= 0.05
236
+ ]
237
+ tied.sort(
238
+ key=lambda item: (
239
+ -(float(item.get("capability_overlap") or 0.0)),
240
+ float(item.get("load") or 1.0),
241
+ -(float(item.get("past_performance") or 0.0)),
242
+ )
243
+ )
244
+ return tied[0]
245
+
246
+
247
+ def _agent_performance_score(db, agent_id: str, *, user_id: str | None = None) -> float:
248
+ query = db.query(SystemEvent).filter(SystemEvent.agent_id == normalize_uuid(agent_id))
249
+ if user_id:
250
+ query = query.filter(SystemEvent.user_id == normalize_uuid(user_id))
251
+ rows = query.order_by(SystemEvent.timestamp.desc()).limit(50).all()
252
+ if not rows:
253
+ return 0.5
254
+ successes = sum(1 for row in rows if row.type.endswith(".completed") or row.type == "execution.completed")
255
+ failures = sum(1 for row in rows if row.type.endswith(".failed") or row.type.startswith("error."))
256
+ total = max(1, successes + failures)
257
+ return max(0.1, min(1.0, (successes + 0.5) / (total + 1.0)))
258
+
259
+
260
+ def _is_stale(last_seen: datetime | None, now: datetime) -> bool:
261
+ if not last_seen:
262
+ return True
263
+ if last_seen.tzinfo is None:
264
+ last_seen = last_seen.replace(tzinfo=timezone.utc)
265
+ return last_seen < now - timedelta(minutes=STALE_AGENT_MINUTES)
266
+
267
+
268
+ def serialize_agent_registry(row: AgentRegistry) -> dict[str, Any]:
269
+ return {
270
+ "agent_id": str(row.agent_id),
271
+ "capabilities": row.capabilities or [],
272
+ "current_state": row.current_state or {},
273
+ "load": float(row.load or 0.0),
274
+ "health_status": row.health_status,
275
+ "last_seen": row.last_seen.isoformat() if row.last_seen else None,
276
+ }
277
+
278
+
279
+ def dispatch_delegated_run(
280
+ db,
281
+ *,
282
+ parent_run,
283
+ selected_agent: dict,
284
+ delegation_mode: str,
285
+ user_id: str,
286
+ trace_id: str | None = None,
287
+ ) -> dict[str, Any] | None:
288
+ try:
289
+ import uuid as _uuid
290
+
291
+ from AINDY.agents.agent_runtime.shared import _OBJECTIVE_ATTR, _run_objective
292
+ from AINDY.agents.agent_runtime.shared import LOCAL_AGENT_ID
293
+ from AINDY.agents.capability_service import mint_token
294
+ from AINDY.db.models import AgentRun
295
+
296
+ objective = _run_objective(parent_run)
297
+ if not objective or getattr(parent_run, "user_id", None) is None:
298
+ return None
299
+
300
+ enforce_delegation_guardrails(
301
+ db,
302
+ parent_run=parent_run,
303
+ selected_agent_id=selected_agent.get("agent_id"),
304
+ trace_id=trace_id or parent_run.trace_id,
305
+ )
306
+ child_run_id = _uuid.uuid4()
307
+ child_correlation_id = f"run_{_uuid.uuid4()}"
308
+ selected_agent_id = normalize_uuid(selected_agent.get("agent_id"))
309
+
310
+ child_run = AgentRun(
311
+ id=child_run_id,
312
+ user_id=parent_run.user_id,
313
+ agent_type=str(selected_agent.get("agent_id", "default"))[:64],
314
+ plan=parent_run.plan,
315
+ executive_summary=parent_run.executive_summary,
316
+ overall_risk=parent_run.overall_risk or "high",
317
+ status="approved",
318
+ steps_total=parent_run.steps_total,
319
+ correlation_id=child_correlation_id,
320
+ trace_id=trace_id or parent_run.trace_id,
321
+ parent_run_id=parent_run.id,
322
+ spawned_by_agent_id=selected_agent_id,
323
+ coordination_role=delegation_mode,
324
+ )
325
+ setattr(child_run, _OBJECTIVE_ATTR, objective)
326
+ db.add(child_run)
327
+ db.flush()
328
+
329
+ child_token = mint_token(
330
+ run_id=str(child_run_id),
331
+ user_id=str(parent_run.user_id),
332
+ plan=child_run.plan,
333
+ db=db,
334
+ approval_mode="manual",
335
+ agent_type=getattr(parent_run, "agent_type", "default") or "default",
336
+ )
337
+ if child_token:
338
+ child_run.capability_token = child_token
339
+ child_run.execution_token = child_token.get("execution_token")
340
+
341
+ parent_run.status = "delegated"
342
+ parent_run.completed_at = None
343
+ db.flush()
344
+ db.refresh(child_run)
345
+
346
+ assign_operation(
347
+ db,
348
+ operation={
349
+ "name": objective,
350
+ "description": parent_run.executive_summary or objective,
351
+ "request": objective,
352
+ "required_capabilities": list(
353
+ (child_token or {}).get("allowed_capabilities") or []
354
+ ),
355
+ "child_run_id": str(child_run.id),
356
+ "parent_run_id": str(parent_run.id),
357
+ },
358
+ user_id=user_id,
359
+ trace_id=trace_id or parent_run.trace_id,
360
+ sender_agent_id=str(getattr(parent_run, "spawned_by_agent_id", None) or LOCAL_AGENT_ID),
361
+ )
362
+ return _serialize_delegated_run(child_run)
363
+ except AgentRuntimeGuardrailViolation:
364
+ raise
365
+ except Exception as exc:
366
+ import logging as _logging
367
+
368
+ _logging.getLogger(__name__).warning(
369
+ "[AgentCoordinator] dispatch_delegated_run failed: %s", exc
370
+ )
371
+ return None
372
+
373
+
374
+ def _serialize_delegated_run(run) -> dict[str, Any]:
375
+ return {
376
+ "run_id": str(run.id),
377
+ "parent_run_id": str(run.parent_run_id) if run.parent_run_id else None,
378
+ "spawned_by_agent_id": str(run.spawned_by_agent_id) if run.spawned_by_agent_id else None,
379
+ "status": run.status,
380
+ "coordination_role": run.coordination_role,
381
+ "correlation_id": run.correlation_id,
382
+ }
383
+
384
+
385
+ def detect_run_conflict(
386
+ db,
387
+ *,
388
+ user_id: str,
389
+ objective: str,
390
+ agent_id: str | None = None,
391
+ ) -> dict[str, Any]:
392
+ from AINDY.agents.agent_runtime.shared import _run_objective
393
+ from AINDY.db.models import AgentRun
394
+
395
+ uid = normalize_uuid(user_id)
396
+ active_runs = (
397
+ db.query(AgentRun)
398
+ .filter(
399
+ AgentRun.user_id == uid,
400
+ AgentRun.status.in_(["approved", "executing", "delegated"]),
401
+ )
402
+ .order_by(AgentRun.created_at.desc())
403
+ .limit(20)
404
+ .all()
405
+ )
406
+ normalized_objective = str(objective or "").strip().lower()
407
+ for run in active_runs:
408
+ run_obj = str(_run_objective(run) or "").strip().lower()
409
+ if run_obj == normalized_objective:
410
+ if agent_id and getattr(run, "correlation_id", None) == agent_id:
411
+ continue
412
+ return {
413
+ "conflict": True,
414
+ "conflicting_run_id": str(run.id),
415
+ "conflicting_status": run.status,
416
+ }
417
+ return {
418
+ "conflict": False,
419
+ "conflicting_run_id": None,
420
+ "conflicting_status": None,
421
+ }
422
+
423
+
424
+ def detect_memory_write_conflict(
425
+ db,
426
+ *,
427
+ user_id: str,
428
+ memory_path: str,
429
+ agent_id: str | None = None,
430
+ ) -> dict[str, Any]:
431
+ uid = normalize_uuid(user_id)
432
+ window = datetime.now(timezone.utc) - timedelta(seconds=30)
433
+ recent = (
434
+ db.query(SystemEvent)
435
+ .filter(
436
+ SystemEvent.user_id == uid,
437
+ SystemEvent.type == "agent.message.memory_share",
438
+ SystemEvent.timestamp >= window,
439
+ )
440
+ .order_by(SystemEvent.timestamp.desc())
441
+ .limit(10)
442
+ .all()
443
+ )
444
+ for event in recent:
445
+ payload = event.payload or {}
446
+ if payload.get("memory_path") == memory_path:
447
+ conflicting_agent = str(event.agent_id) if event.agent_id else None
448
+ if agent_id and conflicting_agent == agent_id:
449
+ continue
450
+ return {
451
+ "conflict": True,
452
+ "conflicting_agent_id": conflicting_agent,
453
+ "message": (
454
+ f"Memory path '{memory_path}' was written by agent "
455
+ f"{conflicting_agent} within the last 30 seconds."
456
+ ),
457
+ }
458
+ return {
459
+ "conflict": False,
460
+ "conflicting_agent_id": None,
461
+ "message": "No conflict detected.",
462
+ }
463
+
464
+
@@ -0,0 +1,163 @@
1
+ """
2
+ AgentEventService — thin helper for emitting lifecycle events (Sprint N+8).
3
+
4
+ emit_event() is the single entry point for agent lifecycle persistence.
5
+ Critical execution paths pass required=True so missing audit events fail closed.
6
+
7
+ Usage:
8
+ from AINDY.agents.agent_event_service import emit_event
9
+ emit_event(
10
+ run_id=str(run.id),
11
+ user_id=run.user_id,
12
+ correlation_id=run.correlation_id,
13
+ event_type="PLAN_CREATED",
14
+ payload={"overall_risk": "low", "steps_total": 3},
15
+ db=db,
16
+ )
17
+ """
18
+ import logging
19
+ import uuid
20
+ from datetime import datetime, timezone
21
+ from typing import Optional
22
+
23
+ from sqlalchemy.orm import Session
24
+
25
+ from AINDY.core.execution_signal_helper import queue_system_event
26
+ from AINDY.core.system_event_service import SystemEventEmissionError
27
+ from AINDY.platform_layer.trace_context import get_parent_event_id
28
+ from AINDY.platform_layer.trace_context import get_trace_id
29
+ from AINDY.utils.uuid_utils import normalize_uuid
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ AGENT_EVENT_TYPES = {
34
+ "PLAN_CREATED",
35
+ "APPROVED",
36
+ "REJECTED",
37
+ "EXECUTION_STARTED",
38
+ "COMPLETED",
39
+ "EXECUTION_FAILED",
40
+ "CAPABILITY_DENIED",
41
+ "RECOVERED",
42
+ "REPLAY_CREATED",
43
+ }
44
+
45
+
46
+ def emit_event(
47
+ run_id: str,
48
+ user_id: str,
49
+ event_type: str,
50
+ db: Session,
51
+ correlation_id: Optional[str] = None,
52
+ payload: Optional[dict] = None,
53
+ required: bool = False,
54
+ ) -> str | None:
55
+ """
56
+ Persist one AgentEvent lifecycle row.
57
+
58
+ Raises when required=True and either the AgentEvent row or matching
59
+ SystemEvent cannot be persisted.
60
+
61
+ Args:
62
+ run_id: UUID string of the AgentRun
63
+ user_id: Owner user ID
64
+ event_type: One of AGENT_EVENT_TYPES (PLAN_CREATED, APPROVED, etc.)
65
+ db: SQLAlchemy session
66
+ correlation_id: Optional run_<uuid4> token (None for pre-N+8 runs)
67
+ payload: Optional dict of event-specific data
68
+ """
69
+ try:
70
+ from AINDY.db.models import AgentEvent
71
+
72
+ if event_type not in AGENT_EVENT_TYPES:
73
+ logger.warning(
74
+ "[AgentEventService] Unknown event type %s for run %s",
75
+ event_type,
76
+ run_id,
77
+ )
78
+
79
+ parsed_run_id = run_id
80
+ if isinstance(run_id, str):
81
+ try:
82
+ parsed_run_id = uuid.UUID(run_id)
83
+ except ValueError:
84
+ parsed_run_id = run_id
85
+
86
+ normalized_user_id = normalize_uuid(user_id) if user_id is not None else None
87
+
88
+ system_event_id = queue_system_event(
89
+ db=db,
90
+ event_type=f"agent.{str(event_type).lower()}",
91
+ user_id=user_id,
92
+ trace_id=get_trace_id() or correlation_id or run_id,
93
+ parent_event_id=get_parent_event_id(),
94
+ source="agent",
95
+ payload={
96
+ "run_id": run_id,
97
+ "correlation_id": correlation_id,
98
+ "event_type": event_type,
99
+ **(payload or {}),
100
+ },
101
+ required=required,
102
+ )
103
+
104
+ normalized_system_event_id = None
105
+ if system_event_id:
106
+ try:
107
+ candidate = uuid.UUID(str(system_event_id))
108
+ from AINDY.db.models.system_event import SystemEvent
109
+
110
+ exists = (
111
+ db.query(SystemEvent.id)
112
+ .filter(SystemEvent.id == candidate)
113
+ .first()
114
+ )
115
+ if exists:
116
+ normalized_system_event_id = normalize_uuid(candidate)
117
+ else:
118
+ logger.warning(
119
+ "[AgentEventService] SystemEvent %s missing; linking skipped for %s",
120
+ system_event_id,
121
+ event_type,
122
+ )
123
+ except Exception:
124
+ logger.warning(
125
+ "[AgentEventService] Invalid SystemEvent %s for %s",
126
+ system_event_id,
127
+ event_type,
128
+ )
129
+
130
+ event = AgentEvent(
131
+ id=uuid.uuid4(),
132
+ run_id=parsed_run_id,
133
+ correlation_id=correlation_id,
134
+ user_id=normalized_user_id,
135
+ event_type=event_type,
136
+ payload=payload or {},
137
+ system_event_id=normalized_system_event_id,
138
+ occurred_at=datetime.now(timezone.utc),
139
+ )
140
+ db.add(event)
141
+ db.commit()
142
+
143
+ logger.debug(
144
+ "[AgentEventService] Emitted %s for run %s (correlation=%s)",
145
+ event_type,
146
+ run_id,
147
+ correlation_id,
148
+ )
149
+ return str(system_event_id) if system_event_id else None
150
+
151
+ except Exception as exc:
152
+ logger.warning(
153
+ "[AgentEventService] Failed to emit %s for run %s: %s",
154
+ event_type,
155
+ run_id,
156
+ exc,
157
+ )
158
+ if required:
159
+ raise SystemEventEmissionError(
160
+ f"Required agent event '{event_type}' failed for run {run_id}"
161
+ ) from exc
162
+ return None
163
+