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,693 @@
1
+ """
2
+ LiveEventSource for real-time event streaming from Redis.
3
+
4
+ This module provides the LiveEventSource class that handles real-time event streaming
5
+ from Redis with proper polling, deduplication, and workflow completion detection.
6
+
7
+ Key Features:
8
+ - Redis polling with explicit 50ms sleep interval to prevent CPU spinning
9
+ - Integration with MessageDeduplicator to filter duplicate events
10
+ - Workflow completion detection via Temporal status monitoring
11
+ - Graceful degradation when Redis or Temporal unavailable
12
+ - Keepalive and timeout handling for long-running streams
13
+ - Support for both Upstash REST API and standard redis-py clients
14
+
15
+ Test Strategy:
16
+ - Test Redis polling with mock events at 50ms intervals
17
+ - Test deduplication of overlapping events (history + live)
18
+ - Test completion detection stops streaming when workflow reaches terminal state
19
+ - Test timeout behavior (0 = no timeout, streams until task completes)
20
+ - Test keepalive events sent every 15 seconds
21
+ - Test graceful degradation when Redis unavailable
22
+ - Test graceful degradation when Temporal unavailable
23
+ - Test various event types: message, status, tool_started, tool_completed, etc.
24
+ - Test Redis LLEN and LRANGE operations with both client types
25
+ - Test terminal states: COMPLETED, FAILED, TERMINATED, CANCELLED
26
+ """
27
+
28
+ import asyncio
29
+ import json
30
+ import time
31
+ from typing import Any, AsyncGenerator, Dict, List, Optional
32
+
33
+ import structlog
34
+
35
+ from .deduplication import MessageDeduplicator
36
+
37
+ logger = structlog.get_logger(__name__)
38
+
39
+
40
+ class LiveEventSource:
41
+ """
42
+ Handles real-time event streaming from Redis with polling and completion detection.
43
+
44
+ This class polls Redis for new events at 50ms intervals, deduplicates events
45
+ against historical data, and monitors workflow status to detect completion.
46
+
47
+ The streaming stops when:
48
+ 1. Workflow reaches terminal state (COMPLETED, FAILED, CANCELLED, TERMINATED)
49
+ 2. Timeout is reached (default 0 = no timeout, streams until task completes)
50
+ 3. An exception occurs that cannot be recovered
51
+
52
+ Events are yielded as dictionaries with the following structure:
53
+ {
54
+ "event_type": "message" | "status" | "tool_started" | "tool_completed" | ...,
55
+ "data": {...}, # Event-specific data
56
+ "timestamp": "2024-01-15T10:30:00Z"
57
+ }
58
+ """
59
+
60
+ # Terminal workflow states that indicate streaming should stop
61
+ TERMINAL_STATES = {"COMPLETED", "FAILED", "CANCELLED", "TERMINATED"}
62
+
63
+ # Terminal database execution states (lowercase, as stored in DB)
64
+ TERMINAL_DB_STATES = {"completed", "failed", "cancelled", "terminated", "interrupted"}
65
+
66
+ # Temporal status cache TTL (seconds) to reduce API load
67
+ TEMPORAL_STATUS_CACHE_TTL = 1.0
68
+
69
+ # Database status poll interval (seconds) - check for status changes
70
+ DB_STATUS_POLL_INTERVAL = 2.0
71
+
72
+ def __init__(
73
+ self,
74
+ execution_id: str,
75
+ organization_id: str,
76
+ redis_client,
77
+ workflow_handle,
78
+ deduplicator: MessageDeduplicator,
79
+ timeout_seconds: int = 0, # 0 = no timeout, stream until task completes
80
+ keepalive_interval: int = 15,
81
+ db_session=None, # SQLAlchemy session for status polling
82
+ ):
83
+ """
84
+ Initialize LiveEventSource.
85
+
86
+ Args:
87
+ execution_id: Execution ID to stream events for
88
+ organization_id: Organization ID for authorization
89
+ redis_client: Redis client instance (UpstashRedisClient or StandardRedisClient)
90
+ workflow_handle: Temporal workflow handle for status checks (can be None)
91
+ deduplicator: MessageDeduplicator instance for filtering duplicates
92
+ timeout_seconds: Maximum streaming duration in seconds (default: 0 = no timeout)
93
+ keepalive_interval: Seconds between keepalive messages (default: 15)
94
+ db_session: Database session for polling execution status (optional)
95
+ """
96
+ self.execution_id = execution_id
97
+ self.organization_id = organization_id
98
+ self.redis_client = redis_client
99
+ self.workflow_handle = workflow_handle
100
+ self.deduplicator = deduplicator
101
+ self.timeout_seconds = timeout_seconds
102
+ self.keepalive_interval = keepalive_interval
103
+ self.db_session = db_session
104
+
105
+ # Streaming state
106
+ self._start_time = None
107
+ self._last_keepalive = None
108
+ self._last_redis_index = -1 # Track last processed Redis event index
109
+ self._stopped = False
110
+ self._is_workflow_running = True # Assume running until proven otherwise
111
+
112
+ # Temporal status caching
113
+ self._cached_temporal_status = None
114
+ self._cached_workflow_description = None
115
+ self._last_temporal_check = 0
116
+
117
+ # Database status caching
118
+ self._cached_db_status = None
119
+ self._last_db_status_check = 0
120
+
121
+ # Redis key for events
122
+ self._redis_key = f"execution:{execution_id}:events"
123
+
124
+ logger.info(
125
+ "live_event_source_initialized",
126
+ execution_id=execution_id[:8],
127
+ timeout_seconds=timeout_seconds,
128
+ keepalive_interval=keepalive_interval,
129
+ has_workflow_handle=workflow_handle is not None,
130
+ has_redis_client=redis_client is not None,
131
+ )
132
+
133
+ async def stream(self) -> AsyncGenerator[Dict[str, Any], None]:
134
+ """
135
+ Stream live events from Redis until workflow completes or timeout.
136
+
137
+ This is the main entry point for streaming. It polls Redis every 50ms
138
+ for new events, deduplicates them, and yields them as dictionaries.
139
+
140
+ Yields:
141
+ Event dictionaries with event_type, data, and timestamp
142
+
143
+ Example:
144
+ ```python
145
+ async for event in live_source.stream():
146
+ print(f"Event: {event['event_type']}")
147
+ # Process event...
148
+ ```
149
+ """
150
+ self._start_time = time.time()
151
+ self._last_keepalive = self._start_time
152
+
153
+ logger.info(
154
+ "live_streaming_started",
155
+ execution_id=self.execution_id[:8],
156
+ timeout_seconds=self.timeout_seconds,
157
+ )
158
+
159
+ try:
160
+ while not self.should_stop:
161
+ current_time = time.time()
162
+
163
+ # Check timeout (skip if timeout_seconds is 0 = no timeout)
164
+ elapsed = current_time - self._start_time
165
+ if self.timeout_seconds > 0 and elapsed >= self.timeout_seconds:
166
+ logger.warning(
167
+ "live_streaming_timeout",
168
+ execution_id=self.execution_id[:8],
169
+ elapsed_seconds=int(elapsed),
170
+ timeout_seconds=self.timeout_seconds,
171
+ )
172
+ self._stopped = True
173
+ break
174
+
175
+ # Send keepalive if needed
176
+ if current_time - self._last_keepalive >= self.keepalive_interval:
177
+ yield self._create_keepalive_event()
178
+ self._last_keepalive = current_time
179
+
180
+ # Check workflow completion (if handle available)
181
+ if self.workflow_handle is not None:
182
+ try:
183
+ is_complete = await self._check_completion()
184
+ if is_complete:
185
+ logger.info(
186
+ "workflow_completed_stopping_stream",
187
+ execution_id=self.execution_id[:8],
188
+ status=self._cached_temporal_status,
189
+ )
190
+ self._stopped = True
191
+ break
192
+ except Exception as e:
193
+ # Don't fail streaming if status check fails
194
+ logger.warning(
195
+ "workflow_status_check_failed",
196
+ execution_id=self.execution_id[:8],
197
+ error=str(e),
198
+ )
199
+
200
+ # Check database status for changes (every 2 seconds)
201
+ db_status_event = await self._check_db_status()
202
+ if db_status_event:
203
+ # Emit status change event
204
+ yield db_status_event
205
+
206
+ # Poll Redis for new events
207
+ if self.redis_client:
208
+ try:
209
+ new_events = await self._poll_redis(self._last_redis_index)
210
+ for event in new_events:
211
+ # Deduplicate event
212
+ if self.deduplicator.is_sent(event):
213
+ logger.debug(
214
+ "duplicate_event_skipped",
215
+ execution_id=self.execution_id[:8],
216
+ event_type=event.get("event_type"),
217
+ message_id=event.get("message_id"),
218
+ )
219
+ continue
220
+
221
+ # Mark as sent and yield
222
+ self.deduplicator.mark_sent(event)
223
+ yield event
224
+ except Exception as redis_error:
225
+ # Log error but don't fail streaming
226
+ logger.error(
227
+ "redis_poll_error",
228
+ execution_id=self.execution_id[:8],
229
+ error=str(redis_error),
230
+ )
231
+ # Yield degraded state notification
232
+ yield self._create_degraded_event(str(redis_error))
233
+
234
+ # Sleep 50ms before next poll - reduced from 200ms for faster event delivery
235
+ # This 4x improvement significantly reduces perceived latency
236
+ await asyncio.sleep(0.05)
237
+
238
+ except Exception as e:
239
+ logger.error(
240
+ "live_streaming_error",
241
+ execution_id=self.execution_id[:8],
242
+ error=str(e),
243
+ )
244
+ raise
245
+ finally:
246
+ elapsed = time.time() - self._start_time
247
+ logger.info(
248
+ "live_streaming_stopped",
249
+ execution_id=self.execution_id[:8],
250
+ elapsed_seconds=int(elapsed),
251
+ events_processed=self._last_redis_index + 1,
252
+ )
253
+
254
+ async def _poll_redis(self, last_index: int) -> List[Dict[str, Any]]:
255
+ """
256
+ Poll Redis for new events since last_index.
257
+
258
+ This method:
259
+ 1. Uses LLEN to get total event count
260
+ 2. Uses LRANGE to get new events since last index
261
+ 3. Parses events from JSON
262
+ 4. Reverses them to chronological order (Redis uses LPUSH, newest first)
263
+ 5. Extracts message_id for deduplication
264
+
265
+ Args:
266
+ last_index: Last processed event index (0-based)
267
+
268
+ Returns:
269
+ List of new event dictionaries in chronological order
270
+
271
+ Note:
272
+ Redis stores events in reverse chronological order (LPUSH adds to head).
273
+ We reverse them here to get chronological order (oldest first).
274
+ """
275
+ if not self.redis_client:
276
+ return []
277
+
278
+ try:
279
+ # Get total event count
280
+ total_events = await self.redis_client.llen(self._redis_key)
281
+
282
+ if total_events is None or total_events == 0:
283
+ return []
284
+
285
+ # Check if there are new events
286
+ if total_events <= (last_index + 1):
287
+ return []
288
+
289
+ logger.debug(
290
+ "redis_new_events_found",
291
+ execution_id=self.execution_id[:8],
292
+ total=total_events,
293
+ last_index=last_index,
294
+ new_count=total_events - (last_index + 1),
295
+ )
296
+
297
+ # Get all events (they're in reverse chronological order from LPUSH)
298
+ all_redis_events = await self.redis_client.lrange(self._redis_key, 0, -1)
299
+
300
+ if not all_redis_events:
301
+ return []
302
+
303
+ # Reverse to get chronological order (oldest first)
304
+ chronological_events = list(reversed(all_redis_events))
305
+
306
+ # Extract only NEW events we haven't processed yet
307
+ new_events = []
308
+ for i in range(last_index + 1, len(chronological_events)):
309
+ event_json = chronological_events[i]
310
+
311
+ try:
312
+ event_data = json.loads(event_json)
313
+ event_type = event_data.get("event_type", "message")
314
+
315
+ # Extract payload based on event structure
316
+ # Two formats:
317
+ # 1. Message events: flat structure {role, content, timestamp, message_id}
318
+ # 2. Other events: nested {event_type, data: {...}, timestamp}
319
+ if "data" in event_data and isinstance(event_data["data"], dict):
320
+ if event_type == "message" and "role" in event_data["data"]:
321
+ # Message events expect flat structure
322
+ payload = event_data["data"].copy()
323
+ else:
324
+ # Chunk events and others expect nested structure
325
+ payload = {
326
+ "data": event_data["data"],
327
+ "timestamp": event_data.get("timestamp"),
328
+ }
329
+ else:
330
+ # Fallback for legacy format
331
+ payload = event_data.copy()
332
+
333
+ # Ensure message_id exists for deduplication
334
+ if event_type == "message" and isinstance(payload, dict):
335
+ if not payload.get("message_id"):
336
+ # Generate stable message_id
337
+ timestamp = payload.get("timestamp") or event_data.get("timestamp")
338
+ role = payload.get("role", "unknown")
339
+ if timestamp:
340
+ try:
341
+ from datetime import datetime
342
+ timestamp_micros = int(
343
+ datetime.fromisoformat(timestamp.replace("Z", "+00:00")).timestamp()
344
+ * 1000000
345
+ )
346
+ except Exception:
347
+ timestamp_micros = int(time.time() * 1000000)
348
+ else:
349
+ timestamp_micros = int(time.time() * 1000000)
350
+
351
+ generated_id = f"{self.execution_id}_{role}_{timestamp_micros}"
352
+ payload["message_id"] = generated_id
353
+
354
+ logger.debug(
355
+ "generated_message_id_for_redis_event",
356
+ execution_id=self.execution_id[:8],
357
+ role=role,
358
+ generated_id=generated_id,
359
+ )
360
+
361
+ # Store event type and payload
362
+ event = {
363
+ "event_type": event_type,
364
+ **payload, # Merge payload into event
365
+ }
366
+
367
+ new_events.append(event)
368
+
369
+ # Update last processed index
370
+ self._last_redis_index = i
371
+
372
+ logger.debug(
373
+ "redis_event_parsed",
374
+ execution_id=self.execution_id[:8],
375
+ event_type=event_type,
376
+ index=i,
377
+ )
378
+
379
+ except json.JSONDecodeError as e:
380
+ logger.warning(
381
+ "invalid_redis_event_json",
382
+ execution_id=self.execution_id[:8],
383
+ event=event_json[:100],
384
+ error=str(e),
385
+ )
386
+ # Update index even for invalid events
387
+ self._last_redis_index = i
388
+ continue
389
+ except Exception as e:
390
+ logger.error(
391
+ "redis_event_processing_error",
392
+ execution_id=self.execution_id[:8],
393
+ event=event_json[:100],
394
+ error=str(e),
395
+ )
396
+ # Update index even for failed events
397
+ self._last_redis_index = i
398
+ continue
399
+
400
+ return new_events
401
+
402
+ except Exception as e:
403
+ logger.error(
404
+ "redis_poll_failed",
405
+ execution_id=self.execution_id[:8],
406
+ error=str(e),
407
+ )
408
+ return []
409
+
410
+ async def _check_completion(self) -> bool:
411
+ """
412
+ Check if workflow is in terminal state (completed, failed, cancelled).
413
+
414
+ This method uses cached status if within TTL (1 second) to reduce
415
+ Temporal API load. Fresh status is fetched if cache expired.
416
+
417
+ Returns:
418
+ True if workflow is in terminal state, False otherwise
419
+
420
+ Note:
421
+ Temporal execution status enum values:
422
+ - RUNNING: Workflow is actively processing
423
+ - COMPLETED: Workflow completed successfully
424
+ - FAILED: Workflow failed with error
425
+ - CANCELLED: Workflow was cancelled by user
426
+ - TERMINATED: Workflow was terminated
427
+ - TIMED_OUT: Workflow exceeded timeout
428
+ - CONTINUED_AS_NEW: Workflow continued as new execution
429
+ """
430
+ if not self.workflow_handle:
431
+ # No workflow handle - can't check completion
432
+ return False
433
+
434
+ try:
435
+ current_time = time.time()
436
+
437
+ # Use cached status if within TTL
438
+ if (
439
+ self._cached_temporal_status
440
+ and (current_time - self._last_temporal_check) < self.TEMPORAL_STATUS_CACHE_TTL
441
+ ):
442
+ temporal_status = self._cached_temporal_status
443
+ logger.debug(
444
+ "using_cached_temporal_status",
445
+ execution_id=self.execution_id[:8],
446
+ status=temporal_status,
447
+ )
448
+ else:
449
+ # Cache expired or not set - fetch fresh status
450
+ t0 = time.time()
451
+ description = await self.workflow_handle.describe()
452
+ temporal_status = description.status.name # Get enum name (e.g., "RUNNING")
453
+ describe_duration = int((time.time() - t0) * 1000)
454
+
455
+ # Update cache
456
+ self._cached_temporal_status = temporal_status
457
+ self._cached_workflow_description = description
458
+ self._last_temporal_check = t0
459
+
460
+ logger.debug(
461
+ "temporal_status_fetched",
462
+ execution_id=self.execution_id[:8],
463
+ status=temporal_status,
464
+ duration_ms=describe_duration,
465
+ )
466
+
467
+ # Log slow describe calls (>100ms)
468
+ if describe_duration > 100:
469
+ logger.warning(
470
+ "slow_temporal_describe",
471
+ execution_id=self.execution_id[:8],
472
+ duration_ms=describe_duration,
473
+ )
474
+
475
+ # Update running state
476
+ previous_running = self._is_workflow_running
477
+ self._is_workflow_running = temporal_status == "RUNNING"
478
+
479
+ # Log state changes
480
+ if previous_running != self._is_workflow_running:
481
+ logger.info(
482
+ "workflow_running_state_changed",
483
+ execution_id=self.execution_id[:8],
484
+ temporal_status=temporal_status,
485
+ is_running=self._is_workflow_running,
486
+ )
487
+
488
+ # Check if terminal state reached
489
+ return temporal_status in self.TERMINAL_STATES
490
+
491
+ except Exception as e:
492
+ error_msg = str(e).lower()
493
+
494
+ # Check if workflow is not found - could be:
495
+ # 1. Not started yet (race condition) - keep waiting
496
+ # 2. Already completed and cleaned up - check DB
497
+ if "workflow not found" in error_msg or "not found" in error_msg:
498
+ # Track how many times we've seen "not found"
499
+ if not hasattr(self, '_not_found_count'):
500
+ self._not_found_count = 0
501
+ self._not_found_count += 1
502
+
503
+ # First few times: might not be started yet, wait and retry
504
+ if self._not_found_count <= 10: # ~10 seconds of retries
505
+ logger.debug(
506
+ "workflow_not_found_waiting",
507
+ execution_id=self.execution_id[:8],
508
+ attempt=self._not_found_count,
509
+ )
510
+ return False # Keep streaming, check again later
511
+
512
+ # After 10 attempts: workflow probably doesn't exist
513
+ logger.info(
514
+ "workflow_not_found_stopping_temporal_polling",
515
+ execution_id=self.execution_id[:8],
516
+ attempts=self._not_found_count,
517
+ )
518
+ # Disable further workflow checks by clearing the handle
519
+ # Stream will continue from Redis/DB only
520
+ self.workflow_handle = None
521
+ return False
522
+
523
+ logger.warning(
524
+ "temporal_status_check_error",
525
+ execution_id=self.execution_id[:8],
526
+ error=str(e),
527
+ )
528
+ # Don't treat transient errors as completion - keep streaming
529
+ return False
530
+
531
+ async def _check_db_status(self) -> Optional[Dict[str, Any]]:
532
+ """
533
+ Check database execution status and emit status event if changed.
534
+
535
+ This polls the database every 2 seconds to detect status changes
536
+ (running, waiting_for_input, completed, failed, etc.) and returns
537
+ a status event if the status has changed since last check.
538
+
539
+ Returns:
540
+ Status event dict if status changed, None otherwise
541
+ """
542
+ if not self.db_session:
543
+ return None
544
+
545
+ try:
546
+ current_time = time.time()
547
+
548
+ # Check if we should poll (respect poll interval)
549
+ if (current_time - self._last_db_status_check) < self.DB_STATUS_POLL_INTERVAL:
550
+ return None
551
+
552
+ # Query execution status from database
553
+ from control_plane_api.app.models.execution import Execution
554
+ import uuid as uuid_module
555
+
556
+ execution = self.db_session.query(Execution).filter(
557
+ Execution.id == uuid_module.UUID(self.execution_id),
558
+ Execution.organization_id == self.organization_id
559
+ ).first()
560
+
561
+ if not execution:
562
+ return None
563
+
564
+ db_status = execution.status
565
+ self._last_db_status_check = current_time
566
+
567
+ # Check if status changed
568
+ status_event = None
569
+ if db_status != self._cached_db_status:
570
+ logger.info(
571
+ "execution_status_changed",
572
+ execution_id=self.execution_id[:8],
573
+ old_status=self._cached_db_status,
574
+ new_status=db_status,
575
+ )
576
+
577
+ self._cached_db_status = db_status
578
+
579
+ # Return status event
580
+ status_event = {
581
+ "event_type": "status",
582
+ "status": db_status,
583
+ "data": {
584
+ "execution_id": self.execution_id,
585
+ "status": db_status,
586
+ "source": "database_poll",
587
+ }
588
+ }
589
+
590
+ # Check if DB status is a terminal state - signal completion
591
+ # This is a fallback when Temporal workflow handle is unavailable
592
+ if db_status in self.TERMINAL_DB_STATES:
593
+ logger.info(
594
+ "db_terminal_state_detected",
595
+ execution_id=self.execution_id[:8],
596
+ db_status=db_status,
597
+ )
598
+ self._stopped = True
599
+
600
+ return status_event
601
+
602
+ except Exception as e:
603
+ logger.warning(
604
+ "db_status_check_error",
605
+ execution_id=self.execution_id[:8],
606
+ error=str(e),
607
+ )
608
+ return None
609
+
610
+ @property
611
+ def should_stop(self) -> bool:
612
+ """
613
+ Whether streaming should stop.
614
+
615
+ Returns:
616
+ True if streaming should stop (completed or timeout), False otherwise
617
+ """
618
+ return self._stopped
619
+
620
+ def _create_keepalive_event(self) -> Dict[str, Any]:
621
+ """
622
+ Create a keepalive event.
623
+
624
+ Keepalive events are sent periodically to prevent connection timeout
625
+ and to inform the client that streaming is still active.
626
+
627
+ Returns:
628
+ Keepalive event dictionary
629
+ """
630
+ elapsed = time.time() - self._start_time
631
+ remaining = max(0, self.timeout_seconds - elapsed)
632
+
633
+ event = {
634
+ "event_type": "keepalive",
635
+ "data": {
636
+ "execution_id": self.execution_id,
637
+ "elapsed_seconds": int(elapsed),
638
+ "remaining_seconds": int(remaining),
639
+ },
640
+ "timestamp": self._current_timestamp(),
641
+ }
642
+
643
+ logger.debug(
644
+ "keepalive_sent",
645
+ execution_id=self.execution_id[:8],
646
+ elapsed_seconds=int(elapsed),
647
+ remaining_seconds=int(remaining),
648
+ )
649
+
650
+ return event
651
+
652
+ def _create_degraded_event(self, reason: str) -> Dict[str, Any]:
653
+ """
654
+ Create a degraded state event.
655
+
656
+ Degraded events inform the client that streaming quality is reduced
657
+ due to Redis unavailability or other issues.
658
+
659
+ Args:
660
+ reason: Reason for degraded state
661
+
662
+ Returns:
663
+ Degraded event dictionary
664
+ """
665
+ event = {
666
+ "event_type": "degraded",
667
+ "data": {
668
+ "reason": "redis_unavailable",
669
+ "fallback": "temporal_polling",
670
+ "message": f"Real-time events unavailable: {reason}",
671
+ "execution_id": self.execution_id,
672
+ },
673
+ "timestamp": self._current_timestamp(),
674
+ }
675
+
676
+ logger.warning(
677
+ "degraded_state_notification",
678
+ execution_id=self.execution_id[:8],
679
+ reason=reason,
680
+ )
681
+
682
+ return event
683
+
684
+ def _current_timestamp(self) -> str:
685
+ """
686
+ Get current timestamp in ISO format.
687
+
688
+ Returns:
689
+ ISO format timestamp string (e.g., "2024-01-15T10:30:00Z")
690
+ """
691
+ from datetime import datetime, timezone
692
+
693
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")