langstage-cli 0.6.44__tar.gz → 0.6.46__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 (73) hide show
  1. {langstage_cli-0.6.44/langstage_cli.egg-info → langstage_cli-0.6.46}/PKG-INFO +1 -1
  2. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/langstage_cli/cli.py +42 -14
  3. {langstage_cli-0.6.44 → langstage_cli-0.6.46/langstage_cli.egg-info}/PKG-INFO +1 -1
  4. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/langstage_cli.egg-info/SOURCES.txt +2 -0
  5. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/pyproject.toml +1 -1
  6. langstage_cli-0.6.46/tests/test_tool_card_args.py +100 -0
  7. langstage_cli-0.6.46/tests/test_usage_hint_186.py +52 -0
  8. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/LICENSE +0 -0
  9. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/README.md +0 -0
  10. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/deepagent_code/__init__.py +0 -0
  11. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/langstage_cli/__init__.py +0 -0
  12. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/langstage_cli/agui_stream.py +0 -0
  13. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/langstage_cli/config.py +0 -0
  14. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/langstage_cli/exit_codes.py +0 -0
  15. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/langstage_cli/sessions.py +0 -0
  16. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/langstage_cli.egg-info/dependency_links.txt +0 -0
  17. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/langstage_cli.egg-info/entry_points.txt +0 -0
  18. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/langstage_cli.egg-info/requires.txt +0 -0
  19. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/langstage_cli.egg-info/top_level.txt +0 -0
  20. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/setup.cfg +0 -0
  21. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_advertised_wave4.py +0 -0
  22. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_agui_stream.py +0 -0
  23. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_approval_menu_abort.py +0 -0
  24. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_async_mode.py +0 -0
  25. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_checkpointer.py +0 -0
  26. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_cli.py +0 -0
  27. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_cli_help.py +0 -0
  28. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_codeconfig.py +0 -0
  29. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_config.py +0 -0
  30. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_config_command.py +0 -0
  31. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_configurable.py +0 -0
  32. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_deferred_pass.py +0 -0
  33. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_dumb_term.py +0 -0
  34. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_exit_codes.py +0 -0
  35. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_generic_interrupt.py +0 -0
  36. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_help_render.py +0 -0
  37. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_history_persist.py +0 -0
  38. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_hitl_approval_preview.py +0 -0
  39. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_hitl_resume_warning.py +0 -0
  40. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_init.py +0 -0
  41. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_interrupt_non_tty.py +0 -0
  42. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_legacy_config_home.py +0 -0
  43. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_load_error.py +0 -0
  44. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_message_boundaries.py +0 -0
  45. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_no_color.py +0 -0
  46. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_no_interactive_approve.py +0 -0
  47. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_no_persist_resume.py +0 -0
  48. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_persist_env.py +0 -0
  49. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_quiet_output.py +0 -0
  50. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_reasoning_render.py +0 -0
  51. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_rename_shim.py +0 -0
  52. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_render_markdown.py +0 -0
  53. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_session_lifecycle.py +0 -0
  54. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_sessions.py +0 -0
  55. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_show_config.py +0 -0
  56. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_show_config_malformed.py +0 -0
  57. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_spec_loading_core.py +0 -0
  58. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_spec_resolution.py +0 -0
  59. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_spinner_reply_survives.py +0 -0
  60. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_stdin_interactive.py +0 -0
  61. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_stdin_single_shot.py +0 -0
  62. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_stream_mode.py +0 -0
  63. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_streaming_marker.py +0 -0
  64. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_terminal_width.py +0 -0
  65. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_toml_relative_paths.py +0 -0
  66. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_tool_output_escape.py +0 -0
  67. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_turn_error_verbose.py +0 -0
  68. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_turn_exit_and_render.py +0 -0
  69. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_unicode_console.py +0 -0
  70. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_verbose_command.py +0 -0
  71. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_verify_flag.py +0 -0
  72. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_workspace.py +0 -0
  73. {langstage_cli-0.6.44 → langstage_cli-0.6.46}/tests/test_wrong_type_graph.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langstage-cli
3
- Version: 0.6.44
3
+ Version: 0.6.46
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
@@ -1001,6 +1001,15 @@ def _default_agent_path() -> Optional[Path]:
1001
1001
  return path if path.exists() else None
