langstage-cli 0.6.44__tar.gz → 0.6.45__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.44/langstage_cli.egg-info → langstage_cli-0.6.45}/PKG-INFO +1 -1
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/langstage_cli/cli.py +22 -11
- {langstage_cli-0.6.44 → langstage_cli-0.6.45/langstage_cli.egg-info}/PKG-INFO +1 -1
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/langstage_cli.egg-info/SOURCES.txt +1 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/pyproject.toml +1 -1
- langstage_cli-0.6.45/tests/test_tool_card_args.py +100 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/LICENSE +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/README.md +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/deepagent_code/__init__.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/langstage_cli/__init__.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/langstage_cli/agui_stream.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/langstage_cli/config.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/langstage_cli/exit_codes.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/langstage_cli/sessions.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/langstage_cli.egg-info/dependency_links.txt +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/langstage_cli.egg-info/entry_points.txt +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/langstage_cli.egg-info/requires.txt +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/langstage_cli.egg-info/top_level.txt +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/setup.cfg +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_advertised_wave4.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_agui_stream.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_approval_menu_abort.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_async_mode.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_checkpointer.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_cli.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_cli_help.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_codeconfig.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_config.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_config_command.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_configurable.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_deferred_pass.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_dumb_term.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_exit_codes.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_generic_interrupt.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_help_render.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_history_persist.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_hitl_approval_preview.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_hitl_resume_warning.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_init.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_interrupt_non_tty.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_legacy_config_home.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_load_error.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_message_boundaries.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_no_color.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_no_interactive_approve.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_no_persist_resume.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_persist_env.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_quiet_output.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_reasoning_render.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_rename_shim.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_render_markdown.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_session_lifecycle.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_sessions.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_show_config.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_show_config_malformed.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_spec_loading_core.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_spec_resolution.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_spinner_reply_survives.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_stdin_interactive.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_stdin_single_shot.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_stream_mode.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_streaming_marker.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_terminal_width.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_toml_relative_paths.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_tool_output_escape.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_turn_error_verbose.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_turn_exit_and_render.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_unicode_console.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_verbose_command.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_verify_flag.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_workspace.py +0 -0
- {langstage_cli-0.6.44 → langstage_cli-0.6.45}/tests/test_wrong_type_graph.py +0 -0
|
@@ -1140,20 +1140,31 @@ def _absolutize_file_spec(spec: str) -> str:
|
|
|
1140
1140
|
return f"{Path(path_part).expanduser().resolve()}{suffix}"
|
|
1141
1141
|
|
|
1142
1142
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1143
|
+
# Per-value cap on the tool-call card (default output; ``-v`` shows values in full).
|
|
1144
|
+
_TOOL_ARG_VALUE_MAX = 50
|
|
1145
1145
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1146
|
+
|
|
1147
|
+
def get_tool_arg_preview(args: Dict[str, Any], verbose: bool = False) -> str:
|
|
1148
|
+
"""The arguments a tool was called with, for the tool-call card.
|
|
1149
|
+
|
|
1150
|
+
A lone argument shows as its bare value (``read_file └─ docs/a.md``). Several show
|
|
1151
|
+
as ``name=value`` pairs, every one of them: showing only the first misstated the
|
|
1152
|
+
call, ``add(a=17, b=25)`` read as "add 17" (gh #184). Each value is capped at 50
|
|
1153
|
+
characters unless ``verbose``. Values are escaped before they are truncated, so a
|
|
1154
|
+
tool arg can't move the cursor or retitle the terminal, and a cut can't leave half
|
|
1155
|
+
an escape sequence live (gh #180).
|
|
1148
1156
|
"""
|
|
1149
1157
|
if not args:
|
|
1150
1158
|
return ""
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
if len(
|
|
1155
|
-
|
|
1156
|
-
|
|
1159
|
+
limit = None if verbose else _TOOL_ARG_VALUE_MAX
|
|
1160
|
+
if not isinstance(args, dict):
|
|
1161
|
+
return _approval_text(args, limit)
|
|
1162
|
+
if len(args) == 1:
|
|
1163
|
+
(value,) = args.values()
|
|
1164
|
+
return _approval_text(str(value), limit)
|
|
1165
|
+
return ", ".join(
|
|
1166
|
+
f"{_approval_text(str(k), None)}={_approval_text(v, limit)}" for k, v in args.items()
|
|
1167
|
+
)
|
|
1157
1168
|
|
|
1158
1169
|
|
|
1159
1170
|
# Per-value cap for the HITL approval preview. Values are truncated, never dropped: the
|
|
@@ -1495,7 +1506,7 @@ def print_chunk(chunk: Dict[str, Any], verbose: bool = False):
|
|
|
1495
1506
|
for tool_call in chunk["tool_calls"]:
|
|
1496
1507
|
tool_name = _escape_controls(str(tool_call["name"])) # gh #180
|
|
1497
1508
|
args = tool_call.get("args", {})
|
|
1498
|
-
arg_preview = get_tool_arg_preview(args)
|
|
1509
|
+
arg_preview = get_tool_arg_preview(args, verbose)
|
|
1499
1510
|
|
|
1500
1511
|
safe_print(f"\n{GREEN}● {tool_name}{RESET}")
|
|
1501
1512
|
if arg_preview:
|
|
@@ -59,6 +59,7 @@ tests/test_stream_mode.py
|
|
|
59
59
|
tests/test_streaming_marker.py
|
|
60
60
|
tests/test_terminal_width.py
|
|
61
61
|
tests/test_toml_relative_paths.py
|
|
62
|
+
tests/test_tool_card_args.py
|
|
62
63
|
tests/test_tool_output_escape.py
|
|
63
64
|
tests/test_turn_error_verbose.py
|
|
64
65
|
tests/test_turn_exit_and_render.py
|
|
@@ -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.45"
|
|
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"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""The tool-call card shows every argument, by name (gh #184).
|
|
2
|
+
|
|
3
|
+
It used to show only the first argument's value, so ``add(a=17, b=25)`` rendered as
|
|
4
|
+
``add └─ 17`` and ``transfer_funds(to, amount, currency)`` as ``└─ alice``, in default
|
|
5
|
+
and ``-v`` output alike. Under ``--no-interactive`` the card is the only record of
|
|
6
|
+
what ran.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import io
|
|
10
|
+
import re
|
|
11
|
+
from contextlib import redirect_stdout
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
pytest.importorskip("ag_ui_langgraph")
|
|
16
|
+
|
|
17
|
+
from langchain_core.messages import AIMessage, ToolMessage # noqa: E402
|
|
18
|
+
from langgraph.graph import END, START, StateGraph # noqa: E402
|
|
19
|
+
from langgraph.graph.message import MessagesState # noqa: E402
|
|
20
|
+
|
|
21
|
+
from langstage_cli import cli # noqa: E402
|
|
22
|
+
from langstage_cli.agui_stream import build_session_agent # noqa: E402
|
|
23
|
+
from langstage_cli.cli import get_tool_arg_preview, run_single_turn_agui # noqa: E402
|
|
24
|
+
|
|
25
|
+
_SGR = re.compile(r"\x1b\[[0-9;]*m")
|
|
26
|
+
TRANSFER = {"to": "alice", "amount": 1000000, "currency": "USD"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_every_argument_is_named():
|
|
30
|
+
assert get_tool_arg_preview({"a": 17, "b": 25}) == "a=17, b=25"
|
|
31
|
+
assert get_tool_arg_preview(TRANSFER) == "to=alice, amount=1000000, currency=USD"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_a_single_argument_stays_a_bare_value():
|
|
35
|
+
assert get_tool_arg_preview({"path": "docs/a.md"}) == "docs/a.md"
|
|
36
|
+
assert get_tool_arg_preview({}) == ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_values_are_capped_but_never_dropped():
|
|
40
|
+
long = "x" * 200
|
|
41
|
+
preview = get_tool_arg_preview({"path": "a.md", "content": long, "mode": "w"})
|
|
42
|
+
assert preview == f"path=a.md, content={'x' * 50}..., mode=w"
|
|
43
|
+
# Structured values read as JSON, not Python reprs.
|
|
44
|
+
assert (
|
|
45
|
+
get_tool_arg_preview({"ids": [1, 2], "opts": {"k": "v"}}) == 'ids=[1, 2], opts={"k": "v"}'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_verbose_shows_values_in_full():
|
|
50
|
+
long = "y" * 200
|
|
51
|
+
assert (
|
|
52
|
+
get_tool_arg_preview({"path": "a.md", "content": long}, verbose=True)
|
|
53
|
+
== f"path=a.md, content={long}"
|
|
54
|
+
)
|
|
55
|
+
assert get_tool_arg_preview({"q": long}, verbose=True) == long
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_names_and_values_are_escaped():
|
|
59
|
+
preview = get_tool_arg_preview({"p\x1b[2K": "a\x1b]0;t\x07", "b": "c\nd"})
|
|
60
|
+
assert "\x1b" not in preview and "\x07" not in preview and "\n" not in preview
|
|
61
|
+
assert preview == r"p\x1b[2K=a\x1b]0;t\x07, b=c\nd"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _transfer_graph():
|
|
65
|
+
def call(state):
|
|
66
|
+
tc = {"id": "t1", "name": "transfer_funds", "args": TRANSFER}
|
|
67
|
+
return {"messages": [AIMessage(content="", tool_calls=[tc])]}
|
|
68
|
+
|
|
69
|
+
def tool(state):
|
|
70
|
+
return {"messages": [ToolMessage(content="ok", tool_call_id="t1")]}
|
|
71
|
+
|
|
72
|
+
def reply(state):
|
|
73
|
+
return {"messages": [AIMessage(content="Transferred.")]}
|
|
74
|
+
|
|
75
|
+
g = StateGraph(MessagesState)
|
|
76
|
+
for n, f in [("call", call), ("tool", tool), ("reply", reply)]:
|
|
77
|
+
g.add_node(n, f)
|
|
78
|
+
g.add_edge(START, "call")
|
|
79
|
+
g.add_edge("call", "tool")
|
|
80
|
+
g.add_edge("tool", "reply")
|
|
81
|
+
g.add_edge("reply", END)
|
|
82
|
+
return g.compile()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.parametrize("verbose", [False, True])
|
|
86
|
+
async def test_card_in_a_real_turn(monkeypatch, verbose):
|
|
87
|
+
"""The issue's keyless repro, through the AG-UI stream the CLI renders."""
|
|
88
|
+
monkeypatch.setattr(cli, "Spinner", lambda *a, **k: None)
|
|
89
|
+
buf = io.StringIO()
|
|
90
|
+
with redirect_stdout(buf):
|
|
91
|
+
await run_single_turn_agui(
|
|
92
|
+
build_session_agent(_transfer_graph()),
|
|
93
|
+
"pay alice",
|
|
94
|
+
"t-184",
|
|
95
|
+
interactive=False,
|
|
96
|
+
verbose=verbose,
|
|
97
|
+
)
|
|
98
|
+
plain = _SGR.sub("", buf.getvalue())
|
|
99
|
+
assert "● transfer_funds" in plain, plain
|
|
100
|
+
assert "└─ to=alice, amount=1000000, currency=USD" in plain, plain
|
|
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
|
|
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
|
|
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
|