기능세션

시작

세션 생성 요청의 필드, 응답, Idempotency-Key, 목록·공유 범위·제목, 오류

세션은 프로필 하나로 도는 대화다. 프로필이 저장소를 쓰면 저장소도 하나 고른다. 세션을 만들면 첫 메시지가 turn 1로 접수되고, scheduler가 그 세션의 worker 컨테이너를 띄운다. scope는 sessions:write다. SDK 예제는 SDK 설치에서 만든 werft 클라이언트를, HTTP API 예제는 서버 주소 API와 key KEY 셸 변수를 쓴다.

웹 콘솔에서는 에이전트 상세의 "이 에이전트로 새 세션"을 누르고 첫 메시지를 보낸다. 세션은 활성 release로 시작하고(GitHub 설정이 있는 에이전트는 저장소 없이, 설정이 없는 옛 에이전트는 release의 저장소로), 같은 POST /v1/sessions를 로그인한 사용자 권한으로 부른다. 화면에서 미리 알 수 있는 상태(활성 release 없음, 일시정지, 권한 없음 등)에서는 버튼이 꺼지고 그 이유가 보인다. 자격 연결 누락처럼 보낸 뒤에야 아는 거절은 대화상자에 이유가 나온다.

예제의 GATE-SPEC {…} 메시지는 개발용 서버의 fake 모델이 읽는 대본이다. fake 모델은 대본대로 도구를 부르고 답하므로 출력이 늘 같다. 실제 모델로 띄운 서버에서는 같은 도구 호출과 출력이 나오지 않으니 평소의 지시를 보낸다.

만들기

const created = await werft.sessions.create({
  profile_id: "claude-coding-local",
  repository_id: "sample-app",
  message: 'GATE-SPEC {"id":"start","steps":[],"final":"started"}',
});
console.log(created);
const sessionId = created.session_id;
{
  session_id: '…',
  turn_id: '1',
  receipt_id: '…',
  receipt_status: 'accepted',
  status: 'queued'
}
필드필수내용
profile_idagent_release_id가 없으면 예프로필 id(프로필 고르기)
repository_id프로필에 따라카탈로그의 저장소 id. 보내면 worker가 이 저장소를 clone한다. 프로필의 repository_mode가 none이면 보내지 않고, required인 프로필은 꼭 보낸다. 단 GitHub 설정이 있는 에이전트의 세션과 워크스페이스 상한이 정하는 프로필은 required여도 빼고 저장소 없이 시작할 수 있다. 에이전트 release로 만든 세션에서 빼면, GitHub 설정이 있는 에이전트는 저장소 없이 시작하고 필요한 저장소만 붙이며, 설정이 없는 옛 에이전트는 release의 저장소로 시작한다(저장소 없는 세션)
message예첫 turn의 입력. UTF-8 32,768바이트까지
model프로필이 허용한 모델 중 하나. 빼면 기본 모델
title세션 이름. 빼면 첫 message에서 만든다(제목)
agent_release_id이 세션을 에이전트 release에 묶는다(세션에 묶기). 이때 profile_id는 빼면 release의 값이다. repository_id를 빼면 GitHub 설정이 있는 에이전트는 저장소 없이 시작하고, 설정이 없는 옛 에이전트는 release의 저장소로 시작한다

응답은 201이다. 접수만 된 것이고 결과는 turn 조회로 본다(상태와 수명).

Idempotency-Key

세션 API의 모든 POST에는 Idempotency-Key 헤더가 필요하다. 응답을 받지 못했으면 같은 key와 같은 본문으로 다시 보낸다. 서버는 명령을 한 번만 실행한다. 세션 생성과 메시지는 처음 응답을 그대로 돌려주고, interrupt 같은 제어는 그 receipt의 지금 상태를 돌려준다. 같은 key에 다른 본문이면 409 IDEMPOTENCY_CONFLICT다.

SDK는 호출마다 key를 하나 만들고, 그 호출의 재시도에 같은 key를 보낸다. 프로세스를 다시 띄운 뒤에도 같은 명령으로 이어 가려면 key를 직접 만들어 idempotencyKey로 넘긴다.

const params = {
  profile_id: "claude-coding-local",
  repository_id: "sample-app",
  message: 'GATE-SPEC {"id":"retry","steps":[],"final":"once"}',
};
const idempotencyKey = crypto.randomUUID();
const first = await werft.sessions.create(params, { idempotencyKey });
const second = await werft.sessions.create(params, { idempotencyKey });
console.log(first.session_id === second.session_id ? "same session" : "two sessions");
same session

