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.
- {langstage_cli-0.6.7/langstage_cli.egg-info → langstage_cli-0.6.9}/PKG-INFO +4 -8
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/README.md +3 -7
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli/cli.py +25 -36
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli/config.py +6 -4
- {langstage_cli-0.6.7 → langstage_cli-0.6.9/langstage_cli.egg-info}/PKG-INFO +4 -8
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/pyproject.toml +1 -1
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_cli_help.py +3 -1
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_codeconfig.py +12 -9
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_quiet_output.py +8 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_show_config.py +4 -7
- langstage_cli-0.6.9/tests/test_stream_mode.py +71 -0
- langstage_cli-0.6.7/tests/test_stream_mode.py +0 -76
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/LICENSE +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/deepagent_code/__init__.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli/__init__.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli/agui_stream.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli.egg-info/SOURCES.txt +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli.egg-info/dependency_links.txt +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli.egg-info/entry_points.txt +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli.egg-info/requires.txt +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/langstage_cli.egg-info/top_level.txt +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/setup.cfg +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_agui_stream.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_checkpointer.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_cli.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_config.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_configurable.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_help_render.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_no_interactive_approve.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_rename_shim.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_spec_resolution.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_streaming_marker.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_turn_exit_and_render.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_unicode_console.py +0 -0
- {langstage_cli-0.6.7 → langstage_cli-0.6.9}/tests/test_verify_flag.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
+
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|