Skip to main content

Python stdio sidecar bridging LangGraph agents to the LangStage VS Code chat extension

Project description

langstage-vscode

langstage-vscode

Chat with your own LangGraph agent from inside VS Code — in the same chat panel as Copilot — via the @langstage chat participant.

Renamed from deepagent-vscode (the old package name now just installs this one; python -m deepagent_vscode and the old sidecar command still work).

It has two parts in one repo:

  • extension/ — a TypeScript VS Code extension that registers the @langstage chat participant and renders agent output in the chat view.
  • langstage_vscode/ — a small Python stdio sidecar that loads your agent and streams its events. Built on langstage-core, so it speaks the same typed event vocabulary as the other LangStage stages (langstage, langstage-jupyter, langstage-cli).
┌─ VS Code chat panel ────────────────────────────┐
│  @langstage  (TypeScript extension)              │
│        │  spawns                                 │
│        ▼                                          │
│  python -m langstage_vscode   (stdio sidecar)    │
│        │  NDJSON over stdin/stdout               │
│        ▼                                          │
│  your LangGraph / deepagents agent               │
└──────────────────────────────────────────────────┘

Status: early. The extension is not yet on the VS Code Marketplace (run it from source for now), and interactive approval of human-in-the-loop interrupts is not wired into the chat UI yet (the sidecar already supports the round-trip, and --repl can drive it end to end from the CLI).

Every stage for your LangGraph agent

langstage-vscode is the VS Code 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 langstage-cli -a my_agent.py:graph
VS Code langstage-vscode you are here
Reference agent langstage-hermes LANGSTAGE_AGENT_SPEC=langstage_hermes.agent:graph on any stage
Shared core langstage-core typed events + config resolver + AG-UI bridge behind every stage

📖 Full documentation: https://dkedar7.github.io/langstage-docs/

Serve over AG-UI

The sidecar already streams every turn through the in-process AG-UI adapter. Your agent — any LangGraph CompiledGraph — can also be served over the AG-UI protocol as a standalone HTTP endpoint, without changing your agent code:

pip install "langstage-core[agui]"
langstage-agui --agent my_agent.py:graph

Install

Sidecar (Python)

pip install langstage-vscode

--demo (the keyless echo stub) runs on this base install — since 0.5.0 the base deps pull the AG-UI runtime, which brings langgraph, so no extra is needed.