새 의도에는 새 key를 쓴다. timeout 뒤 새 key로 보내면 세션이 둘 생길 수 있다.

목록 보기

const page = await werft.sessions.list({ limit: 5 });
for (const session of page.items) {
  console.log(session.id, session.title, session.status);
}

// for await walks every page.
for await (const session of werft.sessions.list({ status: "idle" })) {
  console.log(session.id);
}

await하면 첫 페이지(items, next_cursor, nextPage())를, for await하면 모든 페이지의 항목을 준다.

limit, status로 거른다. key가 볼 수 있는 세션만 나온다.

relation은 볼 수 있는 세션 가운데서만 더 좁힌다. started는 내가 시작한 세션(key라면 그 key나 key를 만든 사람이 시작한 세션), shared는 남이 시작해 나를 지정하거나 내가 속한 팀을 지정해 공유한 세션, administered는 내가 관리하는(직접 또는 팀으로) 에이전트의 세션이다. 워크스페이스 전체에 보이는 남의 세션은 all(기본값)에만 나온다. q는 제목에 그 글자가 들어 있거나(대소문자 무시) id가 그 글자로 시작하는 세션을 찾는다. agent_id는 그 에이전트의 release로 만든 세션만 남긴다(어느 release든).

각 항목의 agent(id, name, release_version_no, release_label)는 세션이 묶인 에이전트와 release다. release_version_no는 그 release가 쓰는 카드 version이고, release_label은 release의 이름(만든 시각, 예: 2026-10-30_14:30:28)이다. 에이전트 release 없이 만든 세션은 null이다. created_by(user_id, display_name)는 세션을 시작한 사람이고, key로 만든 세션은 key를 만든 사람이다. 인계 뒤에도 바뀌지 않는다. 인계받은 사람은 그 세션을 읽을 수 있지만 started에는 시작한 사람에게만 나온다. channel은 세션을 만든 요청이 들어온 곳(web, api, slack)이다. 시작한 사람이나 경로를 기록에서 알 수 없는 예전 세션은 null이다. slack_permalink는 Slack에서 시작한 세션의 첫 메시지 링크다(원래 Slack 메시지 링크). 다른 경로로 만든 세션과 링크를 남기기 전 세션은 null이다.

상태별 개수는 SDK에 없어 HTTP API로 읽는다. GET /v1/sessions/counts는 같은 relation·q로 목록을 끝까지 읽었을 때 상태별로 몇 개인지와 total을 준다. 볼 수 없는 세션은 세지 않는다.

curl -sS "$API/v1/sessions/counts?relation=started" -H "Authorization: Bearer $KEY" | jq -c

레퍼런스: GET /v1/sessions/counts

목록과 상세의 repository는 세션을 만들 때의 저장소다. id는 카탈로그 키(repository_id와 같다), full_name은 화면에 보일 이름으로 저장소 URL에서 host·인증 정보·query·.git을 뺀 경로(owner/name, 그룹이 있으면 group/sub/name)이고 그렇게 읽히지 않는 URL이면 카탈로그 키다. branch는 worker가 clone한 브랜치이고, push가 쓰는 agent/<session-id>가 아니다. 세 값 모두 세션을 만들 때 적어 둔 것이라 운영자가 나중에 카탈로그를 바꾸거나 저장소를 빼도 바뀌지 않는다. 저장소 없이 만든 세션은 repository가 null이고, 카탈로그가 생기기 전에 만든 세션은 id가 null이다. 저장소 URL 자체는 어떤 응답에도 나오지 않는다.

const session = await werft.sessions.get(sessionId);
console.log(session.repository);
{ id: 'sample-app', full_name: 'agent/sample-app', branch: 'main' }

공유 범위

세션을 누가 볼 수 있는지는 공유 범위로 읽는다. 세션을 볼 수 있으면 누구나 읽는다. scope는 sessions:read다.

const share = await werft.sessions.share(sessionId);
console.log(share.scope, share.revision);
workspace 0