1002
1002
 
1003
1003
 
1004
+ # A MESSAGE that is really an agent spec: ``my_agent.py:graph`` or ``pkg.module:agent``.
1005
+ _SPEC_LIKE = re.compile(r"^(?:(?:[A-Za-z]:)?[^\s:]+\.py|[A-Za-z_][\w.]*):[A-Za-z_]\w*$")
1006
+
1007
+
1008
+ def _looks_like_agent_spec(message: Optional[str]) -> bool:
1009
+ """Whether a positional MESSAGE looks like an agent spec meant for ``-a`` (gh #186)."""
1010
+ return bool(message) and bool(_SPEC_LIKE.match(message.strip()))
1011
+
1012
+
1004
1013
  def _no_agent_toml_hint(cfg: Any, toml_sources: list) -> List[str]:
1005
1014
  """Lines explaining why a ``langstage.toml`` that was read supplied no spec (gh #148).
1006
1015
 
@@ -1140,20 +1149,31 @@ def _absolutize_file_spec(spec: str) -> str:
1140
1149
  return f"{Path(path_part).expanduser().resolve()}{suffix}"
1141
1150
 
1142
1151
 
1143
- def get_tool_arg_preview(args: Dict[str, Any]) -> str:
1144
- """Get a preview of the first argument value (nanocode style).
1152
+ # Per-value cap on the tool-call card (default output; ``-v`` shows values in full).
1153
+ _TOOL_ARG_VALUE_MAX = 50
1154
+
1145
1155
 
1146
- The value is escaped before it is truncated, so a tool arg can't move the cursor
1147
- or retitle the terminal, and a cut can't leave half an escape sequence live (gh #180).
1156
+ def get_tool_arg_preview(args: Dict[str, Any], verbose: bool = False) -> str:
1157
+ """The arguments a tool was called with, for the tool-call card.
1158
+
1159
+ A lone argument shows as its bare value (``read_file └─ docs/a.md``). Several show
1160
+ as ``name=value`` pairs, every one of them: showing only the first misstated the
1161
+ call, ``add(a=17, b=25)`` read as "add 17" (gh #184). Each value is capped at 50
1162
+ characters unless ``verbose``. Values are escaped before they are truncated, so a
1163
+ tool arg can't move the cursor or retitle the terminal, and a cut can't leave half
1164
+ an escape sequence live (gh #180).
1148
1165
  """
1149
1166
  if not args:
1150
1167
  return ""
1151
- # Get first value
1152
- first_val = _escape_controls(str(list(args.values())[0]))
1153
- # Truncate if needed
1154
- if len(first_val) > 50:
1155
- return first_val[:50] + "..."
1156
- return first_val
1168
+ limit = None if verbose else _TOOL_ARG_VALUE_MAX
1169
+ if not isinstance(args, dict):
1170
+ return _approval_text(args, limit)
1171
+ if len(args) == 1:
1172
+ (value,) = args.values()
1173
+ return _approval_text(str(value), limit)
1174
+ return ", ".join(
1175
+ f"{_approval_text(str(k), None)}={_approval_text(v, limit)}" for k, v in args.items()
1176
+ )
1157
1177
 
1158
1178
 
1159
1179
  # Per-value cap for the HITL approval preview. Values are truncated, never dropped: the
@@ -1495,7 +1515,7 @@ def print_chunk(chunk: Dict[str, Any], verbose: bool = False):
1495
1515
  for tool_call in chunk["tool_calls"]:
1496
1516
  tool_name = _escape_controls(str(tool_call["name"])) # gh #180
1497
1517
  args = tool_call.get("args", {})
1498
- arg_preview = get_tool_arg_preview(args)
1518
+ arg_preview = get_tool_arg_preview(args, verbose)
1499
1519
 
1500
1520
  safe_print(f"\n{GREEN}● {tool_name}{RESET}")
1501
1521
  if arg_preview:
