Skip to main content

Migrating from the Chat SDK to the Agent SDK

Implementations built using the Chat SDK can be upgraded to use the Agent SDK instead. This upgrade lets you extend your AI assistant's abilities by giving it access to custom tools, such as calling your APIs or navigating your web application.

This guide covers the migration process from @kapaai/react-sdk (Chat SDK) to @kapaai/agent-react (Agent SDK).

Installation

# Remove old
npm uninstall @kapaai/react-sdk

# Install new
npm install @kapaai/agent-core @kapaai/agent-react

@kapaai/agent-core is a peer dependency of @kapaai/agent-react. If you plan to use Zod schemas for tool definitions, install zod and zod-to-json-schema as well.

Quickstart: using the built-in UI

The simplest migration path is to use the Agent SDK's built-in UI components. This gives you a complete chat experience out of the box.

import { AgentProvider, AgentChat } from '@kapaai/agent-react';

function App() {
return (
<AgentProvider
projectId="your-project-id"
integrationId="your-integration-id"
model="kapa-agent-1.0"
getSessionToken={async () => {
const res = await fetch('/api/kapa-session', { method: 'POST' });
return res.json(); // { session_token, expires_at }
}}
theme={{ colorScheme: 'dark', accentColor: '#2563eb' }}
>
<div style={{ height: 600 }}>
<AgentChat
branding={{
title: 'Support Agent',
subtitle: 'How can I help?',
examplePrompts: ['How do I get started?'],
}}
/>
</div>
</AgentProvider>
);
}

AgentChat fills its parent container and includes a header, message list with markdown rendering, tool call cards with sources, input with send/stop, and a footer. For a slide-in drawer, use AgentPanel instead:

<AgentPanel
open={isPanelOpen}
onClose={() => setIsPanelOpen(false)}
width={480}
branding={{ title: 'Support Agent' }}
/>

The full list of available components:

  • AgentChat: Complete chat UI (header, messages, input, footer)
  • AgentPanel: Slide-in drawer wrapping AgentChat
  • AgentInput: Text input with send/stop button
  • AgentMessageBubble: Message rendering with markdown and tool calls
  • ToolCallCard: Tool call display with args/result/sources tabs
  • SourceTiles: Overlapping favicon circles for sources
  • ExamplePrompts: Starter prompt buttons

You can use these directly, mix them with your own components, or ignore them entirely and build fully custom UI using just the useAgentChat() hook (same headless pattern as the Chat SDK).

Provider setup

Before (Chat SDK)

import { KapaProvider } from '@kapaai/react-sdk';

<KapaProvider
integrationId="your-integration-id"
customizationId="optional-customization-id"
sourceGroupIDsInclude={['group-1']}
uncertainAnswerCallout="This answer may not be accurate."
userTrackingMode="cookie"
botProtectionMechanism="recaptcha"
callbacks={{
askAI: {
onQuerySubmit: (p) => console.log(p),
onAnswerGenerationCompleted: (p) => console.log(p),
},
}}
>
<App />
</KapaProvider>

After (Agent SDK)

import { AgentProvider } from '@kapaai/agent-react';

<AgentProvider
projectId="your-project-id"
integrationId="your-integration-id"
model="kapa-agent-1.0"
getSessionToken={async () => {
const res = await fetch('/api/kapa-session', { method: 'POST' });
return res.json(); // { session_token, expires_at }
}}
user={{ email: 'user@example.com', unique_client_id: 'user-123' }}
customInstructions="Optional system prompt additions"
sourceGroupIdsInclude={['group-1']}
tools={myTools}
context={myToolContext}
onEvent={(event) => console.log(event.type, event.data)}
theme={{ colorScheme: 'dark', accentColor: '#9333ea' }}
>
<App />
</AgentProvider>

Key differences

ConcernChat SDKAgent SDK
AuthenticationHandled internally (captcha + integration ID via proxy)You provide a getSessionToken function. Your backend creates session tokens using your API key via POST <https://api.kapa.ai/agent/v1/projects/{id}/agent/sessions/> with an X-API-KEY header. The SDK handles token refresh and 401 retry automatically.
Required propsintegrationIdprojectId, integrationId, model, getSessionToken
User identityuserTrackingMode (cookie/fingerprint/none) + window.kapaSettings.useruser prop with { email?, unique_client_id? }
Bot protectionBuilt-in reCAPTCHA/hCaptchaNot applicable. Session token auth replaces captcha.
Prompt customizationcustomizationId prop (server-side)customInstructions prop (injected into the system prompt)
Source filteringsourceGroupIDsIncludesourceGroupIdsInclude (same concept, slightly different casing)
Uncertainty calloutuncertainAnswerCallout propNot supported. The Agent SDK does not expose uncertainty detection.

