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,588 @@
1
+ """
2
+ Message history loader for streaming execution data.
3
+
4
+ This module provides a HistoryLoader class that retrieves historical messages from
5
+ the database (PostgreSQL Session table) with Temporal workflow state as fallback.
6
+ Messages are streamed progressively using an async generator for non-blocking
7
+ rendering in the UI.
8
+
9
+ Key Features:
10
+ - Primary source: PostgreSQL Session table (fast, reliable)
11
+ - Fallback source: Temporal workflow state (when DB is empty)
12
+ - Progressive streaming: yields messages one-at-a-time for instant UI rendering
13
+ - Message deduplication: integrates with MessageDeduplicator
14
+ - Message limiting: caps at last 200 messages for performance
15
+ - Chronological sorting: ensures proper conversation flow
16
+ - Timeout protection: 3-second timeout for Temporal queries
17
+
18
+ Architecture:
19
+ This class is part of the Resumable Execution Stream Architecture:
20
+ 1. HistoryLoader: Loads and streams historical messages (this module)
21
+ 2. MessageDeduplicator: Prevents duplicate messages across history/live streams
22
+ 3. LiveStreamProcessor: Handles real-time Redis stream events (future)
23
+
24
+ Test Strategy:
25
+ - Unit test database loading with various message counts (0, 1, 100, 300)
26
+ - Test Temporal fallback when DB empty
27
+ - Test yielding behavior (progressive, not batched)
28
+ - Test timeout handling for Temporal queries (3s limit)
29
+ - Test message sorting and chronological order
30
+ - Test message limit enforcement (200 cap)
31
+ - Test edge cases: no messages, DB errors, empty session record
32
+ - Integration test with MessageDeduplicator
33
+ """
34
+
35
+ import asyncio
36
+ import hashlib
37
+ import time
38
+ from datetime import datetime
39
+ from typing import Any, AsyncGenerator, Dict, List, Optional
40
+
41
+ import structlog
42
+ from sqlalchemy.orm import Session as SQLAlchemySession
43
+
44
+ from control_plane_api.app.models.session import Session as SessionModel
45
+ from control_plane_api.app.workflows.agent_execution import (
46
+ AgentExecutionWorkflow,
47
+ ChatMessage,
48
+ )
49
+ from .deduplication import MessageDeduplicator
50
+
51
+ logger = structlog.get_logger(__name__)
52
+
53
+
54
+ # Message limit for performance optimization
55
+ # Loading >200 messages can slow down initial rendering
56
+ # Most conversations don't exceed 100 messages
57
+ MAX_HISTORY_MESSAGES = 200
58
+
59
+
60
+ class HistoryLoader:
61
+ """
62
+ Handles loading historical messages from database with Temporal fallback.
63
+
64
+ This class manages the initial message history load when a client connects
65
+ to the streaming execution endpoint. It attempts to load messages from the
66
+ PostgreSQL Session table first (fast, reliable), falling back to Temporal
67
+ workflow state if the database has no messages (e.g., new execution, DB lag).
68
+
69
+ The loader yields messages progressively as an async generator, allowing
70
+ the UI to render messages immediately without waiting for the entire history
71
+ to load. This provides instant feedback to users even for long conversations.
72
+
73
+ Message deduplication is handled via the provided MessageDeduplicator instance,
74
+ ensuring that messages aren't duplicated between history and live streams.
75
+
76
+ Example usage:
77
+ deduplicator = MessageDeduplicator()
78
+ loader = HistoryLoader(
79
+ execution_id="exec-123",
80
+ organization_id="org-456",
81
+ db_session=db,
82
+ temporal_client=temporal_client,
83
+ deduplicator=deduplicator,
84
+ )
85
+
86
+ async for message in loader.stream():
87
+ # Send message to client
88
+ yield format_sse_message(message)
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ execution_id: str,
94
+ organization_id: str,
95
+ db_session: SQLAlchemySession,
96
+ temporal_client: Any, # temporalio.client.Client
97
+ deduplicator: MessageDeduplicator,
98
+ workflow_id: Optional[str] = None,
99
+ ):
100
+ """
101
+ Initialize the history loader.
102
+
103
+ Args:
104
+ execution_id: Execution ID to load history for
105
+ organization_id: Organization ID for security filtering
106
+ db_session: SQLAlchemy database session
107
+ temporal_client: Temporal client for workflow queries
108
+ deduplicator: Message deduplicator instance
109
+ workflow_id: Workflow ID for Temporal queries (defaults to agent-execution-{execution_id})
110
+ """
111
+ self.execution_id = execution_id
112
+ self.organization_id = organization_id
113
+ self.db_session = db_session
114
+ self.temporal_client = temporal_client
115
+ self.deduplicator = deduplicator
116
+ self.workflow_id = workflow_id or f"agent-execution-{execution_id}"
117
+
118
+ # Statistics for monitoring
119
+ self._stats = {
120
+ "db_messages_loaded": 0,
121
+ "temporal_messages_loaded": 0,
122
+ "messages_sent": 0,
123
+ "messages_skipped_empty": 0,
124
+ "messages_deduplicated": 0,
125
+ "db_load_duration_ms": 0,
126
+ "temporal_load_duration_ms": 0,
127
+ }
128
+
129
+ async def stream(self) -> AsyncGenerator[Dict[str, Any], None]:
130
+ """
131
+ Stream historical messages progressively.
132
+
133
+ This method loads messages from the database first, falling back to
134
+ Temporal workflow state if no messages are found. Messages are yielded
135
+ one at a time for non-blocking progressive rendering.
136
+
137
+ The method performs the following steps:
138
+ 1. Load messages from database (PostgreSQL Session table)
139
+ 2. If no messages found, try Temporal workflow fallback
140
+ 3. Sort messages chronologically
141
+ 4. Limit to last 200 messages if needed
142
+ 5. Yield messages one at a time, checking deduplication
143
+
144
+ Yields:
145
+ Message dictionaries with keys:
146
+ - message_id: Unique message identifier
147
+ - role: Message role (user, assistant, system, tool)
148
+ - content: Message content
149
+ - timestamp: ISO format timestamp
150
+ - tool_name, tool_input, tool_output: Tool data (if applicable)
151
+ - workflow_name, workflow_steps, etc.: Workflow data (if applicable)
152
+ - user_id, user_name, user_email, user_avatar: User attribution (if applicable)
153
+
154
+ Raises:
155
+ Exception: If both database and Temporal loading fail (logged but not raised)
156
+ """
157
+ import time
158
+
159
+ # Step 1: Try loading from database
160
+ t0 = time.time()
161
+ messages = await self._load_from_database()
162
+ self._stats["db_load_duration_ms"] = int((time.time() - t0) * 1000)
163
+ self._stats["db_messages_loaded"] = len(messages)
164
+
165
+ # Step 2: Fallback to Temporal if no messages in database
166
+ if not messages:
167
+ logger.info(
168
+ "no_database_history_attempting_temporal_fallback",
169
+ execution_id=self.execution_id,
170
+ )
171
+ t0 = time.time()
172
+ messages = await self._load_from_temporal()
173
+ self._stats["temporal_load_duration_ms"] = int((time.time() - t0) * 1000)
174
+ self._stats["temporal_messages_loaded"] = len(messages)
175
+
176
+ if not messages:
177
+ logger.info(
178
+ "no_history_messages_found",
179
+ execution_id=self.execution_id,
180
+ stats=self._stats,
181
+ )
182
+ return
183
+
184
+ # Step 3: Sort messages chronologically
185
+ # CRITICAL: Messages must be in exact order for proper conversation flow
186
+ messages.sort(key=lambda m: self._parse_timestamp(m.get("timestamp", "")))
187
+
188
+ logger.info(
189
+ "history_messages_loaded_and_sorted",
190
+ execution_id=self.execution_id,
191
+ message_count=len(messages),
192
+ first_timestamp=messages[0].get("timestamp") if messages else None,
193
+ last_timestamp=messages[-1].get("timestamp") if messages else None,
194
+ stats=self._stats,
195
+ )
196
+
197
+ # Step 4: Limit to last N messages for performance
198
+ if len(messages) > MAX_HISTORY_MESSAGES:
199
+ original_count = len(messages)
200
+ messages = messages[-MAX_HISTORY_MESSAGES:]
201
+ logger.info(
202
+ "history_messages_limited_for_performance",
203
+ execution_id=self.execution_id,
204
+ original_count=original_count,
205
+ limited_count=len(messages),
206
+ limit=MAX_HISTORY_MESSAGES,
207
+ )
208
+
209
+ # Step 5: Stream messages one at a time
210
+ for msg in messages:
211
+ # Skip messages with empty content UNLESS they have tool/workflow data
212
+ has_content = msg.get("content") and msg.get("content").strip()
213
+ has_tool_data = bool(
214
+ msg.get("tool_name")
215
+ or msg.get("tool_input")
216
+ or msg.get("tool_output")
217
+ or msg.get("tool_error")
218
+ )
219
+ has_workflow_data = bool(
220
+ msg.get("workflow_name")
221
+ or msg.get("workflow_steps")
222
+ or msg.get("workflow_status")
223
+ )
224
+
225
+ if not has_content and not has_tool_data and not has_workflow_data:
226
+ self._stats["messages_skipped_empty"] += 1
227
+ logger.debug(
228
+ "skipping_empty_message",
229
+ execution_id=self.execution_id,
230
+ message_id=msg.get("message_id"),
231
+ role=msg.get("role"),
232
+ )
233
+ continue
234
+
235
+ # Check deduplication
236
+ if self.deduplicator.is_sent(msg):
237
+ self._stats["messages_deduplicated"] += 1
238
+ logger.debug(
239
+ "skipping_duplicate_message",
240
+ execution_id=self.execution_id,
241
+ message_id=msg.get("message_id"),
242
+ role=msg.get("role"),
243
+ )
244
+ continue
245
+
246
+ # Mark as sent for deduplication
247
+ self.deduplicator.mark_sent(msg)
248
+ self._stats["messages_sent"] += 1
249
+
250
+ # Yield the message
251
+ yield msg
252
+
253
+ logger.info(
254
+ "history_streaming_complete",
255
+ execution_id=self.execution_id,
256
+ stats=self._stats,
257
+ )
258
+
259
+ async def _load_from_database(self) -> List[Dict[str, Any]]:
260
+ """
261
+ Load messages from PostgreSQL Session table.
262
+
263
+ This is the primary source for message history. The Session table stores
264
+ messages as JSONB array, which is fast to query and doesn't require joins.
265
+
266
+ The method queries the Session table by execution_id and organization_id,
267
+ extracts the messages array, and converts to standard message dictionaries.
268
+
269
+ Returns:
270
+ List of message dictionaries, or empty list if no session found
271
+ """
272
+ try:
273
+ # Query session record by execution_id only
274
+ # NOTE: We don't filter by organization_id here because:
275
+ # 1. execution_id is globally unique (UUID)
276
+ # 2. Authorization is already enforced at the WebSocket/API level
277
+ # 3. Worker may persist with different org_id format than UI queries with
278
+ # (e.g., 'kubiya-ai' vs 'org_lAowz6o1YKbB4YUt')
279
+ session_record = (
280
+ self.db_session.query(SessionModel)
281
+ .filter(
282
+ SessionModel.execution_id == self.execution_id,
283
+ )
284
+ .first()
285
+ )
286
+
287
+ if not session_record:
288
+ logger.warning(
289
+ "no_session_record_found_in_database",
290
+ execution_id=self.execution_id,
291
+ queried_org_id=self.organization_id,
292
+ )
293
+ return []
294
+
295
+ # Extract messages from JSONB array
296
+ messages_data = session_record.messages or []
297
+
298
+ if not messages_data:
299
+ logger.warning(
300
+ "session_record_found_but_no_messages",
301
+ execution_id=self.execution_id,
302
+ session_id=session_record.session_id,
303
+ created_at=str(session_record.created_at),
304
+ )
305
+ return []
306
+
307
+ logger.info(
308
+ "loaded_messages_from_database",
309
+ execution_id=self.execution_id,
310
+ message_count=len(messages_data),
311
+ )
312
+
313
+ # Convert to standard message dictionaries
314
+ # Messages are already dicts in JSONB, so just ensure they have required fields
315
+ messages = []
316
+ for msg_data in messages_data:
317
+ # Ensure message has required fields
318
+ message = {
319
+ "message_id": msg_data.get("message_id"),
320
+ "role": msg_data.get("role"),
321
+ "content": msg_data.get("content"),
322
+ "timestamp": msg_data.get("timestamp"),
323
+ }
324
+
325
+ # Add optional fields if present
326
+ optional_fields = [
327
+ "tool_name",
328
+ "tool_input",
329
+ "tool_output",
330
+ "tool_error",
331
+ "workflow_name",
332
+ "workflow_status",
333
+ "workflow_steps",
334
+ "workflow_runner",
335
+ "workflow_type",
336
+ "workflow_duration",
337
+ "workflow_error",
338
+ "user_id",
339
+ "user_name",
340
+ "user_email",
341
+ "user_avatar",
342
+ "metadata",
343
+ ]
344
+ for field in optional_fields:
345
+ if field in msg_data and msg_data[field] is not None:
346
+ message[field] = msg_data[field]
347
+
348
+ messages.append(message)
349
+
350
+ # Normalize old message ID formats for backward compatibility
351
+ self._normalize_message_ids(messages)
352
+
353
+ return messages
354
+
355
+ except Exception as e:
356
+ logger.error(
357
+ "database_load_failed",
358
+ execution_id=self.execution_id,
359
+ error=str(e),
360
+ error_type=type(e).__name__,
361
+ )
362
+ return []
363
+
364
+ async def _load_from_temporal(self) -> List[Dict[str, Any]]:
365
+ """
366
+ Fallback: Load messages from Temporal workflow state.
367
+
368
+ This method is used when the database has no messages, which can happen
369
+ for new executions or if there's DB replication lag. It queries the
370
+ Temporal workflow state to get the current message history.
371
+
372
+ The method has a 3-second timeout to prevent blocking the stream if the
373
+ Temporal worker is down or slow. If the timeout is exceeded, an empty
374
+ list is returned and the stream continues without history.
375
+
376
+ Returns:
377
+ List of message dictionaries, or empty list if query fails/times out
378
+ """
379
+ try:
380
+ # Get workflow handle
381
+ workflow_handle = self.temporal_client.get_workflow_handle(
382
+ self.workflow_id
383
+ )
384
+
385
+ # Query workflow state with 3-second timeout
386
+ # This prevents 29-second hangs when worker is down
387
+ try:
388
+ state = await asyncio.wait_for(
389
+ workflow_handle.query(AgentExecutionWorkflow.get_state),
390
+ timeout=3.0,
391
+ )
392
+ except asyncio.TimeoutError:
393
+ logger.warning(
394
+ "temporal_fallback_timeout",
395
+ execution_id=self.execution_id,
396
+ workflow_id=self.workflow_id,
397
+ timeout_seconds=3.0,
398
+ )
399
+ return []
400
+
401
+ if not state or not state.messages or len(state.messages) == 0:
402
+ logger.info(
403
+ "temporal_fallback_no_messages",
404
+ execution_id=self.execution_id,
405
+ workflow_id=self.workflow_id,
406
+ )
407
+ return []
408
+
409
+ logger.info(
410
+ "loaded_messages_from_temporal",
411
+ execution_id=self.execution_id,
412
+ workflow_id=self.workflow_id,
413
+ message_count=len(state.messages),
414
+ )
415
+
416
+ # Convert ChatMessage objects to dictionaries
417
+ messages = []
418
+ for i, msg in enumerate(state.messages):
419
+ # Generate message_id if missing
420
+ message_id = getattr(msg, "message_id", None)
421
+ if not message_id:
422
+ # Use index-based ID for temporal messages
423
+ message_id = f"{self.execution_id}_{msg.role}_{i}"
424
+
425
+ message = {
426
+ "message_id": message_id,
427
+ "role": msg.role,
428
+ "content": msg.content,
429
+ "timestamp": msg.timestamp,
430
+ }
431
+
432
+ # Add optional fields if present
433
+ if msg.tool_name:
434
+ message["tool_name"] = msg.tool_name
435
+ if hasattr(msg, "tool_input") and msg.tool_input:
436
+ message["tool_input"] = msg.tool_input
437
+ if hasattr(msg, "tool_output") and msg.tool_output:
438
+ message["tool_output"] = msg.tool_output
439
+
440
+ # Add user attribution if present
441
+ if hasattr(msg, "user_id") and msg.user_id:
442
+ message["user_id"] = msg.user_id
443
+ if hasattr(msg, "user_name") and msg.user_name:
444
+ message["user_name"] = msg.user_name
445
+ if hasattr(msg, "user_email") and msg.user_email:
446
+ message["user_email"] = msg.user_email
447
+ if hasattr(msg, "user_avatar") and msg.user_avatar:
448
+ message["user_avatar"] = msg.user_avatar
449
+
450
+ messages.append(message)
451
+
452
+ # Normalize message IDs
453
+ self._normalize_message_ids(messages)
454
+
455
+ return messages
456
+
457
+ except Exception as e:
458
+ logger.error(
459
+ "temporal_fallback_failed",
460
+ execution_id=self.execution_id,
461
+ workflow_id=self.workflow_id,
462
+ error=str(e),
463
+ error_type=type(e).__name__,
464
+ )
465
+ return []
466
+
467
+ def _normalize_message_ids(self, messages: List[Dict[str, Any]]) -> None:
468
+ """
469
+ Normalize old message ID formats for backward compatibility.
470
+
471
+ This method handles legacy message ID formats to ensure consistent IDs
472
+ across database reloads. Old formats used timestamps or simple indices,
473
+ which change on each load. New format uses turn-based numbering which
474
+ is stable.
475
+
476
+ Message ID formats:
477
+ - New (turn-based): {execution_id}_{role}_{turn_number}
478
+ Example: "exec123_assistant_5"
479
+ - Old (timestamp-based): {execution_id}_{role}_{timestamp_micros}
480
+ Example: "exec123_assistant_1234567890123456"
481
+ - Old (index-based): {execution_id}_{role}_{idx}
482
+ Example: "exec123_assistant_42" (ambiguous with turn-based)
483
+
484
+ Detection heuristic:
485
+ - If last part is < 10000, assume turn-based (new format) - keep as-is
486
+ - If last part is >= 10000, assume timestamp-based (old format) - use content hash
487
+ - If can't parse, use content hash
488
+
489
+ Args:
490
+ messages: List of message dictionaries to normalize in-place
491
+ """
492
+ normalized_count = 0
493
+
494
+ for msg in messages:
495
+ message_id = msg.get("message_id")
496
+ if not message_id:
497
+ continue
498
+
499
+ parts = message_id.split("_")
500
+
501
+ # Check if format is: {execution_id}_{role}_{number}
502
+ if len(parts) >= 3 and parts[-2] in ["user", "assistant", "system"]:
503
+ try:
504
+ last_part = int(parts[-1])
505
+
506
+ # Turn numbers are small (1-100), timestamps are huge (1e15)
507
+ if last_part < 10000:
508
+ # New format (turn-based) - keep as-is
509
+ continue
510
+
511
+ # Old format (timestamp-based) - normalize to content hash
512
+ normalized_count += 1
513
+
514
+ except (ValueError, IndexError):
515
+ # Can't parse as number - might be hash or other format
516
+ normalized_count += 1
517
+
518
+ # Generate stable ID based on content hash
519
+ content = msg.get("content", "") or ""
520
+ role = msg.get("role", "unknown")
521
+ execution_id = parts[0] if parts else self.execution_id
522
+
523
+ content_hash = hashlib.md5(content.encode()).hexdigest()[:8]
524
+ new_id = f"{execution_id}_{role}_{content_hash}"
525
+
526
+ old_id = message_id
527
+ msg["message_id"] = new_id
528
+
529
+ logger.debug(
530
+ "normalized_message_id",
531
+ execution_id=self.execution_id,
532
+ old_id=old_id,
533
+ new_id=new_id,
534
+ role=role,
535
+ )
536
+
537
+ if normalized_count > 0:
538
+ logger.info(
539
+ "normalized_message_ids_for_backward_compatibility",
540
+ execution_id=self.execution_id,
541
+ normalized_count=normalized_count,
542
+ total_messages=len(messages),
543
+ )
544
+
545
+ def _parse_timestamp(self, timestamp_str: str) -> datetime:
546
+ """
547
+ Parse ISO format timestamp string.
548
+
549
+ Handles both with and without 'Z' suffix. Returns datetime.min for
550
+ invalid/missing timestamps to ensure they sort first.
551
+
552
+ Args:
553
+ timestamp_str: ISO format timestamp (e.g., "2024-01-15T10:30:00Z")
554
+
555
+ Returns:
556
+ datetime object, or datetime.min if parsing fails
557
+ """
558
+ if not timestamp_str:
559
+ return datetime.min
560
+
561
+ try:
562
+ # Handle 'Z' suffix for UTC timestamps
563
+ normalized = timestamp_str.replace("Z", "+00:00")
564
+ return datetime.fromisoformat(normalized)
565
+ except Exception as e:
566
+ logger.warning(
567
+ "failed_to_parse_timestamp",
568
+ execution_id=self.execution_id,
569
+ timestamp=timestamp_str,
570
+ error=str(e),
571
+ )
572
+ return datetime.min
573
+
574
+ def get_stats(self) -> Dict[str, Any]:
575
+ """
576
+ Get history loading statistics.
577
+
578
+ Returns:
579
+ Dictionary with statistics:
580
+ - db_messages_loaded: Messages loaded from database
581
+ - temporal_messages_loaded: Messages loaded from Temporal
582
+ - messages_sent: Messages yielded to stream
583
+ - messages_skipped_empty: Messages skipped due to empty content
584
+ - messages_deduplicated: Messages skipped due to deduplication
585
+ - db_load_duration_ms: Database query duration
586
+ - temporal_load_duration_ms: Temporal query duration
587
+ """
588
+ return self._stats.copy()