Compare commits

..
Author SHA1 Message Date
Nick Hollon 83b0d1215b overview: restore thread.output (final-state Awaitable, shares values SSE) 2026-05-14 09:45:36 -07:00
Nick Hollon 08a43ad674 diagram: rename .input.respond to .run.respond to match §3 table 2026-05-13 15:20:25 -07:00
Nick Hollon 07095fdf8b roll in Notion edits: move nested handles to §3.1; rename respond; pin server version 2026-05-13 15:16:33 -07:00
Nick Hollon e450e49afc address Notion review: §2.1 nested handles + §4.1 available projections 2026-05-13 15:13:28 -07:00
Nick Hollon 766103173a high-level design overview for v3 streaming (RFC) 2026-05-13 12:47:51 -07:00
Mason DaughertyandGitHub 4a86705bd7 ci: disable uv cache in publish pipeline (#7777)
Disable `uv` dependency caching throughout the release workflow so
build, validation, PyPI publish, and GitHub release jobs do not restore
shared cache state.

Release jobs already pass the built distributions through explicit
artifacts; keeping dependency caches out of this path avoids
cache-poisoning risk and makes the existing pre-release validation
comment match the actual workflow behavior.
2026-05-12 10:10:50 -07:00
2 changed files with 159 additions and 0 deletions
+3
View File
@@ -32,6 +32,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
cache-suffix: "release"
enable-cache: false
working-directory: ${{ inputs.working-directory }}
# We want to keep this build stage *separate* from the release stage,
@@ -268,6 +269,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
cache-suffix: "release"
enable-cache: false
working-directory: ${{ inputs.working-directory }}
- uses: actions/download-artifact@v8
@@ -309,6 +311,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
cache-suffix: "release"
enable-cache: false
working-directory: ${{ inputs.working-directory }}
- uses: actions/download-artifact@v8
@@ -0,0 +1,156 @@
# Python SDK v3 streaming — high-level design
---
## 1. What it is
A thread-centric streaming surface for `langgraph-sdk` (Python). One context manager per thread, typed command dispatch, typed projections. Mirrors `@langchain/langgraph-sdk` (JS).
```python
async with client.threads.stream(assistant_id="agent") as thread:
await thread.run.start(input={"x": 1})
async for message in thread.messages:
...
final_state = await thread.values
```
Additive — `client.runs.stream(...)` and `client.threads.join_stream(...)` stay.
---
## 2. Architecture
```
User code
│ async with client.threads.stream(...) as thread:
│ await thread.run.start(...)
│ async for msg in thread.messages: ...
┌─────────────────────────────────────────────────────────────────┐
│ Public surface — langgraph_sdk._async.stream │
│ │
│ AsyncThreadStream │
│ .run.start .run.respond .agent.get_tree │
│ .events │
│ .values .output .messages .tool_calls │
│ .subgraphs .subagents .extensions["name"] │
│ .interrupted .interrupts │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Internals — langgraph_sdk.stream │
│ │
│ StreamController (event router, per-run dispatch) │
│ ├─ projections (values / messages / tool_calls / …) │
│ └─ MultiCursorBuffer (per-projection replay log) │
│ │
│ Subscription matcher (channel + namespace filters) │
│ Union-filter SSE (one shared connection per thread) │
│ Lifecycle watcher SSE (always-on, surfaces interrupts) │
│ │
│ ProtocolSseTransport ──── WebSocketTransport │
└─────────────────────────────────────────────────────────────────┘
v3 protocol over HTTP/SSE or WebSocket
┌─────────────────────────────────────────────────────────────────┐
│ langgraph-api server │
│ POST /threads/{thread_id}/commands │
│ POST /threads/{thread_id}/stream/events │
│ WS /threads/{thread_id}/stream/events │
└─────────────────────────────────────────────────────────────────┘
```
Each `AsyncThreadStream` holds one union-filter SSE (subscription set rotates as projections come and go) plus one always-on lifecycle SSE — two HTTP connections per active thread. `AsyncThreadStream` is transport-agnostic; SSE and WebSocket implement the same internal `TransportAdapter` contract.
---
## 3. User-facing surface
| API | Notes |
|---|---|
| `client.threads.stream(thread_id=None, *, assistant_id, headers=None)` | Entry point. Returns an `AsyncThreadStream` async context manager. Mints `uuid.uuid4()` when `thread_id` is None. |
| `thread.run.start(input=, config=, metadata=)` | Dispatches `run.start`. Returns `{"run_id": "..."}`. |
| `thread.run.respond(...)` | Resume after interrupt. |
| `thread.agent.get_tree(...)` | Agent introspection. |
| `thread.events` | Raw `AsyncIterator[Event]` over every channel. |
| `thread.values` | `AsyncIterator[snapshot]` plus `Awaitable[final_state]`. |
| `thread.output` | `Awaitable[final_state]`. Convenience — shares the `values` SSE; skip when you don't need intermediate snapshots. |
| `thread.messages` | `AsyncIterator[StreamingMessageHandle]` — typed over `langchain-core` `BaseMessage`. |
| `thread.tool_calls` | `AsyncIterator[ToolCallHandle]`. |
| `thread.subgraphs` / `thread.subagents` | Nested handles for graph composition. |
| `thread.extensions["name"]` | Per-extension dispatch on `custom:<name>`. |
| `thread.interrupted` / `thread.interrupts` | Lifecycle state, always current. |
### 3.1 Nested handles — subgraphs and subagents
`thread.subgraphs` and `thread.subagents` are streams of **invocations**, not static lists of registered components. Each iteration yields one handle scoped to one execution:
```python
async with client.threads.stream(assistant_id="agent") as thread:
await thread.run.start(input={...})
async for subgraph in thread.subgraphs:
async for message in subgraph.messages:
...
async for call in subgraph.tool_calls:
...
```
A `SubgraphHandle` (or `SubagentHandle`) exposes the same projection surface as the top-level thread — `messages`, `tool_calls`, `subgraphs`, `subagents`, media — filtered to events whose namespace matches that invocation's path. Nesting composes: a subgraph that itself invokes another subgraph yields a fresh handle from `subgraph.subgraphs`. There is no static registration step on the SDK side; the set of subgraphs/subagents that appear is discovered at runtime from event namespaces emitted by the graph.
---
## 4. Migration
Pre-v3 (untyped, one run per call):
```python
async for chunk in client.runs.stream(
thread_id, assistant_id, input={...}, stream_mode="messages",
):
# chunk.event and chunk.data are untyped
...
```
v3 (typed, thread-centric, multiple runs per thread):
```python
async with client.threads.stream(thread_id, assistant_id="agent") as thread:
await thread.run.start(input={...})
async for message in thread.messages:
...
```
Differences:
- The thread is the context manager — multiple `run.start` calls compose inside one session.
- Projections replace `stream_mode` — pick the typed iterable you need.
- Reattach is automatic — `client.threads.stream(thread_id="existing-id", ...)` replays buffered events and goes live. Replaces `client.threads.join_stream(...)`.
- The protocol is parsed once at the SDK boundary; projections expose Python objects, not raw frames.
### 4.1 Available projections
The complete set, all defined in `langgraph_sdk._async.stream`:
- `thread.events` — raw `Event` dicts over every channel; untyped, useful for debug or to drop below the typed surface.
- `thread.values` — state snapshots plus final state. Replaces `stream_mode="values"`.
- `thread.output``Awaitable[final_state]` only. Shares the `values` SSE; convenience for callers who don't need intermediate snapshots.
- `thread.messages``StreamingMessageHandle` typed over `langchain-core` `BaseMessage`. Replaces `stream_mode="messages"`.
- `thread.tool_calls``ToolCallHandle` per tool invocation.
- `thread.subgraphs` / `thread.subagents` — nested handles per invocation (see §3.1).
- `thread.extensions["name"]` — per-extension events on `custom:<name>` channels. Replaces `stream_mode="custom"`.
The set is closed at the SDK boundary; `extensions["name"]` is the open-ended escape hatch for server-side transformers that emit on custom channels.
---
## 5. Dependencies
- `langchain-protocol>=0.0.15` — CDDL-derived `TypedDict`s for the protocol wire shape.
- `langchain-core>=1.4.0,<2` — required by the messages projection to construct `BaseMessage` subclasses. Matches the JS SDK's hard-dep stance.
- `httpx`, `orjson` — already required.
- Python 3.10+.
- Server: `langgraph-api` with `FF_V2_EVENT_STREAMING` enabled and ≥ 0.9.0rc1.