langgraph-stream-parser 0.1.7__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. langgraph_stream_parser-0.2.1/.github/workflows/ci.yml +37 -0
  2. langgraph_stream_parser-0.2.1/.github/workflows/release.yml +32 -0
  3. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/CHANGELOG.md +37 -0
  4. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/PKG-INFO +96 -10
  5. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/README.md +88 -7
  6. langgraph_stream_parser-0.2.1/assets/header.svg +87 -0
  7. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/pyproject.toml +13 -3
  8. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/__init__.py +17 -2
  9. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/adapters/__init__.py +3 -0
  10. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/adapters/base.py +10 -0
  11. langgraph_stream_parser-0.2.1/src/langgraph_stream_parser/adapters/session.py +256 -0
  12. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/compat.py +32 -0
  13. langgraph_stream_parser-0.2.1/src/langgraph_stream_parser/demo/__init__.py +15 -0
  14. langgraph_stream_parser-0.2.1/src/langgraph_stream_parser/demo/agent.py +102 -0
  15. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/events.py +201 -26
  16. langgraph_stream_parser-0.2.1/src/langgraph_stream_parser/extractors/__init__.py +29 -0
  17. langgraph_stream_parser-0.2.1/src/langgraph_stream_parser/extractors/builtins.py +384 -0
  18. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/extractors/messages.py +72 -5
  19. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/handlers/messages.py +29 -13
  20. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/handlers/updates.py +81 -6
  21. langgraph_stream_parser-0.2.1/src/langgraph_stream_parser/host/__init__.py +16 -0
  22. langgraph_stream_parser-0.2.1/src/langgraph_stream_parser/host/__main__.py +17 -0
  23. langgraph_stream_parser-0.2.1/src/langgraph_stream_parser/host/config.py +291 -0
  24. langgraph_stream_parser-0.2.1/src/langgraph_stream_parser/host/loader.py +83 -0
  25. langgraph_stream_parser-0.2.1/src/langgraph_stream_parser/host/workspace.py +42 -0
  26. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/parser.py +14 -7
  27. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/fixtures/mocks.py +102 -0
  28. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/test_compat.py +26 -0
  29. langgraph_stream_parser-0.2.1/tests/test_demo.py +36 -0
  30. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/test_events.py +63 -2
  31. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/test_extractors.py +166 -1
  32. langgraph_stream_parser-0.2.1/tests/test_generic_extractor.py +195 -0
  33. langgraph_stream_parser-0.2.1/tests/test_host.py +149 -0
  34. langgraph_stream_parser-0.2.1/tests/test_host_config.py +120 -0
  35. langgraph_stream_parser-0.2.1/tests/test_lc14_compat.py +99 -0
  36. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/test_parser.py +19 -18
  37. langgraph_stream_parser-0.2.1/tests/test_real_model.py +81 -0
  38. langgraph_stream_parser-0.2.1/tests/test_reasoning_display.py +205 -0
  39. langgraph_stream_parser-0.2.1/tests/test_session_adapter.py +287 -0
  40. langgraph_stream_parser-0.2.1/tests/test_wire_contract.py +140 -0
  41. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/uv.lock +77 -18
  42. langgraph_stream_parser-0.1.7/.claude/settings.local.json +0 -20
  43. langgraph_stream_parser-0.1.7/src/langgraph_stream_parser/extractors/__init__.py +0 -15
  44. langgraph_stream_parser-0.1.7/src/langgraph_stream_parser/extractors/builtins.py +0 -166
  45. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/.gitignore +0 -0
  46. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/LICENSE +0 -0
  47. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/examples/agent.py +0 -0
  48. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/examples/fastapi_websocket.py +0 -0
  49. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/examples/jupyter_example.ipynb +0 -0
  50. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/spec.md +0 -0
  51. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/adapters/cli.py +0 -0
  52. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/adapters/fastapi.py +0 -0
  53. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/adapters/jupyter.py +0 -0
  54. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/adapters/print.py +0 -0
  55. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/extractors/base.py +0 -0
  56. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/extractors/interrupts.py +0 -0
  57. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/handlers/__init__.py +0 -0
  58. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/src/langgraph_stream_parser/resume.py +0 -0
  59. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/__init__.py +0 -0
  60. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/fixtures/__init__.py +0 -0
  61. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/test_cli_adapter.py +0 -0
  62. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/test_dual_mode.py +0 -0
  63. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/test_fastapi_adapter.py +0 -0
  64. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/test_jupyter.py +0 -0
  65. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/test_print_adapter.py +0 -0
  66. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/test_resume.py +0 -0
  67. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/test_subagent.py +0 -0
  68. {langgraph_stream_parser-0.1.7 → langgraph_stream_parser-0.2.1}/tests/test_v2_stream.py +0 -0
