chore(docs): checkpoint unfinished setup and provenance repairs

This commit is contained in:
Tyler Slaton
2026-09-13 11:26:36 -07:00
parent 45d7ec00bb
commit 077c596257
58 changed files with 6248 additions and 2691 deletions
@@ -41,6 +41,7 @@ export default function GenUiAgentDemo() {
);
}
// @region[gen-ui-agent-state-rendering]
type AgentState = {
steps?: Step[];
};
@@ -73,3 +74,4 @@ function Chat() {
/>
);
}
// @endregion[gen-ui-agent-state-rendering]
@@ -189,6 +189,7 @@ export async function* convertStream(
delta: (parsedContent as { delta: unknown[] }).delta,
};
}
// @region[gen-ui-agent-backend]
// `set_steps` is the gen-ui-agent demo's custom plan tool (see
// state-tools.ts). The tool's server handler returns `{ steps }`;
// translate that into a STATE_DELTA that adds `/steps` on the
@@ -220,6 +221,7 @@ export async function* convertStream(
],
};
}
// @endregion[gen-ui-agent-backend]
// `set_notes` is the shared-state-read-write demo's agent-authored
// notes tool (see server-tools.ts). Its server handler returns
// `{ notes }`; translate that into a STATE_DELTA that adds `/notes`
@@ -17,6 +17,7 @@ from google.adk.tools import ToolContext
from agents.shared_chat import get_model, stop_on_terminal_text
# @region[gen-ui-agent-backend]
def set_steps(tool_context: ToolContext, steps: list[dict]) -> dict:
"""Publish the current plan + step statuses.
@@ -59,3 +60,4 @@ gen_ui_agent = LlmAgent(
tools=[set_steps, AGUIToolset()],
after_model_callback=stop_on_terminal_text,
)
# @endregion[gen-ui-agent-backend]
@@ -41,6 +41,7 @@ export default function GenUiAgentDemo() {
);
}
// @region[gen-ui-agent-state-rendering]
type AgentState = {
steps?: Step[];
};
@@ -73,3 +74,4 @@ function Chat() {
/>
);
}
// @endregion[gen-ui-agent-state-rendering]
@@ -114,8 +114,8 @@
"shell_docs_path": "/generative-ui/tool-rendering"
},
"shared-state-read-write": {
"og_docs_url": "https://docs.copilotkit.ai/langgraph/shared-state/in-app-agent-write",
"shell_docs_path": "/shared-state/in-app-agent-write"
"og_docs_url": "https://docs.copilotkit.ai/langgraph/shared-state/state-inputs-outputs",
"shell_docs_path": "/shared-state/state-inputs-outputs"
},
"shared-state-streaming": {
"og_docs_url": "https://docs.copilotkit.ai/langgraph/shared-state/predictive-state-updates",
@@ -38,6 +38,7 @@ class GenUiAgentState(AgentState):
steps: Annotated[NotRequired[list[Step]], _last_steps, OmitFromInput]
# @region[gen-ui-agent-backend]
@tool
def set_steps(
steps: list[Step], tool_call_id: Annotated[str, InjectedToolCallId]
@@ -59,6 +60,9 @@ def set_steps(
)
# @endregion[gen-ui-agent-backend]
SYSTEM_PROMPT = (
"You are an agentic planner. For each user request, follow this exact "
"sequence:\n"
@@ -6,7 +6,7 @@
// composer renders the mic button.
// 2. Handle `POST /transcribe` by invoking an OpenAI-backed
// `TranscriptionServiceOpenAI` (from `@copilotkit/voice`), so recorded
// audio is transcribed and the transcript auto-sends.
// audio is transcribed and placed in the chat composer for review.
// 3. Return a deterministic 4xx when `OPENAI_API_KEY` is not configured,
// instead of an opaque 5xx. The V2 runtime's `handleTranscribe` maps
// error messages containing "api key" or "unauthorized" to
@@ -41,6 +41,7 @@ export default function GenUiAgentDemo() {
);
}
// @region[gen-ui-agent-state-rendering]
type AgentState = {
steps?: Step[];
};
@@ -73,3 +74,4 @@ function Chat() {
/>
);
}
// @endregion[gen-ui-agent-state-rendering]
File diff suppressed because it is too large Load Diff
@@ -3,33 +3,33 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "concurrently \"next dev --turbopack\" \"cd src/agent && npx @langchain/langgraph-cli@1.2.1 dev --port 8123 --no-browser\"",
"dev": "concurrently \"next dev --turbopack\" \"cd src/agent && npx @langchain/langgraph-cli@1.4.6 dev --port 8123 --no-browser\"",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test:e2e": "playwright test"
},
"dependencies": {
"@copilotkit/a2ui-renderer": "1.68.2",
"@copilotkit/react-core": "1.68.2",
"@copilotkit/react-ui": "1.68.2",
"@copilotkit/runtime": "1.68.2",
"@copilotkit/sdk-js": "1.68.2",
"@copilotkit/shared": "1.68.2",
"@copilotkit/voice": "1.68.2",
"@copilotkit/a2ui-renderer": "1.71.1",
"@copilotkit/react-core": "1.71.1",
"@copilotkit/react-ui": "1.71.1",
"@copilotkit/runtime": "1.71.1",
"@copilotkit/sdk-js": "1.71.1",
"@copilotkit/shared": "1.71.1",
"@copilotkit/voice": "1.71.1",
"@hashbrownai/core": "0.5.0-beta.4",
"@hashbrownai/react": "0.5.0-beta.4",
"@json-render/core": "0.18.0",
"@json-render/react": "0.18.0",
"@langchain/core": "1.1.44",
"@langchain/langgraph": "1.3.0",
"@langchain/langgraph-checkpoint": "1.0.0",
"@langchain/openai": "1.4.4",
"@langchain/core": "1.2.11",
"@langchain/langgraph": "1.4.15",
"@langchain/langgraph-checkpoint": "1.1.5",
"@langchain/openai": "1.5.13",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-separator": "^1.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"langchain": "1.3.4",
"langchain": "1.5.11",
"lucide-react": "^0.460.0",
"next": "^15.5.15",
"openai": "^5.9.0",
@@ -12,14 +12,11 @@
*/
import { z } from "zod";
import { RunnableConfig } from "@langchain/core/runnables";
import type { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import {
AIMessage,
SystemMessage,
ToolMessage,
} from "@langchain/core/messages";
import type { AIMessage } from "@langchain/core/messages";
import { SystemMessage, ToolMessage } from "@langchain/core/messages";
import type { ToolRunnableConfig } from "@langchain/core/tools";
import {
Annotation,
@@ -69,6 +66,7 @@ export type AgentState = typeof AgentStateAnnotation.State;
// the `steps` channel (the UI re-renders from shared state).
// ---------------------------------------------------------------------------
// @region[gen-ui-agent-backend]
const setSteps = tool(
async ({ steps }, config: ToolRunnableConfig) => {
const toolCallId = config.toolCall?.id;
@@ -106,6 +104,7 @@ const setSteps = tool(
}),
},
);
// @endregion[gen-ui-agent-backend]
const tools = [setSteps];
File diff suppressed because it is too large Load Diff
@@ -3,24 +3,26 @@
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "npx @langchain/langgraph-cli@1.2.1 dev --port 8123 --no-browser",
"dev": "npx @langchain/langgraph-cli@1.4.6 dev --port 8123 --no-browser",
"start": "node --import ./disable-file-persistence.mjs --import tsx liveness.mjs"
},
"dependencies": {
"@copilotkit/sdk-js": "1.68.2",
"@langchain/core": "1.1.44",
"@langchain/langgraph": "1.3.0",
"@langchain/langgraph-api": "1.1.17",
"@langchain/langgraph-checkpoint": "1.0.0",
"@langchain/langgraph-sdk": "1.6.5",
"@langchain/openai": "1.4.4",
"langchain": "1.3.4",
"@copilotkit/sdk-js": "1.71.1",
"@langchain/core": "1.2.11",
"@langchain/langgraph": "1.4.15",
"@langchain/langgraph-api": "1.4.6",
"@langchain/langgraph-checkpoint": "1.1.5",
"@langchain/langgraph-sdk": "1.11.0",
"@langchain/openai": "1.5.13",
"langchain": "1.5.11",
"tsx": "^4.19.3",
"typescript": "^5.6.3",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.9.0"
"@langchain/langgraph-cli": "1.4.6",
"@types/node": "^22.9.0",
"ts-node": "10.9.2"
},
"overrides": {
"@ag-ui/langgraph": "0.0.42"
@@ -6,7 +6,7 @@
// composer renders the mic button.
// 2. Handle `POST /transcribe` by invoking an OpenAI-backed
// `TranscriptionServiceOpenAI` (from `@copilotkit/voice`), so recorded
// audio is transcribed and the transcript auto-sends.
// audio is transcribed and placed in the chat composer for review.
// 3. Return a deterministic 4xx when `OPENAI_API_KEY` is not configured,
// instead of an opaque 5xx. The V2 runtime's `handleTranscribe` maps
// error messages containing "api key" or "unauthorized" to
@@ -34,6 +34,7 @@ import type { TranscribeFileOptions } from "@copilotkit/runtime/v2";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
import { TranscriptionServiceOpenAI } from "@copilotkit/voice";
import OpenAI from "openai";
import { resolveTranscriptionBaseUrl } from "@/lib/transcription-base-url";
const LANGGRAPH_URL =
process.env.AGENT_URL ||
@@ -58,7 +59,12 @@ class GuardedOpenAITranscriptionService extends TranscriptionService {
super();
const apiKey = process.env.OPENAI_API_KEY;
this.delegate = apiKey
? new TranscriptionServiceOpenAI({ openai: new OpenAI({ apiKey }) })
? new TranscriptionServiceOpenAI({
openai: new OpenAI({
apiKey,
baseURL: resolveTranscriptionBaseUrl(),
}),
})
: null;
}
@@ -41,6 +41,7 @@ export default function GenUiAgentDemo() {
);
}
// @region[gen-ui-agent-state-rendering]
type AgentState = {
steps?: Step[];
};
@@ -73,3 +74,4 @@ function Chat() {
/>
);
}
// @endregion[gen-ui-agent-state-rendering]
@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
import { resolveTranscriptionBaseUrl } from "./transcription-base-url";
describe("resolveTranscriptionBaseUrl", () => {
it("keeps an explicit transcription endpoint authoritative", () => {
expect(
resolveTranscriptionBaseUrl({
OPENAI_TRANSCRIPTION_BASE_URL: "https://transcription.example/v1",
AIMOCK_URL: "http://127.0.0.1:4410",
}),
).toBe("https://transcription.example/v1");
});
it("uses the local AIMock OpenAI API only when it is configured", () => {
expect(
resolveTranscriptionBaseUrl({ AIMOCK_URL: "http://127.0.0.1:4410/" }),
).toBe("http://127.0.0.1:4410/v1");
expect(
resolveTranscriptionBaseUrl({ AIMOCK_URL: "http://127.0.0.1:4410/v1" }),
).toBe("http://127.0.0.1:4410/v1");
});
it("retains the production OpenAI endpoint without local configuration", () => {
expect(resolveTranscriptionBaseUrl({})).toBe("https://api.openai.com/v1");
});
});
@@ -0,0 +1,29 @@
type TranscriptionEnvironment = Pick<
NodeJS.ProcessEnv,
"AIMOCK_URL" | "OPENAI_TRANSCRIPTION_BASE_URL"
>;
const OPENAI_TRANSCRIPTIONS_URL = "https://api.openai.com/v1";
/**
* Keep real deployments on OpenAI unless a transcription-specific endpoint is
* configured. Local AIMock runs opt in through AIMOCK_URL, which is normally
* a bare origin while the OpenAI client requires its /v1 API prefix.
*/
export function resolveTranscriptionBaseUrl(
env: TranscriptionEnvironment = process.env,
): string {
if (env.OPENAI_TRANSCRIPTION_BASE_URL) {
return env.OPENAI_TRANSCRIPTION_BASE_URL;
}
if (env.AIMOCK_URL) {
const aimockOrigin = env.AIMOCK_URL.replace(/\/+$/, "").replace(
/\/v1$/,
"",
);
return `${aimockOrigin}/v1`;
}
return OPENAI_TRANSCRIPTIONS_URL;
}
@@ -6,11 +6,13 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
test: {
// CVDIAG backend instrumentation unit tests (L1-E). The integration's
// broader suite is Playwright e2e (`test:e2e`); this config scopes vitest
// to the co-located cvdiag unit tests so they run without the Next.js
// build toolchain.
include: ["src/cvdiag-backend.test.ts"],
// Small backend unit contracts (CVDIAG plus local transcription endpoint
// resolution). The integration's broader suite is Playwright e2e
// (`test:e2e`), so keep this independent of the Next.js build toolchain.
include: [
"src/cvdiag-backend.test.ts",
"src/lib/transcription-base-url.test.ts",
],
environment: "node",
},
resolve: {
@@ -301,6 +301,11 @@ demos:
- generative-ui
route: /demos/gen-ui-agent
animated_preview_url:
highlight:
- src/agents/gen_ui_agent.py
- src/app/demos/gen-ui-agent/page.tsx
- src/app/demos/gen-ui-agent/InlineAgentStateCard.tsx
- src/app/api/copilotkit/route.ts
- id: gen-ui-tool-based
name: Tool-Based Generative UI
description: Agent uses tools to trigger UI generation
@@ -71,6 +71,7 @@ def set_steps(steps: list[dict]) -> str:
# ---- State hook ---------------------------------------------------------
# @region[gen-ui-agent-backend]
async def steps_state_from_args(context: Any) -> dict | None:
"""Emit a StateSnapshotEvent for the ``steps`` slot on every ``set_steps``.
@@ -131,6 +132,9 @@ async def steps_state_from_args(context: Any) -> dict | None:
return {"steps": cleaned}
# @endregion[gen-ui-agent-backend]
# ---- Prompt addendum ----------------------------------------------------
@@ -41,6 +41,7 @@ export default function GenUiAgentDemo() {
);
}
// @region[gen-ui-agent-state-rendering]
type AgentState = {
steps?: Step[];
};
@@ -73,3 +74,4 @@ function Chat() {
/>
);
}
// @endregion[gen-ui-agent-state-rendering]
@@ -55,6 +55,10 @@ const sharedFrontendEntries = [
"shared-state-read-write/page.tsx",
),
},
{
source: path.join(showcaseRoot, "shared/react/demos/gen-ui-agent/page.tsx"),
targets: demoTargets(selectedReactIntegrations, "gen-ui-agent/page.tsx"),
},
{
source: path.join(showcaseRoot, "shared/react/demos/auth/page.tsx"),
targets: demoTargets(selectedReactIntegrations, "auth/page.tsx"),
@@ -0,0 +1,77 @@
"use client";
import React from "react";
import {
CopilotChat,
CopilotKit,
useAgent,
UseAgentUpdate,
} from "@copilotkit/react-core/v2";
import type { Step } from "./InlineAgentStateCard";
import { MessageListWithState } from "./message-list-with-state";
import { useSuggestions } from "./suggestions";
/**
* Agentic Generative UI In-Chat State Rendering
*
* The deep agent on the backend defines its own state schema
* (`steps: list[Step]`) and exposes a custom `set_steps` tool that the model
* calls to mutate that state. Every `set_steps` call streams the updated
* `steps` to the client.
*
* On the client we subscribe to that live state via `useAgent` (v2) and
* render a single `InlineAgentStateCard` inside the chat transcript via
* `messageView.children`. The card re-renders in place as state arrives
* no per-message claims, no duplicate cards.
*
* This mirrors the pattern used by every other integration's gen-ui-agent
* demo (mastra, strands, ag2, agno, crewai-crews, langgraph-typescript,
* pydantic-ai, ...) and replaces the earlier `useCoAgentStateRender`
* approach which produced one card per state-changing message.
*/
export default function GenUiAgentDemo() {
return (
<CopilotKit runtimeUrl="/api/copilotkit" agent="gen-ui-agent">
<div className="flex justify-center items-center h-screen w-full">
<div className="h-full w-full max-w-4xl">
<Chat />
</div>
</div>
</CopilotKit>
);
}
// @region[gen-ui-agent-state-rendering]
type AgentState = {
steps?: Step[];
};
function Chat() {
const { agent } = useAgent({
agentId: "gen-ui-agent",
updates: [UseAgentUpdate.OnStateChanged],
});
useSuggestions();
const steps = (agent.state as AgentState | undefined)?.steps ?? [];
const status = agent.isRunning ? "inProgress" : "complete";
return (
<CopilotChat
agentId="gen-ui-agent"
className="h-full rounded-2xl"
messageView={{
children: ({ messageElements, interruptElement }) => (
<MessageListWithState
messageElements={messageElements}
interruptElement={interruptElement}
steps={steps}
status={status}
/>
),
}}
/>
);
}
// @endregion[gen-ui-agent-state-rendering]
@@ -43,6 +43,7 @@ import {
import { buildAngularBackendOverview } from "@/lib/angular-backend-overview";
import { docsComponents } from "@/lib/mdx-registry";
import { resolveFrontendDocPage } from "@/lib/frontend-doc-policy";
import { resolveFrameworkContent } from "@/lib/framework-content-resolution";
import {
getFrontendGuidanceContentSlug,
getFrontendContentSlug,
@@ -823,47 +824,9 @@ export default async function FrameworkScopedDocsPage({
frameworkOverviews[scopedFramework]?.frameworkName ??
scopedFramework;
let contentSlugPath: string = slugPath;
let doc: ReturnType<typeof loadDoc> = null;
// Content resolution order depends on docs_mode:
//
// authored — per-framework MDX wins for every slug. Authored pages
// can replace root pages while keeping the framework's
// authored sidebar IA.
// Only fall back to root if the framework simply has no
// file for the requested slug (preserves the "shared"
// fallback for slugs the framework intentionally leaves
// to the agnostic page, e.g. enterprise CTAs).
// generated — root MDX wins (Model 1, current behavior); the
// per-framework tree is a sparse override layer.
if (docsMode === "authored") {
const frameworkPath = `integrations/${docsFolder}/${slugPath}`;
doc = loadDoc(frameworkPath);
if (doc) contentSlugPath = frameworkPath;
if (!doc) doc = loadDoc(slugPath);
} else {
// A few root pages are shared nav shims/overviews whose framework-scoped
// URLs should render the per-framework MDX when it exists.
//
// - `/quickstart` at the root is a routing shim; real quickstart content
// lives per-framework.
// - `/threads-import` is a cross-source overview at the root, but ADK and
// LangGraph have source-specific import guides at the same framework URL.
if (slugPath === "quickstart" || slugPath === "threads-import") {
const overridePath = `integrations/${docsFolder}/${slugPath}`;
doc = loadDoc(overridePath);
if (doc) contentSlugPath = overridePath;
}
if (!doc) {
doc = loadDoc(slugPath);
if (!doc) {
const fallbackPath = `integrations/${docsFolder}/${slugPath}`;
doc = loadDoc(fallbackPath);
if (doc) contentSlugPath = fallbackPath;
}
}
}
const resolvedContent = resolveFrameworkContent(scopedFramework, slugPath);
const contentSlugPath = resolvedContent?.contentSlugPath ?? slugPath;
const doc = resolvedContent?.doc ?? null;
// Authored integrations own their full docs tree and sidebar IA.
// Generated integrations use the root docs IA with a sparse
@@ -3,6 +3,7 @@ import path from "path";
import { AG_UI_CONTENT_DIR } from "@/lib/sitemap-helpers";
import { loadDoc } from "@/lib/docs-render";
import { resolveFrontendDocPage } from "@/lib/frontend-doc-policy";
import { resolveFrameworkContent } from "@/lib/framework-content-resolution";
import { resolveAngularDoc } from "@/lib/angular-doc-navigation";
import {
getFrontendContentSlug,
@@ -377,41 +378,21 @@ function resolveFrameworkScopedPage(
tail: string,
url: string,
): ResolvedPage | null {
const docsFolder = getDocsFolder(framework);
const docsMode = getDocsMode(framework);
if (docsMode === "hidden") return null;
const resolved = resolveFrameworkContent(framework, tail);
if (!resolved) return null;
const rootSlugPath = tail;
const frameworkSlugPath = `integrations/${docsFolder}/${tail}`;
// `authored` frameworks own their entire IA — try the per-framework
// tree first. `generated` is the inverse — root wins, framework
// tree is the override, except quickstart where the root file is
// only a routing shim and the page route prefers framework content.
const candidateOrder =
docsMode === "authored" || tail === "quickstart"
? [frameworkSlugPath, rootSlugPath]
: [rootSlugPath, frameworkSlugPath];
if (tail === "index") {
candidateOrder.push(`integrations/${docsFolder}/quickstart`);
}
for (const candidate of candidateOrder) {
const doc = loadDoc(candidate);
if (!doc) continue;
return {
page: {
url,
title: doc.fm.title,
description: doc.fm.description,
filePath: doc.filePath,
loadSlug: candidate,
framework,
},
const { doc, contentSlugPath } = resolved;
return {
page: {
url,
title: doc.fm.title,
description: doc.fm.description,
filePath: doc.filePath,
loadSlug: contentSlugPath,
framework,
};
}
return null;
},
framework,
};
}
/**
@@ -2,10 +2,10 @@
title: State Rendering
icon: "lucide/Bot"
description: Render your agent's state with custom UI components in real-time.
snippet_cell: shared-state-streaming
snippet_cell: gen-ui-agent
---
<InlineDemo demo="shared-state-streaming" />
<InlineDemo demo="gen-ui-agent" />
## What is this?
@@ -26,21 +26,17 @@ Use state rendering when you want to:
## How it works in code
On the frontend, subscribe to the agent's state. Each time the backend
forwards a fresh value, your component re-renders with the latest partial
output.
On the frontend, subscribe to the agent's `steps` state. Each time the
backend publishes the current plan, your component re-renders the progress
card inside the chat transcript.
<Snippet region="frontend-use-coagent-state" title="frontend/src/app/page.tsx — agent state subscription" />
<Snippet region="gen-ui-agent-state-rendering" title="frontend/src/app/demos/gen-ui-agent/page.tsx — state subscription and progress card" />
On the backend, a state-streaming mapping forwards a specific tool argument
straight into a state key *as it's being generated*. Some frameworks provide
that as middleware; direct SDK adapters can emit `STATE_SNAPSHOT` events from
their streaming loop. Either way, the UI can watch the answer assemble
token-by-token rather than appearing in one burst between checkpoints.
On the backend, `set_steps` publishes the complete step list as agent state.
The exact bridge differs by integration: the selected source below is the
implementation that makes the frontend's `agent.state.steps` update.
<FrameworkSetup concept="state-streaming-setup" />
<Snippet region="state-streaming-middleware" title="backend — state streaming mapping" />
<Snippet region="gen-ui-agent-backend" title="backend — publish the live steps state" />
<IntegrationGrid
path="generative-ui/state-rendering"
@@ -1,287 +1,26 @@
---
title: Readables
icon: "lucide/BookA"
description: Share app specific context with your agent.
hideTOC: true
title: Agent app context
icon: "lucide/ArrowRight"
description: Publish UI-owned context to a LangGraph agent without giving the agent a write path.
snippet_cell: readonly-state-agent-context
---
import AgentContextJsonString from "@/snippets/shared/basics/agent-context-json-string.mdx";
One of the most common use cases for CopilotKit is to register app state and context using `useAgentContext`.
This way, you can notify CopilotKit of what is going on in your app in real time.
Some examples might be: the current user, the current page, etc.
<InlineDemo demo="readonly-state-agent-context" />
This context can then be shared with your LangGraph agent.
Use read-only context for values the UI owns, such as the current user, timezone, or recent activity. The agent receives the values on a run but cannot mutate them.
## Implementation
<Callout>
Check out the [Frontend Data documentation](/langgraph-python/agent-app-context) to understand what this is and how to use it.
</Callout>
## Publish context in React
<AgentContextJsonString />
Call [useAgentContext](/reference/hooks/useAgentContext) for each value. The selected Showcase source publishes a display name, timezone, and recent activity.
<TailoredContent
className="step"
id="impl"
>
<TailoredContentOption
id="graph"
title="Custom graph"
description="I'm using a custom graph, where I define the nodes and edges myself."
>
<Steps>
<Step>
### Add the data to the Copilot
<Snippet framework="langgraph-python" cell="readonly-state-agent-context" region="use-agent-context-call" title="src/app/demos/readonly-state-agent-context/page.tsx — UI context values" />
The `useAgentContext` hook is used to add data as context to the agent.
## Read it in LangGraph
```tsx title="YourComponent.tsx"
"use client" // only necessary if you are using Next.js with the App Router. // [!code highlight]
import { useAgentContext } from "@copilotkit/react-core/v2"; // [!code highlight]
import { useState } from 'react';
`CopilotKitMiddleware` makes the registered context available to the LangGraph agent's model call. Keep the agent's system prompt explicit about how to use it.
export function YourComponent() {
// Create colleagues state with some sample data
const [colleagues, setColleagues] = useState([
{ id: 1, name: "John Doe", role: "Developer" },
{ id: 2, name: "Jane Smith", role: "Designer" },
{ id: 3, name: "Bob Wilson", role: "Product Manager" }
]);
<Snippet framework="langgraph-python" cell="readonly-state-agent-context" region="agent-context-setup" title="src/agents/readonly_state_agent_context.py — LangGraph context bridge" />
// Share context with the agent
// [!code highlight:4]
useAgentContext({
description: "The current user's colleagues",
value: colleagues,
});
return (
// Your custom UI component
<>...</>
);
}
```
</Step>
## Try it
<Step>
### Set up your agent state
Make sure your agent state inherits from CopilotKit state definition
<Tabs groupId="language_langgraph_agent" items={['Python', 'TypeScript']} default="Python" persist>
<Tab value="Python">
```python title="agent.py"
# ...
from copilotkit import CopilotKitState # extends MessagesState
# ...
# This is the state of the agent.
# It inherits from the CopilotKitState properties from CopilotKit.
class AgentState(CopilotKitState):
# ... Your defined state properties
```
</Tab>
<Tab value="TypeScript">
```typescript title="agent-js/src/agent.ts"
// ...
import { StateSchema } from "@langchain/langgraph";
import { CopilotKitStateSchema } from "@copilotkit/sdk-js/langgraph";
// ...
// This is the state of the agent.
// It inherits from the CopilotKitState properties from CopilotKit.
export const AgentStateSchema = new StateSchema({
// ... Your defined state properties
...CopilotKitStateSchema.fields,
});
export type AgentState = typeof AgentStateSchema.State;
```
</Tab>
</Tabs>
</Step>
<Step>
### Consume the data in your LangGraph agent
The state of a LangGraph agent is the "hub" for applicative information used by the agent.
Naturally, the context from CopilotKit will be injected there.
<Tabs groupId="language_langgraph_agent" items={['Python', 'TypeScript']} default="Python" persist>
<Tab value="Python">
```python title="agent.py"
import json # [!code highlight]
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI
from copilotkit import CopilotKitState
# add the agent state definition from the previous step
class AgentState(CopilotKitState):
# ... Your defined state properties
def chat_node(state: AgentState, config: RunnableConfig):
# Extract the colleagues from CopilotKit context
colleagues_context_item = next(
(item for item in state["copilotkit"]["context"] if item.get("description") == "The current user's colleagues"),
None
)
# [!code highlight:7]
# The value is a JSON string, so parse it to read the fields
colleagues = []
if colleagues_context_item:
colleagues = json.loads(colleagues_context_item["value"])
# Now each entry is a dict, so colleague["name"] works
colleague_list = ", ".join(f"{c['name']} ({c['role']})" for c in colleagues)
# Provide the list of colleagues to the LLM
system_message = SystemMessage(
content=f"""You are a helpful assistant that can help emailing colleagues.
The user's colleagues are: {colleague_list}"""
)
response = ChatOpenAI(model="gpt-5.4").invoke(
[system_message, *state["messages"]],
config
)
return {
**state,
"messages": response,
}
```
</Tab>
<Tab value="TypeScript">
```typescript title="agent-js/src/agent.ts"
import { SystemMessage } from "@langchain/core/messages";
import { ChatOpenAI } from "@langchain/openai";
// add the agent state definition from the previous step
export const AgentStateSchema = new StateSchema({
// ... Your defined state properties
...CopilotKitStateSchema.fields,
});
export type AgentState = typeof AgentStateSchema.State;
async function chat_node(state: AgentState, config: RunnableConfig) {
// Extract the colleagues from CopilotKit context
const copilotKitContext = state.copilotKit.context
const colleaguesContextItem = copilotKitContext.find(contextItem => contextItem.description === "The current user's colleagues")
// [!code highlight:5]
// The value is a JSON string, so parse it to read the fields
const colleagues = colleaguesContextItem ? JSON.parse(colleaguesContextItem.value) : [];
// Now each entry is an object, so colleague.name works
const colleagueList = colleagues.map((c) => `${c.name} (${c.role})`).join(", ");
// Provide the list of colleagues to the LLM
const systemMessage = new SystemMessage({
content: `
You are a helpful assistant that can help emailing colleagues.
The user's colleagues are: ${colleagueList}
`,
});
const response = await new ChatOpenAI({ model: "gpt-5.4" }).invoke(
[systemMessage, ...state.messages],
config
);
return {
...state,
messages: response,
};
}
```
</Tab>
</Tabs>
</Step>
<Step>
### Give it a try!
Ask your agent a question about the context. It should be able to answer!
</Step>
</Steps>
</TailoredContentOption>
<TailoredContentOption
id="prebuilt"
title="Prebuilt agent"
description={`I'm using a prebuilt agent like "create_agent" by LangGraph`}
>
<Steps>
<Step>
### Add the data to the Copilot
The `useAgentContext` hook is used to add data as context to the agent.
```tsx title="YourComponent.tsx"
"use client" // only necessary if you are using Next.js with the App Router. // [!code highlight]
import { useAgentContext } from "@copilotkit/react-core/v2"; // [!code highlight]
import { useState } from 'react';
export function YourComponent() {
// Create colleagues state with some sample data
const [colleagues, setColleagues] = useState([
{ id: 1, name: "John Doe", role: "Developer" },
{ id: 2, name: "Jane Smith", role: "Designer" },
{ id: 3, name: "Bob Wilson", role: "Product Manager" }
]);
// Share context with the agent
// [!code highlight:4]
useAgentContext({
description: "The current user's colleagues",
value: colleagues,
});
return (
// Your custom UI component
<>...</>
);
}
```
</Step>
<Step>
### Consume the data in your LangGraph agent
The state of a LangGraph agent is the "hub" for applicative information used by the agent.
Naturally, the context from CopilotKit will be injected there.
In addition, the CopilotKitMiddleware is what takes context, and passes it on to your agent
<Tabs groupId="language_langgraph_agent" items={['Python', 'TypeScript']} default="Python" persist>
<Tab value="Python">
```python title="agent.py"
from langchain.agents import create_agent
from copilotkit import CopilotKitMiddleware, CopilotKitState # [!code highlight]
graph = create_agent( # create_agent supersedes the deprecated create_react_agent, which accepts neither middleware= nor system_prompt=
model="openai:gpt-5.4",
tools=[], # Backend tools go here
middleware=[CopilotKitMiddleware()], # [!code highlight]
system_prompt="You are a helpful assistant.",
state_schema=CopilotKitState # [!code highlight]
)
```
</Tab>
<Tab value="TypeScript">
```typescript title="agent-js/src/agent.ts"
import { createAgent } from "langchain";
import { copilotkitMiddleware } from "@copilotkit/sdk-js/langgraph"; // [!code highlight]
export const agenticChatGraph = createAgent({ // createAgent supersedes the deprecated createReactAgent, which accepts neither middleware nor systemPrompt
model: "openai:gpt-5.4",
tools: [], // Backend tools go here
middleware: [copilotkitMiddleware], // [!code highlight]
systemPrompt: "You are a helpful assistant.",
});
```
</Tab>
</Tabs>
</Step>
<Step>
### Give it a try!
Ask your agent a question about the context. It should be able to answer!
</Step>
</Steps>
</TailoredContentOption>
</TailoredContent>
Change the display name or timezone, then ask the chat about your context. The answer should use the current UI values without changing them.
@@ -1,70 +1,30 @@
---
title: Multi-Agent Flows
description: Use multiple agents to orchestrate complex flows.
title: Multi-agent flows
icon: "lucide/Users"
description: Delegate work from a LangGraph supervisor to specialized agents and show each delegation in the UI.
snippet_cell: subagents
---
<Frame>
<img
src="https://cdn.copilotkit.ai/docs/copilotkit/images/coagents/multi-agent-flows.png"
alt="Multi-Agent Flows"
/>
</Frame>
<InlineDemo demo="subagents" />
## What are Multi-Agent Flows?
The selected Showcase uses a LangGraph supervisor with research, writing, and critique agents exposed as tools. Each completed delegation is appended to shared state so the UI can render a live log.
When building agentic applications, you often want to orchestrate complex flows together that require the coordination of multiple
agents. This is traditionally called multi-agent orchestration.
## Define the specialists and supervisor tools
## When should I use this?
Each tool calls one specialist and records a delegation result in the `delegations` state field.
Multi-agent flows are useful when you want to orchestrate complex flows together that require the coordination of multiple agents. As
your agentic application grows, delegation of sub-tasks to other agents can help you scale key pieces of your application.
<Snippet framework="langgraph-python" cell="subagents" region="subagent-setup" title="src/agents/subagents.py — specialist agents and shared state" />
- Divide context into smaller chunks
- Delegate sub-tasks to other agents
- Use a single agent to orchestrate the flow
<Snippet framework="langgraph-python" cell="subagents" region="supervisor-delegation-tools" title="src/agents/subagents.py — supervisor delegation tools" />
## How does CopilotKit support this?
## Render delegation activity
CopilotKit can be used in either of two distinct modes: **Router Mode**, or **Agent Lock**. By default, CopilotKit
will use Router Mode, leveraging your defined LLM to route requests between agents.
The React demo registers renderers for the three tool names and reads the delegation log from agent state. See [useRenderTool](/reference/hooks/useRenderTool) for the renderer contract.
### Router Mode (default)
<Snippet framework="langgraph-python" cell="subagents" region="subagent-tool-renderers" title="src/app/demos/subagents/page.tsx — tool renderers" />
Router Mode is enabled by default when using CoAgents. To use it, specify a runtime URL prop in the `CopilotKit` provider component and omit the `agent` prop, like so:
<Snippet framework="langgraph-python" cell="subagents" region="delegation-log-frontend" title="src/app/demos/subagents/delegation-log.tsx — delegation log" />
```tsx
<CopilotKit runtimeUrl="<copilot-runtime-url>">
{/* Your application components */}
</CopilotKit>
```
## Try it
In router mode, CopilotKit acts as a central hub, dynamically selecting and _routing_ requests between different agents or actions based on the user's input. This mode can be good for chat-first experiences where an LLM chatbot is the entry point for a range of interactions, which can stay in the chat UI or expand to include native React UI widgets.
In this mode, CopilotKit will intelligently route requests to the most appropriate agent or action based on the context and user input.
Be advised that when using this mode, you'll have to "exit the workflow" explicitly in your agent code.
You can find more information about it in the ["Exiting the agent loop" section](/integrations/langgraph/advanced/exit-agent).
<Callout type="warn">
Router mode requires that you set up an LLM adapter. See how in ["Set up a
copilot
runtime"](/quickstart?copilot-hosting=self-hosted#set-up-a-copilot-runtime-endpoint)
section of the docs.
</Callout>
### Agent Lock Mode
To use Agent Lock Mode, specify the agent name in the `CopilotKit` component with the `agent` prop:
```tsx
// [!code word:agent]
<CopilotKit runtimeUrl="<copilot-runtime-url>" agent="<the-name-of-the-agent>">
{/* Your application components */}
</CopilotKit>
```
In this mode, CopilotKit is configured to work exclusively with a specific agent. This mode is useful when you want to focus on a particular task or domain. Whereas in Router Mode the LLM and CopilotKit's router are free to switch between agents to handle user requests, in Agent Lock Mode all requests will stay within a single workflow graph, ensuring precise control over the workflow.
Use whichever mode works best for your app experience! Also, note that while you cannot nest `CopilotKit` providers, you can use different agents or modes in different areas of your app — for example, you may want a chatbot in router mode that can call on any agent or tool, but may also want to integrate one specific agent elsewhere for a more focused workflow.
Ask for a short researched recommendation. The chat should show research, writing, and critique activity, while the delegation log records each completed step.
@@ -584,21 +584,36 @@ Before you begin, you'll need the following:
<Tab value="TypeScript">
### Run an existing TypeScript LangGraph agent
The selected TypeScript Showcase runs its graph with Node.js 20 and
`@langchain/langgraph-cli`. Start in your agent directory; keep your
existing graph implementation, then use the Showcase package as the
dependency and start-script reference.
This path assumes you already have a TypeScript LangGraph project.
Keep your graph and its helpers together; the Showcase package and
configuration below are references for the dependencies, CLI command,
and graph registration shape.
```bash
mkdir my-langgraph-agent
cd my-langgraph-agent
npm init -y
```
<Callout type="info" title="Want the complete runnable Showcase agent?">
The excerpts on this page are not a starter project: the selected
graph imports its own helpers and shared tools, and its configuration
registers other Showcase graphs. To run that exact sample, clone the
integration directory intact:
Merge the dependencies and `dev` script below into your agent's
`package.json`, then install them. The script starts the graph on
port `8123`, which matches the selected Showcase runtime's local
deployment URL.
```bash
git clone --depth 1 https://github.com/CopilotKit/CopilotKit.git
cd CopilotKit/showcase/integrations/langgraph-typescript
cp .env.example .env
# Set OPENAI_API_KEY in .env, then:
cd src/agent
npm ci
npm run dev
```
This starts the selected agent on `http://localhost:8123`. The
`langgraph.json` file loads `../../.env`, so leave `.env` in the
integration directory.
</Callout>
For your existing project, merge the dependencies and `dev` script
below into its `package.json`, install them, and keep your own graph
files and imports in place. The script starts the graph on port `8123`,
which matches the selected Showcase runtime's local deployment URL.
<Snippet
framework="langgraph-typescript"
@@ -612,20 +627,23 @@ Before you begin, you'll need the following:
npm run dev
```
Register the graph export that should receive chat runs in
Register the graph export that should receive chat runs in your own
`langgraph.json`. The selected Showcase maps `starterAgent` to the
`graph` export in `graph.ts`; replace that path and export only when
your existing agent uses different names.
`graph` export in `graph.ts`; use your own path and export when they
differ. Its full configuration is a reference only because it also
registers the other graphs included with the Showcase sample.
<Snippet
framework="langgraph-typescript"
cell="cli-start"
file="src/agent/langgraph.json"
title="src/agent/langgraph.json — selected TypeScript graph registrations"
title="src/agent/langgraph.json — selected TypeScript graph registrations (reference)"
/>
The selected graph compiles a `StateGraph` and exports it as
`graph`, matching the `starterAgent` registration above:
The selected graph compiles a `StateGraph` and exports it as `graph`,
matching the `starterAgent` registration above. This excerpt depends
on the selected sample's chat node and tools, so use it to compare
graph shape rather than pasting it into an otherwise empty project:
<Snippet
framework="langgraph-typescript"
@@ -1,598 +1,26 @@
---
title: "State streaming"
title: State streaming
icon: "lucide/Podcast"
description: Stream in-progress agent state updates to the frontend.
description: Stream a LangGraph tool argument into shared state while the tool call is generated.
snippet_cell: shared-state-streaming
---
<IframeSwitcher
id="predictive-state-updates-example"
exampleUrl="https://feature-viewer.copilotkit.ai/langgraph/feature/predictive_state_updates?sidebar=false&chatDefaultOpen=false"
codeUrl="https://feature-viewer.copilotkit.ai/langgraph/feature/predictive_state_updates?view=code&sidebar=false&codeLayout=tabs"
exampleLabel="Demo"
codeLabel="Code"
height="700px"
/>
<Callout type="info">
This example demonstrates predictive state updates in the [CopilotKit Feature Viewer](https://feature-viewer.copilotkit.ai/langgraph/feature/predictive_state_updates).
</Callout>
<InlineDemo demo="shared-state-streaming" />
## What is this?
The LangGraph Showcase streams the `write_document` tool's `document` argument into shared state as the model generates it. The document panel therefore updates before the tool call completes.
A LangGraph agent's state updates discontinuosly; only across node transitions in the graph.
But even a _single node_ in the graph often takes many seconds to run and contain sub-steps of interest to the user.
## Backend mapping
**Agent-native applications** reflect to the end-user what the agent is doing **as continuously possible.**
`StateStreamingMiddleware` maps one exact tool argument to one state key. Keep the state key, tool name, and tool argument aligned with the tool your model calls.
CopilotKit enables this through its concept of **_predictive state updates_**.
<Snippet framework="langgraph-python" cell="shared-state-streaming" region="state-streaming-middleware" title="src/agents/shared_state_streaming.py — LangGraph state-streaming mapping" />
## Render the state
## When should I use this?
You can use this when you want to provide the user with feedback about what your agent is doing, specifically to:
Subscribe to state and run-status updates, then render the streamed `document` value. See the [useAgent reference](/reference/hooks/useAgent) for the subscription API.
- **Keep users engaged** by avoiding long loading indicators
- **Build trust** by demonstrating what the agent is working on
- Enable **agent steering** - allowing users to course-correct the agent if needed
<Snippet framework="langgraph-python" cell="shared-state-streaming" region="frontend-use-coagent-state" title="src/app/demos/shared-state-streaming/page.tsx — streamed document subscription" />
## Important Note
## Try it
When a node in your LangGraph finishes executing, **its returned state becomes the single source of truth**. While intermediate state updates are great for real-time feedback, any changes you want to persist must be explicitly included in the node's final returned state. Otherwise, they will be overwritten when the node completes.
## Implementation
<TailoredContent id="agent-type">
<TailoredContentOption
id="custom-graph"
title="Custom graph"
description="I'm using a custom graph, where I define the nodes and edges myself."
>
<Steps>
<Step>
### Install the CopilotKit SDK
<InstallSDKSnippet/>
</Step>
<Step>
### Define the state
We'll be defining a `observed_steps` field in the state, which will be updated as the agent writes different sections of the report.
<Tabs groupId="language_langgraph_agent" items={["Python", "TypeScript"]} persist>
<Tab value="Python">
```python title="agent.py"
from copilotkit import CopilotKitState
from typing import Literal
class AgentState(CopilotKitState):
observed_steps: list[str] # Array of completed steps
```
</Tab>
<Tab value="TypeScript">
```ts title="agent-js/src/agent.ts"
import { StateSchema } from "@langchain/langgraph";
import { CopilotKitStateSchema } from "@copilotkit/sdk-js/langgraph";
import { z } from "zod";
export const AgentStateSchema = new StateSchema({
observed_steps: z.array(z.string()).default(() => []), // Array of completed steps
...CopilotKitStateSchema.fields,
});
export type AgentState = typeof AgentStateSchema.State;
```
</Tab>
</Tabs>
</Step>
<Step>
### Emit the intermediate state
<TailoredContent
id="state-emission"
header={
<div>
<p className="text-xl font-semibold">How would you like to emit state updates?</p>
<p className="text-base">
You can either manually emit state updates or configure specific tool calls to emit updates.
</p>
</div>
}
>
<TailoredContentOption
id="manual-emission"
title="Manual Predictive State Updates"
description="Manually emit state updates for maximum control over when updates occur."
icon={<FaArrowUp />}
>
For long-running tasks, you can emit state updates progressively as predictions of the final state. In this example, we simulate a long-running task by executing a series of steps with a one second delay between each update.
<Tabs groupId="language_langgraph_agent" items={['Python', 'TypeScript']} default="Python" persist>
<Tab value="Python">
```python title="agent.py"
from langchain_core.runnables import RunnableConfig
from copilotkit.langgraph import copilotkit_emit_state # [!code highlight]
# ...
async def chat_node(state: AgentState, config: RunnableConfig) -> Command[Literal["cpk_action_node", "tool_node", "__end__"]]:
# ...
# Simulate executing steps one by one
steps = [
"Analyzing input data...",
"Identifying key patterns...",
"Generating recommendations...",
"Formatting final output..."
]
for step in steps:
state["observed_steps"] = state.get("observed_steps", []) + [step]
await copilotkit_emit_state(config, state) # [!code highlight]
await asyncio.sleep(1)
# ...
```
</Tab>
<Tab value="TypeScript">
```ts title="agent-js/src/agent.ts"
import { copilotkitEmitState } from "@copilotkit/sdk-js/langgraph"; // [!code highlight]
// ...
async function chat_node(state: AgentState, config: RunnableConfig) {
// ...
// Simulate executing steps one by one
const steps = [
"Analyzing input data...",
"Identifying key patterns...",
"Generating recommendations...",
"Formatting final output..."
];
for (const step of steps) {
state.observed_steps = [...(state.observed_steps ?? []), step];
copilotkitEmitState(config, state);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
```
</Tab>
</Tabs>
</TailoredContentOption>
<TailoredContentOption
id="tool-emission"
title="Tool-Based Predictive State Updates"
description="Configure specific tool calls to automatically emit intermediate state updates."
icon={<FaWrench />}
>
For long-running tasks, you can configure CopilotKit to automatically predict state updates when specific tool calls are made. In this example, we'll configure CopilotKit to predict state updates whenever the LLM calls the step progress tool.
<Tabs groupId="language_langgraph_agent" items={['Python', 'TypeScript']} default="Python" persist>
<Tab value="Python">
```python
import uuid
from copilotkit import CopilotKitState
from copilotkit.langgraph import copilotkit_customize_config
from langchain.tools import ToolRuntime, tool
from langchain_core.messages import AIMessage, SystemMessage, ToolMessage
from langchain_core.runnables import RunnableConfig
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.prebuilt import ToolNode
from langgraph.types import Command
class AgentState(CopilotKitState):
observed_steps: list[str]
@tool
def step_progress_tool(steps: list[str], runtime: ToolRuntime) -> Command:
"""Report the current steps being executed."""
return Command(
update={
"observed_steps": steps,
"messages": [
ToolMessage(
content="Steps recorded to shared state.",
name="step_progress_tool",
id=str(uuid.uuid4()),
tool_call_id=runtime.tool_call_id,
)
],
}
)
tools = [step_progress_tool]
model = ChatOpenAI(model="gpt-5.4")
async def chat_node(state: AgentState, config: RunnableConfig):
streaming_config = copilotkit_customize_config(
config,
emit_intermediate_state=[
{
"state_key": "observed_steps",
"tool": "step_progress_tool",
"tool_argument": "steps",
},
]
)
model_with_tools = model.bind_tools(
[
*state["copilotkit"]["actions"],
*tools,
],
parallel_tool_calls=False,
)
response = await model_with_tools.ainvoke(
[
SystemMessage(
content="You are a task performer. Report your steps "
"using step_progress_tool."
),
*state["messages"],
],
streaming_config,
)
return {"messages": [response]}
def route_after_chat(state: AgentState):
last_message = state["messages"][-1]
if not isinstance(last_message, AIMessage) or not last_message.tool_calls:
return END
frontend_action_names = {
action["name"] for action in state["copilotkit"]["actions"]
}
if any(
call["name"] not in frontend_action_names
for call in last_message.tool_calls
):
return "tool_node"
# Frontend action calls are returned to CopilotKit for execution in the UI.
return END
workflow = (
StateGraph(AgentState)
.add_node("chat_node", chat_node)
.add_node("tool_node", ToolNode(tools))
.add_edge(START, "chat_node")
.add_edge("tool_node", "chat_node")
.add_conditional_edges(
"chat_node",
route_after_chat,
["tool_node", END],
)
)
graph = workflow.compile(checkpointer=MemorySaver())
```
</Tab>
<Tab value="TypeScript">
```typescript
import { randomUUID } from "node:crypto";
import { z } from "zod";
import type { RunnableConfig } from "@langchain/core/runnables";
import { tool } from "@langchain/core/tools";
import type { ToolRunnableConfig } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import type { AIMessage } from "@langchain/core/messages";
import { SystemMessage, ToolMessage } from "@langchain/core/messages";
import {
Annotation,
Command,
MemorySaver,
START,
StateGraph,
} from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import {
copilotkitCustomizeConfig,
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
// 1. Define shared state with CopilotKit annotations
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
observed_steps: Annotation<string[]>,
});
type AgentState = typeof AgentStateAnnotation.State;
// 2. Define the tool with proper ToolMessage handling
const stepProgressTool = tool(
async ({ steps }, config: ToolRunnableConfig) => {
const toolCallId = config.toolCall?.id;
if (typeof toolCallId !== "string" || toolCallId.length === 0) {
throw new Error(
"StepProgressTool: missing tool_call_id — tool was invoked outside a " +
"ToolNode context. Refusing to emit a ToolMessage with an empty " +
"tool_call_id (OpenAI rejects those)."
);
}
return new Command({
update: {
observed_steps: steps,
messages: [
new ToolMessage({
content: "Steps recorded to shared state.",
name: "StepProgressTool",
id: randomUUID(),
tool_call_id: toolCallId,
}),
],
},
});
},
{
name: "StepProgressTool",
description: "Records progress by updating the steps array",
schema: z.object({
steps: z.array(z.string()),
}),
}
);
const tools = [stepProgressTool];
// 3. Define the chat node
async function chatNode(state: AgentState, config: RunnableConfig) {
const model = new ChatOpenAI({
model: "gpt-5.4",
modelKwargs: { parallel_tool_calls: false },
});
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
// Configure CopilotKit to stream tool arguments into state
const streamingConfig = copilotkitCustomizeConfig(config, {
emitIntermediateState: [
{
stateKey: "observed_steps",
tool: "StepProgressTool",
toolArgument: "steps",
},
],
});
const response = await modelWithTools.invoke(
[
new SystemMessage(
"You are a task performer. Pretend doing tasks you are given, " +
"report the steps using StepProgressTool."
),
...state.messages,
],
streamingConfig
);
return { messages: response };
}
// 4. Define routing logic
function shouldContinue({ messages, copilotkit }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const hasBackendToolCall = lastMessage.tool_calls.some((toolCall) => {
return (
!actions || actions.every((action) => action.name !== toolCall.name)
);
});
if (hasBackendToolCall) {
return "tool_node";
}
}
return "__end__";
}
// 5. Compile the graph
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chatNode)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue as any);
const memory = new MemorySaver();
export const graph = workflow.compile({
checkpointer: memory,
});
```
</Tab>
</Tabs>
</TailoredContentOption>
</TailoredContent>
</Step>
<Step>
### Observe the predictions
These predictions will be emitted as the agent runs, allowing you to track its progress before the final state is determined.
```tsx title="ui/app/page.tsx"
import { useAgent } from '@copilotkit/react-core'; // [!code highlight]
const YourMainContent = () => {
// [!code highlight:3]
const { agent } = useAgent({
agentId: "sample_agent",
});
const observedSteps = (agent.state.observed_steps as string[]) ?? [];
return (
<div>
<h1>Agent Progress</h1>
{observedSteps.length > 0 && (
<div>
<h3>Steps:</h3>
<ul>
{observedSteps.map((step, i) => (
<li key={i}>{step}</li>
))}
</ul>
</div>
)}
</div>
)
}
```
</Step>
<Step>
### Give it a try!
Now you'll notice that the state predictions are emitted as the agent makes progress, giving you insight into its work before the final state is determined.
You can apply this pattern to any long-running task in your agent.
<video src="https://cdn.copilotkit.ai/docs/copilotkit/images/coagents/intermediate-state-render.mp4" className="rounded-lg shadow-xl" loop playsInline controls autoPlay muted />
<Callout>
This video shows the result of `npx copilotkit@latest init` with the [implementation](#implementation) section applied to it!
</Callout>
</Step>
</Steps>
</TailoredContentOption>
<TailoredContentOption
id="prebuilt"
title="Prebuilt agent"
description={`I'm using a prebuilt agent like "create_agent" by LangGraph`}
>
<Steps>
<Step>
### Define the state schema
We'll define an `observed_steps` field in the state, which will be updated as the agent reports progress.
<Tabs groupId="language_langgraph_agent" items={["Python", "TypeScript"]} persist>
<Tab value="Python">
```python title="agent.py"
from copilotkit import CopilotKitState
class AgentState(CopilotKitState):
observed_steps: list[str]
```
</Tab>
<Tab value="TypeScript">
```ts title="agent-js/src/agent.ts"
import { StateSchema } from "@langchain/langgraph";
import { CopilotKitStateSchema } from "@copilotkit/sdk-js/langgraph";
import { z } from "zod";
export const AgentStateSchema = new StateSchema({
observed_steps: z.array(z.string()).default(() => []),
...CopilotKitStateSchema.fields,
});
export type AgentState = typeof AgentStateSchema.State;
```
</Tab>
</Tabs>
</Step>
<Step>
### Add StateStreamingMiddleware to your agent
Instead of manually calling emit functions or configuring the `RunnableConfig`, pass `StateStreamingMiddleware` as middleware to your prebuilt agent.
Each `StateItem` maps a tool argument to a state key — the same mapping concept as the tool-based approach in custom graphs.
<Tabs groupId="language_langgraph_agent" items={["Python", "TypeScript"]} persist>
<Tab value="Python">
```python title="agent.py"
from copilotkit import CopilotKitMiddleware, CopilotKitState, StateStreamingMiddleware, StateItem # [!code highlight]
from langchain.agents import create_agent
from langchain.tools import tool
@tool
def step_progress_tool(steps: list[str]):
"""Reports the current steps being executed"""
graph = create_agent(
model="openai:gpt-5.4",
tools=[step_progress_tool],
middleware=[
CopilotKitMiddleware(),
StateStreamingMiddleware( # [!code highlight]
StateItem(state_key="observed_steps", tool="step_progress_tool", tool_argument="steps") # [!code highlight]
), # [!code highlight]
],
system_prompt="You are a task performer. Report your steps using step_progress_tool.",
state_schema=AgentState
)
```
</Tab>
<Tab value="TypeScript">
```ts title="agent-js/src/agent.ts"
import { createAgent } from "langchain";
import { copilotkitMiddleware } from "@copilotkit/sdk-js/langgraph";
import { stateStreamingMiddleware, stateItem } from "@copilotkit/sdk-js/langgraph-middlewares"; // [!code highlight]
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const stepProgressTool = tool(
async (args) => args,
{
name: "step_progress_tool",
description: "Reports the current steps being executed",
schema: z.object({ steps: z.array(z.string()) }),
}
);
export const graph = createAgent({
model: "openai:gpt-5.4",
tools: [stepProgressTool],
middleware: [
copilotkitMiddleware,
stateStreamingMiddleware( // [!code highlight]
stateItem({ stateKey: "observed_steps", tool: "step_progress_tool", toolArgument: "steps" }) // [!code highlight]
), // [!code highlight]
],
stateSchema: AgentStateSchema,
systemPrompt: "You are a task performer. Report your steps using step_progress_tool.",
});
```
</Tab>
</Tabs>
</Step>
<Step>
### Observe the predictions
These predictions will be emitted as the agent runs, allowing you to track its progress before the final state is determined.
```tsx title="ui/app/page.tsx"
import { useAgent } from '@copilotkit/react-core'; // [!code highlight]
const YourMainContent = () => {
// [!code highlight:3]
const { agent } = useAgent({
agentId: "sample_agent",
});
const observedSteps = (agent.state.observed_steps as string[]) ?? [];
return (
<div>
<h1>Agent Progress</h1>
{observedSteps.length > 0 && (
<div>
<h3>Steps:</h3>
<ul>
{observedSteps.map((step, i) => (
<li key={i}>{step}</li>
))}
</ul>
</div>
)}
</div>
)
}
```
</Step>
<Step>
### Give it a try!
Now you'll notice that the state predictions are emitted as the agent makes progress, giving you insight into its work before the final state is determined.
You can apply this pattern to any long-running task in your agent.
<video src="https://cdn.copilotkit.ai/docs/copilotkit/images/coagents/intermediate-state-render.mp4" className="rounded-lg shadow-xl" loop playsInline controls autoPlay muted />
<Callout>
This video shows the result of `npx copilotkit@latest init` with the [implementation](#implementation) section applied to it!
</Callout>
</Step>
</Steps>
</TailoredContentOption>
</TailoredContent>
Ask the demo to draft a two-paragraph product update. The document panel should grow while the run is active, then retain the completed document.
@@ -1,214 +1,28 @@
---
title: Input/Output Schemas
icon: "lucide/Play"
description: Decide which state properties are received and returned to the frontend
title: "Shared state: read and write"
icon: "lucide/Repeat"
description: Let the UI provide preferences and let a LangGraph agent return notes through shared state.
snippet_cell: shared-state-read-write
---
## What is this?
Not all state properties are relevant for frontend-backend sharing.
This guide shows how to ensure only the right portion of state is communicated back and forth.
<InlineDemo demo="shared-state-read-write" />
This guide is based on [LangGraph's Input/Output Schema feature](https://docs.langchain.com/oss/python/langgraph/use-graph-api#define-input-and-output-schemas)
This Showcase cell has two directions: the UI writes `preferences` with `agent.setState`, and the LangGraph agent writes `notes` with its `set_notes` tool. A middleware turns the latest preferences into a system message on every run.
## When should I use this?
## Agent bridge
Depending on your implementation, some properties are meant to be processed internally, while some others are the way for the UI to communicate user input.
In addition, some state properties contain a lot of information. Syncing them back and forth between the agent and UI can be costly, while it might not have any practical benefit.
The agent declares both state fields, registers the note-writing tool, and installs the preference middleware beside `CopilotKitMiddleware`.
## Implementation
<Snippet framework="langgraph-python" cell="shared-state-read-write" region="shared-state-setup" title="src/agents/shared_state_read_write.py — preference-to-prompt and notes bridge" />
<Steps>
<Step>
### Examine our old state
LangGraph is stateful. As you transition between nodes, that state is updated and passed to the next node. For this example,
let's assume that the state our agent should be using, can be described like this:
<Tabs groupId="language_langgraph_agent" items={['Python', 'TypeScript']} default="Python" persist>
<Tab value="Python">
```python title="agent.py"
from copilotkit import CopilotKitState
from typing import Literal
## UI read and write path
class AgentState(CopilotKitState):
question: str
answer: str
resources: List[str]
```
</Tab>
<Tab value="TypeScript">
```typescript title="agent-js/sample_agent/agent.ts"
import { Annotation } from "@langchain/langgraph";
import { CopilotKitStateAnnotation } from "@copilotkit/sdk-js/langgraph";
import { z } from "zod";
Use [useAgent](/reference/hooks/useAgent) to observe notes and `agent.setState` to publish the next preference value.
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
question: Annotation<string>,
answer: Annotation<string>,
resources: Annotation<string[]>,
});
<Snippet framework="langgraph-python" cell="shared-state-read-write" region="use-agent-read" title="src/app/demos/shared-state-read-write/page.tsx — state subscription" />
export type AgentState = typeof AgentStateAnnotation.State;
```
</Tab>
</Tabs>
</Step>
<Step>
### Divide state to Input and Output
Our example case lists several state properties, which with its own purpose:
- The question is being asked by the user, expecting the llm to answer
- The answer is what the LLM returns
- The resources list will be used by the LLM to answer the question, and should not be communicated to the user, or set by them.
<Snippet framework="langgraph-python" cell="shared-state-read-write" region="use-agent-write" title="src/app/demos/shared-state-read-write/page.tsx — preference update" />
<Tabs groupId="language_langgraph_agent" items={['Python', 'TypeScript']} default="Python" persist>
<Tab value="Python">
```python title="agent.py"
from langchain_core.runnables import RunnableConfig
from copilotkit import CopilotKitState
from typing import Literal
## Try it
# Divide the state to 3 parts
# Input schema for inputs you are willing to accept from the frontend
class InputState(CopilotKitState):
question: str
# Output schema for output you are willing to pass to the frontend
class OutputState(CopilotKitState):
answer: str
# The full schema, including the inputs, outputs and internal state ("resources" in our case)
class OverallState(InputState, OutputState):
resources: List[str]
async def answer_node(state: OverallState, config: RunnableConfig):
"""
Standard chat node, meant to answer general questions.
"""
model = ChatOpenAI()
# add the input question in the system prompt so it's passed to the LLM
system_message = SystemMessage(
content=f"You are a helpful assistant. Answer the question: {state.get('question')}"
)
response = await model.ainvoke([
system_message,
*state["messages"],
], config)
# ...add the rest of the agent implementation
# extract the answer, which will be assigned to the state soon
answer = response.content
return {
"messages": response,
# include the answer in the returned state
"answer": answer
}
# finally, before compiling the graph, we define the 3 state components
builder = StateGraph(OverallState, input=InputState, output=OutputState)
# add all the different nodes and edges and compile the graph
builder.add_node("answer_node", answer_node)
builder.add_edge(START, "answer_node")
builder.add_edge("answer_node", END)
graph = builder.compile()
```
</Tab>
<Tab value="TypeScript">
```typescript title="agent-js/sample_agent/agent.ts"
import { Annotation, StateGraph, START, END } from "@langchain/langgraph";
import { CopilotKitStateAnnotation } from "@copilotkit/sdk-js/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { SystemMessage } from "@langchain/core/messages";
import type { RunnableConfig } from "@langchain/core/runnables";
// Divide the state to 3 parts
// An input annotation for inputs you are willing to accept from the frontend
const InputAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
question: Annotation<string>,
});
// Output annotation for output you are willing to pass to the frontend
const OutputAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
answer: Annotation<string>,
});
// The full annotation, including the inputs, outputs and internal state ("resources" in our case)
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
question: Annotation<string>,
answer: Annotation<string>,
resources: Annotation<string[]>,
});
export type AgentState = typeof AgentStateAnnotation.State;
async function answerNode(state: AgentState, config: RunnableConfig) {
const model = new ChatOpenAI();
const systemMessage = new SystemMessage({
content: `You are a helpful assistant. Answer the question: ${state.question}.`,
});
const response = await model.invoke(
[systemMessage, ...state.messages],
config
);
// ...add the rest of the agent implementation
// extract the answer, which will be assigned to the state soon
const answer = typeof response.content === 'string'
? response.content
: JSON.stringify(response.content);
return {
messages: [response],
// include the answer in the returned state
answer,
}
}
// finally, before compiling the graph, we define the 3 state components
// StateGraph accepts the full state annotation as the first parameter,
// with optional input/output annotations to filter what's communicated with the frontend
const workflow = new StateGraph(AgentStateAnnotation, {
input: InputAnnotation,
output: OutputAnnotation,
})
.addNode("answer_node", answerNode)
.addEdge(START, "answer_node")
.addEdge("answer_node", END);
export const graph = workflow.compile();
```
</Tab>
</Tabs>
</Step>
<Step>
### Give it a try!
Now that we know which state properties our agent emits, we can inspect the state and expect the following to happen:
- While we are able to provide a question, we will not receive it back from the agent. If we are using it in our UI, we need to remember the UI is the source of truth for it
- Answer will change once it's returned back from the agent
- The UI has no access to resources.
```tsx
import { useAgent } from "@copilotkit/react-core/v2"; // [!code highlight]
const { agent } = useAgent({
agentId: "sample_agent",
});
const answer = agent.state.answer as string;
console.log(answer) // You can expect seeing "answer" change, while the others are not returned from the agent
```
</Step>
</Steps>
Set a name and tone in **Your preferences**, then ask the agent to remember a detail. Its reply should use the selected preferences and the note should appear in the notes card.
@@ -1,13 +1,17 @@
---
title: Multimodal Attachments
icon: "lucide/Paperclip"
description: Let users send images, audio, video, and documents to the AI alongside their messages.
description: Configure chat attachments; the selected Showcase path verifies image and PDF turns.
snippet_cell: multimodal
---
<InlineDemo demo="multimodal" />
You have a working CopilotChat and want users to attach files — images, PDFs, audio, video — that the AI can see and respond to. By the end of this guide, your chat will support drag-and-drop file attachments with previews, lightbox viewing, and multimodal AI responses.
You have a working CopilotChat and want users to attach files alongside their
messages. The selected Showcase path below verifies image and PDF attachments.
The attachments API can accept other MIME types in your own app, but that does
not establish that a selected Showcase agent or its configured model processes
those types.
## The Showcase attachment path
@@ -29,20 +33,27 @@ responds.
## Quick start
Add `attachments` to your `CopilotChat` component:
For a smaller application-owned setup, add an `attachments` configuration to
your `CopilotChat` component. Start with the same image-and-PDF policy the
selected Showcase demo uses:
```tsx title="page.tsx"
import { CopilotChat } from "@copilotkit/react-core/v2";
<CopilotChat
agentId="my-agent"
attachments={{ enabled: true }} // [!code highlight]
attachments={{
enabled: true,
accept: "image/*,application/pdf",
maxSize: 10 * 1024 * 1024,
}} // [!code highlight]
/>
```
That's it. Users can now click the attachment button or drag-and-drop files into the chat. The files are sent as part of the message content to your agent.
Users can now click the attachment button or drag files into the chat. Validate
the MIME types with the model and agent runtime you deploy.
## Configuration
## Attachments API configuration
The `attachments` prop accepts an `AttachmentsConfig` object:
@@ -51,8 +62,8 @@ The `attachments` prop accepts an `AttachmentsConfig` object:
attachments={{
enabled: true,
// Omit `accept` to allow all file types (default: "*/*").
// Restrict with a MIME filter if needed:
accept: "image/*,audio/*,video/*,application/pdf",
// Choose only types your agent and model handle:
accept: "image/*,application/pdf",
maxSize: 10 * 1024 * 1024, // 10MB limit (default: 20MB)
}}
/>
@@ -67,19 +78,21 @@ The `attachments` prop accepts an `AttachmentsConfig` object:
| `onUpload` | `(file: File) => AttachmentUploadResult \| Promise<...>` | — | Custom upload handler. See [Custom upload handler](#custom-upload-handler). |
| `onUploadFailed` | `(error: AttachmentUploadError) => void` | — | Called when a file fails validation or upload. See [Handling upload errors](#handling-upload-errors). |
## Supported file types
## File types and model support
Attachments are categorized by modality based on their MIME type:
`accept` controls the browser file picker; it does not make a model understand
the selected file. The selected Showcase cells exercise only these paths:
| Modality | MIME types | Preview | AI support |
| --- | --- | --- | --- |
| **Image** | `image/*` | Thumbnail with lightbox | Supported by most vision-capable models (GPT-4o, Claude, etc.) |
| **Audio** | `audio/*` | Audio player | Model-dependent |
| **Video** | `video/*` | Thumbnail with play button + lightbox | Model-dependent |
| **Document** | Everything else | File icon + name; PDF and text get lightbox preview | Sent as file content — model support varies |
| Type | Selected Showcase evidence | Your application |
| --- | --- | --- |
| **Image** | Image upload and a sample image turn | Use a vision-capable agent/model. |
| **PDF** | PDF upload and a sample PDF turn | Ensure your agent extracts or otherwise handles document content. |
| **Audio or video** | No selected Showcase end-to-end claim | Add your MIME filter only after validating the agent and model path. |
<Callout type="info">
Not all models support all modalities. For example, OpenAI's GPT-4o supports images but not audio file parts. If the model doesn't support a file type, you'll get a `RUN_ERROR` event. Use the `onError` callback to handle this gracefully.
Model and runtime support varies by MIME type. Handle a rejected or failed
attachment with `onUploadFailed`, and handle errors returned after dispatch
with `onError`.
</Callout>
## Custom upload handler
@@ -214,6 +227,9 @@ When a user attaches files and sends a message, CopilotKit:
1. Reads each file (via the default base64 reader or your `onUpload` handler)
2. Builds an array of `InputContent` parts — text + one part per attachment
3. Adds the message to the agent with `content: [{ type: "text", ... }, { type: "image", source: ... }, ...]`
4. The agent receives the multimodal content via the AG-UI protocol and forwards it to the model
4. The agent receives the multipart content through AG-UI and handles it
according to its model integration
The attachments are part of the standard AG-UI `InputContent` schema, so any AG-UI-compatible agent (BuiltInAgent, LangGraph, custom) can receive them.
Attachments use the standard AG-UI `InputContent` schema. That protocol shape
does not by itself prove that a given agent or model supports every MIME type;
use the selected image/PDF path above or validate your own runtime path.
+25 -6
View File
@@ -1,19 +1,22 @@
---
title: Voice
description: Real-time speech-to-text in the chat composer. The user speaks, the runtime transcribes, the agent runs the resulting prompt.
description: Configure runtime speech-to-text that places a transcript in the chat composer.
icon: "lucide/Mic"
snippet_cell: voice
---
<InlineDemo demo="voice" />
You have a working chat surface and you want users to be able to speak instead of type. By the end of this guide, the chat composer will sprout a mic button, recorded audio will be transcribed by the runtime, and the transcript will auto-send to the agent like any other message.
You have a working chat surface and want users to be able to speak instead of
type. The runtime can transcribe recorded audio into editable text in the chat
composer; the user can review and send that text as a normal message.
## When to use this
- **Hands-free or accessibility flows** where typing isn't the right input modality.
- **Mobile or kiosk surfaces** where a long voice query is faster than thumb-typing.
- **Demo and test loops** where you want canned audio to drive the chat without a microphone.
- **Demo and test loops** where a prepared text sample can exercise the composer
without a microphone.
If you only need file uploads (audio, images, video, documents), use [Multimodal Attachments](/multimodal-attachments) instead. Voice is specifically about live transcription of recorded speech into chat input.
@@ -23,15 +26,31 @@ If you only need file uploads (audio, images, video, documents), use [Multimodal
<Snippet region="voice-page" title="frontend/src/app/page.tsx — chat surface" />
The `runtimeUrl="/api/copilotkit-voice"` points the browser to your Next.js API route. When the user clicks the mic, the chat captures audio, POSTs it to that runtime route's `/transcribe` endpoint, drops the resulting transcript into the composer, and submits.
The `runtimeUrl="/api/copilotkit-voice"` points the browser to your Next.js API
route. When the user clicks the mic, the chat captures audio, POSTs it to that
runtime route's `/transcribe` endpoint, and places the resulting transcript in
the composer for review.
### Driving the demo without a mic
For Playwright runs, screenshots, or any flow where prompting for mic permissions is awkward, ship a button that emits a canned sample phrase through an `onTranscribed` callback, bypassing the transcription endpoint entirely:
For Playwright runs, screenshots, or any flow where prompting for mic
permissions is awkward, the Showcase offers a button that inserts a prepared
sample phrase through an `onTranscribed` callback. It bypasses microphone
capture and the transcription endpoint entirely:
<Snippet region="sample-audio-button" title="frontend/src/app/sample-audio-button.tsx" />
The parent chat component can then drop that text into the composer's textarea (matched via `data-testid="copilot-chat-textarea"`) using the native value setter and a synthetic `input` event so React's managed state updates correctly.
The parent chat component drops that text into the composer's textarea (matched
via `data-testid="copilot-chat-textarea"`) using the native value setter and a
synthetic `input` event so React's managed state updates correctly. This proves
the prepared-text path only; validate microphone permissions and transcription
with your configured runtime separately.
## Try it
Wait until **Try a sample audio** is enabled, select it, then send the inserted
weather question from the composer. The buttons label is historical: it
inserts prepared text and does not upload or transcribe audio.
## Backend
@@ -108,8 +108,20 @@ test("gives LangGraph TypeScript a source-backed existing-agent path", () => {
expect(guide).toContain('file="src/agent/package.json"');
expect(guide).toContain('file="src/agent/langgraph.json"');
expect(guide).toContain('region="cli-start-graph-export"');
expect(guide).toContain(
"git clone --depth 1 https://github.com/CopilotKit/CopilotKit.git",
);
expect(guide).toContain(
"cd CopilotKit/showcase/integrations/langgraph-typescript",
);
expect(guide).toContain("cp .env.example .env");
expect(guide).toContain("cd src/agent");
expect(guide).toContain("npm ci");
expect(guide).toContain("npm install");
expect(guide).toContain("npm run dev");
expect(guide).toContain("not a starter project");
expect(guide).toContain("reference only");
expect(guide).not.toContain("mkdir my-langgraph-agent");
const output = renderPageToLlmText({
url: "langgraph-typescript/quickstart",
@@ -123,10 +135,140 @@ test("gives LangGraph TypeScript a source-backed existing-agent path", () => {
);
expect(output).toContain('"starterAgent": "./graph.ts:graph"');
expect(output).toContain("export const graph = workflow.compile");
expect(output).toContain("Want the complete runnable Showcase agent?");
expect(output).toContain(
"git clone --depth 1 https://github.com/CopilotKit/CopilotKit.git",
);
expect(output).toContain("cp .env.example .env");
expect(output).toContain("configuration is a reference only");
expect(output).not.toContain("Missing snippet");
expect(output).not.toContain("<Snippet");
});
test("limits selected Showcase multimodal claims to images and PDFs", () => {
const guide = source("multimodal-attachments");
expect(guide).toContain("snippet_cell: multimodal");
expect(guide).toContain('region="multimodal-attachments"');
expect(guide).toContain('region="multimodal-upload-adapter"');
expect(guide).toContain('accept: "image/*,application/pdf"');
expect(guide).toContain("No selected Showcase end-to-end claim");
expect(guide).not.toContain("Audio player");
expect(guide).not.toContain(
"GPT-4o supports images but not audio file parts",
);
const doc = loadDoc("multimodal-attachments");
if (!doc) throw new Error("Missing multimodal attachments guide");
const output = renderPageToLlmText({
url: "built-in-agent/multimodal-attachments",
title: doc.fm.title,
filePath: doc.filePath,
loadSlug: "multimodal-attachments",
framework: "built-in-agent",
});
expect(output).toContain("Try with sample image");
expect(output).toContain("accept: ACCEPT_MIME");
expect(output).toContain("No selected Showcase end-to-end claim");
expect(output).not.toContain("Missing snippet");
expect(output).not.toContain("<Snippet");
});
test("separates the Voice sample-text path from microphone transcription", () => {
const guide = source("voice");
expect(guide).toContain("snippet_cell: voice");
expect(guide).toContain('region="voice-page"');
expect(guide).toContain('region="sample-audio-button"');
expect(guide).toContain('region="voice-runtime"');
expect(guide).toContain(
"prepared text and does not upload or transcribe audio",
);
expect(guide).not.toContain("transcript will auto-send");
const doc = loadDoc("voice");
if (!doc) throw new Error("Missing voice guide");
const output = renderPageToLlmText({
url: "built-in-agent/voice",
title: doc.fm.title,
filePath: doc.filePath,
loadSlug: "voice",
framework: "built-in-agent",
});
expect(output).toContain("Try a sample audio");
expect(output).toContain(
"prepared text and does not upload or transcribe audio",
);
expect(output).not.toContain("Missing snippet");
expect(output).not.toContain("<Snippet");
});
test("replaces four LangGraph Python legacy viewer guides with Showcase sources", () => {
const cases = [
{
slug: "integrations/langgraph/shared-state/predictive-state-updates",
url: "langgraph-python/shared-state/predictive-state-updates",
sourceTerms: [
"shared-state-streaming",
"state-streaming-middleware",
"frontend-use-coagent-state",
],
outputTerms: [
"StateStreamingMiddleware",
'agentId: "shared-state-streaming"',
],
},
{
slug: "integrations/langgraph/shared-state/state-inputs-outputs",
url: "langgraph-python/shared-state/state-inputs-outputs",
sourceTerms: [
"shared-state-read-write",
"shared-state-setup",
"use-agent-write",
],
outputTerms: ["PreferencesInjectorMiddleware", "agent.setState"],
},
{
slug: "integrations/langgraph/agent-app-context",
url: "langgraph-python/agent-app-context",
sourceTerms: [
"readonly-state-agent-context",
"use-agent-context-call",
"agent-context-setup",
],
outputTerms: ["useAgentContext({", "CopilotKitMiddleware"],
},
{
slug: "integrations/langgraph/multi-agent-flows",
url: "langgraph-python/multi-agent-flows",
sourceTerms: [
"subagents",
"supervisor-delegation-tools",
"delegation-log-frontend",
],
outputTerms: ["research_agent", "useRenderTool("],
},
];
for (const item of cases) {
const guide = source(item.slug);
for (const term of item.sourceTerms) expect(guide).toContain(term);
expect(guide).not.toContain("IframeSwitcher");
expect(guide).not.toContain("feature-viewer.copilotkit.ai");
const doc = loadDoc(item.slug);
if (!doc) throw new Error(`Missing ${item.slug}`);
const output = renderPageToLlmText({
url: item.url,
title: doc.fm.title,
filePath: doc.filePath,
loadSlug: item.slug,
framework: "langgraph-python",
});
for (const term of item.outputTerms) expect(output).toContain(term);
expect(output).not.toContain("Missing snippet");
expect(output).not.toContain("<Snippet");
}
});
test("uses the CrewAI Showcase flow and browser tool for HITL", () => {
const guide = source("integrations/crewai-flows/human-in-the-loop/flow");
expect(guide).toContain('framework="crewai-crews"');
@@ -0,0 +1,32 @@
import { expect, test } from "vitest";
import { resolveFrameworkContent } from "../framework-content-resolution";
test("resolves the same sparse overrides used by framework-scoped HTML", () => {
expect(
resolveFrameworkContent("langgraph-python", "auth")?.contentSlugPath,
).toBe("auth");
expect(
resolveFrameworkContent("built-in-agent", "auth")?.contentSlugPath,
).toBe("auth");
expect(
resolveFrameworkContent("langgraph-typescript", "quickstart")
?.contentSlugPath,
).toBe("integrations/langgraph/quickstart");
expect(
resolveFrameworkContent("google-adk", "threads-import")?.contentSlugPath,
).toBe("integrations/adk/threads-import");
});
test("keeps generated root pages ahead of sparse framework fallbacks", () => {
expect(
resolveFrameworkContent("langgraph-python", "agentic-chat-ui")
?.contentSlugPath,
).toBe("agentic-chat-ui");
});
test("does not synthesize content for unknown framework paths", () => {
expect(
resolveFrameworkContent("langgraph-python", "not-a-real-guide"),
).toBeNull();
});
@@ -0,0 +1,104 @@
import { expect, test } from "vitest";
import { loadDoc } from "../docs-render";
import { renderPageToLlmText } from "../llm-text";
import {
selectedShowcaseGuideBindings,
showcaseSourceCarrier,
} from "../selected-showcase-guide-bindings";
/** A deliberate registry alias, not a claim that quickstart is a demo source. */
const ACCEPTED_NON_CARRIER_BINDINGS = new Set(["built-in-agent:agentic-chat"]);
test("selected runnable Showcase cells resolve to an effective guide with a source path", () => {
const bindings = selectedShowcaseGuideBindings();
expect(bindings.length).toBeGreaterThan(0);
const missing = bindings
.filter((binding) => {
const key = `${binding.framework}:${binding.cell}`;
return (
binding.route !== null &&
!ACCEPTED_NON_CARRIER_BINDINGS.has(key) &&
showcaseSourceCarrier(binding) === null
);
})
.map(
(binding) =>
`${binding.framework}:${binding.cell} -> ${binding.contentSlugPath}`,
);
expect(missing).toEqual([]);
const accepted = bindings.filter((binding) =>
ACCEPTED_NON_CARRIER_BINDINGS.has(`${binding.framework}:${binding.cell}`),
);
expect(accepted).toHaveLength(1);
expect(accepted[0]!.contentSlugPath).toBe(
"integrations/built-in-agent/quickstart",
);
expect(showcaseSourceCarrier(accepted[0]!)).toBeNull();
const renderCache = new Map<string, string>();
const unresolvedOutput = new Set<string>();
for (const binding of bindings) {
const key = `${binding.framework}:${binding.contentSlugPath}`;
const carrier = showcaseSourceCarrier(binding);
if (
binding.route === null ||
ACCEPTED_NON_CARRIER_BINDINGS.has(
`${binding.framework}:${binding.cell}`,
) ||
carrier === null
) {
continue;
}
let output = renderCache.get(key);
if (!output) {
const doc = loadDoc(binding.contentSlugPath);
if (!doc) throw new Error(`Missing effective guide: ${key}`);
output = renderPageToLlmText(
{
url: `${binding.framework}/${binding.contentSlugPath}`,
title: doc.fm.title,
description: doc.fm.description,
filePath: doc.filePath,
loadSlug: binding.contentSlugPath,
framework: binding.framework,
},
{ framework: binding.framework },
);
renderCache.set(key, output);
}
for (const marker of [
"Missing snippet",
"snippet skipped:",
"setup skipped:",
"<Snippet",
"<FrameworkSetup",
]) {
if (output.includes(marker)) {
unresolvedOutput.add(`${key}: ${marker}`);
}
}
if (carrier === "inline-demo") {
if (!output.includes("<!-- interactive demo:")) {
unresolvedOutput.add(`${key}: interactive demo missing`);
}
}
}
expect([...unresolvedOutput].sort()).toEqual([]);
});
test("selected command-only cells still resolve an effective guide", () => {
const commandCells = selectedShowcaseGuideBindings().filter(
(binding) => binding.command !== null,
);
expect(commandCells.length).toBeGreaterThan(0);
expect(
commandCells.every((binding) => binding.contentSlugPath.length > 0),
).toBe(true);
});
@@ -2,6 +2,23 @@ import { describe, expect, it } from "vitest";
import { matchesSeoRedirectSource, seoRedirects } from "../seo-redirects";
describe("seoRedirects", () => {
it("keeps the retired LangGraph shared-state write URL on its source-backed guide", () => {
expect(seoRedirects).toEqual(
expect.arrayContaining([
{
id: "LGP-state-write-legacy",
source: "/langgraph/shared-state/in-app-agent-write",
destination: "/langgraph-python/shared-state/state-inputs-outputs",
},
{
id: "LGP-state-write-canonical",
source: "/langgraph-python/shared-state/in-app-agent-write",
destination: "/langgraph-python/shared-state/state-inputs-outputs",
},
]),
);
});
it("consolidates Conversational Flow URLs under CrewAI", () => {
expect(seoRedirects).toEqual(
expect.arrayContaining([
@@ -0,0 +1,50 @@
import { loadDoc } from "./docs-render";
import { getDocsFolder, getDocsMode } from "./registry";
/**
* Resolve the MDX source for a framework-scoped docs URL.
*
* This is deliberately shared by the HTML page and the `.md` / `.mdx`
* endpoint. Keeping the candidate order here prevents raw Markdown from
* silently choosing a different framework override than the page a reader
* sees. The caller still owns route-only behavior such as framework landing
* pages and the "not available" shell.
*/
export function resolveFrameworkContent(
framework: string,
slugPath: string,
): {
contentSlugPath: string;
doc: NonNullable<ReturnType<typeof loadDoc>>;
} | null {
const docsMode = getDocsMode(framework);
if (docsMode === "hidden") return null;
const docsFolder = getDocsFolder(framework);
const frameworkSlugPath = `integrations/${docsFolder}/${slugPath}`;
const rootSlugPath = slugPath;
// Authored integrations own their pages. Generated integrations use root
// content by default, except these root routing shims which intentionally
// resolve to a framework-specific guide when it exists.
const frameworkFirst =
docsMode === "authored" ||
slugPath === "quickstart" ||
slugPath === "threads-import";
const candidates = frameworkFirst
? [frameworkSlugPath, rootSlugPath]
: [rootSlugPath, frameworkSlugPath];
// The raw-Markdown resolver asks for `index` when a framework path has no
// tail. The HTML route renders its landing page separately, but Markdown
// should still expose that framework's quickstart when it has no index.mdx.
if (slugPath === "index") {
candidates.push(`integrations/${docsFolder}/quickstart`);
}
for (const candidate of new Set(candidates)) {
const doc = loadDoc(candidate);
if (doc) return { contentSlugPath: candidate, doc };
}
return null;
}
+7 -1
View File
@@ -5,6 +5,7 @@ export interface Feature {
name: string;
category: string;
description: string;
shell_docs_path?: string | null;
}
export interface FeatureCategory {
@@ -17,7 +18,9 @@ export interface Demo {
name: string;
description: string;
tags: string[];
route: string;
route?: string | null;
command?: string | null;
highlight?: string[];
animated_preview_url?: string | null;
}
@@ -121,6 +124,9 @@ export interface Integration {
};
features: string[];
demos: Demo[];
docs_links?: {
features?: Record<string, { shell_docs_path?: string | null }>;
};
}
export interface Registry {
@@ -0,0 +1,101 @@
import { inlineSnippets } from "./docs-render";
import { resolveFrameworkContent } from "./framework-content-resolution";
import { getIntegration, getRegistry } from "./registry";
/** The selected-five React audit scope. This is a scope, not a docs mapping. */
export const SELECTED_REACT_INTEGRATIONS = [
"langgraph-python",
"langgraph-typescript",
"google-adk",
"strands",
"built-in-agent",
] as const;
export interface SelectedShowcaseGuideBinding {
framework: (typeof SELECTED_REACT_INTEGRATIONS)[number];
cell: string;
route: string | null;
command: string | null;
contentSlugPath: string;
rawSource: string;
source: string;
}
function docsPathForFeature(
framework: string,
featureId: string,
): string | null {
const integration = getIntegration(framework);
const integrationPath =
integration?.docs_links?.features?.[featureId]?.shell_docs_path;
const registryPath = getRegistry().feature_registry.features.find(
(feature) => feature.id === featureId,
)?.shell_docs_path;
const path = integrationPath ?? registryPath;
return typeof path === "string" && path.startsWith("/")
? path.slice(1)
: null;
}
/**
* Derive actual selected feature-guide bindings from registry demos and their
* canonical docs-links. There is intentionally no hand-maintained list of
* feature routes or cells here.
*/
export function selectedShowcaseGuideBindings(): SelectedShowcaseGuideBinding[] {
const bindings: SelectedShowcaseGuideBinding[] = [];
for (const framework of SELECTED_REACT_INTEGRATIONS) {
const integration = getIntegration(framework);
if (!integration) continue;
const unavailable = new Set(integration.not_supported_features ?? []);
for (const demo of integration.demos) {
if (unavailable.has(demo.id)) continue;
const docsPath = docsPathForFeature(framework, demo.id);
if (!docsPath) continue;
const resolved = resolveFrameworkContent(framework, docsPath);
if (!resolved) continue;
bindings.push({
framework,
cell: demo.id,
route: demo.route ?? null,
command: demo.command ?? null,
contentSlugPath: resolved.contentSlugPath,
rawSource: resolved.doc.source,
source: inlineSnippets(resolved.doc.source, resolved.contentSlugPath),
});
}
}
return bindings;
}
export type ShowcaseSourceCarrier =
| "inline-demo"
| "snippet"
| "framework-setup"
| "shared-content";
/**
* Identify the source carrier after shared MDX imports are expanded. The
* caller must still render it: a carrier tag alone is not proof that a
* framework has the requested region or setup bundle.
*/
export function showcaseSourceCarrier(
binding: SelectedShowcaseGuideBinding,
): ShowcaseSourceCarrier | null {
if (/<InlineDemo\b/.test(binding.source)) return "inline-demo";
if (/<Snippet\b/.test(binding.source)) return "snippet";
if (/<FrameworkSetup\b/.test(binding.source)) return "framework-setup";
// SharedContent is an imported MDX source carrier. It is used by the slots
// wrapper with a runtime `components` prop, so it intentionally remains a
// component boundary after inlining rather than looking like a bare Snippet.
return /import\s+SharedContent\s+from\s+["']@\/snippets\//.test(
binding.rawSource,
)
? "shared-content"
: null;
}
@@ -359,6 +359,19 @@ const DEEP_COAGENTS: RedirectEntry[] = [
// ---------------------------------------------------------------------------
const SPECIFIC_FRAMEWORK: RedirectEntry[] = [
// The selected LGP shared-state cell now documents the source-backed
// state-inputs-outputs guide. Keep both the legacy and canonical slugs
// from serving the retired Feature Viewer page.
{
id: "LGP-state-write-legacy",
source: "/langgraph/shared-state/in-app-agent-write",
destination: "/langgraph-python/shared-state/state-inputs-outputs",
},
{
id: "LGP-state-write-canonical",
source: "/langgraph-python/shared-state/in-app-agent-write",
destination: "/langgraph-python/shared-state/state-inputs-outputs",
},
{
id: "CF-mode-parity",
source: "/crewai-conversational-flows/feature-parity",
@@ -0,0 +1,59 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { fileToDataAttachment } from "../../../shared/react/demos/multimodal/file-to-data-attachment";
describe("fileToDataAttachment", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("uses FileReader data URLs to preserve bytes, MIME type, name, and size", async () => {
const file = new File([new Uint8Array([0, 1, 2, 255])], "receipt.pdf", {
type: "application/pdf",
});
await expect(fileToDataAttachment(file)).resolves.toEqual({
type: "data",
value: "AAEC/w==",
mimeType: "application/pdf",
metadata: {
filename: "receipt.pdf",
size: 4,
},
});
});
it("keeps an empty file valid and supplies the runtime-safe fallback MIME type", async () => {
const file = new File([], "recording", { type: "" });
await expect(fileToDataAttachment(file)).resolves.toEqual({
type: "data",
value: "",
mimeType: "application/octet-stream",
metadata: {
filename: "recording",
size: 0,
},
});
});
it("rejects when the browser FileReader cannot read the selected file", async () => {
class FailingFileReader {
error = new DOMException("read failed", "NotReadableError");
onerror: ((event: ProgressEvent<FileReader>) => void) | null = null;
onload: ((event: ProgressEvent<FileReader>) => void) | null = null;
result: string | ArrayBuffer | null = null;
readAsDataURL(_file: Blob) {
this.onerror?.(new ProgressEvent("error") as ProgressEvent<FileReader>);
}
}
vi.stubGlobal("FileReader", FailingFileReader);
await expect(
fileToDataAttachment(
new File(["broken"], "broken.pdf", { type: "application/pdf" }),
),
).rejects.toThrow("read failed");
});
});
@@ -0,0 +1,484 @@
{
"integration": "built-in-agent",
"scope": "Local strict AIMock replay; public 1.71.1 plus local core URL fix. Published unpatched compatibility remains RED. Voice covers a prepared transcript, not audio transcription.",
"before_log": "stable1711-localcore-built-in-agent-full-d6.log",
"after_log": "repair021-built-in-agent-full-d6-strict-turn-final.log",
"before_counts": {
"total": 39,
"passed": 31
},
"after_counts": {
"total": 39,
"passed": 38
},
"checks": [
{
"feature": "a2ui-recovery",
"before": {
"pass": true,
"duration_ms": 7851
},
"after": {
"pass": true,
"duration_ms": 6618
},
"catalog_disposition": "published"
},
{
"feature": "agent-config",
"before": {
"pass": true,
"duration_ms": 14817
},
"after": {
"pass": true,
"duration_ms": 14868
},
"catalog_disposition": "published"
},
{
"feature": "agentic-chat",
"before": {
"pass": true,
"duration_ms": 8537
},
"after": {
"pass": true,
"duration_ms": 8868
},
"catalog_disposition": "published"
},
{
"feature": "auth",
"before": {
"pass": true,
"duration_ms": 54415
},
"after": {
"pass": true,
"duration_ms": 53114
},
"catalog_disposition": "published"
},
{
"feature": "beautiful-chat-bar-chart",
"before": {
"pass": true,
"duration_ms": 7764
},
"after": {
"pass": true,
"duration_ms": 6714
},
"catalog_disposition": "published"
},
{
"feature": "beautiful-chat-pie-chart",
"before": {
"pass": true,
"duration_ms": 7733
},
"after": {
"pass": true,
"duration_ms": 6794
},
"catalog_disposition": "published"
},
{
"feature": "beautiful-chat-schedule-meeting",
"before": {
"pass": true,
"duration_ms": 4928
},
"after": {
"pass": true,
"duration_ms": 5719
},
"catalog_disposition": "published"
},
{
"feature": "beautiful-chat-search-flights",
"before": {
"pass": true,
"duration_ms": 6927
},
"after": {
"pass": true,
"duration_ms": 5867
},
"catalog_disposition": "published"
},
{
"feature": "beautiful-chat-toggle-theme",
"before": {
"pass": true,
"duration_ms": 7742
},
"after": {
"pass": true,
"duration_ms": 6614
},
"catalog_disposition": "published"
},
{
"feature": "byoc",
"before": {
"pass": true,
"duration_ms": 5881
},
"after": {
"pass": true,
"duration_ms": 4544
},
"catalog_disposition": "published"
},
{
"feature": "chat-css",
"before": {
"pass": true,
"duration_ms": 6162
},
"after": {
"pass": true,
"duration_ms": 4707
},
"catalog_disposition": "published"
},
{
"feature": "chat-slots",
"before": {
"pass": true,
"duration_ms": 5425
},
"after": {
"pass": true,
"duration_ms": 4893
},
"catalog_disposition": "published"
},
{
"feature": "frontend-tools",
"before": {
"pass": true,
"duration_ms": 9686
},
"after": {
"pass": true,
"duration_ms": 9401
},
"catalog_disposition": "published"
},
{
"feature": "frontend-tools-async",
"before": {
"pass": false,
"duration_ms": 126862
},
"after": {
"pass": true,
"duration_ms": 5331
},
"catalog_disposition": "published"
},
{
"feature": "gen-ui-a2ui-fixed",
"before": {
"pass": true,
"duration_ms": 5588
},
"after": {
"pass": true,
"duration_ms": 4492
},
"catalog_disposition": "published"
},
{
"feature": "gen-ui-agent",
"before": {
"pass": true,
"duration_ms": 9939
},
"after": {
"pass": true,
"duration_ms": 9966
},
"catalog_disposition": "published"
},
{
"feature": "gen-ui-custom",
"before": {
"pass": true,
"duration_ms": 5798
},
"after": {
"pass": true,
"duration_ms": 4589
},
"catalog_disposition": "published"
},
{
"feature": "gen-ui-declarative",
"before": {
"pass": true,
"duration_ms": 11950
},
"after": {
"pass": true,
"duration_ms": 10851
},
"catalog_disposition": "published"
},
{
"feature": "gen-ui-headless-complete",
"before": {
"pass": false,
"duration_ms": 160444
},
"after": {
"pass": true,
"duration_ms": 20800
},
"catalog_disposition": "published"
},
{
"feature": "gen-ui-open",
"before": {
"pass": false,
"duration_ms": 187126
},
"after": {
"pass": true,
"duration_ms": 4650
},
"catalog_disposition": "published"
},
{
"feature": "gen-ui-open-advanced",
"before": {
"pass": false,
"duration_ms": 67763
},
"after": {
"pass": true,
"duration_ms": 4547
},
"catalog_disposition": "published"
},
{
"feature": "headless-simple",
"before": {
"pass": true,
"duration_ms": 13907
},
"after": {
"pass": true,
"duration_ms": 12510
},
"catalog_disposition": "published"
},
{
"feature": "hitl-approve-deny",
"before": {
"pass": true,
"duration_ms": 7936
},
"after": {
"pass": true,
"duration_ms": 6356
},
"catalog_disposition": "published"
},
{
"feature": "hitl-text-input",
"before": {
"pass": true,
"duration_ms": 7342
},
"after": {
"pass": true,
"duration_ms": 6354
},
"catalog_disposition": "published"
},
{
"feature": "mcp-apps",
"before": {
"pass": true,
"duration_ms": 7304
},
"after": {
"pass": true,
"duration_ms": 6676
},
"catalog_disposition": "published"
},
{
"feature": "multimodal",
"before": {
"pass": false,
"duration_ms": 122065
},
"after": {
"pass": true,
"duration_ms": 5377
},
"catalog_disposition": "published"
},
{
"feature": "prebuilt-popup",
"before": {
"pass": true,
"duration_ms": 6113
},
"after": {
"pass": true,
"duration_ms": 4897
},
"catalog_disposition": "published"
},
{
"feature": "prebuilt-sidebar",
"before": {
"pass": true,
"duration_ms": 6130
},
"after": {
"pass": true,
"duration_ms": 4660
},
"catalog_disposition": "published"
},
{
"feature": "readonly-state-context",
"before": {
"pass": false,
"duration_ms": 130296
},
"after": {
"pass": true,
"duration_ms": 5009
},
"catalog_disposition": "published"
},
{
"feature": "reasoning-custom",
"before": {
"pass": true,
"duration_ms": 5661
},
"after": {
"pass": true,
"duration_ms": 4561
},
"catalog_disposition": "published"
},
{
"feature": "reasoning-default",
"before": {
"pass": true,
"duration_ms": 5324
},
"after": {
"pass": true,
"duration_ms": 4557
},
"catalog_disposition": "published"
},
{
"feature": "shared-state-read",
"before": {
"pass": true,
"duration_ms": 8151
},
"after": {
"pass": true,
"duration_ms": 6861
},
"catalog_disposition": "published"
},
{
"feature": "shared-state-write",
"before": {
"pass": true,
"duration_ms": 7934
},
"after": {
"pass": true,
"duration_ms": 7068
},
"catalog_disposition": "published"
},
{
"feature": "subagents",
"before": {
"pass": true,
"duration_ms": 8851
},
"after": {
"pass": true,
"duration_ms": 7968
},
"catalog_disposition": "published"
},
{
"feature": "threadid-frontend-tool-roundtrip",
"before": {
"pass": false,
"duration_ms": 122374
},
"after": {
"pass": false,
"duration_ms": 120695
},
"catalog_disposition": "unshipped"
},
{
"feature": "tool-rendering",
"before": {
"pass": true,
"duration_ms": 6453
},
"after": {
"pass": true,
"duration_ms": 5557
},
"catalog_disposition": "published"
},
{
"feature": "tool-rendering-custom-catchall",
"before": {
"pass": true,
"duration_ms": 8656
},
"after": {
"pass": true,
"duration_ms": 7852
},
"catalog_disposition": "published"
},
{
"feature": "tool-rendering-default-catchall",
"before": {
"pass": true,
"duration_ms": 5551
},
"after": {
"pass": true,
"duration_ms": 5109
},
"catalog_disposition": "published"
},
{
"feature": "voice",
"before": {
"pass": false,
"duration_ms": 62402
},
"after": {
"pass": true,
"duration_ms": 2559
},
"catalog_disposition": "published"
}
]
}
@@ -0,0 +1,17 @@
# Built-in Agent local runtime comparison
Latest bounded full runs: **31/39 before → 38/39 after**. All 38 published checks pass; the unshipped thread-ID demo remains a strict-fixture failure.
| Check | Before | After |
| ------------------------ | ------ | ----- |
| frontend-tools-async | Fail | Pass |
| gen-ui-headless-complete | Fail | Pass |
| gen-ui-open | Fail | Pass |
| gen-ui-open-advanced | Fail | Pass |
| multimodal | Fail | Pass |
| readonly-state-context | Fail | Pass |
| voice | Fail | Pass |
The runs use local AIMock and the unreleased core URL repair. Fixes include both application behavior and test/replay accuracy; this is not a claim that seven independent backend defects were fixed. Voice validates the prepared transcript handoff, not microphone capture or transcription. Exact per-check outcomes and original log paths are in [the machine-readable comparison](built-in-agent-runtime-comparison-20260913.json).
Normal-browser observations: [image and PDF](built-in-agent-multimodal-browser-20260913.md), [voice sample](built-in-agent-voice-browser-20260913.md).
@@ -0,0 +1,7 @@
# Built-in Agent voice sample: normal-browser check
2026-09-13, actual fresh IAB route `http://localhost:3117/demos/voice`, public SDK 1.71.1 plus the local core URL repair and strict AIMock. No harness headers.
The page initially displayed a disabled **Connecting…** sample button. After discovery it enabled **Try a sample audio**. Clicking it inserted `What is the weather in Tokyo?` into the composer. Pressing Enter produced the visible assistant response: “Looking up the weather in Tokyo for you. Tokyo is 22°C and partly cloudy.”
This independently confirms the repaired readiness gate and preloaded transcript-to-agent flow. It does not exercise microphone capture, audio upload, transcription, or a live speech provider. Those outcomes must be checked separately before claiming complete voice qualification. The unchanged D6 weather assertion also passed after the preserved pre-dispatch failure; canonical repair commit `6c38d7ef74`.
@@ -0,0 +1,44 @@
[
"showcase/integrations/built-in-agent/src/app/demos/gen-ui-agent/page.tsx",
"showcase/integrations/built-in-agent/src/lib/factory/tanstack-factory.ts",
"showcase/integrations/google-adk/src/agents/gen_ui_agent.py",
"showcase/integrations/google-adk/src/app/demos/gen-ui-agent/page.tsx",
"showcase/integrations/langgraph-python/docs-links.json",
"showcase/integrations/langgraph-python/src/agents/gen_ui_agent.py",
"showcase/integrations/langgraph-python/src/app/api/copilotkit-voice/[[...slug]]/route.ts",
"showcase/integrations/langgraph-python/src/app/demos/gen-ui-agent/page.tsx",
"showcase/integrations/langgraph-typescript/package-lock.json",
"showcase/integrations/langgraph-typescript/package.json",
"showcase/integrations/langgraph-typescript/src/agent/gen-ui-agent.ts",
"showcase/integrations/langgraph-typescript/src/agent/package-lock.json",
"showcase/integrations/langgraph-typescript/src/agent/package.json",
"showcase/integrations/langgraph-typescript/src/app/api/copilotkit-voice/[[...slug]]/route.ts",
"showcase/integrations/langgraph-typescript/src/app/demos/gen-ui-agent/page.tsx",
"showcase/integrations/langgraph-typescript/vitest.config.ts",
"showcase/integrations/strands/manifest.yaml",
"showcase/integrations/strands/src/agents/gen_ui_agent.py",
"showcase/integrations/strands/src/app/demos/gen-ui-agent/page.tsx",
"showcase/scripts/sync-shared-frontends.ts",
"showcase/shell-docs/src/app/[framework]/[[...slug]]/page.tsx",
"showcase/shell-docs/src/app/llms-mdx/[[...slug]]/route.ts",
"showcase/shell-docs/src/content/docs/generative-ui/state-rendering.mdx",
"showcase/shell-docs/src/content/docs/integrations/langgraph/agent-app-context.mdx",
"showcase/shell-docs/src/content/docs/integrations/langgraph/multi-agent-flows.mdx",
"showcase/shell-docs/src/content/docs/integrations/langgraph/quickstart.mdx",
"showcase/shell-docs/src/content/docs/integrations/langgraph/shared-state/predictive-state-updates.mdx",
"showcase/shell-docs/src/content/docs/integrations/langgraph/shared-state/state-inputs-outputs.mdx",
"showcase/shell-docs/src/content/docs/multimodal-attachments.mdx",
"showcase/shell-docs/src/content/docs/voice.mdx",
"showcase/shell-docs/src/lib/__tests__/current-v2-authored-guides.test.ts",
"showcase/shell-docs/src/lib/__tests__/seo-redirects.test.ts",
"showcase/shell-docs/src/lib/registry.ts",
"showcase/shell-docs/src/lib/seo-redirects.ts",
"showcase/integrations/langgraph-typescript/src/lib/transcription-base-url.test.ts",
"showcase/integrations/langgraph-typescript/src/lib/transcription-base-url.ts",
"showcase/shared/react/demos/gen-ui-agent/page.tsx",
"showcase/shell-docs/src/lib/__tests__/framework-content-resolution.test.ts",
"showcase/shell-docs/src/lib/__tests__/selected-showcase-guide-bindings.test.ts",
"showcase/shell-docs/src/lib/framework-content-resolution.ts",
"showcase/shell-docs/src/lib/selected-showcase-guide-bindings.ts",
"showcase/shell/src/lib/file-to-data-attachment.test.ts"
]
@@ -0,0 +1,35 @@
# Pause checkpoint — September 13, 2026
Paused at the user's request. **The goal is not complete. No integration has complete docs/setup/provider qualification.**
## Verified and committed
- Built-in Agent final strict local replay: **38/38 published checks pass; raw matrix 38/39**. The sole raw failure is an explicitly unshipped thread-ID demo, preserved in the log. See [comparison](built-in-agent-runtime-comparison-20260913.md). This uses public CopilotKit 1.71.1 plus the unreleased local core URL repair. Unpatched public 1.71.1 remains incompatible with the relative runtime URL.
- Actual normal-browser image/PDF sample actions, configured-agent behavior, scoped TypeScript Authentication prompt, and prepared voice transcript handoff were observed. Voice microphone capture/transcription and paperclip upload were not proved by the sample actions.
- Shared React Auth, attachment, state, and voice examples are canonicalized and synchronized. Commits: `1947f20ccf`, `6c38d7ef74`.
- BIA discovery routes and request-capture accuracy repaired: `be9052f5c5`, `1f9082b919`. Strict replay regression: `0579611845`, `cf8066044c`.
- Source-backed guide family and command-only source bundling: `f50d3ad853`; pretypecheck, 18 focused docs checks and nine bundling checks passed at that revision.
- LGTS tool schemas now match their implementations; isolated strict graph typecheck passed. Commit `45d7ec00bb`.
- Latest ADK/Strands direct Python dependency sets resolve, install, and import in isolated environments. This is not a runtime matrix or full guide setup pass.
## Unfinished work preserved as WIP
1. **LGTS latest stable dependency update.** Four package/lock files. Both ordinary `npm ci --ignore-scripts` runs passed (agent 259 packages; UI 958), and strict graph typecheck passed. Agent registered graphs but could not bind because an old audit watcher held the port. No latest-pinned UI/agent matrix ran. Logs are `checkpoint-lgts-*-latest-*.log` in this directory.
2. **Shared resolver and selected-guide guard (REPAIR-022/027).** HTML and Markdown now share a WIP resolver; the guard derives bindings from Showcase metadata and checks actual rendered output. It caught the wrong read/write route and then **20 setup-skipped markers**. Last full selected-guide verdict is RED, not qualified.
3. **LGP four guides (REPAIR-023026).** Streaming, read/write, readonly context, and subagents have source-backed rewrites. Read/write mapping and old-URL redirects are corrected in WIP. Rerun still required; do not reinstate narrative exceptions.
4. **State-rendering identity (part of REPAIR-027).** Root guide incorrectly used unsupported streaming-state demos for BIA/Strands. WIP switches it to actual `gen-ui-agent`, adds canonical frontend and backend source regions. This final atomic edit is unreviewed/unvalidated. Source-file inventory: [checkpoint files](checkpoint-20260913-source-files.json).
5. **Media/voice completeness.** WIP image/PDF scope and prepared-text wording; a real FileReader adapter test; LGTS local-only transcription base-URL helper/test/config. These latest tests have not run. Strict AIMock WAV upload fixture and valid/wrong/empty HTTP checks are not implemented. Browser paperclip verification needs a shared harness capability; no integration-specific shortcut was added.
6. **Exact setup and final validation.** Clone-based LGTS BYO instructions and reproduction script exist; final latest-pinned graph boot remains pending. Other four fresh matrices, exact setup checks, rendered route review, docs typecheck/build/full suite, and final reader sanity remain open. Last historical full docs run was 947/948; its stale assertion was fixed and focused checks passed, but no fresh full-suite green exists.
## Safe restart order
1. Read this checkpoint, the hill tracker, current git status, and `showcase/AGENTS.md`. Keep root out of raw code; use the existing Terra/high agents.
2. Independently review the final Gen UI/source-identity WIP. Run shared frontend drift check and pretypecheck, then the focused rendered guard. Resolve REPAIR-027's real setup omissions without suppressing markers or using another framework's source.
3. Start one latest-pinned LGTS stack; finish its exact setup and full runtime matrix. Continue LGP, ADK, and Strands sequentially. Preserve public-package vs local-patch evidence.
4. Complete the bounded upload/transcription replay and final docs/reader gates. Update per-guide status from actual evidence.
## Resource shutdown
All agents paused. Docs preview/generator/Vitest workers stopped. BIA UI and AIMock stopped. Old LGTS CLI trees `97732/97071/97074` and `96390/97072/98397` stopped; 8123 and 8124 released. No new stack was launched after the pause request. Preserve user/app processes. Continue with the 30 GB aggregate audit limit and one framework stack.
Checkpoint commits save unfinished work for resumption. They are not a merge, release, deployment, or claim of validation.
@@ -0,0 +1,2 @@
added 259 packages in 3s
@@ -0,0 +1,83 @@
> langgraph-typescript-agent@0.0.1 dev
> npx @langchain/langgraph-cli@1.4.6 dev --port 8123 --no-browser
Welcome to
╦ ┌─┐┌┐┌┌─┐╔═╗┬─┐┌─┐┌─┐┬ ┬
║ ├─┤││││ ┬║ ╦├┬┘├─┤├─┘├─┤
╩═╝┴ ┴┘└┘└─┘╚═╝┴└─┴ ┴┴ ┴ ┴.js
- 🚀 API: http://localhost:8123
- 🎨 Studio UI: https://smith.langchain.com/studio?baseUrl=http://localhost:8123
This in-memory server is designed for development and testing.
For production use, please use LangSmith Deployment.
info: ▪ Starting server...
info: ▪ Initializing storage...
info: ▪ Registering graphs from /Users/tylerslaton/.codex/worktrees/3715/CopilotKit/showcase/integrations/langgraph-typescript/src/agent
info: ┏ Registering graph with id 'sample_agent'
info: ┗ [1] { graph_id: 'sample_agent' }
info: ┏ Registering graph with id 'starterAgent'
info: ┗ [1] { graph_id: 'starterAgent' }
info: ┏ Registering graph with id 'beautiful_chat'
info: ┗ [1] { graph_id: 'beautiful_chat' }
info: ┏ Registering graph with id 'headless_complete'
info: ┗ [1] { graph_id: 'headless_complete' }
info: ┏ Registering graph with id 'multimodal'
info: ┗ [1] { graph_id: 'multimodal' }
info: ┏ Registering graph with id 'agent_config_agent'
info: ┗ [1] { graph_id: 'agent_config_agent' }
info: ┏ Registering graph with id 'agentic-chat-reasoning'
info: ┗ [1] { graph_id: 'agentic-chat-reasoning' }
info: ┏ Registering graph with id 'tool_rendering'
info: ┗ [1] { graph_id: 'tool_rendering' }
info: ┏ Registering graph with id 'tool-rendering-reasoning-chain'
info: ┗ [1] { graph_id: 'tool-rendering-reasoning-chain' }
info: ┏ Registering graph with id 'interrupt_agent'
info: ┗ [1] { graph_id: 'interrupt_agent' }
info: ┏ Registering graph with id 'a2ui_dynamic'
info: ┗ [1] { graph_id: 'a2ui_dynamic' }
info: ┏ Registering graph with id 'a2ui_fixed'
info: ┗ [1] { graph_id: 'a2ui_fixed' }
info: ┏ Registering graph with id 'a2ui_recovery'
info: ┗ [1] { graph_id: 'a2ui_recovery' }
info: ┏ Registering graph with id 'mcp_apps'
info: ┗ [1] { graph_id: 'mcp_apps' }
info: ┏ Registering graph with id 'frontend_tools'
info: ┗ [1] { graph_id: 'frontend_tools' }
info: ┏ Registering graph with id 'frontend_tools_async'
info: ┗ [1] { graph_id: 'frontend_tools_async' }
info: ┏ Registering graph with id 'hitl_in_app'
info: ┗ [1] { graph_id: 'hitl_in_app' }
info: ┏ Registering graph with id 'hitl_in_chat'
info: ┗ [1] { graph_id: 'hitl_in_chat' }
info: ┏ Registering graph with id 'readonly_state_agent_context'
info: ┗ [1] { graph_id: 'readonly_state_agent_context' }
info: ┏ Registering graph with id 'byoc_hashbrown'
info: ┗ [1] { graph_id: 'byoc_hashbrown' }
info: ┏ Registering graph with id 'byoc_json_render'
info: ┗ [1] { graph_id: 'byoc_json_render' }
info: ┏ Registering graph with id 'open_gen_ui'
info: ┗ [1] { graph_id: 'open_gen_ui' }
info: ┏ Registering graph with id 'open_gen_ui_advanced'
info: ┗ [1] { graph_id: 'open_gen_ui_advanced' }
info: ┏ Registering graph with id 'shared_state_read_write'
info: ┗ [1] { graph_id: 'shared_state_read_write' }
info: ┏ Registering graph with id 'shared_state_streaming'
info: ┗ [1] { graph_id: 'shared_state_streaming' }
info: ┏ Registering graph with id 'subagents'
info: ┗ [1] { graph_id: 'subagents' }
info: ┏ Registering graph with id 'gen_ui_agent'
info: ┗ [1] { graph_id: 'gen_ui_agent' }
info: ┏ Registering graph with id 'gen_ui_tool_based'
info: ┗ [1] { graph_id: 'gen_ui_tool_based' }
info: ▪ Starting 10 workers
error: ▪ Error: listen EADDRINUSE: address already in use ::1:8123
 at Server.setupListenHandle [as _listen2] (node:net:1939:16)
 at listenInCluster (node:net:1996:12)
 at GetAddrInfoReqWrap.callback (node:net:2205:7)
 at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:134:8)
@@ -0,0 +1,6 @@
npm warn deprecated hast@1.0.0: Renamed to rehype
npm warn deprecated lodash.get@4.4.2: This package is deprecated. Use the optional chaining (?.) operator instead.
npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead
npm warn deprecated recharts@2.15.4: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide
added 958 packages in 22s
+18 -8
View File
@@ -3,7 +3,7 @@
This is the live progress document for repairing the five selected agent integrations in React:
LangGraph Python, LangGraph JS, Google ADK, Strands, and Built-in Agent.
**Current position: repair underway. No integration is qualified yet.**
**Current position: paused at the users request. No integration is fully qualified yet.**
The [per-defect ledger](repair-status.json) tracks all 37 confirmed findings, fix commits,
after-evidence, and independent reviews. New failures found during repairs use separate `REPAIR-*`
@@ -53,13 +53,13 @@ Local execution follows the [resource budget](resource-budget.md): the user-appr
results below used Webpack because the default Turbopack command failed.
- Keep new results in iteration records; do not overwrite the original failed verdicts.
| Integration | Original baseline | Current repair status | Qualification |
| ---------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
| LangGraph Python | 38/40; voice and multimodal failed | Latest pinned graph booted; 3 strict local cells green. Full refresh pending. | Not qualified |
| LangGraph JS | 37/40 under Webpack; default dev broken | Default Turbopack command and 3 local probes green. Full refresh pending. | Not qualified |
| Google ADK | UI startup blocked by conflicting routes | Startup and normal-browser AIMock context fixed; scoped auth/chat/tool evidence green. Full refresh pending. | Not qualified |
| Strands | 34/36; voice and multimodal failed | Recipe/state bridge and 3 related strict local cells green. Full refresh pending. | Not qualified |
| Built-in Agent | 35/39; three confirmed fixture gaps plus a timeout | 37/39 full strict local matrix with public 1.71.1 plus the local REPAIR-011 core fix. Voice and an unshipped thread-ID demo remain RED; clean public 1.71.1 still fails Invalid URL. | Not qualified |
| Integration | Original baseline | Current repair status | Qualification |
| ---------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
| LangGraph Python | 38/40; voice and multimodal failed | Latest pinned graph booted; 3 strict local cells green. Full refresh pending. | Not qualified |
| LangGraph JS | 37/40 under Webpack; default dev broken | Default Turbopack command and 3 local probes green. Full refresh pending. | Not qualified |
| Google ADK | UI startup blocked by conflicting routes | Startup and normal-browser AIMock context fixed; scoped auth/chat/tool evidence green. Full refresh pending. | Not qualified |
| Strands | 34/36; voice and multimodal failed | Recipe/state bridge and 3 related strict local cells green. Full refresh pending. | Not qualified |
| Built-in Agent | 35/39; three confirmed fixture gaps plus a timeout | Final 38/39 raw strict local matrix: all 38 published checks pass; the unshipped thread-ID demo remains RED. Uses public 1.71.1 plus local core URL fix; unpatched public package still fails. Voice audio transcription remains untested. | Not qualified |
Check counts differ from routed-demo counts because D6 expands some features. The two LangGraph
interrupt demos remain explicitly quarantined pending supported SDK behavior. See the
@@ -188,3 +188,13 @@ progress; it is not a forecast or a claim that the unfinished work already passe
- Strict tool-followup replay regression and explicit qualification instructions are committed in `0579611845`. The default replay mode for unrelated integrations was preserved.
No integration is fully qualified yet. Local patched behavior, latest published compatibility, exact setup reproduction, and live-provider behavior remain distinct results.
### Built-in Agent final runtime checkpoint
The full run after voice commit `6c38d7ef74` is **38/39 raw checks; 38/38 published checks**. The sole raw failure is the explicitly unshipped thread-ID demo; it remains in the evidence. [Before/after comparison](built-in-agent-runtime-comparison-20260913.md) records all seven changed outcomes. Voice passes the prepared transcript handoff, while actual audio capture/transcription remains a separate open gate. Full documentation/setup qualification and unpatched published SDK compatibility are not inferred from this runtime pass.
The audit-owned BIA UI and AIMock were stopped before starting LangGraph TypeScript.
## Latest pause checkpoint
Paused on September 13 at the users request. Audit servers and workers are stopped. See [the restart checkpoint](checkpoint-20260913.md) for verified commits, unfinished source units, exact remaining failures, and restart order. Final BIA replay is 38/38 published checks, with the unshipped raw failure and local SDK patch kept explicit. REPAIR-027 still has a RED selected-guide setup gate; its final state-rendering edit is unvalidated WIP.
@@ -1,32 +1,48 @@
# LangGraph TypeScript bring-your-own setup reproduction — 2026-09-13
# LangGraph TypeScript Showcase-agent setup reproduction — 2026-09-13
Scope: the corrected `/langgraph-typescript/quickstart` TypeScript
bring-your-own branch. This validation copied the selected Showcase agent
layout into `/private/tmp/lgts-byoc-setup-20260913`, installed from the exact
shown `src/agent/package.json`, and did not start a LangGraph or Next server.
Scope: the TypeScript branch of `/langgraph-typescript/quickstart` after its
copy-path correction. It now distinguishes an existing-agent reference from
the complete runnable Showcase sample.
The earlier attempt copied `src/agent` and `shared-tools` directly. That did
not reproduce the guide because it included existing `node_modules` and named
an undocumented partial layout. It is retained as a rejected observation, not
as setup evidence.
## Reproduction
`tasks/docs-feature-audit/reproduce-lgts-byoc-setup.sh` creates a fresh
temporary copy of the complete checked-in
`showcase/integrations/langgraph-typescript` directory, excluding installed
dependencies. This mirrors the guide's complete-sample clone layout. It then:
1. copies `.env.example` to `.env` at the integration root, matching the
selected `langgraph.json` `../../.env` path;
2. runs `npm ci --ignore-scripts --no-audit --no-fund` in `src/agent` from the
checked-in lockfile; and
3. type-checks the selected `graph.ts` with strict TypeScript settings, without
starting a server.
The rendered package, configuration, and graph snippets remain reference
material for an **existing** TypeScript agent. They do not form a standalone
copy path: the graph imports `openai-headers`, shared tools, and its full
configuration registers other sample graphs.
## Result
`npm install --ignore-scripts --no-audit --no-fund` completed successfully
(`up to date in 267ms`). Its package configuration and imports therefore
resolve in an isolated app layout containing `src/agent` and the selected
`shared-tools` dependency.
Clean rerun passed on 2026-09-13:
The focused graph typecheck used:
```bash
NODE_OPTIONS=--max-old-space-size=4096 ./node_modules/.bin/tsc --noEmit \
--module nodenext --moduleResolution nodenext --target es2022 --skipLibCheck graph.ts
```text
added 204 packages in 1s
```
It reaches the graph and reports three type errors at lines 75, 98, and 136:
two `string` versus `SalesStage` schema mismatches and one optional-field
versus `Flight` mismatch. The exact command run in the checked-in selected
source reports the same three errors. This is an existing Showcase graph type
error, not an isolated-install or documented-config failure.
The strict graph check completed with no diagnostics. Before this rerun, the
selected `graph.ts` reported three real source errors: the two sales-tool
schemas accepted arbitrary `stage` strings although the shared implementation
uses `SalesStage`, and the flight schema made fields optional although its
shared implementation accepts complete `Flight` values. The source now uses
the declared sales-stage values and requires every `Flight` field, so the
strict check passes in the clean sample layout.
The reproduction script is
`tasks/docs-feature-audit/reproduce-lgts-byoc-setup.sh`. It creates and removes
its own `/private/tmp` directory and intentionally does not run `npm run dev`.
CLI graph-load/start validation needs the dedicated runtime slot after the
active Built-in Agent matrix ends.
No LangGraph or Next server is started by this reproduction; graph-load/start
validation requires a separate runtime slot.
@@ -188,3 +188,10 @@
- Explicit qualification-mode AIMock (`AIMOCK_STRICT_TURN_INDEX=1`) ran all 39 current BIA D6 checks against the existing local UI and fixture-only mock: 37 passed, 2 failed, no checks skipped. Durable aggregate: `tasks/docs-feature-audit/repair021-built-in-agent-full-d6-strict-turn.log`.
- `threadid-frontend-tool-roundtrip` fails with `503 Strict mode: no fixture matched`. It is deliberately a manifest feature without a `demos:` entry, so this is retained as unshipped/non-applicable evidence rather than omitted or treated as a published guide cell.
- `voice` has no request start or user/assistant DOM after the immediate sample-button click (`runStartCount=0`). Its sample writes the composer during the provisional-agent window, before CopilotChat enables submit after runtime discovery. The byte-identical five-integration voice components need the same canonical `isReady` gate as multimodal; its repair/replay is pending at this checkpoint.
### Final strict qualification replay
- After the canonical voice readiness gate, the complete strict-turn replay ran 39 checks against the local Built-in Agent UI and fixture-only AIMock: `tasks/docs-feature-audit/repair021-built-in-agent-full-d6-strict-turn-final.log`.
- Raw result: 38 passed, 1 failed. The only failure is `threadid-frontend-tool-roundtrip`, which made two real attempts and received `503 Strict mode: no fixture matched`. The Built-in Agent manifest declares this as a feature but contains no `demos:` entry; it is retained as an unshipped, non-applicable raw-matrix failure rather than silently skipped.
- Published-catalog result: 38 applicable cells passed. This result uses the strict local AIMock qualification configuration and the local core artifact patched only with unreleased REPAIR-011 relative-URL behavior. It does not qualify the latest published CopilotKit 1.71.1 artifact, which remains red until that shared repair is released.
- Voices strict D6 and normal-browser sample are green only for the bundled text-transcript handoff. Neither exercises microphone capture nor WAV upload/transcription; that provider/API path remains untested.
+80 -1
View File
@@ -1114,6 +1114,85 @@
],
"sanity_review": "Independent static review clear; actual normal browser image/PDF proof is separate from the focused route checks.",
"remaining": "Complete voice diagnosis and selected published-guide runtime qualification. These runs use public 1.71.1 plus the local core URL fix; unpatched published compatibility remains blocked."
},
{
"id": "REPAIR-022",
"area": "HTML/Markdown effective source resolver diverges for generated threads-import",
"severity": "high",
"stage": "implementing",
"before_record": "Generated HTML and Markdown used different source candidate ordering.",
"fix_commits": [],
"after_evidence": [
"Unfinished source is explicitly preserved in the September 13 pause checkpoint; see checkpoint-20260913.md."
],
"sanity_review": null,
"remaining": "Shared resolver and derived output guard are unfinished WIP; validate final route parity."
},
{
"id": "REPAIR-023",
"area": "LangGraph Python streaming guide lacks Showcase source binding",
"severity": "high",
"stage": "implementing",
"before_record": "Effective predictive-state-updates guide used legacy viewer and handwritten examples.",
"fix_commits": [],
"after_evidence": [
"Unfinished source is explicitly preserved in the September 13 pause checkpoint; see checkpoint-20260913.md."
],
"sanity_review": null,
"remaining": "Source-backed rewrite is checkpointed WIP; final rendered/setup/runtime checks pending."
},
{
"id": "REPAIR-024",
"area": "LangGraph Python read/write guide and route binding drift",
"severity": "high",
"stage": "implementing",
"before_record": "Feature docs link selected in-app-agent-write instead of the repaired state-inputs-outputs guide.",
"fix_commits": [],
"after_evidence": [
"Unfinished source is explicitly preserved in the September 13 pause checkpoint; see checkpoint-20260913.md."
],
"sanity_review": null,
"remaining": "Rewrite, owning docs-link correction and old-URL redirects are checkpointed WIP; final validation pending."
},
{
"id": "REPAIR-025",
"area": "LangGraph Python readonly context guide lacks Showcase source binding",
"severity": "high",
"stage": "implementing",
"before_record": "Effective agent-app-context guide lacked the selected Showcase source carrier.",
"fix_commits": [],
"after_evidence": [
"Unfinished source is explicitly preserved in the September 13 pause checkpoint; see checkpoint-20260913.md."
],
"sanity_review": null,
"remaining": "Source-backed rewrite is checkpointed WIP; final rendered/setup/runtime checks pending."
},
{
"id": "REPAIR-026",
"area": "LangGraph Python subagents guide lacks Showcase source binding",
"severity": "high",
"stage": "implementing",
"before_record": "Effective multi-agent-flows guide relied on narrative/handwritten content instead of actual selected sources.",
"fix_commits": [],
"after_evidence": [
"Unfinished source is explicitly preserved in the September 13 pause checkpoint; see checkpoint-20260913.md."
],
"sanity_review": null,
"remaining": "Source-backed rewrite is checkpointed WIP; final rendered/setup/runtime checks pending."
},
{
"id": "REPAIR-027",
"area": "Selected framework setup omissions and state-rendering identity mismatch",
"severity": "high",
"stage": "implementing",
"before_record": "Actual rendered selected-guide guard found 20 setup-skipped markers and BIA/Strands state-rendering demo failures; root state-rendering used unsupported streaming-state identity.",
"fix_commits": [],
"after_evidence": [
"Unfinished source is explicitly preserved in the September 13 pause checkpoint; see checkpoint-20260913.md."
],
"sanity_review": null,
"remaining": "Central source/setup repairs are unfinished. Last selected-guide guard is RED. Final Gen UI identity/frontend/backend region edit was not validated; review then rerun before qualification."
}
]
],
"pause_checkpoint": "checkpoint-20260913.md"
}
@@ -0,0 +1,33 @@
{"level":"info","msg":"probe.e2e-full.service-start","ts":"2026-09-13T18:09:31.991Z","component":"docs-feature-audit-local-d6","slug":"built-in-agent","featureCount":1,"backendUrl":"http://localhost:3117"}
{"level":"info","msg":"CVDIAG component=harness-d6 boundary=inbound run_id=a64d7f65-2293-49cc-96e5-b4237b1305b2 slug=built-in-agent header_present=true header_value_prefix=built-in-age hop=- status=ok test_id=d6-built-in-agent-a64d7f65-2293-49cc-96e5-b4237b1305b2 error=","ts":"2026-09-13T18:09:32.245Z","component":"docs-feature-audit-local-d6"}
[sse-interceptor] mainFrame framenavigated — resetting per-stream tracking state { url: 'http://localhost:3117/demos/voice', hadTrackedRequest: false }
[sse-interceptor] mainFrame framenavigated — resetting per-stream tracking state { url: 'http://localhost:3117/demos/voice', hadTrackedRequest: false }
{"level":"info","msg":"probe.e2e-full.runFeature.hydration-timing","ts":"2026-09-13T18:09:33.081Z","component":"docs-feature-audit-local-d6","slug":"built-in-agent","featureType":"voice","hydrated":true,"hydrationMs":25}
[conversation-runner] starting conversation { totalTurns: 1, settleMs: 1500, chatInputSelector: '(cascade)' }
[conversation-runner] resolving chat input selector { candidateCount: 6, override: '(none — using cascade)' }
[conversation-runner] chat input selector resolved { selector: '[data-testid="copilot-chat-textarea"]' }
[conversation-runner] resolved chat input selector at boot { selector: '[data-testid="copilot-chat-textarea"]' }
[conversation-runner] turn 1/1 — sending message { inputLength: 0, timeoutMs: 30000 }
[conversation-runner] turn 1/1 — running preFill hook
[conversation-runner] turn 1/1 — preFill hook completed
[conversation-runner] turn 1/1 — skipFill=true, waiting for textarea content then pressing Enter
[conversation-runner] waitForContentAndSend — start { selector: '[data-testid="copilot-chat-textarea"]', timeoutMs: 30000 }
[conversation-runner] waitForContentAndSend — textarea has content, pressing Enter { valueLength: 29, valuePreview: 'What is the weather in Tokyo?' }
[conversation-runner] turn 1/1 — waiting for assistant settle {
selector: '[data-testid="copilot-chat-textarea"]',
turnIndex: 1,
settleMs: 1500,
timeoutMs: 30000
}
[conversation-runner] turn 1/1 — assistant settled { bubbleIndex: 0, textLength: 72, hasAssertions: true }
[conversation-runner] turn 1/1 — settled metadata { turnNum: 1, bubbleIndex: 0, textLength: 72 }
[conversation-runner] turn 1/1 — running assertions { bubbleIndex: 0, textLength: 72 }
[conversation-runner] turn 1/1 — assertions passed
[conversation-runner] conversation completed successfully { turnsCompleted: 1, totalDurationMs: 2242 }
{"level":"warn","msg":"probe.e2e-full.writer-missing","ts":"2026-09-13T18:09:35.344Z","component":"docs-feature-audit-local-d6","key":"d6:built-in-agent/voice"}
{"level":"info","msg":"probe.e2e-full.feature-complete","ts":"2026-09-13T18:09:35.345Z","component":"docs-feature-audit-local-d6","slug":"built-in-agent","featureType":"voice","pass":true,"durationMs":3103}
{"level":"info","msg":"CVDIAG component=harness-d6 boundary=cv-verdict run_id=a64d7f65-2293-49cc-96e5-b4237b1305b2 slug=built-in-agent header_present=true header_value_prefix=built-in-age hop=- status=ok test_id=d6-built-in-agent-a64d7f65-2293-49cc-96e5-b4237b1305b2 error=","ts":"2026-09-13T18:09:35.376Z","component":"docs-feature-audit-local-d6"}
{"level":"info","msg":"probe.e2e-full.service-complete","ts":"2026-09-13T18:09:35.376Z","component":"docs-feature-audit-local-d6","slug":"built-in-agent","passed":1,"failed":0,"skipped":0,"incapable":0,"total":1,"state":"green","durationMs":3385}
{"level":"warn","msg":"probe.e2e-full.aggregate-writer-missing","ts":"2026-09-13T18:09:35.376Z","component":"docs-feature-audit-local-d6","key":"d6:built-in-agent"}
✓ d6:built-in-agent green (3.6s)
{"slug":"built-in-agent","results":[{"key":"d6:built-in-agent","state":"green","durationMs":3650,"signal":{"shape":"package","slug":"built-in-agent","backendUrl":"http://localhost:3117","total":1,"passed":1,"failed":[],"skipped":[]}}]}
File diff suppressed because it is too large Load Diff
@@ -1,18 +1,25 @@
#!/usr/bin/env bash
# Reproduces the documented LangGraph TypeScript BYO agent install/typecheck
# Reproduces the documented complete LangGraph TypeScript Showcase agent setup
# without starting an agent server. Run from the repository root.
set -euo pipefail
repo_root="$(git rev-parse --show-toplevel)"
temp_root="$(mktemp -d /private/tmp/lgts-byoc-setup.XXXXXX)"
temp_root="$(mktemp -d /private/tmp/lgts-showcase-agent.XXXXXX)"
trap 'rm -rf "$temp_root"' EXIT
mkdir -p "$temp_root/src"
cp -R "$repo_root/showcase/integrations/langgraph-typescript/src/agent" "$temp_root/src/agent"
cp -R "$repo_root/showcase/integrations/langgraph-typescript/shared-tools" "$temp_root/shared-tools"
# This mirrors the directory produced by the guide's clone command while
# deliberately excluding installed dependencies. The selected graph imports
# files outside src/agent, so copying only its rendered snippets would not
# reproduce the documented runnable sample.
tar -C "$repo_root/showcase/integrations" \
--exclude='langgraph-typescript/node_modules' \
--exclude='langgraph-typescript/src/agent/node_modules' \
-cf - langgraph-typescript | tar -C "$temp_root" -xf -
cd "$temp_root/src/agent"
NODE_OPTIONS=--max-old-space-size=4096 npm install --ignore-scripts --no-audit --no-fund
integration_root="$temp_root/langgraph-typescript"
cp "$integration_root/.env.example" "$integration_root/.env"
cd "$integration_root/src/agent"
NODE_OPTIONS=--max-old-space-size=4096 npm ci --ignore-scripts --no-audit --no-fund
NODE_OPTIONS=--max-old-space-size=4096 ./node_modules/.bin/tsc \
--noEmit --module nodenext --moduleResolution nodenext --target es2022 \
--noEmit --strict --module nodenext --moduleResolution nodenext --target es2022 \
--skipLibCheck graph.ts