The Python two-snake logo in blue and yellow beside the python wordmark in grey on a white card

Python's yield streams input into a running AI agent — the difference from return, in Node.js too

How Python's yield differs from return, and how an async generator passed as a prompt streams input into a long-running AI agent, with the Node.js equivalent.

On this page

Introduction

A long-running AI agent sometimes needs new information injected mid-execution (a human’s approval, an external event, a follow-up instruction) without recreating the session and resending its entire history. Python’s yield turns out to be the mechanism behind that pattern: a keyword simple enough to skim past without noticing it connects directly to agent architecture. The pattern is an async generator held open as the input stream: yield pauses a function with its state intact, so an agent’s session can stay “warm” while new messages arrive one at a time. This article works through yield from its difference with return, to that streaming-input pattern, to the same thing implemented in Node.js.

return and yield — a fundamental difference in control flow

return “exits immediately”

return ends the function on the spot. All local variables in the function are discarded, and the next call starts over from the first line.

def get_greeting():
name = "Alice"
return f"Hello, {name}"
# the function ends completely here; name is discarded too
result = get_greeting() # runs from the start every time

yield “pauses and resumes”

Using yield turns the function into a generator. It returns a value while pausing, with the function’s state (variables, execution position) all preserved. When the next value is requested, execution resumes from the line where it paused.

def count_up():
n = 1
while True:
yield n # return n and pause
n += 1 # on resume, continues from here
counter = count_up()
print(next(counter)) # 1
print(next(counter)) # 2 (remembers the state n=1)
print(next(counter)) # 3
return restarts; yield resumesTwo columns. On the left, each call runs the function from the start and discards its state on return, so n is 1 every time. On the right, each next() resumes the generator, which yields an incrementing n and then suspends with that state kept.return — resets every callcall 1run (n=1)call 2run (n=1)call 3run (n=1)return: state discardedreturn: state discardedstarts from n=1 every timeyield — keeps state, resumesnext() #1run → yield n=1next() #2resume → yield n=2next() #3resume → yield n=3paused (n=1 kept)paused (n=2 kept)keeps state, continues from last time
return restarts from scratch; yield resumes where it stopped.

Comparison summary

Property return yield
Function end Ends immediately Pauses (resumable)
State retention Discarded Preserved
Call model Runs from the start every time Resumes from the previous pause point
An analogy Sending a letter (one and done) Making a phone call (keeping the line open)

Streaming input into an agent — the async iterator pattern

The yield property of “returning values one at a time while keeping state” connects directly to the input pattern for a long-running AI agent.

The problem: the limit of a single request

In an ordinary API call, all input must be present at request time.

# conventional: pass all input up front
response = await client.query(prompt="Analyse this data")

But in a long-running agent, some situations call for injecting new information mid-execution. Recreating the session each time means resending the entire past conversation history, which is inefficient.

The solution: streaming input with an async generator

Passing a Python async generator as the prompt keeps the session “open”, dynamically feeding in messages that arrive later.

import asyncio
async def message_streamer():
# send the initial instruction
yield "Starting the agent. Please analyse the S3 logs."
# wait for events from an external queue (SQS, etc.) and inject as they arrive
while True:
event = await queue.get()
yield event.message
if event.is_last:
break
# pass the iterator to the SDK — the session is kept alive (pseudo-code)
response = await client.query(prompt=message_streamer())

The point is that thanks to yield, the function does not end and enters a “waiting state” until an event arrives in the queue. When an event arrives it resumes and feeds a new message to the agent.

The exact SDK entry point here is illustrative: the method that accepts an async iterable of messages goes by different names across SDKs (query, run, chat), and these APIs are evolving quickly, so check your SDK’s current signature. What is stable across them is the pattern: an async generator held open as the input stream.

Input streaming vs output streamingTwo rows. The upper row flows left to right: an async generator yields messages into the SDK, which injects them into an agent that is already running. The lower row flows right to left: the agent generates tokens, the SDK receives them as chunks, and the UI shows them as they arrive.Input streaming vs output streamingInput streaming — an async iterator passed as the promptUser / queueasync generatorSDKholds the session openAI agentrunningyield msginjected liveto the agentOutput streaming — streaming=TrueUser / UItokens shown as they landSDKreceives chunksAI agentgenerating tokenschunktoken streamfrom the agent
Input flows into a running agent; output flows back out of it.

Note the difference from streaming=True

The streaming=True you see in many SDKs is a setting for streaming the output (response). Passing an async iterator as the prompt, on the other hand, is a mechanism for streaming the input. The direction is reversed, so take care not to confuse them.

Setting Direction Purpose
streaming=True Output streaming Receive the response token by token
Pass an async iterator as the prompt Input streaming Dynamically inject messages into a running agent

Use cases — when streaming input is needed

1. Human-in-the-Loop (HITL)

When the agent asks for confirmation, such as “may I delete this file?”, the session can wait for the human’s response while staying alive.

async def hitl_streamer():
yield "Please start cleaning up the production database."
# when the agent asks for confirmation, receive the human's answer from the UI
approval = await wait_for_human_approval()
yield f"Approval: {approval}"

The benefit is being able to insert human judgement while keeping the agent “warm”.

2. Injecting real-time external events

A case where a system event, such as “the customer upgraded their plan”, is injected while a support bot is mid-analysis of the same customer’s problem.

async def event_injector():
yield "Please analyse the support ticket for customer #12345."
async for event in system_event_stream:
yield f"[SYSTEM EVENT: {event.description}]"

The agent receives the event and can adjust the direction of its answer in real time.

3. Adding instructions incrementally

A case where, while analysing a 100-page document, an instruction like “actually, add this perspective too” gets injected mid-analysis. Because there is no need to recreate the session, the analysis context so far is preserved.

Implementing it in Node.js — async function*

The same pattern works not only in Python but in Node.js. The syntax is async function* (with an asterisk) and yield.

// a Node.js async generator
async function* messageStreamer() {
yield "Initializing agent...";
while (true) {
const event = await waitForNextEvent();
yield event.text;
if (event.type === 'close') break;
}
}
// pass the iterator to the SDK (pseudo-code)
const client = new AgentClient();
await client.query({ prompt: messageStreamer() });

Python vs Node.js comparison

Item Python Node.js
Syntax async def func(): + yield async function* func() + yield
How to consume async for item in gen: for await (const item of gen)
Generator detection Auto-detected by the presence of yield Explicit via the asterisk of function*
Ecosystem asyncio-based EventEmitter / Promise-based

Node.js differs from Python in needing a * on the function declaration, but the concept is identical. In either language the behaviour is the same: “send out values one at a time without ending the function, keeping state preserved”.

Summary

  • yield returns a value without ending the function. If return is a “letter”, yield is a “phone call” that keeps the line open for exchanging information.
  • It connects directly to streaming input for a long-running agent. Passing an async iterator as the prompt lets messages be dynamically injected into a running agent.
  • It realises patterns common in practice — HITL, external-event injection, incremental instructions. The overhead of recreating the session is avoided in each one.
  • The same concept works in Python and Node.js. There is a syntactic difference, but the design idea is shared.

yield is not merely a language feature; it is a foundation for AI agent design patterns, and the streaming-input pattern is worth reaching for whenever a long-running agent needs it.

Share this article