graphflow-stream
The astream_events LangGraph gives Python, for graph-flow in Rust.
Install
Usage
use ;
use ;
;
let = spawn_task;
while let Some = rx.recv.await
let result = handle.await??; // same TaskResult you'd get from task.run()
Just want the full streamed text back, no manual loop? One line:
let text = collect_text.await?;
emit_token/emit_started/emit_finished/emit_failed are ambient — call them from anywhere inside Task::run, no new trait to implement, no-op if nothing is listening. spawn_graph(flow_runner, session_id, buffer) does the same for a whole FlowRunner run; SubgraphTask wraps a nested Graph as one Task and streams through automatically.
Replay debugging: record(rx).await turns a run into a Recording (serializable, so it can be saved to disk), and recording.replay(buffer) plays it back on a fresh channel with the original timing — inspect a past run, or demo a UI without hitting an LLM again.
Orchestration: map over a runtime list, vote across runs
graph_flow's built-in FanOutTask runs a fixed set of children decided at construction time. DynamicMapTask covers what LangGraph's Send API covers in Python — fan out over however many items context holds this run (one child per retrieved document, one per subtask an LLM just planned):
use DynamicMapTask;
let map_task = new.with_prefix;
map_task.run.await?; // writes summaries.<doc_id>.response for each doc
EnsembleTask runs the same task several times concurrently and reduces the responses — self-consistency prompting, sample an LLM call a few times and combine instead of trusting one draw:
use ;
let ensemble = new;
let result = ensemble.run.await?; // most common of 5 concurrent runs
majority_vote ships built in; pass any Fn(Vec<String>) -> String for a custom reducer (join, longest, an LLM-as-judge pick).
More examples (full_graph, sse_axum, websocket_axum, replay, map_and_ensemble) in examples/.
Benchmarks
cargo bench (criterion, benches/overhead.rs):
| Scenario | Time |
|---|---|
task.run() direct — no graphflow-stream involved |
~1.0 µs |
emit_token() with nobody listening (ambient no-op) |
~30 ns / call |
spawn_task() streaming 100 tokens to a draining receiver |
~2.0 µs / token |
record() capturing 100 streamed tokens |
~1.4 µs / token |
DynamicMapTask::run, 10 items |
~239 µs |
EnsembleTask::run, 5 runs |
~215 µs |
License
MIT