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,1063 @@
1
+ """
2
+ Agno runtime implementation.
3
+
4
+ This is the main runtime class that provides Agno framework integration
5
+ for the Agent Control Plane.
6
+ """
7
+
8
+ import asyncio
9
+ import queue
10
+ import threading
11
+ import time
12
+ import structlog
13
+ from typing import Dict, Any, Optional, AsyncIterator, Callable, TYPE_CHECKING
14
+
15
+ from ..base import (
16
+ RuntimeType,
17
+ RuntimeExecutionResult,
18
+ RuntimeExecutionContext,
19
+ RuntimeCapabilities,
20
+ BaseRuntime,
21
+ RuntimeRegistry,
22
+ )
23
+ from control_plane_api.worker.services.event_publisher import (
24
+ EventPublisher,
25
+ EventPublisherConfig,
26
+ EventPriority,
27
+ )
28
+ from control_plane_api.worker.utils.tool_validation import (
29
+ validate_and_sanitize_tools,
30
+ sanitize_tool_name,
31
+ )
32
+
33
+ from .config import build_agno_agent_config
34
+ from .hooks import create_tool_hook_for_streaming, create_tool_hook_with_callback
35
+ from .mcp_builder import build_agno_mcp_tools
36
+ from .utils import (
37
+ build_conversation_messages,
38
+ extract_usage,
39
+ extract_tool_messages,
40
+ extract_response_content,
41
+ )
42
+
43
+ if TYPE_CHECKING:
44
+ from control_plane_client import ControlPlaneClient
45
+ from services.cancellation_manager import CancellationManager
46
+
47
+ logger = structlog.get_logger(__name__)
48
+
49
+
50
+ @RuntimeRegistry.register(RuntimeType.DEFAULT)
51
+ class AgnoRuntime(BaseRuntime):
52
+ """
53
+ Runtime implementation using Agno framework.
54
+
55
+ This runtime wraps the Agno-based agent execution logic,
56
+ providing a clean interface that conforms to the AgentRuntime protocol.
57
+
58
+ Features:
59
+ - LiteLLM-based model execution
60
+ - Real-time streaming with event batching
61
+ - Tool execution hooks
62
+ - Conversation history support
63
+ - Comprehensive usage tracking
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ control_plane_client: "ControlPlaneClient",
69
+ cancellation_manager: "CancellationManager",
70
+ **kwargs,
71
+ ):
72
+ """
73
+ Initialize the Agno runtime.
74
+
75
+ Args:
76
+ control_plane_client: Client for Control Plane API
77
+ cancellation_manager: Manager for execution cancellation
78
+ **kwargs: Additional configuration options
79
+ """
80
+ super().__init__(control_plane_client, cancellation_manager, **kwargs)
81
+ self._custom_tools: Dict[str, Any] = {} # tool_id -> tool instance
82
+
83
+ def get_runtime_type(self) -> RuntimeType:
84
+ """Return RuntimeType.DEFAULT."""
85
+ return RuntimeType.DEFAULT
86
+
87
+ def get_capabilities(self) -> RuntimeCapabilities:
88
+ """Return Agno runtime capabilities."""
89
+ return RuntimeCapabilities(
90
+ streaming=True,
91
+ tools=True,
92
+ mcp=True, # Agno supports MCP via MCPTools
93
+ hooks=True,
94
+ cancellation=True,
95
+ conversation_history=True,
96
+ custom_tools=True # Agno supports custom Python tools
97
+ )
98
+
99
+ def _validate_mcp_tool_names(self, mcp_tool: Any, execution_id: str) -> Any:
100
+ """
101
+ Validate and sanitize MCP tool function names.
102
+
103
+ This ensures MCP tools from external servers meet universal LLM provider requirements.
104
+ Agno's MCPTools.functions property contains the actual tool definitions.
105
+
106
+ Args:
107
+ mcp_tool: Connected MCPTools instance
108
+ execution_id: Execution ID for logging
109
+
110
+ Returns:
111
+ The MCP tool instance with validated function names
112
+ """
113
+ if not hasattr(mcp_tool, 'functions') or not mcp_tool.functions:
114
+ return mcp_tool
115
+
116
+ server_name = getattr(mcp_tool, '_server_name', mcp_tool.name)
117
+
118
+ # Convert functions dict to list of Function objects for validation
119
+ # mcp_tool.functions is a Dict[str, Function], we need to validate the Function objects
120
+ functions_list = list(mcp_tool.functions.values())
121
+
122
+ # Validate and sanitize the function names
123
+ validated_functions, validation_report = validate_and_sanitize_tools(
124
+ functions_list,
125
+ tool_name_getter=lambda f: getattr(f, 'name', str(f)),
126
+ auto_fix=True,
127
+ provider_context=f"agno_mcp_{server_name}"
128
+ )
129
+
130
+ # Log validation results
131
+ sanitized_count = sum(1 for r in validation_report if r['action'] == 'sanitized')
132
+ filtered_count = sum(1 for r in validation_report if r['action'] == 'filtered')
133
+
134
+ if sanitized_count > 0:
135
+ self.logger.warning(
136
+ "mcp_tool_names_sanitized",
137
+ server_name=server_name,
138
+ execution_id=execution_id,
139
+ sanitized_count=sanitized_count,
140
+ total_functions=len(mcp_tool.functions),
141
+ details=[r for r in validation_report if r['action'] == 'sanitized'][:5] # Limit details
142
+ )
143
+
144
+ if filtered_count > 0:
145
+ self.logger.error(
146
+ "mcp_tool_names_filtered",
147
+ server_name=server_name,
148
+ execution_id=execution_id,
149
+ filtered_count=filtered_count,
150
+ total_functions=len(mcp_tool.functions),
151
+ details=[r for r in validation_report if r['action'] == 'filtered']
152
+ )
153
+
154
+ # Reconstruct the functions dict from validated Function objects
155
+ # Preserve the Dict[str, Function] structure that Agno expects
156
+ from collections import OrderedDict
157
+ validated_functions_dict = OrderedDict()
158
+ for func in validated_functions:
159
+ func_name = getattr(func, 'name', str(func))
160
+ validated_functions_dict[func_name] = func
161
+
162
+ # Update the MCP tool with validated functions dict
163
+ mcp_tool.functions = validated_functions_dict
164
+
165
+ return mcp_tool
166
+
167
+ async def _execute_impl(
168
+ self, context: RuntimeExecutionContext
169
+ ) -> RuntimeExecutionResult:
170
+ """
171
+ Execute agent using Agno framework without streaming.
172
+
173
+ Args:
174
+ context: Execution context with prompt, history, config
175
+
176
+ Returns:
177
+ RuntimeExecutionResult with response and metadata
178
+ """
179
+ mcp_tools_instances = []
180
+
181
+ try:
182
+ # Build MCP tools from context
183
+ mcp_tools_instances = build_agno_mcp_tools(context.mcp_servers)
184
+
185
+ # Connect MCP tools
186
+ connected_mcp_tools = []
187
+ for mcp_tool in mcp_tools_instances:
188
+ try:
189
+ await mcp_tool.connect()
190
+
191
+ # Verify the tool is actually initialized (agno doesn't raise on failure)
192
+ if not mcp_tool.initialized:
193
+ server_name = getattr(mcp_tool, '_server_name', mcp_tool.name)
194
+ error_msg = f"Failed to initialize MCP tool: {server_name}"
195
+ self.logger.error(
196
+ "mcp_tool_initialization_failed",
197
+ server_name=server_name,
198
+ execution_id=context.execution_id,
199
+ error=error_msg,
200
+ )
201
+ raise RuntimeError(error_msg)
202
+
203
+ # Verify it has tools available
204
+ if not mcp_tool.functions:
205
+ server_name = getattr(mcp_tool, '_server_name', mcp_tool.name)
206
+ error_msg = f"MCP tool {server_name} has no functions available"
207
+ self.logger.error(
208
+ "mcp_tool_has_no_functions",
209
+ server_name=server_name,
210
+ execution_id=context.execution_id,
211
+ error=error_msg,
212
+ )
213
+ raise RuntimeError(error_msg)
214
+
215
+ self.logger.info(
216
+ "mcp_tool_connected",
217
+ execution_id=context.execution_id,
218
+ server_name=getattr(mcp_tool, '_server_name', mcp_tool.name),
219
+ function_count=len(mcp_tool.functions),
220
+ )
221
+
222
+ # UNIVERSAL VALIDATION: Validate MCP tool names
223
+ validated_mcp_tool = self._validate_mcp_tool_names(mcp_tool, context.execution_id)
224
+ connected_mcp_tools.append(validated_mcp_tool)
225
+
226
+ except Exception as e:
227
+ server_name = getattr(mcp_tool, '_server_name', mcp_tool.name)
228
+ self.logger.error(
229
+ "mcp_tool_connection_failed",
230
+ error=str(e),
231
+ error_type=type(e).__name__,
232
+ server_name=server_name,
233
+ execution_id=context.execution_id,
234
+ )
235
+ import traceback
236
+ self.logger.debug(
237
+ "mcp_tool_connection_error_traceback",
238
+ traceback=traceback.format_exc(),
239
+ execution_id=context.execution_id,
240
+ )
241
+
242
+ # Publish MCP connection error event
243
+ try:
244
+ from control_plane_api.worker.utils.error_publisher import (
245
+ ErrorEventPublisher, ErrorSeverity, ErrorCategory
246
+ )
247
+ error_publisher = ErrorEventPublisher(self.control_plane)
248
+ await error_publisher.publish_error(
249
+ execution_id=context.execution_id,
250
+ exception=e,
251
+ severity=ErrorSeverity.WARNING,
252
+ category=ErrorCategory.MCP_CONNECTION,
253
+ stage="initialization",
254
+ component="mcp_server",
255
+ operation=f"connect_{server_name}",
256
+ metadata={"server_name": server_name},
257
+ recovery_actions=[
258
+ "Verify MCP server is running and accessible",
259
+ "Check MCP server configuration",
260
+ "Review network connectivity",
261
+ ],
262
+ )
263
+ except Exception as publish_error:
264
+ # Log warning but don't block execution
265
+ self.logger.warning(
266
+ f"Failed to publish MCP connection error: {publish_error}",
267
+ execution_id=context.execution_id,
268
+ )
269
+
270
+ # Continue with other MCP tools even if one fails
271
+
272
+ # Use only successfully connected tools
273
+ mcp_tools_instances = connected_mcp_tools
274
+
275
+ # Merge regular skills with custom tools
276
+ # IMPORTANT: Deep copy skills to isolate Function objects between executions
277
+ # This prevents schema corruption from shared mutable state in Function.parameters
278
+ from copy import deepcopy
279
+
280
+ all_skills = []
281
+ if context.skills:
282
+ for skill in context.skills:
283
+ try:
284
+ # Deep copy the skill to ensure Function objects are isolated
285
+ # This prevents process_entrypoint() from modifying shared state
286
+ if hasattr(skill, 'functions') and hasattr(skill.functions, 'items'):
287
+ copied_skill = deepcopy(skill)
288
+ all_skills.append(copied_skill)
289
+ self.logger.debug(
290
+ "skill_deep_copied",
291
+ skill_name=getattr(skill, 'name', 'unknown'),
292
+ function_count=len(skill.functions) if hasattr(skill, 'functions') else 0,
293
+ execution_id=context.execution_id,
294
+ )
295
+ else:
296
+ # For non-Toolkit skills, use as-is
297
+ all_skills.append(skill)
298
+ except Exception as e:
299
+ # If deep copy fails, fall back to original skill and log warning
300
+ self.logger.warning(
301
+ "skill_deep_copy_failed_using_original",
302
+ skill_name=getattr(skill, 'name', 'unknown'),
303
+ error=str(e),
304
+ error_type=type(e).__name__,
305
+ execution_id=context.execution_id,
306
+ )
307
+ all_skills.append(skill)
308
+
309
+ # Add custom tools
310
+ if self._custom_tools:
311
+ for tool_id, custom_tool in self._custom_tools.items():
312
+ try:
313
+ # Get toolkit from custom tool
314
+ toolkit = custom_tool.get_tools()
315
+
316
+ # Extract tools - handle both Toolkit objects and iterables
317
+ if hasattr(toolkit, 'tools'):
318
+ all_skills.extend(toolkit.tools)
319
+ elif hasattr(toolkit, '__iter__'):
320
+ all_skills.extend(toolkit)
321
+ else:
322
+ all_skills.append(toolkit)
323
+
324
+ self.logger.debug(
325
+ "custom_tool_loaded",
326
+ tool_id=tool_id,
327
+ execution_id=context.execution_id
328
+ )
329
+ except Exception as e:
330
+ self.logger.error(
331
+ "custom_tool_load_failed",
332
+ tool_id=tool_id,
333
+ error=str(e),
334
+ execution_id=context.execution_id
335
+ )
336
+
337
+ # Extract metadata for Langfuse tracking
338
+ user_id = None
339
+ session_id = None
340
+ agent_name = None
341
+
342
+ if context.user_metadata:
343
+ user_id = context.user_metadata.get("user_email") or context.user_metadata.get("user_id")
344
+ session_id = context.user_metadata.get("session_id") or context.execution_id
345
+ agent_name = context.user_metadata.get("agent_name") or context.agent_id
346
+
347
+ # DEBUG: Log metadata extraction
348
+ self.logger.warning(
349
+ "🔍 DEBUG: AGNO RUNTIME (_execute_impl) - METADATA EXTRACTION",
350
+ context_user_metadata=context.user_metadata,
351
+ extracted_user_id=user_id,
352
+ extracted_session_id=session_id,
353
+ extracted_agent_name=agent_name,
354
+ organization_id=context.organization_id,
355
+ )
356
+
357
+ # Create Agno agent with all tools (skills + MCP tools) and metadata
358
+ agent = build_agno_agent_config(
359
+ agent_id=context.agent_id,
360
+ system_prompt=context.system_prompt,
361
+ model_id=context.model_id,
362
+ skills=all_skills,
363
+ mcp_tools=mcp_tools_instances,
364
+ tool_hooks=None,
365
+ user_id=user_id,
366
+ session_id=session_id,
367
+ organization_id=context.organization_id,
368
+ agent_name=agent_name,
369
+ skill_configs=context.skill_configs,
370
+ user_metadata=context.user_metadata,
371
+ )
372
+
373
+ # Register for cancellation
374
+ self.cancellation_manager.register(
375
+ execution_id=context.execution_id,
376
+ instance=agent,
377
+ instance_type="agent",
378
+ )
379
+
380
+ # Log tool schema snapshots for debugging
381
+ # This helps detect schema inconsistencies and parameter mismatches
382
+ if context.execution_id:
383
+ for skill in all_skills:
384
+ if hasattr(skill, 'functions') and hasattr(skill.functions, 'items'):
385
+ skill_name = getattr(skill, 'name', 'unknown')
386
+ for func_name, func_obj in skill.functions.items():
387
+ if hasattr(func_obj, 'parameters') and isinstance(func_obj.parameters, dict):
388
+ param_names = list(func_obj.parameters.get('properties', {}).keys())
389
+ self.logger.debug(
390
+ "tool_schema_snapshot",
391
+ execution_id=context.execution_id,
392
+ skill_name=skill_name,
393
+ tool_name=func_name,
394
+ parameters=param_names,
395
+ )
396
+
397
+ # Build conversation context
398
+ messages = build_conversation_messages(context.conversation_history)
399
+
400
+ # Determine if we need async execution (when MCP tools are present)
401
+ has_async_tools = len(mcp_tools_instances) > 0
402
+
403
+ # Execute without streaming
404
+ if has_async_tools:
405
+ # Use async agent.arun() for MCP tools
406
+ if messages:
407
+ result = await agent.arun(context.prompt, stream=False, messages=messages)
408
+ else:
409
+ result = await agent.arun(context.prompt, stream=False)
410
+ else:
411
+ # Use sync agent.run() for non-MCP tools
412
+ def run_agent():
413
+ if messages:
414
+ return agent.run(context.prompt, stream=False, messages=messages)
415
+ else:
416
+ return agent.run(context.prompt, stream=False)
417
+
418
+ # Run in thread pool to avoid blocking
419
+ result = await asyncio.to_thread(run_agent)
420
+
421
+ # Cleanup
422
+ self.cancellation_manager.unregister(context.execution_id)
423
+
424
+ # Extract response and metadata
425
+ response_content = extract_response_content(result)
426
+ usage = extract_usage(result)
427
+ tool_messages = extract_tool_messages(result)
428
+
429
+ return RuntimeExecutionResult(
430
+ response=response_content,
431
+ usage=usage,
432
+ success=True,
433
+ finish_reason="stop",
434
+ run_id=getattr(result, "run_id", None),
435
+ model=context.model_id,
436
+ tool_messages=tool_messages,
437
+ )
438
+
439
+ except asyncio.CancelledError:
440
+ # Handle cancellation
441
+ self.cancellation_manager.cancel(context.execution_id)
442
+ self.cancellation_manager.unregister(context.execution_id)
443
+ raise
444
+
445
+ except Exception as e:
446
+ self.logger.error(
447
+ "agno_execution_failed",
448
+ execution_id=context.execution_id,
449
+ error=str(e),
450
+ )
451
+ self.cancellation_manager.unregister(context.execution_id)
452
+
453
+ # Publish error event
454
+ try:
455
+ from control_plane_api.worker.utils.error_publisher import (
456
+ ErrorEventPublisher, ErrorSeverity, ErrorCategory
457
+ )
458
+ error_publisher = ErrorEventPublisher(self.control_plane)
459
+ await error_publisher.publish_error(
460
+ execution_id=context.execution_id,
461
+ exception=e,
462
+ severity=ErrorSeverity.CRITICAL,
463
+ category=ErrorCategory.UNKNOWN,
464
+ stage="execution",
465
+ component="agno_runtime",
466
+ operation="agent_execution",
467
+ include_stack_trace=True,
468
+ )
469
+ except Exception:
470
+ pass # Never break execution flow
471
+
472
+ return RuntimeExecutionResult(
473
+ response="",
474
+ usage={},
475
+ success=False,
476
+ error=str(e),
477
+ )
478
+
479
+ finally:
480
+ # Close MCP tool connections
481
+ for mcp_tool in mcp_tools_instances:
482
+ try:
483
+ await mcp_tool.close()
484
+ self.logger.debug(
485
+ "mcp_tool_closed",
486
+ execution_id=context.execution_id,
487
+ )
488
+ except Exception as e:
489
+ self.logger.error(
490
+ "mcp_tool_close_failed",
491
+ error=str(e),
492
+ execution_id=context.execution_id,
493
+ )
494
+
495
+ async def _stream_execute_impl(
496
+ self,
497
+ context: RuntimeExecutionContext,
498
+ event_callback: Optional[Callable[[Dict], None]] = None,
499
+ ) -> AsyncIterator[RuntimeExecutionResult]:
500
+ """
501
+ Execute agent with streaming using Agno framework with efficient event batching.
502
+
503
+ This implementation uses the EventPublisher service to batch message chunks,
504
+ reducing HTTP requests by 90-96% while keeping tool events immediate.
505
+
506
+ Args:
507
+ context: Execution context
508
+ event_callback: Optional callback for real-time events
509
+
510
+ Yields:
511
+ RuntimeExecutionResult chunks as they arrive in real-time
512
+ """
513
+ # Create event publisher with batching
514
+ event_publisher = EventPublisher(
515
+ control_plane=self.control_plane,
516
+ execution_id=context.execution_id,
517
+ config=EventPublisherConfig.from_env(),
518
+ )
519
+
520
+ mcp_tools_instances = []
521
+
522
+ try:
523
+ # Build MCP tools from context
524
+ mcp_tools_instances = build_agno_mcp_tools(context.mcp_servers)
525
+
526
+ # Connect MCP tools
527
+ connected_mcp_tools = []
528
+ for mcp_tool in mcp_tools_instances:
529
+ try:
530
+ await mcp_tool.connect()
531
+
532
+ # Verify the tool is actually initialized (agno doesn't raise on failure)
533
+ if not mcp_tool.initialized:
534
+ server_name = getattr(mcp_tool, '_server_name', mcp_tool.name)
535
+ error_msg = f"Failed to initialize MCP tool: {server_name}"
536
+ self.logger.error(
537
+ "mcp_tool_initialization_failed",
538
+ server_name=server_name,
539
+ execution_id=context.execution_id,
540
+ error=error_msg,
541
+ )
542
+ raise RuntimeError(error_msg)
543
+
544
+ # Verify it has tools available
545
+ if not mcp_tool.functions:
546
+ server_name = getattr(mcp_tool, '_server_name', mcp_tool.name)
547
+ error_msg = f"MCP tool {server_name} has no functions available"
548
+ self.logger.error(
549
+ "mcp_tool_has_no_functions",
550
+ server_name=server_name,
551
+ execution_id=context.execution_id,
552
+ error=error_msg,
553
+ )
554
+ raise RuntimeError(error_msg)
555
+
556
+ self.logger.info(
557
+ "mcp_tool_connected_streaming",
558
+ execution_id=context.execution_id,
559
+ server_name=getattr(mcp_tool, '_server_name', mcp_tool.name),
560
+ function_count=len(mcp_tool.functions),
561
+ )
562
+
563
+ # UNIVERSAL VALIDATION: Validate MCP tool names
564
+ validated_mcp_tool = self._validate_mcp_tool_names(mcp_tool, context.execution_id)
565
+ connected_mcp_tools.append(validated_mcp_tool)
566
+
567
+ except Exception as e:
568
+ self.logger.error(
569
+ "mcp_tool_connection_failed_streaming",
570
+ error=str(e),
571
+ error_type=type(e).__name__,
572
+ server_name=getattr(mcp_tool, '_server_name', mcp_tool.name),
573
+ execution_id=context.execution_id,
574
+ )
575
+ import traceback
576
+ self.logger.debug(
577
+ "mcp_tool_connection_error_traceback",
578
+ traceback=traceback.format_exc(),
579
+ execution_id=context.execution_id,
580
+ )
581
+ # Continue with other MCP tools even if one fails
582
+
583
+ # Use only successfully connected tools
584
+ mcp_tools_instances = connected_mcp_tools
585
+
586
+ # Build conversation context
587
+ messages = build_conversation_messages(context.conversation_history)
588
+
589
+ # Determine if we need async execution (when MCP tools are present)
590
+ has_async_tools = len(mcp_tools_instances) > 0
591
+
592
+ # Stream execution - publish events INSIDE the thread (like old code)
593
+ accumulated_response = ""
594
+ run_result = None
595
+
596
+ # Create queue for streaming chunks from thread to async
597
+ chunk_queue = queue.Queue()
598
+
599
+ # Generate unique message ID
600
+ message_id = f"{context.execution_id}_msg_{int(time.time() * 1000000)}"
601
+
602
+ # Merge regular skills with custom tools
603
+ all_skills = list(context.skills) if context.skills else []
604
+
605
+ # Add custom tools
606
+ if self._custom_tools:
607
+ for tool_id, custom_tool in self._custom_tools.items():
608
+ try:
609
+ # Get toolkit from custom tool
610
+ toolkit = custom_tool.get_tools()
611
+
612
+ # Extract tools - handle both Toolkit objects and iterables
613
+ if hasattr(toolkit, 'tools'):
614
+ all_skills.extend(toolkit.tools)
615
+ elif hasattr(toolkit, '__iter__'):
616
+ all_skills.extend(toolkit)
617
+ else:
618
+ all_skills.append(toolkit)
619
+
620
+ self.logger.debug(
621
+ "custom_tool_loaded_streaming",
622
+ tool_id=tool_id,
623
+ execution_id=context.execution_id
624
+ )
625
+ except Exception as e:
626
+ self.logger.error(
627
+ "custom_tool_load_failed_streaming",
628
+ tool_id=tool_id,
629
+ error=str(e),
630
+ execution_id=context.execution_id
631
+ )
632
+
633
+ # Initialize enforcement service
634
+ enforcement_context = {
635
+ "organization_id": context.organization_id,
636
+ "user_email": context.user_email,
637
+ "user_id": context.user_id,
638
+ "user_roles": context.user_roles or [],
639
+ "team_id": context.team_id,
640
+ "team_name": context.team_name,
641
+ "agent_id": context.agent_id,
642
+ "environment": context.environment,
643
+ "model_id": context.model_id,
644
+ }
645
+
646
+ # Import enforcement dependencies
647
+ from control_plane_api.app.lib.policy_enforcer_client import create_policy_enforcer_client
648
+ from control_plane_api.worker.services.tool_enforcement import ToolEnforcementService
649
+ import os
650
+
651
+ # Get enforcer client (using the same token as the control plane)
652
+ enforcer_client = None
653
+ enforcement_service = None
654
+
655
+ # Check if enforcement is enabled (opt-in via environment variable)
656
+ enforcement_enabled = os.environ.get("KUBIYA_ENFORCE_ENABLED", "").lower() in ("true", "1", "yes")
657
+
658
+ if not enforcement_enabled:
659
+ self.logger.info(
660
+ "policy_enforcement_disabled",
661
+ reason="KUBIYA_ENFORCE_ENABLED not set",
662
+ execution_id=context.execution_id[:8],
663
+ note="Set KUBIYA_ENFORCE_ENABLED=true to enable policy enforcement"
664
+ )
665
+ else:
666
+ try:
667
+ # Get enforcer URL - default to control plane enforcer proxy
668
+ enforcer_url = os.environ.get("ENFORCER_SERVICE_URL")
669
+ if not enforcer_url:
670
+ # Use control plane's enforcer proxy as default
671
+ control_plane_url = os.environ.get("CONTROL_PLANE_URL", "http://localhost:8000")
672
+ enforcer_url = f"{control_plane_url.rstrip('/')}/api/v1/enforcer"
673
+ self.logger.debug(
674
+ "using_control_plane_enforcer_proxy",
675
+ enforcer_url=enforcer_url,
676
+ execution_id=context.execution_id[:8],
677
+ )
678
+
679
+ # Use async context manager properly (we're in an async function)
680
+ enforcer_client_context = create_policy_enforcer_client(
681
+ enforcer_url=enforcer_url,
682
+ api_key=self.control_plane.api_key,
683
+ auth_type="UserKey"
684
+ )
685
+ enforcer_client = await enforcer_client_context.__aenter__()
686
+ if enforcer_client:
687
+ enforcement_service = ToolEnforcementService(enforcer_client)
688
+ self.logger.info(
689
+ "policy_enforcement_enabled",
690
+ enforcer_url=enforcer_url,
691
+ execution_id=context.execution_id[:8],
692
+ )
693
+ except Exception as e:
694
+ self.logger.warning(
695
+ "enforcement_service_init_failed",
696
+ error=str(e),
697
+ execution_id=context.execution_id[:8],
698
+ )
699
+
700
+ # Create tool hook that publishes directly to Control Plane with enforcement
701
+ tool_hook = create_tool_hook_for_streaming(
702
+ control_plane=self.control_plane,
703
+ execution_id=context.execution_id,
704
+ message_id=message_id, # Link tools to this assistant message turn
705
+ enforcement_context=enforcement_context,
706
+ enforcement_service=enforcement_service,
707
+ )
708
+
709
+ # Extract metadata for Langfuse tracking
710
+ user_id = None
711
+ session_id = None
712
+ agent_name = None
713
+
714
+ if context.user_metadata:
715
+ user_id = context.user_metadata.get("user_email") or context.user_metadata.get("user_id")
716
+ session_id = context.user_metadata.get("session_id") or context.execution_id
717
+ agent_name = context.user_metadata.get("agent_name") or context.agent_id
718
+
719
+ # DEBUG: Log metadata extraction
720
+ self.logger.warning(
721
+ "🔍 DEBUG: AGNO RUNTIME (_stream_execute_impl) - METADATA EXTRACTION",
722
+ context_user_metadata=context.user_metadata,
723
+ extracted_user_id=user_id,
724
+ extracted_session_id=session_id,
725
+ extracted_agent_name=agent_name,
726
+ organization_id=context.organization_id,
727
+ )
728
+
729
+ # Create Agno agent with all tools (skills + MCP tools), tool hooks, and metadata
730
+ agent = build_agno_agent_config(
731
+ agent_id=context.agent_id,
732
+ system_prompt=context.system_prompt,
733
+ model_id=context.model_id,
734
+ skills=all_skills,
735
+ mcp_tools=mcp_tools_instances,
736
+ tool_hooks=[tool_hook],
737
+ user_id=user_id,
738
+ session_id=session_id,
739
+ organization_id=context.organization_id,
740
+ agent_name=agent_name,
741
+ skill_configs=context.skill_configs,
742
+ user_metadata=context.user_metadata,
743
+ )
744
+
745
+ # Register for cancellation
746
+ self.cancellation_manager.register(
747
+ execution_id=context.execution_id,
748
+ instance=agent,
749
+ instance_type="agent",
750
+ )
751
+
752
+ # Cache execution metadata
753
+ self.control_plane.cache_metadata(context.execution_id, "AGENT")
754
+
755
+ def stream_agent_run():
756
+ """
757
+ Run agent with streaming and publish events directly to Control Plane.
758
+ This runs in a thread pool, so blocking HTTP calls are OK here.
759
+ Put chunks in queue for async iterator to yield in real-time.
760
+ """
761
+ nonlocal accumulated_response, run_result
762
+ run_id_published = False
763
+
764
+ # Use thread-local event loop from control_plane client
765
+ # This ensures all async operations (tool hooks, event publishing) share the same loop
766
+ # and it persists until explicitly cleaned up at the end of execution
767
+ thread_loop = self.control_plane._get_thread_event_loop()
768
+
769
+ try:
770
+ # Use async streaming for MCP tools, sync for others
771
+ if has_async_tools:
772
+ # For async tools (MCP), we need to use agent.arun() in an async context
773
+ # Use the thread-local event loop instead of creating a new one
774
+ if messages:
775
+ stream_response = thread_loop.run_until_complete(
776
+ agent.arun(context.prompt, stream=True, messages=messages)
777
+ )
778
+ else:
779
+ stream_response = thread_loop.run_until_complete(
780
+ agent.arun(context.prompt, stream=True)
781
+ )
782
+ else:
783
+ # Use sync agent.run() for non-MCP tools
784
+ if messages:
785
+ stream_response = agent.run(
786
+ context.prompt,
787
+ stream=True,
788
+ messages=messages,
789
+ )
790
+ else:
791
+ stream_response = agent.run(context.prompt, stream=True)
792
+
793
+ # Iterate over streaming chunks and publish IMMEDIATELY
794
+ for chunk in stream_response:
795
+ # Capture run_id for cancellation (first chunk)
796
+ if not run_id_published and hasattr(chunk, "run_id") and chunk.run_id:
797
+ self.cancellation_manager.set_run_id(
798
+ context.execution_id, chunk.run_id
799
+ )
800
+
801
+ # Publish run_id event
802
+ self.control_plane.publish_event(
803
+ execution_id=context.execution_id,
804
+ event_type="run_started",
805
+ data={
806
+ "run_id": chunk.run_id,
807
+ "execution_id": context.execution_id,
808
+ "cancellable": True,
809
+ }
810
+ )
811
+ run_id_published = True
812
+
813
+ # Extract content
814
+ chunk_content = ""
815
+ if hasattr(chunk, "content") and chunk.content:
816
+ if isinstance(chunk.content, str):
817
+ chunk_content = chunk.content
818
+ elif hasattr(chunk.content, "text"):
819
+ chunk_content = chunk.content.text
820
+
821
+ # Filter out whitespace-only chunks to prevent "(no content)" in UI
822
+ if chunk_content and chunk_content.strip():
823
+ accumulated_response += chunk_content
824
+
825
+ # Queue chunk for batched publishing (via EventPublisher in async context)
826
+ # This reduces 300 HTTP requests → 12 requests (96% reduction)
827
+ chunk_queue.put(("chunk", chunk_content, message_id))
828
+
829
+ # Store final result
830
+ run_result = stream_response
831
+
832
+ # Signal completion
833
+ chunk_queue.put(("done", run_result))
834
+
835
+ except Exception as e:
836
+ self.logger.error("streaming_error", error=str(e))
837
+ chunk_queue.put(("error", e))
838
+ raise
839
+
840
+ # Start streaming in background thread
841
+ stream_thread = threading.Thread(target=stream_agent_run, daemon=True)
842
+ stream_thread.start()
843
+
844
+ # Yield chunks as they arrive in the queue and publish via EventPublisher
845
+ while True:
846
+ try:
847
+ # Non-blocking get with short timeout for responsiveness
848
+ queue_item = await asyncio.to_thread(chunk_queue.get, timeout=0.1)
849
+
850
+ if queue_item[0] == "chunk":
851
+ # Unpack chunk data
852
+ _, chunk_content, msg_id = queue_item
853
+
854
+ # Publish chunk via EventPublisher (batched, non-blocking)
855
+ await event_publisher.publish(
856
+ event_type="message_chunk",
857
+ data={
858
+ "role": "assistant",
859
+ "content": chunk_content,
860
+ "is_chunk": True,
861
+ "message_id": msg_id,
862
+ },
863
+ priority=EventPriority.NORMAL, # Batched
864
+ )
865
+
866
+ # Yield chunk immediately to iterator
867
+ yield RuntimeExecutionResult(
868
+ response=chunk_content,
869
+ usage={},
870
+ success=True,
871
+ )
872
+ elif queue_item[0] == "done":
873
+ # Final result - extract metadata and break
874
+ run_result = queue_item[1]
875
+ break
876
+ elif queue_item[0] == "error":
877
+ # Error occurred in thread
878
+ raise queue_item[1]
879
+
880
+ except queue.Empty:
881
+ # Queue empty, check if thread is still alive
882
+ if not stream_thread.is_alive():
883
+ # Thread died without putting "done" - something went wrong
884
+ break
885
+ # Thread still running, continue waiting
886
+ continue
887
+
888
+ # Wait for thread to complete
889
+ await asyncio.to_thread(stream_thread.join, timeout=5.0)
890
+
891
+ # Yield final result with complete metadata
892
+ usage = extract_usage(run_result) if run_result else {}
893
+ tool_messages = extract_tool_messages(run_result) if run_result else []
894
+
895
+ yield RuntimeExecutionResult(
896
+ response=accumulated_response, # Full accumulated response
897
+ usage=usage,
898
+ success=True,
899
+ finish_reason="stop",
900
+ run_id=getattr(run_result, "run_id", None) if run_result else None,
901
+ model=context.model_id,
902
+ tool_messages=tool_messages,
903
+ metadata={"accumulated_response": accumulated_response},
904
+ )
905
+
906
+ finally:
907
+ # Flush and close event publisher to ensure all batched events are sent
908
+ await event_publisher.flush()
909
+ await event_publisher.close()
910
+
911
+ # Clean up thread-local event loop used by tool hooks
912
+ # This prevents resource leaks and "await wasn't used with future" errors
913
+ self.control_plane.close_thread_event_loop()
914
+
915
+ # Close MCP tool connections
916
+ for mcp_tool in mcp_tools_instances:
917
+ try:
918
+ await mcp_tool.close()
919
+ self.logger.debug(
920
+ "mcp_tool_closed_streaming",
921
+ execution_id=context.execution_id,
922
+ )
923
+ except Exception as e:
924
+ self.logger.error(
925
+ "mcp_tool_close_failed_streaming",
926
+ error=str(e),
927
+ execution_id=context.execution_id,
928
+ )
929
+
930
+ # Close enforcer client context manager (fix resource leak)
931
+ if 'enforcer_client_context' in locals() and enforcer_client_context is not None:
932
+ try:
933
+ await enforcer_client_context.__aexit__(None, None, None)
934
+ self.logger.debug(
935
+ "enforcer_client_closed",
936
+ execution_id=context.execution_id[:8],
937
+ )
938
+ except Exception as e:
939
+ self.logger.warning(
940
+ "enforcer_client_close_failed",
941
+ error=str(e),
942
+ execution_id=context.execution_id[:8],
943
+ )
944
+
945
+ # Cleanup
946
+ self.cancellation_manager.unregister(context.execution_id)
947
+
948
+ # ==================== Custom Tool Extension API ====================
949
+
950
+ def get_custom_tool_requirements(self) -> Dict[str, Any]:
951
+ """
952
+ Get requirements for creating custom tools for Agno runtime.
953
+
954
+ Returns:
955
+ Dictionary with format, examples, and documentation for Agno custom tools
956
+ """
957
+ return {
958
+ "format": "python_class",
959
+ "description": "Python class with get_tools() method returning Agno Toolkit",
960
+ "example_code": '''
961
+ from agno.tools import Toolkit
962
+
963
+ class MyCustomTool:
964
+ """Custom tool for Agno runtime."""
965
+
966
+ def get_tools(self) -> Toolkit:
967
+ """Return Agno toolkit with custom functions."""
968
+ return Toolkit(
969
+ name="my_tool",
970
+ tools=[self.my_function]
971
+ )
972
+
973
+ def my_function(self, arg: str) -> str:
974
+ """Tool function description."""
975
+ return f"Result: {arg}"
976
+ ''',
977
+ "documentation_url": "https://docs.agno.ai/custom-tools",
978
+ "required_methods": ["get_tools"],
979
+ "schema": {
980
+ "type": "object",
981
+ "required": ["get_tools"],
982
+ "properties": {
983
+ "get_tools": {
984
+ "type": "method",
985
+ "returns": "Toolkit"
986
+ }
987
+ }
988
+ }
989
+ }
990
+
991
+ def validate_custom_tool(self, tool: Any) -> tuple[bool, Optional[str]]:
992
+ """
993
+ Validate a custom tool for Agno runtime.
994
+
995
+ Args:
996
+ tool: Tool instance to validate
997
+
998
+ Returns:
999
+ Tuple of (is_valid, error_message)
1000
+ """
1001
+ # Check for get_tools method
1002
+ if not hasattr(tool, 'get_tools'):
1003
+ return False, "Tool must have get_tools() method"
1004
+
1005
+ if not callable(getattr(tool, 'get_tools')):
1006
+ return False, "get_tools must be callable"
1007
+
1008
+ # Try calling to validate return type
1009
+ try:
1010
+ toolkit = tool.get_tools()
1011
+
1012
+ # Check if it's a Toolkit-like object (has tools attribute or is iterable)
1013
+ if not (hasattr(toolkit, 'tools') or hasattr(toolkit, '__iter__')):
1014
+ return False, f"get_tools() must return Toolkit or iterable, got {type(toolkit)}"
1015
+
1016
+ except Exception as e:
1017
+ return False, f"get_tools() failed: {str(e)}"
1018
+
1019
+ return True, None
1020
+
1021
+ def register_custom_tool(self, tool: Any, metadata: Optional[Dict] = None) -> str:
1022
+ """
1023
+ Register a custom tool with Agno runtime.
1024
+
1025
+ Args:
1026
+ tool: Tool instance with get_tools() method
1027
+ metadata: Optional metadata (name, description, etc.)
1028
+
1029
+ Returns:
1030
+ Tool identifier for this registered tool
1031
+
1032
+ Raises:
1033
+ ValueError: If tool validation fails
1034
+ """
1035
+ # Validate first
1036
+ is_valid, error = self.validate_custom_tool(tool)
1037
+ if not is_valid:
1038
+ raise ValueError(f"Invalid custom tool: {error}")
1039
+
1040
+ # Generate tool ID
1041
+ tool_name = metadata.get("name") if metadata else tool.__class__.__name__
1042
+ tool_id = f"custom_{tool_name}_{id(tool)}"
1043
+
1044
+ # Store tool instance
1045
+ self._custom_tools[tool_id] = tool
1046
+
1047
+ self.logger.info(
1048
+ "custom_tool_registered",
1049
+ tool_id=tool_id,
1050
+ tool_class=tool.__class__.__name__,
1051
+ tool_name=tool_name
1052
+ )
1053
+
1054
+ return tool_id
1055
+
1056
+ def get_registered_custom_tools(self) -> list[str]:
1057
+ """
1058
+ Get list of registered custom tool identifiers.
1059
+
1060
+ Returns:
1061
+ List of tool IDs
1062
+ """
1063
+ return list(self._custom_tools.keys())