mech.app

The mech.app newsletter

Agentic AI, minus the noise.

Get practical field notes on AI agents, automation, developer tools and security delivered to your inbox.

No spam. Unsubscribe anytime.

Dev Tools

CopilotKit's AG-UI Protocol: Multi-Platform Agent Frontends with Shared State

How CopilotKit serializes agent state across React, Angular, and Slack while maintaining generative UI consistency and human-in-the-loop flows.

Source: github.com
CopilotKit's AG-UI Protocol: Multi-Platform Agent Frontends with Shared State

CopilotKit started as a React library for embedding AI assistants. It now ships as a multi-platform agent framework where the same backend serves web, mobile, and Slack surfaces. The AG-UI Protocol is the wire format that makes this work. It serializes agent state, tool calls, and generative UI components so a workflow initiated in a browser can continue in Slack without duplicating orchestration logic.

The project has 36,629 stars and is trending #12 in TypeScript repositories. Google, LangChain, AWS, and Microsoft have adopted the AG-UI Protocol. This is not a toy. It is production infrastructure for teams that need agents to operate across multiple surfaces without rewriting the entire stack for each platform.

The Cross-Platform Problem

Most agent frameworks assume a single frontend. You write a React component, wire up tool calls, and ship. When you need a Slack bot or mobile app, you rebuild the agent logic in a different runtime. State lives in separate databases. Human-in-the-loop approvals use different UI primitives. You end up with three agents that drift.

CopilotKit inverts this. The agent backend is platform-agnostic. The AG-UI Protocol defines how state, tool calls, and UI components serialize over the wire. Each platform (React, Angular, Vue, React Native, Slack) implements a client that speaks the protocol. The agent does not care whether it is rendering a modal in a browser or a message in Slack.

AG-UI Protocol Wire Format

The protocol uses JSON-RPC 2.0 as the transport. Each message includes:

  • State snapshot: Current agent memory, tool outputs, and user context
  • UI component descriptor: A platform-agnostic representation of what to render
  • Action metadata: Tool name, parameters, approval requirements, and execution status

When an agent decides to call a tool, it sends a message like this:

{
  "jsonrpc": "2.0",
  "method": "agent.toolCall",
  "params": {
    "toolName": "createInvoice",
    "arguments": { "amount": 1500, "clientId": "abc123" },
    "requiresApproval": true,
    "uiComponent": {
      "type": "form",
      "fields": [
        { "name": "amount", "type": "currency", "editable": false },
        { "name": "clientId", "type": "text", "editable": false }
      ],
      "actions": ["approve", "reject", "edit"]
    }
  },
  "id": "call_xyz789"
}

The React client renders a form with approve/reject buttons. The Slack client renders a message with interactive buttons. The mobile client renders a native sheet. The agent does not know which surface is active. It only knows the tool call needs approval and provides a UI descriptor.

Component Registration Across Frameworks

CopilotKit avoids duplicating tool definitions by separating the agent logic from the UI layer. You define tools once in the backend:

const createInvoiceTool = {
  name: "createInvoice",
  description: "Generate a new invoice",
  parameters: z.object({
    amount: z.number(),
    clientId: z.string(),
  }),
  requiresApproval: true,
  execute: async ({ amount, clientId }) => {
    // Backend logic
  },
};

Each frontend framework registers a UI component for that tool. In React:

useCopilotAction({
  name: "createInvoice",
  render: ({ amount, clientId, onApprove, onReject }) => (
    <InvoiceApprovalCard
      amount={amount}
      client={clientId}
      onApprove={onApprove}
      onReject={onReject}
    />
  ),
});

In Slack, the same tool maps to a Block Kit message:

slackClient.registerAction("createInvoice", ({ amount, clientId }) => ({
  blocks: [
    { type: "section", text: { type: "mrkdwn", text: `Invoice: $${amount}` } },
    {
      type: "actions",
      elements: [
        { type: "button", text: "Approve", action_id: "approve" },
        { type: "button", text: "Reject", action_id: "reject" },
      ],
    },
  ],
}));

The agent backend does not care. It sends the tool call descriptor. The client decides how to render it.

Session Handoff Between Surfaces

The hardest problem is session continuity. A user starts a workflow in the web app, leaves for lunch, and wants to approve the next step in Slack. CopilotKit handles this with a shared state store and session tokens.

When the agent initiates a workflow, it creates a session ID. The web client stores this in local storage. The Slack client stores it in a thread metadata field. When the user switches surfaces, the new client fetches the session state from the backend.

The state store is pluggable. You can use Redis, Postgres, or any key-value store. The protocol defines a standard shape:

