langstage-cli 0.5.4__tar.gz → 0.5.6__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 (23) hide show
  1. {langstage_cli-0.5.4/langstage_cli.egg-info → langstage_cli-0.5.6}/PKG-INFO +9 -6
  2. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/README.md +6 -3
  3. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli/cli.py +73 -46
  4. {langstage_cli-0.5.4 → langstage_cli-0.5.6/langstage_cli.egg-info}/PKG-INFO +9 -6
  5. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli.egg-info/SOURCES.txt +3 -1
  6. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli.egg-info/requires.txt +2 -2
  7. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/pyproject.toml +5 -3
  8. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/tests/test_show_config.py +2 -2
  9. langstage_cli-0.5.6/tests/test_stream_mode.py +36 -0
  10. langstage_cli-0.5.6/tests/test_unicode_console.py +31 -0
  11. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/LICENSE +0 -0
  12. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/deepagent_code/__init__.py +0 -0
  13. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli/__init__.py +0 -0
  14. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli/config.py +0 -0
  15. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli.egg-info/dependency_links.txt +0 -0
  16. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli.egg-info/entry_points.txt +0 -0
  17. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/langstage_cli.egg-info/top_level.txt +0 -0
  18. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/setup.cfg +0 -0
  19. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/tests/test_cli.py +0 -0
  20. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/tests/test_cli_help.py +0 -0
  21. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/tests/test_codeconfig.py +0 -0
  22. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/tests/test_config.py +0 -0
  23. {langstage_cli-0.5.4 → langstage_cli-0.5.6}/tests/test_rename_shim.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langstage-cli
3
- Version: 0.5.4
3
+ Version: 0.5.6
4
4
  Summary: The terminal stage for your LangGraph agent — Claude Code-style CLI for any CompiledGraph
5
5
  Author-email: Kedar Dabhadkar <kdabhadk@gmail.com>
6
6
  License-Expression: MIT
@@ -19,7 +19,7 @@ Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: langgraph>=0.2.0
22
- Requires-Dist: langgraph-stream-parser<0.5,>=0.3
22
+ Requires-Dist: langgraph-stream-parser<0.7,>=0.6.6
23
23
  Requires-Dist: click>=8.0.0
24
24
  Requires-Dist: python-dotenv
25
25
  Provides-Extra: dev
@@ -30,7 +30,7 @@ Requires-Dist: ruff==0.15.15; extra == "dev"
30
30
  Requires-Dist: mypy>=1.0.0; extra == "dev"
31
31
  Requires-Dist: deepagents>=0.3; extra == "dev"
32
32
  Provides-Extra: agui
33
- Requires-Dist: langgraph-stream-parser[agui]<0.5,>=0.4; extra == "agui"
33
+ Requires-Dist: langgraph-stream-parser[agui]<0.7,>=0.6.6; extra == "agui"
34
34
  Dynamic: license-file
35
35
 
36
36
  # langstage-cli
@@ -146,7 +146,7 @@ langstage-cli
146
146
  # Working directory
147
147
  export LANGSTAGE_WORKSPACE_ROOT="/path/to/workspace"
148
148
 
149
- # Stream mode (updates or values)
149
+ # Stream mode (updates or messages)
150
150
  export LANGSTAGE_STREAM_MODE="updates"
151
151
  ```
152
152
 
@@ -169,7 +169,9 @@ Example `langstage.toml`:
169
169
  ```toml
170
170
  [agent]
171
171
  spec = "my_agent.py:graph"
172
- workspace_root = "."
172
+
173
+ [workspace]
174
+ root = "."
173
175
 
174
176
  [ui]
175
177
  verbose = true
@@ -195,7 +197,8 @@ Options:
195
197
  -f, --file PATH Read message from a file (any extension)
196
198
  --interactive/--no-interactive Handle interrupts (default: interactive)
197
199
  --async-mode/--sync-mode Async streaming (default: sync)
198
- --stream-mode TEXT Stream mode (updates or values)
200
+ --stream-mode [updates|messages]
201
+ Stream mode (default: updates)
199
202
  -v, --verbose Verbose output
200
203
  ```
201
204
 
@@ -111,7 +111,7 @@ langstage-cli
111
111
  # Working directory
112
112
  export LANGSTAGE_WORKSPACE_ROOT="/path/to/workspace"
113
113
 
114
- # Stream mode (updates or values)
114
+ # Stream mode (updates or messages)
115
115
  export LANGSTAGE_STREAM_MODE="updates"
116
116
  ```
117
117
 
