langstage-cli 0.6.4__tar.gz → 0.6.6__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 (34) hide show
  1. {langstage_cli-0.6.4/langstage_cli.egg-info → langstage_cli-0.6.6}/PKG-INFO +16 -3
  2. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/README.md +13 -0
  3. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/langstage_cli/cli.py +42 -15
  4. {langstage_cli-0.6.4 → langstage_cli-0.6.6/langstage_cli.egg-info}/PKG-INFO +16 -3
  5. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/langstage_cli.egg-info/SOURCES.txt +3 -1
  6. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/langstage_cli.egg-info/requires.txt +2 -2
  7. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/pyproject.toml +4 -3
  8. langstage_cli-0.6.6/tests/test_verify_flag.py +45 -0
  9. langstage_cli-0.6.6/tests/test_workspace.py +48 -0
  10. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/LICENSE +0 -0
  11. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/deepagent_code/__init__.py +0 -0
  12. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/langstage_cli/__init__.py +0 -0
  13. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/langstage_cli/agui_stream.py +0 -0
  14. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/langstage_cli/config.py +0 -0
  15. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/langstage_cli.egg-info/dependency_links.txt +0 -0
  16. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/langstage_cli.egg-info/entry_points.txt +0 -0
  17. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/langstage_cli.egg-info/top_level.txt +0 -0
  18. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/setup.cfg +0 -0
  19. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_agui_stream.py +0 -0
  20. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_checkpointer.py +0 -0
  21. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_cli.py +0 -0
  22. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_cli_help.py +0 -0
  23. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_codeconfig.py +0 -0
  24. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_config.py +0 -0
  25. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_help_render.py +0 -0
  26. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_no_interactive_approve.py +0 -0
  27. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_quiet_output.py +0 -0
  28. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_rename_shim.py +0 -0
  29. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_show_config.py +0 -0
  30. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_spec_resolution.py +0 -0
  31. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_stream_mode.py +0 -0
  32. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_streaming_marker.py +0 -0
  33. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/tests/test_turn_exit_and_render.py +0 -0
  34. {langstage_cli-0.6.4 → langstage_cli-0.6.6}/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.4
3
+ Version: 0.6.6
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.4
22
+ Requires-Dist: langstage-core[agui]>=1.0.7
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.4; extra == "agui"
35
+ Requires-Dist: langstage-core[agui]>=1.0.7; extra == "agui"
36
36
  Dynamic: license-file
37
37
 
38
38
  # langstage-cli
@@ -204,9 +204,22 @@ Options:
204
204
  --demo Run with the built-in keyless demo agent
205
205
  --show-config Print the resolved configuration and exit
206
206
  -q, --quiet Scriptable single-shot output: only the reply
207
+ --verify Preflight the agent (one real turn); exit 0/1
207
208
  --version Show the version and exit
208
209
  ```
209
210
 
211
+ ### Verifying an agent (CI gate)
212
+
213
+ `--verify` loads the configured agent and runs **one real turn**, exiting `0` if it
214
+ completed cleanly and non-zero otherwise — so you can gate on it in CI before
215
+ trusting an agent. It catches a missing key, a broken tool, or a non-runnable graph
216
+ that a static "it imports" check would wave through (it runs the same
217
+ `langstage-core` preflight every LangStage surface uses):
218
+
219
+ ```bash
220
+ langstage-cli --verify -a my_agent.py:graph || { echo "agent broken" >&2; exit 1; }
221
+ ```
222
+
210
223
  ### Scriptable output
211
224
 
212
225
  A single-shot run (a `MESSAGE` argument or `-f/--file`) prints only the agent's
@@ -167,9 +167,22 @@ Options:
167
167
  --demo Run with the built-in keyless demo agent
168
168
  --show-config Print the resolved configuration and exit
169
169
  -q, --quiet Scriptable single-shot output: only the reply
170
+ --verify Preflight the agent (one real turn); exit 0/1
170
171
  --version Show the version and exit
171
172
  ```
172
173
 
174
+ ### Verifying an agent (CI gate)
175
+
176
+ `--verify` loads the configured agent and runs **one real turn**, exiting `0` if it
177
+ completed cleanly and non-zero otherwise — so you can gate on it in CI before
178
+ trusting an agent. It catches a missing key, a broken tool, or a non-runnable graph
179
+ that a static "it imports" check would wave through (it runs the same
180
+ `langstage-core` preflight every LangStage surface uses):
181
+
182
+ ```bash
183
+ langstage-cli --verify -a my_agent.py:graph || { echo "agent broken" >&2; exit 1; }
184
+ ```
185
+
173
186
  ### Scriptable output
174
187
 
175
188
  A single-shot run (a `MESSAGE` argument or `-f/--file`) prints only the agent's