FieldTypePurpose
sessionIdstringUnique identifier for the workflow
agentStateJSONCurrent memory and tool outputs
pendingActionsarrayTool calls waiting for approval
surfacestringLast active platform (web, slack, mobile)
userIdstringUser who initiated the session

When a user approves a tool call in Slack, the Slack client sends an approval message to the backend. The backend updates the session state and broadcasts the change to all connected clients. If the web app is still open, it receives a WebSocket event and updates the UI in real time.

Human-in-the-Loop Across Interaction Primitives

Approval flows are the boundary condition. A browser has modals, forms, and drag-and-drop. Slack has buttons and text input. Mobile has native sheets and haptic feedback. CopilotKit does not try to unify these. It provides a declarative approval API and lets each client implement it natively.

The agent backend marks a tool call as requiring approval. The client decides how to present it. In React, you might render a modal with a form. In Slack, you render a message with buttons. In React Native, you render a bottom sheet.

The protocol includes a uiHints field that suggests interaction patterns:

{
  "uiHints": {
    "preferredLayout": "modal",
    "dismissible": false,
    "confirmationRequired": true
  }
}

The React client interprets preferredLayout: "modal" as a blocking modal. The Slack client ignores it because Slack does not have modals. The mobile client might show a full-screen sheet instead of a bottom sheet.

Observability and Failure Modes

CopilotKit exposes agent execution traces through a structured logging API. Each tool call, state transition, and approval event emits a log entry with:

  • Session ID
  • Tool name
  • Platform
  • User ID
  • Timestamp
  • Success or failure

You can pipe these to Datadog, Grafana, or any observability stack. The protocol includes a trace field in every message so you can correlate events across platforms.

Common failure modes:

  • Session expiry: If a user does not interact for 24 hours, the session expires. The next action triggers a session-not-found error. The client can either restore from a checkpoint or start a new session.
  • Platform mismatch: If an agent tries to render a web-only component in Slack, the Slack client falls back to a text description. The protocol includes a fallbackText field for this.
  • Approval timeout: If a tool call requires approval and the user does not respond within a timeout, the agent can either cancel the action or escalate to a different user.
  • State divergence: If two clients modify the same session simultaneously, the backend uses last-write-wins. You can enable optimistic locking with version numbers.

Deployment Shape

CopilotKit runs as a Node.js service. You deploy it alongside your agent backend. The service handles WebSocket connections, session state, and protocol translation. Each frontend client connects via WebSocket or HTTP polling.

Typical architecture:

┌─────────────┐
│ React App   │──┐
└─────────────┘  │
                 ├──> WebSocket ──> CopilotKit Service ──> Agent Backend
┌─────────────┐  │                        │
│ Slack Bot   │──┘                        │
└─────────────┘                           ▼
                                    State Store (Redis)

The service is stateless. All session data lives in the state store. You can run multiple instances behind a load balancer. WebSocket connections use sticky sessions.

Security Boundaries

Each session includes a user ID. The backend validates that the user has permission to execute the requested tool. CopilotKit does not enforce this. You write the authorization logic in your agent backend.

The protocol supports signed tokens. When a client connects, it sends a JWT. The backend verifies the signature and extracts the user ID. The service rejects any action that does not match the session’s user ID.

Cross-platform sessions introduce a risk: a user could start a workflow in the web app and then approve it in Slack using a different account. CopilotKit mitigates this by requiring the same user ID across all surfaces. If the Slack user ID does not match the session user ID, the approval fails.

Trade-Offs and Constraints

AspectBenefitCost
Platform-agnostic stateOne agent serves all surfacesUI components must be registered per platform
Declarative approvalsFlexible interaction patternsNo unified approval UI across platforms
Shared session storeReal-time sync across clientsRequires external state store (Redis, Postgres)
Protocol-based architectureEasy to add new platformsMore moving parts than single-platform SDKs
WebSocket transportLow latency for state updatesRequires sticky sessions or connection pooling

The biggest constraint is that you cannot use platform-specific features without breaking the abstraction. If you want to use React Server Components or Slack’s workflow builder, you need to implement those outside the AG-UI Protocol.

Technical Verdict

Use CopilotKit when you need the same agent to operate across web, mobile, and messaging platforms without duplicating orchestration logic. It is a good fit for internal tools, customer support bots, and approval workflows where users switch between devices.

Avoid it if you only need a single frontend or if your agent relies heavily on platform-specific UI patterns. The protocol abstraction adds complexity. If you are building a React-only app, a simpler library like Vercel AI SDK or LangChain.js will be faster to ship.

The AG-UI Protocol is still early. The spec is not finalized. If you adopt it, expect breaking changes. But if you need multi-platform agent frontends today, this is the most complete open-source option.