-
Notifications
You must be signed in to change notification settings - Fork 450
feat(multiagent): Add stream_async #961
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Update docstrings to match Agent's minimal style (use dict keys instead of class names) - Add isinstance checks for result event detection for type safety - Improve _stream_with_timeout to handle None timeout case - Add MultiAgentResultEvent for consistency with Agent pattern - Yield TypedEvent objects internally, convert to dict at API boundary - All 154 tests passing
- Remove unnecessary asyncio.gather() after event loop completion - Same issue as tool executor PR strands-agents#954 - By the time loop exits, all tasks have already completed - Gather was waiting for already-finished tasks (no-op) - All 154 tests passing
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The typed events shape & fields are a blocker for me
Properly resolved conflicts by combining both features: - Kept streaming implementation (stream_async) from multiagent-streaming branch - Adopted new invocation_state API from main (invocation_state=invocation_state instead of **invocation_state) Changes in multiagent files: - base.py: Added deprecation warning for **kwargs (from main) - graph.py: Changed stream_async calls to use invocation_state=invocation_state - swarm.py: Changed stream_async calls to use invocation_state=invocation_state - test_graph.py: Updated tests to verify stream_async usage with new API - test_swarm.py: Updated tests to verify stream_async usage with new API This merge combines: 1. Real-time event streaming from multiagent-streaming branch 2. New invocation_state parameter convention from main (commit 7fbc9dc)
Multi-Agent System Events (Updated: Oct 17)This document describes the events emitted by multi-agent systems (Graph and Swarm) during execution via their All examples are based on the actual event structure defined in the code. OverviewMulti-agent systems emit events that provide visibility into:
All events are emitted as dictionaries through the Event Types1. Node Start EventEmitted when a node begins execution. Structure: {
"type": "multiagent_node_start",
"node_id": "agent1",
"node_type": "agent" # or "multiagent" for nested Graph/Swarm
}When emitted:
Example: {
"type": "multiagent_node_start",
"node_id": "researcher",
"node_type": "agent"
}2. Node Stream EventEmitted during node execution to forward agent events with node context. This wraps ALL agent events. Structure: {
"type": "multiagent_node_stream",
"node_id": "agent1",
"event": {
# Original agent event (nested)
}
}When emitted:
Nested event types (in the
Examples: Lifecycle event: {
"type": "multiagent_node_stream",
"node_id": "researcher",
"event": {
"init_event_loop": True
}
}Text streaming: {
"type": "multiagent_node_stream",
"node_id": "researcher",
"event": {
"data": "Hello! I'm researching...",
"delta": {"text": "Hello! I'm researching..."},
"agent": "<Agent object>",
"event_loop_cycle_id": "uuid...",
"request_state": {}
}
}Raw model event: {
"type": "multiagent_node_stream",
"node_id": "researcher",
"event": {
"event": {
"contentBlockDelta": {
"delta": {"text": "Hello"},
"contentBlockIndex": 0
}
}
}
}Agent result: {
"type": "multiagent_node_stream",
"node_id": "researcher",
"event": {
"result": AgentResult(
stop_reason="end_turn",
message={"role": "assistant", "content": [{"text": "Research complete"}]},
metrics=EventLoopMetrics(...),
state={}
)
}
}3. Node Stop EventEmitted when a node stops execution (success or failure). Similar to Structure: {
"type": "multiagent_node_stop",
"node_id": "agent1",
"node_result": NodeResult(...)
}NodeResult Fields:
For Agent nodes, AgentResult contains:
When emitted:
Example (Success): {
"type": "multiagent_node_stop",
"node_id": "researcher",
"node_result": NodeResult(
result=AgentResult(
stop_reason="end_turn",
message={
"role": "assistant",
"content": [{"text": "Research findings: ..."}]
},
metrics=EventLoopMetrics(
cycle_count=1,
accumulated_usage={"inputTokens": 18, "outputTokens": 23, "totalTokens": 41},
accumulated_metrics={"latencyMs": 1975}
),
state={}
),
execution_time=2340,
status=Status.COMPLETED,
accumulated_usage={"inputTokens": 18, "outputTokens": 23, "totalTokens": 41},
accumulated_metrics={"latencyMs": 1975},
execution_count=1
)
}Example (Failure): {
"type": "multiagent_node_stop",
"node_id": "researcher",
"node_result": NodeResult(
result=Exception("Node execution failed"),
execution_time=500,
status=Status.FAILED,
accumulated_usage={"inputTokens": 0, "outputTokens": 0, "totalTokens": 0},
accumulated_metrics={"latencyMs": 500},
execution_count=1
)
}4. Handoff EventEmitted during node transitions in multi-agent systems. Structure: {
"type": "multiagent_handoff",
"from_nodes": ["node1"], # List of completing nodes
"to_nodes": ["node2"], # List of starting nodes
"message": "Optional handoff message" # Only present if provided
}When emitted:
Example (Swarm): {
"type": "multiagent_handoff",
"from_nodes": ["researcher"],
"to_nodes": ["analyst"],
"message": "Need calculations for the data"
}Example (Graph batch transition): {
"type": "multiagent_handoff",
"from_nodes": ["node1", "node2"],
"to_nodes": ["node3", "node4"]
}5. Final Result EventEmitted when multi-agent execution completes with final result. Structure: {
"type": "multiagent_result",
"result": GraphResult(...) or SwarmResult(...)
}When emitted:
Key Result Fields:
GraphResult additional fields:
SwarmResult additional fields:
Example (Graph): {
"type": "multiagent_result",
"result": GraphResult(
status=Status.COMPLETED,
results={
"researcher": NodeResult(...),
"analyst": NodeResult(...)
},
accumulated_usage={"inputTokens": 75, "outputTokens": 49, "totalTokens": 124},
accumulated_metrics={"latencyMs": 3897},
execution_count=2,
execution_time=4586,
total_nodes=2,
completed_nodes=2,
failed_nodes=0,
execution_order=["researcher", "analyst"],
edges=[("researcher", "analyst")],
entry_points=["researcher"]
)
}Example (Swarm): {
"type": "multiagent_result",
"result": SwarmResult(
status=Status.COMPLETED,
results={
"researcher": NodeResult(...)
},
accumulated_usage={"inputTokens": 602, "outputTokens": 33, "totalTokens": 635},
accumulated_metrics={"latencyMs": 2641},
execution_count=1,
execution_time=2958,
node_history=[SwarmNode(node_id="researcher")]
)
}Complete Event Flow ExamplesGraph Executionasync for event in graph.stream_async("Analyze this data"):
# Event 1: First node starts
# {
# "type": "multiagent_node_start",
# "node_id": "researcher",
# "node_type": "agent"
# }
# Events 2-15: Researcher agent lifecycle and streaming events (wrapped)
# {
# "type": "multiagent_node_stream",
# "node_id": "researcher",
# "event": {"init_event_loop": True}
# }
# {
# "type": "multiagent_node_stream",
# "node_id": "researcher",
# "event": {"start": True}
# }
# {
# "type": "multiagent_node_stream",
# "node_id": "researcher",
# "event": {"data": "Researching...", "delta": {...}}
# }
# ... (more streaming events)
# {
# "type": "multiagent_node_stream",
# "node_id": "researcher",
# "event": {"result": AgentResult(...)}
# }
# Event 16: First node stops
# {
# "type": "multiagent_node_stop",
# "node_id": "researcher",
# "node_result": NodeResult(...)
# }
# Event 17: Handoff to next batch
# {
# "type": "multiagent_handoff",
# "from_nodes": ["researcher"],
# "to_nodes": ["analyst"]
# }
# Event 18: Second node starts
# {
# "type": "multiagent_node_start",
# "node_id": "analyst",
# "node_type": "agent"
# }
# Events 19-32: Analyst agent events (similar to researcher)
# ... (lifecycle, streaming, result)
# Event 33: Second node stops
# {
# "type": "multiagent_node_stop",
# "node_id": "analyst",
# "node_result": NodeResult(...)
# }
# Event 34: Final result
# {
# "type": "multiagent_result",
# "result": GraphResult(...)
# }Swarm Executionasync for event in swarm.stream_async("Calculate 10 + 5"):
# Event 1: First agent starts
# {
# "type": "multiagent_node_start",
# "node_id": "researcher",
# "node_type": "agent"
# }
# Events 2-20: Researcher agent events (wrapped)
# {
# "type": "multiagent_node_stream",
# "node_id": "researcher",
# "event": {"data": "I need calculations...", "delta": {...}}
# }
# ... (more streaming events including handoff tool use)
# Event 21: Researcher stops
# {
# "type": "multiagent_node_stop",
# "node_id": "researcher",
# "node_result": NodeResult(...)
# }
# Event 22: Handoff to analyst
# {
# "type": "multiagent_handoff",
# "from_nodes": ["researcher"],
# "to_nodes": ["analyst"],
# "message": "Need calculations"
# }
# Event 23: Analyst starts
# {
# "type": "multiagent_node_start",
# "node_id": "analyst",
# "node_type": "agent"
# }
# Events 24-40: Analyst agent events
# ... (lifecycle, streaming, result)
# Event 41: Analyst stops
# {
# "type": "multiagent_node_stop",
# "node_id": "analyst",
# "node_result": NodeResult(...)
# }
# Event 42: Final result
# {
# "type": "multiagent_result",
# "result": SwarmResult(...)
# }Usage PatternsMonitoring Progressasync for event in graph.stream_async(task):
if event.get("type") == "multiagent_node_start":
print(f"Starting: {event['node_id']}")
elif event.get("type") == "multiagent_node_stop":
node_result = event["node_result"]
print(f"Stopped: {event['node_id']} in {node_result.execution_time}ms")Collecting Text Outputoutputs = []
async for event in graph.stream_async(task):
if event.get("type") == "multiagent_node_stream":
nested = event["event"]
if "data" in nested:
outputs.append({
"node": event["node_id"],
"text": nested["data"]
})Getting Node Resultsresults = {}
async for event in graph.stream_async(task):
if event.get("type") == "multiagent_node_stop":
node_result = event["node_result"]
results[event["node_id"]] = node_resultTracking Handoffshandoffs = []
async for event in swarm.stream_async(task):
if event.get("type") == "multiagent_handoff":
handoffs.append({
"from": event["from_nodes"],
"to": event["to_nodes"],
"message": event.get("message")
})Real-time Streaming Displayasync for event in graph.stream_async(task):
if event.get("type") == "multiagent_node_stream":
nested = event["event"]
# Display text as it streams
if "data" in nested:
print(nested["data"], end="", flush=True)
# Show when agent completes
if "result" in nested:
print(f"\n[{event['node_id']} completed]")Access Node Results Immediatelyasync for event in graph.stream_async(task):
if event.get("type") == "multiagent_node_stop":
node_id = event["node_id"]
node_result = event["node_result"]
# Check status
if node_result.status == Status.FAILED:
print(f"❌ {node_id} failed: {node_result.result}")
continue
# For successful Agent nodes, access AgentResult
agent_result = node_result.result
if hasattr(agent_result, "stop_reason"):
print(f"{node_id} stopped: {agent_result.stop_reason}")
if hasattr(agent_result, "message"):
content = agent_result.message.get("content", [])
for item in content:
if "text" in item:
print(f"{node_id} output: {item['text']}")
# Access accumulated metrics
print(f"{node_id} tokens: {node_result.accumulated_usage}")Event Identification PatternsTo identify event types, check the async for event in graph.stream_async(task):
event_type = event.get("type")
if event_type == "multiagent_node_start":
# Node starting
node_id = event["node_id"]
node_type = event["node_type"]
elif event_type == "multiagent_node_stream":
# Forwarded agent event
node_id = event["node_id"]
agent_event = event["event"]
# Check nested event type
if "init_event_loop" in agent_event:
# Agent lifecycle start
pass
elif "data" in agent_event:
# Text streaming
text = agent_event["data"]
elif "result" in agent_event:
# Agent result
result = agent_event["result"]
elif "message" in agent_event:
# Agent message
message = agent_event["message"]
elif event_type == "multiagent_node_stop":
# Node stopped
node_id = event["node_id"]
node_result = event["node_result"]
elif event_type == "multiagent_handoff":
# Node transition
from_nodes = event["from_nodes"]
to_nodes = event["to_nodes"]
message = event.get("message") # Optional
elif event_type == "multiagent_result":
# Final result
final_result = event["result"]Nested Multi-Agent SystemsWhen a Graph or Swarm contains another Graph or Swarm as a node, events are nested: # Outer graph with nested graph as a node
async for event in outer_graph.stream_async(task):
if event.get("type") == "multiagent_node_stream":
nested = event["event"]
# The nested event might itself be a multi-agent event
if nested.get("type") == "multiagent_node_start":
# Nested graph's node is starting
print(f"Nested node starting: {nested['node_id']}")
elif nested.get("type") == "multiagent_node_stream":
# Doubly nested event from agent in nested graph
doubly_nested = nested["event"]
if "data" in doubly_nested:
print(f"Text from nested agent: {doubly_nested['data']}")This allows you to track execution at any depth of nesting while maintaining context about which node at each level is generating the events. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed the logic and the integ tests. Will review the unit tests after the comments are addressed.
Description
This PR adds streaming support to the Swarm and Graph multi-agent systems, enabling real-time event emission during multi-agent execution. This brings multi-agent systems to feature parity with the single Agent class streaming capabilities.
Key Changes
New Event Types (
src/strands/types/_events.py):MultiAgentNodeStartEvent: Emitted when a node begins executionMultiAgentNodeCompleteEvent: Emitted when a node completes executionMultiAgentNodeStreamEvent: Forwards agent events with node contextMultiAgentHandoffEvent: Emitted during agent handoffs in Swarm (includes from_node, to_node, and message)Swarm Streaming (
src/strands/multiagent/swarm.py):stream_async()method that yields events during executioninvoke_async()to usestream_async()internally (maintains backward compatibility)Graph Streaming (
src/strands/multiagent/graph.py):stream_async()method for real-time event streaminginvoke_async()to consumestream_async()eventsTesting:
Benefits
Related Issues
Documentation PR
Type of Change
New feature
Testing
How have you tested the change?
tests/strands/multiagent/test_swarm.py,tests/strands/multiagent/test_graph.py)Verify that the changes do not break functionality or introduce warnings in consuming repositories: agents-docs, agents-tools, agents-cli
hatch run prepareChecklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.