Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions tests/fakes/strands.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ def get_config(self) -> dict[str, Any]:
"""Return the reported config (used by the manifest model descriptor)."""
return self._config

async def structured_output(self, output_model: Any, prompt: Any = None, **kwargs: Any): # ty: ignore[invalid-method-override]
async def structured_output(
self, output_model: Any, prompt: Any, system_prompt: Any = None, **kwargs: Any
):
"""Yield a single empty structured-output instance (unused by most tests)."""
yield {"output": output_model()}

Expand Down Expand Up @@ -79,7 +81,9 @@ def get_config(self) -> dict[str, Any]:
"""Return the reported config."""
return self._config

async def structured_output(self, output_model: Any, prompt: Any = None, **kwargs: Any): # ty: ignore[invalid-method-override]
async def structured_output(
self, output_model: Any, prompt: Any, system_prompt: Any = None, **kwargs: Any
):
"""Yield a single empty structured-output instance (unused)."""
yield {"output": output_model()}

Expand Down Expand Up @@ -125,7 +129,9 @@ def get_config(self) -> dict[str, Any]:
"""Return a minimal config."""
return {"model_id": "boom-model"}

async def structured_output(self, output_model: Any, prompt: Any = None, **kwargs: Any): # ty: ignore[invalid-method-override]
async def structured_output(
self, output_model: Any, prompt: Any, system_prompt: Any = None, **kwargs: Any
):
"""Raise on structured output too."""
raise RuntimeError(self._message)
yield # pragma: no cover — makes this an async generator
Expand Down
44 changes: 34 additions & 10 deletions tests/resolve/test_delegation.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@
from strands.tools.decorator import DecoratedFunctionTool
from strands.types.tools import AgentTool

from strands_compose.config.resolvers.orchestrations.builders import _delegate_tool
from strands_compose.config.schema import DelegateConnectionDef
from strands_compose.config.resolvers.orchestrations.builders import build_delegate
from strands_compose.config.schema import (
AgentDef,
DelegateConnectionDef,
DelegateOrchestrationDef,
)
from strands_compose.exceptions import ConfigurationError
from strands_compose.tools import multiagent_as_tool
from strands_compose.types import Node
from tests.fakes import FakeModel, ToolThenTextModel


Expand All @@ -34,6 +39,25 @@ def _conn(agent: str, **kwargs) -> DelegateConnectionDef:
return DelegateConnectionDef(agent=agent, description="do work", **kwargs)


def _build_delegate_tool(orch_name: str, conn: DelegateConnectionDef, node: Node) -> AgentTool:
"""Build a real delegate orchestration and return the tool wired for *conn*.

Nothing is faked: ``build_delegate`` runs for real and the tool comes out of
the built agent's registry.
"""
coordinator = "coordinator"
agent = build_delegate(
orch_name,
DelegateOrchestrationDef(entry_name=coordinator, connections=[conn]),
{coordinator: _agent(coordinator), conn.agent: node},
coordinator,
{coordinator: AgentDef(system_prompt="coordinate")},
{},
{},
)
return agent.tool_registry.registry[conn.agent]


async def _call(tool: AgentTool, prompt: str) -> None:
async for _ in tool.stream(
{"toolUseId": "t1", "name": tool.tool_name, "input": {"input": prompt}}, {}
Expand All @@ -59,7 +83,7 @@ def test_multiagent_as_tool_accepts_explicit_name():

def test_agent_connection_uses_the_strands_native_adapter():
"""An Agent must go through Agent.as_tool so interrupts can propagate and resume."""
tool = _delegate_tool("team", _conn("helper"), _agent("helper"))
tool = _build_delegate_tool("team", _conn("helper"), _agent("helper"))

assert tool.tool_type == "agent"
assert not isinstance(tool, DecoratedFunctionTool)
Expand All @@ -68,7 +92,7 @@ def test_agent_connection_uses_the_strands_native_adapter():

def test_orchestration_connection_falls_back_to_the_multiagent_wrapper():
"""A Swarm has no upstream as_tool, so it keeps the hand-rolled wrapper."""
tool = _delegate_tool("outer", _conn("team"), _swarm("team"))
tool = _build_delegate_tool("outer", _conn("team"), _swarm("team"))

assert isinstance(tool, DecoratedFunctionTool)
assert tool.tool_name == "team"
Expand All @@ -79,7 +103,7 @@ def test_orchestration_connection_falls_back_to_the_multiagent_wrapper():

async def test_preserve_context_true_accumulates_history():
agent = _agent("helper")
tool = _delegate_tool("team", _conn("helper", preserve_context=True), agent)
tool = _build_delegate_tool("team", _conn("helper", preserve_context=True), agent)

await _call(tool, "first")
await _call(tool, "second")
Expand All @@ -89,7 +113,7 @@ async def test_preserve_context_true_accumulates_history():

async def test_preserve_context_false_resets_between_calls():
agent = _agent("helper")
tool = _delegate_tool("team", _conn("helper", preserve_context=False), agent)
tool = _build_delegate_tool("team", _conn("helper", preserve_context=False), agent)

await _call(tool, "first")
await _call(tool, "second")
Expand All @@ -108,7 +132,7 @@ def test_preserve_context_false_with_a_session_manager_is_rejected(tmp_path):
)

with pytest.raises(ValueError, match="session manager"):
_delegate_tool("team", _conn("helper", preserve_context=False), agent)
_build_delegate_tool("team", _conn("helper", preserve_context=False), agent)


def test_preserve_context_true_allows_a_session_managed_agent(tmp_path):
Expand All @@ -117,15 +141,15 @@ def test_preserve_context_true_allows_a_session_managed_agent(tmp_path):
session_manager=FileSessionManager(session_id="s2", storage_dir=str(tmp_path)),
)

tool = _delegate_tool("team", _conn("helper", preserve_context=True), agent)
tool = _build_delegate_tool("team", _conn("helper", preserve_context=True), agent)

assert tool.tool_name == "helper"


def test_preserve_context_false_on_an_orchestration_is_rejected():
"""A Swarm cannot be reset to a baseline, so the request must not be ignored."""
with pytest.raises(ConfigurationError, match="no baseline to reset to"):
_delegate_tool("outer", _conn("team", preserve_context=False), _swarm("team"))
_build_delegate_tool("outer", _conn("team", preserve_context=False), _swarm("team"))


async def test_orchestration_interrupt_becomes_a_tool_error():
Expand All @@ -150,7 +174,7 @@ def risky(name: str) -> str:
builder.set_entry_point("worker")
graph = builder.build()

tool = _delegate_tool("outer", _conn("pipeline"), graph)
tool = _build_delegate_tool("outer", _conn("pipeline"), graph)

results = [
event
Expand Down