레퍼런스

SDK 동작 상세

@werft-ai/sdk가 재시도, 오류, 이벤트 스트림, turn 대기, close, push 승인에서 지키는 계약

@werft-ai/sdk가 HTTP API의 규칙을 어떻게 감싸는지 적는다. 사용법은 SDK 설치, 클라이언트 설정과 오류에 있고, 이 페이지는 그 아래의 세부 계약만 다룬다. HTTP API 자체의 규칙(scope, Idempotency-Key, 오류 코드, 이벤트 스트림)은 API 유즈케이스 가이드가 정본이다. 아래 내용은 packages/sdk의 코드에서 확인되는 것만 적었다.

클라이언트와 타입

  • apiKey는 Authorization: Bearer <key>로만 보낸다. 오류 메시지, inspect, JSON 어디에도 나오지 않는다.
  • 타입은 OpenAPI에서 생성한 타입에서 이름만 붙여 가져온다. 응답 타입의 enum은 열린 union이라, 서버가 새 값(새 상태, 새 오류 코드)을 보내도 타입과 코드가 깨지지 않는다. 요청 타입은 닫혀 있다.
  • sessions.list와 turns.list는 await하면 첫 페이지, for await하면 모든 항목을 준다. page.nextPage()는 마지막 페이지에서 null이다.
  • pause·resume·terminate·close의 expected_revision은 호출자가 넘긴다. SDK가 대신 읽어 채우지 않고, REVISION_CONFLICT도 자동으로 다시 보내지 않는다.
  • limits.get(GET /v1/limits)은 로그인한 workspace owner만 읽는다. API key로 부르면 403이다.

재시도와 Idempotency-Key

  • 재시도하는 경우: 응답 본문의 retryable: true, API 오류 본문이 아닌 502·504(프록시 응답), 응답을 읽지 못한 연결 실패와 시도 timeout.
    • 연결 실패·timeout과 API 오류 본문이 아닌 502·504는 GET과 Idempotency-Key가 있는 명령만 다시 보낸다. key가 없는 openPullRequest·commentPullRequest는 서버가 이미 실행했을 수 있어 다시 보내지 않는다.
  • 재시도하지 않는 경우: 409 전부, 500. 그 밖의 API 오류는 응답 본문의 retryable이 true일 때만 다시 보낸다. 4xx도 본문이 retryable: true면 재시도한다(429 포함).
  • 기다리는 시간: Retry-After가 있으면 그 값을 쓴다. maxRetryDelayMs(기본 60초)를 넘으면 기다리지 않고 바로 실패하고, 오류의 retryAfterMs에 그 값을 싣는다. 없으면 min(0.5초·2^n, 8초, maxRetryDelayMs) 안에서 무작위로 기다린다.
  • waitForTurn·waitForReceipt·waitForPushRequest는 재시도할 수 있는 읽기 실패를 다음 확인으로 넘긴다. 그 오류에 Retry-After가 있으면 다음 확인은 그 시간이 지난 뒤이고, 이벤트가 와도 앞당기지 않는다.
  • key가 필요한 명령은 호출마다 key를 하나 정하고(직접 넘긴 idempotencyKey가 있으면 그것, 없으면 새로 만든 값), 그 호출의 모든 재시도에 같은 key를 보낸다. 같은 key로 같은 요청을 다시 보내면 서버가 명령을 한 번만 실행한다.
  • sessions.requests.answer의 기본 key는 answer:<request_id>다. 요청 하나에는 답이 하나뿐이다.
  • 프로세스를 다시 띄운 뒤 이어서 재시도하려면 오류의 idempotencyKey를 저장해 두었다가 { idempotencyKey }로 넘긴다.

오류

오류 클래스는 실패 방식마다 하나다. 서버 오류는 code로 가른다.

