# Copilot

> client.copilot drives the Copilot assistant's threads and streamed turns. It works only inside a Nodaro app, with a signed-in user's own session.

Source: https://nodaro.ai/docs/developers/sdk/copilot

**`client.copilot`** drives Copilot, Nodaro's assistant for workflows. A **thread** is a conversation opened on a workflow, and a **turn** is one message plus everything the assistant does to answer it. The methods call `/v1/copilot/*`. See [Workflow Copilot](https://nodaro.ai/docs/get-started/workflow-copilot) for the feature.

**Copilot works only inside a Nodaro app.** Every route refuses a caller that does not carry a signed-in user's own session, with `403 in_app_only`. The SDK throws it as `ForbiddenError`, whose `code` reads `forbidden`, so check its `message`. API tokens and OAuth tokens cannot drive anyone's Copilot. Use it from a browser app with [`supabaseAuth`](https://nodaro.ai/docs/developers/sdk/auth#supabaseauthsupabase). It runs on Nodaro Cloud. A deployment with the feature switched off answers `503 feature_disabled` when you open a thread or send a turn.

## Methods

| Method | What it does |
| --- | --- |
| [`create(input)`](#createinput) | Open a thread on a workflow, or on a new one |
| [`list(params)`](#listparams) | Read the active thread of a workflow |
| [`get(id, opts?)`](#getid-opts) | Read a thread with its messages |
| [`archive(id)`](#archiveid) | Archive a thread |
| [`cancel(id)`](#cancelid) | Stop the running turn |
| [`stream(threadId, opts)`](#streamthreadid-opts) | Send a message and read the turn's frames as they arrive |

These methods return the API's `{ data }` envelope as it arrives, except `stream()`, which yields frames.

## client.copilot

### create(input)

Opens a thread. Give an existing `workflowId`, or a `prompt`, and the server creates a workflow seeded by it. Opening a workflow that already has an active thread returns that thread instead of a second one.

```ts
create(input: { workflowId?: string; prompt?: string; name?: string }): Promise<{
data: { thread: CopilotThread; workflow: CopilotThreadWorkflow }
}>
```

<TypeTable
type={{
workflowId: { type: 'string', description: "An existing workflow. Give workflowId or prompt." },
prompt: { type: 'string', description: "A prompt to seed a new workflow with." },
name: { type: 'string', description: "The new workflow's name, with prompt." },
}}
/>

```ts
const { data } = await client.copilot.create({ workflowId })
const threadId = data.thread.id
```

A thread has `id`, `workflowId`, `runMode` (`ask`, which proposes runs and waits, or `auto`, which runs within `autoRunLimitCredits`), `modelTier`, `allowPublishing`, `userTurnCount`, `lastMessageAt` and `createdAt`. `surface` names the assistant the thread belongs to; when it is absent, the deployment did not say.

### list(params)

Reads the active thread of a workflow, or `null` when there is none.

```ts
list(params: { workflowId: string }): Promise<{ data: { thread: CopilotThread | null } }>
```

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "The workflow id." },
}}
/>

```ts
const { data: { thread } } = await client.copilot.list({ workflowId })
```

### get(id, opts?)

Reads a thread with its messages. The thread also carries a derived `status` (`running` or `idle`) and `activeTurnId`.

```ts
get(id: string, opts?: { after?: number; limit?: number }): Promise<{
data: { thread: CopilotThread; messages: CopilotMessage[] }
}>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The thread id." },
after: { type: 'number', description: "Return only messages after this sequence number, to catch up." },
limit: { type: 'number', description: "The page size, up to the server's limit." },
}}
/>

```ts
const { data: { messages } } = await client.copilot.get(threadId, { after: lastSeq })
```

Each message has `id`, `seq`, `turnId`, `role`, `createdAt` and `parts`: text parts and tool-call parts.

### archive(id)

Archives a thread. The messages stay readable; nothing is deleted. A thread with a running turn cannot be archived.

```ts
archive(id: string): Promise<{ data: { archived: true } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The thread id." },
}}
/>

```ts
await client.copilot.archive(threadId)
```

### cancel(id)

Stops the running turn of a thread and answers which turn it asked to stop. That turn's stream ends with a `done` frame.

```ts
cancel(id: string): Promise<{ data: { cancelling: true; turnId: string } }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The thread id." },
}}
/>

```ts
await client.copilot.cancel(threadId)
```

### stream(threadId, opts)

Sends a message and yields the turn's frames as they arrive, as server-sent events. Render text, tool activity and proposals live instead of waiting for the full answer. It is the only method here that spends credits.

```ts
stream(threadId: string, opts: {
message: string
baseVersion?: number
tier?: "economy" | "standard" | "premium"
signal?: AbortSignal
}): AsyncGenerator<CopilotStreamFrame>
```

<TypeTable
type={{
threadId: { type: 'string', required: true, description: "The thread id." },
message: { type: 'string', required: true, description: "The user's message." },
baseVersion: { type: 'number', description: "The workflow version the user sees." },
tier: { type: '"economy" | "standard" | "premium"', description: "The model tier for this turn." },
signal: { type: 'AbortSignal', description: "Ends the turn's stream." },
}}
/>

```ts
try {
for await (const frame of client.copilot.stream(threadId, { message: "Tidy the graph" })) {
if (frame.type === "token") appendText(frame.data.text)
if (frame.type === "run_proposed") await askTheUser(frame.data)
if (frame.type === "done") break
}
} catch (err) {
if ((err as Error).name !== "AbortError") throw err // a stopped turn is a normal ending
}
```

`CopilotStreamFrame` is a union on `type`:

| Frame | What it carries |
| --- | --- |
| `metadata` | The turn's ids, model, `runMode`, `autoRunLimitCredits` and settings |
| `token` | A chunk of the answer's text |
| `tool_call` | A tool the assistant uses, `started`, then `finished` or `failed` |
| `workflow_updated` | The nodes added, changed and removed, and the new version |
| `workflow_created` | A workflow the assistant created |
| `run_proposed` | What the turn wants to run, for a person to confirm |
| `memory_saved` | Something the assistant chose to remember |
| `usage` | Tokens used and `creditsCharged` |
| `done` | The end: `completed`, `capped` or `cancelled` |
| `error` | The turn failed, with a `code` and a `message` |

A frame's `data` is passed through as it is, so fields this SDK does not model still reach you. A frame kind it does not model is skipped instead of throwing, so a newer server cannot break an older client. This version does not model `action_proposed`, the proposal card of threads on the `studio` surface, so those proposals do not reach the caller.

**The turn's lifetime is yours.** The client's `timeoutMs` does not apply to a turn, because a turn can run for minutes. Pass `signal`, or stop iterating, which ends the request. Aborting rejects the iteration with the runtime's own `AbortError`, not a `NodaroError`, whether it happens before the first frame or between two. An error status on the opening request still throws the usual [typed error](https://nodaro.ai/docs/developers/sdk/errors), before any frame.

## Frequently asked questions

### Can I use client.copilot with an API token?

No. Every copilot route refuses API tokens and OAuth tokens with 403 in_app_only. Only a signed-in user's own session, sent with supabaseAuth from a Nodaro app, can use it.

### What does client.copilot.stream return?

An async iterator of typed frames: the turn's metadata, chunks of text, tool calls, workflow updates, run proposals, usage and a final done or error frame. Iterate it with for await.

### Does the client timeout stop a long Copilot turn?

No. The timeout does not apply to a turn, because a turn can run for minutes. Pass an AbortSignal, or stop iterating, to end it.

### Which frames might a caller not see?

Frame kinds this SDK version does not model are skipped. That includes the proposal card of threads on the studio surface, called action_proposed, so those proposals do not reach the caller.
