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.

7 sections

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#

TSX
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:

  • id
  • messages
  • conversationMessages
  • toolMessages
  • visibleMessages
  • renderedNodes
  • latestAssistantMessage
  • latestToolMessage
  • latestUserMessage
  • lastTurn
  • pendingTurn
  • inlineFeed
  • streamingContent
  • statusLabel
  • isReady
  • isLoading
  • isLoadingHistory
  • isThinking
  • hasOlderMessages
  • error
  • issue

Actions:

  • loadMore()
  • reset()

Composer#

TS
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#

TS
agent.connections.items
agent.connections.needsAttention
agent.connections.connect(serverUrl)
agent.connections.disconnect(serverUrl)

Approvals#

TS
agent.approvals.pending
agent.approvals.resolve(id, approved)

Requests#

TS
agent.requests.pending
agent.requests.submit(id, values)
agent.requests.cancel(id)

Feedback#

TS
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:

TSX
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.

If you do not provide onAuthRequired, the React package uses the built-in popup auth controller.

That gives you:

  • popupAuthState
  • startOrRetryPopupAuth()
  • 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 userIdentity for same-user OAuth-backed MCP flows
  • keep clientTools explicit and small
  • use displayText when you add hidden prompt scoping
  • render pending approvals and input requests from SDK state
  • use statusLabel and connections.needsAttention instead of inventing your own connection-status heuristics