langstage-cli 0.6.1__tar.gz → 0.6.2__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.1/langstage_cli.egg-info → langstage_cli-0.6.2}/PKG-INFO +19 -11
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/README.md +16 -8
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/langstage_cli/cli.py +32 -16
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/langstage_cli/config.py +6 -8
- {langstage_cli-0.6.1 → langstage_cli-0.6.2/langstage_cli.egg-info}/PKG-INFO +19 -11
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/langstage_cli.egg-info/SOURCES.txt +1 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/langstage_cli.egg-info/requires.txt +2 -2
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/pyproject.toml +3 -3
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_config.py +7 -4
- langstage_cli-0.6.2/tests/test_turn_exit_and_render.py +72 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/LICENSE +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/deepagent_code/__init__.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/langstage_cli/__init__.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/langstage_cli/agui_stream.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/langstage_cli.egg-info/dependency_links.txt +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/langstage_cli.egg-info/entry_points.txt +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/langstage_cli.egg-info/top_level.txt +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/setup.cfg +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_agui_stream.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_checkpointer.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_cli.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_cli_help.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_codeconfig.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_help_render.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_no_interactive_approve.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_rename_shim.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_show_config.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_spec_resolution.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_stream_mode.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_streaming_marker.py +0 -0
- {langstage_cli-0.6.1 → langstage_cli-0.6.2}/tests/test_unicode_console.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.2
|
|
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: langstage-core[agui]>=1.0
|
|
22
|
+
Requires-Dist: langstage-core[agui]>=1.0.4
|
|
23
23
|
Requires-Dist: click>=8.0.0
|
|
24
24
|
Requires-Dist: python-dotenv
|
|
25
25
|
Provides-Extra: dev
|
|
@@ -32,7 +32,7 @@ Requires-Dist: deepagents>=0.3; extra == "dev"
|
|
|
32
32
|
Requires-Dist: ag-ui-langgraph>=0.0.41; extra == "dev"
|
|
33
33
|
Requires-Dist: fastapi; extra == "dev"
|
|
34
34
|
Provides-Extra: agui
|
|
35
|
-
Requires-Dist: langstage-core[agui]>=1.0; extra == "agui"
|
|
35
|
+
Requires-Dist: langstage-core[agui]>=1.0.4; extra == "agui"
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
38
|
# langstage-cli
|
|
@@ -54,16 +54,16 @@ langstage-cli is the terminal stage of the **LangStage family**: write your agen
|
|
|
54
54
|
| Terminal | langstage-cli | **you are here** |
|
|
55
55
|
| VS Code | [langstage-vscode](https://github.com/dkedar7/langstage-vscode) | chat participant + stdio sidecar |
|
|
56
56
|
| Reference agent | [langstage-hermes](https://github.com/dkedar7/langstage-hermes) | `LANGSTAGE_AGENT_SPEC=langstage_hermes.agent:graph` on any stage |
|
|
57
|
-
| Shared core | [
|
|
57
|
+
| Shared core | [langstage-core](https://github.com/dkedar7/langstage-core) | typed events + config resolver behind every stage |
|
|
58
58
|
|
|
59
59
|
📖 **Full documentation:** <https://dkedar7.github.io/langstage-docs/>
|
|
60
60
|
|
|
61
61
|
### Serve over AG-UI
|
|
62
62
|
|
|
63
|
-
This surface's agent — any LangGraph `CompiledGraph` — can also be served over the [AG-UI protocol](https://github.com/dkedar7/
|
|
63
|
+
This surface's agent — any LangGraph `CompiledGraph` — can also be served over the [AG-UI protocol](https://github.com/dkedar7/langstage-core) as a standalone HTTP endpoint:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
pip install "
|
|
66
|
+
pip install "langstage-core[agui]"
|
|
67
67
|
langstage-agui --agent my_agent.py:graph
|
|
68
68
|
```
|
|
69
69
|
|
|
@@ -249,14 +249,22 @@ langstage-cli -a my_agent.py:graph # or :agent for the deepagents example
|
|
|
249
249
|
|
|
250
250
|
## Programmatic Use
|
|
251
251
|
|
|
252
|
+
Since 1.0, streaming runs through the shared core's in-process AG-UI adapter
|
|
253
|
+
(`pip install "langstage-core[agui]"`):
|
|
254
|
+
|
|
252
255
|
```python
|
|
253
|
-
|
|
256
|
+
import asyncio
|
|
257
|
+
from langstage_core import load_agent_spec
|
|
258
|
+
from langstage_core.agui import build_agent, iter_chunk_frames
|
|
259
|
+
|
|
260
|
+
agent = build_agent(load_agent_spec("my_agent.py:graph"))
|
|
254
261
|
|
|
255
|
-
|
|
262
|
+
async def main():
|
|
263
|
+
async for chunk in iter_chunk_frames(agent, "Hello!", thread_id="s1"):
|
|
264
|
+
if chunk.get("chunk"):
|
|
265
|
+
print(chunk["chunk"], end="")
|
|
256
266
|
|
|
257
|
-
|
|
258
|
-
if chunk.get("chunk"):
|
|
259
|
-
print(chunk["chunk"], end="")
|
|
267
|
+
asyncio.run(main())
|
|
260
268
|
```
|
|
261
269
|
|
|
262
270
|
## License
|
|
@@ -17,16 +17,16 @@ langstage-cli is the terminal stage of the **LangStage family**: write your agen
|
|
|
17
17
|
| Terminal | langstage-cli | **you are here** |
|
|
18
18
|
| VS Code | [langstage-vscode](https://github.com/dkedar7/langstage-vscode) | chat participant + stdio sidecar |
|
|
19
19
|
| Reference agent | [langstage-hermes](https://github.com/dkedar7/langstage-hermes) | `LANGSTAGE_AGENT_SPEC=langstage_hermes.agent:graph` on any stage |
|
|
20
|
-
| Shared core | [
|
|
20
|
+
| Shared core | [langstage-core](https://github.com/dkedar7/langstage-core) | typed events + config resolver behind every stage |
|
|
21
21
|
|
|
22
22
|
📖 **Full documentation:** <https://dkedar7.github.io/langstage-docs/>
|
|
23
23
|
|
|
24
24
|
### Serve over AG-UI
|
|
25
25
|
|
|
26
|
-
This surface's agent — any LangGraph `CompiledGraph` — can also be served over the [AG-UI protocol](https://github.com/dkedar7/
|
|
26
|
+
This surface's agent — any LangGraph `CompiledGraph` — can also be served over the [AG-UI protocol](https://github.com/dkedar7/langstage-core) as a standalone HTTP endpoint:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
pip install "
|
|
29
|
+
pip install "langstage-core[agui]"
|
|
30
30
|
langstage-agui --agent my_agent.py:graph
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -212,14 +212,22 @@ langstage-cli -a my_agent.py:graph # or :agent for the deepagents example
|
|
|
212
212
|
|
|
213
213
|
## Programmatic Use
|
|
214
214
|
|
|
215
|
+
Since 1.0, streaming runs through the shared core's in-process AG-UI adapter
|
|
216
|
+
(`pip install "langstage-core[agui]"`):
|
|
217
|
+
|
|
215
218
|
```python
|
|
216
|
-
|
|
219
|
+
import asyncio
|
|
220
|
+
from langstage_core import load_agent_spec
|
|
221
|
+
from langstage_core.agui import build_agent, iter_chunk_frames
|
|
222
|
+
|
|
223
|
+
agent = build_agent(load_agent_spec("my_agent.py:graph"))
|
|
217
224
|
|
|
218
|
-
|
|
225
|
+
async def main():
|
|
226
|
+
async for chunk in iter_chunk_frames(agent, "Hello!", thread_id="s1"):
|
|
227
|
+
if chunk.get("chunk"):
|
|
228
|
+
print(chunk["chunk"], end="")
|
|
219
229
|
|
|
220
|
-
|
|
221
|
-
if chunk.get("chunk"):
|
|
222
|
-
print(chunk["chunk"], end="")
|
|
230
|
+
asyncio.run(main())
|
|
223
231
|
```
|
|
224
232
|
|
|
225
233
|
## License
|
|
@@ -618,15 +618,20 @@ def print_chunk(chunk: Dict[str, Any], verbose: bool = False):
|
|
|
618
618
|
print_chunk._streaming_text = True
|
|
619
619
|
print(text, end="")
|
|
620
620
|
else:
|
|
621
|
-
# Print the cyan bullet ONCE at the start of a streamed AI turn
|
|
622
|
-
#
|
|
623
|
-
# model emits one chunk per token, so
|
|
624
|
-
#
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
621
|
+
# Print the cyan bullet ONCE at the start of a streamed AI turn AND
|
|
622
|
+
# again when the node changes mid-turn, then append subsequent tokens
|
|
623
|
+
# with no marker. A token-streaming model emits one chunk per token, so
|
|
624
|
+
# a per-chunk marker jammed a `⏺` before every token (gh #34); but a
|
|
625
|
+
# per-turn-only marker ran two nodes' messages together on one line with
|
|
626
|
+
# no separator (gh #43). Break + re-mark on a node change.
|
|
627
|
+
if not print_chunk._streaming_text or print_chunk._streaming_node != node:
|
|
628
|
+
if print_chunk._streaming_text:
|
|
629
|
+
print() # node changed mid-turn — break before the new marker
|
|
628
630
|
print(f"{CYAN}⏺{RESET} {render_markdown(text)}", end="")
|
|
631
|
+
print_chunk._streaming_node = node
|
|
629
632
|
print_chunk._streaming_text = True
|
|
633
|
+
else:
|
|
634
|
+
print(render_markdown(text), end="")
|
|
630
635
|
|
|
631
636
|
# Handle tool calls - green tool name
|
|
632
637
|
elif "tool_calls" in chunk:
|
|
@@ -1171,8 +1176,10 @@ async def run_single_turn_agui(
|
|
|
1171
1176
|
thread_id: str,
|
|
1172
1177
|
interactive: bool = True,
|
|
1173
1178
|
verbose: bool = False,
|
|
1174
|
-
) -> float:
|
|
1175
|
-
"""
|
|
1179
|
+
) -> tuple[float, bool]:
|
|
1180
|
+
"""Stream a turn through the in-process AG-UI adapter. Returns
|
|
1181
|
+
``(elapsed_seconds, had_error)`` — ``had_error`` is True if any frame reported
|
|
1182
|
+
``status == "error"``, so a single-shot caller can exit non-zero (gh #47).
|
|
1176
1183
|
adapter, rendering with the same ``print_chunk``. Text + tool calls/results
|
|
1177
1184
|
reach parity with the default path (and tool *results* are also shown).
|
|
1178
1185
|
|
|
@@ -1185,6 +1192,7 @@ async def run_single_turn_agui(
|
|
|
1185
1192
|
|
|
1186
1193
|
print_chunk._streaming_text = False # fresh marker state per turn (gh #34)
|
|
1187
1194
|
start_time = time.time()
|
|
1195
|
+
had_error = False
|
|
1188
1196
|
resume = None # first pass sends the message; later passes carry the decision
|
|
1189
1197
|
|
|
1190
1198
|
while True:
|
|
@@ -1199,6 +1207,8 @@ async def run_single_turn_agui(
|
|
|
1199
1207
|
spinner.stop()
|
|
1200
1208
|
first_chunk = False
|
|
1201
1209
|
print_chunk(chunk, verbose=verbose)
|
|
1210
|
+
if chunk.get("status") == "error":
|
|
1211
|
+
had_error = True
|
|
1202
1212
|
if chunk.get("status") == "interrupt":
|
|
1203
1213
|
has_interrupt = True
|
|
1204
1214
|
interrupt_data = chunk.get("interrupt", {})
|
|
@@ -1222,7 +1232,7 @@ async def run_single_turn_agui(
|
|
|
1222
1232
|
else:
|
|
1223
1233
|
break
|
|
1224
1234
|
|
|
1225
|
-
return time.time() - start_time
|
|
1235
|
+
return time.time() - start_time, had_error
|
|
1226
1236
|
|
|
1227
1237
|
|
|
1228
1238
|
def run_conversation_loop(
|
|
@@ -1284,15 +1294,17 @@ def run_conversation_loop(
|
|
|
1284
1294
|
print(f"{initial_message}")
|
|
1285
1295
|
print()
|
|
1286
1296
|
|
|
1287
|
-
duration = asyncio.run(
|
|
1297
|
+
duration, had_error = asyncio.run(
|
|
1288
1298
|
run_single_turn_agui(agui_agent, initial_message, thread_id, interactive, verbose)
|
|
1289
1299
|
)
|
|
1290
1300
|
print_timing(duration, verbose)
|
|
1291
1301
|
print()
|
|
1292
1302
|
|
|
1293
|
-
# Exit after single-shot execution
|
|
1303
|
+
# Exit after single-shot execution. Propagate the turn's error status so
|
|
1304
|
+
# main() can exit non-zero — a single-shot/piped caller must be able to tell
|
|
1305
|
+
# a failed run from a success (gh #47).
|
|
1294
1306
|
if single_shot:
|
|
1295
|
-
return
|
|
1307
|
+
return had_error
|
|
1296
1308
|
|
|
1297
1309
|
# Main conversation loop
|
|
1298
1310
|
while True:
|
|
@@ -1355,7 +1367,7 @@ def run_conversation_loop(
|
|
|
1355
1367
|
print() # Space before response
|
|
1356
1368
|
|
|
1357
1369
|
# Run the agent (AG-UI is the only streaming path since langstage-core 1.0)
|
|
1358
|
-
duration = asyncio.run(
|
|
1370
|
+
duration, _ = asyncio.run(
|
|
1359
1371
|
run_single_turn_agui(agui_agent, user_input, thread_id, interactive, verbose)
|
|
1360
1372
|
)
|
|
1361
1373
|
print_timing(duration, verbose)
|
|
@@ -1622,8 +1634,10 @@ def main(
|
|
|
1622
1634
|
agent_description = get_agent_description(graph)
|
|
1623
1635
|
|
|
1624
1636
|
# Run the conversation loop
|
|
1625
|
-
# Single-shot mode: exit after processing if message was provided via CLI
|
|
1626
|
-
|
|
1637
|
+
# Single-shot mode: exit after processing if message was provided via CLI.
|
|
1638
|
+
# In single-shot mode it returns whether the agent turn errored, so a piped /
|
|
1639
|
+
# scripted caller can tell a failed run from a success (gh #47).
|
|
1640
|
+
turn_had_error = run_conversation_loop(
|
|
1627
1641
|
graph=graph,
|
|
1628
1642
|
config=config_dict,
|
|
1629
1643
|
agent_name=agent_name,
|
|
@@ -1636,6 +1650,8 @@ def main(
|
|
|
1636
1650
|
initial_message=message,
|
|
1637
1651
|
single_shot=bool(message),
|
|
1638
1652
|
)
|
|
1653
|
+
if turn_had_error:
|
|
1654
|
+
sys.exit(1)
|
|
1639
1655
|
|
|
1640
1656
|
except FileNotFoundError as e:
|
|
1641
1657
|
print(f"{RED}⏺ Error: {e}{RESET}")
|
|
@@ -11,7 +11,6 @@ lookups.
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import os
|
|
14
|
-
import tomllib
|
|
15
14
|
import warnings
|
|
16
15
|
from dataclasses import dataclass
|
|
17
16
|
from pathlib import Path
|
|
@@ -25,15 +24,14 @@ class ConfigError(Exception):
|
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
def load_config(start: Optional[Path] = None) -> Tuple[dict, List[Path]]:
|
|
28
|
-
"""Load global + project ``
|
|
27
|
+
"""Load global + project ``langstage.toml`` (legacy ``deepagents.toml``), merged.
|
|
29
28
|
|
|
30
|
-
Delegates to the shared loader so every
|
|
31
|
-
|
|
29
|
+
Delegates to the shared loader so every LangStage tool reads the same files with
|
|
30
|
+
the same precedence. Since langstage-core 1.0.3 a malformed config file is skipped
|
|
31
|
+
with a stderr notice rather than raising — a typo in a config file must not crash
|
|
32
|
+
the CLI (gh langstage-jupyter#42). Returns ``(config, sources_used)``.
|
|
32
33
|
"""
|
|
33
|
-
|
|
34
|
-
return load_toml_config(start)
|
|
35
|
-
except tomllib.TOMLDecodeError as e:
|
|
36
|
-
raise ConfigError(f"Invalid TOML: {e}") from e
|
|
34
|
+
return load_toml_config(start)
|
|
37
35
|
|
|
38
36
|
|
|
39
37
|
def get(config: dict, dotted_key: str, default: Any = None) -> Any:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langstage-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
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: langstage-core[agui]>=1.0
|
|
22
|
+
Requires-Dist: langstage-core[agui]>=1.0.4
|
|
23
23
|
Requires-Dist: click>=8.0.0
|
|
24
24
|
Requires-Dist: python-dotenv
|
|
25
25
|
Provides-Extra: dev
|
|
@@ -32,7 +32,7 @@ Requires-Dist: deepagents>=0.3; extra == "dev"
|
|
|
32
32
|
Requires-Dist: ag-ui-langgraph>=0.0.41; extra == "dev"
|
|
33
33
|
Requires-Dist: fastapi; extra == "dev"
|
|
34
34
|
Provides-Extra: agui
|
|
35
|
-
Requires-Dist: langstage-core[agui]>=1.0; extra == "agui"
|
|
35
|
+
Requires-Dist: langstage-core[agui]>=1.0.4; extra == "agui"
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
38
|
# langstage-cli
|
|
@@ -54,16 +54,16 @@ langstage-cli is the terminal stage of the **LangStage family**: write your agen
|
|
|
54
54
|
| Terminal | langstage-cli | **you are here** |
|
|
55
55
|
| VS Code | [langstage-vscode](https://github.com/dkedar7/langstage-vscode) | chat participant + stdio sidecar |
|
|
56
56
|
| Reference agent | [langstage-hermes](https://github.com/dkedar7/langstage-hermes) | `LANGSTAGE_AGENT_SPEC=langstage_hermes.agent:graph` on any stage |
|
|
57
|
-
| Shared core | [
|
|
57
|
+
| Shared core | [langstage-core](https://github.com/dkedar7/langstage-core) | typed events + config resolver behind every stage |
|
|
58
58
|
|
|
59
59
|
📖 **Full documentation:** <https://dkedar7.github.io/langstage-docs/>
|
|
60
60
|
|
|
61
61
|
### Serve over AG-UI
|
|
62
62
|
|
|
63
|
-
This surface's agent — any LangGraph `CompiledGraph` — can also be served over the [AG-UI protocol](https://github.com/dkedar7/
|
|
63
|
+
This surface's agent — any LangGraph `CompiledGraph` — can also be served over the [AG-UI protocol](https://github.com/dkedar7/langstage-core) as a standalone HTTP endpoint:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
pip install "
|
|
66
|
+
pip install "langstage-core[agui]"
|
|
67
67
|
langstage-agui --agent my_agent.py:graph
|
|
68
68
|
```
|
|
69
69
|
|
|
@@ -249,14 +249,22 @@ langstage-cli -a my_agent.py:graph # or :agent for the deepagents example
|
|
|
249
249
|
|
|
250
250
|
## Programmatic Use
|
|
251
251
|
|
|
252
|
+
Since 1.0, streaming runs through the shared core's in-process AG-UI adapter
|
|
253
|
+
(`pip install "langstage-core[agui]"`):
|
|
254
|
+
|
|
252
255
|
```python
|
|
253
|
-
|
|
256
|
+
import asyncio
|
|
257
|
+
from langstage_core import load_agent_spec
|
|
258
|
+
from langstage_core.agui import build_agent, iter_chunk_frames
|
|
259
|
+
|
|
260
|
+
agent = build_agent(load_agent_spec("my_agent.py:graph"))
|
|
254
261
|
|
|
255
|
-
|
|
262
|
+
async def main():
|
|
263
|
+
async for chunk in iter_chunk_frames(agent, "Hello!", thread_id="s1"):
|
|
264
|
+
if chunk.get("chunk"):
|
|
265
|
+
print(chunk["chunk"], end="")
|
|
256
266
|
|
|
257
|
-
|
|
258
|
-
if chunk.get("chunk"):
|
|
259
|
-
print(chunk["chunk"], end="")
|
|
267
|
+
asyncio.run(main())
|
|
260
268
|
```
|
|
261
269
|
|
|
262
270
|
## License
|
|
@@ -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.2"
|
|
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"
|
|
@@ -29,7 +29,7 @@ dependencies = [
|
|
|
29
29
|
# in-process AG-UI adapter — there is no built-in parser fallback — so the
|
|
30
30
|
# AG-UI runtime (ag-ui-langgraph[fastapi] + uvicorn, via core's [agui] extra)
|
|
31
31
|
# is a HARD dep. A bare `pip install langstage-cli` must be able to run a turn.
|
|
32
|
-
"langstage-core[agui]>=1.0",
|
|
32
|
+
"langstage-core[agui]>=1.0.4",
|
|
33
33
|
"click>=8.0.0",
|
|
34
34
|
"python-dotenv",
|
|
35
35
|
]
|
|
@@ -53,7 +53,7 @@ dev = [
|
|
|
53
53
|
]
|
|
54
54
|
# Redundant since AG-UI moved into base deps (core 1.0); kept as a no-op alias so
|
|
55
55
|
# existing `pip install langstage-cli[agui]` scripts/READMEs still resolve.
|
|
56
|
-
agui = ["langstage-core[agui]>=1.0"]
|
|
56
|
+
agui = ["langstage-core[agui]>=1.0.4"]
|
|
57
57
|
|
|
58
58
|
[project.scripts]
|
|
59
59
|
langstage-cli = "langstage_cli.cli:main"
|
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
import pytest
|
|
8
7
|
|
|
9
8
|
from langstage_cli import config
|
|
10
9
|
|
|
@@ -70,13 +69,17 @@ def test_walks_up_for_project_config(tmp_path, monkeypatch):
|
|
|
70
69
|
assert sources == [project / "deepagents.toml"]
|
|
71
70
|
|
|
72
71
|
|
|
73
|
-
def
|
|
72
|
+
def test_invalid_toml_degrades_gracefully(tmp_path, monkeypatch):
|
|
73
|
+
# Since langstage-core 1.0.3 (gh langstage-jupyter#42) a malformed config file is
|
|
74
|
+
# skipped with a stderr notice, NOT raised — a typo in a config must not crash the
|
|
75
|
+
# CLI (config resolves at import time on sibling surfaces). load_config() returns
|
|
76
|
+
# usable (empty) config instead of raising.
|
|
74
77
|
home = tmp_path / "home"
|
|
75
78
|
_write(home / "config.toml", "this is = not = valid toml\n")
|
|
76
79
|
monkeypatch.setenv("DEEPAGENTS_CONFIG_HOME", str(home))
|
|
77
80
|
monkeypatch.chdir(tmp_path)
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
cfg, _sources = config.load_config() # must NOT raise
|
|
82
|
+
assert cfg == {} # the malformed file was skipped, no garbage loaded
|
|
80
83
|
|
|
81
84
|
|
|
82
85
|
def test_get_dotted_path():
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Regressions for gh #47 (error exit code) and gh #43 (multi-node run-on).
|
|
2
|
+
|
|
3
|
+
#47 — a runtime error inside the agent used to exit 0, so a single-shot / piped
|
|
4
|
+
caller couldn't tell a failed run from a success. run_single_turn_agui now reports
|
|
5
|
+
whether the turn errored so main() can exit non-zero.
|
|
6
|
+
|
|
7
|
+
#43 — non-verbose streaming printed one `⏺` marker per turn and appended every
|
|
8
|
+
chunk, so two nodes' messages ran together on one line. It now starts a fresh
|
|
9
|
+
marker on a node change (the node is carried on each chunk since langstage-core 1.0.4).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import io
|
|
13
|
+
from contextlib import redirect_stdout
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
pytest.importorskip("ag_ui_langgraph")
|
|
18
|
+
pytest.importorskip("fastapi")
|
|
19
|
+
|
|
20
|
+
from langstage_cli.agui_stream import build_session_agent # noqa: E402
|
|
21
|
+
from langstage_cli.cli import print_chunk, run_single_turn_agui # noqa: E402
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def test_turn_reports_error_frame_for_nonzero_exit(monkeypatch):
|
|
25
|
+
# gh #47: an error FRAME (e.g. a RunErrorEvent — a recursion-limit failure) is
|
|
26
|
+
# displayed but the run still "completes", so before the fix the CLI exited 0
|
|
27
|
+
# and a single-shot caller couldn't tell a failed run from a success. (A raised
|
|
28
|
+
# exception is separately caught by main() -> exit 1; this is the framed case.)
|
|
29
|
+
async def fake_stream(agent, message, thread_id, resume=None):
|
|
30
|
+
yield {"status": "streaming", "chunk": "partial output", "node": "agent"}
|
|
31
|
+
yield {"status": "error", "error": "Recursion limit of 25 reached"}
|
|
32
|
+
yield {"status": "complete"}
|
|
33
|
+
|
|
34
|
+
import langstage_cli.agui_stream as agui_mod
|
|
35
|
+
|
|
36
|
+
monkeypatch.setattr(agui_mod, "agui_stream_updates", fake_stream)
|
|
37
|
+
_elapsed, had_error = await run_single_turn_agui(object(), "hi", "t-err", interactive=False)
|
|
38
|
+
assert had_error is True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def test_successful_turn_reports_no_error():
|
|
42
|
+
from langstage_core import load_agent_spec
|
|
43
|
+
|
|
44
|
+
agent = build_session_agent(load_agent_spec("langstage_core.demo.stub:graph"))
|
|
45
|
+
_elapsed, had_error = await run_single_turn_agui(agent, "hi", "t-ok", interactive=False)
|
|
46
|
+
assert had_error is False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_print_chunk_breaks_on_node_change_nonverbose():
|
|
50
|
+
# gh #43: two nodes' output must not render as one run-on line.
|
|
51
|
+
print_chunk._streaming_text = False
|
|
52
|
+
print_chunk._streaming_node = None
|
|
53
|
+
buf = io.StringIO()
|
|
54
|
+
with redirect_stdout(buf):
|
|
55
|
+
print_chunk({"status": "streaming", "chunk": "first out", "node": "first"})
|
|
56
|
+
print_chunk({"status": "streaming", "chunk": "second out", "node": "second"})
|
|
57
|
+
out = buf.getvalue()
|
|
58
|
+
assert "first out" in out and "second out" in out
|
|
59
|
+
assert "first outsecond out" not in out # the two nodes are separated, not a run-on
|
|
60
|
+
assert out.count("⏺") == 2 # a fresh marker per node
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_print_chunk_same_node_stays_on_one_marker():
|
|
64
|
+
# A token-streamed reply from ONE node keeps a single marker (gh #34 not undone).
|
|
65
|
+
print_chunk._streaming_text = False
|
|
66
|
+
print_chunk._streaming_node = None
|
|
67
|
+
buf = io.StringIO()
|
|
68
|
+
with redirect_stdout(buf):
|
|
69
|
+
print_chunk({"status": "streaming", "chunk": "hel", "node": "agent"})
|
|
70
|
+
print_chunk({"status": "streaming", "chunk": "lo", "node": "agent"})
|
|
71
|
+
out = buf.getvalue()
|
|
72
|
+
assert out.count("⏺") == 1
|
|
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
|