agentrun-sdk 0.1.2__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 agentrun-sdk might be problematic. Click here for more details.

Files changed (115) hide show
  1. agentrun_operation_sdk/cli/__init__.py +1 -0
  2. agentrun_operation_sdk/cli/cli.py +19 -0
  3. agentrun_operation_sdk/cli/common.py +21 -0
  4. agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
  5. agentrun_operation_sdk/cli/runtime/commands.py +203 -0
  6. agentrun_operation_sdk/client/client.py +75 -0
  7. agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
  8. agentrun_operation_sdk/operations/runtime/configure.py +101 -0
  9. agentrun_operation_sdk/operations/runtime/launch.py +82 -0
  10. agentrun_operation_sdk/operations/runtime/models.py +31 -0
  11. agentrun_operation_sdk/services/runtime.py +152 -0
  12. agentrun_operation_sdk/utils/logging_config.py +72 -0
  13. agentrun_operation_sdk/utils/runtime/config.py +94 -0
  14. agentrun_operation_sdk/utils/runtime/container.py +280 -0
  15. agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
  16. agentrun_operation_sdk/utils/runtime/schema.py +56 -0
  17. agentrun_sdk/__init__.py +7 -0
  18. agentrun_sdk/agent/__init__.py +25 -0
  19. agentrun_sdk/agent/agent.py +696 -0
  20. agentrun_sdk/agent/agent_result.py +46 -0
  21. agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
  22. agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
  23. agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
  24. agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
  25. agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
  26. agentrun_sdk/agent/state.py +97 -0
  27. agentrun_sdk/event_loop/__init__.py +9 -0
  28. agentrun_sdk/event_loop/event_loop.py +499 -0
  29. agentrun_sdk/event_loop/streaming.py +319 -0
  30. agentrun_sdk/experimental/__init__.py +4 -0
  31. agentrun_sdk/experimental/hooks/__init__.py +15 -0
  32. agentrun_sdk/experimental/hooks/events.py +123 -0
  33. agentrun_sdk/handlers/__init__.py +10 -0
  34. agentrun_sdk/handlers/callback_handler.py +70 -0
  35. agentrun_sdk/hooks/__init__.py +49 -0
  36. agentrun_sdk/hooks/events.py +80 -0
  37. agentrun_sdk/hooks/registry.py +247 -0
  38. agentrun_sdk/models/__init__.py +10 -0
  39. agentrun_sdk/models/anthropic.py +432 -0
  40. agentrun_sdk/models/bedrock.py +649 -0
  41. agentrun_sdk/models/litellm.py +225 -0
  42. agentrun_sdk/models/llamaapi.py +438 -0
  43. agentrun_sdk/models/mistral.py +539 -0
  44. agentrun_sdk/models/model.py +95 -0
  45. agentrun_sdk/models/ollama.py +357 -0
  46. agentrun_sdk/models/openai.py +436 -0
  47. agentrun_sdk/models/sagemaker.py +598 -0
  48. agentrun_sdk/models/writer.py +449 -0
  49. agentrun_sdk/multiagent/__init__.py +22 -0
  50. agentrun_sdk/multiagent/a2a/__init__.py +15 -0
  51. agentrun_sdk/multiagent/a2a/executor.py +148 -0
  52. agentrun_sdk/multiagent/a2a/server.py +252 -0
  53. agentrun_sdk/multiagent/base.py +92 -0
  54. agentrun_sdk/multiagent/graph.py +555 -0
  55. agentrun_sdk/multiagent/swarm.py +656 -0
  56. agentrun_sdk/py.typed +1 -0
  57. agentrun_sdk/session/__init__.py +18 -0
  58. agentrun_sdk/session/file_session_manager.py +216 -0
  59. agentrun_sdk/session/repository_session_manager.py +152 -0
  60. agentrun_sdk/session/s3_session_manager.py +272 -0
  61. agentrun_sdk/session/session_manager.py +73 -0
  62. agentrun_sdk/session/session_repository.py +51 -0
  63. agentrun_sdk/telemetry/__init__.py +21 -0
  64. agentrun_sdk/telemetry/config.py +194 -0
  65. agentrun_sdk/telemetry/metrics.py +476 -0
  66. agentrun_sdk/telemetry/metrics_constants.py +15 -0
  67. agentrun_sdk/telemetry/tracer.py +563 -0
  68. agentrun_sdk/tools/__init__.py +17 -0
  69. agentrun_sdk/tools/decorator.py +569 -0
  70. agentrun_sdk/tools/executor.py +137 -0
  71. agentrun_sdk/tools/loader.py +152 -0
  72. agentrun_sdk/tools/mcp/__init__.py +13 -0
  73. agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
  74. agentrun_sdk/tools/mcp/mcp_client.py +423 -0
  75. agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
  76. agentrun_sdk/tools/mcp/mcp_types.py +63 -0
  77. agentrun_sdk/tools/registry.py +607 -0
  78. agentrun_sdk/tools/structured_output.py +421 -0
  79. agentrun_sdk/tools/tools.py +217 -0
  80. agentrun_sdk/tools/watcher.py +136 -0
  81. agentrun_sdk/types/__init__.py +5 -0
  82. agentrun_sdk/types/collections.py +23 -0
  83. agentrun_sdk/types/content.py +188 -0
  84. agentrun_sdk/types/event_loop.py +48 -0
  85. agentrun_sdk/types/exceptions.py +81 -0
  86. agentrun_sdk/types/guardrails.py +254 -0
  87. agentrun_sdk/types/media.py +89 -0
  88. agentrun_sdk/types/session.py +152 -0
  89. agentrun_sdk/types/streaming.py +201 -0
  90. agentrun_sdk/types/tools.py +258 -0
  91. agentrun_sdk/types/traces.py +5 -0
  92. agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
  93. agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
  94. agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
  95. agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
  96. agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
  97. agentrun_wrapper/__init__.py +11 -0
  98. agentrun_wrapper/_utils/__init__.py +6 -0
  99. agentrun_wrapper/_utils/endpoints.py +16 -0
  100. agentrun_wrapper/identity/__init__.py +5 -0
  101. agentrun_wrapper/identity/auth.py +211 -0
  102. agentrun_wrapper/memory/__init__.py +6 -0
  103. agentrun_wrapper/memory/client.py +1697 -0
  104. agentrun_wrapper/memory/constants.py +103 -0
  105. agentrun_wrapper/memory/controlplane.py +626 -0
  106. agentrun_wrapper/py.typed +1 -0
  107. agentrun_wrapper/runtime/__init__.py +13 -0
  108. agentrun_wrapper/runtime/app.py +473 -0
  109. agentrun_wrapper/runtime/context.py +34 -0
  110. agentrun_wrapper/runtime/models.py +25 -0
  111. agentrun_wrapper/services/__init__.py +1 -0
  112. agentrun_wrapper/services/identity.py +192 -0
  113. agentrun_wrapper/tools/__init__.py +6 -0
  114. agentrun_wrapper/tools/browser_client.py +325 -0
  115. agentrun_wrapper/tools/code_interpreter_client.py +186 -0
