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,46 @@
|
|
|
1
|
+
"""Agent result handling for SDK.
|
|
2
|
+
|
|
3
|
+
This module defines the AgentResult class which encapsulates the complete response from an agent's processing cycle.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..telemetry.metrics import EventLoopMetrics
|
|
10
|
+
from ..types.content import Message
|
|
11
|
+
from ..types.streaming import StopReason
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AgentResult:
|
|
16
|
+
"""Represents the last result of invoking an agent with a prompt.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
stop_reason: The reason why the agent's processing stopped.
|
|
20
|
+
message: The last message generated by the agent.
|
|
21
|
+
metrics: Performance metrics collected during processing.
|
|
22
|
+
state: Additional state information from the event loop.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
stop_reason: StopReason
|
|
26
|
+
message: Message
|
|
27
|
+
metrics: EventLoopMetrics
|
|
28
|
+
state: Any
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
"""Get the agent's last message as a string.
|
|
32
|
+
|
|
33
|
+
This method extracts and concatenates all text content from the final message, ignoring any non-text content
|
|
34
|
+
like images or structured data.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The agent's last message as a string.
|
|
38
|
+
"""
|
|
39
|
+
content_array = self.message.get("content", [])
|
|
40
|
+
|
|
41
|
+
result = ""
|
|
42
|
+
for item in content_array:
|
|
43
|
+
if isinstance(item, dict) and "text" in item:
|
|
44
|
+
result += item.get("text", "") + "\n"
|
|
45
|
+
|
|
46
|
+
return result
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""This package provides classes for managing conversation history during agent execution.
|
|
2
|
+
|
|
3
|
+
It includes:
|
|
4
|
+
|
|
5
|
+
- ConversationManager: Abstract base class defining the conversation management interface
|
|
6
|
+
- NullConversationManager: A no-op implementation that does not modify conversation history
|
|
7
|
+
- SlidingWindowConversationManager: An implementation that maintains a sliding window of messages to control context
|
|
8
|
+
size while preserving conversation coherence
|
|
9
|
+
- SummarizingConversationManager: An implementation that summarizes older context instead
|
|
10
|
+
of simply trimming it
|
|
11
|
+
|
|
12
|
+
Conversation managers help control memory usage and context length while maintaining relevant conversation state, which
|
|
13
|
+
is critical for effective agent interactions.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .conversation_manager import ConversationManager
|
|
17
|
+
from .null_conversation_manager import NullConversationManager
|
|
18
|
+
from .sliding_window_conversation_manager import SlidingWindowConversationManager
|
|
19
|
+
from .summarizing_conversation_manager import SummarizingConversationManager
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ConversationManager",
|
|
23
|
+
"NullConversationManager",
|
|
24
|
+
"SlidingWindowConversationManager",
|
|
25
|
+
"SummarizingConversationManager",
|
|
26
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Abstract interface for conversation history management."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
5
|
+
|
|
6
|
+
from ...types.content import Message
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ...agent.agent import Agent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConversationManager(ABC):
|
|
13
|
+
"""Abstract base class for managing conversation history.
|
|
14
|
+
|
|
15
|
+
This class provides an interface for implementing conversation management strategies to control the size of message
|
|
16
|
+
arrays/conversation histories, helping to:
|
|
17
|
+
|
|
18
|
+
- Manage memory usage
|
|
19
|
+
- Control context length
|
|
20
|
+
- Maintain relevant conversation state
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
"""Initialize the ConversationManager.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
removed_message_count: The messages that have been removed from the agents messages array.
|
|
28
|
+
These represent messages provided by the user or LLM that have been removed, not messages
|
|
29
|
+
included by the conversation manager through something like summarization.
|
|
30
|
+
"""
|
|
31
|
+
self.removed_message_count = 0
|
|
32
|
+
|
|
33
|
+
def restore_from_session(self, state: dict[str, Any]) -> Optional[list[Message]]:
|
|
34
|
+
"""Restore the Conversation Manager's state from a session.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
state: Previous state of the conversation manager
|
|
38
|
+
Returns:
|
|
39
|
+
Optional list of messages to prepend to the agents messages. By default returns None.
|
|
40
|
+
"""
|
|
41
|
+
if state.get("__name__") != self.__class__.__name__:
|
|
42
|
+
raise ValueError("Invalid conversation manager state.")
|
|
43
|
+
self.removed_message_count = state["removed_message_count"]
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
def get_state(self) -> dict[str, Any]:
|
|
47
|
+
"""Get the current state of a Conversation Manager as a Json serializable dictionary."""
|
|
48
|
+
return {
|
|
49
|
+
"__name__": self.__class__.__name__,
|
|
50
|
+
"removed_message_count": self.removed_message_count,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def apply_management(self, agent: "Agent", **kwargs: Any) -> None:
|
|
55
|
+
"""Applies management strategy to the provided agent.
|
|
56
|
+
|
|
57
|
+
Processes the conversation history to maintain appropriate size by modifying the messages list in-place.
|
|
58
|
+
Implementations should handle message pruning, summarization, or other size management techniques to keep the
|
|
59
|
+
conversation context within desired bounds.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
agent: The agent whose conversation history will be manage.
|
|
63
|
+
This list is modified in-place.
|
|
64
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
65
|
+
"""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def reduce_context(self, agent: "Agent", e: Optional[Exception] = None, **kwargs: Any) -> None:
|
|
70
|
+
"""Called when the model's context window is exceeded.
|
|
71
|
+
|
|
72
|
+
This method should implement the specific strategy for reducing the window size when a context overflow occurs.
|
|
73
|
+
It is typically called after a ContextWindowOverflowException is caught.
|
|
74
|
+
|
|
75
|
+
Implementations might use strategies such as:
|
|
76
|
+
|
|
77
|
+
- Removing the N oldest messages
|
|
78
|
+
- Summarizing older context
|
|
79
|
+
- Applying importance-based filtering
|
|
80
|
+
- Maintaining critical conversation markers
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
agent: The agent whose conversation history will be reduced.
|
|
84
|
+
This list is modified in-place.
|
|
85
|
+
e: The exception that triggered the context reduction, if any.
|
|
86
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
87
|
+
"""
|
|
88
|
+
pass
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Null implementation of conversation management."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ...agent.agent import Agent
|
|
7
|
+
|
|
8
|
+
from ...types.exceptions import ContextWindowOverflowException
|
|
9
|
+
from .conversation_manager import ConversationManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NullConversationManager(ConversationManager):
|
|
13
|
+
"""A no-op conversation manager that does not modify the conversation history.
|
|
14
|
+
|
|
15
|
+
Useful for:
|
|
16
|
+
|
|
17
|
+
- Testing scenarios where conversation management should be disabled
|
|
18
|
+
- Cases where conversation history is managed externally
|
|
19
|
+
- Situations where the full conversation history should be preserved
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def apply_management(self, agent: "Agent", **kwargs: Any) -> None:
|
|
23
|
+
"""Does nothing to the conversation history.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
agent: The agent whose conversation history will remain unmodified.
|
|
27
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
28
|
+
"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
def reduce_context(self, agent: "Agent", e: Optional[Exception] = None, **kwargs: Any) -> None:
|
|
32
|
+
"""Does not reduce context and raises an exception.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
agent: The agent whose conversation history will remain unmodified.
|
|
36
|
+
e: The exception that triggered the context reduction, if any.
|
|
37
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
e: If provided.
|
|
41
|
+
ContextWindowOverflowException: If e is None.
|
|
42
|
+
"""
|
|
43
|
+
if e:
|
|
44
|
+
raise e
|
|
45
|
+
else:
|
|
46
|
+
raise ContextWindowOverflowException("Context window overflowed!")
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Sliding window conversation history management."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from ...agent.agent import Agent
|
|
8
|
+
|
|
9
|
+
from ...types.content import Messages
|
|
10
|
+
from ...types.exceptions import ContextWindowOverflowException
|
|
11
|
+
from .conversation_manager import ConversationManager
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SlidingWindowConversationManager(ConversationManager):
|
|
17
|
+
"""Implements a sliding window strategy for managing conversation history.
|
|
18
|
+
|
|
19
|
+
This class handles the logic of maintaining a conversation window that preserves tool usage pairs and avoids
|
|
20
|
+
invalid window states.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, window_size: int = 40, should_truncate_results: bool = True):
|
|
24
|
+
"""Initialize the sliding window conversation manager.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
window_size: Maximum number of messages to keep in the agent's history.
|
|
28
|
+
Defaults to 40 messages.
|
|
29
|
+
should_truncate_results: Truncate tool results when a message is too large for the model's context window
|
|
30
|
+
"""
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.window_size = window_size
|
|
33
|
+
self.should_truncate_results = should_truncate_results
|
|
34
|
+
|
|
35
|
+
def apply_management(self, agent: "Agent", **kwargs: Any) -> None:
|
|
36
|
+
"""Apply the sliding window to the agent's messages array to maintain a manageable history size.
|
|
37
|
+
|
|
38
|
+
This method is called after every event loop cycle to apply a sliding window if the message count
|
|
39
|
+
exceeds the window size.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
agent: The agent whose messages will be managed.
|
|
43
|
+
This list is modified in-place.
|
|
44
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
45
|
+
"""
|
|
46
|
+
messages = agent.messages
|
|
47
|
+
|
|
48
|
+
if len(messages) <= self.window_size:
|
|
49
|
+
logger.debug(
|
|
50
|
+
"message_count=<%s>, window_size=<%s> | skipping context reduction", len(messages), self.window_size
|
|
51
|
+
)
|
|
52
|
+
return
|
|
53
|
+
self.reduce_context(agent)
|
|
54
|
+
|
|
55
|
+
def reduce_context(self, agent: "Agent", e: Optional[Exception] = None, **kwargs: Any) -> None:
|
|
56
|
+
"""Trim the oldest messages to reduce the conversation context size.
|
|
57
|
+
|
|
58
|
+
The method handles special cases where trimming the messages leads to:
|
|
59
|
+
- toolResult with no corresponding toolUse
|
|
60
|
+
- toolUse with no corresponding toolResult
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
agent: The agent whose messages will be reduce.
|
|
64
|
+
This list is modified in-place.
|
|
65
|
+
e: The exception that triggered the context reduction, if any.
|
|
66
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ContextWindowOverflowException: If the context cannot be reduced further.
|
|
70
|
+
Such as when the conversation is already minimal or when tool result messages cannot be properly
|
|
71
|
+
converted.
|
|
72
|
+
"""
|
|
73
|
+
messages = agent.messages
|
|
74
|
+
|
|
75
|
+
# Try to truncate the tool result first
|
|
76
|
+
last_message_idx_with_tool_results = self._find_last_message_with_tool_results(messages)
|
|
77
|
+
if last_message_idx_with_tool_results is not None and self.should_truncate_results:
|
|
78
|
+
logger.debug(
|
|
79
|
+
"message_index=<%s> | found message with tool results at index", last_message_idx_with_tool_results
|
|
80
|
+
)
|
|
81
|
+
results_truncated = self._truncate_tool_results(messages, last_message_idx_with_tool_results)
|
|
82
|
+
if results_truncated:
|
|
83
|
+
logger.debug("message_index=<%s> | tool results truncated", last_message_idx_with_tool_results)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# Try to trim index id when tool result cannot be truncated anymore
|
|
87
|
+
# If the number of messages is less than the window_size, then we default to 2, otherwise, trim to window size
|
|
88
|
+
trim_index = 2 if len(messages) <= self.window_size else len(messages) - self.window_size
|
|
89
|
+
|
|
90
|
+
# Find the next valid trim_index
|
|
91
|
+
while trim_index < len(messages):
|
|
92
|
+
if (
|
|
93
|
+
# Oldest message cannot be a toolResult because it needs a toolUse preceding it
|
|
94
|
+
any("toolResult" in content for content in messages[trim_index]["content"])
|
|
95
|
+
or (
|
|
96
|
+
# Oldest message can be a toolUse only if a toolResult immediately follows it.
|
|
97
|
+
any("toolUse" in content for content in messages[trim_index]["content"])
|
|
98
|
+
and trim_index + 1 < len(messages)
|
|
99
|
+
and not any("toolResult" in content for content in messages[trim_index + 1]["content"])
|
|
100
|
+
)
|
|
101
|
+
):
|
|
102
|
+
trim_index += 1
|
|
103
|
+
else:
|
|
104
|
+
break
|
|
105
|
+
else:
|
|
106
|
+
# If we didn't find a valid trim_index, then we throw
|
|
107
|
+
raise ContextWindowOverflowException("Unable to trim conversation context!") from e
|
|
108
|
+
|
|
109
|
+
# trim_index represents the number of messages being removed from the agents messages array
|
|
110
|
+
self.removed_message_count += trim_index
|
|
111
|
+
|
|
112
|
+
# Overwrite message history
|
|
113
|
+
messages[:] = messages[trim_index:]
|
|
114
|
+
|
|
115
|
+
def _truncate_tool_results(self, messages: Messages, msg_idx: int) -> bool:
|
|
116
|
+
"""Truncate tool results in a message to reduce context size.
|
|
117
|
+
|
|
118
|
+
When a message contains tool results that are too large for the model's context window, this function
|
|
119
|
+
replaces the content of those tool results with a simple error message.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
messages: The conversation message history.
|
|
123
|
+
msg_idx: Index of the message containing tool results to truncate.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if any changes were made to the message, False otherwise.
|
|
127
|
+
"""
|
|
128
|
+
if msg_idx >= len(messages) or msg_idx < 0:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
message = messages[msg_idx]
|
|
132
|
+
changes_made = False
|
|
133
|
+
tool_result_too_large_message = "The tool result was too large!"
|
|
134
|
+
for i, content in enumerate(message.get("content", [])):
|
|
135
|
+
if isinstance(content, dict) and "toolResult" in content:
|
|
136
|
+
tool_result_content_text = next(
|
|
137
|
+
(item["text"] for item in content["toolResult"]["content"] if "text" in item),
|
|
138
|
+
"",
|
|
139
|
+
)
|
|
140
|
+
# make the overwriting logic togglable
|
|
141
|
+
if (
|
|
142
|
+
message["content"][i]["toolResult"]["status"] == "error"
|
|
143
|
+
and tool_result_content_text == tool_result_too_large_message
|
|
144
|
+
):
|
|
145
|
+
logger.info("ToolResult has already been updated, skipping overwrite")
|
|
146
|
+
return False
|
|
147
|
+
# Update status to error with informative message
|
|
148
|
+
message["content"][i]["toolResult"]["status"] = "error"
|
|
149
|
+
message["content"][i]["toolResult"]["content"] = [{"text": tool_result_too_large_message}]
|
|
150
|
+
changes_made = True
|
|
151
|
+
|
|
152
|
+
return changes_made
|
|
153
|
+
|
|
154
|
+
def _find_last_message_with_tool_results(self, messages: Messages) -> Optional[int]:
|
|
155
|
+
"""Find the index of the last message containing tool results.
|
|
156
|
+
|
|
157
|
+
This is useful for identifying messages that might need to be truncated to reduce context size.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
messages: The conversation message history.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Index of the last message with tool results, or None if no such message exists.
|
|
164
|
+
"""
|
|
165
|
+
# Iterate backwards through all messages (from newest to oldest)
|
|
166
|
+
for idx in range(len(messages) - 1, -1, -1):
|
|
167
|
+
# Check if this message has any content with toolResult
|
|
168
|
+
current_message = messages[idx]
|
|
169
|
+
has_tool_result = False
|
|
170
|
+
|
|
171
|
+
for content in current_message.get("content", []):
|
|
172
|
+
if isinstance(content, dict) and "toolResult" in content:
|
|
173
|
+
has_tool_result = True
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
if has_tool_result:
|
|
177
|
+
return idx
|
|
178
|
+
|
|
179
|
+
return None
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Summarizing conversation history management with configurable options."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Any, List, Optional
|
|
5
|
+
|
|
6
|
+
from typing_extensions import override
|
|
7
|
+
|
|
8
|
+
from ...types.content import Message
|
|
9
|
+
from ...types.exceptions import ContextWindowOverflowException
|
|
10
|
+
from .conversation_manager import ConversationManager
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..agent import Agent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_SUMMARIZATION_PROMPT = """You are a conversation summarizer. Provide a concise summary of the conversation \
|
|
20
|
+
history.
|
|
21
|
+
|
|
22
|
+
Format Requirements:
|
|
23
|
+
- You MUST create a structured and concise summary in bullet-point format.
|
|
24
|
+
- You MUST NOT respond conversationally.
|
|
25
|
+
- You MUST NOT address the user directly.
|
|
26
|
+
|
|
27
|
+
Task:
|
|
28
|
+
Your task is to create a structured summary document:
|
|
29
|
+
- It MUST contain bullet points with key topics and questions covered
|
|
30
|
+
- It MUST contain bullet points for all significant tools executed and their results
|
|
31
|
+
- It MUST contain bullet points for any code or technical information shared
|
|
32
|
+
- It MUST contain a section of key insights gained
|
|
33
|
+
- It MUST format the summary in the third person
|
|
34
|
+
|
|
35
|
+
Example format:
|
|
36
|
+
|
|
37
|
+
## Conversation Summary
|
|
38
|
+
* Topic 1: Key information
|
|
39
|
+
* Topic 2: Key information
|
|
40
|
+
*
|
|
41
|
+
## Tools Executed
|
|
42
|
+
* Tool X: Result Y"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SummarizingConversationManager(ConversationManager):
|
|
46
|
+
"""Implements a summarizing window manager.
|
|
47
|
+
|
|
48
|
+
This manager provides a configurable option to summarize older context instead of
|
|
49
|
+
simply trimming it, helping preserve important information while staying within
|
|
50
|
+
context limits.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
summary_ratio: float = 0.3,
|
|
56
|
+
preserve_recent_messages: int = 10,
|
|
57
|
+
summarization_agent: Optional["Agent"] = None,
|
|
58
|
+
summarization_system_prompt: Optional[str] = None,
|
|
59
|
+
):
|
|
60
|
+
"""Initialize the summarizing conversation manager.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
summary_ratio: Ratio of messages to summarize vs keep when context overflow occurs.
|
|
64
|
+
Value between 0.1 and 0.8. Defaults to 0.3 (summarize 30% of oldest messages).
|
|
65
|
+
preserve_recent_messages: Minimum number of recent messages to always keep.
|
|
66
|
+
Defaults to 10 messages.
|
|
67
|
+
summarization_agent: Optional agent to use for summarization instead of the parent agent.
|
|
68
|
+
If provided, this agent can use tools as part of the summarization process.
|
|
69
|
+
summarization_system_prompt: Optional system prompt override for summarization.
|
|
70
|
+
If None, uses the default summarization prompt.
|
|
71
|
+
"""
|
|
72
|
+
super().__init__()
|
|
73
|
+
if summarization_agent is not None and summarization_system_prompt is not None:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"Cannot provide both summarization_agent and summarization_system_prompt. "
|
|
76
|
+
"Agents come with their own system prompt."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self.summary_ratio = max(0.1, min(0.8, summary_ratio))
|
|
80
|
+
self.preserve_recent_messages = preserve_recent_messages
|
|
81
|
+
self.summarization_agent = summarization_agent
|
|
82
|
+
self.summarization_system_prompt = summarization_system_prompt
|
|
83
|
+
self._summary_message: Optional[Message] = None
|
|
84
|
+
|
|
85
|
+
@override
|
|
86
|
+
def restore_from_session(self, state: dict[str, Any]) -> Optional[list[Message]]:
|
|
87
|
+
"""Restores the Summarizing Conversation manager from its previous state in a session.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
state: The previous state of the Summarizing Conversation Manager.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Optionally returns the previous conversation summary if it exists.
|
|
94
|
+
"""
|
|
95
|
+
super().restore_from_session(state)
|
|
96
|
+
self._summary_message = state.get("summary_message")
|
|
97
|
+
return [self._summary_message] if self._summary_message else None
|
|
98
|
+
|
|
99
|
+
def get_state(self) -> dict[str, Any]:
|
|
100
|
+
"""Returns a dictionary representation of the state for the Summarizing Conversation Manager."""
|
|
101
|
+
return {"summary_message": self._summary_message, **super().get_state()}
|
|
102
|
+
|
|
103
|
+
def apply_management(self, agent: "Agent", **kwargs: Any) -> None:
|
|
104
|
+
"""Apply management strategy to conversation history.
|
|
105
|
+
|
|
106
|
+
For the summarizing conversation manager, no proactive management is performed.
|
|
107
|
+
Summarization only occurs when there's a context overflow that triggers reduce_context.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
agent: The agent whose conversation history will be managed.
|
|
111
|
+
The agent's messages list is modified in-place.
|
|
112
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
113
|
+
"""
|
|
114
|
+
# No proactive management - summarization only happens on context overflow
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
def reduce_context(self, agent: "Agent", e: Optional[Exception] = None, **kwargs: Any) -> None:
|
|
118
|
+
"""Reduce context using summarization.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
agent: The agent whose conversation history will be reduced.
|
|
122
|
+
The agent's messages list is modified in-place.
|
|
123
|
+
e: The exception that triggered the context reduction, if any.
|
|
124
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ContextWindowOverflowException: If the context cannot be summarized.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
# Calculate how many messages to summarize
|
|
131
|
+
messages_to_summarize_count = max(1, int(len(agent.messages) * self.summary_ratio))
|
|
132
|
+
|
|
133
|
+
# Ensure we don't summarize recent messages
|
|
134
|
+
messages_to_summarize_count = min(
|
|
135
|
+
messages_to_summarize_count, len(agent.messages) - self.preserve_recent_messages
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if messages_to_summarize_count <= 0:
|
|
139
|
+
raise ContextWindowOverflowException("Cannot summarize: insufficient messages for summarization")
|
|
140
|
+
|
|
141
|
+
# Adjust split point to avoid breaking ToolUse/ToolResult pairs
|
|
142
|
+
messages_to_summarize_count = self._adjust_split_point_for_tool_pairs(
|
|
143
|
+
agent.messages, messages_to_summarize_count
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if messages_to_summarize_count <= 0:
|
|
147
|
+
raise ContextWindowOverflowException("Cannot summarize: insufficient messages for summarization")
|
|
148
|
+
|
|
149
|
+
# Extract messages to summarize
|
|
150
|
+
messages_to_summarize = agent.messages[:messages_to_summarize_count]
|
|
151
|
+
remaining_messages = agent.messages[messages_to_summarize_count:]
|
|
152
|
+
|
|
153
|
+
# Keep track of the number of messages that have been summarized thus far.
|
|
154
|
+
self.removed_message_count += len(messages_to_summarize)
|
|
155
|
+
# If there is a summary message, don't count it in the removed_message_count.
|
|
156
|
+
if self._summary_message:
|
|
157
|
+
self.removed_message_count -= 1
|
|
158
|
+
|
|
159
|
+
# Generate summary
|
|
160
|
+
self._summary_message = self._generate_summary(messages_to_summarize, agent)
|
|
161
|
+
|
|
162
|
+
# Replace the summarized messages with the summary
|
|
163
|
+
agent.messages[:] = [self._summary_message] + remaining_messages
|
|
164
|
+
|
|
165
|
+
except Exception as summarization_error:
|
|
166
|
+
logger.error("Summarization failed: %s", summarization_error)
|
|
167
|
+
raise summarization_error from e
|
|
168
|
+
|
|
169
|
+
def _generate_summary(self, messages: List[Message], agent: "Agent") -> Message:
|
|
170
|
+
"""Generate a summary of the provided messages.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
messages: The messages to summarize.
|
|
174
|
+
agent: The agent instance to use for summarization.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A message containing the conversation summary.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
Exception: If summary generation fails.
|
|
181
|
+
"""
|
|
182
|
+
# Choose which agent to use for summarization
|
|
183
|
+
summarization_agent = self.summarization_agent if self.summarization_agent is not None else agent
|
|
184
|
+
|
|
185
|
+
# Save original system prompt and messages to restore later
|
|
186
|
+
original_system_prompt = summarization_agent.system_prompt
|
|
187
|
+
original_messages = summarization_agent.messages.copy()
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
# Only override system prompt if no agent was provided during initialization
|
|
191
|
+
if self.summarization_agent is None:
|
|
192
|
+
# Use custom system prompt if provided, otherwise use default
|
|
193
|
+
system_prompt = (
|
|
194
|
+
self.summarization_system_prompt
|
|
195
|
+
if self.summarization_system_prompt is not None
|
|
196
|
+
else DEFAULT_SUMMARIZATION_PROMPT
|
|
197
|
+
)
|
|
198
|
+
# Temporarily set the system prompt for summarization
|
|
199
|
+
summarization_agent.system_prompt = system_prompt
|
|
200
|
+
summarization_agent.messages = messages
|
|
201
|
+
|
|
202
|
+
# Use the agent to generate summary with rich content (can use tools if needed)
|
|
203
|
+
result = summarization_agent("Please summarize this conversation.")
|
|
204
|
+
|
|
205
|
+
return result.message
|
|
206
|
+
|
|
207
|
+
finally:
|
|
208
|
+
# Restore original agent state
|
|
209
|
+
summarization_agent.system_prompt = original_system_prompt
|
|
210
|
+
summarization_agent.messages = original_messages
|
|
211
|
+
|
|
212
|
+
def _adjust_split_point_for_tool_pairs(self, messages: List[Message], split_point: int) -> int:
|
|
213
|
+
"""Adjust the split point to avoid breaking ToolUse/ToolResult pairs.
|
|
214
|
+
|
|
215
|
+
Uses the same logic as SlidingWindowConversationManager for consistency.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
messages: The full list of messages.
|
|
219
|
+
split_point: The initially calculated split point.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
The adjusted split point that doesn't break ToolUse/ToolResult pairs.
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ContextWindowOverflowException: If no valid split point can be found.
|
|
226
|
+
"""
|
|
227
|
+
if split_point > len(messages):
|
|
228
|
+
raise ContextWindowOverflowException("Split point exceeds message array length")
|
|
229
|
+
|
|
230
|
+
if split_point == len(messages):
|
|
231
|
+
return split_point
|
|
232
|
+
|
|
233
|
+
# Find the next valid split_point
|
|
234
|
+
while split_point < len(messages):
|
|
235
|
+
if (
|
|
236
|
+
# Oldest message cannot be a toolResult because it needs a toolUse preceding it
|
|
237
|
+
any("toolResult" in content for content in messages[split_point]["content"])
|
|
238
|
+
or (
|
|
239
|
+
# Oldest message can be a toolUse only if a toolResult immediately follows it.
|
|
240
|
+
any("toolUse" in content for content in messages[split_point]["content"])
|
|
241
|
+
and split_point + 1 < len(messages)
|
|
242
|
+
and not any("toolResult" in content for content in messages[split_point + 1]["content"])
|
|
243
|
+
)
|
|
244
|
+
):
|
|
245
|
+
split_point += 1
|
|
246
|
+
else:
|
|
247
|
+
break
|
|
248
|
+
else:
|
|
249
|
+
# If we didn't find a valid split_point, then we throw
|
|
250
|
+
raise ContextWindowOverflowException("Unable to trim conversation context!")
|
|
251
|
+
|
|
252
|
+
return split_point
|