kubiya-control-plane-api 0.9.15__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 (479) hide show
  1. control_plane_api/LICENSE +676 -0
  2. control_plane_api/README.md +350 -0
  3. control_plane_api/__init__.py +4 -0
  4. control_plane_api/__version__.py +8 -0
  5. control_plane_api/alembic/README +1 -0
  6. control_plane_api/alembic/env.py +121 -0
  7. control_plane_api/alembic/script.py.mako +28 -0
  8. control_plane_api/alembic/versions/2613c65c3dbe_initial_database_setup.py +32 -0
  9. control_plane_api/alembic/versions/2df520d4927d_merge_heads.py +28 -0
  10. control_plane_api/alembic/versions/43abf98d6a01_add_paused_status_to_executions.py +73 -0
  11. control_plane_api/alembic/versions/6289854264cb_merge_multiple_heads.py +28 -0
  12. control_plane_api/alembic/versions/6a4d4dc3d8dc_generate_execution_transitions.py +50 -0
  13. control_plane_api/alembic/versions/87d11cf0a783_add_disconnected_status_to_worker_.py +44 -0
  14. control_plane_api/alembic/versions/add_ephemeral_queue_support.py +85 -0
  15. control_plane_api/alembic/versions/add_model_type_to_llm_models.py +31 -0
  16. control_plane_api/alembic/versions/add_plan_executions_table.py +114 -0
  17. control_plane_api/alembic/versions/add_trace_span_tables.py +154 -0
  18. control_plane_api/alembic/versions/add_user_info_to_traces.py +36 -0
  19. control_plane_api/alembic/versions/adjusting_foreign_keys.py +32 -0
  20. control_plane_api/alembic/versions/b4983d976db2_initial_tables.py +1128 -0
  21. control_plane_api/alembic/versions/d181a3b40e71_rename_custom_metadata_to_metadata_in_.py +50 -0
  22. control_plane_api/alembic/versions/df9117888e82_add_missing_columns.py +82 -0
  23. control_plane_api/alembic/versions/f25de6ad895a_missing_migrations.py +34 -0
  24. control_plane_api/alembic/versions/f71305fb69b9_fix_ephemeral_queue_deletion_foreign_key.py +54 -0
  25. control_plane_api/alembic/versions/mark_local_exec_queues_as_ephemeral.py +68 -0
  26. control_plane_api/alembic.ini +148 -0
  27. control_plane_api/api/index.py +12 -0
  28. control_plane_api/app/__init__.py +11 -0
  29. control_plane_api/app/activities/__init__.py +20 -0
  30. control_plane_api/app/activities/agent_activities.py +384 -0
  31. control_plane_api/app/activities/plan_generation_activities.py +499 -0
  32. control_plane_api/app/activities/team_activities.py +424 -0
  33. control_plane_api/app/activities/temporal_cloud_activities.py +588 -0
  34. control_plane_api/app/config/__init__.py +35 -0
  35. control_plane_api/app/config/api_config.py +469 -0
  36. control_plane_api/app/config/config_loader.py +224 -0
  37. control_plane_api/app/config/model_pricing.py +323 -0
  38. control_plane_api/app/config/storage_config.py +159 -0
  39. control_plane_api/app/config.py +115 -0
  40. control_plane_api/app/controllers/__init__.py +0 -0
  41. control_plane_api/app/controllers/execution_environment_controller.py +1315 -0
  42. control_plane_api/app/database.py +135 -0
  43. control_plane_api/app/exceptions.py +408 -0
  44. control_plane_api/app/lib/__init__.py +11 -0
  45. control_plane_api/app/lib/environment.py +65 -0
  46. control_plane_api/app/lib/event_bus/__init__.py +17 -0
  47. control_plane_api/app/lib/event_bus/base.py +136 -0
  48. control_plane_api/app/lib/event_bus/manager.py +335 -0
  49. control_plane_api/app/lib/event_bus/providers/__init__.py +6 -0
  50. control_plane_api/app/lib/event_bus/providers/http_provider.py +166 -0
  51. control_plane_api/app/lib/event_bus/providers/nats_provider.py +324 -0
  52. control_plane_api/app/lib/event_bus/providers/redis_provider.py +233 -0
  53. control_plane_api/app/lib/event_bus/providers/websocket_provider.py +497 -0
  54. control_plane_api/app/lib/job_executor.py +330 -0
  55. control_plane_api/app/lib/kubiya_client.py +293 -0
  56. control_plane_api/app/lib/litellm_pricing.py +166 -0
  57. control_plane_api/app/lib/mcp_validation.py +163 -0
  58. control_plane_api/app/lib/nats/__init__.py +13 -0
  59. control_plane_api/app/lib/nats/credentials_manager.py +288 -0
  60. control_plane_api/app/lib/nats/listener.py +374 -0
  61. control_plane_api/app/lib/planning_prompt_builder.py +153 -0
  62. control_plane_api/app/lib/planning_tools/__init__.py +41 -0
  63. control_plane_api/app/lib/planning_tools/agents.py +409 -0
  64. control_plane_api/app/lib/planning_tools/agno_toolkit.py +836 -0
  65. control_plane_api/app/lib/planning_tools/base.py +119 -0
  66. control_plane_api/app/lib/planning_tools/cognitive_memory_tools.py +403 -0
  67. control_plane_api/app/lib/planning_tools/context_graph_tools.py +545 -0
  68. control_plane_api/app/lib/planning_tools/environments.py +218 -0
  69. control_plane_api/app/lib/planning_tools/knowledge.py +204 -0
  70. control_plane_api/app/lib/planning_tools/models.py +93 -0
  71. control_plane_api/app/lib/planning_tools/planning_service.py +646 -0
  72. control_plane_api/app/lib/planning_tools/resources.py +242 -0
  73. control_plane_api/app/lib/planning_tools/teams.py +334 -0
  74. control_plane_api/app/lib/policy_enforcer_client.py +1016 -0
  75. control_plane_api/app/lib/redis_client.py +803 -0
  76. control_plane_api/app/lib/sqlalchemy_utils.py +486 -0
  77. control_plane_api/app/lib/state_transition_tools/__init__.py +7 -0
  78. control_plane_api/app/lib/state_transition_tools/execution_context.py +388 -0
  79. control_plane_api/app/lib/storage/__init__.py +20 -0
  80. control_plane_api/app/lib/storage/base_provider.py +274 -0
  81. control_plane_api/app/lib/storage/provider_factory.py +157 -0
  82. control_plane_api/app/lib/storage/vercel_blob_provider.py +468 -0
  83. control_plane_api/app/lib/supabase.py +71 -0
  84. control_plane_api/app/lib/supabase_utils.py +138 -0
  85. control_plane_api/app/lib/task_planning/__init__.py +138 -0
  86. control_plane_api/app/lib/task_planning/agent_factory.py +308 -0
  87. control_plane_api/app/lib/task_planning/agents.py +389 -0
  88. control_plane_api/app/lib/task_planning/cache.py +218 -0
  89. control_plane_api/app/lib/task_planning/entity_resolver.py +273 -0
  90. control_plane_api/app/lib/task_planning/helpers.py +293 -0
  91. control_plane_api/app/lib/task_planning/hooks.py +474 -0
  92. control_plane_api/app/lib/task_planning/models.py +503 -0
  93. control_plane_api/app/lib/task_planning/plan_validator.py +166 -0
  94. control_plane_api/app/lib/task_planning/planning_workflow.py +2911 -0
  95. control_plane_api/app/lib/task_planning/runner.py +656 -0
  96. control_plane_api/app/lib/task_planning/streaming_hook.py +213 -0
  97. control_plane_api/app/lib/task_planning/workflow.py +424 -0
  98. control_plane_api/app/lib/templating/__init__.py +88 -0
  99. control_plane_api/app/lib/templating/compiler.py +278 -0
  100. control_plane_api/app/lib/templating/engine.py +178 -0
  101. control_plane_api/app/lib/templating/parsers/__init__.py +29 -0
  102. control_plane_api/app/lib/templating/parsers/base.py +96 -0
  103. control_plane_api/app/lib/templating/parsers/env.py +85 -0
  104. control_plane_api/app/lib/templating/parsers/graph.py +112 -0
  105. control_plane_api/app/lib/templating/parsers/secret.py +87 -0
  106. control_plane_api/app/lib/templating/parsers/simple.py +81 -0
  107. control_plane_api/app/lib/templating/resolver.py +366 -0
  108. control_plane_api/app/lib/templating/types.py +214 -0
  109. control_plane_api/app/lib/templating/validator.py +201 -0
  110. control_plane_api/app/lib/temporal_client.py +232 -0
  111. control_plane_api/app/lib/temporal_credentials_cache.py +178 -0
  112. control_plane_api/app/lib/temporal_credentials_service.py +203 -0
  113. control_plane_api/app/lib/validation/__init__.py +24 -0
  114. control_plane_api/app/lib/validation/runtime_validation.py +388 -0
  115. control_plane_api/app/main.py +531 -0
  116. control_plane_api/app/middleware/__init__.py +10 -0
  117. control_plane_api/app/middleware/auth.py +645 -0
  118. control_plane_api/app/middleware/exception_handler.py +267 -0
  119. control_plane_api/app/middleware/prometheus_middleware.py +173 -0
  120. control_plane_api/app/middleware/rate_limiting.py +384 -0
  121. control_plane_api/app/middleware/request_id.py +202 -0
  122. control_plane_api/app/models/__init__.py +40 -0
  123. control_plane_api/app/models/agent.py +90 -0
  124. control_plane_api/app/models/analytics.py +206 -0
  125. control_plane_api/app/models/associations.py +107 -0
  126. control_plane_api/app/models/auth_user.py +73 -0
  127. control_plane_api/app/models/context.py +161 -0
  128. control_plane_api/app/models/custom_integration.py +99 -0
  129. control_plane_api/app/models/environment.py +64 -0
  130. control_plane_api/app/models/execution.py +125 -0
  131. control_plane_api/app/models/execution_transition.py +50 -0
  132. control_plane_api/app/models/job.py +159 -0
  133. control_plane_api/app/models/llm_model.py +78 -0
  134. control_plane_api/app/models/orchestration.py +66 -0
  135. control_plane_api/app/models/plan_execution.py +102 -0
  136. control_plane_api/app/models/presence.py +49 -0
  137. control_plane_api/app/models/project.py +61 -0
  138. control_plane_api/app/models/project_management.py +85 -0
  139. control_plane_api/app/models/session.py +29 -0
  140. control_plane_api/app/models/skill.py +155 -0
  141. control_plane_api/app/models/system_tables.py +43 -0
  142. control_plane_api/app/models/task_planning.py +372 -0
  143. control_plane_api/app/models/team.py +86 -0
  144. control_plane_api/app/models/trace.py +257 -0
  145. control_plane_api/app/models/user_profile.py +54 -0
  146. control_plane_api/app/models/worker.py +221 -0
  147. control_plane_api/app/models/workflow.py +161 -0
  148. control_plane_api/app/models/workspace.py +50 -0
  149. control_plane_api/app/observability/__init__.py +177 -0
  150. control_plane_api/app/observability/context_logging.py +475 -0
  151. control_plane_api/app/observability/decorators.py +337 -0
  152. control_plane_api/app/observability/local_span_processor.py +702 -0
  153. control_plane_api/app/observability/metrics.py +303 -0
  154. control_plane_api/app/observability/middleware.py +246 -0
  155. control_plane_api/app/observability/optional.py +115 -0
  156. control_plane_api/app/observability/tracing.py +382 -0
  157. control_plane_api/app/policies/README.md +149 -0
  158. control_plane_api/app/policies/approved_users.rego +62 -0
  159. control_plane_api/app/policies/business_hours.rego +51 -0
  160. control_plane_api/app/policies/rate_limiting.rego +100 -0
  161. control_plane_api/app/policies/tool_enforcement/README.md +336 -0
  162. control_plane_api/app/policies/tool_enforcement/bash_command_validation.rego +71 -0
  163. control_plane_api/app/policies/tool_enforcement/business_hours_enforcement.rego +82 -0
  164. control_plane_api/app/policies/tool_enforcement/mcp_tool_allowlist.rego +58 -0
  165. control_plane_api/app/policies/tool_enforcement/production_safeguards.rego +80 -0
  166. control_plane_api/app/policies/tool_enforcement/role_based_tool_access.rego +44 -0
  167. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  168. control_plane_api/app/routers/__init__.py +4 -0
  169. control_plane_api/app/routers/agents.py +382 -0
  170. control_plane_api/app/routers/agents_v2.py +1598 -0
  171. control_plane_api/app/routers/analytics.py +1310 -0
  172. control_plane_api/app/routers/auth.py +59 -0
  173. control_plane_api/app/routers/client_config.py +57 -0
  174. control_plane_api/app/routers/context_graph.py +561 -0
  175. control_plane_api/app/routers/context_manager.py +577 -0
  176. control_plane_api/app/routers/custom_integrations.py +490 -0
  177. control_plane_api/app/routers/enforcer.py +132 -0
  178. control_plane_api/app/routers/environment_context.py +252 -0
  179. control_plane_api/app/routers/environments.py +761 -0
  180. control_plane_api/app/routers/execution_environment.py +847 -0
  181. control_plane_api/app/routers/executions/__init__.py +28 -0
  182. control_plane_api/app/routers/executions/router.py +286 -0
  183. control_plane_api/app/routers/executions/services/__init__.py +22 -0
  184. control_plane_api/app/routers/executions/services/demo_worker_health.py +156 -0
  185. control_plane_api/app/routers/executions/services/status_service.py +420 -0
  186. control_plane_api/app/routers/executions/services/test_worker_health.py +480 -0
  187. control_plane_api/app/routers/executions/services/worker_health.py +514 -0
  188. control_plane_api/app/routers/executions/streaming/__init__.py +22 -0
  189. control_plane_api/app/routers/executions/streaming/deduplication.py +352 -0
  190. control_plane_api/app/routers/executions/streaming/event_buffer.py +353 -0
  191. control_plane_api/app/routers/executions/streaming/event_formatter.py +964 -0
  192. control_plane_api/app/routers/executions/streaming/history_loader.py +588 -0
  193. control_plane_api/app/routers/executions/streaming/live_source.py +693 -0
  194. control_plane_api/app/routers/executions/streaming/streamer.py +849 -0
  195. control_plane_api/app/routers/executions.py +4888 -0
  196. control_plane_api/app/routers/health.py +165 -0
  197. control_plane_api/app/routers/health_v2.py +394 -0
  198. control_plane_api/app/routers/integration_templates.py +496 -0
  199. control_plane_api/app/routers/integrations.py +287 -0
  200. control_plane_api/app/routers/jobs.py +1809 -0
  201. control_plane_api/app/routers/metrics.py +517 -0
  202. control_plane_api/app/routers/models.py +82 -0
  203. control_plane_api/app/routers/models_v2.py +628 -0
  204. control_plane_api/app/routers/plan_executions.py +1481 -0
  205. control_plane_api/app/routers/plan_generation_async.py +304 -0
  206. control_plane_api/app/routers/policies.py +669 -0
  207. control_plane_api/app/routers/presence.py +234 -0
  208. control_plane_api/app/routers/projects.py +987 -0
  209. control_plane_api/app/routers/runners.py +379 -0
  210. control_plane_api/app/routers/runtimes.py +172 -0
  211. control_plane_api/app/routers/secrets.py +171 -0
  212. control_plane_api/app/routers/skills.py +1010 -0
  213. control_plane_api/app/routers/skills_definitions.py +140 -0
  214. control_plane_api/app/routers/storage.py +456 -0
  215. control_plane_api/app/routers/task_planning.py +611 -0
  216. control_plane_api/app/routers/task_queues.py +650 -0
  217. control_plane_api/app/routers/team_context.py +274 -0
  218. control_plane_api/app/routers/teams.py +1747 -0
  219. control_plane_api/app/routers/templates.py +248 -0
  220. control_plane_api/app/routers/traces.py +571 -0
  221. control_plane_api/app/routers/websocket_client.py +479 -0
  222. control_plane_api/app/routers/websocket_executions_status.py +437 -0
  223. control_plane_api/app/routers/websocket_gateway.py +323 -0
  224. control_plane_api/app/routers/websocket_traces.py +576 -0
  225. control_plane_api/app/routers/worker_queues.py +2555 -0
  226. control_plane_api/app/routers/worker_websocket.py +419 -0
  227. control_plane_api/app/routers/workers.py +1004 -0
  228. control_plane_api/app/routers/workflows.py +204 -0
  229. control_plane_api/app/runtimes/__init__.py +6 -0
  230. control_plane_api/app/runtimes/validation.py +344 -0
  231. control_plane_api/app/schemas/__init__.py +1 -0
  232. control_plane_api/app/schemas/job_schemas.py +302 -0
  233. control_plane_api/app/schemas/mcp_schemas.py +311 -0
  234. control_plane_api/app/schemas/template_schemas.py +133 -0
  235. control_plane_api/app/schemas/trace_schemas.py +168 -0
  236. control_plane_api/app/schemas/worker_queue_observability_schemas.py +165 -0
  237. control_plane_api/app/services/__init__.py +1 -0
  238. control_plane_api/app/services/agno_planning_strategy.py +233 -0
  239. control_plane_api/app/services/agno_service.py +838 -0
  240. control_plane_api/app/services/claude_code_planning_service.py +203 -0
  241. control_plane_api/app/services/context_graph_client.py +224 -0
  242. control_plane_api/app/services/custom_integration_service.py +415 -0
  243. control_plane_api/app/services/integration_resolution_service.py +345 -0
  244. control_plane_api/app/services/litellm_service.py +394 -0
  245. control_plane_api/app/services/plan_generator.py +79 -0
  246. control_plane_api/app/services/planning_strategy.py +66 -0
  247. control_plane_api/app/services/planning_strategy_factory.py +118 -0
  248. control_plane_api/app/services/policy_service.py +615 -0
  249. control_plane_api/app/services/state_transition_service.py +755 -0
  250. control_plane_api/app/services/storage_service.py +593 -0
  251. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  252. control_plane_api/app/services/toolsets/context_graph_skill.py +432 -0
  253. control_plane_api/app/services/trace_retention.py +354 -0
  254. control_plane_api/app/services/worker_queue_metrics_service.py +190 -0
  255. control_plane_api/app/services/workflow_cancellation_manager.py +135 -0
  256. control_plane_api/app/services/workflow_operations_service.py +611 -0
  257. control_plane_api/app/skills/__init__.py +100 -0
  258. control_plane_api/app/skills/base.py +239 -0
  259. control_plane_api/app/skills/builtin/__init__.py +37 -0
  260. control_plane_api/app/skills/builtin/agent_communication/__init__.py +8 -0
  261. control_plane_api/app/skills/builtin/agent_communication/skill.py +246 -0
  262. control_plane_api/app/skills/builtin/code_ingestion/__init__.py +4 -0
  263. control_plane_api/app/skills/builtin/code_ingestion/skill.py +267 -0
  264. control_plane_api/app/skills/builtin/cognitive_memory/__init__.py +4 -0
  265. control_plane_api/app/skills/builtin/cognitive_memory/skill.py +174 -0
  266. control_plane_api/app/skills/builtin/contextual_awareness/__init__.py +4 -0
  267. control_plane_api/app/skills/builtin/contextual_awareness/skill.py +387 -0
  268. control_plane_api/app/skills/builtin/data_visualization/__init__.py +4 -0
  269. control_plane_api/app/skills/builtin/data_visualization/skill.py +154 -0
  270. control_plane_api/app/skills/builtin/docker/__init__.py +4 -0
  271. control_plane_api/app/skills/builtin/docker/skill.py +104 -0
  272. control_plane_api/app/skills/builtin/file_generation/__init__.py +4 -0
  273. control_plane_api/app/skills/builtin/file_generation/skill.py +94 -0
  274. control_plane_api/app/skills/builtin/file_system/__init__.py +4 -0
  275. control_plane_api/app/skills/builtin/file_system/skill.py +110 -0
  276. control_plane_api/app/skills/builtin/knowledge_api/__init__.py +5 -0
  277. control_plane_api/app/skills/builtin/knowledge_api/skill.py +124 -0
  278. control_plane_api/app/skills/builtin/python/__init__.py +4 -0
  279. control_plane_api/app/skills/builtin/python/skill.py +92 -0
  280. control_plane_api/app/skills/builtin/remote_filesystem/__init__.py +5 -0
  281. control_plane_api/app/skills/builtin/remote_filesystem/skill.py +170 -0
  282. control_plane_api/app/skills/builtin/shell/__init__.py +4 -0
  283. control_plane_api/app/skills/builtin/shell/skill.py +161 -0
  284. control_plane_api/app/skills/builtin/slack/__init__.py +3 -0
  285. control_plane_api/app/skills/builtin/slack/skill.py +302 -0
  286. control_plane_api/app/skills/builtin/workflow_executor/__init__.py +4 -0
  287. control_plane_api/app/skills/builtin/workflow_executor/skill.py +469 -0
  288. control_plane_api/app/skills/business_intelligence.py +189 -0
  289. control_plane_api/app/skills/config.py +63 -0
  290. control_plane_api/app/skills/loaders/__init__.py +14 -0
  291. control_plane_api/app/skills/loaders/base.py +73 -0
  292. control_plane_api/app/skills/loaders/filesystem_loader.py +199 -0
  293. control_plane_api/app/skills/registry.py +125 -0
  294. control_plane_api/app/utils/helpers.py +12 -0
  295. control_plane_api/app/utils/workflow_executor.py +354 -0
  296. control_plane_api/app/workflows/__init__.py +11 -0
  297. control_plane_api/app/workflows/agent_execution.py +520 -0
  298. control_plane_api/app/workflows/agent_execution_with_skills.py +223 -0
  299. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  300. control_plane_api/app/workflows/plan_generation.py +254 -0
  301. control_plane_api/app/workflows/team_execution.py +442 -0
  302. control_plane_api/scripts/seed_models.py +240 -0
  303. control_plane_api/scripts/validate_existing_tool_names.py +492 -0
  304. control_plane_api/shared/__init__.py +8 -0
  305. control_plane_api/shared/version.py +17 -0
  306. control_plane_api/test_deduplication.py +274 -0
  307. control_plane_api/test_executor_deduplication_e2e.py +309 -0
  308. control_plane_api/test_job_execution_e2e.py +283 -0
  309. control_plane_api/test_real_integration.py +193 -0
  310. control_plane_api/version.py +38 -0
  311. control_plane_api/worker/__init__.py +0 -0
  312. control_plane_api/worker/activities/__init__.py +0 -0
  313. control_plane_api/worker/activities/agent_activities.py +1585 -0
  314. control_plane_api/worker/activities/approval_activities.py +234 -0
  315. control_plane_api/worker/activities/job_activities.py +199 -0
  316. control_plane_api/worker/activities/runtime_activities.py +1167 -0
  317. control_plane_api/worker/activities/skill_activities.py +282 -0
  318. control_plane_api/worker/activities/team_activities.py +479 -0
  319. control_plane_api/worker/agent_runtime_server.py +370 -0
  320. control_plane_api/worker/binary_manager.py +333 -0
  321. control_plane_api/worker/config/__init__.py +31 -0
  322. control_plane_api/worker/config/worker_config.py +273 -0
  323. control_plane_api/worker/control_plane_client.py +1491 -0
  324. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  325. control_plane_api/worker/health_monitor.py +159 -0
  326. control_plane_api/worker/metrics.py +237 -0
  327. control_plane_api/worker/models/__init__.py +1 -0
  328. control_plane_api/worker/models/error_events.py +105 -0
  329. control_plane_api/worker/models/inputs.py +89 -0
  330. control_plane_api/worker/runtimes/__init__.py +35 -0
  331. control_plane_api/worker/runtimes/agent_runtime/runtime.py +485 -0
  332. control_plane_api/worker/runtimes/agno/__init__.py +34 -0
  333. control_plane_api/worker/runtimes/agno/config.py +248 -0
  334. control_plane_api/worker/runtimes/agno/hooks.py +385 -0
  335. control_plane_api/worker/runtimes/agno/mcp_builder.py +195 -0
  336. control_plane_api/worker/runtimes/agno/runtime.py +1063 -0
  337. control_plane_api/worker/runtimes/agno/utils.py +163 -0
  338. control_plane_api/worker/runtimes/base.py +979 -0
  339. control_plane_api/worker/runtimes/claude_code/__init__.py +38 -0
  340. control_plane_api/worker/runtimes/claude_code/cleanup.py +184 -0
  341. control_plane_api/worker/runtimes/claude_code/client_pool.py +529 -0
  342. control_plane_api/worker/runtimes/claude_code/config.py +829 -0
  343. control_plane_api/worker/runtimes/claude_code/hooks.py +482 -0
  344. control_plane_api/worker/runtimes/claude_code/litellm_proxy.py +1702 -0
  345. control_plane_api/worker/runtimes/claude_code/mcp_builder.py +467 -0
  346. control_plane_api/worker/runtimes/claude_code/mcp_discovery.py +558 -0
  347. control_plane_api/worker/runtimes/claude_code/runtime.py +1546 -0
  348. control_plane_api/worker/runtimes/claude_code/tool_mapper.py +403 -0
  349. control_plane_api/worker/runtimes/claude_code/utils.py +149 -0
  350. control_plane_api/worker/runtimes/factory.py +173 -0
  351. control_plane_api/worker/runtimes/model_utils.py +107 -0
  352. control_plane_api/worker/runtimes/validation.py +93 -0
  353. control_plane_api/worker/services/__init__.py +1 -0
  354. control_plane_api/worker/services/agent_communication_tools.py +908 -0
  355. control_plane_api/worker/services/agent_executor.py +485 -0
  356. control_plane_api/worker/services/agent_executor_v2.py +793 -0
  357. control_plane_api/worker/services/analytics_collector.py +457 -0
  358. control_plane_api/worker/services/analytics_service.py +464 -0
  359. control_plane_api/worker/services/approval_tools.py +310 -0
  360. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  361. control_plane_api/worker/services/cancellation_manager.py +177 -0
  362. control_plane_api/worker/services/code_ingestion_tools.py +465 -0
  363. control_plane_api/worker/services/contextual_awareness_tools.py +405 -0
  364. control_plane_api/worker/services/data_visualization.py +834 -0
  365. control_plane_api/worker/services/event_publisher.py +531 -0
  366. control_plane_api/worker/services/jira_tools.py +257 -0
  367. control_plane_api/worker/services/remote_filesystem_tools.py +498 -0
  368. control_plane_api/worker/services/runtime_analytics.py +328 -0
  369. control_plane_api/worker/services/session_service.py +365 -0
  370. control_plane_api/worker/services/skill_context_enhancement.py +181 -0
  371. control_plane_api/worker/services/skill_factory.py +471 -0
  372. control_plane_api/worker/services/system_prompt_enhancement.py +410 -0
  373. control_plane_api/worker/services/team_executor.py +715 -0
  374. control_plane_api/worker/services/team_executor_v2.py +1866 -0
  375. control_plane_api/worker/services/tool_enforcement.py +254 -0
  376. control_plane_api/worker/services/workflow_executor/__init__.py +52 -0
  377. control_plane_api/worker/services/workflow_executor/event_processor.py +287 -0
  378. control_plane_api/worker/services/workflow_executor/event_publisher.py +210 -0
  379. control_plane_api/worker/services/workflow_executor/executors/__init__.py +15 -0
  380. control_plane_api/worker/services/workflow_executor/executors/base.py +270 -0
  381. control_plane_api/worker/services/workflow_executor/executors/json_executor.py +50 -0
  382. control_plane_api/worker/services/workflow_executor/executors/python_executor.py +50 -0
  383. control_plane_api/worker/services/workflow_executor/models.py +142 -0
  384. control_plane_api/worker/services/workflow_executor_tools.py +1748 -0
  385. control_plane_api/worker/skills/__init__.py +12 -0
  386. control_plane_api/worker/skills/builtin/context_graph_search/README.md +213 -0
  387. control_plane_api/worker/skills/builtin/context_graph_search/__init__.py +5 -0
  388. control_plane_api/worker/skills/builtin/context_graph_search/agno_impl.py +808 -0
  389. control_plane_api/worker/skills/builtin/context_graph_search/skill.yaml +67 -0
  390. control_plane_api/worker/skills/builtin/contextual_awareness/__init__.py +4 -0
  391. control_plane_api/worker/skills/builtin/contextual_awareness/agno_impl.py +62 -0
  392. control_plane_api/worker/skills/builtin/data_visualization/agno_impl.py +18 -0
  393. control_plane_api/worker/skills/builtin/data_visualization/skill.yaml +84 -0
  394. control_plane_api/worker/skills/builtin/docker/agno_impl.py +65 -0
  395. control_plane_api/worker/skills/builtin/docker/skill.yaml +60 -0
  396. control_plane_api/worker/skills/builtin/file_generation/agno_impl.py +47 -0
  397. control_plane_api/worker/skills/builtin/file_generation/skill.yaml +64 -0
  398. control_plane_api/worker/skills/builtin/file_system/agno_impl.py +32 -0
  399. control_plane_api/worker/skills/builtin/file_system/skill.yaml +54 -0
  400. control_plane_api/worker/skills/builtin/knowledge_api/__init__.py +4 -0
  401. control_plane_api/worker/skills/builtin/knowledge_api/agno_impl.py +50 -0
  402. control_plane_api/worker/skills/builtin/knowledge_api/skill.yaml +66 -0
  403. control_plane_api/worker/skills/builtin/python/agno_impl.py +25 -0
  404. control_plane_api/worker/skills/builtin/python/skill.yaml +60 -0
  405. control_plane_api/worker/skills/builtin/schema_fix_mixin.py +260 -0
  406. control_plane_api/worker/skills/builtin/shell/agno_impl.py +31 -0
  407. control_plane_api/worker/skills/builtin/shell/skill.yaml +60 -0
  408. control_plane_api/worker/skills/builtin/slack/__init__.py +3 -0
  409. control_plane_api/worker/skills/builtin/slack/agno_impl.py +1282 -0
  410. control_plane_api/worker/skills/builtin/slack/skill.yaml +276 -0
  411. control_plane_api/worker/skills/builtin/workflow_executor/agno_impl.py +62 -0
  412. control_plane_api/worker/skills/builtin/workflow_executor/skill.yaml +79 -0
  413. control_plane_api/worker/skills/loaders/__init__.py +5 -0
  414. control_plane_api/worker/skills/loaders/base.py +23 -0
  415. control_plane_api/worker/skills/loaders/filesystem_loader.py +357 -0
  416. control_plane_api/worker/skills/registry.py +208 -0
  417. control_plane_api/worker/tests/__init__.py +1 -0
  418. control_plane_api/worker/tests/conftest.py +12 -0
  419. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  420. control_plane_api/worker/tests/e2e/test_context_graph_real_api.py +338 -0
  421. control_plane_api/worker/tests/e2e/test_context_graph_templates_e2e.py +523 -0
  422. control_plane_api/worker/tests/e2e/test_enforcement_e2e.py +344 -0
  423. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  424. control_plane_api/worker/tests/e2e/test_single_execution_mode.py +656 -0
  425. control_plane_api/worker/tests/integration/__init__.py +0 -0
  426. control_plane_api/worker/tests/integration/test_builtin_skills_fixes.py +245 -0
  427. control_plane_api/worker/tests/integration/test_context_graph_search_integration.py +365 -0
  428. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  429. control_plane_api/worker/tests/integration/test_hook_enforcement_integration.py +579 -0
  430. control_plane_api/worker/tests/integration/test_scheduled_job_workflow.py +237 -0
  431. control_plane_api/worker/tests/integration/test_system_prompt_enhancement_integration.py +343 -0
  432. control_plane_api/worker/tests/unit/__init__.py +0 -0
  433. control_plane_api/worker/tests/unit/test_builtin_skill_autoload.py +396 -0
  434. control_plane_api/worker/tests/unit/test_context_graph_search.py +450 -0
  435. control_plane_api/worker/tests/unit/test_context_graph_templates.py +403 -0
  436. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  437. control_plane_api/worker/tests/unit/test_control_plane_client_jobs.py +345 -0
  438. control_plane_api/worker/tests/unit/test_job_activities.py +353 -0
  439. control_plane_api/worker/tests/unit/test_skill_context_enhancement.py +321 -0
  440. control_plane_api/worker/tests/unit/test_system_prompt_enhancement.py +415 -0
  441. control_plane_api/worker/tests/unit/test_tool_enforcement.py +324 -0
  442. control_plane_api/worker/utils/__init__.py +1 -0
  443. control_plane_api/worker/utils/chunk_batcher.py +330 -0
  444. control_plane_api/worker/utils/environment.py +65 -0
  445. control_plane_api/worker/utils/error_publisher.py +260 -0
  446. control_plane_api/worker/utils/event_batcher.py +256 -0
  447. control_plane_api/worker/utils/logging_config.py +335 -0
  448. control_plane_api/worker/utils/logging_helper.py +326 -0
  449. control_plane_api/worker/utils/parameter_validator.py +120 -0
  450. control_plane_api/worker/utils/retry_utils.py +60 -0
  451. control_plane_api/worker/utils/streaming_utils.py +665 -0
  452. control_plane_api/worker/utils/tool_validation.py +332 -0
  453. control_plane_api/worker/utils/workspace_manager.py +163 -0
  454. control_plane_api/worker/websocket_client.py +393 -0
  455. control_plane_api/worker/worker.py +1297 -0
  456. control_plane_api/worker/workflows/__init__.py +0 -0
  457. control_plane_api/worker/workflows/agent_execution.py +909 -0
  458. control_plane_api/worker/workflows/scheduled_job_wrapper.py +332 -0
  459. control_plane_api/worker/workflows/team_execution.py +611 -0
  460. kubiya_control_plane_api-0.9.15.dist-info/METADATA +354 -0
  461. kubiya_control_plane_api-0.9.15.dist-info/RECORD +479 -0
  462. kubiya_control_plane_api-0.9.15.dist-info/WHEEL +5 -0
  463. kubiya_control_plane_api-0.9.15.dist-info/entry_points.txt +5 -0
  464. kubiya_control_plane_api-0.9.15.dist-info/licenses/LICENSE +676 -0
  465. kubiya_control_plane_api-0.9.15.dist-info/top_level.txt +3 -0
  466. scripts/__init__.py +1 -0
  467. scripts/migrations.py +39 -0
  468. scripts/seed_worker_queues.py +128 -0
  469. scripts/setup_agent_runtime.py +142 -0
  470. worker_internal/__init__.py +1 -0
  471. worker_internal/planner/__init__.py +1 -0
  472. worker_internal/planner/activities.py +1499 -0
  473. worker_internal/planner/agent_tools.py +197 -0
  474. worker_internal/planner/event_models.py +148 -0
  475. worker_internal/planner/event_publisher.py +67 -0
  476. worker_internal/planner/models.py +199 -0
  477. worker_internal/planner/retry_logic.py +134 -0
  478. worker_internal/planner/worker.py +300 -0
  479. worker_internal/planner/workflows.py +970 -0
