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,373 @@
1
+ """Streaming utilities for agent and team execution"""
2
+
3
+ from typing import Dict, Any, Callable, Optional
4
+ import structlog
5
+
6
+ logger = structlog.get_logger()
7
+
8
+
9
+ class StreamingHelper:
10
+ """
11
+ Helper for handling streaming from Agno Agent/Team executions.
12
+
13
+ Provides utilities for:
14
+ - Publishing events to Control Plane
15
+ - Tracking run_id from streaming chunks
16
+ - Collecting response content
17
+ - Publishing tool execution events
18
+ - Handling member message streaming
19
+ - Tracking tool IDs for proper start/complete matching
20
+ """
21
+
22
+ def __init__(self, control_plane_client, execution_id: str):
23
+ self.control_plane = control_plane_client
24
+ self.execution_id = execution_id
25
+ self.run_id_published = False
26
+ self.response_content = []
27
+ self.member_message_ids = {} # Track message_id per member
28
+ self.active_streaming_member = None # Track which member is streaming
29
+ self.tool_execution_ids = {} # Track tool IDs for matching start/complete events
30
+
31
+ def handle_run_id(self, chunk: Any, on_run_id: Optional[Callable[[str], None]] = None) -> None:
32
+ """
33
+ Capture and publish run_id from first streaming chunk.
34
+
35
+ Args:
36
+ chunk: Streaming chunk from Agno
37
+ on_run_id: Optional callback when run_id is captured
38
+ """
39
+ if not self.run_id_published and hasattr(chunk, 'run_id') and chunk.run_id:
40
+ run_id = chunk.run_id
41
+
42
+ logger.info("run_id_captured", execution_id=self.execution_id[:8], run_id=run_id[:16])
43
+
44
+ # Publish to Control Plane for UI
45
+ self.control_plane.publish_event(
46
+ execution_id=self.execution_id,
47
+ event_type="run_started",
48
+ data={
49
+ "run_id": run_id,
50
+ "execution_id": self.execution_id,
51
+ "cancellable": True,
52
+ }
53
+ )
54
+
55
+ self.run_id_published = True
56
+
57
+ # Call callback if provided (for cancellation manager)
58
+ if on_run_id:
59
+ on_run_id(run_id)
60
+
61
+ async def handle_content_chunk(
62
+ self,
63
+ chunk: Any,
64
+ message_id: str,
65
+ print_to_console: bool = True
66
+ ) -> Optional[str]:
67
+ """
68
+ Handle content chunk from streaming response.
69
+
70
+ Args:
71
+ chunk: Streaming chunk
72
+ message_id: Unique message ID for this turn
73
+ print_to_console: Whether to print to stdout
74
+
75
+ Returns:
76
+ Content string if present, None otherwise
77
+ """
78
+ # Check for both 'response' (RuntimeExecutionResult) and 'content' (legacy/Agno)
79
+ content = None
80
+
81
+ # DEBUG: Log what attributes the chunk has
82
+ print(f"[DEBUG] StreamingHelper.handle_content_chunk: chunk type = {type(chunk).__name__}")
83
+ print(f"[DEBUG] StreamingHelper.handle_content_chunk: has 'response' = {hasattr(chunk, 'response')}")
84
+ print(f"[DEBUG] StreamingHelper.handle_content_chunk: has 'content' = {hasattr(chunk, 'content')}")
85
+
86
+ if hasattr(chunk, 'response') and chunk.response:
87
+ content = str(chunk.response)
88
+ print(f"[DEBUG] StreamingHelper.handle_content_chunk: extracted from 'response': {repr(content[:100])}")
89
+ elif hasattr(chunk, 'content') and chunk.content:
90
+ content = str(chunk.content)
91
+ print(f"[DEBUG] StreamingHelper.handle_content_chunk: extracted from 'content': {repr(content[:100])}")
92
+ else:
93
+ print(f"[DEBUG] StreamingHelper.handle_content_chunk: NO CONTENT FOUND!")
94
+
95
+ if content:
96
+ self.response_content.append(content)
97
+
98
+ if print_to_console:
99
+ print(content, end='', flush=True)
100
+
101
+ # Stream to Control Plane for real-time UI updates (NON-BLOCKING)
102
+ await self.control_plane.publish_event_async(
103
+ execution_id=self.execution_id,
104
+ event_type="message_chunk",
105
+ data={
106
+ "role": "assistant",
107
+ "content": content,
108
+ "is_chunk": True,
109
+ "message_id": message_id,
110
+ }
111
+ )
112
+
113
+ return content
114
+
115
+ return None
116
+
117
+ def get_full_response(self) -> str:
118
+ """Get the complete response accumulated from all chunks."""
119
+ return ''.join(self.response_content)
120
+
121
+ def handle_member_content_chunk(
122
+ self,
123
+ member_name: str,
124
+ content: str,
125
+ print_to_console: bool = True
126
+ ) -> str:
127
+ """
128
+ Handle content chunk from a team member.
129
+
130
+ Args:
131
+ member_name: Name of the team member
132
+ content: Content string
133
+ print_to_console: Whether to print to stdout
134
+
135
+ Returns:
136
+ The member's message_id
137
+ """
138
+ import time
139
+
140
+ # Generate unique message ID for this member if not exists
141
+ if member_name not in self.member_message_ids:
142
+ self.member_message_ids[member_name] = f"{self.execution_id}_{member_name}_{int(time.time() * 1000000)}"
143
+
144
+ # Print member name header once when they start
145
+ if print_to_console:
146
+ print(f"\n[{member_name}] ", end='', flush=True)
147
+
148
+ # If switching to a different member, mark the previous one as complete
149
+ if self.active_streaming_member and self.active_streaming_member != member_name:
150
+ self.publish_member_complete(self.active_streaming_member)
151
+
152
+ # Track that this member is now actively streaming
153
+ self.active_streaming_member = member_name
154
+
155
+ # Print content without repeated member name prefix
156
+ if print_to_console:
157
+ print(content, end='', flush=True)
158
+
159
+ # Stream member chunk to Control Plane
160
+ message_id = self.member_message_ids[member_name]
161
+ self.control_plane.publish_event(
162
+ execution_id=self.execution_id,
163
+ event_type="member_message_chunk",
164
+ data={
165
+ "role": "assistant",
166
+ "content": content,
167
+ "is_chunk": True,
168
+ "message_id": message_id,
169
+ "source": "team_member",
170
+ "member_name": member_name,
171
+ }
172
+ )
173
+
174
+ return message_id
175
+
176
+ def publish_member_complete(self, member_name: str) -> None:
177
+ """
178
+ Publish member_message_complete event.
179
+
180
+ Args:
181
+ member_name: Name of the member to mark as complete
182
+ """
183
+ if member_name in self.member_message_ids:
184
+ self.control_plane.publish_event(
185
+ execution_id=self.execution_id,
186
+ event_type="member_message_complete",
187
+ data={
188
+ "message_id": self.member_message_ids[member_name],
189
+ "member_name": member_name,
190
+ "source": "team_member",
191
+ }
192
+ )
193
+
194
+ def finalize_streaming(self) -> None:
195
+ """
196
+ Finalize streaming by marking any active member as complete.
197
+ Call this when streaming ends.
198
+ """
199
+ if self.active_streaming_member:
200
+ self.publish_member_complete(self.active_streaming_member)
201
+ self.active_streaming_member = None
202
+
203
+ def publish_tool_start(
204
+ self,
205
+ tool_name: str,
206
+ tool_execution_id: str,
207
+ tool_args: Optional[Dict[str, Any]] = None,
208
+ source: str = "agent",
209
+ member_name: Optional[str] = None
210
+ ) -> str:
211
+ """
212
+ Publish tool execution start event.
213
+
214
+ Args:
215
+ tool_name: Name of the tool
216
+ tool_execution_id: Unique ID for this tool execution
217
+ tool_args: Tool arguments
218
+ source: "agent" or "team_member" or "team_leader" or "team"
219
+ member_name: Name of member (if tool is from a member)
220
+
221
+ Returns:
222
+ message_id for this tool execution
223
+ """
224
+ import time
225
+
226
+ message_id = f"{self.execution_id}_tool_{tool_execution_id}"
227
+ is_member_tool = member_name is not None
228
+ parent_message_id = self.member_message_ids.get(member_name) if is_member_tool else None
229
+
230
+ # Store tool info for matching with completion event
231
+ tool_key = f"{member_name or 'leader'}_{tool_name}_{int(time.time())}"
232
+ self.tool_execution_ids[tool_key] = {
233
+ "tool_execution_id": tool_execution_id,
234
+ "message_id": message_id,
235
+ "tool_name": tool_name,
236
+ "member_name": member_name,
237
+ "parent_message_id": parent_message_id,
238
+ }
239
+
240
+ event_type = "member_tool_started" if is_member_tool else "tool_started"
241
+
242
+ self.control_plane.publish_event(
243
+ execution_id=self.execution_id,
244
+ event_type=event_type,
245
+ data={
246
+ "tool_name": tool_name,
247
+ "tool_execution_id": tool_execution_id,
248
+ "message_id": message_id,
249
+ "tool_arguments": tool_args,
250
+ "source": "team_member" if is_member_tool else "team_leader",
251
+ "member_name": member_name,
252
+ "parent_message_id": parent_message_id,
253
+ "message": f"🔧 Executing tool: {tool_name}",
254
+ }
255
+ )
256
+
257
+ return message_id
258
+
259
+ def publish_tool_complete(
260
+ self,
261
+ tool_name: str,
262
+ tool_execution_id: str,
263
+ status: str = "success",
264
+ output: Optional[str] = None,
265
+ error: Optional[str] = None,
266
+ source: str = "agent",
267
+ member_name: Optional[str] = None
268
+ ) -> None:
269
+ """
270
+ Publish tool execution completion event.
271
+
272
+ Args:
273
+ tool_name: Name of the tool
274
+ tool_execution_id: Unique ID for this tool execution
275
+ status: "success" or "failed"
276
+ output: Tool output (if successful)
277
+ error: Error message (if failed)
278
+ source: "agent" or "team_member" or "team_leader" or "team"
279
+ member_name: Name of member (if tool is from a member)
280
+ """
281
+ import time
282
+
283
+ # Find the stored tool info from the start event
284
+ tool_key_pattern = f"{member_name or 'leader'}_{tool_name}"
285
+ matching_tool = None
286
+ for key, tool_info in list(self.tool_execution_ids.items()):
287
+ if key.startswith(tool_key_pattern):
288
+ matching_tool = tool_info
289
+ # Remove from tracking dict
290
+ del self.tool_execution_ids[key]
291
+ break
292
+
293
+ if matching_tool:
294
+ message_id = matching_tool["message_id"]
295
+ parent_message_id = matching_tool["parent_message_id"]
296
+ # Use the stored tool_execution_id from the start event
297
+ tool_execution_id = matching_tool["tool_execution_id"]
298
+ else:
299
+ # Fallback if start event wasn't captured
300
+ message_id = f"{self.execution_id}_tool_{tool_execution_id}"
301
+ parent_message_id = self.member_message_ids.get(member_name) if member_name else None
302
+ logger.warning("tool_completion_without_start", tool_name=tool_name, member_name=member_name)
303
+
304
+ is_member_tool = member_name is not None
305
+ event_type = "member_tool_completed" if is_member_tool else "tool_completed"
306
+
307
+ self.control_plane.publish_event(
308
+ execution_id=self.execution_id,
309
+ event_type=event_type,
310
+ data={
311
+ "tool_name": tool_name,
312
+ "tool_execution_id": tool_execution_id, # Now uses the stored ID from start event
313
+ "message_id": message_id,
314
+ "status": status,
315
+ "tool_output": output[:1000] if output else None, # Limit size
316
+ "tool_error": error,
317
+ "source": "team_member" if is_member_tool else "team_leader",
318
+ "member_name": member_name,
319
+ "parent_message_id": parent_message_id,
320
+ "message": f"{'✅' if status == 'success' else '❌'} Tool {status}: {tool_name}",
321
+ }
322
+ )
323
+
324
+
325
+ def create_tool_hook(control_plane_client, execution_id: str):
326
+ """
327
+ Create a tool hook function for Agno Agent/Team.
328
+
329
+ This hook is called before and after each tool execution
330
+ to publish real-time updates to the Control Plane.
331
+
332
+ Args:
333
+ control_plane_client: Control Plane client instance
334
+ execution_id: Execution ID
335
+
336
+ Returns:
337
+ Hook function compatible with Agno tool_hooks
338
+ """
339
+ import time
340
+
341
+ def tool_hook(tool_name: str, tool_args: dict, result: Any = None, error: Exception = None):
342
+ """Tool hook for real-time updates"""
343
+ tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
344
+
345
+ if error is None and result is None:
346
+ # Tool starting
347
+ control_plane_client.publish_event(
348
+ execution_id=execution_id,
349
+ event_type="tool_started",
350
+ data={
351
+ "tool_name": tool_name,
352
+ "tool_execution_id": tool_execution_id,
353
+ "tool_arguments": tool_args,
354
+ "message": f"🔧 Starting: {tool_name}",
355
+ }
356
+ )
357
+ else:
358
+ # Tool completed
359
+ status = "failed" if error else "success"
360
+ control_plane_client.publish_event(
361
+ execution_id=execution_id,
362
+ event_type="tool_completed",
363
+ data={
364
+ "tool_name": tool_name,
365
+ "tool_execution_id": tool_execution_id,
366
+ "status": status,
367
+ "tool_output": str(result)[:1000] if result else None,
368
+ "tool_error": str(error) if error else None,
369
+ "message": f"{'✅' if status == 'success' else '❌'} {status}: {tool_name}",
370
+ }
371
+ )
372
+
373
+ return tool_hook