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,1167 @@
1
+ """Runtime-based execution activities for Temporal workflows.
2
+
3
+ This module provides activities that use the RuntimeFactory/RuntimeRegistry system
4
+ for agent execution, supporting multiple runtimes (Agno/Default, Claude Code, etc.)
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Optional, List, Dict, Any
9
+ from temporalio import activity
10
+ from temporalio.exceptions import ApplicationError
11
+ import structlog
12
+ import os
13
+ import asyncio
14
+ import time
15
+ import httpx
16
+ from types import GeneratorType
17
+
18
+ from control_plane_api.worker.runtimes.base import (
19
+ RuntimeType,
20
+ RuntimeExecutionContext,
21
+ RuntimeExecutionResult,
22
+ )
23
+ from control_plane_api.worker.runtimes.factory import RuntimeFactory
24
+ from control_plane_api.worker.control_plane_client import get_control_plane_client
25
+ from control_plane_api.worker.services.cancellation_manager import CancellationManager
26
+ from control_plane_api.worker.services.runtime_analytics import submit_runtime_analytics
27
+ from control_plane_api.worker.services.analytics_service import AnalyticsService
28
+ from control_plane_api.worker.utils.logging_config import sanitize_value
29
+
30
+ logger = structlog.get_logger(__name__)
31
+
32
+
33
+ def serialize_tool_output(output: Any, max_length: int = 10000) -> Optional[str]:
34
+ """
35
+ Safely serialize tool output for JSON encoding.
36
+
37
+ Handles:
38
+ - Generator objects (consumes and converts to string)
39
+ - Large strings (truncates with indication)
40
+ - None values
41
+ - Other types (converts to string)
42
+
43
+ Args:
44
+ output: Tool output to serialize
45
+ max_length: Maximum length for output string (default 10000)
46
+
47
+ Returns:
48
+ Serialized string or None
49
+ """
50
+ if output is None:
51
+ return None
52
+
53
+ try:
54
+ # Check if it's a generator - consume it first
55
+ if isinstance(output, GeneratorType):
56
+ # Consume generator and join results
57
+ output = ''.join(str(item) for item in output)
58
+
59
+ # Convert to string
60
+ output_str = str(output)
61
+
62
+ # Truncate if too long
63
+ if len(output_str) > max_length:
64
+ return output_str[:max_length] + f"\n... (truncated, {len(output_str) - max_length} chars omitted)"
65
+
66
+ return output_str
67
+
68
+ except Exception as e:
69
+ logger.warning("failed_to_serialize_tool_output", error=str(e))
70
+ return f"<Failed to serialize output: {str(e)}>"
71
+
72
+
73
+ def inject_env_vars_into_mcp_servers(
74
+ mcp_servers: Dict[str, Any],
75
+ agent_config: Optional[Dict[str, Any]] = None,
76
+ runtime_config: Optional[Dict[str, Any]] = None,
77
+ control_plane_client: Optional[Any] = None,
78
+ agent_id: Optional[str] = None,
79
+ team_id: Optional[str] = None,
80
+ ) -> Dict[str, Any]:
81
+ """
82
+ Inject environment variables into MCP server configurations (runtime-agnostic).
83
+
84
+ This ensures MCP servers have access to critical environment variables like:
85
+ - KUBIYA_API_KEY: For API authentication
86
+ - KUBIYA_API_BASE: For API base URL
87
+ - Agent/team-specific environment variables from agent_config
88
+ - Resolved environment (inherited vars, decrypted secrets, integration tokens)
89
+
90
+ This function is runtime-agnostic and can be used by any runtime (Default, Claude Code, etc.)
91
+
92
+ Args:
93
+ mcp_servers: Dictionary of MCP server configurations
94
+ agent_config: Optional agent configuration with env_vars
95
+ runtime_config: Optional runtime configuration with env vars
96
+ control_plane_client: Optional Control Plane API client for fetching resolved environment
97
+ agent_id: Optional agent ID for fetching agent-specific resolved environment
98
+ team_id: Optional team ID for fetching team-specific resolved environment
99
+
100
+ Returns:
101
+ Modified MCP server configurations with injected env vars
102
+ """
103
+ if not mcp_servers:
104
+ return mcp_servers
105
+
106
+ # Collect environment variables to inject
107
+ env_vars_to_inject = {}
108
+
109
+ # Add Kubiya API credentials from OS environment
110
+ kubiya_api_key = os.environ.get("KUBIYA_API_KEY")
111
+ if kubiya_api_key:
112
+ env_vars_to_inject["KUBIYA_API_KEY"] = kubiya_api_key
113
+
114
+ kubiya_api_base = os.environ.get("KUBIYA_API_BASE")
115
+ if kubiya_api_base:
116
+ env_vars_to_inject["KUBIYA_API_BASE"] = kubiya_api_base
117
+
118
+ # Layer 2: Fetch RESOLVED environment from Control Plane (includes inherited vars, secrets, tokens)
119
+ if control_plane_client and (agent_id or team_id):
120
+ try:
121
+ resolved_env = {}
122
+ if agent_id:
123
+ logger.info("fetching_resolved_agent_environment", agent_id=agent_id[:8])
124
+ resolved_env = control_plane_client.get_agent_execution_environment(agent_id)
125
+ elif team_id:
126
+ logger.info("fetching_resolved_team_environment", team_id=team_id[:8])
127
+ resolved_env = control_plane_client.get_team_execution_environment(team_id)
128
+
129
+ if resolved_env:
130
+ logger.info(
131
+ "resolved_environment_fetched",
132
+ entity_type="agent" if agent_id else "team",
133
+ entity_id=(agent_id or team_id)[:8],
134
+ env_var_count=len(resolved_env),
135
+ env_var_keys=list(resolved_env.keys()),
136
+ )
137
+ env_vars_to_inject.update(resolved_env)
138
+ else:
139
+ logger.warning("resolved_environment_empty", entity_id=(agent_id or team_id)[:8])
140
+ except Exception as e:
141
+ logger.error(
142
+ "resolved_environment_fetch_failed",
143
+ entity_id=(agent_id or team_id)[:8] if (agent_id or team_id) else "unknown",
144
+ error=str(e),
145
+ error_type=type(e).__name__,
146
+ fallback_behavior="continuing_with_partial_environment",
147
+ exc_info=True,
148
+ )
149
+
150
+ # Add any env vars from agent_config
151
+ if agent_config:
152
+ agent_env_vars = agent_config.get("env_vars", {})
153
+ if agent_env_vars and isinstance(agent_env_vars, dict):
154
+ env_vars_to_inject.update(agent_env_vars)
155
+ elif agent_env_vars:
156
+ logger.warning("agent_config.env_vars is not a dict, skipping", type=type(agent_env_vars).__name__)
157
+
158
+ # Also check runtime_config for env vars
159
+ if runtime_config:
160
+ runtime_env_vars = runtime_config.get("env", {})
161
+ if runtime_env_vars and isinstance(runtime_env_vars, dict):
162
+ env_vars_to_inject.update(runtime_env_vars)
163
+ elif runtime_env_vars:
164
+ logger.warning("runtime_config.env is not a dict, skipping", type=type(runtime_env_vars).__name__)
165
+
166
+ # ALSO inject collected env vars into runtime_config.env for SDK usage
167
+ # This ensures runtime SDK (Claude Code, Agno) has access to resolved environment
168
+ if runtime_config is not None:
169
+ if "env" not in runtime_config:
170
+ runtime_config["env"] = {}
171
+ # Merge all collected env vars into runtime_config.env (in-place mutation)
172
+ # Note: This happens after we've collected from runtime_config, so we're merging
173
+ # OS env + resolved env + agent_config env + runtime_config env back into runtime_config
174
+ runtime_config["env"].update(env_vars_to_inject)
175
+ logger.debug("updated_runtime_config_env", total_env_count=len(runtime_config["env"]))
176
+
177
+ if not env_vars_to_inject:
178
+ logger.debug("No environment variables to inject into MCP servers")
179
+ return mcp_servers
180
+
181
+ logger.info(
182
+ "Injecting environment variables into MCP servers",
183
+ server_count=len(mcp_servers),
184
+ env_var_keys=list(env_vars_to_inject.keys()),
185
+ )
186
+
187
+ def resolve_template_string(value: str, env_vars: Dict[str, str]) -> str:
188
+ """
189
+ Resolve template variables in a string.
190
+ Replaces {{VAR_NAME}} with the value from env_vars.
191
+ """
192
+ import re
193
+ result = value
194
+ for var_name, var_value in env_vars.items():
195
+ # Match {{VAR_NAME}} patterns
196
+ pattern = r'\{\{' + re.escape(var_name) + r'\}\}'
197
+ result = re.sub(pattern, var_value, result)
198
+ return result
199
+
200
+ def resolve_templates_in_dict(data: Any, env_vars: Dict[str, str]) -> Any:
201
+ """
202
+ Recursively resolve template variables in dictionaries, lists, and strings.
203
+ """
204
+ if isinstance(data, dict):
205
+ return {k: resolve_templates_in_dict(v, env_vars) for k, v in data.items()}
206
+ elif isinstance(data, list):
207
+ return [resolve_templates_in_dict(item, env_vars) for item in data]
208
+ elif isinstance(data, str):
209
+ return resolve_template_string(data, env_vars)
210
+ else:
211
+ return data
212
+
213
+ # Inject env vars into each MCP server
214
+ modified_servers = {}
215
+ for server_name, server_config in mcp_servers.items():
216
+ try:
217
+ # Handle different MCP server configuration formats
218
+ if hasattr(server_config, 'env'):
219
+ # StdioServerParameters or similar object with env attribute
220
+ if server_config.env is None:
221
+ server_config.env = {}
222
+ # Merge env vars (don't override existing ones from server config)
223
+ server_config.env = {**env_vars_to_inject, **server_config.env}
224
+ logger.debug(
225
+ f"Injected env vars into MCP server '{server_name}' (object with env attribute)",
226
+ env_count=len(server_config.env),
227
+ )
228
+ elif isinstance(server_config, dict):
229
+ # Dictionary-based configuration
230
+ # First, resolve template variables in the entire config
231
+ server_config = resolve_templates_in_dict(server_config, env_vars_to_inject)
232
+
233
+ # Then add env vars to the env field
234
+ if 'env' not in server_config:
235
+ server_config['env'] = {}
236
+ # Merge env vars (don't override existing ones from server config)
237
+ server_config['env'] = {**env_vars_to_inject, **server_config['env']}
238
+ logger.debug(
239
+ f"Injected env vars and resolved templates in MCP server '{server_name}' (dict config)",
240
+ env_count=len(server_config['env']),
241
+ )
242
+ else:
243
+ # Unknown format - try to set env attribute directly
244
+ try:
245
+ if not hasattr(server_config, 'env'):
246
+ setattr(server_config, 'env', {})
247
+ server_config.env = {**env_vars_to_inject, **getattr(server_config, 'env', {})}
248
+ logger.debug(
249
+ f"Injected env vars into MCP server '{server_name}' (setattr)",
250
+ env_count=len(server_config.env),
251
+ )
252
+ except Exception as attr_error:
253
+ logger.warning(
254
+ f"Could not inject env vars into MCP server '{server_name}' - unsupported format",
255
+ server_type=type(server_config).__name__,
256
+ error=str(attr_error),
257
+ )
258
+
259
+ modified_servers[server_name] = server_config
260
+
261
+ except Exception as e:
262
+ logger.error(
263
+ f"Error injecting env vars into MCP server '{server_name}'",
264
+ error=str(e),
265
+ exc_info=True,
266
+ )
267
+ # Keep original server config if injection fails
268
+ modified_servers[server_name] = server_config
269
+
270
+ logger.info(
271
+ "✅ Environment variables injected into MCP servers",
272
+ server_count=len(modified_servers),
273
+ env_vars_injected=list(env_vars_to_inject.keys()),
274
+ )
275
+
276
+ return modified_servers
277
+
278
+
279
+ @dataclass
280
+ class ActivityRuntimeExecuteInput:
281
+ """Input for runtime-based execution activity"""
282
+ execution_id: str
283
+ agent_id: str
284
+ organization_id: str
285
+ prompt: str
286
+ runtime_type: str = "default" # "default", "claude_code", etc.
287
+ system_prompt: Optional[str] = None
288
+ model_id: Optional[str] = None
289
+ model_config: Optional[Dict[str, Any]] = None
290
+ agent_config: Optional[Dict[str, Any]] = None
291
+ skills: Optional[List[Dict[str, Any]]] = None
292
+ mcp_servers: Optional[Dict[str, Any]] = None
293
+ conversation_history: Optional[List[Dict[str, Any]]] = None
294
+ user_metadata: Optional[Dict[str, Any]] = None
295
+ runtime_config: Optional[Dict[str, Any]] = None
296
+ stream: bool = False
297
+ conversation_turn: int = 1 # Track turn number for analytics
298
+ user_message_id: Optional[str] = None # Message ID from workflow signal for deduplication
299
+ user_id: Optional[str] = None # User who sent the message
300
+ user_name: Optional[str] = None
301
+ user_email: Optional[str] = None
302
+ user_avatar: Optional[str] = None
303
+ # Enforcement context fields
304
+ user_roles: Optional[List[str]] = None
305
+ team_id: Optional[str] = None
306
+ team_name: Optional[str] = None
307
+ environment: str = "production"
308
+ # NEW: Session ID for client pooling (enables client reuse across followups)
309
+ session_id: Optional[str] = None
310
+
311
+ def __post_init__(self):
312
+ if self.model_config is None:
313
+ self.model_config = {}
314
+ if self.agent_config is None:
315
+ self.agent_config = {}
316
+ if self.skills is None:
317
+ self.skills = []
318
+ if self.mcp_servers is None:
319
+ self.mcp_servers = {}
320
+ if self.conversation_history is None:
321
+ self.conversation_history = []
322
+ if self.user_metadata is None:
323
+ self.user_metadata = {}
324
+ if self.runtime_config is None:
325
+ self.runtime_config = {}
326
+ if self.user_roles is None:
327
+ self.user_roles = []
328
+
329
+
330
+ @dataclass
331
+ class PublishUserMessageInput:
332
+ """Input for publishing user message to stream"""
333
+ execution_id: str
334
+ prompt: str
335
+ timestamp: str
336
+ message_id: Optional[str] = None
337
+ user_id: Optional[str] = None
338
+ user_name: Optional[str] = None
339
+ user_email: Optional[str] = None
340
+ user_avatar: Optional[str] = None
341
+
342
+
343
+ @activity.defn
344
+ async def publish_user_message(input: PublishUserMessageInput) -> Dict[str, Any]:
345
+ """
346
+ Publish user message to SSE stream immediately.
347
+
348
+ This ensures the user message appears in chronological order in the UI,
349
+ before the assistant response starts streaming.
350
+
351
+ Args:
352
+ input: User message details to publish
353
+
354
+ Returns:
355
+ Dict with success status
356
+ """
357
+ activity.logger.info(
358
+ "Publishing user message to stream",
359
+ extra={
360
+ "execution_id": input.execution_id,
361
+ "message_id": input.message_id,
362
+ "has_user_metadata": bool(input.user_id),
363
+ }
364
+ )
365
+
366
+ try:
367
+ # Get Control Plane client
368
+ control_plane = get_control_plane_client()
369
+
370
+ # Initialize event bus (Redis) for real-time streaming
371
+ await control_plane.initialize_event_bus()
372
+
373
+ # Publish user message event
374
+ control_plane.publish_event(
375
+ execution_id=input.execution_id,
376
+ event_type="message",
377
+ data={
378
+ "role": "user",
379
+ "content": input.prompt,
380
+ "timestamp": input.timestamp,
381
+ "message_id": input.message_id,
382
+ "user_id": input.user_id,
383
+ "user_name": input.user_name,
384
+ "user_email": input.user_email,
385
+ "user_avatar": input.user_avatar,
386
+ }
387
+ )
388
+
389
+ activity.logger.info(
390
+ "✅ User message published to stream",
391
+ extra={
392
+ "execution_id": input.execution_id,
393
+ "message_id": input.message_id,
394
+ }
395
+ )
396
+
397
+ return {"success": True, "message_id": input.message_id}
398
+
399
+ except Exception as e:
400
+ error_msg = str(e) or repr(e) or "Unknown error publishing user message"
401
+ activity.logger.error(
402
+ "Failed to publish user message",
403
+ extra={
404
+ "execution_id": input.execution_id,
405
+ "error": error_msg,
406
+ "error_type": type(e).__name__,
407
+ },
408
+ exc_info=True,
409
+ )
410
+ # Don't fail the workflow if publishing fails - this is non-critical
411
+ return {"success": False, "error": error_msg}
412
+
413
+
414
+ @activity.defn
415
+ async def execute_with_runtime(input: ActivityRuntimeExecuteInput) -> Dict[str, Any]:
416
+ """
417
+ Execute agent using the RuntimeFactory/RuntimeRegistry system.
418
+
419
+ This activity:
420
+ 1. Creates a runtime based on runtime_type (default, claude_code, etc.)
421
+ 2. Builds execution context
422
+ 3. Executes (streaming or non-streaming)
423
+ 4. Returns results
424
+
425
+ Args:
426
+ input: Activity input with execution details and runtime_type
427
+
428
+ Returns:
429
+ Dict with response, usage, success flag, etc.
430
+ """
431
+ logger.info(
432
+ "runtime_execution_initializing",
433
+ execution_id=input.execution_id,
434
+ agent_id=input.agent_id,
435
+ organization=input.organization_id,
436
+ runtime_type=input.runtime_type,
437
+ model=input.model_id or 'default',
438
+ stream=input.stream,
439
+ skills_count=len(input.skills),
440
+ mcp_servers_count=len(input.mcp_servers),
441
+ prompt_preview=input.prompt[:100] + "..." if len(input.prompt) > 100 else input.prompt
442
+ )
443
+
444
+ activity.logger.info(
445
+ "Executing with Runtime system",
446
+ extra={
447
+ "execution_id": input.execution_id,
448
+ "agent_id": input.agent_id,
449
+ "organization_id": input.organization_id,
450
+ "runtime_type": input.runtime_type,
451
+ "model_id": input.model_id,
452
+ "stream": input.stream,
453
+ }
454
+ )
455
+
456
+ try:
457
+ # Track execution start time for analytics
458
+ turn_start_time = time.time()
459
+
460
+ # Get Control Plane client and cancellation manager
461
+ control_plane = get_control_plane_client()
462
+
463
+ # Initialize event bus (Redis) for real-time streaming
464
+ # This must be called in async context to establish connections
465
+ await control_plane.initialize_event_bus()
466
+
467
+ cancellation_manager = CancellationManager()
468
+
469
+ # STEP 0: Resolve execution environment (secrets, integrations, env vars)
470
+ # Call Control Plane API to get resolved execution environment
471
+ logger.info("resolving_execution_environment", agent_id=input.agent_id)
472
+ resolved_env_vars = {}
473
+ resolved_mcp_servers = {}
474
+
475
+ try:
476
+ # Get Kubiya API token from environment
477
+ kubiya_token = os.environ.get("KUBIYA_API_KEY")
478
+ if not kubiya_token:
479
+ raise ValueError("KUBIYA_API_KEY environment variable not set")
480
+
481
+ # Get Control Plane URL
482
+ control_plane_url = os.environ.get("CONTROL_PLANE_URL", "https://control-plane.kubiya.ai")
483
+
484
+ # Call Control Plane API to resolve execution environment
485
+ api_url = f"{control_plane_url}/api/v1/execution-environment/agents/{input.agent_id}/resolved/full"
486
+
487
+ logger.debug("control_plane_api_call", api_url=api_url)
488
+
489
+ async with httpx.AsyncClient(timeout=30.0) as client:
490
+ response = await client.get(
491
+ api_url,
492
+ headers={
493
+ "Authorization": f"UserKey {kubiya_token}",
494
+ "Accept": "application/json",
495
+ }
496
+ )
497
+
498
+ if response.status_code == 200:
499
+ resolved_env = response.json()
500
+ resolved_env_vars = resolved_env.get("env_vars", {})
501
+ resolved_mcp_servers = resolved_env.get("mcp_servers", {})
502
+
503
+ logger.info(
504
+ "execution_environment_resolved",
505
+ env_var_count=len(resolved_env_vars),
506
+ mcp_server_count=len(resolved_mcp_servers)
507
+ )
508
+
509
+ logger.debug(
510
+ "resolved_env_var_keys",
511
+ env_var_keys=list(resolved_env_vars.keys())
512
+ )
513
+
514
+ logger.debug(
515
+ "resolved_mcp_server_names",
516
+ mcp_server_names=list(resolved_mcp_servers.keys())
517
+ )
518
+
519
+ # Log detailed env var info at DEBUG level with sanitization
520
+ for key, value in resolved_env_vars.items():
521
+ logger.debug(
522
+ "env_var_detail",
523
+ key=key,
524
+ value=sanitize_value(key, value),
525
+ value_length=len(str(value))
526
+ )
527
+
528
+ activity.logger.info(
529
+ "execution_environment_resolved_from_api",
530
+ extra={
531
+ "execution_id": input.execution_id,
532
+ "agent_id": input.agent_id,
533
+ "env_var_count": len(resolved_env_vars),
534
+ "mcp_server_count": len(resolved_mcp_servers),
535
+ }
536
+ )
537
+ else:
538
+ logger.warning(
539
+ "execution_environment_api_error",
540
+ status_code=response.status_code,
541
+ error_preview=response.text[:200]
542
+ )
543
+ activity.logger.warning(
544
+ "execution_environment_api_error",
545
+ extra={
546
+ "execution_id": input.execution_id,
547
+ "status_code": response.status_code,
548
+ "error": response.text[:500],
549
+ }
550
+ )
551
+
552
+ except Exception as e:
553
+ logger.error("execution_environment_resolution_error", error=str(e))
554
+ activity.logger.error(
555
+ "execution_environment_resolution_error",
556
+ extra={
557
+ "execution_id": input.execution_id,
558
+ "agent_id": input.agent_id,
559
+ "error": str(e),
560
+ },
561
+ exc_info=True,
562
+ )
563
+ # Continue with empty env vars - don't fail execution
564
+
565
+ # CRITICAL: Inject resolved env vars into os.environ so skills can access them
566
+ # This is needed because Python tools (via agno) use exec() which reads from os.environ
567
+ # Store original values to restore later (to avoid polluting the worker's environment)
568
+ original_env_values = {}
569
+ for key, value in resolved_env_vars.items():
570
+ if key in os.environ:
571
+ original_env_values[key] = os.environ[key]
572
+ os.environ[key] = str(value)
573
+ logger.debug(
574
+ "injected_env_var_to_os_environ",
575
+ key=key,
576
+ value_length=len(str(value)),
577
+ )
578
+
579
+ if resolved_env_vars:
580
+ logger.info(
581
+ "env_vars_injected_to_process",
582
+ count=len(resolved_env_vars),
583
+ keys=list(resolved_env_vars.keys()),
584
+ )
585
+
586
+ # Initialize analytics service for submission
587
+ analytics_service = AnalyticsService(
588
+ control_plane_url=control_plane.base_url if hasattr(control_plane, 'base_url') else "http://localhost:8000",
589
+ api_key=os.environ.get("KUBIYA_API_KEY", ""),
590
+ )
591
+
592
+ # Parse runtime type
593
+ try:
594
+ runtime_type_enum = RuntimeType(input.runtime_type)
595
+ except ValueError:
596
+ logger.error(f"Invalid runtime_type: {input.runtime_type}, falling back to DEFAULT")
597
+ runtime_type_enum = RuntimeType.DEFAULT
598
+
599
+ # Create runtime using factory
600
+ factory = RuntimeFactory()
601
+ runtime = factory.create_runtime(
602
+ runtime_type=runtime_type_enum,
603
+ control_plane_client=control_plane,
604
+ cancellation_manager=cancellation_manager,
605
+ )
606
+
607
+ logger.info(
608
+ f"Created runtime",
609
+ extra={
610
+ "runtime_type": runtime_type_enum,
611
+ "runtime_class": runtime.__class__.__name__,
612
+ "capabilities": runtime.get_capabilities(),
613
+ }
614
+ )
615
+
616
+ # Fetch and instantiate skills if runtime supports tools
617
+ skills = input.skills or []
618
+ if runtime.supports_tools():
619
+ logger.info("fetching_skills_from_control_plane", agent_id=input.agent_id)
620
+ try:
621
+ skill_configs = control_plane.get_skills(input.agent_id)
622
+ if skill_configs:
623
+ logger.info(
624
+ "skills_resolved",
625
+ skill_count=len(skill_configs),
626
+ types=[t.get('type') for t in skill_configs],
627
+ names=[t.get('name') for t in skill_configs],
628
+ enabled=[t.get('enabled', True) for t in skill_configs]
629
+ )
630
+
631
+ # DEBUG: Show full config for workflow_executor skills
632
+ for cfg in skill_configs:
633
+ if cfg.get('type') in ['workflow_executor', 'workflow']:
634
+ logger.debug(
635
+ "workflow_executor_skill_config",
636
+ name=cfg.get('name'),
637
+ type=cfg.get('type'),
638
+ enabled=cfg.get('enabled', True),
639
+ config_keys=list(cfg.get('configuration', {}).keys())
640
+ )
641
+
642
+ # Import here to avoid circular dependency
643
+ from control_plane_api.worker.services.skill_factory import SkillFactory
644
+
645
+ logger.debug(
646
+ "before_skill_factory",
647
+ execution_id=input.execution_id,
648
+ execution_id_type=type(input.execution_id).__name__,
649
+ execution_id_bool=bool(input.execution_id),
650
+ skill_configs_count=len(skill_configs)
651
+ )
652
+
653
+ # Always include built-in context_graph_search skill
654
+ builtin_skill_types = {'context_graph_search'}
655
+ existing_skill_types = {cfg.get('type') for cfg in skill_configs}
656
+
657
+ for builtin_type in builtin_skill_types:
658
+ if builtin_type not in existing_skill_types:
659
+ builtin_config = {
660
+ 'name': builtin_type,
661
+ 'type': builtin_type,
662
+ 'enabled': True,
663
+ 'configuration': {}
664
+ }
665
+ skill_configs.append(builtin_config)
666
+ logger.info("auto_included_builtin_skill", skill_type=builtin_type)
667
+
668
+ # Determine runtime type from agent config or input
669
+ runtime_type = (input.agent_config or {}).get("runtime", input.runtime_type or "agno")
670
+
671
+ # Instantiate skills for all runtimes
672
+ # For Claude Code: custom skills will be converted to MCP servers by build_mcp_servers()
673
+ # For other runtimes: skills are used directly
674
+ skill_factory = SkillFactory(runtime_type=runtime_type)
675
+ skill_factory.initialize()
676
+
677
+ skills = skill_factory.create_skills_from_list(
678
+ skill_configs,
679
+ execution_id=input.execution_id # Pass execution_id for control plane streaming
680
+ )
681
+
682
+ if skills:
683
+ skill_types = [type(s).__name__ for s in skills]
684
+ logger.info(
685
+ "skills_instantiated",
686
+ skill_count=len(skills),
687
+ runtime_type=runtime_type,
688
+ skill_classes=skill_types
689
+ )
690
+ else:
691
+ logger.warning("no_skills_instantiated", runtime_type=runtime_type)
692
+ else:
693
+ logger.warning("no_skills_found", message="Using built-in skills only")
694
+
695
+ # Still include built-in skills even when no skills configured
696
+ from control_plane_api.worker.services.skill_factory import SkillFactory
697
+
698
+ builtin_skill_configs = [
699
+ {
700
+ 'name': 'context_graph_search',
701
+ 'type': 'context_graph_search',
702
+ 'enabled': True,
703
+ 'configuration': {}
704
+ }
705
+ ]
706
+
707
+ runtime_type = (input.agent_config or {}).get("runtime", input.runtime_type or "agno")
708
+
709
+ # Instantiate builtin skills for ALL runtimes (including Claude Code)
710
+ # For Claude Code, these Toolkit objects will be converted to MCP servers by build_mcp_servers()
711
+ skill_factory = SkillFactory(runtime_type=runtime_type)
712
+ skill_factory.initialize()
713
+ skills = skill_factory.create_skills_from_list(
714
+ builtin_skill_configs,
715
+ execution_id=input.execution_id
716
+ )
717
+
718
+ if skills:
719
+ skill_types = [type(s).__name__ if hasattr(s, '__name__') else s.get('type', 'unknown') for s in skills]
720
+ logger.info(
721
+ "builtin_skills_instantiated",
722
+ skill_count=len(skills),
723
+ runtime_type=runtime_type,
724
+ skill_types=skill_types
725
+ )
726
+ except Exception as e:
727
+ logger.error("skill_fetch_error", error=str(e), exc_info=True)
728
+
729
+ # Merge MCP servers: resolved_mcp_servers (from DB with templates resolved) + input.mcp_servers (from workflow)
730
+ # Input MCP servers override resolved ones (allows runtime overrides)
731
+ merged_mcp_servers = {**resolved_mcp_servers, **(input.mcp_servers or {})}
732
+
733
+ logger.info(
734
+ "mcp_servers_merged",
735
+ from_execution_env=len(resolved_mcp_servers),
736
+ from_workflow_input=len(input.mcp_servers) if input.mcp_servers else 0,
737
+ total_merged=len(merged_mcp_servers),
738
+ server_names=list(merged_mcp_servers.keys()) if merged_mcp_servers else []
739
+ )
740
+
741
+ # Inject environment variables into MCP servers (runtime-agnostic)
742
+ # This ensures all MCP servers have access to KUBIYA_API_KEY, KUBIYA_API_BASE, etc.
743
+ # Also includes resolved_env_vars from execution environment (secrets, integrations)
744
+ agent_config_with_env = {
745
+ **(input.agent_config or {}),
746
+ "env_vars": {
747
+ **resolved_env_vars, # Include resolved secrets/integrations
748
+ **(input.agent_config or {}).get("env_vars", {}), # Override with explicit agent config
749
+ }
750
+ }
751
+
752
+ mcp_servers_with_env = inject_env_vars_into_mcp_servers(
753
+ mcp_servers=merged_mcp_servers,
754
+ agent_config=agent_config_with_env,
755
+ runtime_config=input.runtime_config,
756
+ )
757
+
758
+ # Enrich user_metadata with additional fields for Langfuse tracking
759
+ enriched_user_metadata = dict(input.user_metadata or {})
760
+
761
+ # Add user_email if provided separately
762
+ if input.user_email and "user_email" not in enriched_user_metadata:
763
+ enriched_user_metadata["user_email"] = input.user_email
764
+
765
+ # Add user_id if provided separately
766
+ if input.user_id and "user_id" not in enriched_user_metadata:
767
+ enriched_user_metadata["user_id"] = input.user_id
768
+
769
+ # Add user_name if provided separately
770
+ if input.user_name and "user_name" not in enriched_user_metadata:
771
+ enriched_user_metadata["user_name"] = input.user_name
772
+
773
+ # Add session_id from runtime_config
774
+ if input.runtime_config and "session_id" in input.runtime_config:
775
+ enriched_user_metadata["session_id"] = input.runtime_config["session_id"]
776
+ elif "session_id" not in enriched_user_metadata:
777
+ # Default to execution_id for session tracking
778
+ enriched_user_metadata["session_id"] = input.execution_id
779
+
780
+ # Add agent_name if not already present (for generation_name in Langfuse)
781
+ if "agent_name" not in enriched_user_metadata:
782
+ # Try to get from agent_config or use agent_id
783
+ if input.agent_config and "name" in input.agent_config:
784
+ enriched_user_metadata["agent_name"] = input.agent_config["name"]
785
+ else:
786
+ enriched_user_metadata["agent_name"] = input.agent_id
787
+
788
+ logger.info(
789
+ "Enriched user_metadata for Langfuse tracking",
790
+ extra={
791
+ "execution_id": input.execution_id,
792
+ "has_user_email": "user_email" in enriched_user_metadata,
793
+ "has_session_id": "session_id" in enriched_user_metadata,
794
+ "has_agent_name": "agent_name" in enriched_user_metadata,
795
+ }
796
+ )
797
+
798
+ # Build runtime config with resolved environment variables and session_id
799
+ # This includes secrets, integrations, custom env vars, and session_id for client pooling
800
+ runtime_config_with_env = {
801
+ **(input.runtime_config or {}),
802
+ "env": {
803
+ **resolved_env_vars, # Secrets, integrations, custom env vars
804
+ **(input.runtime_config or {}).get("env", {}), # Override with explicit runtime config
805
+ },
806
+ # NEW: Pass session_id for client pooling (enables reuse across followups)
807
+ "session_id": input.session_id or input.execution_id, # Fallback to execution_id
808
+ }
809
+
810
+ logger.info(
811
+ "runtime_config_session_id",
812
+ session_id=(input.session_id or input.execution_id)[:16],
813
+ execution_id=input.execution_id[:16],
814
+ is_reuse_enabled=bool(input.session_id),
815
+ note="Session ID enables client reuse for followup messages"
816
+ )
817
+
818
+ env_vars = runtime_config_with_env.get('env', {})
819
+ logger.info(
820
+ "environment_variables_passed_to_runtime",
821
+ total_env_vars=len(env_vars),
822
+ env_var_keys=list(env_vars.keys())
823
+ )
824
+
825
+ # Log detailed env var info at DEBUG level with sanitization
826
+ for key, value in env_vars.items():
827
+ logger.debug(
828
+ "runtime_env_var_detail",
829
+ key=key,
830
+ value=sanitize_value(key, str(value)),
831
+ value_length=len(str(value))
832
+ )
833
+
834
+ # Create execution workspace
835
+ from control_plane_api.worker.utils.workspace_manager import ensure_workspace
836
+
837
+ workspace_path = None
838
+ try:
839
+ workspace_path = ensure_workspace(input.execution_id)
840
+
841
+ logger.info(
842
+ "execution_workspace_created",
843
+ execution_id=input.execution_id[:8] if len(input.execution_id) >= 8 else input.execution_id,
844
+ path=str(workspace_path) if workspace_path else None,
845
+ )
846
+ except Exception as e:
847
+ logger.warning(
848
+ "execution_workspace_creation_failed",
849
+ execution_id=input.execution_id[:8] if len(input.execution_id) >= 8 else input.execution_id,
850
+ error=str(e),
851
+ error_type=type(e).__name__,
852
+ fallback="skills_and_runtime_will_use_defaults",
853
+ )
854
+
855
+ # Build execution context
856
+ context = RuntimeExecutionContext(
857
+ execution_id=input.execution_id,
858
+ agent_id=input.agent_id,
859
+ organization_id=input.organization_id,
860
+ prompt=input.prompt,
861
+ system_prompt=input.system_prompt,
862
+ conversation_history=input.conversation_history,
863
+ model_id=input.model_id,
864
+ model_config=input.model_config,
865
+ agent_config=input.agent_config,
866
+ skills=skills, # Use fetched skills
867
+ mcp_servers=mcp_servers_with_env, # Use MCP servers with injected env vars
868
+ user_metadata=enriched_user_metadata, # Use enriched metadata
869
+ runtime_config=runtime_config_with_env, # Include resolved env vars!
870
+ runtime_type=runtime_type_enum, # Runtime type for validation
871
+ # Enforcement context
872
+ user_email=input.user_email,
873
+ user_id=input.user_id,
874
+ user_roles=input.user_roles or [],
875
+ team_id=input.team_id,
876
+ team_name=input.team_name,
877
+ environment=input.environment,
878
+ workspace_directory=str(workspace_path) if workspace_path else None,
879
+ )
880
+
881
+ # Execute based on streaming preference
882
+ if input.stream:
883
+ # Streaming execution
884
+ logger.info(
885
+ "🎬 Starting streaming execution",
886
+ execution_id=input.execution_id,
887
+ agent_id=input.agent_id
888
+ )
889
+ accumulated_response = ""
890
+ final_result = None
891
+
892
+ # Generate unique message ID for this turn (execution_id + timestamp)
893
+ message_id = f"{input.execution_id}_{int(time.time() * 1000000)}"
894
+
895
+ # Track tool events published
896
+ tool_events_published = {"start": 0, "complete": 0}
897
+
898
+ # Define event callback for publishing tool events to Control Plane
899
+ def event_callback(event: Dict):
900
+ """Callback to publish events (tool start/complete, content chunks) to Control Plane SSE"""
901
+ event_type = event.get("type")
902
+
903
+ if event_type == "content_chunk":
904
+ # Content chunks are already handled below via result.response
905
+ pass
906
+ elif event_type == "tool_start":
907
+ # Publish tool start event (synchronous - this runs in async context via callback)
908
+ try:
909
+ logger.info(
910
+ "tool_start_event",
911
+ tool_name=event.get('tool_name'),
912
+ tool_execution_id=event.get('tool_execution_id')
913
+ )
914
+ control_plane.publish_event(
915
+ execution_id=input.execution_id,
916
+ event_type="tool_started", # Match default runtime event type
917
+ data={
918
+ "tool_name": event.get("tool_name"),
919
+ "tool_execution_id": event.get("tool_execution_id"),
920
+ "tool_arguments": event.get("tool_args", {}),
921
+ "message": f"🔧 Executing tool: {event.get('tool_name')}",
922
+ "source": "agent",
923
+ }
924
+ )
925
+ tool_events_published["start"] += 1
926
+ logger.debug(
927
+ "tool_started_event_published",
928
+ event_number=tool_events_published['start'],
929
+ tool_name=event.get('tool_name')
930
+ )
931
+ except Exception as e:
932
+ logger.error("tool_start_event_publish_failed", error=str(e), exc_info=True)
933
+ elif event_type == "tool_complete":
934
+ # Publish tool complete event
935
+ try:
936
+ status = event.get("status", "success")
937
+ logger.info(
938
+ "tool_complete_event",
939
+ tool_name=event.get('tool_name'),
940
+ status=status
941
+ )
942
+ control_plane.publish_event(
943
+ execution_id=input.execution_id,
944
+ event_type="tool_completed", # Match default runtime event type
945
+ data={
946
+ "tool_name": event.get("tool_name"),
947
+ "tool_execution_id": event.get("tool_execution_id"),
948
+ "status": status,
949
+ "tool_output": serialize_tool_output(event.get("output")), # Safely serialize output (handles generators)
950
+ "tool_error": event.get("error"),
951
+ "message": f"Tool {status}: {event.get('tool_name')}",
952
+ "source": "agent",
953
+ }
954
+ )
955
+ tool_events_published["complete"] += 1
956
+ logger.debug(
957
+ "tool_completed_event_published",
958
+ event_number=tool_events_published['complete'],
959
+ tool_name=event.get('tool_name')
960
+ )
961
+ except Exception as e:
962
+ logger.error("tool_complete_event_publish_failed", error=str(e), exc_info=True)
963
+
964
+ # Stream execution with event callback
965
+ # Note: AgnoRuntime publishes chunks via EventPublisher internally
966
+ # But ClaudeCodeRuntime needs us to publish chunks here
967
+ is_agno_runtime = runtime_type_enum == RuntimeType.DEFAULT
968
+
969
+ # Track last heartbeat time for periodic heartbeats during streaming
970
+ last_heartbeat = time.time()
971
+ chunk_count = 0
972
+
973
+ async for result in runtime.stream_execute(context, event_callback):
974
+ # Only process non-empty content (filter out empty strings and whitespace)
975
+ if result.response and result.response.strip():
976
+ accumulated_response += result.response
977
+ chunk_count += 1
978
+
979
+ # Publish chunks for non-Agno runtimes (e.g., Claude Code)
980
+ # AgnoRuntime publishes internally via EventPublisher to avoid duplicates
981
+ if not is_agno_runtime:
982
+ try:
983
+ await control_plane.publish_event_async(
984
+ execution_id=input.execution_id,
985
+ event_type="message_chunk",
986
+ data={
987
+ "role": "assistant",
988
+ "content": result.response,
989
+ "is_chunk": True,
990
+ "message_id": message_id,
991
+ }
992
+ )
993
+ except Exception as e:
994
+ logger.warning(f"Failed to publish streaming chunk: {e}")
995
+
996
+ # Send heartbeat every 10 seconds or every 50 chunks to detect hung executions
997
+ current_time = time.time()
998
+ if current_time - last_heartbeat > 10 or chunk_count % 50 == 0:
999
+ activity.heartbeat({
1000
+ "status": "streaming",
1001
+ "chunks_received": chunk_count,
1002
+ "response_length": len(accumulated_response),
1003
+ "elapsed_seconds": int(current_time - last_heartbeat)
1004
+ })
1005
+ last_heartbeat = current_time
1006
+
1007
+ if result.finish_reason:
1008
+ final_result = result
1009
+ break
1010
+
1011
+ if not final_result:
1012
+ raise RuntimeError("Streaming execution did not provide final result")
1013
+
1014
+ # Log tool event summary
1015
+ logger.info(
1016
+ "tool_events_summary",
1017
+ tool_started_events=tool_events_published['start'],
1018
+ tool_completed_events=tool_events_published['complete'],
1019
+ tool_messages_in_result=len(final_result.tool_messages or [])
1020
+ )
1021
+
1022
+ # Analytics now handled by separate Temporal activity in workflow
1023
+ # See: workflow calls submit_runtime_analytics_activity after this returns
1024
+
1025
+ # Log before return to verify we reach this point
1026
+ logger.info(
1027
+ "activity_about_to_return_streaming",
1028
+ execution_id=input.execution_id,
1029
+ turn_number=input.conversation_turn,
1030
+ note="About to return streaming activity result to Temporal"
1031
+ )
1032
+
1033
+ return {
1034
+ "success": final_result.success,
1035
+ "response": accumulated_response,
1036
+ "usage": final_result.usage or {},
1037
+ "model": final_result.model,
1038
+ "finish_reason": final_result.finish_reason,
1039
+ "tool_messages": final_result.tool_messages or [],
1040
+ "metadata": final_result.metadata or {},
1041
+ "error": final_result.error,
1042
+ }
1043
+
1044
+ else:
1045
+ # Non-streaming execution
1046
+ logger.info(
1047
+ "🎬 Starting non-streaming execution",
1048
+ execution_id=input.execution_id,
1049
+ agent_id=input.agent_id
1050
+ )
1051
+ result = await runtime.execute(context)
1052
+
1053
+ # Analytics now handled by separate Temporal activity in workflow
1054
+ # See: workflow calls submit_runtime_analytics_activity after this returns
1055
+
1056
+ # Log before return to verify we reach this point
1057
+ logger.info(
1058
+ "activity_about_to_return_non_streaming",
1059
+ execution_id=input.execution_id,
1060
+ turn_number=input.conversation_turn,
1061
+ note="About to return non-streaming activity result to Temporal"
1062
+ )
1063
+
1064
+ return {
1065
+ "success": result.success,
1066
+ "response": result.response,
1067
+ "usage": result.usage or {},
1068
+ "model": result.model,
1069
+ "finish_reason": result.finish_reason,
1070
+ "tool_messages": result.tool_messages or [],
1071
+ "metadata": result.metadata or {},
1072
+ "error": result.error,
1073
+ }
1074
+
1075
+ except asyncio.CancelledError as e:
1076
+ # DURABILITY FIX: Handle activity-level cancellation gracefully
1077
+ # This catches cancellations from Temporal (workflow cancellation, activity timeout, etc.)
1078
+ logger.warning(
1079
+ "Activity execution cancelled by Temporal",
1080
+ extra={
1081
+ "execution_id": input.execution_id,
1082
+ "runtime_type": input.runtime_type,
1083
+ "conversation_turn": input.conversation_turn,
1084
+ },
1085
+ )
1086
+
1087
+ # Return a partial result instead of failing
1088
+ # This allows the workflow to handle the interruption and potentially resume
1089
+ return {
1090
+ "success": False, # Mark as failure since we couldn't complete
1091
+ "response": "",
1092
+ "usage": {},
1093
+ "model": input.model_id,
1094
+ "finish_reason": "cancelled",
1095
+ "tool_messages": [],
1096
+ "metadata": {
1097
+ "interrupted": True,
1098
+ "can_resume": False, # Activity-level cancellation can't resume easily
1099
+ "cancellation_source": "temporal_activity",
1100
+ },
1101
+ "error": "Execution was cancelled by Temporal",
1102
+ }
1103
+
1104
+ except Exception as e:
1105
+ # Ensure error message is never empty
1106
+ error_msg = str(e) or repr(e) or f"{type(e).__name__}: No error details available"
1107
+
1108
+ logger.error(
1109
+ "Runtime execution failed",
1110
+ extra={
1111
+ "execution_id": input.execution_id,
1112
+ "runtime_type": input.runtime_type,
1113
+ "error": error_msg,
1114
+ "error_type": type(e).__name__,
1115
+ },
1116
+ exc_info=True,
1117
+ )
1118
+
1119
+ # Publish error event to Control Plane for real-time UI updates
1120
+ try:
1121
+ from control_plane_api.worker.utils.error_publisher import (
1122
+ ErrorEventPublisher, ErrorSeverity, ErrorCategory
1123
+ )
1124
+
1125
+ error_publisher = ErrorEventPublisher(control_plane)
1126
+
1127
+ # Determine error category based on error message
1128
+ error_str = error_msg.lower()
1129
+ category = ErrorCategory.UNKNOWN
1130
+ if "timeout" in error_str:
1131
+ category = ErrorCategory.TIMEOUT
1132
+ elif "import" in error_str or "module" in error_str:
1133
+ category = ErrorCategory.RUNTIME_INIT
1134
+ elif "api" in error_str or "model" in error_str or "anthropic" in error_str:
1135
+ category = ErrorCategory.MODEL_ERROR
1136
+ elif "network" in error_str or "connection" in error_str:
1137
+ category = ErrorCategory.NETWORK
1138
+ elif "auth" in error_str or "credential" in error_str:
1139
+ category = ErrorCategory.AUTHENTICATION
1140
+
1141
+ await error_publisher.publish_error(
1142
+ execution_id=input.execution_id,
1143
+ exception=e,
1144
+ severity=ErrorSeverity.CRITICAL,
1145
+ category=category,
1146
+ stage="execution",
1147
+ component=f"{input.runtime_type}_runtime",
1148
+ operation="agent_execution",
1149
+ metadata={
1150
+ "agent_id": input.agent_id,
1151
+ "model_id": input.model_id,
1152
+ "conversation_turn": input.conversation_turn,
1153
+ }
1154
+ )
1155
+ except Exception as publish_error:
1156
+ # Never let error publishing break the main flow
1157
+ logger.warning(
1158
+ "failed_to_publish_error_event",
1159
+ error=str(publish_error)
1160
+ )
1161
+
1162
+ # Raise ApplicationError so Temporal marks the workflow as FAILED
1163
+ raise ApplicationError(
1164
+ f"Runtime execution failed: {error_msg}",
1165
+ non_retryable=False, # Allow retries per retry policy
1166
+ type=type(e).__name__
1167
+ )