@@ -0,0 +1,37 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest]
16
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
17
+ include:
18
+ # One Windows job for cross-platform coverage (primary dev env).
19
+ - os: windows-latest
20
+ python-version: "3.12"
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Set up Python ${{ matrix.python-version }}
26
+ uses: actions/setup-python@v5
27
+ with:
28
+ python-version: ${{ matrix.python-version }}
29
+
30
+ - name: Install uv
31
+ uses: astral-sh/setup-uv@v3
32
+
33
+ - name: Install
34
+ run: uv pip install --system -e ".[dev]"
35
+
36
+ - name: Test
37
+ run: pytest -ra --cov=langgraph_stream_parser --cov-report=term-missing
@@ -0,0 +1,32 @@
1
+ name: Release
2
+
3
+ # Mirrors the deepagent-hermes release.yml: tag-driven publish to PyPI.
4
+ # Tag any commit with `v*` (e.g. `v0.2.1`) and this builds + uploads.
5
+
6
+ on:
7
+ push:
8
+ tags:
9
+ - "v*"
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.13"
19
+ - name: Install build
20
+ run: pip install build
21
+ - name: Build wheel + sdist
22
+ # `python -m build` (no flag) runs sdist → wheel-from-sdist,
23
+ # matching the publish path local pre-flight checks should
24
+ # exercise. See deepagent-hermes v0.1.2 incident for why this
25
+ # matters: a wheel built directly with --wheel can ship fine
26
+ # while the wheel-from-sdist drops every Python file because
27
+ # of a restrictive [tool.hatch.build.targets.sdist] include.
28
+ run: python -m build
29
+ - name: Publish to PyPI
30
+ uses: pypa/gh-action-pypi-publish@release/v1
31
+ with:
32
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.9] - 2026-05-19
4
+
5
+ Compatibility refresh for **langgraph 1.2**, **langchain-core 1.4**, and **deepagents 0.6**.
6
+
7
+ ### Added
8
+ - `UsageEvent.cache_read_tokens` and `UsageEvent.cache_creation_tokens` — populated from `usage_metadata.input_token_details` (`cache_read`, `cache_creation`). Default 0; omitted from `to_dict()` when zero.
9
+ - `ContentEvent.is_subagent` and `ReasoningEvent.is_subagent` — set to `True` when stream metadata carries `ls_agent_type == "subagent"` (deepagents >= 0.6). Lets consumers distinguish subagent output even when `lc_agent_name` is absent.
10
+ - `"respond"` decision support in `InterruptEvent.build_decisions()` / `create_resume()`: pass `response="..."` to send a text reply in place of the tool call. Matches the deepagents 0.6 decision verb set.
11
+ - `InterruptEvent.build_decisions(..., use_edited_action=False)` escape hatch for runtimes that still expect the legacy `{"type": "edit", "args": ...}` resume shape.
12
+
13
+ ### Changed
14
+ - `extract_message_content()` now skips the full set of non-text content blocks defined by langchain-core 1.4 standard content blocks: `tool_call`, `tool_use`, `tool_call_chunk`, `server_tool_call`, `server_tool_call_chunk`, `server_tool_result`, `invalid_tool_call`, `image`, `audio`, `video`, `file` (plus the existing `reasoning` / `thinking`). Previously these could leak as stringified dicts into `ContentEvent.content`. Tool lifecycle and reasoning events are unchanged.
15
+ - `InterruptEvent.build_decisions("edit", ...)` now emits the modern `{"type": "edit", "edited_action": {"name", "args"}}` shape by default, matching LangGraph 1.1+ / deepagents 0.5+. Set `use_edited_action=False` for the legacy shape.
16
+ - `InterruptEvent.allowed_decisions` defaults to `{"approve", "reject", "edit", "respond"}` when no review configs are present (was `{"approve", "reject"}`).
17
+ - Dev dependency bumped: `langgraph>=1.1.0`, `langchain-core>=1.4.0`.
18
+
19
+ ### Notes
20
+ - **No breaking change for default tool extractors**: deepagents 0.6 ships new built-in tools (`glob_search`, `grep_search`, `execute`, `start_async_task` / `check_async_task` / `update_async_task` / `cancel_async_task` / `list_async_tasks`, plus QuickJS `CodeInterpreterMiddleware`). These flow through the regular `ToolCallStartEvent` / `ToolCallEndEvent` lifecycle — no parser change required.
21
+ - **v3 `stream_events` typed projections** (LangGraph 1.2 beta) are not yet supported. v2 `StreamPart` parsing remains the recommended path for `stream()` / `astream()`.
22
+
23
+ ## [0.1.8] - 2026-04-18
24
+
25
+ ### Added
26
+ - `ReasoningEvent` dataclass for reasoning / thinking content; emitted from langchain-core `reasoning` and `thinking` content blocks, and from `think_tool` reflections. Carries a `source` field (`"content_block"` or `"think_tool"`) so UIs can distinguish provenance.
27
+ - `DisplayEvent` dataclass for rich inline content (dataframes, images, plotly, html, json) from `display_inline`-style tools. Carries `display_type`, `data`, `title`, `status`, `error`, `tool_name`, `tool_call_id`, `node`, `namespace`.
28
+ - `extract_reasoning_content()` helper in `extractors.messages` for parsing reasoning blocks from `AIMessageChunk.content`.
29
+ - `UpdatesHandler._event_from_extraction()` routes extractor output to typed events; unknown `extracted_type` values still flow through `ToolExtractedEvent` for custom extractors.
30
+ - README sections: "Reasoning & Thinking" and "Rich Inline Display" with typed matching examples.
31
+
32
+ ### Changed
33
+ - `think_tool` output is now a `ReasoningEvent(source="think_tool")` instead of `ToolExtractedEvent(extracted_type="reflection")`. Legacy dict API (`stream_graph_updates`) still produces `{"chunk": text}` for backward compatibility.
34
+ - `display_inline` tool output is now a `DisplayEvent` instead of `ToolExtractedEvent(extracted_type="display_inline")`.
35
+ - `extract_message_content()` now skips reasoning blocks so they can be surfaced as `ReasoningEvent` separately.
36
+
37
+ ### Fixed
38
+ - Removed dead `has_messages` variable in `_parse_v2`.
39
+
3
40
  ## [0.1.7] - 2026-04-18