@@ -134,7 +134,9 @@ Example `langstage.toml`:
134
134
  ```toml
135
135
  [agent]
136
136
  spec = "my_agent.py:graph"
137
- workspace_root = "."
137
+
138
+ [workspace]
139
+ root = "."
138
140
 
139
141
  [ui]
140
142
  verbose = true
@@ -160,7 +162,8 @@ Options:
160
162
  -f, --file PATH Read message from a file (any extension)
161
163
  --interactive/--no-interactive Handle interrupts (default: interactive)
162
164
  --async-mode/--sync-mode Async streaming (default: sync)
163
- --stream-mode TEXT Stream mode (updates or values)
165
+ --stream-mode [updates|messages]
166
+ Stream mode (default: updates)
164
167
  -v, --verbose Verbose output
165
168
  ```
166
169
 
@@ -190,11 +190,15 @@ class Spinner:
190
190
  frame = SPINNER_FRAMES[self.frame_idx % len(SPINNER_FRAMES)]
191
191
  elapsed = time.time() - self.start_time
192
192
  elapsed_str = f"{int(elapsed)}s"
193
- print(
194
- f"\r{CYAN}{frame}{RESET} {DIM}{self.message}... {elapsed_str}{RESET}",
195
- end="",
196
- flush=True,
197
- )
193
+ # Never let a stdout hiccup in this daemon thread crash the run.
194
+ try:
195
+ print(
196
+ f"\r{CYAN}{frame}{RESET} {DIM}{self.message}... {elapsed_str}{RESET}",
197
+ end="",
198
+ flush=True,
199
+ )
200
+ except (UnicodeEncodeError, ValueError):
201
+ pass
198
202
  self.frame_idx += 1
199
203
  time.sleep(0.08)
200
204
 
