# Copilot

> client.copilot은 Copilot 어시스턴트의 스레드와 스트리밍 턴을 다룹니다. Nodaro 앱 안에서, 로그인한 사용자 본인의 세션으로만 동작합니다.

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

<strong>`client.copilot`</strong>은 Nodaro의 워크플로 어시스턴트인 Copilot을 다룹니다. **스레드**는 워크플로에서 연 대화이고, **턴**은 메시지 하나와 어시스턴트가 그에 답하기 위해 하는 모든 일입니다. 이 메서드들은 `/v1/copilot/*`를 호출합니다. 기능 자체는 [Workflow Copilot](https://nodaro.ai/docs/get-started/workflow-copilot)을 참조하세요.

**Copilot은 Nodaro 앱 안에서만 동작합니다.** 모든 라우트는 로그인한 사용자 본인의 세션을 담지 않은 호출자를 `403 in_app_only`로 거부합니다. SDK는 이를 `ForbiddenError`로 발생시키며, 그 `code`는 `forbidden`이므로 `message`를 확인하세요. API 토큰과 OAuth 토큰으로는 누구의 Copilot도 다룰 수 없습니다. [`supabaseAuth`](https://nodaro.ai/docs/developers/sdk/auth#supabaseauthsupabase)를 사용하는 브라우저 앱에서 사용하세요. Nodaro Cloud에서 동작합니다. 이 기능을 끈 배포 환경은 스레드를 열거나 턴을 보낼 때 `503 feature_disabled`로 응답합니다.

## 메서드
| 메서드 | 기능 |
| --- | --- |
| [`create(input)`](#createinput) | 워크플로에서, 또는 새 워크플로에서 스레드 열기 |
| [`list(params)`](#listparams) | 워크플로의 활성 스레드 읽기 |
| [`get(id, opts?)`](#getid-opts) | 메시지와 함께 스레드 읽기 |
| [`archive(id)`](#archiveid) | 스레드 보관 |
| [`cancel(id)`](#cancelid) | 실행 중인 턴 중지 |
| [`stream(threadId, opts)`](#streamthreadid-opts) | 메시지를 보내고 턴의 프레임을 도착하는 대로 읽기 |

이 메서드들은 API의 `{ data }` 엔벨로프를 받은 그대로 반환합니다. 프레임을 내보내는 `stream()`만 예외입니다.

## client.copilot
### create(input)
스레드를 엽니다. 기존 `workflowId`를 주거나 `prompt`를 주면, 서버가 그 프롬프트로 시작하는 워크플로를 만듭니다. 이미 활성 스레드가 있는 워크플로를 열면 두 번째 스레드를 만들지 않고 그 스레드를 반환합니다.

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

<TypeTable
type={{
workflowId: { type: 'string', description: "기존 워크플로입니다. workflowId나 prompt 중 하나를 주세요." },
prompt: { type: 'string', description: "새 워크플로를 시작할 프롬프트입니다." },
name: { type: 'string', description: "새 워크플로의 이름이며, prompt와 함께 사용합니다." },
}}
/>

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

스레드에는 `id`, `workflowId`, `runMode`, `modelTier`, `allowPublishing`, `userTurnCount`, `lastMessageAt`, `createdAt`이 있습니다. `runMode`는 실행을 제안하고 기다리는 `ask`이거나, `autoRunLimitCredits` 안에서 실행하는 `auto`입니다. `surface`는 스레드가 속한 어시스턴트를 나타냅니다. 이 값이 없으면 배포 환경이 알려 주지 않은 것입니다.

### list(params)
워크플로의 활성 스레드를 읽습니다. 활성 스레드가 없으면 `null`입니다.

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

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "워크플로 ID입니다." },
}}
/>

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

### get(id, opts?)
메시지와 함께 스레드를 읽습니다. 스레드에는 계산된 `status`(`running` 또는 `idle`)와 `activeTurnId`도 들어 있습니다.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "스레드 ID입니다." },
after: { type: 'number', description: "이 시퀀스 번호 이후의 메시지만 반환해, 놓친 메시지를 따라잡습니다." },
limit: { type: 'number', description: "페이지 크기이며, 서버의 한도까지 지정할 수 있습니다." },
}}
/>

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

각 메시지에는 `id`, `seq`, `turnId`, `role`, `createdAt`, `parts`가 있습니다. `parts`는 텍스트 부분과 도구 호출 부분입니다.

### archive(id)
스레드를 보관합니다. 메시지는 계속 읽을 수 있으며, 아무것도 삭제되지 않습니다. 실행 중인 턴이 있는 스레드는 보관할 수 없습니다.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "스레드 ID입니다." },
}}
/>

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