4
41
 
5
42
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-stream-parser
3
- Version: 0.1.7
3
+ Version: 0.2.1
4
4
  Summary: Universal parser for LangGraph streaming outputs
5
5
  Project-URL: Homepage, https://github.com/dkedar7/langgraph-stream-parser
6
6
  Project-URL: Documentation, https://github.com/dkedar7/langgraph-stream-parser#readme
@@ -20,11 +20,13 @@ Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Requires-Python: >=3.10
23
+ Provides-Extra: demo
24
+ Requires-Dist: deepagents>=0.3; extra == 'demo'
23
25
  Provides-Extra: dev
24
26
  Requires-Dist: fastapi>=0.100; extra == 'dev'
25
27
  Requires-Dist: httpx>=0.25; extra == 'dev'
26
- Requires-Dist: langchain-core>=0.2.0; extra == 'dev'
27
- Requires-Dist: langgraph>=0.2.0; extra == 'dev'
28
+ Requires-Dist: langchain-core>=1.4.0; extra == 'dev'
29
+ Requires-Dist: langgraph>=1.1.0; extra == 'dev'
28
30
  Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
29
31
  Requires-Dist: pytest-cov>=4.0; extra == 'dev'
30
32
  Requires-Dist: pytest>=7.0; extra == 'dev'
@@ -34,8 +36,15 @@ Requires-Dist: fastapi>=0.100; extra == 'fastapi'
34
36
  Provides-Extra: jupyter
35
37
  Requires-Dist: ipython>=8.0; extra == 'jupyter'
36
38
  Requires-Dist: rich>=13.0; extra == 'jupyter'
39
+ Provides-Extra: real
40
+ Requires-Dist: langchain-openai>=0.2; extra == 'real'
41
+ Requires-Dist: langgraph>=1.1.0; extra == 'real'
37
42
  Description-Content-Type: text/markdown
38
43
 
44
+ <p align="center">
45
+ <img src="assets/header.svg" alt="langgraph-stream-parser" width="100%">
46
+ </p>
47
+
39
48
  # langgraph-stream-parser
40
49
 
41
50
  Universal parser for LangGraph streaming outputs. Normalizes complex, variable output shapes from `graph.stream()` and `graph.astream()` into consistent, typed event objects.
@@ -81,13 +90,17 @@ for event in parser.parse(graph.stream(input_data, stream_mode="updates")):
81
90
  | Event | Description |
82
91
  |-------|-------------|
83
92
  | `ContentEvent` | Text content from AI messages. Includes `agent_name` when from a deep agent subagent. |
93
+ | `ReasoningEvent` | Reasoning / thinking text — from langchain-core `reasoning` content blocks or `think_tool` reflections |
84
94
  | `ToolCallStartEvent` | Tool call initiated by AI |
85
95
  | `ToolCallEndEvent` | Tool call completed with result |
86
- | `ToolExtractedEvent` | Special content extracted from tool (e.g., reflections, todos) |
96
+ | `ToolExtractedEvent` | Special content extracted from tool (e.g., todos, custom extractors) |
97
+ | `DisplayEvent` | Rich inline content (dataframe, image, plotly, html, json) from `display_inline`-style tools |
87
98
  | `InterruptEvent` | Human-in-the-loop interrupt requiring decision |
88
99
  | `StateUpdateEvent` | Non-message state updates (opt-in) |
89
- | `UsageEvent` | Token usage metadata (input/output/total tokens) |
100
+ | `UsageEvent` | Token usage metadata (input/output/total/cache_read/cache_creation tokens) |
90
101
  | `CustomEvent` | Custom data emitted via `get_stream_writer()` |
102
+ | `ValuesEvent` | Full state snapshot from `stream_mode="values"` (v2) |
103
+ | `DebugEvent` | Debug, checkpoint, or task trace from v2 streaming |
91
104
  | `CompleteEvent` | Stream finished successfully |
92
105
  | `ErrorEvent` | Error during streaming |