@@ -751,25 +755,26 @@ async def run_single_turn_async(
751
755
  spinner = Spinner("Thinking")
752
756
  spinner.start()
753
757
 
754
- async for chunk in astream_graph_updates(
755
- graph, input_data, config=config, stream_mode=stream_mode
756
- ):
757
- # Stop spinner on first chunk
758
- if first_chunk:
759
- spinner.stop()
760
- first_chunk = False
761
-
762
- print_chunk(chunk, verbose=verbose)
763
-
764
- if chunk.get("status") == "interrupt":
765
- has_interrupt = True
766
- # Count pending action requests
767
- interrupt_data = chunk.get("interrupt", {})
768
- action_requests = interrupt_data.get("action_requests", [])
769
- num_pending_actions = len(action_requests) if action_requests else 1
770
-
771
- # Ensure spinner is stopped even if no chunks received
772
- if first_chunk:
758
+ try:
759
+ async for chunk in astream_graph_updates(
760
+ graph, input_data, config=config, stream_mode=stream_mode
761
+ ):
762
+ # Stop spinner on first chunk
763
+ if first_chunk:
764
+ spinner.stop()
765
+ first_chunk = False
766
+
767
+ print_chunk(chunk, verbose=verbose)
768
+
769
+ if chunk.get("status") == "interrupt":
770
+ has_interrupt = True
771
+ # Count pending action requests
772
+ interrupt_data = chunk.get("interrupt", {})
773
+ action_requests = interrupt_data.get("action_requests", [])
774
+ num_pending_actions = len(action_requests) if action_requests else 1
775
+ finally:
776
+ # Always stop the spinner (see sync variant) so a streaming error
777
+ # can't leave a daemon thread holding stdout at interpreter shutdown.
773
778
  spinner.stop()
774
779
 
775
780
  if has_interrupt and interactive:
@@ -800,25 +805,27 @@ def run_single_turn_sync(
800
805
  spinner = Spinner("Thinking")
801
806
  spinner.start()
802
807
 
803
- for chunk in stream_graph_updates(
804
- graph, input_data, config=config, stream_mode=stream_mode
805
- ):
806
- # Stop spinner on first chunk
807
- if first_chunk:
808
- spinner.stop()
809
- first_chunk = False
810
-
811
- print_chunk(chunk, verbose=verbose)
812
-
813
- if chunk.get("status") == "interrupt":
814
- has_interrupt = True
815
- # Count pending action requests
816
- interrupt_data = chunk.get("interrupt", {})
817
- action_requests = interrupt_data.get("action_requests", [])
818
- num_pending_actions = len(action_requests) if action_requests else 1
819
-
820
- # Ensure spinner is stopped even if no chunks received
821
- if first_chunk:
808
+ try:
809
+ for chunk in stream_graph_updates(
810
+ graph, input_data, config=config, stream_mode=stream_mode
811
+ ):
812
+ # Stop spinner on first chunk
813
+ if first_chunk:
814
+ spinner.stop()
815
+ first_chunk = False
816
+
817
+ print_chunk(chunk, verbose=verbose)
818
+
819
+ if chunk.get("status") == "interrupt":
820
+ has_interrupt = True
821
+ # Count pending action requests
822
+ interrupt_data = chunk.get("interrupt", {})
823
+ action_requests = interrupt_data.get("action_requests", [])
824
+ num_pending_actions = len(action_requests) if action_requests else 1
825
+ finally:
826
+ # Always stop the spinner — if the stream raises, an unstopped daemon
827
+ # spinner thread holds stdout at interpreter shutdown, which crashes
828
+ # with "Fatal Python error: _enter_buffered_busy".
822
829
  spinner.stop()
823
830
 
824
831
  if has_interrupt and interactive:
@@ -1335,7 +1342,8 @@ def run_conversation_loop(
1335
1342
  )
1336
1343
  @click.option(
1337
1344
  "--stream-mode",
1338
- help="Stream mode for LangGraph (default: 'updates')",
1345
+ type=click.Choice(["updates", "messages"]),
1346
+ help="Stream mode: 'updates' (default) or 'messages' (token-level).",
1339
1347
  )
1340
1348
  @click.option(
1341
1349
  "--verbose",
@@ -1348,7 +1356,7 @@ def run_conversation_loop(
1348
1356
  "--demo",
1349
1357
  is_flag=True,
1350
1358
  default=False,
1351
- help="Run with the built-in keyless demo agent no API key needed",
1359
+ help="Run with the built-in keyless demo agent (no API key needed)",
1352
1360
  )
1353
1361
  @click.option(
1354
1362
  "--show-config",
@@ -1387,7 +1395,7 @@ def main(
1387
1395
  \b
1388
1396
  - LANGSTAGE_AGENT_SPEC: Agent location (same formats as above).
1389
1397
  - LANGSTAGE_WORKSPACE_ROOT: Working directory for the agent
1390
- - LANGSTAGE_STREAM_MODE: Stream mode for LangGraph (updates or values)
1398
+ - LANGSTAGE_STREAM_MODE: Stream mode for LangGraph (updates or messages)
1391
1399
 
1392
1400
  Reads ~/.langstage/config.toml (global) and langstage.toml (project,
1393
1401
  walks up from cwd). Precedence: CLI args > env vars > project TOML >
@@ -1403,6 +1411,16 @@ def main(
1403
1411
  langstage-cli --demo "try it with no API key"
1404
1412
  langstage-cli --show-config
1405
1413
  """
1414
+ # Windows consoles default to cp1252, where the spinner (Braille frames) and
1415
+ # status glyphs (✓ ⏺ —) raise UnicodeEncodeError — the documented
1416
+ # `langstage-cli --demo "hello"` one-liner crashed before any output. Force
1417
+ # UTF-8 with errors="replace" so a glyph can never crash the process.
1418
+ for _stream in (sys.stdout, sys.stderr):
1419
+ try:
1420
+ _stream.reconfigure(encoding="utf-8", errors="replace")
1421
+ except (AttributeError, ValueError): # non-reconfigurable stream
1422
+ pass
1423
+
1406
1424
  if demo:
1407
1425
  if agent_spec:
1408
1426
  print(f"{RED}⏺ Error: --demo and -a/--agent are mutually exclusive{RESET}")
@@ -1467,6 +1485,15 @@ def main(
1467
1485
  final_spec = cfg.agent_spec
1468
1486
  final_graph_name_default = cfg.graph_name
1469
1487
  final_stream_mode = cfg.stream_mode
1488
+ # The CLI's render path only supports 'updates' / 'messages'. Reject anything
1489
+ # else (e.g. LANGSTAGE_STREAM_MODE=values, which bypasses the flag's Choice)
1490
+ # with a clean error instead of a fatal crash deep in the stream worker.
1491
+ if final_stream_mode not in ("updates", "messages"):
1492
+ print(
1493
+ f"{RED}⏺ Error: unsupported stream mode {final_stream_mode!r} "
1494
+ f"(use 'updates' or 'messages'){RESET}"
1495
+ )
1496
+ sys.exit(2)
1470
1497
  use_async = cfg.async_mode
1471
1498
  verbose = cfg.verbose
1472
1499
  # Only change directory when a workspace root was actually configured.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langstage-cli
3
- Version: 0.5.4
3
+ Version: 0.5.6
4
4
  Summary: The terminal stage for your LangGraph agent — Claude Code-style CLI for any CompiledGraph
5
5
  Author-email: Kedar Dabhadkar <kdabhadk@gmail.com>
6
6
  License-Expression: MIT
@@ -19,7 +19,7 @@ Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: langgraph>=0.2.0
22
- Requires-Dist: langgraph-stream-parser<0.5,>=0.3
22
+ Requires-Dist: langgraph-stream-parser<0.7,>=0.6.6
23
23
  Requires-Dist: click>=8.0.0
24
24
  Requires-Dist: python-dotenv
25
25
  Provides-Extra: dev
@@ -30,7 +30,7 @@ Requires-Dist: ruff==0.15.15; extra == "dev"
30
30
  Requires-Dist: mypy>=1.0.0; extra == "dev"
31
31
  Requires-Dist: deepagents>=0.3; extra == "dev"
32
32
  Provides-Extra: agui
33
- Requires-Dist: langgraph-stream-parser[agui]<0.5,>=0.4; extra == "agui"
33
+ Requires-Dist: langgraph-stream-parser[agui]<0.7,>=0.6.6; extra == "agui"
34
34
  Dynamic: license-file
35
35
 
36
36
  # langstage-cli
@@ -146,7 +146,7 @@ langstage-cli
146
146
  # Working directory
147
147
  export LANGSTAGE_WORKSPACE_ROOT="/path/to/workspace"
148
148
 
149
- # Stream mode (updates or values)
149
+ # Stream mode (updates or messages)
150
150
  export LANGSTAGE_STREAM_MODE="updates"
151
151
  ```
152
152
 
@@ -169,7 +169,9 @@ Example `langstage.toml`:
169
169
  ```toml
170
170
  [agent]
171
171
  spec = "my_agent.py:graph"
172
- workspace_root = "."
172
+
173
+ [workspace]
174
+ root = "."
173
175
 
174
176
  [ui]
175
177
  verbose = true
@@ -195,7 +197,8 @@ Options:
195
197
  -f, --file PATH Read message from a file (any extension)
196
198
  --interactive/--no-interactive Handle interrupts (default: interactive)
197
199
  --async-mode/--sync-mode Async streaming (default: sync)
198
- --stream-mode TEXT Stream mode (updates or values)
200
+ --stream-mode [updates|messages]
201
+ Stream mode (default: updates)
199
202
  -v, --verbose Verbose output
200
203
  ```
201
204
 
@@ -16,4 +16,6 @@ tests/test_cli_help.py
16
16
  tests/test_codeconfig.py
17
17
  tests/test_config.py
18
18
  tests/test_rename_shim.py
19
- tests/test_show_config.py
19
+ tests/test_show_config.py
20
+ tests/test_stream_mode.py
21
+ tests/test_unicode_console.py
@@ -1,10 +1,10 @@
1
1
  langgraph>=0.2.0
2
- langgraph-stream-parser<0.5,>=0.3
2
+ langgraph-stream-parser<0.7,>=0.6.6
3
3
  click>=8.0.0
4
4
  python-dotenv
5
5
 
6
6
  [agui]
7
- langgraph-stream-parser[agui]<0.5,>=0.4
7
+ langgraph-stream-parser[agui]<0.7,>=0.6.6
8
8
 
9
9
  [dev]
10
10
  pytest>=7.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "langstage-cli"
7
- version = "0.5.4"
7
+ version = "0.5.6"
8
8
  description = "The terminal stage for your LangGraph agent — Claude Code-style CLI for any CompiledGraph"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -25,7 +25,9 @@ classifiers = [
25
25
 
26
26
  dependencies = [
27
27
  "langgraph>=0.2.0",
28
- "langgraph-stream-parser>=0.3,<0.5",
28
+ # >=0.6.6 modernizes off the stale <0.5 pin and carries the dict-form-message
29
+ # fix, so a CompiledGraph returning {"role":...,"content":...} renders (gh #-dogfood).
30
+ "langgraph-stream-parser>=0.6.6,<0.7",
29
31
  "click>=8.0.0",
30
32
  "python-dotenv",
31
33
  ]
@@ -43,7 +45,7 @@ dev = [
43
45
  # pull deepagents + langchain + langgraph.
44
46
  "deepagents>=0.3",
45
47
  ]
46
- agui = ["langgraph-stream-parser[agui]>=0.4,<0.5"]
48
+ agui = ["langgraph-stream-parser[agui]>=0.6.6,<0.7"]
47
49
 
48
50
  [project.scripts]
49
51
  langstage-cli = "langstage_cli.cli:main"
@@ -15,12 +15,12 @@ from langstage_cli.cli import main
15
15
  def test_show_config_reflects_cli_flags():
16
16
  with CliRunner().isolated_filesystem(): # no stray toml
17
17
  r = CliRunner().invoke(
18
- main, ["-a", "fromcli.py:graph", "-v", "--stream-mode", "values", "--show-config"]
18
+ main, ["-a", "fromcli.py:graph", "-v", "--stream-mode", "messages", "--show-config"]
19
19
  )
20
20
  assert r.exit_code == 0, r.output
21
21
  # CLI-set values appear with the [override] source, not [default].
22
22
  assert re.search(r"agent_spec\s*=\s*fromcli\.py:graph\s*\[override\]", r.output), r.output
23
- assert re.search(r"stream_mode\s*=\s*values\s*\[override\]", r.output), r.output
23
+ assert re.search(r"stream_mode\s*=\s*messages\s*\[override\]", r.output), r.output
24
24
  assert re.search(r"verbose\s*=\s*True\s*\[override\]", r.output), r.output
25
25
 
26
26
 
@@ -0,0 +1,36 @@
1
+ """--stream-mode validation (gh #-dogfood).
2
+
3
+ `values` was advertised but unsupported by the CLI's render path; passing it
4
+ crashed with a fatal interpreter-shutdown error (a ValueError surfaced while the
5
+ spinner daemon thread held stdout). Now: the flag is a Choice {updates,messages},
6
+ and the resolved value (incl. LANGSTAGE_STREAM_MODE, which bypasses the flag) is
7
+ validated up front with a clean error.
8
+ """
9
+
10
+ from click.testing import CliRunner
11
+
12
+ from langstage_cli.cli import main
13
+
14
+
15
+ def test_stream_mode_values_flag_rejected_cleanly():
16
+ r = CliRunner().invoke(main, ["--demo", "--no-interactive", "--stream-mode", "values", "hi"])
17
+ assert r.exit_code != 0
18
+ assert "values" in r.output
19
+ assert "Fatal" not in r.output # no interpreter-shutdown crash
20
+
21
+
22
+ def test_stream_mode_messages_flag_accepted():
23
+ # A valid mode must still be accepted by the Choice (parses, doesn't error on the flag).
24
+ r = CliRunner().invoke(main, ["--stream-mode", "messages", "--show-config"])
25
+ assert r.exit_code == 0, r.output
26
+ assert "messages" in r.output
27
+
28
+
29
+ def test_stream_mode_values_via_env_rejected():
30
+ r = CliRunner().invoke(
31
+ main,
32
+ ["--demo", "--no-interactive", "hi"],
33
+ env={"LANGSTAGE_STREAM_MODE": "values"},
34
+ )
35
+ assert r.exit_code == 2, r.output
36
+ assert "unsupported stream mode" in r.output.lower()
@@ -0,0 +1,31 @@
1
+ """Regression: the headline `langstage-cli --demo` must not crash on a non-UTF-8
2
+ console (Windows cp1252).
3
+
4
+ The spinner (Braille frames) and status glyphs (✓ ⏺) raised UnicodeEncodeError on
5
+ a default Windows console, so the documented one-liner crashed before output.
6
+ `main()` now reconfigures stdio to UTF-8 (errors="replace"). We force `cp1252`
7
+ via PYTHONIOENCODING so this reproduces on any platform — without the fix this
8
+ test exits non-zero with a UnicodeEncodeError traceback.
9
+ """
10
+
11
+ import os
12
+ import subprocess
13
+ import sys
14
+
15
+
16
+ def test_demo_runs_on_cp1252_console():
17
+ env = dict(os.environ)
18
+ env["PYTHONIOENCODING"] = "cp1252"
19
+ env.pop("PYTHONUTF8", None)
20
+ proc = subprocess.run(
21
+ [sys.executable, "-c", "from langstage_cli.cli import main; main()", "--demo", "hello"],
22
+ capture_output=True,
23
+ text=True,
24
+ encoding="utf-8",
25
+ errors="replace",
26
+ env=env,
27
+ )
28
+ assert proc.returncode == 0, f"stdout={proc.stdout!r}\nstderr={proc.stderr!r}"
29
+ assert "UnicodeEncodeError" not in proc.stderr, proc.stderr
30
+ # The demo stub echoes the input back.
31
+ assert "demo agent" in proc.stdout.lower() or "hello" in proc.stdout.lower(), proc.stdout
File without changes
File without changes