@@ -0,0 +1,97 @@
1
+ """Agent state management."""
2
+
3
+ import copy
4
+ import json
5
+ from typing import Any, Dict, Optional
6
+
7
+
8
+ class AgentState:
9
+ """Represents an Agent's stateful information outside of context provided to a model.
10
+
11
+ Provides a key-value store for agent state with JSON serialization validation and persistence support.
12
+ Key features:
13
+ - JSON serialization validation on assignment
14
+ - Get/set/delete operations
15
+ """
16
+
17
+ def __init__(self, initial_state: Optional[Dict[str, Any]] = None):
18
+ """Initialize AgentState."""
19
+ self._state: Dict[str, Dict[str, Any]]
20
+ if initial_state:
21
+ self._validate_json_serializable(initial_state)
22
+ self._state = copy.deepcopy(initial_state)
23
+ else:
24
+ self._state = {}
25
+
26
+ def set(self, key: str, value: Any) -> None:
27
+ """Set a value in the state.
28
+
29
+ Args:
30
+ key: The key to store the value under
31
+ value: The value to store (must be JSON serializable)
32
+
33
+ Raises:
34
+ ValueError: If key is invalid, or if value is not JSON serializable
35
+ """
36
+ self._validate_key(key)
37
+ self._validate_json_serializable(value)
38
+
39
+ self._state[key] = copy.deepcopy(value)
40
+
41
+ def get(self, key: Optional[str] = None) -> Any:
42
+ """Get a value or entire state.
43
+
44
+ Args:
45
+ key: The key to retrieve (if None, returns entire state object)
46
+
47
+ Returns:
48
+ The stored value, entire state dict, or None if not found
49
+ """
50
+ if key is None:
51
+ return copy.deepcopy(self._state)
52
+ else:
53
+ # Return specific key
54
+ return copy.deepcopy(self._state.get(key))
55
+
56
+ def delete(self, key: str) -> None:
57
+ """Delete a specific key from the state.
58
+
59
+ Args:
60
+ key: The key to delete
61
+ """
62
+ self._validate_key(key)
63
+
64
+ self._state.pop(key, None)
65
+
66
+ def _validate_key(self, key: str) -> None:
67
+ """Validate that a key is valid.
68
+
69
+ Args:
70
+ key: The key to validate
71
+
72
+ Raises:
73
+ ValueError: If key is invalid
74
+ """
75
+ if key is None:
76
+ raise ValueError("Key cannot be None")
77
+ if not isinstance(key, str):
78
+ raise ValueError("Key must be a string")
79
+ if not key.strip():
80
+ raise ValueError("Key cannot be empty")
81
+
82
+ def _validate_json_serializable(self, value: Any) -> None:
83
+ """Validate that a value is JSON serializable.
84
+
85
+ Args:
86
+ value: The value to validate
87
+
88
+ Raises:
89
+ ValueError: If value is not JSON serializable
90
+ """
91
+ try:
92
+ json.dumps(value)
93
+ except (TypeError, ValueError) as e:
94
+ raise ValueError(
95
+ f"Value is not JSON serializable: {type(value).__name__}. "
96
+ f"Only JSON-compatible types (str, int, float, bool, list, dict, None) are allowed."
97
+ ) from e
@@ -0,0 +1,9 @@
1
+ """This package provides the core event loop implementation for the agents SDK.
2
+
3
+ The event loop enables conversational AI agents to process messages, execute tools, and handle errors in a controlled,
4
+ iterative manner.
5
+ """
6
+
7
+ from . import event_loop
8
+
9
+ __all__ = ["event_loop"]
@@ -0,0 +1,499 @@
1
+ """This module implements the central event loop.
2
+
3
+ The event loop allows agents to:
4
+
5
+ 1. Process conversation messages
6
+ 2. Execute tools based on model requests
7
+ 3. Handle errors and recovery strategies
8
+ 4. Manage recursive execution cycles
9
+ """
10
+
11
+ import logging
12
+ import time
13
+ import uuid
14
+ from typing import TYPE_CHECKING, Any, AsyncGenerator, cast
15
+
16
+ from opentelemetry import trace as trace_api
17
+
18
+ from ..experimental.hooks import (
19
+ AfterModelInvocationEvent,
20
+ AfterToolInvocationEvent,
21
+ BeforeModelInvocationEvent,
22
+ BeforeToolInvocationEvent,
23
+ )
24
+ from ..hooks import (
25
+ MessageAddedEvent,
26
+ )
27
+ from ..telemetry.metrics import Trace
28
+ from ..telemetry.tracer import get_tracer
29
+ from ..tools.executor import run_tools, validate_and_prepare_tools
30
+ from ..types.content import Message
31
+ from ..types.exceptions import (
32
+ ContextWindowOverflowException,
33
+ EventLoopException,
34
+ MaxTokensReachedException,
35
+ ModelThrottledException,
36
+ )
37
+ from ..types.streaming import Metrics, StopReason
38
+ from ..types.tools import ToolChoice, ToolChoiceAuto, ToolConfig, ToolGenerator, ToolResult, ToolUse
39
+ from .streaming import stream_messages
40
+
41
+ if TYPE_CHECKING:
42
+ from ..agent import Agent
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ MAX_ATTEMPTS = 6
47
+ INITIAL_DELAY = 4
48
+ MAX_DELAY = 240 # 4 minutes
49
+
50
+
51
+ async def event_loop_cycle(agent: "Agent", invocation_state: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None]:
52
+ """Execute a single cycle of the event loop.
53
+
54
+ This core function processes a single conversation turn, handling model inference, tool execution, and error
55
+ recovery. It manages the entire lifecycle of a conversation turn, including:
56
+
57
+ 1. Initializing cycle state and metrics
58
+ 2. Checking execution limits
59
+ 3. Processing messages with the model
60
+ 4. Handling tool execution requests
61
+ 5. Managing recursive calls for multi-turn tool interactions
62
+ 6. Collecting and reporting metrics
63
+ 7. Error handling and recovery
64
+
65
+ Args:
66
+ agent: The agent for which the cycle is being executed.
67
+ invocation_state: Additional arguments including:
68
+
69
+ - request_state: State maintained across cycles
70
+ - event_loop_cycle_id: Unique ID for this cycle
71
+ - event_loop_cycle_span: Current tracing Span for this cycle
72
+
73
+ Yields:
74
+ Model and tool stream events. The last event is a tuple containing:
75
+
76
+ - StopReason: Reason the model stopped generating (e.g., "tool_use")
77
+ - Message: The generated message from the model
78
+ - EventLoopMetrics: Updated metrics for the event loop
79
+ - Any: Updated request state
80
+
81
+ Raises:
82
+ EventLoopException: If an error occurs during execution
83
+ ContextWindowOverflowException: If the input is too large for the model
84
+ """
85
+ # Initialize cycle state
86
+ invocation_state["event_loop_cycle_id"] = uuid.uuid4()
87
+
88
+ # Initialize state and get cycle trace
89
+ if "request_state" not in invocation_state:
90
+ invocation_state["request_state"] = {}
91
+ attributes = {"event_loop_cycle_id": str(invocation_state.get("event_loop_cycle_id"))}
92
+ cycle_start_time, cycle_trace = agent.event_loop_metrics.start_cycle(attributes=attributes)
93
+ invocation_state["event_loop_cycle_trace"] = cycle_trace
94
+
95
+ yield {"callback": {"start": True}}
96
+ yield {"callback": {"start_event_loop": True}}
97
+
98
+ # Create tracer span for this event loop cycle
99
+ tracer = get_tracer()
100
+ cycle_span = tracer.start_event_loop_cycle_span(
101
+ invocation_state=invocation_state, messages=agent.messages, parent_span=agent.trace_span
102
+ )
103
+ invocation_state["event_loop_cycle_span"] = cycle_span
104
+
105
+ # Create a trace for the stream_messages call
106
+ stream_trace = Trace("stream_messages", parent_id=cycle_trace.id)
107
+ cycle_trace.add_child(stream_trace)
108
+
109
+ # Process messages with exponential backoff for throttling
110
+ message: Message
111
+ stop_reason: StopReason
112
+ usage: Any
113
+ metrics: Metrics
114
+
115
+ # Retry loop for handling throttling exceptions
116
+ current_delay = INITIAL_DELAY
117
+ for attempt in range(MAX_ATTEMPTS):
118
+ model_id = agent.model.config.get("model_id") if hasattr(agent.model, "config") else None
119
+ model_invoke_span = tracer.start_model_invoke_span(
120
+ messages=agent.messages,
121
+ parent_span=cycle_span,
122
+ model_id=model_id,
123
+ )
124
+ with trace_api.use_span(model_invoke_span):
125
+ tool_specs = agent.tool_registry.get_all_tool_specs()
126
+
127
+ agent.hooks.invoke_callbacks(
128
+ BeforeModelInvocationEvent(
129
+ agent=agent,
130
+ )
131
+ )
132
+
133
+ try:
134
+ # TODO: To maintain backwards compatibility, we need to combine the stream event with invocation_state
135
+ # before yielding to the callback handler. This will be revisited when migrating to strongly
136
+ # typed events.
137
+ async for event in stream_messages(agent.model, agent.system_prompt, agent.messages, tool_specs):
138
+ if "callback" in event:
139
+ yield {
140
+ "callback": {
141
+ **event["callback"],
142
+ **(invocation_state if "delta" in event["callback"] else {}),
143
+ }
144
+ }
145
+
146
+ stop_reason, message, usage, metrics = event["stop"]
147
+ invocation_state.setdefault("request_state", {})
148
+
149
+ agent.hooks.invoke_callbacks(
150
+ AfterModelInvocationEvent(
151
+ agent=agent,
152
+ stop_response=AfterModelInvocationEvent.ModelStopResponse(
153
+ stop_reason=stop_reason,
154
+ message=message,
155
+ ),
156
+ )
157
+ )
158
+
159
+ if model_invoke_span:
160
+ tracer.end_model_invoke_span(model_invoke_span, message, usage, stop_reason)
161
+ break # Success! Break out of retry loop
162
+
163
+ except Exception as e:
164
+ if model_invoke_span:
165
+ tracer.end_span_with_error(model_invoke_span, str(e), e)
166
+
167
+ agent.hooks.invoke_callbacks(
168
+ AfterModelInvocationEvent(
169
+ agent=agent,
170
+ exception=e,
171
+ )
172
+ )
173
+
174
+ if isinstance(e, ModelThrottledException):
175
+ if attempt + 1 == MAX_ATTEMPTS:
176
+ yield {"callback": {"force_stop": True, "force_stop_reason": str(e)}}
177
+ raise e
178
+
179
+ logger.debug(
180
+ "retry_delay_seconds=<%s>, max_attempts=<%s>, current_attempt=<%s> "
181
+ "| throttling exception encountered "
182
+ "| delaying before next retry",
183
+ current_delay,
184
+ MAX_ATTEMPTS,
185
+ attempt + 1,
186
+ )
187
+ time.sleep(current_delay)
188
+ current_delay = min(current_delay * 2, MAX_DELAY)
189
+
190
+ yield {"callback": {"event_loop_throttled_delay": current_delay, **invocation_state}}
191
+ else:
192
+ raise e
193
+
194
+ try:
195
+ if stop_reason == "max_tokens":
196
+ """
197
+ Handle max_tokens limit reached by the model.
198
+
199
+ When the model reaches its maximum token limit, this represents a potentially unrecoverable
200
+ state where the model's response was truncated. By default, Strands fails hard with an
201
+ MaxTokensReachedException to maintain consistency with other failure types.
202
+ """
203
+ raise MaxTokensReachedException(
204
+ message=(
205
+ "Agent has reached an unrecoverable state due to max_tokens limit. "
206
+ "For more information see: "
207
+ "https://strandsagents.com/latest/user-guide/concepts/agents/agent-loop/#maxtokensreachedexception"
208
+ ),
209
+ incomplete_message=message,
210
+ )
211
+ # Add message in trace and mark the end of the stream messages trace
212
+ stream_trace.add_message(message)
213
+ stream_trace.end()
214
+
215
+ # Add the response message to the conversation
216
+ agent.messages.append(message)
217
+ agent.hooks.invoke_callbacks(MessageAddedEvent(agent=agent, message=message))
218
+ yield {"callback": {"message": message}}
219
+
220
+ # Update metrics
221
+ agent.event_loop_metrics.update_usage(usage)
222
+ agent.event_loop_metrics.update_metrics(metrics)
223
+
224
+ # If the model is requesting to use tools
225
+ if stop_reason == "tool_use":
226
+ # Handle tool execution
227
+ events = _handle_tool_execution(
228
+ stop_reason,
229
+ message,
230
+ agent=agent,
231
+ cycle_trace=cycle_trace,
232
+ cycle_span=cycle_span,
233
+ cycle_start_time=cycle_start_time,
234
+ invocation_state=invocation_state,
235
+ )
236
+ async for event in events:
237
+ yield event
238
+
239
+ return
240
+
241
+ # End the cycle and return results
242
+ agent.event_loop_metrics.end_cycle(cycle_start_time, cycle_trace, attributes)
243
+ if cycle_span:
244
+ tracer.end_event_loop_cycle_span(
245
+ span=cycle_span,
246
+ message=message,
247
+ )
248
+ except EventLoopException as e:
249
+ if cycle_span:
250
+ tracer.end_span_with_error(cycle_span, str(e), e)
251
+
252
+ # Don't yield or log the exception - we already did it when we
253
+ # raised the exception and we don't need that duplication.
254
+ raise
255
+ except (ContextWindowOverflowException, MaxTokensReachedException) as e:
256
+ # Special cased exceptions which we want to bubble up rather than get wrapped in an EventLoopException
257
+ if cycle_span:
258
+ tracer.end_span_with_error(cycle_span, str(e), e)
259
+ raise e
260
+ except Exception as e:
261
+ if cycle_span:
262
+ tracer.end_span_with_error(cycle_span, str(e), e)
263
+
264
+ # Handle any other exceptions
265
+ yield {"callback": {"force_stop": True, "force_stop_reason": str(e)}}
266
+ logger.exception("cycle failed")
267
+ raise EventLoopException(e, invocation_state["request_state"]) from e
268
+
269
+ yield {"stop": (stop_reason, message, agent.event_loop_metrics, invocation_state["request_state"])}
270
+
271
+
272
+ async def recurse_event_loop(agent: "Agent", invocation_state: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None]:
273
+ """Make a recursive call to event_loop_cycle with the current state.
274
+
275
+ This function is used when the event loop needs to continue processing after tool execution.
276
+
277
+ Args:
278
+ agent: Agent for which the recursive call is being made.
279
+ invocation_state: Arguments to pass through event_loop_cycle
280
+
281
+
282
+ Yields:
283
+ Results from event_loop_cycle where the last result contains:
284
+
285
+ - StopReason: Reason the model stopped generating
286
+ - Message: The generated message from the model
287
+ - EventLoopMetrics: Updated metrics for the event loop
288
+ - Any: Updated request state
289
+ """
290
+ cycle_trace = invocation_state["event_loop_cycle_trace"]
291
+
292
+ # Recursive call trace
293
+ recursive_trace = Trace("Recursive call", parent_id=cycle_trace.id)
294
+ cycle_trace.add_child(recursive_trace)
295
+
296
+ yield {"callback": {"start": True}}
297
+
298
+ events = event_loop_cycle(agent=agent, invocation_state=invocation_state)
299
+ async for event in events:
300
+ yield event
301
+
302
+ recursive_trace.end()
303
+
304
+
305
+ async def run_tool(agent: "Agent", tool_use: ToolUse, invocation_state: dict[str, Any]) -> ToolGenerator:
306
+ """Process a tool invocation.
307
+
308
+ Looks up the tool in the registry and streams it with the provided parameters.
309
+
310
+ Args:
311
+ agent: The agent for which the tool is being executed.
312
+ tool_use: The tool object to process, containing name and parameters.
313
+ invocation_state: Context for the tool invocation, including agent state.
314
+
315
+ Yields:
316
+ Tool events with the last being the tool result.
317
+ """
318
+ logger.debug("tool_use=<%s> | streaming", tool_use)
319
+ tool_name = tool_use["name"]
320
+
321
+ # Get the tool info
322
+ tool_info = agent.tool_registry.dynamic_tools.get(tool_name)
323
+ tool_func = tool_info if tool_info is not None else agent.tool_registry.registry.get(tool_name)
324
+
325
+ # Add standard arguments to invocation_state for Python tools
326
+ invocation_state.update(
327
+ {
328
+ "model": agent.model,
329
+ "system_prompt": agent.system_prompt,
330
+ "messages": agent.messages,
331
+ "tool_config": ToolConfig( # for backwards compatability
332
+ tools=[{"toolSpec": tool_spec} for tool_spec in agent.tool_registry.get_all_tool_specs()],
333
+ toolChoice=cast(ToolChoice, {"auto": ToolChoiceAuto()}),
334
+ ),
335
+ }
336
+ )
337
+
338
+ before_event = agent.hooks.invoke_callbacks(
339
+ BeforeToolInvocationEvent(
340
+ agent=agent,
341
+ selected_tool=tool_func,
342
+ tool_use=tool_use,
343
+ invocation_state=invocation_state,
344
+ )
345
+ )
346
+
347
+ try:
348
+ selected_tool = before_event.selected_tool
349
+ tool_use = before_event.tool_use
350
+ invocation_state = before_event.invocation_state # Get potentially modified invocation_state from hook
351
+
352
+ # Check if tool exists
353
+ if not selected_tool:
354
+ if tool_func == selected_tool:
355
+ logger.error(
356
+ "tool_name=<%s>, available_tools=<%s> | tool not found in registry",
357
+ tool_name,
358
+ list(agent.tool_registry.registry.keys()),
359
+ )
360
+ else:
361
+ logger.debug(
362
+ "tool_name=<%s>, tool_use_id=<%s> | a hook resulted in a non-existing tool call",
363
+ tool_name,
364
+ str(tool_use.get("toolUseId")),
365
+ )
366
+
367
+ result: ToolResult = {
368
+ "toolUseId": str(tool_use.get("toolUseId")),
369
+ "status": "error",
370
+ "content": [{"text": f"Unknown tool: {tool_name}"}],
371
+ }
372
+ # for every Before event call, we need to have an AfterEvent call
373
+ after_event = agent.hooks.invoke_callbacks(
374
+ AfterToolInvocationEvent(
375
+ agent=agent,
376
+ selected_tool=selected_tool,
377
+ tool_use=tool_use,
378
+ invocation_state=invocation_state, # Keep as invocation_state for backward compatibility with hooks
379
+ result=result,
380
+ )
381
+ )
382
+ yield after_event.result
383
+ return
384
+
385
+ async for event in selected_tool.stream(tool_use, invocation_state):
386
+ yield event
387
+
388
+ result = event
389
+
390
+ after_event = agent.hooks.invoke_callbacks(
391
+ AfterToolInvocationEvent(
392
+ agent=agent,
393
+ selected_tool=selected_tool,
394
+ tool_use=tool_use,
395
+ invocation_state=invocation_state, # Keep as invocation_state for backward compatibility with hooks
396
+ result=result,
397
+ )
398
+ )
399
+ yield after_event.result
400
+
401
+ except Exception as e:
402
+ logger.exception("tool_name=<%s> | failed to process tool", tool_name)
403
+ error_result: ToolResult = {
404
+ "toolUseId": str(tool_use.get("toolUseId")),
405
+ "status": "error",
406
+ "content": [{"text": f"Error: {str(e)}"}],
407
+ }
408
+ after_event = agent.hooks.invoke_callbacks(
409
+ AfterToolInvocationEvent(
410
+ agent=agent,
411
+ selected_tool=selected_tool,
412
+ tool_use=tool_use,
413
+ invocation_state=invocation_state, # Keep as invocation_state for backward compatibility with hooks
414
+ result=error_result,
415
+ exception=e,
416
+ )
417
+ )
418
+ yield after_event.result
419
+
420
+
421
+ async def _handle_tool_execution(
422
+ stop_reason: StopReason,
423
+ message: Message,
424
+ agent: "Agent",
425
+ cycle_trace: Trace,
426
+ cycle_span: Any,
427
+ cycle_start_time: float,
428
+ invocation_state: dict[str, Any],
429
+ ) -> AsyncGenerator[dict[str, Any], None]:
430
+ tool_uses: list[ToolUse] = []
431
+ tool_results: list[ToolResult] = []
432
+ invalid_tool_use_ids: list[str] = []
433
+
434
+ """
435
+ Handles the execution of tools requested by the model during an event loop cycle.
436
+
437
+ Args:
438
+ stop_reason: The reason the model stopped generating.
439
+ message: The message from the model that may contain tool use requests.
440
+ event_loop_metrics: Metrics tracking object for the event loop.
441
+ event_loop_parent_span: Span for the parent of this event loop.
442
+ cycle_trace: Trace object for the current event loop cycle.
443
+ cycle_span: Span object for tracing the cycle (type may vary).
444
+ cycle_start_time: Start time of the current cycle.
445
+ invocation_state: Additional keyword arguments, including request state.
446
+
447
+ Yields:
448
+ Tool stream events along with events yielded from a recursive call to the event loop. The last event is a tuple
449
+ containing:
450
+ - The stop reason,
451
+ - The updated message,
452
+ - The updated event loop metrics,
453
+ - The updated request state.
454
+ """
455
+ validate_and_prepare_tools(message, tool_uses, tool_results, invalid_tool_use_ids)
456
+
457
+ if not tool_uses:
458
+ yield {"stop": (stop_reason, message, agent.event_loop_metrics, invocation_state["request_state"])}
459
+ return
460
+
461
+ def tool_handler(tool_use: ToolUse) -> ToolGenerator:
462
+ return run_tool(agent, tool_use, invocation_state)
463
+
464
+ tool_events = run_tools(
465
+ handler=tool_handler,
466
+ tool_uses=tool_uses,
467
+ event_loop_metrics=agent.event_loop_metrics,
468
+ invalid_tool_use_ids=invalid_tool_use_ids,
469
+ tool_results=tool_results,
470
+ cycle_trace=cycle_trace,
471
+ parent_span=cycle_span,
472
+ )
473
+ async for tool_event in tool_events:
474
+ yield tool_event
475
+
476
+ # Store parent cycle ID for the next cycle
477
+ invocation_state["event_loop_parent_cycle_id"] = invocation_state["event_loop_cycle_id"]
478
+
479
+ tool_result_message: Message = {
480
+ "role": "user",
481
+ "content": [{"toolResult": result} for result in tool_results],
482
+ }
483
+
484
+ agent.messages.append(tool_result_message)
485
+ agent.hooks.invoke_callbacks(MessageAddedEvent(agent=agent, message=tool_result_message))
486
+ yield {"callback": {"message": tool_result_message}}
487
+
488
+ if cycle_span:
489
+ tracer = get_tracer()
490
+ tracer.end_event_loop_cycle_span(span=cycle_span, message=message, tool_result_message=tool_result_message)
491
+
492
+ if invocation_state["request_state"].get("stop_event_loop", False):
493
+ agent.event_loop_metrics.end_cycle(cycle_start_time, cycle_trace)
494
+ yield {"stop": (stop_reason, message, agent.event_loop_metrics, invocation_state["request_state"])}
495
+ return
496
+
497
+ events = recurse_event_loop(agent=agent, invocation_state=invocation_state)
498
+ async for event in events:
499
+ yield event