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.
- agentrun_operation_sdk/cli/__init__.py +1 -0
- agentrun_operation_sdk/cli/cli.py +19 -0
- agentrun_operation_sdk/cli/common.py +21 -0
- agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
- agentrun_operation_sdk/cli/runtime/commands.py +203 -0
- agentrun_operation_sdk/client/client.py +75 -0
- agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
- agentrun_operation_sdk/operations/runtime/configure.py +101 -0
- agentrun_operation_sdk/operations/runtime/launch.py +82 -0
- agentrun_operation_sdk/operations/runtime/models.py +31 -0
- agentrun_operation_sdk/services/runtime.py +152 -0
- agentrun_operation_sdk/utils/logging_config.py +72 -0
- agentrun_operation_sdk/utils/runtime/config.py +94 -0
- agentrun_operation_sdk/utils/runtime/container.py +280 -0
- agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
- agentrun_operation_sdk/utils/runtime/schema.py +56 -0
- agentrun_sdk/__init__.py +7 -0
- agentrun_sdk/agent/__init__.py +25 -0
- agentrun_sdk/agent/agent.py +696 -0
- agentrun_sdk/agent/agent_result.py +46 -0
- agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
- agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
- agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
- agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
- agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
- agentrun_sdk/agent/state.py +97 -0
- agentrun_sdk/event_loop/__init__.py +9 -0
- agentrun_sdk/event_loop/event_loop.py +499 -0
- agentrun_sdk/event_loop/streaming.py +319 -0
- agentrun_sdk/experimental/__init__.py +4 -0
- agentrun_sdk/experimental/hooks/__init__.py +15 -0
- agentrun_sdk/experimental/hooks/events.py +123 -0
- agentrun_sdk/handlers/__init__.py +10 -0
- agentrun_sdk/handlers/callback_handler.py +70 -0
- agentrun_sdk/hooks/__init__.py +49 -0
- agentrun_sdk/hooks/events.py +80 -0
- agentrun_sdk/hooks/registry.py +247 -0
- agentrun_sdk/models/__init__.py +10 -0
- agentrun_sdk/models/anthropic.py +432 -0
- agentrun_sdk/models/bedrock.py +649 -0
- agentrun_sdk/models/litellm.py +225 -0
- agentrun_sdk/models/llamaapi.py +438 -0
- agentrun_sdk/models/mistral.py +539 -0
- agentrun_sdk/models/model.py +95 -0
- agentrun_sdk/models/ollama.py +357 -0
- agentrun_sdk/models/openai.py +436 -0
- agentrun_sdk/models/sagemaker.py +598 -0
- agentrun_sdk/models/writer.py +449 -0
- agentrun_sdk/multiagent/__init__.py +22 -0
- agentrun_sdk/multiagent/a2a/__init__.py +15 -0
- agentrun_sdk/multiagent/a2a/executor.py +148 -0
- agentrun_sdk/multiagent/a2a/server.py +252 -0
- agentrun_sdk/multiagent/base.py +92 -0
- agentrun_sdk/multiagent/graph.py +555 -0
- agentrun_sdk/multiagent/swarm.py +656 -0
- agentrun_sdk/py.typed +1 -0
- agentrun_sdk/session/__init__.py +18 -0
- agentrun_sdk/session/file_session_manager.py +216 -0
- agentrun_sdk/session/repository_session_manager.py +152 -0
- agentrun_sdk/session/s3_session_manager.py +272 -0
- agentrun_sdk/session/session_manager.py +73 -0
- agentrun_sdk/session/session_repository.py +51 -0
- agentrun_sdk/telemetry/__init__.py +21 -0
- agentrun_sdk/telemetry/config.py +194 -0
- agentrun_sdk/telemetry/metrics.py +476 -0
- agentrun_sdk/telemetry/metrics_constants.py +15 -0
- agentrun_sdk/telemetry/tracer.py +563 -0
- agentrun_sdk/tools/__init__.py +17 -0
- agentrun_sdk/tools/decorator.py +569 -0
- agentrun_sdk/tools/executor.py +137 -0
- agentrun_sdk/tools/loader.py +152 -0
- agentrun_sdk/tools/mcp/__init__.py +13 -0
- agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
- agentrun_sdk/tools/mcp/mcp_client.py +423 -0
- agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
- agentrun_sdk/tools/mcp/mcp_types.py +63 -0
- agentrun_sdk/tools/registry.py +607 -0
- agentrun_sdk/tools/structured_output.py +421 -0
- agentrun_sdk/tools/tools.py +217 -0
- agentrun_sdk/tools/watcher.py +136 -0
- agentrun_sdk/types/__init__.py +5 -0
- agentrun_sdk/types/collections.py +23 -0
- agentrun_sdk/types/content.py +188 -0
- agentrun_sdk/types/event_loop.py +48 -0
- agentrun_sdk/types/exceptions.py +81 -0
- agentrun_sdk/types/guardrails.py +254 -0
- agentrun_sdk/types/media.py +89 -0
- agentrun_sdk/types/session.py +152 -0
- agentrun_sdk/types/streaming.py +201 -0
- agentrun_sdk/types/tools.py +258 -0
- agentrun_sdk/types/traces.py +5 -0
- agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
- agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
- agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
- agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
- agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
- agentrun_wrapper/__init__.py +11 -0
- agentrun_wrapper/_utils/__init__.py +6 -0
- agentrun_wrapper/_utils/endpoints.py +16 -0
- agentrun_wrapper/identity/__init__.py +5 -0
- agentrun_wrapper/identity/auth.py +211 -0
- agentrun_wrapper/memory/__init__.py +6 -0
- agentrun_wrapper/memory/client.py +1697 -0
- agentrun_wrapper/memory/constants.py +103 -0
- agentrun_wrapper/memory/controlplane.py +626 -0
- agentrun_wrapper/py.typed +1 -0
- agentrun_wrapper/runtime/__init__.py +13 -0
- agentrun_wrapper/runtime/app.py +473 -0
- agentrun_wrapper/runtime/context.py +34 -0
- agentrun_wrapper/runtime/models.py +25 -0
- agentrun_wrapper/services/__init__.py +1 -0
- agentrun_wrapper/services/identity.py +192 -0
- agentrun_wrapper/tools/__init__.py +6 -0
- agentrun_wrapper/tools/browser_client.py +325 -0
- 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
|