chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
**/node_modules
|
||||
.next
|
||||
__pycache__
|
||||
*.pyc
|
||||
.git
|
||||
**/vitest.config.ts
|
||||
**/test-setup.ts
|
||||
**/*.test.ts
|
||||
**/*.test.tsx
|
||||
**/*.spec.ts
|
||||
**/*.spec.tsx
|
||||
@@ -0,0 +1,11 @@
|
||||
# Anthropic API Key (required)
|
||||
ANTHROPIC_API_KEY=replace-with-your-key
|
||||
|
||||
# Claude model to use (optional, defaults to claude-sonnet-4.6)
|
||||
ANTHROPIC_MODEL=claude-sonnet-4.6
|
||||
|
||||
# Agent backend URL (Next.js → agent server proxy)
|
||||
AGENT_URL=http://localhost:8000
|
||||
|
||||
# Showcase
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||
@@ -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,64 @@
|
||||
# Stage 1: Build Next.js frontend + compile TypeScript agent server
|
||||
FROM node:22-slim AS builder
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --legacy-peer-deps
|
||||
COPY . .
|
||||
# Build Next.js frontend
|
||||
RUN npm run build
|
||||
# Compile TypeScript agent server to JS so boot is a straight `node` call
|
||||
# instead of `npx tsx` (which does a fresh in-process TS compile on each
|
||||
# cold start).
|
||||
RUN npx tsc --outDir /app/dist --module commonjs --moduleResolution node \
|
||||
--target es2020 --esModuleInterop true --resolveJsonModule true \
|
||||
--skipLibCheck true src/agent_server.ts
|
||||
|
||||
# Stage 2: 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 + pruned prod node_modules
|
||||
COPY --chown=app:app --from=builder /app/.next ./.next
|
||||
COPY --chown=app:app --from=builder /app/node_modules ./node_modules
|
||||
COPY --chown=app:app --from=builder /app/package.json ./
|
||||
COPY --chown=app:app --from=builder /app/public ./public
|
||||
|
||||
# Precompiled agent tree (from builder stage) — tsc emits agent_server.js
|
||||
# plus agent/*.js prompt modules; copy the whole dist/ tree so require()
|
||||
# resolves the sibling imports at runtime.
|
||||
COPY --chown=app:app --from=builder /app/dist/ ./
|
||||
|
||||
# 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 subprocess that tries
|
||||
# to mkdir under /app at runtime (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 `node`, shell scripts, healthchecks). 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,43 @@
|
||||
# Parity Notes
|
||||
|
||||
Baseline: `showcase/integrations/langgraph-python/`.
|
||||
|
||||
The Claude Agent SDK (TypeScript) backend is a single pass-through
|
||||
`agent_server.ts` that forwards AG-UI tool schemas (frontend-registered +
|
||||
runtime-injected) directly to Claude via the Anthropic Messages API.
|
||||
Demos whose LangGraph reference depends on primitives the pass-through
|
||||
does not implement are skipped below.
|
||||
|
||||
## New demos (post-PR #4271)
|
||||
|
||||
- `byoc-json-render` — Claude system prompt (`src/agent/byoc-json-render-prompt.ts`) instructs the model to emit a `@json-render/react` flat spec; frontend renderer wraps `<Renderer />` in `<JSONUIProvider>` and forwards `children` from the MetricCard registry entry.
|
||||
- `byoc-hashbrown` — Claude system prompt (`src/agent/byoc-hashbrown-prompt.ts`) emits the hashbrown `{ ui: [...] }` JSON envelope with `data` props as JSON strings, matching the strict hashbrown schema.
|
||||
- `multimodal` — agent_server routes vision-capable Sonnet for the `/multimodal` endpoint, maps AG-UI `binary` parts directly to Anthropic `image` / `document` blocks (no pypdf flattening needed — Claude reads PDFs natively). Frontend keeps the legacy-shape shim so AG-UI schema validation passes and keeps the LFS-pointer guard in `sample-attachment-buttons.tsx`.
|
||||
- `voice` — dedicated route mounts `GuardedOpenAITranscriptionService` on the V2 runtime instance (throws a typed auth error mapped to 401 when `OPENAI_API_KEY` is missing). Framework-agnostic port.
|
||||
- `agent-config` — `forwardedProps` arrives verbatim on `RunAgentInput` at the Claude agent; `agent_server.ts` builds the system prompt from `tone` / `expertise` / `responseLength` per run. No LangGraph `configurable` repacking needed because the pass-through doesn't use LangGraph config semantics.
|
||||
- `auth` — framework-agnostic port using `createCopilotRuntimeHandler` + `hooks.onRequest` with the shared `DEMO_AUTH_HEADER`; `useDemoAuth` defaults to `true`, `ChatErrorBoundary` catches render-time errors in the signed-out state.
|
||||
|
||||
## New demos (showcase-fill-186)
|
||||
|
||||
- `tool-rendering-default-catchall` / `tool-rendering-custom-catchall` —
|
||||
ported by registering the mock tool suite (`get_weather`, `search_flights`,
|
||||
`get_stock_price`, `roll_dice`) via `useFrontendTool` on the page itself.
|
||||
The Claude Agent SDK pass-through forwards these to the Anthropic Messages
|
||||
API, so the catch-all renderer paints every result without backend agent
|
||||
changes.
|
||||
- `agentic-chat-reasoning` / `reasoning-default-render` /
|
||||
`tool-rendering-reasoning-chain` — `agent_server.ts` now opts into
|
||||
Anthropic extended thinking (`thinking: { type: "enabled" }`) on the new
|
||||
`/reasoning` endpoint and forwards `thinking_delta` events as AG-UI
|
||||
`REASONING_MESSAGE_START | CONTENT | END`. Default model is
|
||||
Claude 3.7 Sonnet; override via `CLAUDE_REASONING_MODEL`. A dedicated Next.js
|
||||
runtime route (`/api/copilotkit-reasoning/route.ts`) wires those agent ids
|
||||
to the new endpoint. Reasoning-chain reuses per-tool renderers + a wildcard
|
||||
catch-all.
|
||||
|
||||
## Blocked demos
|
||||
|
||||
- `beautiful-chat` — flagship cell that composes A2UI (dynamic + fixed schema), OpenGenerativeUI, and MCP Apps through a single combined runtime alongside agent-authored tools; porting requires dedicated Claude agent branches for each feature, not a single pass-through.
|
||||
- `headless-complete` — multi-component chat surface assumes backend-authored tools (`get_weather`, `highlight_note`, `display_stock`) defined in the LangGraph `headless_complete` graph; the pass-through has no equivalent agent.
|
||||
- `a2ui-fixed-schema` — relies on the backend graph's `display_flight` tool emitting an `a2ui_operations` container via `a2ui.render(...)`; the pass-through agent cannot synthesise that backend-defined tool.
|
||||
- `hitl-in-chat` / `hitl-in-chat-booking` / `gen-ui-interrupt` / `interrupt-headless` — all require LangGraph's `interrupt()` primitive and the `on_interrupt` custom event; the Claude Agent SDK has no equivalent suspend/resume primitive that pauses a run mid-tool and rehydrates from a typed `Command(resume=...)`. (Frontend-side approval flows already work — see `hitl-in-app`.)
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"framework": "claude-sdk-typescript",
|
||||
"features": {
|
||||
"cli-start": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/quickstart",
|
||||
"shell_docs_path": "/quickstart"
|
||||
},
|
||||
"agentic-chat": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/prebuilt-components",
|
||||
"shell_docs_path": "/prebuilt-components"
|
||||
},
|
||||
"frontend-tools": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/frontend-tools",
|
||||
"shell_docs_path": "/frontend-tools"
|
||||
},
|
||||
"frontend-tools-async": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/frontend-tools",
|
||||
"shell_docs_path": "/frontend-tools"
|
||||
},
|
||||
"hitl-in-chat": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/human-in-the-loop",
|
||||
"shell_docs_path": "/human-in-the-loop"
|
||||
},
|
||||
"hitl-in-app": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/human-in-the-loop",
|
||||
"shell_docs_path": "/human-in-the-loop"
|
||||
},
|
||||
"tool-rendering": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/generative-ui/tool-rendering",
|
||||
"shell_docs_path": "/generative-ui/tool-rendering"
|
||||
},
|
||||
"gen-ui-tool-based": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/generative-ui/tool-based",
|
||||
"shell_docs_path": "/generative-ui/tool-based"
|
||||
},
|
||||
"gen-ui-agent": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/generative-ui/display",
|
||||
"shell_docs_path": "/generative-ui/display"
|
||||
},
|
||||
"shared-state-read": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/shared-state/agent-readonly",
|
||||
"shell_docs_path": "/shared-state/agent-readonly"
|
||||
},
|
||||
"shared-state-read-write": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/shared-state",
|
||||
"shell_docs_path": "/shared-state"
|
||||
},
|
||||
"shared-state-streaming": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/shared-state/streaming",
|
||||
"shell_docs_path": "/shared-state/streaming"
|
||||
},
|
||||
"readonly-state-agent-context": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/shared-state/agent-readonly",
|
||||
"shell_docs_path": "/shared-state/agent-readonly"
|
||||
},
|
||||
"agent-config": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/agent-config",
|
||||
"shell_docs_path": "/agent-config"
|
||||
},
|
||||
"subagents": {
|
||||
"og_docs_url": "https://docs.copilotkit.ai/claude-sdk-typescript/multi-agent/subagents",
|
||||
"shell_docs_path": "/multi-agent/subagents"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Make runtime configuration explicit
|
||||
|
||||
The Claude Agent SDK demo reads configuration from shared state and folds it
|
||||
into the system prompt. This keeps the agent behavior visible to the UI and
|
||||
lets users tune model behavior without rebuilding the backend.
|
||||
|
||||
<DemoCode file="src/agent/agent-config-prompt.ts" region="agent-config-setup" />
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,11 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Add CopilotKit context to the Claude prompt
|
||||
|
||||
CopilotKit forwards readable app context in the AG-UI run input. Append that
|
||||
context to the Claude system prompt before starting the run so the agent can
|
||||
answer with the current UI state in mind.
|
||||
|
||||
<DemoCode file="src/agent_server.ts" region="agent-context-setup" />
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,19 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Install the Claude Agent SDK packages
|
||||
|
||||
```bash
|
||||
npm install @ag-ui/core @ag-ui/encoder @ag-ui/claude-agent-sdk @anthropic-ai/claude-agent-sdk@^0.2.58 @anthropic-ai/sdk zod
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Bridge Claude Agent SDK to AG-UI
|
||||
|
||||
Use `ClaudeAgentAdapter` from `@ag-ui/claude-agent-sdk`. The adapter
|
||||
receives the AG-UI run input, emits AG-UI events back to CopilotKit, and can
|
||||
expose backend tools through an in-process Claude SDK MCP server.
|
||||
|
||||
<DemoCode file="src/claude-agent-sdk-adapter.ts" region="claude-agent-sdk-agent-setup" />
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,16 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Forward browser tools to Claude
|
||||
|
||||
Frontend tools registered with `useFrontendTool` arrive in the AG-UI run
|
||||
input. Convert each AG-UI tool definition into an Anthropic Messages API
|
||||
tool schema before calling the model. Runs that carry frontend tools use
|
||||
the direct Messages API path rather than the Claude Agent SDK.
|
||||
|
||||
<DemoCode
|
||||
file="src/agent_server.ts"
|
||||
region="frontend-tools-setup"
|
||||
highlight="28-32"
|
||||
/>
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,12 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Model approvals as async frontend tools
|
||||
|
||||
Claude Agent SDK human-in-the-loop demos use CopilotKit's promise-based
|
||||
frontend tool flow. The agent calls an approval tool, the UI resolves the
|
||||
tool result after the user decides, and the same Claude run continues with
|
||||
that result. Register the approval UI from the page component.
|
||||
|
||||
<DemoCode file="src/app/demos/hitl-in-chat/page.tsx" region="hitl-frontend-tool" title="page.tsx - useHumanInTheLoop" />
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,12 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Run Claude through an AG-UI endpoint
|
||||
|
||||
Programmatic control starts from the same AG-UI run boundary as the chat UI.
|
||||
Wrap Claude Agent SDK once, then trigger runs from a custom UI with
|
||||
`useAgent` or the AG-UI client. Inside your component, add the user
|
||||
message to the agent and dispatch the run.
|
||||
|
||||
<DemoCode file="src/app/demos/headless-simple/chat.tsx" region="use-agent-simple" title="chat.tsx - useAgent run control" />
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,11 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Put shared state in the system prompt and tools
|
||||
|
||||
The demo exposes frontend preferences to Claude and gives the agent a tool
|
||||
for writing notes back into CopilotKit state. Keep the state contract small,
|
||||
typed, and mirrored by the UI components that read the same values.
|
||||
|
||||
<DemoCode file="src/agent/shared-state-read-write-prompt.ts" region="shared-state-setup" />
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,11 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Stream partial state updates while Claude responds
|
||||
|
||||
For streaming state, parse the agent's structured deltas as they arrive and
|
||||
emit CopilotKit state updates before the final message is complete. This
|
||||
branch runs inside the streamed tool-argument handler.
|
||||
|
||||
<DemoCode file="src/agent_server.ts" region="state-streaming-delta-emission" />
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,12 @@
|
||||
<Steps>
|
||||
<Step>
|
||||
### Represent subagents as delegation tools
|
||||
|
||||
The Claude Agent SDK demo keeps a supervisor prompt and exposes delegation
|
||||
tools for specialist agents. CopilotKit can then render delegation progress
|
||||
while the supervisor coordinates the run. Start by giving the supervisor
|
||||
one tool schema per specialist.
|
||||
|
||||
<DemoCode file="src/agent/subagents-prompts.ts" region="supervisor-delegation-tools" title="subagents-prompts.ts - supervisor tool schemas" />
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cleanup() {
|
||||
kill $AGENT_PID $NEXTJS_PID $WATCHDOG_PID 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "========================================="
|
||||
echo "[entrypoint] Starting showcase package: claude-sdk-typescript"
|
||||
echo "[entrypoint] Time: $(date -u)"
|
||||
echo "[entrypoint] PORT=${PORT:-not set}"
|
||||
echo "[entrypoint] NODE_ENV=${NODE_ENV:-not set}"
|
||||
echo "========================================="
|
||||
|
||||
if [ -z "$ANTHROPIC_API_KEY" ]; then
|
||||
echo "[entrypoint] WARNING: ANTHROPIC_API_KEY is not set! Agent will fail."
|
||||
else
|
||||
echo "[entrypoint] ANTHROPIC_API_KEY: set (${#ANTHROPIC_API_KEY} chars)"
|
||||
fi
|
||||
|
||||
# Start Claude agent backend (TypeScript, compiled to JS).
|
||||
# Log prefixing uses bash process substitution (`&> >(awk …)`) rather than a
|
||||
# pipe (`| sed …`): process substitution leaves `$!` pointing at the real
|
||||
# node process, so `wait -n $AGENT_PID` monitors the right thing.
|
||||
# `awk` with `fflush()` line-flushes each prefixed line to the container log.
|
||||
echo "[entrypoint] Starting Claude agent on port 8000..."
|
||||
# Instrumentation: package-claude-sdk-typescript health probes fail on
|
||||
# Railway but process claims to listen — narrow the cold-start window by
|
||||
# logging immediately before node exec so we can compare against the
|
||||
# agent_server.ts module-loaded / pre-Anthropic / listening prints below.
|
||||
echo "[entrypoint] pre-node $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
node /app/agent_server.js &> >(awk '{print "[agent] " $0; fflush()}') &
|
||||
AGENT_PID=$!
|
||||
sleep 2
|
||||
if kill -0 $AGENT_PID 2>/dev/null; then
|
||||
echo "[entrypoint] Agent started (PID: $AGENT_PID)"
|
||||
else
|
||||
echo "[entrypoint] ERROR: Agent failed to start — exiting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "========================================="
|
||||
echo "[entrypoint] Starting Next.js frontend on port ${PORT:-10000}..."
|
||||
echo "========================================="
|
||||
|
||||
PORT=${PORT:-10000}
|
||||
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 agent process stays alive (so `wait -n` never
|
||||
# fires and the container never restarts) but stops responding on :8000.
|
||||
# Poll the agent's /health endpoint every 30s; 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: `node /app/agent_server.js` runs the compiled
|
||||
# @anthropic-ai/claude-agent-sdk bundle and was observed restart-looping
|
||||
# on Railway starting 04-20 16:54 UTC — the 90s (3-strike) budget was
|
||||
# shorter than the cold-start path on a fresh container. Wait up to 180s
|
||||
# for the first successful health probe before arming the strike counter
|
||||
# so slow cold-starts aren't killed in a loop.
|
||||
(
|
||||
GRACE=180
|
||||
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:8000/health > /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 30; do
|
||||
if ! kill -0 $AGENT_PID 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
if curl -fsS --max-time 5 http://127.0.0.1:8000/health > /dev/null 2>&1; then
|
||||
FAILS=0
|
||||
else
|
||||
FAILS=$((FAILS + 1))
|
||||
echo "[watchdog] Agent health probe failed (count=$FAILS)"
|
||||
if [ $FAILS -ge 3 ]; then
|
||||
echo "[watchdog] Agent unresponsive for ~90s — killing PID $AGENT_PID to trigger container restart"
|
||||
kill -9 $AGENT_PID 2>/dev/null || true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
) &
|
||||
WATCHDOG_PID=$!
|
||||
|
||||
echo "[entrypoint] Watchdog started (PID: $WATCHDOG_PID)"
|
||||
echo "[entrypoint] All processes running. Waiting..."
|
||||
|
||||
wait -n $AGENT_PID $NEXTJS_PID
|
||||
EXIT_CODE=$?
|
||||
if ! kill -0 $AGENT_PID 2>/dev/null; then
|
||||
echo "[entrypoint] Agent (PID: $AGENT_PID) exited with code $EXIT_CODE"
|
||||
elif ! kill -0 $NEXTJS_PID 2>/dev/null; then
|
||||
echo "[entrypoint] Next.js (PID: $NEXTJS_PID) exited with code $EXIT_CODE"
|
||||
else
|
||||
echo "[entrypoint] A process exited with code $EXIT_CODE"
|
||||
fi
|
||||
|
||||
exit $EXIT_CODE
|
||||
@@ -0,0 +1,536 @@
|
||||
name: Claude Agent SDK (TypeScript)
|
||||
slug: claude-sdk-typescript
|
||||
category: agent-framework
|
||||
language: typescript
|
||||
logo: /logos/claude-sdk-typescript.svg
|
||||
description: CopilotKit integration with Anthropic's Claude via the Claude SDK for TypeScript
|
||||
partner_docs: null
|
||||
repo: https://github.com/CopilotKit/CopilotKit/tree/main/showcase/integrations/claude-sdk-typescript
|
||||
copilotkit_version: 2.0.0
|
||||
deployed: true
|
||||
docs_mode: generated
|
||||
sort_order: 80
|
||||
generative_ui:
|
||||
- constrained-explicit
|
||||
- a2ui-fixed-schema
|
||||
- a2ui-dynamic-schema
|
||||
interaction_modalities:
|
||||
- sidebar
|
||||
- embedded
|
||||
- chat
|
||||
not_supported_features:
|
||||
# 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
|
||||
features:
|
||||
- beautiful-chat
|
||||
- cli-start
|
||||
- agentic-chat
|
||||
- prebuilt-sidebar
|
||||
- prebuilt-popup
|
||||
- chat-slots
|
||||
- chat-customization-css
|
||||
- headless-simple
|
||||
- headless-complete
|
||||
- reasoning-custom
|
||||
- reasoning-default
|
||||
- frontend-tools
|
||||
- frontend-tools-async
|
||||
- hitl-in-chat
|
||||
- hitl-in-app
|
||||
- declarative-gen-ui
|
||||
- a2ui-fixed-schema
|
||||
- mcp-apps
|
||||
- gen-ui-agent
|
||||
- gen-ui-tool-based
|
||||
- tool-rendering-default-catchall
|
||||
- tool-rendering-custom-catchall
|
||||
- tool-rendering
|
||||
- tool-rendering-reasoning-chain
|
||||
- shared-state-read
|
||||
- shared-state-read-write
|
||||
- shared-state-streaming
|
||||
- readonly-state-agent-context
|
||||
- subagents
|
||||
- multimodal
|
||||
- auth
|
||||
- declarative-hashbrown
|
||||
- declarative-json-render
|
||||
- open-gen-ui
|
||||
- open-gen-ui-advanced
|
||||
- voice
|
||||
- agent-config
|
||||
agent_config_pattern: shared-state
|
||||
interrupt_pattern: promise-based
|
||||
a2ui_pattern: schema-loading
|
||||
auth_pattern: runtime-onrequest
|
||||
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 claude-sdk-typescript"
|
||||
- id: agentic-chat
|
||||
name: Agentic Chat
|
||||
description: Natural conversation with frontend tool execution
|
||||
tags:
|
||||
- chat-ui
|
||||
route: /demos/agentic-chat
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/agentic-chat/page.tsx
|
||||
- src/app/api/copilotkit/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_server.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_server.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_server.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_server.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 Chat (Simple)
|
||||
description: Minimal custom chat surface built on useAgent
|
||||
tags:
|
||||
- chat-ui
|
||||
route: /demos/headless-simple
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/headless-simple/page.tsx
|
||||
- src/app/api/copilotkit/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_server.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_server.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: hitl-in-app
|
||||
name: In-App Human in the Loop (Frontend Tools + async HITL)
|
||||
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_server.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: tool-rendering
|
||||
name: Tool Rendering
|
||||
description: Backend agent tools rendered as UI components
|
||||
tags:
|
||||
- agent-capabilities
|
||||
route: /demos/tool-rendering
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/tool-rendering/page.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: declarative-gen-ui
|
||||
name: Declarative Generative UI (A2UI)
|
||||
description: Canonical A2UI BYOC — custom catalog wired via a2ui.catalog on the provider; backend agent owns the generate_a2ui tool with injectA2UITool false
|
||||
tags:
|
||||
- generative-ui
|
||||
route: /demos/declarative-gen-ui
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.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: gen-ui-agent
|
||||
name: Agentic Generative UI
|
||||
description: Long-running agent tasks with generated UI
|
||||
tags:
|
||||
- generative-ui
|
||||
route: /demos/gen-ui-agent
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/gen-ui-agent/page.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: gen-ui-tool-based
|
||||
name: Tool-Based Generative UI
|
||||
description: Agent uses frontend tools to trigger UI generation — useFrontendTool with a render function paints structured haiku output as a styled card alongside the chat
|
||||
tags:
|
||||
- generative-ui
|
||||
route: /demos/gen-ui-tool-based
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/gen-ui-tool-based/page.tsx
|
||||
- src/app/demos/gen-ui-tool-based/bar-chart.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: shared-state-streaming
|
||||
name: State Streaming
|
||||
description: Per-token state delta streaming from agent to UI
|
||||
tags:
|
||||
- agent-state
|
||||
route: /demos/shared-state-streaming
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/shared-state-streaming/page.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: shared-state-read-write
|
||||
name: Shared State (Read + Write)
|
||||
description: Bidirectional shared state — UI writes preferences via agent.setState, agent reads them every turn and writes notes back via a set_notes tool
|
||||
tags:
|
||||
- agent-state
|
||||
route: /demos/shared-state-read-write
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/agent/shared-state-read-write-prompt.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-shared-state-read-write/route.ts
|
||||
- id: shared-state-read
|
||||
name: Shared State (Read)
|
||||
description: Agent reads frontend-provided state as context
|
||||
tags:
|
||||
- agent-state
|
||||
route: /demos/shared-state-read
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/shared-state-read/page.tsx
|
||||
- src/app/demos/shared-state-read/recipe-card.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: subagents
|
||||
name: Sub-Agents
|
||||
description: Supervisor delegates to research / writing / critique sub-agents (each a single secondary Anthropic Messages call); every delegation appears in a live log via shared state
|
||||
tags:
|
||||
- agent-capabilities
|
||||
route: /demos/subagents
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/agent/subagents-prompts.ts
|
||||
- src/app/demos/subagents/page.tsx
|
||||
- src/app/demos/subagents/delegation-log.tsx
|
||||
- src/app/api/copilotkit-subagents/route.ts
|
||||
- id: readonly-state-agent-context
|
||||
name: Readonly State (Agent 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_server.ts
|
||||
- src/app/demos/readonly-state-agent-context/page.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: open-gen-ui
|
||||
name: Fully Open-Ended Generative UI
|
||||
description: Agent generates UI from an arbitrary component library
|
||||
tags:
|
||||
- generative-ui
|
||||
route: /demos/open-gen-ui
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/open-gen-ui/page.tsx
|
||||
- src/app/api/copilotkit-ogui/route.ts
|
||||
- id: open-gen-ui-advanced
|
||||
name: "Open-Ended Gen UI (Advanced: with frontend function calling)"
|
||||
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_server.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
|
||||
- id: declarative-json-render
|
||||
name: "Declarative UI: json-render"
|
||||
description: Declarative generative UI via @json-render/react — Claude emits a flat element-tree spec the renderer consumes
|
||||
tags:
|
||||
- generative-ui
|
||||
route: /demos/declarative-json-render
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/agent/byoc-json-render-prompt.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/catalog.ts
|
||||
- src/app/demos/declarative-json-render/registry.tsx
|
||||
- src/app/api/copilotkit-declarative-json-render/route.ts
|
||||
- id: declarative-hashbrown
|
||||
name: "Declarative UI: Hashbrown"
|
||||
description: Declarative streaming UI via @hashbrownai/react useJsonParser — Claude emits a catalog-constrained UI envelope
|
||||
tags:
|
||||
- generative-ui
|
||||
route: /demos/declarative-hashbrown
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/agent/byoc-hashbrown-prompt.ts
|
||||
- src/app/demos/declarative-hashbrown/page.tsx
|
||||
- src/app/demos/declarative-hashbrown/hashbrown-renderer.tsx
|
||||
- src/app/api/copilotkit-declarative-hashbrown/route.ts
|
||||
- id: mcp-apps
|
||||
name: MCP Apps
|
||||
description: MCP server-driven UI via activity renderers — runtime auto-applies the MCP Apps middleware and the built-in MCPAppsActivityRenderer paints sandboxed iframes inline in the chat
|
||||
tags:
|
||||
- generative-ui
|
||||
route: /demos/mcp-apps
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/app/demos/mcp-apps/page.tsx
|
||||
- src/app/api/copilotkit-mcp-apps/route.ts
|
||||
- id: multimodal
|
||||
name: Multimodal Attachments
|
||||
description: Image and PDF attachments forwarded to Claude's vision-capable model
|
||||
tags:
|
||||
- interactivity
|
||||
route: /demos/multimodal
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/multimodal/page.tsx
|
||||
- src/app/demos/multimodal/sample-attachment-buttons.tsx
|
||||
- src/app/api/copilotkit-multimodal/route.ts
|
||||
- id: voice
|
||||
name: Voice Input
|
||||
description: Mic + sample audio transcription via a guarded OpenAI Whisper service
|
||||
tags:
|
||||
- interactivity
|
||||
route: /demos/voice
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/voice/page.tsx
|
||||
- src/app/demos/voice/sample-audio-button.tsx
|
||||
- src/app/api/copilotkit-voice/[[...slug]]/route.ts
|
||||
- id: agent-config
|
||||
name: Agent Config Object
|
||||
description: forwardedProps route provider-configured tone/expertise/length into the Claude system prompt per turn
|
||||
tags:
|
||||
- agent-capabilities
|
||||
route: /demos/agent-config
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/agent/agent-config-prompt.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/api/copilotkit-agent-config/route.ts
|
||||
- id: auth
|
||||
name: Authentication
|
||||
description: Bearer-token gate via the V2 runtime onRequest hook with typed 401 surface
|
||||
tags:
|
||||
- agent-capabilities
|
||||
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: tool-rendering-default-catchall
|
||||
name: "Tool Rendering: Default Catch-all"
|
||||
description: Zero-config wildcard renderer via useDefaultRenderTool — built-in tool-call card paints every tool
|
||||
tags:
|
||||
- agent-capabilities
|
||||
route: /demos/tool-rendering-default-catchall
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/app/demos/tool-rendering-default-catchall/page.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: tool-rendering-custom-catchall
|
||||
name: "Tool Rendering: Custom Catch-all"
|
||||
description: Single branded wildcard renderer registered via useDefaultRenderTool({ render })
|
||||
tags:
|
||||
- agent-capabilities
|
||||
route: /demos/tool-rendering-custom-catchall
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/app/demos/tool-rendering-custom-catchall/page.tsx
|
||||
- src/app/demos/tool-rendering-custom-catchall/custom-catchall-renderer.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: reasoning-default
|
||||
name: Reasoning (Default)
|
||||
description: Built-in CopilotChatReasoningMessage card paints Claude extended-thinking output — zero custom slots
|
||||
tags:
|
||||
- agent-capabilities
|
||||
route: /demos/reasoning-default
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/reasoning-default/page.tsx
|
||||
- src/app/demos/reasoning-default/suggestions.ts
|
||||
- src/app/api/copilotkit-reasoning/route.ts
|
||||
- id: reasoning-custom
|
||||
name: Reasoning (Custom)
|
||||
description: Custom reasoningMessage slot renders Claude extended-thinking deltas as a tagged amber banner
|
||||
tags:
|
||||
- agent-capabilities
|
||||
route: /demos/reasoning-custom
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/reasoning-custom/page.tsx
|
||||
- src/app/demos/reasoning-custom/reasoning-block.tsx
|
||||
- src/app/demos/reasoning-custom/suggestions.ts
|
||||
- src/app/api/copilotkit-reasoning/route.ts
|
||||
- id: tool-rendering-reasoning-chain
|
||||
name: "Tool Rendering: Reasoning Chain"
|
||||
description: Reasoning slot + per-tool renderers + wildcard catch-all in one cell
|
||||
tags:
|
||||
- agent-capabilities
|
||||
route: /demos/tool-rendering-reasoning-chain
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/tool-rendering-reasoning-chain/page.tsx
|
||||
- src/app/demos/tool-rendering-reasoning-chain/reasoning-block.tsx
|
||||
- src/app/demos/tool-rendering-reasoning-chain/weather-card.tsx
|
||||
- src/app/demos/tool-rendering-reasoning-chain/flight-list-card.tsx
|
||||
- src/app/api/copilotkit-reasoning/route.ts
|
||||
- id: beautiful-chat
|
||||
name: Beautiful Chat
|
||||
description: Polished brand-themed chat surface over the shared Claude SDK agent with seeded suggestion pills
|
||||
tags:
|
||||
- chat-ui
|
||||
route: /demos/beautiful-chat
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/app/demos/beautiful-chat/page.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: hitl-in-chat
|
||||
name: In-Chat HITL (useHumanInTheLoop — ergonomic API)
|
||||
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/app/demos/hitl-in-chat/page.tsx
|
||||
- src/app/demos/hitl-in-chat/time-picker-card.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: gen-ui-interrupt
|
||||
name: In-Chat HITL (useInterrupt — low-level primitive)
|
||||
description: Interactive time-picker component rendered inline in the chat via useFrontendTool with an async handler — the Promise resolves only when the user picks a slot or cancels
|
||||
tags:
|
||||
- generative-ui
|
||||
route: /demos/gen-ui-interrupt
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.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: Headless Interrupt (testing)
|
||||
description: Resolve interrupts from a plain button grid — no chat, no useInterrupt render prop; the time-picker popup appears in the app surface outside the chat
|
||||
tags:
|
||||
- interactivity
|
||||
route: /demos/interrupt-headless
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent_server.ts
|
||||
- src/app/demos/interrupt-headless/page.tsx
|
||||
- src/app/api/copilotkit/route.ts
|
||||
- id: headless-complete
|
||||
name: Headless Chat (Complete)
|
||||
description: Full custom chat surface built on `useAgent` — exercises the full rendering stack (text, per-tool renderers, frontend components) with backend get_weather / get_stock_price tools and a frontend highlight_note tool
|
||||
tags:
|
||||
- chat-ui
|
||||
route: /demos/headless-complete
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent/headless-complete-prompt.ts
|
||||
- src/app/demos/headless-complete/page.tsx
|
||||
- src/app/demos/headless-complete/chat/chat.tsx
|
||||
- src/app/demos/headless-complete/hooks/use-tool-renderers.tsx
|
||||
- src/app/api/copilotkit-headless-complete/route.ts
|
||||
- id: a2ui-fixed-schema
|
||||
name: A2UI (Fixed Schema)
|
||||
description: Fixed-schema A2UI — backend display_flight tool emits an a2ui_operations container that ships a JSON component tree plus per-call data; frontend catalog binds component names to React renderers
|
||||
tags:
|
||||
- generative-ui
|
||||
route: /demos/a2ui-fixed-schema
|
||||
animated_preview_url:
|
||||
highlight:
|
||||
- src/agent/a2ui-fixed-prompt.ts
|
||||
- src/agent/a2ui_schemas/flight_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
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// 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;
|
||||
+13993
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "@copilotkit/showcase-claude-sdk-typescript",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "concurrently \"next dev --turbopack\" \"npx tsx --watch src/agent_server.ts\"",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-ui/client": "0.0.57",
|
||||
"@ag-ui/claude-agent-sdk": "0.0.3",
|
||||
"@ag-ui/core": "0.0.57",
|
||||
"@ag-ui/encoder": "0.0.57",
|
||||
"@anthropic-ai/sdk": "0.57.0",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.2.58",
|
||||
"@copilotkit/a2ui-renderer": "1.61.2",
|
||||
"@copilotkit/react-core": "1.61.2",
|
||||
"@copilotkit/runtime": "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",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dotenv": "^16.4.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^1.14.0",
|
||||
"next": "^15.5.15",
|
||||
"openai": "5.9.0",
|
||||
"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": "^3.5.0",
|
||||
"yaml": "^2.8.4",
|
||||
"zod": "^3.25.76",
|
||||
"zod4": "npm:zod@^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"concurrently": "^9.1.0",
|
||||
"postcss": "^8.5.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@copilotkit/web-inspector": {
|
||||
"@copilotkit/core": "1.61.2"
|
||||
}
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@copilotkit/web-inspector>@copilotkit/core": "1.61.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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": "claude-sdk-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,
|
||||
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL || "",
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY || "",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bd4aa7b049f1c3e324dfd15af4068d7f8fbf2eae1dd044df270dddc5f38a5c57
|
||||
size 87078
|
||||
@@ -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,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3da2afae36a1a81fd2c02f15e54bfc38b6c22e41655c31a5b54ff1e0e3daab41
|
||||
size 2486
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:01aa5681de99461247543e9215c1e4da3242e26b2bee11593fcdbe209672d973
|
||||
size 10083
|
||||
@@ -0,0 +1,33 @@
|
||||
# QA: Agent Config Object — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/agent-config`
|
||||
- [ ] Verify the three select controls (Tone, Expertise, Response length) render
|
||||
- [ ] Verify the chat interface loads
|
||||
|
||||
### 2. Forwarded props routing
|
||||
|
||||
- [ ] With Tone=professional, Expertise=beginner, Length=detailed, send "Explain what a pointer is"
|
||||
- [ ] Verify Claude responds in multiple paragraphs with analogies / no jargon
|
||||
- [ ] Switch Tone=casual, Expertise=expert, Length=concise
|
||||
- [ ] Send "Same question"
|
||||
- [ ] Verify the reply shifts tone and shortens dramatically
|
||||
|
||||
### 3. Defaults
|
||||
|
||||
- [ ] Reload the page — settings reset to professional / intermediate / concise
|
||||
- [ ] Verify responses follow defaults when forwarded props are absent
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds
|
||||
- Claude adapts observable tone/length across settings
|
||||
- `forwardedProps` is threaded into the agent's system prompt every turn
|
||||
@@ -0,0 +1,62 @@
|
||||
# QA: Agentic Chat — Claude Agent SDK (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,36 @@
|
||||
# QA: Authentication — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Initial state
|
||||
|
||||
- [ ] Navigate to `/demos/auth`
|
||||
- [ ] Verify the banner says "Signed in as demo user" (default = authenticated)
|
||||
- [ ] Verify the "Sign out" button is visible
|
||||
- [ ] Send a message, verify Claude responds
|
||||
|
||||
### 2. Unauthenticated path
|
||||
|
||||
- [ ] Click "Sign out"
|
||||
- [ ] Verify the banner flips to "Signed out"
|
||||
- [ ] Send a message
|
||||
- [ ] Verify a visible 401 error banner appears below the chat with an actionable message
|
||||
- [ ] Verify the page does NOT white-screen — the chat area shows the error boundary fallback
|
||||
|
||||
### 3. Recovery
|
||||
|
||||
- [ ] Click "Sign in"
|
||||
- [ ] Verify the banner returns to "Signed in as demo user"
|
||||
- [ ] Send a new message
|
||||
- [ ] Verify the chat works normally again
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads without crash when starting unauthenticated (if navigated by URL)
|
||||
- 401 surfaces via in-page banner, not console-only
|
||||
- ChatErrorBoundary catches render errors from unauthenticated state
|
||||
@@ -0,0 +1,34 @@
|
||||
# QA: BYOC hashbrown — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (check /api/copilotkit)
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/byoc-hashbrown`
|
||||
- [ ] Verify the chat interface loads with the BYOC hashbrown header
|
||||
- [ ] Verify three suggestion pills are visible ("Sales dashboard", "Revenue by category", "Expense trend")
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
- [ ] Click "Sales dashboard" suggestion
|
||||
- [ ] Wait for Claude to reply. Verify the assistant message renders MetricCards + BarChart + PieChart progressively via hashbrown's `useJsonParser`.
|
||||
- [ ] Click "Revenue by category" suggestion
|
||||
- [ ] Verify a PieChart renders.
|
||||
- [ ] Click "Expense trend" suggestion
|
||||
- [ ] Verify a BarChart renders.
|
||||
|
||||
### 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
|
||||
- Claude responds within 15 seconds
|
||||
- hashbrown progressive parser assembles UI from streaming JSON envelope
|
||||
@@ -0,0 +1,35 @@
|
||||
# QA: BYOC json-render — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (check /api/copilotkit)
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/byoc-json-render`
|
||||
- [ ] Verify the chat interface loads
|
||||
- [ ] Verify three suggestion pills are visible ("Sales dashboard", "Revenue by category", "Expense trend")
|
||||
|
||||
### 2. Feature-Specific Checks
|
||||
|
||||
- [ ] Click "Sales dashboard" suggestion
|
||||
- [ ] Wait for Claude to reply. Verify the assistant message renders a MetricCard and a BarChart (not raw JSON).
|
||||
- [ ] Click "Revenue by category" suggestion
|
||||
- [ ] Verify a PieChart renders.
|
||||
- [ ] Click "Expense trend" suggestion
|
||||
- [ ] Verify a BarChart renders.
|
||||
- [ ] Type "Show me a free-form sentence" and verify the assistant falls back to plain text (Claude should still return JSON per the prompt, but the renderer falls through to the default bubble if the content isn't a valid spec).
|
||||
|
||||
### 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
|
||||
- Claude responds within 15 seconds
|
||||
- Structured JSON output renders through `@json-render/react`'s `<Renderer />` with the three catalog components
|
||||
@@ -0,0 +1,19 @@
|
||||
# QA: Chat Customization (CSS) — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/chat-customization-css
|
||||
- [ ] Verify the chat surface is restyled via the CSS variables in `theme.css`
|
||||
- [ ] Send a basic message
|
||||
- [ ] Verify the agent response renders inside the themed surface
|
||||
- [ ] Verify no console errors
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Default `<CopilotChat />` chrome is visibly re-themed (colors / spacing / typography)
|
||||
- No layout or runtime errors
|
||||
@@ -0,0 +1,19 @@
|
||||
# QA: Chat Customization (Slots) — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/chat-slots
|
||||
- [ ] Verify the custom welcome screen renders on first load
|
||||
- [ ] Verify the custom disclaimer below the input is visible
|
||||
- [ ] Send a message and verify assistant messages use the custom assistant-message slot
|
||||
- [ ] Verify no console errors
|
||||
|
||||
## Expected Results
|
||||
|
||||
- All three slot components (welcome, disclaimer, assistant message) render with custom styling
|
||||
- Chat exchange works identically to the default chat
|
||||
@@ -0,0 +1,19 @@
|
||||
# QA: Declarative Generative UI (A2UI) — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (served via /api/copilotkit-declarative-gen-ui)
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/declarative-gen-ui
|
||||
- [ ] Send a prompt that asks for a generated card / layout (e.g. a product card or status panel)
|
||||
- [ ] Verify the runtime injects the `render_a2ui` tool and the agent calls it
|
||||
- [ ] Verify the client A2UI catalog renders the emitted operations (Card, StatusBadge, Metric, InfoRow, PrimaryButton)
|
||||
- [ ] Verify no console errors
|
||||
|
||||
## Expected Results
|
||||
|
||||
- A2UI-rendered surfaces appear inside the chat thread
|
||||
- No UI errors
|
||||
@@ -0,0 +1,19 @@
|
||||
# QA: Frontend Tools (Async) — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/frontend-tools-async
|
||||
- [ ] Ask the agent to fetch a note (e.g. "Look up my note about project kickoff")
|
||||
- [ ] Verify the async `query_notes` tool fires and resolves
|
||||
- [ ] Verify the agent uses the resolved note content in its reply
|
||||
- [ ] Verify no console errors
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Async tool resolution is awaited and surfaced correctly
|
||||
- No UI errors
|
||||
@@ -0,0 +1,19 @@
|
||||
# QA: Frontend Tools — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/frontend-tools
|
||||
- [ ] Send "Change the background to a sunset gradient"
|
||||
- [ ] Verify the `change_background` frontend tool runs and the page background changes
|
||||
- [ ] Verify the agent confirms the change in a follow-up message
|
||||
- [ ] Verify no console errors
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Frontend tool executes on the client and the agent sees the result
|
||||
- No UI errors or broken layouts
|
||||
@@ -0,0 +1,62 @@
|
||||
# QA: Agentic Generative UI — Claude Agent SDK (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 — Claude Agent SDK (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,19 @@
|
||||
# QA: Headless Chat (Simple) — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/headless-simple
|
||||
- [ ] Verify a minimal custom chat surface renders (not `<CopilotChat />`)
|
||||
- [ ] Verify the input field and send affordance are visible
|
||||
- [ ] Send a message and verify the agent's response appears in the custom list
|
||||
- [ ] Verify no console errors
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Custom UI built via `useAgent` works end-to-end against the Claude pass-through
|
||||
- No layout or runtime errors
|
||||
@@ -0,0 +1,21 @@
|
||||
# QA: In-App Human-in-the-Loop — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/hitl-in-app
|
||||
- [ ] Ask the agent to perform an action that requires approval
|
||||
- [ ] Verify the approval dialog renders OUTSIDE the chat (app-level modal)
|
||||
- [ ] Approve the action and verify the agent continues with the user's decision
|
||||
- [ ] Repeat with rejection and verify the agent honors it
|
||||
- [ ] Verify no console errors
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Async frontend tool blocks until user resolves the modal
|
||||
- Agent outcome differs between approve and reject
|
||||
- No UI errors
|
||||
@@ -0,0 +1,57 @@
|
||||
# QA: Human in the Loop — Claude Agent SDK (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,21 @@
|
||||
# QA: MCP Apps — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (served via /api/copilotkit-mcp-apps)
|
||||
- Excalidraw MCP server is reachable (https://mcp.excalidraw.com)
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/mcp-apps
|
||||
- [ ] Click the "Draw a flowchart" suggestion
|
||||
- [ ] Verify the MCP middleware fetches the Excalidraw UI resource
|
||||
- [ ] Verify the `MCPAppsActivityRenderer` mounts a sandboxed iframe inline in chat
|
||||
- [ ] Verify you can interact with the Excalidraw sandbox
|
||||
- [ ] Verify no console errors
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Inline MCP app iframe renders with working Excalidraw surface
|
||||
- No UI errors
|
||||
@@ -0,0 +1,46 @@
|
||||
# QA: Multimodal Attachments — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (check /api/copilotkit)
|
||||
- Backend uses Claude Sonnet (vision-capable) for this route
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/multimodal`
|
||||
- [ ] Verify the chat interface loads
|
||||
- [ ] Verify the header "Multimodal attachments"
|
||||
- [ ] Verify two sample-attachment buttons are visible: "Try with sample image" and "Try with sample PDF"
|
||||
|
||||
### 2. Image Attachments
|
||||
|
||||
- [ ] Click "Try with sample image"
|
||||
- [ ] Type "What do you see in this image?"
|
||||
- [ ] Verify Claude describes the image content
|
||||
|
||||
### 3. PDF Attachments
|
||||
|
||||
- [ ] Click "Try with sample PDF"
|
||||
- [ ] Type "What's this document about?"
|
||||
- [ ] Verify Claude summarizes the PDF content
|
||||
|
||||
### 4. Upload via paperclip
|
||||
|
||||
- [ ] Click the paperclip icon in the composer
|
||||
- [ ] Upload a local .png or .pdf (< 10MB)
|
||||
- [ ] Verify attachment appears in the composer
|
||||
- [ ] Send a question about the attachment
|
||||
|
||||
### 5. Error Handling
|
||||
|
||||
- [ ] Verify files >10MB are rejected with a visible error
|
||||
- [ ] Verify invalid mime types are rejected
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds
|
||||
- Claude describes image / PDF content accurately within 20 seconds
|
||||
- No console errors during normal usage
|
||||
@@ -0,0 +1,19 @@
|
||||
# QA: Open-Ended Generative UI (Advanced) — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (served via /api/copilotkit-ogui)
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/open-gen-ui-advanced
|
||||
- [ ] Issue a prompt that exercises a sandbox function (see `suggestions.ts`)
|
||||
- [ ] Verify the generated iframe can invoke the page-provided `sandboxFunctions`
|
||||
- [ ] Verify the host page reacts to the sandboxed call
|
||||
- [ ] Verify no console errors
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Host <-> sandbox function bridge works end-to-end
|
||||
- No UI errors
|
||||
@@ -0,0 +1,20 @@
|
||||
# QA: Open-Ended Generative UI — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (served via /api/copilotkit-ogui)
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/open-gen-ui
|
||||
- [ ] Click the "3D axis visualization" suggestion
|
||||
- [ ] Verify the runtime's `generateSandboxedUi` tool is called and an `open-generative-ui` activity is emitted
|
||||
- [ ] Verify the built-in `OpenGenerativeUIActivityRenderer` mounts the sandboxed iframe
|
||||
- [ ] Verify the visualization renders as described in the suggestion
|
||||
- [ ] Verify no console errors
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Agent-authored HTML / CSS renders inside a sandboxed iframe
|
||||
- No UI errors
|
||||
@@ -0,0 +1,35 @@
|
||||
# QA: Pre-Built Popup — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (check /api/health)
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to the prebuilt-popup demo page
|
||||
- [ ] Verify the main content area shows the popup demo heading
|
||||
- [ ] Verify the `<CopilotPopup />` floats over the page with `defaultOpen={true}`
|
||||
- [ ] Verify the popup launcher (floating bubble) is visible
|
||||
- [ ] Close the popup and verify the launcher remains
|
||||
- [ ] Re-open the popup from the launcher
|
||||
|
||||
### 2. Chat Interaction
|
||||
|
||||
- [ ] Send a basic message in the popup chat
|
||||
- [ ] Verify the agent responds inside the popup overlay
|
||||
- [ ] Verify the "Say hi" suggestion is visible
|
||||
- [ ] Click the suggestion and verify the agent replies
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Verify no console errors on page load
|
||||
- [ ] Verify main page content remains scrollable while popup is open
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Popup floats over page content with a custom chat input placeholder
|
||||
- Chat exchange works identically to the full-page chat demo
|
||||
- No UI errors or broken layouts
|
||||
@@ -0,0 +1,35 @@
|
||||
# QA: Pre-Built Sidebar — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy (check /api/health)
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to the prebuilt-sidebar demo page
|
||||
- [ ] Verify the main content area shows the sidebar demo heading
|
||||
- [ ] Verify the `<CopilotSidebar />` renders with `defaultOpen={true}`
|
||||
- [ ] Verify the sidebar launcher button is visible
|
||||
- [ ] Close the sidebar and verify the launcher remains
|
||||
- [ ] Re-open the sidebar from the launcher
|
||||
|
||||
### 2. Chat Interaction
|
||||
|
||||
- [ ] Send a basic message ("Hello") in the sidebar chat
|
||||
- [ ] Verify the agent responds inside the sidebar panel
|
||||
- [ ] Verify the "Say hi" suggestion is visible
|
||||
- [ ] Click the suggestion and verify the agent replies
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
- [ ] Verify no console errors on page load
|
||||
- [ ] Verify main content remains interactive while sidebar is open
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Page renders with sidebar docked on the right
|
||||
- Chat exchange works identically to the full-page chat demo
|
||||
- No UI errors or broken layouts
|
||||
@@ -0,0 +1,20 @@
|
||||
# QA: Readonly State (Agent Context) — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
|
||||
## Test Steps
|
||||
|
||||
- [ ] Navigate to /demos/readonly-state-agent-context
|
||||
- [ ] Verify the page renders the read-only context surface on the left
|
||||
- [ ] Ask the agent about the context values (e.g. "What is my current plan?")
|
||||
- [ ] Verify the agent's reply references the context values exposed via `useAgentContext`
|
||||
- [ ] Update the context on the page and verify the agent's next answer reflects the change
|
||||
- [ ] Verify no console errors
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Agent consistently sees the frontend-provided context
|
||||
- No UI errors
|
||||
@@ -0,0 +1,76 @@
|
||||
# QA: Shared State (Read + Write) — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo deployed and accessible at `/demos/shared-state-read-write`
|
||||
- Agent backend healthy (`GET /api/copilotkit-shared-state-read-write`)
|
||||
- `ANTHROPIC_API_KEY` set on the agent backend
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Page loads
|
||||
|
||||
- [ ] Navigate to `/demos/shared-state-read-write`
|
||||
- [ ] Sidebar renders the **Your preferences** card with empty Name,
|
||||
tone = `casual`, language = `English`, no interests selected
|
||||
- [ ] Sidebar renders the **Agent notes** card showing "No notes yet."
|
||||
- [ ] Chat panel loads on the right with placeholder
|
||||
"Chat with the agent..."
|
||||
- [ ] Three suggestion chips visible:
|
||||
"Greet me", "Remember something", "Plan a weekend"
|
||||
- [ ] No console errors
|
||||
|
||||
### 2. UI -> agent (write) — preferences steer the model
|
||||
|
||||
- [ ] Set Name = `Alex`
|
||||
- [ ] Set Tone = `playful`
|
||||
- [ ] Pick interests `Cooking` and `Travel`
|
||||
- [ ] The "Shared state" preview at the bottom of the card updates to
|
||||
reflect the new JSON
|
||||
- [ ] Click the "Greet me" suggestion (or send "Hi, who am I?")
|
||||
- [ ] Agent response addresses the user as "Alex" and uses a playful
|
||||
tone (exclamations, casual phrasing)
|
||||
- [ ] Agent does NOT use a formal register
|
||||
|
||||
### 3. Agent -> UI (read) — set_notes tool writes shared state
|
||||
|
||||
- [ ] Send: "Remember that I prefer morning meetings and that I don't
|
||||
eat dairy."
|
||||
- [ ] Within ~10s the **Agent notes** card flips from "No notes yet."
|
||||
to a list of 1-3 short bullet items including a morning-meetings
|
||||
note and a no-dairy note
|
||||
- [ ] The notes appear without a page reload (live STATE_SNAPSHOT)
|
||||
- [ ] Send: "Also, I work in Eastern Time."
|
||||
- [ ] Notes card now shows the previous notes PLUS a timezone note
|
||||
(agent passed the FULL list, not just the new one)
|
||||
|
||||
### 4. UI -> agent (write back) — clearing notes
|
||||
|
||||
- [ ] Click the **Clear** button on the notes card
|
||||
- [ ] Notes card returns to "No notes yet."
|
||||
- [ ] Preferences are unchanged
|
||||
- [ ] Send: "What do you remember about me?"
|
||||
- [ ] Agent response references preferences (name, tone) but no notes
|
||||
|
||||
### 5. Persistence across turns
|
||||
|
||||
- [ ] After clearing, change Tone to `formal` and Language to `Spanish`
|
||||
- [ ] Send: "Saludos."
|
||||
- [ ] Agent replies in Spanish using a formal register
|
||||
- [ ] Send a follow-up; agent still respects the same preferences
|
||||
|
||||
### 6. Error handling
|
||||
|
||||
- [ ] Send an empty message — handled gracefully (input ignored or no-op)
|
||||
- [ ] If `ANTHROPIC_API_KEY` is missing on the backend, the chat shows
|
||||
an error message via the AG-UI run-error path
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Preferences edits propagate into agent state on every keystroke /
|
||||
toggle (via `agent.setState`)
|
||||
- Agent's system prompt reflects the latest preferences each turn
|
||||
- `set_notes` mutations land in the UI within ~1s of the tool call
|
||||
ending, via `STATE_SNAPSHOT`
|
||||
- No full page reloads during the demo
|
||||
- No console errors in the happy path
|
||||
@@ -0,0 +1,75 @@
|
||||
# QA: Shared State (Reading) — Claude Agent SDK (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 — Claude Agent SDK (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) — Claude Agent SDK (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,78 @@
|
||||
# QA: Sub-Agents — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo deployed and accessible at `/demos/subagents`
|
||||
- Agent backend healthy (`GET /api/copilotkit-subagents`)
|
||||
- `ANTHROPIC_API_KEY` set on the agent backend
|
||||
- Optional: `ANTHROPIC_SUBAGENT_MODEL` (or `CLAUDE_SUBAGENT_MODEL`) set
|
||||
if you want the secondary calls to use a different model than the
|
||||
supervisor
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Page loads
|
||||
|
||||
- [ ] Navigate to `/demos/subagents`
|
||||
- [ ] Main panel shows a **Sub-agent delegations** card with header
|
||||
`0 calls` and an italic placeholder "Ask the supervisor to
|
||||
complete a task."
|
||||
- [ ] Right panel shows the chat with placeholder
|
||||
"Give the supervisor a task..."
|
||||
- [ ] Three suggestion chips visible:
|
||||
"Write a blog post", "Explain a topic", "Summarize a topic"
|
||||
- [ ] No console errors
|
||||
|
||||
### 2. Single delegation flow
|
||||
|
||||
- [ ] Click "Explain a topic" (or send a similar prompt)
|
||||
- [ ] Supervisor running badge appears in the header
|
||||
- [ ] A delegation entry appears with sub-agent = `Research`,
|
||||
status = `running`, an italic "Waiting for sub-agent…" placeholder
|
||||
- [ ] Within ~15s the entry flips to `completed` and shows a bulleted
|
||||
list of facts
|
||||
- [ ] A second delegation entry (`Writing`) appears with `running`,
|
||||
then flips to `completed` showing a 1-paragraph draft
|
||||
- [ ] A third entry (`Critique`) appears, runs, and completes with
|
||||
2-3 critiques
|
||||
- [ ] Delegation count in the header reads `3 calls`
|
||||
- [ ] Supervisor running badge disappears once the run finishes
|
||||
- [ ] Supervisor's final chat message is a brief summary
|
||||
|
||||
### 3. Live state streaming
|
||||
|
||||
- [ ] During step 2, observe the panel updating without scrolling /
|
||||
reloading: each `running -> completed` transition is visible
|
||||
- [ ] The order of entries in the panel matches the order the
|
||||
supervisor called them (research first, then writing, then
|
||||
critique)
|
||||
- [ ] Each entry's task text matches what the supervisor passed (e.g.
|
||||
writing entry references the research output)
|
||||
|
||||
### 4. Multi-turn
|
||||
|
||||
- [ ] After the first run completes, send a follow-up like
|
||||
"Now do the same for renewable energy storage"
|
||||
- [ ] New delegations are appended to the existing log (count goes
|
||||
up); previous entries remain visible
|
||||
|
||||
### 5. Error handling
|
||||
|
||||
- [ ] Send an empty message — handled gracefully
|
||||
- [ ] If a sub-agent call fails (e.g. revoke API key transiently),
|
||||
the delegation entry shows status `failed` with the error
|
||||
message; the supervisor continues / surfaces the failure
|
||||
instead of fabricating a result
|
||||
- [ ] No unhandled console errors in the happy path
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Each sub-agent invocation produces exactly one entry in the
|
||||
delegation log
|
||||
- Entries transition `running` -> `completed` (or `failed`) live, via
|
||||
AG-UI `STATE_SNAPSHOT`
|
||||
- Status badges are colour-coded (yellow for running, green for
|
||||
completed, red for failed)
|
||||
- The supervisor's text replies stay short — the bulk of the output
|
||||
lives in the delegation log
|
||||
- Total run time is bounded (no runaway loops); MAX_TOOL_ITERATIONS = 10
|
||||
@@ -0,0 +1,61 @@
|
||||
# QA: Tool Rendering — Claude Agent SDK (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,40 @@
|
||||
# QA: Voice Input — Claude Agent SDK (TypeScript)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Demo is deployed and accessible
|
||||
- Agent backend is healthy
|
||||
- `OPENAI_API_KEY` is set on the deployment (Whisper transcription)
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Basic Functionality
|
||||
|
||||
- [ ] Navigate to `/demos/voice`
|
||||
- [ ] Verify the chat interface loads with the header "Voice input"
|
||||
- [ ] Verify a "Play sample" button is visible
|
||||
- [ ] Verify the composer mic button is rendered
|
||||
|
||||
### 2. Sample audio transcription
|
||||
|
||||
- [ ] Click "Play sample"
|
||||
- [ ] Wait for status to change from "Transcribing…" back to idle
|
||||
- [ ] Verify the chat input is populated with text similar to "What is the weather in Tokyo?"
|
||||
|
||||
### 3. Mic recording
|
||||
|
||||
- [ ] Click the mic button in the composer
|
||||
- [ ] Grant microphone permission if prompted
|
||||
- [ ] Speak briefly then click again to stop
|
||||
- [ ] Verify transcribed text appears in the composer
|
||||
|
||||
### 4. Error handling (key missing)
|
||||
|
||||
- [ ] In a deployment without `OPENAI_API_KEY`, click the sample button
|
||||
- [ ] Verify the demo surfaces a clean 401 error (not 500/503)
|
||||
|
||||
## Expected Results
|
||||
|
||||
- Chat loads within 3 seconds
|
||||
- Transcription completes within 8 seconds for sample audio
|
||||
- Auth-failed path returns HTTP 401 with a readable error message
|
||||
+90
@@ -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();
|
||||
});
|
||||
});
|
||||
+79
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
+124
@@ -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);
|
||||
});
|
||||
});
|
||||
+26
@@ -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");
|
||||
});
|
||||
});
|
||||
+52
@@ -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,27 @@
|
||||
import type Anthropic from "@anthropic-ai/sdk";
|
||||
|
||||
export const A2UI_DYNAMIC_SYSTEM_PROMPT = [
|
||||
"You are a demo assistant for Declarative Generative UI (A2UI - Dynamic Schema).",
|
||||
"Whenever a response would benefit from a rich visual, call generate_a2ui.",
|
||||
"Use it for dashboards, KPI summaries, status reports, pie charts, bar charts,",
|
||||
"card layouts, info grids, and anything more structured than plain text.",
|
||||
"generate_a2ui takes one context string summarizing the user request.",
|
||||
"Keep chat replies to one short sentence and let the UI do the talking.",
|
||||
].join(" ");
|
||||
|
||||
export const GENERATE_A2UI_TOOL_SCHEMA: Anthropic.Tool = {
|
||||
name: "generate_a2ui",
|
||||
description:
|
||||
"Generate dynamic A2UI components based on the conversation. A secondary Claude call designs the UI schema and data using the registered catalog.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
context: {
|
||||
type: "string",
|
||||
description:
|
||||
"Conversation context summary the secondary Claude call should design UI from.",
|
||||
},
|
||||
},
|
||||
required: ["context"],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* A2UI Fixed Schema demo — backend agent constants.
|
||||
*
|
||||
* Mirrors `agents/a2ui_fixed.py` in the langgraph-python and ag2 references.
|
||||
* The component tree (schema) lives on the backend as JSON; the agent only
|
||||
* streams *data* into the data model at runtime via the `display_flight`
|
||||
* tool. The frontend catalog (see
|
||||
* `src/app/demos/a2ui-fixed-schema/a2ui/catalog.ts`) binds component names
|
||||
* from the JSON schema to React renderers.
|
||||
*
|
||||
* The dedicated runtime route at
|
||||
* `src/app/api/copilotkit-a2ui-fixed-schema/route.ts` runs the A2UI
|
||||
* middleware with `injectA2UITool: false` because this backend owns the
|
||||
* rendering tool itself.
|
||||
*
|
||||
* The schema is imported as JSON (resolveJsonModule: true in tsconfig) so
|
||||
* the build doesn't depend on filesystem layout at runtime.
|
||||
*/
|
||||
|
||||
import type Anthropic from "@anthropic-ai/sdk";
|
||||
|
||||
// @region[backend-render-operations]
|
||||
// @region[backend-schema-json-load]
|
||||
import flightSchema from "./a2ui_schemas/flight_schema.json";
|
||||
|
||||
export const A2UI_FIXED_CATALOG_ID = "copilotkit://flight-fixed-catalog";
|
||||
export const A2UI_FIXED_SURFACE_ID = "flight-fixed-schema";
|
||||
|
||||
export const FLIGHT_SCHEMA: unknown[] = flightSchema as unknown[];
|
||||
// @endregion[backend-schema-json-load]
|
||||
|
||||
export const A2UI_FIXED_SYSTEM_PROMPT =
|
||||
"You help users find flights. When asked about a flight, call " +
|
||||
"display_flight with origin (3-letter code), destination (3-letter " +
|
||||
"code), airline, and price (e.g. '$289'). Keep any chat reply to one " +
|
||||
"short sentence.";
|
||||
|
||||
export const DISPLAY_FLIGHT_TOOL_SCHEMA: Anthropic.Tool = {
|
||||
name: "display_flight",
|
||||
description:
|
||||
"Show a flight card for the given trip. Emits an a2ui_operations " +
|
||||
"container the frontend renders into a flight card via the fixed " +
|
||||
"schema catalog.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
origin: {
|
||||
type: "string",
|
||||
description: "Origin airport code, e.g. 'SFO'",
|
||||
},
|
||||
destination: {
|
||||
type: "string",
|
||||
description: "Destination airport code, e.g. 'JFK'",
|
||||
},
|
||||
airline: { type: "string", description: "Airline name, e.g. 'United'" },
|
||||
price: { type: "string", description: "Price string, e.g. '$289'" },
|
||||
},
|
||||
required: ["origin", "destination", "airline", "price"],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the `a2ui_operations` payload the A2UI runtime middleware
|
||||
* detects in tool results and forwards to the frontend renderer.
|
||||
*
|
||||
* Ops MUST use the v0.9 NESTED operation shape
|
||||
* (`{ version, createSurface: {...} }` / `updateComponents` /
|
||||
* `updateDataModel`) that `@ag-ui/a2ui-middleware`'s
|
||||
* `getOperationSurfaceId` and the React A2UI renderer walk. The legacy
|
||||
* flat shape (`{ type: "create_surface", surfaceId, ... }`) looks
|
||||
* plausible but the middleware's matcher never recognizes it — every op
|
||||
* lands on the fallback "default" surface and the renderer never
|
||||
* receives the schema, so the `a2ui-fixed-card` never mounts. See the
|
||||
* identical fix note in `showcase/shared/python/tools/generate_a2ui.py`
|
||||
* (`build_a2ui_operations_from_tool_call`).
|
||||
*/
|
||||
export function buildDisplayFlightOperations(input: {
|
||||
origin: string;
|
||||
destination: string;
|
||||
airline: string;
|
||||
price: string;
|
||||
}): { a2ui_operations: unknown[] } {
|
||||
return {
|
||||
a2ui_operations: [
|
||||
{
|
||||
version: "v0.9",
|
||||
createSurface: {
|
||||
surfaceId: A2UI_FIXED_SURFACE_ID,
|
||||
catalogId: A2UI_FIXED_CATALOG_ID,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "v0.9",
|
||||
updateComponents: {
|
||||
surfaceId: A2UI_FIXED_SURFACE_ID,
|
||||
components: FLIGHT_SCHEMA,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "v0.9",
|
||||
updateDataModel: {
|
||||
surfaceId: A2UI_FIXED_SURFACE_ID,
|
||||
path: "/",
|
||||
value: input,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
// @endregion[backend-render-operations]
|
||||
@@ -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,91 @@
|
||||
/**
|
||||
* Builds the Claude system prompt for the agent-config demo from the
|
||||
* frontend-supplied `forwardedProps`. Mirrors the LangGraph Python
|
||||
* implementation (`agent_config_agent.py`) so the two showcases behave
|
||||
* identically for tone/expertise/responseLength.
|
||||
*
|
||||
* `forwardedProps` arrives at the agent via AG-UI's `RunAgentInput`. The
|
||||
* CopilotKit runtime forwards the provider's `properties` prop verbatim
|
||||
* into `forwardedProps`, so the Claude agent can read the tone / expertise
|
||||
* / responseLength keys directly — no LangGraph-style configurable
|
||||
* repacking is required for this runtime.
|
||||
*/
|
||||
|
||||
// @region[agent-config-setup]
|
||||
export type Tone = "professional" | "casual" | "enthusiastic";
|
||||
export type Expertise = "beginner" | "intermediate" | "expert";
|
||||
export type ResponseLength = "concise" | "detailed";
|
||||
|
||||
const DEFAULT_TONE: Tone = "professional";
|
||||
const DEFAULT_EXPERTISE: Expertise = "intermediate";
|
||||
const DEFAULT_RESPONSE_LENGTH: ResponseLength = "concise";
|
||||
|
||||
const VALID_TONES: ReadonlySet<string> = new Set<Tone>([
|
||||
"professional",
|
||||
"casual",
|
||||
"enthusiastic",
|
||||
]);
|
||||
const VALID_EXPERTISE: ReadonlySet<string> = new Set<Expertise>([
|
||||
"beginner",
|
||||
"intermediate",
|
||||
"expert",
|
||||
]);
|
||||
const VALID_RESPONSE_LENGTHS: ReadonlySet<string> = new Set<ResponseLength>([
|
||||
"concise",
|
||||
"detailed",
|
||||
]);
|
||||
|
||||
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 readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
export function buildAgentConfigSystemPrompt(
|
||||
forwardedProps: Record<string, unknown>,
|
||||
): string {
|
||||
const rawTone = readString(forwardedProps.tone) ?? DEFAULT_TONE;
|
||||
const rawExpertise =
|
||||
readString(forwardedProps.expertise) ?? DEFAULT_EXPERTISE;
|
||||
const rawLength =
|
||||
readString(forwardedProps.responseLength) ?? DEFAULT_RESPONSE_LENGTH;
|
||||
|
||||
const tone = (VALID_TONES.has(rawTone) ? rawTone : DEFAULT_TONE) as Tone;
|
||||
const expertise = (
|
||||
VALID_EXPERTISE.has(rawExpertise) ? rawExpertise : DEFAULT_EXPERTISE
|
||||
) as Expertise;
|
||||
const responseLength = (
|
||||
VALID_RESPONSE_LENGTHS.has(rawLength) ? rawLength : DEFAULT_RESPONSE_LENGTH
|
||||
) as ResponseLength;
|
||||
|
||||
return [
|
||||
"You are a helpful assistant.",
|
||||
"",
|
||||
`Tone: ${TONE_RULES[tone]}`,
|
||||
`Expertise level: ${EXPERTISE_RULES[expertise]}`,
|
||||
`Response length: ${LENGTH_RULES[responseLength]}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export const AGENT_CONFIG_DEFAULT_SYSTEM_PROMPT = buildAgentConfigSystemPrompt(
|
||||
{},
|
||||
);
|
||||
// @endregion[agent-config-setup]
|
||||
@@ -0,0 +1,170 @@
|
||||
export interface DataRow {
|
||||
date: string;
|
||||
category: string;
|
||||
subcategory: string;
|
||||
amount: string;
|
||||
type: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 = generateMockData();
|
||||
|
||||
export function queryDataImpl(_query: string): DataRow[] {
|
||||
return MOCK_DATA;
|
||||
}
|
||||
|
||||
const CATALOG_ID = "copilotkit://app-dashboard-catalog";
|
||||
const FLIGHT_SURFACE_ID = "flight-search-results";
|
||||
|
||||
function buildFlightComponents(
|
||||
flights: Flight[],
|
||||
): Array<Record<string, unknown>> {
|
||||
const cardIds: string[] = [];
|
||||
const cards = flights.map((flight, index) => {
|
||||
const id = `flight-card-${index}`;
|
||||
cardIds.push(id);
|
||||
return {
|
||||
id,
|
||||
component: "FlightCard",
|
||||
airline: flight.airline ?? "",
|
||||
airlineLogo: flight.airlineLogo ?? "",
|
||||
flightNumber: flight.flightNumber ?? "",
|
||||
origin: flight.origin ?? "",
|
||||
destination: flight.destination ?? "",
|
||||
date: flight.date ?? "",
|
||||
departureTime: flight.departureTime ?? "",
|
||||
arrivalTime: flight.arrivalTime ?? "",
|
||||
duration: flight.duration ?? "",
|
||||
status: flight.status ?? "",
|
||||
statusColor: flight.statusColor ?? "",
|
||||
price: flight.price ?? "",
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
id: "root",
|
||||
component: "Row",
|
||||
children: cardIds,
|
||||
gap: 16,
|
||||
},
|
||||
...cards,
|
||||
];
|
||||
}
|
||||
|
||||
export function renderFlightsImpl(flights: Flight[]): {
|
||||
a2ui_operations: Array<Record<string, unknown>>;
|
||||
} {
|
||||
return {
|
||||
a2ui_operations: [
|
||||
{
|
||||
version: "v0.9",
|
||||
createSurface: {
|
||||
surfaceId: FLIGHT_SURFACE_ID,
|
||||
catalogId: CATALOG_ID,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "v0.9",
|
||||
updateComponents: {
|
||||
surfaceId: FLIGHT_SURFACE_ID,
|
||||
components: buildFlightComponents(flights),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Claude system prompt for the byoc-hashbrown demo.
|
||||
*
|
||||
* The frontend parses the streaming response with
|
||||
* `@hashbrownai/react`'s `useJsonParser` + `useUiKit`, which expects a
|
||||
* single top-level JSON envelope:
|
||||
*
|
||||
* {
|
||||
* "ui": [
|
||||
* { "metric": { "props": { ... } } },
|
||||
* { "pieChart": { "props": { ... } } },
|
||||
* { "barChart": { "props": { ... } } },
|
||||
* { "dealCard": { "props": { ... } } },
|
||||
* { "Markdown": { "props": { ... } } }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* Each entry is a single-key object `{ <tagName>: { props: { ... } } }`.
|
||||
* `data` props on charts are JSON-encoded strings — this is a deliberate
|
||||
* quirk of the hashbrown schema that keeps the shape stable under partial
|
||||
* streaming.
|
||||
*/
|
||||
export 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}]"}}}]}
|
||||
`;
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Claude system prompt for the byoc-json-render demo.
|
||||
*
|
||||
* Mirrors the langgraph-python `byoc_json_render_agent` prompt verbatim
|
||||
* so behaviour lines up with the reference. The frontend expects a single
|
||||
* JSON object shaped like `@json-render/react`'s flat spec format
|
||||
* (`{ root, elements }`) — any prose / markdown fences would break the
|
||||
* progressive parser in `json-render-renderer.tsx`.
|
||||
*/
|
||||
export const BYOC_JSON_RENDER_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.
|
||||
`.trim();
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Gen UI (Agent-based) demo — backend agent constants.
|
||||
*
|
||||
* Mirrors `src/agent/gen-ui-agent.ts` in the langgraph-typescript sibling
|
||||
* (itself ported from `src/agents/gen_ui_agent.py` in langgraph-python).
|
||||
* The agent plans a task as 3 steps and walks each one
|
||||
* pending -> in_progress -> completed, calling the backend `set_steps`
|
||||
* tool after every transition. Each call REPLACES `state.steps`
|
||||
* wholesale (last-write-wins) and is streamed to the UI via
|
||||
* STATE_SNAPSHOT, so the frontend's `InlineAgentStateCard` re-renders a
|
||||
* live progress card from `useAgent` state.
|
||||
*
|
||||
* The step shape matches the frontend's `Step` type
|
||||
* (`src/app/demos/gen-ui-agent/InlineAgentStateCard.tsx`):
|
||||
* { id: string, title: string, status: "pending" | "in_progress" | "completed" }
|
||||
*/
|
||||
|
||||
import type Anthropic from "@anthropic-ai/sdk";
|
||||
|
||||
export const GEN_UI_AGENT_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.";
|
||||
|
||||
export const SET_STEPS_TOOL_SCHEMA: Anthropic.Tool = {
|
||||
name: "set_steps",
|
||||
description:
|
||||
"Publish the current plan + step statuses. Call this every time a " +
|
||||
"step transitions (including the first enumeration of steps). The " +
|
||||
"list REPLACES the previous one, so always pass the full list.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
steps: {
|
||||
type: "array",
|
||||
description: "The full list of steps with their current statuses.",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
description: "Unique identifier for the step.",
|
||||
},
|
||||
title: {
|
||||
type: "string",
|
||||
description: "Short description of the step.",
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
enum: ["pending", "in_progress", "completed"],
|
||||
description: "Current status of the step.",
|
||||
},
|
||||
},
|
||||
required: ["id", "title", "status"],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["steps"],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Headless Chat (Complete) demo — backend agent constants.
|
||||
*
|
||||
* Mirrors `agents/headless_complete.py` in the langgraph-python reference
|
||||
* and the equivalent claude-sdk-python set-up. The cell exercises every
|
||||
* CopilotKit rendering surface in a fully headless chat composed from
|
||||
* `useAgent` (no `<CopilotChat />`). To exercise those surfaces this
|
||||
* agent ships:
|
||||
*
|
||||
* - two backend tools (`get_weather`, `get_stock_price`) — render via
|
||||
* app-registered `useRenderTool` renderers on the frontend,
|
||||
* - access to a frontend-registered `useComponent` tool
|
||||
* (`highlight_note`) — the agent "calls" it (tool definition is
|
||||
* forwarded by the AG-UI client) and the UI flows through the same
|
||||
* `useRenderToolCall` path.
|
||||
*
|
||||
* The system prompt nudges the model toward the right surface per user
|
||||
* question and falls back to plain text otherwise.
|
||||
*/
|
||||
|
||||
import type Anthropic from "@anthropic-ai/sdk";
|
||||
|
||||
export const HEADLESS_COMPLETE_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.\n\n" +
|
||||
"Routing rules:\n" +
|
||||
" - If the user asks about weather for a place, call `get_weather` " +
|
||||
"with the location.\n" +
|
||||
" - If the user asks about a stock or ticker (AAPL, TSLA, MSFT, ...), " +
|
||||
"call `get_stock_price` with the ticker.\n" +
|
||||
" - If the user asks for a revenue, sales, or trend chart, call " +
|
||||
"`get_revenue_chart`.\n" +
|
||||
" - 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.\n" +
|
||||
" - Otherwise, reply in plain text.\n\n" +
|
||||
"After a tool returns, write one short sentence summarizing the " +
|
||||
"result. Never fabricate data a tool could provide.";
|
||||
|
||||
export const HEADLESS_GET_WEATHER_TOOL_SCHEMA: Anthropic.Tool = {
|
||||
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. Use this whenever the user asks about " +
|
||||
"weather anywhere.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
location: {
|
||||
type: "string",
|
||||
description: "The city or region to get weather for.",
|
||||
},
|
||||
},
|
||||
required: ["location"],
|
||||
},
|
||||
};
|
||||
|
||||
export const HEADLESS_GET_STOCK_PRICE_TOOL_SCHEMA: Anthropic.Tool = {
|
||||
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. Use this whenever the user asks about a stock " +
|
||||
"price.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
ticker: {
|
||||
type: "string",
|
||||
description: "Stock ticker symbol, e.g. 'AAPL'.",
|
||||
},
|
||||
},
|
||||
required: ["ticker"],
|
||||
},
|
||||
};
|
||||
|
||||
export const HEADLESS_GET_REVENUE_CHART_TOOL_SCHEMA: Anthropic.Tool = {
|
||||
name: "get_revenue_chart",
|
||||
description:
|
||||
"Return a mock six-month revenue trend chart. Use this when the " +
|
||||
"user asks for revenue, sales, or trend charts.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
};
|
||||
|
||||
export function getWeatherImpl(location: string): Record<string, unknown> {
|
||||
return {
|
||||
city: location,
|
||||
temperature: 68,
|
||||
humidity: 55,
|
||||
wind_speed: 10,
|
||||
conditions: "Sunny",
|
||||
};
|
||||
}
|
||||
|
||||
export function getStockPriceImpl(ticker: string): Record<string, unknown> {
|
||||
return {
|
||||
ticker: ticker.toUpperCase(),
|
||||
price_usd: 189.42,
|
||||
change_pct: 1.27,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRevenueChartImpl(): Record<string, unknown> {
|
||||
return {
|
||||
title: "Revenue trend",
|
||||
subtitle: "Last six months, USD thousands",
|
||||
data: [
|
||||
{ label: "Jan", value: 42 },
|
||||
{ label: "Feb", value: 48 },
|
||||
{ label: "Mar", value: 53 },
|
||||
{ label: "Apr", value: 57 },
|
||||
{ label: "May", value: 63 },
|
||||
{ label: "Jun", value: 71 },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* Agent Server for Claude Agent SDK (TypeScript)
|
||||
*
|
||||
* Express server that hosts a Claude-powered agent backend.
|
||||
* The Next.js CopilotKit runtime proxies requests here via AG-UI protocol.
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import express from "express";
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import { EventEncoder } from "@ag-ui/encoder";
|
||||
import type { BaseEvent, RunAgentInput, Message } from "@ag-ui/core";
|
||||
import { EventType } from "@ag-ui/core";
|
||||
import * as dotenv from "dotenv";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
dotenv.config({ path: ".env.local" });
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
app.use(express.json({ limit: "2mb" }));
|
||||
|
||||
const HOST = process.env.AGENT_HOST || "0.0.0.0";
|
||||
const PORT = parseInt(process.env.AGENT_PORT || "8123", 10);
|
||||
const CLAUDE_MODEL = process.env.CLAUDE_MODEL || "claude-3-5-haiku-20241022";
|
||||
|
||||
if (!process.env.ANTHROPIC_API_KEY) {
|
||||
console.error("[agent_server] FATAL: ANTHROPIC_API_KEY is not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const anthropic = new Anthropic();
|
||||
|
||||
console.log("[agent_server] Initializing Claude agent server");
|
||||
console.log(`[agent_server] Model: ${CLAUDE_MODEL}`);
|
||||
console.log("[agent_server] ANTHROPIC_API_KEY: set");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildAnthropicMessages(messages: Message[]): Anthropic.MessageParam[] {
|
||||
const result: Anthropic.MessageParam[] = [];
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "user") {
|
||||
const userContent =
|
||||
typeof msg.content === "string"
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content);
|
||||
result.push({
|
||||
role: "user",
|
||||
content: userContent,
|
||||
});
|
||||
} else if (msg.role === "assistant") {
|
||||
const toolCalls = msg.toolCalls;
|
||||
|
||||
if (toolCalls && toolCalls.length > 0) {
|
||||
const content: Anthropic.ContentBlockParam[] = [];
|
||||
|
||||
if (msg.content) {
|
||||
content.push({ type: "text", text: msg.content });
|
||||
}
|
||||
|
||||
for (const tc of toolCalls) {
|
||||
let input: Record<string, unknown> = {};
|
||||
try {
|
||||
input = JSON.parse(tc.function.arguments);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`[agent_server] Failed to parse tool call arguments for ${tc.function?.name}: ${e}`,
|
||||
);
|
||||
}
|
||||
content.push({
|
||||
type: "tool_use",
|
||||
id: tc.id,
|
||||
name: tc.function.name,
|
||||
input,
|
||||
});
|
||||
}
|
||||
|
||||
result.push({ role: "assistant", content });
|
||||
} else {
|
||||
result.push({
|
||||
role: "assistant",
|
||||
content: msg.content ?? "",
|
||||
});
|
||||
}
|
||||
} else if (msg.role === "tool") {
|
||||
result.push({
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: msg.toolCallId ?? "",
|
||||
content:
|
||||
typeof msg.content === "string"
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
// skip "system" and "developer" roles -- not forwarded to Anthropic (system prompt is built from input.context)
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildTools(tools: RunAgentInput["tools"]): Anthropic.Tool[] {
|
||||
if (!tools || tools.length === 0) return [];
|
||||
|
||||
return tools.map((tool) => {
|
||||
let inputSchema: Anthropic.Tool.InputSchema = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
};
|
||||
if (tool.parameters) {
|
||||
try {
|
||||
const parsed =
|
||||
typeof tool.parameters === "string"
|
||||
? JSON.parse(tool.parameters)
|
||||
: tool.parameters;
|
||||
inputSchema = parsed as Anthropic.Tool.InputSchema;
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`[agent_server] Failed to parse parameters schema for tool "${tool.name}": ${e}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: tool.name,
|
||||
description: tool.description ?? "",
|
||||
input_schema: inputSchema,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AG-UI streaming endpoint
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
app.post("/", async (req: Request, res: Response): Promise<void> => {
|
||||
const input = req.body as RunAgentInput;
|
||||
|
||||
const encoder = new EventEncoder();
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
|
||||
const runId = input.runId ?? randomUUID();
|
||||
const threadId = input.threadId ?? randomUUID();
|
||||
const msgId = randomUUID();
|
||||
|
||||
const emit = (event: BaseEvent) => {
|
||||
res.write(encoder.encodeSSE(event));
|
||||
};
|
||||
|
||||
try {
|
||||
// Run started
|
||||
emit({ type: EventType.RUN_STARTED, runId, threadId });
|
||||
|
||||
const messages = buildAnthropicMessages(input.messages ?? []);
|
||||
const tools = buildTools(input.tools);
|
||||
|
||||
// Build system prompt from context
|
||||
let systemPrompt =
|
||||
"You are a helpful AI assistant powered by Anthropic's Claude.";
|
||||
if (input.context && input.context.length > 0) {
|
||||
const contextStr = input.context
|
||||
.map((c) => `${c.description}: ${c.value}`)
|
||||
.join("\n");
|
||||
systemPrompt += `\n\nContext:\n${contextStr}`;
|
||||
}
|
||||
|
||||
const claudeRequest: Anthropic.MessageCreateParamsStreaming = {
|
||||
model: CLAUDE_MODEL,
|
||||
max_tokens: 4096,
|
||||
system: systemPrompt,
|
||||
messages,
|
||||
stream: true,
|
||||
...(tools.length > 0 ? { tools } : {}),
|
||||
};
|
||||
|
||||
const stream = await anthropic.messages.stream(claudeRequest);
|
||||
|
||||
let toolCallId: string | null = null;
|
||||
let toolCallName: string | null = null;
|
||||
let textMessageStarted = false;
|
||||
|
||||
for await (const event of stream) {
|
||||
if (event.type === "message_start") {
|
||||
// Don't emit TEXT_MESSAGE_START here — wait until we actually
|
||||
// receive a text_delta so tool-call-only responses stay clean.
|
||||
} else if (event.type === "content_block_start") {
|
||||
if (event.content_block.type === "tool_use") {
|
||||
toolCallId = event.content_block.id;
|
||||
toolCallName = event.content_block.name;
|
||||
emit({
|
||||
type: EventType.TOOL_CALL_START,
|
||||
toolCallId,
|
||||
toolCallName,
|
||||
parentMessageId: msgId,
|
||||
});
|
||||
}
|
||||
} else if (event.type === "content_block_delta") {
|
||||
if (event.delta.type === "text_delta") {
|
||||
// Lazily emit TEXT_MESSAGE_START on first text content
|
||||
if (!textMessageStarted) {
|
||||
emit({
|
||||
type: EventType.TEXT_MESSAGE_START,
|
||||
messageId: msgId,
|
||||
role: "assistant",
|
||||
});
|
||||
textMessageStarted = true;
|
||||
}
|
||||
emit({
|
||||
type: EventType.TEXT_MESSAGE_CONTENT,
|
||||
messageId: msgId,
|
||||
delta: event.delta.text,
|
||||
});
|
||||
} else if (event.delta.type === "input_json_delta") {
|
||||
if (toolCallId) {
|
||||
emit({
|
||||
type: EventType.TOOL_CALL_ARGS,
|
||||
toolCallId,
|
||||
delta: event.delta.partial_json,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (event.type === "content_block_stop") {
|
||||
if (toolCallId) {
|
||||
emit({
|
||||
type: EventType.TOOL_CALL_END,
|
||||
toolCallId,
|
||||
});
|
||||
toolCallId = null;
|
||||
toolCallName = null;
|
||||
}
|
||||
} else if (event.type === "message_stop") {
|
||||
// Only close the text message if we opened one
|
||||
if (textMessageStarted) {
|
||||
emit({
|
||||
type: EventType.TEXT_MESSAGE_END,
|
||||
messageId: msgId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Design note: this is a pass-through architecture — all tools are
|
||||
// registered by the frontend via CopilotKit and forwarded here as
|
||||
// AG-UI tool definitions. When Claude responds with stop_reason
|
||||
// "tool_use", the tool calls have already been emitted above as
|
||||
// TOOL_CALL_START/ARGS/END events. The AG-UI client will execute
|
||||
// them on the frontend and re-invoke the agent with results. No
|
||||
// server-side tool execution loop is needed.
|
||||
|
||||
emit({ type: EventType.RUN_FINISHED, runId, threadId });
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
console.error(`[agent_server] ERROR: ${err.message}`);
|
||||
try {
|
||||
emit({
|
||||
type: EventType.RUN_ERROR,
|
||||
runId,
|
||||
threadId,
|
||||
message: "An error occurred while processing the request",
|
||||
code: "AGENT_ERROR",
|
||||
});
|
||||
} catch {
|
||||
// Client may have disconnected — cannot write error event
|
||||
}
|
||||
}
|
||||
|
||||
res.end();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Health check
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
app.get("/health", (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
status: "ok",
|
||||
model: CLAUDE_MODEL,
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(PORT, HOST, () => {
|
||||
console.log(`[agent_server] Listening on http://${HOST}:${PORT}`);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "claude-sdk-typescript-agent",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ag-ui/core": "^0.0.48",
|
||||
"@ag-ui/encoder": "^0.0.48",
|
||||
"@anthropic-ai/sdk": "^0.57.0",
|
||||
"dotenv": "^16.4.0",
|
||||
"express": "^4.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* System prompt + state-injection helpers for the Shared State (Read +
|
||||
* Write) demo. Mirrors `langgraph-python/src/agents/shared_state_read_write.py`.
|
||||
*
|
||||
* The UI owns a `preferences` object that it writes into agent state via
|
||||
* `agent.setState({ preferences })`. The AG-UI client forwards that into
|
||||
* the next `RunAgentInput.state.preferences`. Every turn we read those
|
||||
* preferences and prepend them to the Claude system prompt, so the UI's
|
||||
* writes visibly steer the agent.
|
||||
*
|
||||
* Conversely, the agent's `set_notes` tool writes a `notes: string[]`
|
||||
* slot back into shared state, which the agent_server emits as a
|
||||
* `STATE_SNAPSHOT` so the UI's `useAgent` hook re-renders the notes
|
||||
* sidebar in real time.
|
||||
*/
|
||||
|
||||
// @region[shared-state-setup]
|
||||
export interface Preferences {
|
||||
name?: string;
|
||||
tone?: "formal" | "casual" | "playful";
|
||||
language?: string;
|
||||
interests?: string[];
|
||||
}
|
||||
|
||||
export const SHARED_STATE_READ_WRITE_BASE_SYSTEM =
|
||||
"You are a helpful, concise assistant. " +
|
||||
"The user's preferences are supplied via shared state and will be " +
|
||||
"added to the system prompt 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). " +
|
||||
"Each note should be under 120 characters.";
|
||||
|
||||
export const SET_NOTES_TOOL_SCHEMA = {
|
||||
name: "set_notes" as const,
|
||||
description:
|
||||
"Replace the notes array in shared state with the full updated list. " +
|
||||
"Use whenever the user asks you to 'remember' something, or when you " +
|
||||
"have an observation worth surfacing in the UI's notes panel. " +
|
||||
"Always pass the FULL notes list (existing + new), not a diff. " +
|
||||
"Keep each note short (< 120 chars).",
|
||||
input_schema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
notes: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description:
|
||||
"The complete updated notes array. Replaces the current notes.",
|
||||
},
|
||||
},
|
||||
required: ["notes"],
|
||||
},
|
||||
};
|
||||
|
||||
function isStringArray(value: unknown): value is string[] {
|
||||
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerce an arbitrary `unknown` (e.g. `input.state.preferences`) into a
|
||||
* sanitized `Preferences` object. We accept partial shapes — any field
|
||||
* may be missing — and silently drop fields that don't match the
|
||||
* expected types so a misbehaving frontend can't poison the prompt.
|
||||
*/
|
||||
export function coercePreferences(value: unknown): Preferences {
|
||||
if (!value || typeof value !== "object") return {};
|
||||
const v = value as Record<string, unknown>;
|
||||
const out: Preferences = {};
|
||||
if (typeof v.name === "string") out.name = v.name;
|
||||
if (v.tone === "formal" || v.tone === "casual" || v.tone === "playful") {
|
||||
out.tone = v.tone;
|
||||
}
|
||||
if (typeof v.language === "string") out.language = v.language;
|
||||
if (isStringArray(v.interests)) out.interests = v.interests;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function buildPreferencesPreamble(prefs: Preferences): string | 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 [
|
||||
"The user has shared these preferences with you:",
|
||||
...lines,
|
||||
"Tailor every response to these preferences. Address the user by name " +
|
||||
"when appropriate.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function buildSharedStateReadWriteSystemPrompt(
|
||||
prefs: Preferences,
|
||||
): string {
|
||||
const preamble = buildPreferencesPreamble(prefs);
|
||||
if (!preamble) return SHARED_STATE_READ_WRITE_BASE_SYSTEM;
|
||||
return `${SHARED_STATE_READ_WRITE_BASE_SYSTEM}\n\n${preamble}`;
|
||||
}
|
||||
// @endregion[shared-state-setup]
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* System prompts for the Sub-Agents demo.
|
||||
*
|
||||
* Mirrors the language used by the LangGraph Python and Google ADK
|
||||
* references (`langgraph-python/src/agents/subagents.py`,
|
||||
* `google-adk/src/agents/subagents_agent.py`) so the showcase produces
|
||||
* comparable output across runtimes.
|
||||
*
|
||||
* Each sub-agent is a *single Anthropic Messages API call* with a
|
||||
* dedicated system prompt — no tools, no recursion. The supervisor sees
|
||||
* each delegation as a tool call whose result is the sub-agent's reply.
|
||||
*/
|
||||
|
||||
// @region[subagent-setup]
|
||||
// Each sub-agent is defined by its own system prompt. The supervisor
|
||||
// invokes them as tools; on each call the agent server issues a single
|
||||
// Anthropic Messages API request with the matching prompt below. They
|
||||
// don't share memory or tools with the supervisor — the supervisor only
|
||||
// ever sees what the sub-agent returns as a tool result.
|
||||
export const RESEARCH_SUBAGENT_SYSTEM =
|
||||
"You are a research sub-agent. Given a topic, produce a concise " +
|
||||
"bulleted list of 3-5 key facts. No preamble, no closing.";
|
||||
|
||||
export const WRITING_SUBAGENT_SYSTEM =
|
||||
"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.";
|
||||
|
||||
export const CRITIQUE_SUBAGENT_SYSTEM =
|
||||
"You are an editorial critique sub-agent. Given a draft, give 2-3 crisp, " +
|
||||
"actionable critiques. No preamble.";
|
||||
|
||||
export 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. Each tool returns a JSON object shaped " +
|
||||
"`{status: 'completed' | 'failed', result?: string, error?: string}`. " +
|
||||
"If a sub-agent fails, surface the failure briefly to the user (don't " +
|
||||
"fabricate a result) and decide whether to retry. 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, including the in-flight 'running' state.";
|
||||
|
||||
export type SubAgentName =
|
||||
| "research_agent"
|
||||
| "writing_agent"
|
||||
| "critique_agent";
|
||||
|
||||
export const SUBAGENT_SYSTEM_BY_NAME: Record<SubAgentName, string> = {
|
||||
research_agent: RESEARCH_SUBAGENT_SYSTEM,
|
||||
writing_agent: WRITING_SUBAGENT_SYSTEM,
|
||||
critique_agent: CRITIQUE_SUBAGENT_SYSTEM,
|
||||
};
|
||||
// @endregion[subagent-setup]
|
||||
|
||||
// @region[supervisor-delegation-tools]
|
||||
// The supervisor delegates by calling tools. Each entry below is an
|
||||
// Anthropic tool schema that the supervisor LLM "calls" to delegate
|
||||
// work; the run loop in `agent_server.ts` runs the matching sub-agent
|
||||
// synchronously, records the delegation into shared agent state, and
|
||||
// returns the sub-agent's output as a tool_result the supervisor can
|
||||
// read on its next step.
|
||||
export const SUBAGENT_TOOL_SCHEMAS = [
|
||||
{
|
||||
name: "research_agent" as const,
|
||||
description:
|
||||
"Delegate a research task to the research sub-agent. Use for: " +
|
||||
"gathering facts, background, definitions, statistics. Returns a " +
|
||||
"JSON object {status, result?, error?}.",
|
||||
input_schema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
task: {
|
||||
type: "string",
|
||||
description: "The research task — a topic or question.",
|
||||
},
|
||||
},
|
||||
required: ["task"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writing_agent" as const,
|
||||
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`. Same return shape.",
|
||||
input_schema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
task: {
|
||||
type: "string",
|
||||
description:
|
||||
"The drafting brief — include any facts the writer should use.",
|
||||
},
|
||||
},
|
||||
required: ["task"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "critique_agent" as const,
|
||||
description:
|
||||
"Delegate a critique task to the critique sub-agent. Use for: " +
|
||||
"reviewing a draft and suggesting concrete improvements. Same " +
|
||||
"return shape.",
|
||||
input_schema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
task: {
|
||||
type: "string",
|
||||
description: "The draft to critique.",
|
||||
},
|
||||
},
|
||||
required: ["task"],
|
||||
},
|
||||
},
|
||||
];
|
||||
// @endregion[supervisor-delegation-tools]
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Tool Rendering demo family — backend agent constants.
|
||||
*
|
||||
* The tool-rendering, tool-rendering-default-catchall,
|
||||
* tool-rendering-custom-catchall, and tool-rendering-reasoning-chain
|
||||
* demos register RENDER-ONLY hooks on the frontend (`useRenderTool` /
|
||||
* `useDefaultRenderTool`): the page paints cards for tool calls but
|
||||
* registers no handlers, so it never produces tool results. In the
|
||||
* langgraph-python reference these tools are owned by the backend
|
||||
* graph. On the Claude pass-through (`makeAgentHandler`) the calls
|
||||
* were forwarded to the frontend, the result never materialized, and
|
||||
* every card sat in its loading state forever — the same failure mode
|
||||
* the `/gen-ui-agent` endpoint fixed for `set_steps`.
|
||||
*
|
||||
* This module gives the family the same treatment as
|
||||
* `/headless-complete`: backend-owned tool schemas + deterministic
|
||||
* mock impls, executed inside `runAgenticLoop` so multi-leg chains
|
||||
* (5x d20 rolls, AAPL→MSFT comparison, flights→destination-weather)
|
||||
* complete and the cards leave their loading state.
|
||||
*
|
||||
* Determinism: the impls echo optional value-carrying arguments when
|
||||
* the model provides them (the aimock fixtures pass e.g.
|
||||
* `{"value":11}` / `{"ticker":"AAPL","price_usd":338.37}`), and fall
|
||||
* back to canned data otherwise — mirroring `getWeatherImpl` /
|
||||
* `getStockPriceImpl` in `headless-complete-prompt.ts`.
|
||||
*/
|
||||
|
||||
import type Anthropic from "@anthropic-ai/sdk";
|
||||
|
||||
export const TOOL_RENDERING_SYSTEM_PROMPT =
|
||||
"You are a helpful, concise assistant in a demo that renders every " +
|
||||
"tool call as a branded card. Pick the right tool for each user " +
|
||||
"question and fall back to plain text when none fit.\n\n" +
|
||||
"Routing rules:\n" +
|
||||
" - Weather questions → call `get_weather` with the location.\n" +
|
||||
" - Flight searches → call `search_flights` with origin and " +
|
||||
"destination airport codes.\n" +
|
||||
" - Stock/ticker questions → call `get_stock_price` with the ticker.\n" +
|
||||
" - A d20 roll → call `roll_d20`. If the user asks for several " +
|
||||
"rolls, call it once per roll, one call per turn.\n" +
|
||||
" - 'Chain a few tools' → call get_weather, search_flights, and " +
|
||||
"roll_d20 together in a single turn.\n\n" +
|
||||
"After the tools return, write one short sentence summarizing the " +
|
||||
"results. Never fabricate data a tool could provide.";
|
||||
|
||||
export const REASONING_CHAIN_SYSTEM_PROMPT =
|
||||
"You are a helpful assistant that thinks step-by-step and chains " +
|
||||
"tools across turns. When a request needs two pieces of data " +
|
||||
"(compare two stocks, roll two dice, flights plus destination " +
|
||||
"weather), fetch them with sequential tool calls — one call per " +
|
||||
"turn — reasoning between calls about what to fetch next. After " +
|
||||
"the final tool returns, summarize the comparison in one sentence.";
|
||||
|
||||
export const SEARCH_FLIGHTS_TOOL_SCHEMA: Anthropic.Tool = {
|
||||
name: "search_flights",
|
||||
description:
|
||||
"Search for flights between two airports. Returns a mock payload " +
|
||||
"with origin, destination, and a list of flights (airline, flight " +
|
||||
"number, departure/arrival times, price in USD).",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
origin: {
|
||||
type: "string",
|
||||
description: "Origin airport code, e.g. SFO.",
|
||||
},
|
||||
destination: {
|
||||
type: "string",
|
||||
description: "Destination airport code, e.g. JFK.",
|
||||
},
|
||||
},
|
||||
required: ["origin", "destination"],
|
||||
},
|
||||
};
|
||||
|
||||
export const ROLL_D20_TOOL_SCHEMA: Anthropic.Tool = {
|
||||
name: "roll_d20",
|
||||
description:
|
||||
"Roll a 20-sided die and return the value. Accepts an optional " +
|
||||
"`value` to make demo runs deterministic; omit it for a random roll.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
value: {
|
||||
type: "number",
|
||||
description: "Optional fixed result for deterministic demos.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ROLL_DICE_TOOL_SCHEMA: Anthropic.Tool = {
|
||||
name: "roll_dice",
|
||||
description:
|
||||
"Roll a die with the given number of sides and return the value.",
|
||||
input_schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
sides: {
|
||||
type: "number",
|
||||
description: "Number of sides on the die, e.g. 20 or 6.",
|
||||
},
|
||||
},
|
||||
required: ["sides"],
|
||||
},
|
||||
};
|
||||
|
||||
/** Mirrors the `Flight` shape rendered by
|
||||
* `demos/tool-rendering/flight-list-card.tsx`. */
|
||||
export function searchFlightsImpl(
|
||||
origin: string,
|
||||
destination: string,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
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:50",
|
||||
price_usd: 312,
|
||||
},
|
||||
{
|
||||
airline: "JetBlue",
|
||||
flight: "B6722",
|
||||
depart: "17:05",
|
||||
arrive: "01:35",
|
||||
price_usd: 289,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function rollD20Impl(value?: number): Record<string, unknown> {
|
||||
return {
|
||||
value:
|
||||
typeof value === "number" ? value : Math.floor(Math.random() * 20) + 1,
|
||||
};
|
||||
}
|
||||
|
||||
/** Deterministic per-sides results so narrations stay consistent with
|
||||
* the aimock fixtures (d20 → 14, d6 → 4). */
|
||||
const ROLL_DICE_FIXED_RESULTS: Record<number, number> = { 20: 14, 6: 4 };
|
||||
|
||||
export function rollDiceImpl(sides: number): Record<string, unknown> {
|
||||
const value = ROLL_DICE_FIXED_RESULTS[sides] ?? Math.ceil(sides / 2);
|
||||
return { sides, value };
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
import { HttpAgent } from "@ag-ui/client";
|
||||
|
||||
const AIMOCK_CONTEXT = "claude-sdk-typescript";
|
||||
|
||||
function shouldAttachAimockContext(): boolean {
|
||||
return [process.env.ANTHROPIC_BASE_URL, process.env.AIMOCK_URL].some(
|
||||
(value) => value?.includes("aimock"),
|
||||
);
|
||||
}
|
||||
|
||||
export function claudeHttpAgentConfig(url: string): {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
} {
|
||||
return {
|
||||
url,
|
||||
...(shouldAttachAimockContext()
|
||||
? { headers: { "x-aimock-context": AIMOCK_CONTEXT } }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createClaudeHttpAgent(url: string): HttpAgent {
|
||||
return new HttpAgent(claudeHttpAgentConfig(url));
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export function internalRuntimeErrorResponse(route: string, error: unknown) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
const errorId = randomUUID();
|
||||
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
at: new Date().toISOString(),
|
||||
level: "error",
|
||||
route,
|
||||
errorId,
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
}),
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "internal runtime error", errorId },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Dedicated runtime for the A2UI — Fixed Schema cell. Splitting into its
|
||||
// own endpoint lets us set `a2ui.injectA2UITool: false` — the backend
|
||||
// Claude SDK agent owns the `display_flight` tool which emits its own
|
||||
// `a2ui_operations` container directly in the tool result.
|
||||
//
|
||||
// Reference:
|
||||
// - showcase/integrations/langgraph-python/src/app/api/copilotkit-a2ui-fixed-schema/route.ts
|
||||
// - src/agent/a2ui-fixed-prompt.ts (the Claude SDK backend)
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { createClaudeHttpAgent } from "@/app/api/_shared/claude-http-agent";
|
||||
import { internalRuntimeErrorResponse } from "@/app/api/_shared/route-error";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
|
||||
|
||||
const a2uiFixedSchemaAgent = createClaudeHttpAgent(
|
||||
`${AGENT_URL}/a2ui-fixed-schema`,
|
||||
);
|
||||
|
||||
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: { "a2ui-fixed-schema": a2uiFixedSchemaAgent },
|
||||
a2ui: {
|
||||
// The backend agent emits its own `a2ui_operations` container inside
|
||||
// `display_flight` (see src/agent/a2ui-fixed-prompt.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 of the agent's existing tools.
|
||||
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) {
|
||||
return internalRuntimeErrorResponse(
|
||||
"/api/copilotkit-a2ui-fixed-schema",
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Dedicated runtime for the Agent Config Object demo.
|
||||
*
|
||||
* Proxies to the Claude agent_server's `/agent-config` endpoint, which
|
||||
* reads the provider's `properties` (forwarded by the runtime as
|
||||
* `forwardedProps`) and composes the Claude system prompt from
|
||||
* tone / expertise / responseLength before each turn.
|
||||
*
|
||||
* The Claude agent reads `forwardedProps` directly off the AG-UI
|
||||
* `RunAgentInput`, so this runtime can register a plain HttpAgent with
|
||||
* no compatibility repacking step.
|
||||
*/
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import type { AbstractAgent } from "@ag-ui/client";
|
||||
import { createClaudeHttpAgent } from "@/app/api/_shared/claude-http-agent";
|
||||
import { internalRuntimeErrorResponse } from "@/app/api/_shared/route-error";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
|
||||
|
||||
const agentConfigAgent = createClaudeHttpAgent(`${AGENT_URL}/agent-config`);
|
||||
|
||||
const agents: Record<string, AbstractAgent> = {
|
||||
"agent-config-demo": agentConfigAgent,
|
||||
default: agentConfigAgent,
|
||||
};
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore -- see main route.ts
|
||||
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) {
|
||||
return internalRuntimeErrorResponse("/api/copilotkit-agent-config", error);
|
||||
}
|
||||
};
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Dedicated runtime for the /demos/auth cell — Claude Agent SDK port.
|
||||
*
|
||||
* 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. Validates 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 we use
|
||||
* `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.
|
||||
*/
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
createCopilotRuntimeHandler,
|
||||
} from "@copilotkit/runtime/v2";
|
||||
import type { AbstractAgent } from "@ag-ui/client";
|
||||
import { createClaudeHttpAgent } from "@/app/api/_shared/claude-http-agent";
|
||||
import { DEMO_AUTH_HEADER } from "@/app/demos/auth/demo-token";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
|
||||
|
||||
const authDemoAgent = createClaudeHttpAgent(`${AGENT_URL}/`);
|
||||
|
||||
const agents: Record<string, AbstractAgent> = {
|
||||
"auth-demo": authDemoAgent,
|
||||
default: authDemoAgent,
|
||||
};
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore -- see main route.ts
|
||||
agents,
|
||||
});
|
||||
|
||||
const BASE_PATH = "/api/copilotkit-auth";
|
||||
|
||||
const handler = createCopilotRuntimeHandler({
|
||||
runtime,
|
||||
basePath: BASE_PATH,
|
||||
hooks: {
|
||||
onRequest: ({ request }) => {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
if (authHeader !== DEMO_AUTH_HEADER) {
|
||||
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" },
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const POST = (req: NextRequest) => handler(req);
|
||||
export const GET = (req: NextRequest) => handler(req);
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
// 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 langgraph-python reference uses.
|
||||
//
|
||||
// Mirrors:
|
||||
// showcase/integrations/pydantic-ai/src/app/api/copilotkit-beautiful-chat/route.ts
|
||||
// showcase/integrations/langgraph-python/src/app/api/copilotkit-beautiful-chat/route.ts
|
||||
//
|
||||
// Backend wiring: CST exposes a dedicated `/beautiful-chat` agentic-loop
|
||||
// mount because this flagship cell mixes backend-owned tools
|
||||
// (`query_data`, `search_flights`, `generate_a2ui`, `manage_todos`) with
|
||||
// frontend tools and middleware-provided tools. Proxying to the default `/`
|
||||
// pass-through leaves backend tool calls unresolved.
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import type { AbstractAgent } from "@ag-ui/client";
|
||||
import { createClaudeHttpAgent } from "@/app/api/_shared/claude-http-agent";
|
||||
import { internalRuntimeErrorResponse } from "@/app/api/_shared/route-error";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
|
||||
|
||||
// The beautiful-chat page resolves <CopilotKit agent="beautiful-chat">
|
||||
// here; internal components (headless-chat, example-canvas) also call
|
||||
// `useAgent()` with no args, which defaults to agentId "default". Alias
|
||||
// default to the same pass-through backend so those hooks resolve.
|
||||
const agents: Record<string, AbstractAgent> = {
|
||||
"beautiful-chat": createClaudeHttpAgent(`${AGENT_URL}/beautiful-chat`),
|
||||
default: createClaudeHttpAgent(`${AGENT_URL}/beautiful-chat`),
|
||||
};
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore -- see main route.ts
|
||||
agents,
|
||||
openGenerativeUI: true,
|
||||
a2ui: {
|
||||
// Do NOT inject a competing runtime render_a2ui tool. Frontend +
|
||||
// middleware own the a2ui surface here.
|
||||
injectA2UITool: false,
|
||||
// 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: "excalidraw",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
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) {
|
||||
return internalRuntimeErrorResponse(
|
||||
"/api/copilotkit-beautiful-chat",
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// Dedicated runtime for the Declarative Generative UI (A2UI) cell.
|
||||
// The backend owns the `generate_a2ui` tool and performs a secondary Claude
|
||||
// call against `render_a2ui`, so the runtime must not inject a competing
|
||||
// A2UI tool. It still reads the page-registered catalog and forwards
|
||||
// `a2ui_operations` tool results to the renderer.
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { createClaudeHttpAgent } from "@/app/api/_shared/claude-http-agent";
|
||||
import { internalRuntimeErrorResponse } from "@/app/api/_shared/route-error";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
|
||||
|
||||
// @region[a2ui-runtime-setup]
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore -- see main route.ts
|
||||
agents: {
|
||||
"declarative-gen-ui": createClaudeHttpAgent(
|
||||
`${AGENT_URL}/declarative-gen-ui`,
|
||||
),
|
||||
},
|
||||
a2ui: {
|
||||
injectA2UITool: false,
|
||||
defaultCatalogId: "declarative-gen-ui-catalog",
|
||||
},
|
||||
});
|
||||
// @endregion[a2ui-runtime-setup]
|
||||
|
||||
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) {
|
||||
return internalRuntimeErrorResponse(
|
||||
"/api/copilotkit-declarative-gen-ui",
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Dedicated runtime for the Declarative Hashbrown demo.
|
||||
*
|
||||
* Proxies to the Claude agent_server's `/byoc-hashbrown` endpoint which
|
||||
* instructs Claude to emit the hashbrown-shaped `{ ui: [...] }` JSON
|
||||
* envelope that `@hashbrownai/react`'s `useJsonParser` consumes
|
||||
* progressively in `hashbrown-renderer.tsx`.
|
||||
*/
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import type { AbstractAgent } from "@ag-ui/client";
|
||||
import { createClaudeHttpAgent } from "@/app/api/_shared/claude-http-agent";
|
||||
import { internalRuntimeErrorResponse } from "@/app/api/_shared/route-error";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
|
||||
|
||||
const agents: Record<string, AbstractAgent> = {
|
||||
"declarative-hashbrown-demo": createClaudeHttpAgent(
|
||||
`${AGENT_URL}/byoc-hashbrown`,
|
||||
),
|
||||
};
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore -- see main route.ts
|
||||
agents,
|
||||
});
|
||||
|
||||
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) {
|
||||
return internalRuntimeErrorResponse(
|
||||
"/api/copilotkit-declarative-hashbrown",
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Dedicated runtime for the Declarative JSON Render demo.
|
||||
*
|
||||
* Proxies to the Claude agent_server's `/byoc-json-render` endpoint, which
|
||||
* swaps in a system prompt instructing Claude to emit a `@json-render/react`
|
||||
* flat-spec JSON object. Isolated from the shared `/api/copilotkit` runtime
|
||||
* so the declarative-render system prompt cannot leak into other demos.
|
||||
*/
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import type { AbstractAgent } from "@ag-ui/client";
|
||||
import { createClaudeHttpAgent } from "@/app/api/_shared/claude-http-agent";
|
||||
import { internalRuntimeErrorResponse } from "@/app/api/_shared/route-error";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
|
||||
|
||||
const agents: Record<string, AbstractAgent> = {
|
||||
byoc_json_render: createClaudeHttpAgent(`${AGENT_URL}/byoc-json-render`),
|
||||
};
|
||||
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore -- see main route.ts
|
||||
agents,
|
||||
});
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
endpoint: "/api/copilotkit-declarative-json-render",
|
||||
serviceAdapter: new ExperimentalEmptyAdapter(),
|
||||
runtime,
|
||||
});
|
||||
return await handleRequest(req);
|
||||
} catch (error: unknown) {
|
||||
return internalRuntimeErrorResponse(
|
||||
"/api/copilotkit-declarative-json-render",
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Dedicated runtime for the Headless Chat (Complete) cell. The Claude
|
||||
// SDK backend exposes its own `get_weather` and `get_stock_price` tools
|
||||
// at the `/headless-complete` endpoint (see
|
||||
// `src/agent/headless-complete-prompt.ts`). The frontend additionally
|
||||
// registers a `highlight_note` tool via `useComponent` — that one is
|
||||
// forwarded to Claude as part of the AG-UI request and intercepted by
|
||||
// the AG-UI client when Claude calls it.
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import { createClaudeHttpAgent } from "@/app/api/_shared/claude-http-agent";
|
||||
import { internalRuntimeErrorResponse } from "@/app/api/_shared/route-error";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
|
||||
|
||||
const headlessCompleteAgent = createClaudeHttpAgent(
|
||||
`${AGENT_URL}/headless-complete`,
|
||||
);
|
||||
|
||||
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: { "headless-complete": headlessCompleteAgent },
|
||||
});
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
endpoint: "/api/copilotkit-headless-complete",
|
||||
serviceAdapter: new ExperimentalEmptyAdapter(),
|
||||
runtime,
|
||||
});
|
||||
return await handleRequest(req);
|
||||
} catch (error: unknown) {
|
||||
return internalRuntimeErrorResponse(
|
||||
"/api/copilotkit-headless-complete",
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
// CopilotKit runtime for the MCP Apps cell.
|
||||
//
|
||||
// The runtime's `mcpApps` config auto-applies the MCP Apps middleware to the
|
||||
// agent: when the agent calls a tool backed by an MCP UI resource, the
|
||||
// middleware fetches the resource and emits the activity event that the
|
||||
// built-in `MCPAppsActivityRenderer` (registered by CopilotKit internally)
|
||||
// renders in the chat as a sandboxed iframe.
|
||||
//
|
||||
// Reference:
|
||||
// https://docs.copilotkit.ai/generative-ui/mcp-apps
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
import {
|
||||
CopilotRuntime,
|
||||
ExperimentalEmptyAdapter,
|
||||
copilotRuntimeNextJSAppRouterEndpoint,
|
||||
} from "@copilotkit/runtime";
|
||||
import type { AbstractAgent } from "@ag-ui/client";
|
||||
import { createClaudeHttpAgent } from "@/app/api/_shared/claude-http-agent";
|
||||
import { internalRuntimeErrorResponse } from "@/app/api/_shared/route-error";
|
||||
|
||||
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000";
|
||||
|
||||
const agents: Record<string, AbstractAgent> = {
|
||||
"mcp-apps": createClaudeHttpAgent(`${AGENT_URL}/mcp-apps`),
|
||||
"headless-complete": createClaudeHttpAgent(`${AGENT_URL}/headless-complete`),
|
||||
};
|
||||
|
||||
// @region[runtime-mcpapps-config]
|
||||
// The `mcpApps.servers` config is all you need server-side. The runtime
|
||||
// auto-applies the MCP Apps middleware to every registered agent: on each
|
||||
// MCP tool call it fetches the associated UI resource and emits an
|
||||
// `activity` event that the built-in `MCPAppsActivityRenderer` renders
|
||||
// inline in the chat.
|
||||
const runtime = new CopilotRuntime({
|
||||
// @ts-ignore -- see main route.ts
|
||||
agents,
|
||||
mcpApps: {
|
||||
servers: [
|
||||
{
|
||||
type: "http",
|
||||
url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
|
||||
// Always pin a stable `serverId`. Without it CopilotKit hashes the
|
||||
// URL, and a URL change silently breaks restoration of persisted
|
||||
// MCP Apps in prior conversation threads.
|
||||
serverId: "excalidraw",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
// @endregion[runtime-mcpapps-config]
|
||||
|
||||
export const POST = async (req: NextRequest) => {
|
||||
try {
|
||||
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
|
||||
endpoint: "/api/copilotkit-mcp-apps",
|
||||
serviceAdapter: new ExperimentalEmptyAdapter(),
|
||||
runtime,
|
||||
});
|
||||
return await handleRequest(req);
|
||||
} catch (error: unknown) {
|
||||
return internalRuntimeErrorResponse("/api/copilotkit-mcp-apps", error);
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user