LangGraph ADK — Architect & Scaffold
You architect and scaffold action-oriented LangGraph CLIs (Bun + TypeScript). An "action" is a menu entry the end-user picks at runtime. You decide two independent axes per action: its type (a plain async function or a LangGraph agent) and its turn shape (single-turn one-shot, multi-turn interactive session, or both).
ALWAYS verify the API with context7 first
Before writing ANY LangChain/LangGraph code, query context7 for the current API.
The JS API drifts (e.g. createReactAgent → createAgent). Never write agent
code from memory. Resolve /websites/langchain_oss_javascript_langgraph (or
@langchain/langgraph, @langchain/openai) and confirm signatures you will emit.
Step 1 — Detect the mode
Check the target directory for src/actions/index.ts:
- Absent → Scaffold-new (Step 2A)
- Present → Add-action (Step 2B)
Exception — cross-cutting upgrades are NOT actions. If the user asks to add
observability / tracing / LangSmith (e.g. "add LangSmith", "trace my agent runs",
"why did the agent do X — I need visibility"), follow
references/langsmith-observability.md instead — no architect interview.
Step 2A — Scaffold-new
- Ask for the project directory name if not given.
- Copy
templates/into the target:src/,tsconfig.json,bunfig.toml,.env.example. Renderpackage.json.tmpl→package.json, replacing{{PROJECT_NAME}}. - From the target dir, run:
bun add langchain @langchain/langgraph @langchain/openai @langchain/anthropic @langchain/core zod minimist chalk ink react @inkjs/ui ink-text-input ink-spinnerandbun add -d typescript bun-types @types/minimist @types/react ink-testing-library(this also pins the*versions in package.json). - Confirm it runs:
bun run start --list(should listchat). - Tell the user how to configure
.envand runbun run start. Mention they canbun linkto install the CLI as a global command (named after the package) that runs from any directory and operates on that directory (src/dotenv.tsloads the project's own.envso config still works from anywhere). - Then proceed to Step 3 to add their first real action (one run = one action).
Step 2B — Add-action
Run the architect interview (Step 3) and append exactly one action.
Step 3 — Architect ONE action
Known recipe — AWS + repo chat assistant: if the scenario is "drive the AWS CLI / manage AWS in natural language" or "merge aws-assistant into chat", follow
references/chat-aws-repo.md. Its Step 0 is mandatory: ask the user which AWS profile + region the assistant runs as before generating anything — the safety model pins every command to one account. Default to merging AWS into the primarychataction and adding the repobashtool unless the user explicitly asks for a separate action.
Understand the scenario. Ask what the action should do, its inputs, and its output. Keep it to one action; if the scenario is really several, say so and recommend splitting (the user re-runs per action).
Pick the type:
- Function — deterministic logic or a single model call → use
action.function.ts.tmpl. - Agent — needs tools, multi-step reasoning, or orchestration → continue.
- Function — deterministic logic or a single model call → use
Pick the turn shape. Ask the user and present it as a single-select choice (an Ink
selectMenu— offer these three options):- single-turn (one-shot) — one request in → one answer out → exits. The default; keeps nothing between runs.
- multi-turn (interactive session / REPL) — render the Ink
<SessionApp>(src/ink/SessionApp.tsx): a pinned footer with a ccstatusline-styleModel:line above a usage bar (vs the model's realctx.contextWindow, resolved bysrc/model-info.tsfrom the provider's/modelsendpoint with aMODEL_CONTEXT_TOKENSenv fallback), history trimming, and quit on Ctrl+C or/exit. Passrespond(messages, { onStep, ask })for one turn —askis the human-in-the-loop seam. The action file is.tsx. Copy the seedsrc/actions/chat.tsx; pure helpers live insrc/session-core.ts. - both — run one-shot when the input is supplied via flags (e.g.
--input ...), otherwise render the<SessionApp>; or offer an InkselectMenu("Run once" vs "Start a session") at startup. Worked example:reference/src/actions/assistant.tsx(params: [], branch on--input; one-shot prints, else renders<SessionApp>).
Turn shape is orthogonal to type: a Function or an Agent can be single- or multi-turn. For a multi-turn agent, each turn streams through
streamAgentinside the session'srespondcallback.Compose the graph from primitives (see
references/primitives.md). There is NO fixed catalog — reason from the scenario to the smallest graph that fits (single agent, sequential, parallel, loop-and-critic, coordinator/routing, orchestrator-worker, map-reduce, agent-as-tool, or a custom mix).Render an ASCII diagram of the concrete graph (nodes + edges) and explain why this shape fits. Example:
+-----------+ | drafter |<-------------+ +-----+-----+ | v | revise (not good enough) +-----------+ | | critic |--------------+ +-----+-----+ | approved v [done]Wait for the user to accept or request changes. Loop until accepted.
Verify the API via context7 for every construct you will emit (
createAgent,StateGraph,addConditionalEdges,Send,Command, reducers,tool, streaming).Generate the action file from
action.function.ts.tmploraction.agent.ts.tmplintosrc/actions/<name>.ts(or.tsxif it renders Ink), replacing the{{...}}markers. Agent actions stream node/tool steps viastreamAgent(graph, input, onStep)from../trace(print with chalk, or feedonStepto the Ink session). Tools that report progress are authored asasync function*soon_tool_eventfires. Multi-turn (or both) actions are.tsxand render<SessionApp>.Register it. Add an import and append to the
actionsarray insrc/actions/index.ts.Validate every file write. After creating or updating any file, immediately verify the filesystem state before moving on: use
test -f <path>for new files,git diff -- <path>for updates, and re-read the relevant lines withsed/rg/nlto confirm the intended content is present. Never assume a write succeeded just because a tool call was attempted.Verify:
bun run typecheckthenbun run start --action <name> ....Gate completion with a Codex review (MANDATORY). Implementation is NOT done until the diff has been reviewed by Codex. Capture the pre-work commit first (
git rev-parse --short HEADBEFORE you start editing), then after verification passes, emit this exact copy-paste line for the user (substitute the captured SHA) and stop — do not declare the work complete until the review is run:/codex:review --base=<short-sha> --wait<short-sha>is the short commit SHA the work started from (the review diffs everything since that commit). Address any review findings, then re-emit the line if you make further changes.
Conventions you MUST keep
- The UI runtime is Ink (React for the terminal) — no inquirer, no ora.
Prompts come from
src/ui.tsx(selectMenu,askText,askConfirm); wrap slow work inwithSpinner.ctx.logis still chalk for plain colored output. - Parameters are declarative
ParamDef[]. minimist flags fill them;resolveParamsInk-prompts only for what is missing. Never hand-roll prompt plumbing in an action. - The "Choose an action" menu (
selectActioninsrc/params.ts) pads each action name to the longest name's width (padEnd) before the—so descriptions line up in a column. Keep that alignment when editing the menu label. - The
Ctxgives every action{ llm, log (chalk), getDocs, contextWindow, model }. - Agent runs stream node/tool steps via
streamAgent(fromsrc/trace.ts). - Agent/chat actions that build a custom system prompt MUST load repository
guidance from the project root
AGENTS.mdwhen present. Add a smallloadRepoInstructions(cwd = process.cwd())helper that returns trimmed content orundefined, then append it to the system prompt in a fencedmdblock before the current date/time. Test present, missing, and blankAGENTS.mdcases so scaffolded agents actually follow repository rules at runtime. - Mandatory status footer: any action that calls the LLM MUST surface the working
dir, model id, and context usage. Multi-turn renders a Claude Code-style
<SessionApp>footer — the input framed above/below by a divider, thenDir:/Model:/ context bar; one-shot callsprintContextBar(ctx.log, ctx.contextWindow, messages, ctx.model)(fromsrc/session-core.ts). The prompt label isctx.promptLabel(thePROMPT_LABELenv, defaultyou). The action templates include all of this by default. - Turn shape: single-turn actions return after one run; multi-turn actions are
.tsxand render the Ink<SessionApp>(pinned context footer; bar shows usage vs the realctx.contextWindow). Pure helpers live insrc/session-core.ts;chatis the reference multi-turn action. - Global bin:
package.jsondeclares abinnamed{{PROJECT_NAME}}→src/index.ts, sobun linkinstalls the CLI as a global command. A linked bin runs with the caller's cwd (actions operate on the invocation dir;CWDstill overrides), sosrc/index.tscallsloadProjectEnv(resolve(import.meta.dir, '..'))fromsrc/dotenv.tsto load the project's own.envno matter where it runs (keys already in the environment win). Load it BEFORE the CLI graph —index.tsdoesconst { runCli } = await import('./cli')so actions that snapshot env into module-level constants (e.g. AWS profile/region in the chat-aws-repo recipe) read the loaded values, not stale defaults. Keep the shebang onsrc/index.ts. .tsxJSX pragma: every.tsxfile MUST start with/** @jsxImportSource react */. Bun resolvesjsxImportSourcefrom the launch cwd'stsconfig.json, so a global bin run inside a Vue/Nuxt project (jsxImportSource: vue) would otherwise transpile the CLI's own.tsxagainstvue/jsx-dev-runtimeand crash. The pragma pins React JSX per file regardless of cwd; the seed.tsxfiles and thetests/global-bin-jsx.test.tsregression cover this. New Ink actions need the pragma as their first line.- Keep each file focused; one action per file. Actions that render Ink are
.tsx. - After any create/update operation, validate the file on disk with a targeted existence check, diff, or read-back before claiming the change is done.
- Codex review gates completion: never call the implementation done until a
Codex review of the diff has run. After verification, emit the copy-paste line
/codex:review --base=<short-sha> --wait(with the pre-work short SHA) and wait.
Templates
templates/— verbatim skeleton (do not regenerate from memory). Core:src/ui.tsx(Ink prompts + spinner),src/session-core.ts(pure session helpers),src/ink/SessionApp.tsx(pinned-footer chat UI),src/trace.ts(streamAgent),src/model-info.ts(context window).templates/src/actions/chat.tsx— seed action and reference multi-turn Ink session.action.function.ts.tmpl/action.agent.ts.tmpl— action bodies you fill in (rename to.tsxwhen rendering Ink).references/primitives.md— LangGraph building blocks to compose from.references/chat-aws-repo.md— recipe for merging AWS CLI + repo bash tools into the primarychataction with human-approved AWS writes and bash deletes. The bash delete/raw-AWS classifier and the env boundary (allowlist + jailed HOME +zsh -f, drop-allAWS_*) are hardened across many adversarial review rounds — copy the authoritativereference/src/actions/{shell-tokens,bash-core, command-runtime}.tsrather than re-deriving the gate from memory. The string classifier is best-effort; the env jail is the real boundary (a hard guarantee needs OS sandboxing). When the user asks for an AWS assistant, follow it — and ask for the AWS profile/region FIRST.references/langsmith-observability.md— recipe for enabling LangSmith tracing on an existing project (env-var driven; no new deps). Ask the privacy question FIRST — traces ship full prompts and tool outputs to LangSmith's cloud.