클래스언제주요 필드
APIError서버가 오류로 답했다status, code(서버 error.code 그대로), retryable, requestId, details, retryAfterMs
ConnectionError응답을 읽지 못했다. 명령이 실행됐는지 모른다code: "NETWORK_ERROR"
TimeoutError시도가 timeoutMs를 넘었다code: "TIMEOUT"
AbortedError호출자의 signal이 abort했다name: "AbortError", cause는 signal의 reason
TurnBlockedErrorwaitForTurn에서만. 세션이 paused·stopped·recovery_required·closed라 turn이 스스로 끝나지 않는다details.admission_state

모두 WerftError를 잇고 attempts를 싣는다. key가 있는 명령의 오류는 abort까지 idempotencyKey를 싣는다. 단, 헤더 검증이 key를 정하기 전에 던지는 TypeError에는 없다.

import { isApiError } from "@werft-ai/sdk";

try {
  await werft.sessions.pause(id, { expected_revision: revision });
} catch (error) {
  if (isApiError(error, "REVISION_CONFLICT")) {
    // 다른 사람이 세션을 바꿨다. 다시 읽고 판단한다.
  }
  throw error;
}

이벤트 스트림

const stream = werft.sessions.events.stream(sessionId, { lastEventId, signal });
for await (const event of stream) {
  console.log(event.kind, event.turn_id, event.payload);
}
// stream.lastEventId를 저장해 두면 다른 프로세스에서 그 뒤부터 받는다.
  • 끊기면 Last-Event-ID로 다시 연결해 그 이벤트 바로 뒤부터 받는다.
  • turn이나 세션이 끝나도 스스로 닫히지 않는다. signal이나 stream.close()로 끝낸다.
  • 400·401·403·404·410은 반복을 멈추고 APIError를 던진다.
  • 읽지 않은 이벤트가 10,000개를 넘으면, 받아 둔 이벤트를 모두 넘긴 뒤 code: "STREAM_OVERFLOW"인 WerftError를 던진다. 넘긴 이벤트는 빈틈없이 이어지므로 stream.lastEventId로 다시 열면 빠지는 이벤트가 없다.
  • lastEventId 없이 열면 세션 전체를 처음부터 다시 받는다. owner마다 열 수 있는 스트림 수에 한도가 있다(넘으면 429).

turn 대기와 권한 요청 응답

waitForTurn은 turn이 끝날 때까지 REST로 2초마다 확인한다. 같은 루프에서 세션의 대기 중인 요청을 handler에 넘긴다.

import { type PendingRequestHandlers, waitForTurn } from "@werft-ai/sdk";

const approvals: PendingRequestHandlers = {
  permission(request) {
    const command = (request.input as { command?: unknown } | null)?.command;
    if (request.tool === "Bash" && command === "git status") return { action: "allow" };
    return { action: "deny", reason: "This script only allows: git status" };
  },
};

const turn = await waitForTurn(werft, sessionId, turnId, {
  signal: AbortSignal.timeout(30 * 60_000),
  onRequest: approvals,
  onAnswer: (outcome) => console.log(outcome.request_id, outcome.outcome),
  events: { onEvent: (event) => console.log(event.kind) }, // 선택: 이벤트가 오면 바로 깨어난다
});
console.log(turn.status); // "failed"도 오류가 아니라 값이다
  • 끝났다는 판단은 GET …/turns/{turn_id}의 status가 terminal(completed·failed·interrupted·cancelled·outcome_unknown)이거나 ended_at이 있을 때다. result 이벤트는 완료 판정이 아니다.
  • handler는 보안 경계다. 정확히 일치하는 입력만 허락하고 나머지는 거절한다. 거절에는 타입상 reason이 필요하다.
  • handler가 undefined를 돌려주거나 그 kind의 handler가 없으면 요청을 열어 두고 콘솔이나 Slack의 사람에게 맡긴다.
  • 답의 결과(AnswerOutcome): accepted(저장됨, receipt가 나중에 failed가 될 수 있다), left_open, too_late(REQUEST_EXPIRED·REQUEST_STALE), conflict(다른 답이 먼저 저장됨).
  • 답하려면 key에 sessions:approve가 있어야 한다. 없으면 403으로 대기가 실패한다.
  • 시한은 signal 하나다. 질문을 열어 둔 채 기다리면 사람이 답할 때까지 기다린다.
  • events를 켜면 스트림 하나를 더 쓴다. 스트림이 실패해도 polling으로 계속한다.

