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,479 @@
1
+ """
2
+ WebSocket Client Endpoint for Execution Streaming.
3
+
4
+ Provides persistent WebSocket connections for frontend clients to receive
5
+ real-time execution updates, eliminating the 300-second HTTP timeout limitation.
6
+
7
+ Features:
8
+ - Long-running execution streaming (no timeout)
9
+ - Authentication via JWT tokens
10
+ - Automatic reconnection support with Last-Event-ID
11
+ - Heartbeat/keepalive mechanism (ping/pong)
12
+ - Gap detection and recovery
13
+ - Graceful degradation when services unavailable
14
+
15
+ Architecture:
16
+ Browser → WebSocket → Control Plane API → Redis → Execution Events
17
+
18
+ PostgreSQL (historical messages)
19
+
20
+ This replaces the SSE-based streaming with a more robust WebSocket solution.
21
+ """
22
+
23
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, status
24
+ from typing import Optional, Dict, Any
25
+ import structlog
26
+ import json
27
+ import asyncio
28
+ from datetime import datetime, timezone
29
+
30
+ from control_plane_api.app.middleware.auth import get_current_organization
31
+ from control_plane_api.app.lib.redis_client import get_redis_client
32
+ from control_plane_api.app.routers.executions.streaming.streamer import ExecutionStreamer
33
+ from control_plane_api.app.models.execution import Execution
34
+ from control_plane_api.app.database import get_db
35
+ from control_plane_api.app.lib.temporal_client import get_temporal_client
36
+ from sqlalchemy.orm import Session
37
+
38
+ logger = structlog.get_logger()
39
+
40
+ router = APIRouter()
41
+
42
+
43
+ class ClientConnectionManager:
44
+ """
45
+ Manages WebSocket connections for frontend clients.
46
+
47
+ Features:
48
+ - Per-organization connection limits
49
+ - Connection tracking and cleanup
50
+ - Statistics and monitoring
51
+ """
52
+
53
+ def __init__(self):
54
+ # execution_id -> WebSocket
55
+ self._connections: Dict[str, WebSocket] = {}
56
+
57
+ # organization_id -> Set[execution_id]
58
+ self._org_connections: Dict[str, set] = {}
59
+
60
+ # Connection statistics
61
+ self._stats = {
62
+ "total_connections": 0,
63
+ "active_connections": 0,
64
+ "messages_sent": 0,
65
+ "errors": 0,
66
+ }
67
+
68
+ async def connect(
69
+ self,
70
+ execution_id: str,
71
+ organization_id: str,
72
+ websocket: WebSocket,
73
+ ) -> None:
74
+ """Register a new client WebSocket connection."""
75
+ await websocket.accept()
76
+
77
+ # Track connection
78
+ self._connections[execution_id] = websocket
79
+
80
+ if organization_id not in self._org_connections:
81
+ self._org_connections[organization_id] = set()
82
+ self._org_connections[organization_id].add(execution_id)
83
+
84
+ # Update stats
85
+ self._stats["total_connections"] += 1
86
+ self._stats["active_connections"] = len(self._connections)
87
+
88
+ logger.info(
89
+ "client_websocket_connected",
90
+ execution_id=execution_id[:8],
91
+ organization_id=organization_id[:8],
92
+ active_connections=self._stats["active_connections"],
93
+ )
94
+
95
+ async def disconnect(self, execution_id: str, organization_id: str) -> None:
96
+ """Unregister a client WebSocket connection."""
97
+ if execution_id in self._connections:
98
+ del self._connections[execution_id]
99
+
100
+ if organization_id in self._org_connections:
101
+ self._org_connections[organization_id].discard(execution_id)
102
+ if not self._org_connections[organization_id]:
103
+ del self._org_connections[organization_id]
104
+
105
+ self._stats["active_connections"] = len(self._connections)
106
+
107
+ logger.info(
108
+ "client_websocket_disconnected",
109
+ execution_id=execution_id[:8],
110
+ organization_id=organization_id[:8],
111
+ active_connections=self._stats["active_connections"],
112
+ )
113
+
114
+ def get_stats(self) -> Dict[str, int]:
115
+ """Get connection statistics."""
116
+ return self._stats.copy()
117
+
118
+
119
+ # Global connection manager
120
+ client_manager = ClientConnectionManager()
121
+
122
+
123
+ async def send_json(websocket: WebSocket, event_type: str, data: Any) -> None:
124
+ """
125
+ Send JSON message via WebSocket.
126
+
127
+ Args:
128
+ websocket: WebSocket connection
129
+ event_type: Event type (e.g., 'message', 'status', 'connected')
130
+ data: Event data payload
131
+ """
132
+ try:
133
+ message = {
134
+ "type": event_type,
135
+ **data
136
+ }
137
+ await websocket.send_text(json.dumps(message))
138
+ client_manager._stats["messages_sent"] += 1
139
+ logger.info("websocket_message_sent", event_type=event_type, data_keys=list(data.keys()))
140
+ except Exception as e:
141
+ logger.error("failed_to_send_websocket_message", error=str(e), event_type=event_type)
142
+ client_manager._stats["errors"] += 1
143
+ raise
144
+
145
+
146
+ async def handle_ping_pong(websocket: WebSocket) -> None:
147
+ """
148
+ Handle ping/pong heartbeat messages.
149
+
150
+ Responds to ping messages with pong to keep connection alive.
151
+ """
152
+ try:
153
+ await send_json(websocket, "pong", {"timestamp": int(datetime.now(timezone.utc).timestamp() * 1000)})
154
+ except Exception as e:
155
+ logger.error("failed_to_send_pong", error=str(e))
156
+
157
+
158
+ async def handle_auth(websocket: WebSocket, token: str) -> Optional[str]:
159
+ """
160
+ Handle authentication message.
161
+
162
+ Supports multiple authentication methods:
163
+ 1. JWT tokens with org_id claim (Auth0 tokens)
164
+ 2. Kubiya API keys (validated via Kubiya API)
165
+
166
+ Args:
167
+ websocket: WebSocket connection
168
+ token: Authentication token (JWT or API key)
169
+
170
+ Returns:
171
+ organization_id if authentication successful, None otherwise
172
+ """
173
+ try:
174
+ # Import auth utilities
175
+ from control_plane_api.app.middleware.auth import (
176
+ decode_jwt_token,
177
+ validate_kubiya_api_key,
178
+ is_kubiya_api_key_jwt,
179
+ )
180
+
181
+ organization_id = None
182
+ user_id = None
183
+
184
+ # First, try to decode as JWT
185
+ decoded = decode_jwt_token(token)
186
+
187
+ if decoded:
188
+ # Check for org_id in various claim locations
189
+ organization_id = (
190
+ decoded.get('https://kubiya.ai/org_id') or
191
+ decoded.get('org_id') or
192
+ decoded.get('organization_id') or
193
+ decoded.get('organization') # Kubiya API keys use 'organization' field
194
+ )
195
+ user_id = decoded.get('sub') or decoded.get('user_id') or decoded.get('email')
196
+
197
+ # If JWT decode failed or org_id not found, try Kubiya API validation
198
+ if not organization_id:
199
+ logger.debug("jwt_org_id_not_found_trying_api_validation")
200
+
201
+ # Try UserKey format first (for CLI API keys)
202
+ user_data = await validate_kubiya_api_key(f"UserKey {token}", use_userkey=True)
203
+
204
+ if not user_data:
205
+ # Try Bearer format
206
+ user_data = await validate_kubiya_api_key(f"Bearer {token}", use_userkey=False)
207
+
208
+ if user_data:
209
+ organization_id = user_data.get('org')
210
+ user_id = user_data.get('uuid') or user_data.get('email')
211
+ logger.info("websocket_auth_via_kubiya_api", org=organization_id)
212
+
213
+ if not organization_id:
214
+ logger.error("org_id_missing", decoded_claims=list(decoded.keys()) if decoded else [])
215
+ await send_json(websocket, "auth_error", {
216
+ "error": "Organization ID not found in token",
217
+ "code": "ORG_ID_MISSING",
218
+ })
219
+ return None
220
+
221
+ logger.info(
222
+ "websocket_authenticated",
223
+ organization_id=organization_id[:8] if len(organization_id) > 8 else organization_id,
224
+ user_id=user_id[:8] if user_id and len(user_id) > 8 else user_id,
225
+ )
226
+
227
+ # Send auth success
228
+ await send_json(websocket, "auth_success", {
229
+ "organization_id": organization_id,
230
+ "user_id": user_id,
231
+ "authenticated_at": datetime.now(timezone.utc).isoformat(),
232
+ })
233
+
234
+ return organization_id
235
+
236
+ except Exception as e:
237
+ logger.error("authentication_failed", error=str(e), error_type=type(e).__name__)
238
+ await send_json(websocket, "auth_error", {
239
+ "error": "Authentication failed",
240
+ "code": "AUTH_FAILED",
241
+ })
242
+ return None
243
+
244
+
245
+ @router.websocket("/ws/executions/{execution_id}")
246
+ async def websocket_execution_stream(
247
+ websocket: WebSocket,
248
+ execution_id: str,
249
+ last_event_id: Optional[str] = None,
250
+ db: Session = Depends(get_db),
251
+ ):
252
+ """
253
+ WebSocket endpoint for execution streaming.
254
+
255
+ Streams execution events to frontend clients with automatic reconnection support.
256
+
257
+ Args:
258
+ websocket: WebSocket connection
259
+ execution_id: Execution ID to stream
260
+ last_event_id: Last received event ID (for resumption)
261
+ db: Database session
262
+
263
+ Flow:
264
+ 1. Accept WebSocket connection
265
+ 2. Wait for auth message with JWT token
266
+ 3. Validate token and extract organization_id
267
+ 4. Send 'connected' event
268
+ 5. Load and stream historical messages (last 200)
269
+ 6. Subscribe to Redis for live events
270
+ 7. Stream events until execution completes or client disconnects
271
+ 8. Handle ping/pong for keepalive
272
+ 9. Support resumption via last_event_id
273
+ """
274
+ organization_id: Optional[str] = None
275
+ authenticated = False
276
+
277
+ try:
278
+ # Accept connection first (before manager tracks it)
279
+ await websocket.accept()
280
+
281
+ # Track connection in manager
282
+ client_manager._connections[execution_id] = websocket
283
+ client_manager._org_connections.setdefault("pending", set()).add(execution_id)
284
+ client_manager._stats["total_connections"] += 1
285
+ client_manager._stats["active_connections"] = len(client_manager._connections)
286
+
287
+ logger.info(
288
+ "client_websocket_connection_started",
289
+ execution_id=execution_id[:8],
290
+ last_event_id=last_event_id,
291
+ )
292
+
293
+ # Wait for authentication message (timeout after 5 seconds)
294
+ try:
295
+ auth_message = await asyncio.wait_for(websocket.receive_text(), timeout=5.0)
296
+ auth_data = json.loads(auth_message)
297
+
298
+ if auth_data.get("type") == "auth" and "token" in auth_data:
299
+ organization_id = await handle_auth(websocket, auth_data["token"])
300
+ if not organization_id:
301
+ await websocket.close(code=4001, reason="Authentication failed")
302
+ return
303
+
304
+ authenticated = True
305
+
306
+ # Update connection with actual org_id
307
+ # Remove from pending
308
+ if "pending" in client_manager._org_connections:
309
+ client_manager._org_connections["pending"].discard(execution_id)
310
+ if not client_manager._org_connections["pending"]:
311
+ del client_manager._org_connections["pending"]
312
+
313
+ # Add to actual org
314
+ client_manager._org_connections.setdefault(organization_id, set()).add(execution_id)
315
+
316
+ except asyncio.TimeoutError:
317
+ logger.error("auth_timeout", execution_id=execution_id[:8])
318
+ await websocket.close(code=4002, reason="Authentication timeout")
319
+ return
320
+ except json.JSONDecodeError:
321
+ logger.error("invalid_auth_message", execution_id=execution_id[:8])
322
+ await websocket.close(code=4003, reason="Invalid authentication message")
323
+ return
324
+
325
+ # Send connected event
326
+ await send_json(websocket, "connected", {
327
+ "execution_id": execution_id,
328
+ "organization_id": organization_id,
329
+ "connected_at": datetime.now(timezone.utc).isoformat(),
330
+ })
331
+
332
+ # Initialize streaming
333
+ redis_client = get_redis_client()
334
+ temporal_client = await get_temporal_client()
335
+
336
+ # Get execution type from database
337
+ execution = db.query(Execution).filter(Execution.id == execution_id).first()
338
+ execution_type = execution.execution_type if execution else "AGENT"
339
+
340
+ streamer = ExecutionStreamer(
341
+ execution_id=execution_id,
342
+ organization_id=organization_id,
343
+ db_session=db,
344
+ redis_client=redis_client,
345
+ temporal_client=temporal_client,
346
+ last_event_id=last_event_id,
347
+ timeout_seconds=0, # No timeout for WebSocket
348
+ execution_type=execution_type,
349
+ )
350
+
351
+ logger.info("streaming_initialized", execution_id=execution_id[:8])
352
+
353
+ # Helper to parse SSE events and convert to WebSocket JSON
354
+ def parse_sse_event(sse_text: str) -> tuple[str, dict]:
355
+ """Parse SSE format to (event_type, data_dict)."""
356
+ lines = sse_text.strip().split('\n')
357
+ event_type = "message"
358
+ data_lines = []
359
+
360
+ # Check for keepalive (just comment lines, no event/data)
361
+ is_keepalive = all(line.startswith(':') or not line.strip() for line in lines)
362
+ if is_keepalive:
363
+ return "keepalive", {}
364
+
365
+ for line in lines:
366
+ if line.startswith("event: "):
367
+ event_type = line[7:].strip()
368
+ elif line.startswith("data: "):
369
+ # Collect data line (could be multiline)
370
+ data_lines.append(line[6:])
371
+ elif line and not line.startswith("id:") and not line.startswith(':') and data_lines:
372
+ # Continuation of data line
373
+ data_lines.append(line)
374
+
375
+ # Join all data lines
376
+ data_str = ''.join(data_lines).strip()
377
+
378
+ try:
379
+ data = json.loads(data_str) if data_str else {}
380
+ except json.JSONDecodeError as e:
381
+ logger.warning("json_decode_error", error=str(e), data_str=data_str[:200])
382
+ data = {"raw": data_str}
383
+
384
+ return event_type, data
385
+
386
+ # Listen for both streaming events and client messages (ping/pong)
387
+ async def listen_stream():
388
+ """Listen for streaming events from ExecutionStreamer."""
389
+ try:
390
+ async for sse_event in streamer.stream():
391
+ # Parse SSE event and convert to WebSocket JSON
392
+ event_type, data = parse_sse_event(sse_event)
393
+
394
+ # Skip keepalive messages
395
+ if event_type == "keepalive":
396
+ continue
397
+
398
+ # DEBUG: Log what we're parsing
399
+ logger.info(
400
+ "parsed_sse_event",
401
+ event_type=event_type,
402
+ data_keys=list(data.keys()) if isinstance(data, dict) else [],
403
+ sse_preview=sse_event[:200] if len(sse_event) > 200 else sse_event
404
+ )
405
+
406
+ await send_json(websocket, event_type, data)
407
+ except Exception as e:
408
+ logger.error("streaming_error", error=str(e), error_type=type(e).__name__)
409
+ raise
410
+
411
+ async def listen_client():
412
+ """Listen for client messages (ping, resume, etc.)."""
413
+ try:
414
+ while True:
415
+ message = await websocket.receive_text()
416
+ data = json.loads(message)
417
+
418
+ if data.get("type") == "ping":
419
+ await handle_ping_pong(websocket)
420
+
421
+ elif data.get("type") == "resume":
422
+ # Handle resume request
423
+ logger.info("resume_requested", execution_id=execution_id[:8], last_event_id=data.get("last_event_id"))
424
+
425
+ except WebSocketDisconnect:
426
+ pass
427
+ except Exception as e:
428
+ logger.error("client_message_error", error=str(e))
429
+
430
+ # Run both listeners concurrently
431
+ # When one finishes (e.g., client disconnect), cancel the other
432
+ stream_task = asyncio.create_task(listen_stream())
433
+ client_task = asyncio.create_task(listen_client())
434
+
435
+ # Wait for either task to complete
436
+ done, pending = await asyncio.wait(
437
+ {stream_task, client_task},
438
+ return_when=asyncio.FIRST_COMPLETED
439
+ )
440
+
441
+ # Cancel any pending tasks
442
+ for task in pending:
443
+ task.cancel()
444
+ try:
445
+ await task
446
+ except asyncio.CancelledError:
447
+ pass
448
+
449
+ except WebSocketDisconnect:
450
+ logger.info("client_disconnected", execution_id=execution_id[:8])
451
+
452
+ except Exception as e:
453
+ logger.error(
454
+ "websocket_error",
455
+ execution_id=execution_id[:8],
456
+ error=str(e),
457
+ error_type=type(e).__name__,
458
+ )
459
+ try:
460
+ await send_json(websocket, "error", {"error": str(e)})
461
+ except:
462
+ pass
463
+
464
+ finally:
465
+ # Cleanup
466
+ if organization_id:
467
+ await client_manager.disconnect(execution_id, organization_id)
468
+
469
+ logger.info("client_websocket_closed", execution_id=execution_id[:8])
470
+
471
+
472
+ @router.get("/ws/stats")
473
+ async def websocket_stats():
474
+ """
475
+ Get WebSocket connection statistics.
476
+
477
+ Returns statistics about active connections, messages sent, etc.
478
+ """
479
+ return client_manager.get_stats()