@@ -3457,10 +3477,18 @@ def main(
3457
3477
  # of only pointing at the env var (gh #148).
3458
3478
  for line in _no_agent_toml_hint(cfg, toml_sources):
3459
3479
  _status(f"{YELLOW}{line}{RESET}")
3480
+ # The spec went in as the MESSAGE: name the command that works (gh #186).
3481
+ if _looks_like_agent_spec(message):
3482
+ _status(
3483
+ f"{YELLOW}The agent spec goes after -a: langstage-cli -a {message.strip()}{RESET}"
3484
+ )
3460
3485
  _status(f"\n{DIM}Usage:{RESET}")
3461
- _status(" langstage-cli path/to/agent.py:graph")
3462
- _status(" langstage-cli mypackage.module:agent")
3463
- _status(f"\n{DIM}Or set the LANGSTAGE_AGENT_SPEC environment variable{RESET}")
3486
+ _status(" langstage-cli -a path/to/agent.py:graph")
3487
+ _status(" langstage-cli -a mypackage.module:agent")
3488
+ _status(
3489
+ f"\n{DIM}Or set LANGSTAGE_AGENT_SPEC, put the spec under [agent] spec in "
3490
+ f"langstage.toml, or run `langstage-cli init` for a starter agent{RESET}"
3491
+ )
3464
3492
  sys.exit(1)
3465
3493
 
3466
3494
  # The BASE directory a relative spec resolves against depends on WHERE it came
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langstage-cli
3
- Version: 0.6.44
3
+ Version: 0.6.46
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
@@ -59,10 +59,12 @@ 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
65
66
  tests/test_unicode_console.py
67
+ tests/test_usage_hint_186.py
66
68
  tests/test_verbose_command.py
67
69
  tests/test_verify_flag.py
68
70
  tests/test_workspace.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "langstage-cli"
7
- version = "0.6.44"
7
+ version = "0.6.46"
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
@@ -0,0 +1,52 @@
1
+ """gh #186: the "No agent specified" hint shows commands that work (with -a)."""
2
+
3
+ import pytest
4
+ from click.testing import CliRunner
5
+
6
+ from langstage_cli import cli
7
+ from langstage_cli.cli import _looks_like_agent_spec, main
8
+
9
+
10
+ @pytest.fixture
11
+ def no_agent(tmp_path, monkeypatch):
12
+ monkeypatch.chdir(tmp_path)
13
+ for var in ("LANGSTAGE_AGENT_SPEC", "DEEPAGENT_AGENT_SPEC", "DEEPAGENT_SPEC"):
14
+ monkeypatch.delenv(var, raising=False)
15
+ monkeypatch.setattr(cli, "_default_agent_path", lambda: None)
16
+
17
+
18
+ def test_usage_hint_uses_dash_a(no_agent):
19
+ r = CliRunner().invoke(main, ["hi"])
20
+ assert r.exit_code == 1, r.output
21
+ assert "No agent specified" in r.output
22
+ assert "langstage-cli -a path/to/agent.py:graph" in r.output
23
+ assert "langstage-cli -a mypackage.module:agent" in r.output
24
+ assert "[agent] spec" in r.output and "langstage-cli init" in r.output
25
+ # Not a spec-looking message: no "goes after -a" line.
26
+ assert "goes after -a" not in r.output
27
+
28
+
29
+ @pytest.mark.parametrize("spec", ["my_agent.py:graph", "mypackage.module:agent"])
30
+ def test_spec_passed_as_message_names_the_working_command(no_agent, spec):
31
+ r = CliRunner().invoke(main, [spec])
32
+ assert r.exit_code == 1, r.output
33
+ assert f"The agent spec goes after -a: langstage-cli -a {spec}" in r.output
34
+
35
+
36
+ @pytest.mark.parametrize(
37
+ "message,expected",
38
+ [
39
+ ("my_agent.py:graph", True),
40
+ ("./agents/a.py:graph", True),
41
+ (r"C:\work\agent.py:graph", True),
42
+ ("pkg.module:agent", True),
43
+ ("hello", False),
44
+ ("what is 2:3", False),
45
+ ("ratio 3:1 please", False),
46
+ ("http://example.org:80", False),
47
+ ("", False),
48
+ (None, False),
49
+ ],
50
+ )
51
+ def test_looks_like_agent_spec(message, expected):
52
+ assert _looks_like_agent_spec(message) is expected
File without changes
File without changes
File without changes