Skip to content

Commit 3af994e

Browse files
committed
Merge remote-tracking branch 'upstream/main' into feature/redactedContent
2 parents 530e2ce + ffc7c5e commit 3af994e

28 files changed

+681
-396
lines changed

.github/workflows/pr-and-push.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Pull Request and Push Action
2+
3+
on:
4+
pull_request: # Safer than pull_request_target for untrusted code
5+
branches: [ main ]
6+
types: [opened, synchronize, reopened, ready_for_review, review_requested, review_request_removed]
7+
push:
8+
branches: [ main ] # Also run on direct pushes to main
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
call-test-lint:
15+
uses: ./.github/workflows/test-lint.yml
16+
permissions:
17+
contents: read
18+
with:
19+
ref: ${{ github.event.pull_request.head.sha }}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Publish Python Package
2+
3+
on:
4+
release:
5+
types:
6+
- published
7+
8+
jobs:
9+
call-test-lint:
10+
uses: ./.github/workflows/test-lint.yml
11+
with:
12+
ref: ${{ github.event.release.target_commitish }}
13+
14+
build:
15+
name: Build distribution 📦
16+
permissions:
17+
contents: read
18+
needs:
19+
- call-test-lint
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
with:
25+
persist-credentials: false
26+
27+
- name: Set up Python
28+
uses: actions/setup-python@v5
29+
with:
30+
python-version: '3.10'
31+
32+
- name: Install dependencies
33+
run: |
34+
python -m pip install --upgrade pip
35+
pip install hatch twine
36+
37+
- name: Validate version
38+
run: |
39+
version=$(hatch version)
40+
if [[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
41+
echo "Valid version format"
42+
exit 0
43+
else
44+
echo "Invalid version format"
45+
exit 1
46+
fi
47+
48+
- name: Build
49+
run: |
50+
hatch build
51+
52+
- name: Store the distribution packages
53+
uses: actions/upload-artifact@v4
54+
with:
55+
name: python-package-distributions
56+
path: dist/
57+
58+
deploy:
59+
name: Upload release to PyPI
60+
permissions:
61+
contents: read
62+
needs:
63+
- build
64+
runs-on: ubuntu-latest
65+
66+
# environment is used by PyPI Trusted Publisher and is strongly encouraged
67+
# https://docs.pypi.org/trusted-publishers/adding-a-publisher/
68+
environment:
69+
name: pypi
70+
url: https://pypi.org/p/strands-agents
71+
permissions:
72+
# IMPORTANT: this permission is mandatory for Trusted Publishing
73+
id-token: write
74+
75+
steps:
76+
- name: Download all the dists
77+
uses: actions/download-artifact@v4
78+
with:
79+
name: python-package-distributions
80+
path: dist/
81+
- name: Publish distribution 📦 to PyPI
82+
uses: pypa/gh-action-pypi-publish@release/v1

.github/workflows/test-lint-pr.yml renamed to .github/workflows/test-lint.yml

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
name: Test and Lint
22

33
on:
4-
pull_request: # Safer than pull_request_target for untrusted code
5-
branches: [ main ]
6-
types: [opened, synchronize, reopened, ready_for_review, review_requested, review_request_removed]
7-
push:
8-
branches: [ main ] # Also run on direct pushes to main
9-
concurrency:
10-
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11-
cancel-in-progress: true
4+
workflow_call:
5+
inputs:
6+
ref:
7+
required: true
8+
type: string
129

1310
jobs:
1411
unit-test:
@@ -56,7 +53,7 @@ jobs:
5653
- name: Checkout code
5754
uses: actions/checkout@v4
5855
with:
59-
ref: ${{ github.event.pull_request.head.sha }} # Explicitly define which commit to check out
56+
ref: ${{ inputs.ref }} # Explicitly define which commit to check out
6057
persist-credentials: false # Don't persist credentials for subsequent actions
6158
- name: Set up Python
6259
uses: actions/setup-python@v5
@@ -78,7 +75,7 @@ jobs:
7875
- name: Checkout code
7976
uses: actions/checkout@v4
8077
with:
81-
ref: ${{ github.event.pull_request.head.sha }}
78+
ref: ${{ inputs.ref }}
8279
persist-credentials: false
8380

8481
- name: Set up Python

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ __pycache__*
77
.pytest_cache
88
.ruff_cache
99
*.bak
10-
.vscode
10+
.vscode
11+
dist

pyproject.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
[build-system]
2-
requires = ["hatchling"]
2+
requires = ["hatchling", "hatch-vcs"]
33
build-backend = "hatchling.build"
44

55
[project]
66
name = "strands-agents"
7-
version = "0.1.5"
7+
dynamic = ["version"]
88
description = "A model-driven approach to building AI agents in just a few lines of code"
99
readme = "README.md"
1010
requires-python = ">=3.10"
@@ -79,6 +79,10 @@ openai = [
7979
"openai>=1.68.0,<2.0.0",
8080
]
8181

82+
[tool.hatch.version]
83+
# Tells Hatch to use your version control system (git) to determine the version.
84+
source = "vcs"
85+
8286
[tool.hatch.envs.hatch-static-analysis]
8387
features = ["anthropic", "litellm", "llamaapi", "ollama", "openai"]
8488
dependencies = [

src/strands/agent/agent.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@
4444
logger = logging.getLogger(__name__)
4545

4646

47+
# Sentinel class and object to distinguish between explicit None and default parameter value
48+
class _DefaultCallbackHandlerSentinel:
49+
"""Sentinel class to distinguish between explicit None and default parameter value."""
50+
51+
pass
52+
53+
54+
_DEFAULT_CALLBACK_HANDLER = _DefaultCallbackHandlerSentinel()
55+
56+
4757
class Agent:
4858
"""Core Agent interface.
4959
@@ -70,7 +80,7 @@ def __init__(self, agent: "Agent") -> None:
7080
# agent tools and thus break their execution.
7181
self._agent = agent
7282

73-
def __getattr__(self, name: str) -> Callable:
83+
def __getattr__(self, name: str) -> Callable[..., Any]:
7484
"""Call tool as a function.
7585
7686
This method enables the method-style interface (e.g., `agent.tool.tool_name(param="value")`).
@@ -165,7 +175,7 @@ def caller(**kwargs: Any) -> Any:
165175
self._agent._record_tool_execution(tool_use, tool_result, user_message_override, messages)
166176

167177
# Apply window management
168-
self._agent.conversation_manager.apply_management(self._agent.messages)
178+
self._agent.conversation_manager.apply_management(self._agent)
169179

170180
return tool_result
171181

@@ -177,7 +187,9 @@ def __init__(
177187
messages: Optional[Messages] = None,
178188
tools: Optional[List[Union[str, Dict[str, str], Any]]] = None,
179189
system_prompt: Optional[str] = None,
180-
callback_handler: Optional[Callable] = PrintingCallbackHandler(),
190+
callback_handler: Optional[
191+
Union[Callable[..., Any], _DefaultCallbackHandlerSentinel]
192+
] = _DEFAULT_CALLBACK_HANDLER,
181193
conversation_manager: Optional[ConversationManager] = None,
182194
max_parallel_tools: int = os.cpu_count() or 1,
183195
record_direct_tool_call: bool = True,
@@ -204,7 +216,8 @@ def __init__(
204216
system_prompt: System prompt to guide model behavior.
205217
If None, the model will behave according to its default settings.
206218
callback_handler: Callback for processing events as they happen during agent execution.
207-
Defaults to strands.handlers.PrintingCallbackHandler if None.
219+
If not provided (using the default), a new PrintingCallbackHandler instance is created.
220+
If explicitly set to None, null_callback_handler is used.
208221
conversation_manager: Manager for conversation history and context window.
209222
Defaults to strands.agent.conversation_manager.SlidingWindowConversationManager if None.
210223
max_parallel_tools: Maximum number of tools to run in parallel when the model returns multiple tool calls.
@@ -222,7 +235,17 @@ def __init__(
222235
self.messages = messages if messages is not None else []
223236

224237
self.system_prompt = system_prompt
225-
self.callback_handler = callback_handler or null_callback_handler
238+
239+
# If not provided, create a new PrintingCallbackHandler instance
240+
# If explicitly set to None, use null_callback_handler
241+
# Otherwise use the passed callback_handler
242+
self.callback_handler: Union[Callable[..., Any], PrintingCallbackHandler]
243+
if isinstance(callback_handler, _DefaultCallbackHandlerSentinel):
244+
self.callback_handler = PrintingCallbackHandler()
245+
elif callback_handler is None:
246+
self.callback_handler = null_callback_handler
247+
else:
248+
self.callback_handler = callback_handler
226249

227250
self.conversation_manager = conversation_manager if conversation_manager else SlidingWindowConversationManager()
228251

@@ -415,7 +438,7 @@ def target_callback() -> None:
415438
thread.join()
416439

417440
def _run_loop(
418-
self, prompt: str, kwargs: Any, supplementary_callback_handler: Optional[Callable] = None
441+
self, prompt: str, kwargs: Dict[str, Any], supplementary_callback_handler: Optional[Callable[..., Any]] = None
419442
) -> AgentResult:
420443
"""Execute the agent's event loop with the given prompt and parameters."""
421444
try:
@@ -439,9 +462,9 @@ def _run_loop(
439462
return self._execute_event_loop_cycle(invocation_callback_handler, kwargs)
440463

441464
finally:
442-
self.conversation_manager.apply_management(self.messages)
465+
self.conversation_manager.apply_management(self)
443466

444-
def _execute_event_loop_cycle(self, callback_handler: Callable, kwargs: dict[str, Any]) -> AgentResult:
467+
def _execute_event_loop_cycle(self, callback_handler: Callable[..., Any], kwargs: Dict[str, Any]) -> AgentResult:
445468
"""Execute the event loop cycle with retry logic for context window limits.
446469
447470
This internal method handles the execution of the event loop cycle and implements
@@ -483,7 +506,7 @@ def _execute_event_loop_cycle(self, callback_handler: Callable, kwargs: dict[str
483506
except ContextWindowOverflowException as e:
484507
# Try reducing the context size and retrying
485508

486-
self.conversation_manager.reduce_context(messages, e=e)
509+
self.conversation_manager.reduce_context(self, e=e)
487510
return self._execute_event_loop_cycle(callback_handler_override, kwargs)
488511

489512
def _record_tool_execution(

src/strands/agent/conversation_manager/conversation_manager.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Abstract interface for conversation history management."""
22

33
from abc import ABC, abstractmethod
4-
from typing import Optional
4+
from typing import TYPE_CHECKING, Optional
55

6-
from ...types.content import Messages
6+
if TYPE_CHECKING:
7+
from ...agent.agent import Agent
78

89

910
class ConversationManager(ABC):
@@ -19,22 +20,22 @@ class ConversationManager(ABC):
1920

2021
@abstractmethod
2122
# pragma: no cover
22-
def apply_management(self, messages: Messages) -> None:
23-
"""Applies management strategy to the provided list of messages.
23+
def apply_management(self, agent: "Agent") -> None:
24+
"""Applies management strategy to the provided agent.
2425
2526
Processes the conversation history to maintain appropriate size by modifying the messages list in-place.
2627
Implementations should handle message pruning, summarization, or other size management techniques to keep the
2728
conversation context within desired bounds.
2829
2930
Args:
30-
messages: The conversation history to manage.
31+
agent: The agent whose conversation history will be manage.
3132
This list is modified in-place.
3233
"""
3334
pass
3435

3536
@abstractmethod
3637
# pragma: no cover
37-
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
38+
def reduce_context(self, agent: "Agent", e: Optional[Exception] = None) -> None:
3839
"""Called when the model's context window is exceeded.
3940
4041
This method should implement the specific strategy for reducing the window size when a context overflow occurs.
@@ -48,7 +49,7 @@ def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> N
4849
- Maintaining critical conversation markers
4950
5051
Args:
51-
messages: The conversation history to reduce.
52+
agent: The agent whose conversation history will be reduced.
5253
This list is modified in-place.
5354
e: The exception that triggered the context reduction, if any.
5455
"""

src/strands/agent/conversation_manager/null_conversation_manager.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Null implementation of conversation management."""
22

3-
from typing import Optional
3+
from typing import TYPE_CHECKING, Optional
4+
5+
if TYPE_CHECKING:
6+
from ...agent.agent import Agent
47

5-
from ...types.content import Messages
68
from ...types.exceptions import ContextWindowOverflowException
79
from .conversation_manager import ConversationManager
810

@@ -17,19 +19,19 @@ class NullConversationManager(ConversationManager):
1719
- Situations where the full conversation history should be preserved
1820
"""
1921

20-
def apply_management(self, messages: Messages) -> None:
22+
def apply_management(self, _agent: "Agent") -> None:
2123
"""Does nothing to the conversation history.
2224
2325
Args:
24-
messages: The conversation history that will remain unmodified.
26+
agent: The agent whose conversation history will remain unmodified.
2527
"""
2628
pass
2729

28-
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
30+
def reduce_context(self, _agent: "Agent", e: Optional[Exception] = None) -> None:
2931
"""Does not reduce context and raises an exception.
3032
3133
Args:
32-
messages: The conversation history that will remain unmodified.
34+
agent: The agent whose conversation history will remain unmodified.
3335
e: The exception that triggered the context reduction, if any.
3436
3537
Raises:

0 commit comments

Comments
 (0)