93
106
 
@@ -169,6 +182,22 @@ for event in parser.parse(graph.stream(input_data, config=config)):
169
182
  break
170
183
  ```
171
184
 
185
+ Supported decision types (deepagents 0.6+ / LangGraph 1.1+): `"approve"`, `"reject"`, `"edit"`, `"respond"`.
186
+
187
+ ```python
188
+ # Edit args before approval — emits the modern ``edited_action`` shape
189
+ resume = event.create_resume(
190
+ "edit",
191
+ args_modifier=lambda args: {**args, "safe": True},
192
+ )
193
+
194
+ # Reply with text in place of running the tool
195
+ resume = event.create_resume("respond", response="Please rephrase that.")
196
+
197
+ # For older LangGraph runtimes that expect ``{"type": "edit", "args": ...}``:
198
+ resume = event.create_resume("edit", args_modifier=fn, use_edited_action=False)
199
+ ```
200
+
172
201
  ### Custom Tool Extractors
173
202
 
174
203
  ```python
@@ -251,12 +280,14 @@ for event in parser.parse(stream):
251
280
  print(event.content, end="")
252
281
  ```
253
282
 
254
- For [LangChain deep agents](https://docs.langchain.com/oss/python/deepagents/subagents), `ContentEvent.agent_name` is extracted from `lc_agent_name` metadata:
283
+ For [LangChain deep agents](https://docs.langchain.com/oss/python/deepagents/subagents), `ContentEvent.agent_name` is extracted from `lc_agent_name` metadata, and `ContentEvent.is_subagent` is set to `True` when deepagents (>= 0.6) tags the run with `ls_agent_type="subagent"`. Match on either signal:
255
284
 
256
285
  ```python
257
- case ContentEvent(content=text, agent_name=name):
258
- label = f"[{name}] " if name else ""
286
+ case ContentEvent(content=text, agent_name=name, is_subagent=True):
287
+ label = f"[{name or 'subagent'}] "
259
288
  print(f"{label}{text}", end="")
289
+ case ContentEvent(content=text):
290
+ print(text, end="")
260
291
  ```
261
292
 
262
293
  ### Custom Stream Mode
@@ -272,6 +303,60 @@ for event in parser.parse(stream):
272
303
  print(f"Progress: {data}")
273
304
  ```
274
305
 
306
+ ### Reasoning & Thinking
307
+
308
+ Reasoning content arrives as a distinct `ReasoningEvent` so UIs can render it differently from the final answer (greyed out, collapsed, etc.). Two sources, same event type:
309
+
310
+ ```python
311
+ for event in parser.parse(stream):
312
+ match event:
313
+ case ReasoningEvent(content=text, source="content_block"):
314
+ # From langchain-core reasoning blocks (Anthropic thinking,
315
+ # OpenAI reasoning summaries). Streamed token-by-token.
316
+ print(f"\033[90m{text}\033[0m", end="") # grey
317
+ case ReasoningEvent(content=text, source="think_tool"):
318
+ # From the built-in think_tool ThinkToolExtractor.
319
+ print(f"💭 {text}")
320
+ case ContentEvent(content=text):
321
+ print(text, end="")
322
+ ```
323
+
324
+ ### Rich Inline Display
325
+
326
+ For agents that need to show DataFrames, charts, images, or HTML in the transcript, use a `display_inline`-style tool. The parser recognizes the convention and emits a typed `DisplayEvent` — no stringified dict hacks.
327
+
328
+ **Tool side** — return a JSON string with `display_type`, `data`, `title`, `status`:
329
+
330
+ ```python
331
+ def show_dataframe(df_name: str) -> str:
332
+ import json
333
+ df = load(df_name)
334
+ return json.dumps({
335
+ "type": "display_inline",
336
+ "display_type": "dataframe",
337
+ "title": df_name,
338
+ "data": df.to_html(), # or fig.to_json() for plotly,
339
+ "status": "success", # base64 PNG for matplotlib, etc.
340
+ })
341
+ ```
342
+
343
+ Configure your LangGraph tool registration with `name="display_inline"` (or register a custom `DisplayInlineExtractor` for a different name).
344
+
345
+ **Consumer side** — match on `DisplayEvent`:
346
+
347
+ ```python
348
+ for event in parser.parse(stream):
349
+ match event:
350
+ case DisplayEvent(display_type="dataframe", data=html, title=title):
351
+ ui.show_html(f"<h3>{title}</h3>{html}")
352
+ case DisplayEvent(display_type="plotly", data=plotly_json):
353
+ ui.show_plotly(json.loads(plotly_json))
354
+ case DisplayEvent(display_type=kind, data=data):
355
+ ui.show_generic(kind, data)
356
+ ```
357
+
358
+ The `display_type` field is consumer-defined — any string the tool and UI agree on. Common values: `"dataframe"`, `"image"`, `"plotly"`, `"html"`, `"json"`.
359
+
275
360
  ### Configuration Options
276
361
 
