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.
- AINDY/_version.py +1 -0
- AINDY/agents/__init__.py +1 -0
- AINDY/agents/agent_coordinator.py +464 -0
- AINDY/agents/agent_event_service.py +163 -0
- AINDY/agents/agent_message_bus.py +199 -0
- AINDY/agents/agent_runtime/__init__.py +83 -0
- AINDY/agents/agent_runtime/approvals.py +149 -0
- AINDY/agents/agent_runtime/creation.py +224 -0
- AINDY/agents/agent_runtime/execution.py +299 -0
- AINDY/agents/agent_runtime/planner_backends.py +205 -0
- AINDY/agents/agent_runtime/planning.py +232 -0
- AINDY/agents/agent_runtime/presentation.py +150 -0
- AINDY/agents/agent_runtime/replay.py +76 -0
- AINDY/agents/agent_runtime/shared.py +97 -0
- AINDY/agents/agent_runtime.py +7 -0
- AINDY/agents/agent_tools.py +26 -0
- AINDY/agents/autonomous_controller.py +219 -0
- AINDY/agents/capability_service.py +602 -0
- AINDY/agents/dead_letter_service.py +114 -0
- AINDY/agents/runtime_api.py +338 -0
- AINDY/agents/runtime_guardrails.py +293 -0
- AINDY/agents/stuck_run_service.py +397 -0
- AINDY/agents/stuck_run_watchdog.py +119 -0
- AINDY/agents/tool_registry.py +188 -0
- AINDY/agents/tool_syscalls.py +29 -0
- AINDY/apscheduler/__init__.py +1 -0
- AINDY/apscheduler/schedulers/__init__.py +1 -0
- AINDY/apscheduler/schedulers/background.py +51 -0
- AINDY/apscheduler/triggers/__init__.py +1 -0
- AINDY/apscheduler/triggers/cron.py +20 -0
- AINDY/apscheduler/triggers/interval.py +6 -0
- AINDY/auth/__init__.py +2 -0
- AINDY/auth/api_key_auth.py +22 -0
- AINDY/cli.py +605 -0
- AINDY/config.py +407 -0
- AINDY/core/__init__.py +1 -0
- AINDY/core/distributed_queue.py +1237 -0
- AINDY/core/execution_dispatcher.py +582 -0
- AINDY/core/execution_envelope.py +118 -0
- AINDY/core/execution_gate.py +572 -0
- AINDY/core/execution_guard.py +110 -0
- AINDY/core/execution_helper.py +81 -0
- AINDY/core/execution_pipeline/__init__.py +23 -0
- AINDY/core/execution_pipeline/context.py +98 -0
- AINDY/core/execution_pipeline/pipeline.py +358 -0
- AINDY/core/execution_pipeline/resources.py +163 -0
- AINDY/core/execution_pipeline/runtime_state.py +121 -0
- AINDY/core/execution_pipeline/shared.py +26 -0
- AINDY/core/execution_pipeline/signals.py +237 -0
- AINDY/core/execution_pipeline/waits.py +91 -0
- AINDY/core/execution_record_service.py +88 -0
- AINDY/core/execution_service.py +84 -0
- AINDY/core/execution_signal_helper.py +135 -0
- AINDY/core/execution_unit_service.py +476 -0
- AINDY/core/flow_run_rehydration.py +396 -0
- AINDY/core/observability_events.py +164 -0
- AINDY/core/request_metric_writer.py +155 -0
- AINDY/core/response_adapter.py +70 -0
- AINDY/core/resume_watchdog.py +134 -0
- AINDY/core/retry_policy.py +265 -0
- AINDY/core/route_execution_guard.py +249 -0
- AINDY/core/router_guard.py +223 -0
- AINDY/core/system_event_service.py +626 -0
- AINDY/core/system_event_types.py +55 -0
- AINDY/core/wait_condition.py +179 -0
- AINDY/core/wait_rehydration.py +309 -0
- AINDY/db/__init__.py +7 -0
- AINDY/db/base.py +5 -0
- AINDY/db/create_all.py +15 -0
- AINDY/db/dao/__init__.py +153 -0
- AINDY/db/dao/memory_node_dao.py +1716 -0
- AINDY/db/dao/memory_trace_dao.py +161 -0
- AINDY/db/database.py +192 -0
- AINDY/db/model_registry.py +73 -0
- AINDY/db/models/__init__.py +70 -0
- AINDY/db/models/agent.py +54 -0
- AINDY/db/models/agent_event.py +52 -0
- AINDY/db/models/agent_registry.py +23 -0
- AINDY/db/models/agent_run.py +137 -0
- AINDY/db/models/api_key.py +84 -0
- AINDY/db/models/background_task_lease.py +17 -0
- AINDY/db/models/capability.py +80 -0
- AINDY/db/models/dynamic_flow.py +55 -0
- AINDY/db/models/dynamic_node.py +56 -0
- AINDY/db/models/effect_record.py +89 -0
- AINDY/db/models/event_edge.py +46 -0
- AINDY/db/models/execution_unit.py +178 -0
- AINDY/db/models/flow_run.py +94 -0
- AINDY/db/models/job_log.py +45 -0
- AINDY/db/models/memory_metrics.py +20 -0
- AINDY/db/models/memory_node_history.py +44 -0
- AINDY/db/models/memory_trace.py +22 -0
- AINDY/db/models/memory_trace_node.py +22 -0
- AINDY/db/models/nodus_scheduled_job.py +74 -0
- AINDY/db/models/nodus_trace_event.py +74 -0
- AINDY/db/models/request_metric.py +20 -0
- AINDY/db/models/system_event.py +33 -0
- AINDY/db/models/system_health_log.py +13 -0
- AINDY/db/models/system_state_snapshot.py +23 -0
- AINDY/db/models/user.py +26 -0
- AINDY/db/models/user_identity.py +69 -0
- AINDY/db/models/waiting_flow_run.py +33 -0
- AINDY/db/models/webhook_subscription.py +51 -0
- AINDY/db/mongo_setup.py +179 -0
- AINDY/db/schema_contract.py +628 -0
- AINDY/db/schema_ops.py +110 -0
- AINDY/deepseek_config.json +27 -0
- AINDY/exception_handlers.py +208 -0
- AINDY/kernel/__init__.py +171 -0
- AINDY/kernel/circuit_breaker.py +177 -0
- AINDY/kernel/condition_codes.py +154 -0
- AINDY/kernel/errors.py +5 -0
- AINDY/kernel/event_bus.py +557 -0
- AINDY/kernel/redis_wait_registry.py +115 -0
- AINDY/kernel/resource_manager.py +911 -0
- AINDY/kernel/resume_spec.py +59 -0
- AINDY/kernel/scheduler/__init__.py +32 -0
- AINDY/kernel/scheduler/common.py +81 -0
- AINDY/kernel/scheduler/core.py +141 -0
- AINDY/kernel/scheduler/cross_instance.py +147 -0
- AINDY/kernel/scheduler/dispatch.py +85 -0
- AINDY/kernel/scheduler/engine.py +32 -0
- AINDY/kernel/scheduler/persistence.py +100 -0
- AINDY/kernel/scheduler/recovery.py +122 -0
- AINDY/kernel/scheduler/waits.py +211 -0
- AINDY/kernel/scheduler_engine.py +2 -0
- AINDY/kernel/syscall_dispatcher.py +902 -0
- AINDY/kernel/syscall_handlers.py +21 -0
- AINDY/kernel/syscall_registry.py +1313 -0
- AINDY/kernel/syscall_versioning.py +250 -0
- AINDY/kernel/tenant_context.py +171 -0
- AINDY/main.py +95 -0
- AINDY/memory/__init__.py +26 -0
- AINDY/memory/bridge.py +263 -0
- AINDY/memory/embedding_jobs.py +194 -0
- AINDY/memory/embedding_service.py +185 -0
- AINDY/memory/ingest_queue.py +190 -0
- AINDY/memory/memory_address_space.py +358 -0
- AINDY/memory/memory_capture_engine.py +562 -0
- AINDY/memory/memory_helpers.py +137 -0
- AINDY/memory/memory_ingest_service.py +221 -0
- AINDY/memory/memory_persistence.py +248 -0
- AINDY/memory/memory_scoring_service.py +181 -0
- AINDY/memory/nodus_memory_bridge.py +395 -0
- AINDY/middleware.py +185 -0
- AINDY/nodus/__init__.py +28 -0
- AINDY/nodus/runtime/__init__.py +1 -0
- AINDY/nodus/runtime/aindy_runtime.py +14 -0
- AINDY/nodus/runtime/embedding.py +4 -0
- AINDY/nodus/runtime/memory_bridge.py +265 -0
- AINDY/nodus/stdlib/.nodus/deps.json +8 -0
- AINDY/platform/dist/assets/AgentApprovalInbox-JvpJhWo-.js +1 -0
- AINDY/platform/dist/assets/AgentApprovalInbox-xp4dnTLc.js +1 -0
- AINDY/platform/dist/assets/AgentConsole-BwJhlQMZ.js +1 -0
- AINDY/platform/dist/assets/AgentConsole-DmK_D2nk.js +1 -0
- AINDY/platform/dist/assets/AgentRegistry-DipCGz5Q.js +1 -0
- AINDY/platform/dist/assets/AgentRegistry-DosperjU.js +1 -0
- AINDY/platform/dist/assets/ExecutionConsole-BpMjUGGt.js +1 -0
- AINDY/platform/dist/assets/ExecutionConsole-DviutjyX.js +1 -0
- AINDY/platform/dist/assets/FlowEngineConsole-BwPKPq6f.js +3 -0
- AINDY/platform/dist/assets/FlowEngineConsole-DgNp6YSR.js +3 -0
- AINDY/platform/dist/assets/HealthDashboard-BipaBrfK.js +1 -0
- AINDY/platform/dist/assets/HealthDashboard-lJV4ohCV.js +1 -0
- AINDY/platform/dist/assets/ObservabilityDashboard-Cjue5l6w.js +68 -0
- AINDY/platform/dist/assets/ObservabilityDashboard-dzZSTNIx.js +68 -0
- AINDY/platform/dist/assets/RippleTraceViewer-BOf-8HOL.js +1 -0
- AINDY/platform/dist/assets/RippleTraceViewer-DmGwtwnz.js +1 -0
- AINDY/platform/dist/assets/SurfacePrimitives-ClHsSZxI.js +1 -0
- AINDY/platform/dist/assets/SurfacePrimitives-CsnK-Fe4.js +1 -0
- AINDY/platform/dist/assets/agent-BQJyJP8K.js +1 -0
- AINDY/platform/dist/assets/agent-D2xJbeWj.js +1 -0
- AINDY/platform/dist/assets/index-BljliAfI.css +1 -0
- AINDY/platform/dist/assets/index-CI4gqdMf.js +77 -0
- AINDY/platform/dist/assets/index-DFmlbApI.css +1 -0
- AINDY/platform/dist/assets/index-lajStSSe.js +77 -0
- AINDY/platform/dist/assets/operator-Cpajhmrr.js +1 -0
- AINDY/platform/dist/assets/operator-DY3yPK_P.js +1 -0
- AINDY/platform/dist/index.html +13 -0
- AINDY/platform_layer/__init__.py +39 -0
- AINDY/platform_layer/agent_plugin_contracts.py +50 -0
- AINDY/platform_layer/api_key_service.py +162 -0
- AINDY/platform_layer/app_runtime.py +19 -0
- AINDY/platform_layer/async_execution_context.py +22 -0
- AINDY/platform_layer/async_job_service.py +1321 -0
- AINDY/platform_layer/bootstrap_contract.py +144 -0
- AINDY/platform_layer/bootstrap_graph.py +82 -0
- AINDY/platform_layer/cache_backend.py +23 -0
- AINDY/platform_layer/deepseek_client.py +228 -0
- AINDY/platform_layer/deployment_contract.py +1193 -0
- AINDY/platform_layer/domain_health.py +59 -0
- AINDY/platform_layer/event_service.py +712 -0
- AINDY/platform_layer/event_trace_service.py +291 -0
- AINDY/platform_layer/extension_abi.py +288 -0
- AINDY/platform_layer/extension_boundary.py +72 -0
- AINDY/platform_layer/extension_capabilities.py +225 -0
- AINDY/platform_layer/extension_execution_model.py +374 -0
- AINDY/platform_layer/extension_policy.py +255 -0
- AINDY/platform_layer/extension_provenance.py +418 -0
- AINDY/platform_layer/extension_provenance_inventory.py +99 -0
- AINDY/platform_layer/extension_runtime_api.py +180 -0
- AINDY/platform_layer/extension_runtime_inventory.py +180 -0
- AINDY/platform_layer/extension_worker.py +1299 -0
- AINDY/platform_layer/external_call_service.py +123 -0
- AINDY/platform_layer/health_service.py +924 -0
- AINDY/platform_layer/kernel_proc_reader.py +231 -0
- AINDY/platform_layer/llm_client.py +105 -0
- AINDY/platform_layer/log_config.py +127 -0
- AINDY/platform_layer/memory_runtime.py +69 -0
- AINDY/platform_layer/metrics.py +340 -0
- AINDY/platform_layer/node_registry.py +812 -0
- AINDY/platform_layer/nodus_script_store.py +92 -0
- AINDY/platform_layer/openai_client.py +319 -0
- AINDY/platform_layer/otel.py +98 -0
- AINDY/platform_layer/platform_loader.py +255 -0
- AINDY/platform_layer/plugin_artifacts.py +164 -0
- AINDY/platform_layer/plugin_host.py +1185 -0
- AINDY/platform_layer/public_contract.py +417 -0
- AINDY/platform_layer/rate_limiter.py +73 -0
- AINDY/platform_layer/recovery_jobs.py +279 -0
- AINDY/platform_layer/registry.py +1875 -0
- AINDY/platform_layer/registry_contracts.py +476 -0
- AINDY/platform_layer/response_adapters.py +98 -0
- AINDY/platform_layer/runtime_agent_defaults.py +211 -0
- AINDY/platform_layer/runtime_callback_host.py +97 -0
- AINDY/platform_layer/runtime_callback_worker.py +97 -0
- AINDY/platform_layer/runtime_compatibility.py +37 -0
- AINDY/platform_layer/sandbox_certification.py +920 -0
- AINDY/platform_layer/sandbox_runner.py +2437 -0
- AINDY/platform_layer/scheduler_service.py +807 -0
- AINDY/platform_layer/system_state_service.py +280 -0
- AINDY/platform_layer/trace_context.py +107 -0
- AINDY/platform_layer/user_ids.py +31 -0
- AINDY/platform_layer/watcher_contract.py +24 -0
- AINDY/platform_layer/watcher_service.py +80 -0
- AINDY/plugins/nodes/__init__.py +13 -0
- AINDY/routes/__init__.py +52 -0
- AINDY/routes/agent_router.py +366 -0
- AINDY/routes/auth_router.py +178 -0
- AINDY/routes/coordination_router.py +443 -0
- AINDY/routes/db_verify_router.py +35 -0
- AINDY/routes/flow_router.py +200 -0
- AINDY/routes/health_router.py +659 -0
- AINDY/routes/memory_metrics_router.py +101 -0
- AINDY/routes/memory_router.py +737 -0
- AINDY/routes/memory_trace_router.py +176 -0
- AINDY/routes/observability_router.py +416 -0
- AINDY/routes/platform/__init__.py +73 -0
- AINDY/routes/platform/admin_router.py +63 -0
- AINDY/routes/platform/flows_router.py +167 -0
- AINDY/routes/platform/keys_router.py +116 -0
- AINDY/routes/platform/nodes_router.py +105 -0
- AINDY/routes/platform/nodus_flow_router.py +84 -0
- AINDY/routes/platform/nodus_router.py +161 -0
- AINDY/routes/platform/nodus_schedule_router.py +97 -0
- AINDY/routes/platform/nodus_shared.py +151 -0
- AINDY/routes/platform/platform_ops_router.py +257 -0
- AINDY/routes/platform/queue_router.py +134 -0
- AINDY/routes/platform/schemas.py +146 -0
- AINDY/routes/platform/webhooks_router.py +108 -0
- AINDY/routes/platform_router.py +121 -0
- AINDY/routes/version_router.py +92 -0
- AINDY/routes/watcher_router.py +208 -0
- AINDY/routing.py +80 -0
- AINDY/runtime/__init__.py +227 -0
- AINDY/runtime/execution_loop.py +1 -0
- AINDY/runtime/execution_registry.py +37 -0
- AINDY/runtime/flow_definitions.py +26 -0
- AINDY/runtime/flow_definitions_engine.py +181 -0
- AINDY/runtime/flow_definitions_extended.py +86 -0
- AINDY/runtime/flow_definitions_memory.py +462 -0
- AINDY/runtime/flow_definitions_observability.py +204 -0
- AINDY/runtime/flow_engine/__init__.py +53 -0
- AINDY/runtime/flow_engine/entrypoints.py +167 -0
- AINDY/runtime/flow_engine/event_router.py +94 -0
- AINDY/runtime/flow_engine/node_executor.py +60 -0
- AINDY/runtime/flow_engine/registry.py +38 -0
- AINDY/runtime/flow_engine/runner.py +396 -0
- AINDY/runtime/flow_engine/runner_completion.py +204 -0
- AINDY/runtime/flow_engine/runner_failure.py +106 -0
- AINDY/runtime/flow_engine/runner_steps.py +370 -0
- AINDY/runtime/flow_engine/serialization.py +173 -0
- AINDY/runtime/flow_engine/shared.py +37 -0
- AINDY/runtime/flow_helpers.py +20 -0
- AINDY/runtime/flow_registry.py +319 -0
- AINDY/runtime/memory/__init__.py +19 -0
- AINDY/runtime/memory/context_builder.py +35 -0
- AINDY/runtime/memory/filters.py +62 -0
- AINDY/runtime/memory/memory_feedback.py +64 -0
- AINDY/runtime/memory/memory_learning.py +126 -0
- AINDY/runtime/memory/memory_metrics.py +151 -0
- AINDY/runtime/memory/metrics_store.py +143 -0
- AINDY/runtime/memory/native_scorer.py +166 -0
- AINDY/runtime/memory/orchestrator.py +194 -0
- AINDY/runtime/memory/query_expander.py +19 -0
- AINDY/runtime/memory/scorer.py +162 -0
- AINDY/runtime/memory/strategies.py +51 -0
- AINDY/runtime/memory/types.py +61 -0
- AINDY/runtime/memory_loop.py +228 -0
- AINDY/runtime/nodus_adapter.py +1002 -0
- AINDY/runtime/nodus_builtins.py +530 -0
- AINDY/runtime/nodus_execution_service.py +707 -0
- AINDY/runtime/nodus_flow_compiler.py +269 -0
- AINDY/runtime/nodus_runtime_adapter.py +459 -0
- AINDY/runtime/nodus_schedule_service.py +478 -0
- AINDY/runtime/nodus_security.py +180 -0
- AINDY/runtime/nodus_trace_service.py +151 -0
- AINDY/runtime/nodus_worker.py +272 -0
- AINDY/runtime_only.py +325 -0
- AINDY/runtime_plugins.json +10 -0
- AINDY/schemas/__init__.py +2 -0
- AINDY/schemas/auth_schemas.py +17 -0
- AINDY/services/auth_service.py +489 -0
- AINDY/spa_fallback.py +153 -0
- AINDY/startup.py +1542 -0
- AINDY/system_manifest.json +22 -0
- AINDY/utils/__init__.py +36 -0
- AINDY/utils/normalize_encoding.py +18 -0
- AINDY/utils/sanitize_text.py +41 -0
- AINDY/utils/text_constraints.py +66 -0
- AINDY/utils/uuid_utils.py +17 -0
- AINDY/version.json +6 -0
- AINDY/watcher/__init__.py +5 -0
- AINDY/watcher/classifier.py +292 -0
- AINDY/watcher/config.py +116 -0
- AINDY/watcher/constants.py +33 -0
- AINDY/watcher/session_tracker.py +338 -0
- AINDY/watcher/signal_emitter.py +208 -0
- AINDY/watcher/watcher.py +152 -0
- AINDY/watcher/window_detector.py +251 -0
- AINDY/worker/__init__.py +112 -0
- AINDY/worker/__main__.py +85 -0
- AINDY/worker/health_server.py +221 -0
- AINDY/worker/memory_ingest_worker.py +60 -0
- AINDY/worker/metric_writer_worker.py +58 -0
- AINDY/worker/worker_loop.py +897 -0
- AINDY/worker.py +106 -0
- aindy_runtime-1.1.0.dist-info/METADATA +539 -0
- aindy_runtime-1.1.0.dist-info/RECORD +342 -0
- aindy_runtime-1.1.0.dist-info/WHEEL +5 -0
- aindy_runtime-1.1.0.dist-info/entry_points.txt +2 -0
- aindy_runtime-1.1.0.dist-info/licenses/LICENSE +21 -0
- aindy_runtime-1.1.0.dist-info/top_level.txt +1 -0
AINDY/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.0"
|
AINDY/agents/__init__.py
ADDED
|
@@ -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
|
+
|