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.
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 | typed events + 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"
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, 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.
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 chose Exit at the approval prompt. --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. The reply it renders is the graph's messages channel.
A graph whose state has no messages (say {"query", "answer"}) still runs: the CLI
prints its final state as JSON, with a note on stderr. 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.
Release files for langstage-cli 0.6.36
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| langstage_cli-0.6.36.tar.gz | 131.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| langstage_cli-0.6.36-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 198.3 kB
Release files / langstage_cli-0.6.36.tar.gz
| Download URL | langstage_cli-0.6.36.tar.gz |
|---|---|
| Size | 131.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9d80eed88b34a2d2545b7d1a2e61433a5c3e25b83126bd032f7353e5839781d0
|
|
BLAKE2b-256 checksum How to use checksums |
3cd984947a193b71e2aac3f04062f23043361c6544e030963f50fe0dfbf446a9
|
| 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.36-py3-none-any.whl
| Download URL | langstage_cli-0.6.36-py3-none-any.whl |
|---|---|
| Size | 66.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7efb0284e976f6da9e11ee277156c4f72b238b6661766442fe4f48fc3ab6e6c1
|
|
BLAKE2b-256 checksum How to use checksums |
cb9c510c29aaaf57bbdaa82a32fcbdabf8d2d28ca07f57268855ee4518912914
|
| 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}
|