scope는 private(만든 사람만), people(user_ids의 사람), team(team_ids 팀의 지금 구성원), workspace(모든 구성원) 중 하나다. 한 번도 바꾸지 않은 세션은 workspace, revision 0이다. 에이전트의 세션은 워크스페이스 설정에 따라 workspace여도 에이전트 안쪽만 볼 수 있고, 그때 audience_limited가 true다(바깥 member가 할 수 있는 일). 콘솔의 세션 화면은 운영 칸의 공유 범위에 이 값을 읽기 전용으로 보이고, audience_limited이면 워크스페이스(담당 팀 밖 사람은 보지 못함, 워크스페이스 설정)이라 적는다. 범위를 바꾸는 POST /v1/sessions/{id}/share는 웹 로그인으로만 부르므로 SDK 메서드가 없다(setSessionShare).

제목

title을 주면 공백·줄바꿈 연속을 공백 하나로 줄이고 앞뒤를 잘라 저장한다. 그 결과가 비거나 80자(이모지·결합 문자 하나가 1자)를 넘으면 400이다. 빼면 첫 message에서 같은 규칙으로 만들고, 80자를 넘으면 앞 79자에 …를 붙인다. 모델은 부르지 않으므로 같은 메시지는 늘 같은 제목이다. 공백뿐인 메시지와 이 기능 전에 만든 세션은 title이 null이다.

이름 바꾸기는 SDK에 없어 HTTP API로 부른다.

curl -sS -X PATCH "$API/v1/sessions/$SID" -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' -d '{"title":"결제 테스트 고치기"}' | jq '{id, title}'

이름은 만든 사람(key가 만든 세션이면 그 key), 에이전트 관리자, owner·admin이 바꾼다. 공유로 보기만 하는 사람은 403, 볼 수 없는 사람은 404다. Idempotency-Key는 필요 없다. 바뀌면 SSE에 title을 실은 status 이벤트가 온다. 레퍼런스: PATCH /v1/sessions/{id}

오류

서버 오류는 SDK에서 APIError로 온다. isApiError(error, code)로 가르고, retryable이 true인 오류는 SDK가 스스로 다시 보낸다(재시도와 Idempotency-Key).

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

try {
  await werft.sessions.create({
    profile_id: "claude-coding-local",
    repository_id: "sample-app",
    model: "claude-opus-9",
    message: "hello",
  });
} catch (error) {
  if (isApiError(error, "UNSUPPORTED_CAPABILITY")) {
    console.log(error.status, error.code, error.retryable, error.message);
  } else throw error;
}
422 UNSUPPORTED_CAPABILITY false Model claude-opus-9 is not on the allowed list of profile claude-coding-local; name a model the profile offers
응답뜻
400본문이 schema와 다르거나 Idempotency-Key가 없다
401key가 없거나 틀렸거나 폐기됐다
403key에 sessions:write가 없다. 또는 에이전트 바깥의 사람(개인 key면 만든 사람)이고 워크스페이스의 agent_outsider_access가 use가 아니다(details.reason_code가 role)
409 IDEMPOTENCY_CONFLICT같은 key에 다른 본문
409 PROFILE_BLOCKEDowner가 운영자 프로필을 차단했다. 워크스페이스 프로필이면 그 바탕 프로필이 막혔다(details.reason_code가 base_blocked). 다시 보내도 같다
413메시지가 32,768바이트를 넘는다. 저장소 상한이면 STORAGE_LIMIT_EXCEEDED
403 GITHUB_REFUSED명시한 repository_id가 에이전트의 GitHub 설정 목록이나 워크스페이스 상한 밖이다(details.reason_code가 repository_not_allowed)
422 UNSUPPORTED_CAPABILITY프로필이나 저장소가 없거나, 그 짝이 허용되지 않았거나, 모델을 프로필이 허용하지 않는다
429 RATE_LIMITED잠시 뒤 다시 보낸다

실행 slot이 모자라면 거절하지 않는다. 세션이 queued로 기다린다.

SDK의 오류 클래스(APIError, ConnectionError, TimeoutError 등)와 재시도 횟수·timeout은 모든 호출에 같으므로 클라이언트 설정과 오류에 모았다.

다음 단계

  • 상태와 수명: 세션과 turn의 상태
  • 보내기: 같은 세션에 메시지 더 보내기
  • SDK 샘플: key와 카탈로그 확인, 세션 목록, 이벤트 이어 받기, 재시도, 질문·권한 요청 답변, 제어와 복구, push 승인과 PR을 유즈케이스별 SDK 샘플로

이 페이지