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

# 에러와 실패 모델

> 타입 에러 클래스, 실패 4층, 구조화 로깅

SDK는 실패를 네 층으로 나눠, 어느 것이 예외를 던지고 어느 것이 결과로 돌아오는지 엄격히 분리합니다. `.message` 문자열 매칭이 아니라 타입 클래스로 분기하고, 구조화 로거로 재시도까지 겉으로 드러나게 두세요.

## 타입 에러

`.message`가 아니라 에러 클래스로 분기하세요. 모든 에러가 안정적 `.code`, (해당되면) 그 실행의 `.traceId`, 서버가 판정한 `.retryable`을 들고 옵니다.

```ts theme={null}
import { NoraAuthError, NoraProviderError } from "@conscience-technology/nora-sdk";

try {
  await nora.improvements.approve("imp_1");
} catch (e) {
  if (e instanceof NoraAuthError) {
    // 인증 · 스코프 · 주체 문제
    // 예: read 토큰으로 approve 호출 → e.code === "auth.scope_denied"
  } else if (e instanceof NoraProviderError) {
    // 상위 LLM 프로바이더 문제 (e.retryable 확인)
  }
}
```

**계층**:

```
NoraError (base)
  ├── NoraAuthError       : 401 · 스코프 거부 · onBehalfOf 주체 없음
  ├── NoraRequestError    : Flow 없음, 미게시, 필수 스코프 누락
  ├── NoraAccessError     : 리소스 단위 접근 규칙 거부
  ├── NoraProviderError   : 상위 LLM 프로바이더 (.retryable로 판정)
  └── NoraPlatformError   : Nora 서버 5xx, 전송 실패
```

## 실패 4층

| 층                 | 예                                        | SDK 동작                                             |
| ----------------- | ---------------------------------------- | -------------------------------------------------- |
| **전송 · 인증**       | 네트워크 · 401 · 스코프 거부 · `onBehalfOf` 주체 없음 | **예외를 던짐**                                         |
| **요청**            | Flow 슬러그 없음 · Flow 미게시 · PAT에 필수 스코프 없음  | **예외를 던짐**                                         |
| **실행 결과**         | 턴 소진 · 비용 상한 · 툴 실패 · `needs_review`     | **결과로 돌려줌** (`traceId` + 부분 `outputs` + `failure`) |
| **제약 안내(notice)** | 툴 호출 한도 도달 · 리트리벌 결과 0 · 출력 잘림           | **실패 아님**, `notices[]`에 실림                         |

**이 규칙을 잘 읽어 두세요.** Flow가 시작됐지만 도중에 실패하면 SDK는 예외를 **던지지 않고**, `{ status: "failed", traceId, outputs: <부분>, failure: {...}, notices: [...] }`를 돌려줍니다. 예외만 잡는 조용한 try/catch는 이 실패를 놓칩니다. `flows.run`의 반환값 `.status`를 반드시 확인하세요.

### 왜 나뉘어 있나

"요청이 그래프에 도달하지도 못했다" 는 것은 예외로 던져 호출자가 크고 빠르게 실패하게 합니다. "그래프는 돌았는데 답이 깔끔하지 않다" 는 것은 결과로 돌려 다음을 다 챙깁니다.

* 트레이스 UI와 `feedback` / `signals.report` 용 `traceId`
* 만들어진 부분 `outputs`
* 재시도 · 다운그레이드 결정에 쓰는 기계 판독 가능 `failure.code`
* 실행을 제약한 `notices[]` (관리자 UI에 노출하고, 최종 사용자에는 X)

## 로깅

기본이 구조화 · 레드액션입니다. 토큰 · 시크릿 · `onBehalfOf` 값 · 요청/응답 본문은 `debug: true`를 안 켜면 로그에 남지 않습니다.

```ts theme={null}
const nora = createClient({
  // …
  logger: pino(),                       // 여러분의 로깅 스택에 SDK 로그 꽂기
  onLog: (e) => metrics.count(e.event), // SDK 내부 이벤트(재시도, 서킷, 폴백) 관측
  debug: false,                         // true = 요청/응답 본문 포함
});
```

재시도는 언제나 로깅됩니다. 각 시도마다 `attempt`, 실패 `code`, 결정이 남습니다. 조용한 재시도는 없습니다.

권장:

* Nora 로그를 서비스 로거(pino / winston / bunyan)에 연결해 로그 레벨과 목적지를 맞추세요.
* `onLog`를 메트릭에 연결하세요. 재시도율, 서킷 오픈율, 프로바이더 폴백 빈도는 상위 프로바이더가 삐걱대기 시작할 때 가장 먼저 움직이는 지표입니다.
* 프로덕션에서는 `debug`를 끄고, 사고 재현할 때만 요청 단위 스코프드 클론으로 켜세요.

## 흔한 패턴

### `retryable`일 때만 재시도

```ts theme={null}
try {
  return await nora.improvements.approve(id);
} catch (e) {
  if (e instanceof NoraProviderError && e.retryable) {
    // SDK가 이미 maxRetries 안에서 재시도했음. 앱 레벨 재시도는
    // 더 긴 백오프나 dedupe 키 같은 추가 컨텍스트가 있을 때만
    return await withBackoff(() => nora.improvements.approve(id));
  }
  throw e;
}
```

### partial이면 다운그레이드

```ts theme={null}
const r = await nora.flows.run("support", input);

if (r.status === "failed") {
  return { text: fallbackAnswer, traceId: r.traceId, failure: r.failure };
}
if (r.status === "needs_review") {
  await queueForReview(r.traceId);
  return { text: r.outputs.answer, review: true };
}
return { text: r.outputs.answer };
```

### `feedback`와 `signals.report` 구분

```ts theme={null}
// 답 품질 (사용자가 답을 어떻게 봤는지)
await nora.feedback(traceId, { rating: "dislike", correction: { before, after } });

// 비즈니스 결과 (실제로 다운스트림에서 무슨 일이 있었는지)
await nora.signals.report(traceId, { outcome: "reopened", reason: "다음 날 다시 문의" });
```

섞으면 탐지기와 군집이 시끄러워집니다. 의도를 분리해서 담으세요.
