> ## Documentation Index
> Fetch the complete documentation index at: https://opensandbox-feat-types-open-question-labels.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# React integration

> Create sessions on your server; attach, submit work and reopen them with useAgent

`@opencomputer/react` provides `useAgent`: a session's conversation, turn
activity and input controls. Your server creates sessions and checks access;
the hook reads their event log and sends turns through your routes. The API
key stays on the server.

```bash theme={null}
npm install @opencomputer/react
```

## Create the session on your server

Use the [management client](/reference/typescript-sdk/agents) from trusted
code. Address the agent by its environment alias, with one key per
submission:

```ts theme={null}
import { OpenComputer } from "@opencomputer/sdk/agents";

const oc = new OpenComputer({ apiKey: process.env.OPENCOMPUTER_API_KEY! });
const { session } = await oc.sessions.create(
  { agentId: "<agent-id>@development", labels: { request: taskId } },
  { idempotencyKey: taskId },
);
// Return session.id to the browser; use it when reopening this conversation.
```

`taskId` belongs to one submission. The platform chooses the deployment
active in the environment and records it on the session, so retry an
uncertain create with the same parameters and key: it returns the same
session even after a redeploy. Nothing in your application handles
deployment ids; pinning one is an advanced option of `sessions.create`.
Creating a session and sending its first turn are separate calls with
separate keys.

## Expose three routes

The hook needs these [management API](/agents/api) routes, relative to its
`basePath`:

| Route | Purpose |
| - | - |
| `GET /sessions/<id>/events?after=<seq>` | Read conversation history and new events |
| `POST /sessions/<id>/turns` | Submit `{ input, idempotencyKey, payload?, answers? }` |
| `POST /sessions/<id>/interrupt` | Request Stop for the running turn |

Authenticate each request and check access to the session before forwarding
it with your organization API key. For example, a Next.js route handler:

```ts theme={null}
// app/api/agent/sessions/[id]/[action]/route.ts
const allowed: Record<string, string[]> = {
  GET: ["events"],
  POST: ["turns", "interrupt"],
};

async function proxy(
  request: Request,
  { params }: { params: Promise<{ id: string; action: string }> },
) {
  const { id, action } = await params;
  const user = await requireUser(request);
  if (!allowed[request.method]?.includes(action) || !(await userOwnsSession(user, id))) {
    return new Response(null, { status: 404 });
  }
  const upstream = await fetch(
    `https://app.opencomputer.dev/api/managed-agents/sessions/${encodeURIComponent(id)}/${action}${new URL(request.url).search}`,
    {
      method: request.method,
      headers: {
        "x-api-key": process.env.OPENCOMPUTER_API_KEY!,
        "content-type": "application/json",
      },
      body: request.method === "POST" ? await request.text() : undefined,
      redirect: "error",
    },
  );
  return new Response(upstream.body, {
    status: upstream.status,
    headers: { "content-type": "application/json" },
  });
}

export const GET = proxy;
export const POST = proxy;
```

`requireUser` and `userOwnsSession` are your application's access checks.
The latter must enforce the application's project, environment and agent
scope as well as the user's access. Forward JSON bodies and response statuses
unchanged: the hook supplies the turn key in the body and reads the admission
receipt or error envelope from the response.

## Attach and submit

Keep each submission's text and key until its receipt arrives. If the reply
is lost, Retry sends the same submission; it must not generate a new key.

```tsx theme={null}
import { useState } from "react";
import { useAgent } from "@opencomputer/react";

type Submission = { text: string; idempotencyKey: string };