@@ -0,0 +1,576 @@
1
+ """
2
+ WebSocket Endpoint for Trace Streaming.
3
+
4
+ Provides persistent WebSocket connections for frontend clients to receive
5
+ real-time trace and span updates for the observability UI.
6
+
7
+ Features:
8
+ - Organization-scoped live trace streaming
9
+ - Single trace detail streaming (for waterfall view)
10
+ - Authentication via JWT tokens
11
+ - Heartbeat/keepalive mechanism
12
+ - Redis pub/sub for event distribution
13
+
14
+ Architecture:
15
+ Browser → WebSocket → Control Plane API → Redis Pub/Sub → Trace Events
16
+
17
+ LocalStorageSpanProcessor
18
+
19
+ This enables real-time updates in the Observability UI without polling.
20
+ """
21
+
22
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends
23
+ from typing import Optional, Dict, Any, Set
24
+ import structlog
25
+ import json
26
+ import asyncio
27
+ from datetime import datetime, timezone
28
+
29
+ from control_plane_api.app.lib.redis_client import get_redis_client
30
+ from control_plane_api.app.database import get_db
31
+ from control_plane_api.app.models.trace import Trace
32
+ from sqlalchemy.orm import Session
33
+
34
+ logger = structlog.get_logger()
35
+
36
+ router = APIRouter()
37
+
38
+
39
+ class TraceConnectionManager:
40
+ """
41
+ Manages WebSocket connections for trace streaming.
42
+
43
+ Features:
44
+ - Per-organization trace list streaming
45
+ - Per-trace detail streaming
46
+ - Connection tracking and cleanup
47
+ """
48
+
49
+ def __init__(self):
50
+ # organization_id -> Set[WebSocket]
51
+ self._org_connections: Dict[str, Set[WebSocket]] = {}
52
+
53
+ # trace_id -> Set[WebSocket]
54
+ self._trace_connections: Dict[str, Set[WebSocket]] = {}
55
+
56
+ # WebSocket -> organization_id
57
+ self._ws_to_org: Dict[WebSocket, str] = {}
58
+
59
+ # WebSocket -> trace_id
60
+ self._ws_to_trace: Dict[WebSocket, str] = {}
61
+
62
+ # Statistics
63
+ self._stats = {
64
+ "total_connections": 0,
65
+ "active_connections": 0,
66
+ "messages_sent": 0,
67
+ "errors": 0,
68
+ }
69
+
70
+ async def connect_org(
71
+ self,
72
+ organization_id: str,
73
+ websocket: WebSocket,
74
+ ) -> None:
75
+ """Register a new organization-level WebSocket connection."""
76
+ await websocket.accept()
77
+
78
+ if organization_id not in self._org_connections:
79
+ self._org_connections[organization_id] = set()
80
+ self._org_connections[organization_id].add(websocket)
81
+ self._ws_to_org[websocket] = organization_id
82
+
83
+ self._stats["total_connections"] += 1
84
+ self._stats["active_connections"] += 1
85
+
86
+ logger.info(
87
+ "trace_websocket_org_connected",
88
+ organization_id=organization_id[:8] if len(organization_id) > 8 else organization_id,
89
+ active_connections=self._stats["active_connections"],
90
+ )
91
+
92
+ async def connect_trace(
93
+ self,
94
+ trace_id: str,
95
+ organization_id: str,
96
+ websocket: WebSocket,
97
+ ) -> None:
98
+ """Register a new trace-level WebSocket connection."""
99
+ await websocket.accept()
100
+
101
+ if trace_id not in self._trace_connections:
102
+ self._trace_connections[trace_id] = set()
103
+ self._trace_connections[trace_id].add(websocket)
104
+ self._ws_to_trace[websocket] = trace_id
105
+ self._ws_to_org[websocket] = organization_id
106
+
107
+ self._stats["total_connections"] += 1
108
+ self._stats["active_connections"] += 1
109
+
110
+ logger.info(
111
+ "trace_websocket_trace_connected",
112
+ trace_id=trace_id[:8],
113
+ organization_id=organization_id[:8] if len(organization_id) > 8 else organization_id,
114
+ active_connections=self._stats["active_connections"],
115
+ )
116
+
117
+ async def disconnect(self, websocket: WebSocket) -> None:
118
+ """Unregister a WebSocket connection."""
119
+ # Remove from org connections
120
+ org_id = self._ws_to_org.pop(websocket, None)
121
+ if org_id and org_id in self._org_connections:
122
+ self._org_connections[org_id].discard(websocket)
123
+ if not self._org_connections[org_id]:
124
+ del self._org_connections[org_id]
125
+
126
+ # Remove from trace connections
127
+ trace_id = self._ws_to_trace.pop(websocket, None)
128
+ if trace_id and trace_id in self._trace_connections:
129
+ self._trace_connections[trace_id].discard(websocket)
130
+ if not self._trace_connections[trace_id]:
131
+ del self._trace_connections[trace_id]
132
+
133
+ self._stats["active_connections"] = max(0, self._stats["active_connections"] - 1)
134
+
135
+ logger.info(
136
+ "trace_websocket_disconnected",
137
+ organization_id=org_id[:8] if org_id and len(org_id) > 8 else org_id,
138
+ trace_id=trace_id[:8] if trace_id else None,
139
+ active_connections=self._stats["active_connections"],
140
+ )
141
+
142
+ def get_org_connections(self, organization_id: str) -> Set[WebSocket]:
143
+ """Get all WebSocket connections for an organization."""
144
+ return self._org_connections.get(organization_id, set())
145
+
146
+ def get_trace_connections(self, trace_id: str) -> Set[WebSocket]:
147
+ """Get all WebSocket connections for a trace."""
148
+ return self._trace_connections.get(trace_id, set())
149
+
150
+ def get_stats(self) -> Dict[str, int]:
151
+ """Get connection statistics."""
152
+ return self._stats.copy()
153
+
154
+
155
+ # Global connection manager
156
+ trace_manager = TraceConnectionManager()
157
+
158
+
159
+ async def send_json(websocket: WebSocket, event_type: str, data: Any) -> bool:
160
+ """
161
+ Send JSON message via WebSocket.
162
+
163
+ Returns True if successful, False if failed.
164
+ """
165
+ try:
166
+ message = {
167
+ "type": event_type,
168
+ "timestamp": datetime.now(timezone.utc).isoformat(),
169
+ **(data if isinstance(data, dict) else {"data": data})
170
+ }
171
+ await websocket.send_text(json.dumps(message, default=str))
172
+ trace_manager._stats["messages_sent"] += 1
173
+ return True
174
+ except Exception as e:
175
+ logger.error("failed_to_send_websocket_message", error=str(e), event_type=event_type)
176
+ trace_manager._stats["errors"] += 1
177
+ return False
178
+
179
+
180
+ async def handle_auth(websocket: WebSocket, token: str) -> Optional[str]:
181
+ """
182
+ Handle authentication message.
183
+
184
+ Returns organization_id if authentication successful, None otherwise.
185
+ """
186
+ try:
187
+ from control_plane_api.app.middleware.auth import decode_jwt_token
188
+
189
+ decoded = decode_jwt_token(token)
190
+
191
+ if not decoded:
192
+ logger.error("jwt_decode_failed", reason="Invalid token format")
193
+ await send_json(websocket, "auth_error", {
194
+ "error": "Invalid authentication token",
195
+ "code": "INVALID_TOKEN",
196
+ })
197
+ return None
198
+
199
+ organization_id = (
200
+ decoded.get('https://kubiya.ai/org_id') or
201
+ decoded.get('org_id') or
202
+ decoded.get('organization_id')
203
+ )
204
+
205
+ if not organization_id:
206
+ logger.error("org_id_missing", decoded_claims=list(decoded.keys()))
207
+ await send_json(websocket, "auth_error", {
208
+ "error": "Organization ID not found in token",
209
+ "code": "ORG_ID_MISSING",
210
+ })
211
+ return None
212
+
213
+ user_id = decoded.get('sub')
214
+
215
+ logger.info(
216
+ "trace_websocket_authenticated",
217
+ organization_id=organization_id[:8] if len(organization_id) > 8 else organization_id,
218
+ user_id=user_id[:8] if user_id and len(user_id) > 8 else user_id,
219
+ )
220
+
221
+ await send_json(websocket, "auth_success", {
222
+ "organization_id": organization_id,
223
+ "user_id": user_id,
224
+ "authenticated_at": datetime.now(timezone.utc).isoformat(),
225
+ })
226
+
227
+ return organization_id
228
+
229
+ except Exception as e:
230
+ logger.error("authentication_failed", error=str(e), error_type=type(e).__name__)
231
+ await send_json(websocket, "auth_error", {
232
+ "error": "Authentication failed",
233
+ "code": "AUTH_FAILED",
234
+ })
235
+ return None
236
+
237
+
238
+ async def subscribe_to_redis_channel(
239
+ websocket: WebSocket,
240
+ channel: str,
241
+ organization_id: str,
242
+ trace_id: Optional[str] = None,
243
+ ):
244
+ """
245
+ Subscribe to Redis pub/sub channel and forward events to WebSocket.
246
+
247
+ This runs until the WebSocket disconnects or an error occurs.
248
+ """
249
+ redis_client = get_redis_client()
250
+ if not redis_client:
251
+ logger.warning("redis_not_available", channel=channel)
252
+ return
253
+
254
+ try:
255
+ # Get the underlying redis connection for pub/sub
256
+ # Note: This depends on the type of redis client
257
+ if hasattr(redis_client, '_redis'):
258
+ # StandardRedisClient
259
+ pubsub = redis_client._redis.pubsub()
260
+ await pubsub.subscribe(channel)
261
+
262
+ logger.info(
263
+ "redis_channel_subscribed",
264
+ channel=channel,
265
+ organization_id=organization_id[:8] if len(organization_id) > 8 else organization_id,
266
+ )
267
+
268
+ # Listen for messages
269
+ while True:
270
+ try:
271
+ message = await pubsub.get_message(
272
+ ignore_subscribe_messages=True,
273
+ timeout=1.0
274
+ )
275
+
276
+ if message and message.get("type") == "message":
277
+ data = message.get("data")
278
+ if isinstance(data, bytes):
279
+ data = data.decode("utf-8")
280
+
281
+ try:
282
+ event = json.loads(data)
283
+ event_type = event.get("type", "trace_event")
284
+
285
+ # Filter by trace_id if this is a trace-specific connection
286
+ if trace_id:
287
+ event_trace_id = event.get("data", {}).get("trace_id")
288
+ if event_trace_id != trace_id:
289
+ continue
290
+
291
+ success = await send_json(websocket, event_type, event.get("data", {}))
292
+ if not success:
293
+ break
294
+
295
+ except json.JSONDecodeError:
296
+ logger.warning("invalid_redis_message", data=str(data)[:100])
297
+
298
+ except asyncio.TimeoutError:
299
+ # Send keepalive ping
300
+ try:
301
+ await send_json(websocket, "ping", {})
302
+ except Exception:
303
+ break
304
+
305
+ else:
306
+ # UpstashRedisClient doesn't support pub/sub subscription
307
+ # Fall back to polling approach
308
+ logger.warning(
309
+ "redis_pubsub_not_supported",
310
+ client_type=type(redis_client).__name__,
311
+ message="Upstash REST API doesn't support pub/sub subscription",
312
+ )
313
+
314
+ # Simple keepalive loop until disconnect
315
+ while True:
316
+ await asyncio.sleep(30)
317
+ try:
318
+ await send_json(websocket, "ping", {})
319
+ except Exception:
320
+ break
321
+
322
+ except asyncio.CancelledError:
323
+ logger.info("redis_subscription_cancelled", channel=channel)
324
+ raise
325
+ except Exception as e:
326
+ logger.error("redis_subscription_error", error=str(e), channel=channel)
327
+ finally:
328
+ # Cleanup
329
+ if hasattr(redis_client, '_redis') and 'pubsub' in dir():
330
+ try:
331
+ await pubsub.unsubscribe(channel)
332
+ await pubsub.close()
333
+ except Exception:
334
+ pass
335
+
336
+
337
+ @router.websocket("/ws/traces/live")
338
+ async def websocket_trace_list_stream(
339
+ websocket: WebSocket,
340
+ db: Session = Depends(get_db),
341
+ ):
342
+ """
343
+ WebSocket endpoint for live trace list streaming.
344
+
345
+ Streams all new traces and trace updates for the authenticated organization.
346
+
347
+ Flow:
348
+ 1. Accept WebSocket connection
349
+ 2. Wait for auth message with JWT token
350
+ 3. Validate token and extract organization_id
351
+ 4. Send 'connected' event
352
+ 5. Subscribe to Redis channel for trace events
353
+ 6. Stream events until client disconnects
354
+ """
355
+ organization_id: Optional[str] = None
356
+
357
+ try:
358
+ await websocket.accept()
359
+
360
+ logger.info("trace_list_websocket_started")
361
+
362
+ # Wait for authentication message
363
+ try:
364
+ auth_message = await asyncio.wait_for(websocket.receive_text(), timeout=5.0)
365
+ auth_data = json.loads(auth_message)
366
+
367
+ if auth_data.get("type") == "auth" and "token" in auth_data:
368
+ organization_id = await handle_auth(websocket, auth_data["token"])
369
+ if not organization_id:
370
+ await websocket.close(code=4001, reason="Authentication failed")
371
+ return
372
+ else:
373
+ await websocket.close(code=4003, reason="Invalid authentication message")
374
+ return
375
+
376
+ except asyncio.TimeoutError:
377
+ logger.error("trace_auth_timeout")
378
+ await websocket.close(code=4002, reason="Authentication timeout")
379
+ return
380
+ except json.JSONDecodeError:
381
+ logger.error("trace_invalid_auth_message")
382
+ await websocket.close(code=4003, reason="Invalid authentication message")
383
+ return
384
+
385
+ # Track connection
386
+ if organization_id not in trace_manager._org_connections:
387
+ trace_manager._org_connections[organization_id] = set()
388
+ trace_manager._org_connections[organization_id].add(websocket)
389
+ trace_manager._ws_to_org[websocket] = organization_id
390
+ trace_manager._stats["active_connections"] += 1
391
+
392
+ # Send connected event
393
+ await send_json(websocket, "connected", {
394
+ "organization_id": organization_id,
395
+ "connected_at": datetime.now(timezone.utc).isoformat(),
396
+ "subscription": "trace_list",
397
+ })
398
+
399
+ # Create tasks for Redis subscription and client message handling
400
+ redis_task = asyncio.create_task(
401
+ subscribe_to_redis_channel(
402
+ websocket,
403
+ f"traces:{organization_id}",
404
+ organization_id,
405
+ )
406
+ )
407
+
408
+ # Handle incoming messages (ping/pong, etc.)
409
+ try:
410
+ while True:
411
+ try:
412
+ message = await asyncio.wait_for(websocket.receive_text(), timeout=60.0)
413
+ data = json.loads(message)
414
+
415
+ if data.get("type") == "ping":
416
+ await send_json(websocket, "pong", {
417
+ "timestamp": int(datetime.now(timezone.utc).timestamp() * 1000)
418
+ })
419
+
420
+ except asyncio.TimeoutError:
421
+ # Send ping to keep connection alive
422
+ await send_json(websocket, "ping", {})
423
+
424
+ except WebSocketDisconnect:
425
+ logger.info("trace_list_websocket_disconnected")
426
+
427
+ finally:
428
+ redis_task.cancel()
429
+ try:
430
+ await redis_task
431
+ except asyncio.CancelledError:
432
+ pass
433
+
434
+ except WebSocketDisconnect:
435
+ logger.info("trace_list_websocket_disconnected_early")
436
+ except Exception as e:
437
+ logger.error("trace_list_websocket_error", error=str(e), exc_info=True)
438
+ finally:
439
+ if organization_id:
440
+ await trace_manager.disconnect(websocket)
441
+
442
+
443
+ @router.websocket("/ws/traces/{trace_id}")
444
+ async def websocket_trace_detail_stream(
445
+ websocket: WebSocket,
446
+ trace_id: str,
447
+ db: Session = Depends(get_db),
448
+ ):
449
+ """
450
+ WebSocket endpoint for single trace detail streaming.
451
+
452
+ Streams span updates for a specific trace (for waterfall view).
453
+
454
+ Flow:
455
+ 1. Accept WebSocket connection
456
+ 2. Wait for auth message with JWT token
457
+ 3. Validate token and verify trace belongs to organization
458
+ 4. Send 'connected' event with initial trace data
459
+ 5. Subscribe to Redis channel filtered by trace_id
460
+ 6. Stream span events until client disconnects or trace completes
461
+ """
462
+ organization_id: Optional[str] = None
463
+
464
+ try:
465
+ await websocket.accept()
466
+
467
+ logger.info("trace_detail_websocket_started", trace_id=trace_id[:8])
468
+
469
+ # Wait for authentication message
470
+ try:
471
+ auth_message = await asyncio.wait_for(websocket.receive_text(), timeout=5.0)
472
+ auth_data = json.loads(auth_message)
473
+
474
+ if auth_data.get("type") == "auth" and "token" in auth_data:
475
+ organization_id = await handle_auth(websocket, auth_data["token"])
476
+ if not organization_id:
477
+ await websocket.close(code=4001, reason="Authentication failed")
478
+ return
479
+ else:
480
+ await websocket.close(code=4003, reason="Invalid authentication message")
481
+ return
482
+
483
+ except asyncio.TimeoutError:
484
+ logger.error("trace_detail_auth_timeout", trace_id=trace_id[:8])
485
+ await websocket.close(code=4002, reason="Authentication timeout")
486
+ return
487
+ except json.JSONDecodeError:
488
+ logger.error("trace_detail_invalid_auth_message", trace_id=trace_id[:8])
489
+ await websocket.close(code=4003, reason="Invalid authentication message")
490
+ return
491
+
492
+ # Verify trace exists and belongs to organization
493
+ trace = db.query(Trace).filter(
494
+ Trace.trace_id == trace_id,
495
+ Trace.organization_id == organization_id,
496
+ ).first()
497
+
498
+ if not trace:
499
+ await send_json(websocket, "error", {
500
+ "error": f"Trace {trace_id} not found",
501
+ "code": "TRACE_NOT_FOUND",
502
+ })
503
+ await websocket.close(code=4004, reason="Trace not found")
504
+ return
505
+
506
+ # Track connection
507
+ if trace_id not in trace_manager._trace_connections:
508
+ trace_manager._trace_connections[trace_id] = set()
509
+ trace_manager._trace_connections[trace_id].add(websocket)
510
+ trace_manager._ws_to_trace[websocket] = trace_id
511
+ trace_manager._ws_to_org[websocket] = organization_id
512
+ trace_manager._stats["active_connections"] += 1
513
+
514
+ # Send connected event with trace info
515
+ await send_json(websocket, "connected", {
516
+ "trace_id": trace_id,
517
+ "organization_id": organization_id,
518
+ "trace_name": trace.name,
519
+ "trace_status": trace.status.value if trace.status else "running",
520
+ "connected_at": datetime.now(timezone.utc).isoformat(),
521
+ "subscription": "trace_detail",
522
+ })
523
+
524
+ # Create tasks for Redis subscription and client message handling
525
+ redis_task = asyncio.create_task(
526
+ subscribe_to_redis_channel(
527
+ websocket,
528
+ f"traces:{organization_id}",
529
+ organization_id,
530
+ trace_id=trace_id, # Filter by this trace
531
+ )
532
+ )
533
+
534
+ # Handle incoming messages (ping/pong, etc.)
535
+ try:
536
+ while True:
537
+ try:
538
+ message = await asyncio.wait_for(websocket.receive_text(), timeout=60.0)
539
+ data = json.loads(message)
540
+
541
+ if data.get("type") == "ping":
542
+ await send_json(websocket, "pong", {
543
+ "timestamp": int(datetime.now(timezone.utc).timestamp() * 1000)
544
+ })
545
+
546
+ except asyncio.TimeoutError:
547
+ # Check if trace is still running
548
+ db.refresh(trace)
549
+ if trace.status and trace.status.value != "running":
550
+ await send_json(websocket, "trace_completed", {
551
+ "trace_id": trace_id,
552
+ "status": trace.status.value,
553
+ "duration_ms": trace.duration_ms,
554
+ })
555
+ break
556
+
557
+ # Send ping to keep connection alive
558
+ await send_json(websocket, "ping", {})
559
+
560
+ except WebSocketDisconnect:
561
+ logger.info("trace_detail_websocket_disconnected", trace_id=trace_id[:8])
562
+
563
+ finally:
564
+ redis_task.cancel()
565
+ try:
566
+ await redis_task
567
+ except asyncio.CancelledError:
568
+ pass
569
+
570
+ except WebSocketDisconnect:
571
+ logger.info("trace_detail_websocket_disconnected_early", trace_id=trace_id[:8])
572
+ except Exception as e:
573
+ logger.error("trace_detail_websocket_error", error=str(e), trace_id=trace_id[:8], exc_info=True)
574
+ finally:
575
+ if organization_id:
576
+ await trace_manager.disconnect(websocket)