Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
TypeScript SDK

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.

Available on Nodaro Cloud

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 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. 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

MethodWhat it does
create(input)Open a thread on a workflow, or on a new one
list(params)Read the active thread of a workflow
get(id, opts?)Read a thread with its messages
archive(id)Archive a thread
cancel(id)Stop the running turn
stream(threadId, 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.

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

Prop

Type

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.

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

Prop

Type

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.

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

Prop

Type

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.

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

Prop

Type

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.

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

Prop

Type

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.

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

Prop

Type

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:

FrameWhat it carries
metadataThe turn's ids, model, runMode, autoRunLimitCredits and settings
tokenA chunk of the answer's text
tool_callA tool the assistant uses, started, then finished or failed
workflow_updatedThe nodes added, changed and removed, and the new version
workflow_createdA workflow the assistant created
run_proposedWhat the turn wants to run, for a person to confirm
memory_savedSomething the assistant chose to remember
usageTokens used and creditsCharged
doneThe end: completed, capped or cancelled
errorThe 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, before any frame.

Frequently asked questions

Last updated on

On this page