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
18 changes: 17 additions & 1 deletion src/strands_compose/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,13 @@ def build_manifest(

Pure function: no I/O, no network calls, no mutation of inputs.

Delegate orchestrations are built as a new :class:`strands.Agent` forked
from an entry agent's blueprint. Because the delegate agent reports token
usage under the orchestration name, it is included in ``manifest.agents``
(in addition to the orchestration entry) so consumers can trace any token
event back to a model descriptor. Swarm and Graph orchestrations do not
report token usage directly and are therefore not duplicated.

Args:
agents: Resolved agents keyed by name.
orchestrators: Resolved orchestrations keyed by name.
Expand All @@ -280,8 +287,17 @@ def build_manifest(
Raises:
ValueError: If *entry* cannot be resolved by object identity.
"""
agent_descriptors = [_agent_descriptor(name, agent) for name, agent in agents.items()]

# Delegate orchestrations are Agents themselves — they report token usage
# under the orchestration name, so include them in agents so consumers can
# look up the model for any incoming token event by name.
for name, orch in orchestrators.items():
if isinstance(orch, Agent):
agent_descriptors.append(_agent_descriptor(name, orch))

return SessionManifest(
agents=[_agent_descriptor(name, agent) for name, agent in agents.items()],
agents=agent_descriptors,
orchestrations=[
_orchestration_descriptor(name, orch) for name, orch in orchestrators.items()
],
Expand Down
57 changes: 55 additions & 2 deletions tests/unit/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,15 @@ def test_manifest_entry_not_found_raises_value_error(self):
)

def test_manifest_orchestration_kind_delegate(self):
"""Agent orchestration → kind='delegate'."""
"""Agent orchestration → kind='delegate'; delegate also appears in agents."""
agent = _mock_agent()
delegate = Mock(spec=Agent)
delegate._session_manager = None
delegate.description = None
delegate.model = Mock()
delegate.model.get_config.return_value = {}
delegate.model.__class__.__module__ = "strands.models"
delegate.model.__class__.__qualname__ = "TestModel"

manifest = build_manifest(
agents={"agent1": agent},
Expand All @@ -261,6 +266,10 @@ def test_manifest_orchestration_kind_delegate(self):
assert manifest.orchestrations[0].nodes == []
assert manifest.orchestrations[0].edges is None
assert manifest.orchestrations[0].entry_node_id is None
# Delegate agent is also included in manifest.agents under its orch name
agent_names = [a.name for a in manifest.agents]
assert "agent1" in agent_names
assert "delegate1" in agent_names

def test_manifest_orchestration_kind_swarm(self):
"""Swarm orchestration → kind='swarm'."""
Expand Down Expand Up @@ -546,10 +555,15 @@ def test_manifest_graph_entry_node_id_none_when_empty(self):
assert manifest.orchestrations[0].entry_node_id is None

def test_manifest_delegate_empty_topology(self):
"""Delegate orchestration has empty topology."""
"""Delegate orchestration has empty topology; delegate appears in agents."""
agent = _mock_agent()
delegate = Mock(spec=Agent)
delegate._session_manager = None
delegate.description = None
delegate.model = Mock()
delegate.model.get_config.return_value = {}
delegate.model.__class__.__module__ = "strands.models"
delegate.model.__class__.__qualname__ = "TestModel"

manifest = build_manifest(
agents={"agent1": agent},
Expand All @@ -561,6 +575,45 @@ def test_manifest_delegate_empty_topology(self):
assert orch_desc.nodes == []
assert orch_desc.edges is None
assert orch_desc.entry_node_id is None
assert len(manifest.agents) == 2
assert {a.name for a in manifest.agents} == {"agent1", "delegate1"}

def test_manifest_delegate_agent_descriptor_uses_orchestration_name(self):
"""Delegate added to agents uses the orchestration name, not the entry agent name."""
agent = _mock_agent(model_id="claude-3")
delegate = Mock(spec=Agent)
delegate._session_manager = None
delegate.description = "orchestrator"
delegate.model = Mock()
delegate.model.get_config.return_value = {"model_id": "claude-3"}
delegate.model.__class__.__module__ = "strands.models"
delegate.model.__class__.__qualname__ = "TestModel"

manifest = build_manifest(
agents={"manager": agent},
orchestrators={"main": delegate},
entry=delegate,
)

delegate_agent = next(a for a in manifest.agents if a.name == "main")
assert delegate_agent.model.model_id == "claude-3"

def test_manifest_non_delegate_orchestration_not_added_to_agents(self):
"""Swarm and Graph orchestrations are not added to manifest.agents."""
agent = _mock_agent()
swarm = Mock(spec=Swarm)
swarm.nodes = {}
swarm.entry_point = None
swarm.session_manager = None

manifest = build_manifest(
agents={"agent1": agent},
orchestrators={"swarm1": swarm},
entry=swarm,
)

assert len(manifest.agents) == 1
assert manifest.agents[0].name == "agent1"


# ── first_session_id ─────────────────────────────────────────────────────────
Expand Down
Loading