277
362
  ```python
@@ -453,16 +538,17 @@ The package includes extractors for common LangGraph tools:
453
538
 
454
539
  - **ThinkToolExtractor**: Extracts reflections from `think_tool`
455
540
  - **TodoExtractor**: Extracts todo lists from `write_todos`
541
+ - **DisplayInlineExtractor**: Extracts inline display artifacts from `display_inline`
456
542
 
457
543
  ## Examples
458
544
 
459
545
  ### FastAPI WebSocket Streaming
460
546
 
461
- See [examples/fastapi_websocket.py](examples/fastapi_websocket.py) for a complete example of streaming LangGraph events to a web client via WebSockets.
547
+ See [examples/fastapi_websocket.py](examples/fastapi_websocket.py) for a complete example using `FastAPIAdapter` to stream LangGraph events to a web client.
462
548
 
463
549
  ```bash
464
550
  # Install dependencies
465
- pip install fastapi uvicorn websockets
551
+ pip install 'langgraph-stream-parser[fastapi]' uvicorn
466
552
 
467
553
  # Run the example
468
554
  uvicorn examples.fastapi_websocket:app --reload
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="assets/header.svg" alt="langgraph-stream-parser" width="100%">
3
+ </p>
4
+
1
5
  # langgraph-stream-parser
2
6
 
3
7
  Universal parser for LangGraph streaming outputs. Normalizes complex, variable output shapes from `graph.stream()` and `graph.astream()` into consistent, typed event objects.
@@ -43,13 +47,17 @@ for event in parser.parse(graph.stream(input_data, stream_mode="updates")):
43
47
  | Event | Description |
44
48
  |-------|-------------|
45
49
  | `ContentEvent` | Text content from AI messages. Includes `agent_name` when from a deep agent subagent. |
50
+ | `ReasoningEvent` | Reasoning / thinking text — from langchain-core `reasoning` content blocks or `think_tool` reflections |
46
51
  | `ToolCallStartEvent` | Tool call initiated by AI |
47
52
  | `ToolCallEndEvent` | Tool call completed with result |
48
- | `ToolExtractedEvent` | Special content extracted from tool (e.g., reflections, todos) |
53
+ | `ToolExtractedEvent` | Special content extracted from tool (e.g., todos, custom extractors) |
54
+ | `DisplayEvent` | Rich inline content (dataframe, image, plotly, html, json) from `display_inline`-style tools |
49
55
  | `InterruptEvent` | Human-in-the-loop interrupt requiring decision |
50
56
  | `StateUpdateEvent` | Non-message state updates (opt-in) |
51
- | `UsageEvent` | Token usage metadata (input/output/total tokens) |
57
+ | `UsageEvent` | Token usage metadata (input/output/total/cache_read/cache_creation tokens) |
52
58
  | `CustomEvent` | Custom data emitted via `get_stream_writer()` |
59
+ | `ValuesEvent` | Full state snapshot from `stream_mode="values"` (v2) |
60
+ | `DebugEvent` | Debug, checkpoint, or task trace from v2 streaming |
53
61
  | `CompleteEvent` | Stream finished successfully |
54
62
  | `ErrorEvent` | Error during streaming |
55
63
 
@@ -131,6 +139,22 @@ for event in parser.parse(graph.stream(input_data, config=config)):
131
139
  break
132
140
  ```
133
141
 
142
+ Supported decision types (deepagents 0.6+ / LangGraph 1.1+): `"approve"`, `"reject"`, `"edit"`, `"respond"`.
143
+
144
+ ```python
145
+ # Edit args before approval — emits the modern ``edited_action`` shape
146
+ resume = event.create_resume(
147
+ "edit",
148
+ args_modifier=lambda args: {**args, "safe": True},
149
+ )
150
+
151
+ # Reply with text in place of running the tool
152
+ resume = event.create_resume("respond", response="Please rephrase that.")
153
+
154
+ # For older LangGraph runtimes that expect ``{"type": "edit", "args": ...}``:
155
+ resume = event.create_resume("edit", args_modifier=fn, use_edited_action=False)
156
+ ```
157
+
134
158
  ### Custom Tool Extractors
135
159
 
136
160
  ```python
@@ -213,12 +237,14 @@ for event in parser.parse(stream):
213
237
  print(event.content, end="")
214
238
  ```
215
239
 
216
- For [LangChain deep agents](https://docs.langchain.com/oss/python/deepagents/subagents), `ContentEvent.agent_name` is extracted from `lc_agent_name` metadata:
240
+ For [LangChain deep agents](https://docs.langchain.com/oss/python/deepagents/subagents), `ContentEvent.agent_name` is extracted from `lc_agent_name` metadata, and `ContentEvent.is_subagent` is set to `True` when deepagents (>= 0.6) tags the run with `ls_agent_type="subagent"`. Match on either signal:
217
241
 
218
242
  ```python
219
- case ContentEvent(content=text, agent_name=name):
220
- label = f"[{name}] " if name else ""
243
+ case ContentEvent(content=text, agent_name=name, is_subagent=True):
244
+ label = f"[{name or 'subagent'}] "
221
245
  print(f"{label}{text}", end="")
