Claude Managed Agents

Connect Anthropic's Managed Agents sessions to assistant-ui with the external store runtime, folding the session event log into messages, rendering approval gates, and using sessions as the thread list.

Claude Managed Agents is Anthropic's hosted agent platform: Anthropic runs the agent loop and provisions a sandboxed container per session, and your client drives the session over an event stream. A session holds the entire conversation server-side as a durable event log, streams every step (text, tool calls, approval stops, status) as typed events, and accepts user messages and tool confirmations back.

That shape maps directly onto two assistant-ui primitives, with no adapter package in between:

  • useExternalStoreRuntime renders messages you derive from the session's event log. The log is the single source of truth, so replaying a stored session and tailing a live one run through the same pure function and can never disagree.
  • useRemoteThreadListRuntime turns the session list into the thread sidebar. A thread is a session; there is no conversations table anywhere in the app.

Anthropic ships an official reference implementation of this integration: the Claude Managed Agents quickstart is a complete Next.js app (composer, thread, sidebar, tool cards, approval gate) built exactly this way. This page teaches the pattern; the quickstart is the runnable proof.

Managed Agents is a beta API (managed-agents-2026-04-01; the SDK sets the header automatically). The quickstart declares @anthropic-ai/sdk ^0.113.0 (the session event helpers and token previews need 0.109.0 or later), and @assistant-ui/react 0.14.27 or later for the toolkit API and the approval gate on the external store runtime.

The event-to-message mapping

Everything the session emits arrives as a typed event. The integration is one pure fold from the event array to assistant-ui's message model:

Managed Agents eventassistant-ui
user.messageA user message
agent.messageAssistant text (buffered, authoritative)
event_start / event_deltaThe same text, streamed early as token previews
agent.thinkingA reasoning part (progress signal only; the API sends no reasoning text)
agent.tool_use / agent.mcp_tool_use / agent.custom_tool_useA tool-call part; the toolCallId is the event id
agent.tool_result (and mcp / custom variants)That part's result
session.status_idle with stop_reason: requires_actionrequires-action message status, plus an approval on each blocked tool part
user.tool_confirmationThe approval, settled (allowed or denied)
session.status_running / status_idleWhether the turn is live (isRunning)
session.errorAn error status on the message, or a retry banner

Because the fold is pure, opening an old chat replays sessions.events.list() through it, and a live chat feeds the SSE tail through it, and the two paths cannot render differently. Approvals, denials, and charts all come back after a reload because they are in the log, not in browser state.

The runtime wiring is a structural subset of ThreadMessageLike, handed to the external store as-is:

const runtime = useExternalStoreRuntime<ThreadMessageLike>({
  messages: snapshot.messages,
  convertMessage: (m) => m,
  isRunning: isBusy(snapshot),

  onNew: async (message) => {
    const id = await ensureSession();
    await sendMessage(id, textOf(message));
  },

  // The Stop button becomes a real server-side interrupt.
  onCancel: async () => controller.interrupt(),

  // The Allow / Deny click on a gated tool call. approvalId is the
  // tool_use event id from session.status_idle { requires_action }.
  onRespondToToolApproval: async ({ approvalId, approved, reason }) => {
    controller.respondToApproval(approvalId, approved, reason);
  },
});

Sessions are the thread list

The sidebar is a RemoteThreadListAdapter over the Managed Agents session API. Thread id and session id are the same string, so nothing maps between the two worlds:

Adapter methodManaged Agents call
listsessions.list(), filtered to the sessions this app created (a metadata tag)
initializesessions.create(), invoked by assistant-ui on the first message of a new chat
renamesessions.update({ title })
archivesessions.archive()
unarchiveThrows. Managed Agents sessions cannot be unarchived, and switching to an archived thread auto-unarchives by default, so do not render archived sessions as switchable (the quickstart's ownership gate rejects archived ids outright)
deletesessions.delete()
fetchsessions.retrieve()
generateTitleReads the title back after the server retitles the session from the first message, so the sidebar row updates without a second model call

A brand-new chat has no session until the first send: the composer works immediately, and initialize() creates the session lazily when the first message (or first attachment upload) needs one. Kill the server, restart, reload, and every conversation comes back, because none of it ever lived in the app.

The approval gate

Managed Agents supports per-tool permission policies. A tool configured as always_ask (the quickstart gates bash this way) does not run when the agent reaches for it. The session emits the agent.tool_use event, then parks:

{ "type": "session.status_idle", "stop_reason": { "type": "requires_action", "event_ids": ["sevt_..."] } }

The fold stamps an approval onto that tool part and sets the message status to requires-action, which is everything assistant-ui needs to render Allow and Deny on the tool card. The click flows back through onRespondToToolApproval as a user.tool_confirmation event:

{ "type": "user.tool_confirmation", "tool_use_id": "sevt_...", "result": "deny", "deny_message": "Not on this box." }

Two wire details matter. The tool_use_id is the tool-use event id, not an Anthropic toolu_ id. And a denial reaches the agent as the tool's result, so it adjusts course instead of retrying the same command.

Custom client-executed tools ride the same requires_action stop but take a user.custom_tool_result instead of a confirmation; sending a confirmation for one is a 400. The quickstart's inline chart tool is the worked example: the session parks, the card renders the chart from the tool's input, and the client answers so the agent continues.

Token streaming

By default assistant text arrives as whole agent.message events when a model request finishes. Opting the stream into event_deltas: ["agent.message", "agent.thinking"] adds token previews: an event_start announces the upcoming event, event_delta fragments stream the text, and the buffered event lands last as the authoritative record. Concatenating a preview's deltas in arrival order yields a prefix of the final text, but under load the server may shed the remaining deltas for an event, so the prefix is not necessarily the whole message. The fold therefore appends fragments for display and discards the accumulated preview when the buffered event arrives; never treat a preview as final. When a turn errors or is interrupted, the buffered event may never arrive at all, but span.model_request_end still does, so close any unreconciled preview when you see it.

Previews are best-effort and gated per organization. Build against the buffered events and treat deltas as an enhancement: an org without the streaming gate runs the identical code path with replies arriving whole.

Security boundary

The Anthropic API key stays server-side; the browser talks only to your own route handlers, which relay to Managed Agents. Because a session id arrives from the browser and becomes an API path parameter, validate ownership on every route: the id must resolve, belong to your agent, and carry your app's metadata tag before any read or write. The API key can see the whole workspace; that gate is what keeps a guessed id from reading it. The quickstart's ownedSession() is the reference shape.

Run the reference

git clone https://github.com/anthropics/claude-quickstarts
cd claude-quickstarts/managed-agents/assistant-ui
npm install
cp .env.example .env   # add ANTHROPIC_API_KEY, or `ant auth login` once
npm run setup          # one-time: creates the agent + environment, paste the IDs into .env
npm run dev            # drop sample_data/sales.csv into the chat

The quickstart's README walks through every file: the reducer, the session controller, the thread list adapter, the tool cards, and the attachment adapter that uploads composer files into the session sandbox. For the platform itself, start with Anthropic's Managed Agents overview and events reference.