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,1747 @@
1
+ import httpx
2
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
3
+ from fastapi.responses import StreamingResponse
4
+ from sqlalchemy.orm import Session
5
+ from sqlalchemy.exc import IntegrityError
6
+ from typing import List, Union, Dict, Any, Optional
7
+ from datetime import datetime, timezone
8
+ from enum import Enum
9
+ import structlog
10
+ import uuid
11
+
12
+ from control_plane_api.app.database import get_db
13
+ from control_plane_api.app.models.team import Team, TeamStatus
14
+ from control_plane_api.app.models.agent import Agent
15
+ from control_plane_api.app.models.skill import Skill, SkillAssociation
16
+ from control_plane_api.app.models.project import Project
17
+ from control_plane_api.app.models.project_management import ProjectTeam
18
+ from control_plane_api.app.models.worker import WorkerQueue
19
+ from control_plane_api.app.models.execution import Execution
20
+ from control_plane_api.app.models.associations import ExecutionParticipant, TeamEnvironment
21
+ from control_plane_api.app.middleware.auth import get_current_organization
22
+ from sqlalchemy.orm import joinedload
23
+ from control_plane_api.app.lib.temporal_client import get_temporal_client
24
+ from control_plane_api.app.workflows.agent_execution import AgentExecutionWorkflow, TeamExecutionInput
25
+ from control_plane_api.app.workflows.team_execution import TeamExecutionWorkflow
26
+ from control_plane_api.app.routers.projects import get_default_project_id
27
+ from control_plane_api.app.routers.agents_v2 import ExecutionEnvironment
28
+ from control_plane_api.app.lib.mcp_validation import validate_execution_environment_mcp, MCPValidationError
29
+ from control_plane_api.app.observability import (
30
+ instrument_endpoint,
31
+ create_span_with_context,
32
+ add_span_event,
33
+ add_span_error,
34
+ )
35
+ from pydantic import BaseModel, Field, field_validator
36
+
37
+ logger = structlog.get_logger()
38
+
39
+ router = APIRouter()
40
+
41
+
42
+ def get_entity_skills(db: Session, organization_id: str, entity_type: str, entity_id: str) -> List[dict]:
43
+ """Get skills associated with an entity"""
44
+ # Get associations with joined skills
45
+ associations = db.query(SkillAssociation).options(
46
+ joinedload(SkillAssociation.skill)
47
+ ).filter(
48
+ SkillAssociation.organization_id == organization_id,
49
+ SkillAssociation.entity_type == entity_type,
50
+ SkillAssociation.entity_id == entity_id
51
+ ).all()
52
+
53
+ skills = []
54
+ for assoc in associations:
55
+ skill = assoc.skill
56
+ if skill and skill.enabled:
57
+ # Merge configuration with override
58
+ config = skill.configuration or {}
59
+ override = assoc.configuration_override
60
+ if override:
61
+ config = {**config, **override}
62
+
63
+ skills.append({
64
+ "id": str(skill.id),
65
+ "name": skill.name,
66
+ "type": skill.skill_type,
67
+ "description": skill.description,
68
+ "enabled": skill.enabled,
69
+ "configuration": config,
70
+ })
71
+
72
+ return skills
73
+
74
+
75
+ def get_team_projects(db: Session, team_id: str) -> list[dict]:
76
+ """Get all projects a team belongs to"""
77
+ try:
78
+ # Query project_teams join table with joined projects
79
+ project_teams = db.query(ProjectTeam).options(
80
+ joinedload(ProjectTeam.project)
81
+ ).filter(
82
+ ProjectTeam.team_id == team_id
83
+ ).all()
84
+
85
+ projects = []
86
+ for pt in project_teams:
87
+ if pt.project:
88
+ projects.append({
89
+ "id": str(pt.project.id),
90
+ "name": pt.project.name,
91
+ "key": pt.project.key,
92
+ "description": pt.project.description,
93
+ })
94
+
95
+ return projects
96
+ except Exception as e:
97
+ logger.warning("failed_to_fetch_team_projects", error=str(e), team_id=team_id)
98
+ return []
99
+
100
+
101
+ # Enhanced Pydantic schemas aligned with Agno Team capabilities
102
+
103
+ class ReasoningConfig(BaseModel):
104
+ """Reasoning configuration for the team"""
105
+ enabled: bool = Field(False, description="Enable reasoning for the team")
106
+ model: Optional[str] = Field(None, description="Model to use for reasoning")
107
+ agent_id: Optional[str] = Field(None, description="Agent ID to use for reasoning")
108
+ min_steps: Optional[int] = Field(1, description="Minimum reasoning steps", ge=1)
109
+ max_steps: Optional[int] = Field(10, description="Maximum reasoning steps", ge=1, le=100)
110
+
111
+
112
+ class LLMConfig(BaseModel):
113
+ """LLM configuration for the team"""
114
+ model: Optional[str] = Field(None, description="Default model for the team")
115
+ temperature: Optional[float] = Field(None, description="Temperature for generation", ge=0.0, le=2.0)
116
+ max_tokens: Optional[int] = Field(None, description="Maximum tokens to generate", ge=1)
117
+ top_p: Optional[float] = Field(None, description="Top-p sampling", ge=0.0, le=1.0)
118
+ top_k: Optional[int] = Field(None, description="Top-k sampling", ge=0)
119
+ stop: Optional[List[str]] = Field(None, description="Stop sequences")
120
+ frequency_penalty: Optional[float] = Field(None, description="Frequency penalty", ge=-2.0, le=2.0)
121
+ presence_penalty: Optional[float] = Field(None, description="Presence penalty", ge=-2.0, le=2.0)
122
+
123
+
124
+ class SessionConfig(BaseModel):
125
+ """Session configuration for the team"""
126
+ user_id: Optional[str] = Field(None, description="User ID for the session")
127
+ session_id: Optional[str] = Field(None, description="Session ID")
128
+ auto_save: bool = Field(True, description="Auto-save session state")
129
+ persist: bool = Field(True, description="Persist session across runs")
130
+
131
+
132
+ class TeamConfiguration(BaseModel):
133
+ """
134
+ Comprehensive team configuration aligned with Agno's Team capabilities.
135
+ This allows full control over team behavior, reasoning, tools, and LLM settings.
136
+ """
137
+ # Members
138
+ member_ids: List[str] = Field(default_factory=list, description="List of agent IDs in the team")
139
+
140
+ # Instructions
141
+ instructions: Union[str, List[str]] = Field(
142
+ default="",
143
+ description="Instructions for the team - can be a single string or list of instructions"
144
+ )
145
+
146
+ # Reasoning
147
+ reasoning: Optional[ReasoningConfig] = Field(None, description="Reasoning configuration")
148
+
149
+ # LLM Configuration
150
+ llm: Optional[LLMConfig] = Field(None, description="LLM configuration for the team")
151
+
152
+ # Tools & Knowledge
153
+ tools: List[Dict[str, Any]] = Field(
154
+ default_factory=list,
155
+ description="Tools available to the team - list of tool configurations"
156
+ )
157
+ knowledge_base: Optional[Dict[str, Any]] = Field(
158
+ None,
159
+ description="Knowledge base configuration (vector store, embeddings, etc.)"
160
+ )
161
+
162
+ # Session & State
163
+ session: Optional[SessionConfig] = Field(None, description="Session configuration")
164
+ dependencies: Dict[str, Any] = Field(
165
+ default_factory=dict,
166
+ description="External dependencies (databases, APIs, services)"
167
+ )
168
+
169
+ # Advanced Options
170
+ markdown: bool = Field(True, description="Enable markdown formatting in responses")
171
+ add_datetime_to_instructions: bool = Field(
172
+ False,
173
+ description="Automatically add current datetime to instructions"
174
+ )
175
+ structured_outputs: bool = Field(False, description="Enable structured outputs")
176
+ response_model: Optional[str] = Field(None, description="Response model schema name")
177
+
178
+ # Monitoring & Debugging
179
+ debug_mode: bool = Field(False, description="Enable debug mode with verbose logging")
180
+ monitoring: bool = Field(False, description="Enable monitoring and telemetry")
181
+
182
+ # Custom Metadata
183
+ metadata: Dict[str, Any] = Field(
184
+ default_factory=dict,
185
+ description="Additional custom metadata for the team"
186
+ )
187
+
188
+
189
+ class TeamCreate(BaseModel):
190
+ """Create a new team with full Agno capabilities"""
191
+ name: str = Field(..., description="Team name", min_length=1, max_length=255)
192
+ description: Optional[str] = Field(None, description="Team description")
193
+ runtime: Optional[str] = Field(
194
+ "default",
195
+ description="Runtime type for team leader: 'default' (Agno) or 'claude_code' (Claude Code SDK). Default: 'default'"
196
+ )
197
+ configuration: TeamConfiguration = Field(
198
+ default_factory=TeamConfiguration,
199
+ description="Team configuration aligned with Agno Team"
200
+ )
201
+ skill_ids: list[str] = Field(default_factory=list, description="Tool set IDs to associate with this team")
202
+ skill_configurations: dict[str, dict] = Field(default_factory=dict, description="Tool set configurations keyed by skill ID")
203
+ execution_environment: ExecutionEnvironment | None = Field(None, description="Execution environment: env vars, secrets, integrations")
204
+
205
+ @field_validator('runtime')
206
+ @classmethod
207
+ def validate_runtime(cls, v: Optional[str]) -> Optional[str]:
208
+ """Validate runtime is a valid value"""
209
+ if v is not None and v not in ["default", "claude_code"]:
210
+ raise ValueError(f"Invalid runtime type '{v}'. Must be 'default' or 'claude_code'.")
211
+ return v
212
+
213
+
214
+ class TeamUpdate(BaseModel):
215
+ """Update an existing team"""
216
+ name: Optional[str] = Field(None, description="Team name", min_length=1, max_length=255)
217
+ description: Optional[str] = Field(None, description="Team description")
218
+ status: Optional[TeamStatus] = Field(None, description="Team status")
219
+ runtime: Optional[str] = Field(None, description="Runtime type: 'default' (Agno) or 'claude_code' (Claude Code SDK)")
220
+ configuration: Optional[TeamConfiguration] = Field(None, description="Team configuration")
221
+ skill_ids: list[str] | None = None
222
+ skill_configurations: dict[str, dict] | None = None
223
+ environment_ids: list[str] | None = None
224
+ execution_environment: ExecutionEnvironment | None = None
225
+
226
+ @field_validator('runtime')
227
+ @classmethod
228
+ def validate_runtime(cls, v: Optional[str]) -> Optional[str]:
229
+ """Validate runtime is a valid value"""
230
+ if v is not None and v not in ["default", "claude_code"]:
231
+ raise ValueError(f"Invalid runtime type '{v}'. Must be 'default' or 'claude_code'.")
232
+ return v
233
+
234
+
235
+ class TeamResponse(BaseModel):
236
+ """Team response with structured configuration"""
237
+ id: str
238
+ organization_id: str
239
+ name: str
240
+ description: Optional[str]
241
+ status: TeamStatus
242
+ runtime: str = Field(
243
+ default="default",
244
+ description="Runtime type for team leader: 'default' (Agno) or 'claude_code' (Claude Code SDK)"
245
+ )
246
+ configuration: TeamConfiguration
247
+ created_at: datetime
248
+ updated_at: datetime
249
+ projects: List[dict] = Field(default_factory=list, description="Projects this team belongs to")
250
+ skill_ids: Optional[List[str]] = Field(default_factory=list, description="IDs of associated skills")
251
+ skills: Optional[List[dict]] = Field(default_factory=list, description="Associated skills with details")
252
+ execution_environment: ExecutionEnvironment | None = None
253
+
254
+ class Config:
255
+ from_attributes = True
256
+
257
+
258
+ class TeamWithAgentsResponse(TeamResponse):
259
+ """Team response including member agents"""
260
+ agents: List[dict]
261
+
262
+
263
+ class TeamExecutionRequest(BaseModel):
264
+ prompt: str = Field(..., description="The prompt/task to execute")
265
+ system_prompt: str | None = Field(None, description="Optional system prompt for team coordination")
266
+ stream: bool = Field(False, description="Whether to stream the response")
267
+ worker_queue_id: str = Field(..., description="Worker queue ID (UUID) to route execution to - REQUIRED")
268
+ user_metadata: dict | None = Field(None, description="User attribution metadata (optional, auto-filled from token)")
269
+ execution_environment: ExecutionEnvironment | None = Field(None, description="Optional execution environment overrides (working_dir, env_vars, etc.)")
270
+
271
+
272
+ class TeamExecutionResponse(BaseModel):
273
+ execution_id: str
274
+ workflow_id: str
275
+ status: str
276
+ message: str
277
+
278
+
279
+ @router.post("", response_model=TeamResponse, status_code=status.HTTP_201_CREATED)
280
+ @instrument_endpoint("teams.create_team")
281
+ def create_team(
282
+ team_data: TeamCreate,
283
+ request: Request,
284
+ db: Session = Depends(get_db),
285
+ organization: dict = Depends(get_current_organization),
286
+ ):
287
+ """
288
+ Create a new team with full Agno capabilities.
289
+
290
+ Supports comprehensive configuration including:
291
+ - Member agents
292
+ - Instructions and reasoning
293
+ - Tools and knowledge bases
294
+ - LLM settings
295
+ - Session management
296
+ """
297
+ try:
298
+ logger.info(
299
+ "create_team_request",
300
+ team_name=team_data.name,
301
+ org_id=organization["id"],
302
+ org_name=organization.get("name"),
303
+ member_count=len(team_data.configuration.member_ids) if team_data.configuration.member_ids else 0,
304
+ skill_count=len(team_data.skill_ids) if team_data.skill_ids else 0,
305
+ )
306
+
307
+ # Check if team name already exists in this organization
308
+ existing_team = db.query(Team).filter(
309
+ Team.name == team_data.name,
310
+ Team.organization_id == organization["id"]
311
+ ).first()
312
+ if existing_team:
313
+ logger.warning(
314
+ "team_name_already_exists",
315
+ team_name=team_data.name,
316
+ org_id=organization["id"],
317
+ )
318
+ raise HTTPException(status_code=400, detail="Team with this name already exists in your organization")
319
+
320
+ # Validate member_ids if provided
321
+ if team_data.configuration.member_ids:
322
+ logger.info(
323
+ "validating_team_members",
324
+ member_ids=team_data.configuration.member_ids,
325
+ org_id=organization["id"],
326
+ )
327
+ for agent_id in team_data.configuration.member_ids:
328
+ try:
329
+ # Query database for agent validation
330
+ agent = db.query(Agent).filter(
331
+ Agent.id == agent_id,
332
+ Agent.organization_id == organization["id"]
333
+ ).first()
334
+
335
+ logger.debug(
336
+ "agent_validation_result",
337
+ agent_id=agent_id,
338
+ found=agent is not None,
339
+ )
340
+
341
+ if not agent:
342
+ logger.warning(
343
+ "agent_not_found",
344
+ agent_id=agent_id,
345
+ org_id=organization["id"],
346
+ )
347
+ raise HTTPException(
348
+ status_code=400,
349
+ detail=f"Agent with ID '{agent_id}' not found. Please create the agent first."
350
+ )
351
+ except HTTPException:
352
+ raise
353
+ except Exception as e:
354
+ logger.error(
355
+ "agent_validation_failed",
356
+ agent_id=agent_id,
357
+ error=str(e),
358
+ error_type=type(e).__name__,
359
+ org_id=organization["id"],
360
+ )
361
+ raise HTTPException(
362
+ status_code=500,
363
+ detail=f"Failed to validate agent '{agent_id}': {str(e)}"
364
+ )
365
+
366
+ # Validate runtime compatibility: Claude Code teams require all members to be Claude Code
367
+ if team_data.runtime == "claude_code" and team_data.configuration.member_ids:
368
+ logger.info(
369
+ "validating_claude_code_team_runtime",
370
+ team_name=team_data.name,
371
+ member_count=len(team_data.configuration.member_ids),
372
+ )
373
+
374
+ non_claude_code_members = []
375
+
376
+ for agent_id in team_data.configuration.member_ids:
377
+ try:
378
+ # Fetch agent runtime
379
+ agent = db.query(Agent).filter(
380
+ Agent.id == agent_id,
381
+ Agent.organization_id == organization["id"]
382
+ ).first()
383
+
384
+ if agent:
385
+ agent_runtime = agent.runtime or "default"
386
+ agent_name = agent.name
387
+
388
+ if agent_runtime != "claude_code":
389
+ non_claude_code_members.append({
390
+ "id": str(agent_id),
391
+ "name": agent_name,
392
+ "runtime": agent_runtime
393
+ })
394
+ logger.warning(
395
+ "member_runtime_mismatch",
396
+ agent_id=str(agent_id),
397
+ agent_name=agent_name,
398
+ agent_runtime=agent_runtime,
399
+ team_runtime="claude_code",
400
+ )
401
+ except Exception as e:
402
+ logger.error(
403
+ "runtime_validation_failed",
404
+ agent_id=agent_id,
405
+ error=str(e),
406
+ )
407
+ # Continue checking other members
408
+ continue
409
+
410
+ if non_claude_code_members:
411
+ member_details = ", ".join([
412
+ f"{m['name']} (runtime: {m['runtime']})"
413
+ for m in non_claude_code_members
414
+ ])
415
+ error_msg = (
416
+ f"Cannot create Claude Code team with non-Claude Code members. "
417
+ f"The following members must use 'claude_code' runtime: {member_details}. "
418
+ f"Either change the team runtime to 'default' or update all member agents to use 'claude_code' runtime."
419
+ )
420
+ logger.warning(
421
+ "claude_code_team_validation_failed",
422
+ team_name=team_data.name,
423
+ non_claude_code_count=len(non_claude_code_members),
424
+ non_claude_code_members=non_claude_code_members,
425
+ )
426
+ raise HTTPException(
427
+ status_code=400,
428
+ detail=error_msg
429
+ )
430
+
431
+ logger.info(
432
+ "claude_code_team_validation_passed",
433
+ team_name=team_data.name,
434
+ all_members_claude_code=True,
435
+ )
436
+
437
+ # Validate MCP server configuration if present
438
+ if team_data.execution_environment and team_data.execution_environment.mcp_servers:
439
+ try:
440
+ mcp_validation = validate_execution_environment_mcp(
441
+ team_data.execution_environment.model_dump(),
442
+ strict=False
443
+ )
444
+
445
+ if not mcp_validation["valid"]:
446
+ error_msg = "MCP configuration validation failed:\n" + "\n".join(
447
+ f" - {err}" for err in mcp_validation["errors"]
448
+ )
449
+ logger.error(
450
+ "mcp_validation_failed",
451
+ team_name=team_data.name,
452
+ errors=mcp_validation["errors"],
453
+ )
454
+ raise HTTPException(status_code=400, detail=error_msg)
455
+
456
+ if mcp_validation["warnings"]:
457
+ logger.warning(
458
+ "mcp_validation_warnings",
459
+ team_name=team_data.name,
460
+ warnings=mcp_validation["warnings"],
461
+ required_secrets=mcp_validation.get("required_secrets", []),
462
+ required_env_vars=mcp_validation.get("required_env_vars", []),
463
+ )
464
+
465
+ logger.info(
466
+ "mcp_validation_passed",
467
+ team_name=team_data.name,
468
+ server_count=len(team_data.execution_environment.mcp_servers),
469
+ required_secrets=mcp_validation.get("required_secrets", []),
470
+ required_env_vars=mcp_validation.get("required_env_vars", []),
471
+ )
472
+ except MCPValidationError as e:
473
+ logger.error(
474
+ "mcp_validation_error",
475
+ team_name=team_data.name,
476
+ error=str(e),
477
+ )
478
+ raise HTTPException(status_code=400, detail=str(e))
479
+
480
+ # Convert TeamConfiguration to dict for JSON storage
481
+ configuration_dict = team_data.configuration.model_dump(exclude_none=True)
482
+
483
+ team = Team(
484
+ organization_id=organization["id"],
485
+ name=team_data.name,
486
+ description=team_data.description,
487
+ runtime=team_data.runtime or "default", # Set runtime for team leader
488
+ configuration=configuration_dict,
489
+ skill_ids=team_data.skill_ids,
490
+ execution_environment=team_data.execution_environment.model_dump() if team_data.execution_environment else {},
491
+ )
492
+ db.add(team)
493
+ db.commit()
494
+ db.refresh(team)
495
+
496
+ logger.info(
497
+ "team_created",
498
+ team_id=str(team.id),
499
+ team_name=team.name,
500
+ org_id=organization["id"],
501
+ )
502
+ except HTTPException:
503
+ raise
504
+ except IntegrityError as e:
505
+ db.rollback()
506
+ logger.error(
507
+ "database_integrity_error",
508
+ error=str(e),
509
+ team_name=team_data.name,
510
+ org_id=organization["id"],
511
+ )
512
+ raise HTTPException(
513
+ status_code=400,
514
+ detail=f"Database constraint violation: {str(e.orig) if hasattr(e, 'orig') else str(e)}"
515
+ )
516
+ except Exception as e:
517
+ db.rollback()
518
+ logger.error(
519
+ "team_creation_failed",
520
+ error=str(e),
521
+ error_type=type(e).__name__,
522
+ team_name=team_data.name,
523
+ org_id=organization["id"],
524
+ )
525
+ raise HTTPException(
526
+ status_code=500,
527
+ detail=f"Failed to create team: {str(e)}"
528
+ )
529
+
530
+ # Sync agent.team_id relationship for initial members
531
+ if team_data.configuration.member_ids:
532
+ for agent_id in team_data.configuration.member_ids:
533
+ try:
534
+ agent = db.query(Agent).filter(Agent.id == agent_id).first()
535
+ if agent:
536
+ agent.team_id = team.id
537
+ db.commit()
538
+ except Exception as e:
539
+ logger.warning(
540
+ "failed_to_sync_agent_team_id",
541
+ error=str(e),
542
+ agent_id=agent_id,
543
+ team_id=str(team.id)
544
+ )
545
+ db.rollback()
546
+
547
+ # Automatically assign team to the default project
548
+ default_project_id = get_default_project_id(db, organization)
549
+ if default_project_id:
550
+ try:
551
+ project_team = ProjectTeam(
552
+ project_id=default_project_id,
553
+ team_id=team.id,
554
+ role=None,
555
+ added_by=organization.get("user_id"),
556
+ )
557
+ db.add(project_team)
558
+ db.commit()
559
+ logger.info(
560
+ "team_added_to_default_project",
561
+ team_id=str(team.id),
562
+ project_id=default_project_id,
563
+ org_id=organization["id"]
564
+ )
565
+ except Exception as e:
566
+ logger.warning(
567
+ "failed_to_add_team_to_default_project",
568
+ error=str(e),
569
+ team_id=str(team.id),
570
+ org_id=organization["id"]
571
+ )
572
+ db.rollback()
573
+
574
+ # Create skill associations if skills were provided
575
+ if team_data.skill_ids:
576
+ try:
577
+ for skill_id in team_data.skill_ids:
578
+ config_override = team_data.skill_configurations.get(skill_id, {})
579
+
580
+ skill_assoc = SkillAssociation(
581
+ organization_id=organization["id"],
582
+ skill_id=skill_id,
583
+ entity_type="team",
584
+ entity_id=team.id,
585
+ configuration_override=config_override,
586
+ )
587
+ db.add(skill_assoc)
588
+
589
+ db.commit()
590
+ logger.info(
591
+ "team_skills_associated",
592
+ team_id=str(team.id),
593
+ skill_count=len(team_data.skill_ids),
594
+ org_id=organization["id"]
595
+ )
596
+ except Exception as e:
597
+ logger.warning(
598
+ "failed_to_associate_team_skills",
599
+ error=str(e),
600
+ team_id=str(team.id),
601
+ org_id=organization["id"]
602
+ )
603
+ db.rollback()
604
+
605
+ # Parse configuration back to TeamConfiguration for response
606
+ response_team = TeamResponse(
607
+ id=str(team.id),
608
+ organization_id=team.organization_id,
609
+ name=team.name,
610
+ description=team.description,
611
+ status=team.status,
612
+ runtime=team.runtime.value if team.runtime else "default", # Include runtime in response
613
+ configuration=TeamConfiguration(**team.configuration),
614
+ created_at=team.created_at,
615
+ updated_at=team.updated_at,
616
+ projects=get_team_projects(db, str(team.id)),
617
+ skill_ids=team.skill_ids or [], # Include skill_ids in response
618
+ skills=[], # Skills will be loaded separately if needed
619
+ )
620
+ return response_team
621
+
622
+
623
+ @router.get("", response_model=List[TeamWithAgentsResponse])
624
+ @instrument_endpoint("teams.list_teams")
625
+ def list_teams(
626
+ skip: int = 0,
627
+ limit: int = 100,
628
+ status_filter: Optional[TeamStatus] = None,
629
+ db: Session = Depends(get_db),
630
+ organization: dict = Depends(get_current_organization),
631
+ ):
632
+ """
633
+ List all teams with their configurations and member agents.
634
+
635
+ Supports filtering by status and pagination.
636
+ Only returns teams belonging to the current organization.
637
+ """
638
+ try:
639
+ query = db.query(Team).filter(Team.organization_id == organization["id"])
640
+ if status_filter:
641
+ query = query.filter(Team.status == status_filter)
642
+ teams = query.offset(skip).limit(limit).all()
643
+
644
+ if not teams:
645
+ return []
646
+
647
+ team_ids = [team.id for team in teams]
648
+
649
+ # BATCH 1: Fetch all projects for all teams in one query
650
+ try:
651
+ project_teams = db.query(ProjectTeam).options(
652
+ joinedload(ProjectTeam.project)
653
+ ).filter(
654
+ ProjectTeam.team_id.in_(team_ids)
655
+ ).all()
656
+ except Exception as project_error:
657
+ logger.error("failed_to_fetch_projects", error=str(project_error), org_id=organization["id"])
658
+ project_teams = []
659
+
660
+ # Group projects by team_id
661
+ projects_by_team = {}
662
+ for pt in project_teams:
663
+ team_id_str = str(pt.team_id)
664
+ if pt.project:
665
+ if team_id_str not in projects_by_team:
666
+ projects_by_team[team_id_str] = []
667
+ projects_by_team[team_id_str].append({
668
+ "id": str(pt.project.id),
669
+ "name": pt.project.name,
670
+ "key": pt.project.key,
671
+ "description": pt.project.description,
672
+ })
673
+
674
+ # BATCH 2: Fetch all skill associations for all teams in one query
675
+ try:
676
+ skill_associations = db.query(SkillAssociation).options(
677
+ joinedload(SkillAssociation.skill)
678
+ ).filter(
679
+ SkillAssociation.organization_id == organization["id"],
680
+ SkillAssociation.entity_type == "team",
681
+ SkillAssociation.entity_id.in_(team_ids)
682
+ ).all()
683
+ except Exception as skill_error:
684
+ logger.error("failed_to_fetch_skills", error=str(skill_error), org_id=organization["id"])
685
+ skill_associations = []
686
+
687
+ # Group skills by team_id
688
+ skills_by_team = {}
689
+ for assoc in skill_associations:
690
+ team_id_str = str(assoc.entity_id)
691
+ skill = assoc.skill
692
+ if skill and skill.enabled:
693
+ if team_id_str not in skills_by_team:
694
+ skills_by_team[team_id_str] = []
695
+
696
+ # Merge configuration with override
697
+ config = skill.configuration or {}
698
+ override = assoc.configuration_override
699
+ if override:
700
+ config = {**config, **override}
701
+
702
+ skills_by_team[team_id_str].append({
703
+ "id": str(skill.id),
704
+ "name": skill.name,
705
+ "type": skill.skill_type,
706
+ "description": skill.description,
707
+ "enabled": skill.enabled,
708
+ "configuration": config,
709
+ })
710
+
711
+ # BATCH 3: Collect all unique agent IDs from all teams
712
+ all_agent_ids = set()
713
+ for team in teams:
714
+ try:
715
+ team_config = TeamConfiguration(**(team.configuration or {}))
716
+ if team_config.member_ids:
717
+ all_agent_ids.update(team_config.member_ids)
718
+ except Exception as config_error:
719
+ logger.warning("failed_to_parse_team_config", error=str(config_error), team_id=str(team.id))
720
+
721
+ # Fetch all agents in one query
722
+ agents_by_id = {}
723
+ if all_agent_ids:
724
+ try:
725
+ db_agents = db.query(Agent).filter(Agent.id.in_(list(all_agent_ids))).all()
726
+ agents_by_id = {
727
+ str(agent.id): {
728
+ "id": str(agent.id),
729
+ "name": agent.name,
730
+ "status": agent.status,
731
+ "capabilities": agent.capabilities,
732
+ "description": agent.description,
733
+ }
734
+ for agent in db_agents
735
+ }
736
+ except Exception as agent_error:
737
+ logger.error("failed_to_fetch_agents", error=str(agent_error), org_id=organization["id"])
738
+
739
+ # Build response for each team
740
+ result = []
741
+ for team in teams:
742
+ try:
743
+ team_id = str(team.id)
744
+ team_config = TeamConfiguration(**(team.configuration or {}))
745
+
746
+ # Get agents for this team from the batched data
747
+ agents = []
748
+ if team_config.member_ids:
749
+ agents = [agents_by_id[agent_id] for agent_id in team_config.member_ids if agent_id in agents_by_id]
750
+
751
+ # Get skills from batched data
752
+ skills = skills_by_team.get(team_id, [])
753
+ skill_ids = [ts["id"] for ts in skills]
754
+
755
+ result.append(TeamWithAgentsResponse(
756
+ id=team_id,
757
+ organization_id=team.organization_id,
758
+ name=team.name,
759
+ description=team.description,
760
+ status=team.status,
761
+ runtime=team.runtime.value if team.runtime else "default", # Include runtime in response
762
+ configuration=team_config,
763
+ created_at=team.created_at,
764
+ updated_at=team.updated_at,
765
+ projects=projects_by_team.get(team_id, []),
766
+ agents=agents,
767
+ skill_ids=skill_ids,
768
+ skills=skills,
769
+ ))
770
+ except Exception as team_error:
771
+ logger.error("failed_to_build_team_response", error=str(team_error), team_id=str(team.id))
772
+ # Skip this team and continue with others
773
+
774
+ logger.info(
775
+ "teams_listed_successfully",
776
+ count=len(result),
777
+ org_id=organization["id"],
778
+ )
779
+
780
+ return result
781
+
782
+ except Exception as e:
783
+ logger.error(
784
+ "teams_list_failed",
785
+ error=str(e),
786
+ error_type=type(e).__name__,
787
+ org_id=organization["id"]
788
+ )
789
+ raise HTTPException(
790
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
791
+ detail=f"Failed to list teams: {str(e)}"
792
+ )
793
+
794
+
795
+ @router.get("/{team_id}", response_model=TeamWithAgentsResponse)
796
+ @instrument_endpoint("teams.get_team")
797
+ def get_team(
798
+ team_id: str,
799
+ db: Session = Depends(get_db),
800
+ organization: dict = Depends(get_current_organization),
801
+ ):
802
+ """
803
+ Get a specific team by ID with full configuration and member agents.
804
+
805
+ Returns the team with structured configuration and list of member agents.
806
+ Only returns teams belonging to the current organization.
807
+ """
808
+ team = db.query(Team).filter(
809
+ Team.id == team_id,
810
+ Team.organization_id == organization["id"]
811
+ ).first()
812
+ if not team:
813
+ raise HTTPException(status_code=404, detail="Team not found")
814
+
815
+ # Parse configuration
816
+ team_config = TeamConfiguration(**(team.configuration or {}))
817
+
818
+ # Get agents from configuration.member_ids (source of truth)
819
+ # instead of team.agents relationship to avoid ghost agents
820
+ member_ids = team_config.member_ids
821
+ agents = []
822
+ if member_ids:
823
+ # Query agents that actually exist in the database
824
+ db_agents = db.query(Agent).filter(Agent.id.in_(member_ids)).all()
825
+ agents = [
826
+ {
827
+ "id": str(agent.id),
828
+ "name": agent.name,
829
+ "status": agent.status,
830
+ "capabilities": agent.capabilities,
831
+ "description": agent.description,
832
+ }
833
+ for agent in db_agents
834
+ ]
835
+
836
+ # Get skills for this team
837
+ skills = get_entity_skills(db, organization["id"], "team", team_id)
838
+ skill_ids = [ts["id"] for ts in skills]
839
+
840
+ # Include agents in response
841
+ return TeamWithAgentsResponse(
842
+ id=str(team.id),
843
+ organization_id=team.organization_id,
844
+ name=team.name,
845
+ description=team.description,
846
+ status=team.status,
847
+ runtime=team.runtime.value if team.runtime else "default", # Include runtime in response
848
+ configuration=team_config,
849
+ created_at=team.created_at,
850
+ updated_at=team.updated_at,
851
+ projects=get_team_projects(db, team_id),
852
+ agents=agents,
853
+ skill_ids=skill_ids,
854
+ skills=skills,
855
+ )
856
+
857
+
858
+ @router.patch("/{team_id}", response_model=TeamResponse)
859
+ @instrument_endpoint("teams.update_team")
860
+ def update_team(
861
+ team_id: str,
862
+ team_data: TeamUpdate,
863
+ db: Session = Depends(get_db),
864
+ organization: dict = Depends(get_current_organization),
865
+ ):
866
+ """
867
+ Update a team's configuration, name, description, or status.
868
+
869
+ Supports partial updates - only provided fields are updated.
870
+ Validates member_ids if configuration is being updated.
871
+ Only allows updating teams belonging to the current organization.
872
+ """
873
+ team = db.query(Team).filter(
874
+ Team.id == team_id,
875
+ Team.organization_id == organization["id"]
876
+ ).first()
877
+ if not team:
878
+ raise HTTPException(status_code=404, detail="Team not found")
879
+
880
+ update_data = team_data.model_dump(exclude_unset=True)
881
+
882
+ # Extract skill data before processing
883
+ skill_ids = update_data.pop("skill_ids", None)
884
+ skill_configurations = update_data.pop("skill_configurations", None)
885
+
886
+ # Extract environment data before processing (many-to-many via junction table)
887
+ environment_ids = update_data.pop("environment_ids", None)
888
+
889
+ # Handle execution_environment - convert to dict if present
890
+ if "execution_environment" in update_data and update_data["execution_environment"]:
891
+ if isinstance(update_data["execution_environment"], ExecutionEnvironment):
892
+ update_data["execution_environment"] = update_data["execution_environment"].model_dump()
893
+ # If None, keep as None to preserve existing value
894
+
895
+ logger.info(
896
+ "team_update_request",
897
+ team_id=team_id,
898
+ has_skill_ids=skill_ids is not None,
899
+ skill_count=len(skill_ids) if skill_ids else 0,
900
+ skill_ids=skill_ids,
901
+ )
902
+
903
+ # Check if name is being updated and if it already exists
904
+ if "name" in update_data and update_data["name"] != team.name:
905
+ existing_team = db.query(Team).filter(Team.name == update_data["name"]).first()
906
+ if existing_team:
907
+ raise HTTPException(status_code=400, detail="Team with this name already exists")
908
+
909
+ # Handle configuration update specially
910
+ if "configuration" in update_data and update_data["configuration"]:
911
+ new_config = update_data["configuration"]
912
+
913
+ # new_config is already a dict from model_dump(exclude_unset=True)
914
+ # Validate member_ids if provided and sync the agent.team_id relationship
915
+ if isinstance(new_config, dict) and 'member_ids' in new_config:
916
+ new_member_ids = set(new_config.get('member_ids', []))
917
+
918
+ # Validate all agent IDs exist
919
+ for agent_id in new_member_ids:
920
+ agent = db.query(Agent).filter(
921
+ Agent.id == agent_id,
922
+ Agent.organization_id == organization["id"]
923
+ ).first()
924
+ if not agent:
925
+ raise HTTPException(
926
+ status_code=400,
927
+ detail=f"Agent with ID '{agent_id}' not found. Please create the agent first."
928
+ )
929
+
930
+ # Sync the agent.team_id relationship
931
+ # Get current team members from configuration
932
+ current_config = TeamConfiguration(**(team.configuration or {}))
933
+ current_member_ids = set(current_config.member_ids or [])
934
+
935
+ # Remove agents that are no longer in the team
936
+ agents_to_remove = current_member_ids - new_member_ids
937
+ for agent_id in agents_to_remove:
938
+ try:
939
+ agent = db.query(Agent).filter(Agent.id == agent_id).first()
940
+ if agent:
941
+ agent.team_id = None
942
+ db.commit()
943
+ except Exception as e:
944
+ logger.warning("failed_to_remove_agent_from_team", error=str(e), agent_id=agent_id)
945
+ db.rollback()
946
+
947
+ # Add agents that are newly added to the team
948
+ agents_to_add = new_member_ids - current_member_ids
949
+ for agent_id in agents_to_add:
950
+ try:
951
+ agent = db.query(Agent).filter(Agent.id == agent_id).first()
952
+ if agent:
953
+ agent.team_id = team_id
954
+ db.commit()
955
+ except Exception as e:
956
+ logger.warning("failed_to_add_agent_to_team", error=str(e), agent_id=agent_id)
957
+ db.rollback()
958
+
959
+ # new_config is already a dict, just assign it
960
+ team.configuration = new_config
961
+ del update_data["configuration"]
962
+
963
+ # Validate runtime compatibility when updating runtime or members
964
+ # Check if runtime is being set to claude_code
965
+ final_runtime = update_data.get("runtime", team.runtime)
966
+
967
+ # Get final member list (either from update or existing)
968
+ if "configuration" in locals() and isinstance(new_config, dict) and 'member_ids' in new_config:
969
+ final_member_ids = new_config.get('member_ids', [])
970
+ else:
971
+ current_config = TeamConfiguration(**(team.configuration or {}))
972
+ final_member_ids = current_config.member_ids or []
973
+
974
+ # Validate Claude Code team runtime requirements
975
+ if final_runtime == "claude_code" and final_member_ids:
976
+ logger.info(
977
+ "validating_claude_code_team_runtime_on_update",
978
+ team_id=team_id,
979
+ member_count=len(final_member_ids),
980
+ )
981
+
982
+ non_claude_code_members = []
983
+
984
+ for agent_id in final_member_ids:
985
+ try:
986
+ agent = db.query(Agent).filter(
987
+ Agent.id == agent_id,
988
+ Agent.organization_id == organization["id"]
989
+ ).first()
990
+
991
+ if agent:
992
+ agent_runtime = agent.runtime or "default"
993
+ agent_name = agent.name
994
+
995
+ if agent_runtime != "claude_code":
996
+ non_claude_code_members.append({
997
+ "id": str(agent_id),
998
+ "name": agent_name,
999
+ "runtime": agent_runtime
1000
+ })
1001
+ logger.warning(
1002
+ "member_runtime_mismatch_on_update",
1003
+ agent_id=str(agent_id),
1004
+ agent_name=agent_name,
1005
+ agent_runtime=agent_runtime,
1006
+ team_runtime="claude_code",
1007
+ )
1008
+ except Exception as e:
1009
+ logger.error(
1010
+ "runtime_validation_failed_on_update",
1011
+ agent_id=agent_id,
1012
+ error=str(e),
1013
+ )
1014
+ continue
1015
+
1016
+ if non_claude_code_members:
1017
+ member_details = ", ".join([
1018
+ f"{m['name']} (runtime: {m['runtime']})"
1019
+ for m in non_claude_code_members
1020
+ ])
1021
+ error_msg = (
1022
+ f"Cannot update to Claude Code team with non-Claude Code members. "
1023
+ f"The following members must use 'claude_code' runtime: {member_details}. "
1024
+ f"Either keep the team runtime as 'default' or update all member agents to use 'claude_code' runtime."
1025
+ )
1026
+ logger.warning(
1027
+ "claude_code_team_update_validation_failed",
1028
+ team_id=team_id,
1029
+ non_claude_code_count=len(non_claude_code_members),
1030
+ )
1031
+ raise HTTPException(
1032
+ status_code=400,
1033
+ detail=error_msg
1034
+ )
1035
+
1036
+ logger.info(
1037
+ "claude_code_team_update_validation_passed",
1038
+ team_id=team_id,
1039
+ all_members_claude_code=True,
1040
+ )
1041
+
1042
+ # Validate MCP server configuration if being updated
1043
+ if "execution_environment" in update_data and update_data["execution_environment"]:
1044
+ exec_env_dict = update_data["execution_environment"]
1045
+ if exec_env_dict and exec_env_dict.get("mcp_servers"):
1046
+ try:
1047
+ mcp_validation = validate_execution_environment_mcp(
1048
+ exec_env_dict,
1049
+ strict=False
1050
+ )
1051
+
1052
+ if not mcp_validation["valid"]:
1053
+ error_msg = "MCP configuration validation failed:\n" + "\n".join(
1054
+ f" - {err}" for err in mcp_validation["errors"]
1055
+ )
1056
+ logger.error(
1057
+ "mcp_validation_failed",
1058
+ team_id=team_id,
1059
+ errors=mcp_validation["errors"],
1060
+ )
1061
+ raise HTTPException(status_code=400, detail=error_msg)
1062
+
1063
+ if mcp_validation["warnings"]:
1064
+ logger.warning(
1065
+ "mcp_validation_warnings",
1066
+ team_id=team_id,
1067
+ warnings=mcp_validation["warnings"],
1068
+ required_secrets=mcp_validation.get("required_secrets", []),
1069
+ required_env_vars=mcp_validation.get("required_env_vars", []),
1070
+ )
1071
+
1072
+ logger.info(
1073
+ "mcp_validation_passed",
1074
+ team_id=team_id,
1075
+ server_count=len(exec_env_dict.get("mcp_servers", {})),
1076
+ required_secrets=mcp_validation.get("required_secrets", []),
1077
+ required_env_vars=mcp_validation.get("required_env_vars", []),
1078
+ )
1079
+ except MCPValidationError as e:
1080
+ logger.error(
1081
+ "mcp_validation_error",
1082
+ team_id=team_id,
1083
+ error=str(e),
1084
+ )
1085
+ raise HTTPException(status_code=400, detail=str(e))
1086
+
1087
+ # Update other fields
1088
+ for field, value in update_data.items():
1089
+ if hasattr(team, field):
1090
+ setattr(team, field, value)
1091
+
1092
+ # Update skill_ids if provided
1093
+ if skill_ids is not None:
1094
+ team.skill_ids = skill_ids
1095
+
1096
+ team.updated_at = datetime.utcnow()
1097
+ db.commit()
1098
+ db.refresh(team)
1099
+
1100
+ # Update skill associations if skill_ids was provided
1101
+ if skill_ids is not None:
1102
+ try:
1103
+ # Delete existing associations
1104
+ db.query(SkillAssociation).filter(
1105
+ SkillAssociation.entity_type == "team",
1106
+ SkillAssociation.entity_id == team_id
1107
+ ).delete()
1108
+
1109
+ # Create new associations
1110
+ for skill_id in skill_ids:
1111
+ config_override = (skill_configurations or {}).get(skill_id, {})
1112
+
1113
+ skill_assoc = SkillAssociation(
1114
+ organization_id=organization["id"],
1115
+ skill_id=skill_id,
1116
+ entity_type="team",
1117
+ entity_id=team_id,
1118
+ configuration_override=config_override,
1119
+ )
1120
+ db.add(skill_assoc)
1121
+
1122
+ db.commit()
1123
+ logger.info(
1124
+ "team_skills_updated",
1125
+ team_id=team_id,
1126
+ skill_count=len(skill_ids),
1127
+ org_id=organization["id"]
1128
+ )
1129
+ except Exception as e:
1130
+ db.rollback()
1131
+ logger.warning(
1132
+ "failed_to_update_team_skills",
1133
+ error=str(e),
1134
+ team_id=team_id,
1135
+ org_id=organization["id"]
1136
+ )
1137
+
1138
+ # Update environment associations if environment_ids was provided
1139
+ if environment_ids is not None:
1140
+ try:
1141
+ # Delete existing environment associations
1142
+ db.query(TeamEnvironment).filter(
1143
+ TeamEnvironment.team_id == team_id
1144
+ ).delete()
1145
+
1146
+ # Create new environment associations
1147
+ for environment_id in environment_ids:
1148
+ team_env = TeamEnvironment(
1149
+ team_id=team_id,
1150
+ environment_id=environment_id,
1151
+ organization_id=organization["id"],
1152
+ )
1153
+ db.add(team_env)
1154
+
1155
+ db.commit()
1156
+ logger.info(
1157
+ "team_environments_updated",
1158
+ team_id=team_id,
1159
+ environment_count=len(environment_ids),
1160
+ org_id=organization["id"]
1161
+ )
1162
+ except Exception as e:
1163
+ db.rollback()
1164
+ logger.warning(
1165
+ "failed_to_update_team_environments",
1166
+ error=str(e),
1167
+ team_id=team_id,
1168
+ org_id=organization["id"]
1169
+ )
1170
+
1171
+ # Return with parsed configuration
1172
+ return TeamResponse(
1173
+ id=str(team.id),
1174
+ organization_id=team.organization_id,
1175
+ name=team.name,
1176
+ description=team.description,
1177
+ status=team.status,
1178
+ runtime=team.runtime.value if team.runtime else "default", # Include runtime in response
1179
+ configuration=TeamConfiguration(**(team.configuration or {})),
1180
+ created_at=team.created_at,
1181
+ updated_at=team.updated_at,
1182
+ projects=get_team_projects(db, team_id),
1183
+ skill_ids=team.skill_ids or [], # Include skill_ids in response
1184
+ skills=[], # Skills will be loaded separately if needed
1185
+ )
1186
+
1187
+
1188
+ @router.delete("/{team_id}", status_code=status.HTTP_204_NO_CONTENT)
1189
+ @instrument_endpoint("teams.delete_team")
1190
+ def delete_team(
1191
+ team_id: str,
1192
+ db: Session = Depends(get_db),
1193
+ organization: dict = Depends(get_current_organization),
1194
+ ):
1195
+ """Delete a team - only if it belongs to the current organization"""
1196
+ team = db.query(Team).filter(
1197
+ Team.id == team_id,
1198
+ Team.organization_id == organization["id"]
1199
+ ).first()
1200
+ if not team:
1201
+ raise HTTPException(status_code=404, detail="Team not found")
1202
+
1203
+ db.delete(team)
1204
+ db.commit()
1205
+ return None
1206
+
1207
+
1208
+ @router.post("/{team_id}/agents/{agent_id}", response_model=TeamWithAgentsResponse)
1209
+ @instrument_endpoint("teams.add_agent_to_team")
1210
+ def add_agent_to_team(
1211
+ team_id: str,
1212
+ agent_id: str,
1213
+ db: Session = Depends(get_db),
1214
+ organization: dict = Depends(get_current_organization),
1215
+ ):
1216
+ """
1217
+ Add an agent to a team.
1218
+
1219
+ This sets the agent's team_id foreign key. You can also manage members
1220
+ through the team's configuration.member_ids field.
1221
+ Only allows adding agents to teams belonging to the current organization.
1222
+ """
1223
+ team = db.query(Team).filter(
1224
+ Team.id == team_id,
1225
+ Team.organization_id == organization["id"]
1226
+ ).first()
1227
+ if not team:
1228
+ raise HTTPException(status_code=404, detail="Team not found")
1229
+
1230
+ agent = db.query(Agent).filter(
1231
+ Agent.id == agent_id,
1232
+ Agent.organization_id == organization["id"]
1233
+ ).first()
1234
+ if not agent:
1235
+ raise HTTPException(status_code=404, detail="Agent not found")
1236
+
1237
+ agent.team_id = team_id
1238
+ db.commit()
1239
+ db.refresh(team)
1240
+
1241
+ # Parse configuration
1242
+ team_config = TeamConfiguration(**(team.configuration or {}))
1243
+
1244
+ # Get agents from configuration.member_ids (source of truth)
1245
+ member_ids = team_config.member_ids
1246
+ agents = []
1247
+ if member_ids:
1248
+ db_agents = db.query(Agent).filter(Agent.id.in_(member_ids)).all()
1249
+ agents = [
1250
+ {
1251
+ "id": str(a.id),
1252
+ "name": a.name,
1253
+ "status": a.status,
1254
+ "capabilities": a.capabilities,
1255
+ "description": a.description,
1256
+ }
1257
+ for a in db_agents
1258
+ ]
1259
+
1260
+ # Return team with agents
1261
+ return TeamWithAgentsResponse(
1262
+ id=str(team.id),
1263
+ organization_id=team.organization_id,
1264
+ name=team.name,
1265
+ description=team.description,
1266
+ status=team.status,
1267
+ configuration=team_config,
1268
+ created_at=team.created_at,
1269
+ updated_at=team.updated_at,
1270
+ agents=agents,
1271
+ )
1272
+
1273
+
1274
+ @router.delete("/{team_id}/agents/{agent_id}", response_model=TeamWithAgentsResponse)
1275
+ @instrument_endpoint("teams.remove_agent_from_team")
1276
+ def remove_agent_from_team(
1277
+ team_id: str,
1278
+ agent_id: str,
1279
+ db: Session = Depends(get_db),
1280
+ organization: dict = Depends(get_current_organization),
1281
+ ):
1282
+ """
1283
+ Remove an agent from a team.
1284
+
1285
+ This clears the agent's team_id foreign key.
1286
+ Only allows removing agents from teams belonging to the current organization.
1287
+ """
1288
+ team = db.query(Team).filter(
1289
+ Team.id == team_id,
1290
+ Team.organization_id == organization["id"]
1291
+ ).first()
1292
+ if not team:
1293
+ raise HTTPException(status_code=404, detail="Team not found")
1294
+
1295
+ agent = db.query(Agent).filter(
1296
+ Agent.id == agent_id,
1297
+ Agent.team_id == team_id,
1298
+ Agent.organization_id == organization["id"]
1299
+ ).first()
1300
+ if not agent:
1301
+ raise HTTPException(status_code=404, detail="Agent not found in this team")
1302
+
1303
+ agent.team_id = None
1304
+ db.commit()
1305
+ db.refresh(team)
1306
+
1307
+ # Parse configuration
1308
+ team_config = TeamConfiguration(**(team.configuration or {}))
1309
+
1310
+ # Get agents from configuration.member_ids (source of truth)
1311
+ member_ids = team_config.member_ids
1312
+ agents = []
1313
+ if member_ids:
1314
+ db_agents = db.query(Agent).filter(Agent.id.in_(member_ids)).all()
1315
+ agents = [
1316
+ {
1317
+ "id": str(a.id),
1318
+ "name": a.name,
1319
+ "status": a.status,
1320
+ "capabilities": a.capabilities,
1321
+ "description": a.description,
1322
+ }
1323
+ for a in db_agents
1324
+ ]
1325
+
1326
+ # Return team with agents
1327
+ return TeamWithAgentsResponse(
1328
+ id=str(team.id),
1329
+ organization_id=team.organization_id,
1330
+ name=team.name,
1331
+ description=team.description,
1332
+ status=team.status,
1333
+ configuration=team_config,
1334
+ created_at=team.created_at,
1335
+ updated_at=team.updated_at,
1336
+ agents=agents,
1337
+ )
1338
+
1339
+
1340
+ @router.post("/{team_id}/execute", response_model=TeamExecutionResponse)
1341
+ @instrument_endpoint("teams.execute_team")
1342
+ async def execute_team(
1343
+ team_id: str,
1344
+ execution_request: TeamExecutionRequest,
1345
+ request: Request,
1346
+ db: Session = Depends(get_db),
1347
+ organization: dict = Depends(get_current_organization),
1348
+ ):
1349
+ """
1350
+ Execute a team task by submitting to Temporal workflow.
1351
+
1352
+ This creates an execution record and starts a Temporal workflow.
1353
+ The actual execution happens asynchronously on the Temporal worker.
1354
+
1355
+ The runner_name should come from the Composer UI where user selects
1356
+ from available runners (fetched from Kubiya API /api/v1/runners).
1357
+ """
1358
+ try:
1359
+ # Get team details from local DB
1360
+ team = db.query(Team).filter(
1361
+ Team.id == team_id,
1362
+ Team.organization_id == organization["id"]
1363
+ ).first()
1364
+
1365
+ if not team:
1366
+ raise HTTPException(status_code=404, detail="Team not found")
1367
+
1368
+ # DEBUG: Log team runtime immediately after fetch
1369
+ print(f"🔍 DEBUG [execute_team]: Fetched team '{team.name}' (ID: {team.id})")
1370
+ print(f"🔍 DEBUG [execute_team]: team.runtime = {team.runtime} (type: {type(team.runtime)})")
1371
+
1372
+ # Parse team configuration
1373
+ team_config = TeamConfiguration(**(team.configuration or {}))
1374
+
1375
+ # Validate and get worker queue
1376
+ worker_queue_id = execution_request.worker_queue_id
1377
+
1378
+ worker_queue = db.query(WorkerQueue).filter(
1379
+ WorkerQueue.id == worker_queue_id,
1380
+ WorkerQueue.organization_id == organization["id"]
1381
+ ).first()
1382
+
1383
+ if not worker_queue:
1384
+ raise HTTPException(
1385
+ status_code=status.HTTP_404_NOT_FOUND,
1386
+ detail=f"Worker queue '{worker_queue_id}' not found. Please select a valid worker queue."
1387
+ )
1388
+
1389
+ # Check if queue has active workers
1390
+ if worker_queue.status != "active":
1391
+ raise HTTPException(
1392
+ status_code=status.HTTP_400_BAD_REQUEST,
1393
+ detail=f"Worker queue '{worker_queue.name}' is not active"
1394
+ )
1395
+
1396
+ # Extract user metadata - ALWAYS use JWT-decoded organization data as source of truth
1397
+ user_metadata = execution_request.user_metadata or {}
1398
+ # Override with JWT data (user can't spoof their identity)
1399
+ user_metadata["user_id"] = organization.get("user_id")
1400
+ user_metadata["user_email"] = organization.get("user_email")
1401
+ user_metadata["user_name"] = organization.get("user_name")
1402
+ # Keep user_avatar from request if provided (not in JWT)
1403
+ if not user_metadata.get("user_avatar"):
1404
+ user_metadata["user_avatar"] = None
1405
+
1406
+ logger.info(
1407
+ "execution_user_metadata",
1408
+ user_id=user_metadata.get("user_id"),
1409
+ user_name=user_metadata.get("user_name"),
1410
+ user_email=user_metadata.get("user_email"),
1411
+ org_id=organization.get("id"),
1412
+ )
1413
+
1414
+ # Create execution record in database
1415
+ execution_id = str(uuid.uuid4())
1416
+
1417
+ execution = Execution(
1418
+ id=execution_id,
1419
+ organization_id=organization["id"],
1420
+ execution_type="TEAM",
1421
+ entity_id=team_id,
1422
+ entity_name=team.name,
1423
+ prompt=execution_request.prompt,
1424
+ system_prompt=execution_request.system_prompt,
1425
+ status="PENDING",
1426
+ worker_queue_id=worker_queue_id,
1427
+ runner_name=worker_queue.name, # Store queue name for display
1428
+ user_id=user_metadata.get("user_id"),
1429
+ user_name=user_metadata.get("user_name"),
1430
+ user_email=user_metadata.get("user_email"),
1431
+ user_avatar=user_metadata.get("user_avatar"),
1432
+ usage={},
1433
+ execution_metadata={
1434
+ "kubiya_org_id": organization["id"],
1435
+ "kubiya_org_name": organization["name"],
1436
+ "worker_queue_name": worker_queue.display_name or worker_queue.name,
1437
+ "team_execution": True,
1438
+ },
1439
+ )
1440
+ db.add(execution)
1441
+ db.commit()
1442
+ db.refresh(execution)
1443
+
1444
+ # Add creator as the first participant (owner role) for multiplayer support
1445
+ user_id = user_metadata.get("user_id")
1446
+ if user_id:
1447
+ try:
1448
+ participant = ExecutionParticipant(
1449
+ execution_id=execution_id,
1450
+ organization_id=organization["id"],
1451
+ user_id=user_id,
1452
+ user_name=user_metadata.get("user_name"),
1453
+ user_email=user_metadata.get("user_email"),
1454
+ user_avatar=user_metadata.get("user_avatar"),
1455
+ role="owner",
1456
+ )
1457
+ db.add(participant)
1458
+ db.commit()
1459
+ logger.info(
1460
+ "owner_participant_added",
1461
+ execution_id=execution_id,
1462
+ user_id=user_id,
1463
+ )
1464
+ except Exception as participant_error:
1465
+ db.rollback()
1466
+ logger.warning(
1467
+ "failed_to_add_owner_participant",
1468
+ error=str(participant_error),
1469
+ execution_id=execution_id,
1470
+ )
1471
+ # Don't fail execution creation if participant tracking fails
1472
+
1473
+ # Get resolved execution environment with templates compiled
1474
+ resolved_env = {} # Initialize to empty dict to avoid UnboundLocalError
1475
+ try:
1476
+ async with httpx.AsyncClient() as client:
1477
+ resolved_env_response = await client.get(
1478
+ f"{str(request.base_url).rstrip('/')}/api/v1/execution-environment/teams/{team_id}/resolved/full",
1479
+ headers={"Authorization": request.headers.get("authorization")}
1480
+ )
1481
+ if resolved_env_response.status_code == 200:
1482
+ resolved_env = resolved_env_response.json()
1483
+ mcp_servers = resolved_env.get("mcp_servers", {})
1484
+ resolved_instructions = resolved_env.get("instructions")
1485
+ resolved_description = resolved_env.get("description")
1486
+ logger.info(
1487
+ "execution_environment_resolved_for_team_execution",
1488
+ team_id=team_id[:8],
1489
+ mcp_server_count=len(mcp_servers),
1490
+ has_resolved_instructions=bool(resolved_instructions)
1491
+ )
1492
+ else:
1493
+ logger.warning(
1494
+ "failed_to_resolve_team_execution_environment",
1495
+ team_id=team_id[:8],
1496
+ status=resolved_env_response.status_code
1497
+ )
1498
+ # Fallback to non-resolved config
1499
+ mcp_servers = team_config.metadata.get("mcpServers", {}) if team_config.metadata else {}
1500
+ resolved_instructions = None
1501
+ resolved_description = None
1502
+ except Exception as e:
1503
+ logger.error(
1504
+ "team_execution_environment_resolution_error",
1505
+ team_id=team_id[:8],
1506
+ error=str(e)
1507
+ )
1508
+ # Fallback to non-resolved config
1509
+ mcp_servers = team_config.metadata.get("mcpServers", {}) if team_config.metadata else {}
1510
+ resolved_instructions = None
1511
+ resolved_description = None
1512
+
1513
+ # Use LLM config from team configuration if available
1514
+ model_id = team_config.llm.model if team_config.llm and team_config.llm.model else "kubiya/claude-sonnet-4"
1515
+
1516
+ # Build model config from LLM configuration
1517
+ model_config = {}
1518
+ if team_config.llm:
1519
+ if team_config.llm.temperature is not None:
1520
+ model_config["temperature"] = team_config.llm.temperature
1521
+ if team_config.llm.max_tokens is not None:
1522
+ model_config["max_tokens"] = team_config.llm.max_tokens
1523
+ if team_config.llm.top_p is not None:
1524
+ model_config["top_p"] = team_config.llm.top_p
1525
+ if team_config.llm.stop is not None:
1526
+ model_config["stop"] = team_config.llm.stop
1527
+ if team_config.llm.frequency_penalty is not None:
1528
+ model_config["frequency_penalty"] = team_config.llm.frequency_penalty
1529
+ if team_config.llm.presence_penalty is not None:
1530
+ model_config["presence_penalty"] = team_config.llm.presence_penalty
1531
+
1532
+ # Submit to Temporal workflow
1533
+ # Task queue is the worker queue UUID
1534
+ task_queue = worker_queue_id
1535
+
1536
+ # Get org-specific Temporal credentials and client
1537
+ from control_plane_api.app.lib.temporal_credentials_service import get_temporal_credentials_for_org
1538
+ from control_plane_api.app.lib.temporal_client import get_temporal_client_for_org
1539
+
1540
+ token = request.state.kubiya_token
1541
+ temporal_credentials = await get_temporal_credentials_for_org(
1542
+ org_id=organization["id"],
1543
+ token=token,
1544
+ use_fallback=True # Enable fallback during migration
1545
+ )
1546
+
1547
+ # Create org-specific Temporal client
1548
+ temporal_client = await get_temporal_client_for_org(
1549
+ namespace=temporal_credentials["namespace"],
1550
+ api_key=temporal_credentials["api_key"],
1551
+ host=temporal_credentials["host"],
1552
+ )
1553
+
1554
+ # Start workflow
1555
+ # Use resolved instructions (with templates compiled) if available
1556
+ # Priority: request > resolved > team_config.instructions
1557
+ system_prompt = execution_request.system_prompt
1558
+ if not system_prompt:
1559
+ if resolved_instructions:
1560
+ # Use resolved instructions with templates compiled
1561
+ if isinstance(resolved_instructions, list):
1562
+ system_prompt = "\n".join(resolved_instructions)
1563
+ else:
1564
+ system_prompt = resolved_instructions
1565
+ elif team_config.instructions:
1566
+ # Fallback to non-resolved instructions
1567
+ if isinstance(team_config.instructions, list):
1568
+ system_prompt = "\n".join(team_config.instructions)
1569
+ else:
1570
+ system_prompt = team_config.instructions
1571
+
1572
+ # Get API key from Authorization header
1573
+ auth_header = request.headers.get("authorization", "")
1574
+ api_key = auth_header.replace("UserKey ", "").replace("Bearer ", "") if auth_header else None
1575
+
1576
+ # Get control plane URL from request
1577
+ control_plane_url = str(request.base_url).rstrip("/")
1578
+
1579
+ # CRITICAL: Use real-time timestamp for initial message to ensure chronological ordering
1580
+ # This prevents timestamp mismatches between initial and follow-up messages
1581
+ initial_timestamp = datetime.now(timezone.utc).isoformat()
1582
+
1583
+ # Handle runtime type - SQLAlchemy may return enum or string
1584
+ runtime_value = team.runtime
1585
+ print(f"🔍 DEBUG [execute_team]: runtime_value = {runtime_value} (type: {type(runtime_value)})")
1586
+ print(f"🔍 DEBUG [execute_team]: hasattr(runtime_value, 'value') = {hasattr(runtime_value, 'value')}")
1587
+ if runtime_value:
1588
+ # If it's an enum, get its value; if it's already a string, use it
1589
+ runtime_type_str = runtime_value.value if hasattr(runtime_value, 'value') else str(runtime_value)
1590
+ else:
1591
+ runtime_type_str = "default"
1592
+ print(f"🔍 DEBUG [execute_team]: Final runtime_type_str = '{runtime_type_str}'")
1593
+
1594
+ # Override team_config with execution_environment.working_dir if provided
1595
+ team_configuration = team.configuration or {}
1596
+ if execution_request.execution_environment and execution_request.execution_environment.working_dir:
1597
+ team_configuration = team_configuration.copy()
1598
+ team_configuration["cwd"] = execution_request.execution_environment.working_dir
1599
+ logger.info(
1600
+ "execution_working_dir_override",
1601
+ execution_id=execution_id,
1602
+ working_dir=execution_request.execution_environment.working_dir,
1603
+ )
1604
+
1605
+ workflow_input = TeamExecutionInput(
1606
+ execution_id=execution_id,
1607
+ team_id=team_id,
1608
+ organization_id=organization["id"],
1609
+ prompt=execution_request.prompt,
1610
+ system_prompt=system_prompt,
1611
+ model_id=model_id,
1612
+ model_config=model_config,
1613
+ team_config=team_configuration,
1614
+ mcp_servers=mcp_servers,
1615
+ user_metadata=user_metadata,
1616
+ runtime_type=runtime_type_str,
1617
+ control_plane_url=control_plane_url,
1618
+ api_key=api_key,
1619
+ initial_message_timestamp=initial_timestamp,
1620
+ graph_api_url=resolved_env.get("graph_api_url"),
1621
+ dataset_name=resolved_env.get("dataset_name"),
1622
+ )
1623
+
1624
+ workflow_handle = await temporal_client.start_workflow(
1625
+ TeamExecutionWorkflow.run,
1626
+ workflow_input, # Pass TeamExecutionInput directly
1627
+ id=f"team-execution-{execution_id}",
1628
+ task_queue=task_queue,
1629
+ )
1630
+
1631
+ logger.info(
1632
+ "team_execution_submitted",
1633
+ execution_id=execution_id,
1634
+ team_id=team_id,
1635
+ workflow_id=workflow_handle.id,
1636
+ task_queue=task_queue,
1637
+ temporal_namespace=temporal_credentials["namespace"],
1638
+ worker_queue_id=worker_queue_id,
1639
+ worker_queue_name=worker_queue.name,
1640
+ org_id=organization["id"],
1641
+ org_name=organization["name"],
1642
+ )
1643
+
1644
+ return TeamExecutionResponse(
1645
+ execution_id=execution_id,
1646
+ workflow_id=workflow_handle.id,
1647
+ status="PENDING",
1648
+ message=f"Execution submitted to worker queue: {worker_queue.name}",
1649
+ )
1650
+
1651
+ except HTTPException:
1652
+ raise
1653
+ except Exception as e:
1654
+ logger.error(
1655
+ "team_execution_failed",
1656
+ error=str(e),
1657
+ team_id=team_id,
1658
+ org_id=organization["id"]
1659
+ )
1660
+ raise HTTPException(
1661
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1662
+ detail=f"Failed to execute team: {str(e)}"
1663
+ )
1664
+
1665
+
1666
+ @router.post("/{team_id}/execute/stream")
1667
+ @instrument_endpoint("teams.execute_team_stream")
1668
+ def execute_team_stream(
1669
+ team_id: str,
1670
+ execution_request: TeamExecutionRequest,
1671
+ db: Session = Depends(get_db),
1672
+ ):
1673
+ """
1674
+ Execute a team task with streaming response.
1675
+
1676
+ The team leader coordinates and delegates the task to appropriate team members.
1677
+ Results are streamed back in real-time.
1678
+ """
1679
+ from control_plane_api.app.services.litellm_service import litellm_service
1680
+
1681
+ team = db.query(Team).filter(Team.id == team_id).first()
1682
+ if not team:
1683
+ raise HTTPException(status_code=404, detail="Team not found")
1684
+
1685
+ # Get team agents
1686
+ agents = team.agents
1687
+ if not agents:
1688
+ raise HTTPException(
1689
+ status_code=400,
1690
+ detail="Team has no agents. Add agents to the team before executing tasks."
1691
+ )
1692
+
1693
+ # Build team coordination prompt
1694
+ agent_descriptions = []
1695
+ for agent in agents:
1696
+ caps = ", ".join(agent.capabilities) if agent.capabilities else "general"
1697
+ agent_descriptions.append(
1698
+ f"- {agent.name}: {agent.description or 'No description'} (Capabilities: {caps})"
1699
+ )
1700
+
1701
+ # Create a coordination system prompt
1702
+ coordination_prompt = f"""You are a Team Coordinator managing a team with the following agents:
1703
+
1704
+ {chr(10).join(agent_descriptions)}
1705
+
1706
+ Your task is to:
1707
+ 1. Analyze the user's request
1708
+ 2. Determine which agent(s) are best suited for the task
1709
+ 3. Delegate or route the task appropriately
1710
+ 4. Synthesize and present the results
1711
+
1712
+ User Request: {execution_request.prompt}
1713
+
1714
+ Please coordinate the team to complete this request effectively."""
1715
+
1716
+ # Parse team configuration
1717
+ team_config = TeamConfiguration(**(team.configuration or {}))
1718
+
1719
+ # Use LLM config from team configuration if available
1720
+ model = team_config.llm.model if team_config.llm and team_config.llm.model else "kubiya/claude-sonnet-4"
1721
+
1722
+ # Build LLM kwargs from configuration
1723
+ llm_kwargs = {}
1724
+ if team_config.llm:
1725
+ if team_config.llm.temperature is not None:
1726
+ llm_kwargs["temperature"] = team_config.llm.temperature
1727
+ if team_config.llm.max_tokens is not None:
1728
+ llm_kwargs["max_tokens"] = team_config.llm.max_tokens
1729
+ if team_config.llm.top_p is not None:
1730
+ llm_kwargs["top_p"] = team_config.llm.top_p
1731
+ if team_config.llm.stop is not None:
1732
+ llm_kwargs["stop"] = team_config.llm.stop
1733
+ if team_config.llm.frequency_penalty is not None:
1734
+ llm_kwargs["frequency_penalty"] = team_config.llm.frequency_penalty
1735
+ if team_config.llm.presence_penalty is not None:
1736
+ llm_kwargs["presence_penalty"] = team_config.llm.presence_penalty
1737
+
1738
+ # Execute coordination using LiteLLM (streaming)
1739
+ return StreamingResponse(
1740
+ litellm_service.execute_agent_stream(
1741
+ prompt=coordination_prompt,
1742
+ model=model,
1743
+ system_prompt=execution_request.system_prompt or "You are an expert team coordinator. Delegate tasks efficiently and synthesize results clearly.",
1744
+ **llm_kwargs,
1745
+ ),
1746
+ media_type="text/event-stream",
1747
+ )