langgraph-stream-parser 0.2.1__tar.gz → 0.3.0__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 (69) hide show
  1. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/CHANGELOG.md +43 -0
  2. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/PKG-INFO +22 -1
  3. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/README.md +21 -0
  4. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/pyproject.toml +1 -1
  5. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/demo/__init__.py +7 -1
  6. langgraph_stream_parser-0.3.0/src/langgraph_stream_parser/demo/stub.py +139 -0
  7. langgraph_stream_parser-0.3.0/src/langgraph_stream_parser/host/__init__.py +17 -0
  8. langgraph_stream_parser-0.3.0/src/langgraph_stream_parser/host/__main__.py +18 -0
  9. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/host/config.py +99 -33
  10. langgraph_stream_parser-0.3.0/tests/test_demo_stub.py +65 -0
  11. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_host.py +1 -1
  12. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_host_config.py +79 -0
  13. langgraph_stream_parser-0.2.1/src/langgraph_stream_parser/host/__init__.py +0 -16
  14. langgraph_stream_parser-0.2.1/src/langgraph_stream_parser/host/__main__.py +0 -17
  15. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/.github/workflows/ci.yml +0 -0
  16. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/.github/workflows/release.yml +0 -0
  17. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/.gitignore +0 -0
  18. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/LICENSE +0 -0
  19. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/assets/header.svg +0 -0
  20. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/examples/agent.py +0 -0
  21. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/examples/fastapi_websocket.py +0 -0
  22. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/examples/jupyter_example.ipynb +0 -0
  23. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/spec.md +0 -0
  24. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/__init__.py +0 -0
  25. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/adapters/__init__.py +0 -0
  26. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/adapters/base.py +0 -0
  27. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/adapters/cli.py +0 -0
  28. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/adapters/fastapi.py +0 -0
  29. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/adapters/jupyter.py +0 -0
  30. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/adapters/print.py +0 -0
  31. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/adapters/session.py +0 -0
  32. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/compat.py +0 -0
  33. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/demo/agent.py +0 -0
  34. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/events.py +0 -0
  35. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/extractors/__init__.py +0 -0
  36. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/extractors/base.py +0 -0
  37. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/extractors/builtins.py +0 -0
  38. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/extractors/interrupts.py +0 -0
  39. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/extractors/messages.py +0 -0
  40. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/handlers/__init__.py +0 -0
  41. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/handlers/messages.py +0 -0
  42. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/handlers/updates.py +0 -0
  43. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/host/loader.py +0 -0
  44. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/host/workspace.py +0 -0
  45. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/parser.py +0 -0
  46. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/src/langgraph_stream_parser/resume.py +0 -0
  47. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/__init__.py +0 -0
  48. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/fixtures/__init__.py +0 -0
  49. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/fixtures/mocks.py +0 -0
  50. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_cli_adapter.py +0 -0
  51. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_compat.py +0 -0
  52. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_demo.py +0 -0
  53. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_dual_mode.py +0 -0
  54. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_events.py +0 -0
  55. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_extractors.py +0 -0
  56. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_fastapi_adapter.py +0 -0
  57. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_generic_extractor.py +0 -0
  58. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_jupyter.py +0 -0
  59. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_lc14_compat.py +0 -0
  60. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_parser.py +0 -0
  61. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_print_adapter.py +0 -0
  62. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_real_model.py +0 -0
  63. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_reasoning_display.py +0 -0
  64. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_resume.py +0 -0
  65. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_session_adapter.py +0 -0
  66. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_subagent.py +0 -0
  67. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_v2_stream.py +0 -0
  68. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/tests/test_wire_contract.py +0 -0
  69. {langgraph_stream_parser-0.2.1 → langgraph_stream_parser-0.3.0}/uv.lock +0 -0
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] - 2026-06-10
4
+
5
+ The host family is renamed to **LangStage** ("every stage for your LangGraph agent"); this package keeps its name as the shared core.
6
+
7
+ ### Added
8
+ - **`LANGSTAGE_*` is the canonical config vocabulary**: `LANGSTAGE_AGENT_SPEC` etc. env vars, project `langstage.toml`, global `~/.langstage/config.toml`, and `LANGSTAGE_CONFIG_HOME`. The legacy names (`DEEPAGENT_*`, `deepagents.toml`, `~/.deepagents/config.toml`, `DEEPAGENTS_CONFIG_HOME`) still resolve everywhere as deprecated fallbacks — canonical wins when both are set; legacy env use emits a once-per-var `DeprecationWarning`. Moving the global config out of `~/.deepagents/` also exits the schema collision with LangChain's dcode, which owns that directory.
9
+ - Host subclasses may declare either spelling in their `_ENV` maps; both names resolve (`_env_pair` derivation), so downstream hosts need no immediate change.
10
+
11
+ ### Changed
12
+ - `HostConfig.title` default: `"Deep Agent"` → `"LangStage"`.
13
+ - `describe()` shows both vocabularies per field (`env: LANGSTAGE_X (legacy DEEPAGENT_X)`).
14
+ - README family table updated to the LangStage package names.
15
+
16
+ ## [0.2.2] - 2026-06-10
17
+
18
+ ### Added
19
+ - `demo.create_stub_agent()` / `langgraph_stream_parser.demo.stub:graph` — the keyless, deterministic echo agent behind every surface's `--demo` mode. Lazy imports; base install stays dependency-free.
20
+
21
+ ## [0.2.1] - 2026-06-08
22
+
23
+ ### Added
24
+ - `GenericToolExtractor` + `default_extractor` fallback for unknown tools (#16).
25
+ - Tag-driven Release workflow (#17).
26
+
27
+ ## [0.2.0] - 2026-06-02
28
+
29
+ Repositions langgraph-stream-parser as the **shared runtime substrate** for the deep-agent host family (`cowork-dash`, `deepagent-lab`, `deepagent-code`, `deepagent-vscode`).
30
+
31
+ ### Added
32
+ - **`host/` submodule** — shared host conventions:
33
+ - `load_agent_spec("path.py:var" | "module:var")` — strict agent-spec loader (the `:object` suffix is required).
34
+ - `HostConfig` — layered config resolver: `defaults < deepagents.toml < DEEPAGENT_* env < overrides`, with per-field source tracking. Subclass and extend the `_ENV` / `_TOML` maps (merged across the MRO) to add host-specific keys. `DEEPAGENT_AGENT_SPEC` is the canonical agent-spec env var.
35
+ - `load_toml_config()` — loads + deep-merges global `~/.deepagents/config.toml` (override dir via `DEEPAGENTS_CONFIG_HOME`) and the nearest project `deepagents.toml`.
36
+ - `Workspace` — workspace-root wrapper with traversal-safe `subpath()`.
37
+ - `python -m langgraph_stream_parser.host` (and `HostConfig.describe()`) — prints each resolved value, its source, and the env var / TOML key that sets it.
38
+ - **`adapters.SessionAdapter`** — session-scoped streaming for web hosts: per-session event queue, cancellation, side-channel `push_event()`, and persistent SSE that survives client reconnects.
39
+ - **`demo.create_default_agent()`** — shared filesystem-backed default agent factory (behind the `[demo]` extra; lazy-imports `deepagents`).
40
+ - **Four built-in extractors** for the agentskills.io / Hermes pattern, wired into the default set so every host gets them through `compat`: `SkillManageExtractor` (`skill_manage`), `SkillViewExtractor` (`skill_view`), `CompressionExtractor` (`__compression__`), `MemoryExtractor` (`memory`).
41
+ - `event_to_dict(event, *, max_result_len=...)` — lets hosts drop bespoke serializer shims.
42
+
43
+ ### Fixed
44
+ - `skip_tools` previously suppressed a tool's **extractor** as well as its lifecycle events, silently dropping `todo_list` / `reflection`. Extractors now run for skipped tools; only the lifecycle (start/end) events are suppressed.
45
+
3
46
  ## [0.1.9] - 2026-05-19
4
47
 
5
48
  Compatibility refresh for **langgraph 1.2**, **langchain-core 1.4**, and **deepagents 0.6**.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-stream-parser
3
- Version: 0.2.1
3
+ Version: 0.3.0
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
@@ -49,6 +49,27 @@ Description-Content-Type: text/markdown
49
49
 
50
50
  Universal parser for LangGraph streaming outputs. Normalizes complex, variable output shapes from `graph.stream()` and `graph.astream()` into consistent, typed event objects.
51
51
 
52
+ ## Every stage for your LangGraph agent
53
+
54
+ `langgraph-stream-parser` is the shared core of the **[LangStage](https://github.com/dkedar7/langstage) family**: write your agent once — any LangGraph `CompiledGraph` — and run it on every stage with the same spec string (`module:attr` or `path/to/file.py:attr`), the same `langstage.toml` config file, and the same `LANGSTAGE_*` environment variables. (The pre-rename `deepagents.toml` / `DEEPAGENT_*` vocabulary still resolves as a deprecated fallback.)
55
+
56
+ | Stage | Package | Try it |
57
+ |---|---|---|
58
+ | Web app | [langstage](https://github.com/dkedar7/langstage) | `langstage run --agent my_agent.py:graph` |
59
+ | JupyterLab | [langstage-jupyter](https://github.com/dkedar7/langstage-jupyter) | `pip install langstage-jupyter`, then the chat sidebar in `jupyter lab` |
60
+ | Terminal | [langstage-cli](https://github.com/dkedar7/langstage-cli) | `langstage-cli -a my_agent.py:graph` |
61
+ | VS Code | [langstage-vscode](https://github.com/dkedar7/langstage-vscode) | chat participant + stdio sidecar |
62
+ | Reference agent | [langstage-hermes](https://github.com/dkedar7/langstage-hermes) | `LANGSTAGE_AGENT_SPEC=langstage_hermes.agent:graph` on any stage |
63
+ | Shared core | langgraph-stream-parser | **you are here** |
64
+
65
+ No agent yet? Every stage has a keyless demo mode backed by this package's stub agent — no API key required:
66
+
67
+ ```bash
68
+ export LANGSTAGE_AGENT_SPEC=langgraph_stream_parser.demo.stub:graph # or each CLI's --demo flag
69
+ ```
70
+
71
+ And the resolved configuration (each value, its source, and the env var / `langstage.toml` key that sets it) is printable everywhere: `python -m langgraph_stream_parser.host`, or each CLI's `--show-config`.
72
+
52
73
  ## Installation
53
74
 
54
75
  ```bash
@@ -6,6 +6,27 @@
6
6
 
7
7
  Universal parser for LangGraph streaming outputs. Normalizes complex, variable output shapes from `graph.stream()` and `graph.astream()` into consistent, typed event objects.
8
8
 
9
+ ## Every stage for your LangGraph agent
10
+
11
+ `langgraph-stream-parser` is the shared core of the **[LangStage](https://github.com/dkedar7/langstage) family**: write your agent once — any LangGraph `CompiledGraph` — and run it on every stage with the same spec string (`module:attr` or `path/to/file.py:attr`), the same `langstage.toml` config file, and the same `LANGSTAGE_*` environment variables. (The pre-rename `deepagents.toml` / `DEEPAGENT_*` vocabulary still resolves as a deprecated fallback.)
12
+
13
+ | Stage | Package | Try it |
14
+ |---|---|---|
15
+ | Web app | [langstage](https://github.com/dkedar7/langstage) | `langstage run --agent my_agent.py:graph` |
16
+ | JupyterLab | [langstage-jupyter](https://github.com/dkedar7/langstage-jupyter) | `pip install langstage-jupyter`, then the chat sidebar in `jupyter lab` |
17
+ | Terminal | [langstage-cli](https://github.com/dkedar7/langstage-cli) | `langstage-cli -a my_agent.py:graph` |
18
+ | VS Code | [langstage-vscode](https://github.com/dkedar7/langstage-vscode) | chat participant + stdio sidecar |
19
+ | Reference agent | [langstage-hermes](https://github.com/dkedar7/langstage-hermes) | `LANGSTAGE_AGENT_SPEC=langstage_hermes.agent:graph` on any stage |
20
+ | Shared core | langgraph-stream-parser | **you are here** |
21
+
22
+ No agent yet? Every stage has a keyless demo mode backed by this package's stub agent — no API key required:
23
+
24
+ ```bash
25
+ export LANGSTAGE_AGENT_SPEC=langgraph_stream_parser.demo.stub:graph # or each CLI's --demo flag
26
+ ```
27
+
28
+ And the resolved configuration (each value, its source, and the env var / `langstage.toml` key that sets it) is printable everywhere: `python -m langgraph_stream_parser.host`, or each CLI's `--show-config`.
29
+
9
30
  ## Installation
10
31
 
11
32
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "langgraph-stream-parser"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "Universal parser for LangGraph streaming outputs"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -9,7 +9,13 @@ everything.
9
9
  Requires the optional ``deepagents`` dependency:
10
10
 
11
11
  pip install langgraph-stream-parser[demo]
12
+
13
+ This package also ships the keyless **stub agent** behind every surface's
14
+ ``--demo`` mode (:func:`create_stub_agent` / spec
15
+ ``langgraph_stream_parser.demo.stub:graph``) — that one needs no extra and no
16
+ API key.
12
17
  """
13
18
  from .agent import create_default_agent
19
+ from .stub import create_stub_agent
14
20
 
15
- __all__ = ["create_default_agent"]
21
+ __all__ = ["create_default_agent", "create_stub_agent"]
@@ -0,0 +1,139 @@
1
+ """A keyless, deterministic stub agent — the engine behind every ``--demo`` mode.
2
+
3
+ Every LangStage surface (langstage, langstage-cli, langstage-jupyter,
4
+ langstage-vscode) offers a demo mode so a new user can see the surface working
5
+ before configuring a real agent or any API key. This module is the single
6
+ shared implementation: a real compiled LangGraph graph with a checkpointer,
7
+ streaming token-by-token through the exact same parser path as a production
8
+ agent — but the "model" is a local echo, so it needs no network and no keys.
9
+
10
+ Point any surface at it with the standard spec string::
11
+
12
+ LANGSTAGE_AGENT_SPEC=langgraph_stream_parser.demo.stub:graph
13
+
14
+ or build a customized one in code::
15
+
16
+ from langgraph_stream_parser.demo import create_stub_agent
17
+ agent = create_stub_agent(name="My Demo")
18
+
19
+ ``langgraph`` and ``langchain-core`` are imported lazily — importing this
20
+ module stays dependency-free; only building the agent requires them (every
21
+ host surface already depends on both).
22
+ """
23
+ from __future__ import annotations
24
+
25
+ from typing import Any
26
+
27
+ DEFAULT_REPLY_PREFIX = "(demo agent) You said: "
28
+ DEFAULT_NAME = "Demo Agent"
29
+
30
+ _GRAPH_CACHE: Any = None
31
+
32
+
33
+ def create_stub_agent(
34
+ *,
35
+ name: str = DEFAULT_NAME,
36
+ reply_prefix: str = DEFAULT_REPLY_PREFIX,
37
+ checkpointer: Any = None,
38
+ ):
39
+ """Build the echo stub agent.
40
+
41
+ Args:
42
+ name: Display name surfaced in host UIs.
43
+ reply_prefix: Prepended to the echoed user message in every reply.
44
+ checkpointer: LangGraph checkpointer. Defaults to an in-memory saver
45
+ so multi-turn threads work out of the box.
46
+
47
+ Returns:
48
+ A compiled LangGraph graph that replies to each user message with
49
+ ``reply_prefix + <last human message>``, streamed token-by-token.
50
+
51
+ Raises:
52
+ RuntimeError: If ``langgraph`` / ``langchain-core`` are not installed.
53
+ """
54
+ try:
55
+ from langchain_core.callbacks import CallbackManagerForLLMRun
56
+ from langchain_core.language_models import BaseChatModel
57
+ from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage
58
+ from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
59
+ from langgraph.checkpoint.memory import MemorySaver
60
+ from langgraph.graph import END, START, MessagesState, StateGraph
61
+ except ImportError as e: # pragma: no cover — exercised only without the deps
62
+ raise RuntimeError(
63
+ "The stub agent needs langgraph + langchain-core "
64
+ "(every deep-agent surface already installs them): "
65
+ f"{e}"
66
+ ) from e
67
+
68
+ from typing import Iterator, List, Optional
69
+
70
+ def _last_human(messages: List[BaseMessage]) -> str:
71
+ for message in reversed(messages):
72
+ if getattr(message, "type", None) == "human":
73
+ content = message.content
74
+ return content if isinstance(content, str) else str(content)
75
+ return ""
76
+
77
+ class EchoChatModel(BaseChatModel):
78
+ """A no-API chat model that echoes the user's last message.
79
+
80
+ Implements ``_stream`` so that under LangGraph's ``messages`` stream
81
+ mode the reply is emitted token-by-token, matching how a real chat
82
+ model behaves through the parser.
83
+ """
84
+
85
+ @property
86
+ def _llm_type(self) -> str:
87
+ return "echo-stub"
88
+
89
+ def _generate(
90
+ self,
91
+ messages: List[BaseMessage],
92
+ stop: Optional[List[str]] = None,
93
+ run_manager: Optional[CallbackManagerForLLMRun] = None,
94
+ **kwargs: Any,
95
+ ) -> ChatResult:
96
+ text = reply_prefix + _last_human(messages)
97
+ return ChatResult(generations=[ChatGeneration(message=AIMessage(content=text))])
98
+
99
+ def _stream(
100
+ self,
101
+ messages: List[BaseMessage],
102
+ stop: Optional[List[str]] = None,
103
+ run_manager: Optional[CallbackManagerForLLMRun] = None,
104
+ **kwargs: Any,
105
+ ) -> Iterator[ChatGenerationChunk]:
106
+ text = reply_prefix + _last_human(messages)
107
+ tokens = text.split(" ")
108
+ for i, token in enumerate(tokens):
109
+ piece = token if i == len(tokens) - 1 else token + " "
110
+ chunk = ChatGenerationChunk(message=AIMessageChunk(content=piece))
111
+ if run_manager is not None:
112
+ run_manager.on_llm_new_token(piece, chunk=chunk)
113
+ yield chunk
114
+
115
+ model = EchoChatModel()
116
+
117
+ def _respond(state: MessagesState) -> dict:
118
+ return {"messages": [model.invoke(state["messages"])]}
119
+
120
+ builder = StateGraph(MessagesState)
121
+ builder.add_node("respond", _respond)
122
+ builder.add_edge(START, "respond")
123
+ builder.add_edge("respond", END)
124
+
125
+ graph = builder.compile(checkpointer=checkpointer or MemorySaver())
126
+ graph.name = name
127
+ return graph
128
+
129
+
130
+ def __getattr__(attr: str) -> Any:
131
+ # ``graph`` is built lazily on first access so plain imports of this module
132
+ # never require langgraph — but ``load_agent_spec("...demo.stub:graph")``
133
+ # gets a ready compiled agent.
134
+ if attr == "graph":
135
+ global _GRAPH_CACHE
136
+ if _GRAPH_CACHE is None:
137
+ _GRAPH_CACHE = create_stub_agent()
138
+ return _GRAPH_CACHE
139
+ raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")
@@ -0,0 +1,17 @@
1
+ """Host conventions shared across the LangStage surfaces.
2
+
3
+ Agent-spec loading, the shared ``LANGSTAGE_*`` config schema (legacy
4
+ ``DEEPAGENT_*`` still resolves), and a workspace wrapper — the plumbing every
5
+ host (``langstage``, ``langstage-jupyter``, ``langstage-cli``,
6
+ ``langstage-vscode``) needs but used to reimplement.
7
+ """
8
+ from .config import HostConfig, load_toml_config
9
+ from .loader import load_agent_spec
10
+ from .workspace import Workspace
11
+
12
+ __all__ = [
13
+ "load_agent_spec",
14
+ "HostConfig",
15
+ "load_toml_config",
16
+ "Workspace",
17
+ ]
@@ -0,0 +1,18 @@
1
+ """``python -m langgraph_stream_parser.host`` — print the resolved shared config.
2
+
3
+ Shows each ``LANGSTAGE_*`` value (legacy ``DEEPAGENT_*`` names still resolve),
4
+ where it resolved from (default / TOML / env / override), and the env var +
5
+ ``langstage.toml`` key that set it — so you never have to remember the
6
+ variable names. Hosts can ship their own subclass printer for host-specific
7
+ keys; this covers the shared core.
8
+ """
9
+ from .config import HostConfig
10
+
11
+
12
+ def main() -> int:
13
+ print(HostConfig.resolve().describe())
14
+ return 0
15
+
16
+
17
+ if __name__ == "__main__":
18
+ raise SystemExit(main())
@@ -1,21 +1,30 @@
1
- """Shared ``DEEPAGENT_*`` configuration for hosts.
1
+ """Shared ``LANGSTAGE_*`` configuration for hosts.
2
2
 
3
3
  ``HostConfig`` holds the keys every host has in common (agent spec, workspace
4
4
  root, bind/title basics) and resolves them from one layered chain:
5
5
 
6
- defaults < deepagents.toml < DEEPAGENT_* env vars < explicit overrides
6
+ defaults < langstage.toml < LANGSTAGE_* env vars < explicit overrides
7
7
 
8
8
  Host-specific keys (theme, auth, model, Jupyter token, ...) belong in each
9
9
  host's own subclass — drift *below* the shared core is fine — but every host
10
10
  gets the same resolution order, the same TOML files, and the same env-var
11
11
  names, so there's one place to look.
12
12
 
13
+ Legacy vocabulary (the pre-LangStage names) still works everywhere as a
14
+ deprecated fallback: ``DEEPAGENT_*`` env vars, project ``deepagents.toml``,
15
+ global ``~/.deepagents/config.toml``, and ``DEEPAGENTS_CONFIG_HOME``. The
16
+ canonical names win when both are set; using only the legacy env names emits
17
+ a once-per-var ``DeprecationWarning``. Moving the global config out of
18
+ ``~/.deepagents/`` also exits the schema collision with LangChain's dcode,
19
+ which owns that directory now.
20
+
13
21
  Discoverability: ``HostConfig.resolve().describe()`` (or
14
22
  ``python -m langgraph_stream_parser.host``) prints each value, where it came
15
23
  from, and the env var / TOML key that sets it — so you never have to remember
16
- whether it's ``DEEPAGENT_SPEC`` or ``DEEPAGENT_AGENT_SPEC`` (it's the latter).
24
+ the variable names.
17
25
  """
18
26
  import os
27
+ import warnings
19
28
  from dataclasses import MISSING, dataclass, fields, replace
20
29
  from pathlib import Path
21
30
  from typing import Any, Callable, ClassVar
@@ -28,8 +37,40 @@ except ModuleNotFoundError: # pragma: no cover - 3.10 path
28
37
  except ModuleNotFoundError:
29
38
  _tomllib = None # type: ignore
30
39
 
31
- GLOBAL_TOML = Path.home() / ".deepagents" / "config.toml"
32
- PROJECT_TOML = "deepagents.toml"
40
+ GLOBAL_TOML = Path.home() / ".langstage" / "config.toml"
41
+ PROJECT_TOML = "langstage.toml"
42
+ # Pre-rename locations, still honoured as fallbacks.
43
+ LEGACY_GLOBAL_TOML = Path.home() / ".deepagents" / "config.toml"
44
+ LEGACY_PROJECT_TOML = "deepagents.toml"
45
+
46
+ _CANONICAL_ENV_PREFIX = "LANGSTAGE"
47
+ _LEGACY_ENV_PREFIX = "DEEPAGENT"
48
+
49
+ _warned_legacy_env: set[str] = set()
50
+
51
+
52
+ def _env_pair(declared: str) -> tuple[str, str]:
53
+ """Return ``(canonical, legacy)`` env-var names for a declared name.
54
+
55
+ Hosts may declare either spelling in their ``_ENV`` maps during the
56
+ rename transition; both resolve, canonical wins.
57
+ """
58
+ if declared.startswith(_CANONICAL_ENV_PREFIX):
59
+ return declared, _LEGACY_ENV_PREFIX + declared[len(_CANONICAL_ENV_PREFIX):]
60
+ if declared.startswith(_LEGACY_ENV_PREFIX):
61
+ return _CANONICAL_ENV_PREFIX + declared[len(_LEGACY_ENV_PREFIX):], declared
62
+ return declared, declared
63
+
64
+
65
+ def _warn_legacy_env(legacy: str, canonical: str) -> None:
66
+ if legacy in _warned_legacy_env:
67
+ return
68
+ _warned_legacy_env.add(legacy)
69
+ warnings.warn(
70
+ f"{legacy} is deprecated; use {canonical}.",
71
+ DeprecationWarning,
72
+ stacklevel=4,
73
+ )
33
74
 
34
75
 
35
76
  def _env_bool(value: str | None, default: bool = False) -> bool:
@@ -43,17 +84,27 @@ def _env_bool(value: str | None, default: bool = False) -> bool:
43
84
 
44
85
 
45
86
  def _global_toml_path() -> Path:
46
- override = os.getenv("DEEPAGENTS_CONFIG_HOME")
47
- return (Path(override).expanduser() / "config.toml") if override else GLOBAL_TOML
87
+ override = os.getenv("LANGSTAGE_CONFIG_HOME") or os.getenv("DEEPAGENTS_CONFIG_HOME")
88
+ if override:
89
+ return Path(override).expanduser() / "config.toml"
90
+ # New home wins when present; otherwise fall back to the legacy location
91
+ # (which load_toml_config skips anyway if the file doesn't exist).
92
+ return GLOBAL_TOML if GLOBAL_TOML.is_file() else LEGACY_GLOBAL_TOML
48
93
 
49
94
 
50
95
  def _find_project_toml(start: Path | None = None) -> Path | None:
51
- """Walk up from ``start`` (or cwd) looking for ``deepagents.toml``."""
96
+ """Walk up from ``start`` (or cwd) looking for ``langstage.toml``.
97
+
98
+ Checks ``langstage.toml`` then legacy ``deepagents.toml`` in each
99
+ directory, so the nearest file wins and the new name wins within a
100
+ directory.
101
+ """
52
102
  here = (start or Path.cwd()).resolve()
53
103
  for directory in (here, *here.parents):
54
- candidate = directory / PROJECT_TOML
55
- if candidate.is_file():
56
- return candidate
104
+ for fname in (PROJECT_TOML, LEGACY_PROJECT_TOML):
105
+ candidate = directory / fname
106
+ if candidate.is_file():
107
+ return candidate
57
108
  return None
58
109
 
59
110
 
@@ -75,11 +126,13 @@ def _read_toml(path: Path) -> dict:
75
126
 
76
127
 
77
128
  def load_toml_config(start: Path | None = None) -> tuple[dict, list[Path]]:
78
- """Load + deep-merge the global and project ``deepagents.toml`` files.
129
+ """Load + deep-merge the global and project ``langstage.toml`` files.
79
130
 
80
- Global is ``~/.deepagents/config.toml`` (override the dir with
81
- ``DEEPAGENTS_CONFIG_HOME``); project is the nearest ``deepagents.toml`` at
82
- or above ``start``/cwd. Project wins on conflicts. Returns
131
+ Global is ``~/.langstage/config.toml`` (override the dir with
132
+ ``LANGSTAGE_CONFIG_HOME``; legacy ``~/.deepagents/config.toml`` and
133
+ ``DEEPAGENTS_CONFIG_HOME`` still work as fallbacks); project is the
134
+ nearest ``langstage.toml`` — or legacy ``deepagents.toml`` — at or above
135
+ ``start``/cwd. Project wins on conflicts. Returns
83
136
  ``(merged_config, sources_used)``; ``({}, [])`` if no TOML reader is
84
137
  available (Python 3.10 without ``tomli``).
85
138
  """
@@ -120,28 +173,30 @@ class HostConfig:
120
173
  @dataclass
121
174
  class WebConfig(HostConfig):
122
175
  theme: str = "auto"
123
- _ENV = {"theme": ("DEEPAGENT_THEME", str)}
176
+ _ENV = {"theme": ("LANGSTAGE_THEME", str)}
124
177
  _TOML = {"theme": "ui.theme"}
125
178
 
126
179
  ``resolve()`` merges the maps across the MRO, so the subclass inherits all
127
180
  of ``HostConfig``'s keys and adds its own.
128
181
  """
129
182
 
130
- agent_spec: str | None = None # DEEPAGENT_AGENT_SPEC ("path.py:var")
131
- workspace_root: Path = Path(".") # DEEPAGENT_WORKSPACE_ROOT
132
- host: str = "localhost" # DEEPAGENT_HOST
133
- port: int = 8050 # DEEPAGENT_PORT
134
- debug: bool = False # DEEPAGENT_DEBUG
135
- title: str = "Deep Agent" # DEEPAGENT_TITLE
183
+ agent_spec: str | None = None # LANGSTAGE_AGENT_SPEC ("path.py:var")
184
+ workspace_root: Path = Path(".") # LANGSTAGE_WORKSPACE_ROOT
185
+ host: str = "localhost" # LANGSTAGE_HOST
186
+ port: int = 8050 # LANGSTAGE_PORT
187
+ debug: bool = False # LANGSTAGE_DEBUG
188
+ title: str = "LangStage" # LANGSTAGE_TITLE
136
189
 
137
- # field -> (DEEPAGENT_* env var, caster). DEEPAGENT_AGENT_SPEC is canonical.
190
+ # field -> (env var, caster). Canonical names are LANGSTAGE_*; the
191
+ # matching DEEPAGENT_* legacy names resolve as deprecated fallbacks
192
+ # (see _env_pair).
138
193
  _ENV: ClassVar[dict[str, tuple[str, Callable[[str], Any]]]] = {
139
- "agent_spec": ("DEEPAGENT_AGENT_SPEC", str),
140
- "workspace_root": ("DEEPAGENT_WORKSPACE_ROOT", Path),
141
- "host": ("DEEPAGENT_HOST", str),
142
- "port": ("DEEPAGENT_PORT", int),
143
- "debug": ("DEEPAGENT_DEBUG", _env_bool),
144
- "title": ("DEEPAGENT_TITLE", str),
194
+ "agent_spec": ("LANGSTAGE_AGENT_SPEC", str),
195
+ "workspace_root": ("LANGSTAGE_WORKSPACE_ROOT", Path),
196
+ "host": ("LANGSTAGE_HOST", str),
197
+ "port": ("LANGSTAGE_PORT", int),
198
+ "debug": ("LANGSTAGE_DEBUG", _env_bool),
199
+ "title": ("LANGSTAGE_TITLE", str),
145
200
  }
146
201
  # field -> dotted key in deepagents.toml
147
202
  _TOML: ClassVar[dict[str, str]] = {
@@ -226,10 +281,17 @@ class HostConfig:
226
281
 
227
282
  if name in env_map:
228
283
  var, caster = env_map[name]
229
- ev = env.get(var)
284
+ canonical, legacy = _env_pair(var)
285
+ ev = env.get(canonical)
286
+ used = canonical
287
+ if ev is None or ev == "":
288
+ ev = env.get(legacy)
289
+ used = legacy
290
+ if ev not in (None, "") and legacy != canonical:
291
+ _warn_legacy_env(legacy, canonical)
230
292
  if ev is not None and ev != "":
231
293
  val = caster(ev)
232
- src = f"env:{var}"
294
+ src = f"env:{used}"
233
295
 
234
296
  if name in overrides:
235
297
  val = overrides[name]
@@ -270,7 +332,11 @@ class HostConfig:
270
332
  origin = src.get(f.name, "default")
271
333
  hints = []
272
334
  if f.name in env_map:
273
- hints.append(f"env: {env_map[f.name][0]}")
335
+ canonical, legacy = _env_pair(env_map[f.name][0])
336
+ env_hint = f"env: {canonical}"
337
+ if legacy != canonical:
338
+ env_hint += f" (legacy {legacy})"
339
+ hints.append(env_hint)
274
340
  if f.name in toml_map:
275
341
  hints.append(f"toml: {toml_map[f.name]}")
276
342
  hint = f" ({', '.join(hints)})" if hints else ""
@@ -280,7 +346,7 @@ class HostConfig:
280
346
  if toml_paths:
281
347
  lines.append(" TOML read from: " + ", ".join(str(p) for p in toml_paths))
282
348
  else:
283
- lines.append(" TOML: no deepagents.toml found")
349
+ lines.append(" TOML: no langstage.toml (or legacy deepagents.toml) found")
284
350
  return "\n".join(lines)
285
351
 
286
352
 
@@ -0,0 +1,65 @@
1
+ """Tests for the keyless stub agent behind every surface's --demo mode.
2
+
3
+ Unlike the default agent (which needs deepagents + an API key), the stub only
4
+ needs langgraph + langchain-core — both in the dev extras — so these tests
5
+ exercise it end to end through the parser.
6
+ """
7
+ from langgraph_stream_parser import StreamParser, load_agent_spec, prepare_agent_input
8
+ from langgraph_stream_parser.demo import create_stub_agent
9
+ from langgraph_stream_parser.events import CompleteEvent, ContentEvent
10
+
11
+ STREAM_MODE = ["updates", "messages"]
12
+
13
+
14
+ def _run_turn(graph, message: str, thread_id: str = "t1"):
15
+ parser = StreamParser(stream_mode=STREAM_MODE)
16
+ stream = graph.stream(
17
+ prepare_agent_input(message=message),
18
+ config={"configurable": {"thread_id": thread_id}},
19
+ stream_mode=STREAM_MODE,
20
+ )
21
+ return list(parser.parse(stream))
22
+
23
+
24
+ def _content(events) -> str:
25
+ return "".join(e.content for e in events if isinstance(e, ContentEvent))
26
+
27
+
28
+ def test_streams_echo_through_the_parser():
29
+ graph = create_stub_agent()
30
+ events = _run_turn(graph, "hello demo")
31
+
32
+ assert "(demo agent) You said: hello demo" in _content(events)
33
+ assert isinstance(events[-1], CompleteEvent)
34
+
35
+
36
+ def test_streams_token_by_token():
37
+ graph = create_stub_agent()
38
+ events = _run_turn(graph, "one two three four")
39
+ content_events = [e for e in events if isinstance(e, ContentEvent)]
40
+ # The echo splits on spaces — a multi-word message must arrive in pieces.
41
+ assert len(content_events) > 1
42
+
43
+
44
+ def test_multi_turn_thread_persists():
45
+ graph = create_stub_agent()
46
+ _run_turn(graph, "first", thread_id="conv")
47
+ state = graph.get_state({"configurable": {"thread_id": "conv"}})
48
+ _run_turn(graph, "second", thread_id="conv")
49
+ state2 = graph.get_state({"configurable": {"thread_id": "conv"}})
50
+ assert len(state2.values["messages"]) > len(state.values["messages"])
51
+
52
+
53
+ def test_custom_name_and_prefix():
54
+ graph = create_stub_agent(name="My Demo", reply_prefix="echo: ")
55
+ assert graph.name == "My Demo"
56
+ events = _run_turn(graph, "hi")
57
+ assert "echo: hi" in _content(events)
58
+
59
+
60
+ def test_loadable_via_standard_spec_string():
61
+ graph = load_agent_spec("langgraph_stream_parser.demo.stub:graph")
62
+ events = _run_turn(graph, "spec works", thread_id="spec")
63
+ assert "spec works" in _content(events)
64
+ # The module-level graph is cached — same object on a second load.
65
+ assert load_agent_spec("langgraph_stream_parser.demo.stub:graph") is graph
@@ -67,7 +67,7 @@ class TestHostConfig:
67
67
  assert cfg.host == "localhost"
68
68
  assert cfg.port == 8050
69
69
  assert cfg.debug is False
70
- assert cfg.title == "Deep Agent"
70
+ assert cfg.title == "LangStage"
71
71
 
72
72
  def test_from_env(self, monkeypatch):
73
73
  monkeypatch.setenv("DEEPAGENT_AGENT_SPEC", "agent.py:graph")
@@ -111,6 +111,85 @@ class TestSubclass:
111
111
  assert "DEEPAGENT_THEME" in cfg.describe()
112
112
 
113
113
 
114
+ class TestLangstageVocabulary:
115
+ """Canonical LANGSTAGE_* / langstage.toml with deprecated legacy fallbacks."""
116
+
117
+ def test_canonical_env_resolves(self, isolated_global, tmp_path):
118
+ cfg = HostConfig.resolve(
119
+ env={"LANGSTAGE_AGENT_SPEC": "a.py:g", "LANGSTAGE_PORT": "9100"},
120
+ toml_start=tmp_path,
121
+ )
122
+ assert cfg.agent_spec == "a.py:g"
123
+ assert cfg.port == 9100
124
+ assert cfg.sources["agent_spec"] == "env:LANGSTAGE_AGENT_SPEC"
125
+
126
+ def test_canonical_beats_legacy(self, isolated_global, tmp_path):
127
+ cfg = HostConfig.resolve(
128
+ env={"LANGSTAGE_PORT": "1111", "DEEPAGENT_PORT": "2222"},
129
+ toml_start=tmp_path,
130
+ )
131
+ assert cfg.port == 1111
132
+ assert cfg.sources["port"] == "env:LANGSTAGE_PORT"
133
+
134
+ def test_legacy_env_warns_once(self, isolated_global, tmp_path):
135
+ import langgraph_stream_parser.host.config as config_mod
136
+
137
+ config_mod._warned_legacy_env.discard("DEEPAGENT_TITLE")
138
+ with pytest.warns(DeprecationWarning, match="LANGSTAGE_TITLE"):
139
+ HostConfig.resolve(env={"DEEPAGENT_TITLE": "Old"}, toml_start=tmp_path)
140
+
141
+ def test_langstage_toml_preferred_in_same_dir(self, isolated_global, tmp_path):
142
+ (tmp_path / "deepagents.toml").write_text("[server]\nport = 1000\n")
143
+ (tmp_path / "langstage.toml").write_text("[server]\nport = 2000\n")
144
+ cfg = HostConfig.resolve(env={}, toml_start=tmp_path)
145
+ assert cfg.port == 2000
146
+
147
+ def test_legacy_toml_still_read(self, isolated_global, tmp_path):
148
+ (tmp_path / "deepagents.toml").write_text("[server]\nport = 1234\n")
149
+ cfg = HostConfig.resolve(env={}, toml_start=tmp_path)
150
+ assert cfg.port == 1234
151
+
152
+ def test_nearest_toml_wins_across_dirs(self, isolated_global, tmp_path):
153
+ # langstage.toml in the parent must NOT beat deepagents.toml in cwd.
154
+ (tmp_path / "langstage.toml").write_text("[server]\nport = 1000\n")
155
+ sub = tmp_path / "sub"
156
+ sub.mkdir()
157
+ (sub / "deepagents.toml").write_text("[server]\nport = 2000\n")
158
+ cfg = HostConfig.resolve(env={}, toml_start=sub)
159
+ assert cfg.port == 2000
160
+
161
+ def test_langstage_config_home_override(self, tmp_path, monkeypatch):
162
+ gdir = tmp_path / "newhome"
163
+ gdir.mkdir()
164
+ (gdir / "config.toml").write_text("[server]\nport = 4321\n")
165
+ monkeypatch.delenv("DEEPAGENTS_CONFIG_HOME", raising=False)
166
+ monkeypatch.setenv("LANGSTAGE_CONFIG_HOME", str(gdir))
167
+ cfg = HostConfig.resolve(env={}, toml_start=tmp_path)
168
+ assert cfg.port == 4321
169
+
170
+ def test_describe_shows_both_vocabularies(self, isolated_global, tmp_path):
171
+ text = HostConfig.resolve(env={}, toml_start=tmp_path).describe()
172
+ assert "LANGSTAGE_AGENT_SPEC" in text
173
+ assert "legacy DEEPAGENT_AGENT_SPEC" in text
174
+
175
+ def test_subclass_legacy_declaration_resolves_canonical_name(
176
+ self, isolated_global, tmp_path
177
+ ):
178
+ """A host still declaring DEEPAGENT_* in its _ENV map picks up the
179
+ LANGSTAGE_* var without any subclass change."""
180
+ from dataclasses import dataclass
181
+ from typing import ClassVar
182
+
183
+ @dataclass
184
+ class OldHost(HostConfig):
185
+ theme: str = "auto"
186
+ _ENV: ClassVar[dict] = {"theme": ("DEEPAGENT_THEME", str)}
187
+
188
+ cfg = OldHost.resolve(env={"LANGSTAGE_THEME": "dark"}, toml_start=tmp_path)
189
+ assert cfg.theme == "dark"
190
+ assert cfg.sources["theme"] == "env:LANGSTAGE_THEME"
191
+
192
+
114
193
  class TestFromEnvBackCompat:
115
194
  def test_from_env_skips_toml(self, isolated_global, tmp_path, monkeypatch):
116
195
  _toml(tmp_path, "[server]\nport = 1234\n")
@@ -1,16 +0,0 @@
1
- """Host conventions shared across deep-agent surfaces.
2
-
3
- Agent-spec loading, the shared ``DEEPAGENT_*`` config schema, and a workspace
4
- wrapper — the plumbing every host (``cowork-dash``, ``deepagent-lab``,
5
- ``deepagent-code``, ``deepagent-vscode``) needs but used to reimplement.
6
- """
7
- from .config import HostConfig, load_toml_config
8
- from .loader import load_agent_spec
9
- from .workspace import Workspace
10
-
11
- __all__ = [
12
- "load_agent_spec",
13
- "HostConfig",
14
- "load_toml_config",
15
- "Workspace",
16
- ]
@@ -1,17 +0,0 @@
1
- """``python -m langgraph_stream_parser.host`` — print the resolved shared config.
2
-
3
- Shows each ``DEEPAGENT_*`` value, where it resolved from (default / TOML / env /
4
- override), and the env var + ``deepagents.toml`` key that set it — so you never
5
- have to remember the variable names. Hosts can ship their own subclass printer
6
- for host-specific keys; this covers the shared core.
7
- """
8
- from .config import HostConfig
9
-
10
-
11
- def main() -> int:
12
- print(HostConfig.resolve().describe())
13
- return 0
14
-
15
-
16
- if __name__ == "__main__":
17
- raise SystemExit(main())