한 번만 처리하려면 answerPending(werft, sessionId, approvals)를 쓴다.

세션 닫기 (close)

할 일을 마친 세션을 끝낸다. terminate와 달리 아무것도 죽이지 않는다. 돌던 turn이 끝나고 checkpoint가 커밋된 뒤 세션이 closed가 된다. scope는 sessions:control이다.

import { closeBlocked, closeResult, waitForReceipt } from "@werft-ai/sdk";

const { revision } = await werft.sessions.get(sessionId);
const accepted = await werft.sessions.close(sessionId, {
  expected_revision: revision,
  reason: "script finished",
});
const receipt = await waitForReceipt(werft, accepted.receipt_id, {
  signal: AbortSignal.timeout(10 * 60_000),
});
console.log(receipt.status, closeResult(receipt));
// { resulting_admission_state: "closed", checkpoint_revision: 3, cancelled_turn_count: 0 }
  • 닫는 동안 세션은 closing이다(isClosing(session)). 이때 보낸 메시지와 대부분의 제어는 409 SESSION_CLOSING이다. terminate는 closing 중에도 받아들여 close를 앞지르고, close의 receipt는 CONTROL_SUPERSEDED로 실패한다.
  • drain이 60초를 넘으면 세션 attention이 CLOSE_BLOCKED가 된다(closeBlocked(session)이 그 reason을 준다). 열린 질문이나 승인 요청이 있으면 답하거나 turn을 interrupt할 때까지 close가 기다린다. 그래서 waitForReceipt에는 signal로 시한을 준다.
  • 대기 중이던 입력은 close가 커밋될 때 취소된다(cancelled_turn_count). 닫힌 뒤의 메시지·제어는 409 SESSION_CLOSED다.
  • 자기 turn 안에서 자기 세션을 닫는 에이전트는 receipt를 기다리지 않는다. drain이 그 turn이 끝나기를 기다리기 때문이다.

push 승인

import { waitForPushRequest } from "@werft-ai/sdk";

const push = await werft.sessions.pushRequests.create(sessionId);
// push.git_commit을 검토한 뒤, 승인 key로:
await approver.sessions.pushRequests.approve(sessionId, push.id, {
  git_commit: push.git_commit,
});
const done = await waitForPushRequest(werft, sessionId, push.id, { signal });
if (done.state === "pushed")
  await approver.sessions.pushRequests.openPullRequest(sessionId, push.id);

SDK는 CHECKPOINT_MOVED를 따로 처리하지 않는다. 받아도 다시 읽어 다시 승인하지 않으므로, 검토한 commit만 승인한다.

버전

  • SDK 버전(SDK_VERSION)은 패키지 자신의 semver다. 게시 버전은 npm view @werft-ai/sdk dist-tags로 본다.
  • API_SPEC_VERSION은 타입을 생성한 스펙의 info.version이다. 런타임에 서버 버전과 맞춰 보지는 않는다.
  • SSE envelope은 schema_version 1만 읽는다(SUPPORTED_SSE_SCHEMA_VERSIONS).

다른 곳에 있는 것

내용위치
설치, 서버 주소와 API key로 클라이언트 만들기SDK 설치
첫 스크립트: 세션 만들기, 권한 요청에 답하기, 닫기첫 세션 만들기, 권한 요청과 닫기
클라이언트 옵션, 승인용 key, 감싸는 API, 저장소의 예제클라이언트 설정과 오류
이벤트 스트림 받기스트리밍 받기
세션·메시지·승인의 메서드세션, 메시지, 승인과 push
유즈케이스별 샘플SDK 샘플

이 페이지