Headless usage: useChat -> useAgentChat

If you are building a fully custom UI with the Chat SDK's useChat hook, here is how each field maps to the Agent SDK's useAgentChat hook.

Before

import { useChat } from '@kapaai/react-sdk';

const {
submitQuery,
isPreparingAnswer,
isGeneratingAnswer,
resetConversation,
stopGeneration,
addFeedback,
error,
conversation,
threadId,
} = useChat();

After

import { useAgentChat } from '@kapaai/agent-react';

const {
sendMessage,
isStreaming,
resetConversation,
stopGeneration,
messages,
threadId,
inputValue,
setInputValue,
approveToolCall,
rejectToolCall,
getFaviconUrl,
} = useAgentChat();

Field-by-field mapping

submitQuery(query) -> sendMessage(text)

Same purpose. Both accept a string and trigger a request. sendMessage returns a Promise<void>.

isPreparingAnswer / isGeneratingAnswer -> isStreaming

The Agent SDK uses a single isStreaming boolean. It becomes true immediately when sendMessage is called (before the HTTP request is made) and stays true until the agent loop finishes. This means you can use isStreaming to both disable the input field and show a stop button. This is how the SDK's built-in AgentChat component works internally.

If you need to distinguish "request sent but no tokens yet" from "tokens are arriving", you can track this yourself by watching for the first assistant message content update while isStreaming is true.

resetConversation -> resetConversation

Direct equivalent. Also clears the input value.

stopGeneration -> stopGeneration

Direct equivalent. Works at any point while isStreaming is true, including before tokens arrive.

addFeedback -> not available

The Agent SDK does not support feedback/reactions and this is not currently planned. If you need thumbs up/down, you would need to build that independently against your own endpoint.

error -> not available as a top-level field

Errors surface in two ways:

  1. As assistant messages with isError: true and an error description in content.
  2. Via the onEvent callback on AgentProvider with type: 'response_error'.
// Checking for error in messages
const lastMessage = messages[messages.length - 1];
if (lastMessage?.role === 'assistant' && lastMessage.isError) {
// Show error UI
}

// Or via events
<AgentProvider onEvent={(event) => {
if (event.type === 'response_error') {
showError(event.data.error);
}
}} />

conversation -> messages

This is the largest structural change. See the next section.

threadId -> threadId

Direct equivalent.

Conversation model

Before: Conversation class with QA objects

const { conversation } = useChat();

// Array-like class of QA pairs
const latest = conversation.getLatest();
console.log(latest.question); // user's question
console.log(latest.answer); // markdown string
console.log(latest.sources); // StreamSource[]
console.log(latest.status); // "streaming" | "pending_completion" | "completed"
console.log(latest.id); // questionAnswerId (null while streaming)
console.log(latest.reaction); // "upvote" | "downvote" | null
console.log(latest.metadata?.is_uncertain); // boolean

After: flat ConversationMessage[]

const { messages } = useAgentChat();

// Flat array of user and assistant messages
messages.forEach((msg) => {
if (msg.role === 'user') {
console.log(msg.content); // user's question
}
if (msg.role === 'assistant') {
console.log(msg.content); // full markdown answer
console.log(msg.blocks); // ContentBlock[] for interleaved rendering
console.log(msg.isError); // true if this is an error message
}
});

Key differences

No QA pairing. Messages are not grouped into question-answer pairs. They are a flat list of alternating user and assistant messages.

No message IDs. ConversationMessage objects do not have an id field. When rendering with .map(), use the array index as the React key. This is safe because messages are append-only (never reordered or removed mid-conversation). The SDK's built-in AgentChat component uses this pattern.

No status field. There is no streaming / pending_completion / completed status per message. Use isStreaming at the hook level plus the message's position (last message while streaming is the one being generated).

No uncertainty. The Agent SDK does not expose uncertainty detection. There is no equivalent to metadata.is_uncertain.

No reaction field. Since feedback is not supported, there is no reaction field on messages.

Rendering the assistant response

Before

const { conversation } = useChat();
const latest = conversation.getLatest();

// Render the markdown answer through your own parser
<MyMarkdownRenderer content={latest.answer} />

After: Option A. Flat markdown (closest to Chat SDK)

const { messages } = useAgentChat();
const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant');

// message.content is the full markdown text, same as qa.answer
<MyMarkdownRenderer content={lastAssistant?.content ?? ''} />

