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,1598 @@
1
+ """
2
+ Multi-tenant agents router with Temporal workflow integration.
3
+
4
+ This router handles agent CRUD operations and execution submissions.
5
+ All operations are scoped to the authenticated organization.
6
+ """
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
9
+ from typing import Dict, List, Optional
10
+ from datetime import datetime, timezone
11
+ from pydantic import BaseModel, Field
12
+ import structlog
13
+ import uuid
14
+ import httpx
15
+ import os
16
+ from sqlalchemy.orm import Session, joinedload
17
+ from sqlalchemy import and_
18
+
19
+ from control_plane_api.app.middleware.auth import get_current_organization
20
+ from control_plane_api.app.database import get_db
21
+ from control_plane_api.app.lib.temporal_client import get_temporal_client
22
+ from control_plane_api.app.lib.mcp_validation import validate_execution_environment_mcp, MCPValidationError
23
+ from control_plane_api.app.workflows.agent_execution import AgentExecutionWorkflow, AgentExecutionInput
24
+ from control_plane_api.app.routers.projects import get_default_project_id
25
+ from control_plane_api.app.lib.validation import validate_agent_for_runtime
26
+ from control_plane_api.app.schemas.mcp_schemas import MCPServerConfig
27
+ from control_plane_api.app.models import (
28
+ Agent, AgentStatus, Execution, ExecutionStatus, ExecutionType,
29
+ ExecutionTriggerSource, ExecutionParticipant, ParticipantRole,
30
+ Skill, SkillAssociation, AgentEnvironment, Environment, Project,
31
+ ProjectAgent, WorkerQueue
32
+ )
33
+ from control_plane_api.app.lib.sqlalchemy_utils import model_to_dict, models_to_dict_list
34
+ from control_plane_api.app.observability import (
35
+ instrument_endpoint,
36
+ create_span_with_context,
37
+ add_span_event,
38
+ add_span_error,
39
+ )
40
+
41
+ logger = structlog.get_logger()
42
+
43
+ router = APIRouter()
44
+
45
+
46
+ class ExecutionEnvironment(BaseModel):
47
+ """Execution environment configuration for agents/teams"""
48
+ working_dir: str | None = Field(None, description="Working directory for execution (overrides default workspace)")
49
+ env_vars: dict[str, str] = Field(default_factory=dict, description="Environment variables (key-value pairs)")
50
+ secrets: list[str] = Field(default_factory=list, description="Secret names from Kubiya vault")
51
+ integration_ids: list[str] = Field(default_factory=list, description="Integration UUIDs for delegated credentials")
52
+ mcp_servers: Dict[str, MCPServerConfig] = Field(
53
+ default_factory=dict,
54
+ description="MCP (Model Context Protocol) server configurations. Supports stdio, HTTP, and SSE transports."
55
+ )
56
+
57
+
58
+ def get_agent_projects(db: Session, agent_id: str) -> list[dict]:
59
+ """Get all projects an agent belongs to"""
60
+ try:
61
+ # Query project_agents join table with Project relationship
62
+ project_agents = (
63
+ db.query(ProjectAgent)
64
+ .join(Project, ProjectAgent.project_id == Project.id)
65
+ .filter(ProjectAgent.agent_id == agent_id)
66
+ .all()
67
+ )
68
+
69
+ projects = []
70
+ for pa in project_agents:
71
+ if pa.project:
72
+ projects.append({
73
+ "id": str(pa.project.id),
74
+ "name": pa.project.name,
75
+ "key": pa.project.key,
76
+ "description": pa.project.description,
77
+ })
78
+
79
+ return projects
80
+ except Exception as e:
81
+ logger.warning("failed_to_fetch_agent_projects", error=str(e), agent_id=agent_id)
82
+ return []
83
+
84
+
85
+ def get_agent_environments(db: Session, agent_id: str) -> list[dict]:
86
+ """Get all environments an agent is assigned to"""
87
+ try:
88
+ # Query agent_environments join table with Environment relationship
89
+ agent_envs = (
90
+ db.query(AgentEnvironment)
91
+ .join(Environment, AgentEnvironment.environment_id == Environment.id)
92
+ .filter(AgentEnvironment.agent_id == agent_id)
93
+ .all()
94
+ )
95
+
96
+ environments = []
97
+ for ae in agent_envs:
98
+ env = db.query(Environment).filter(Environment.id == ae.environment_id).first()
99
+ if env:
100
+ environments.append({
101
+ "id": str(env.id),
102
+ "name": env.name,
103
+ "display_name": env.display_name,
104
+ "status": env.status,
105
+ })
106
+
107
+ return environments
108
+ except Exception as e:
109
+ logger.warning("failed_to_fetch_agent_environments", error=str(e), agent_id=agent_id)
110
+ return []
111
+
112
+
113
+ def get_entity_skills(db: Session, organization_id: str, entity_type: str, entity_id: str) -> list[dict]:
114
+ """Get skills associated with an entity"""
115
+ try:
116
+ # Get associations with joined skills
117
+ skill_associations = (
118
+ db.query(SkillAssociation)
119
+ .join(Skill, SkillAssociation.skill_id == Skill.id)
120
+ .filter(
121
+ SkillAssociation.organization_id == organization_id,
122
+ SkillAssociation.entity_type == entity_type,
123
+ SkillAssociation.entity_id == entity_id
124
+ )
125
+ .all()
126
+ )
127
+
128
+ skills = []
129
+ for assoc in skill_associations:
130
+ skill = db.query(Skill).filter(Skill.id == assoc.skill_id).first()
131
+ if skill and skill.enabled:
132
+ # Merge configuration with override
133
+ config = skill.configuration or {}
134
+ override = assoc.configuration_override
135
+ if override:
136
+ config = {**config, **override}
137
+
138
+ skills.append({
139
+ "id": str(skill.id),
140
+ "name": skill.name,
141
+ "type": skill.skill_type,
142
+ "description": skill.description,
143
+ "enabled": skill.enabled,
144
+ "configuration": config,
145
+ })
146
+
147
+ return skills
148
+ except Exception as e:
149
+ logger.warning("failed_to_fetch_entity_skills", error=str(e), entity_type=entity_type, entity_id=entity_id)
150
+ return []
151
+
152
+
153
+ def get_agent_skills_with_inheritance(db: Session, organization_id: str, agent_id: str, team_id: str | None) -> list[dict]:
154
+ """
155
+ Get all skills for an agent, including those inherited from the team.
156
+ Team skills are inherited by all team members.
157
+
158
+ Inheritance order (later overrides earlier):
159
+ 1. Team skills (if agent is part of a team)
160
+ 2. Agent skills
161
+ """
162
+ seen_ids = set()
163
+ skills = []
164
+
165
+ # 1. Get team skills first (if agent is part of a team)
166
+ if team_id:
167
+ try:
168
+ team_skills = get_entity_skills(db, organization_id, "team", team_id)
169
+ for skill in team_skills:
170
+ if skill["id"] not in seen_ids:
171
+ skills.append(skill)
172
+ seen_ids.add(skill["id"])
173
+ except Exception as e:
174
+ logger.warning("failed_to_fetch_team_skills_for_agent", error=str(e), team_id=team_id, agent_id=agent_id)
175
+
176
+ # 2. Get agent-specific skills (these override team skills if there's a conflict)
177
+ try:
178
+ agent_skills = get_entity_skills(db, organization_id, "agent", agent_id)
179
+ for skill in agent_skills:
180
+ if skill["id"] not in seen_ids:
181
+ skills.append(skill)
182
+ seen_ids.add(skill["id"])
183
+ except Exception as e:
184
+ logger.warning("failed_to_fetch_agent_skills", error=str(e), agent_id=agent_id)
185
+
186
+ return skills
187
+
188
+
189
+ # Pydantic schemas
190
+ class AgentCreate(BaseModel):
191
+ name: str = Field(..., description="Agent name")
192
+ description: str | None = Field(None, description="Agent description")
193
+ system_prompt: str | None = Field(None, description="System prompt for the agent")
194
+ capabilities: list = Field(default_factory=list, description="Agent capabilities")
195
+ configuration: dict = Field(default_factory=dict, description="Agent configuration")
196
+ model_id: str | None = Field(None, description="LiteLLM model identifier")
197
+ model: str | None = Field(None, description="Model identifier (alias for model_id)")
198
+ llm_config: dict = Field(default_factory=dict, description="Model-specific configuration")
199
+ runtime: str | None = Field(None, description="Runtime type: 'default' (Agno) or 'claude_code' (Claude Code SDK)")
200
+ runner_name: str | None = Field(None, description="Preferred runner for this agent")
201
+ team_id: str | None = Field(None, description="Team ID to assign this agent to")
202
+ environment_ids: list[str] = Field(default_factory=list, description="Environment IDs to deploy this agent to")
203
+ skill_ids: list[str] = Field(default_factory=list, description="Tool set IDs to associate with this agent")
204
+ skill_configurations: dict[str, dict] = Field(default_factory=dict, description="Tool set configurations keyed by skill ID")
205
+ execution_environment: ExecutionEnvironment | None = Field(None, description="Execution environment: env vars, secrets, integrations")
206
+
207
+
208
+ class AgentUpdate(BaseModel):
209
+ name: str | None = None
210
+ description: str | None = None
211
+ system_prompt: str | None = None
212
+ status: str | None = None
213
+ capabilities: list | None = None
214
+ configuration: dict | None = None
215
+ state: dict | None = None
216
+ model_id: str | None = None
217
+ model: str | None = None # Alias for model_id
218
+ llm_config: dict | None = None
219
+ runtime: str | None = None
220
+ runner_name: str | None = None
221
+ team_id: str | None = None
222
+ environment_ids: list[str] | None = None
223
+ skill_ids: list[str] | None = None
224
+ skill_configurations: dict[str, dict] | None = None
225
+ execution_environment: ExecutionEnvironment | None = None
226
+
227
+
228
+ class AgentResponse(BaseModel):
229
+ id: str
230
+ organization_id: str
231
+ name: str
232
+ description: str | None
233
+ system_prompt: str | None
234
+ status: str
235
+ capabilities: list
236
+ configuration: dict
237
+ model_id: str | None
238
+ llm_config: dict
239
+ runtime: str | None
240
+ runner_name: str | None
241
+ team_id: str | None
242
+ created_at: str
243
+ updated_at: str
244
+ last_active_at: str | None
245
+ state: dict
246
+ error_message: str | None
247
+ projects: list[dict] = Field(default_factory=list, description="Projects this agent belongs to")
248
+ environments: list[dict] = Field(default_factory=list, description="Environments this agent is deployed to")
249
+ skill_ids: list[str] | None = Field(default_factory=list, description="IDs of associated skills")
250
+ skills: list[dict] | None = Field(default_factory=list, description="Associated skills with details")
251
+ execution_environment: ExecutionEnvironment | None = None
252
+
253
+
254
+ class AgentExecutionRequest(BaseModel):
255
+ prompt: str = Field(..., description="The prompt to execute")
256
+ system_prompt: str | None = Field(None, description="Optional system prompt")
257
+ stream: bool = Field(False, description="Whether to stream the response")
258
+ worker_queue_id: str = Field(..., description="Worker queue ID (UUID) to route execution to - REQUIRED")
259
+ user_metadata: dict | None = Field(None, description="User attribution metadata (optional, auto-filled from token)")
260
+ execution_environment: ExecutionEnvironment | None = Field(None, description="Optional execution environment settings (working_dir, etc.)")
261
+
262
+
263
+ class AgentExecutionResponse(BaseModel):
264
+ execution_id: str
265
+ workflow_id: str
266
+ status: str
267
+ message: str
268
+
269
+
270
+ def get_or_create_default_shell_skill(db: Session, organization_id: str) -> Optional[str]:
271
+ """
272
+ Get or create the default shell skill for an organization.
273
+
274
+ Args:
275
+ db: Database session
276
+ organization_id: Organization ID
277
+
278
+ Returns:
279
+ Shell skill ID if found/created, None if failed
280
+ """
281
+ try:
282
+ # First, try to find existing shell skill
283
+ existing_skill = (
284
+ db.query(Skill)
285
+ .filter(
286
+ Skill.organization_id == organization_id,
287
+ Skill.skill_type == "shell",
288
+ Skill.enabled == True
289
+ )
290
+ .first()
291
+ )
292
+
293
+ if existing_skill:
294
+ logger.info(
295
+ "found_existing_shell_skill",
296
+ skill_id=str(existing_skill.id),
297
+ org_id=organization_id
298
+ )
299
+ return str(existing_skill.id)
300
+
301
+ # Create default shell skill if none exists
302
+ skill_id = uuid.uuid4()
303
+ now = datetime.utcnow()
304
+
305
+ skill = Skill(
306
+ id=skill_id,
307
+ organization_id=organization_id,
308
+ name="Shell",
309
+ skill_type="shell",
310
+ description="Execute shell commands on the system",
311
+ icon="Terminal",
312
+ enabled=True,
313
+ configuration={},
314
+ created_at=now,
315
+ updated_at=now,
316
+ )
317
+
318
+ db.add(skill)
319
+ db.commit()
320
+ db.refresh(skill)
321
+
322
+ logger.info(
323
+ "default_shell_skill_created",
324
+ skill_id=str(skill_id),
325
+ org_id=organization_id
326
+ )
327
+
328
+ return str(skill_id)
329
+ except Exception as e:
330
+ db.rollback()
331
+ logger.error(
332
+ "failed_to_get_or_create_shell_skill",
333
+ error=str(e),
334
+ org_id=organization_id
335
+ )
336
+ return None
337
+
338
+
339
+ @router.post("", response_model=AgentResponse, status_code=status.HTTP_201_CREATED)
340
+ @instrument_endpoint("agents_v2.create_agent")
341
+ async def create_agent(
342
+ agent_data: AgentCreate,
343
+ request: Request,
344
+ organization: dict = Depends(get_current_organization),
345
+ db: Session = Depends(get_db),
346
+ ):
347
+ """Create a new agent in the organization"""
348
+ try:
349
+
350
+ agent_id = str(uuid.uuid4())
351
+ now = datetime.utcnow().isoformat()
352
+
353
+ # Handle model field - prefer 'model' over 'model_id' for backward compatibility
354
+ model_id = agent_data.model or agent_data.model_id
355
+
356
+ # Validate model_id against runtime type
357
+ runtime = agent_data.runtime or "default"
358
+ is_valid, errors = validate_agent_for_runtime(
359
+ runtime_type=runtime,
360
+ model_id=model_id,
361
+ agent_config=agent_data.configuration,
362
+ system_prompt=agent_data.system_prompt
363
+ )
364
+ if not is_valid:
365
+ error_msg = "Agent validation failed:\n" + "\n".join(f" - {err}" for err in errors)
366
+ logger.error(
367
+ "agent_validation_failed",
368
+ runtime=runtime,
369
+ model_id=model_id,
370
+ errors=errors,
371
+ org_id=organization["id"]
372
+ )
373
+ raise HTTPException(
374
+ status_code=status.HTTP_400_BAD_REQUEST,
375
+ detail=error_msg
376
+ )
377
+
378
+ # Validate MCP server configuration if present
379
+ if agent_data.execution_environment and agent_data.execution_environment.mcp_servers:
380
+ try:
381
+ mcp_validation = validate_execution_environment_mcp(
382
+ agent_data.execution_environment.model_dump(by_alias=True),
383
+ strict=False # Non-strict: allow warnings for missing secrets/env vars
384
+ )
385
+ if not mcp_validation["valid"]:
386
+ error_msg = "MCP configuration validation failed:\n" + "\n".join(f" - {err}" for err in mcp_validation["errors"])
387
+ logger.error(
388
+ "mcp_validation_failed",
389
+ errors=mcp_validation["errors"],
390
+ warnings=mcp_validation["warnings"],
391
+ org_id=organization["id"]
392
+ )
393
+ raise HTTPException(
394
+ status_code=status.HTTP_400_BAD_REQUEST,
395
+ detail=error_msg
396
+ )
397
+ # Log warnings if any
398
+ if mcp_validation["warnings"]:
399
+ logger.warning(
400
+ "mcp_validation_warnings",
401
+ warnings=mcp_validation["warnings"],
402
+ required_secrets=mcp_validation["required_secrets"],
403
+ required_env_vars=mcp_validation["required_env_vars"],
404
+ org_id=organization["id"]
405
+ )
406
+ except MCPValidationError as e:
407
+ raise HTTPException(
408
+ status_code=status.HTTP_400_BAD_REQUEST,
409
+ detail=str(e)
410
+ )
411
+
412
+ # Store system_prompt in configuration for persistence
413
+ configuration = agent_data.configuration.copy() if agent_data.configuration else {}
414
+ if agent_data.system_prompt is not None:
415
+ configuration["system_prompt"] = agent_data.system_prompt
416
+
417
+ # Create Agent object
418
+ agent = Agent(
419
+ id=agent_id,
420
+ organization_id=organization["id"],
421
+ name=agent_data.name,
422
+ description=agent_data.description,
423
+ status=AgentStatus.IDLE,
424
+ capabilities=agent_data.capabilities,
425
+ configuration=configuration,
426
+ model_id=model_id,
427
+ model_config=agent_data.llm_config,
428
+ runtime=agent_data.runtime or "default",
429
+ runner_name=agent_data.runner_name,
430
+ team_id=agent_data.team_id,
431
+ execution_environment=agent_data.execution_environment.model_dump(by_alias=True) if agent_data.execution_environment else {},
432
+ state={},
433
+ created_at=now,
434
+ updated_at=now,
435
+ )
436
+
437
+ db.add(agent)
438
+ db.commit()
439
+ db.refresh(agent)
440
+
441
+ # Automatically assign agent to the default project
442
+ default_project_id = get_default_project_id(db, organization)
443
+ if default_project_id:
444
+ try:
445
+ project_agent = ProjectAgent(
446
+ id=uuid.uuid4(),
447
+ project_id=default_project_id,
448
+ agent_id=agent_id,
449
+ role=None,
450
+ added_at=now,
451
+ added_by=organization.get("user_id"),
452
+ )
453
+ db.add(project_agent)
454
+ db.commit()
455
+ logger.info(
456
+ "agent_added_to_default_project",
457
+ agent_id=str(agent_id),
458
+ project_id=default_project_id,
459
+ org_id=organization["id"]
460
+ )
461
+ except Exception as e:
462
+ db.rollback()
463
+ logger.warning(
464
+ "failed_to_add_agent_to_default_project",
465
+ error=str(e),
466
+ agent_id=str(agent_id),
467
+ org_id=organization["id"]
468
+ )
469
+
470
+ # VALIDATION: Ensure at least one skill is associated with the agent
471
+ # Agents without skills are non-functional
472
+ if not agent_data.skill_ids or len(agent_data.skill_ids) == 0:
473
+ # Auto-add shell skill as default
474
+ shell_skill_id = get_or_create_default_shell_skill(db, organization["id"])
475
+
476
+ if shell_skill_id:
477
+ agent_data.skill_ids = [shell_skill_id]
478
+ logger.info(
479
+ "auto_added_shell_skill",
480
+ agent_id=str(agent_id),
481
+ org_id=organization["id"],
482
+ reason="no_skills_provided"
483
+ )
484
+ else:
485
+ db.rollback()
486
+ raise HTTPException(
487
+ status_code=status.HTTP_400_BAD_REQUEST,
488
+ detail="At least one skill is required to create an agent. Unable to add default shell skill. Please add a skill manually."
489
+ )
490
+
491
+ # Create skill associations if skills were provided
492
+ if agent_data.skill_ids:
493
+ try:
494
+ for skill_id in agent_data.skill_ids:
495
+ config_override = agent_data.skill_configurations.get(skill_id, {})
496
+
497
+ skill_association = SkillAssociation(
498
+ id=uuid.uuid4(),
499
+ organization_id=organization["id"],
500
+ skill_id=skill_id,
501
+ entity_type="agent",
502
+ entity_id=agent_id,
503
+ configuration_override=config_override,
504
+ created_at=now,
505
+ )
506
+ db.add(skill_association)
507
+
508
+ db.commit()
509
+ logger.info(
510
+ "agent_skills_associated",
511
+ agent_id=str(agent_id),
512
+ skill_count=len(agent_data.skill_ids),
513
+ org_id=organization["id"]
514
+ )
515
+ except Exception as e:
516
+ db.rollback()
517
+ logger.warning(
518
+ "failed_to_associate_agent_skills",
519
+ error=str(e),
520
+ agent_id=str(agent_id),
521
+ org_id=organization["id"]
522
+ )
523
+
524
+ # Create environment associations if environments were provided
525
+ if agent_data.environment_ids:
526
+ try:
527
+ for environment_id in agent_data.environment_ids:
528
+ agent_env = AgentEnvironment(
529
+ id=uuid.uuid4(),
530
+ agent_id=agent_id,
531
+ environment_id=environment_id,
532
+ organization_id=organization["id"],
533
+ assigned_at=now,
534
+ assigned_by=organization.get("user_id"),
535
+ )
536
+ db.add(agent_env)
537
+
538
+ db.commit()
539
+ logger.info(
540
+ "agent_environments_associated",
541
+ agent_id=str(agent_id),
542
+ environment_count=len(agent_data.environment_ids),
543
+ org_id=organization["id"]
544
+ )
545
+ except Exception as e:
546
+ db.rollback()
547
+ logger.warning(
548
+ "failed_to_associate_agent_environments",
549
+ error=str(e),
550
+ agent_id=str(agent_id),
551
+ org_id=organization["id"]
552
+ )
553
+
554
+ logger.info(
555
+ "agent_created",
556
+ agent_id=str(agent_id),
557
+ agent_name=agent_data.name,
558
+ org_id=organization["id"],
559
+ org_slug=organization["slug"]
560
+ )
561
+
562
+ # Get skills with team inheritance
563
+ team_id = str(agent.team_id) if agent.team_id else None
564
+ skills = get_agent_skills_with_inheritance(db, organization["id"], str(agent_id), team_id)
565
+
566
+ # Extract system_prompt from configuration
567
+ configuration = agent.configuration or {}
568
+ system_prompt = configuration.get("system_prompt")
569
+
570
+ return AgentResponse(
571
+ id=str(agent.id),
572
+ organization_id=agent.organization_id,
573
+ name=agent.name,
574
+ description=agent.description,
575
+ system_prompt=system_prompt,
576
+ status=agent.status,
577
+ capabilities=agent.capabilities,
578
+ configuration=agent.configuration,
579
+ model_id=agent.model_id,
580
+ llm_config=agent.model_config or {},
581
+ runtime=agent.runtime,
582
+ runner_name=agent.runner_name,
583
+ team_id=str(agent.team_id) if agent.team_id else None,
584
+ created_at=agent.created_at.isoformat() if agent.created_at else None,
585
+ updated_at=agent.updated_at.isoformat() if agent.updated_at else None,
586
+ last_active_at=agent.last_active_at.isoformat() if agent.last_active_at else None,
587
+ state=agent.state or {},
588
+ error_message=agent.error_message,
589
+ projects=get_agent_projects(db, str(agent_id)),
590
+ environments=get_agent_environments(db, str(agent_id)),
591
+ skill_ids=[ts["id"] for ts in skills],
592
+ skills=skills,
593
+ execution_environment=(
594
+ ExecutionEnvironment(**agent.execution_environment)
595
+ if agent.execution_environment
596
+ else None
597
+ ),
598
+ )
599
+
600
+ except HTTPException:
601
+ raise
602
+ except Exception as e:
603
+ db.rollback()
604
+ logger.error("agent_creation_failed", error=str(e), org_id=organization["id"])
605
+ raise HTTPException(
606
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
607
+ detail=f"Failed to create agent: {str(e)}"
608
+ )
609
+
610
+
611
+ @router.get("", response_model=List[AgentResponse])
612
+ @instrument_endpoint("agents_v2.list_agents")
613
+ async def list_agents(
614
+ request: Request,
615
+ skip: int = 0,
616
+ limit: int = 100,
617
+ status_filter: str | None = None,
618
+ organization: dict = Depends(get_current_organization),
619
+ db: Session = Depends(get_db),
620
+ ):
621
+ """List all agents in the organization"""
622
+ try:
623
+ # Query agents for this organization
624
+ query = db.query(Agent).filter(Agent.organization_id == organization["id"])
625
+
626
+ if status_filter:
627
+ query = query.filter(Agent.status == status_filter)
628
+
629
+ agents_list = query.order_by(Agent.created_at.desc()).offset(skip).limit(limit).all()
630
+
631
+ if not agents_list:
632
+ return []
633
+
634
+ # Batch fetch all agent-project relationships in one query
635
+ agent_ids = [str(agent.id) for agent in agents_list]
636
+ from sqlalchemy import UUID as SQLUUID
637
+ agent_project_associations = (
638
+ db.query(ProjectAgent)
639
+ .join(Project, ProjectAgent.project_id == Project.id)
640
+ .filter(ProjectAgent.agent_id.in_([agent.id for agent in agents_list]))
641
+ .all()
642
+ )
643
+
644
+ # Group projects by agent_id
645
+ projects_by_agent = {}
646
+ for pa in agent_project_associations:
647
+ agent_id = str(pa.agent_id)
648
+ if pa.project:
649
+ if agent_id not in projects_by_agent:
650
+ projects_by_agent[agent_id] = []
651
+ projects_by_agent[agent_id].append({
652
+ "id": str(pa.project.id),
653
+ "name": pa.project.name,
654
+ "key": pa.project.key,
655
+ "description": pa.project.description,
656
+ })
657
+
658
+ # Batch fetch environments for all agents
659
+ agent_env_associations = (
660
+ db.query(AgentEnvironment)
661
+ .join(Environment, AgentEnvironment.environment_id == Environment.id)
662
+ .filter(AgentEnvironment.agent_id.in_([agent.id for agent in agents_list]))
663
+ .all()
664
+ )
665
+
666
+ # Group environments by agent_id
667
+ environments_by_agent = {}
668
+ for ae in agent_env_associations:
669
+ agent_id = str(ae.agent_id)
670
+ env = db.query(Environment).filter(Environment.id == ae.environment_id).first()
671
+ if env:
672
+ if agent_id not in environments_by_agent:
673
+ environments_by_agent[agent_id] = []
674
+ environments_by_agent[agent_id].append({
675
+ "id": str(env.id),
676
+ "name": env.name,
677
+ "display_name": env.display_name,
678
+ "status": env.status,
679
+ })
680
+
681
+ # Batch fetch skills for all agents (including team inheritance)
682
+ # Collect all unique team IDs
683
+ team_ids = set()
684
+ for agent in agents_list:
685
+ if agent.team_id:
686
+ team_ids.add(agent.team_id)
687
+
688
+ # BATCH FETCH: Get all team skills in one query
689
+ team_skills = {}
690
+ if team_ids:
691
+ team_skill_associations = (
692
+ db.query(SkillAssociation)
693
+ .join(Skill, SkillAssociation.skill_id == Skill.id)
694
+ .filter(
695
+ SkillAssociation.organization_id == organization["id"],
696
+ SkillAssociation.entity_type == "team",
697
+ SkillAssociation.entity_id.in_(team_ids)
698
+ )
699
+ .all()
700
+ )
701
+
702
+ for assoc in team_skill_associations:
703
+ team_id = str(assoc.entity_id)
704
+ skill = db.query(Skill).filter(Skill.id == assoc.skill_id).first()
705
+ if skill and skill.enabled:
706
+ if team_id not in team_skills:
707
+ team_skills[team_id] = []
708
+
709
+ config = skill.configuration or {}
710
+ override = assoc.configuration_override
711
+ if override:
712
+ config = {**config, **override}
713
+
714
+ team_skills[team_id].append({
715
+ "id": str(skill.id),
716
+ "name": skill.name,
717
+ "type": skill.skill_type,
718
+ "description": skill.description,
719
+ "enabled": skill.enabled,
720
+ "configuration": config,
721
+ })
722
+
723
+ # BATCH FETCH: Get all agent skills in one query
724
+ agent_skill_associations = (
725
+ db.query(SkillAssociation)
726
+ .join(Skill, SkillAssociation.skill_id == Skill.id)
727
+ .filter(
728
+ SkillAssociation.organization_id == organization["id"],
729
+ SkillAssociation.entity_type == "agent",
730
+ SkillAssociation.entity_id.in_([agent.id for agent in agents_list])
731
+ )
732
+ .all()
733
+ )
734
+
735
+ agent_direct_skills = {}
736
+ for assoc in agent_skill_associations:
737
+ agent_id = str(assoc.entity_id)
738
+ skill = db.query(Skill).filter(Skill.id == assoc.skill_id).first()
739
+ if skill and skill.enabled:
740
+ if agent_id not in agent_direct_skills:
741
+ agent_direct_skills[agent_id] = []
742
+
743
+ config = skill.configuration or {}
744
+ override = assoc.configuration_override
745
+ if override:
746
+ config = {**config, **override}
747
+
748
+ agent_direct_skills[agent_id].append({
749
+ "id": str(skill.id),
750
+ "name": skill.name,
751
+ "type": skill.skill_type,
752
+ "description": skill.description,
753
+ "enabled": skill.enabled,
754
+ "configuration": config,
755
+ })
756
+
757
+ # Combine team and agent skills with proper inheritance
758
+ skills_by_agent = {}
759
+ for agent in agents_list:
760
+ agent_id = str(agent.id)
761
+ team_id = str(agent.team_id) if agent.team_id else None
762
+
763
+ # Start with empty list
764
+ combined_skills = []
765
+ seen_ids = set()
766
+
767
+ # Add team skills first (if agent is part of a team)
768
+ if team_id and team_id in team_skills:
769
+ for skill in team_skills[team_id]:
770
+ if skill["id"] not in seen_ids:
771
+ combined_skills.append(skill)
772
+ seen_ids.add(skill["id"])
773
+
774
+ # Add agent-specific skills (these override team skills)
775
+ if agent_id in agent_direct_skills:
776
+ for skill in agent_direct_skills[agent_id]:
777
+ if skill["id"] not in seen_ids:
778
+ combined_skills.append(skill)
779
+ seen_ids.add(skill["id"])
780
+
781
+ skills_by_agent[agent_id] = combined_skills
782
+
783
+ agents = []
784
+ for agent in agents_list:
785
+ # Extract system_prompt from configuration
786
+ configuration = agent.configuration or {}
787
+ system_prompt = configuration.get("system_prompt")
788
+
789
+ agent_id = str(agent.id)
790
+
791
+ agents.append(AgentResponse(
792
+ id=agent_id,
793
+ organization_id=agent.organization_id,
794
+ name=agent.name,
795
+ description=agent.description,
796
+ system_prompt=system_prompt,
797
+ status=agent.status,
798
+ capabilities=agent.capabilities,
799
+ configuration=agent.configuration,
800
+ model_id=agent.model_id,
801
+ llm_config=agent.model_config or {},
802
+ runtime=agent.runtime,
803
+ runner_name=agent.runner_name,
804
+ team_id=str(agent.team_id) if agent.team_id else None,
805
+ created_at=agent.created_at.isoformat() if agent.created_at else None,
806
+ updated_at=agent.updated_at.isoformat() if agent.updated_at else None,
807
+ last_active_at=agent.last_active_at.isoformat() if agent.last_active_at else None,
808
+ state=agent.state or {},
809
+ error_message=agent.error_message,
810
+ projects=projects_by_agent.get(agent_id, []),
811
+ environments=environments_by_agent.get(agent_id, []),
812
+ skill_ids=[ts["id"] for ts in skills_by_agent.get(agent_id, [])],
813
+ skills=skills_by_agent.get(agent_id, []),
814
+ execution_environment=(
815
+ ExecutionEnvironment(**agent.execution_environment)
816
+ if agent.execution_environment
817
+ else None
818
+ ),
819
+ ))
820
+
821
+ logger.info(
822
+ "agents_listed",
823
+ count=len(agents),
824
+ org_id=organization["id"],
825
+ org_slug=organization["slug"]
826
+ )
827
+
828
+ return agents
829
+
830
+ except Exception as e:
831
+ logger.error("agents_list_failed", error=str(e), org_id=organization["id"])
832
+ raise HTTPException(
833
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
834
+ detail=f"Failed to list agents: {str(e)}"
835
+ )
836
+
837
+
838
+ @router.get("/{agent_id}", response_model=AgentResponse)
839
+ @instrument_endpoint("agents_v2.get_agent")
840
+ async def get_agent(
841
+ agent_id: str,
842
+ request: Request,
843
+ organization: dict = Depends(get_current_organization),
844
+ db: Session = Depends(get_db),
845
+ ):
846
+ """Get a specific agent by ID"""
847
+ try:
848
+ # Query agent
849
+ agent = db.query(Agent).filter(
850
+ Agent.id == agent_id,
851
+ Agent.organization_id == organization["id"]
852
+ ).first()
853
+
854
+ if not agent:
855
+ raise HTTPException(status_code=404, detail="Agent not found")
856
+
857
+ # DIAGNOSTIC: Log raw agent data from database
858
+ logger.info(
859
+ "get_agent_database_result",
860
+ agent_id=agent_id,
861
+ has_execution_environment=agent.execution_environment is not None,
862
+ execution_environment_type=type(agent.execution_environment).__name__ if agent.execution_environment else None,
863
+ execution_environment_keys=list(agent.execution_environment.keys()) if isinstance(agent.execution_environment, dict) else None,
864
+ has_mcp_servers=bool(agent.execution_environment.get("mcp_servers")) if isinstance(agent.execution_environment, dict) else False,
865
+ org_id=organization["id"]
866
+ )
867
+
868
+ # Get skills with team inheritance
869
+ team_id = str(agent.team_id) if agent.team_id else None
870
+ skills = get_agent_skills_with_inheritance(db, organization["id"], str(agent_id), team_id)
871
+
872
+ # Parse execution_environment if it exists
873
+ execution_env = None
874
+ if agent.execution_environment:
875
+ try:
876
+ execution_env = ExecutionEnvironment(**agent.execution_environment)
877
+ # DIAGNOSTIC: Log parsed execution_environment
878
+ logger.info(
879
+ "get_agent_execution_env_parsed",
880
+ agent_id=agent_id,
881
+ has_mcp_servers=bool(execution_env.mcp_servers) if execution_env else False,
882
+ mcp_server_count=len(execution_env.mcp_servers) if execution_env and execution_env.mcp_servers else 0,
883
+ org_id=organization["id"]
884
+ )
885
+ except Exception as e:
886
+ logger.error(
887
+ "get_agent_execution_env_parse_failed",
888
+ agent_id=agent_id,
889
+ error=str(e),
890
+ raw_value=agent.execution_environment,
891
+ org_id=organization["id"]
892
+ )
893
+ execution_env = None
894
+
895
+ # Extract system_prompt from configuration
896
+ configuration = agent.configuration or {}
897
+ system_prompt = configuration.get("system_prompt")
898
+
899
+ return AgentResponse(
900
+ id=str(agent.id),
901
+ organization_id=agent.organization_id,
902
+ name=agent.name,
903
+ description=agent.description,
904
+ system_prompt=system_prompt,
905
+ status=agent.status,
906
+ capabilities=agent.capabilities,
907
+ configuration=agent.configuration,
908
+ model_id=agent.model_id,
909
+ llm_config=agent.model_config or {},
910
+ runtime=agent.runtime,
911
+ runner_name=agent.runner_name,
912
+ team_id=str(agent.team_id) if agent.team_id else None,
913
+ created_at=agent.created_at.isoformat() if agent.created_at else None,
914
+ updated_at=agent.updated_at.isoformat() if agent.updated_at else None,
915
+ last_active_at=agent.last_active_at.isoformat() if agent.last_active_at else None,
916
+ state=agent.state or {},
917
+ error_message=agent.error_message,
918
+ projects=get_agent_projects(db, str(agent_id)),
919
+ environments=get_agent_environments(db, str(agent_id)),
920
+ skill_ids=[ts["id"] for ts in skills],
921
+ skills=skills,
922
+ execution_environment=execution_env,
923
+ )
924
+
925
+ except HTTPException:
926
+ raise
927
+ except Exception as e:
928
+ logger.error("agent_get_failed", error=str(e), agent_id=agent_id)
929
+ raise HTTPException(
930
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
931
+ detail=f"Failed to get agent: {str(e)}"
932
+ )
933
+
934
+
935
+ @router.patch("/{agent_id}", response_model=AgentResponse)
936
+ @instrument_endpoint("agents_v2.update_agent")
937
+ async def update_agent(
938
+ agent_id: str,
939
+ agent_data: AgentUpdate,
940
+ request: Request,
941
+ organization: dict = Depends(get_current_organization),
942
+ db: Session = Depends(get_db),
943
+ ):
944
+ """Update an agent"""
945
+ try:
946
+ # Check if agent exists and belongs to organization
947
+ agent = db.query(Agent).filter(
948
+ Agent.id == agent_id,
949
+ Agent.organization_id == organization["id"]
950
+ ).first()
951
+
952
+ if not agent:
953
+ raise HTTPException(status_code=404, detail="Agent not found")
954
+
955
+ # Build update dict
956
+ update_data = agent_data.model_dump(exclude_unset=True)
957
+
958
+ # DIAGNOSTIC: Log full request data
959
+ logger.info(
960
+ "update_agent_request",
961
+ agent_id=agent_id,
962
+ has_execution_environment="execution_environment" in update_data,
963
+ execution_environment_keys=list(update_data.get("execution_environment", {}).keys()) if isinstance(update_data.get("execution_environment"), dict) else None,
964
+ has_mcp_servers=bool(update_data.get("execution_environment", {}).get("mcp_servers")) if isinstance(update_data.get("execution_environment"), dict) else False,
965
+ mcp_server_count=len(update_data.get("execution_environment", {}).get("mcp_servers", {})) if isinstance(update_data.get("execution_environment"), dict) else 0,
966
+ org_id=organization["id"]
967
+ )
968
+
969
+ # Extract skill data before database update
970
+ skill_ids = update_data.pop("skill_ids", None)
971
+ skill_configurations = update_data.pop("skill_configurations", None)
972
+
973
+ # Extract environment data before database update (many-to-many via junction table)
974
+ environment_ids = update_data.pop("environment_ids", None)
975
+
976
+ # Extract system_prompt and store it in configuration
977
+ system_prompt = update_data.pop("system_prompt", None)
978
+ if system_prompt is not None:
979
+ # Merge system_prompt into existing configuration
980
+ existing_config = agent.configuration or {}
981
+ merged_config = {**existing_config, "system_prompt": system_prompt}
982
+ update_data["configuration"] = merged_config
983
+
984
+ # Handle model field - prefer 'model' over 'model_id' for backward compatibility
985
+ if "model" in update_data and update_data["model"]:
986
+ update_data["model_id"] = update_data.pop("model")
987
+ elif "model" in update_data:
988
+ # Remove null model field
989
+ update_data.pop("model")
990
+
991
+ # Map llm_config to model_config for database
992
+ if "llm_config" in update_data:
993
+ update_data["model_config"] = update_data.pop("llm_config")
994
+
995
+ # Validate model_id and runtime if being updated
996
+ if "model_id" in update_data or "runtime" in update_data:
997
+ # Merge updates with existing values
998
+ final_model_id = update_data.get("model_id", agent.model_id)
999
+ final_runtime = update_data.get("runtime", agent.runtime or "default")
1000
+ final_config = update_data.get("configuration", agent.configuration or {})
1001
+
1002
+ is_valid, errors = validate_agent_for_runtime(
1003
+ runtime_type=final_runtime,
1004
+ model_id=final_model_id,
1005
+ agent_config=final_config,
1006
+ system_prompt=system_prompt
1007
+ )
1008
+ if not is_valid:
1009
+ error_msg = "Agent validation failed:\n" + "\n".join(f" - {err}" for err in errors)
1010
+ logger.error(
1011
+ "agent_validation_failed",
1012
+ runtime=final_runtime,
1013
+ model_id=final_model_id,
1014
+ errors=errors,
1015
+ org_id=organization["id"]
1016
+ )
1017
+ raise HTTPException(
1018
+ status_code=status.HTTP_400_BAD_REQUEST,
1019
+ detail=error_msg
1020
+ )
1021
+
1022
+ # Handle execution_environment - convert to dict if present
1023
+ if "execution_environment" in update_data and update_data["execution_environment"]:
1024
+ if isinstance(update_data["execution_environment"], ExecutionEnvironment):
1025
+ update_data["execution_environment"] = update_data["execution_environment"].model_dump(by_alias=True)
1026
+ # If None, keep as None to preserve existing value
1027
+
1028
+ # Validate MCP server configuration if being updated
1029
+ if "execution_environment" in update_data and update_data["execution_environment"]:
1030
+ exec_env_dict = update_data["execution_environment"]
1031
+ if exec_env_dict and exec_env_dict.get("mcp_servers"):
1032
+ try:
1033
+ mcp_validation = validate_execution_environment_mcp(
1034
+ exec_env_dict,
1035
+ strict=False
1036
+ )
1037
+
1038
+ if not mcp_validation["valid"]:
1039
+ error_msg = "MCP configuration validation failed:\n" + "\n".join(
1040
+ f" - {err}" for err in mcp_validation["errors"]
1041
+ )
1042
+ logger.error(
1043
+ "mcp_validation_failed",
1044
+ agent_id=agent_id,
1045
+ errors=mcp_validation["errors"],
1046
+ org_id=organization["id"]
1047
+ )
1048
+ raise HTTPException(status_code=400, detail=error_msg)
1049
+
1050
+ if mcp_validation["warnings"]:
1051
+ logger.warning(
1052
+ "mcp_validation_warnings",
1053
+ agent_id=agent_id,
1054
+ warnings=mcp_validation["warnings"],
1055
+ required_secrets=mcp_validation.get("required_secrets", []),
1056
+ required_env_vars=mcp_validation.get("required_env_vars", []),
1057
+ org_id=organization["id"]
1058
+ )
1059
+
1060
+ logger.info(
1061
+ "mcp_validation_passed",
1062
+ agent_id=agent_id,
1063
+ server_count=len(exec_env_dict.get("mcp_servers", {})),
1064
+ required_secrets=mcp_validation.get("required_secrets", []),
1065
+ required_env_vars=mcp_validation.get("required_env_vars", []),
1066
+ org_id=organization["id"]
1067
+ )
1068
+ except MCPValidationError as e:
1069
+ logger.error(
1070
+ "mcp_validation_error",
1071
+ agent_id=agent_id,
1072
+ error=str(e),
1073
+ org_id=organization["id"]
1074
+ )
1075
+ raise HTTPException(status_code=400, detail=str(e))
1076
+
1077
+ # Note: skill_ids is not stored in agents table - skills are tracked via skill_associations junction table
1078
+ # The skill associations will be updated separately below if skill_ids was provided
1079
+
1080
+ update_data["updated_at"] = datetime.utcnow()
1081
+
1082
+ # DIAGNOSTIC: Log what's being sent to database
1083
+ logger.info(
1084
+ "update_agent_database_update",
1085
+ agent_id=agent_id,
1086
+ update_keys=list(update_data.keys()),
1087
+ has_execution_environment="execution_environment" in update_data,
1088
+ execution_environment_value=update_data.get("execution_environment"),
1089
+ org_id=organization["id"]
1090
+ )
1091
+
1092
+ # Update agent fields
1093
+ for key, value in update_data.items():
1094
+ setattr(agent, key, value)
1095
+
1096
+ db.commit()
1097
+ db.refresh(agent)
1098
+
1099
+ # DIAGNOSTIC: Log database result
1100
+ logger.info(
1101
+ "update_agent_database_result",
1102
+ agent_id=agent_id,
1103
+ success=True,
1104
+ returned_execution_environment=agent.execution_environment,
1105
+ org_id=organization["id"]
1106
+ )
1107
+
1108
+ # Update skill associations if skill_ids was provided
1109
+ if skill_ids is not None:
1110
+ # VALIDATION: Prevent removing all skills from an agent
1111
+ if len(skill_ids) == 0:
1112
+ raise HTTPException(
1113
+ status_code=status.HTTP_400_BAD_REQUEST,
1114
+ detail="Cannot remove all skills from an agent. At least one skill is required for agent functionality."
1115
+ )
1116
+
1117
+ try:
1118
+ # Delete existing associations (scoped to organization)
1119
+ db.query(SkillAssociation).filter(
1120
+ SkillAssociation.organization_id == organization["id"],
1121
+ SkillAssociation.entity_type == "agent",
1122
+ SkillAssociation.entity_id == agent_id
1123
+ ).delete()
1124
+
1125
+ # Create new associations
1126
+ now = datetime.utcnow()
1127
+ for skill_id in skill_ids:
1128
+ config_override = (skill_configurations or {}).get(skill_id, {})
1129
+
1130
+ skill_association = SkillAssociation(
1131
+ id=uuid.uuid4(),
1132
+ organization_id=organization["id"],
1133
+ skill_id=skill_id,
1134
+ entity_type="agent",
1135
+ entity_id=agent_id,
1136
+ configuration_override=config_override,
1137
+ created_at=now,
1138
+ )
1139
+ db.add(skill_association)
1140
+
1141
+ db.commit()
1142
+ logger.info(
1143
+ "agent_skills_updated",
1144
+ agent_id=agent_id,
1145
+ skill_count=len(skill_ids),
1146
+ org_id=organization["id"]
1147
+ )
1148
+ except Exception as e:
1149
+ db.rollback()
1150
+ logger.error(
1151
+ "failed_to_update_agent_skills",
1152
+ error=str(e),
1153
+ agent_id=agent_id,
1154
+ org_id=organization["id"]
1155
+ )
1156
+ raise HTTPException(
1157
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1158
+ detail=f"Failed to update agent skills: {str(e)}"
1159
+ )
1160
+
1161
+ # Update environment associations if environment_ids was provided
1162
+ if environment_ids is not None:
1163
+ try:
1164
+ # Delete existing environment associations
1165
+ db.query(AgentEnvironment).filter(
1166
+ AgentEnvironment.agent_id == agent_id
1167
+ ).delete()
1168
+
1169
+ # Create new environment associations
1170
+ for environment_id in environment_ids:
1171
+ agent_env = AgentEnvironment(
1172
+ id=uuid.uuid4(),
1173
+ agent_id=agent_id,
1174
+ environment_id=environment_id,
1175
+ organization_id=organization["id"],
1176
+ assigned_at=datetime.utcnow(),
1177
+ )
1178
+ db.add(agent_env)
1179
+
1180
+ db.commit()
1181
+ logger.info(
1182
+ "agent_environments_updated",
1183
+ agent_id=agent_id,
1184
+ environment_count=len(environment_ids),
1185
+ org_id=organization["id"]
1186
+ )
1187
+ except Exception as e:
1188
+ db.rollback()
1189
+ logger.warning(
1190
+ "failed_to_update_agent_environments",
1191
+ error=str(e),
1192
+ agent_id=agent_id,
1193
+ org_id=organization["id"]
1194
+ )
1195
+
1196
+ logger.info(
1197
+ "agent_updated",
1198
+ agent_id=agent_id,
1199
+ org_id=organization["id"],
1200
+ fields_updated=list(update_data.keys())
1201
+ )
1202
+
1203
+ # Get skills with team inheritance
1204
+ team_id = str(agent.team_id) if agent.team_id else None
1205
+ skills = get_agent_skills_with_inheritance(db, organization["id"], agent_id, team_id)
1206
+
1207
+ # Parse execution_environment if it exists
1208
+ execution_env = None
1209
+ if agent.execution_environment:
1210
+ try:
1211
+ execution_env = ExecutionEnvironment(**agent.execution_environment)
1212
+ except Exception:
1213
+ execution_env = None
1214
+
1215
+ # Extract system_prompt from configuration
1216
+ configuration = agent.configuration or {}
1217
+ system_prompt = configuration.get("system_prompt")
1218
+
1219
+ return AgentResponse(
1220
+ id=str(agent.id),
1221
+ organization_id=agent.organization_id,
1222
+ name=agent.name,
1223
+ description=agent.description,
1224
+ system_prompt=system_prompt,
1225
+ status=agent.status,
1226
+ capabilities=agent.capabilities,
1227
+ configuration=agent.configuration,
1228
+ model_id=agent.model_id,
1229
+ llm_config=agent.model_config or {},
1230
+ runtime=agent.runtime,
1231
+ runner_name=agent.runner_name,
1232
+ team_id=str(agent.team_id) if agent.team_id else None,
1233
+ created_at=agent.created_at.isoformat() if agent.created_at else None,
1234
+ updated_at=agent.updated_at.isoformat() if agent.updated_at else None,
1235
+ last_active_at=agent.last_active_at.isoformat() if agent.last_active_at else None,
1236
+ state=agent.state or {},
1237
+ error_message=agent.error_message,
1238
+ projects=get_agent_projects(db, agent_id),
1239
+ environments=get_agent_environments(db, agent_id),
1240
+ skill_ids=[ts["id"] for ts in skills],
1241
+ skills=skills,
1242
+ execution_environment=execution_env,
1243
+ )
1244
+
1245
+ except HTTPException:
1246
+ raise
1247
+ except Exception as e:
1248
+ db.rollback()
1249
+ logger.error("agent_update_failed", error=str(e), agent_id=agent_id)
1250
+ raise HTTPException(
1251
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1252
+ detail=f"Failed to update agent: {str(e)}"
1253
+ )
1254
+
1255
+
1256
+ @router.delete("/{agent_id}", status_code=status.HTTP_204_NO_CONTENT)
1257
+ @instrument_endpoint("agents_v2.delete_agent")
1258
+ async def delete_agent(
1259
+ agent_id: str,
1260
+ request: Request,
1261
+ organization: dict = Depends(get_current_organization),
1262
+ db: Session = Depends(get_db),
1263
+ ):
1264
+ """Delete an agent"""
1265
+ try:
1266
+ # Find the agent first
1267
+ agent = db.query(Agent).filter(
1268
+ Agent.id == agent_id,
1269
+ Agent.organization_id == organization["id"]
1270
+ ).first()
1271
+
1272
+ if not agent:
1273
+ raise HTTPException(status_code=404, detail="Agent not found")
1274
+
1275
+ # Delete the agent (cascading deletes will handle related records)
1276
+ db.delete(agent)
1277
+ db.commit()
1278
+
1279
+ logger.info("agent_deleted", agent_id=agent_id, org_id=organization["id"])
1280
+
1281
+ return None
1282
+
1283
+ except HTTPException:
1284
+ raise
1285
+ except Exception as e:
1286
+ logger.error("agent_delete_failed", error=str(e), agent_id=agent_id)
1287
+ raise HTTPException(
1288
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1289
+ detail=f"Failed to delete agent: {str(e)}"
1290
+ )
1291
+
1292
+
1293
+ @router.post("/{agent_id}/execute", response_model=AgentExecutionResponse)
1294
+ @instrument_endpoint("agents_v2.execute_agent")
1295
+ async def execute_agent(
1296
+ agent_id: str,
1297
+ execution_request: AgentExecutionRequest,
1298
+ request: Request,
1299
+ organization: dict = Depends(get_current_organization),
1300
+ db: Session = Depends(get_db),
1301
+ ):
1302
+ """
1303
+ Execute an agent by submitting to Temporal workflow.
1304
+
1305
+ This creates an execution record and starts a Temporal workflow.
1306
+ The actual execution happens asynchronously on the Temporal worker.
1307
+
1308
+ The runner_name should come from the Composer UI where user selects
1309
+ from available runners (fetched from Kubiya API /api/v1/runners).
1310
+ """
1311
+ try:
1312
+ # Get agent details
1313
+ agent = db.query(Agent).filter(
1314
+ Agent.id == agent_id,
1315
+ Agent.organization_id == organization["id"]
1316
+ ).first()
1317
+
1318
+ if not agent:
1319
+ raise HTTPException(status_code=404, detail="Agent not found")
1320
+
1321
+ # Create execution record
1322
+ execution_id = uuid.uuid4()
1323
+ now = datetime.utcnow()
1324
+
1325
+ # Validate and get worker queue
1326
+ worker_queue_id = execution_request.worker_queue_id
1327
+
1328
+ worker_queue = db.query(WorkerQueue).filter(
1329
+ WorkerQueue.id == worker_queue_id,
1330
+ WorkerQueue.organization_id == organization["id"]
1331
+ ).first()
1332
+
1333
+ if not worker_queue:
1334
+ raise HTTPException(
1335
+ status_code=status.HTTP_404_NOT_FOUND,
1336
+ detail=f"Worker queue '{worker_queue_id}' not found. Please select a valid worker queue."
1337
+ )
1338
+
1339
+ # Check if queue has active workers
1340
+ if worker_queue.status != "active":
1341
+ raise HTTPException(
1342
+ status_code=status.HTTP_400_BAD_REQUEST,
1343
+ detail=f"Worker queue '{worker_queue.name}' is not active"
1344
+ )
1345
+
1346
+ # Extract user metadata - ALWAYS use JWT-decoded organization data as source of truth
1347
+ user_metadata = execution_request.user_metadata or {}
1348
+ # Override with JWT data (user can't spoof their identity)
1349
+ user_metadata["user_id"] = organization.get("user_id")
1350
+ user_metadata["user_email"] = organization.get("user_email")
1351
+ user_metadata["user_name"] = organization.get("user_name")
1352
+ # Keep user_avatar from request if provided (not in JWT)
1353
+ if not user_metadata.get("user_avatar"):
1354
+ user_metadata["user_avatar"] = None
1355
+
1356
+ execution = Execution(
1357
+ id=execution_id,
1358
+ organization_id=organization["id"],
1359
+ execution_type=ExecutionType.AGENT,
1360
+ entity_id=agent_id,
1361
+ entity_name=agent.name,
1362
+ prompt=execution_request.prompt,
1363
+ system_prompt=execution_request.system_prompt,
1364
+ status=ExecutionStatus.PENDING,
1365
+ worker_queue_id=worker_queue_id,
1366
+ runner_name=worker_queue.name, # Store queue name for display
1367
+ user_id=user_metadata.get("user_id"),
1368
+ user_name=user_metadata.get("user_name"),
1369
+ user_email=user_metadata.get("user_email"),
1370
+ user_avatar=user_metadata.get("user_avatar"),
1371
+ usage={},
1372
+ execution_metadata={
1373
+ "kubiya_org_id": organization["id"],
1374
+ "kubiya_org_name": organization["name"],
1375
+ "worker_queue_name": worker_queue.display_name or worker_queue.name,
1376
+ },
1377
+ created_at=now,
1378
+ updated_at=now,
1379
+ )
1380
+
1381
+ db.add(execution)
1382
+ db.commit()
1383
+ db.refresh(execution)
1384
+
1385
+ # Add creator as the first participant (owner role) for multiplayer support
1386
+ user_id = user_metadata.get("user_id")
1387
+ if user_id:
1388
+ try:
1389
+ participant = ExecutionParticipant(
1390
+ id=uuid.uuid4(),
1391
+ execution_id=execution_id,
1392
+ organization_id=organization["id"],
1393
+ user_id=user_id,
1394
+ user_name=user_metadata.get("user_name"),
1395
+ user_email=user_metadata.get("user_email"),
1396
+ user_avatar=user_metadata.get("user_avatar"),
1397
+ role=ParticipantRole.OWNER,
1398
+ )
1399
+ db.add(participant)
1400
+ db.commit()
1401
+ logger.info(
1402
+ "owner_participant_added",
1403
+ execution_id=str(execution_id),
1404
+ user_id=user_id,
1405
+ )
1406
+ except Exception as participant_error:
1407
+ db.rollback()
1408
+ logger.warning(
1409
+ "failed_to_add_owner_participant",
1410
+ error=str(participant_error),
1411
+ execution_id=str(execution_id),
1412
+ )
1413
+ # Don't fail execution creation if participant tracking fails
1414
+
1415
+ # Get resolved execution environment with templates compiled
1416
+ # This includes MCP servers with all {{.secret.x}} and {{.env.X}} resolved
1417
+ # Call internal function directly to avoid HTTP/auth issues
1418
+ resolved_env = {} # Initialize to empty dict to avoid UnboundLocalError
1419
+ try:
1420
+ from control_plane_api.app.routers.execution_environment import resolve_agent_execution_environment_internal
1421
+
1422
+ # Get token from request
1423
+ token = request.state.kubiya_token
1424
+
1425
+ resolved_env = await resolve_agent_execution_environment_internal(
1426
+ agent_id=agent_id,
1427
+ org_id=organization["id"],
1428
+ db=db,
1429
+ token=token
1430
+ )
1431
+
1432
+ mcp_servers = resolved_env.get("mcp_servers", {})
1433
+ resolved_system_prompt = resolved_env.get("system_prompt")
1434
+ resolved_description = resolved_env.get("description")
1435
+
1436
+ # DEBUG: Log detailed MCP server info
1437
+ logger.info(
1438
+ "🔍 DEBUG: execution_environment_resolved_for_execution",
1439
+ agent_id=agent_id[:8],
1440
+ mcp_server_count=len(mcp_servers),
1441
+ mcp_server_names=list(mcp_servers.keys()) if mcp_servers else [],
1442
+ has_resolved_prompt=bool(resolved_system_prompt)
1443
+ )
1444
+
1445
+ if mcp_servers:
1446
+ for server_name, server_config in mcp_servers.items():
1447
+ logger.info(
1448
+ "🔍 DEBUG: MCP server config from API",
1449
+ server_name=server_name,
1450
+ has_url="url" in server_config,
1451
+ has_headers="headers" in server_config,
1452
+ has_transport="transport_type" in server_config or "type" in server_config
1453
+ )
1454
+ else:
1455
+ logger.warning(
1456
+ "🔍 DEBUG: NO MCP SERVERS returned from execution env resolution",
1457
+ agent_id=agent_id[:8],
1458
+ resolved_env_keys=list(resolved_env.keys())
1459
+ )
1460
+
1461
+ except Exception as e:
1462
+ logger.error(
1463
+ "execution_environment_resolution_error",
1464
+ agent_id=agent_id[:8],
1465
+ error=str(e),
1466
+ exc_info=True
1467
+ )
1468
+ # Don't fallback to old configuration.mcpServers format
1469
+ # MCP servers should only come from execution_environment.mcp_servers
1470
+ agent_configuration = agent.configuration or {}
1471
+ if "mcpServers" in agent_configuration:
1472
+ logger.warning(
1473
+ "ignoring_legacy_mcp_servers_in_configuration",
1474
+ agent_id=agent_id[:8],
1475
+ legacy_servers=list(agent_configuration.get("mcpServers", {}).keys()),
1476
+ recommendation="Move MCP servers to execution_environment.mcp_servers"
1477
+ )
1478
+ mcp_servers = {} # Don't use old format
1479
+ resolved_system_prompt = None
1480
+ resolved_description = None
1481
+
1482
+ # Use resolved system prompt if available, otherwise use original
1483
+ agent_configuration = agent.configuration or {}
1484
+
1485
+ # Override agent_config with execution_environment.working_dir if provided
1486
+ if execution_request.execution_environment and execution_request.execution_environment.working_dir:
1487
+ agent_configuration = agent_configuration.copy()
1488
+ agent_configuration["cwd"] = execution_request.execution_environment.working_dir
1489
+ logger.info(
1490
+ "execution_working_dir_override",
1491
+ execution_id=str(execution_id),
1492
+ working_dir=execution_request.execution_environment.working_dir,
1493
+ )
1494
+
1495
+ # Submit to Temporal workflow
1496
+ # Task queue is the worker queue UUID
1497
+ task_queue = str(worker_queue_id)
1498
+
1499
+ # Use shared agent execution Temporal client (from env vars)
1500
+ # Agent executions run in a shared namespace (agent-control-plane.lpagu in us-east-1)
1501
+ # NOT in org-specific namespaces like the plan worker
1502
+ from control_plane_api.app.lib.temporal_client import get_temporal_client
1503
+
1504
+ temporal_client = await get_temporal_client()
1505
+
1506
+ # Start workflow
1507
+ # Use resolved system prompt (with templates compiled) if available
1508
+ # Priority: request > resolved > configuration > agent.system_prompt
1509
+ system_prompt = (
1510
+ execution_request.system_prompt or
1511
+ resolved_system_prompt or
1512
+ agent_configuration.get("system_prompt") or
1513
+ agent.system_prompt
1514
+ )
1515
+
1516
+ # Get API key from Authorization header
1517
+ auth_header = request.headers.get("authorization", "")
1518
+ api_key = auth_header.replace("UserKey ", "").replace("Bearer ", "") if auth_header else None
1519
+
1520
+ # Get control plane URL from request
1521
+ control_plane_url = str(request.base_url).rstrip("/")
1522
+
1523
+ # CRITICAL: Use real-time timestamp for initial message to ensure chronological ordering
1524
+ # This prevents timestamp mismatches between initial and follow-up messages
1525
+ initial_timestamp = datetime.now(timezone.utc).isoformat()
1526
+
1527
+ workflow_input = AgentExecutionInput(
1528
+ execution_id=str(execution_id),
1529
+ agent_id=str(agent_id),
1530
+ organization_id=organization["id"],
1531
+ prompt=execution_request.prompt,
1532
+ system_prompt=system_prompt,
1533
+ model_id=agent.model_id,
1534
+ model_config=agent.model_config or {},
1535
+ agent_config=agent_configuration,
1536
+ mcp_servers=mcp_servers,
1537
+ user_metadata=user_metadata,
1538
+ runtime_type=agent.runtime or "default",
1539
+ control_plane_url=control_plane_url,
1540
+ api_key=api_key,
1541
+ initial_message_timestamp=initial_timestamp,
1542
+ graph_api_url=resolved_env.get("graph_api_url"),
1543
+ dataset_name=resolved_env.get("dataset_name"),
1544
+ )
1545
+
1546
+ # DEBUG: Log workflow input MCP servers
1547
+ logger.info(
1548
+ "🔍 DEBUG: Starting workflow with MCP servers",
1549
+ execution_id=str(execution_id),
1550
+ mcp_servers_count=len(mcp_servers),
1551
+ mcp_servers_type=str(type(mcp_servers)),
1552
+ mcp_server_names=list(mcp_servers.keys()) if mcp_servers else []
1553
+ )
1554
+
1555
+ workflow_handle = await temporal_client.start_workflow(
1556
+ AgentExecutionWorkflow.run,
1557
+ workflow_input,
1558
+ id=f"agent-execution-{execution_id}",
1559
+ task_queue=task_queue,
1560
+ )
1561
+
1562
+ # Get namespace from env for logging (agent execution uses shared namespace)
1563
+ temporal_namespace = os.getenv("TEMPORAL_NAMESPACE", "agent-control-plane.lpagu")
1564
+
1565
+ logger.info(
1566
+ "agent_execution_submitted",
1567
+ execution_id=str(execution_id),
1568
+ agent_id=str(agent_id),
1569
+ workflow_id=workflow_handle.id,
1570
+ task_queue=task_queue,
1571
+ temporal_namespace=temporal_namespace,
1572
+ worker_queue_id=str(worker_queue_id),
1573
+ worker_queue_name=worker_queue.name,
1574
+ org_id=organization["id"],
1575
+ org_name=organization["name"],
1576
+ )
1577
+
1578
+ return AgentExecutionResponse(
1579
+ execution_id=str(execution_id),
1580
+ workflow_id=workflow_handle.id,
1581
+ status="PENDING",
1582
+ message=f"Execution submitted to worker queue: {worker_queue.name}",
1583
+ )
1584
+
1585
+ except HTTPException:
1586
+ raise
1587
+ except Exception as e:
1588
+ db.rollback()
1589
+ logger.error(
1590
+ "agent_execution_failed",
1591
+ error=str(e),
1592
+ agent_id=str(agent_id),
1593
+ org_id=organization["id"]
1594
+ )
1595
+ raise HTTPException(
1596
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1597
+ detail=f"Failed to execute agent: {str(e)}"
1598
+ )