> ## Documentation Index
> Fetch the complete documentation index at: https://docs.devin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Devin with the AI SDK UI (useChat)

> Stream a Devin cloud session into useChat with @cognition-ai/ai-sdk: UI message streams, history replay, resumable turns and tool approvals.

`@cognition-ai/ai-sdk` turns the typed events of a `@cognition-ai/sdk` session into
[AI SDK UI message streams](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol), so a Devin cloud
session can back `useChat` from any server route — Node.js, Cloudflare Workers or Deno. Devin's
messages and thoughts become text and reasoning parts, its tool calls become `dynamic-tool` parts,
and its plans and pull requests become data parts your UI can render.

This is the package for a chat you host yourself, talking to Devin's cloud. For Devin running
inside a sandbox you own, see the [AI SDK harness](https://docs.devin.ai/sdk/harness/overview.md); for a ready-made agent
on Cloudflare, see [Cloudflare Agents](https://docs.devin.ai/sdk/cloudflare-agents/overview.md), which is built on this
package.

## Setup

```sh theme={null}
npm install @cognition-ai/ai-sdk@beta @cognition-ai/sdk@beta ai
```

> [!NOTE]
>
> The `@cognition-ai` packages are in beta. Every release is a `0.0.1-beta.N` prerelease on the
> `beta` dist-tag (currently `0.0.1-beta.7`), so the `@beta` suffix is required: a bare
> `npm install @cognition-ai/sdk` fails on purpose. Pin the exact version in your lockfile, and keep
> every `@cognition-ai` package you use (`sdk`, `ai-sdk`, `cloudflare-agents`, `harness-devin`) on
> the same version; they release together. Each published version is stable, but the API can
> change between beta bumps; see the [changelog](https://docs.devin.ai/sdk/changelog.md) for what moved.

## A chat route

```ts app/api/chat/route.ts theme={null}
import {
  devinSessionMetadata,
  toPromptInput,
  toUIMessageStream,
  type DevinUIMessage,
} from "@cognition-ai/ai-sdk";
import { createDevin } from "@cognition-ai/sdk";
import { createUIMessageStreamResponse } from "ai";

const devin = await createDevin({ apiKey: process.env.DEVIN_API_KEY });

export async function POST(request: Request) {
  const { id, messages } = (await request.json()) as { id: string; messages: DevinUIMessage[] };
  const session = await devin.attach(id);
  return createUIMessageStreamResponse({
    stream: toUIMessageStream(session.stream(toPromptInput(messages), { signal: request.signal }), {
      messageMetadata: devinSessionMetadata(session),
    }),
  });
}
```

On the client, `useChat<DevinUIMessage>()` types the Devin data parts.

* `toPromptInput(messages)` sends only the latest user message. The Devin session keeps its own
  transcript, so there is nothing to replay.
* `toUIMessageStream(events)` accepts `session.stream(prompt)` for one turn, or `session.events()`
  to observe a session.
* `{ signal: request.signal }` cancels the Devin turn as soon as the client stops (`useChat`'s
  `stop()`). Without it, cancelling the stream cancels the turn when Devin's next update arrives.
* `devinSessionMetadata(session)` puts `{ sessionId, sessionUrl }` on the message, plus the
  `stopReason` when the turn finishes.

## What the stream contains

| Devin event | UI message chunks |
| - | - |
| `thought_delta`, `message_delta` | `reasoning-*` / `text-*` parts, one per Devin message id |
| `tool_call` | `dynamic-tool` part: input once final, then output or error |
| permission request (with `approvals`) | `tool-approval-request` for the tool call |
| `plan` | `data-devin-plan` (one part, updated in place) |
| `pull_request` | `data-devin-pull-request` (one part per URL) |
| `status`, `activity`, `lifecycle` | transient `data-devin-status` / `-activity` / `-lifecycle` (use `onData`) |
| end of turn | `finish` with the stop reason, or `abort` when the turn was cancelled |
| `user_message`, `typing`, `raw`, mode/commands | nothing; observe them with `onEvent` |

A tool part's `input` is the SDK's normalized [`ToolCallDetail`](https://docs.devin.ai/sdk/guides/streaming-events#tool-calls-the-same-on-both-agents)
(`exec` with `command`, `cwd`, `exitCode`; `edit` with `files`; `mcp` with `server` and `name`;
…), and its `output` is `{ detail, text, rawOutput }`, so a UI can render a `bash` card with the
command and its exit code instead of raw JSON. UI message chunks only append, so a Devin message
that is revised without extending its text starts a new part.

## Reloading history

`toUIMessages(replay)` rebuilds the chat after a page reload, using the same rules as the stream.
Status, activity and lifecycle parts are transient, so they are not in the result.

```ts theme={null}
const { session, replay } = await devin.loadSession(id);
const messages = await toUIMessages(replay);
```

## Reconnecting to a running turn

For `useChat`'s `resumeStream()`, serve a `GET` route that observes the session. `endWhenIdle` ends
the stream when the turn goes idle; `session.events()` otherwise never ends.

```ts app/api/chat/[id]/stream/route.ts theme={null}
export async function GET(request: Request, { params }: { params: { id: string } }) {
  const session = await devin.attach(params.id);
  return createUIMessageStreamResponse({
    stream: toUIMessageStream(session.events({ signal: request.signal }), { endWhenIdle: true }),
  });
}
```

## Approving tool calls

`ToolApprovals` turns Devin permission requests into AI SDK tool approvals. The stream sends a
`tool-approval-request` and finishes; the user answers with `addToolApprovalResponse`, and the
next request resumes the same Devin turn. Keep one `ToolApprovals` per session in something that
outlives a request, such as a Durable Object.

```ts theme={null}
const approvals = new ToolApprovals();
session.onPermission = approvals.onPermission;

export async function POST(request: Request) {
  const { messages } = (await request.json()) as { messages: DevinUIMessage[] };
  const stream =
    approvals.respond(messages) ??
    toUIMessageStream(session.stream(toPromptInput(messages)), { approvals });
  return createUIMessageStreamResponse({ stream });
}
```

On the client, set `sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses` so
answering sends the request. Approving picks Devin's allow-once option and denying picks
reject-once (a denied tool shows as `output-denied`).

> [!NOTE]
>
> Devin's cloud also reports permission decisions it needs as `permission_request` tool calls in
> the transcript (`detail.tool === "permission_request"`, with a `requestId`), answered with
> `session.respondToPermission()`. A UI that renders tool parts can offer Allow / Deny on those
> directly; the [Cloudflare Agents chat example](https://docs.devin.ai/sdk/examples/cloudflare-agents.md) does both.

## Where it runs

Anywhere `@cognition-ai/sdk` connects to the cloud: Node.js, Bun, Cloudflare Workers, Deno 2.5+.
See [Where it runs](https://docs.devin.ai/sdk/guides/cloud-sessions.md#where-it-runs) for the runtime rules.


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