Agent SDK
Build custom React UI
Use useAppAgent from the React package to build a branded assistant with shared runtime semantics and your own product UI.
Use @emcy/agent-sdk/react when the assistant is part of your product rather than a generic widget.
This is the recommended path for:
- side panels
- docked assistants
- custom transcript layouts
- product-specific approvals and follow-up input UIs
- assistants that need to react to app state or navigation
The core hook#
import { useAppAgent } from "@emcy/agent-sdk/react";
export function SupportAssistant() {
const agent = useAppAgent({
apiKey: process.env.NEXT_PUBLIC_EMCY_API_KEY!,
agentId: process.env.NEXT_PUBLIC_SUPPORT_AGENT_ID!,
serviceUrl: process.env.NEXT_PUBLIC_EMCY_SERVICE_URL,
appSessionKey: session.id,
userIdentity: {
subject: session.user.id,
email: session.user.email,
organizationId: session.organizationId,
displayName: session.user.name,
},
clientTools,
appContext: {
currentSurface: "customer-detail",
currentCustomerId: customer.id,
hostRefreshInstruction:
"After any successful customer mutation, call refreshCustomerView before you answer.",
},
feedbackSource: "customer-detail-assistant",
});
return null;
}What the hook returns#
Conversation#
agent.conversation is the main state surface.
Important fields:
idmessagesconversationMessagestoolMessagesvisibleMessagesrenderedNodeslatestAssistantMessagelatestToolMessagelatestUserMessagelastTurnpendingTurninlineFeedstreamingContentstatusLabelisReadyisLoadingisLoadingHistoryisThinkinghasOlderMessageserrorissue
Actions:
loadMore()reset()
Composer#
agent.composer.send(prompt, { displayText? })
agent.composer.cancel()Use displayText when you want the user-facing echo to be different from the exact prompt you send to the runtime.
That is useful when you add hidden scoping or host context to the actual prompt.
Connections#
agent.connections.items
agent.connections.needsAttention
agent.connections.connect(serverUrl)
agent.connections.disconnect(serverUrl)Approvals#
agent.approvals.pending
agent.approvals.resolve(id, approved)Requests#
agent.requests.pending
agent.requests.submit(id, values)
agent.requests.cancel(id)Feedback#
agent.feedback.isSubmitting
agent.feedback.error
agent.feedback.lastSubmittedAt
agent.feedback.submit({
sentiment: "up",
comment: "Helpful answer",
})A real UI pattern#
This is the basic shape most custom React integrations should follow:
function BillingAssistantPanel() {
const agent = useAppAgent({
apiKey: "emcy_sk_xxxx",
agentId: "ag_xxxxx",
appSessionKey: session.id,
userIdentity: {
subject: session.user.id,
email: session.user.email,
organizationId: session.organizationId,
},
clientTools,
appContext,
});
return (
<div className="grid gap-4">
<header>
<div>{agent.conversation.statusLabel}</div>
{agent.connections.needsAttention ? <button>Reconnect</button> : null}
</header>
<section>
{agent.conversation.renderedNodes.map((node) => {
if (node.kind === "user") return <div key={node.id}>{node.content}</div>;
if (node.kind === "assistant") return <div key={node.id}>{node.content}</div>;
return <div key={node.id}>{node.tools.length} tools</div>;
})}
</section>
<footer>
<button
onClick={() =>
agent.composer.send(
`Summarize the current billing state for account ${accountId}.`,
{ displayText: "Summarize the current billing state." },
)
}
>
Ask
</button>
</footer>
</div>
);
}Client tools plus server tools#
The right split is:
- server/MCP tools for real backend data and mutations
- client tools for UI work inside the current app
Example:
- MCP tool:
update_invoice_status - client tool:
refreshInvoices
That lets the agent change real server state and then make the page reflect the new result immediately.
Popup auth on web#
If you do not provide onAuthRequired, the React package uses the built-in popup auth controller.
That gives you:
popupAuthStatestartOrRetryPopupAuth()cancelPopupAuth()
Use those to render product-specific reconnect and retry affordances while still using Emcy's popup flow.
Checklist for a clean custom React integration#
- always pass
appSessionKey - pass
userIdentityfor same-user OAuth-backed MCP flows - keep
clientToolsexplicit and small - use
displayTextwhen you add hidden prompt scoping - render pending approvals and input requests from SDK state
- use
statusLabelandconnections.needsAttentioninstead of inventing your own connection-status heuristics
