kubiya-control-plane-api 0.1.0__py3-none-any.whl → 0.3.4__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.

Potentially problematic release.


This version of kubiya-control-plane-api might be problematic. Click here for more details.

Files changed (185) hide show
  1. control_plane_api/README.md +266 -0
  2. control_plane_api/__init__.py +0 -0
  3. control_plane_api/__version__.py +1 -0
  4. control_plane_api/alembic/README +1 -0
  5. control_plane_api/alembic/env.py +98 -0
  6. control_plane_api/alembic/script.py.mako +28 -0
  7. control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
  8. control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
  9. control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
  10. control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
  11. control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
  12. control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
  13. control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
  14. control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
  15. control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
  16. control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
  17. control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
  18. control_plane_api/alembic.ini +148 -0
  19. control_plane_api/api/index.py +12 -0
  20. control_plane_api/app/__init__.py +11 -0
  21. control_plane_api/app/activities/__init__.py +20 -0
  22. control_plane_api/app/activities/agent_activities.py +379 -0
  23. control_plane_api/app/activities/team_activities.py +410 -0
  24. control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
  25. control_plane_api/app/config/__init__.py +35 -0
  26. control_plane_api/app/config/api_config.py +354 -0
  27. control_plane_api/app/config/model_pricing.py +318 -0
  28. control_plane_api/app/config.py +95 -0
  29. control_plane_api/app/database.py +135 -0
  30. control_plane_api/app/exceptions.py +408 -0
  31. control_plane_api/app/lib/__init__.py +11 -0
  32. control_plane_api/app/lib/job_executor.py +312 -0
  33. control_plane_api/app/lib/kubiya_client.py +235 -0
  34. control_plane_api/app/lib/litellm_pricing.py +166 -0
  35. control_plane_api/app/lib/planning_tools/__init__.py +22 -0
  36. control_plane_api/app/lib/planning_tools/agents.py +155 -0
  37. control_plane_api/app/lib/planning_tools/base.py +189 -0
  38. control_plane_api/app/lib/planning_tools/environments.py +214 -0
  39. control_plane_api/app/lib/planning_tools/resources.py +240 -0
  40. control_plane_api/app/lib/planning_tools/teams.py +198 -0
  41. control_plane_api/app/lib/policy_enforcer_client.py +939 -0
  42. control_plane_api/app/lib/redis_client.py +436 -0
  43. control_plane_api/app/lib/supabase.py +71 -0
  44. control_plane_api/app/lib/temporal_client.py +138 -0
  45. control_plane_api/app/lib/validation/__init__.py +20 -0
  46. control_plane_api/app/lib/validation/runtime_validation.py +287 -0
  47. control_plane_api/app/main.py +128 -0
  48. control_plane_api/app/middleware/__init__.py +8 -0
  49. control_plane_api/app/middleware/auth.py +513 -0
  50. control_plane_api/app/middleware/exception_handler.py +267 -0
  51. control_plane_api/app/middleware/rate_limiting.py +384 -0
  52. control_plane_api/app/middleware/request_id.py +202 -0
  53. control_plane_api/app/models/__init__.py +27 -0
  54. control_plane_api/app/models/agent.py +79 -0
  55. control_plane_api/app/models/analytics.py +206 -0
  56. control_plane_api/app/models/associations.py +81 -0
  57. control_plane_api/app/models/environment.py +63 -0
  58. control_plane_api/app/models/execution.py +93 -0
  59. control_plane_api/app/models/job.py +179 -0
  60. control_plane_api/app/models/llm_model.py +75 -0
  61. control_plane_api/app/models/presence.py +49 -0
  62. control_plane_api/app/models/project.py +47 -0
  63. control_plane_api/app/models/session.py +38 -0
  64. control_plane_api/app/models/team.py +66 -0
  65. control_plane_api/app/models/workflow.py +55 -0
  66. control_plane_api/app/policies/README.md +121 -0
  67. control_plane_api/app/policies/approved_users.rego +62 -0
  68. control_plane_api/app/policies/business_hours.rego +51 -0
  69. control_plane_api/app/policies/rate_limiting.rego +100 -0
  70. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  71. control_plane_api/app/routers/__init__.py +4 -0
  72. control_plane_api/app/routers/agents.py +364 -0
  73. control_plane_api/app/routers/agents_v2.py +1260 -0
  74. control_plane_api/app/routers/analytics.py +1014 -0
  75. control_plane_api/app/routers/context_manager.py +562 -0
  76. control_plane_api/app/routers/environment_context.py +270 -0
  77. control_plane_api/app/routers/environments.py +715 -0
  78. control_plane_api/app/routers/execution_environment.py +517 -0
  79. control_plane_api/app/routers/executions.py +1911 -0
  80. control_plane_api/app/routers/health.py +92 -0
  81. control_plane_api/app/routers/health_v2.py +326 -0
  82. control_plane_api/app/routers/integrations.py +274 -0
  83. control_plane_api/app/routers/jobs.py +1344 -0
  84. control_plane_api/app/routers/models.py +82 -0
  85. control_plane_api/app/routers/models_v2.py +361 -0
  86. control_plane_api/app/routers/policies.py +639 -0
  87. control_plane_api/app/routers/presence.py +234 -0
  88. control_plane_api/app/routers/projects.py +902 -0
  89. control_plane_api/app/routers/runners.py +379 -0
  90. control_plane_api/app/routers/runtimes.py +172 -0
  91. control_plane_api/app/routers/secrets.py +155 -0
  92. control_plane_api/app/routers/skills.py +1001 -0
  93. control_plane_api/app/routers/skills_definitions.py +140 -0
  94. control_plane_api/app/routers/task_planning.py +1256 -0
  95. control_plane_api/app/routers/task_queues.py +654 -0
  96. control_plane_api/app/routers/team_context.py +270 -0
  97. control_plane_api/app/routers/teams.py +1400 -0
  98. control_plane_api/app/routers/worker_queues.py +1545 -0
  99. control_plane_api/app/routers/workers.py +935 -0
  100. control_plane_api/app/routers/workflows.py +204 -0
  101. control_plane_api/app/runtimes/__init__.py +6 -0
  102. control_plane_api/app/runtimes/validation.py +344 -0
  103. control_plane_api/app/schemas/job_schemas.py +295 -0
  104. control_plane_api/app/services/__init__.py +1 -0
  105. control_plane_api/app/services/agno_service.py +619 -0
  106. control_plane_api/app/services/litellm_service.py +190 -0
  107. control_plane_api/app/services/policy_service.py +525 -0
  108. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  109. control_plane_api/app/skills/__init__.py +44 -0
  110. control_plane_api/app/skills/base.py +229 -0
  111. control_plane_api/app/skills/business_intelligence.py +189 -0
  112. control_plane_api/app/skills/data_visualization.py +154 -0
  113. control_plane_api/app/skills/docker.py +104 -0
  114. control_plane_api/app/skills/file_generation.py +94 -0
  115. control_plane_api/app/skills/file_system.py +110 -0
  116. control_plane_api/app/skills/python.py +92 -0
  117. control_plane_api/app/skills/registry.py +65 -0
  118. control_plane_api/app/skills/shell.py +102 -0
  119. control_plane_api/app/skills/workflow_executor.py +469 -0
  120. control_plane_api/app/utils/workflow_executor.py +354 -0
  121. control_plane_api/app/workflows/__init__.py +11 -0
  122. control_plane_api/app/workflows/agent_execution.py +507 -0
  123. control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
  124. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  125. control_plane_api/app/workflows/team_execution.py +399 -0
  126. control_plane_api/scripts/seed_models.py +239 -0
  127. control_plane_api/worker/__init__.py +0 -0
  128. control_plane_api/worker/activities/__init__.py +0 -0
  129. control_plane_api/worker/activities/agent_activities.py +1241 -0
  130. control_plane_api/worker/activities/approval_activities.py +234 -0
  131. control_plane_api/worker/activities/runtime_activities.py +388 -0
  132. control_plane_api/worker/activities/skill_activities.py +267 -0
  133. control_plane_api/worker/activities/team_activities.py +1217 -0
  134. control_plane_api/worker/config/__init__.py +31 -0
  135. control_plane_api/worker/config/worker_config.py +275 -0
  136. control_plane_api/worker/control_plane_client.py +529 -0
  137. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  138. control_plane_api/worker/models/__init__.py +1 -0
  139. control_plane_api/worker/models/inputs.py +89 -0
  140. control_plane_api/worker/runtimes/__init__.py +31 -0
  141. control_plane_api/worker/runtimes/base.py +789 -0
  142. control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
  143. control_plane_api/worker/runtimes/default_runtime.py +617 -0
  144. control_plane_api/worker/runtimes/factory.py +173 -0
  145. control_plane_api/worker/runtimes/validation.py +93 -0
  146. control_plane_api/worker/services/__init__.py +1 -0
  147. control_plane_api/worker/services/agent_executor.py +422 -0
  148. control_plane_api/worker/services/agent_executor_v2.py +383 -0
  149. control_plane_api/worker/services/analytics_collector.py +457 -0
  150. control_plane_api/worker/services/analytics_service.py +464 -0
  151. control_plane_api/worker/services/approval_tools.py +310 -0
  152. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  153. control_plane_api/worker/services/cancellation_manager.py +177 -0
  154. control_plane_api/worker/services/data_visualization.py +827 -0
  155. control_plane_api/worker/services/jira_tools.py +257 -0
  156. control_plane_api/worker/services/runtime_analytics.py +328 -0
  157. control_plane_api/worker/services/session_service.py +194 -0
  158. control_plane_api/worker/services/skill_factory.py +175 -0
  159. control_plane_api/worker/services/team_executor.py +574 -0
  160. control_plane_api/worker/services/team_executor_v2.py +465 -0
  161. control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
  162. control_plane_api/worker/tests/__init__.py +1 -0
  163. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  164. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  165. control_plane_api/worker/tests/integration/__init__.py +0 -0
  166. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  167. control_plane_api/worker/tests/unit/__init__.py +0 -0
  168. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  169. control_plane_api/worker/utils/__init__.py +1 -0
  170. control_plane_api/worker/utils/chunk_batcher.py +305 -0
  171. control_plane_api/worker/utils/retry_utils.py +60 -0
  172. control_plane_api/worker/utils/streaming_utils.py +373 -0
  173. control_plane_api/worker/worker.py +753 -0
  174. control_plane_api/worker/workflows/__init__.py +0 -0
  175. control_plane_api/worker/workflows/agent_execution.py +589 -0
  176. control_plane_api/worker/workflows/team_execution.py +429 -0
  177. kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
  178. kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
  179. kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
  180. kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
  181. kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
  182. kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
  183. kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
  184. {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
  185. {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,617 @@
1
+ """
2
+ Default runtime implementation using Agno framework.
3
+
4
+ This runtime adapter wraps the existing Agno-based agent execution logic,
5
+ providing a clean interface that conforms to the AgentRuntime protocol.
6
+ """
7
+
8
+ from typing import Dict, Any, Optional, AsyncIterator, Callable, TYPE_CHECKING
9
+ import structlog
10
+ import asyncio
11
+ import os
12
+
13
+ from agno.agent import Agent
14
+ from agno.models.litellm import LiteLLM
15
+
16
+ from .base import (
17
+ RuntimeType,
18
+ RuntimeExecutionResult,
19
+ RuntimeExecutionContext,
20
+ RuntimeCapabilities,
21
+ BaseRuntime,
22
+ RuntimeRegistry,
23
+ )
24
+
25
+ if TYPE_CHECKING:
26
+ from control_plane_client import ControlPlaneClient
27
+ from services.cancellation_manager import CancellationManager
28
+
29
+ logger = structlog.get_logger(__name__)
30
+
31
+
32
+ @RuntimeRegistry.register(RuntimeType.DEFAULT)
33
+ class DefaultRuntime(BaseRuntime):
34
+ """
35
+ Runtime implementation using Agno framework.
36
+
37
+ This is the default runtime that wraps the existing Agno-based
38
+ agent execution logic. It maintains backward compatibility while
39
+ conforming to the new AgentRuntime interface.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ control_plane_client: "ControlPlaneClient",
45
+ cancellation_manager: "CancellationManager",
46
+ **kwargs,
47
+ ):
48
+ """
49
+ Initialize the Agno runtime.
50
+
51
+ Args:
52
+ control_plane_client: Client for Control Plane API
53
+ cancellation_manager: Manager for execution cancellation
54
+ **kwargs: Additional configuration options
55
+ """
56
+ super().__init__(control_plane_client, cancellation_manager, **kwargs)
57
+
58
+ def get_runtime_type(self) -> RuntimeType:
59
+ """Return RuntimeType.DEFAULT."""
60
+ return RuntimeType.DEFAULT
61
+
62
+ def get_capabilities(self) -> RuntimeCapabilities:
63
+ """Return Agno runtime capabilities."""
64
+ return RuntimeCapabilities(
65
+ streaming=True,
66
+ tools=True,
67
+ mcp=False,
68
+ hooks=True,
69
+ cancellation=True,
70
+ conversation_history=True,
71
+ custom_tools=False
72
+ )
73
+
74
+ async def _execute_impl(
75
+ self, context: RuntimeExecutionContext
76
+ ) -> RuntimeExecutionResult:
77
+ """
78
+ Execute agent using Agno framework without streaming.
79
+
80
+ Args:
81
+ context: Execution context with prompt, history, config
82
+
83
+ Returns:
84
+ RuntimeExecutionResult with response and metadata
85
+ """
86
+ try:
87
+ # Create Agno agent
88
+ agent = self._create_agno_agent(context)
89
+
90
+ # Register for cancellation
91
+ self.cancellation_manager.register(
92
+ execution_id=context.execution_id,
93
+ instance=agent,
94
+ instance_type="agent",
95
+ )
96
+
97
+ # Build conversation context
98
+ messages = self._build_conversation_messages(context)
99
+
100
+ # Execute without streaming
101
+ def run_agent():
102
+ if messages:
103
+ return agent.run(context.prompt, stream=False, messages=messages)
104
+ else:
105
+ return agent.run(context.prompt, stream=False)
106
+
107
+ # Run in thread pool to avoid blocking
108
+ result = await asyncio.to_thread(run_agent)
109
+
110
+ # Cleanup
111
+ self.cancellation_manager.unregister(context.execution_id)
112
+
113
+ # Extract response and metadata
114
+ response_content = (
115
+ result.content if hasattr(result, "content") else str(result)
116
+ )
117
+ usage = self._extract_usage(result)
118
+ tool_messages = self._extract_tool_messages(result)
119
+
120
+ return RuntimeExecutionResult(
121
+ response=response_content,
122
+ usage=usage,
123
+ success=True,
124
+ finish_reason="stop",
125
+ run_id=getattr(result, "run_id", None),
126
+ model=context.model_id,
127
+ tool_messages=tool_messages,
128
+ )
129
+
130
+ except asyncio.CancelledError:
131
+ # Handle cancellation
132
+ self.cancellation_manager.cancel(context.execution_id)
133
+ self.cancellation_manager.unregister(context.execution_id)
134
+ raise
135
+
136
+ except Exception as e:
137
+ self.logger.error(
138
+ "Agno execution failed",
139
+ execution_id=context.execution_id,
140
+ error=str(e),
141
+ )
142
+ self.cancellation_manager.unregister(context.execution_id)
143
+
144
+ return RuntimeExecutionResult(
145
+ response="",
146
+ usage={},
147
+ success=False,
148
+ error=str(e),
149
+ )
150
+
151
+ async def _stream_execute_impl(
152
+ self,
153
+ context: RuntimeExecutionContext,
154
+ event_callback: Optional[Callable[[Dict], None]] = None,
155
+ ) -> AsyncIterator[RuntimeExecutionResult]:
156
+ """
157
+ Execute agent with streaming using Agno framework.
158
+
159
+ Args:
160
+ context: Execution context
161
+ event_callback: Optional callback for real-time events
162
+
163
+ Yields:
164
+ RuntimeExecutionResult chunks as they arrive in real-time
165
+ """
166
+ try:
167
+ # Build conversation context
168
+ messages = self._build_conversation_messages(context)
169
+
170
+ # Stream execution - publish events INSIDE the thread (like old code)
171
+ accumulated_response = ""
172
+ run_result = None
173
+ import time
174
+ import queue
175
+
176
+ # Create queue for streaming chunks from thread to async
177
+ chunk_queue = queue.Queue()
178
+
179
+ # Generate unique message ID
180
+ message_id = f"{context.execution_id}_msg_{int(time.time() * 1000000)}"
181
+
182
+ # Create tool hook that publishes directly to Control Plane
183
+ def tool_hook(name: str = None, function_name: str = None, function=None, arguments: dict = None, **kwargs):
184
+ """Hook to capture tool execution for real-time streaming"""
185
+ tool_name = name or function_name or "unknown"
186
+ tool_args = arguments or {}
187
+ tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
188
+
189
+ # Publish tool start event (blocking call - OK in thread)
190
+ self.control_plane.publish_event(
191
+ execution_id=context.execution_id,
192
+ event_type="tool_started",
193
+ data={
194
+ "tool_name": tool_name,
195
+ "tool_execution_id": tool_execution_id,
196
+ "tool_arguments": tool_args,
197
+ "message": f"🔧 Executing tool: {tool_name}",
198
+ }
199
+ )
200
+
201
+ # Execute tool
202
+ result = None
203
+ error = None
204
+ try:
205
+ if function and callable(function):
206
+ result = function(**tool_args) if tool_args else function()
207
+ else:
208
+ raise ValueError(f"Function not callable: {function}")
209
+ status = "success"
210
+ except Exception as e:
211
+ error = e
212
+ status = "failed"
213
+
214
+ # Publish tool completion event
215
+ self.control_plane.publish_event(
216
+ execution_id=context.execution_id,
217
+ event_type="tool_completed",
218
+ data={
219
+ "tool_name": tool_name,
220
+ "tool_execution_id": tool_execution_id,
221
+ "status": status,
222
+ "tool_output": str(result)[:1000] if result else None,
223
+ "tool_error": str(error) if error else None,
224
+ "message": f"{'✅' if status == 'success' else '❌'} Tool {status}: {tool_name}",
225
+ }
226
+ )
227
+
228
+ if error:
229
+ raise error
230
+ return result
231
+
232
+ # Create Agno agent with tool hooks
233
+ agent = self._create_agno_agent_with_hooks(context, tool_hook)
234
+
235
+ # Register for cancellation
236
+ self.cancellation_manager.register(
237
+ execution_id=context.execution_id,
238
+ instance=agent,
239
+ instance_type="agent",
240
+ )
241
+
242
+ # Cache execution metadata
243
+ self.control_plane.cache_metadata(context.execution_id, "AGENT")
244
+
245
+ def stream_agent_run():
246
+ """
247
+ Run agent with streaming and publish events directly to Control Plane.
248
+ This runs in a thread pool, so blocking HTTP calls are OK here.
249
+ Put chunks in queue for async iterator to yield in real-time.
250
+ """
251
+ nonlocal accumulated_response, run_result
252
+ run_id_published = False
253
+
254
+ try:
255
+ if messages:
256
+ stream_response = agent.run(
257
+ context.prompt,
258
+ stream=True,
259
+ messages=messages,
260
+ )
261
+ else:
262
+ stream_response = agent.run(context.prompt, stream=True)
263
+
264
+ # Iterate over streaming chunks and publish IMMEDIATELY
265
+ for chunk in stream_response:
266
+ # Capture run_id for cancellation (first chunk)
267
+ if not run_id_published and hasattr(chunk, "run_id") and chunk.run_id:
268
+ self.cancellation_manager.set_run_id(
269
+ context.execution_id, chunk.run_id
270
+ )
271
+
272
+ # Publish run_id event
273
+ self.control_plane.publish_event(
274
+ execution_id=context.execution_id,
275
+ event_type="run_started",
276
+ data={
277
+ "run_id": chunk.run_id,
278
+ "execution_id": context.execution_id,
279
+ "cancellable": True,
280
+ }
281
+ )
282
+ run_id_published = True
283
+
284
+ # Extract content
285
+ chunk_content = ""
286
+ if hasattr(chunk, "content") and chunk.content:
287
+ if isinstance(chunk.content, str):
288
+ chunk_content = chunk.content
289
+ elif hasattr(chunk.content, "text"):
290
+ chunk_content = chunk.content.text
291
+
292
+ if chunk_content:
293
+ accumulated_response += chunk_content
294
+
295
+ # IMMEDIATELY publish to Control Plane (blocking call in thread - this is OK!)
296
+ self.control_plane.publish_event(
297
+ execution_id=context.execution_id,
298
+ event_type="message_chunk",
299
+ data={
300
+ "role": "assistant",
301
+ "content": chunk_content,
302
+ "is_chunk": True,
303
+ "message_id": message_id,
304
+ }
305
+ )
306
+
307
+ # Put chunk in queue for async iterator to yield
308
+ chunk_queue.put(("chunk", chunk_content))
309
+
310
+ # Store final result
311
+ run_result = stream_response
312
+
313
+ # Signal completion
314
+ chunk_queue.put(("done", run_result))
315
+
316
+ except Exception as e:
317
+ self.logger.error("Streaming error", error=str(e))
318
+ chunk_queue.put(("error", e))
319
+ raise
320
+
321
+ # Start streaming in background thread
322
+ import threading
323
+ stream_thread = threading.Thread(target=stream_agent_run, daemon=True)
324
+ stream_thread.start()
325
+
326
+ # Yield chunks as they arrive in the queue
327
+ while True:
328
+ try:
329
+ # Non-blocking get with short timeout for responsiveness
330
+ item_type, item_data = await asyncio.to_thread(chunk_queue.get, timeout=0.1)
331
+
332
+ if item_type == "chunk":
333
+ # Yield chunk immediately
334
+ yield RuntimeExecutionResult(
335
+ response=item_data,
336
+ usage={},
337
+ success=True,
338
+ )
339
+ elif item_type == "done":
340
+ # Final result - extract metadata and break
341
+ run_result = item_data
342
+ break
343
+ elif item_type == "error":
344
+ # Error occurred in thread
345
+ raise item_data
346
+
347
+ except queue.Empty:
348
+ # Queue empty, check if thread is still alive
349
+ if not stream_thread.is_alive():
350
+ # Thread died without putting "done" - something went wrong
351
+ break
352
+ # Thread still running, continue waiting
353
+ continue
354
+
355
+ # Wait for thread to complete
356
+ await asyncio.to_thread(stream_thread.join, timeout=5.0)
357
+
358
+ # Yield final result with complete metadata
359
+ usage = self._extract_usage(run_result) if run_result else {}
360
+ tool_messages = (
361
+ self._extract_tool_messages(run_result) if run_result else []
362
+ )
363
+
364
+ yield RuntimeExecutionResult(
365
+ response=accumulated_response, # Full accumulated response
366
+ usage=usage,
367
+ success=True,
368
+ finish_reason="stop",
369
+ run_id=getattr(run_result, "run_id", None) if run_result else None,
370
+ model=context.model_id,
371
+ tool_messages=tool_messages,
372
+ metadata={"accumulated_response": accumulated_response},
373
+ )
374
+
375
+ finally:
376
+ # Cleanup
377
+ self.cancellation_manager.unregister(context.execution_id)
378
+
379
+ # Private helper methods
380
+
381
+ def _create_agno_agent_with_hooks(
382
+ self,
383
+ context: RuntimeExecutionContext,
384
+ tool_hook: Callable,
385
+ ) -> Agent:
386
+ """
387
+ Create Agno Agent instance with tool hooks.
388
+
389
+ Args:
390
+ context: Execution context
391
+ tool_hook: Tool hook function for real-time events
392
+
393
+ Returns:
394
+ Configured Agno Agent instance
395
+ """
396
+ # Get LiteLLM configuration
397
+ litellm_api_base = os.getenv(
398
+ "LITELLM_API_BASE", "https://llm-proxy.kubiya.ai"
399
+ )
400
+ litellm_api_key = os.getenv("LITELLM_API_KEY")
401
+
402
+ if not litellm_api_key:
403
+ raise ValueError("LITELLM_API_KEY environment variable not set")
404
+
405
+ model = context.model_id or os.environ.get(
406
+ "LITELLM_DEFAULT_MODEL", "kubiya/claude-sonnet-4"
407
+ )
408
+
409
+ # Create agent with tool hooks
410
+ agent = Agent(
411
+ name=f"Agent {context.agent_id}",
412
+ role=context.system_prompt or "You are a helpful AI assistant",
413
+ model=LiteLLM(
414
+ id=f"openai/{model}",
415
+ api_base=litellm_api_base,
416
+ api_key=litellm_api_key,
417
+ ),
418
+ tools=context.skills if context.skills else None,
419
+ tool_hooks=[tool_hook], # Add hook for real-time tool updates
420
+ )
421
+
422
+ return agent
423
+
424
+ def _create_agno_agent(
425
+ self,
426
+ context: RuntimeExecutionContext,
427
+ event_callback: Optional[Callable] = None,
428
+ ) -> Agent:
429
+ """
430
+ Create Agno Agent instance.
431
+
432
+ Args:
433
+ context: Execution context
434
+ event_callback: Optional callback for tool execution events
435
+
436
+ Returns:
437
+ Configured Agno Agent instance
438
+ """
439
+ # Get LiteLLM configuration
440
+ litellm_api_base = os.getenv(
441
+ "LITELLM_API_BASE", "https://llm-proxy.kubiya.ai"
442
+ )
443
+ litellm_api_key = os.getenv("LITELLM_API_KEY")
444
+
445
+ if not litellm_api_key:
446
+ raise ValueError("LITELLM_API_KEY environment variable not set")
447
+
448
+ model = context.model_id or os.environ.get(
449
+ "LITELLM_DEFAULT_MODEL", "kubiya/claude-sonnet-4"
450
+ )
451
+
452
+ # Create tool hooks if event_callback provided
453
+ tool_hooks = []
454
+ if event_callback:
455
+ tool_hooks.append(self._create_tool_hook(context.execution_id, event_callback))
456
+
457
+ # Create agent
458
+ agent = Agent(
459
+ name=f"Agent {context.agent_id}",
460
+ role=context.system_prompt or "You are a helpful AI assistant",
461
+ model=LiteLLM(
462
+ id=f"openai/{model}",
463
+ api_base=litellm_api_base,
464
+ api_key=litellm_api_key,
465
+ ),
466
+ tools=context.skills if context.skills else None,
467
+ tool_hooks=tool_hooks if tool_hooks else None,
468
+ )
469
+
470
+ return agent
471
+
472
+ def _create_tool_hook(
473
+ self, execution_id: str, event_callback: Callable
474
+ ) -> Callable:
475
+ """
476
+ Create a tool hook for capturing tool execution events.
477
+
478
+ Args:
479
+ execution_id: Execution ID
480
+ event_callback: Callback to publish events
481
+
482
+ Returns:
483
+ Tool hook function
484
+ """
485
+
486
+ def tool_hook(
487
+ name: str = None,
488
+ function_name: str = None,
489
+ function=None,
490
+ arguments: dict = None,
491
+ **kwargs,
492
+ ):
493
+ """Hook to capture tool execution for real-time streaming"""
494
+ import time
495
+
496
+ tool_name = name or function_name or "unknown"
497
+ tool_args = arguments or {}
498
+ tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
499
+
500
+ # Publish tool start event
501
+ event_callback(
502
+ {
503
+ "type": "tool_start",
504
+ "tool_name": tool_name,
505
+ "tool_execution_id": tool_execution_id,
506
+ "tool_args": tool_args,
507
+ "execution_id": execution_id,
508
+ }
509
+ )
510
+
511
+ # Execute tool
512
+ result = None
513
+ error = None
514
+ try:
515
+ if function and callable(function):
516
+ result = function(**tool_args) if tool_args else function()
517
+ else:
518
+ raise ValueError(f"Function not callable: {function}")
519
+
520
+ status = "success"
521
+
522
+ except Exception as e:
523
+ error = e
524
+ status = "failed"
525
+
526
+ # Publish tool completion event
527
+ event_callback(
528
+ {
529
+ "type": "tool_complete",
530
+ "tool_name": tool_name,
531
+ "tool_execution_id": tool_execution_id,
532
+ "status": status,
533
+ "output": str(result)[:1000] if result else None,
534
+ "error": str(error) if error else None,
535
+ "execution_id": execution_id,
536
+ }
537
+ )
538
+
539
+ if error:
540
+ raise error
541
+
542
+ return result
543
+
544
+ return tool_hook
545
+
546
+ def _build_conversation_messages(
547
+ self, context: RuntimeExecutionContext
548
+ ) -> list:
549
+ """
550
+ Build conversation messages for Agno from context history.
551
+
552
+ Args:
553
+ context: Execution context with history
554
+
555
+ Returns:
556
+ List of message dicts for Agno
557
+ """
558
+ if not context.conversation_history:
559
+ return []
560
+
561
+ # Convert to Agno message format
562
+ messages = []
563
+ for msg in context.conversation_history:
564
+ role = msg.get("role", "user")
565
+ content = msg.get("content", "")
566
+
567
+ # Agno uses 'user' and 'assistant' roles
568
+ if role in ["user", "assistant"]:
569
+ messages.append({"role": role, "content": content})
570
+
571
+ return messages
572
+
573
+ def _extract_usage(self, result: Any) -> Dict[str, Any]:
574
+ """
575
+ Extract usage metrics from Agno result.
576
+
577
+ Args:
578
+ result: Agno run result
579
+
580
+ Returns:
581
+ Usage metrics dict
582
+ """
583
+ usage = {}
584
+ if hasattr(result, "metrics") and result.metrics:
585
+ metrics = result.metrics
586
+ usage = {
587
+ "prompt_tokens": getattr(metrics, "input_tokens", 0),
588
+ "completion_tokens": getattr(metrics, "output_tokens", 0),
589
+ "total_tokens": getattr(metrics, "total_tokens", 0),
590
+ }
591
+ return usage
592
+
593
+ def _extract_tool_messages(self, result: Any) -> list:
594
+ """
595
+ Extract tool messages from Agno result.
596
+
597
+ Args:
598
+ result: Agno run result
599
+
600
+ Returns:
601
+ List of tool message dicts
602
+ """
603
+ tool_messages = []
604
+
605
+ # Check if result has messages attribute
606
+ if hasattr(result, "messages") and result.messages:
607
+ for msg in result.messages:
608
+ if hasattr(msg, "role") and msg.role == "tool":
609
+ tool_messages.append(
610
+ {
611
+ "role": "tool",
612
+ "content": getattr(msg, "content", ""),
613
+ "tool_use_id": getattr(msg, "tool_use_id", None),
614
+ }
615
+ )
616
+
617
+ return tool_messages