@@ -18,7 +18,7 @@ from typing import Any, Dict, List, Optional, Tuple
18
18
 
19
19
  import click
20
20
 
21
- from langstage_core import load_agent_spec
21
+ from langstage_core import apply_workspace, load_agent_spec
22
22
  from langstage_cli import config as config_module
23
23
 
24
24
  # Platform-specific imports for keyboard input
@@ -1514,6 +1514,15 @@ def run_conversation_loop(
1514
1514
  "chatter, timing, and color, and emit only the agent's reply. Auto-enabled "
1515
1515
  "when a single-shot run is piped (stdout is not a TTY).",
1516
1516
  )
1517
+ @click.option(
1518
+ "--verify",
1519
+ "verify_agent",
1520
+ is_flag=True,
1521
+ default=False,
1522
+ help="Preflight the configured agent: run ONE real turn and exit 0 if it "
1523
+ "completed cleanly, non-zero otherwise. Catches a missing key / broken tool "
1524
+ "/ bad graph before you rely on it (e.g. in CI).",
1525
+ )
1517
1526
  def main(
1518
1527
  message: Optional[str],
1519
1528
  agent_spec: Optional[str],
@@ -1527,6 +1536,7 @@ def main(
1527
1536
  agui: bool,
1528
1537
  show_config: bool,
1529
1538
  quiet: bool,
1539
+ verify_agent: bool,
1530
1540
  ):
1531
1541
  """
1532
1542
  Run a LangGraph agent from the command line.
@@ -1561,6 +1571,7 @@ def main(
1561
1571
  langstage-cli -f ./prompt.md
1562
1572
  langstage-cli --demo "try it with no API key"
1563
1573
  langstage-cli --show-config
1574
+ langstage-cli --verify -a my_agent.py # preflight one real turn; exit 0/1
1564
1575
  """
1565
1576
  # Windows consoles default to cp1252, where the spinner (Braille frames) and
1566
1577
  # status glyphs (✓ ⏺ —) raise UnicodeEncodeError — the documented
@@ -1572,16 +1583,17 @@ def main(
1572
1583
  except (AttributeError, ValueError): # non-reconfigurable stream
1573
1584
  pass
1574
1585
 
1575
- # Scriptable single-shot output (gh #53). A single-shot run (a MESSAGE arg or
1576
- # -f/--file) that is piped — stdout is not a TTY — auto-enables quiet so the
1577
- # consumer gets only the reply; --quiet forces it even in a terminal. Color is
1578
- # additionally stripped whenever stdout is not a TTY, matching well-behaved CLIs.
1586
+ # Scriptable output (gh #53). A single-shot run (a MESSAGE arg or -f/--file) or
1587
+ # a --verify preflight that is piped — stdout is not a TTY — auto-enables quiet
1588
+ # so the consumer gets only the reply/verdict (no spinner or "Loaded" line);
1589
+ # --quiet forces it in a terminal. Color is additionally stripped whenever stdout
1590
+ # is not a TTY, matching well-behaved CLIs.
1579
1591
  try:
1580
1592
  _is_tty = sys.stdout.isatty()
1581
1593
  except (AttributeError, ValueError):
1582
1594
  _is_tty = False
1583
1595
  global _QUIET
1584
- _QUIET = quiet or (bool(message or prompt_file) and not _is_tty)
1596
+ _QUIET = quiet or ((bool(message or prompt_file) or verify_agent) and not _is_tty)
1585
1597
  if _QUIET or not _is_tty:
1586
1598
  _disable_ansi()
1587
1599
 
@@ -1661,10 +1673,9 @@ def main(
1661
1673
  sys.exit(2)
1662
1674
  use_async = cfg.async_mode
1663
1675
  verbose = cfg.verbose
1664
- # Only change directory when a workspace root was actually configured.
1665
- workspace_root = (
1666
- cfg.workspace_root if cfg.sources.get("workspace_root") != "default" else None
1667
- )
1676
+ # Whether a workspace root was explicitly configured (vs the default cwd);
1677
+ # cli chdirs into it only when it was, matching prior behavior.
1678
+ workspace_explicit = cfg.sources.get("workspace_root") != "default"
1668
1679
 
1669
1680
  # If no spec provided, try the default agent
1670
1681
  if not final_spec:
@@ -1684,11 +1695,12 @@ def main(
1684
1695
  # under workspace_root, not where the user is and put the file. (gh #30)
1685
1696
  final_spec = _absolutize_file_spec(final_spec)
1686
1697
 
1687
- # Change to workspace root if specified
1688
- if workspace_root:
1689
- workspace_path = Path(workspace_root).expanduser().resolve()
1690
- if workspace_path.exists():
1691
- os.chdir(workspace_path)
1698
+ # Apply the resolved workspace as the single source of truth (ADR 0005):
1699
+ # publish it (env + active) so the agent's tools can read workspace_root(),
1700
+ # and chdir into it (cli is single-process) when one was explicitly
1701
+ # configured. Runs AFTER _absolutize_file_spec so `-a my_agent.py` still
1702
+ # resolves against the invocation cwd, not the workspace (gh #30).
1703
+ apply_workspace(Path(cfg.workspace_root).expanduser(), chdir=workspace_explicit)
1692
1704
 
1693
1705
  # Load the graph with a spinner (both are chrome; quiet mode stays silent
1694
1706
  # until the reply). (gh #53)
@@ -1700,6 +1712,21 @@ def main(
1700
1712
  loading.stop()
1701
1713
  print(f"{GREEN}✓{RESET} {DIM}Loaded {final_spec}{RESET}")
1702
1714
 
1715
+ # --verify: preflight the configured agent by running ONE real turn through
1716
+ # the shared core primitive (langstage-core >= 1.0.6), then exit on its
1717
+ # verdict. A green here means the agent actually completed a turn — not just
1718
+ # that it imported — so `langstage-cli --verify -a my_agent.py` is a real CI
1719
+ # gate. Delegates to core.verify so "healthy" means the same across surfaces.
1720
+ if verify_agent:
1721
+ from langstage_core.agui import verify as _core_verify
1722
+
1723
+ result = _core_verify(graph)
1724
+ if result.ok:
1725
+ print(f"{GREEN}✓{RESET} agent verified: {result.reason}")
1726
+ sys.exit(0)
1727
+ _status(f"{RED}✗ agent verification failed: {result.reason}{RESET}")
1728
+ sys.exit(1)
1729
+
1703
1730
  # Seed LangGraph RunnableConfig from TOML [configurable] table if present
1704
1731
  config_dict: Dict[str, Any] = {"configurable": {}}
1705
1732
  toml_configurable = config_module.get(toml_config, "configurable")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langstage-cli
3
- Version: 0.6.4
3
+ Version: 0.6.6
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.4
22
+ Requires-Dist: langstage-core[agui]>=1.0.7
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.4; extra == "agui"
35
+ Requires-Dist: langstage-core[agui]>=1.0.7; extra == "agui"
36
36
  Dynamic: license-file
37
37
 
38
38
  # langstage-cli
@@ -204,9 +204,22 @@ Options:
204
204
  --demo Run with the built-in keyless demo agent
205
205
  --show-config Print the resolved configuration and exit
206
206
  -q, --quiet Scriptable single-shot output: only the reply
207
+ --verify Preflight the agent (one real turn); exit 0/1
207
208
  --version Show the version and exit
208
209
  ```
209
210
 
211
+ ### Verifying an agent (CI gate)
212
+
213
+ `--verify` loads the configured agent and runs **one real turn**, exiting `0` if it
214
+ completed cleanly and non-zero otherwise — so you can gate on it in CI before
215
+ trusting an agent. It catches a missing key, a broken tool, or a non-runnable graph
216
+ that a static "it imports" check would wave through (it runs the same
217
+ `langstage-core` preflight every LangStage surface uses):
218
+
219
+ ```bash
220
+ langstage-cli --verify -a my_agent.py:graph || { echo "agent broken" >&2; exit 1; }
221
+ ```
222
+
210
223
  ### Scriptable output
211
224
 
212
225
  A single-shot run (a `MESSAGE` argument or `-f/--file`) prints only the agent's
@@ -27,4 +27,6 @@ tests/test_spec_resolution.py
27
27
  tests/test_stream_mode.py
28
28
  tests/test_streaming_marker.py
29
29
  tests/test_turn_exit_and_render.py
30
- tests/test_unicode_console.py
30
+ tests/test_unicode_console.py
31
+ tests/test_verify_flag.py
32
+ tests/test_workspace.py
@@ -1,10 +1,10 @@
1
1
  langgraph>=0.2.0
2
- langstage-core[agui]>=1.0.4
2
+ langstage-core[agui]>=1.0.7
3
3
  click>=8.0.0
4
4
  python-dotenv
5
5
 
6
6
  [agui]
7
- langstage-core[agui]>=1.0.4
7
+ langstage-core[agui]>=1.0.7
8
8
 
9
9
  [dev]
10
10
  pytest>=7.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "langstage-cli"
7
- version = "0.6.4"
7
+ version = "0.6.6"
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,8 @@ 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.4",
32
+ # >=1.0.6 for the shared preflight primitive core.verify() used by --verify.
33
+ "langstage-core[agui]>=1.0.7",
33
34
  "click>=8.0.0",
34
35
  "python-dotenv",
35
36
  ]
@@ -53,7 +54,7 @@ dev = [
53
54
  ]
54
55
  # Redundant since AG-UI moved into base deps (core 1.0); kept as a no-op alias so
55
56
  # existing `pip install langstage-cli[agui]` scripts/READMEs still resolve.
56
- agui = ["langstage-core[agui]>=1.0.4"]
57
+ agui = ["langstage-core[agui]>=1.0.7"]
57
58
 
58
59
  [project.scripts]
59
60
  langstage-cli = "langstage_cli.cli:main"
@@ -0,0 +1,45 @@
1
+ """`--verify` preflights the configured agent with a real turn (ADR 0004 adoption).
2
+
3
+ It delegates to `langstage_core.agui.verify`, so a green here means the agent
4
+ actually completed a turn — a real CI gate — not just that it imported. Exit 0 on
5
+ success, non-zero on failure, with the diagnostic on stderr so stdout stays clean.
6
+ """
7
+
8
+ import textwrap
9
+
10
+ from click.testing import CliRunner
11
+
12
+ from langstage_cli.cli import main
13
+
14
+ _BROKEN_AGENT = textwrap.dedent(
15
+ """
16
+ from langgraph.graph import StateGraph, START, END, MessagesState
17
+
18
+ def boom(state):
19
+ raise RuntimeError("tool exploded")
20
+
21
+ b = StateGraph(MessagesState)
22
+ b.add_node("boom", boom)
23
+ b.add_edge(START, "boom")
24
+ b.add_edge("boom", END)
25
+ graph = b.compile()
26
+ """
27
+ )
28
+
29
+
30
+ def test_verify_demo_passes_exit_zero(tmp_path, monkeypatch):
31
+ monkeypatch.chdir(tmp_path)
32
+ r = CliRunner().invoke(main, ["--demo", "--verify"])
33
+ assert r.exit_code == 0, r.output
34
+ assert "verified" in r.output # the pass verdict, on stdout
35
+ # A preflight is not a chat: no header/marker leaks.
36
+ assert "⏺" not in r.output and "\x1b[" not in r.output, r.output
37
+
38
+
39
+ def test_verify_broken_agent_fails_exit_one(tmp_path, monkeypatch):
40
+ (tmp_path / "broken.py").write_text(_BROKEN_AGENT)
41
+ monkeypatch.chdir(tmp_path)
42
+ r = CliRunner().invoke(main, ["-a", "broken.py:graph", "--verify"])
43
+ assert r.exit_code == 1
44
+ assert r.stdout.strip() == "", r.stdout # stdout stays clean for scripting
45
+ assert "verification failed" in r.stderr, r.stderr
@@ -0,0 +1,48 @@
1
+ """cli applies the resolved workspace via core.apply_workspace (ADR 0005).
2
+
3
+ The acceptance behavior all the workspace bugs were about: with a workspace
4
+ configured, a turn whose agent writes a *relative* file must land that file in the
5
+ workspace, not the launch cwd.
6
+ """
7
+
8
+ import os
9
+ from pathlib import Path
10
+
11
+ from click.testing import CliRunner
12
+
13
+ from langstage_cli.cli import main
14
+
15
+ _WRITER_AGENT = (
16
+ "from pathlib import Path\n"
17
+ "from langgraph.graph import StateGraph, START, END, MessagesState\n"
18
+ "from langchain_core.messages import AIMessage\n"
19
+ "def node(s):\n"
20
+ " Path('marker.txt').write_text('hi')\n"
21
+ " return {'messages': [AIMessage(content='wrote marker')]}\n"
22
+ "b = StateGraph(MessagesState)\n"
23
+ "b.add_node('n', node)\n"
24
+ "b.add_edge(START, 'n')\n"
25
+ "b.add_edge('n', END)\n"
26
+ "graph = b.compile()\n"
27
+ )
28
+
29
+
30
+ def test_configured_workspace_is_where_the_agent_writes(tmp_path, monkeypatch):
31
+ ws = tmp_path / "ws"
32
+ agent = tmp_path / "writer.py"
33
+ agent.write_text(_WRITER_AGENT)
34
+ # Workspace comes from the env (cli has no --workspace flag); track both names
35
+ # so monkeypatch restores them (apply_workspace also sets the legacy one).
36
+ monkeypatch.setenv("LANGSTAGE_WORKSPACE_ROOT", str(ws))
37
+ monkeypatch.setenv("DEEPAGENT_WORKSPACE_ROOT", "")
38
+
39
+ origin = Path.cwd()
40
+ try:
41
+ r = CliRunner().invoke(main, ["-a", f"{agent}:graph", "go"])
42
+ finally:
43
+ os.chdir(origin) # cli chdir'd into the workspace; restore for other tests
44
+
45
+ assert r.exit_code == 0, r.output
46
+ # The relative write landed in the configured workspace, not the launch cwd.
47
+ assert (ws / "marker.txt").read_text() == "hi"
48
+ assert not (origin / "marker.txt").exists()
File without changes
File without changes