export function Chat({ sessionId }: { sessionId: string }) {
  const { messages, turns, send, stop, isReplaying, error } = useAgent({
    sessionId,
    basePath: "/api/agent",
  });
  const [draft, setDraft] = useState("");
  const [pending, setPending] = useState<Submission | null>(null);
  const [sending, setSending] = useState(false);

  async function submit() {
    const submission = pending ?? {
      text: draft.trim(),
      idempotencyKey: crypto.randomUUID(),
    };
    setPending(submission);
    setSending(true);
    try {
      await send(submission.text, { idempotencyKey: submission.idempotencyKey });
      setPending(null);
      setDraft("");
    } catch {
      // The hook exposes the error. Keep this submission for retry.
    } finally {
      setSending(false);
    }
  }

  return (
    <section>
      {isReplaying ? <p>Loading the conversation…</p> : null}
      {messages.map((message) => (
        <p key={message.id}><strong>{message.role}:</strong> {message.text}</p>
      ))}
      <input
        value={draft}
        disabled={sending || pending !== null}
        onChange={(event) => setDraft(event.target.value)}
      />
      <button disabled={sending || (!pending && !draft.trim())} onClick={() => void submit()}>
        {sending ? "Sending…" : pending ? "Retry" : "Send"}
      </button>
      <button disabled={!turns.some((turn) => turn.status === "running")} onClick={() => void stop()}>
        Stop
      </button>
      {error ? <p role="alert">{error}</p> : null}
    </section>
  );
}
```

This example retains the submission for the life of the component. To retry
across navigation or a reload, retain the same envelope there too. Include
`payload` in it when sending structured input. An explicit rejection such as
`session_ended` requires resolving that error; a new key starts new work and
is not recovery of an uncertain request.

Turns sent while another runs are queued. `send` resolves on admission, not
when the agent finishes. It returns `{ sessionId, turnId, status, duplicate }`:

* A new turn is `queued` or `running`.
* A repeated key returns the existing turn and its current status, including
  `completed`, `failed` or `cancelled`, with `duplicate: true`.
* Keep both the text and payload unchanged when retrying a key.

The input appears in `messages` after admission. Its event-log record
confirms that message instead of adding a second one.

### Send options and errors

| Option | Purpose |
| - | - |
| `idempotencyKey` | One key per submission, reused on retries. Without it, **each call** generates a new key and can admit a new turn. |
| `payload` | Structured JSON available to the agent as `useInput().payload`, beside the text |
| `answers` | The id of the open question this input answers; `answer()` sets it |

`send` rejects with a `SendError`. Rejection alone does **not** establish
whether a turn was admitted:

| `code` | Meaning | `status` |
| - | - | - |
| The API's code, such as `session_ended`, `memory_admission_unconfirmed` or `insufficient_credits` | An error response; follow that code's [recovery contract](/agents/api#turns) | HTTP status |
| `network_error` | Transport or response-reading failure; admission may have succeeded | none |
| `invalid_response` | The response was not a turn receipt; admission may have succeeded | none |
| `empty_input` | Blank text; nothing sent | none |
| `busy` | Create mode only: a turn already runs; nothing sent | none |

The hook also puts the message in `error`. A turn that fails after admission
is separate: `turn.failed` sets the turn's `failure` and the hook's `error`.

## Display activity and results

`turns` comes from the same durable event log as `messages`. Use its statuses
to distinguish queued work from running and settled turns:

```tsx theme={null}
{turns.map((turn) => (
  <section key={turn.id}>
    <h3>{turn.input} — {turn.status}</h3>
    {turn.toolCalls.map((call) => (
      <p key={call.callId}>{call.title}: {call.status}</p>
    ))}
    {turn.result !== undefined ? <pre>{JSON.stringify(turn.result, null, 2)}</pre> : null}
    {turn.failure ? <p role="alert">{turn.failure.message}</p> : null}
  </section>
))}
```

| Turn field | Meaning |
| - | - |
| `id`, `status`, `input` | Turn ID, lifecycle status and admitted text |
| `messages` | This turn's messages, also present in the hook's `messages` |
| `toolCalls` | `{ callId, tool, title, input?, output?, status }` in start order; values are decoded JSON |
| `result` | This turn's latest committed [result-tool output](/agents/tools#the-session-result), if any; ordinary tool output does not become a result |
| `failure` | `{ code, message }` from `turn.failed` |
| `outcome` | `"question"` when the turn completed by asking |

Reporting a result does not finish a turn. The session's latest result may
also belong to an earlier turn; see [result provenance](/agents/sessions)
before treating a session as ready for review.

A tool call is `running`, `completed`, `failed` or `cancelled`. Calls still
running when their turn settles are settled too. The hook exposes failure
status but does not currently retain `tool.failed.message` or `settledBy` on
`ToolCall`; those details remain in the [event log](/agents/events#tools),
also available through `onEvent`. Older tool records may lack a name or
output.

`isRunning` is an activity hint, not the session's lifecycle status. It also
includes this hook instance's unsettled admission receipts. After reopening,
a queued turn is visible in `turns` but does not by itself set `isRunning`.
Use `turns` for queued/running counts and the [session API](/agents/api#sessions)
for session status.

## Answer a question

An agent can end a turn by [asking a question](/agents/tools#ask-a-question).
The hook exposes the open question as `question`, `{ id, text, options,
turnId? }`, or `null`, and `answer(questionId, text, options?)` sends the
reply. The asking turn's `outcome` is `"question"`.

```tsx theme={null}
const { question, answer } = useAgent({ sessionId, basePath: "/api/agent" });

