langstage-cli 0.6.7__tar.gz → 0.6.9__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 (36) hide show
  1. {langstage_cli-0.6.7/langstage_cli.egg-info → langstage_cli-0.6.9}/PKG-INFO +4 -8
  2. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/README.md +3 -7
  3. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli/cli.py +25 -36
  4. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli/config.py +6 -4
  5. {langstage_cli-0.6.7 → langstage_cli-0.6.9/langstage_cli.egg-info}/PKG-INFO +4 -8
  6. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/pyproject.toml +1 -1
  7. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_cli_help.py +3 -1
  8. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_codeconfig.py +12 -9
  9. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_quiet_output.py +8 -0
  10. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_show_config.py +4 -7
  11. langstage_cli-0.6.9/tests/test_stream_mode.py +71 -0
  12. langstage_cli-0.6.7/tests/test_stream_mode.py +0 -76
  13. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/LICENSE +0 -0
  14. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/deepagent_code/__init__.py +0 -0
  15. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli/__init__.py +0 -0
  16. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli/agui_stream.py +0 -0
  17. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli.egg-info/SOURCES.txt +0 -0
  18. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli.egg-info/dependency_links.txt +0 -0
  19. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli.egg-info/entry_points.txt +0 -0
  20. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli.egg-info/requires.txt +0 -0
  21. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli.egg-info/top_level.txt +0 -0
  22. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/setup.cfg +0 -0
  23. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_agui_stream.py +0 -0
  24. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_checkpointer.py +0 -0
  25. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_cli.py +0 -0
  26. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_config.py +0 -0
  27. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_configurable.py +0 -0
  28. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_help_render.py +0 -0
  29. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_no_interactive_approve.py +0 -0
  30. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_rename_shim.py +0 -0
  31. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_spec_resolution.py +0 -0
  32. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_streaming_marker.py +0 -0
  33. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_turn_exit_and_render.py +0 -0
  34. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_unicode_console.py +0 -0
  35. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_verify_flag.py +0 -0
  36. {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langstage-cli
3
- Version: 0.6.7
3
+ Version: 0.6.9
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
@@ -41,7 +41,9 @@ Dynamic: license-file
41
41
 
42
42
  > Renamed from **deepagent-code** (the old package name now just installs this one, and the `deepagent-code` command still works). Not to be confused with LangChain's **`deepagents-code`** (`dcode`) — that's a separate project; `langstage-cli` is the terminal stage of the [LangStage family](#every-stage-for-your-langgraph-agent).
43
43
 
44
- ![langstage-cli](examples/image.png)
44
+ <p align="center">
45
+ <img src="assets/header.svg" alt="langstage-cli — the terminal stage for your LangGraph agent" width="100%" />
46
+ </p>
45
47
 
46
48
  ## Every stage for your LangGraph agent
47
49
 
@@ -146,9 +148,6 @@ langstage-cli
146
148
 
147
149
  # Working directory
148
150
  export LANGSTAGE_WORKSPACE_ROOT="/path/to/workspace"
149
-
150
- # Stream mode (updates or messages)
151
- export LANGSTAGE_STREAM_MODE="updates"
152
151
  ```
153
152
 
154
153
  ## Configuration Files
@@ -177,7 +176,6 @@ root = "."
177
176
  [ui]
178
177
  verbose = true
179
178
  async_mode = false
180
- stream_mode = "auto" # auto | updates | messages
181
179
 
182
180
  [configurable]
183
181
  # seeds LangGraph RunnableConfig.configurable
@@ -198,8 +196,6 @@ Options:
198
196
  -f, --file PATH Read message from a file (any extension)
199
197
  --interactive/--no-interactive Handle interrupts (default: interactive)
200
198
  --async-mode/--sync-mode Async streaming (default: sync)
201
- --stream-mode [auto|updates|messages]
202
- Stream mode (default: auto)
203
199
  -v, --verbose Verbose output
204
200
  --demo Run with the built-in keyless demo agent
205
201
  --show-config Print the resolved configuration and exit
@@ -4,7 +4,9 @@
4
4
 
5
5
  > Renamed from **deepagent-code** (the old package name now just installs this one, and the `deepagent-code` command still works). Not to be confused with LangChain's **`deepagents-code`** (`dcode`) — that's a separate project; `langstage-cli` is the terminal stage of the [LangStage family](#every-stage-for-your-langgraph-agent).
6
6
 
7
- ![langstage-cli](examples/image.png)
7
+ <p align="center">
8
+ <img src="assets/header.svg" alt="langstage-cli — the terminal stage for your LangGraph agent" width="100%" />
9
+ </p>
8
10
 
9
11
  ## Every stage for your LangGraph agent
10
12
 
@@ -109,9 +111,6 @@ langstage-cli
109
111
 
110
112
  # Working directory
111
113
  export LANGSTAGE_WORKSPACE_ROOT="/path/to/workspace"
112
-
113
- # Stream mode (updates or messages)
114
- export LANGSTAGE_STREAM_MODE="updates"
115
114
  ```
116
115
 
117
116
  ## Configuration Files
@@ -140,7 +139,6 @@ root = "."
140
139
  [ui]
141
140
  verbose = true
142
141
  async_mode = false
143
- stream_mode = "auto" # auto | updates | messages
144
142
 
145
143
  [configurable]
146
144
  # seeds LangGraph RunnableConfig.configurable
@@ -161,8 +159,6 @@ Options:
161
159
  -f, --file PATH Read message from a file (any extension)
162
160
  --interactive/--no-interactive Handle interrupts (default: interactive)
163
161
  --async-mode/--sync-mode Async streaming (default: sync)
164
- --stream-mode [auto|updates|messages]
165
- Stream mode (default: auto)
166
162
  -v, --verbose Verbose output
167
163
  --demo Run with the built-in keyless demo agent
168
164
  --show-config Print the resolved configuration and exit
@@ -76,11 +76,12 @@ def _status(msg: str) -> None:
76
76
  print(msg, file=sys.stderr if _QUIET else sys.stdout)
77
77
 
78
78
 
79
- # Inherited HostConfig keys the terminal CLI never reads: it starts no server
80
- # (host/port/debug are inert) and the header box uses the loaded graph's name,
81
- # not `title`. `--show-config` / `/config` omit these so the diagnostic only
82
- # advertises knobs this surface actually honors. (gh #36)
83
- _INERT_KEYS = ["host", "port", "debug", "title"]
79
+ # Keys the terminal CLI never honors, so `--show-config` / `/config` omit them and
80
+ # the diagnostic only advertises knobs that actually do something. The inherited
81
+ # HostConfig server keys it starts no server (host/port/debug are inert) and the
82
+ # header box uses the loaded graph's name, not `title` (gh #36) — plus `stream_mode`,
83
+ # which has had no effect since the AG-UI streaming migration (gh #62).
84
+ _INERT_KEYS = ["host", "port", "debug", "title", "stream_mode"]
84
85
 
85
86
  # Spinner frames for thinking animation
86
87
  SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
@@ -173,12 +174,13 @@ def rl_wrap(code: str) -> str:
173
174
  return code
174
175
 
175
176
 
176
- def make_prompt(text: str = "❯", color: str = BRIGHT_BLUE) -> str:
177
+ def make_prompt(text: str = "❯", color: str | None = None) -> str:
177
178
  """Create a prompt string with proper readline escaping for ANSI codes.
178
179
 
179
180
  This prevents line wrapping issues on Windows and other terminals.
180
181
  """
181
- return f"{rl_wrap(BOLD)}{rl_wrap(color)}{text}{rl_wrap(RESET)} "
182
+ prompt_color = BRIGHT_BLUE if color is None else color
183
+ return f"{rl_wrap(BOLD)}{rl_wrap(prompt_color)}{text}{rl_wrap(RESET)} "
182
184
 
183
185
 
184
186
  def register_command(
@@ -864,20 +866,6 @@ def handle_interrupt_input(num_actions: int = 1) -> List[Dict[str, Any]]:
864
866
  sys.exit(0)
865
867
 
866
868
 
867
- def _resolve_stream_mode(stream_mode: str):
868
- """Map the user-facing stream mode to what LangGraph actually streams.
869
-
870
- 'auto' = dual-channel ``["updates", "messages"]``: the parser renders LLM
871
- tokens live when the agent streams them, and falls back to the finished
872
- message content otherwise — so a node that returns a complete (non-token-
873
- streamed) ``AIMessage`` still renders instead of an empty turn. (Bare
874
- 'messages' only carries LLM token streams, which is exactly why it was a
875
- silent-blank trap — gh #-dogfood.) 'updates'/'messages' pass through as the
876
- explicit single-mode power-user choices.
877
- """
878
- return ["updates", "messages"] if stream_mode == "auto" else stream_mode
879
-
880
-
881
869
  def print_help():
882
870
  """Print formatted help information."""
883
871
  print(f"\n{BOLD}{BRIGHT_CYAN}Commands{RESET}")
@@ -979,14 +967,12 @@ def cmd_status(args: str, context: Dict[str, Any]) -> Optional[str]:
979
967
  agent_name = context.get("agent_name", "Unknown")
980
968
  verbose = context.get("verbose", False)
981
969
  use_async = context.get("use_async", False)
982
- stream_mode = context.get("stream_mode", "updates")
983
970
 
984
971
  print(f"\n{BOLD}{BRIGHT_CYAN}Session Status{RESET}")
985
972
  print(f"{DIM}{'─' * 30}{RESET}")
986
973
  print(f" {DIM}Agent:{RESET} {agent_name}")
987
974
  print(f" {DIM}Thread ID:{RESET} {thread_id[:8]}...")
988
975
  print(f" {DIM}Mode:{RESET} {'async' if use_async else 'sync'}")
989
- print(f" {DIM}Stream:{RESET} {stream_mode}")
990
976
  print(f" {DIM}Verbose:{RESET} {'on' if verbose else 'off'}")
991
977
  print(f" {DIM}CWD:{RESET} {os.getcwd()}")
992
978
  print()
@@ -1482,9 +1468,11 @@ def run_conversation_loop(
1482
1468
  @click.option(
1483
1469
  "--stream-mode",
1484
1470
  type=click.Choice(["auto", "updates", "messages"]),
1485
- help="Stream mode: 'auto' (default; token streaming when the agent emits "
1486
- "tokens, full-message fallback otherwise), 'updates' (whole-message), or "
1487
- "'messages' (token-level only a finished AIMessage renders nothing here).",
1471
+ # DEPRECATED (gh #62): a no-op since the AG-UI streaming migration all three
1472
+ # modes render identically. Kept hidden + accepted so existing `--stream-mode X`
1473
+ # invocations don't hard-error; a one-line notice fires when it is passed.
1474
+ hidden=True,
1475
+ help="(deprecated: no effect since the AG-UI streaming migration).",
1488
1476
  )
1489
1477
  @click.option(
1490
1478
  "--verbose",
@@ -1565,7 +1553,6 @@ def main(
1565
1553
  \b
1566
1554
  - LANGSTAGE_AGENT_SPEC: Agent location (same formats as above).
1567
1555
  - LANGSTAGE_WORKSPACE_ROOT: Working directory for the agent
1568
- - LANGSTAGE_STREAM_MODE: Stream mode for LangGraph (auto, updates, or messages)
1569
1556
 
1570
1557
  Reads ~/.langstage/config.toml (global) and langstage.toml (project,
1571
1558
  walks up from cwd). Precedence: CLI args > env vars > project TOML >
@@ -1628,6 +1615,14 @@ def main(
1628
1615
  "verbose": True if verbose else None,
1629
1616
  }
1630
1617
 
1618
+ # --stream-mode is deprecated and inert since the AG-UI streaming migration
1619
+ # (gh #62): accepted so scripts don't break, but say so once when it's passed.
1620
+ if stream_mode is not None:
1621
+ _status(
1622
+ f"{DIM}⏺ Note: --stream-mode is deprecated and has no effect "
1623
+ f"(streaming is uniform since the AG-UI migration).{RESET}"
1624
+ )
1625
+
1631
1626
  if show_config:
1632
1627
  # See _INERT_KEYS: hide the server/web keys this surface ignores. (gh #36)
1633
1628
  print(
@@ -1670,16 +1665,10 @@ def main(
1670
1665
  )
1671
1666
  final_spec = cfg.agent_spec
1672
1667
  final_graph_name_default = cfg.graph_name
1668
+ # stream_mode is deprecated and inert (gh #62) — it only ever comes from the
1669
+ # accepted-and-ignored flag now (env/TOML no longer resolve it), so there is
1670
+ # nothing to validate; it changes no rendering.
1673
1671
  final_stream_mode = cfg.stream_mode
1674
- # The CLI's render path only supports 'updates' / 'messages'. Reject anything
1675
- # else (e.g. LANGSTAGE_STREAM_MODE=values, which bypasses the flag's Choice)
1676
- # with a clean error instead of a fatal crash deep in the stream worker.
1677
- if final_stream_mode not in ("auto", "updates", "messages"):
1678
- _status(
1679
- f"{RED}⏺ Error: unsupported stream mode {final_stream_mode!r} "
1680
- f"(use 'auto', 'updates', or 'messages')"
1681
- )
1682
- sys.exit(2)
1683
1672
  use_async = cfg.async_mode
1684
1673
  verbose = cfg.verbose
1685
1674
  # Whether a workspace root was explicitly configured (vs the default cwd);
@@ -84,16 +84,18 @@ class CodeConfig(HostConfig):
84
84
  resolver); the even-older ``DEEPAGENT_SPEC`` alias is reconciled below.
85
85
  """
86
86
 
87
+ # stream_mode is retained as an inert field (default only) so existing code and
88
+ # `--stream-mode` overrides don't break, but it is DEPRECATED: it has no effect
89
+ # since the AG-UI streaming migration (all three modes render identically). It is
90
+ # no longer resolved from LANGSTAGE_STREAM_MODE / [ui] stream_mode, and is omitted
91
+ # from `--show-config`. (gh #62)
87
92
  stream_mode: str = "auto"
88
93
  graph_name: str = "graph"
89
94
  verbose: bool = False
90
95
  async_mode: bool = False
91
96
 
92
- _ENV: ClassVar[dict] = {
93
- "stream_mode": ("LANGSTAGE_STREAM_MODE", str),
94
- }
97
+ _ENV: ClassVar[dict] = {}
95
98
  _TOML: ClassVar[dict] = {
96
- "stream_mode": "ui.stream_mode",
97
99
  "graph_name": "agent.graph_name",
98
100
  "verbose": "ui.verbose",
99
101
  "async_mode": "ui.async_mode",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langstage-cli
3
- Version: 0.6.7
3
+ Version: 0.6.9
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
@@ -41,7 +41,9 @@ Dynamic: license-file
41
41
 
42
42
  > Renamed from **deepagent-code** (the old package name now just installs this one, and the `deepagent-code` command still works). Not to be confused with LangChain's **`deepagents-code`** (`dcode`) — that's a separate project; `langstage-cli` is the terminal stage of the [LangStage family](#every-stage-for-your-langgraph-agent).
43
43
 
44
- ![langstage-cli](examples/image.png)
44
+ <p align="center">
45
+ <img src="assets/header.svg" alt="langstage-cli — the terminal stage for your LangGraph agent" width="100%" />
46
+ </p>
45
47
 
46
48
  ## Every stage for your LangGraph agent
47
49
 
@@ -146,9 +148,6 @@ langstage-cli
146
148
 
147
149
  # Working directory
148
150
  export LANGSTAGE_WORKSPACE_ROOT="/path/to/workspace"
149
-
150
- # Stream mode (updates or messages)
151
- export LANGSTAGE_STREAM_MODE="updates"
152
151
  ```
153
152
 
154
153
  ## Configuration Files
@@ -177,7 +176,6 @@ root = "."
177
176
  [ui]
178
177
  verbose = true
179
178
  async_mode = false
180
- stream_mode = "auto" # auto | updates | messages
181
179
 
182
180
  [configurable]
183
181
  # seeds LangGraph RunnableConfig.configurable
@@ -198,8 +196,6 @@ Options:
198
196
  -f, --file PATH Read message from a file (any extension)
199
197
  --interactive/--no-interactive Handle interrupts (default: interactive)
200
198
  --async-mode/--sync-mode Async streaming (default: sync)
201
- --stream-mode [auto|updates|messages]
202
- Stream mode (default: auto)
203
199
  -v, --verbose Verbose output
204
200
  --demo Run with the built-in keyless demo agent
205
201
  --show-config Print the resolved configuration and exit
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "langstage-cli"
7
- version = "0.6.7"
7
+ version = "0.6.9"
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"
@@ -21,7 +21,9 @@ def test_help_leads_with_canonical_names():
21
21
  # canonical LANGSTAGE_* / langstage.toml are present...
22
22
  assert "LANGSTAGE_AGENT_SPEC" in out
23
23
  assert "LANGSTAGE_WORKSPACE_ROOT" in out
24
- assert "LANGSTAGE_STREAM_MODE" in out
25
24
  assert "langstage.toml" in out
25
+ # ...and the deprecated, no-op stream-mode knob is no longer advertised (gh #62)
26
+ assert "LANGSTAGE_STREAM_MODE" not in out
27
+ assert "--stream-mode" not in out
26
28
  # ...and the legacy names are no longer presented as the only config vocab
27
29
  assert "DEEPAGENT_AGENT_SPEC" not in out
@@ -28,24 +28,26 @@ def test_defaults(isolated, tmp_path):
28
28
  assert cfg.agent_spec is None
29
29
 
30
30
 
31
- def test_env_stream_mode(isolated, tmp_path):
32
- cfg = CodeConfig.resolve(env={"DEEPAGENT_STREAM_MODE": "values"}, toml_start=tmp_path)
33
- assert cfg.stream_mode == "values"
34
- assert cfg.sources["stream_mode"] == "env:DEEPAGENT_STREAM_MODE"
31
+ def test_stream_mode_env_and_toml_are_deprecated_and_ignored(isolated, tmp_path):
32
+ # gh #62: stream_mode has no effect since the AG-UI migration, so it is no longer
33
+ # resolved from LANGSTAGE_STREAM_MODE / [ui] stream_mode both are now ignored and
34
+ # the field stays at its default.
35
+ _toml(tmp_path, '[ui]\nstream_mode = "messages"\n')
36
+ cfg = CodeConfig.resolve(env={"LANGSTAGE_STREAM_MODE": "updates"}, toml_start=tmp_path)
37
+ assert cfg.stream_mode == "auto" # default, neither env nor TOML applied
38
+ assert cfg.sources["stream_mode"] == "default"
35
39
 
36
40
 
37
41
  def test_toml_keys(isolated, tmp_path):
38
42
  _toml(
39
43
  tmp_path,
40
- '[agent]\nspec = "a.py:g"\ngraph_name = "myg"\n'
41
- '[ui]\nverbose = true\nasync_mode = true\nstream_mode = "values"\n',
44
+ '[agent]\nspec = "a.py:g"\ngraph_name = "myg"\n[ui]\nverbose = true\nasync_mode = true\n',
42
45
  )
43
46
  cfg = CodeConfig.resolve(env={}, toml_start=tmp_path)
44
47
  assert cfg.agent_spec == "a.py:g"
45
48
  assert cfg.graph_name == "myg"
46
49
  assert cfg.verbose is True
47
50
  assert cfg.async_mode is True
48
- assert cfg.stream_mode == "values"
49
51
 
50
52
 
51
53
  def test_deepagent_spec_alias_is_deprecated(isolated, tmp_path):
@@ -75,5 +77,6 @@ def test_overrides_win(isolated, tmp_path):
75
77
  def test_describe_lists_var_names(isolated, tmp_path):
76
78
  text = CodeConfig.resolve(env={}, toml_start=tmp_path).describe()
77
79
  assert "DEEPAGENT_AGENT_SPEC" in text
78
- assert "DEEPAGENT_STREAM_MODE" in text
79
- assert "stream_mode" in text
80
+ # A field with a live TOML key is listed with its key (stream_mode's env/TOML
81
+ # were removed as a deprecated no-op — gh #62 — so it's no longer var-backed).
82
+ assert "graph_name" in text
@@ -36,6 +36,14 @@ def test_quiet_flag_forces_clean_output(tmp_path, monkeypatch):
36
36
  assert "\x1b[" not in r.output and "⏺" not in r.output, r.output
37
37
 
38
38
 
39
+ def test_make_prompt_uses_disabled_color_globals_after_ansi_is_disabled():
40
+ c._disable_ansi()
41
+
42
+ prompt = c.make_prompt()
43
+
44
+ assert "\x1b[" not in prompt, repr(prompt)
45
+
46
+
39
47
  def test_errors_go_to_stderr_not_stdout(tmp_path, monkeypatch):
40
48
  # A failed single-shot run must keep stdout clean so a scripted caller can
41
49
  # consume the (empty) reply and read the diagnostic off stderr / the exit code.
@@ -14,13 +14,10 @@ from langstage_cli.cli import main
14
14
 
15
15
  def test_show_config_reflects_cli_flags():
16
16
  with CliRunner().isolated_filesystem(): # no stray toml
17
- r = CliRunner().invoke(
18
- main, ["-a", "fromcli.py:graph", "-v", "--stream-mode", "messages", "--show-config"]
19
- )
17
+ r = CliRunner().invoke(main, ["-a", "fromcli.py:graph", "-v", "--show-config"])
20
18
  assert r.exit_code == 0, r.output
21
19
  # CLI-set values appear with the [override] source, not [default].
22
20
  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*messages\s*\[override\]", r.output), r.output
24
21
  assert re.search(r"verbose\s*=\s*True\s*\[override\]", r.output), r.output
25
22
 
26
23
 
@@ -49,15 +46,15 @@ def test_show_config_without_flags_still_reports_env():
49
46
 
50
47
  def test_show_config_omits_server_only_keys():
51
48
  """The terminal CLI starts no server and titles the header from the graph
52
- name, so host/port/debug/title are inert and must not be advertised. (gh #36)"""
49
+ name, so host/port/debug/title are inert and must not be advertised (gh #36).
50
+ stream_mode is likewise omitted — it's a deprecated no-op (gh #62)."""
53
51
  with CliRunner().isolated_filesystem():
54
52
  r = CliRunner().invoke(main, ["--show-config"])
55
53
  assert r.exit_code == 0, r.output
56
- for key in ("host", "port", "debug", "title"):
54
+ for key in ("host", "port", "debug", "title", "stream_mode"):
57
55
  assert not re.search(rf"^\s*{key}\s*=", r.output, re.MULTILINE), f"{key} should be omitted"
58
56
  # ...but keys the CLI actually honors are still shown.
59
57
  assert re.search(r"^\s*agent_spec\s*=", r.output, re.MULTILINE), r.output
60
- assert re.search(r"^\s*stream_mode\s*=", r.output, re.MULTILINE), r.output
61
58
 
62
59
 
63
60
  def test_show_config_title_env_not_advertised_as_effective():
@@ -0,0 +1,71 @@
1
+ """--stream-mode is deprecated and inert (gh #62).
2
+
3
+ `--stream-mode {auto,updates,messages}` (and `LANGSTAGE_STREAM_MODE` / `[ui]
4
+ stream_mode`) was advertised as three streaming behaviors, but since the AG-UI
5
+ streaming migration all three render identically — the setting has no effect. The
6
+ dead knob is now: hidden from `--help`, omitted from `--show-config`, no longer
7
+ resolved from env / TOML, and accepted-and-ignored on the CLI (so existing
8
+ `--stream-mode X` invocations don't hard-error) with a one-line deprecation notice.
9
+
10
+ The unrelated "a finished AIMessage still renders" behavior (the reason the old
11
+ default was 'auto') is intrinsic to the AG-UI path now and is kept as a regression.
12
+ """
13
+
14
+ import textwrap
15
+
16
+ from click.testing import CliRunner
17
+
18
+ from langstage_cli.cli import main
19
+
20
+ # A graph whose node returns a *finished* AIMessage (no token streaming).
21
+ FINISHED_AIMESSAGE_AGENT = textwrap.dedent(
22
+ """
23
+ from langchain_core.messages import AIMessage
24
+ from langgraph.graph import START, END, StateGraph, MessagesState
25
+
26
+ def _respond(state):
27
+ return {"messages": [AIMessage(content="FINISHED_MARKER_42")]}
28
+
29
+ builder = StateGraph(MessagesState)
30
+ builder.add_node("respond", _respond)
31
+ builder.add_edge(START, "respond")
32
+ builder.add_edge("respond", END)
33
+ graph = builder.compile()
34
+ """
35
+ )
36
+
37
+
38
+ def test_finished_aimessage_still_renders(tmp_path, monkeypatch):
39
+ """Regression: a finished (non-token-streamed) AIMessage's content must render."""
40
+ (tmp_path / "fin_agent.py").write_text(FINISHED_AIMESSAGE_AGENT)
41
+ monkeypatch.chdir(tmp_path)
42
+ r = CliRunner().invoke(main, ["-a", "fin_agent.py:graph", "--no-interactive", "ping"])
43
+ assert r.exit_code == 0, r.output
44
+ assert "FINISHED_MARKER_42" in r.output
45
+
46
+
47
+ def test_stream_mode_is_omitted_from_show_config():
48
+ # gh #62: the deprecated no-op knob is no longer advertised in --show-config.
49
+ r = CliRunner().invoke(main, ["--show-config"])
50
+ assert r.exit_code == 0, r.output
51
+ assert "stream_mode" not in r.output
52
+
53
+
54
+ def test_stream_mode_flag_is_accepted_and_ignored_with_a_notice():
55
+ # An existing `--stream-mode X` invocation must not hard-error; it is accepted,
56
+ # ignored, and prints a one-line deprecation notice.
57
+ r = CliRunner().invoke(main, ["--demo", "--no-interactive", "--stream-mode", "messages", "hi"])
58
+ assert r.exit_code == 0, r.output
59
+ assert "deprecated" in r.output.lower()
60
+
61
+
62
+ def test_stream_mode_env_is_ignored_not_rejected():
63
+ # LANGSTAGE_STREAM_MODE used to be validated (and an unsupported value exited 2).
64
+ # It is now simply ignored — a run proceeds normally regardless of its value.
65
+ r = CliRunner().invoke(
66
+ main,
67
+ ["--demo", "--no-interactive", "hi"],
68
+ env={"LANGSTAGE_STREAM_MODE": "values"},
69
+ )
70
+ assert r.exit_code == 0, r.output
71
+ assert "unsupported stream mode" not in r.output.lower()
@@ -1,76 +0,0 @@
1
- """--stream-mode validation + the 'auto' default (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 {auto,updates,
6
- messages}, and the resolved value (incl. LANGSTAGE_STREAM_MODE, which bypasses
7
- the flag) is validated up front with a clean error.
8
-
9
- Separately, single 'messages' mode only carries LLM *token* streams, so a node
10
- that returns a finished (non-token-streamed) AIMessage rendered an empty turn.
11
- The default is now 'auto' (dual updates+messages) so that agent shape — the one
12
- the README's own "Creating Your Own Agent" example produces — still renders.
13
- """
14
-
15
- import textwrap
16
-
17
- from click.testing import CliRunner
18
-
19
- from langstage_cli.cli import main
20
-
21
- # A graph whose node returns a *finished* AIMessage (no token streaming) — the
22
- # shape that was a silent blank turn under bare 'messages'.
23
- FINISHED_AIMESSAGE_AGENT = textwrap.dedent(
24
- """
25
- from langchain_core.messages import AIMessage
26
- from langgraph.graph import START, END, StateGraph, MessagesState
27
-
28
- def _respond(state):
29
- return {"messages": [AIMessage(content="FINISHED_MARKER_42")]}
30
-
31
- builder = StateGraph(MessagesState)
32
- builder.add_node("respond", _respond)
33
- builder.add_edge(START, "respond")
34
- builder.add_edge("respond", END)
35
- graph = builder.compile()
36
- """
37
- )
38
-
39
-
40
- def test_default_auto_renders_finished_aimessage(tmp_path, monkeypatch):
41
- """Regression: the default mode must render a finished AIMessage's content."""
42
- (tmp_path / "fin_agent.py").write_text(FINISHED_AIMESSAGE_AGENT)
43
- monkeypatch.chdir(tmp_path)
44
- r = CliRunner().invoke(main, ["-a", "fin_agent.py:graph", "--no-interactive", "ping"])
45
- assert r.exit_code == 0, r.output
46
- assert "FINISHED_MARKER_42" in r.output
47
-
48
-
49
- def test_default_stream_mode_is_auto():
50
- r = CliRunner().invoke(main, ["--show-config"])
51
- assert r.exit_code == 0, r.output
52
- assert "auto" in r.output
53
-
54
-
55
- def test_stream_mode_values_flag_rejected_cleanly():
56
- r = CliRunner().invoke(main, ["--demo", "--no-interactive", "--stream-mode", "values", "hi"])
57
- assert r.exit_code != 0
58
- assert "values" in r.output
59
- assert "Fatal" not in r.output # no interpreter-shutdown crash
60
-
61
-
62
- def test_stream_mode_messages_flag_accepted():
63
- # A valid mode must still be accepted by the Choice (parses, doesn't error on the flag).
64
- r = CliRunner().invoke(main, ["--stream-mode", "messages", "--show-config"])
65
- assert r.exit_code == 0, r.output
66
- assert "messages" in r.output
67
-
68
-
69
- def test_stream_mode_values_via_env_rejected():
70
- r = CliRunner().invoke(
71
- main,
72
- ["--demo", "--no-interactive", "hi"],
73
- env={"LANGSTAGE_STREAM_MODE": "values"},
74
- )
75
- assert r.exit_code == 2, r.output
76
- assert "unsupported stream mode" in r.output.lower()
File without changes
File without changes