Extension (from source, until it's on the Marketplace)

cd extension
npm install
npm run compile

Then press F5 in VS Code (with the extension/ folder open) to launch an Extension Development Host with @langstage available.

Configure

In VS Code settings:

Setting Description Default
langstage.agentSpec Your agent, as path/to/agent.py:graph or module:graph (falls back to LANGSTAGE_AGENT_SPEC / langstage.toml)
langstage.pythonPath Python interpreter that has langstage-vscode installed python

The sidecar resolves its configuration through the family-standard chain — defaults < langstage.toml (global + project) < LANGSTAGE_* env < CLI flags — so a project with [agent] spec = "my_agent.py:graph" in its langstage.toml needs no VS Code setting at all. Inspect the resolved values:

langstage-vscode-sidecar --show-config

Preflight the interpreter and your agent before wiring up chat — --selfcheck (alias --smoke) loads the configured agent (or the demo stub), asserts it's a runnable graph, drives one turn, and exits 0 (healthy) / non-zero with a precise message (add --json for a machine-readable verdict):

langstage-vscode-sidecar --selfcheck                       # validate the runtime via the demo stub
langstage-vscode-sidecar --selfcheck --agent ./my.py:graph # validate the configured agent

--selfcheck answers "is the runtime healthy?"; --message answers "what does my agent actually say?" — it drives one turn with your prompt and prints the reply, then exits (no NDJSON + shutdown to hand-craft). Add --json to get the raw event frames instead of the assembled text:

langstage-vscode-sidecar --demo --message "hello"                       # prints the reply
langstage-vscode-sidecar --agent ./my.py:graph --message "summarize the repo"
langstage-vscode-sidecar --agent ./my.py:graph --message "hi" --json    # raw event frames

--message answers "what does my agent say once?"; --repl answers "does it remember?" — the multi-turn companion to --message. It reads one prompt per line and drives a turn, but keeps one long-lived session (a single session_id, so a single LangGraph thread_id) alive for every turn — the same per-conversation shape the VS Code extension uses — so a checkpointer-backed agent's memory persists across turns. That makes the checkpointer caveat below verifiable from the CLI in ten seconds: tell it your name, ask on the next line. Exit with Ctrl-D (EOF) or a :quit line; --json streams the raw event frames instead of the assembled text, just like --message:

langstage-vscode-sidecar --agent ./my.py:graph --repl
> my name is Kedar
...
> what is my name?
...
> :quit

An agent compiled with a checkpointer (graph.compile(checkpointer=MemorySaver())) will recall the first line on the second; one without a checkpointer won't — which is exactly the missing-checkpointer / wrong-session_id mistake to catch before wiring up the extension (see the memory note under Sidecar protocol).

Both turn-drivers are interrupt-aware. If your agent pauses on a human-in-the-loop interrupt(...) (the common deepagents / LangGraph approval pattern), the turn is no longer a silent blank — the pending action and the decisions it allows are surfaced on stderr (stdout stays the clean reply channel), and one-shot --message exits with a distinct code 2 so an interrupt is scriptable, distinct from a clean reply (0) or an error (1):

$ langstage-vscode-sidecar --agent ./hitl.py:graph --message "do it"
interrupt: agent paused awaiting a decision
  action: confirm   allowed: reject | edit | respond | approve
  resume by sending a `decision` command (add --json to see the full request)
$ echo $?
2

With --json, the raw {"type": "interrupt", ...} frame streams on stdout, so a consumer keys on it directly.

--repl can also answer the interrupt, completing the interruptdecision round-trip without hand-writing the stdio protocol. When a turn ends on an interrupt, the session enters decision mode: the next line becomes a decision on the same session, so it resumes that thread's pending interrupt.

$ printf 'do it\napprove\n:quit\n' | langstage-vscode-sidecar --agent ./hitl.py:graph --repl
interrupt: agent paused awaiting a decision
  action: confirm   allowed: reject | edit | respond | approve
  answer it here: `:decision <verb>` (or a bare `<verb>`) using a verb above
  payloads: reject [<text>] | edit <json> | respond <text>
resumed with: {'decisions': [{'type': 'approve'}]}
  • Type :decision <verb> (same :-prefixed namespace as :quit), or just the bare verb — a bare verb is only read as a decision while an interrupt is pending; the rest of the time approve is ordinary chat text.
  • The verbs come from that interrupt's own allowed_decisions, so an approval-only agent offers and accepts exactly reject | approve. Payloads follow the LangChain HITL decisions: approve, reject [<text>], respond <text>, edit <json> (free text becomes message, a JSON object is merged in, e.g. edit {"edited_action": {"name": "confirm", "args": {}}}).
  • While an interrupt is pending, a line that isn't a valid decision is refused on stderr and re-prompted with the interrupt left pending — it is never silently sent as a new message (which would just re-interrupt and look accepted) and never swallowed. :quit is always the way out.
  • --json composes: the answer line emits ack with "ref": "decision", so the trace reads ready → ack message → interrupt → complete → turn_end → ack decision → content → complete → turn_end. (Both turns emit complete — an interrupt turn is interrupt → complete → turn_end; it is paused, not finished-with-a-reply, so detect the pause via the interrupt frame, not the absence of complete.)
  • --repl exit codes: 0 on a clean session (including an interrupt that was answered), 1 if the agent could not start at all, and 2 if the session ends with an interrupt still unanswered — the same "paused awaiting a decision" signal --message uses.

You can still drive decision over the raw stdio protocol directly ({"type": "decision", "session_id": "...", "decisions": [{"type": "approve"}]}) — that is what the VS Code extension does, since interactive approval is not wired into the chat UI yet.

Your agent is any LangGraph CompiledGraph (e.g. from deepagents), exported under the name in the spec:

# my_agent.py
from deepagents import create_deep_agent
graph = create_deep_agent(...)   # -> langstage.agentSpec = "my_agent.py:graph"

Usage

Open the chat panel and start a message with @langstage:

@langstage summarize the failing tests in this repo and propose a fix

The extension streams the agent's content, tool calls, reasoning, and todo updates into the chat response.

Sidecar protocol

The extension talks to the sidecar over newline-delimited JSON. You can drive it directly for testing:

LANGSTAGE_AGENT_SPEC=./my_agent.py:graph python -m langstage_vscode

# or with no agent and no API key at all — the keyless stub runs on a base install
python -m langstage_vscode --demo

Commands (client → sidecar), one JSON object per line:

{"type": "message",  "session_id": "s1", "content": "hello"}
{"type": "decision", "session_id": "s1", "decisions": [{"type": "approve"}]}
{"type": "cancel",   "session_id": "s1"}   // abort the in-flight turn, keep the session
{"type": "shutdown"}

A cancel stops the turn currently streaming for that session_id cooperatively — it emits a distinct cancelled frame (neither complete nor error) then turn_end, and leaves the process, the session, and its in-process checkpointer alive, so the next message on the same session_id resumes with memory intact. That is the difference from killing the sidecar to stop a turn, which throws the conversation's memory away. A cancel with no turn in flight for the session is answered with an error frame (no turn in progress for session '…'), consistent with the decision/message guards.

Events (sidecar → client) — the event_to_dict() shapes from langstage-core, plus a few protocol frames:

{"type": "ready"}                          // emitted once at startup
{"type": "ack", "ref": "message"}          // command accepted
{"type": "content", "content": "..."}      // assistant text
{"type": "tool_start", "name": "...", ...} // tool call
{"type": "tool_end", "name": "...", ...}   // tool result
{"type": "interrupt", "action_requests": [...]}  // human-in-the-loop
{"type": "complete"}                       // turn finished (success) — see the note below
{"type": "cancelled", "session_id": "s1"}  // turn stopped by a `cancel` (not complete/error)
{"type": "error", "error": "..."}          // protocol error (bad/unknown command)
                                           // OR an exception raised by the agent.
                                           // On agent failure the turn emits this
                                           // INSTEAD of "complete", then "turn_end".
{"type": "turn_end", "session_id": "s1"}

A client must handle error: a malformed/unknown command, a message with no content, an invalid decision (including a well-formed one sent when the session has no pending interrupt to resume), and an agent crashing mid-turn all emit an error frame. On the agent-failure path there is no complete — the sequence is ack → error → turn_end — so don't key turn-completion off complete alone.

Two more terminal shapes are not complete. An interrupt turn emits interrupt → complete → turn_end: it does still emit complete, but the agent produced no reply — it is paused awaiting a decision, so detect the pause via the interrupt frame, not by the presence of complete. A cancelled turn (a client cancel) emits cancelled → turn_end with no complete at all — a cancelled turn is neither complete nor error.

session_id and conversational memory. The sidecar maps each session_id to a LangGraph thread_id in the run config. Multi-turn memory across messages with the same session_id therefore only works if your agent was compiled with a checkpointer (e.g. graph.compile(checkpointer=...), or create_deep_agent(..., checkpointer=...)). A plain create_react_agent graph with no checkpointer is stateless: the second turn won't remember the first, even with a matching session_id. This is expected LangGraph behavior, not a sidecar bug.

The VS Code extension keeps one sidecar process alive per conversation — it spawns the sidecar on the first @langstage message and reuses that same process (and the same session_id) for every following turn — so an in-process checkpointer like MemorySaver persists across turns in chat, not just when you drive the stdio protocol by hand (gh #54). The process is restarted on a config change (interpreter / agent spec) and when you start a new chat, so a new conversation begins with a clean thread. If you drive the sidecar yourself, keep one process alive and send each turn to it — a fresh process per message gets a fresh in-process checkpointer and forgets the prior turn; a persistent checkpointer (SqliteSaver, PostgresSaver, …) keyed by thread_id is what survives across separate processes. The --repl flag (see Configure) does exactly this — one process, one session across turns — so you can verify this memory behavior from the CLI without hand-crafting the protocol.

Development

# Sidecar
pip install -e ".[dev]"
pytest

# Extension
cd extension
npm install
npm run compile

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

langstage_vscode-0.5.19.tar.gz (147.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

langstage_vscode-0.5.19-py3-none-any.whl (30.8 kB view details)

Uploaded Python 3

File details

Details for the file langstage_vscode-0.5.19.tar.gz.

File metadata

  • Download URL: langstage_vscode-0.5.19.tar.gz
  • Upload date:
  • Size: 147.7 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for langstage_vscode-0.5.19.tar.gz
Algorithm Hash digest
SHA256 df53386f8d6daba86f2bd55860195500405dcd6204c6d787adeb657e13e9cf84
MD5 73702b8a1199b907547d0460e7523da5
BLAKE2b-256 4f9d2e319abc7387c561feeba55c7caf1c14dc558f54b4267c9334086b420ca1

See more details on using hashes here.

File details

Details for the file langstage_vscode-0.5.19-py3-none-any.whl.

File metadata

  • Download URL: langstage_vscode-0.5.19-py3-none-any.whl
  • Upload date:
  • Size: 30.8 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for langstage_vscode-0.5.19-py3-none-any.whl
Algorithm Hash digest
SHA256 185dd3c5ea3ad9ea2c67b5b93532a521eaf8a5029dd3a694e5155d627e84862b
MD5 faa6f66e15798d33c821b532ccc00b56
BLAKE2b-256 2d6f630ade41ea007d8cb4ad5c64eff38c4bdcad0c83e1e2ee3729452e6c5230

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page