The blocks array gives you interleaved text and tool call segments, which is how the agent naturally works (text, then tool calls, then more text, etc.):

{message.blocks.map((block, i) => {
if (block.type === 'text') {
return <MyMarkdownRenderer key={i} content={block.content} />;
}
if (block.type === 'tool_calls') {
return block.toolCalls.map((tc) => (
<MyToolCallCard key={tc.id} toolCall={tc} />
));
}
})}

Each text block's content is markdown you can pass through your own parser (including Latex rendering).

Sources

Before

const { conversation } = useChat();
const latest = conversation.getLatest();
// Sources are per-QA
latest.sources.forEach((s) => {
console.log(s.title, s.source_url, s.source_type);
});

After

Sources are per-tool-call, not per-message. They appear on the search_knowledge_base tool call results:

// Collect all sources from an assistant message
function getSourcesFromMessage(message: ConversationMessage): RelevantSource[] {
if (message.role !== 'assistant') return [];
return message.blocks
.filter((b) => b.type === 'tool_calls')
.flatMap((b) => b.toolCalls)
.flatMap((tc) => tc.sources ?? []);
}

The source type has slightly different field names:

Chat SDK (StreamSource)Agent SDK (RelevantSource)
titletitle
subtitle(not present)
source_urlsourceUrl
source_typesourceType

Callbacks / Events

Before: callbacks prop

<KapaProvider
callbacks={{
askAI: {
onQuerySubmit: ({ question, conversation, threadId }) => {},
onAnswerGenerationCompleted: ({ questionAnswerId, question, answer, conversation, threadId }) => {},
onAnswerGenerationStop: ({ question, conversation, threadId }) => {},
onConversationReset: ({ conversation, threadId }) => {},
onFeedbackSubmit: ({ feedbackId, reaction, comment, questionAnswerId, question, answer }) => {},
},
}}
/>

After: onEvent prop

<AgentProvider
onEvent={(event) => {
switch (event.type) {
case 'message_sent':
// { messageLength: number }
break;
case 'response_completed':
// { threadId: string | null, toolCallCount: number }
break;
case 'response_error':
// { error: string, threadId: string | null }
break;
case 'generation_stopped':
// { threadId: string | null }
break;
case 'tool_executed':
// { toolName: string, status: 'completed' | 'error', durationMs: number, error?: string }
break;
case 'tool_approved':
// { toolName: string, toolCallId: string }
break;
case 'tool_denied':
// { toolName: string, toolCallId: string }
break;
case 'conversation_reset':
// {}
break;
}
}}
/>

Mapping

Chat SDK callbackAgent SDK eventNotes
onQuerySubmitmessage_sentPayload is simpler: just { messageLength }. No conversation snapshot.
onAnswerGenerationCompletedresponse_completedPayload has { threadId, toolCallCount }. No question/answer text. Read from messages if needed.
onAnswerGenerationStopgeneration_stoppedSame concept.
onConversationResetconversation_resetSame concept, empty payload.
onFeedbackSubmitNot availableFeedback is not supported.

Theming

The Chat SDK has no built-in theming. The Agent SDK does.

<AgentProvider
theme={{
accentColor: '#2563eb', // hex color, auto-generates a 10-shade palette
colorScheme: 'dark', // 'dark' | 'light' | 'auto'
fontFamily: 'Inter, system-ui, sans-serif',
fontSize: 14, // base px, all sizes scale proportionally
radius: 'soft', // 'sharp' | 'soft' | 'round' | 'pill'
}}
/>

If building custom UI, access the color scheme via useAgentColorScheme() for dark/light switching. The SDK's built-in components consume the theme automatically.

Tool approval (new)

The Agent SDK supports human-in-the-loop approval for tools that have needsApproval: true. This is new, with no Chat SDK equivalent.

const { approveToolCall, rejectToolCall } = useAgentChat();

// In your tool call UI
<button onClick={() => approveToolCall(toolCall.id)}>Allow</button>
<button onClick={() => rejectToolCall(toolCall.id)}>Deny</button>

Tool calls with needsApproval will have status: 'approval_requested' until the user approves or denies them.

Features not available in the Agent SDK

FeatureChat SDKAgent SDK
Feedback (thumbs up/down)addFeedback()Not supported, not planned
Uncertainty detectionmetadata.is_uncertainNot supported
Deep thinking modeuseDeepThinking() hookNot available
Per-QA IDs on messagesqa.id (questionAnswerId)Not surfaced
Separate streaming statesisPreparingAnswer + isGeneratingAnswerSingle isStreaming
Bot protection (captcha)Built-in reCAPTCHA/hCaptchaNot applicable (session token auth)