chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,16 @@
**/node_modules
.next
.env
.env.local
playwright-report
test-results
**/vitest.config.ts
**/test-setup.ts
**/*.test.ts
**/*.test.tsx
**/*.spec.ts
**/*.spec.tsx
# LangGraph FileSystemPersistence state — can grow to hundreds of MB after
# many local dev runs. Never bake developer-machine state into the image;
# entrypoint.sh deletes it on every fresh container boot anyway.
src/agent/.langgraph_api
@@ -0,0 +1,10 @@
# API Keys (shared across integrations)
OPENAI_API_KEY=replace-with-your-key
ANTHROPIC_API_KEY=replace-with-your-key
# LangGraph
LANGGRAPH_DEPLOYMENT_URL=http://localhost:8123
LANGSMITH_API_KEY=
# Showcase
NEXT_PUBLIC_BASE_URL=http://localhost:3000
@@ -0,0 +1,8 @@
# Sample multimodal demo binaries (PNG + PDF) must stay as regular
# binaries — not LFS pointers — so deploy environments without `git lfs pull`
# (Railway, in particular) serve the actual files. LFS tracking for other
# PNG/PDFs under this package inherits from the repo-root .gitattributes.
public/demo-files/sample.png -filter -diff -merge binary
public/demo-files/sample.pdf -filter -diff -merge binary
# Voice demo sample audio follows the same convention.
public/demo-audio/sample.wav -filter -diff -merge binary
@@ -0,0 +1,13 @@
node_modules/
.next/
.env.local
.env
*.pyc
__pycache__/
.venv/
dist/
playwright-report/
test-results/
# Shared modules (copied by CI)
shared_frontend/
@@ -0,0 +1,70 @@
# Stage 1: Build Next.js frontend
FROM node:22-slim AS frontend
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --legacy-peer-deps
COPY . .
RUN npx next build
# Stage 2: Install agent (prod-only) dependencies
FROM node:22-slim AS agent-deps
WORKDIR /agent
COPY src/agent/package.json src/agent/package-lock.json ./
# `npm ci` without --omit=dev because the agent uses `tsx` as an ESM loader
# (not a watcher) at runtime via `node --import tsx server.mjs` — tsx must
# be resolvable at boot time. This install still excludes top-level devDeps
# (only @types/node), so the node_modules stays lean.
RUN npm ci --legacy-peer-deps
# Stage 3: Production image — runtime only (no build tools)
FROM node:22-slim AS runner
WORKDIR /app
# Install curl — entrypoint.sh watchdog uses it to probe the liveness endpoint.
# node:22-slim does NOT include curl by default, so without this the probe
# exits rc=127 "command not found" every cycle and the watchdog kill-loops
# the agent process indefinitely.
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Create unprivileged runtime user BEFORE any COPY so --chown resolves
# by name and so recursive chown over /app is never needed (fast builds).
RUN (groupadd --system --gid 1001 app 2>/dev/null || true) \
&& (useradd --system --uid 1001 --gid 1001 --no-create-home app 2>/dev/null || true) \
&& mkdir -p /home/app && chown app:app /home/app
# Next.js build artifacts
COPY --chown=app:app --from=frontend /app/.next ./.next
COPY --chown=app:app --from=frontend /app/node_modules ./node_modules
COPY --chown=app:app --from=frontend /app/package.json ./
COPY --chown=app:app --from=frontend /app/public ./public
# Agent code + prod-only deps (installed in agent-deps stage, not at runtime)
COPY --chown=app:app --from=agent-deps /agent/node_modules ./src/agent/node_modules
COPY --chown=app:app src/agent ./src/agent
COPY --chown=app:app shared-tools/ ./shared-tools/
# Entrypoint
COPY --chown=app:app entrypoint.sh ./
RUN chmod +x entrypoint.sh
# Ensure WORKDIR itself is owned by `app` — `WORKDIR /app` at the top of the
# stage creates /app as root, and `COPY --chown=app:app` only reassigns the
# copied files, NOT the parent dir. Without this, any CLI that tries to
# mkdir under /app at runtime (langgraph worker caches, Next.js build
# caches, etc.) hits EACCES under the unprivileged user and crashes the
# container.
RUN chown app:app /app
USER app
EXPOSE 10000
# Intentionally NOT setting `ENV NODE_ENV=production` at the image level.
# NODE_ENV=production at the image level would leak into every child process
# (TS agent via `tsx` ESM loader, shell scripts, healthchecks) — several of
# these frameworks change behavior based on NODE_ENV and should NOT see
# "production" when they're running as an agent backend. entrypoint.sh
# scopes NODE_ENV=production to the Next.js invocation only so non-Next
# children see the host's environment.
ENV PORT=10000
ENV HOSTNAME=0.0.0.0
CMD ["./entrypoint.sh"]
@@ -0,0 +1,37 @@
{
"framework": "langgraph-typescript",
"features": {
"agentic-chat": {
"og_docs_url": "https://docs.copilotkit.ai/langgraph/prebuilt-components",
"shell_docs_path": "/agentic-chat-ui"
},
"hitl-in-chat": {
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/your-components/interactive",
"shell_docs_path": "/generative-ui/your-components/interactive"
},
"tool-rendering": {
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/tool-rendering",
"shell_docs_path": "/generative-ui/tool-rendering"
},
"gen-ui-tool-based": {
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/your-components/display-only",
"shell_docs_path": "/generative-ui/your-components/display-only"
},
"gen-ui-agent": {
"og_docs_url": "https://docs.copilotkit.ai/langgraph/generative-ui/state-rendering",
"shell_docs_path": "/generative-ui/state-rendering"
},
"shared-state-read-write": {
"og_docs_url": "https://docs.copilotkit.ai/langgraph/shared-state/in-app-agent-write",
"shell_docs_path": "/shared-state"
},
"shared-state-streaming": {
"og_docs_url": "https://docs.copilotkit.ai/langgraph/shared-state/predictive-state-updates",
"shell_docs_path": "/shared-state/streaming"
},
"subagents": {
"og_docs_url": "https://docs.copilotkit.ai/langgraph/multi-agent-flows",
"shell_docs_path": "/multi-agent/subagents"
}
}
}
@@ -0,0 +1,21 @@
<Steps>
<Step>
### Install the CopilotKit LangGraph SDK
```bash
npm install @copilotkit/sdk-js
```
</Step>
<Step>
### Wire CopilotKit state into your graph
Agent config flows from the UI through `useAgentContext` and arrives on
`state.copilotkit.context`. Use `CopilotKitStateAnnotation` to expose
that channel — your chat node reads it to assemble the system prompt
on every turn.
<DemoCode file="src/agent/agent-config.ts" region="agent-config-setup" />
</Step>
</Steps>
@@ -0,0 +1,20 @@
<Steps>
<Step>
### Install the CopilotKit LangGraph SDK
```bash
npm install @copilotkit/sdk-js
```
</Step>
<Step>
### Read CopilotKit context in your graph
`useAgentContext` entries arrive on `state.copilotkit.context`. Extend
your graph state with `CopilotKitStateAnnotation`, then prepend that
read-only context as a system message before invoking the model.
<DemoCode file="src/agent/readonly-state.ts" region="agent-context-setup" />
</Step>
</Steps>
@@ -0,0 +1,19 @@
First, extend your graph's state annotation with `CopilotKitStateAnnotation`
and bind forwarded actions through `convertActionsToDynamicStructuredTools`.
The annotation is what makes every CopilotKit feature on the frontend —
frontend tools, shared state, agent context, and generative UI components —
visible to your LangGraph agent on every turn.
<DemoCode file="src/agent/frontend-tools.ts" region="setup" />
<Accordions>
<Accordion title="Install the SDK">
If `@copilotkit/sdk-js` isn't already in your project, add it so the
imports above resolve:
```bash
npm install @copilotkit/sdk-js
```
</Accordion>
</Accordions>
@@ -0,0 +1,22 @@
<Steps>
<Step>
### Install the CopilotKit LangGraph SDK
```bash
npm install @copilotkit/sdk-js
```
</Step>
<Step>
### Wire CopilotKit state + tools into your graph
Frontend tools registered with `useFrontendTool` arrive on the agent's
state at `state.copilotkit.actions`. Use `CopilotKitStateAnnotation` to
expose that channel on your graph, then call
`convertActionsToDynamicStructuredTools(...)` inside your chat node to
bind the LLM to those actions.
<DemoCode file="src/agent/frontend-tools.ts" region="setup" />
</Step>
</Steps>
@@ -0,0 +1,21 @@
<Steps>
<Step>
### Install the CopilotKit LangGraph SDK
```bash
npm install @copilotkit/sdk-js
```
</Step>
<Step>
### Wire CopilotKit state + tools into your graph
Tool-based HITL (`useHumanInTheLoop`) registers the tool on the frontend
and forwards it via `state.copilotkit.actions` — the same wiring as
frontend tools. The graph-paused pattern (`useInterrupt`) uses
LangGraph's native `interrupt(...)` primitive inside a node.
<DemoCode file="src/agent/frontend-tools.ts" region="setup" />
</Step>
</Steps>
@@ -0,0 +1,21 @@
<Steps>
<Step>
### Install the CopilotKit LangGraph SDK
```bash
npm install @copilotkit/sdk-js
```
</Step>
<Step>
### Wire CopilotKit state + tools into your graph
Programmatic control (`copilotkit.runAgent`, `agent.subscribe`,
`agent.addMessage`) drives runs through the same agent your chat UI
uses, so the backend wiring is the same `CopilotKitStateAnnotation`
setup.
<DemoCode file="src/agent/frontend-tools.ts" region="setup" />
</Step>
</Steps>
@@ -0,0 +1,25 @@
<Steps>
<Step>
### Install the CopilotKit LangGraph SDK
```bash
npm install @copilotkit/sdk-js
```
</Step>
<Step>
### Wire CopilotKit state into your graph
Shared state requires that your graph's state annotation extends
`CopilotKitStateAnnotation`'s `spec`. That's what gives the runtime a
place to forward state updates from `agent.setState(...)`.
<DemoCode file="src/agent/shared-state-read-write.ts" region="shared-state-setup" />
See `src/agent/shared-state-read-write.ts` for the bidirectional
pattern: a `set_notes` tool emits a `Command({ update: ... })` to
push agent-authored state, and a preferences-injecting chat node
reads UI-authored state every turn.
</Step>
</Steps>
@@ -0,0 +1,21 @@
<Steps>
<Step>
### Install the CopilotKit LangGraph SDK
```bash
npm install @copilotkit/sdk-js
```
</Step>
<Step>
### Map generated content into shared state while it streams
Extend your graph state with `CopilotKitStateAnnotation`, then call
`copilotkitCustomizeConfig` with an `emitIntermediateState` mapping. This
forwards the `write_document.document` tool argument into `state.document`
while the model is still generating it.
<DemoCode file="src/agent/shared-state-streaming.ts" region="state-streaming-middleware" />
</Step>
</Steps>
@@ -0,0 +1,20 @@
<Steps>
<Step>
### Install the CopilotKit LangGraph SDK
```bash
npm install @copilotkit/sdk-js
```
</Step>
<Step>
### Wire CopilotKit state + tools into your graph
Sub-agents are tools on a supervisor agent. The supervisor's graph state
extends `CopilotKitStateAnnotation` so delegations stream back to the UI
through shared state in real time.
<DemoCode file="src/agent/subagents.ts" region="subagent-setup" />
</Step>
</Steps>
+624
View File
@@ -0,0 +1,624 @@
#!/bin/bash
set -e
# ---------------------------------------------------------------------------
# Agent process-tree kill.
#
# The agent is launched as a compound command through a process substitution:
# cd /app/src/agent && ... npm start &> >(awk …) &
# so $AGENT_PID (=$!) is the *outer subshell* wrapping that pipeline — NOT the
# `npm` wrapper nor the `node` server it forks. A plain `kill -9 $AGENT_PID`
# therefore reaps only the subshell: `npm` and `node` are reparented to PID 1
# and KEEP RUNNING — still bound to :8123, still holding the bloated in-memory
# state. The size-gate's whole promise ("kill agent → container restart →
# boot-purge") is then broken: the frontend proxies to a dead-but-not-restarted
# agent forever (edge 502s), and even if the container does exit, a surviving
# orphan can still hold :8123 across the restart.
#
# We cannot `kill -- -$PGID` because a non-interactive script has job control
# OFF: the agent subshell, npm, node, next.js AND the main shell all share the
# shell's process group, so a group kill would take out the whole entrypoint.
# Instead we walk the process tree via /proc (node:22-slim ships neither
# `ps` nor `pgrep`) and SIGKILL every descendant, deepest-first, in a BOUNDED
# re-scan loop that keeps the root alive as the walk anchor until the subtree
# is drained, then kills the root last (see _kill_agent_tree for why).
#
# Defined ABOVE cleanup() on purpose: cleanup() (the EXIT/SIGTERM trap) calls
# _kill_agent_tree, so the helper must already exist whenever the trap can
# first fire — including the early `exit 1` below if the agent fails to start.
# ---------------------------------------------------------------------------
_agent_descendants() {
# Print all descendant PIDs of $1 (children, grandchildren, …), deepest-first.
local root="$1" pid ppid stat _state _rest
# Fail closed on a dangerous or meaningless root. An empty / non-numeric root
# would make the PPID comparison below match nothing (harmless) but a root of
# "0" or "1" is catastrophic: "0" means "every process in the caller's process
# group" and "1" is init — a caller that then fed the result to a kill could
# wipe the whole container. Refuse anything that is not an integer >= 2.
case "$root" in
''|*[!0-9]*) echo "[proctree] WARNING: refusing descendant scan for non-numeric root '${root}'" >&2; return 0 ;;
esac
if [ "$root" -le 1 ]; then
echo "[proctree] WARNING: refusing descendant scan for reserved root ${root} (0=process-group, 1=init)" >&2
return 0
fi
for pid in $(cd /proc 2>/dev/null && ls -d [0-9]* 2>/dev/null); do
[ -r "/proc/$pid/stat" ] || continue
# /proc/PID/stat is: "PID (comm) STATE PPID PGRP …". comm can contain
# spaces AND parens, so strip through the final ") " before splitting; PPID
# is then the 2nd field of the remainder (1st is STATE). "${x##*) }" takes
# the LONGEST prefix up to the LAST ") ", and no field after the real
# closing paren contains ")", so even a comm like "(evil) S 1)" parses to
# the true PPID — the last ") " is always the comm's real terminator.
stat=$(cat "/proc/$pid/stat" 2>/dev/null) || continue
# The remainder after the comm's terminating ") " is "STATE PPID PGRP …", so
# PPID is the 2nd whitespace-separated field. Use the `read` builtin instead
# of `echo … | awk` to avoid forking awk once per /proc entry (a fork-storm
# under a large process table). `read` word-splits on IFS; discard STATE
# into _state, capture PPID, discard the rest into _rest.
read -r _state ppid _rest <<< "${stat##*) }"
if [ "$ppid" = "$root" ]; then
_agent_descendants "$pid"
echo "$pid"
fi
done
}
_kill_agent_tree() {
# SIGKILL the agent subshell AND its npm→node descendants so the real server
# actually dies and frees :8123 — not just the log-pipeline subshell.
#
# A single snapshot-then-kill is racy: a descendant that forks a new child (or
# a child that reparents) BETWEEN the scan and the kill is missed by the walk,
# reparents to PID 1, and keeps :8123 bound — defeating the whole tree-kill.
# So we re-scan in a BOUNDED loop, killing the currently-live descendants
# deepest-first each pass, until a scan comes back empty (or the bound is
# hit). Crucially we keep the ROOT alive as the walk anchor across passes and
# kill it LAST: killing root first would immediately reparent every descendant
# to PID 1, making them unreachable by a root-anchored PPID walk. Leaving root
# alive (it is an idle subshell that spawns nothing on its own) means a child
# that forks between two passes is still attached to a live chain from root
# and is reaped on the next pass.
#
# Residual limitation: a descendant that FULLY reparents to PID 1 (double-fork
# / daemonize) before we reach it is no longer on any PPID chain from root and
# cannot be found by a /proc PPID walk. That is inherent to PPID-based reaping
# without job control (no ps/pgrep in node:22-slim; job control off in a
# non-interactive script, so no process-group kill). The agent's npm→node tree
# does not daemonize, so this loop covers the real failure surface.
#
# Fail closed on a dangerous or meaningless root, BEFORE any kill runs. If the
# caller passes an empty / non-numeric PID, or the reserved 0 (SIGKILL to the
# WHOLE caller process group) or 1 (init), refuse outright — a bare `kill -9 0`
# here would SIGKILL the entire entrypoint. This makes `kill -9 0`/`kill -9 1`
# structurally impossible regardless of what the caller passes.
local root="$1" p descendants
case "$root" in
''|*[!0-9]*) echo "[proctree] WARNING: refusing tree-kill for non-numeric PID '${root}'" >&2; return 0 ;;
esac
if [ "$root" -le 1 ]; then
echo "[proctree] WARNING: refusing tree-kill for reserved PID ${root} (0=process-group, 1=init)" >&2
return 0
fi
for _ in 1 2 3 4 5; do
descendants=$(_agent_descendants "$root")
[ -z "$descendants" ] && break
for p in $descendants; do
kill -9 "$p" 2>/dev/null || true
done
# `|| true`: under set -e a non-zero `sleep` (e.g. a future busybox/Alpine
# rebase whose sleep can fail) would abort this tree-kill mid-walk, leaving
# the root un-killed and the real npm→node server orphaned. The guard keeps
# the walk running to completion regardless of sleep's exit status.
sleep 0.2 || true
done
kill -9 "$root" 2>/dev/null || true
}
# ---------------------------------------------------------------------------
# Numeric-config validator.
#
# Every operator-overridable numeric knob (size threshold, check intervals,
# strike budgets, grace/timeout windows) is read from an env var with a `:-`
# default. A non-integer or empty override (e.g. LANGGRAPH_SIZE_CHECK_INTERVAL
# ="60s") does NOT fall back to the default on its own — it propagates as a bad
# value into an arithmetic test (`[ .. -ge .. ]`), a `sleep`, or a loop count.
# Under `set -e` those failures are inconsistent: a bad `sleep $INTERVAL` makes
# the size-monitor loop exit on its FIRST iteration, silently disabling the
# whole size guard for the container's lifetime; a bad arithmetic test inside an
# `if` evaluates false and skips the guard with no warning. Either way an
# operator typo silently DISABLES a guard.
#
# _require_int validates ONE such var by name and rewrites it in place: if the
# current value is a positive integer it is kept; otherwise a WARNING is logged
# and the documented default is substituted. It fails SAFE — it never aborts
# and never leaves a guard fed by a bad value. Run over EVERY numeric config
# var at startup (see the validation pass below) so no `sleep`/loop-count/
# arithmetic test downstream can be silently broken by a bad override.
#
# Args: $1 = variable NAME (validated + reassigned via printf -v)
# $2 = documented default (used verbatim on fallback)
# $3 = human label for the warning
_require_int() {
local name="$1" default="$2" label="$3" value
eval "value=\${$name}"
# Valid ONLY if a positive integer with no leading zero: first digit 1-9,
# rest digits ([1-9][0-9]*). This rejects — and falls back to the default
# for — every operator-typo class that would break a guard:
# • empty / non-numeric ("", "60s", "abc")
# • "0" — a zero threshold/interval/limit turns a guard into an instant-fire
# kill loop (SIZE_THRESHOLD_MB=0 kills on cycle 1) or a busy-spin
# (SIZE_CHECK_INTERVAL=0 → `while sleep 0`)
# • leading-zero / octal forms ("010", "08") — bash arithmetic reads a
# leading-zero literal as OCTAL, so "010" becomes 8 (wrong value) and an
# "08"/"09" digit aborts the script under `set -e` ("value too great for
# base").
# The `[1-9]*([0-9])` extglob-free equivalent is written as two arms: a lone
# single digit 1-9, or a 1-9 lead followed by one-or-more digits.
#
# UPPER BOUND (fail-safe, same class as the checks above): even a value that is
# syntactically all-digits can be pathologically large. bash arithmetic is
# signed 64-bit, so a 19-digit value can still parse but a 20+ digit value
# OVERFLOWS — `[ "$x" -ge "$y" ]` then aborts with "value too great for base"
# (suppressed to false inside an `if`, silently disabling the guard) or wraps
# to a negative/garbage magnitude. No real knob (interval seconds, strike
# count, size-MB threshold) is ever more than a handful of digits, so cap the
# length at 10 digits (max 9,999,999,999 — years of seconds, petabytes of MB;
# comfortably inside the signed-64-bit range with no overflow risk). A longer
# value is treated exactly like any other bad override: WARN + fall back to the
# documented default rather than silently disabling the guard.
if [ "${#value}" -gt 10 ]; then
echo "[entrypoint] WARNING: ${label} (${name}) is too large (got: '${value}', ${#value} digits — max 10) — falling back to default ${default}"
printf -v "$name" '%s' "$default"
return 0
fi
case "$value" in
[1-9]) : ;; # single positive digit
[1-9][0-9]*) # multi-digit, must be all digits after the lead
case "$value" in
*[!0-9]*)
echo "[entrypoint] WARNING: ${label} (${name}) is not a positive integer (got: '${value}') — falling back to default ${default}"
printf -v "$name" '%s' "$default"
;;
esac
;;
*)
echo "[entrypoint] WARNING: ${label} (${name}) is not a positive integer (got: '${value}') — falling back to default ${default}"
printf -v "$name" '%s' "$default"
;;
esac
}
cleanup() {
# Tree-kill the agent (not a bare `kill $AGENT_PID`): $AGENT_PID is the
# process-sub subshell, so a single-PID kill on the normal shutdown path
# (graceful exit / SIGTERM on every Railway redeploy/rollover) would reap
# only the subshell and ORPHAN the real npm→node server — reparented to
# PID 1, still holding :8123 across the restart. See _kill_agent_tree.
_kill_agent_tree "$AGENT_PID"
# Tree-kill Next.js too: NEXTJS_PID is ALSO a process-sub subshell wrapping
# `npx next start` (which forks npm→node), exactly like AGENT_PID. A bare
# `kill $NEXTJS_PID` would reap only the wrapper subshell and ORPHAN the real
# Next.js node server — reparented to PID 1, still holding $PORT across the
# Railway redeploy/rollover SIGTERM, so the new container cannot bind $PORT.
# (Same orphan class already fixed for the agent; route the frontend through
# the same guarded walk.) WATCHDOG_PID is a genuine single-PID subshell we
# spawn directly (`( … ) &`, not process-sub-wrapped). It DOES fork one child
# — the size sub-loop (SIZE_PID) — but a bare `kill $WATCHDOG_PID` is still
# correct because the watchdog subshell arms its OWN inner EXIT trap that reaps
# that child (a $BASHPID PPID-walk, arm-then-spawn, no leak window) whenever the
# subshell exits, including on this SIGTERM. So the outer cleanup only needs to
# signal the watchdog; the watchdog cleans up its own subtree. SIZE_PID is
# local to the watchdog subshell and never visible here, so this outer shell
# cannot (and need not) kill it directly.
_kill_agent_tree "$NEXTJS_PID"
kill $WATCHDOG_PID 2>/dev/null || true
# NOTE: the size sub-loop is intentionally NOT killed here. It is spawned
# inside the watchdog subshell ( ) & so its PID is never visible in this outer
# shell. The watchdog subshell registers its own EXIT trap (armed BEFORE the
# spawn) that reaps the sub-loop via a $BASHPID PPID-walk on any exit path,
# including the SIGTERM the `kill $WATCHDOG_PID` above delivers; see the
# "trap ... EXIT" inside the ( ) & block below.
}
trap cleanup EXIT
# ---------------------------------------------------------------------------
# Size-check seam: extract the per-cycle size-check-and-kill decision so it
# can be exercised directly in tests without running the full entrypoint stack.
#
# Usage (normal): called by the watchdog size sub-loop below.
# Usage (test): LANGGRAPH_SIZE_THRESHOLD_MB=X LANGGRAPH_PERSIST_DIR_OVERRIDE=Y
# AGENT_PID=Z bash entrypoint.sh --check-size-once
#
# Returns 0 if no action taken, 1 if the agent was killed (threshold exceeded).
# Callers under set -e should invoke with || true if 1 is acceptable.
# ---------------------------------------------------------------------------
_watchdog_check_size_once() {
local persist_dir="${PERSIST_DIR}"
local threshold_mb="${SIZE_THRESHOLD_MB}"
local agent_pid="${AGENT_PID}"
if ! kill -0 "$agent_pid" 2>/dev/null; then
return 0
fi
if [ ! -d "$persist_dir" ]; then
echo "[watchdog:size] Persistence dir ${persist_dir} does not exist — skipping size check"
return 0
fi
# du -sm returns on-disk size in MiB as an integer. This is a heuristic
# proxy for the in-memory superjson-serialised string size: @langchain/
# langgraph-api serialises state to disk on a 3-second timer, so on-disk
# size closely tracks serialised size but is NOT a proven bound. We set a
# conservative threshold (200 MB, well under V8's ~512 MB string ceiling)
# to leave margin for this approximation.
# || true prevents set -e from killing this subshell if du fails.
DIR_SIZE_MB=$(du -sm "$persist_dir" 2>/dev/null | awk '{print $1}') || true
# Validate for NUMERICITY, not merely emptiness. A non-integer value (junk
# du output, a transient error, a test-seam stub) would otherwise reach the
# `[ "$DIR_SIZE_MB" -ge ... ]` comparison below and throw "integer expression
# expected". Because that comparison sits inside an `if`, set -e is
# suppressed and the failed test evaluates false — SILENTLY skipping the size
# gate for this cycle with NO warning (unlike the empty-string case). Match
# on ^[0-9]+$ and emit the SAME "size guard inactive" WARNING so the gate can
# never silently disappear.
case "$DIR_SIZE_MB" in
''|*[!0-9]*)
echo "[watchdog:size] WARNING: Could not read a numeric size of ${persist_dir} (got: '${DIR_SIZE_MB}') — size guard inactive this cycle"
return 0
;;
esac
# Validate the THRESHOLD for numericity too — same set -e hazard class as
# DIR_SIZE_MB above. threshold_mb comes from LANGGRAPH_SIZE_THRESHOLD_MB, an
# operator-supplied override. A non-integer value (e.g. "200MB", "abc") would
# otherwise reach the `[ "$DIR_SIZE_MB" -ge "$threshold_mb" ]` comparison and
# throw "integer expression expected"; because that comparison sits inside an
# `if`, set -e is suppressed and the failed test evaluates false — SILENTLY
# skipping the size gate EVERY cycle with NO warning. Match on ^[0-9]+$ and
# emit the SAME "size guard inactive" WARNING so the gate can never silently
# disappear behind a bad override.
case "$threshold_mb" in
''|*[!0-9]*)
echo "[watchdog:size] WARNING: LANGGRAPH_SIZE_THRESHOLD_MB is not a positive integer (got: '${threshold_mb}') — size guard inactive this cycle"
return 0
;;
esac
echo "[watchdog:size] Persistence dir size: ${DIR_SIZE_MB}MB (threshold: ${threshold_mb}MB)"
if [ "$DIR_SIZE_MB" -ge "$threshold_mb" ]; then
echo "[watchdog:size] Size threshold exceeded (${DIR_SIZE_MB}MB >= ${threshold_mb}MB) — killing agent PID $agent_pid (and its npm→node tree) to trigger container restart and boot-purge"
# NOTE: this kill WILL terminate any in-flight streaming runs — accepted
# tradeoff vs OOM/crash. The gate is threshold-based (not a fixed timer)
# so it fires only when state has grown dangerously large.
# Tree-kill (not a bare `kill -9 $agent_pid`): $agent_pid is the process-sub
# subshell, so a single-PID kill would orphan the real npm→node server,
# leaving :8123 bound and the boot-purge never re-run. See _kill_agent_tree.
_kill_agent_tree "$agent_pid"
return 1
fi
return 0
}
# ---------------------------------------------------------------------------
# Test seam: --check-size-once mode
# Runs exactly one size-check cycle using env vars for configuration, then
# exits. Designed to be called from tests with a stubbed `du` on PATH.
# Exit code: 0 = under threshold (no kill), 1 = threshold exceeded (kill issued).
# ---------------------------------------------------------------------------
if [ "${1:-}" = "--check-size-once" ]; then
# In test-seam mode the caller owns the agent PID lifecycle. Clearing the
# EXIT trap ensures cleanup() does NOT send a spurious SIGTERM to AGENT_PID
# on the way out, which would mask whether the size gate actually fired
# (gate fires → SIGKILL; cleanup fires → SIGTERM; both make poll() non-None,
# but only SIGKILL proves the gate killed the right PID).
trap - EXIT
PERSIST_DIR=${LANGGRAPH_PERSIST_DIR_OVERRIDE:-/app/src/agent/.langgraph_api}
SIZE_THRESHOLD_MB=${LANGGRAPH_SIZE_THRESHOLD_MB:-200}
_require_int SIZE_THRESHOLD_MB 200 "LangGraph persistence size threshold (MB)"
# Do NOT default AGENT_PID to 0. The old `AGENT_PID=${AGENT_PID:-0}` sentinel
# meant that an unset/empty AGENT_PID flowed into `_kill_agent_tree "0"` →
# `kill -9 0`, SIGKILLing the caller's ENTIRE process group. If AGENT_PID is
# unset/empty here, skip the check with a warning rather than defaulting to a
# dangerous PID. (_kill_agent_tree/_watchdog_check_size_once also fail closed
# on PID <= 1, but we refuse earlier so no bogus PID is ever passed at all.)
case "${AGENT_PID:-}" in
''|*[!0-9]*)
echo "[watchdog:size] WARNING: AGENT_PID is unset or non-numeric (got: '${AGENT_PID:-}') — skipping size check" >&2
exit 0
;;
esac
_watchdog_check_size_once
exit $?
fi
echo "========================================="
echo "[entrypoint] Starting showcase package: langgraph-typescript"
echo "[entrypoint] Time: $(date -u)"
echo "[entrypoint] PORT=${PORT:-not set}"
echo "[entrypoint] NODE_ENV=${NODE_ENV:-not set}"
echo "========================================="
# Purge any persisted FileSystemPersistence state from a prior container.
# @langchain/langgraph-api (v1.1.17) serialises ALL accumulated thread/run/
# checkpoint state via superjson.stringify on a 3-second timer; after enough
# runs the serialised string exceeds V8's ~512 MB string limit → uncaught
# RangeError in a Timeout callback → event-loop hang → watchdog kill.
# Because the state persists to disk, a plain container restart reloads the
# bloated file and re-crashes immediately. Deleting on every fresh boot breaks
# the cycle.
#
# PERSIST_DIR can be overridden by env var (useful for tests; defaults to the
# in-container path used by @langchain/langgraph-api's FileSystemPersistence).
PERSIST_DIR=${LANGGRAPH_PERSIST_DIR_OVERRIDE:-/app/src/agent/.langgraph_api}
if [ -d "$PERSIST_DIR" ]; then
echo "[entrypoint] Purging stale LangGraph persistence state from prior container boot (${PERSIST_DIR})"
rm -rf "$PERSIST_DIR"
echo "[entrypoint] Purge complete"
else
echo "[entrypoint] No prior persistence state found — clean boot"
fi
# Start LangGraph agent server in background.
# `npm start` runs `node --import tsx liveness.mjs` (see src/agent/package.json).
# liveness.mjs binds :8124/ok immediately using only node:http, then dynamic-
# imports server.mjs to kick off the real @langchain/langgraph-api bootstrap.
# We avoid `langgraph-cli dev` for the same reasons as before: dev wraps the
# server in `tsx watch` + chokidar + Studio IPC, and its schema-extraction
# worker is cold on first request (multi-second TS program compile).
# Production path:
# 1. `node --import tsx liveness.mjs` — tsx is a one-shot ESM hook so the
# subsequent dynamic import of server.mjs (and thence graph.ts) resolves
# without pre-compilation. NOT a watcher.
# 2. liveness.mjs brings up :8124/ok before any heavy import runs.
# 3. Dynamic-imported server.mjs pre-warms the schema cache before the
# first external /assistants/*/schemas hits.
#
# --host 0.0.0.0 via HOST env; binds IPv4+IPv6 so the Next.js frontend can
# reach the agent regardless of how `localhost` resolves in the container.
#
# Log prefixing uses bash process substitution (`&> >(awk …)`) rather than a
# pipe (`| sed …`) so `$!` (captured below as AGENT_PID) refers to the agent's
# own launch process and NOT the awk log-formatter — `wait -n $AGENT_PID` thus
# monitors the agent side, not the log pipeline. Note `$!`/AGENT_PID is the
# WRAPPING SUBSHELL of the `... &` compound command, NOT the real npm→node
# server it forks (the server is a DESCENDANT, reached only via the tree-kill —
# see the file header and _kill_agent_tree). Never `kill $AGENT_PID` directly:
# that reaps only the wrapper subshell and orphans the real server on :8123.
echo "[entrypoint] Starting LangGraph TS agent on port 8123 (prod mode, no CLI)..."
cd /app/src/agent && PORT=8123 HOST=0.0.0.0 npm start &> >(awk '{print "[agent] " $0; fflush()}') &
AGENT_PID=$!
cd /app
sleep 3
if kill -0 $AGENT_PID 2>/dev/null; then
echo "[entrypoint] Agent server started (PID: $AGENT_PID)"
else
echo "[entrypoint] ERROR: Agent server failed to start — exiting"
exit 1
fi
echo "========================================="
echo "[entrypoint] Starting Next.js frontend on port ${PORT:-10000}..."
echo "========================================="
PORT=${PORT:-10000}
# Scope NODE_ENV=production to the Next.js invocation ONLY, not the whole
# container environment. `ENV NODE_ENV=production` at the image level would
# leak into every child process (agent, shell, healthchecks). `env` prefix
# binds the value to this single exec.
env NODE_ENV=production npx next start --port $PORT &> >(awk '{print "[nextjs] " $0; fflush()}') &
NEXTJS_PID=$!
echo "[entrypoint] Next.js started (PID: $NEXTJS_PID)"
# Watchdog: Railway deploys of showcase packages have been observed to hit a
# silent agent hang — the langgraph process stays alive (so `wait -n` never
# fires and the container never restarts) but stops responding on :8123.
# Poll the liveness sidecar on :8124/ok every 30s (bound by liveness.mjs
# BEFORE server.mjs is dynamic-imported, so it is up within ms of node boot —
# independent of the multi-minute @langchain/langgraph-api top-level import
# that gates the main Hono bind on :8123). After 3 consecutive failures
# (~90s of unreachable agent), kill the agent process so `wait -n` returns
# and Railway restarts the container. Generalized from
# showcase/integrations/crewai-crews/entrypoint.sh (PRs #4114 + #4115).
#
# Startup grace: the prod path (see above — we deliberately avoid
# `langgraph-cli dev`) still pays a heavy cold-start from the top-level
# `@langchain/langgraph-api` import (schema extraction + graph compile) that
# gates the main Hono bind on :8123. On fresh Railway containers this routinely
# exceeds the 90s (3-strike) budget introduced in PR #4116, producing the 04-20
# restart loop seen on deployment
# 58bbebe8-7a94-4f99-b6e4-ffcbb4eb78b9. Wait up to 180s for the first
# healthy /ok probe before arming the strike counter; if /ok comes up
# sooner, fall through immediately. If 180s elapses without success, arm
# the counter anyway — the steady-state watchdog will handle a true hang.
#
# Size-gated restart: the watchdog also periodically checks the on-disk size
# of the PERSIST_DIR. @langchain/langgraph-api serialises state on a 3-second
# timer; if the state grows excessively (threads accumulating across a long
# deployment) the serialised string can approach V8's ~512 MB string limit.
# We set a conservative SIZE_THRESHOLD_MB (200 MB — well under the ceiling,
# with margin for the on-disk→in-memory approximation) and kill the agent
# when crossed, triggering a container restart which re-runs the boot-purge
# above. This kill WILL terminate any in-flight streaming runs — accepted
# tradeoff vs OOM/crash. The gate is threshold-based, not a fixed timer,
# so it fires only when state has grown dangerously large.
# We do NOT call POST /internal/truncate because:
# 1. ops.truncate with runs+threads+checkpointer+store=true wipes ALL runs
# including in-flight ones — the "in-flight not disrupted" comment on the
# original implementation was incorrect (R7-C1).
# 2. /internal/truncate is an unpinned internal library route with no
# stability guarantee across patch releases (C2).
SIZE_THRESHOLD_MB=${LANGGRAPH_SIZE_THRESHOLD_MB:-200}
# 60s interval: the 3-second serialize timer means state can grow rapidly
# under heavy probe fan-out (the original crash scenario). 300s (5 min)
# leaves too large a window — at typical probe rates state can exceed 512MB
# before the next check. 60s keeps the check-to-ceiling budget comfortable.
SIZE_CHECK_INTERVAL=${LANGGRAPH_SIZE_CHECK_INTERVAL:-60}
# Startup grace window, steady-state health-probe interval and strike budget.
# All operator-overridable so deploy tuning does not require an image rebuild.
STARTUP_GRACE_SECONDS=${LANGGRAPH_STARTUP_GRACE_SECONDS:-180}
HEALTH_CHECK_INTERVAL=${LANGGRAPH_HEALTH_CHECK_INTERVAL:-30}
HEALTH_STRIKE_LIMIT=${LANGGRAPH_HEALTH_STRIKE_LIMIT:-3}
# ---------------------------------------------------------------------------
# Numeric-config validation pass (CLASS 1 structural guard).
#
# Validate EVERY operator-overridable numeric knob at STARTUP, before any of
# them can feed a `sleep`, a loop count, or an arithmetic test. A bad override
# (non-integer / empty) on ANY of these would otherwise silently disable a guard
# — most dangerously LANGGRAPH_SIZE_CHECK_INTERVAL="60s", which makes the very
# first `while sleep $SIZE_CHECK_INTERVAL` fail and kills the size-monitor loop
# for the container's lifetime. Each bad value WARNs and falls back to the
# documented default (fail-safe: never abort, never leave a guard disabled).
_require_int SIZE_THRESHOLD_MB 200 "LangGraph persistence size threshold (MB)"
_require_int SIZE_CHECK_INTERVAL 60 "LangGraph size-check interval (s)"
_require_int STARTUP_GRACE_SECONDS 180 "LangGraph startup grace window (s)"
_require_int HEALTH_CHECK_INTERVAL 30 "LangGraph health-probe interval (s)"
_require_int HEALTH_STRIKE_LIMIT 3 "LangGraph health strike limit"
(
GRACE=$STARTUP_GRACE_SECONDS
# Arm the size sub-loop's reaping trap BEFORE spawning it (arm-then-spawn), and
# start the size monitor BEFORE the startup-grace loop.
#
# TRAP ORDERING (no-leak-window): the reaping trap is registered FIRST and does
# not depend on SIZE_PID being assigned yet. The previous order spawned the
# sub-loop (`( … ) &`, SIZE_PID=$!) and only THEN registered
# `trap 'kill "$SIZE_PID"' EXIT` — leaving a tiny window in which an outer
# SIGTERM arriving between the `&` and the `trap` would exit this watchdog
# subshell with NO trap armed, orphaning the size sub-loop (reparented to PID 1,
# left spinning for the container's life). We instead arm a trap that reaps by
# a PPID walk of THIS subshell ($BASHPID) — it finds the sub-loop regardless of
# whether SIZE_PID has been assigned, so cleanup no longer DEPENDS on the
# ordering of SIZE_PID's assignment. SIZE_PID is still captured and the
# direct `kill "$SIZE_PID"` below is RETAINED as a belt-and-suspenders backstop
# to the $BASHPID PPID-walk (do not remove it): if the walk ever misses the
# sub-loop, the explicit PID kill still reaps it.
_reap_watchdog_children() {
local sp
for sp in $(_agent_descendants "$BASHPID"); do
kill "$sp" 2>/dev/null || true
done
[ -n "${SIZE_PID:-}" ] && kill "$SIZE_PID" 2>/dev/null || true
return 0
}
trap _reap_watchdog_children EXIT
# Size-gated restart sub-loop: periodically check the persistence dir size
# and kill the agent if it exceeds SIZE_THRESHOLD_MB. The container restart
# will re-run the boot-purge, clearing accumulated state safely.
#
# STARTED BEFORE THE GRACE LOOP (cold-start coverage): the size ceiling must be
# guarded during the up-to-180s startup-grace window too, not only after it.
# The previous order started this monitor only AFTER the grace loop returned,
# so a pathological cold boot that bloats PERSIST_DIR during startup went
# unguarded for up to 180s. Starting it here is safe because
# _watchdog_check_size_once already fail-closes on every not-yet-ready
# condition: agent PID not alive (`kill -0` fails → return 0), PERSIST_DIR
# missing (freshly purged on boot → return 0), and non-numeric/absent size or
# threshold (WARN + return 0). Early cycles are therefore harmless no-ops
# until the dir exists and actually grows.
(
echo "[watchdog:size] Starting size-gated restart monitor (threshold=${SIZE_THRESHOLD_MB}MB, interval=${SIZE_CHECK_INTERVAL}s, dir=${PERSIST_DIR})"
while sleep $SIZE_CHECK_INTERVAL; do
# _watchdog_check_size_once returns:
# 0 = no action (under threshold, dir missing, or a transient/junk read
# that left the guard inactive for this cycle only)
# 1 = REAL threshold kill issued (agent tree SIGKILLed)
# A bare `|| break` treated ANY non-zero identically, so a transient check
# error would PERMANENTLY end the monitor for the container's lifetime.
# Break ONLY on the real-kill signal (rc==1): after that kill the agent is
# dead, the outer `wait -n $AGENT_PID` fires, and Railway restarts the
# container (re-running the boot-purge) — so the monitor correctly stops.
# For rc==0 (including transient errors, which are surfaced as a WARNING
# inside the function) the monitor MUST stay live and re-check next cycle.
_watchdog_check_size_once && continue
rc=$?
if [ "$rc" -eq 1 ]; then
# Real kill issued — agent is terminating; stop the monitor and let the
# container restart flow (wait -n → exit → Railway restart) proceed.
break
fi
# Any other non-zero is a transient hiccup, not a kill: keep monitoring.
echo "[watchdog:size] Size check returned transient status ${rc} — keeping monitor active"
done
) &
SIZE_PID=$!
echo "[watchdog] Startup grace: waiting up to ${GRACE}s for first successful health probe before arming strike counter"
ELAPSED=0
while [ $ELAPSED -lt $GRACE ]; do
if ! kill -0 $AGENT_PID 2>/dev/null; then
# Agent died during startup — wait -n in the main shell will handle it.
exit 0
fi
if curl -fsS --max-time 5 http://127.0.0.1:8124/ok > /dev/null 2>&1; then
echo "[watchdog] Agent healthy after ${ELAPSED}s — arming strike counter"
break
fi
sleep 5
ELAPSED=$((ELAPSED + 5))
done
if [ $ELAPSED -ge $GRACE ]; then
echo "[watchdog] Grace window elapsed without successful probe — arming strike counter anyway"
fi
FAILS=0
while sleep "$HEALTH_CHECK_INTERVAL"; do
if ! kill -0 $AGENT_PID 2>/dev/null; then
break
fi
if curl -fsS --max-time 5 http://127.0.0.1:8124/ok > /dev/null 2>&1; then
FAILS=0
else
FAILS=$((FAILS + 1))
echo "[watchdog] Agent health probe failed (count=$FAILS)"
if [ $FAILS -ge "$HEALTH_STRIKE_LIMIT" ]; then
echo "[watchdog] Agent unresponsive for ~$((HEALTH_CHECK_INTERVAL * HEALTH_STRIKE_LIMIT))s — killing PID $AGENT_PID (and its npm→node tree) to trigger container restart"
# Tree-kill for the same reason as the size gate: $AGENT_PID is the
# process-sub subshell; a single-PID kill would orphan npm→node and
# leave :8123 bound to a hung agent that `wait -n` never observes dying.
_kill_agent_tree "$AGENT_PID"
break
fi
fi
done
) &
WATCHDOG_PID=$!
echo "[entrypoint] Watchdog started (PID: $WATCHDOG_PID, probing http://127.0.0.1:8124/ok, startup grace ${STARTUP_GRACE_SECONDS}s, size-guard threshold ${SIZE_THRESHOLD_MB}MB every ${SIZE_CHECK_INTERVAL}s)"
echo "[entrypoint] All processes running. Waiting..."
# Only wait on agent + next.js — NOT the watchdog. The watchdog's job is to
# kill the agent when it hangs; if the watchdog exits first, `wait -n` would
# otherwise return with the watchdog's exit code and short-circuit before
# the agent's true exit status is observable.
#
# `|| EXIT_CODE=$?` is LOAD-BEARING under `set -e`: the PRIMARY designed exit
# path here is a NON-ZERO wait (137 = the size-gate / watchdog SIGKILL of the
# agent tree, or an agent crash). Without the `||` guard, `set -e` aborts the
# script AT this line on exactly those interesting exits, making the entire
# "which process exited with code N" diagnostic below AND the final explicit
# `exit $EXIT_CODE` dead code — the container still restarts (EXIT trap runs
# cleanup, script exits non-zero) but the operator-facing diagnostic never
# prints. Capturing the code preserves it exactly (incl. 137) for both the
# diagnostic and the final exit, and the container-restart path is unchanged.
#
# REAPED_PID via `wait -n -p VAR` (bash >= 5.1; node:22-slim ships 5.2) captures
# the ACTUAL PID `wait -n` reaped. The previous `kill -0` if/elif merely INFERRED
# which process exited by probing liveness AFTER the wait — racy on a near-
# simultaneous exit: if both are dead by the time we probe, the first `kill -0`
# branch always wins and mislabels the diagnostic (e.g. reports the agent when
# Next.js is what actually exited, attaching the wrong code to the wrong name).
# Keying the message off the reaped PID names the correct process every time.
# `|| EXIT_CODE=$?` remains LOAD-BEARING (see above): -p does not change that a
# non-zero wait would abort under set -e without the guard.
REAPED_PID=""
EXIT_CODE=0
wait -n -p REAPED_PID "$AGENT_PID" "$NEXTJS_PID" || EXIT_CODE=$?
if [ "$REAPED_PID" = "$AGENT_PID" ]; then
echo "[entrypoint] Agent (PID: $AGENT_PID) exited with code $EXIT_CODE"
elif [ "$REAPED_PID" = "$NEXTJS_PID" ]; then
echo "[entrypoint] Next.js (PID: $NEXTJS_PID) exited with code $EXIT_CODE"
else
echo "[entrypoint] A process (PID: ${REAPED_PID:-unknown}) exited with code $EXIT_CODE"
fi
exit $EXIT_CODE
@@ -0,0 +1,548 @@
name: LangGraph (TypeScript)
slug: langgraph-typescript
category: popular
language: typescript
logo: /logos/langgraph-typescript.svg
description: >-
TypeScript variant of the LangGraph integration. Uses @langchain/langgraph
to define a graph-based agent with tool calling, shared state, and memory,
connected to CopilotKit via the LangGraph AG-UI adapter.
partner_docs: https://docs.copilotkit.ai/integrations/langgraph
repo: https://github.com/CopilotKit/CopilotKit/tree/main/showcase/integrations/langgraph-typescript
copilotkit_version: 2.0.0
deployed: true
docs_mode: generated
sort_order: 11
generative_ui:
- constrained-explicit
- a2ui-fixed-schema
- a2ui-dynamic-schema
interaction_modalities:
- sidebar
- embedded
- chat
managed_platform:
name: LangGraph Platform
url: https://langsmith.com
not_supported_features:
- shared-state-streaming
# QUARANTINED pending a @copilotkit/react-core release. gen-ui-interrupt /
# interrupt-headless fail turn-2 on a useInterrupt/useHeadlessInterrupt
# RESUME-PATH hook bug in @copilotkit/react-core/v2: backend resumes + streams
# (HTTP 200) but the frontend never appends the confirmation assistant bubble,
# so the harness DOM settle-check times out. Fix is a published-package change
# (out of scope here); honestly marked not-supported (skipped-incapable, not
# green, not red) rather than a regression. Demos remain wired below.
- gen-ui-interrupt
- interrupt-headless
- reasoning-default-render
- agentic-chat-reasoning
- tool-rendering-reasoning-chain
features:
- cli-start
- agentic-chat
- hitl-in-app
- hitl-in-chat
- gen-ui-tool-based
- shared-state-read
- shared-state-read-write
- subagents
- tool-rendering
- tool-rendering-default-catchall
- tool-rendering-custom-catchall
- reasoning-custom
- reasoning-default
- gen-ui-agent
- beautiful-chat
- prebuilt-sidebar
- prebuilt-popup
- chat-slots
- chat-customization-css
- headless-simple
- headless-complete
- auth
- multimodal
- agent-config
- declarative-gen-ui
- a2ui-fixed-schema
- a2ui-recovery
- mcp-apps
- frontend-tools
- frontend-tools-async
- readonly-state-agent-context
- voice
- declarative-hashbrown
- declarative-json-render
- open-gen-ui
- open-gen-ui-advanced
a2ui_pattern: schema-loading
interrupt_pattern: native
thread_persistence_pattern: langgraph
agent_config_pattern: shared-state
auth_pattern: langgraph
demos:
- id: cli-start
name: CLI Start Command
description: Copy-paste command to clone the canonical starter
tags:
- chat-ui
command: "npx copilotkit@latest init --framework langgraph-typescript"
- id: agentic-chat
name: "Pre-Built: CopilotChat"
description: Natural conversation with frontend tool execution
tags:
- chat-ui
route: /demos/agentic-chat
animated_preview_url:
- id: hitl-in-app
name: "Human in the Loop: In-app"
description: Agent requests approval via useFrontendTool with an async handler; the approval UI pops up as an app-level modal OUTSIDE the chat, and a completion callback resolves the pending tool Promise with the user's decision
tags:
- interactivity
route: /demos/hitl-in-app
animated_preview_url:
highlight:
- src/agent/hitl-in-app.ts
- src/app/demos/hitl-in-app/page.tsx
- src/app/demos/hitl-in-app/approval-dialog.tsx
- src/app/api/copilotkit/route.ts
- id: hitl-in-chat
name: "Human In the Loop: In-chat"
description: Interactive approval/decision surface rendered inline in the chat via the high-level `useHumanInTheLoop` hook
tags:
- interactivity
route: /demos/hitl-in-chat
animated_preview_url:
highlight:
- src/agent/hitl-in-chat.ts
- src/app/demos/hitl-in-chat/page.tsx
- src/app/demos/hitl-in-chat/time-picker-card.tsx
- src/app/api/copilotkit/route.ts
- id: tool-rendering
name: "Generative UI: Tool Rendering (Custom)"
description: Backend agent tools rendered as UI components
tags:
- agent-capabilities
route: /demos/tool-rendering
animated_preview_url:
highlight:
- src/agent/tool-rendering.ts
- src/app/demos/tool-rendering/page.tsx
- id: gen-ui-tool-based
name: "Generative UI: useComponent"
description: Agent uses tools to trigger UI generation
tags:
- generative-ui
route: /demos/gen-ui-tool-based
animated_preview_url:
highlight:
- src/app/demos/gen-ui-tool-based/page.tsx
- src/app/api/copilotkit/route.ts
- id: gen-ui-agent
name: "Generative UI: Agent State"
description: Long-running agent tasks with generated UI
tags:
- generative-ui
route: /demos/gen-ui-agent
animated_preview_url:
- id: shared-state-streaming
name: "Shared State: Streaming"
description: Per-token state delta streaming from agent to UI
tags:
- agent-state
route: /demos/shared-state-streaming
animated_preview_url:
- id: shared-state-read-write
name: "Shared State: Read + Write"
description: Bidirectional shared state — UI writes preferences via agent.setState; agent writes notes via a Command-returning tool
tags:
- agent-state
route: /demos/shared-state-read-write
animated_preview_url:
highlight:
- src/agent/shared-state-read-write.ts
- src/app/demos/shared-state-read-write/page.tsx
- src/app/demos/shared-state-read-write/preferences-card.tsx
- src/app/demos/shared-state-read-write/notes-card.tsx
- src/app/api/copilotkit/route.ts
- id: shared-state-read
name: "Shared State: Read-only"
description: "Recipe editor publishes form state via agent.setState; the agent reads the recipe context but does not mutate it (no backend tool — neutral default agent)."
tags:
- agent-state
route: /demos/shared-state-read
animated_preview_url:
highlight:
- src/app/demos/shared-state-read/page.tsx
- src/app/demos/shared-state-read/recipe-card.tsx
- src/app/demos/shared-state-read/types.ts
- src/app/api/copilotkit/route.ts
- id: subagents
name: Sub-Agents
description: Supervisor delegates to research / writing / critique sub-agents exposed as tools — each delegation appended to a live log in shared state
tags:
- agent-capabilities
route: /demos/subagents
animated_preview_url:
highlight:
- src/agent/subagents.ts
- src/app/demos/subagents/page.tsx
- src/app/demos/subagents/delegation-log.tsx
- src/app/api/copilotkit/route.ts
- id: beautiful-chat
name: Beautiful Chat
description: Canonical polished starter chat — brand fonts, theme tokens, suggestion pills
tags:
- chat-ui
route: /demos/beautiful-chat
animated_preview_url:
highlight:
- src/agent/beautiful-chat.ts
- src/app/demos/beautiful-chat/page.tsx
- src/app/demos/beautiful-chat/components/example-layout/index.tsx
- src/app/demos/beautiful-chat/components/example-canvas/index.tsx
- src/app/demos/beautiful-chat/components/generative-ui/charts/bar-chart.tsx
- src/app/demos/beautiful-chat/components/generative-ui/charts/pie-chart.tsx
- src/app/demos/beautiful-chat/hooks/use-example-suggestions.tsx
- src/app/demos/beautiful-chat/hooks/use-generative-ui-examples.tsx
- src/app/api/copilotkit-beautiful-chat/route.ts
- id: prebuilt-sidebar
name: "Pre-Built: Sidebar"
description: Docked sidebar chat via <CopilotSidebar />
tags:
- chat-ui
route: /demos/prebuilt-sidebar
animated_preview_url:
highlight:
- src/agent/graph.ts
- src/app/demos/prebuilt-sidebar/page.tsx
- src/app/api/copilotkit/route.ts
- id: prebuilt-popup
name: "Pre-Built: Popup"
description: Floating popup chat via <CopilotPopup />
tags:
- chat-ui
route: /demos/prebuilt-popup
animated_preview_url:
highlight:
- src/agent/graph.ts
- src/app/demos/prebuilt-popup/page.tsx
- src/app/api/copilotkit/route.ts
- id: chat-slots
name: "Chat Customization: Slots"
description: Customize CopilotChat via its slot system
tags:
- chat-ui
route: /demos/chat-slots
animated_preview_url:
highlight:
- src/agent/graph.ts
- src/app/demos/chat-slots/page.tsx
- src/app/demos/chat-slots/slot-wrappers.tsx
- src/app/api/copilotkit/route.ts
- id: chat-customization-css
name: "Chat Customization: CSS"
description: Default CopilotChat re-themed via CopilotKitCSSProperties
tags:
- chat-ui
route: /demos/chat-customization-css
animated_preview_url:
highlight:
- src/agent/graph.ts
- src/app/demos/chat-customization-css/page.tsx
- src/app/demos/chat-customization-css/theme.css
- src/app/api/copilotkit/route.ts
- id: headless-simple
name: "Headless UI: Simple"
description: Minimal custom chat surface built on useAgent
tags:
- chat-ui
route: /demos/headless-simple
animated_preview_url:
highlight:
- src/agent/graph.ts
- src/app/demos/headless-simple/page.tsx
- src/app/api/copilotkit/route.ts
- id: headless-complete
name: "Headless UI: Complete"
description: Full chat implementation built from scratch on useAgent
tags:
- chat-ui
route: /demos/headless-complete
animated_preview_url:
highlight:
- src/agent/headless-complete.ts
- src/app/demos/headless-complete/page.tsx
- src/app/demos/headless-complete/chat/chat.tsx
- src/app/demos/headless-complete/chat/message-list.tsx
- src/app/demos/headless-complete/hooks/use-tool-renderers.tsx
- src/app/demos/headless-complete/hooks/use-frontend-components.ts
- src/app/demos/headless-complete/hooks/use-headless-suggestions.ts
- src/app/demos/headless-complete/attachments/use-attachments-config.ts
- src/app/demos/headless-complete/tools/weather-card.tsx
- src/app/demos/headless-complete/tools/stock-card.tsx
- src/app/demos/headless-complete/tools/chart-card.tsx
- src/app/api/copilotkit-mcp-apps/route.ts
- id: auth
name: Authentication
description: Bearer-token gate via runtime onRequest hook with unauthenticated / authenticated states
tags:
- chat-ui
route: /demos/auth
animated_preview_url:
highlight:
- src/app/demos/auth/page.tsx
- src/app/demos/auth/auth-banner.tsx
- src/app/demos/auth/use-demo-auth.ts
- src/app/demos/auth/demo-token.ts
- src/app/api/copilotkit-auth/[[...slug]]/route.ts
- id: multimodal
name: Attachments
description: Image and PDF uploads via CopilotChat attachments, processed by a vision-capable agent
tags:
- chat-ui
route: /demos/multimodal
animated_preview_url:
highlight:
- src/agent/multimodal.ts
- src/app/demos/multimodal/page.tsx
- src/app/demos/multimodal/sample-attachment-buttons.tsx
- src/app/api/copilotkit-multimodal/route.ts
- id: agent-config
name: Agent Config Object
description: Forward a typed config object (tone / expertise / response length) from the provider to the agent's dynamic system prompt
tags:
- platform
route: /demos/agent-config
animated_preview_url:
highlight:
- src/agent/agent-config.ts
- src/app/demos/agent-config/page.tsx
- src/app/demos/agent-config/config-card.tsx
- src/app/demos/agent-config/use-agent-config.ts
- src/app/demos/agent-config/config-types.ts
- src/app/api/copilotkit-agent-config/route.ts
- id: tool-rendering-default-catchall
name: "Generative UI: Tool Rendering (Default)"
description: Out-of-the-box tool rendering — backend defines the tools; the frontend adds zero custom renderers and relies on CopilotKit's built-in default UI
tags:
- generative-ui
route: /demos/tool-rendering-default-catchall
animated_preview_url:
- id: tool-rendering-custom-catchall
name: "Generative UI: Tool Rendering (Catch-all)"
description: Single branded wildcard renderer via useDefaultRenderTool — the same app-designed card paints every tool call
tags:
- generative-ui
route: /demos/tool-rendering-custom-catchall
animated_preview_url:
- id: tool-rendering-reasoning-chain
name: "Generative UI: Tool calls + reasoning"
description: Sequential tool calls with reasoning tokens rendered side-by-side
tags:
- generative-ui
route: /demos/tool-rendering-reasoning-chain
animated_preview_url:
- id: reasoning-custom
name: "Reasoning: Custom"
description: Visible reasoning/thinking chain alongside the final answer
tags:
- chat-ui
route: /demos/reasoning-custom
animated_preview_url:
highlight:
- src/app/demos/reasoning-custom/page.tsx
- src/app/demos/reasoning-custom/reasoning-block.tsx
- src/app/demos/reasoning-custom/suggestions.ts
- id: reasoning-default
name: "Reasoning: Default"
description: Built-in CopilotChatReasoningMessage rendering with no slot override.
tags:
- chat-ui
route: /demos/reasoning-default
animated_preview_url:
highlight:
- src/app/demos/reasoning-default/page.tsx
- src/app/demos/reasoning-default/suggestions.ts
- id: gen-ui-interrupt
name: "Human in the Loop: Interrupts"
description: Interactive component rendered inline in the chat via the lower-level `useInterrupt` primitive — direct control over the LangGraph interrupt lifecycle
tags:
- generative-ui
route: /demos/gen-ui-interrupt
animated_preview_url:
highlight:
- src/agent/interrupt-agent.ts
- src/app/demos/gen-ui-interrupt/page.tsx
- src/app/demos/gen-ui-interrupt/_components/time-picker-card.tsx
- src/app/api/copilotkit/route.ts
- id: interrupt-headless
name: "Human in the Loop: Headless Interrupts"
description: Resolve langgraph interrupts from a plain button grid — no chat, no useInterrupt render prop
tags:
- interactivity
route: /demos/interrupt-headless
animated_preview_url:
highlight:
- src/agent/interrupt-agent.ts
- src/app/demos/interrupt-headless/page.tsx
- src/app/api/copilotkit/route.ts
- id: declarative-gen-ui
name: "Declarative UI: Dynamic A2UI"
description: Canonical A2UI BYOC — custom catalog (Card/StatusBadge/Metric/InfoRow/PrimaryButton) wired via a2ui.catalog on the provider
tags:
- generative-ui
route: /demos/declarative-gen-ui
animated_preview_url:
highlight:
- src/agent/a2ui-dynamic.ts
- src/app/demos/declarative-gen-ui/page.tsx
- src/app/demos/declarative-gen-ui/a2ui/catalog.ts
- src/app/demos/declarative-gen-ui/a2ui/definitions.ts
- src/app/demos/declarative-gen-ui/a2ui/renderers.tsx
- src/app/api/copilotkit-declarative-gen-ui/route.ts
- id: a2ui-fixed-schema
name: "Declarative UI: Fixed A2UI"
description: A2UI rendering against a known client-side schema
tags:
- generative-ui
route: /demos/a2ui-fixed-schema
animated_preview_url:
highlight:
- src/agent/a2ui-fixed.ts
- src/agent/a2ui_schemas/flight_schema.json
- src/agent/a2ui_schemas/booked_schema.json
- src/app/demos/a2ui-fixed-schema/page.tsx
- src/app/demos/a2ui-fixed-schema/a2ui/catalog.ts
- src/app/demos/a2ui-fixed-schema/a2ui/definitions.ts
- src/app/demos/a2ui-fixed-schema/a2ui/renderers.tsx
- src/app/api/copilotkit-a2ui-fixed-schema/route.ts
- id: a2ui-recovery
name: A2UI Error Recovery
description: Makes the A2UI validate->retry recovery loop visible — an invalid first render heals to a valid one, and an always-invalid render shows a graceful recovery-exhausted fallback. Backend-owned via getA2UITools (injectA2UITool=false); reuses the declarative-gen-ui catalog.
tags:
- generative-ui
route: /demos/a2ui-recovery
animated_preview_url:
highlight:
- src/agent/recovery-agent.ts
- src/app/demos/a2ui-recovery/page.tsx
- src/app/demos/a2ui-recovery/suggestions.ts
- src/app/api/copilotkit-a2ui-recovery/route.ts
- id: mcp-apps
name: MCP Apps
description: MCP server-driven UI via activity renderers
tags:
- generative-ui
route: /demos/mcp-apps
animated_preview_url:
highlight:
- src/agent/mcp-apps.ts
- src/app/demos/mcp-apps/page.tsx
- src/app/api/copilotkit-mcp-apps/route.ts
- id: frontend-tools
name: "Frontend Tools: In-app Actions"
description: Agent invokes client-side handlers registered with useFrontendTool
tags:
- interactivity
route: /demos/frontend-tools
animated_preview_url:
highlight:
- src/agent/frontend-tools.ts
- src/app/demos/frontend-tools/page.tsx
- src/app/api/copilotkit/route.ts
- id: frontend-tools-async
name: "Frontend Tools: Async"
description: useFrontendTool with an async handler — agent awaits a client-side async operation (simulated notes DB query) and uses the returned result
tags:
- interactivity
route: /demos/frontend-tools-async
animated_preview_url:
highlight:
- src/agent/frontend-tools-async.ts
- src/app/demos/frontend-tools-async/page.tsx
- src/app/demos/frontend-tools-async/notes-card.tsx
- src/app/api/copilotkit/route.ts
- id: readonly-state-agent-context
name: "Shared State: Frontend Context"
description: Frontend provides read-only context to the agent via useAgentContext
tags:
- agent-state
route: /demos/readonly-state-agent-context
animated_preview_url:
highlight:
- src/agent/readonly-state.ts
- src/app/demos/readonly-state-agent-context/page.tsx
- src/app/api/copilotkit/route.ts
- id: voice
name: Voice
description: Speech-to-text via @copilotkit/voice with a bundled sample audio button
tags:
- chat-ui
route: /demos/voice
animated_preview_url:
highlight:
- src/agent/graph.ts
- src/app/demos/voice/page.tsx
- src/app/demos/voice/sample-audio-button.tsx
- src/app/api/copilotkit-voice/[[...slug]]/route.ts
- id: declarative-hashbrown
name: "Declarative UI: Hashbrown"
description: Streaming structured output via @hashbrownai/react, rendering a sales dashboard catalog (MetricCard + PieChart + BarChart)
tags:
- generative-ui
route: /demos/declarative-hashbrown
animated_preview_url:
highlight:
- src/agent/byoc-hashbrown.ts
- src/app/demos/declarative-hashbrown/page.tsx
- src/app/demos/declarative-hashbrown/hashbrown-renderer.tsx
- src/app/demos/declarative-hashbrown/metric-card.tsx
- src/app/demos/declarative-hashbrown/charts/bar-chart.tsx
- src/app/demos/declarative-hashbrown/charts/pie-chart.tsx
- src/app/demos/declarative-hashbrown/types.ts
- src/app/demos/declarative-hashbrown/suggestions.ts
- src/app/api/copilotkit-declarative-hashbrown/route.ts
- id: declarative-json-render
name: "Declarative UI: json-render"
description: Streaming hierarchical JSON UI spec rendered via @json-render/react, with a Zod-validated catalog (MetricCard + PieChart + BarChart)
tags:
- generative-ui
route: /demos/declarative-json-render
animated_preview_url:
highlight:
- src/agent/byoc-json-render.ts
- src/app/demos/declarative-json-render/page.tsx
- src/app/demos/declarative-json-render/json-render-renderer.tsx
- src/app/demos/declarative-json-render/registry.tsx
- src/app/demos/declarative-json-render/catalog.ts
- src/app/demos/declarative-json-render/metric-card.tsx
- src/app/demos/declarative-json-render/charts/bar-chart.tsx
- src/app/demos/declarative-json-render/charts/pie-chart.tsx
- src/app/demos/declarative-json-render/types.ts
- src/app/demos/declarative-json-render/suggestions.ts
- src/app/api/copilotkit-declarative-json-render/route.ts
- id: open-gen-ui
name: "Open Generative UI: Default"
description: Agent generates UI from an arbitrary component library
tags:
- generative-ui
route: /demos/open-gen-ui
animated_preview_url:
highlight:
- src/agent/open-gen-ui.ts
- src/app/demos/open-gen-ui/page.tsx
- src/app/api/copilotkit-ogui/route.ts
- id: open-gen-ui-advanced
name: "Open Generative UI: Custom"
description: Agent-authored UI that can invoke frontend sandbox functions from inside the iframe
tags:
- generative-ui
route: /demos/open-gen-ui-advanced
animated_preview_url:
highlight:
- src/agent/open-gen-ui-advanced.ts
- src/app/demos/open-gen-ui-advanced/page.tsx
- src/app/demos/open-gen-ui-advanced/sandbox-functions.ts
- src/app/demos/open-gen-ui-advanced/suggestions.ts
- src/app/api/copilotkit-ogui/route.ts
@@ -0,0 +1,40 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: ["@copilotkit/runtime"],
// The staged CVDIAG emitter (src/cvdiag/*) uses NodeNext-style relative
// imports with explicit `.js` extensions (e.g. `import … from "./schema.js"`).
// These are kept verbatim from the canonical L0-A sources so
// `showcase cvdiag-stage-ts --check` stays in sync, and `tsc` resolves
// `.js` → `.ts`. Webpack does NOT do that by default, so teach it the same
// extension alias; otherwise `next build` fails with
// "Module not found: Can't resolve './schema.js'".
webpack(config) {
config.resolve.extensionAlias = {
...config.resolve.extensionAlias,
".js": [".ts", ".tsx", ".js"],
};
return config;
},
// Allow iframe embedding from the showcase shell
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "X-Frame-Options",
value: "ALLOWALL",
},
{
key: "Content-Security-Policy",
value: "frame-ancestors *;",
},
],
},
];
},
typescript: { ignoreBuildErrors: true },
};
export default nextConfig;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,63 @@
{
"name": "@copilotkit/showcase-langgraph-typescript",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "concurrently \"next dev --turbopack\" \"cd src/agent && npx @langchain/langgraph-cli@1.2.1 dev --port 8123 --no-browser\"",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test:e2e": "playwright test"
},
"dependencies": {
"@copilotkit/a2ui-renderer": "1.61.2",
"@copilotkit/react-core": "1.61.2",
"@copilotkit/react-ui": "1.61.2",
"@copilotkit/runtime": "1.61.2",
"@copilotkit/sdk-js": "1.61.2",
"@copilotkit/shared": "1.61.2",
"@copilotkit/voice": "1.61.2",
"@hashbrownai/core": "0.5.0-beta.4",
"@hashbrownai/react": "0.5.0-beta.4",
"@json-render/core": "0.18.0",
"@json-render/react": "0.18.0",
"@langchain/core": "1.1.44",
"@langchain/langgraph": "1.3.0",
"@langchain/langgraph-checkpoint": "1.0.0",
"@langchain/openai": "1.4.4",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-separator": "^1.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"langchain": "1.3.4",
"lucide-react": "^0.460.0",
"next": "^15.5.15",
"openai": "^5.9.0",
"pdf-parse": "^1.1.1",
"radix-ui": "^1.4.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-markdown": "^10.1.0",
"recharts": "^2.15.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^2.5.5",
"yaml": "^2.8.4",
"zod": "^3.24.0"
},
"devDependencies": {
"@playwright/test": "^1.50.0",
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22.0.0",
"@types/pdf-parse": "^1.1.4",
"@types/react": "^19.0.0",
"concurrently": "^9.1.0",
"postcss": "^8.5.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.0"
},
"overrides": {
"@copilotkit/web-inspector": {
"@copilotkit/core": "1.61.2"
}
}
}
@@ -0,0 +1,38 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: process.env.BASE_URL || "http://localhost:3000",
trace: "on-first-retry",
extraHTTPHeaders: {
"X-AIMock-Context": "langgraph-typescript",
},
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: process.env.CI
? undefined
: {
command: "pnpm dev",
url: "http://localhost:3000",
reuseExistingServer: true,
env: {
...process.env,
// Default to local aimock (Docker-exposed) when not set.
// In CI the env is inherited from docker-compose (http://aimock:4010/v1).
OPENAI_BASE_URL:
process.env.OPENAI_BASE_URL || "http://localhost:4010/v1",
OPENAI_API_KEY: process.env.OPENAI_API_KEY || "sk-mock-local-dev",
},
},
});
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
@@ -0,0 +1,30 @@
<svg viewBox="0 0 224.43 237.166" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M76.4556 75.7547C97.0552 48.81 114.152 22.1656 120.731 0.648813C120.908 0.0633572 121.594 -0.185355 122.103 0.152636C144.979 15.3013 186.646 25.2726 223.5 25.5066C224.134 25.5107 224.571 26.1359 224.342 26.7272C212.088 57.8146 197.122 113.518 196.54 177.128C196.54 178.073 195.21 178.412 194.742 177.59C173.768 140.886 106.586 89.3107 76.7986 77.138C76.2477 76.9114 76.0919 76.2307 76.4556 75.7547Z" fill="url(#paint0_linear)"/>
<path d="M145.956 59.273C113.757 69.4674 84.3336 75.1349 77.3077 76.4226C76.8608 76.5047 76.7673 77.1231 77.1934 77.2977C107.209 89.777 174.059 141.202 194.835 177.757C194.877 177.837 194.981 177.867 195.064 177.83C195.147 177.791 195.189 177.687 195.158 177.597L145.956 59.273Z" fill="url(#paint1_linear)"/>
<path d="M122.197 0.0860308C149.76 15.1211 181.615 21.8738 223.875 25.4319C224.135 25.4546 224.228 25.8103 223.989 25.9339C218.585 28.7116 187.623 44.4667 164.633 52.905C158.47 55.1657 152.275 57.263 146.174 59.1979C146.039 59.2402 145.894 59.1736 145.842 59.0446L121.563 0.655481C121.397 0.262302 121.823 -0.117886 122.197 0.0860308Z" fill="url(#paint2_linear)"/>
<path d="M121.361 0.145972C121.761 -0.0218955 122.214 0.121754 122.45 0.467128L122.536 0.6264L196.496 177.058L196.548 177.233C196.628 177.642 196.415 178.065 196.015 178.233C195.615 178.401 195.162 178.257 194.926 177.912L194.838 177.753L120.881 1.32093L120.826 1.14599C120.745 0.736568 120.961 0.313703 121.361 0.145972Z" fill="#513C9F"/>
<path d="M223.089 25.5869C223.52 25.3424 224.069 25.4925 224.313 25.9238C224.558 26.3552 224.406 26.9035 223.974 27.1483V27.1509H223.969C223.965 27.153 223.96 27.1575 223.953 27.1614C223.939 27.1695 223.916 27.1821 223.888 27.1979C223.832 27.2294 223.749 27.2755 223.64 27.3363C223.419 27.4593 223.091 27.6413 222.661 27.8768C221.8 28.3482 220.529 29.0371 218.885 29.9029C215.597 31.6348 210.813 34.0821 204.83 36.9423C192.865 42.6619 176.091 50.0388 156.86 56.6737C137.624 63.3089 117.782 68.4614 102.755 71.9534C95.2398 73.6998 88.9245 75.0317 84.4882 75.9274C82.2701 76.3752 80.5211 76.7135 79.3262 76.9405C78.7293 77.0538 78.2706 77.1391 77.9606 77.1963C77.8058 77.2249 77.6873 77.2472 77.6081 77.2616C77.5693 77.2687 77.5395 77.2737 77.5194 77.2773C77.5095 77.2791 77.501 77.2816 77.4959 77.2825H77.488L77.3052 77.2982C76.8881 77.2877 76.5205 76.9859 76.4436 76.5593C76.356 76.0711 76.6815 75.6027 77.1695 75.5149H77.1773C77.182 75.514 77.1888 75.5113 77.1982 75.5096C77.2176 75.5061 77.2481 75.501 77.287 75.494C77.3647 75.4798 77.4812 75.4595 77.6342 75.4313C77.9413 75.3746 78.3978 75.2882 78.992 75.1754C80.1806 74.9497 81.9227 74.6138 84.1331 74.1676C88.5551 73.2748 94.8521 71.9459 102.348 70.204C117.342 66.7197 137.119 61.5841 156.276 54.9766C175.426 48.3694 192.134 41.0193 204.055 35.3208C210.015 32.4718 214.777 30.0352 218.047 28.3128C219.682 27.4518 220.944 26.7694 221.796 26.3024C222.222 26.0693 222.546 25.8906 222.763 25.7697C222.871 25.7092 222.954 25.6619 223.008 25.6313C223.035 25.6163 223.055 25.6049 223.068 25.5974C223.075 25.5937 223.08 25.5914 223.084 25.5895L223.086 25.5869H223.089Z" fill="#513C9F"/>
<path d="M2.77027 236.611C2.20838 237.273 1.21525 237.353 0.553517 236.792C-0.107154 236.23 -0.18789 235.239 0.373357 234.577L2.77027 236.611ZM132.306 21.7584C133.136 22.0085 133.607 22.8858 133.358 23.7167L106.569 112.86H169.57L169.886 112.891C170.602 113.037 171.142 113.672 171.142 114.431C171.142 115.191 170.602 115.825 169.886 115.972L169.57 116.003H105.182L2.77027 236.611L1.57181 235.593L0.373357 234.577L103.044 113.664L130.347 22.8133C130.597 21.9819 131.474 21.5086 132.306 21.7584Z" fill="#ABABAB"/>
<path d="M72.9636 210.65L60.8346 212.356C67.1226 228.985 80.0206 236.249 95.4131 236.249C133.141 236.249 121.625 193.585 143.482 193.585C159.342 193.585 152.899 228.165 187.02 228.165C207.848 228.165 209.927 207.183 206.372 198.156C206.351 198.101 206.331 198.051 206.299 198.002L200.718 189.455C200.355 188.887 199.471 189.101 199.409 189.776L198.369 200.135C198.297 200.856 198.317 201.574 198.401 202.293C199.253 209.45 199.804 226.818 187.02 226.818C173.54 226.818 170.297 192.686 143.482 192.686C112.032 192.686 116.075 234.902 96.7643 234.902C84.0221 234.902 74.3043 220.531 72.9636 210.65Z" fill="url(#paint3_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="171.825" y1="13.8344" x2="135.895" y2="112.635" gradientUnits="userSpaceOnUse">
<stop stop-color="#6430AB"/>
<stop offset="1" stop-color="#AA89D8"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="143.981" y1="69.5214" x2="97.7306" y2="158.891" gradientUnits="userSpaceOnUse">
<stop stop-color="#005DBB"/>
<stop offset="1" stop-color="#3D92E8"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="164.633" y1="13.8337" x2="150.706" y2="57.3959" gradientUnits="userSpaceOnUse">
<stop stop-color="#1B70C4"/>
<stop offset="1" stop-color="#54A4F2"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="60.8346" y1="213.57" x2="207.775" y2="213.57" gradientUnits="userSpaceOnUse">
<stop stop-color="#4497EA"/>
<stop offset="0.254755" stop-color="#1463B2"/>
<stop offset="0.498725" stop-color="#0A437D"/>
<stop offset="0.666667" stop-color="#2476C8"/>
<stop offset="0.972542" stop-color="#0C549A"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1,46 @@
<svg viewBox="0 0 1044.21 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(7.74 31.74)">
<g id="CopilotKit">
<path d="M111.689 53.5173H78.8802C78.647 50.8028 78.0267 48.3403 77.0185 46.1299C76.0487 43.9195 74.6918 42.0192 72.9466 40.4291C71.2403 38.8006 69.1654 37.5591 66.7223 36.7063C64.2791 35.8143 61.5062 35.3683 58.4038 35.3683C52.9745 35.3683 48.379 36.6867 44.6173 39.3235C40.8943 41.9611 38.0633 45.742 36.1243 50.6669C34.224 55.5919 33.2739 61.5063 33.2739 68.4092C33.2739 75.7001 34.2434 81.8084 36.1825 86.7334C38.1602 91.6199 41.0106 95.3042 44.7336 97.7855C48.4566 100.229 52.9358 101.45 58.1711 101.45C61.1573 101.45 63.8331 101.082 66.1988 100.345C68.5644 99.5695 70.6198 98.4647 72.3649 97.0293C74.11 95.5947 75.5258 93.8687 76.6117 91.8523C77.7362 89.7965 78.4923 87.4895 78.8802 84.9297L111.689 85.1629C111.301 90.2041 109.885 95.3426 107.442 100.578C104.999 105.774 101.528 110.583 97.0293 115.004C92.57 119.386 87.0435 122.916 80.4507 125.591C73.8579 128.267 66.1988 129.605 57.4731 129.605C46.5369 129.605 36.7254 127.259 28.0384 122.567C19.3904 117.874 12.5456 110.971 7.50404 101.858C2.50134 92.7443 0 81.5948 0 68.4092C0 55.1467 2.55952 43.9776 7.6786 34.9026C12.7977 25.7893 19.7006 18.9057 28.3875 14.252C37.0744 9.55957 46.7696 7.2133 57.4731 7.2133C64.9965 7.2133 71.9189 8.24101 78.2403 10.2964C84.5614 12.3518 90.1067 15.3572 94.877 19.3129C99.6472 23.2297 103.486 28.0578 106.395 33.7978C109.303 39.5371 111.068 46.1102 111.689 53.5173Z" fill="#010507"/>
<path d="M169.874 129.606C160.179 129.606 151.88 127.686 144.977 123.847C138.074 119.968 132.781 114.578 129.096 107.675C125.412 100.733 123.57 92.6864 123.57 83.5337C123.57 74.3818 125.412 66.3544 129.096 59.4515C132.781 52.5092 138.074 47.1186 144.977 43.2796C151.88 39.4014 160.179 37.4626 169.874 37.4626C179.569 37.4626 187.868 39.4014 194.771 43.2796C201.674 47.1186 206.968 52.5092 210.652 59.4515C214.336 66.3544 216.178 74.3818 216.178 83.5337C216.178 92.6864 214.336 100.733 210.652 107.675C206.968 114.578 201.674 119.968 194.771 123.847C187.868 127.686 179.569 129.606 169.874 129.606ZM170.107 105.872C172.822 105.872 175.168 104.96 177.145 103.138C179.123 101.315 180.655 98.7169 181.741 95.3428C182.827 91.9686 183.37 87.9553 183.37 83.3013C183.37 78.6087 182.827 74.5946 181.741 71.2597C180.655 67.8856 179.123 65.2873 177.145 63.4648C175.168 61.6422 172.822 60.7306 170.107 60.7306C167.237 60.7306 164.775 61.6422 162.719 63.4648C160.664 65.2873 159.093 67.8856 158.007 71.2597C156.922 74.5946 156.378 78.6087 156.378 83.3013C156.378 87.9553 156.922 91.9686 158.007 95.3428C159.093 98.7169 160.664 101.315 162.719 103.138C164.775 104.96 167.237 105.872 170.107 105.872Z" fill="#010507"/>
<path d="M229.208 161.484V38.6256H261.086V54.2155H261.784C262.948 51.1908 264.655 48.4182 266.903 45.8976C269.153 43.3377 271.945 41.3016 275.28 39.7893C278.615 38.2376 282.493 37.4626 286.914 37.4626C292.809 37.4626 298.413 39.0331 303.726 42.174C309.077 45.3157 313.421 50.2603 316.756 57.0078C320.13 63.7561 321.817 72.52 321.817 83.3013C321.817 93.6168 320.208 102.168 316.989 108.955C313.809 115.741 309.543 120.802 304.191 124.137C298.878 127.473 293.041 129.14 286.682 129.14C282.493 129.14 278.751 128.461 275.455 127.104C272.197 125.708 269.405 123.827 267.078 121.462C264.79 119.057 263.025 116.342 261.784 113.317H261.319V161.484H229.208ZM260.621 83.3013C260.621 87.6444 261.183 91.4064 262.308 94.5866C263.471 97.7275 265.101 100.171 267.195 101.916C269.327 103.622 271.868 104.476 274.815 104.476C277.762 104.476 280.263 103.642 282.319 101.974C284.413 100.268 286.003 97.8437 287.089 94.7028C288.214 91.5226 288.776 87.7221 288.776 83.3013C288.776 78.8804 288.214 75.0995 287.089 71.9578C286.003 68.7776 284.413 66.3544 282.319 64.6866C280.263 62.9803 277.762 62.1267 274.815 62.1267C271.868 62.1267 269.327 62.9803 267.195 64.6866C265.101 66.3544 263.471 68.7776 262.308 71.9578C261.183 75.0995 260.621 78.8804 260.621 83.3013Z" fill="#010507"/>
<path d="M335.269 127.977V38.6253H367.38V127.977H335.269ZM351.324 29.318C346.981 29.318 343.258 27.8834 340.156 25.0136C337.054 22.1438 335.502 18.6923 335.502 14.6592C335.502 10.6259 337.054 7.17442 340.156 4.30463C343.258 1.43485 346.981 0 351.324 0C355.707 0 359.429 1.43485 362.493 4.30463C365.596 7.17442 367.147 10.6259 367.147 14.6592C367.147 18.6923 365.596 22.1438 362.493 25.0136C359.429 27.8834 355.707 29.318 351.324 29.318Z" fill="#010507"/>
<path d="M415.851 8.84214V127.977H383.74V8.84214H415.851Z" fill="#010507"/>
<path d="M475.258 129.606C465.563 129.606 457.264 127.686 450.361 123.847C443.458 119.968 438.164 114.578 434.48 107.675C430.796 100.733 428.954 92.6864 428.954 83.5337C428.954 74.3818 430.796 66.3544 434.48 59.4515C438.164 52.5092 443.458 47.1186 450.361 43.2796C457.264 39.4014 465.563 37.4626 475.258 37.4626C484.954 37.4626 493.253 39.4014 500.156 43.2796C507.059 47.1186 512.352 52.5092 516.036 59.4515C519.72 66.3544 521.563 74.3818 521.563 83.5337C521.563 92.6864 519.72 100.733 516.036 107.675C512.352 114.578 507.059 119.968 500.156 123.847C493.253 127.686 484.954 129.606 475.258 129.606ZM475.491 105.872C478.205 105.872 480.552 104.96 482.53 103.138C484.508 101.315 486.039 98.7169 487.125 95.3428C488.211 91.9686 488.754 87.9553 488.754 83.3013C488.754 78.6087 488.211 74.5946 487.125 71.2597C486.039 67.8856 484.508 65.2873 482.53 63.4648C480.552 61.6422 478.205 60.7306 475.491 60.7306C472.621 60.7306 470.158 61.6422 468.103 63.4648C466.048 65.2873 464.477 67.8856 463.391 71.2597C462.305 74.5946 461.763 78.6087 461.763 83.3013C461.763 87.9553 462.305 91.9686 463.391 95.3428C464.477 98.7169 466.048 101.315 468.103 103.138C470.158 104.96 472.621 105.872 475.491 105.872Z" fill="#010507"/>
<path d="M587.877 38.6253V61.8941H529.008V38.6253H587.877ZM540.41 17.2186H572.52V99.2396C572.52 100.481 572.734 101.528 573.16 102.381C573.587 103.196 574.246 103.816 575.138 104.242C576.03 104.63 577.174 104.824 578.57 104.824C579.539 104.824 580.664 104.708 581.944 104.476C583.262 104.242 584.232 104.048 584.852 103.894L589.506 126.464C588.072 126.891 586.016 127.415 583.34 128.035C580.703 128.655 577.561 129.063 573.916 129.256C566.548 129.644 560.363 128.888 555.36 126.987C550.357 125.049 546.595 122.004 544.075 117.854C541.554 113.705 540.332 108.508 540.41 102.265V17.2186Z" fill="#010507"/>
<path d="M602.435 127.977V8.84214H634.778V57.0077H636.406L672.24 8.84214H709.935L669.681 61.8942L710.866 127.977H672.24L645.481 83.3012L634.778 97.2626V127.977H602.435Z" fill="#010507"/>
<path d="M725.597 127.977V38.6253H757.708V127.977H725.597ZM741.653 29.318C737.309 29.318 733.586 27.8834 730.484 25.0136C727.381 22.1438 725.831 18.6923 725.831 14.6592C725.831 10.6259 727.381 7.17442 730.484 4.30463C733.586 1.43485 737.309 0 741.653 0C746.035 0 749.758 1.43485 752.821 4.30463C755.924 7.17442 757.475 10.6259 757.475 14.6592C757.475 18.6923 755.924 22.1438 752.821 25.0136C749.758 27.8834 746.035 29.318 741.653 29.318Z" fill="#010507"/>
<path d="M827.354 38.6253V61.8941H768.484V38.6253H827.354ZM779.886 17.2186H811.994V99.2396C811.994 100.481 812.206 101.528 812.632 102.381C813.066 103.196 813.72 103.816 814.612 104.242C815.504 104.63 816.65 104.824 818.05 104.824C819.015 104.824 820.136 104.708 821.421 104.476C822.739 104.242 823.704 104.048 824.326 103.894L828.983 126.464C827.551 126.891 825.489 127.415 822.812 128.035C820.177 128.655 817.035 129.063 813.393 129.256C806.024 129.644 799.838 128.888 794.836 126.987C789.833 125.049 786.071 122.004 783.55 117.854C781.03 113.705 779.808 108.508 779.886 102.265V17.2186Z" fill="#010507"/>
</g>
</g>
<svg x="859.58" y="7.1" width="176.717" height="186.745" viewBox="0 0 176.717 186.745" fill="none">
<path d="M60.2013 59.6493C76.4214 38.4331 89.8836 17.4532 95.0639 0.510877C95.203 0.0498883 95.7431 -0.145948 96.1441 0.120187C114.156 12.0483 146.965 19.8996 175.984 20.0839C176.484 20.0871 176.827 20.5795 176.647 21.045C166.999 45.5233 155.214 89.384 154.756 139.471C154.756 140.215 153.708 140.482 153.34 139.835C136.825 110.934 83.9258 70.3234 60.4713 60.7386C60.0376 60.5602 59.9148 60.0242 60.2013 59.6493Z" fill="url(#paint0_linear)"/>
<path d="M114.926 46.6717C89.5724 54.6988 66.4044 59.1614 60.8722 60.1754C60.5203 60.24 60.4466 60.7269 60.7822 60.8644C84.4167 70.6906 137.054 111.183 153.414 139.967C153.446 140.03 153.528 140.052 153.594 140.024C153.659 139.993 153.692 139.911 153.667 139.841L114.926 46.6717Z" fill="url(#paint1_linear)"/>
<path d="M96.218 0.0677872C117.921 11.9064 143.004 17.2235 176.279 20.0252C176.484 20.043 176.558 20.3231 176.369 20.4204C172.114 22.6076 147.734 35.0132 129.632 41.6575C124.779 43.4376 119.902 45.089 115.098 46.6126C114.991 46.6459 114.877 46.5934 114.836 46.4919L95.7188 0.516173C95.5878 0.206583 95.9234 -0.0927773 96.218 0.0677872Z" fill="url(#paint2_linear)"/>
<path d="M95.5597 0.114978C95.875 -0.0172013 96.2317 0.0959082 96.417 0.367856L96.4849 0.493267L154.721 139.416L154.762 139.554C154.825 139.876 154.657 140.209 154.343 140.341C154.027 140.473 153.671 140.36 153.485 140.088L153.415 139.963L95.1814 1.04014L95.1383 0.902396C95.075 0.580014 95.2449 0.247049 95.5597 0.114978Z" fill="#513C9F"/>
<path d="M175.661 20.1472C176 19.9546 176.432 20.0729 176.625 20.4124C176.818 20.7522 176.697 21.1839 176.357 21.3767V21.3787H176.353C176.351 21.3803 176.346 21.3839 176.341 21.3869C176.33 21.3933 176.312 21.4033 176.29 21.4157C176.246 21.4405 176.18 21.4768 176.094 21.5247C175.921 21.6215 175.662 21.7648 175.323 21.9503C174.646 22.3215 173.645 22.8639 172.351 23.5456C169.761 24.9093 165.995 26.8363 161.283 29.0884C151.862 33.592 138.655 39.4006 123.512 44.625C108.365 49.8495 92.7416 53.9066 80.9093 56.6563C74.9919 58.0314 70.0192 59.0801 66.5261 59.7854C64.7796 60.138 63.4024 60.4044 62.4615 60.5831C61.9916 60.6723 61.6304 60.7395 61.3863 60.7845C61.2644 60.807 61.1711 60.8246 61.1087 60.8359C61.0782 60.8415 61.0547 60.8454 61.0388 60.8483C61.0311 60.8497 61.0244 60.8517 61.0203 60.8524H61.0142L60.8702 60.8647C60.5418 60.8565 60.2523 60.6188 60.1918 60.2829C60.1228 59.8986 60.3791 59.5297 60.7633 59.4605H60.7695C60.7732 59.4599 60.7786 59.4578 60.786 59.4564C60.8013 59.4537 60.8252 59.4497 60.8559 59.4441C60.9171 59.4329 61.0088 59.417 61.1293 59.3947C61.3711 59.3501 61.7305 59.2821 62.1984 59.1933C63.1343 59.0155 64.506 58.7511 66.2465 58.3997C69.7284 57.6967 74.6866 56.6503 80.5886 55.2788C92.3949 52.5352 107.968 48.4914 123.052 43.2887C138.13 38.0862 151.286 32.2987 160.673 27.8117C165.366 25.5684 169.116 23.6497 171.691 22.2936C172.978 21.6156 173.972 21.0783 174.643 20.7105C174.978 20.527 175.233 20.3863 175.404 20.2911C175.489 20.2435 175.554 20.2062 175.597 20.1822C175.618 20.1704 175.634 20.1614 175.644 20.1554C175.649 20.1525 175.654 20.1507 175.656 20.1493L175.658 20.1472H175.661Z" fill="#513C9F"/>
<path d="M2.18131 186.308C1.73888 186.829 0.956885 186.893 0.435837 186.45C-0.0843773 186.008 -0.147949 185.227 0.293978 184.707L2.18131 186.308ZM104.178 17.1327C104.832 17.3295 105.202 18.0203 105.006 18.6746L83.9124 88.8658H133.52L133.768 88.8905C134.333 89.0058 134.757 89.5053 134.757 90.1035C134.757 90.7016 134.333 91.2011 133.768 91.3164L133.52 91.3411H82.8207L2.18131 186.308L1.23765 185.506L0.293978 184.707L81.1369 89.499L102.636 17.9632C102.832 17.3086 103.523 16.9359 104.178 17.1327Z" fill="#ABABAB"/>
<path d="M57.4516 165.866L47.9012 167.209C52.8524 180.303 63.0083 186.023 75.1284 186.023C104.835 186.023 95.7677 152.43 112.978 152.43C125.466 152.43 120.393 179.658 147.26 179.658C163.66 179.658 165.297 163.136 162.498 156.029C162.481 155.985 162.465 155.946 162.44 155.907L158.046 149.177C157.759 148.73 157.064 148.898 157.015 149.43L156.196 157.587C156.139 158.154 156.155 158.72 156.221 159.286C156.892 164.921 157.326 178.597 147.26 178.597C136.645 178.597 134.092 151.722 112.978 151.722C88.2142 151.722 91.3977 184.962 76.1923 184.962C66.1591 184.962 58.5073 173.647 57.4516 165.866Z" fill="url(#paint3_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="135.295" y1="10.8932" x2="107.004" y2="88.6892" gradientUnits="userSpaceOnUse">
<stop stop-color="#6430AB"/>
<stop offset="1" stop-color="#AA89D8"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="113.371" y1="54.7414" x2="76.9532" y2="125.111" gradientUnits="userSpaceOnUse">
<stop stop-color="#005DBB"/>
<stop offset="1" stop-color="#3D92E8"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="129.632" y1="10.8927" x2="118.666" y2="45.1937" gradientUnits="userSpaceOnUse">
<stop stop-color="#1B70C4"/>
<stop offset="1" stop-color="#54A4F2"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="47.9012" y1="168.165" x2="163.603" y2="168.165" gradientUnits="userSpaceOnUse">
<stop stop-color="#4497EA"/>
<stop offset="0.254755" stop-color="#1463B2"/>
<stop offset="0.498725" stop-color="#0A437D"/>
<stop offset="0.666667" stop-color="#2476C8"/>
<stop offset="0.972542" stop-color="#0C549A"/>
</linearGradient>
</defs>
</svg>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,18 @@
# Voice demo audio
Drop a small (<100KB) WAV file named `sample.wav` in this directory. The voice
demo (`/demos/voice`) fetches it client-side and POSTs to the transcription
endpoint so the flow works without mic permissions.
Expected content: an audio clip speaking the phrase
**"What is the weather in Tokyo?"** — the demo caption advertises that phrase
to the user, and the bundled QA checklist + E2E spec assert the transcribed
text contains "weather" and/or "Tokyo".
Generate locally, for example:
- macOS: `say -o sample.aiff "What is the weather in Tokyo?" && ffmpeg -i sample.aiff -ar 16000 -ac 1 sample.wav`
- Linux: `espeak-ng -w sample.wav "What is the weather in Tokyo?"`
- Windows: PowerShell `System.Speech.Synthesis.SpeechSynthesizer``SetOutputToWaveFile`
Target: 16kHz mono, 3-5s duration, <100KB.
@@ -0,0 +1,22 @@
# Demo Files — Multimodal Demo
This directory bundles sample files referenced by the `/demos/multimodal` page.
Required files (must be committed as binaries; see `.gitattributes` at repo root):
- `sample.png` — a small (< 50 KB) PNG that the "Try with sample image" button
injects into the chat. A CopilotKit logo or other recognizable brand mark
works well so the vision-capable agent has something to describe.
- `sample.pdf` — a small (< 50 KB) one-page PDF that the "Try with sample PDF"
button injects. The content must mention "CopilotKit" so E2E soft
assertions hold (e.g. a one-page export of the CopilotKit quickstart).
The page at `src/app/demos/multimodal/page.tsx` fetches these via the public
path (`/demo-files/sample.png`, `/demo-files/sample.pdf`), wraps the fetched
blob in a `File`, and routes it through the same `AttachmentsConfig.onUpload`
callback the paperclip button uses — so the sample path exercises the exact
same queueing code as a real user upload.
If these files are missing, the demo page still renders but the sample
buttons will surface a fetch error. The paperclip / drag-and-drop paths
continue to work without them.
@@ -0,0 +1,80 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (CopilotKit) /CreationDate (D:20260424104452+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260424104452+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (CopilotKit Quickstart) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 1 /Kids [ 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 942
>>
stream
Gat=)gMRoa&:N^l7S$NN`GHCmThV;Z2^rJ`P):Y@]XYQU3:,AeX>cqAr-QI2/=2N"LfPX,loA_(*1m\5btm94UD2YX&6drn#XTAg+m:hr0`277L6Xugi+Zi-%K4(Zk-_X)UIg0#"[s_$PB)p"JhpIS$sekH(N!]Wm13pi-C<+2Z\%NM%.@X"c6;!4Z\-]lQ_FW]'b$98<b+A;AK8VL'gsRZ%'52u3[a]V,-i>M8Rs*Je-b?d:1;&?$,`ad5V$Xf0gWUq#g$2OoROl&VT9iG>Z7e:(6,L^5qWRe9f&aq&5@k4gFJA#"jE(QdPamJY0Hpqn>r_e:*FVtpYPMT?RZaJ8P7Ca0<':`;qM'##`XXPXO9RF6i%iKqn<L:oPEI$hcphT25S/nN3<DAK4NVdiJL>7#+eNHaXE@Y[_>W6oJG]uZn6;#DFBr_^iD&OGjVg%"ij8a:&7/qaE+7a=d``5hdQO":;.r]$4@TOAQt"^6*,kR%u>XHZrCjf@5l+)<U?'C_6MX"fJq<J@]>%B@BumKlTA+:A9AG5jqSXcFh[j7G)iN0D\N/+I[;hq]7D]ZAE\d8UW,N3+.Br<N@#$kD%W'f-CmKrYTKkeeuX8:pui+(3#6dK@@XC>.sl!SW4(X]/!H@1dH0Qp(55E^KJk1]UY=9;_"eb*-cL!Qq,hJ?,?l*JeVfts!/i_[Ejo:h;fDQSkbMmd[X?rTSO%M5Qeh<MleEs/@&O$fl?bGg9%rX0LW>9/:K"H3?p7"G:0Ha3Qj[6jL<;9nL312;])jCZe6\\@E*Ttr?Xg2lNp?68b2o5,cF;--/kSU`mMnHk40JE<",2f4YKj@2GX^Q[V>`.2@i]-rEiq2ARgR#W*>Hsb'riEWK24?;X\G?nL'id4l2fR3ocbEr3QJOgLr..O8N<DDWc>P*6r.',3Q1&LkGF@<;hrL[kDNqL~>endstream
endobj
xref
0 10
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000000331 00000 n
0000000436 00000 n
0000000629 00000 n
0000000697 00000 n
0000000982 00000 n
0000001041 00000 n
trailer
<<
/ID
[<ad0be0ae447f61082137085c690fb230><ad0be0ae447f61082137085c690fb230>]
% ReportLab generated PDF document -- digest (opensource)
/Info 7 0 R
/Root 6 0 R
/Size 10
>>
startxref
2073
%%EOF
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