### cancel(id)
스레드에서 실행 중인 턴을 중지하고, 중지를 요청한 턴이 무엇인지 응답합니다. 그 턴의 스트림은 `done` 프레임으로 끝납니다.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "스레드 ID입니다." },
}}
/>

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

### stream(threadId, opts)
메시지를 보내고 턴의 프레임을 server-sent events로 도착하는 대로 내보냅니다. 전체 답변을 기다리지 말고 텍스트, 도구 활동, 제안을 실시간으로 렌더링하세요. 여기서 크레딧을 사용하는 메서드는 이것뿐입니다.

```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: "스레드 ID입니다." },
message: { type: 'string', required: true, description: "사용자의 메시지입니다." },
baseVersion: { type: 'number', description: "사용자가 보고 있는 워크플로 버전입니다." },
tier: { type: '"economy" | "standard" | "premium"', description: "이 턴에 쓸 모델 등급입니다." },
signal: { type: 'AbortSignal', description: "턴의 스트림을 끝냅니다." },
}}
/>

```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`은 `type`으로 구분되는 유니언입니다.

| 프레임 | 담긴 내용 |
| --- | --- |
| `metadata` | 턴의 ID, 모델, `runMode`, `autoRunLimitCredits`, 설정 |
| `token` | 답변 텍스트의 한 조각 |
| `tool_call` | 어시스턴트가 사용하는 도구. `started` 다음에 `finished` 또는 `failed` |
| `workflow_updated` | 추가, 변경, 제거된 노드와 새 버전 |
| `workflow_created` | 어시스턴트가 만든 워크플로 |
| `run_proposed` | 턴이 실행하려는 내용. 사람이 확인해야 함 |
| `memory_saved` | 어시스턴트가 기억하기로 한 내용 |
| `usage` | 사용한 토큰과 `creditsCharged` |
| `done` | 종료: `completed`, `capped`, `cancelled` 중 하나 |
| `error` | 턴이 실패함. `code`와 `message` 포함 |

프레임의 `data`는 받은 그대로 전달되므로, 이 SDK가 모델링하지 않는 필드도 그대로 받을 수 있습니다. 모델링하지 않는 종류의 프레임은 오류를 발생시키지 않고 건너뛰므로, 새 서버가 이전 클라이언트를 망가뜨릴 수 없습니다. 이 버전은 `studio` 화면에 있는 스레드의 제안 카드인 `action_proposed`를 모델링하지 않으므로, 그 제안은 호출자에게 전달되지 않습니다.

**턴의 수명은 호출자가 관리합니다.** 턴은 몇 분 동안 이어질 수 있으므로 클라이언트의 `timeoutMs`는 턴에 적용되지 않습니다. `signal`을 전달하거나 순회를 멈추면 요청이 끝납니다. 중단하면 첫 프레임 전이든 두 프레임 사이든, 순회는 `NodaroError`가 아니라 런타임 자체의 `AbortError`로 거부됩니다. 시작 요청이 오류 상태를 받으면, 프레임이 오기 전에 평소의 [타입 지정 오류](https://nodaro.ai/docs/developers/sdk/errors)가 발생합니다.

## 자주 묻는 질문

### API 토큰으로 client.copilot을 사용할 수 있나요?

아니요. 모든 Copilot 라우트는 API 토큰과 OAuth 토큰을 403 in_app_only로 거부합니다. Nodaro 앱에서 supabaseAuth로 보낸, 로그인한 사용자 본인의 세션만 사용할 수 있습니다.

### client.copilot.stream은 무엇을 반환하나요?

타입이 지정된 프레임의 비동기 이터레이터를 반환합니다. 턴의 메타데이터, 텍스트 조각, 도구 호출, 워크플로 업데이트, 실행 제안, 사용량, 그리고 마지막의 done 또는 error 프레임이 들어 있습니다. for await로 순회하세요.

### 클라이언트 타임아웃이 긴 Copilot 턴을 중단하나요?

아니요. 턴은 몇 분 동안 이어질 수 있으므로 타임아웃이 턴에는 적용되지 않습니다. 턴을 끝내려면 AbortSignal을 전달하거나 순회를 멈추세요.

### 호출자에게 전달되지 않을 수 있는 프레임은 무엇인가요?

이 SDK 버전이 모델링하지 않는 종류의 프레임은 건너뜁니다. studio 화면에 있는 스레드의 제안 카드인 action_proposed도 여기에 해당하므로, 그 제안은 호출자에게 전달되지 않습니다.
