langstage-cli
The terminal stage for your LangGraph agent. A Claude Code-style CLI that runs any LangGraph CompiledGraph — yours, not a bundled one — with streaming, tool-call rendering, and human-in-the-loop approval.
Renamed from deepagent-code (the old package name now just installs this one, and the
deepagent-codecommand still works). Not to be confused with LangChain'sdeepagents-code(dcode) — that's a separate project;langstage-cliis the terminal stage of the LangStage family.
The keyless demo agent (langstage-cli -a langstage_core.demo.tools:graph), recorded by CI against the latest release. Docs for the terminal stage
Every stage for your LangGraph agent
langstage-cli is the terminal stage of the LangStage family: write your agent once — any LangGraph CompiledGraph — and run it on every stage with the same spec string (module:attr or path/to/file.py:attr), the same langstage.toml config file, and the same LANGSTAGE_* environment variables.
| Stage | Package | Try it |
|---|---|---|
| Web app | langstage | langstage run --agent my_agent.py:graph |
| JupyterLab | langstage-jupyter | pip install langstage-jupyter, then the chat sidebar in jupyter lab |
| Terminal | langstage-cli | you are here |
| VS Code | langstage-vscode | chat participant + stdio sidecar |
| Reference agent | langstage-hermes | LANGSTAGE_AGENT_SPEC=langstage_hermes.agent:graph on any stage |
| Shared core | langstage-core | AG-UI streaming bridge + config resolver behind every stage |
📖 Full documentation: https://dkedar7.github.io/langstage-docs/
Serve over AG-UI
This surface's agent — any LangGraph CompiledGraph — can also be served over the AG-UI protocol as a standalone HTTP endpoint:
pip install "langstage-core[agui]"
langstage-agui --agent my_agent.py:graph
Installation
pip install langstage-cli
Or install directly from GitHub:
pip install git+https://github.com/dkedar7/langstage-cli.git
Quick Start
No agent or API key yet? See the CLI working in one command:
langstage-cli --demo "hello"
Point it at your own agent (any LangGraph CompiledGraph):
export ANTHROPIC_API_KEY="your_api_key" # if your agent calls Anthropic
langstage-cli -a path/to/your_agent.py:graph
This launches an interactive conversation loop with your agent.
Usage
# Keyless demo agent — no API key, no agent of your own
langstage-cli --demo "Hello"
# Keyless demo of tool calls, reasoning and HITL: say "use a tool", "think" or "ask me"
# ("think" streams its chain of thought, dimmed under "✻ Thinking", before the answer)
langstage-cli -a langstage_core.demo.tools:graph "use a tool"
# Scaffold a runnable starter agent + langstage.toml (then just run it)
langstage-cli init
# Send a message directly to your agent
langstage-cli -a my_agent.py:graph "Hello, agent!"
# Specify a custom agent file
langstage-cli -a my_agent.py:graph
# Use a module path
langstage-cli -a mypackage.agents:chatbot
# Read message from a file
langstage-cli -f ./prompt.md
# Non-interactive mode (auto-approve tool calls)
langstage-cli --no-interactive
# Verbose output (node labels; full traceback when a load or a turn errors)
langstage-cli -v
# Keyless demo agent (no API key needed)
langstage-cli --demo
# Print the resolved configuration: each value, its source, and the
# env var / langstage.toml key that sets it
langstage-cli --show-config
# Resume a previous conversation across separate invocations
langstage-cli "remember: my name is Kedar" # a fresh, PERSISTED session
langstage-cli -c "what's my name?" # continue the most recent session
langstage-cli --list-sessions # list resumable sessions
langstage-cli --resume <id> # resume a specific one
Getting started with your own agent
langstage-cli --demo "hi" # see the CLI work, keyless
langstage-cli init # scaffold my_agent.py + langstage.toml (your own graph)
langstage-cli "hi" # runs MY agent — no -a, no hand-editing
init writes a minimal, immediately-runnable starter agent (the stdlib StateGraph
example below — needs only langgraph, a base dependency) plus a langstage.toml
pointing at it, so the next langstage-cli "..." runs your graph. It refuses to
overwrite an existing my_agent.py / langstage.toml unless you pass --force.
Resuming a session
Every invocation persists its conversation to a durable, per-workspace store so a later run can pick it up — the terminal-agent continuity of Claude Code / Codex / Aider, without you baking a checkpointer into your own graph:
--continue/-c— resume the most recent session for this workspace and keep going. If there's no prior session, it just starts a fresh one.--resume <id>— resume a specific session (ids come from--list-sessions; a bare--resumealso lists them). A short prefix works if it's unambiguous; an ambiguous one is an error that lists the sessions it matches.- A plain run with no flag starts a new session that is still persisted, so it can be continued later.
State lives in ~/.langstage/sessions/<workspace-hash>.sqlite (honoring
LANGSTAGE_CONFIG_HOME; override the whole location with LANGSTAGE_CLI_SESSIONS_DIR).
Persistence is on by default — disable it with --no-persist, LANGSTAGE_PERSIST=0, or
[session] persist = false in langstage.toml. A graph that compiles in its own
checkpointer keeps it (yours always wins); the CLI only supplies a durable one when your
graph has none. Then the CLI's store is not used: /config says so, --list-sessions
marks those sessions, and an in-memory checkpointer (MemorySaver) gets a note on stderr,
because nothing it holds survives the run. A pinned [configurable] thread_id now genuinely persists across runs.
--continue / --resume imply persistence over LANGSTAGE_PERSIST / [session] persist
(a CLI flag outranks them), but an explicit --no-persist on the same command line wins:
langstage-cli --no-persist -c "..." resumes the session read-only — the run sees the
prior conversation, but nothing is written back (no new turns, no index update, and no
store is created if none exists).
Commands
In the interactive loop:
/help(/h,/?) - Show this help message, or/help <command>for one command/status(/s) - Show session status (agent, thread, verbose, cwd)/version(/v) - Show version and the current agent/config(/cfg) - Show the resolved configuration, or set a runtime key:/config [key] [value]/verbose- Toggle verbose output, or set it explicitly:/verbose [on|off]/history(/hist) - Show recent conversation messages, optionally/history [N]/clear(/c) - Clear conversation history/reset(/restart) - Reset the session (clear history and start a new thread)/quit(/q,/exit) - Exit- Tab autocompletes commands; Ctrl+C exits
Environment Variables
# Agent location (path/to/file.py:variable_name or module:variable)
# (DEEPAGENT_AGENT_SPEC / DEEPAGENT_SPEC still accepted as deprecated aliases)
export LANGSTAGE_AGENT_SPEC="my_agent.py:graph"
langstage-cli
# Working directory
export LANGSTAGE_WORKSPACE_ROOT="/path/to/workspace"
Configuration Files
langstage-cli reads TOML config from two locations and merges them
(project overrides global):
- Global:
~/.langstage/config.toml - Project:
langstage.tomlin the current directory or any ancestor
Legacy locations (~/.deepagents/config.toml, deepagents.toml) are still
read as fallbacks; move your config when convenient — ~/.deepagents/ now
belongs to LangChain's dcode.
Precedence: CLI args > env vars > project TOML > global TOML > defaults.
Relative paths in a TOML file resolve against that file's directory, like a path in
pyproject.toml. A relative [agent] spec or [workspace] root in a project
langstage.toml means "relative to the project root", so the project runs the same from
any subdirectory. The same rule applies to the global file: a relative path there
resolves against ~/.langstage/. So in the global config, use ~/... or an absolute
path (or a package.module:graph spec), e.g. spec = "~/agents/my_agent.py:graph".
A path from -a or an env var resolves against the directory you run the command from.
--show-config prints the resolved path. A file-path agent can import modules that sit
next to it, and a package.module:graph spec also finds a package in the project directory.
Example langstage.toml:
[agent]
spec = "my_agent.py:graph"
[workspace]
root = "."
[ui]
verbose = true
[configurable]
# seeds LangGraph RunnableConfig.configurable
thread_id = "my-thread"
CLI Options
Usage: langstage-cli [OPTIONS] [MESSAGE]
Arguments:
MESSAGE Optional input to send to the agent immediately
Options:
-a, --agent TEXT Agent spec (path/to/file.py:graph or module:graph)
-g, --graph-name TEXT Graph variable name (default: "graph")
-f, --file PATH Read message from a file (any extension)
--interactive/--no-interactive Handle interrupts (default: interactive)
-v, --verbose Verbose output
--demo Run with the built-in keyless demo agent
--show-config Print the resolved configuration and exit
-q, --quiet Scriptable single-shot output: only the reply
--verify Preflight the agent (one real turn); exit 0/1
-c, --continue Resume the most recent session for this workspace
--resume [ID] Resume a specific session (bare: list them)
--list-sessions List resumable sessions for this workspace
--persist/--no-persist Persist this session so it can be continued (default: on)
--force For `init`: overwrite existing files
--version Show the version and exit
Subcommand:
init Scaffold my_agent.py + langstage.toml in the current dir
Verifying an agent (CI gate)
--verify loads the configured agent and runs one real turn, exiting 0 if it
completed cleanly and non-zero otherwise — so you can gate on it in CI before
trusting an agent. It catches a missing key, a broken tool, or a non-runnable graph
that a static "it imports" check would wave through (it runs the same
langstage-core preflight every LangStage surface uses):
langstage-cli --verify -a my_agent.py:graph || { echo "agent broken" >&2; exit 1; }
Scriptable output
A single-shot run (a MESSAGE argument or -f/--file) prints only the agent's
reply — no header, spinner, tool chatter, reasoning, timing, or color — as soon as its
output is piped (stdout isn't a TTY). Errors and diagnostics go to stderr, and so does
anything your agent module prints while it is being imported. The process exits
non-zero if the turn failed, so a run is safe to capture:
answer=$(langstage-cli --demo "say hi") || echo "run failed" >&2
echo "$answer" # -> (demo agent) You said: say hi
Piped stdin works the same way. With no MESSAGE or -f, everything on stdin is sent
as one message, like -f. Lines starting with / are part of the message, not
commands. An empty MESSAGE or empty stdin is an error, not a silent no-op. A terminal
still gets the interactive REPL. On Windows that includes mintty / Git Bash, where stdin
is an MSYS pty pipe rather than a console. <NUL counts as empty stdin, not a terminal:
cat prompt.txt | langstage-cli --demo
Pass -q/--quiet to force the same clean output in a terminal. Set
NO_COLOR to any non-empty value to turn color off in a terminal
too; the layout and spinner stay, uncolored. With TERM=dumb (Emacs shell, vim :!)
the CLI sends no escape sequences at all: no color, a spinner that prints once, and an
approval menu you answer by typing a number.
Exit codes
langstage-cli uses the exit codes every LangStage command line shares
(langstage-core ADR 0007):
| Code | Meaning |
|---|---|
0 |
success: the turn completed, --verify passed, init wrote its files |
1 |
failure: no agent configured, the agent failed to load, the turn errored or produced nothing, --verify failed, --resume found no session, init refused to overwrite |
2 |
paused on a human-in-the-loop interrupt: an approval is needed but stdin is not a terminal, or you left the approval prompt with Exit or Ctrl-C. --no-interactive auto-approves instead |
64 |
usage error: an unknown option or bad value, --demo with -a, --continue with --resume, MESSAGE with -f, an empty MESSAGE |
Creating Your Own Agent
Your agent file just needs to export a compiled LangGraph graph — langstage-cli
runs any CompiledGraph. Your message goes in as the graph's messages input, and
the reply it renders is the graph's messages channel. A graph whose state has no
messages (say {"query", "answer"}) doesn't receive your text: a node that reads
state["query"] fails with KeyError, and one that tolerates the missing key runs and
the CLI prints its final state as JSON, with a note on stderr. To drive such a graph from
the CLI, give it a messages channel, or a first node that copies the last message into
query. A turn with no message and no state to show is an error (exit 1), the same
verdict --verify gives.
A spec's inline :name (app.py:prod) wins over -g / [agent] graph_name;
--show-config reports the graph that runs, and a run notes the ignored name.
A minimal stdlib example (no extra deps):
# my_agent.py — needs only langgraph (a base dependency)
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import MessagesState
from langchain_core.messages import AIMessage
def respond(state):
last = state["messages"][-1].content
return {"messages": [AIMessage(content=f"You said: {last}")]}
g = StateGraph(MessagesState)
g.add_node("respond", respond)
g.add_edge(START, "respond")
g.add_edge("respond", END)
graph = g.compile()
Or a full deep agent (requires pip install deepagents):
# my_agent.py
from deepagents import create_deep_agent
from langgraph.checkpoint.memory import MemorySaver
agent = create_deep_agent(
name="My Agent",
model="anthropic:claude-sonnet-4-6",
checkpointer=MemorySaver(),
)
Then run it:
langstage-cli -a my_agent.py:graph # or :agent for the deepagents example
Programmatic Use
Since 1.0, streaming runs through the shared core's in-process AG-UI adapter
(pip install "langstage-core[agui]"):
import asyncio
from langstage_core import load_agent_spec
from langstage_core.agui import build_agent, iter_chunk_frames
agent = build_agent(load_agent_spec("my_agent.py:graph"))
async def main():
async for chunk in iter_chunk_frames(agent, "Hello!", thread_id="s1"):
if chunk.get("chunk"):
print(chunk["chunk"], end="")
asyncio.run(main())
License
MIT License - see LICENSE file for details.
Metadata
Release files for langstage-cli 0.6.46
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| langstage_cli-0.6.46.tar.gz | 149.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| langstage_cli-0.6.46-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 220.7 kB
Release files / langstage_cli-0.6.46.tar.gz
| Download URL | langstage_cli-0.6.46.tar.gz |
|---|---|
| Size | 149.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b36bf86a248d4eb503475280a20398c9acb2d25d669f69526503f9bf410a632a
|
|
BLAKE2b-256 checksum How to use checksums |
4567f9fe438091400500791134405ed4a2ab012e4d135166322fed411930bebd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / langstage_cli-0.6.46-py3-none-any.whl
| Download URL | langstage_cli-0.6.46-py3-none-any.whl |
|---|---|
| Size | 71.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
89f8facb60147cb930eac8008f30e07e4adb2cffc8ff8989635893d6b210b714
|
|
BLAKE2b-256 checksum How to use checksums |
4b892164ce0036a9ba60a3e245514cf4dc57e78ccc313447fdd6afd7bbe8bdab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for langstage-cli 0.6.46
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.