@@ -0,0 +1,44 @@
# QA: A2UI Error Recovery — LangGraph (TypeScript)
## Prerequisites
- Demo is deployed and accessible at `/demos/a2ui-recovery` on the dashboard host
- Agent backend is healthy; `OPENAI_API_KEY` is set; `LANGGRAPH_DEPLOYMENT_URL` points at the LangGraph (TS) deployment exposing the `a2ui_recovery` graph (registered as agent name `a2ui-recovery` — see `src/app/api/copilotkit-a2ui-recovery/route.ts`)
- Requires `@ag-ui/langgraph >= 0.0.39` (the `getA2UITools` validate→retry recovery loop + `a2ui_recovery_exhausted` hard-fail envelope) and the `@copilotkit` A2UI renderer (the `building`/`retrying`/`failed` lifecycle rendering)
- Backend-owned wiring: the route sets `injectA2UITool: false`; the agent owns `generate_a2ui` via `getA2UITools({ recovery: { maxAttempts: 3 } })` (see `src/agent/recovery-agent.ts`)
- Reuses the **declarative-gen-ui** catalog (`catalogId: "declarative-gen-ui-catalog"`) and the Vantage Threads sales context — no new components
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to `/demos/a2ui-recovery`; verify the page renders within 3s and a single `CopilotChat` pane is centered (max-width ~896px, rounded-2xl, full-height)
- [ ] Verify the chat is wired to `runtimeUrl="/api/copilotkit-a2ui-recovery"` and `agent="a2ui-recovery"` (DevTools → Network: sending a message hits that endpoint, not `/api/copilotkit`)
- [ ] Verify both suggestion pills are visible with verbatim titles:
- "Recover a bad render"
- "Show an unrecoverable failure"
### 2. Healing path
- [ ] Click "Recover a bad render" ("Render my Q2 sales dashboard, recovering if the first attempt is malformed.")
- [ ] The inner `render_a2ui` returns **free-form / sloppy** A2UI args (components & data as JSON strings rather than structured arrays). Verify the middleware **heals** them via `parse_and_fix` into a valid surface that paints (no broken surface, no error banner)
- [ ] Verify the **painted** surface is valid: a `declarative-metric` row ("Quarterly Revenue $4.2M", "Win Rate 31%")
- [ ] DevTools → Network: verify the final tool result carries an `a2ui_operations` container (no `a2ui_recovery_exhausted`)
- [ ] Verify the chat reply is one short sentence noting the heal
### 3. Hard-fail (recovery exhausted) path
- [ ] Click "Show an unrecoverable failure" ("Render a dashboard that keeps failing validation so I can see the fallback.")
- [ ] Verify the lifecycle ends in a tasteful `failed` state (NOT a broken/half-rendered surface and NOT a silent drop)
- [ ] DevTools → Network: verify `render_a2ui` was attempted up to the cap (3 attempts, all invalid) and the tool returned an `a2ui_recovery_exhausted` envelope (no `a2ui_operations` painted)
- [ ] Verify the chat reply gracefully explains the fallback (one short sentence)
### 4. Regression / isolation
- [ ] Verify the recovery demo does not affect the declarative-gen-ui or beautiful-chat demos (separate routes/agents)
- [ ] Re-run each pill a second time and verify the same lifecycle
## Notes
- The malformed renders are forced by aimock fixtures (`showcase/aimock/d6/langgraph-typescript/a2ui-recovery.json`): the inner `render_a2ui` call is matched by `userMessage` + `toolName=render_a2ui`. Healing itself is performed live by the toolkit recovery loop inside `@ag-ui/langgraph` `getA2UITools`.
- LangGraph-TypeScript sibling of the langgraph-python `a2ui-recovery` demo (same backend `getA2UITools` recovery loop).
@@ -0,0 +1,62 @@
# QA: Agentic Chat — LangGraph (TypeScript)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the agentic-chat demo page
- [ ] Verify the chat interface loads with a text input placeholder "Type a message"
- [ ] Verify the background container (`data-testid="background-container"`) is visible
- [ ] Verify the default background color is the theme default (rgb(250, 250, 249))
- [ ] Send a basic message (e.g. "Hello")
- [ ] Verify the agent responds with a text message
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Change background" suggestion button is visible
- [ ] Verify "Generate sonnet" suggestion button is visible
- [ ] Click the "Change background" suggestion
- [ ] Verify the suggestion either populates the input or sends the message
#### Background Change (useFrontendTool)
- [ ] Ask "Change the background to a sunset gradient"
- [ ] Verify the background container style changes from the default
- [ ] Verify the change_background tool returns a success status
#### Weather Render Tool (useRenderTool)
- [ ] Type "What's the weather in Tokyo?"
- [ ] Verify loading state shows "Loading weather..." (`data-testid="weather-info-loading"`)
- [ ] Verify WeatherCard renders (`data-testid="weather-info"`) with:
- [ ] City name displayed
- [ ] Temperature in degrees C
- [ ] Humidity percentage
- [ ] Wind speed in mph
- [ ] Conditions text
#### Agent Context
- [ ] Verify the agent knows the user's name is "Bob" (provided via useAgentContext)
- [ ] Ask "What is my name?" and verify the agent responds with "Bob"
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
- [ ] Send a very long message and verify no UI breakage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- Background changes are instant after tool execution
- Weather card renders with all data fields populated
- No UI errors or broken layouts
@@ -0,0 +1,62 @@
# QA: Agentic Generative UI — LangGraph (TypeScript)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the gen-ui-agent demo page
- [ ] Verify the chat interface loads in a centered full-height layout
- [ ] Verify the chat input placeholder "Type a message" is visible
- [ ] Send a basic message
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Simple plan" suggestion button is visible (plan to go to Mars in 5 steps)
- [ ] Verify "Complex plan" suggestion button is visible (plan to make pizza in 10 steps)
#### Task Progress Tracker (useAgent with state streaming)
- [ ] Click "Simple plan" suggestion or type "Build a plan to go to Mars in 5 steps"
- [ ] Verify the TaskProgress component renders (`data-testid="task-progress"`)
- [ ] Verify the progress bar appears with a gradient fill
- [ ] Verify step items appear with descriptions (`data-testid="task-step-text"`)
- [ ] Verify the "N/N Complete" counter updates as steps complete
- [ ] Verify completed steps show:
- Green background gradient
- Check icon
- Green text color
- [ ] Verify the current pending step shows:
- Blue/purple background gradient
- Spinner icon with "Processing..." text
- Pulsing animation
- [ ] Verify future pending steps show:
- Gray background
- Clock icon
- Muted text color
#### Complex Plan
- [ ] Type "Plan to make pizza in 10 steps"
- [ ] Verify 10 steps appear in the progress tracker
- [ ] Verify progress bar width increases as steps complete
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- Task progress tracker shows live step completion
- Progress bar animates smoothly
- No UI errors or broken layouts
@@ -0,0 +1,59 @@
# QA: Tool-Based Generative UI — LangGraph (TypeScript)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the gen-ui-tool-based demo page
- [ ] Verify the CopilotSidebar opens by default with title "Haiku Generator"
- [ ] Verify a placeholder haiku card is displayed on the main area
- [ ] Send a basic message via the sidebar
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Nature Haiku" suggestion button is visible
- [ ] Verify "Ocean Haiku" suggestion button is visible
- [ ] Verify "Spring Haiku" suggestion button is visible
#### Haiku Generation (useFrontendTool)
- [ ] Click the "Nature Haiku" suggestion or type "Write me a haiku about nature"
- [ ] Verify a HaikuCard renders (`data-testid="haiku-card"`) with:
- [ ] Three Japanese text lines (`data-testid="haiku-japanese-line"`)
- [ ] Three English translation lines (`data-testid="haiku-english-line"`)
- [ ] A background gradient style applied to the card
- [ ] Verify the Japanese text contains actual Japanese characters (not Latin)
- [ ] Verify the English lines are a readable English translation
#### Image Display
- [ ] After haiku generation, verify an image renders (`data-testid="haiku-image"`) if the agent provides an image_name
- [ ] Verify the image src points to /images/ with a valid filename from the predefined list
#### Multiple Haikus
- [ ] Generate a second haiku (e.g. "Ocean Haiku")
- [ ] Verify the new haiku card appears at the top
- [ ] Verify the previous haiku card is still visible below
- [ ] Verify the initial placeholder haiku is removed
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Sidebar loads within 3 seconds
- Agent responds and generates haiku within 10 seconds
- Haiku cards display with Japanese and English text
- Generated haikus stack with newest on top
- No UI errors or broken layouts
@@ -0,0 +1,57 @@
# QA: Human in the Loop — LangGraph (TypeScript)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the HITL demo page (`/demos/hitl`) > _Note: the URL path `/demos/hitl` is intentionally shorter than the feature id `hitl-in-chat`._
- [ ] Verify the chat interface loads in a centered max-w-4xl container
- [ ] Verify the chat input placeholder "Type a message" is visible
- [ ] Send a basic message
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Simple plan" suggestion button is visible
- [ ] Verify "Complex plan" suggestion button is visible
- [ ] Click the "Simple plan" suggestion
- [ ] Verify it triggers a message about planning a trip to Mars in 5 steps
#### Step Selection and Approval
The HITL demo renders a single StepSelector card regardless of whether
the underlying flow uses an interrupt hook or a frontend HITL tool. The
framework-specific hook name is an implementation detail covered in each
package's demo source; QA below validates the user-visible card behavior.
- [ ] Send "Plan a trip to Mars in 5 steps"
- [ ] Verify the StepSelector card appears (`data-testid="select-steps"`)
- [ ] Verify step items are displayed with checkboxes (`data-testid="step-item"`)
- [ ] Verify step text is visible (`data-testid="step-text"`)
- [ ] Verify the selected count display shows "N/N selected"
- [ ] Toggle a step checkbox off and verify the count decreases
- [ ] Toggle it back on and verify the count increases
- [ ] Click "Perform Steps (N)" / "Confirm (N)" button
- [ ] Verify the agent continues processing after confirmation
- [ ] In a new conversation, trigger the same flow and click "Reject" (where present)
- [ ] Verify the card reflects the decision (Accepted / Rejected) and buttons disable
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- Step selector renders with toggleable checkboxes
- Accept/Reject flow completes without errors
- No UI errors or broken layouts
@@ -0,0 +1,61 @@
# QA: Shared State (Read + Write) — LangGraph (TypeScript)
## Prerequisites
- Demo is deployed and accessible at `/demos/shared-state-read-write` on the dashboard host
- Agent backend is healthy (`/api/health`); `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the `shared_state_read_write` graph
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to `/demos/shared-state-read-write`; verify the page renders within 3s with the left sidebar (preferences + notes cards) and the right-side `CopilotChat` pane
- [ ] Verify `data-testid="preferences-card"` is visible with heading "Your preferences"
- [ ] Verify `data-testid="notes-card"` is visible with heading "Agent notes" and empty-state `data-testid="notes-empty"` reading "No notes yet. Ask the agent to remember something."
- [ ] Verify the chat input placeholder is "Chat with the agent..."
- [ ] Verify all 3 suggestion pills are visible with verbatim titles: "Greet me", "Remember something", "Plan a weekend"
- [ ] Send "Hello" and verify an assistant text response appears within 10s
### 2. Feature-Specific Checks
#### UI Writes -> Agent Reads (preferences via `agent.setState`)
- [ ] Type "Atai" into `data-testid="pref-name"`; verify `data-testid="pref-state-json"` updates to include `"name": "Atai"`
- [ ] Change `data-testid="pref-tone"` to `formal`; verify the JSON preview reflects `"tone": "formal"`
- [ ] Change `data-testid="pref-language"` to `Spanish`; verify the JSON preview reflects `"language": "Spanish"`
- [ ] Click the `Cooking` and `Travel` interest pills; verify both show the selected style (border `#BEC2FF`, bg `#BEC2FF1A`) and the JSON preview's `interests` array contains both entries
- [ ] Send "What do you know about me?"; verify within 10s the assistant reply references the name "Atai", a formal tone, Spanish, and the Cooking/Travel interests (the chat node injects the preferences into the system prompt each turn)
- [ ] Click the "Plan a weekend" suggestion; verify the reply is tailored to the selected interests
#### Agent Writes -> UI Reads (notes via `set_notes` tool)
- [ ] Click the "Remember something" suggestion (sends "Remember that I prefer morning meetings and that I don't eat dairy.")
- [ ] Within 15s verify `data-testid="notes-list"` appears in the notes card and contains at least 2 `data-testid="note-item"` entries mentioning "morning meetings" and "dairy"
- [ ] Verify `data-testid="notes-empty"` is no longer rendered
- [ ] Send "Also remember I live in Berlin."; verify within 15s the notes list grows (previous notes preserved, new note added) — confirms the agent passes the FULL updated list per `set_notes` contract
#### UI Writes Back to Agent-Authored Slice (clear notes)
- [ ] With notes present, verify `data-testid="notes-clear-button"` is visible
- [ ] Click the Clear button; verify the notes list disappears and `data-testid="notes-empty"` re-renders
- [ ] Ask "What do you remember about me?"; verify the agent no longer cites the cleared notes (state was written back by the UI via `agent.setState({ notes: [] })`)
#### Multi-Turn State Persistence
- [ ] Change tone to `playful` and add the `Music` interest; send "Write me a one-line haiku greeting."; verify the reply is playful and references music
- [ ] Send a follow-up "Do it again in French."; verify the reply remains playful, switches to French, and still acknowledges the music interest — confirms preferences persist across turns without being re-sent
- [ ] Reload the page; verify preferences reset to defaults (`tone: casual`, `language: English`, empty interests, empty name) and notes reset to empty (state is per-session, seeded by the page's `useEffect`)
### 3. Error Handling
- [ ] Attempt to send an empty message; verify it is a no-op (no user bubble, no assistant response)
- [ ] Deselect all interests and clear the name; send "Who am I?"; verify the agent answers without crashing (the chat node skips injection when `preferences` is empty)
- [ ] Verify DevTools -> Console shows no uncaught errors during any flow above
## Expected Results
- Page loads within 3 seconds; assistant text response within 10 seconds
- Preferences writes are reflected in `pref-state-json` synchronously on change
- Agent-authored notes appear in `notes-card` within 15 seconds of a "remember" prompt, and the full prior list is preserved on subsequent `set_notes` calls
- Clear button round-trips UI -> agent state and the agent loses access to the cleared notes on the next turn
- No UI layout breaks, no uncaught console errors
@@ -0,0 +1,75 @@
# QA: Shared State (Reading) — LangGraph (TypeScript)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the shared-state-read demo page
- [ ] Verify the recipe card form loads (`data-testid="recipe-card"`)
- [ ] Verify the CopilotSidebar opens by default with title "AI Recipe Assistant"
- [ ] Send a message via the sidebar
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Initial Recipe State
- [ ] Verify the recipe title input shows "Make Your Recipe"
- [ ] Verify the cooking time dropdown defaults to "45 min"
- [ ] Verify the skill level dropdown defaults to "Intermediate"
- [ ] Verify the default ingredients are displayed:
- [ ] Carrots (3 large, grated) with carrot emoji
- [ ] All-Purpose Flour (2 cups) with wheat emoji
- [ ] Verify the default instruction is displayed: "Preheat oven to 350 F"
#### Suggestions
- [ ] Verify "Create Italian recipe" suggestion is visible
- [ ] Verify "Make it healthier" suggestion is visible
- [ ] Verify "Suggest variations" suggestion is visible
#### Recipe Editing (Local State)
- [ ] Edit the recipe title and verify it updates
- [ ] Change the skill level dropdown and verify it updates
- [ ] Change the cooking time dropdown and verify it updates
- [ ] Toggle a dietary preference checkbox (e.g. "Vegetarian") and verify it's checked
- [ ] Click "+ Add Ingredient" (`data-testid="add-ingredient-button"`) and verify a new empty row appears
- [ ] Edit an ingredient name and amount
- [ ] Remove an ingredient by clicking the "x" button
- [ ] Click "+ Add Step" and verify a new instruction row appears
- [ ] Edit an instruction and verify it saves
- [ ] Remove an instruction by clicking the "x" button
#### AI-Powered Recipe Updates (useAgent with shared state)
- [ ] Click "Create Italian recipe" suggestion
- [ ] Verify the agent updates the recipe title, ingredients, and instructions
- [ ] Verify the ping indicator appears on changed sections
- [ ] Verify the "Improve with AI" button (`data-testid="improve-button"`) changes to "Please Wait..." while loading
- [ ] Click "Improve with AI" and verify the recipe is enhanced
#### Agent Reads Frontend State
- [ ] Edit the recipe (change title, add ingredients)
- [ ] Ask the agent "What recipe am I making?"
- [ ] Verify the agent's response references the current recipe state
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
- [ ] Verify the "Improve with AI" button is disabled while loading
## Expected Results
- Recipe card and sidebar load within 3 seconds
- Agent responds within 10 seconds
- Recipe state syncs bidirectionally between UI and agent
- Ping indicators highlight changed sections
- No UI errors or broken layouts
@@ -0,0 +1,41 @@
# QA: State Streaming — LangGraph (TypeScript)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the shared-state-streaming demo page
- [ ] Verify the chat interface loads with title "State Streaming"
- [ ] Verify the chat input placeholder "Type a message..." is visible
- [ ] Send a basic message (e.g. "Hello! What can you do?")
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Get started" suggestion button is visible
#### Note: Stub Demo
> **Status: Stub** — This demo is currently a stub (TODO: implement)
- [ ] Verify the basic CopilotChat loads and accepts messages
- [ ] Verify the agent responds to messages
- [ ] No custom UI components are expected beyond the chat interface
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- No UI errors or broken layouts
@@ -0,0 +1,41 @@
# QA: Shared State (Writing) — LangGraph (TypeScript)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the shared-state-write demo page
- [ ] Verify the chat interface loads with title "Shared State (Writing)"
- [ ] Verify the chat input placeholder "Type a message..." is visible
- [ ] Send a basic message (e.g. "Hello! What can you do?")
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Get started" suggestion button is visible
#### Note: Stub Demo
> **Status: Stub** — This demo is currently a stub (TODO: implement)
- [ ] Verify the basic CopilotChat loads and accepts messages
- [ ] Verify the agent responds to messages
- [ ] No custom UI components are expected beyond the chat interface
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- No UI errors or broken layouts
@@ -0,0 +1,60 @@
# QA: Sub-Agents — LangGraph (TypeScript)
## Prerequisites
- Demo is deployed and accessible at `/demos/subagents` on the dashboard host
- Agent backend is healthy (`/api/health`); `OPENAI_API_KEY` is set on Railway; `LANGGRAPH_DEPLOYMENT_URL` points at a LangGraph deployment exposing the `subagents` graph
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to `/demos/subagents`; verify the page renders within 3s with the delegation log on the left and the chat pane on the right
- [ ] Verify `data-testid="delegation-log"` is visible with heading "Sub-agent delegations"
- [ ] Verify `data-testid="delegation-count"` shows "0 calls" on first load
- [ ] Verify the empty-state message "Ask the supervisor to complete a task. Every sub-agent it calls will appear here." is visible
- [ ] Verify the chat input placeholder is "Give the supervisor a task..."
- [ ] Verify all 3 suggestion pills are visible with verbatim titles: "Write a blog post", "Explain a topic", "Summarize a topic"
- [ ] Send "Hello" and verify the supervisor responds with a short text message within 10s (no delegations needed for a greeting)
### 2. Feature-Specific Checks
#### Sequential Delegation (research -> write -> critique)
- [ ] Click the "Write a blog post" suggestion (sends a request that explicitly asks for research, write, then critique)
- [ ] While the supervisor is running, verify `data-testid="supervisor-running"` chip appears in the log header reading "Supervisor running"
- [ ] Within 60s verify `data-testid="delegation-entry"` items appear in order — at least 3 entries
- [ ] Verify the first entry is tagged "Research" (`research_agent`) with a bulleted list of facts in its result
- [ ] Verify the second entry is tagged "Writing" (`writing_agent`) with a polished paragraph in its result
- [ ] Verify the third entry is tagged "Critique" (`critique_agent`) with 2-3 actionable critiques in its result
- [ ] Verify each entry shows status "completed"
- [ ] Verify `data-testid="delegation-count"` is "3 calls" (or more if the supervisor delegated additional rounds)
- [ ] Verify the supervisor returns a final summary in the chat after all delegations complete
- [ ] Verify `data-testid="supervisor-running"` is no longer rendered after the run finishes
#### Single Sub-Agent Invocation
- [ ] Click the "Explain a topic" suggestion
- [ ] Verify at least one `delegation-entry` appears with each step labeled correctly (Research / Writing / Critique)
- [ ] Verify each entry's `task` field is non-empty (the supervisor passes a real brief through the `task` argument)
#### Live Log Updates
- [ ] Send "Summarize the current state of reusable rockets in 1 polished paragraph, with research and critique."
- [ ] Verify entries appear progressively in the log (not all at once at the very end) — the log reflects state mutations as each sub-agent's tool returns
- [ ] Verify the chronological order in the log matches the order of supervisor tool calls
### 3. Error Handling
- [ ] Attempt to send an empty message; verify it is a no-op (no user bubble, no supervisor response)
- [ ] Send a trivial greeting ("Hi"); verify the supervisor replies without delegating (delegation count stays the same)
- [ ] Verify DevTools -> Console shows no uncaught errors during any flow above
## Expected Results
- Page loads within 3 seconds
- Supervisor responses (without delegation) within 10 seconds
- Full research -> write -> critique cycle completes within 60 seconds
- Each delegation entry includes the sub-agent label, task brief, and the sub-agent's full result text
- Live log updates as each sub-agent finishes (not after the entire supervisor run)
- No UI layout breaks, no uncaught console errors
@@ -0,0 +1,61 @@
# QA: Tool Rendering — LangGraph (TypeScript)
## Prerequisites
- Demo is deployed and accessible
- Agent backend is healthy (check /api/health)
## Test Steps
### 1. Basic Functionality
- [ ] Navigate to the tool-rendering demo page
- [ ] Verify the chat interface loads in a centered full-height layout
- [ ] Verify the chat input placeholder "Type a message" is visible
- [ ] Send a basic message
- [ ] Verify the agent responds
### 2. Feature-Specific Checks
#### Suggestions
- [ ] Verify "Weather in San Francisco" suggestion button is visible
- [ ] Verify "Weather in New York" suggestion button is visible
- [ ] Verify "Weather in Tokyo" suggestion button is visible
- [ ] Click a weather suggestion and verify it populates the input or sends the message
#### Weather Card Rendering (useRenderTool)
- [ ] Type "What's the weather in San Francisco?"
- [ ] Verify loading state shows "Retrieving weather..." with a spinner
- [ ] Verify the WeatherCard renders (`data-testid="weather-card"`) with:
- [ ] City name displayed (`data-testid="weather-city"`)
- [ ] Temperature in both Celsius and Fahrenheit
- [ ] Humidity percentage (`data-testid="weather-humidity"`)
- [ ] Wind speed in mph (`data-testid="weather-wind"`)
- [ ] Feels-like temperature (`data-testid="weather-feels-like"`)
- [ ] Conditions text with appropriate weather icon (sun/rain/cloud)
- [ ] Verify the card background color matches the weather condition theme:
- Clear/Sunny: #667eea (blue-purple)
- Rain/Storm: #4A5568 (dark gray)
- Cloudy: #718096 (medium gray)
- Snow: #63B3ED (light blue)
#### Multiple Weather Queries
- [ ] Ask about weather in a second city
- [ ] Verify a second WeatherCard renders without breaking the first
- [ ] Verify each card shows the correct city name
### 3. Error Handling
- [ ] Send an empty message (should be handled gracefully)
- [ ] Verify no console errors during normal usage
## Expected Results
- Chat loads within 3 seconds
- Agent responds within 10 seconds
- Weather cards render with all data fields populated
- Weather icon and theme color match the conditions
- No UI errors or broken layouts
@@ -0,0 +1,90 @@
import { describe, it, expect, vi } from "vitest";
import {
generateA2uiImpl,
buildA2uiOperationsFromToolCall,
RENDER_A2UI_TOOL_SCHEMA,
CUSTOM_CATALOG_ID,
} from "../generate-a2ui";
describe("generateA2uiImpl", () => {
it("returns system prompt from context entries", () => {
const result = generateA2uiImpl({
messages: [],
contextEntries: [
{ value: "Component catalog info" },
{ value: "More context" },
],
});
expect(result.systemPrompt).toContain("Component catalog info");
expect(result.systemPrompt).toContain("More context");
});
it("filters empty/missing context values", () => {
const result = generateA2uiImpl({
messages: [],
contextEntries: [{ value: "" }, { noValue: true }, { value: "keep" }],
});
expect(result.systemPrompt).toBe("keep");
});
it("returns tool schema and choice", () => {
const result = generateA2uiImpl({ messages: [] });
expect(result.toolSchema.name).toBe("render_a2ui");
expect(result.toolChoice).toBe("render_a2ui");
});
it("passes messages through", () => {
const msgs = [{ role: "user", content: "hello" }];
const result = generateA2uiImpl({ messages: msgs });
expect(result.messages).toBe(msgs);
});
it("returns the default catalog ID", () => {
const result = generateA2uiImpl({ messages: [] });
expect(result.catalogId).toBe(CUSTOM_CATALOG_ID);
});
});
describe("buildA2uiOperationsFromToolCall", () => {
it("builds create_surface + update_components", () => {
const result = buildA2uiOperationsFromToolCall({
surfaceId: "s1",
catalogId: "cat1",
components: [{ id: "root", component: "Title" }],
});
expect(result.a2ui_operations).toHaveLength(2);
expect(result.a2ui_operations[0].type).toBe("create_surface");
expect(result.a2ui_operations[1].type).toBe("update_components");
});
it("includes update_data_model when data provided", () => {
const result = buildA2uiOperationsFromToolCall({
surfaceId: "s1",
catalogId: "cat1",
components: [{ id: "root" }],
data: { key: "value" },
});
expect(result.a2ui_operations).toHaveLength(3);
expect(result.a2ui_operations[2].type).toBe("update_data_model");
});
it("defaults surfaceId and catalogId", () => {
const result = buildA2uiOperationsFromToolCall({ components: [] });
expect(result.a2ui_operations[0].surfaceId).toBe("dynamic-surface");
expect(result.a2ui_operations[0].catalogId).toBe(CUSTOM_CATALOG_ID);
});
it("warns on empty components", () => {
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
buildA2uiOperationsFromToolCall({
surfaceId: "s1",
catalogId: "c1",
components: [],
});
expect(spy).toHaveBeenCalledWith(
expect.stringContaining("empty components"),
"s1",
);
spy.mockRestore();
});
});
@@ -0,0 +1,79 @@
import { describe, it, expect } from "vitest";
import { getWeatherImpl } from "../get-weather";
describe("getWeatherImpl", () => {
it("returns all required fields", () => {
const result = getWeatherImpl("Tokyo");
expect(result).toHaveProperty("city");
expect(result).toHaveProperty("temperature");
expect(result).toHaveProperty("humidity");
expect(result).toHaveProperty("wind_speed");
expect(result).toHaveProperty("feels_like");
expect(result).toHaveProperty("conditions");
});
it("passes city name through", () => {
expect(getWeatherImpl("San Francisco").city).toBe("San Francisco");
});
it("is deterministic for the same city", () => {
const r1 = getWeatherImpl("Tokyo");
const r2 = getWeatherImpl("Tokyo");
expect(r1.temperature).toBe(r2.temperature);
expect(r1.conditions).toBe(r2.conditions);
});
it("produces different results for different cities", () => {
const r1 = getWeatherImpl("Tokyo");
const r2 = getWeatherImpl("London");
expect(r1).not.toEqual(r2);
});
it("temperature is within expected range (20-95)", () => {
const result = getWeatherImpl("Berlin");
expect(result.temperature).toBeGreaterThanOrEqual(20);
expect(result.temperature).toBeLessThanOrEqual(95);
});
it("humidity is within expected range (30-90)", () => {
const result = getWeatherImpl("Paris");
expect(result.humidity).toBeGreaterThanOrEqual(30);
expect(result.humidity).toBeLessThanOrEqual(90);
});
it("wind_speed is within expected range (2-30)", () => {
const result = getWeatherImpl("Sydney");
expect(result.wind_speed).toBeGreaterThanOrEqual(2);
expect(result.wind_speed).toBeLessThanOrEqual(30);
});
it("conditions is one of the known values", () => {
const known = [
"Sunny",
"Partly Cloudy",
"Cloudy",
"Overcast",
"Light Rain",
"Heavy Rain",
"Thunderstorm",
"Snow",
"Foggy",
"Windy",
];
const result = getWeatherImpl("Miami");
expect(known).toContain(result.conditions);
});
it("is case-insensitive for seed (lowercased internally)", () => {
const r1 = getWeatherImpl("tokyo");
const r2 = getWeatherImpl("TOKYO");
expect(r1.temperature).toBe(r2.temperature);
});
it("feels_like is within ±5 of temperature", () => {
const result = getWeatherImpl("Berlin");
expect(
Math.abs(result.feels_like - result.temperature),
).toBeLessThanOrEqual(5);
});
});
@@ -0,0 +1,61 @@
import { describe, it, expect } from "vitest";
import { queryDataImpl } from "../query-data";
describe("queryDataImpl", () => {
it("returns an array", () => {
expect(Array.isArray(queryDataImpl("test"))).toBe(true);
});
it("returns 66 rows (11 categories x 6 months)", () => {
expect(queryDataImpl("test")).toHaveLength(66);
});
it("rows have expected columns", () => {
const row = queryDataImpl("test")[0];
expect(row).toHaveProperty("date");
expect(row).toHaveProperty("category");
expect(row).toHaveProperty("subcategory");
expect(row).toHaveProperty("amount");
expect(row).toHaveProperty("type");
expect(row).toHaveProperty("notes");
});
it("returns same data regardless of query", () => {
const r1 = queryDataImpl("revenue");
const r2 = queryDataImpl("expenses");
expect(r1).toEqual(r2);
});
it("is deterministic (seeded RNG)", () => {
const r1 = queryDataImpl("test");
const r2 = queryDataImpl("test");
expect(r1).toEqual(r2);
});
it("categories are Revenue or Expenses", () => {
const rows = queryDataImpl("test");
const categories = new Set(rows.map((r) => r.category));
expect(categories).toEqual(new Set(["Revenue", "Expenses"]));
});
it("types are income or expense", () => {
const rows = queryDataImpl("test");
const types = new Set(rows.map((r) => r.type));
expect(types).toEqual(new Set(["income", "expense"]));
});
it("dates are in 2026", () => {
const rows = queryDataImpl("test");
for (const row of rows) {
expect(row.date).toMatch(/^2026-/);
}
});
it("amount is a numeric string", () => {
const rows = queryDataImpl("test");
for (const row of rows) {
expect(Number(row.amount)).not.toBeNaN();
expect(Number(row.amount)).toBeGreaterThan(0);
}
});
});
@@ -0,0 +1,124 @@
import { describe, it, expect } from "vitest";
import {
manageSalesTodosImpl,
getSalesTodosImpl,
INITIAL_SALES_TODOS,
} from "../sales-todos";
describe("INITIAL_SALES_TODOS", () => {
it("has 3 items", () => {
expect(INITIAL_SALES_TODOS).toHaveLength(3);
});
it("has fixed IDs", () => {
expect(INITIAL_SALES_TODOS[0].id).toBe("st-001");
expect(INITIAL_SALES_TODOS[1].id).toBe("st-002");
expect(INITIAL_SALES_TODOS[2].id).toBe("st-003");
});
});
describe("manageSalesTodosImpl", () => {
it("assigns UUID to todos missing ID", () => {
const result = manageSalesTodosImpl([{ title: "New deal" }]);
expect(result[0].id).toBeTruthy();
expect(result[0].id.length).toBeGreaterThan(0);
});
it("preserves existing IDs", () => {
const result = manageSalesTodosImpl([{ id: "keep-me", title: "Deal" }]);
expect(result[0].id).toBe("keep-me");
});
it("provides defaults for missing fields", () => {
const result = manageSalesTodosImpl([{ title: "Minimal" }]);
expect(result[0].stage).toBe("prospect");
expect(result[0].value).toBe(0);
expect(result[0].completed).toBe(false);
expect(result[0].dueDate).toBe("");
expect(result[0].assignee).toBe("");
});
it("preserves provided fields", () => {
const result = manageSalesTodosImpl([
{
id: "x",
title: "Big Deal",
stage: "negotiation",
value: 50000,
dueDate: "2026-05-01",
assignee: "Alice",
completed: true,
},
]);
expect(result[0].title).toBe("Big Deal");
expect(result[0].stage).toBe("negotiation");
expect(result[0].value).toBe(50000);
expect(result[0].completed).toBe(true);
});
it("handles empty array", () => {
const result = manageSalesTodosImpl([]);
expect(result).toEqual([]);
});
it("handles multiple todos", () => {
const result = manageSalesTodosImpl([
{ title: "A" },
{ title: "B" },
{ title: "C" },
]);
expect(result).toHaveLength(3);
// Each should get a unique ID
const ids = new Set(result.map((r) => r.id));
expect(ids.size).toBe(3);
});
it("replaces empty string id with a generated UUID", () => {
const result = manageSalesTodosImpl([{ id: "", title: "x" }]);
expect(result[0].id).toBeTruthy();
expect(result[0].id).not.toBe("");
expect(result[0].id.length).toBeGreaterThan(0);
});
});
describe("getSalesTodosImpl", () => {
it("returns initial todos when undefined", () => {
const result = getSalesTodosImpl(undefined);
expect(result).toHaveLength(3);
expect(result[0].id).toBe("st-001");
});
it("returns initial todos when null", () => {
const result = getSalesTodosImpl(null);
expect(result).toHaveLength(3);
});
it("returns empty array when given empty array", () => {
const result = getSalesTodosImpl([]);
// empty array means user cleared all todos — return empty, not defaults
expect(result).toHaveLength(0);
});
it("returns provided todos when non-empty", () => {
const todos = [
{
id: "1",
title: "Test",
stage: "prospect" as const,
value: 100,
dueDate: "",
assignee: "",
completed: false,
},
];
const result = getSalesTodosImpl(todos);
expect(result).toHaveLength(1);
expect(result[0].title).toBe("Test");
});
it("returns a copy, not the original INITIAL_SALES_TODOS reference", () => {
const result = getSalesTodosImpl(undefined);
expect(result).not.toBe(INITIAL_SALES_TODOS);
expect(result).toEqual(INITIAL_SALES_TODOS);
});
});
@@ -0,0 +1,26 @@
import { describe, it, expect } from "vitest";
import { scheduleMeetingImpl } from "../schedule-meeting";
describe("scheduleMeetingImpl", () => {
it("returns pending_approval status", () => {
expect(scheduleMeetingImpl("discuss roadmap").status).toBe(
"pending_approval",
);
});
it("includes the reason", () => {
expect(scheduleMeetingImpl("quarterly review").reason).toBe(
"quarterly review",
);
});
it("uses default 30-minute duration", () => {
expect(scheduleMeetingImpl("sync").duration_minutes).toBe(30);
});
it("accepts custom duration", () => {
expect(scheduleMeetingImpl("deep dive", 60).duration_minutes).toBe(60);
});
it("includes a message", () => {
const result = scheduleMeetingImpl("onboarding", 45);
expect(result.message).toContain("onboarding");
expect(result.message).toContain("45");
});
});
@@ -0,0 +1,52 @@
import { describe, it, expect } from "vitest";
import { searchFlightsImpl } from "../search-flights";
import type { Flight } from "../types";
describe("searchFlightsImpl", () => {
const mockFlight: Flight = {
airline: "Test Air",
airlineLogo: "https://example.com/logo.png",
flightNumber: "TA100",
origin: "SFO",
destination: "JFK",
date: "Tue, Apr 15",
departureTime: "08:00",
arrivalTime: "16:00",
duration: "5h",
status: "On Time",
statusColor: "#22c55e",
price: "$299",
currency: "USD",
};
it("returns flights and schema", () => {
const result = searchFlightsImpl([mockFlight]);
expect(result).toHaveProperty("flights");
expect(result).toHaveProperty("schema");
});
it("passes flights through unchanged", () => {
const result = searchFlightsImpl([mockFlight]);
expect(result.flights).toHaveLength(1);
expect(result.flights[0]).toEqual(mockFlight);
});
it("returns empty schema object", () => {
const result = searchFlightsImpl([mockFlight]);
expect(result.schema).toEqual({});
});
it("handles empty flights array", () => {
const result = searchFlightsImpl([]);
expect(result.flights).toHaveLength(0);
});
it("handles multiple flights", () => {
const result = searchFlightsImpl([
mockFlight,
{ ...mockFlight, flightNumber: "TA200" },
]);
expect(result.flights).toHaveLength(2);
expect(result.flights[1].flightNumber).toBe("TA200");
});
});
@@ -0,0 +1,98 @@
/**
* Dynamic A2UI generation via secondary LLM call.
* TypeScript equivalent of showcase/shared/python/tools/generate_a2ui.py.
*/
export const CUSTOM_CATALOG_ID = "copilotkit://app-dashboard-catalog";
export const RENDER_A2UI_TOOL_SCHEMA = {
name: "render_a2ui",
description: "Render a dynamic A2UI v0.9 surface.",
parameters: {
type: "object",
properties: {
surfaceId: { type: "string", description: "Unique surface identifier." },
catalogId: { type: "string", description: "The catalog ID." },
components: {
type: "array",
items: { type: "object" },
description: "A2UI v0.9 component array (flat format).",
},
data: {
type: "object",
description: "Optional initial data model for the surface.",
},
},
required: ["surfaceId", "catalogId", "components"],
},
} as const;
export interface GenerateA2UIInput {
messages: Array<Record<string, unknown>>;
contextEntries?: Array<Record<string, unknown>>;
}
export interface GenerateA2UIResult {
systemPrompt: string;
toolSchema: typeof RENDER_A2UI_TOOL_SCHEMA;
toolChoice: string;
messages: Array<Record<string, unknown>>;
catalogId: string;
}
export function generateA2uiImpl(input: GenerateA2UIInput): GenerateA2UIResult {
const contextText = (input.contextEntries ?? [])
.filter(
(e): e is Record<string, unknown> & { value: string } =>
typeof e === "object" &&
typeof e.value === "string" &&
e.value.length > 0,
)
.map((e) => e.value)
.join("\n\n");
return {
systemPrompt: contextText,
toolSchema: RENDER_A2UI_TOOL_SCHEMA,
toolChoice: "render_a2ui",
messages: input.messages,
catalogId: CUSTOM_CATALOG_ID,
};
}
export interface A2UIOperation {
type: "create_surface" | "update_components" | "update_data_model";
surfaceId: string;
catalogId?: string;
components?: Array<Record<string, unknown>>;
data?: Record<string, unknown>;
}
export function buildA2uiOperationsFromToolCall(
args: Record<string, unknown>,
): {
a2ui_operations: A2UIOperation[];
} {
const surfaceId = (args.surfaceId as string) ?? "dynamic-surface";
const catalogId = (args.catalogId as string) ?? CUSTOM_CATALOG_ID;
const components = (args.components as Array<Record<string, unknown>>) ?? [];
const data = args.data as Record<string, unknown> | undefined;
if (components.length === 0) {
console.warn(
"buildA2uiOperationsFromToolCall: empty components for surface",
surfaceId,
);
}
const ops: A2UIOperation[] = [
{ type: "create_surface", surfaceId, catalogId },
{ type: "update_components", surfaceId, components },
];
if (data) {
ops.push({ type: "update_data_model", surfaceId, data });
}
return { a2ui_operations: ops };
}
@@ -0,0 +1,67 @@
/**
* Mock weather data tool implementation.
*
* TypeScript equivalent of showcase/shared/python/tools/get_weather.py.
* Uses a simple seed derived from the city name so repeated calls for
* the same city return consistent results within a session.
*/
import { WeatherResult } from "./types";
const CONDITIONS = [
"Sunny",
"Partly Cloudy",
"Cloudy",
"Overcast",
"Light Rain",
"Heavy Rain",
"Thunderstorm",
"Snow",
"Foggy",
"Windy",
];
/**
* Simple seeded pseudo-random number generator (mulberry32).
* Produces deterministic output for a given seed so the same city
* always returns the same weather within a process lifetime.
*/
function seededRandom(seed: number): () => number {
let t = seed;
return () => {
t = (t + 0x6d2b79f5) | 0;
let v = Math.imul(t ^ (t >>> 15), 1 | t);
v = (v + Math.imul(v ^ (v >>> 7), 61 | v)) ^ v;
return ((v ^ (v >>> 14)) >>> 0) / 4294967296;
};
}
function hashString(s: string): number {
let hash = 0;
for (let i = 0; i < s.length; i++) {
hash = (Math.imul(31, hash) + s.charCodeAt(i)) | 0;
}
return hash;
}
function randInt(rng: () => number, min: number, max: number): number {
return Math.floor(rng() * (max - min + 1)) + min;
}
function randChoice<T>(rng: () => number, arr: T[]): T {
return arr[Math.floor(rng() * arr.length)];
}
export function getWeatherImpl(city: string): WeatherResult {
const rng = seededRandom(hashString(city.toLowerCase()));
const temperature = randInt(rng, 20, 95);
return {
city,
temperature,
humidity: randInt(rng, 30, 90),
wind_speed: randInt(rng, 2, 30),
feels_like: temperature + randInt(rng, -5, 5),
conditions: randChoice(rng, CONDITIONS),
};
}
@@ -0,0 +1,31 @@
/**
* Shared TypeScript tool implementations for CopilotKit showcase.
*
* Pure functions with no framework imports — consumed by
* langgraph-typescript, mastra, claude-sdk-typescript, and any
* future TypeScript-based showcase packages.
*/
export { getWeatherImpl } from "./get-weather";
export { queryDataImpl } from "./query-data";
export type { DataRow } from "./query-data";
export {
manageSalesTodosImpl,
getSalesTodosImpl,
INITIAL_SALES_TODOS,
} from "./sales-todos";
export { searchFlightsImpl } from "./search-flights";
export { scheduleMeetingImpl } from "./schedule-meeting";
export type { ScheduleMeetingResult } from "./schedule-meeting";
export {
generateA2uiImpl,
buildA2uiOperationsFromToolCall,
RENDER_A2UI_TOOL_SCHEMA,
CUSTOM_CATALOG_ID,
} from "./generate-a2ui";
export type {
GenerateA2UIInput,
GenerateA2UIResult,
A2UIOperation,
} from "./generate-a2ui";
export type { SalesTodo, SalesStage, Flight, WeatherResult } from "./types";
@@ -0,0 +1,107 @@
/**
* Query data tool implementation — returns mock financial data.
*
* TypeScript equivalent of showcase/shared/python/tools/query_data.py.
* In the future this could read a CSV, but for TS backend simplicity
* we use generated mock data matching the Python fallback format.
*/
export interface DataRow {
date: string;
category: string;
subcategory: string;
amount: string;
type: string;
notes: string;
}
// Seeded random for deterministic mock data
function seededRandom(seed: number): () => number {
let s = seed;
return () => {
s = (s * 1103515245 + 12345) & 0x7fffffff;
return s / 0x7fffffff;
};
}
function generateMockData(): DataRow[] {
const rand = seededRandom(42);
const categories: Array<{
category: string;
subcategory: string;
type: string;
}> = [
{
category: "Revenue",
subcategory: "Enterprise Subscriptions",
type: "income",
},
{ category: "Revenue", subcategory: "Pro Tier Upgrades", type: "income" },
{ category: "Revenue", subcategory: "API Usage Overages", type: "income" },
{ category: "Revenue", subcategory: "Consulting Services", type: "income" },
{ category: "Revenue", subcategory: "Marketplace Sales", type: "income" },
{
category: "Expenses",
subcategory: "Engineering Salaries",
type: "expense",
},
{ category: "Expenses", subcategory: "Product Team", type: "expense" },
{
category: "Expenses",
subcategory: "AWS Infrastructure",
type: "expense",
},
{ category: "Expenses", subcategory: "Marketing", type: "expense" },
{ category: "Expenses", subcategory: "Customer Success", type: "expense" },
{ category: "Expenses", subcategory: "AI Model Costs", type: "expense" },
];
const notes: Record<string, string> = {
"Enterprise Subscriptions": "3 new enterprise customers",
"Pro Tier Upgrades": "31 upgrades + reduced churn",
"API Usage Overages": "Heavy usage from top-10 accounts",
"Consulting Services": "2 implementation projects",
"Marketplace Sales": "Partner integrations revenue",
"Engineering Salaries": "7 engineers + 2 contractors",
"Product Team": "PM + designers + QA",
"AWS Infrastructure": "Compute + storage + bandwidth",
Marketing: "Paid ads + content + events",
"Customer Success": "3 CSMs + tooling",
"AI Model Costs": "OpenAI + Anthropic API spend",
};
const rows: DataRow[] = [];
const months = ["01", "02", "03", "04", "05", "06"];
for (const month of months) {
for (const cat of categories) {
const baseAmount =
cat.type === "income"
? 15000 + Math.floor(rand() * 35000)
: 8000 + Math.floor(rand() * 40000);
const day = String(1 + Math.floor(rand() * 28)).padStart(2, "0");
rows.push({
date: `2026-${month}-${day}`,
category: cat.category,
subcategory: cat.subcategory,
amount: String(baseAmount),
type: cat.type,
notes: notes[cat.subcategory] ?? "",
});
}
}
return rows;
}
const MOCK_DATA: DataRow[] = generateMockData();
/**
* Query the database. Takes natural language.
*
* Always call before showing a chart or graph. Returns the full
* dataset as a list of row objects.
*/
export function queryDataImpl(_query: string): DataRow[] {
return MOCK_DATA;
}
@@ -0,0 +1,65 @@
/**
* Sales todos tool implementation.
*
* TypeScript equivalent of showcase/shared/python/tools/sales_todos.py.
*/
import { SalesTodo } from "./types";
export const INITIAL_SALES_TODOS: SalesTodo[] = [
{
id: "st-001",
title: "Follow up with Acme Corp on enterprise proposal",
stage: "proposal",
value: 85000,
dueDate: "2026-04-15",
assignee: "Sarah Chen",
completed: false,
},
{
id: "st-002",
title: "Qualify lead from TechFlow demo request",
stage: "prospect",
value: 42000,
dueDate: "2026-04-18",
assignee: "Mike Johnson",
completed: false,
},
{
id: "st-003",
title: "Send contract to DataViz Inc for final review",
stage: "negotiation",
value: 120000,
dueDate: "2026-04-20",
assignee: "Sarah Chen",
completed: false,
},
];
/**
* Assign crypto.randomUUID() to any todos missing an ID, then return
* the updated list.
*/
export function manageSalesTodosImpl(todos: Partial<SalesTodo>[]): SalesTodo[] {
return todos.map((todo) => ({
id: todo.id || crypto.randomUUID(),
title: todo.title ?? "",
stage: todo.stage ?? "prospect",
value: todo.value ?? 0,
dueDate: todo.dueDate ?? "",
assignee: todo.assignee ?? "",
completed: todo.completed ?? false,
}));
}
/**
* Return current todos or initial defaults if none provided.
*/
export function getSalesTodosImpl(
currentTodos?: Partial<SalesTodo>[] | null,
): SalesTodo[] {
if (currentTodos != null) {
return currentTodos.length > 0 ? manageSalesTodosImpl(currentTodos) : [];
}
return [...INITIAL_SALES_TODOS];
}
@@ -0,0 +1,24 @@
/**
* Schedule meeting tool implementation — HITL gated.
*
* TypeScript equivalent of showcase/shared/python/tools/schedule_meeting.py.
*/
export interface ScheduleMeetingResult {
status: "pending_approval";
reason: string;
duration_minutes: number;
message: string;
}
export function scheduleMeetingImpl(
reason: string,
durationMinutes: number = 30,
): ScheduleMeetingResult {
return {
status: "pending_approval",
reason,
duration_minutes: durationMinutes,
message: `Meeting request: ${reason} (${durationMinutes} min). Awaiting user time selection.`,
};
}
@@ -0,0 +1,19 @@
/**
* Search flights tool implementation.
*
* Simple passthrough that wraps flights for AG-UI rendering.
* The frontend GenUI components handle presentation.
*/
import { Flight } from "./types";
/**
* Wrap the provided flights array for consumption by the frontend
* flight-search GenUI component.
*/
export function searchFlightsImpl(flights: Flight[]): {
flights: Flight[];
schema: Record<string, unknown>;
} {
return { flights, schema: {} };
}
@@ -0,0 +1,49 @@
/**
* Shared type definitions for showcase tools.
*
* These mirror the Python types in showcase/shared/python/tools/types.py
* and the frontend types in showcase/shared/frontend/src/types.ts.
*/
export type SalesStage =
| "prospect"
| "qualified"
| "proposal"
| "negotiation"
| "closed-won"
| "closed-lost";
export interface SalesTodo {
id: string;
title: string;
stage: SalesStage;
value: number;
dueDate: string;
assignee: string;
completed: boolean;
}
export interface Flight {
airline: string;
airlineLogo: string;
flightNumber: string;
origin: string;
destination: string;
date: string;
departureTime: string;
arrivalTime: string;
duration: string;
status: string;
statusColor: string;
price: string;
currency: string;
}
export interface WeatherResult {
city: string;
temperature: number;
humidity: number;
wind_speed: number;
feels_like: number;
conditions: string;
}
@@ -0,0 +1,3 @@
# LangGraph API
.langgraph_api
@@ -0,0 +1,22 @@
/**
* LangGraph TypeScript agent for the Declarative Generative UI (A2UI — Dynamic Schema) demo.
*/
import { ChatOpenAI } from "@langchain/openai";
import { createAgent } from "langchain";
import { copilotkitMiddleware } from "@copilotkit/sdk-js/langgraph";
const SYSTEM_PROMPT =
"You are a demo assistant for Declarative Generative UI (A2UI — Dynamic " +
"Schema). Whenever a response would benefit from a rich visual — a " +
"dashboard, status report, KPI summary, card layout, info grid, a " +
"pie/donut chart of part-of-whole breakdowns, or a bar chart comparing " +
"values across categories — call `generate_a2ui` to draw it. Keep chat " +
"replies to one short sentence and let the UI do the talking.";
export const graph = createAgent({
model: new ChatOpenAI({ model: "gpt-4.1" }),
tools: [],
middleware: [copilotkitMiddleware],
systemPrompt: SYSTEM_PROMPT,
});
@@ -0,0 +1,178 @@
/**
* LangGraph TypeScript agent for the Declarative Generative UI (A2UI — Fixed Schema) demo.
*
* Fixed-schema A2UI: the component tree (schema) is authored ahead of time as
* JSON and loaded at startup. The agent only streams *data* into the data model
* at runtime. The frontend registers a matching catalog that pins the schema's
* component names to real React implementations.
*
* Ported from `src/agents/a2ui_fixed.py`.
*/
// @region[backend-render-operations]
// @region[backend-schema-json-load]
import { readFileSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import { z } from "zod";
import { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { AIMessage, SystemMessage } from "@langchain/core/messages";
import {
MemorySaver,
START,
StateGraph,
Annotation,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
const CATALOG_ID = "copilotkit://flight-fixed-catalog";
const SURFACE_ID = "flight-fixed-schema";
const A2UI_OPERATIONS_KEY = "a2ui_operations";
// Schemas are JSON so they can be authored and reviewed independently of the
// agent code.
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const SCHEMAS_DIR = join(__dirname, "a2ui_schemas");
function loadSchema(filename: string): unknown[] {
const full = join(SCHEMAS_DIR, filename);
return JSON.parse(readFileSync(full, "utf-8")) as unknown[];
}
const FLIGHT_SCHEMA = loadSchema("flight_schema.json");
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const BOOKED_SCHEMA = loadSchema("booked_schema.json");
// @endregion[backend-schema-json-load]
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
});
export type AgentState = typeof AgentStateAnnotation.State;
function createSurfaceOp(surfaceId: string, catalogId: string) {
return {
version: "v0.9",
createSurface: { surfaceId, catalogId },
};
}
function updateComponentsOp(surfaceId: string, components: unknown[]) {
return {
version: "v0.9",
updateComponents: { surfaceId, components },
};
}
function updateDataModelOp(
surfaceId: string,
data: unknown,
path: string = "/",
) {
return {
version: "v0.9",
updateDataModel: { surfaceId, path, value: data },
};
}
function renderA2uiOperations(operations: unknown[]): string {
return JSON.stringify({ [A2UI_OPERATIONS_KEY]: operations });
}
const displayFlight = tool(
async ({
origin,
destination,
airline,
price,
}: {
origin: string;
destination: string;
airline: string;
price: string;
}) => {
return renderA2uiOperations([
createSurfaceOp(SURFACE_ID, CATALOG_ID),
updateComponentsOp(SURFACE_ID, FLIGHT_SCHEMA),
updateDataModelOp(SURFACE_ID, { origin, destination, airline, price }),
]);
},
{
name: "display_flight",
description:
'Show a flight card for the given trip. Use short airport codes (e.g. "SFO", "JFK") for origin/destination and a price string like "$289".',
schema: z.object({
origin: z.string(),
destination: z.string(),
airline: z.string(),
price: z.string(),
}),
},
);
// @endregion[backend-render-operations]
const tools = [displayFlight];
const SYSTEM_PROMPT =
"You help users find flights. When asked about a flight, call " +
"display_flight with origin, destination, airline, and price. " +
"Keep any chat reply to one short sentence.";
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o-mini",
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const systemMessage = new SystemMessage({ content: SYSTEM_PROMPT });
const response = await modelWithTools.invoke(
[systemMessage, ...state.messages],
config,
);
return { messages: response };
}
function shouldContinue({ messages, copilotkit }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const toolCallName = lastMessage.tool_calls![0].name;
if (!actions || actions.every((action) => action.name !== toolCallName)) {
return "tool_node";
}
}
return "__end__";
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,20 @@
[
{
"id": "root",
"component": "Column",
"gap": 8,
"children": ["title", "detail"]
},
{
"id": "title",
"component": "Text",
"text": { "path": "/title" },
"variant": "h2"
},
{
"id": "detail",
"component": "Text",
"text": { "path": "/detail" },
"variant": "body"
}
]
@@ -0,0 +1,77 @@
[
{
"id": "root",
"component": "Card",
"child": "content"
},
{
"id": "content",
"component": "Column",
"children": ["title", "route", "meta", "bookButton"]
},
{
"id": "title",
"component": "Title",
"text": "Flight Details"
},
{
"id": "route",
"component": "Row",
"justify": "spaceBetween",
"align": "center",
"children": ["from", "arrow", "to"]
},
{
"id": "from",
"component": "Airport",
"code": { "path": "/origin" }
},
{
"id": "arrow",
"component": "Arrow"
},
{
"id": "to",
"component": "Airport",
"code": { "path": "/destination" }
},
{
"id": "meta",
"component": "Row",
"justify": "spaceBetween",
"align": "center",
"children": ["airline", "price"]
},
{
"id": "airline",
"component": "AirlineBadge",
"name": { "path": "/airline" }
},
{
"id": "price",
"component": "PriceTag",
"amount": { "path": "/price" }
},
{
"id": "bookButton",
"component": "Button",
"variant": "primary",
"child": "bookButtonLabel",
"action": {
"event": {
"name": "book_flight",
"context": {
"origin": { "path": "/origin" },
"destination": { "path": "/destination" },
"airline": { "path": "/airline" },
"price": { "path": "/price" }
}
}
}
},
{
"id": "bookButtonLabel",
"component": "Text",
"text": "Book flight"
}
]
@@ -0,0 +1,177 @@
/**
* LangGraph TypeScript agent backing the Agent Config Object demo.
*
* Reads three frontend-published context values — tone, expertise,
* responseLength — from ``state.copilotkit.context`` and builds its system
* prompt dynamically per turn.
*
* The frontend uses `useAgentContext` to publish the config object on each
* render. This graph reads the latest matching context entry with defensive
* defaults (unknown / missing values fall back to the defaults) and composes
* the system prompt from three small rulebooks before invoking the model.
*/
// @region[agent-config-setup]
import type { RunnableConfig } from "@langchain/core/runnables";
import type { AIMessage } from "@langchain/core/messages";
import { SystemMessage } from "@langchain/core/messages";
import {
MemorySaver,
START,
StateGraph,
Annotation,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import { CopilotKitStateAnnotation } from "@copilotkit/sdk-js/langgraph";
type Tone = "professional" | "casual" | "enthusiastic";
type Expertise = "beginner" | "intermediate" | "expert";
type ResponseLength = "concise" | "detailed";
const DEFAULT_TONE: Tone = "professional";
const DEFAULT_EXPERTISE: Expertise = "intermediate";
const DEFAULT_RESPONSE_LENGTH: ResponseLength = "concise";
const VALID_TONES = new Set<string>(["professional", "casual", "enthusiastic"]);
const VALID_EXPERTISE = new Set<string>(["beginner", "intermediate", "expert"]);
const VALID_RESPONSE_LENGTHS = new Set<string>(["concise", "detailed"]);
interface ResolvedProps {
tone: Tone;
expertise: Expertise;
responseLength: ResponseLength;
}
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
});
type AgentState = typeof AgentStateAnnotation.State;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function containsConfigKeys(value: Record<string, unknown>): boolean {
return "tone" in value || "expertise" in value || "responseLength" in value;
}
function extractConfigContext(context: unknown): Record<string, unknown> {
if (typeof context === "string") {
try {
return extractConfigContext(JSON.parse(context));
} catch {
return {};
}
}
if (Array.isArray(context)) {
for (const entry of [...context].toReversed()) {
const extracted = extractConfigContext(entry);
if (containsConfigKeys(extracted)) return extracted;
}
return {};
}
if (!isRecord(context)) return {};
const value = context.value;
if (typeof value === "string") {
const parsed = extractConfigContext(value);
if (containsConfigKeys(parsed)) return parsed;
}
if (isRecord(value) && containsConfigKeys(value)) return value;
return containsConfigKeys(context) ? context : {};
}
function readForwardedProperties(
config: RunnableConfig | undefined,
): Record<string, unknown> {
const configurable =
(config?.configurable as Record<string, unknown> | undefined) ?? {};
return (configurable.properties as Record<string, unknown> | undefined) ?? {};
}
function readConfig(state: AgentState, config: RunnableConfig): ResolvedProps {
const contextConfig = extractConfigContext(state.copilotkit?.context);
const properties = containsConfigKeys(contextConfig)
? contextConfig
: readForwardedProperties(config);
const toneRaw = properties.tone;
const expertiseRaw = properties.expertise;
const responseLengthRaw = properties.responseLength;
const tone =
typeof toneRaw === "string" && VALID_TONES.has(toneRaw)
? (toneRaw as Tone)
: DEFAULT_TONE;
const expertise =
typeof expertiseRaw === "string" && VALID_EXPERTISE.has(expertiseRaw)
? (expertiseRaw as Expertise)
: DEFAULT_EXPERTISE;
const responseLength =
typeof responseLengthRaw === "string" &&
VALID_RESPONSE_LENGTHS.has(responseLengthRaw)
? (responseLengthRaw as ResponseLength)
: DEFAULT_RESPONSE_LENGTH;
return { tone, expertise, responseLength };
}
const TONE_RULES: Record<Tone, string> = {
professional: "Use neutral, precise language. No emoji. Short sentences.",
casual:
"Use friendly, conversational language. Contractions OK. Light humor welcome.",
enthusiastic:
"Use upbeat, energetic language. Exclamation points OK. Emoji OK.",
};
const EXPERTISE_RULES: Record<Expertise, string> = {
beginner: "Assume no prior knowledge. Define jargon. Use analogies.",
intermediate:
"Assume common terms are understood; explain specialized terms.",
expert: "Assume technical fluency. Use precise terminology. Skip basics.",
};
const LENGTH_RULES: Record<ResponseLength, string> = {
concise: "Respond in 1-3 sentences.",
detailed: "Respond in multiple paragraphs with examples where relevant.",
};
function buildSystemPrompt(props: ResolvedProps): string {
return [
"You are a helpful assistant.",
"",
`Tone: ${TONE_RULES[props.tone]}`,
`Expertise level: ${EXPERTISE_RULES[props.expertise]}`,
`Response length: ${LENGTH_RULES[props.responseLength]}`,
].join("\n");
}
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
model: "gpt-4o-mini",
temperature: 0.4,
});
const props = readConfig(state, config);
const systemPrompt = buildSystemPrompt(props);
const response = (await model.invoke(
[new SystemMessage({ content: systemPrompt }), ...state.messages],
config,
)) as AIMessage;
return { messages: response };
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node")
.addEdge("chat_node", "__end__");
const memory = new MemorySaver();
export const graph = workflow.compile({ checkpointer: memory });
// @endregion[agent-config-setup]
@@ -0,0 +1,41 @@
date,category,subcategory,amount,type,notes
2026-01-05,Revenue,Enterprise Subscriptions,28000,income,3 new enterprise customers (Acme Corp, TechFlow, DataViz Inc)
2026-01-05,Revenue,Pro Tier Upgrades,18000,income,24 users upgraded from free to pro
2026-01-08,Revenue,API Usage Overages,9500,income,High API usage from top 5 customers
2026-01-10,Expenses,Engineering Salaries,42000,expense,7 engineers + 2 contractors
2026-01-10,Expenses,Product Team,18000,expense,PM and 2 designers
2026-01-12,Expenses,AWS Infrastructure,8200,expense,Increased compute for new AI features
2026-01-15,Expenses,Marketing - Paid Ads,12000,expense,Google Ads and LinkedIn campaigns
2026-01-18,Revenue,Consulting Services,14500,income,Custom integration for Acme Corp
2026-01-20,Expenses,Customer Success,15000,expense,3 CSMs + support tools (Intercom)
2026-01-22,Expenses,AI Model Costs,4200,expense,OpenAI API usage for product features
2026-01-25,Revenue,Marketplace Sales,12800,income,Template and plugin sales
2026-01-28,Expenses,Office & Equipment,3500,expense,New laptops and coworking spaces
2026-02-03,Revenue,Enterprise Subscriptions,31000,income,2 new customers + expansion from TechFlow
2026-02-03,Revenue,Pro Tier Upgrades,22500,income,31 upgrades + reduced churn
2026-02-05,Revenue,API Usage Overages,11800,income,DataViz Inc heavy API usage spike
2026-02-07,Expenses,Engineering Salaries,42000,expense,Same headcount as January
2026-02-07,Expenses,Product Team,18000,expense,No changes to product team
2026-02-10,Expenses,AWS Infrastructure,9500,expense,Traffic spike from viral social post
2026-02-12,Expenses,Marketing - Paid Ads,15000,expense,Increased ad spend for Q1 push
2026-02-14,Revenue,Consulting Services,18000,income,2 custom projects (TechFlow + new client)
2026-02-18,Expenses,Customer Success,16500,expense,Hired 1 additional CSM
2026-02-20,Expenses,AI Model Costs,5800,expense,Increased usage from new AI features launch
2026-02-22,Revenue,Marketplace Sales,14200,income,Top template hit featured list
2026-02-25,Expenses,Conference & Travel,4500,expense,Team attended SaaS Conference 2026
2026-02-27,Revenue,Partnership Revenue,11500,income,Referral fees from integration partners
2026-03-02,Revenue,Enterprise Subscriptions,35000,income,Major win: Fortune 500 customer signed
2026-03-02,Revenue,Pro Tier Upgrades,26000,income,42 upgrades - best month yet
2026-03-05,Revenue,API Usage Overages,13200,income,Consistent high usage across top tier
2026-03-08,Expenses,Engineering Salaries,48000,expense,Hired 1 senior engineer for AI team
2026-03-08,Expenses,Product Team,21000,expense,Promoted designer to senior level
2026-03-10,Expenses,AWS Infrastructure,11000,expense,Scaled infrastructure for enterprise client
2026-03-12,Expenses,Marketing - Paid Ads,18000,expense,Doubled down on successful campaigns
2026-03-14,Revenue,Consulting Services,21500,income,Fortune 500 onboarding + 2 other projects
2026-03-16,Expenses,Customer Success,19500,expense,Hired dedicated enterprise CSM
2026-03-18,Expenses,AI Model Costs,7200,expense,Fortune 500 client heavy AI usage
2026-03-20,Revenue,Marketplace Sales,15800,income,3 new templates in top 10
2026-03-22,Expenses,Sales & BD,12000,expense,Hired first sales rep for enterprise
2026-03-24,Revenue,Partnership Revenue,14200,income,New integration partnerships launched
2026-03-26,Expenses,Security & Compliance,6500,expense,SOC 2 audit and security tools
2026-03-28,Revenue,Training & Workshops,10200,income,Conducted 2 customer training sessions
1 date,category,subcategory,amount,type,notes
2 2026-01-05,Revenue,Enterprise Subscriptions,28000,income,3 new enterprise customers (Acme Corp, TechFlow, DataViz Inc)
3 2026-01-05,Revenue,Pro Tier Upgrades,18000,income,24 users upgraded from free to pro
4 2026-01-08,Revenue,API Usage Overages,9500,income,High API usage from top 5 customers
5 2026-01-10,Expenses,Engineering Salaries,42000,expense,7 engineers + 2 contractors
6 2026-01-10,Expenses,Product Team,18000,expense,PM and 2 designers
7 2026-01-12,Expenses,AWS Infrastructure,8200,expense,Increased compute for new AI features
8 2026-01-15,Expenses,Marketing - Paid Ads,12000,expense,Google Ads and LinkedIn campaigns
9 2026-01-18,Revenue,Consulting Services,14500,income,Custom integration for Acme Corp
10 2026-01-20,Expenses,Customer Success,15000,expense,3 CSMs + support tools (Intercom)
11 2026-01-22,Expenses,AI Model Costs,4200,expense,OpenAI API usage for product features
12 2026-01-25,Revenue,Marketplace Sales,12800,income,Template and plugin sales
13 2026-01-28,Expenses,Office & Equipment,3500,expense,New laptops and coworking spaces
14 2026-02-03,Revenue,Enterprise Subscriptions,31000,income,2 new customers + expansion from TechFlow
15 2026-02-03,Revenue,Pro Tier Upgrades,22500,income,31 upgrades + reduced churn
16 2026-02-05,Revenue,API Usage Overages,11800,income,DataViz Inc heavy API usage spike
17 2026-02-07,Expenses,Engineering Salaries,42000,expense,Same headcount as January
18 2026-02-07,Expenses,Product Team,18000,expense,No changes to product team
19 2026-02-10,Expenses,AWS Infrastructure,9500,expense,Traffic spike from viral social post
20 2026-02-12,Expenses,Marketing - Paid Ads,15000,expense,Increased ad spend for Q1 push
21 2026-02-14,Revenue,Consulting Services,18000,income,2 custom projects (TechFlow + new client)
22 2026-02-18,Expenses,Customer Success,16500,expense,Hired 1 additional CSM
23 2026-02-20,Expenses,AI Model Costs,5800,expense,Increased usage from new AI features launch
24 2026-02-22,Revenue,Marketplace Sales,14200,income,Top template hit featured list
25 2026-02-25,Expenses,Conference & Travel,4500,expense,Team attended SaaS Conference 2026
26 2026-02-27,Revenue,Partnership Revenue,11500,income,Referral fees from integration partners
27 2026-03-02,Revenue,Enterprise Subscriptions,35000,income,Major win: Fortune 500 customer signed
28 2026-03-02,Revenue,Pro Tier Upgrades,26000,income,42 upgrades - best month yet
29 2026-03-05,Revenue,API Usage Overages,13200,income,Consistent high usage across top tier
30 2026-03-08,Expenses,Engineering Salaries,48000,expense,Hired 1 senior engineer for AI team
31 2026-03-08,Expenses,Product Team,21000,expense,Promoted designer to senior level
32 2026-03-10,Expenses,AWS Infrastructure,11000,expense,Scaled infrastructure for enterprise client
33 2026-03-12,Expenses,Marketing - Paid Ads,18000,expense,Doubled down on successful campaigns
34 2026-03-14,Revenue,Consulting Services,21500,income,Fortune 500 onboarding + 2 other projects
35 2026-03-16,Expenses,Customer Success,19500,expense,Hired dedicated enterprise CSM
36 2026-03-18,Expenses,AI Model Costs,7200,expense,Fortune 500 client heavy AI usage
37 2026-03-20,Revenue,Marketplace Sales,15800,income,3 new templates in top 10
38 2026-03-22,Expenses,Sales & BD,12000,expense,Hired first sales rep for enterprise
39 2026-03-24,Revenue,Partnership Revenue,14200,income,New integration partnerships launched
40 2026-03-26,Expenses,Security & Compliance,6500,expense,SOC 2 audit and security tools
41 2026-03-28,Revenue,Training & Workshops,10200,income,Conducted 2 customer training sessions
@@ -0,0 +1,37 @@
[
{
"id": "root",
"component": "Row",
"children": {
"componentId": "flight-card",
"path": "/flights"
},
"gap": 16
},
{
"id": "flight-card",
"component": "FlightCard",
"airline": { "path": "airline" },
"airlineLogo": { "path": "airlineLogo" },
"flightNumber": { "path": "flightNumber" },
"origin": { "path": "origin" },
"destination": { "path": "destination" },
"date": { "path": "date" },
"departureTime": { "path": "departureTime" },
"arrivalTime": { "path": "arrivalTime" },
"duration": { "path": "duration" },
"status": { "path": "status" },
"price": { "path": "price" },
"action": {
"event": {
"name": "book_flight",
"context": {
"flightNumber": { "path": "flightNumber" },
"origin": { "path": "origin" },
"destination": { "path": "destination" },
"price": { "path": "price" }
}
}
}
}
]
@@ -0,0 +1,320 @@
/**
* Beautiful Chat — LangGraph TypeScript agent backing the flagship showcase cell.
*
* Ported from langgraph-python/src/agents/beautiful_chat.py. The Python version
* uses LangChain's create_agent + CopilotKitMiddleware + StateStreamingMiddleware;
* this port stays closer to the showcase's existing TS pattern (single
* StateGraph with chat + tool_node, CopilotKit state annotation) so it fits the
* kitchen-sink layout already established in graph.ts.
*
* Local tools:
* - query_data — natural-language query over beautiful-chat-data/db.csv
* - manage_todos — create/update todo list with auto-assigned ids
* - get_todos — read current todos from agent state
* - search_flights — fixed-schema A2UI flight search (2 flights)
*
* Dynamic A2UI (`generate_a2ui`) is injected by the runtime
* (a2ui.injectA2UITool: true) rather than declared here — see the tools block.
*
* Data files: ./beautiful-chat-data/db.csv + schemas/flight_schema.json
*/
import { randomUUID } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { z } from "zod";
import { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import type { ToolRunnableConfig } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import {
AIMessage,
SystemMessage,
ToolMessage,
} from "@langchain/core/messages";
import {
Annotation,
Command,
MemorySaver,
START,
StateGraph,
} from "@langchain/langgraph";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
copilotkitEmitState,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
// ---------------------------------------------------------------------------
// 1. Agent state
// ---------------------------------------------------------------------------
const TodoSchema = z.object({
id: z.string().optional(),
title: z.string(),
description: z.string().optional(),
emoji: z.string().optional(),
status: z.enum(["pending", "completed"]).optional(),
});
type Todo = z.infer<typeof TodoSchema>;
const BeautifulChatStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
todos: Annotation<Todo[]>,
});
export type BeautifulChatState = typeof BeautifulChatStateAnnotation.State;
// ---------------------------------------------------------------------------
// 2. Data loading (at module-init time to avoid repeated FS hits)
// ---------------------------------------------------------------------------
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DATA_DIR = path.join(__dirname, "beautiful-chat-data");
let cachedRows: Record<string, string>[] | null = null;
async function loadRows(): Promise<Record<string, string>[]> {
if (cachedRows) return cachedRows;
const csvPath = path.join(DATA_DIR, "db.csv");
const raw = await fs.readFile(csvPath, "utf-8");
const lines = raw.trim().split(/\r?\n/);
const header = lines[0].split(",");
cachedRows = lines.slice(1).map((line) => {
const cols = line.split(",");
const obj: Record<string, string> = {};
header.forEach((h, i) => {
obj[h] = cols[i] ?? "";
});
return obj;
});
return cachedRows;
}
let cachedFlightSchema: unknown[] | null = null;
async function loadFlightSchema(): Promise<unknown[]> {
if (cachedFlightSchema) return cachedFlightSchema;
const schemaPath = path.join(DATA_DIR, "schemas", "flight_schema.json");
const raw = await fs.readFile(schemaPath, "utf-8");
cachedFlightSchema = JSON.parse(raw);
return cachedFlightSchema;
}
// ---------------------------------------------------------------------------
// 3. Tools
// ---------------------------------------------------------------------------
const queryData = tool(
async ({ query: _query }) => {
const rows = await loadRows();
return JSON.stringify(rows);
},
{
name: "query_data",
description:
"Query the database, takes natural language. Always call before showing a chart or graph.",
schema: z.object({
query: z.string().describe("Natural-language query"),
}),
},
);
const manageTodos = tool(
async ({ todos }, config: ToolRunnableConfig) => {
const toolCallId = config.toolCall?.id;
if (typeof toolCallId !== "string" || toolCallId.length === 0) {
throw new Error(
"manage_todos: missing tool_call_id — tool was invoked outside a " +
"ToolNode context.",
);
}
const withIds = todos.map((t) => ({
...t,
id: t.id && t.id.length > 0 ? t.id : randomUUID(),
}));
// Emit state to the frontend immediately so the canvas updates.
// The Command below updates the graph's internal state, but the
// CopilotKit AG-UI pipeline only picks up state from explicit
// copilotkit_emit_state events (like Python's StateStreamingMiddleware
// does). Without this, useAgent().state.todos stays empty.
await copilotkitEmitState(config, { todos: withIds });
return new Command({
update: {
todos: withIds,
messages: [
new ToolMessage({
content: "Successfully updated todos",
name: "manage_todos",
id: randomUUID(),
tool_call_id: toolCallId,
}),
],
},
});
},
{
name: "manage_todos",
description: "Manage the current todos.",
schema: z.object({
todos: z.array(TodoSchema).describe("Array of todo items"),
}),
},
);
const getTodos = tool(
async () => {
// In the Python version, this reads from runtime.state. TS ToolNode doesn't
// pass state to tools by default, so return an empty list; the agent can
// re-fetch via manage_todos semantics.
return JSON.stringify([]);
},
{
name: "get_todos",
description: "Get the current todos.",
schema: z.object({}),
},
);
const CATALOG_ID = "copilotkit://app-dashboard-catalog";
const FLIGHT_SURFACE_ID = "flight-search-results";
// All fields optional (matching Python's total=False) so the LLM/aimock
// fixture can omit auxiliary fields without tripping zod validation.
const FlightSchema = z.object({
airline: z.string().optional(),
airlineLogo: z.string().optional(),
flightNumber: z.string().optional(),
origin: z.string().optional(),
destination: z.string().optional(),
date: z.string().optional(),
departureTime: z.string().optional(),
arrivalTime: z.string().optional(),
duration: z.string().optional(),
status: z.string().optional(),
price: z.string().optional(),
});
const searchFlights = tool(
async ({ flights }) => {
const schema = await loadFlightSchema();
const ops = [
{
version: "v0.9",
createSurface: {
surfaceId: FLIGHT_SURFACE_ID,
catalogId: CATALOG_ID,
},
},
{
version: "v0.9",
updateComponents: {
surfaceId: FLIGHT_SURFACE_ID,
components: schema,
},
},
{
version: "v0.9",
updateDataModel: {
surfaceId: FLIGHT_SURFACE_ID,
path: "/",
value: { flights },
},
},
];
return JSON.stringify({ a2ui_operations: ops });
},
{
name: "search_flights",
description:
"Search for flights and display the results as rich cards. Return exactly 2 flights.",
schema: z.object({
flights: z.array(FlightSchema).describe("Array of flight result objects"),
}),
},
);
// Dynamic A2UI (`generate_a2ui`) is NOT a local tool here — it is injected by
// the CopilotRuntime (`a2ui: { injectA2UITool: true }` on this demo's route).
// The injected tool arrives via `state.copilotkit.actions`, gets bound in
// chatNode through `convertActionsToDynamicStructuredTools`, and is executed by
// the runtime (shouldContinue routes injected-action calls to `__end__`). This
// mirrors langgraph-python's beautiful_chat, where create_agent +
// CopilotKitMiddleware auto-inject the same tool. The fixed-schema
// `search_flights` tool below stays agent-owned and is unaffected by injection.
const tools = [queryData, manageTodos, getTodos, searchFlights];
// ---------------------------------------------------------------------------
// 4. Chat node
// ---------------------------------------------------------------------------
const SYSTEM_PROMPT = `
You are a polished, professional demo assistant. Keep responses to 1-2 sentences.
Tool guidance:
- Flights: call search_flights to show flight cards with a pre-built schema.
- Dashboards & rich UI: call generate_a2ui to create dashboard UIs with metrics,
charts, tables, and cards. It handles rendering automatically.
- Charts: call query_data first, then render with the chart component.
- Todos: enable app mode first, then manage todos.
`;
async function chatNode(state: BeautifulChatState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o",
modelKwargs: { parallel_tool_calls: false },
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const systemMessage = new SystemMessage({ content: SYSTEM_PROMPT });
const response = await modelWithTools.invoke(
[systemMessage, ...state.messages],
config,
);
return { messages: response };
}
// ---------------------------------------------------------------------------
// 5. Routing
// ---------------------------------------------------------------------------
function shouldContinue({ messages, copilotkit }: BeautifulChatState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const toolCallName = lastMessage.tool_calls![0].name;
if (!actions || actions.every((action) => action.name !== toolCallName)) {
return "tool_node";
}
}
return "__end__";
}
// ---------------------------------------------------------------------------
// 6. Compile
// ---------------------------------------------------------------------------
const workflow = new StateGraph(BeautifulChatStateAnnotation)
.addNode("chat_node", chatNode)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,124 @@
/**
* LangGraph TypeScript agent backing the declarative-hashbrown demo.
*
* Port of `langgraph-python/src/agents/byoc_hashbrown_agent.py`.
*
* Emits hashbrown-shaped structured output (`<ui>...</ui>`) that the frontend
* renderer (`src/app/demos/declarative-hashbrown/hashbrown-renderer.tsx`) parses
* progressively via `@hashbrownai/react`.
*
* A minimal single-node StateGraph (no tools) — the system prompt teaches
* the small component catalog exposed by the frontend kit.
*/
import { RunnableConfig } from "@langchain/core/runnables";
import { SystemMessage } from "@langchain/core/messages";
import {
MemorySaver,
START,
END,
StateGraph,
Annotation,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import { CopilotKitStateAnnotation } from "@copilotkit/sdk-js/langgraph";
// `@hashbrownai/react`'s `useJsonParser(content, kit.schema)` expects the
// agent to stream a JSON object literal matching `kit.schema` — NOT the
// `<ui>...</ui>` XML-style examples shown inside `useUiKit({ examples })`.
// Those XML examples are the hashbrown prompt DSL that hashbrown compiles
// into a schema description when driving the LLM directly. Since this demo
// drives the LLM via langgraph, we mirror hashbrown's wire format — a
// `{ ui: [...] }` envelope — instead.
//
// Every node is a single-key object `{ tagName: { props: {...} } }`. Tag
// names and prop schemas match `useSalesDashboardKit()` in
// `hashbrown-renderer.tsx`. `pieChart`/`barChart` receive `data` as a
// JSON-encoded string to keep the schema stable under partial streaming.
const BYOC_HASHBROWN_SYSTEM_PROMPT = `You are a sales analytics assistant that replies by emitting a single JSON
object consumed by a streaming JSON parser on the frontend.
ALWAYS respond with a single JSON object of the form:
{
"ui": [
{ <componentName>: { "props": { ... } } },
...
]
}
Do NOT wrap the response in code fences. Do NOT include any preface or
explanation outside the JSON object. The response MUST be valid JSON.
Available components and their prop schemas:
- "metric": { "props": { "label": string, "value": string } }
A KPI card. \`value\` is a pre-formatted string like "$1.2M" or "248".
- "pieChart": { "props": { "title": string, "data": string } }
A donut chart. \`data\` is a JSON-encoded STRING (embedded JSON) of an
array of {label, value} objects with at least 3 segments, e.g.
"data": "[{\\"label\\":\\"Enterprise\\",\\"value\\":600000}]".
- "barChart": { "props": { "title": string, "data": string } }
A vertical bar chart. \`data\` is a JSON-encoded STRING of an array of
{label, value} objects with at least 3 bars, typically time-ordered.
- "dealCard": { "props": { "title": string, "stage": string, "value": number } }
A single sales deal. \`stage\` MUST be one of: "prospect", "qualified",
"proposal", "negotiation", "closed-won", "closed-lost". \`value\` is a
raw number (no currency symbol or comma).
- "Markdown": { "props": { "children": string } }
Short explanatory text. Use for section headings and brief summaries.
Standard markdown is supported in \`children\`.
Rules:
- Always produce plausible sample data when the user asks for a dashboard or
chart — do not refuse for lack of data.
- Prefer 3-6 rows of data in charts; keep labels short.
- Use "Markdown" for short headings or linking sentences between visual
components. Do not emit long prose.
- Do not emit components that are not listed above.
- \`data\` props on charts MUST be a JSON STRING — escape inner quotes.
Example response (sales dashboard):
{"ui":[{"Markdown":{"props":{"children":"## Q4 Sales Summary"}}},{"metric":{"props":{"label":"Total Revenue","value":"$1.2M"}}},{"metric":{"props":{"label":"New Customers","value":"248"}}},{"pieChart":{"props":{"title":"Revenue by Segment","data":"[{\\"label\\":\\"Enterprise\\",\\"value\\":600000},{\\"label\\":\\"SMB\\",\\"value\\":400000},{\\"label\\":\\"Startup\\",\\"value\\":200000}]"}}},{"barChart":{"props":{"title":"Monthly Revenue","data":"[{\\"label\\":\\"Oct\\",\\"value\\":350000},{\\"label\\":\\"Nov\\",\\"value\\":400000},{\\"label\\":\\"Dec\\",\\"value\\":450000}]"}}}]}
`;
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
});
type AgentState = typeof AgentStateAnnotation.State;
async function chatNode(state: AgentState, config: RunnableConfig) {
// Force JSON-object output mode. The frontend's `useJsonParser` bails
// to `null` on any non-JSON prefix (code fences, prose preamble, etc.),
// so locking the model to JSON at the API layer keeps the wire
// contract honest. Passed via `modelKwargs` so it survives the
// LangChain → OpenAI chat-completions mapping.
const model = makeChatOpenAI(config, {
model: "gpt-4o-mini",
modelKwargs: { response_format: { type: "json_object" } },
});
const response = await model.invoke(
[
new SystemMessage({ content: BYOC_HASHBROWN_SYSTEM_PROMPT }),
...state.messages,
],
config,
);
return { messages: response };
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node")
.addEdge("chat_node", END);
const memory = new MemorySaver();
export const graph = workflow.compile({ checkpointer: memory });
@@ -0,0 +1,180 @@
/**
* LangGraph TypeScript agent backing the declarative-json-render demo.
*
* Port of `langgraph-python/src/agents/byoc_json_render_agent.py`.
*
* Emits a single JSON object shaped like `@json-render/react`'s flat spec
* format (`{ root, elements }`) so the frontend can feed it directly into
* `<Renderer />` against a Zod-validated catalog of three components —
* MetricCard, BarChart, PieChart.
*/
import { RunnableConfig } from "@langchain/core/runnables";
import { SystemMessage } from "@langchain/core/messages";
import {
MemorySaver,
START,
END,
StateGraph,
Annotation,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import { CopilotKitStateAnnotation } from "@copilotkit/sdk-js/langgraph";
const SYSTEM_PROMPT = `You are a sales-dashboard UI generator for a BYOC json-render demo.
When the user asks for a UI, respond with **exactly one JSON object** and
nothing else — no prose, no markdown fences, no leading explanation. The
object must match this schema (the "flat element map" format consumed by
\`@json-render/react\`):
{
"root": "<id of the root element>",
"elements": {
"<id>": {
"type": "<component name>",
"props": { ... component-specific props ... },
"children": [ "<id>", ... ]
},
...
}
}
Available components (use each name verbatim as "type"):
- MetricCard
props: { "label": string, "value": string, "trend": string | null }
Example trend strings: "+12% vs last quarter", "-3% vs last month", null.
- BarChart
props: {
"title": string,
"description": string | null,
"data": [ { "label": string, "value": number }, ... ]
}
- PieChart
props: {
"title": string,
"description": string | null,
"data": [ { "label": string, "value": number }, ... ]
}
Rules:
1. Output **only** valid JSON. No markdown code fences. No text outside
the object.
2. Every id referenced in \`root\` or any \`children\` array must be a key
in \`elements\`.
3. For a multi-component dashboard, use a root MetricCard and list the
charts in its \`children\` array, OR pick any element as root and list
the others as its children. Do not emit orphan elements.
4. Use realistic sales-domain values (revenue, pipeline, conversion,
categories, months) — the demo is a sales dashboard.
5. \`children\` is optional but when present must be an array of strings.
6. Never invent component types outside the three listed above.
### Worked example — "Show me the sales dashboard with metrics and a revenue chart"
{
"root": "revenue-metric",
"elements": {
"revenue-metric": {
"type": "MetricCard",
"props": {
"label": "Revenue (Q3)",
"value": "$1.24M",
"trend": "+18% vs Q2"
},
"children": ["revenue-bar"]
},
"revenue-bar": {
"type": "BarChart",
"props": {
"title": "Monthly revenue",
"description": "Revenue by month across Q3",
"data": [
{ "label": "Jul", "value": 380000 },
{ "label": "Aug", "value": 410000 },
{ "label": "Sep", "value": 450000 }
]
}
}
}
}
### Worked example — "Break down revenue by category as a pie chart"
{
"root": "category-pie",
"elements": {
"category-pie": {
"type": "PieChart",
"props": {
"title": "Revenue by category",
"description": "Share of total revenue by product category",
"data": [
{ "label": "Enterprise", "value": 540000 },
{ "label": "SMB", "value": 310000 },
{ "label": "Self-serve", "value": 220000 },
{ "label": "Partner", "value": 170000 }
]
}
}
}
}
### Worked example — "Show me monthly expenses as a bar chart"
{
"root": "expense-bar",
"elements": {
"expense-bar": {
"type": "BarChart",
"props": {
"title": "Monthly expenses",
"description": "Operating expenses by month",
"data": [
{ "label": "Jul", "value": 210000 },
{ "label": "Aug", "value": 225000 },
{ "label": "Sep", "value": 240000 }
]
}
}
}
}
Respond with the JSON object only.`;
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
});
type AgentState = typeof AgentStateAnnotation.State;
async function chatNode(state: AgentState, config: RunnableConfig) {
// Force JSON-object output mode so the renderer's parseSpec never has
// to parse around prose or code fences. Passed via `modelKwargs` so it
// survives the LangChain → OpenAI chat-completions mapping.
const model = makeChatOpenAI(config, {
model: "gpt-4o-mini",
temperature: 0.2,
modelKwargs: { response_format: { type: "json_object" } },
});
const response = await model.invoke(
[new SystemMessage({ content: SYSTEM_PROMPT }), ...state.messages],
config,
);
return { messages: response };
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node")
.addEdge("chat_node", END);
const memory = new MemorySaver();
export const graph = workflow.compile({ checkpointer: memory });
@@ -0,0 +1,64 @@
/**
* LangGraph TypeScript agent backing the Frontend Tools (Async) demo.
*
* Demonstrates `useFrontendTool` with an ASYNC handler. The frontend
* registers a `query_notes` tool whose handler awaits a simulated
* client-side DB query (500ms latency) and returns matching notes. The
* agent uses the returned result to summarize what it found.
*
* Like the sibling `frontend-tools` cell, the backend graph registers no
* tools of its own — CopilotKit forwards the frontend tool schema(s) to
* the agent at runtime, and the handler executes in the browser.
*/
import { RunnableConfig } from "@langchain/core/runnables";
import { SystemMessage } from "@langchain/core/messages";
import { MemorySaver, START, StateGraph } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
const AgentStateAnnotation = CopilotKitStateAnnotation;
export type AgentState = typeof AgentStateAnnotation.State;
const SYSTEM_PROMPT =
"You are a helpful assistant that can search the user's personal notes. " +
"When the user asks about their notes, call the `query_notes` tool with " +
"a concise keyword extracted from their request. The tool is provided " +
"by the frontend at runtime and runs entirely in the user's browser — " +
"you do not need to implement it yourself. After the tool returns, " +
"summarize the matching notes clearly and concisely. If no notes match, " +
"say so plainly and offer to try a different keyword.";
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o-mini",
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
]);
const response = await modelWithTools.invoke(
[new SystemMessage({ content: SYSTEM_PROMPT }), ...state.messages],
config,
);
return { messages: response };
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node")
.addEdge("chat_node", "__end__");
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,60 @@
/**
* LangGraph TypeScript agent backing the Frontend Tools (In-App Actions) demo.
*
* The demo is about frontend tools — the agent has no custom backend tools.
* CopilotKit forwards the frontend tool schemas to the agent at runtime via
* `state.copilotkit.actions`; the agent binds them when invoking the model,
* and the handler executes in the browser.
*/
// region: setup
import { RunnableConfig } from "@langchain/core/runnables";
import { SystemMessage } from "@langchain/core/messages";
import { MemorySaver, START, StateGraph } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
// CopilotKit forwards frontend tools to the agent via
// `state.copilotkit.actions`. `CopilotKitStateAnnotation` adds that
// channel to your graph's state; `convertActionsToDynamicStructuredTools`
// turns the forwarded action schemas into LangChain tools you can bind
// at model-invocation time.
const AgentStateAnnotation = CopilotKitStateAnnotation;
export type AgentState = typeof AgentStateAnnotation.State;
const SYSTEM_PROMPT = "You are a helpful, concise assistant.";
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o-mini",
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
]);
const response = await modelWithTools.invoke(
[new SystemMessage({ content: SYSTEM_PROMPT }), ...state.messages],
config,
);
return { messages: response };
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node")
.addEdge("chat_node", "__end__");
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
// endregion
@@ -0,0 +1,198 @@
/**
* LangGraph TypeScript agent backing the Gen UI (Agent-based) demo.
*
* Demonstrates explicit agent state + a state-editing tool. The agent
* plans a task as 3 steps and walks each pending -> in_progress ->
* completed, calling `set_steps` after every transition. The frontend
* subscribes to `state.steps` via `useAgent` and renders a live progress
* card.
*
* Ported from `src/agents/gen_ui_agent.py` in the langgraph-python
* sibling package.
*/
import { z } from "zod";
import { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import {
AIMessage,
SystemMessage,
ToolMessage,
} from "@langchain/core/messages";
import type { ToolRunnableConfig } from "@langchain/core/tools";
import {
Annotation,
Command,
MemorySaver,
START,
StateGraph,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
// ---------------------------------------------------------------------------
// 1. Shared state — `steps` is rendered as a live progress card in the UI.
// ---------------------------------------------------------------------------
const StepSchema = z.object({
id: z.string().describe("Unique identifier for the step."),
title: z.string().describe("Short description of the step."),
status: z
.enum(["pending", "in_progress", "completed"])
.describe("Current status of the step."),
});
export type Step = z.infer<typeof StepSchema>;
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
// Last-write-wins reducer: each set_steps call replaces the full list.
steps: Annotation<Step[]>({
reducer: (_prev, next) => (next != null ? next : (_prev ?? [])),
default: () => [],
}),
});
export type AgentState = typeof AgentStateAnnotation.State;
// ---------------------------------------------------------------------------
// 2. Tool — `set_steps` publishes the current plan + step statuses.
//
// Returns a `Command({ update: ... })` so we can BOTH emit a ToolMessage
// (the LLM sees a well-formed tool result on the next turn) AND mutate
// the `steps` channel (the UI re-renders from shared state).
// ---------------------------------------------------------------------------
const setSteps = tool(
async ({ steps }, config: ToolRunnableConfig) => {
const toolCallId = config.toolCall?.id;
if (typeof toolCallId !== "string" || toolCallId.length === 0) {
throw new Error(
"set_steps: missing tool_call_id — tool was invoked outside a " +
"ToolNode context. Refusing to emit a ToolMessage with an empty " +
"tool_call_id (OpenAI rejects those).",
);
}
return new Command({
update: {
steps,
messages: [
new ToolMessage({
status: "success",
name: "set_steps",
tool_call_id: toolCallId,
content: `Published ${steps.length} step(s).`,
}),
],
},
});
},
{
name: "set_steps",
description:
"Publish the current plan + step statuses. Call this every time a " +
"step transitions (including the first enumeration of steps).",
schema: z.object({
steps: z
.array(StepSchema)
.describe("The full list of steps with their current statuses."),
}),
},
);
const tools = [setSteps];
// ---------------------------------------------------------------------------
// 3. System prompt — matches the LGP agent's instruction sequence.
// ---------------------------------------------------------------------------
const SYSTEM_PROMPT =
"You are an agentic planner. For each user request, follow this exact " +
"sequence:\n" +
"1. Plan exactly 3 concrete steps and call `set_steps` ONCE with all " +
'three steps at status="pending".\n' +
'2. Step 1: call `set_steps` with step 1 at status="in_progress", ' +
'then call `set_steps` again with step 1 at status="completed".\n' +
'3. Step 2: call `set_steps` with step 2 at status="in_progress", ' +
'then call `set_steps` again with step 2 at status="completed".\n' +
'4. Step 3: call `set_steps` with step 3 at status="in_progress", ' +
'then call `set_steps` again with step 3 at status="completed".\n' +
"5. Send ONE final conversational assistant message summarizing the " +
"plan, then stop. Do not call any more tools after step 3 is " +
"completed.\n" +
"\n" +
"Rules: never call set_steps in parallel — always wait for one call to " +
"return before the next. After all three steps are completed you MUST " +
"send a final assistant message and terminate.";
// ---------------------------------------------------------------------------
// 4. Chat node.
// ---------------------------------------------------------------------------
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o-mini",
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const response = await modelWithTools.invoke(
[new SystemMessage({ content: SYSTEM_PROMPT }), ...state.messages],
config,
);
return { messages: response };
}
// ---------------------------------------------------------------------------
// 5. Routing — send tool calls to tool_node unless they're CopilotKit
// frontend actions.
// ---------------------------------------------------------------------------
function shouldContinue({ messages, copilotkit }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const toolCallName = lastMessage.tool_calls![0].name;
if (!actions || actions.every((action) => action.name !== toolCallName)) {
return "tool_node";
}
}
return "__end__";
}
// ---------------------------------------------------------------------------
// 6. Compile the graph.
//
// The prompt drives ~7 set_steps cycles + 1 final model turn, so nominal
// cost is ~15 supersteps. recursion_limit=50 gives ~3x headroom for
// retries inside the LLM loop.
// ---------------------------------------------------------------------------
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
recursionLimit: 50,
});
@@ -0,0 +1,240 @@
/**
* LangGraph TypeScript agent — CopilotKit showcase integration
*
* Defines a graph with a chat node and all showcase tools,
* wired to CopilotKit via the sdk-js LangGraph adapter so frontend actions
* and shared state flow seamlessly.
*/
import { z } from "zod";
import { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { AIMessage, SystemMessage } from "@langchain/core/messages";
import {
MemorySaver,
START,
StateGraph,
Annotation,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { getA2UITools } from "@ag-ui/langgraph";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
import {
getWeatherImpl,
queryDataImpl,
manageSalesTodosImpl,
getSalesTodosImpl,
scheduleMeetingImpl,
searchFlightsImpl,
} from "../../shared-tools";
// ---------------------------------------------------------------------------
// 1. Agent state — extends CopilotKit state with a proverbs list
// ---------------------------------------------------------------------------
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
proverbs: Annotation<string[]>,
});
export type AgentState = typeof AgentStateAnnotation.State;
// ---------------------------------------------------------------------------
// 2. Tools — shared implementations wrapped for LangChain
// ---------------------------------------------------------------------------
const getWeather = tool(
async ({ location }) => JSON.stringify(getWeatherImpl(location)),
{
name: "get_weather",
description: "Get current weather for a location",
schema: z.object({
location: z.string().describe("City name"),
}),
},
);
const queryData = tool(
async ({ query }) => JSON.stringify(queryDataImpl(query)),
{
name: "query_data",
description: "Query financial database for chart data",
schema: z.object({
query: z.string().describe("Natural language query"),
}),
},
);
const manageSalesTodos = tool(
async ({ todos }) => JSON.stringify(manageSalesTodosImpl(todos)),
{
name: "manage_sales_todos",
description: "Create or update the sales todo list",
schema: z.object({
todos: z
.array(
z.object({
id: z.string().optional(),
title: z.string(),
stage: z.string().optional(),
value: z.number().optional(),
dueDate: z.string().optional(),
assignee: z.string().optional(),
completed: z.boolean().optional(),
}),
)
.describe("Array of sales todo items"),
}),
},
);
const getSalesTodos = tool(
async ({ currentTodos }) => JSON.stringify(getSalesTodosImpl(currentTodos)),
{
name: "get_sales_todos",
description: "Get the current sales todo list",
schema: z.object({
currentTodos: z
.array(
z.object({
id: z.string().optional(),
title: z.string().optional(),
stage: z.string().optional(),
value: z.number().optional(),
dueDate: z.string().optional(),
assignee: z.string().optional(),
completed: z.boolean().optional(),
}),
)
.optional()
.nullable()
.describe("Current todos if any"),
}),
},
);
const scheduleMeeting = tool(
async ({ reason, durationMinutes }) =>
JSON.stringify(scheduleMeetingImpl(reason, durationMinutes)),
{
name: "schedule_meeting",
description: "Schedule a meeting (requires user approval via HITL)",
schema: z.object({
reason: z.string().describe("Reason for the meeting"),
durationMinutes: z.number().optional().describe("Duration in minutes"),
}),
},
);
const searchFlights = tool(
async ({ flights }) => JSON.stringify(searchFlightsImpl(flights)),
{
name: "search_flights",
description: "Search for available flights",
schema: z.object({
flights: z
.array(
z.object({
airline: z.string(),
airlineLogo: z.string().optional(),
flightNumber: z.string(),
origin: z.string(),
destination: z.string(),
date: z.string(),
departureTime: z.string(),
arrivalTime: z.string(),
duration: z.string(),
status: z.string(),
statusColor: z.string().optional(),
price: z.string(),
currency: z.string().optional(),
}),
)
.describe("Array of flight results"),
}),
},
);
// Dynamic A2UI via the canonical ag-ui factory (same as beautiful_chat /
// a2ui_dynamic). A secondary LLM designs the surface; the factory forces the
// host catalog and emits the a2ui_operations envelope. Replaces the prior
// hand-rolled generate_a2ui tool.
const generateA2ui = getA2UITools({
model: new ChatOpenAI({ model: "gpt-4.1" }),
defaultCatalogId: "copilotkit://app-dashboard-catalog",
});
const tools = [
getWeather,
queryData,
manageSalesTodos,
getSalesTodos,
scheduleMeeting,
searchFlights,
generateA2ui,
];
// ---------------------------------------------------------------------------
// 3. Chat node — binds backend + frontend tools, invokes the model
// ---------------------------------------------------------------------------
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, { temperature: 0, model: "gpt-4o" });
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const systemMessage = new SystemMessage({
content: `You are a helpful assistant. The current proverbs are ${JSON.stringify(state.proverbs)}.`,
});
const response = await modelWithTools.invoke(
[systemMessage, ...state.messages],
config,
);
return { messages: response };
}
// ---------------------------------------------------------------------------
// 4. Routing — send tool calls to tool_node unless they're CopilotKit actions
// ---------------------------------------------------------------------------
function shouldContinue({ messages, copilotkit }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const toolCallName = lastMessage.tool_calls![0].name;
if (!actions || actions.every((action) => action.name !== toolCallName)) {
return "tool_node";
}
}
return "__end__";
}
// ---------------------------------------------------------------------------
// 5. Compile the graph
// ---------------------------------------------------------------------------
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,197 @@
/**
* LangGraph TypeScript agent backing the Headless Chat (Complete) demo.
*
* The cell exists to prove that every CopilotKit rendering surface works
* when the chat UI is composed manually (no <CopilotChatMessageView /> or
* <CopilotChatAssistantMessage />). To exercise those surfaces we give
* this agent:
*
* - three mock backend tools (get_weather, get_stock_price,
* get_revenue_chart) — render via app-registered `useRenderTool`
* renderers on the frontend,
* - access to a frontend-registered `useComponent` tool
* (`highlight_note`) — the agent "calls" it and the UI flows through
* the same `useRenderToolCall` path,
* - MCP Apps wired through the runtime — the agent can invoke Excalidraw
* MCP tools and the middleware emits activity events that
* `useRenderActivityMessage` picks up.
*
* The system prompt nudges the model toward the right surface per user
* question and falls back to plain text otherwise.
*/
import { z } from "zod";
import type { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { AIMessage, SystemMessage } from "@langchain/core/messages";
import {
MemorySaver,
START,
StateGraph,
Annotation,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
const SYSTEM_PROMPT = `You are a helpful, concise assistant wired into a headless chat surface that demonstrates CopilotKit's full rendering stack. Pick the right surface for each user question and fall back to plain text when none of the tools fit.
Routing rules:
- If the user asks about weather for a place, call \`get_weather\` with the location.
- If the user asks about a stock or ticker (AAPL, TSLA, MSFT, ...), call \`get_stock_price\` with the ticker.
- If the user asks for a chart, graph, or visualization of revenue, sales, or other metrics over time, call \`get_revenue_chart\`.
- If the user asks you to highlight, flag, or mark a short note or phrase, call the frontend \`highlight_note\` tool with the text and a color (yellow, pink, green, or blue). Do NOT ask the user for the color — pick a sensible one if they didn't say.
- If the user asks to draw, sketch, or diagram something, use the Excalidraw MCP tools that are available to you.
- Otherwise, reply in plain text.
After a tool returns, write one short sentence summarizing the result. Never fabricate data a tool could provide.`;
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
});
type AgentState = typeof AgentStateAnnotation.State;
const getWeather = tool(
async ({ location }) =>
JSON.stringify({
city: location,
temperature: 68,
humidity: 55,
wind_speed: 10,
conditions: "Sunny",
}),
{
name: "get_weather",
description:
"Get the current weather for a given location. Returns a mock payload with city, temperature in Fahrenheit, humidity, wind speed, and conditions.",
schema: z.object({
location: z.string().describe("City or location name"),
}),
},
);
const getStockPrice = tool(
async ({ ticker }) =>
JSON.stringify({
ticker: ticker.toUpperCase(),
price_usd: 189.42,
change_pct: 1.27,
}),
{
name: "get_stock_price",
description:
"Get a mock current price for a stock ticker. Returns a payload with the ticker symbol (uppercased), price in USD, and percentage change for the day.",
schema: z.object({
ticker: z.string().describe("Stock ticker symbol"),
}),
},
);
const getRevenueChart = tool(
async () =>
JSON.stringify({
title: "Quarterly revenue",
subtitle: "Last six months · USD thousands",
data: [
{ label: "Jan", value: 38 },
{ label: "Feb", value: 47 },
{ label: "Mar", value: 52 },
{ label: "Apr", value: 49 },
{ label: "May", value: 63 },
{ label: "Jun", value: 71 },
],
}),
{
name: "get_revenue_chart",
description:
"Get a mock six-month revenue series for a chart visualization. Returns a title, subtitle, and an array of {label, value} points. Use this whenever the user asks for a chart, graph, or visualization of revenue, sales, or other quarterly/monthly metrics.",
schema: z.object({}),
},
);
const tools = [getWeather, getStockPrice, getRevenueChart];
/**
* Normalize an AIMessage so that tool_calls in additional_kwargs are promoted
* to the top-level tool_calls array. @langchain/openai streaming sometimes
* places tool_calls only in additional_kwargs when the response also carries
* content text, which causes shouldContinue to miss them.
*/
function normalizeResponse(msg: AIMessage): AIMessage {
if (msg.tool_calls?.length) return msg;
const kw = msg.additional_kwargs as {
tool_calls?: Array<{
id?: string;
type?: string;
function?: { name: string; arguments: string };
}>;
};
if (!kw?.tool_calls?.length) return msg;
const toolCalls = kw.tool_calls.map((tc) => ({
name: tc.function?.name ?? "",
args: tc.function?.arguments ? JSON.parse(tc.function.arguments) : {},
id: tc.id,
type: "tool_call" as const,
}));
return new AIMessage({
content: msg.content,
additional_kwargs: msg.additional_kwargs,
tool_calls: toolCalls,
response_metadata: msg.response_metadata,
id: msg.id,
});
}
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o-mini",
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const response = await modelWithTools.invoke(
[new SystemMessage({ content: SYSTEM_PROMPT }), ...state.messages],
config,
);
return { messages: normalizeResponse(response as AIMessage) };
}
function shouldContinue({ messages, copilotkit }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const toolCallName = lastMessage.tool_calls[0].name;
if (!actions || actions.every((action) => action.name !== toolCallName)) {
return "tool_node";
}
}
return "__end__";
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({ checkpointer: memory });
@@ -0,0 +1,91 @@
/**
* LangGraph TypeScript agent backing the In-App HITL (frontend-tool + popup) demo.
*
* The agent is a support assistant that processes customer-care requests
* (refunds, account changes, escalations). Any action that materially
* affects a customer MUST be confirmed by the human operator via the
* frontend-provided `request_user_approval` tool.
*
* The tool is defined on the frontend via `useFrontendTool` with an async
* handler that opens a modal dialog OUTSIDE the chat surface. The handler
* awaits the user's decision and resolves with
* `{"approved": bool, "reason": str}`. This agent treats that result as
* authoritative: if `approved` is `true`, continue; otherwise, stop and
* explain the decision back to the user.
*/
import { RunnableConfig } from "@langchain/core/runnables";
import { SystemMessage } from "@langchain/core/messages";
import { MemorySaver, START, StateGraph } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
const AgentStateAnnotation = CopilotKitStateAnnotation;
export type AgentState = typeof AgentStateAnnotation.State;
const SYSTEM_PROMPT =
"You are a support operations copilot working alongside a human operator " +
"inside an internal support console. The operator can see a list of open " +
"support tickets on the left side of their screen and is chatting with " +
"you on the right.\n" +
"\n" +
"Whenever the operator asks you to take an action that affects a " +
"customer — for example: issuing a refund, updating a customer's plan, " +
"cancelling a subscription, escalating a ticket, or sending an apology " +
"credit — you MUST first call the frontend-provided " +
"`request_user_approval` tool to obtain the operator's explicit consent.\n" +
"\n" +
"How to use `request_user_approval`:\n" +
"- `message`: a short, plain-English summary of the exact action you " +
" are about to take, including concrete numbers (e.g. '$50 refund to " +
" customer #12345').\n" +
"- `context`: optional extra context the operator might want to review " +
" (the ticket ID, the policy rule you're applying, etc.). Keep it to " +
" one or two short sentences.\n" +
"\n" +
"The tool returns an object of the shape " +
'`{"approved": boolean, "reason": string | null}`.\n' +
"- If `approved` is `true`: confirm in one short sentence that you are " +
" processing the action. You do not actually need to call any other " +
" tool — this is a demo. Just acknowledge.\n" +
"- If `approved` is `false`: acknowledge the rejection in one short " +
" sentence and, if `reason` is non-empty, reflect the operator's " +
" reason back to them. Do NOT retry the action.\n" +
"\n" +
"Keep all chat replies to one or two short sentences. Never make up " +
"customer data — always use whatever the operator told you in the " +
"prompt.";
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o-mini",
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
]);
const response = await modelWithTools.invoke(
[new SystemMessage({ content: SYSTEM_PROMPT }), ...state.messages],
config,
);
return { messages: response };
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node")
.addEdge("chat_node", "__end__");
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,58 @@
/**
* LangGraph TypeScript agent backing the In-Chat HITL (useHumanInTheLoop) demo.
*
* The `book_call` tool is defined on the frontend via `useHumanInTheLoop`,
* so there is no backend tool here. CopilotKit forwards the frontend tool
* schemas to the agent at runtime via `state.copilotkit.actions`; the agent
* binds them when invoking the model so the frontend-rendered time-picker
* can resolve the call.
*/
import { RunnableConfig } from "@langchain/core/runnables";
import { SystemMessage } from "@langchain/core/messages";
import { MemorySaver, START, StateGraph } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
const AgentStateAnnotation = CopilotKitStateAnnotation;
export type AgentState = typeof AgentStateAnnotation.State;
const SYSTEM_PROMPT =
"You help users book an onboarding call with the sales team. " +
"When they ask to book a call, call the frontend-provided " +
"`book_call` tool with a short topic and the user's name. " +
"Keep any chat reply to one short sentence.";
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o-mini",
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
]);
const response = await modelWithTools.invoke(
[new SystemMessage({ content: SYSTEM_PROMPT }), ...state.messages],
config,
);
return { messages: response };
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node")
.addEdge("chat_node", "__end__");
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,181 @@
/**
* LangGraph TypeScript agent for the Interrupt-based Generative UI demos.
*
* Defines a backend tool `schedule_meeting(topic, attendee)` that uses
* langgraph's `interrupt()` primitive to pause the run and surface the
* meeting context to the frontend. The frontend `useInterrupt` renderer
* shows a time picker and resolves with `{chosen_time, chosen_label}` or
* `{cancelled: true}`, which this tool turns into a human-readable result.
*
* Ported from `src/agents/interrupt_agent.py` in the langgraph-python package.
*/
// @region[backend-interrupt-tool]
import { z } from "zod";
import type { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import type { AIMessage } from "@langchain/core/messages";
import { SystemMessage } from "@langchain/core/messages";
import {
MemorySaver,
START,
StateGraph,
Annotation,
interrupt,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
// Demo-only fixed timezone offset (Pacific = UTC-7 in PDT, UTC-8 in PST).
// A real app would use the user's calendar + locale; we hardcode Pacific so
// screenshots are stable. Mirrors interrupt_agent.py.
const DEMO_TZ_OFFSET_HOURS = -7; // PDT
interface TimeSlot {
label: string;
iso: string;
}
function candidateSlots(): TimeSlot[] {
const now = new Date();
// "Tomorrow"
const tomorrow = new Date(now);
tomorrow.setDate(now.getDate() + 1);
// "Monday" — at least 2 days away to avoid collisions with "Tomorrow"
const dayOfWeek = now.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
let daysToMonday = (1 - dayOfWeek + 7) % 7;
if (daysToMonday <= 1) daysToMonday += 7;
const nextMonday = new Date(now);
nextMonday.setDate(now.getDate() + daysToMonday);
const fmt = (d: Date, hour: number, minute = 0): string => {
// Build an ISO-like string with the demo timezone offset
const sign = DEMO_TZ_OFFSET_HOURS >= 0 ? "+" : "-";
const absOffset = Math.abs(DEMO_TZ_OFFSET_HOURS);
const hh = String(absOffset).padStart(2, "0");
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00${sign}${hh}:00`;
};
return [
{ label: "Tomorrow 10:00 AM", iso: fmt(tomorrow, 10) },
{ label: "Tomorrow 2:00 PM", iso: fmt(tomorrow, 14) },
{ label: "Monday 9:00 AM", iso: fmt(nextMonday, 9) },
{ label: "Monday 3:30 PM", iso: fmt(nextMonday, 15, 30) },
];
}
const SYSTEM_PROMPT =
"You are a scheduling assistant. Whenever the user asks you to book a " +
"call / schedule a meeting, you MUST call the `schedule_meeting` tool. " +
"Pass a short `topic` describing the purpose and `attendee` describing " +
"who the meeting is with. After the tool returns, confirm briefly " +
"whether the meeting was scheduled and at what time, or that the user " +
"cancelled.";
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
});
export type AgentState = typeof AgentStateAnnotation.State;
const scheduleMeeting = tool(
async ({ topic, attendee }: { topic: string; attendee?: string | null }) => {
// langgraph's `interrupt()` pauses execution and forwards the payload to
// the client. The frontend v2 `useInterrupt` hook renders the picker and
// calls `resolve(...)` with the user's selection, which comes back here.
const response: unknown = interrupt({
topic,
attendee: attendee ?? null,
slots: candidateSlots(),
});
if (response && typeof response === "object") {
const resp = response as Record<string, unknown>;
if (resp.cancelled) {
return `User cancelled. Meeting NOT scheduled: ${topic}`;
}
const chosenLabel =
(resp.chosen_label as string | undefined) ??
(resp.chosen_time as string | undefined);
if (chosenLabel) {
return `Meeting scheduled for ${chosenLabel}: ${topic}`;
}
}
return `User did not pick a time. Meeting NOT scheduled: ${topic}`;
},
{
name: "schedule_meeting",
description:
"Ask the user to pick a time slot for a call, via an in-chat picker.",
schema: z.object({
topic: z
.string()
.describe("Short human-readable description of the call's purpose."),
attendee: z
.string()
.nullable()
.optional()
.describe("Who the call is with (optional)."),
}),
},
);
// @endregion[backend-interrupt-tool]
const tools = [scheduleMeeting];
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o-mini",
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const systemMessage = new SystemMessage({ content: SYSTEM_PROMPT });
const response = await modelWithTools.invoke(
[systemMessage, ...state.messages],
config,
);
return { messages: response };
}
function shouldContinue({ messages, copilotkit }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const toolCallName = lastMessage.tool_calls![0].name;
if (!actions || actions.every((action) => action.name !== toolCallName)) {
return "tool_node";
}
}
return "__end__";
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,38 @@
{
"node_version": "20",
"dockerfile_lines": [],
"dependencies": ["."],
"graphs": {
"sample_agent": "./graph.ts:graph",
"starterAgent": "./graph.ts:graph",
"beautiful_chat": "./beautiful-chat.ts:graph",
"headless_complete": "./headless-complete.ts:graph",
"multimodal": "./multimodal.ts:graph",
"agent_config_agent": "./agent-config.ts:graph",
"agentic-chat-reasoning": "./reasoning-agent.ts:graph",
"reasoning-default-render": "./reasoning-agent.ts:graph",
"tool_rendering": "./tool-rendering.ts:graph",
"tool-rendering-default-catchall": "./tool-rendering.ts:graph",
"tool-rendering-custom-catchall": "./tool-rendering.ts:graph",
"tool-rendering-reasoning-chain": "./tool-rendering-reasoning-chain.ts:graph",
"interrupt_agent": "./interrupt-agent.ts:graph",
"a2ui_dynamic": "./a2ui-dynamic.ts:graph",
"a2ui_fixed": "./a2ui-fixed.ts:graph",
"a2ui_recovery": "./recovery-agent.ts:graph",
"mcp_apps": "./mcp-apps.ts:graph",
"frontend_tools": "./frontend-tools.ts:graph",
"frontend_tools_async": "./frontend-tools-async.ts:graph",
"hitl_in_app": "./hitl-in-app.ts:graph",
"hitl_in_chat": "./hitl-in-chat.ts:graph",
"readonly_state_agent_context": "./readonly-state.ts:graph",
"byoc_hashbrown": "./byoc-hashbrown.ts:graph",
"byoc_json_render": "./byoc-json-render.ts:graph",
"open_gen_ui": "./open-gen-ui.ts:graph",
"open_gen_ui_advanced": "./open-gen-ui-advanced.ts:graph",
"shared_state_read_write": "./shared-state-read-write.ts:graph",
"shared_state_streaming": "./shared-state-streaming.ts:graph",
"subagents": "./subagents.ts:graph",
"gen_ui_agent": "./gen-ui-agent.ts:graph"
},
"env": "../../.env"
}
@@ -0,0 +1,37 @@
// Liveness front-door. This module is the process entry point (see
// entrypoint.sh). It binds an HTTP server on :8124/ok using ONLY `node:http`
// — zero heavy imports — so the watchdog has a valid probe target within
// milliseconds of `node` boot.
//
// ES modules resolve ALL top-level `import` statements before any module-body
// code runs. `@langchain/langgraph-api/server` (pulled in by server.mjs) takes
// ~4m30s to cold-import on Railway (graph eval + tsx transpile). The watchdog
// in entrypoint.sh allows 180s grace + 3×30s strikes = 270s before killing
// the container — roughly 4s short of when a probe embedded in server.mjs
// would actually bind. Kill-loop forever.
//
// Fix: bind :8124 first from a module with no heavy imports, THEN dynamic-
// import server.mjs to kick off the real langgraph bootstrap. Dynamic imports
// are evaluated at call time, not at module load time, so the listen callback
// fires before the heavy graph is touched.
import { createServer } from "node:http";
const PORT = Number(process.env.HEALTH_PORT || 8124);
createServer((req, res) => {
if (req.url === "/ok") {
res.writeHead(200, { "content-type": "application/json" });
res.end('{"status":"ok"}\n');
return;
}
res.writeHead(404);
res.end();
}).listen(PORT, "0.0.0.0", () => {
console.log(`[liveness] probe listening on 0.0.0.0:${PORT}`);
// Defer real server start until AFTER liveness is bound.
import("./server.mjs").catch((err) => {
console.error("[liveness] server.mjs import failed:", err);
process.exit(1);
});
});
@@ -0,0 +1,117 @@
/**
* LangGraph TypeScript agent for the CopilotKit MCP Apps demo.
*
* This agent has no bespoke tools — the CopilotKit runtime is wired with
* `mcpApps: { servers: [...] }` pointing at the public Excalidraw MCP
* server (see `src/app/api/copilotkit-mcp-apps/route.ts`). The runtime
* auto-applies the MCP Apps middleware which exposes the remote MCP
* server's tools to this agent at request time and emits the activity
* events that CopilotKit's built-in `MCPAppsActivityRenderer` renders in
* the chat as a sandboxed iframe.
*
* Ported from `src/agents/mcp_apps_agent.py`.
*
* NOTE: The TS runtime performs MCP tool injection via the A2UI/MCP-Apps
* middleware before the graph sees the request. The graph itself doesn't
* need bespoke MCP client wiring.
*/
import { RunnableConfig } from "@langchain/core/runnables";
import { AIMessage, SystemMessage } from "@langchain/core/messages";
import {
MemorySaver,
START,
END,
StateGraph,
Annotation,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
const SYSTEM_PROMPT = `You draw simple diagrams in Excalidraw via the MCP tool.
SPEED MATTERS. Produce a correct-enough diagram fast; do not optimize
for polish. Target: one tool call, done in seconds.
When the user asks for a diagram:
1. Call \`create_view\` ONCE with 3-5 elements total: shapes + arrows +
an optional title text.
2. Use straightforward shapes (rectangle, ellipse, diamond) with plain
\`label\` fields (\`{"text": "...", "fontSize": 18}\`) on them.
3. Connect with arrows. Endpoints can be element centers or simple
coordinates — you don't need edge anchors / fixedPoint bindings.
4. Include ONE \`cameraUpdate\` at the END of the elements array that
frames the whole diagram. Use an approved 4:3 size (600x450 or
800x600). No opening camera needed.
5. Reply with ONE short sentence describing what you drew.
Every element needs a unique string \`id\` (e.g. \`"b1"\`, \`"a1"\`,
\`"title"\`). Standard sizes: rectangles 160x70, ellipses/diamonds
120x80, 40-80px gap between shapes.
Do NOT:
- Call \`read_me\`. You already know the basic shape API.
- Make multiple \`create_view\` calls.
- Iterate or refine. Ship on the first shot.
- Add decorative colors / fills / zone backgrounds unless the user
explicitly asks for them.
- Add labels on arrows unless crucial.
If the user asks for something specific (colors, more elements,
particular layout), follow their lead — but still in ONE call.`;
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
});
export type AgentState = typeof AgentStateAnnotation.State;
async function chatNode(state: AgentState, config: RunnableConfig) {
// gpt-4o-mini for speed — Excalidraw element emission is simple JSON and
// we're biasing hard toward sub-30s generation.
const model = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o-mini",
});
// The MCP Apps middleware injects MCP tools into state.copilotkit.actions
// alongside any frontend actions, so a single bind picks up everything.
const copilotActions = convertActionsToDynamicStructuredTools(
state.copilotkit?.actions ?? [],
);
const modelWithTools =
copilotActions.length > 0 ? model.bindTools!(copilotActions) : model;
const systemMessage = new SystemMessage({ content: SYSTEM_PROMPT });
const response = await modelWithTools.invoke(
[systemMessage, ...state.messages],
config,
);
return { messages: response };
}
function shouldContinue({ messages }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
// All tool calls are frontend (MCP-injected) actions — never locally
// handled. End the run and let the runtime dispatch them.
return lastMessage.tool_calls?.length ? END : END;
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,159 @@
/**
* Multimodal LangGraph TypeScript agent — accepts image + document (PDF)
* attachments scoped to the `/demos/multimodal` cell.
*
* Uses a *dedicated* vision-capable graph (gpt-4o) so other demos continue
* to use cheaper, text-only models. Inputs forwarded by the runtime:
* - `{"type": "text", "text": "..."}`
* - `{"type": "image", "source": {"type": "data", "value": "<base64>",
* "mimeType": "image/png"}}`
* - `{"type": "document", "source": {"type": "data", "value": "<base64>",
* "mimeType": "application/pdf"}}`
*
* gpt-4o consumes `image` parts natively. For `document` parts (PDFs) we
* extract text server-side via `pdf-parse` and inline it as a text part
* with a clear delimiter — matching the Python reference's `pypdf`-backed
* extraction so the TS multimodal demo reaches feature parity.
*/
import { RunnableConfig } from "@langchain/core/runnables";
import {
AIMessage,
HumanMessage,
SystemMessage,
type BaseMessage,
} from "@langchain/core/messages";
import {
MemorySaver,
START,
StateGraph,
Annotation,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import { CopilotKitStateAnnotation } from "@copilotkit/sdk-js/langgraph";
import pdfParse from "pdf-parse";
const SYSTEM_PROMPT =
"You are a helpful assistant. The user may attach images or documents " +
"(PDFs). When they do, analyze the attachment carefully and answer the " +
"user's question. If no attachment is present, answer the text question " +
"normally. Keep responses concise (1-3 sentences) unless asked to go deep.";
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
});
type AgentState = typeof AgentStateAnnotation.State;
interface ContentPart {
type?: string;
text?: string;
source?: { type?: string; value?: string; mimeType?: string };
[k: string]: unknown;
}
/**
* Rewrite a multimodal content part into a shape the OpenAI chat API
* understands. Images are converted to OpenAI's `image_url` data-URL parts.
* PDF `document` parts have their text extracted server-side via `pdf-parse`
* and inlined. Plain text passes through unchanged.
*/
async function rewritePart(part: unknown): Promise<unknown> {
if (!part || typeof part !== "object") return part;
const p = part as ContentPart;
if (p.type === "image" && p.source?.type === "data") {
const mime = p.source.mimeType ?? "image/png";
const value = p.source.value ?? "";
const dataUrl = value.startsWith("data:")
? value
: `data:${mime};base64,${value}`;
return {
type: "image_url",
image_url: { url: dataUrl },
};
}
if (p.type === "document" && p.source?.type === "data") {
const mime = p.source.mimeType ?? "";
const value = p.source.value ?? "";
if (mime === "application/pdf" && value) {
try {
// Strip data: prefix if present, then base64-decode.
const base64 = value.startsWith("data:")
? value.slice(value.indexOf(",") + 1)
: value;
const buffer = Buffer.from(base64, "base64");
const parsed = await pdfParse(buffer);
const text = parsed.text.trim();
if (text) {
return {
type: "text",
text:
`[Attached PDF (${parsed.numpages} page${parsed.numpages === 1 ? "" : "s"}) — extracted text follows]\n\n` +
text,
};
}
} catch (err) {
// Fall through to the generic marker below on extraction failure.
const message = err instanceof Error ? err.message : String(err);
return {
type: "text",
text: `[Attached PDF — server-side extraction failed: ${message}]`,
};
}
}
return {
type: "text",
text: `[Attached document${mime ? ` (${mime})` : ""}: contents not extracted server-side; describe what you can infer from the filename/type if asked.]`,
};
}
return part;
}
async function rewriteMessages(
messages: BaseMessage[],
): Promise<BaseMessage[]> {
return Promise.all(
messages.map(async (message) => {
if (!(message instanceof HumanMessage)) return message;
const content = message.content;
if (!Array.isArray(content)) return message;
const rewritten = await Promise.all(content.map(rewritePart));
if (
rewritten.length === content.length &&
rewritten.every((part, i) => part === content[i])
) {
return message;
}
return new HumanMessage({
content: rewritten as never,
id: message.id,
});
}),
);
}
async function chatNode(state: AgentState, config: RunnableConfig) {
// gpt-4o is the vision-capable default; temperature kept low for
// deterministic image-Q&A behavior.
const model = makeChatOpenAI(config, { model: "gpt-4o", temperature: 0.2 });
const messages = await rewriteMessages(state.messages);
const response = (await model.invoke(
[new SystemMessage({ content: SYSTEM_PROMPT }), ...messages],
config,
)) as AIMessage;
return { messages: response };
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node")
.addEdge("chat_node", "__end__");
const memory = new MemorySaver();
export const graph = workflow.compile({ checkpointer: memory });
@@ -0,0 +1,113 @@
/**
* LangGraph TypeScript agent backing the Open-Ended Generative UI (Advanced) demo.
*
* Port of `langgraph-python/src/agents/open_gen_ui_advanced_agent.py`.
*
* The "advanced" variant: the agent-authored, sandboxed UI can invoke
* frontend-registered **sandbox functions** — functions the app defines on
* the host page (see `src/app/demos/open-gen-ui-advanced/sandbox-functions.ts`)
* and makes callable from inside the iframe via
* `await Websandbox.connection.remote.<name>(args)`.
*
* The agent itself doesn't define tools. The frontend-registered
* `generateSandboxedUi` tool arrives via `state.copilotkit.actions` and the
* sandbox-function descriptors arrive via `state.copilotkit.context`. The
* system prompt teaches the LLM the sandbox-function calling contract.
*/
import { RunnableConfig } from "@langchain/core/runnables";
import { SystemMessage } from "@langchain/core/messages";
import {
MemorySaver,
START,
END,
StateGraph,
Annotation,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
const SYSTEM_PROMPT = `You are a UI-generating assistant for the Open Generative UI (Advanced) demo.
On every user turn you MUST call the \`generateSandboxedUi\` frontend tool
exactly once. The generated UI must be INTERACTIVE and must invoke the
available host-side sandbox functions described in your agent context
(delivered via \`copilotkit.context\`) in response to user interactions.
Sandbox-function calling contract (inside the generated iframe):
- Call a host function with:
await Websandbox.connection.remote.<functionName>(args)
The call returns a Promise; await it.
- Each handler returns a plain object. Read the return shape from the
function's description in your context and use the EXACT field names
it returns (e.g. if the description says the handler returns
\`{ ok, value }\`, read \`res.value\` — not \`res.result\`).
- Descriptions, names, and JSON-schema parameter shapes for every
available sandbox function are listed in your context. Read them
carefully and wire at least one interactive UI element to call one.
Sandbox iframe restrictions (CRITICAL):
- The iframe runs with \`sandbox="allow-scripts"\` ONLY. Forms are NOT
allowed. You MUST NOT use \`<form>\` elements or \`<button type="submit">\`.
Clicking a submit button inside a sandboxed form is blocked by the
browser BEFORE any onsubmit handler runs, so the sandbox-function call
never fires.
- Use plain \`<button type="button">\` elements and wire them with
\`addEventListener('click', ...)\` or an inline click handler. Do the same
for "Enter" keypresses on inputs: attach a \`keydown\` listener that
checks \`e.key === 'Enter'\` and calls your handler directly — do NOT
wrap inputs in a \`<form>\`.
Generation guidance:
- Emit \`initialHeight\` and \`placeholderMessages\` first, then CSS, then
HTML, then \`jsFunctions\` / \`jsExpressions\` if helpful.
- Always include a visible result element (e.g. an output div) that you
UPDATE after the sandbox function resolves, so the user can *see* the
round-trip: "Button clicked -> remote call -> visible result".
- Use CDN scripts (Chart.js, D3, etc.) via <script> tags in the HTML head
when you need libraries.
- Do NOT use fetch/XHR, localStorage, or document.cookie — the sandbox has
no same-origin access. ONLY use \`Websandbox.connection.remote.*\` for
host-page interactions.
- Keep your own chat message brief (1 sentence max); the rendered UI is
the real output.
`;
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
});
type AgentState = typeof AgentStateAnnotation.State;
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
model: "gpt-4.1",
modelKwargs: { parallel_tool_calls: false },
});
const frontendTools = convertActionsToDynamicStructuredTools(
state.copilotkit?.actions ?? [],
);
const modelWithTools = model.bindTools!(frontendTools);
const response = await modelWithTools.invoke(
[new SystemMessage({ content: SYSTEM_PROMPT }), ...state.messages],
config,
);
return { messages: response };
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node")
.addEdge("chat_node", END);
const memory = new MemorySaver();
export const graph = workflow.compile({ checkpointer: memory });
@@ -0,0 +1,101 @@
/**
* LangGraph TypeScript agent backing the Open-Ended Generative UI demo (minimal).
*
* Port of `langgraph-python/src/agents/open_gen_ui_agent.py`.
*
* The agent does not define its own tools. All the interesting work happens
* outside the agent:
*
* - The frontend-registered `generateSandboxedUi` tool (auto-registered by
* `CopilotKitProvider` when the runtime has `openGenerativeUI` enabled)
* arrives in `state.copilotkit.actions`. `convertActionsToDynamicStructuredTools`
* turns that into a LangChain tool definition bound on the model call.
* - When the LLM calls `generateSandboxedUi`, the runtime's
* `OpenGenerativeUIMiddleware` (enabled via `openGenerativeUI` in the
* Next.js route — see `src/app/api/copilotkit-ogui/route.ts`) converts
* that streaming tool call into `open-generative-ui` activity events that
* the built-in renderer mounts inside a sandboxed iframe.
*
* This is the minimal variant: no sandbox functions, no app-side tools.
*/
import { RunnableConfig } from "@langchain/core/runnables";
import { AIMessage, SystemMessage } from "@langchain/core/messages";
import {
MemorySaver,
START,
END,
StateGraph,
Annotation,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
const SYSTEM_PROMPT = `You are a UI-generating assistant for an Open Generative UI demo
focused on intricate, educational visualisations (3D axes / rotations,
neural-network activations, sorting-algorithm walkthroughs, Fourier
series, wave interference, planetary orbits, etc.).
On every user turn you MUST call the \`generateSandboxedUi\` frontend tool
exactly once. Design a visually polished, self-contained HTML + CSS +
SVG widget that *teaches* the requested concept.
The frontend injects a detailed "design skill" as agent context
describing the palette, typography, labelling, and motion conventions
expected — follow it closely. Key invariants:
- Use inline SVG (or <canvas>) for geometric content, not stacks of <div>s.
- Every axis is labelled; every colour-coded series has a legend.
- Prefer CSS @keyframes / transitions over setInterval; loop cyclical
concepts with animation-iteration-count: infinite.
- Motion must teach — animate the actual step of the concept, not decoration.
- No fetch / XHR / localStorage — the sandbox has no same-origin access.
Output order:
- \`initialHeight\` (typically 480-560 for visualisations) first.
- A short \`placeholderMessages\` array (2-3 lines describing the build).
- \`css\` (complete).
- \`html\` (streams live — keep it tidy). CDN <script> tags for Chart.js /
D3 / etc. go inside the html.
Keep your own chat message brief (1 sentence) — the real output is the
rendered visualisation.
`;
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
});
type AgentState = typeof AgentStateAnnotation.State;
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
model: "gpt-4.1",
modelKwargs: { parallel_tool_calls: false },
});
const frontendTools = convertActionsToDynamicStructuredTools(
state.copilotkit?.actions ?? [],
);
const modelWithTools = model.bindTools!(frontendTools);
const response = await modelWithTools.invoke(
[new SystemMessage({ content: SYSTEM_PROMPT }), ...state.messages],
config,
);
return { messages: response };
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node")
.addEdge("chat_node", END);
const memory = new MemorySaver();
export const graph = workflow.compile({ checkpointer: memory });
@@ -0,0 +1,136 @@
/**
* makeChatOpenAI — construct a ChatOpenAI that forwards inbound x-* headers
* to the outbound OpenAI HTTP call.
*
* Why this exists: @ag-ui/langgraph@0.0.34 stuffs inbound x-* headers into
* `config.configurable.copilotkit_forwarded_headers`, but @langchain/openai
* does not look at that field. Without this helper, aimock never receives
* the x-aimock-context header and every fixture match returns 404. This
* helper is the TS analog of the Python copilotkit SDK's httpx-hook header
* propagation in `copilotkit_lg_middleware.before_agent`.
*
* Apples-to-apples note: this is a minimal LGT backend fix. The harness,
* d6 probe, conversation-runner, and shared frontend remain untouched.
*
* CVDIAG instrumentation (diagnostic only — DOES NOT change forwarding
* behavior): emits structured `CVDIAG` log lines at the configurable-read
* boundary (is the x-aimock-context header present where this layer reads
* forwarded headers off RunnableConfig.configurable?) and at the
* outbound-llm boundary (the defaultHeaders set on the ChatOpenAI call).
* The breadcrumb header `x-diag-hops` gets this layer's hop tag appended,
* and the correlation headers (x-diag-run-id, x-diag-hops) ride along on
* the outbound call the same way x-aimock-context already does. No new
* forwarding source is introduced — headers still come ONLY from
* config.configurable.copilotkit_forwarded_headers.
*/
import { ChatOpenAI, type ChatOpenAIFields } from "@langchain/openai";
import type { RunnableConfig } from "@langchain/core/runnables";
const CVDIAG_COMPONENT = "backend-langgraph-ts";
const CVDIAG_HOP_TAG = "backend-langgraph-ts";
/**
* Emit a single CVDIAG log line in the shared cross-language convention.
* Never logs full header values — only a 12-char prefix of x-aimock-context.
*/
function cvdiag(
boundary: string,
headers: Record<string, string>,
status: "ok" | "miss" | "error",
extra: { hop?: number | string; error?: string } = {},
): void {
const slug = headers["x-aimock-context"];
const headerPresent = typeof slug === "string" && slug.length > 0;
const runId = headers["x-diag-run-id"] ?? "none";
const testId = headers["x-test-id"] ?? "none";
const prefix = headerPresent ? slug.slice(0, 12) : "";
const hop = extra.hop ?? "-";
const error = extra.error ?? "";
console.log(
`CVDIAG component=${CVDIAG_COMPONENT} boundary=${boundary} ` +
`run_id=${runId} slug=${headerPresent ? slug : "MISSING"} ` +
`header_present=${headerPresent} header_value_prefix=${prefix} ` +
`hop=${hop} status=${status} test_id=${testId} error=${error}`,
);
}
function extractForwardedHeaders(
config?: RunnableConfig,
): Record<string, string> {
const raw = (config?.configurable as Record<string, unknown> | undefined)
?.copilotkit_forwarded_headers;
if (!raw || typeof raw !== "object") {
// CVDIAG: the configurable channel had no forwarded-headers object at
// all. This is the alarm we are hunting — surface it instead of the
// previous silent `return {}`.
cvdiag("configurable-read", {}, "miss", {
error: "no-copilotkit_forwarded_headers-in-configurable",
});
return {};
}
const out: Record<string, string> = {};
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
if (typeof v === "string") out[k] = v;
}
// CVDIAG: we found a forwarded-headers object; report whether the
// x-aimock-context header actually rode along on this channel.
const hasContext =
typeof out["x-aimock-context"] === "string" &&
out["x-aimock-context"].length > 0;
cvdiag("configurable-read", out, hasContext ? "ok" : "miss", {
error: hasContext ? "" : "x-aimock-context-absent-in-configurable",
});
return out;
}
/**
* Drop-in replacement for `new ChatOpenAI({...})` inside a graph node.
* Pass the node's `config: RunnableConfig` as the first argument; the rest
* matches the ChatOpenAI constructor.
*/
export function makeChatOpenAI(
config: RunnableConfig | undefined,
opts: ChatOpenAIFields = {},
): ChatOpenAI {
const forwarded = extractForwardedHeaders(config);
const existing = opts.configuration?.defaultHeaders ?? {};
const merged: Record<string, string> = {
...(existing as Record<string, string>),
...forwarded,
};
// CVDIAG breadcrumb: append this layer's hop tag to x-diag-hops on the
// OUTBOUND headers (comma-joined trail each layer extends). This rides
// along ONLY when forwarded headers already carry the diag context — we
// do not invent a new forwarding source.
if (typeof merged["x-aimock-context"] === "string") {
const existingHops = merged["x-diag-hops"];
merged["x-diag-hops"] =
typeof existingHops === "string" && existingHops.length > 0
? `${existingHops},${CVDIAG_HOP_TAG}`
: CVDIAG_HOP_TAG;
}
// CVDIAG: outbound-llm boundary — what is actually about to be attached
// as defaultHeaders on the ChatOpenAI HTTP call.
const hop = (merged["x-diag-hops"] ?? "").split(",").filter(Boolean).length;
const hasContext = typeof merged["x-aimock-context"] === "string";
cvdiag("outbound-llm", merged, hasContext ? "ok" : "miss", {
hop: hasContext ? hop : "-",
error: hasContext ? "" : "x-aimock-context-absent-on-outbound",
});
// Only attach `configuration` when we actually have headers to add, so we
// don't perturb defaults for callers that never pass config.
if (Object.keys(merged).length === 0) {
return new ChatOpenAI(opts);
}
return new ChatOpenAI({
...opts,
configuration: {
...(opts.configuration ?? {}),
defaultHeaders: merged,
},
});
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
{
"name": "langgraph-typescript-agent",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "npx @langchain/langgraph-cli@1.2.1 dev --port 8123 --no-browser",
"start": "node --import tsx liveness.mjs"
},
"dependencies": {
"@copilotkit/sdk-js": "1.61.2",
"@langchain/core": "1.1.44",
"@langchain/langgraph": "1.3.0",
"@langchain/langgraph-api": "1.1.17",
"@langchain/langgraph-checkpoint": "1.0.0",
"@langchain/langgraph-sdk": "1.6.5",
"@langchain/openai": "1.4.4",
"langchain": "1.3.4",
"tsx": "^4.19.3",
"typescript": "^5.6.3",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.9.0"
},
"overrides": {
"@ag-ui/langgraph": "0.0.42"
}
}
@@ -0,0 +1,81 @@
/**
* LangGraph TypeScript agent backing the Shared State (Agent Read-Only) demo.
*
* Demonstrates the `useAgentContext` hook from @copilotkit/react-core/v2:
* the frontend provides READ-ONLY context *to* the agent. This is the
* reverse direction of writable-shared-state — the UI cannot be edited by
* the agent, but the agent reads this context on every turn via
* CopilotKit's state forwarding, which routes the context entries into the
* model's message history.
*
* No custom state, no tools: this is the minimal shape of the
* useAgentContext pattern. The agent just reads whatever context the
* frontend registered and answers accordingly.
*/
import type { RunnableConfig } from "@langchain/core/runnables";
import { SystemMessage } from "@langchain/core/messages";
import { MemorySaver, START, StateGraph } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import { CopilotKitStateAnnotation } from "@copilotkit/sdk-js/langgraph";
const AgentStateAnnotation = CopilotKitStateAnnotation;
export type AgentState = typeof AgentStateAnnotation.State;
const SYSTEM_PROMPT =
"You are a helpful, concise assistant. The frontend may provide " +
"read-only context about the user (e.g. name, timezone, recent " +
"activity) via the `useAgentContext` hook. Always consult that " +
"context when it is relevant — address the user by name if known, " +
"respect their timezone when mentioning times, and reference " +
"recent activity when it helps you answer. Keep responses short.";
// @region[agent-context-setup]
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, { model: "gpt-5.4" });
// Inject read-only context from useAgentContext / useCopilotReadable.
// Mirrors the `createAppContextBeforeAgent` logic in CopilotKitMiddleware:
// context may be a string or an object — stringify it and prepend as a
// system message right after the main system prompt.
const appContext = state.copilotkit?.context;
const isEmptyContext =
!appContext ||
(typeof appContext === "string" && appContext.trim() === "") ||
(typeof appContext === "object" && Object.keys(appContext).length === 0);
const systemMessages: SystemMessage[] = [
new SystemMessage({ content: SYSTEM_PROMPT }),
];
if (!isEmptyContext) {
const contextContent =
typeof appContext === "string"
? appContext
: JSON.stringify(appContext, null, 2);
systemMessages.push(
new SystemMessage({ content: `App Context:\n${contextContent}` }),
);
}
const response = await model.invoke(
[...systemMessages, ...state.messages],
config,
);
return { messages: response };
}
// @endregion[agent-context-setup]
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node")
.addEdge("chat_node", "__end__");
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,77 @@
/**
* Reasoning agent — minimal ReAct-style agent showcase.
*
* Shared by agentic-chat-reasoning (custom amber ReasoningBlock) and
* reasoning-default-render (CopilotKit's built-in reasoning slot).
*
* TypeScript port of reasoning_agent.py. The Python version relies on
* `deepagents.create_deep_agent` to surface a reasoning chain via an
* internal planner tool; TS has no drop-in equivalent.
*
* To make the AG-UI `ReasoningMessage` slot light up in the TS variant
* we route through a reasoning-capable OpenAI model via the Responses
* API. `@langchain/openai` surfaces the model's thinking tokens as a
* distinct content block that the CopilotKit runtime translates to a
* `role: "reasoning"` AG-UI event — which both reasoning demos render.
*
* Falls back to gpt-4o-mini (no reasoning stream) if `OPENAI_REASONING_MODEL`
* is unset, so local dev without a reasoning-tier key still works (reasoning
* slot just stays empty in that case).
*
* Note: we use a custom StateGraph rather than `createReactAgent` so that the
* per-invocation `config` (with `copilotkit_forwarded_headers`) reaches the
* `ChatOpenAI` construction — required for `x-aimock-context` propagation.
*/
import { RunnableConfig } from "@langchain/core/runnables";
import { SystemMessage, AIMessage } from "@langchain/core/messages";
import {
Annotation,
MemorySaver,
START,
StateGraph,
messagesStateReducer,
BaseMessage,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
const SYSTEM_PROMPT =
"You are a helpful assistant. For each user question, first think " +
"step-by-step about the approach, then give a concise answer.";
const REASONING_MODEL = process.env.OPENAI_REASONING_MODEL ?? "gpt-5-mini";
const AgentStateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: messagesStateReducer,
default: () => [],
}),
});
type AgentState = typeof AgentStateAnnotation.State;
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
model: REASONING_MODEL,
useResponsesApi: true,
reasoning: { effort: "low", summary: "auto" },
});
const response = await model.invoke(
[new SystemMessage({ content: SYSTEM_PROMPT }), ...state.messages],
config,
);
return { messages: response };
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addEdge(START, "chat_node");
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,63 @@
/**
* LangGraph TypeScript agent for the A2UI Error Recovery demo (OSS-158 / OSS-375).
*
* Same dynamic-schema A2UI setup as `a2ui-dynamic.ts` (declarative-gen-ui), but
* with the toolkit's validate->retry recovery loop made *visible*. The two
* aimock pills drive the inner `render_a2ui` sub-agent two ways:
* - HEAL pill: the model emits FREE-FORM / sloppy A2UI args (components and
* data as JSON strings) — the toolkit heals them via parse_and_fix into a
* valid surface in a single pass, which paints.
* - EXHAUST pill: every attempt is structurally invalid (the root references a
* missing child), so the validate->retry loop hits the cap and the tool
* returns the `a2ui_recovery_exhausted` hard-fail envelope, which the
* renderer surfaces as a tasteful `failed` state (no broken surface).
*
* Backend-owned wiring: unlike the declarative-gen-ui demo (which relies on the
* CopilotKit runtime auto-injecting `generate_a2ui`), this agent OWNS the tool
* via `@ag-ui/langgraph` `getA2UITools`, whose body runs the `render_a2ui`
* sub-agent + the toolkit recovery loop IN-GRAPH. The dedicated route sets
* `injectA2UITool: false` so the runtime does not inject a second copy.
*
* Mirrors `showcase/integrations/langgraph-python/src/agents/recovery_agent.py`.
* Catalog is reused from declarative-gen-ui ("declarative-gen-ui-catalog"); the
* Vantage Threads sales dataset + composition rules arrive from the frontend via
* App Context (declarative-gen-ui/sales-context.ts).
*/
import { createAgent } from "langchain";
import { copilotkitMiddleware } from "@copilotkit/sdk-js/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { getA2UITools } from "@ag-ui/langgraph";
import type { A2UIAttemptRecord } from "@ag-ui/langgraph";
const SYSTEM_PROMPT =
"You are the embedded sales analyst for Vantage Threads, the fictional B2B " +
"apparel company described in your App Context. Answer every business " +
"question by calling `generate_a2ui` to draw a rich visual surface, and keep " +
"the chat reply to one short sentence. Ground every number in the sales " +
"dataset from your App Context. `generate_a2ui` handles the rendering — and " +
"its automatic recovery — for you.";
const a2uiTool = getA2UITools({
model: new ChatOpenAI({ model: "gpt-4.1" }),
defaultCatalogId: "declarative-gen-ui-catalog",
// Recovery loop runs by default; pinned here so the renderer's "Retrying…
// (N/M)" label matches the adapter's cap.
recovery: { maxAttempts: 3 },
onA2UIAttempt: (rec: A2UIAttemptRecord) => {
// Dev observability: each attempt (incl. rejected ones) is logged.
// eslint-disable-next-line no-console
console.log(
`[a2ui recovery] attempt ${rec.attempt}: ${rec.ok ? "valid" : "invalid"}`,
rec.errors,
);
},
});
export const graph = createAgent({
model: new ChatOpenAI({ model: "gpt-4.1" }),
// Cast: tool typed against @ag-ui/langgraph's own @langchain/core peer.
tools: [a2uiTool as any],
middleware: [copilotkitMiddleware],
systemPrompt: SYSTEM_PROMPT,
});
@@ -0,0 +1,128 @@
// LangGraph agent — production server (no CLI, no file watch, no Studio IPC).
//
// Dynamic-imported by liveness.mjs (the process entry point) AFTER :8124/ok
// is bound. The run invocation is `node --import tsx liveness.mjs`; tsx is
// used ONLY as a one-shot ESM import hook so subsequent imports (this file,
// graph.ts) resolve without recompilation per-request. Equivalent to
// langgraph-cli dev for the runs,
// threads, assistants, ok, and store surface but without:
//
// - chokidar file watcher that recompiles on any .ts change,
// - Studio auto-open and IPC handshake to smith.langchain.com,
// - spawn.mjs parent/child process with `tsx watch` wrapping the server
// (per dev.mjs to spawn.mjs to tsx cli.mjs watch entrypoint.mjs),
// - dev-mode NODE_ENV=development forcing of LangSmith tenant ID lookup.
//
// Also pre-warms the in-memory schema cache at boot, mirroring what the
// official prod image's build.mts does at image build time — so the first
// assistants schemas request hits the cache instead of kicking off a cold
// TS worker compile.
import process from "node:process";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { startServer } from "@langchain/langgraph-api/server";
import { getStaticGraphSchema } from "@langchain/langgraph-api/schema";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Liveness is now bound by liveness.mjs BEFORE this module is dynamic-imported
// — see showcase/integrations/langgraph-typescript/src/agent/liveness.mjs. The
// previous sibling-probe implementation inside this file did not work: ES
// module semantics resolve all top-level `import` statements before any
// module-body code runs, so `import { startServer } from "@langchain/
// langgraph-api/server"` (~4m30s cold on Railway) ran BEFORE the probe got a
// chance to listen, and the watchdog's 180s + 3×30s = 270s budget killed the
// container ~4s before the probe would have come up. By the time any code in
// this file executes, :8124/ok is already serving 200.
// Graph spec mirrors langgraph.json. We pin the compiled .js entry because the
// production parser walks the source file with the TypeScript API; pointing at
// dist/graph.js still works because the parser reads whatever path we hand it,
// but the runtime import lives in the same location so no tsx is required.
// IMPORTANT: This must stay in sync with langgraph.json. Every graph
// the Next.js API routes reference must be registered here, otherwise the
// LangGraph server returns 404 on /runs/stream and the runtime surfaces
// net::ERR_ABORTED to the browser.
const graphSpec = {
starterAgent: "./graph.ts:graph",
beautiful_chat: "./beautiful-chat.ts:graph",
headless_complete: "./headless-complete.ts:graph",
multimodal: "./multimodal.ts:graph",
agent_config_agent: "./agent-config.ts:graph",
"agentic-chat-reasoning": "./reasoning-agent.ts:graph",
"reasoning-default-render": "./reasoning-agent.ts:graph",
tool_rendering: "./tool-rendering.ts:graph",
"tool-rendering-default-catchall": "./tool-rendering.ts:graph",
"tool-rendering-custom-catchall": "./tool-rendering.ts:graph",
"tool-rendering-reasoning-chain": "./tool-rendering-reasoning-chain.ts:graph",
interrupt_agent: "./interrupt-agent.ts:graph",
a2ui_dynamic: "./a2ui-dynamic.ts:graph",
a2ui_fixed: "./a2ui-fixed.ts:graph",
a2ui_recovery: "./recovery-agent.ts:graph",
mcp_apps: "./mcp-apps.ts:graph",
frontend_tools: "./frontend-tools.ts:graph",
frontend_tools_async: "./frontend-tools-async.ts:graph",
hitl_in_app: "./hitl-in-app.ts:graph",
hitl_in_chat: "./hitl-in-chat.ts:graph",
readonly_state_agent_context: "./readonly-state.ts:graph",
byoc_hashbrown: "./byoc-hashbrown.ts:graph",
byoc_json_render: "./byoc-json-render.ts:graph",
open_gen_ui: "./open-gen-ui.ts:graph",
open_gen_ui_advanced: "./open-gen-ui-advanced.ts:graph",
shared_state_read_write: "./shared-state-read-write.ts:graph",
shared_state_streaming: "./shared-state-streaming.ts:graph",
subagents: "./subagents.ts:graph",
gen_ui_agent: "./gen-ui-agent.ts:graph",
};
// Pre-warm schema cache before we accept traffic. This is what the official
// prod image's build.mts does at image-build time. We do it once at boot —
// same net effect, but cheap enough to run in-process during startup.
async function prewarmSchemas(cwd) {
const specs = Object.fromEntries(
Object.entries(graphSpec).map(([id, spec]) => {
const [userFile, exportSymbol] = spec.split(":", 2);
return [id, { sourceFile: path.resolve(cwd, userFile), exportSymbol }];
}),
);
const start = Date.now();
try {
await getStaticGraphSchema(specs, { timeoutMs: 60_000 });
console.log(`[server] Pre-warmed schemas in ${Date.now() - start}ms`);
} catch (err) {
// Non-fatal: server still starts; first /schemas call will trigger cold
// extraction as the dev-mode path does today.
console.warn(
`[server] Pre-warm failed (non-fatal): ${err?.message ?? err}`,
);
}
}
async function main() {
const cwd = __dirname; // src/agent
const port = Number(process.env.PORT ?? 8123);
const host = process.env.HOST ?? "0.0.0.0";
// Kick off pre-warm and startServer in parallel. startServer registers
// graphs synchronously and binds the port — /ok is live before schemas
// finish warming.
const [{ host: serverHost }] = await Promise.all([
startServer({
port,
nWorkers: Number(process.env.N_WORKERS ?? 10),
host,
cwd,
graphs: graphSpec,
}),
prewarmSchemas(cwd),
]);
console.log(`[server] LangGraph API listening on ${serverHost}`);
}
main().catch((err) => {
console.error("[server] Fatal:", err);
process.exit(1);
});
@@ -0,0 +1,233 @@
/**
* LangGraph TypeScript agent backing the Shared State (Read + Write) demo.
*
* Demonstrates the full bidirectional shared-state pattern between UI and
* agent:
*
* - **UI -> agent (write)**: The UI owns a `preferences` object (the user's
* profile) that it writes into agent state via `agent.setState(...)`. The
* chat node reads those preferences every turn and injects them into the
* system prompt, so the LLM adapts accordingly.
* - **agent -> UI (read)**: The agent can call `set_notes` to update a
* `notes` slot in shared state via a `Command({ update: ... })`. The UI
* reflects every update in real time via `useAgent(...)`.
*
* Together this shows the canonical LangGraph TypeScript bidirectional
* shared state: frontend writes, backend reads AND writes, frontend
* re-renders.
*
* Ported from `src/agents/shared_state_read_write.py` in the
* langgraph-python sibling package.
*/
import { z } from "zod";
import type { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import type { AIMessage } from "@langchain/core/messages";
import { SystemMessage, ToolMessage } from "@langchain/core/messages";
import type { ToolRunnableConfig } from "@langchain/core/tools";
import {
Annotation,
Command,
MemorySaver,
START,
StateGraph,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
// ---------------------------------------------------------------------------
// 1. Shared state — bidirectional channel between UI and agent.
//
// `preferences` is WRITTEN by the UI via `agent.setState(...)` and READ by
// the chat node every turn (it's injected into the system prompt).
//
// `notes` is WRITTEN by the agent via the `set_notes` tool's `Command`
// update, and READ by the UI via `useAgent({ updates: [OnStateChanged] })`.
// ---------------------------------------------------------------------------
// @region[shared-state-setup]
export interface Preferences {
name?: string;
tone?: "formal" | "casual" | "playful";
language?: string;
interests?: string[];
}
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
preferences: Annotation<Preferences | undefined>,
notes: Annotation<string[]>,
});
export type AgentState = typeof AgentStateAnnotation.State;
// @endregion[shared-state-setup]
// ---------------------------------------------------------------------------
// 2. Tool — `set_notes` writes the agent-authored notes slot.
//
// Returns a `Command({ update: ... })` so we can BOTH emit a ToolMessage
// (the LLM sees a well-formed tool result on the next turn) AND mutate
// the `notes` channel (the UI re-renders from shared state).
// ---------------------------------------------------------------------------
// @region[set-notes-tool]
const setNotes = tool(
async ({ notes }, config: ToolRunnableConfig) => {
const toolCallId = config.toolCall?.id;
if (typeof toolCallId !== "string" || toolCallId.length === 0) {
throw new Error(
"set_notes: missing tool_call_id — tool was invoked outside a " +
"ToolNode context. Refusing to emit a ToolMessage with an empty " +
"tool_call_id (OpenAI rejects those).",
);
}
return new Command({
update: {
notes,
messages: [
new ToolMessage({
status: "success",
name: "set_notes",
tool_call_id: toolCallId,
content: "Notes updated.",
}),
],
},
});
},
{
name: "set_notes",
description:
"Replace the notes array in shared state with the full updated list. " +
"Use this tool whenever the user asks you to 'remember' something, or " +
"when you have an observation about the user worth surfacing in the " +
"UI's notes panel. Always pass the FULL notes list (existing notes + " +
"any new ones), not a diff. Keep each note short (< 120 chars).",
schema: z.object({
notes: z
.array(z.string())
.describe("The full updated notes list (replaces previous value)."),
}),
},
);
// @endregion[set-notes-tool]
const tools = [setNotes];
// ---------------------------------------------------------------------------
// 3. Preferences-injecting chat node.
//
// Equivalent to the Python `PreferencesInjectorMiddleware`: every turn we
// read the latest `preferences` from agent state and prepend a
// SystemMessage that tells the LLM about them. This is how UI-written
// state becomes visible to the agent.
// ---------------------------------------------------------------------------
const BASE_SYSTEM_PROMPT =
"You are a helpful, concise assistant. " +
"The user's preferences are supplied via shared state and will be " +
"added as a system message at the start of every turn. Always " +
"respect them. " +
"When the user asks you to remember something, or when you observe " +
"something worth surfacing in the UI, call `set_notes` with the " +
"FULL updated list of short note strings (existing notes + new).";
// @region[preferences-injector]
function buildPreferencesMessage(
prefs: Preferences | undefined,
): SystemMessage | null {
if (!prefs) return null;
const lines: string[] = [];
if (prefs.name) lines.push(`- Name: ${prefs.name}`);
if (prefs.tone) lines.push(`- Preferred tone: ${prefs.tone}`);
if (prefs.language) lines.push(`- Preferred language: ${prefs.language}`);
if (prefs.interests && prefs.interests.length > 0) {
lines.push(`- Interests: ${prefs.interests.join(", ")}`);
}
if (lines.length === 0) return null;
return new SystemMessage({
content: [
"The user has shared these preferences with you:",
...lines,
"Tailor every response to these preferences. Address the user by " +
"name when appropriate.",
].join("\n"),
});
}
// @endregion[preferences-injector]
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o-mini",
modelKwargs: { parallel_tool_calls: false },
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const baseSystem = new SystemMessage({ content: BASE_SYSTEM_PROMPT });
const prefsMessage = buildPreferencesMessage(state.preferences);
const systemMessages = prefsMessage
? [baseSystem, prefsMessage]
: [baseSystem];
const response = await modelWithTools.invoke(
[...systemMessages, ...state.messages],
config,
);
return { messages: response };
}
// ---------------------------------------------------------------------------
// 4. Routing — send tool calls to tool_node unless they're CopilotKit
// frontend actions.
// ---------------------------------------------------------------------------
function shouldContinue({ messages, copilotkit }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const hasBackendToolCall = lastMessage.tool_calls.some((toolCall) => {
return (
!actions || actions.every((action) => action.name !== toolCall.name)
);
});
if (hasBackendToolCall) {
return "tool_node";
}
}
return "__end__";
}
// ---------------------------------------------------------------------------
// 5. Compile the graph.
// ---------------------------------------------------------------------------
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,182 @@
/**
* LangGraph TypeScript agent backing the Shared State Streaming demo.
*
* Demonstrates per-token state-delta streaming. The agent writes a long
* `document` string into shared agent state via a `write_document` tool;
* `copilotkitCustomizeConfig(..., { emitIntermediateState })` tells
* CopilotKit to forward every token of the tool's `document` argument
* directly into the `document` state key as it is generated. The UI
* (useAgent) sees `state.document` grow token-by-token, without waiting
* for the tool call to finish.
*
* This is the canonical per-token state-streaming pattern:
* docs.copilotkit.ai/integrations/langgraph/shared-state/predictive-state-updates
*/
import { randomUUID } from "node:crypto";
import { z } from "zod";
import type { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import type { AIMessage } from "@langchain/core/messages";
import { SystemMessage, ToolMessage } from "@langchain/core/messages";
import type { ToolRunnableConfig } from "@langchain/core/tools";
import {
Annotation,
Command,
MemorySaver,
START,
StateGraph,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
copilotkitCustomizeConfig,
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
// ---------------------------------------------------------------------------
// 1. Shared state — `document` is streamed token-by-token.
// ---------------------------------------------------------------------------
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
document: Annotation<string>,
});
export type AgentState = typeof AgentStateAnnotation.State;
// ---------------------------------------------------------------------------
// 2. Tool — `write_document` writes the document into shared state.
// ---------------------------------------------------------------------------
const writeDocument = tool(
async ({ document }, config: ToolRunnableConfig) => {
const toolCallId = config.toolCall?.id;
if (typeof toolCallId !== "string" || toolCallId.length === 0) {
throw new Error(
"write_document: missing tool_call_id — tool was invoked outside a " +
"ToolNode context. Refusing to emit a ToolMessage with an empty " +
"tool_call_id (OpenAI rejects those).",
);
}
return new Command({
update: {
document,
messages: [
new ToolMessage({
content: "Document written to shared state.",
name: "write_document",
id: randomUUID(),
tool_call_id: toolCallId,
}),
],
},
});
},
{
name: "write_document",
description:
"Write a document for the user.\n\n" +
"Always call this tool when the user asks you to write or draft " +
"something of any length (an essay, poem, email, summary, etc.). " +
"The `document` argument is streamed *per token* into shared agent " +
"state under the `document` key, so the UI can render it as it is " +
"generated.",
schema: z.object({
document: z.string(),
}),
},
);
const tools = [writeDocument];
// ---------------------------------------------------------------------------
// 3. Chat node.
// ---------------------------------------------------------------------------
const SYSTEM_PROMPT =
"You are a collaborative writing assistant. Whenever the user asks " +
"you to write, draft, or revise any piece of text, ALWAYS call the " +
"`write_document` tool with the full content as a single string in " +
"the `document` argument. Never paste the document into a chat " +
"message directly — the document belongs in shared state and the " +
"UI renders it live as you type.";
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
model: "gpt-5.4",
modelKwargs: { parallel_tool_calls: false },
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const systemMessage = new SystemMessage({ content: SYSTEM_PROMPT });
// @region[state-streaming-middleware]
const streamingConfig = copilotkitCustomizeConfig(config, {
emitIntermediateState: [
{
stateKey: "document",
tool: "write_document",
toolArgument: "document",
},
],
});
const response = await modelWithTools.invoke(
[systemMessage, ...state.messages],
streamingConfig,
);
// @endregion[state-streaming-middleware]
return { messages: response };
}
// ---------------------------------------------------------------------------
// 4. Routing — send tool calls to tool_node unless they're CopilotKit
// frontend actions.
// ---------------------------------------------------------------------------
function shouldContinue({ messages, copilotkit }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const hasBackendToolCall = lastMessage.tool_calls.some((toolCall) => {
return (
!actions || actions.every((action) => action.name !== toolCall.name)
);
});
if (hasBackendToolCall) {
return "tool_node";
}
}
return "__end__";
}
// ---------------------------------------------------------------------------
// 5. Compile the graph.
// ---------------------------------------------------------------------------
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,371 @@
/**
* LangGraph TypeScript agent backing the Sub-Agents demo.
*
* Demonstrates multi-agent delegation with a visible delegation log.
*
* A top-level "supervisor" LLM orchestrates three specialized sub-agents,
* exposed as tools:
*
* - `research_agent` — gathers facts
* - `writing_agent` — drafts prose
* - `critique_agent` — reviews drafts
*
* Each sub-agent is a small purpose-built `ChatOpenAI` invocation with
* its own system prompt. Every delegation appends an entry to the
* `delegations` slot in shared agent state so the UI can render a live
* "delegation log" as the supervisor fans work out and collects results.
*
* Ported from `src/agents/subagents.py` in the langgraph-python sibling
* package.
*/
// @region[supervisor-delegation-tools]
// @region[subagent-setup]
import { randomUUID } from "node:crypto";
import { z } from "zod";
import { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import type { ToolRunnableConfig } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import {
AIMessage,
HumanMessage,
SystemMessage,
ToolMessage,
} from "@langchain/core/messages";
import {
Annotation,
Command,
MemorySaver,
START,
StateGraph,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
// ---------------------------------------------------------------------------
// 1. Shared state — `delegations` is rendered as a live log in the UI.
// ---------------------------------------------------------------------------
export type SubAgentName =
| "research_agent"
| "writing_agent"
| "critique_agent";
export interface Delegation {
id: string;
sub_agent: SubAgentName;
task: string;
status: "running" | "completed" | "failed";
result: string;
}
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
// Use a list-extending reducer so parallel tool_calls in a single
// assistant turn don't clobber each other. Without this, each tool
// callback's Command runs against the same task-input snapshot, and the
// channel reducer (last-write-wins by default) silently drops every
// delegation but one.
delegations: Annotation<Delegation[]>({
reducer: (a, b) => [...(a ?? []), ...(b ?? [])],
default: () => [],
}),
});
export type AgentState = typeof AgentStateAnnotation.State;
// ---------------------------------------------------------------------------
// 2. Sub-agents (small purpose-built LLM invocations).
//
// Each sub-agent has its own system prompt and is invoked synchronously
// from inside the matching supervisor tool. They don't share memory or
// tools with the supervisor — the supervisor only sees their return
// value.
// ---------------------------------------------------------------------------
const SUB_AGENT_PROMPTS: Record<SubAgentName, string> = {
research_agent:
"You are a research sub-agent. Given a topic, produce a concise " +
"bulleted list of 3-5 key facts. No preamble, no closing.",
writing_agent:
"You are a writing sub-agent. Given a brief and optional source " +
"facts, produce a polished 1-paragraph draft. Be clear and " +
"concrete. No preamble.",
critique_agent:
"You are an editorial critique sub-agent. Given a draft, give " +
"2-3 crisp, actionable critiques. No preamble.",
};
async function invokeSubAgent(
agent: SubAgentName,
task: string,
config?: RunnableConfig,
): Promise<string> {
const subModel = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o-mini",
});
const result = await subModel.invoke([
new SystemMessage({ content: SUB_AGENT_PROMPTS[agent] }),
new HumanMessage({ content: task }),
]);
const content = (result as AIMessage).content;
if (typeof content === "string") return content;
// Content is sometimes a list of parts — flatten any text parts.
if (Array.isArray(content)) {
return content
.map((part) =>
typeof part === "string"
? part
: "text" in (part as Record<string, unknown>)
? String((part as { text?: unknown }).text ?? "")
: "",
)
.join("");
}
return String(content ?? "");
}
// @endregion[subagent-setup]
// ---------------------------------------------------------------------------
// 3. Helper — emit a single delegation entry plus a ToolMessage.
//
// The `delegations` channel uses a list-extending reducer (see
// AgentStateAnnotation above) so each Command emits ONLY the new entry —
// parallel tool_calls in one assistant turn each contribute their entry
// and the reducer concatenates them. Emitting the full list here would
// cause duplicates under the new reducer.
// ---------------------------------------------------------------------------
function delegationUpdate(
subAgent: SubAgentName,
task: string,
result: string,
toolCallId: string,
status: "completed" | "failed" = "completed",
): Command {
const entry: Delegation = {
id: randomUUID(),
sub_agent: subAgent,
task,
status,
result,
};
return new Command({
update: {
delegations: [entry],
messages: [
new ToolMessage({
status: status === "completed" ? "success" : "error",
name: subAgent,
tool_call_id: toolCallId,
content: result,
}),
],
},
});
}
// Run a sub-agent and return either its output or a scrubbed failure
// message. A thrown error inside a delegation tool would otherwise
// propagate and crash the supervisor turn — the user sees a runtime
// error and no `failed` entry ever lands in the delegation log. Catch
// here so the supervisor can keep working and the UI can render the
// failed delegation just like a successful one.
async function runSubAgentSafely(
agent: SubAgentName,
task: string,
config?: RunnableConfig,
): Promise<{ ok: true; result: string } | { ok: false; result: string }> {
try {
const result = await invokeSubAgent(agent, task, config);
return { ok: true, result };
} catch (err) {
const errName = err instanceof Error ? err.constructor.name : typeof err;
console.error(`[subagents] ${agent} sub-agent invocation failed:`, err);
return {
ok: false,
result: `sub-agent call failed: ${errName} (see server logs)`,
};
}
}
function requireToolCallId(
config: ToolRunnableConfig,
toolName: string,
): string {
const toolCallId = config.toolCall?.id;
if (typeof toolCallId !== "string" || toolCallId.length === 0) {
throw new Error(
`${toolName}: missing tool_call_id on ToolRunnableConfig.toolCall — ` +
"tool was invoked outside a ToolNode context.",
);
}
return toolCallId;
}
// ---------------------------------------------------------------------------
// 4. Supervisor tools — each tool delegates to one sub-agent.
//
// The supervisor LLM "calls" these tools to delegate work; each call
// synchronously runs the matching sub-agent, records the delegation
// into shared state, and returns the sub-agent's output as a
// ToolMessage the supervisor can read on its next step.
// ---------------------------------------------------------------------------
const researchAgentTool = tool(
async ({ task }, config: ToolRunnableConfig) => {
const toolCallId = requireToolCallId(config, "research_agent");
const outcome = await runSubAgentSafely("research_agent", task, config);
return delegationUpdate(
"research_agent",
task,
outcome.result,
toolCallId,
outcome.ok ? "completed" : "failed",
);
},
{
name: "research_agent",
description:
"Delegate a research task to the research sub-agent. " +
"Use for: gathering facts, background, definitions, statistics. " +
"Returns a bulleted list of key facts.",
schema: z.object({
task: z
.string()
.describe("The research question or topic to investigate."),
}),
},
);
const writingAgentTool = tool(
async ({ task }, config: ToolRunnableConfig) => {
const toolCallId = requireToolCallId(config, "writing_agent");
const outcome = await runSubAgentSafely("writing_agent", task, config);
return delegationUpdate(
"writing_agent",
task,
outcome.result,
toolCallId,
outcome.ok ? "completed" : "failed",
);
},
{
name: "writing_agent",
description:
"Delegate a drafting task to the writing sub-agent. " +
"Use for: producing a polished paragraph, draft, or summary. Pass " +
"relevant facts from prior research inside `task`.",
schema: z.object({
task: z
.string()
.describe(
"Brief + optional source facts. The sub-agent returns a 1-paragraph draft.",
),
}),
},
);
const critiqueAgentTool = tool(
async ({ task }, config: ToolRunnableConfig) => {
const toolCallId = requireToolCallId(config, "critique_agent");
const outcome = await runSubAgentSafely("critique_agent", task, config);
return delegationUpdate(
"critique_agent",
task,
outcome.result,
toolCallId,
outcome.ok ? "completed" : "failed",
);
},
{
name: "critique_agent",
description:
"Delegate a critique task to the critique sub-agent. " +
"Use for: reviewing a draft and suggesting concrete improvements.",
schema: z.object({
task: z
.string()
.describe(
"The draft to critique. The sub-agent returns 2-3 critiques.",
),
}),
},
);
// @endregion[supervisor-delegation-tools]
const tools = [researchAgentTool, writingAgentTool, critiqueAgentTool];
// ---------------------------------------------------------------------------
// 5. Supervisor chat node.
// ---------------------------------------------------------------------------
const SUPERVISOR_SYSTEM_PROMPT =
"You are a supervisor agent that coordinates three specialized " +
"sub-agents to produce high-quality deliverables.\n\n" +
"Available sub-agents (call them as tools):\n" +
" - research_agent: gathers facts on a topic.\n" +
" - writing_agent: turns facts + a brief into a polished draft.\n" +
" - critique_agent: reviews a draft and suggests improvements.\n\n" +
"For most non-trivial user requests, delegate in sequence: " +
"research -> write -> critique. Pass the relevant facts/draft " +
"through the `task` argument of each tool. Keep your own " +
"messages short — explain the plan once, delegate, then return " +
"a concise summary once done. The UI shows the user a live log " +
"of every sub-agent delegation.";
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
temperature: 0,
model: "gpt-4o-mini",
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const response = await modelWithTools.invoke(
[
new SystemMessage({ content: SUPERVISOR_SYSTEM_PROMPT }),
...state.messages,
],
config,
);
return { messages: response };
}
function shouldContinue({ messages, copilotkit }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const toolCallName = lastMessage.tool_calls![0].name;
if (!actions || actions.every((action) => action.name !== toolCallName)) {
return "tool_node";
}
}
return "__end__";
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,179 @@
/**
* Tool Rendering (Reasoning Chain) — TypeScript port of
* tool_rendering_reasoning_chain_agent.py.
*
* Minimal ReAct agent with tools: reasoning tokens plus sequential tool
* calls for the tool-rendering-reasoning-chain cell.
*/
import { z } from "zod";
import { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { AIMessage, SystemMessage } from "@langchain/core/messages";
import {
Annotation,
MemorySaver,
START,
StateGraph,
messagesStateReducer,
BaseMessage,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
const SYSTEM_PROMPT =
"You are a travel & lifestyle concierge. When a user asks a question, " +
"reason step-by-step and call 2+ tools in succession when relevant.";
const getWeather = tool(
async ({ location }) => ({
city: location,
temperature: 68,
humidity: 55,
wind_speed: 10,
conditions: "Sunny",
}),
{
name: "get_weather",
description: "Get the current weather for a given location.",
schema: z.object({
location: z.string().describe("City name"),
}),
},
);
const searchFlights = tool(
async ({ origin, destination }) => ({
origin,
destination,
flights: [
{
airline: "United",
flight: "UA231",
depart: "08:15",
arrive: "16:45",
price_usd: 348,
},
{
airline: "Delta",
flight: "DL412",
depart: "11:20",
arrive: "19:55",
price_usd: 312,
},
{
airline: "JetBlue",
flight: "B6722",
depart: "17:05",
arrive: "01:30",
price_usd: 289,
},
],
}),
{
name: "search_flights",
description:
"Search mock flights from an origin airport to a destination airport.",
schema: z.object({
origin: z.string().describe("Origin airport code"),
destination: z.string().describe("Destination airport code"),
}),
},
);
const getStockPrice = tool(
async ({ ticker }) => {
const randInt = (lo: number, hi: number) =>
Math.floor(Math.random() * (hi - lo + 1)) + lo;
const sign = Math.random() < 0.5 ? -1 : 1;
return {
ticker: ticker.toUpperCase(),
price_usd:
Math.round((100 + randInt(0, 400) + randInt(0, 99) / 100) * 100) / 100,
change_pct: Math.round(sign * (randInt(0, 300) / 100) * 100) / 100,
};
},
{
name: "get_stock_price",
description: "Get a mock current price for a stock ticker.",
schema: z.object({
ticker: z.string().describe("Stock ticker symbol"),
}),
},
);
const rollDice = tool(
async ({ sides }) => {
const n = sides ?? 6;
const max = Math.max(2, n);
return { sides: n, result: Math.floor(Math.random() * max) + 1 };
},
{
name: "roll_dice",
description: "Roll a single die with the given number of sides.",
schema: z.object({
sides: z
.number()
.int()
.optional()
.describe("Number of sides on the die (default 6)"),
}),
},
);
// Route through a reasoning-capable model via the Responses API so the
// chain of thought streams as AG-UI `ReasoningMessage` events alongside
// the tool calls. Falls back to gpt-4o-mini (no reasoning stream) if
// `OPENAI_REASONING_MODEL` is unset.
const REASONING_MODEL = process.env.OPENAI_REASONING_MODEL ?? "gpt-5-mini";
const tools = [getWeather, searchFlights, getStockPrice, rollDice];
// Custom StateGraph rather than `createReactAgent` so the per-invocation
// `config` (with `copilotkit_forwarded_headers`) reaches the `ChatOpenAI`
// construction — required for `x-aimock-context` propagation.
const AgentStateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: messagesStateReducer,
default: () => [],
}),
});
type AgentState = typeof AgentStateAnnotation.State;
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, {
model: REASONING_MODEL,
useResponsesApi: true,
reasoning: { effort: "low", summary: "auto" },
});
const modelWithTools = model.bindTools!(tools);
const response = await modelWithTools.invoke(
[new SystemMessage({ content: SYSTEM_PROMPT }), ...state.messages],
config,
);
return { messages: response };
}
function shouldContinue({ messages }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) return "tool_node";
return "__end__";
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,243 @@
/**
* Tool Rendering agent -- TypeScript port of tool_rendering_agent.py.
*
* Backs the tool-rendering demos:
* - tool-rendering-default-catchall (no frontend renderers)
* - tool-rendering-custom-catchall (wildcard renderer on frontend)
* - tool-rendering (per-tool + catch-all on frontend)
*
* All cells share this backend -- they differ only in how the frontend
* renders the same tool calls.
*/
// @region[weather-tool-backend]
import { z } from "zod";
import { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { AIMessage, SystemMessage } from "@langchain/core/messages";
import {
Annotation,
MemorySaver,
START,
StateGraph,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { makeChatOpenAI } from "./openai-headers";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
// ---------------------------------------------------------------------------
// 1. Agent state -- extends CopilotKit state annotation
// ---------------------------------------------------------------------------
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
});
export type AgentState = typeof AgentStateAnnotation.State;
// ---------------------------------------------------------------------------
// 2. System prompt -- matches LGP exactly
// ---------------------------------------------------------------------------
const SYSTEM_PROMPT =
"You are a travel & lifestyle concierge. Use the mock tools for " +
"weather, flights, stock prices, or d20 rolls when the user asks; " +
"otherwise reply in plain text. For flights, default origin to 'SFO' " +
"if the user only names a destination. Call multiple tools in one " +
"turn if asked. After tools return, summarize in one short sentence. " +
"Never fabricate data a tool could provide.";
// ---------------------------------------------------------------------------
// 3. Tools -- aligned with LGP tool definitions
// ---------------------------------------------------------------------------
const getWeather = tool(
async ({ location }) => ({
city: location,
temperature: 68,
humidity: 55,
wind_speed: 10,
conditions: "Sunny",
}),
{
name: "get_weather",
description: "Get the current weather for a given location.",
schema: z.object({
location: z.string().describe("City name"),
}),
},
);
// @endregion[weather-tool-backend]
const searchFlights = tool(
async ({ origin, destination }) => ({
origin,
destination,
flights: [
{
airline: "United",
flight: "UA231",
depart: "08:15",
arrive: "16:45",
price_usd: 348,
},
{
airline: "Delta",
flight: "DL412",
depart: "11:20",
arrive: "19:55",
price_usd: 312,
},
{
airline: "JetBlue",
flight: "B6722",
depart: "17:05",
arrive: "01:30",
price_usd: 289,
},
],
}),
{
name: "search_flights",
description:
"Search mock flights from an origin airport to a destination airport.",
schema: z.object({
origin: z.string().describe("Origin airport code"),
destination: z.string().describe("Destination airport code"),
}),
},
);
const getStockPrice = tool(
async ({ ticker, price_usd, change_pct }) => {
const randInt = (lo: number, hi: number) =>
Math.floor(Math.random() * (hi - lo + 1)) + lo;
const sign = Math.random() < 0.5 ? -1 : 1;
return {
ticker: ticker.toUpperCase(),
price_usd:
price_usd != null
? Math.round(price_usd * 100) / 100
: Math.round((100 + randInt(0, 400) + randInt(0, 99) / 100) * 100) /
100,
change_pct:
change_pct != null
? Math.round(change_pct * 100) / 100
: Math.round(sign * (randInt(0, 300) / 100) * 100) / 100,
};
},
{
name: "get_stock_price",
description:
"Get a mock current price for a stock ticker.\n\n" +
"The optional `price_usd` and `change_pct` arguments let the LLM (or " +
"aimock fixture) script a deterministic ticker quote for testing -- " +
"when supplied, the tool echoes them back verbatim. When omitted (or " +
"null), the tool returns mock random values. Mirrors the " +
"deterministic-`value` pattern on `roll_d20`.",
schema: z.object({
ticker: z.string().describe("Stock ticker symbol"),
price_usd: z
.number()
.optional()
.describe(
"Deterministic price override for testing (echoed back verbatim)",
),
change_pct: z
.number()
.optional()
.describe(
"Deterministic change-pct override for testing (echoed back verbatim)",
),
}),
},
);
const rollD20 = tool(
async ({ value }) => {
const rolled =
typeof value === "number" && value >= 1 && value <= 20
? value
: Math.floor(Math.random() * 20) + 1;
return { sides: 20, value: rolled, result: rolled };
},
{
name: "roll_d20",
description: "Roll a 20-sided die.",
schema: z.object({
value: z
.number()
.int()
.optional()
.describe(
"Deterministic override for the roll result (used by test fixtures)",
),
}),
},
);
const tools = [getWeather, searchFlights, getStockPrice, rollD20];
// ---------------------------------------------------------------------------
// 4. Chat node -- binds backend + frontend tools, invokes the model
// ---------------------------------------------------------------------------
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = makeChatOpenAI(config, { model: "gpt-5.4" });
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const systemMessage = new SystemMessage({ content: SYSTEM_PROMPT });
const response = await modelWithTools.invoke(
[systemMessage, ...state.messages],
config,
);
return { messages: response };
}
// ---------------------------------------------------------------------------
// 5. Routing -- send tool calls to tool_node unless they're CopilotKit
// frontend actions.
// ---------------------------------------------------------------------------
function shouldContinue({ messages, copilotkit }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const toolCallName = lastMessage.tool_calls![0].name;
if (!actions || actions.every((action) => action.name !== toolCallName)) {
return "tool_node";
}
}
return "__end__";
}
// ---------------------------------------------------------------------------
// 6. Compile the graph
// ---------------------------------------------------------------------------
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
@@ -0,0 +1,49 @@
// Dedicated runtime for the A2UI — Fixed Schema cell. Splitting into its own
// endpoint lets us set `a2ui.injectA2UITool: false` — the backend agent owns
// the `display_flight` tool which emits its own `a2ui_operations` container.
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
const LANGGRAPH_URL =
process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123";
const a2uiFixedSchemaAgent = new LangGraphAgent({
deploymentUrl: `${LANGGRAPH_URL}/`,
graphId: "a2ui_fixed",
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
});
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents: { "a2ui-fixed-schema": a2uiFixedSchemaAgent },
a2ui: {
// The backend emits its own `a2ui_operations` container via `display_flight`
// (see src/agent/a2ui-fixed.ts). We still run the A2UI middleware so it
// detects the container in tool results and forwards surfaces to the
// frontend — but we do NOT inject a runtime `render_a2ui` tool on top.
injectA2UITool: false,
},
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-a2ui-fixed-schema",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,58 @@
// Dedicated runtime for the A2UI Error Recovery demo.
// `a2ui.injectA2UITool: false` — the backend LangGraph (TS) agent OWNS
// `generate_a2ui` via `@ag-ui/langgraph` `getA2UITools` (see
// src/agent/recovery-agent.ts), whose body runs the `render_a2ui` sub-agent +
// the toolkit validate->retry recovery loop + the recovery-exhausted hard-fail
// envelope IN-GRAPH (OSS-158 / OSS-375). The runtime must NOT inject a second
// copy (double-bind); this `false` is load-bearing post CopilotKit#5611 (the
// provider catalog otherwise defaults injectA2UITool to true). The middleware
// still renders the building -> retrying (N/M) -> painted / failed lifecycle.
//
// The demo reuses the declarative-gen-ui catalog. The aimock fixtures force the
// inner render_a2ui sub-agent to emit free-form/sloppy args the middleware heals
// (heal pill) or a structurally-invalid surface on every attempt (exhaust pill).
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
const LANGGRAPH_URL =
process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123";
const recoveryAgent = new LangGraphAgent({
deploymentUrl: `${LANGGRAPH_URL}/`,
graphId: "a2ui_recovery",
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
});
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents: { "a2ui-recovery": recoveryAgent },
a2ui: {
injectA2UITool: false,
// Reuse the catalog the page registers (shared with declarative-gen-ui).
defaultCatalogId: "declarative-gen-ui-catalog",
},
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-a2ui-recovery",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,64 @@
// Dedicated runtime for the Agent Config Object demo.
//
// Hosts the `agent_config_agent` graph. The frontend publishes its
// tone / expertise / responseLength toggles to the agent through
// `useAgentContext`, which the runtime serializes onto the AG-UI run as
// a context entry. The TypeScript LangGraph agent reads that context
// entry and adapts its style accordingly.
//
// References:
// - src/agent/agent-config.ts — the graph
// - src/app/demos/agent-config/config-context-relay.tsx — the
// `useAgentContext` publisher
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
const LANGGRAPH_URL =
process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123";
const agentConfigAgent = new LangGraphAgent({
deploymentUrl: LANGGRAPH_URL,
graphId: "agent_config_agent",
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
});
const agents: Record<string, LangGraphAgent> = {
// The page's <CopilotKit agent="agent-config-demo"> resolves here.
"agent-config-demo": agentConfigAgent,
// Internal components (headless-chat, example-canvas) call `useAgent()`
// with no args, which defaults to agentId "default". Alias to the same
// graph so those component hooks resolve instead of throwing
// "Agent 'default' not found".
default: agentConfigAgent,
};
const runtime = new CopilotRuntime({
// @ts-ignore -- Published CopilotRuntime agents type wraps Record in
// MaybePromise<NonEmptyRecord<...>> which rejects plain Records; fixed in
// source, pending release.
agents,
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-agent-config",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,82 @@
// Dedicated runtime for the /demos/auth cell.
//
// Demonstrates framework-native request authentication via the V2 runtime's
// `onRequest` hook, which runs before routing and can short-circuit the
// request by throwing a Response. We validate a static `Authorization: Bearer
// <DEMO_TOKEN>` header; mismatch throws 401 before the request reaches the
// agent.
//
// Implementation note: the V1 Next.js adapter
// (`copilotRuntimeNextJSAppRouterEndpoint`) does NOT forward the `hooks`
// option to the V2 fetch handler. To get `onRequest` wired, this route uses
// `createCopilotRuntimeHandler` from `@copilotkit/runtime/v2` directly — the
// framework-agnostic fetch handler that returns a plain
// `(Request) => Promise<Response>`, which composes cleanly with a Next.js
// App Router route export.
//
// References:
// - packages/runtime/src/v2/runtime/core/hooks.ts (onRequest semantics)
// - packages/runtime/src/v2/runtime/__tests__/hooks.test.ts (throw Response pattern)
import type { NextRequest } from "next/server";
import {
CopilotRuntime,
createCopilotRuntimeHandler,
} from "@copilotkit/runtime/v2";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
import { DEMO_AUTH_HEADER } from "@/app/demos/auth/demo-token";
const LANGGRAPH_URL =
process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123";
// Reuse the neutral `starterAgent` graph for the authenticated path. The
// point of this demo is the gate mechanism, not per-user agent branching —
// authenticated users get the same behavior as any other neutral demo.
const authDemoAgent = new LangGraphAgent({
deploymentUrl: `${LANGGRAPH_URL}/`,
graphId: "starterAgent",
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
});
const runtime = new CopilotRuntime({
agents: {
"auth-demo": authDemoAgent,
// Fallback: useAgent() with no args resolves "default" — alias to the
// same agent so hooks in the demo page resolve cleanly.
default: authDemoAgent,
},
});
const BASE_PATH = "/api/copilotkit-auth";
// Framework-agnostic fetch handler with the auth gate wired up.
const handler = createCopilotRuntimeHandler({
runtime,
basePath: BASE_PATH,
hooks: {
onRequest: ({ request }) => {
const authHeader = request.headers.get("authorization");
if (authHeader !== DEMO_AUTH_HEADER) {
// Throwing a Response short-circuits the pipeline. The runtime maps
// thrown Responses to the HTTP response verbatim (status + body).
throw new Response(
JSON.stringify({
error: "unauthorized",
message:
"Missing or invalid Authorization header. Click Authenticate above to send messages.",
}),
{
status: 401,
headers: { "content-type": "application/json" },
},
);
}
},
},
});
// Next.js App Router bindings. The handler is framework-agnostic — it takes
// a web Request and returns a web Response — so it drops straight into the
// POST/GET exports without any adapter shim.
export const POST = (req: NextRequest) => handler(req);
export const GET = (req: NextRequest) => handler(req);
@@ -0,0 +1,77 @@
// Dedicated runtime for the Beautiful Chat flagship showcase cell.
//
// Beautiful Chat exercises A2UI (dynamic + fixed schema), Open Generative UI,
// and MCP Apps simultaneously — the same combined-runtime shape the canonical
// starter uses. The other langgraph-typescript cells share the default
// /api/copilotkit endpoint, so we split these flags off into their own route
// here to avoid bleeding them globally.
//
// Ported from langgraph-python/src/app/api/copilotkit-beautiful-chat/route.ts.
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
const LANGGRAPH_URL =
process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123";
const beautifulChatAgent = new LangGraphAgent({
deploymentUrl: `${LANGGRAPH_URL}/`,
graphId: "beautiful_chat",
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
});
const agents: Record<string, LangGraphAgent> = {
// The page's <CopilotKit agent="beautiful-chat"> resolves here.
"beautiful-chat": beautifulChatAgent,
// Internal components (headless-chat, example-canvas) may call `useAgent()`
// with no args, which defaults to "default". Alias to the same graph so
// those component hooks resolve instead of throwing "Agent 'default' not
// found".
default: beautifulChatAgent,
};
const runtime = new CopilotRuntime({
// @ts-ignore -- matches main route.ts pattern
agents,
openGenerativeUI: true,
a2ui: {
// Inject the dynamic `generate_a2ui` tool into the agent
injectA2UITool: true,
// Models follow the tool-usage guide and omit `catalogId`, and the
// middleware then falls back to the unregistered spec basic catalog
// ("Catalog not found" render error). Pin the catalog the page registers.
defaultCatalogId: "copilotkit://app-dashboard-catalog",
},
mcpApps: {
servers: [
{
type: "http",
url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
serverId: "beautiful_chat_mcp",
},
],
},
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-beautiful-chat",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,44 @@
// Dedicated runtime for the Declarative Generative UI (A2UI — Dynamic Schema) cell.
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
const LANGGRAPH_URL =
process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123";
const declarativeGenUiAgent = new LangGraphAgent({
deploymentUrl: `${LANGGRAPH_URL}/`,
graphId: "a2ui_dynamic",
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
});
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents: { "declarative-gen-ui": declarativeGenUiAgent },
// No runtime `a2ui` config: the page passes a catalog to the provider
// (`<CopilotKit a2ui={{ catalog }}>`), which auto-enables A2UI and defaults
// tool injection on (CopilotKit >= 1.61.2, PR #5611).
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-declarative-gen-ui",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};
@@ -0,0 +1,48 @@
// Dedicated runtime for the declarative-hashbrown demo.
//
// The demo page (`src/app/demos/declarative-hashbrown/page.tsx`) wraps
// CopilotChat in the HashBrownDashboard provider and overrides the assistant
// message slot with a renderer that consumes hashbrown-shaped structured
// output via `@hashbrownai/react`'s `useUiKit` + `useJsonParser`. The
// agent behind this endpoint uses the `byoc_hashbrown` graph (the internal
// graph ID retains the legacy name; only the user-facing slug, route, and
// frontend folder were renamed).
import { NextRequest, NextResponse } from "next/server";
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
const LANGGRAPH_URL =
process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123";
const byocHashbrownAgent = new LangGraphAgent({
deploymentUrl: `${LANGGRAPH_URL}/`,
graphId: "byoc_hashbrown",
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
});
const runtime = new CopilotRuntime({
// @ts-ignore -- see main route.ts
agents: { "declarative-hashbrown-demo": byocHashbrownAgent },
});
export const POST = async (req: NextRequest) => {
try {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
endpoint: "/api/copilotkit-declarative-hashbrown",
serviceAdapter: new ExperimentalEmptyAdapter(),
runtime,
});
return await handleRequest(req);
} catch (error: unknown) {
const e = error as { message?: string; stack?: string };
return NextResponse.json(
{ error: e.message, stack: e.stack },
{ status: 500 },
);
}
};

Some files were not shown because too many files have changed in this diff Show More