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,656 @@
1
+ """
2
+ End-to-End Tests for Single Execution Mode Fixes
3
+
4
+ This test suite validates the three critical fixes for single execution mode:
5
+ 1. WebSocket switching is disabled in single execution mode
6
+ 2. Execution monitor requires consecutive completion checks
7
+ 3. Extended timeout for long-running executions
8
+
9
+ Tests simulate real execution scenarios that previously caused premature exits.
10
+ """
11
+
12
+ import pytest
13
+ import asyncio
14
+ import os
15
+ import time
16
+ from unittest.mock import Mock, patch, AsyncMock, MagicMock, call
17
+ from pathlib import Path
18
+ import sys
19
+ import httpx
20
+
21
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
22
+
23
+ from control_plane_api.worker.services.event_publisher import (
24
+ EventPublisher,
25
+ EventPublisherConfig,
26
+ TransportMode,
27
+ )
28
+ from control_plane_api.worker.control_plane_client import ControlPlaneClient
29
+
30
+
31
+ class MockHTTPXClient:
32
+ """Mock HTTPX client for testing"""
33
+
34
+ def __init__(self, execution_statuses=None):
35
+ """
36
+ Args:
37
+ execution_statuses: List of status dicts to return on sequential calls
38
+ e.g., [{"status": "running"}, {"status": "completed"}]
39
+ """
40
+ self.execution_statuses = execution_statuses or []
41
+ self.call_count = 0
42
+ self.requests = []
43
+
44
+ async def get(self, url, headers=None, params=None):
45
+ """Mock GET request"""
46
+ self.requests.append({"method": "GET", "url": url, "headers": headers, "params": params})
47
+
48
+ # Return execution status based on call count
49
+ if "/executions" in url and self.call_count < len(self.execution_statuses):
50
+ status_data = self.execution_statuses[self.call_count]
51
+ self.call_count += 1
52
+
53
+ response = Mock()
54
+ response.status_code = 200
55
+ response.json.return_value = [status_data]
56
+ return response
57
+
58
+ # Default response
59
+ response = Mock()
60
+ response.status_code = 200
61
+ response.json.return_value = []
62
+ return response
63
+
64
+ async def post(self, url, json=None, headers=None):
65
+ """Mock POST request"""
66
+ self.requests.append({"method": "POST", "url": url, "json": json, "headers": headers})
67
+
68
+ response = Mock()
69
+ response.status_code = 202
70
+ response.text = "OK"
71
+ return response
72
+
73
+ async def __aenter__(self):
74
+ return self
75
+
76
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
77
+ pass
78
+
79
+
80
+ @pytest.mark.asyncio
81
+ class TestWebSocketDisabledInSingleExecution:
82
+ """Test that WebSocket switching is disabled in single execution mode"""
83
+
84
+ async def test_websocket_disabled_when_env_var_set(self):
85
+ """Verify WebSocket is disabled when KUBIYA_SINGLE_EXECUTION_MODE=true"""
86
+ # Set environment variable
87
+ os.environ["KUBIYA_SINGLE_EXECUTION_MODE"] = "true"
88
+
89
+ try:
90
+ # Create config from env
91
+ config = EventPublisherConfig.from_env()
92
+
93
+ # Assert WebSocket is disabled
94
+ assert config.websocket_enabled is False, "WebSocket should be disabled in single execution mode"
95
+
96
+ # Create event publisher
97
+ mock_control_plane = Mock()
98
+ mock_control_plane.publish_event_async = AsyncMock(return_value=True)
99
+
100
+ publisher = EventPublisher(
101
+ control_plane=mock_control_plane,
102
+ execution_id="test-exec-123",
103
+ config=config,
104
+ )
105
+
106
+ # Verify no transport switch task was created
107
+ assert publisher._transport_switch_task is None, "Transport switch task should not be created"
108
+
109
+ # Verify current transport stays HTTP
110
+ assert publisher._current_transport == TransportMode.HTTP
111
+
112
+ finally:
113
+ # Clean up
114
+ del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
115
+
116
+ async def test_websocket_enabled_when_env_var_not_set(self):
117
+ """Verify WebSocket is enabled by default when not in single execution mode"""
118
+ # Ensure env var is not set
119
+ if "KUBIYA_SINGLE_EXECUTION_MODE" in os.environ:
120
+ del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
121
+
122
+ # Create config from env
123
+ config = EventPublisherConfig.from_env()
124
+
125
+ # Assert WebSocket is enabled by default
126
+ assert config.websocket_enabled is True, "WebSocket should be enabled by default"
127
+
128
+ async def test_websocket_can_be_explicitly_enabled_in_single_mode(self):
129
+ """Verify WebSocket can be explicitly enabled even in single execution mode"""
130
+ # Set both env vars
131
+ os.environ["KUBIYA_SINGLE_EXECUTION_MODE"] = "true"
132
+ os.environ["EVENT_WEBSOCKET_ENABLED"] = "true"
133
+
134
+ try:
135
+ config = EventPublisherConfig.from_env()
136
+
137
+ # Explicit override should take precedence
138
+ assert config.websocket_enabled is True, "Explicit EVENT_WEBSOCKET_ENABLED should override default"
139
+
140
+ finally:
141
+ # Clean up
142
+ del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
143
+ del os.environ["EVENT_WEBSOCKET_ENABLED"]
144
+
145
+ async def test_no_websocket_switch_during_execution(self):
146
+ """Verify that execution doesn't switch to WebSocket after threshold in single mode"""
147
+ os.environ["KUBIYA_SINGLE_EXECUTION_MODE"] = "true"
148
+
149
+ try:
150
+ config = EventPublisherConfig.from_env()
151
+ config.websocket_switch_threshold_seconds = 1 # 1 second for fast test
152
+
153
+ mock_control_plane = Mock()
154
+ mock_control_plane.publish_event_async = AsyncMock(return_value=True)
155
+
156
+ publisher = EventPublisher(
157
+ control_plane=mock_control_plane,
158
+ execution_id="test-exec-123",
159
+ config=config,
160
+ )
161
+
162
+ # Publish some events
163
+ await publisher.publish("message_chunk", {"content": "test1"})
164
+
165
+ # Wait past the threshold
166
+ await asyncio.sleep(1.5)
167
+
168
+ # Publish more events
169
+ await publisher.publish("message_chunk", {"content": "test2"})
170
+
171
+ # Verify transport is still HTTP
172
+ assert publisher._current_transport == TransportMode.HTTP, "Transport should remain HTTP"
173
+ assert publisher._ws_connection is None, "WebSocket connection should not be created"
174
+
175
+ # Clean up
176
+ await publisher.close()
177
+
178
+ finally:
179
+ del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
180
+
181
+
182
+ @pytest.mark.asyncio
183
+ class TestRobustExecutionMonitor:
184
+ """Test that execution monitor requires consecutive checks before shutdown"""
185
+
186
+ async def test_consecutive_completion_checks_required(self):
187
+ """Verify monitor requires 2 consecutive 'completed' checks before shutdown"""
188
+
189
+ # Simulate execution status progression:
190
+ # Call 1: running
191
+ # Call 2: completed (first check)
192
+ # Call 3: completed (second check - should trigger shutdown)
193
+ execution_statuses = [
194
+ {"id": "exec-123", "status": "running"},
195
+ {"id": "exec-123", "status": "completed"},
196
+ {"id": "exec-123", "status": "completed"},
197
+ ]
198
+
199
+ mock_client = MockHTTPXClient(execution_statuses)
200
+
201
+ # Simulate the monitor logic
202
+ consecutive_completion_checks = 0
203
+ required_consecutive_checks = 2
204
+ execution_seen = False
205
+ execution_id = None
206
+ should_shutdown = False
207
+
208
+ # Simulate 3 polling cycles
209
+ for i in range(3):
210
+ async with mock_client as http_client:
211
+ response = await http_client.get(
212
+ "http://test/api/v1/worker-queues/queue-123/executions",
213
+ headers={"Authorization": "Bearer test-key"},
214
+ params={"limit": 5, "status": "all"}
215
+ )
216
+
217
+ executions = response.json()
218
+
219
+ for execution in executions:
220
+ exec_status = execution.get("status", "").lower()
221
+ exec_id = execution.get("id")
222
+
223
+ if not execution_seen:
224
+ if exec_status in ["running", "completed", "failed"]:
225
+ execution_seen = True
226
+ execution_id = exec_id
227
+
228
+ if execution_seen and exec_id == execution_id:
229
+ if exec_status in ["completed", "failed", "cancelled"]:
230
+ consecutive_completion_checks += 1
231
+
232
+ if consecutive_completion_checks >= required_consecutive_checks:
233
+ should_shutdown = True
234
+ break
235
+ else:
236
+ # Execution back to running - reset counter
237
+ consecutive_completion_checks = 0
238
+
239
+ # Assertions
240
+ assert consecutive_completion_checks == 2, "Should have 2 consecutive completion checks"
241
+ assert should_shutdown is True, "Should trigger shutdown after 2 consecutive checks"
242
+ assert mock_client.call_count == 3, "Should have made 3 API calls"
243
+
244
+ async def test_completion_counter_resets_on_running_status(self):
245
+ """Verify completion counter resets if execution goes back to running"""
246
+
247
+ # Simulate: completed → running → completed → completed
248
+ execution_statuses = [
249
+ {"id": "exec-123", "status": "completed"}, # First check
250
+ {"id": "exec-123", "status": "running"}, # Back to running - should reset
251
+ {"id": "exec-123", "status": "completed"}, # First check again
252
+ {"id": "exec-123", "status": "completed"}, # Second check - triggers shutdown
253
+ ]
254
+
255
+ mock_client = MockHTTPXClient(execution_statuses)
256
+
257
+ consecutive_completion_checks = 0
258
+ required_consecutive_checks = 2
259
+ execution_seen = False
260
+ execution_id = None
261
+ should_shutdown = False
262
+
263
+ for i in range(4):
264
+ async with mock_client as http_client:
265
+ response = await http_client.get(
266
+ "http://test/api/v1/worker-queues/queue-123/executions",
267
+ headers={"Authorization": "Bearer test-key"},
268
+ params={"limit": 5, "status": "all"}
269
+ )
270
+
271
+ executions = response.json()
272
+
273
+ for execution in executions:
274
+ exec_status = execution.get("status", "").lower()
275
+ exec_id = execution.get("id")
276
+
277
+ if not execution_seen:
278
+ execution_seen = True
279
+ execution_id = exec_id
280
+
281
+ if execution_seen and exec_id == execution_id:
282
+ if exec_status in ["completed", "failed", "cancelled"]:
283
+ consecutive_completion_checks += 1
284
+
285
+ if consecutive_completion_checks >= required_consecutive_checks:
286
+ should_shutdown = True
287
+ break
288
+ else:
289
+ # Reset counter
290
+ if consecutive_completion_checks > 0:
291
+ consecutive_completion_checks = 0
292
+
293
+ assert should_shutdown is True, "Should eventually trigger shutdown"
294
+ assert mock_client.call_count == 4, "Should have made 4 API calls due to reset"
295
+
296
+ async def test_completion_counter_resets_on_api_failure(self):
297
+ """Verify completion counter resets on API failures for safety"""
298
+
299
+ # This test simulates the behavior where API failures should reset the counter
300
+ consecutive_completion_checks = 1 # Simulate we had one check
301
+
302
+ # Simulate API failure
303
+ try:
304
+ raise Exception("API connection failed")
305
+ except Exception:
306
+ # Counter should be reset on error
307
+ if consecutive_completion_checks > 0:
308
+ consecutive_completion_checks = 0
309
+
310
+ assert consecutive_completion_checks == 0, "Counter should reset on API failure"
311
+
312
+ async def test_prevents_premature_shutdown_at_240_seconds(self):
313
+ """
314
+ Test that execution doesn't shutdown at 240 seconds (WebSocket switch time)
315
+ This is the actual bug we're fixing.
316
+ """
317
+
318
+ # Simulate the scenario:
319
+ # - Execution running at 237 seconds
320
+ # - First check at 240 seconds returns "completed" (false positive)
321
+ # - Second check at 243 seconds returns "running" (execution still active)
322
+ # - Should NOT shutdown
323
+
324
+ execution_statuses = [
325
+ {"id": "exec-123", "status": "running"}, # Before 240s
326
+ {"id": "exec-123", "status": "completed"}, # At 240s (false positive from race condition)
327
+ {"id": "exec-123", "status": "running"}, # At 243s (still active!)
328
+ ]
329
+
330
+ mock_client = MockHTTPXClient(execution_statuses)
331
+
332
+ consecutive_completion_checks = 0
333
+ required_consecutive_checks = 2
334
+ execution_seen = False
335
+ execution_id = None
336
+ should_shutdown = False
337
+
338
+ for i in range(3):
339
+ async with mock_client as http_client:
340
+ response = await http_client.get(
341
+ "http://test/api/v1/worker-queues/queue-123/executions",
342
+ headers={"Authorization": "Bearer test-key"},
343
+ params={"limit": 5, "status": "all"}
344
+ )
345
+
346
+ executions = response.json()
347
+
348
+ for execution in executions:
349
+ exec_status = execution.get("status", "").lower()
350
+ exec_id = execution.get("id")
351
+
352
+ if not execution_seen:
353
+ execution_seen = True
354
+ execution_id = exec_id
355
+
356
+ if execution_seen and exec_id == execution_id:
357
+ if exec_status in ["completed", "failed", "cancelled"]:
358
+ consecutive_completion_checks += 1
359
+
360
+ if consecutive_completion_checks >= required_consecutive_checks:
361
+ should_shutdown = True
362
+ break
363
+ else:
364
+ # Reset on running status
365
+ if consecutive_completion_checks > 0:
366
+ consecutive_completion_checks = 0
367
+
368
+ # Critical assertions
369
+ assert should_shutdown is False, "Should NOT shutdown due to false positive"
370
+ assert consecutive_completion_checks == 0, "Counter should be reset after false positive"
371
+
372
+
373
+ @pytest.mark.asyncio
374
+ class TestLongRunningExecution:
375
+ """Test that long-running executions (>4 minutes) complete successfully"""
376
+
377
+ async def test_execution_continues_beyond_240_seconds(self):
378
+ """Verify execution continues past 240 seconds without premature exit"""
379
+
380
+ os.environ["KUBIYA_SINGLE_EXECUTION_MODE"] = "true"
381
+
382
+ try:
383
+ config = EventPublisherConfig.from_env()
384
+
385
+ # Verify WebSocket is disabled
386
+ assert config.websocket_enabled is False
387
+
388
+ mock_control_plane = Mock()
389
+ mock_control_plane.publish_event_async = AsyncMock(return_value=True)
390
+
391
+ publisher = EventPublisher(
392
+ control_plane=mock_control_plane,
393
+ execution_id="test-long-exec",
394
+ config=config,
395
+ )
396
+
397
+ # Publish events over time
398
+ start_time = time.time()
399
+
400
+ # Simulate publishing events for 5+ minutes (compressed to seconds for test)
401
+ for i in range(6): # Simulate 6 minutes
402
+ await publisher.publish("message_chunk", {"content": f"chunk-{i}", "minute": i})
403
+
404
+ # Small delay to simulate time passing
405
+ await asyncio.sleep(0.1)
406
+
407
+ # Verify transport never switched to WebSocket
408
+ assert publisher._current_transport == TransportMode.HTTP
409
+ assert publisher._ws_connection is None
410
+
411
+ # Flush and close to ensure all batched events are sent
412
+ await publisher.flush()
413
+ await publisher.close()
414
+
415
+ # Verify all events were published via HTTP (may be batched, so at least 1 call)
416
+ assert mock_control_plane.publish_event_async.call_count >= 1, "Should publish events via HTTP"
417
+
418
+ finally:
419
+ if "KUBIYA_SINGLE_EXECUTION_MODE" in os.environ:
420
+ del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
421
+
422
+ async def test_extended_timeout_allows_30_minute_execution(self):
423
+ """Verify the extended timeout of 30 minutes (1800 seconds) is configured"""
424
+
425
+ # The actual monitor uses max_runtime = 1800 seconds
426
+ max_runtime = 1800 # 30 minutes
427
+
428
+ # Verify this is sufficient for long executions
429
+ assert max_runtime == 1800, "Timeout should be 30 minutes (1800 seconds)"
430
+ assert max_runtime > 240, "Timeout should be much longer than WebSocket switch threshold"
431
+
432
+ # Simulate time checks
433
+ simulated_execution_time = 600 # 10 minutes
434
+ assert simulated_execution_time < max_runtime, "10-minute execution should complete within timeout"
435
+
436
+ simulated_execution_time = 1200 # 20 minutes
437
+ assert simulated_execution_time < max_runtime, "20-minute execution should complete within timeout"
438
+
439
+
440
+ @pytest.mark.asyncio
441
+ class TestEventPublishingInAgnoRuntime:
442
+ """Test that event publishing works correctly in Agno runtime"""
443
+
444
+ async def test_event_publisher_awaits_async_functions(self):
445
+ """Verify EventPublisher properly awaits async publish functions"""
446
+
447
+ mock_control_plane = Mock()
448
+ call_tracker = []
449
+
450
+ async def mock_publish_async(execution_id, event_type, data):
451
+ """Mock async publish that tracks calls"""
452
+ call_tracker.append({"execution_id": execution_id, "event_type": event_type, "data": data})
453
+ await asyncio.sleep(0.01) # Simulate async work
454
+ return True
455
+
456
+ mock_control_plane.publish_event_async = mock_publish_async
457
+
458
+ config = EventPublisherConfig(websocket_enabled=False)
459
+ publisher = EventPublisher(
460
+ control_plane=mock_control_plane,
461
+ execution_id="test-exec-456",
462
+ config=config,
463
+ )
464
+
465
+ # Publish multiple events
466
+ # Note: message_chunk is batched (EventPriority.NORMAL)
467
+ # tool_started is immediate (EventPriority.IMMEDIATE by default)
468
+ result1 = await publisher.publish("message_chunk", {"content": "chunk1"})
469
+ result2 = await publisher.publish("tool_started", {"tool": "test"})
470
+
471
+ # Verify both published successfully
472
+ assert result1 is True
473
+ assert result2 is True
474
+
475
+ # Flush to ensure batched events are sent
476
+ await publisher.flush()
477
+ await publisher.close()
478
+
479
+ # Verify events were awaited (not just coroutines created)
480
+ # Note: message_chunk is batched, tool_started is immediate
481
+ assert len(call_tracker) >= 1, "At least one event should be published"
482
+
483
+ # Check that tool_started was published immediately
484
+ tool_events = [e for e in call_tracker if e["event_type"] == "tool_started"]
485
+ assert len(tool_events) == 1, "tool_started should be published immediately"
486
+
487
+ async def test_no_unawaited_coroutine_warnings(self):
488
+ """Verify no 'await wasn't used with future' errors occur"""
489
+
490
+ mock_control_plane = Mock()
491
+
492
+ async def mock_publish_that_raises(execution_id, event_type, data):
493
+ """Simulate an error that might cause unawaited coroutines"""
494
+ if event_type == "error_event":
495
+ raise RuntimeError("await wasn't used with future")
496
+ return True
497
+
498
+ mock_control_plane.publish_event_async = mock_publish_that_raises
499
+
500
+ config = EventPublisherConfig(websocket_enabled=False)
501
+ publisher = EventPublisher(
502
+ control_plane=mock_control_plane,
503
+ execution_id="test-exec-789",
504
+ config=config,
505
+ )
506
+
507
+ # Publish normal event - should work
508
+ result = await publisher.publish("message_chunk", {"content": "test"})
509
+ assert result is True
510
+
511
+ # Publish error event - should handle exception gracefully
512
+ result = await publisher.publish("error_event", {"error": "test"})
513
+ assert result is False # Returns False on error, doesn't raise
514
+
515
+ await publisher.close()
516
+
517
+
518
+ @pytest.mark.asyncio
519
+ class TestIntegrationScenarios:
520
+ """Integration tests simulating real-world scenarios"""
521
+
522
+ async def test_complete_single_execution_flow(self):
523
+ """
524
+ Test complete flow: worker starts → execution runs → completes → worker exits
525
+ This simulates the actual 'kubiya exec' flow.
526
+ """
527
+
528
+ # Setup: Single execution mode
529
+ os.environ["KUBIYA_SINGLE_EXECUTION_MODE"] = "true"
530
+
531
+ try:
532
+ # Phase 1: Worker initialization
533
+ config = EventPublisherConfig.from_env()
534
+ assert config.websocket_enabled is False, "WebSocket should be disabled"
535
+
536
+ # Phase 2: Execution starts
537
+ mock_control_plane = Mock()
538
+ mock_control_plane.publish_event_async = AsyncMock(return_value=True)
539
+
540
+ publisher = EventPublisher(
541
+ control_plane=mock_control_plane,
542
+ execution_id="integration-test-exec",
543
+ config=config,
544
+ )
545
+
546
+ # Phase 3: Simulate execution for 5 minutes with events
547
+ execution_statuses = [
548
+ {"id": "integration-test-exec", "status": "running"},
549
+ {"id": "integration-test-exec", "status": "running"},
550
+ {"id": "integration-test-exec", "status": "running"},
551
+ {"id": "integration-test-exec", "status": "completed"},
552
+ {"id": "integration-test-exec", "status": "completed"}, # Second check triggers shutdown
553
+ ]
554
+
555
+ mock_http_client = MockHTTPXClient(execution_statuses)
556
+
557
+ # Simulate monitoring
558
+ consecutive_completion_checks = 0
559
+ should_shutdown = False
560
+
561
+ for i in range(5):
562
+ # Publish event during execution
563
+ await publisher.publish("message_chunk", {"content": f"Working on task {i}..."})
564
+
565
+ # Monitor checks status
566
+ async with mock_http_client as client:
567
+ response = await client.get(
568
+ "http://test/api/v1/worker-queues/queue-123/executions",
569
+ headers={"Authorization": "Bearer test-key"},
570
+ params={"limit": 5, "status": "all"}
571
+ )
572
+
573
+ executions = response.json()
574
+ for execution in executions:
575
+ exec_status = execution.get("status")
576
+
577
+ if exec_status == "completed":
578
+ consecutive_completion_checks += 1
579
+ if consecutive_completion_checks >= 2:
580
+ should_shutdown = True
581
+ break
582
+
583
+ # Phase 4: Cleanup
584
+ await publisher.flush()
585
+ await publisher.close()
586
+
587
+ # Assertions
588
+ assert publisher._current_transport == TransportMode.HTTP, "Should stay HTTP throughout"
589
+ assert consecutive_completion_checks == 2, "Should have 2 completion checks"
590
+ assert should_shutdown is True, "Should shutdown after verification"
591
+ assert mock_control_plane.publish_event_async.call_count >= 1, "Should publish events (may be batched)"
592
+
593
+ finally:
594
+ del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
595
+
596
+ async def test_execution_with_kubectl_commands_beyond_4_minutes(self):
597
+ """
598
+ Simulate the actual user scenario: kubectl commands running beyond 4 minutes
599
+ This is the exact bug scenario from the issue.
600
+ """
601
+
602
+ os.environ["KUBIYA_SINGLE_EXECUTION_MODE"] = "true"
603
+
604
+ try:
605
+ config = EventPublisherConfig.from_env()
606
+
607
+ mock_control_plane = Mock()
608
+ mock_control_plane.publish_event_async = AsyncMock(return_value=True)
609
+
610
+ publisher = EventPublisher(
611
+ control_plane=mock_control_plane,
612
+ execution_id="kubectl-exec",
613
+ config=config,
614
+ )
615
+
616
+ # Simulate the timeline from the user's logs:
617
+ # 07:02:40 - Execution starts
618
+ # 07:04:09 - kubectl get nodes command (129 seconds)
619
+ # 07:06:41 - kubectl config view command (240 seconds - WebSocket switch time!)
620
+ # Should continue without exiting
621
+
622
+ events = [
623
+ (0, "Execution started"),
624
+ (129, "Running kubectl get nodes -o wide"),
625
+ (240, "Running kubectl config view --minify"), # Critical moment!
626
+ (245, "Running minikube status -p kubiya-k8s"),
627
+ (300, "Task completed successfully"),
628
+ ]
629
+
630
+ for elapsed_time, description in events:
631
+ await publisher.publish("message_chunk", {
632
+ "content": description,
633
+ "elapsed": elapsed_time
634
+ })
635
+
636
+ # At 240 seconds, verify no WebSocket switch occurred
637
+ if elapsed_time == 240:
638
+ assert publisher._current_transport == TransportMode.HTTP, \
639
+ "Should NOT switch to WebSocket at 240 seconds"
640
+ assert publisher._ws_connection is None, \
641
+ "WebSocket connection should NOT be created"
642
+
643
+ await publisher.flush()
644
+ await publisher.close()
645
+
646
+ # Verify all events were published successfully via HTTP (may be batched)
647
+ assert mock_control_plane.publish_event_async.call_count >= 1, \
648
+ "Should publish events via HTTP"
649
+
650
+ finally:
651
+ del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
652
+
653
+
654
+ if __name__ == "__main__":
655
+ """Run tests with pytest"""
656
+ pytest.main([__file__, "-v", "-s"])
File without changes