Skip to content

Commit 3d051b8

Browse files
docs: README snippet — drain observer before exit
CoPilot review on PR #36 caught the snippet running invoke() inside asyncio.run() without awaiting drain(). The CompiledGraph.drain docstring says drain is the MUST for short-lived scripts because invoke() returns before the observer delivery queue finishes — the observer's print lines could fail to fire before the process exits, defeating the example's whole purpose. The repo's own observer demo (examples/04-observer-hooks/main.py) already uses the right pattern: async def main() that awaits invoke() and then awaits drain() in a try/finally, then asyncio.run(main()). Match that here. Also drops the unused ``final =`` assignment. Bumps the preamble line count from "forty lines" to "about fifty lines" to match what the snippet actually is.
1 parent e7c9459 commit 3d051b8

1 file changed

Lines changed: 10 additions & 3 deletions

File tree

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ The OpenTelemetry mapping mandates a private `TracerProvider`. That prevents the
5555

5656
## Hello World
5757

58-
Forty lines that show the engine in action. Three reducer policies declared on one state class. Routing as a pure function of state, not a hidden state machine. An observer attached at compile time that sees every node boundary the engine emits. No LLM, no API key, no boilerplate. Copy it, run it, watch the events fire. Requires Python 3.12 or later.
58+
About fifty lines that show the engine in action. Three reducer policies declared on one state class. Routing as a pure function of state, not a hidden state machine. An observer attached at compile time that sees every node boundary the engine emits. No LLM, no API key, no boilerplate. Copy it, run it, watch the events fire. Requires Python 3.12 or later.
5959

6060
```python
6161
import asyncio
@@ -127,12 +127,19 @@ graph = (
127127
)
128128
graph.attach_observer(trace)
129129

130-
final = asyncio.run(graph.invoke(PipelineState(query="what is RAG?")))
130+
async def main() -> None:
131+
try:
132+
await graph.invoke(PipelineState(query="what is RAG?"))
133+
finally:
134+
await graph.drain()
135+
136+
137+
asyncio.run(main())
131138
# classify: sources=[]
132139
# research: sources=['wikipedia', 'arxiv']
133140
```
134141

135-
A few things to notice in those forty lines:
142+
A few things to notice in this short example:
136143

137144
- **Three reducer policies on one state schema.** `query` and `classification` get the default `last_write_wins`. `sources` is `Annotated[list[str], append]`, so successive writes concatenate. `metadata` is `Annotated[dict[str, str], merge]`, so successive writes shallow-merge. The merge policy lives on the schema, once.
138145
- **Conditional routing as a state function.** `route` reads `state.classification` and returns a node name. The graph engine doesn't care that this happens to be deterministic; it would accept an LLM-driven router with the same shape.

0 commit comments

Comments
 (0)