t from making assumptions about component lifecycles.
import { useAgentState } from '@copilotkit/react';
interface GameState {
board: string[][];
currentPlayer: 'X' | 'O';
winner: string | null;
threats: Array<{ row: number; col: number; severity: 'high' | 'low' }>;
}
export function GameBoard() {
const [state, setState] = useState<GameState>(initialState);
// Exposes state to the agent runtime
useAgentState({
key: 'game_state',
state: state,
render: (current) => `Board: ${JSON.stringify(current.board)} | Player: ${current.currentPlayer}`
});
return <BoardGrid data={state} onCellClick={handleMove} />;
}
Architecture Rationale: State is exposed via a keyed hook rather than global context. This prevents context window overflow by allowing selective serialization. The render callback transforms complex objects into token-efficient strings for the LLM, while the raw JSON remains available for programmatic tool execution.
Actions define what the agent can execute. Each tool requires a strict JSON schema, a handler function, and an optional confirmation gate. The schema ensures type safety before execution, while the handler runs in the browser's main thread.
import { defineAgentTool } from '@copilotkit/react';
export function useGameTools() {
defineAgentTool({
name: 'propose_next_move',
description: 'Suggests the optimal next move for the current player',
parameters: {
type: 'object',
properties: {
targetRow: { type: 'number', minimum: 0, maximum: 2 },
targetCol: { type: 'number', minimum: 0, maximum: 2 },
reasoning: { type: 'string' }
},
required: ['targetRow', 'targetCol']
},
requiresConfirmation: true, // Triggers human-in-the-loop dialog
handler: async ({ targetRow, targetCol }) => {
await executeMove(targetRow, targetCol);
return { success: true, message: 'Move applied' };
}
});
}
Architecture Rationale: Separating schema definition from execution allows the agent to validate intent before triggering side effects. The requiresConfirmation flag delegates control to the frontend, ensuring the user can accept, reject, or modify the agent's proposal. This pattern is critical for maintaining trust in high-stakes workflows.
3. Secure Open-Ended Rendering
When agents generate raw HTML or CSS, the frontend must sanitize the output before injection. Direct insertion without validation introduces XSS vulnerabilities and breaks component isolation.
import DOMPurify from 'dompurify';
import { renderAgentMarkup } from '@copilotkit/react';
export function DynamicCoachCard({ htmlContent }: { htmlContent: string }) {
const sanitized = DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['div', 'span', 'p', 'strong', 'em', 'ul', 'li', 'button'],
ALLOWED_ATTR: ['class', 'data-action', 'style'],
FORBID_TAGS: ['script', 'iframe', 'object', 'embed']
});
return (
<div
className="agent-generated-card"
dangerouslySetInnerHTML={{ __html: sanitized }}
onClick={(e) => handleAgentInteraction(e)}
/>
);
}
Architecture Rationale: Sanitization is applied at the boundary layer, not inside the agent. This ensures that even if the model generates malicious markup, the frontend enforces a strict allowlist. Event delegation (handleAgentInteraction) routes clicks back to the agent runtime without breaking React's virtual DOM reconciliation.
4. Runtime Integration
The frontend communicates with the agent backend through a streaming endpoint. The Vercel AI SDK handles SSE transport, while CopilotKit manages tool routing and state synchronization. Custom orchestrators like LangGraph require explicit context injection.
// /api/agent/route.ts
import { streamAgentResponse } from '@copilotkit/nextjs';
import { createLangGraphClient } from '@/agents/langgraph';
export async function POST(req: Request) {
const { messages, context } = await req.json();
const graph = createLangGraphClient({
systemPrompt: buildSystemPrompt(context), // Manual injection required
tools: registerFrontendTools()
});
return streamAgentResponse({
model: 'gpt-5-mini',
provider: 'azure',
graph,
messages,
context
});
}
Architecture Rationale: Custom agents do not auto-inject frontend state. The context object must be explicitly mapped into the system prompt or graph state. This prevents accidental context window bloat and gives developers precise control over what the agent observes.
Pitfall Guide
1. Assuming Automatic Context Injection for Custom Agents
Explanation: Built-in agent runtimes automatically attach exposed state to the model context. Custom orchestrators (LangGraph, Microsoft Agent Framework) deliver state to a dedicated payload field, but the agent will ignore it unless explicitly injected into the system message or graph state.
Fix: Map context.copilotkit or equivalent payload fields into the system prompt during graph initialization. Log the injected context during development to verify token usage.
2. Unsanitized Open-Ended Rendering
Explanation: Rendering raw HTML/CSS from an agent without sanitization exposes the application to XSS, CSS injection, and layout breakage.
Fix: Always pass agent-generated markup through a strict sanitizer like DOMPurify. Maintain an allowlist of permitted tags and attributes. Never render style attributes without CSP validation.
3. State Serialization Overload
Explanation: Exposing entire component trees or large datasets to the agent exhausts context windows and increases latency.
Fix: Serialize only the minimal state required for decision-making. Use transformation callbacks to convert complex objects into token-efficient strings. Implement state diffing to avoid re-sending unchanged data.
4. Blocking the Main Thread During Tool Execution
Explanation: Long-running tool handlers freeze the UI, breaking streaming feedback and user interaction.
Fix: Mark heavy operations as async and return immediate acknowledgment to the agent. Use optimistic UI updates while the backend processes the request. Stream progress events if the operation exceeds 200ms.
5. Ignoring Human-in-the-Loop Confirmation Gates
Explanation: Allowing agents to execute destructive or state-mutating actions without user approval breaks trust and causes data corruption.
Fix: Enable requiresConfirmation for all write operations. Implement accept/reject/modification workflows. Log confirmation decisions for audit trails and model fine-tuning.
6. Mismatched Type Definitions Between Agent and Frontend
Explanation: The agent generates tool calls with incorrect parameter types or missing required fields, causing runtime validation failures.
Fix: Share TypeScript interfaces between frontend and backend using a monorepo or shared package. Validate tool schemas at build time. Implement fallback handlers that request clarification instead of failing silently.
7. Neglecting Latency in Real-Time State Sync
Explanation: High-frequency state updates (e.g., game loops, drag-and-drop) flood the agent context, causing token overflow and delayed responses.
Fix: Debounce state exposure to 500ms-1s intervals. Use delta updates instead of full state snapshots. Implement a priority queue for state changes, ensuring critical updates (winner detection, inventory changes) bypass throttling.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal dashboard with fixed workflows | Static (AG-UI) | Predefined tools reduce latency and simplify validation | Low (minimal token usage) |
| Dynamic form generation or adaptive layouts | Declarative (A2UI) | Schema validation ensures safe rendering while allowing flexibility | Medium (moderate token overhead) |
| Rich media previews or experimental components | Open-Ended (MCP Apps) | Maximum flexibility for complex visual output | High (requires sanitization pipeline, higher token cost) |
| Real-time collaborative editing | Static + Declarative hybrid | Fast tool execution for actions, JSON for layout updates | Medium-High (requires state diffing) |
| High-security enterprise environment | Static only | Minimal attack surface, strict control over agent capabilities | Low (simpler compliance auditing) |
Configuration Template
// copilotkit.config.ts
import { defineConfig } from '@copilotkit/nextjs';
import { sanitizeAgentMarkup } from '@/utils/sanitization';
import { debounceState } from '@/utils/state-sync';
export default defineConfig({
runtime: {
streaming: {
enabled: true,
transport: 'sse',
heartbeatInterval: 5000
},
context: {
maxTokens: 8000,
stateSync: {
debounceMs: 750,
transform: (state) => debounceState(state),
injectIntoCustomAgents: true
}
},
security: {
openEndedRendering: {
enabled: true,
sanitizer: sanitizeAgentMarkup,
allowedTags: ['div', 'span', 'p', 'button', 'ul', 'li'],
blockInlineStyles: true
},
toolExecution: {
requireConfirmation: ['write', 'delete', 'transfer'],
timeoutMs: 15000,
retryOnFailure: false
}
}
},
ui: {
chatPanel: {
position: 'right',
theme: 'system',
streamingAnimation: 'typewriter',
toolCallRenderer: 'inline'
}
}
});
Quick Start Guide
- Initialize the runtime: Install
@copilotkit/react and @copilotkit/nextjs. Configure the streaming endpoint in your API route using the Vercel AI SDK or your preferred provider.
- Expose critical state: Wrap your main component with
useAgentState, passing only the data the agent needs to make decisions. Transform complex objects into concise strings.
- Register tools: Define agent actions with strict JSON schemas. Set
requiresConfirmation: true for any operation that modifies state or triggers external side effects.
- Connect the backend: Point your frontend to the agent runtime. If using a custom orchestrator like LangGraph, manually map the exposed state into the system prompt or graph initialization payload.
- Test the loop: Trigger a tool call, verify the confirmation dialog appears, execute the action, and confirm state updates propagate back to the agent without context window overflow. Iterate on debounce intervals and sanitization rules before production deployment.