246
+ case ContentEvent(content=text):
247
+ print(text, end="")
222
248
  ```
223
249
 
224
250
  ### Custom Stream Mode
@@ -234,6 +260,60 @@ for event in parser.parse(stream):
234
260
  print(f"Progress: {data}")
235
261
  ```
236
262
 
263
+ ### Reasoning & Thinking
264
+
265
+ Reasoning content arrives as a distinct `ReasoningEvent` so UIs can render it differently from the final answer (greyed out, collapsed, etc.). Two sources, same event type:
266
+
267
+ ```python
268
+ for event in parser.parse(stream):
269
+ match event:
270
+ case ReasoningEvent(content=text, source="content_block"):
271
+ # From langchain-core reasoning blocks (Anthropic thinking,
272
+ # OpenAI reasoning summaries). Streamed token-by-token.
273
+ print(f"\033[90m{text}\033[0m", end="") # grey
274
+ case ReasoningEvent(content=text, source="think_tool"):
275
+ # From the built-in think_tool ThinkToolExtractor.
276
+ print(f"💭 {text}")
277
+ case ContentEvent(content=text):
278
+ print(text, end="")
279
+ ```
280
+
281
+ ### Rich Inline Display
282
+
283
+ For agents that need to show DataFrames, charts, images, or HTML in the transcript, use a `display_inline`-style tool. The parser recognizes the convention and emits a typed `DisplayEvent` — no stringified dict hacks.
284
+
285
+ **Tool side** — return a JSON string with `display_type`, `data`, `title`, `status`:
286
+
287
+ ```python
288
+ def show_dataframe(df_name: str) -> str:
289
+ import json
290
+ df = load(df_name)
291
+ return json.dumps({
292
+ "type": "display_inline",
293
+ "display_type": "dataframe",
294
+ "title": df_name,
295
+ "data": df.to_html(), # or fig.to_json() for plotly,
296
+ "status": "success", # base64 PNG for matplotlib, etc.
297
+ })
298
+ ```
299
+
300
+ Configure your LangGraph tool registration with `name="display_inline"` (or register a custom `DisplayInlineExtractor` for a different name).
301
+
302
+ **Consumer side** — match on `DisplayEvent`:
303
+
304
+ ```python
305
+ for event in parser.parse(stream):
306
+ match event:
307
+ case DisplayEvent(display_type="dataframe", data=html, title=title):
308
+ ui.show_html(f"<h3>{title}</h3>{html}")
309
+ case DisplayEvent(display_type="plotly", data=plotly_json):
310
+ ui.show_plotly(json.loads(plotly_json))
311
+ case DisplayEvent(display_type=kind, data=data):
312
+ ui.show_generic(kind, data)
313
+ ```
314
+
315
+ The `display_type` field is consumer-defined — any string the tool and UI agree on. Common values: `"dataframe"`, `"image"`, `"plotly"`, `"html"`, `"json"`.
316
+
237
317
  ### Configuration Options
238
318
 
239
319
  ```python
@@ -415,16 +495,17 @@ The package includes extractors for common LangGraph tools:
415
495
 
416
496
  - **ThinkToolExtractor**: Extracts reflections from `think_tool`
417
497
  - **TodoExtractor**: Extracts todo lists from `write_todos`
498
+ - **DisplayInlineExtractor**: Extracts inline display artifacts from `display_inline`
418
499
 
419
500
  ## Examples
420
501
 
421
502
  ### FastAPI WebSocket Streaming
422
503
 
423
- See [examples/fastapi_websocket.py](examples/fastapi_websocket.py) for a complete example of streaming LangGraph events to a web client via WebSockets.
504
+ See [examples/fastapi_websocket.py](examples/fastapi_websocket.py) for a complete example using `FastAPIAdapter` to stream LangGraph events to a web client.
424
505
 
425
506
  ```bash
426
507
  # Install dependencies
427
- pip install fastapi uvicorn websockets
508
+ pip install 'langgraph-stream-parser[fastapi]' uvicorn
428
509
 
429
510
  # Run the example
430
511
  uvicorn examples.fastapi_websocket:app --reload