{question ? (
  <section>
    <p>{question.text}</p>
    {question.options.map((option) => (
      <button
        key={option.value}
        onClick={() => void answer(question.id, option.value, { idempotencyKey: `${question.id}:${option.value}` })}
      >
        {option.label}
      </button>
    ))}
  </section>
) : null}
```

`answer(id, text, options)` is `send(text, { ...options, answers: id })`
and resolves with the same receipt. `options` is empty for a free-text
question; answer it with the person's text. `question` clears when the
log records `question.answered` or `question.closed` for it. Answering a
question that is no longer open rejects with a `SendError` whose code is
`question_stale`; the hook's `question` shows the current one.

While a question is open, `send` without `answers` resolves with a held
receipt, `{ status: "held", questionId }` and no `turnId`: the message stays
in `messages`, rebuilt from the log's `message.held` events after a reload,
and reaches the agent with the answer. `dismiss(questionId)` closes the
question without an answer; held messages then run as ordinary turns.

## Stop and reopen

`stop()` requests an interrupt without spending a model turn. It resolves
once the request was accepted and rejects with a `SendError` when it was
not, after setting `error`, so a control that showed a stopping state can be
re-enabled and the request retried; a resolved promise is **not**
confirmation that execution has stopped. Watch the running
turn settle in `turns`. Stop affects that turn, so the next queued turn can
start afterwards. The [interrupt contract](/agents/api#end-and-interrupt)
describes settlement and remote-command guarantees.

The hook does not expose `stopping` or `ended` session status. Read the
session through your server when your interface needs that lifecycle detail.
It also never suspends, resumes or ends an attached session; those operations
belong to the server that created it.

Reopen by mounting the hook with the same `sessionId`. It reads history from
`after` (default `0`), including work completed while the page was closed,
then polls for new events. A failed poll sets `error`, backs off and resumes
from the same cursor; a successful recovery clears that polling error.

Changing `sessionId` replays the new session. A send still in flight for the
previous session settles for its caller without changing the new session's
view. Remount your composer for the new session, for example
`<Chat key={sessionId} sessionId={sessionId} />`, so a retained submission
cannot be retried into another session.

`after` skips earlier events; it is not a snapshot. Start at `0` to reconstruct
the conversation and turn activity. Use another cursor only when the earlier
view is already retained elsewhere. See [Sessions and turns](/agents/sessions)
for the distinction between reopening a conversation and recovering failed
execution.

## Bind memory

Choose [memory bindings](/agents/memory) during server-side session creation;
the browser cannot change them. To create a document and bind it in one
helper call, see [`startOnDocument`](/reference/typescript-sdk/agents#start-a-session-on-a-memory-document)
and its retry limits. For pinned session creation, use the explicit
[document and session calls](/agents/document-memory#start-a-session-on-a-new-document).

Pass `onMemorySaved` to refresh your document panel through an authenticated
owner route:

```tsx theme={null}
const agent = useAgent({
  sessionId,
  basePath: "/api/agent",
  onMemorySaved: () => refreshNotes(),
});
```

`memorySaves` also contains those events, with `resource`, `documentId`,
`revision` and `bytes`. Save events are best-effort; refresh on opening the
panel too, because documents can change without an event in this session.

## Local development

For a local application, pass `agent-id@alias` instead of a session ID:

```tsx theme={null}
const { messages, send, sessionId, isRunning, error } = useAgent(
  "hello-world@development",
);
```

The first `send` creates a session through the authenticated development
bridge. Later calls resume that session; each call streams a turn and then
suspends it. Unlike attach mode, it normally waits through the turn and suspension
before resolving. Failures after admission appear through `error`; the
returned receipt still describes admission, not successful completion. Created sessions
have no memory bindings, and the hook does not recover their identity across
a page reload. Use attach mode for an application users will reopen.

Run the watched agent deployment and web application separately:

```bash theme={null}
npm run deploy -- --watch
npm run dev:web
```

The first provides the development bridge at the default `basePath`; the
second starts the web application. Credentials are not bundled into browser
code.

## Hook reference

| Value | Purpose |
| - | - |
| `messages` | User and assistant messages in log order |
| `turns` | Turns with status, input, messages, tool calls, result and failure |
| `send(text, options?)` | Submit a turn; resolve with a receipt or reject with `SendError` |
| `stop()` | Request an interrupt; rejects with `SendError` on a failed request, settlement arrives in `turns` |
| `question` | The open question, `{ id, text, options, turnId? }`, or `null` |
| `answer(questionId, text, options?)` | Answer the open question; same receipt and errors as `send` |
| `dismiss(questionId)` | Close the open question without an answer |
| `sessionId` | Attached session, or the created one after the first send |
| `isRunning` | Running activity or this instance's unsettled admissions; not a session status |
| `isReplaying` | Attach mode: existing history is still loading |
| `error` | Latest reported request or turn error |
| `memorySaves` | Observed `memory.saved` events |
| `cursor` | Last applied event position |

| Option | Purpose |
| - | - |
| `sessionId` | Attach to an existing session |
| `agent` | Create mode: `agent-id@alias` |
| `basePath` | Route prefix; default `/api/opencomputer/managed-agents` |
| `fetch` | Custom fetch, for example to add a CSRF header |
| `after` | Attach mode: skip events through this position; default `0` |
| `pollIntervalMs` | Attach mode: empty-poll interval; default 1000 ms, 500 ms while running |
| `onEvent` | Every applied event, including replayed history |
| `onMemorySaved` | Each observed `memory.saved` event |
| `source` | Create mode: input source |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.