Skip to content

Commit e4ed952

Browse files
committed
fix(aws-strands): propagate template agent kwargs to new thread instances
1 parent 4d8e777 commit e4ed952

2 files changed

Lines changed: 172 additions & 0 deletions

File tree

integrations/aws-strands/python/src/ag_ui_strands/agent.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ def __init__(
6565
if hasattr(agent, "record_direct_tool_call")
6666
else True,
6767
}
68+
for _attr in (
69+
"trace_attributes",
70+
"agent_id",
71+
"conversation_manager",
72+
"callback_handler",
73+
"hooks",
74+
"session_manager",
75+
"tool_executor",
76+
):
77+
if hasattr(agent, _attr):
78+
self._agent_kwargs[_attr] = getattr(agent, _attr)
79+
if hasattr(agent, "state") and agent.state is not None:
80+
self._agent_kwargs["state"] = agent.state._state
6881

6982
self.name = name
7083
self.description = description
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""Tests for StrandsAgent template kwarg propagation to new thread instances.
2+
3+
StrandsAgent.__init__ currently captures only four attributes from the template
4+
agent (model, system_prompt, tools, record_direct_tool_call). All other
5+
constructor parameters — trace_attributes, agent_id, conversation_manager,
6+
state — are silently discarded, so every new thread starts with default values
7+
regardless of what was configured on the template.
8+
9+
Each test below is written to FAIL with the current code and PASS after the fix.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from unittest.mock import MagicMock, patch
15+
16+
import pytest
17+
from strands import Agent
18+
from strands.tools.registry import ToolRegistry
19+
20+
from ag_ui_strands.agent import StrandsAgent
21+
from ag_ui_strands.config import StrandsAgentConfig
22+
23+
24+
# ---------------------------------------------------------------------------
25+
# Helpers
26+
# ---------------------------------------------------------------------------
27+
28+
def _mock_model():
29+
return MagicMock()
30+
31+
32+
def _run_input(thread_id: str = "t1"):
33+
from ag_ui.core import RunAgentInput, UserMessage
34+
return RunAgentInput(
35+
thread_id=thread_id,
36+
run_id="r1",
37+
state={},
38+
messages=[UserMessage(id="u1", content="hello")],
39+
tools=[],
40+
context=[],
41+
forwarded_props={},
42+
)
43+
44+
45+
class _CapturingCore:
46+
"""Replacement for StrandsAgentCore that records constructor kwargs."""
47+
48+
def __init__(self, **kwargs):
49+
self.init_kwargs = kwargs
50+
self.tool_registry = ToolRegistry()
51+
52+
async def stream_async(self, _msg: str):
53+
if False:
54+
yield
55+
56+
57+
async def _trigger_thread_creation(ag: StrandsAgent, thread_id: str) -> "_CapturingCore":
58+
"""Run the agent far enough to create the thread instance, then return it."""
59+
inp = _run_input(thread_id)
60+
async for _ in ag.run(inp):
61+
break # one event is enough; thread is created before any yield
62+
return ag._agents_by_thread[thread_id]
63+
64+
65+
# ---------------------------------------------------------------------------
66+
# Static tests — check _agent_kwargs at construction time (no async needed)
67+
# ---------------------------------------------------------------------------
68+
69+
class TestTemplateKwargsCapture:
70+
"""StrandsAgent.__init__ must capture all relevant template attributes."""
71+
72+
def test_trace_attributes_captured(self):
73+
"""trace_attributes from the template must appear in _agent_kwargs."""
74+
template = Agent(model=_mock_model(), trace_attributes={"env": "prod"})
75+
ag = StrandsAgent(template, name="test")
76+
77+
assert "trace_attributes" in ag._agent_kwargs, (
78+
"trace_attributes not captured — new threads will lose observability config"
79+
)
80+
assert ag._agent_kwargs["trace_attributes"] == {"env": "prod"}
81+
82+
def test_agent_id_captured(self):
83+
"""agent_id from the template must appear in _agent_kwargs."""
84+
template = Agent(model=_mock_model(), agent_id="my-agent-id")
85+
ag = StrandsAgent(template, name="test")
86+
87+
assert "agent_id" in ag._agent_kwargs, (
88+
"agent_id not captured — new threads will get the default 'default' id"
89+
)
90+
assert ag._agent_kwargs["agent_id"] == "my-agent-id"
91+
92+
def test_conversation_manager_captured(self):
93+
"""conversation_manager from the template must appear in _agent_kwargs."""
94+
from strands.agent.conversation_manager import NullConversationManager
95+
cm = NullConversationManager()
96+
template = Agent(model=_mock_model(), conversation_manager=cm)
97+
ag = StrandsAgent(template, name="test")
98+
99+
assert "conversation_manager" in ag._agent_kwargs, (
100+
"conversation_manager not captured — new threads use a different manager"
101+
)
102+
assert ag._agent_kwargs["conversation_manager"] is cm
103+
104+
def test_initial_state_captured(self):
105+
"""Initial state from the template must be preserved for new threads."""
106+
template = Agent(model=_mock_model(), state={"greeting": "hello", "count": 0})
107+
ag = StrandsAgent(template, name="test")
108+
109+
assert "state" in ag._agent_kwargs, (
110+
"state not captured — new threads always start with empty state"
111+
)
112+
113+
114+
# ---------------------------------------------------------------------------
115+
# Runtime tests — confirm new thread instances are created with the right kwargs
116+
# ---------------------------------------------------------------------------
117+
118+
class TestNewThreadUsesTemplateKwargs:
119+
"""When StrandsAgentCore is instantiated for a new thread it must receive
120+
all template kwargs, not just the four currently hard-coded ones."""
121+
122+
@pytest.mark.asyncio
123+
async def test_new_thread_receives_trace_attributes(self):
124+
"""New thread instance must be constructed with the template trace_attributes."""
125+
template = Agent(model=_mock_model(), trace_attributes={"env": "prod"})
126+
ag = StrandsAgent(template, name="test")
127+
128+
with patch("ag_ui_strands.agent.StrandsAgentCore", _CapturingCore):
129+
instance = await _trigger_thread_creation(ag, "trace-thread")
130+
131+
assert instance.init_kwargs.get("trace_attributes") == {"env": "prod"}, (
132+
f"trace_attributes not passed to new thread. Got: {instance.init_kwargs}"
133+
)
134+
135+
@pytest.mark.asyncio
136+
async def test_new_thread_receives_agent_id(self):
137+
"""New thread instance must be constructed with the template agent_id."""
138+
template = Agent(model=_mock_model(), agent_id="my-agent-id")
139+
ag = StrandsAgent(template, name="test")
140+
141+
with patch("ag_ui_strands.agent.StrandsAgentCore", _CapturingCore):
142+
instance = await _trigger_thread_creation(ag, "id-thread")
143+
144+
assert instance.init_kwargs.get("agent_id") == "my-agent-id", (
145+
f"agent_id not passed to new thread. Got: {instance.init_kwargs}"
146+
)
147+
148+
@pytest.mark.asyncio
149+
async def test_new_thread_receives_initial_state(self):
150+
"""New thread instance must be constructed with the template initial state."""
151+
template = Agent(model=_mock_model(), state={"greeting": "hello"})
152+
ag = StrandsAgent(template, name="test")
153+
154+
with patch("ag_ui_strands.agent.StrandsAgentCore", _CapturingCore):
155+
instance = await _trigger_thread_creation(ag, "state-thread")
156+
157+
assert "state" in instance.init_kwargs, (
158+
f"state not passed to new thread. Got: {instance.init_kwargs}"
159+
)

0 commit comments

Comments
 (0)