@@ -0,0 +1,87 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1280" height="340" viewBox="0 0 1280 340" role="img" aria-label="langgraph-stream-parser — typed events and adapters for LangGraph streaming output">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#0B1221"/>
5
+ <stop offset="1" stop-color="#121226"/>
6
+ </linearGradient>
7
+ <linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
8
+ <stop offset="0" stop-color="#7C5CFF"/>
9
+ <stop offset="1" stop-color="#34D6C8"/>
10
+ </linearGradient>
11
+ <radialGradient id="glow" cx="0.82" cy="0.15" r="0.6">
12
+ <stop offset="0" stop-color="#7C5CFF" stop-opacity="0.22"/>
13
+ <stop offset="1" stop-color="#7C5CFF" stop-opacity="0"/>
14
+ </radialGradient>
15
+ </defs>
16
+
17
+ <rect width="1280" height="340" rx="20" fill="url(#bg)"/>
18
+ <rect width="1280" height="340" rx="20" fill="url(#glow)"/>
19
+ <rect x="1" y="1" width="1278" height="338" rx="19" fill="none" stroke="#26284A" stroke-width="1.5"/>
20
+
21
+ <!-- ===== left: identity ===== -->
22
+ <g transform="translate(72,64)">
23
+ <rect width="92" height="34" rx="17" fill="#1A1535" stroke="url(#accent)" stroke-width="1.5"/>
24
+ <circle cx="22" cy="17" r="5" fill="url(#accent)"/>
25
+ <text x="38" y="23" font-family="'SF Mono','JetBrains Mono','Consolas',monospace" font-size="15" fill="#C9BEFF">v0.2</text>
26
+ </g>
27
+
28
+ <text x="70" y="176" font-family="'SF Mono','JetBrains Mono','Consolas',monospace" font-size="50" font-weight="700" fill="#F2F6FC" letter-spacing="-1">langgraph<tspan fill="url(#accent)">-stream-parser</tspan></text>
29
+
30
+ <text x="72" y="220" font-family="'Segoe UI',Helvetica,Arial,sans-serif" font-size="22" fill="#A6AECB">Typed events &#38; adapters for LangGraph streaming output.</text>
31
+ <text x="72" y="250" font-family="'Segoe UI',Helvetica,Arial,sans-serif" font-size="22" fill="#A6AECB">The shared runtime behind the deep-agent surfaces.</text>
32
+
33
+ <g font-family="'SF Mono','JetBrains Mono','Consolas',monospace" font-size="13" fill="#7C7FA6">
34
+ <text x="72" y="298">StreamParser</text>
35
+ <text x="196" y="298" fill="#45487A">&#8226;</text>
36
+ <text x="216" y="298">extractors</text>
37
+ <text x="312" y="298" fill="#45487A">&#8226;</text>
38
+ <text x="332" y="298">adapters</text>
39
+ <text x="420" y="298" fill="#45487A">&#8226;</text>
40
+ <text x="440" y="298">host &#183; demo</text>
41
+ </g>
42
+
43
+ <!-- ===== right: stream -> typed events ===== -->
44
+ <g transform="translate(840,86)">
45
+ <!-- source: graph.stream() -->
46
+ <rect width="150" height="40" rx="10" fill="#161433" stroke="#3A2F66" stroke-width="1.5"/>
47
+ <text x="18" y="25" font-family="'SF Mono','Consolas',monospace" font-size="13" fill="#9FB0CC">graph.stream()</text>
48
+
49
+ <!-- arrow -->
50
+ <line x1="150" y1="20" x2="196" y2="20" stroke="url(#accent)" stroke-width="2"/>
51
+ <path d="M196 20 l-9 -5 v10 z" fill="#34D6C8"/>
52
+
53
+ <!-- parser node -->
54
+ <rect x="196" y="2" width="118" height="36" rx="10" fill="#1A1535" stroke="url(#accent)" stroke-width="1.5"/>
55
+ <circle cx="216" cy="20" r="5" fill="url(#accent)"/>
56
+ <text x="230" y="25" font-family="'SF Mono','Consolas',monospace" font-size="12" fill="#C9BEFF">parse()</text>
57
+
58
+ <!-- typed event chips -->
59
+ <g font-family="'SF Mono','Consolas',monospace" font-size="12">
60
+ <g transform="translate(40,72)">
61
+ <rect width="130" height="26" rx="6" fill="#10243A" stroke="#2F9BFF" stroke-width="1"/>
62
+ <circle cx="15" cy="13" r="4" fill="#2F9BFF"/>
63
+ <text x="28" y="17" fill="#8FC4FF">ContentEvent</text>
64
+ </g>
65
+ <g transform="translate(190,72)">
66
+ <rect width="150" height="26" rx="6" fill="#0C2A22" stroke="#34D6C8" stroke-width="1"/>
67
+ <circle cx="15" cy="13" r="4" fill="#34D6C8"/>
68
+ <text x="28" y="17" fill="#7FE3D6">ToolCallStart</text>
69
+ </g>
70
+ <g transform="translate(40,108)">
71
+ <rect width="150" height="26" rx="6" fill="#0C2A22" stroke="#34D6C8" stroke-width="1"/>
72
+ <circle cx="15" cy="13" r="4" fill="#34D6C8"/>
73
+ <text x="28" y="17" fill="#7FE3D6">ToolCallEnd</text>
74
+ </g>
75
+ <g transform="translate(210,108)">
76
+ <rect width="130" height="26" rx="6" fill="#241A33" stroke="#7C5CFF" stroke-width="1"/>
77
+ <circle cx="15" cy="13" r="4" fill="#7C5CFF"/>
78
+ <text x="28" y="17" fill="#C9BEFF">Interrupt</text>
79
+ </g>
80
+ <g transform="translate(40,144)">
81
+ <rect width="180" height="26" rx="6" fill="#10243A" stroke="#2F9BFF" stroke-width="1"/>
82
+ <circle cx="15" cy="13" r="4" fill="#2F9BFF"/>
83
+ <text x="28" y="17" fill="#8FC4FF">Reasoning &#183; Display &#8230;</text>
84
+ </g>
85
+ </g>
86
+ </g>
87
+ </svg>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "langgraph-stream-parser"
3
- version = "0.1.7"
3
+ version = "0.2.1"
4
4
  description = "Universal parser for LangGraph streaming outputs"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -38,12 +38,19 @@ jupyter = [
38
38
  fastapi = [
39
39
  "fastapi>=0.100",
40
40
  ]
41
+ demo = [
42
+ "deepagents>=0.3",
43
+ ]
44
+ real = [
45
+ "langchain-openai>=0.2",
46
+ "langgraph>=1.1.0",
47
+ ]
41
48
  dev = [
42
49
  "pytest>=7.0",
43
50
  "pytest-asyncio>=0.21",
44
51
  "pytest-cov>=4.0",
45
- "langgraph>=0.2.0",
46
- "langchain-core>=0.2.0",
52
+ "langgraph>=1.1.0",
53
+ "langchain-core>=1.4.0",
47
54
  "rich>=13.0",
48
55
  "fastapi>=0.100",
49
56
  "httpx>=0.25",
@@ -65,6 +72,9 @@ packages = ["src/langgraph_stream_parser"]
65
72
  [tool.pytest.ini_options]
66
73
  asyncio_mode = "auto"
67
74
  testpaths = ["tests"]
75
+ markers = [
76
+ "real_model: hits a live LLM via OpenRouter (opt-in; skips without OPENROUTER_API_KEY)",
77
+ ]
68
78
 
69
79
  [tool.coverage.run]
70
80
  source = ["src/langgraph_stream_parser"]
@@ -32,9 +32,11 @@ Legacy Dict-Based API:
32
32
  from .parser import StreamParser
33
33
  from .events import (
34
34
  ContentEvent,
35
+ ReasoningEvent,
35
36
  ToolCallStartEvent,
36
37
  ToolCallEndEvent,
37
38
  ToolExtractedEvent,
39
+ DisplayEvent,
38
40
  InterruptEvent,
39
41
  StateUpdateEvent,
40
42
  UsageEvent,
@@ -47,8 +49,14 @@ from .events import (
47
49
  event_to_dict,
48
50
  )
49
51
  from .extractors.base import ToolExtractor
50
- from .extractors.builtins import ThinkToolExtractor, TodoExtractor, DisplayInlineExtractor
52
+ from .extractors.builtins import (
53
+ DisplayInlineExtractor,
54
+ GenericToolExtractor,
55
+ ThinkToolExtractor,
56
+ TodoExtractor,
57
+ )
51
58
  from .resume import create_resume_input, prepare_agent_input
59
+ from .host import load_agent_spec, HostConfig, Workspace
52
60
  from .compat import (
53
61
  stream_graph_updates,
54
62
  astream_graph_updates,
@@ -56,16 +64,18 @@ from .compat import (
56
64
  aresume_graph_from_interrupt,
57
65
  )
58
66
 
59
- __version__ = "0.1.7"
67
+ __version__ = "0.2.1"
60
68
 
61
69
  __all__ = [
62
70
  # Main parser
63
71
  "StreamParser",
64
72
  # Event types
65
73
  "ContentEvent",
74
+ "ReasoningEvent",
66
75
  "ToolCallStartEvent",
67
76
  "ToolCallEndEvent",
68
77
  "ToolExtractedEvent",
78
+ "DisplayEvent",
69
79
  "InterruptEvent",
70
80
  "StateUpdateEvent",
71
81
  "UsageEvent",
@@ -80,9 +90,14 @@ __all__ = [
80
90
  "ThinkToolExtractor",
81
91
  "TodoExtractor",
82
92
  "DisplayInlineExtractor",
93
+ "GenericToolExtractor",
83
94
  # Resume utilities
84
95
  "create_resume_input",
85
96
  "prepare_agent_input",
97
+ # Host conventions
98
+ "load_agent_spec",
99
+ "HostConfig",
100
+ "Workspace",
86
101
  # Serialization
87
102
  "event_to_dict",
88
103
  # Legacy/compat functions
@@ -4,6 +4,7 @@ from .base import BaseAdapter, ToolStatus, ToolState
4
4
  from .print import PrintAdapter
5
5
  from .cli import CLIAdapter
6
6
  from .fastapi import FastAPIAdapter
7
+ from .session import SessionAdapter, Session
7
8
 
8
9
  __all__ = [
9
10
  "BaseAdapter",
@@ -12,4 +13,6 @@ __all__ = [
12
13
  "PrintAdapter",
13
14
  "CLIAdapter",
14
15
  "FastAPIAdapter",
16
+ "SessionAdapter",
17
+ "Session",
15
18
  ]