Async Iterators for Streaming
Stream agent events as they happen and handle each one in your own async code. Async iterators are the streaming interface for asynchronous frameworks like FastAPI, aiohttp, and Express, where you control the flow of execution.
For every event the stream can emit, including text, tool usage, lifecycle, and reasoning events, see the stream event types reference.
Basic Usage
Section titled “Basic Usage”Python uses the stream_async, which is a streaming counterpart to the invoke_async method, for asynchronous streaming. This is ideal for frameworks like FastAPI, aiohttp, or Django Channels.
Note: Python also supports synchronous event handling via callback handlers.
import asynciofrom strands import Agentfrom strands.vended_tools import notebook
# Initialize our agent without a callback handleragent = Agent( tools=[notebook], callback_handler=None)
# Async function that iterates over streamed agent eventsasync def process_streaming_response(): agent_stream = agent.stream_async( 'Create a notebook named "ideas" and add three project ideas.' ) async for event in agent_stream: print(event)
# Run the agentasyncio.run(process_streaming_response())TypeScript uses the stream method for streaming, which is async by default. This is ideal for frameworks like Express.js or NestJS.
// Initialize our agent without a printerconst agent = new Agent({ tools: [notebook], printer: false,})
// Async function that iterates over streamed agent eventsasync function processStreamingResponse(): Promise<void> { for await (const event of agent.stream('Record that my favorite color is blue!')) { console.log(event) }}
// Run the agentawait processStreamingResponse()Server examples
Section titled “Server examples”Here’s how to integrate streaming with web frameworks to create a streaming endpoint:
from fastapi import FastAPI, HTTPExceptionfrom fastapi.responses import StreamingResponsefrom pydantic import BaseModelfrom strands import Agentfrom strands.vended_tools import notebook, http_request
app = FastAPI()
class PromptRequest(BaseModel): prompt: str
@app.post("/stream")async def stream_response(request: PromptRequest): async def generate(): agent = Agent( tools=[notebook, http_request], callback_handler=None )
try: async for event in agent.stream_async(request.prompt): if "data" in event: # Only stream text chunks to the client yield event["data"] except Exception as e: yield f"Error: {str(e)}"
return StreamingResponse( generate(), media_type="text/plain" )Note: This is a conceptual example. Install Express.js with
npm install express @types/expressto use it in your project.
// Install Express: npm install express @types/express
interface PromptRequest { prompt: string}
async function handleStreamRequest(req: any, res: any) { console.log(`Got Request: ${JSON.stringify(req.body)}`) const { prompt } = req.body as PromptRequest
res.setHeader('Content-Type', 'application/x-ndjson')
const agent = new Agent({ tools: [notebook], printer: false, })
for await (const event of agent.stream(prompt)) { // Events automatically serialize to compact JSON via toJSON(), // keeping only relevant data fields. The full Agent instance, // Tool classes, and mutable hook flags (cancel/retry) are excluded. res.write(`${JSON.stringify(event)}\n`) } res.end()}
const app = express()app.use(express.json())app.post('/stream', handleStreamRequest)app.listen(3000)You can then curl your local server with:
curl localhost:3000/stream -d '{"prompt": "Hello"}' -H "Content-Type: application/json"Agentic Loop
Section titled “Agentic Loop”This processor prints each lifecycle event as it arrives, so you can watch the order the agent moves through its loop:
from strands import Agentfrom strands.vended_tools import notebook
# Create agent with event loop trackeragent = Agent( tools=[notebook], callback_handler=None)
# Print the full event lifecycle to the consoleasync for event in agent.stream_async( 'Create a notebook named "ideas" and add three project ideas.'): # Track event loop lifecycle if event.get("init_event_loop", False): print("Event loop initialized") elif event.get("start_event_loop", False): print("Event loop cycle starting") elif "message" in event: print(f"New message created: {event['message']['role']}") elif "result" in event: print("Agent completed with result") elif event.get("force_stop", False): print(f"Event loop force-stopped: {event.get('force_stop_reason', 'unknown reason')}")
# Track tool usage if "current_tool_use" in event and event["current_tool_use"].get("name"): tool_name = event["current_tool_use"]["name"] print(f"Using tool: {tool_name}")
# Show the first 20 characters of each text chunk to keep output readable if "data" in event: data_snippet = event["data"][:20] + ("..." if len(event["data"]) > 20 else "") print(f"Text: {data_snippet}")The output will show the sequence of events:
- First the event loop initializes (
init_event_loop) - Then the cycle begins (
start_event_loop) - New cycles may start multiple times during execution (
start_event_loop) - Text generation and tool usage events occur during the cycle
- Finally, the agent completes with a
resultevent or may be force-stopped (force_stop)
function processEvent(event: AgentStreamEvent): void { // Track agent loop lifecycle switch (event.type) { case 'beforeInvocationEvent': console.log('Agent loop initialized') break case 'beforeModelCallEvent': console.log('Agent loop cycle starting') break case 'afterModelCallEvent': console.log(`New message created: ${event.stopData?.message.role}`) break case 'beforeToolsEvent': console.log('About to execute tool!') break case 'afterToolsEvent': console.log('Finished executing tool!') break case 'afterInvocationEvent': console.log('Agent loop completed') break }
// Track tool usage if ( event.type === 'modelStreamUpdateEvent' && event.event.type === 'modelContentBlockStartEvent' && event.event.start?.type === 'toolUseStart' ) { console.log(`\nUsing tool: ${event.event.start.name}`) }
// Show text snippets if ( event.type === 'modelStreamUpdateEvent' && event.event.type === 'modelContentBlockDeltaEvent' && event.event.delta.type === 'textDelta' ) { process.stdout.write(event.event.delta.text) }}const responseGenerator = agent.stream( 'What is the capital of France and what is 42+7? Record in the notebook.')for await (const event of responseGenerator) { processEvent(event)}The output will show the sequence of events:
- First the invocation starts (
beforeInvocationEvent) - Then the model is called (
beforeModelCallEvent) - The model generates content with delta events (wrapped in
modelStreamUpdateEvent) - Tools may be executed (
beforeToolsEvent,afterToolsEvent) - The model may be called again in subsequent cycles
- Finally, the invocation completes (
afterInvocationEvent)
Streaming from sub-agents
Section titled “Streaming from sub-agents”This example combines agents as tools and tool streaming to stream events from a sub-agent:
from typing import AsyncIteratorfrom dataclasses import dataclassfrom strands import Agent, toolfrom strands.vended_tools import notebook
@dataclassclass SubAgentResult: agent: Agent event: dict
@toolasync def notes_agent(query: str) -> AsyncIterator: """Organize notes using the notebook tool.""" agent = Agent( name="Notes Expert", system_prompt="Organize the user's information with the notebook tool.", callback_handler=None, tools=[notebook] )
result = None async for event in agent.stream_async(query): yield SubAgentResult(agent=agent, event=event) if "result" in event: result = event["result"]
yield str(result)
def process_sub_agent_events(event): """Shared processor for sub-agent streaming events""" tool_stream = event.get("tool_stream_event", {}).get("data")
if isinstance(tool_stream, SubAgentResult): current_tool = tool_stream.event.get("current_tool_use", {}) tool_name = current_tool.get("name")
if tool_name: print(f"Agent '{tool_stream.agent.name}' using tool '{tool_name}'")
# Also show regular text output if "data" in event: print(event["data"], end="")
# Using with async iteratorsorchestrator_async_iterator = Agent( system_prompt="Route note-taking requests to the notes_agent tool.", callback_handler=None, tools=[notes_agent])
# With async-iteratorasync for event in orchestrator_async_iterator.stream_async( 'Create a notebook named "ideas" and add three project ideas.'): process_sub_agent_events(event)
# With callback handlerdef handle_events(**kwargs): process_sub_agent_events(kwargs)
orchestrator_callback = Agent( system_prompt="Route note-taking requests to the notes_agent tool.", callback_handler=handle_events, tools=[notes_agent])
orchestrator_callback('Add two more ideas to the "ideas" notebook.')// Create the math agentconst mathAgent = new Agent({ systemPrompt: 'You are a math expert. Answer a math problem in one sentence', printer: false,})
const calculator = tool({ name: 'mathAgent', description: 'Agent that calculates the answer to a math problem input.', inputSchema: z.object({ input: z.string() }), callback: async function* (input): AsyncGenerator<string, string, unknown> { // Stream from the sub-agent const generator = mathAgent.stream(input.input) let result = await generator.next() while (!result.done) { // Process events from the sub-agent if ( result.value.type === 'modelStreamUpdateEvent' && result.value.event.type === 'modelContentBlockDeltaEvent' && result.value.event.delta.type === 'textDelta' ) { yield result.value.event.delta.text } result = await generator.next() } return result.value.lastMessage.content[0]!.type === 'textBlock' ? result.value.lastMessage.content[0]!.text : result.value.lastMessage.content[0]!.toString() },})
const agent = new Agent({ tools: [calculator] })for await (const event of agent.stream('What is 2 * 3? Use your tool.')) { if (event.type === 'toolStreamUpdateEvent') { console.log(`Tool Event: ${JSON.stringify(event.event.data)}`) }}console.log('\nDone!')