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,665 @@
1
+ """Streaming utilities for agent and team execution"""
2
+
3
+ from typing import Dict, Any, Callable, Optional, List
4
+ import structlog
5
+
6
+ logger = structlog.get_logger()
7
+
8
+
9
+ class StreamingHelper:
10
+ """
11
+ Helper for handling streaming from Agno Agent/Team executions.
12
+
13
+ Provides utilities for:
14
+ - Publishing events to Control Plane
15
+ - Tracking run_id from streaming chunks
16
+ - Collecting response content
17
+ - Publishing tool execution events
18
+ - Handling member message streaming
19
+ - Tracking tool IDs for proper start/complete matching
20
+ - Splitting assistant messages into pre-tool and post-tool phases
21
+ """
22
+
23
+ def __init__(self, control_plane_client, execution_id: str):
24
+ self.control_plane = control_plane_client
25
+ self.execution_id = execution_id
26
+ self.run_id_published = False
27
+ self.response_content = []
28
+ self.member_message_ids = {} # Track message_id per member
29
+ self.active_streaming_member = None # Track which member is streaming
30
+ self.tool_execution_ids = {} # Track tool IDs for matching start/complete events
31
+ self.tool_messages = [] # Track tool messages for session persistence
32
+
33
+ # NEW: Track message phases for proper assistant message splitting
34
+ self.pre_tool_content = [] # Content before first tool use
35
+ self.post_tool_content = [] # Content after tools complete
36
+ self.tool_phase = "pre" # Current phase: "pre", "during", or "post"
37
+ self.first_tool_timestamp = None # Timestamp when first tool started
38
+ self.tools_complete_timestamp = None # Timestamp when all tools completed
39
+ self.has_any_tools = False # Track if any tools were executed
40
+
41
+ def handle_run_id(self, chunk: Any, on_run_id: Optional[Callable[[str], None]] = None) -> None:
42
+ """
43
+ Capture and publish run_id from first streaming chunk.
44
+
45
+ Args:
46
+ chunk: Streaming chunk from Agno
47
+ on_run_id: Optional callback when run_id is captured
48
+ """
49
+ if not self.run_id_published and hasattr(chunk, 'run_id') and chunk.run_id:
50
+ run_id = chunk.run_id
51
+
52
+ logger.info("run_id_captured", execution_id=self.execution_id[:8], run_id=run_id[:16])
53
+
54
+ # Publish to Control Plane for UI
55
+ self.control_plane.publish_event(
56
+ execution_id=self.execution_id,
57
+ event_type="run_started",
58
+ data={
59
+ "run_id": run_id,
60
+ "execution_id": self.execution_id,
61
+ "cancellable": True,
62
+ }
63
+ )
64
+
65
+ self.run_id_published = True
66
+
67
+ # Call callback if provided (for cancellation manager)
68
+ if on_run_id:
69
+ on_run_id(run_id)
70
+
71
+ async def handle_content_chunk(
72
+ self,
73
+ chunk: Any,
74
+ message_id: str,
75
+ print_to_console: bool = True
76
+ ) -> Optional[str]:
77
+ """
78
+ Handle content chunk from streaming response.
79
+
80
+ Tracks content in different phases (pre-tool, during-tool, post-tool)
81
+ to enable proper message splitting around tool usage.
82
+
83
+ Args:
84
+ chunk: Streaming chunk
85
+ message_id: Unique message ID for this turn
86
+ print_to_console: Whether to print to stdout
87
+
88
+ Returns:
89
+ Content string if present, None otherwise
90
+ """
91
+ # Check for both 'response' (RuntimeExecutionResult) and 'content' (legacy/Agno)
92
+ content = None
93
+
94
+ if hasattr(chunk, 'response') and chunk.response:
95
+ content = str(chunk.response)
96
+ elif hasattr(chunk, 'content') and chunk.content:
97
+ content = str(chunk.content)
98
+
99
+ if content:
100
+ # Track content in appropriate phase
101
+ if self.tool_phase == "pre":
102
+ self.pre_tool_content.append(content)
103
+ elif self.tool_phase == "post":
104
+ self.post_tool_content.append(content)
105
+ # Note: During "during" phase, we don't collect assistant content
106
+ # as tools are executing
107
+
108
+ self.response_content.append(content)
109
+
110
+ if print_to_console:
111
+ print(content, end='', flush=True)
112
+
113
+ # Stream to Control Plane for real-time UI updates
114
+ # Use async version since we're in an async context
115
+ try:
116
+ await self.control_plane.publish_event_async(
117
+ execution_id=self.execution_id,
118
+ event_type="message_chunk",
119
+ data={
120
+ "role": "assistant",
121
+ "content": content,
122
+ "is_chunk": True,
123
+ "message_id": message_id,
124
+ "phase": self.tool_phase, # NEW: Include current phase
125
+ }
126
+ )
127
+ except Exception as publish_err:
128
+ # Log but don't fail if event publishing fails
129
+ logger.warning(
130
+ "async_event_publish_failed",
131
+ execution_id=self.execution_id[:8],
132
+ error=str(publish_err)[:200],
133
+ )
134
+
135
+ return content
136
+
137
+ return None
138
+
139
+ def publish_content_chunk(self, content: str, message_id: str) -> None:
140
+ """
141
+ Publish content chunk event (sync wrapper for streaming events).
142
+
143
+ This method is called from sync event callbacks in the runtime streaming path.
144
+ It tracks content in the appropriate phase and publishes to Control Plane.
145
+
146
+ Args:
147
+ content: Content string to publish
148
+ message_id: Unique message ID for this turn
149
+ """
150
+ # Track content
151
+ self.response_content.append(content)
152
+
153
+ # Track in appropriate phase for message splitting
154
+ if self.tool_phase == "pre":
155
+ self.pre_tool_content.append(content)
156
+ elif self.tool_phase == "post":
157
+ self.post_tool_content.append(content)
158
+ # Note: During "during" phase, we don't collect assistant content
159
+ # as tools are executing
160
+
161
+ # Publish to Control Plane (use sync publish_event, not async)
162
+ # Note: This is called from a sync callback, so we can't use await
163
+ try:
164
+ self.control_plane.publish_event(
165
+ execution_id=self.execution_id,
166
+ event_type="message_chunk",
167
+ data={
168
+ "role": "assistant",
169
+ "content": content,
170
+ "is_chunk": True,
171
+ "message_id": message_id,
172
+ "phase": self.tool_phase, # Include current phase
173
+ }
174
+ )
175
+ except Exception as publish_err:
176
+ # Log but don't fail if event publishing fails
177
+ logger.warning(
178
+ "sync_event_publish_failed",
179
+ execution_id=self.execution_id[:8],
180
+ error=str(publish_err)[:200],
181
+ )
182
+
183
+ def get_full_response(self) -> str:
184
+ """Get the complete response accumulated from all chunks."""
185
+ return ''.join(self.response_content)
186
+
187
+ def handle_member_content_chunk(
188
+ self,
189
+ member_name: str,
190
+ content: str,
191
+ print_to_console: bool = True
192
+ ) -> str:
193
+ """
194
+ Handle content chunk from a team member.
195
+
196
+ Args:
197
+ member_name: Name of the team member
198
+ content: Content string
199
+ print_to_console: Whether to print to stdout
200
+
201
+ Returns:
202
+ The member's message_id
203
+ """
204
+ import time
205
+
206
+ # Generate unique message ID for this member if not exists
207
+ if member_name not in self.member_message_ids:
208
+ self.member_message_ids[member_name] = f"{self.execution_id}_{member_name}_{int(time.time() * 1000000)}"
209
+
210
+ # Print member name header once when they start
211
+ if print_to_console:
212
+ print(f"\n[{member_name}] ", end='', flush=True)
213
+
214
+ # If switching to a different member, mark the previous one as complete
215
+ if self.active_streaming_member and self.active_streaming_member != member_name:
216
+ self.publish_member_complete(self.active_streaming_member)
217
+
218
+ # Track that this member is now actively streaming
219
+ self.active_streaming_member = member_name
220
+
221
+ # Print content without repeated member name prefix
222
+ if print_to_console:
223
+ print(content, end='', flush=True)
224
+
225
+ # Stream member chunk to Control Plane
226
+ message_id = self.member_message_ids[member_name]
227
+ self.control_plane.publish_event(
228
+ execution_id=self.execution_id,
229
+ event_type="member_message_chunk",
230
+ data={
231
+ "role": "assistant",
232
+ "content": content,
233
+ "is_chunk": True,
234
+ "message_id": message_id,
235
+ "source": "team_member",
236
+ "member_name": member_name,
237
+ }
238
+ )
239
+
240
+ return message_id
241
+
242
+ def publish_member_complete(self, member_name: str) -> None:
243
+ """
244
+ Publish member_message_complete event and clear the message_id.
245
+
246
+ Args:
247
+ member_name: Name of the member to mark as complete
248
+ """
249
+ if member_name in self.member_message_ids:
250
+ self.control_plane.publish_event(
251
+ execution_id=self.execution_id,
252
+ event_type="member_message_complete",
253
+ data={
254
+ "message_id": self.member_message_ids[member_name],
255
+ "member_name": member_name,
256
+ "source": "team_member",
257
+ }
258
+ )
259
+
260
+ # CRITICAL: Clear the message_id for this member after completing
261
+ # This ensures a NEW message_id is generated for their next turn
262
+ # Without this, all turns from the same member would edit the same message!
263
+ del self.member_message_ids[member_name]
264
+ logger.info("member_message_id_cleared", member_name=member_name, execution_id=self.execution_id[:8])
265
+
266
+ def finalize_streaming(self) -> None:
267
+ """
268
+ Finalize streaming by marking any active member as complete.
269
+ Call this when streaming ends.
270
+ """
271
+ if self.active_streaming_member:
272
+ self.publish_member_complete(self.active_streaming_member)
273
+ self.active_streaming_member = None
274
+
275
+ def get_tool_messages(self) -> List[Dict[str, Any]]:
276
+ """
277
+ Get all tool messages collected during streaming for session persistence.
278
+
279
+ Returns:
280
+ List of tool message dicts with role='system', tool metadata, and timestamps
281
+ """
282
+ return self.tool_messages
283
+
284
+ def publish_tool_start(
285
+ self,
286
+ tool_name: str,
287
+ tool_execution_id: str,
288
+ tool_args: Optional[Dict[str, Any]] = None,
289
+ source: str = "agent",
290
+ member_name: Optional[str] = None
291
+ ) -> str:
292
+ """
293
+ Publish tool execution start event.
294
+
295
+ Also transitions message phase from "pre" to "during" on first tool use.
296
+
297
+ Args:
298
+ tool_name: Name of the tool
299
+ tool_execution_id: Unique ID for this tool execution
300
+ tool_args: Tool arguments
301
+ source: "agent" or "team_member" or "team_leader" or "team"
302
+ member_name: Name of member (if tool is from a member)
303
+
304
+ Returns:
305
+ message_id for this tool execution
306
+ """
307
+ import time
308
+ from datetime import datetime, timezone
309
+
310
+ # Mark transition to "during" phase on first tool
311
+ if self.tool_phase == "pre":
312
+ self.tool_phase = "during"
313
+ self.first_tool_timestamp = datetime.now(timezone.utc).isoformat()
314
+ self.has_any_tools = True
315
+ logger.info(
316
+ "phase_transition_to_during",
317
+ execution_id=self.execution_id[:8],
318
+ tool_name=tool_name,
319
+ pre_tool_content_length=len(''.join(self.pre_tool_content))
320
+ )
321
+
322
+ message_id = f"{self.execution_id}_tool_{tool_execution_id}"
323
+ is_member_tool = member_name is not None
324
+ parent_message_id = self.member_message_ids.get(member_name) if is_member_tool else None
325
+
326
+ # Store tool info for matching with completion event
327
+ tool_key = f"{member_name or 'leader'}_{tool_name}_{int(time.time())}"
328
+ self.tool_execution_ids[tool_key] = {
329
+ "tool_execution_id": tool_execution_id,
330
+ "message_id": message_id,
331
+ "tool_name": tool_name,
332
+ "member_name": member_name,
333
+ "parent_message_id": parent_message_id,
334
+ "tool_args": tool_args, # Store args for persistence
335
+ }
336
+
337
+ event_type = "member_tool_started" if is_member_tool else "tool_started"
338
+
339
+ self.control_plane.publish_event(
340
+ execution_id=self.execution_id,
341
+ event_type=event_type,
342
+ data={
343
+ "tool_name": tool_name,
344
+ "tool_execution_id": tool_execution_id,
345
+ "message_id": message_id,
346
+ "tool_arguments": tool_args,
347
+ "source": "team_member" if is_member_tool else "team_leader",
348
+ "member_name": member_name,
349
+ "parent_message_id": parent_message_id,
350
+ "message": f"🔧 Executing tool: {tool_name}",
351
+ }
352
+ )
353
+
354
+ return message_id
355
+
356
+ def publish_tool_complete(
357
+ self,
358
+ tool_name: str,
359
+ tool_execution_id: str,
360
+ status: str = "success",
361
+ output: Optional[str] = None,
362
+ error: Optional[str] = None,
363
+ source: str = "agent",
364
+ member_name: Optional[str] = None
365
+ ) -> None:
366
+ """
367
+ Publish tool execution completion event.
368
+
369
+ Args:
370
+ tool_name: Name of the tool
371
+ tool_execution_id: Unique ID for this tool execution
372
+ status: "success" or "failed"
373
+ output: Tool output (if successful)
374
+ error: Error message (if failed)
375
+ source: "agent" or "team_member" or "team_leader" or "team"
376
+ member_name: Name of member (if tool is from a member)
377
+ """
378
+ import time
379
+
380
+ # Find the stored tool info from the start event
381
+ tool_key_pattern = f"{member_name or 'leader'}_{tool_name}"
382
+ matching_tool = None
383
+ for key, tool_info in list(self.tool_execution_ids.items()):
384
+ if key.startswith(tool_key_pattern):
385
+ matching_tool = tool_info
386
+ # Remove from tracking dict
387
+ del self.tool_execution_ids[key]
388
+ break
389
+
390
+ if matching_tool:
391
+ message_id = matching_tool["message_id"]
392
+ parent_message_id = matching_tool["parent_message_id"]
393
+ # Use the stored tool_execution_id from the start event
394
+ tool_execution_id = matching_tool["tool_execution_id"]
395
+ tool_args = matching_tool.get("tool_args") # Get stored args
396
+ else:
397
+ # Fallback if start event wasn't captured
398
+ message_id = f"{self.execution_id}_tool_{tool_execution_id}"
399
+ parent_message_id = self.member_message_ids.get(member_name) if member_name else None
400
+ tool_args = None
401
+ logger.warning("tool_completion_without_start", tool_name=tool_name, member_name=member_name)
402
+
403
+ is_member_tool = member_name is not None
404
+ event_type = "member_tool_completed" if is_member_tool else "tool_completed"
405
+
406
+ tool_data = {
407
+ "tool_name": tool_name,
408
+ "tool_execution_id": tool_execution_id, # Now uses the stored ID from start event
409
+ "message_id": message_id,
410
+ "status": status,
411
+ "tool_output": output[:50000] if output else None, # Increased from 1000 to 50000 to preserve Metabase URLs and other important data
412
+ "tool_error": error,
413
+ "source": "team_member" if is_member_tool else "team_leader",
414
+ "member_name": member_name,
415
+ "parent_message_id": parent_message_id,
416
+ "message": f"{'✅' if status == 'success' else '❌'} Tool {status}: {tool_name}",
417
+ }
418
+
419
+ self.control_plane.publish_event(
420
+ execution_id=self.execution_id,
421
+ event_type=event_type,
422
+ data=tool_data
423
+ )
424
+
425
+ # Store tool message for session persistence
426
+ # NEW: Tool messages are now role="user" with tool_result content blocks
427
+ # This aligns with Claude API best practices where tool results come from user
428
+ from datetime import datetime, timezone
429
+
430
+ tool_result_content = output[:50000] if output and status == "success" else (error or "")
431
+
432
+ self.tool_messages.append({
433
+ "role": "user", # CHANGED: Tool results are user role (Claude API format)
434
+ "content": [
435
+ {
436
+ "type": "tool_result",
437
+ "tool_use_id": tool_execution_id,
438
+ "content": tool_result_content,
439
+ "is_error": status != "success",
440
+ }
441
+ ],
442
+ "message_type": "tool_result", # NEW: Mark as tool result message
443
+ "tool_name": tool_name,
444
+ "tool_execution_id": tool_execution_id,
445
+ "tool_input": tool_args, # Frontend expects "tool_input" not "tool_arguments"
446
+ "tool_output": output[:50000] if output else None,
447
+ "tool_error": error,
448
+ "tool_status": status, # Frontend expects "tool_status" not "status"
449
+ "member_name": member_name,
450
+ "message_id": message_id,
451
+ "parent_message_id": parent_message_id,
452
+ "timestamp": datetime.now(timezone.utc).isoformat(),
453
+ })
454
+
455
+ logger.info(
456
+ "tool_result_message_created",
457
+ execution_id=self.execution_id[:8],
458
+ tool_name=tool_name,
459
+ status=status,
460
+ has_output=bool(output),
461
+ has_error=bool(error)
462
+ )
463
+
464
+ def transition_to_post_tool_phase(self):
465
+ """
466
+ Transition from "during" phase to "post" phase.
467
+
468
+ Call this after all tools have completed to start collecting
469
+ post-tool assistant content.
470
+ """
471
+ if self.tool_phase == "during":
472
+ from datetime import datetime, timezone
473
+ self.tool_phase = "post"
474
+ self.tools_complete_timestamp = datetime.now(timezone.utc).isoformat()
475
+ logger.info(
476
+ "phase_transition_to_post",
477
+ execution_id=self.execution_id[:8],
478
+ tools_completed=len(self.tool_messages)
479
+ )
480
+
481
+ def finalize_streaming(self) -> None:
482
+ """
483
+ Finalize streaming by marking any active member as complete
484
+ and transitioning to post-tool phase if needed.
485
+
486
+ Call this when streaming ends.
487
+ """
488
+ # Transition to post phase if we had tools
489
+ if self.has_any_tools and self.tool_phase == "during":
490
+ self.transition_to_post_tool_phase()
491
+
492
+ # Handle team member completion
493
+ if self.active_streaming_member:
494
+ self.publish_member_complete(self.active_streaming_member)
495
+ self.active_streaming_member = None
496
+
497
+ def get_assistant_message_parts(self) -> List[Dict[str, Any]]:
498
+ """
499
+ Get assistant messages split into pre-tool and post-tool parts.
500
+
501
+ Returns:
502
+ List of assistant message dicts. May contain:
503
+ - Pre-tool message (if content exists before first tool)
504
+ - Post-tool message (if content exists after tools complete)
505
+ - Single message (if no tools were used)
506
+ """
507
+ from datetime import datetime, timezone
508
+
509
+ assistant_parts = []
510
+
511
+ # If no tools were used, return single message with all content
512
+ if not self.has_any_tools:
513
+ full_content = ''.join(self.response_content)
514
+ if full_content:
515
+ assistant_parts.append({
516
+ "content": full_content,
517
+ "phase": "complete",
518
+ "timestamp": datetime.now(timezone.utc).isoformat(),
519
+ })
520
+ return assistant_parts
521
+
522
+ # If tools were used, split into pre and post parts
523
+ pre_content = ''.join(self.pre_tool_content)
524
+ if pre_content:
525
+ assistant_parts.append({
526
+ "content": pre_content,
527
+ "phase": "pre",
528
+ "timestamp": self.first_tool_timestamp or datetime.now(timezone.utc).isoformat(),
529
+ })
530
+
531
+ post_content = ''.join(self.post_tool_content)
532
+ if post_content:
533
+ assistant_parts.append({
534
+ "content": post_content,
535
+ "phase": "post",
536
+ "timestamp": self.tools_complete_timestamp or datetime.now(timezone.utc).isoformat(),
537
+ })
538
+
539
+ return assistant_parts
540
+
541
+ def build_structured_messages(
542
+ self,
543
+ execution_id: str,
544
+ turn_number: int,
545
+ user_message: Dict[str, Any],
546
+ ) -> List[Dict[str, Any]]:
547
+ """
548
+ Build structured message list with proper interleaving.
549
+
550
+ Creates proper message flow:
551
+ 1. User message
552
+ 2. Assistant message (pre-tool) if tools were used
553
+ 3. Tool result messages (role="user")
554
+ 4. Assistant message (post-tool) if tools were used
555
+ OR
556
+ 1. User message
557
+ 2. Assistant message (complete) if no tools
558
+
559
+ Args:
560
+ execution_id: Execution ID
561
+ turn_number: Turn number in conversation
562
+ user_message: User message dict (already constructed)
563
+
564
+ Returns:
565
+ List of messages in proper order with message_ids assigned
566
+ """
567
+ messages = [user_message]
568
+
569
+ assistant_parts = self.get_assistant_message_parts()
570
+
571
+ if not self.has_any_tools:
572
+ # Simple case: no tools, single assistant message
573
+ if assistant_parts:
574
+ part = assistant_parts[0]
575
+ messages.append({
576
+ "role": "assistant",
577
+ "content": part["content"],
578
+ "timestamp": part["timestamp"],
579
+ "message_id": f"{execution_id}_assistant_{turn_number}",
580
+ })
581
+ else:
582
+ # Complex case: tools were used, split assistant messages
583
+ for i, part in enumerate(assistant_parts):
584
+ phase_suffix = f"_{part['phase']}" if part['phase'] in ['pre', 'post'] else ""
585
+ messages.append({
586
+ "role": "assistant",
587
+ "content": part["content"],
588
+ "timestamp": part["timestamp"],
589
+ "message_id": f"{execution_id}_assistant_{turn_number}{phase_suffix}",
590
+ "phase": part["phase"],
591
+ })
592
+
593
+ # After pre-tool message, insert tool result messages
594
+ if part["phase"] == "pre":
595
+ messages.extend(self.tool_messages)
596
+
597
+ # If we only have pre-tool content (no post), still add tool messages at end
598
+ if assistant_parts and assistant_parts[-1]["phase"] == "pre":
599
+ messages.extend(self.tool_messages)
600
+ # If we have no assistant parts but have tools, add tool messages
601
+ elif not assistant_parts and self.tool_messages:
602
+ messages.extend(self.tool_messages)
603
+
604
+ logger.info(
605
+ "structured_messages_built",
606
+ execution_id=execution_id[:8],
607
+ turn_number=turn_number,
608
+ total_messages=len(messages),
609
+ has_tools=self.has_any_tools,
610
+ assistant_parts=len(assistant_parts),
611
+ tool_messages=len(self.tool_messages)
612
+ )
613
+
614
+ return messages
615
+
616
+
617
+ def create_tool_hook(control_plane_client, execution_id: str):
618
+ """
619
+ Create a tool hook function for Agno Agent/Team.
620
+
621
+ This hook is called before and after each tool execution
622
+ to publish real-time updates to the Control Plane.
623
+
624
+ Args:
625
+ control_plane_client: Control Plane client instance
626
+ execution_id: Execution ID
627
+
628
+ Returns:
629
+ Hook function compatible with Agno tool_hooks
630
+ """
631
+ import time
632
+
633
+ def tool_hook(tool_name: str, tool_args: dict, result: Any = None, error: Exception = None):
634
+ """Tool hook for real-time updates"""
635
+ tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
636
+
637
+ if error is None and result is None:
638
+ # Tool starting
639
+ control_plane_client.publish_event(
640
+ execution_id=execution_id,
641
+ event_type="tool_started",
642
+ data={
643
+ "tool_name": tool_name,
644
+ "tool_execution_id": tool_execution_id,
645
+ "tool_arguments": tool_args,
646
+ "message": f"🔧 Starting: {tool_name}",
647
+ }
648
+ )
649
+ else:
650
+ # Tool completed
651
+ status = "failed" if error else "success"
652
+ control_plane_client.publish_event(
653
+ execution_id=execution_id,
654
+ event_type="tool_completed",
655
+ data={
656
+ "tool_name": tool_name,
657
+ "tool_execution_id": tool_execution_id,
658
+ "status": status,
659
+ "tool_output": str(result)[:1000] if result else None,
660
+ "tool_error": str(error) if error else None,
661
+ "message": f"{'✅' if status == 'success' else '❌'} {status}: {tool_name}",
662
+ }
663
+ )
664
+
665
+ return tool_hook