기능세션

상태와 수명

세션의 status와 admission_state, turn의 상태, interrupt, attention

세션 상세(GET /v1/sessions/{id})에는 상태가 둘 있다. status는 지금 실행이 어떤지, admission_state는 세션이 새 입력을 받는지다. turn은 따로 상태를 갖고, 끝났는지는 turn 상태로만 판단한다. 시작에서 만든 세션(SDK는 created·sessionId, HTTP API는 SID)으로 본다.

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

await waitForTurn(werft, sessionId, created.turn_id);
const { status, admission_state, current_turn_id, revision, attention } =
  await werft.sessions.get(sessionId);
console.log({ status, admission_state, current_turn_id, revision, attention });
{
  status: 'idle',
  admission_state: 'active',
  current_turn_id: null,
  revision: 0,
  attention: null
}

revision은 pause·resume·close 같은 제어로 세션이 바뀔 때 오른다. turn이 끝나는 것으로는 오르지 않는다. 제어 요청에는 지금 값을 expected_revision으로 보낸다.

status

값뜻
queued입력이 있고 worker를 기다린다
runningworker가 turn을 돌고 있다
needs_input돌고 있는 turn이 답할 수 있는 권한 요청이나 질문을 기다린다
idle돌 turn이 없다. 다음 메시지를 받는다
stopped실행이 멈췄다(interrupt, terminate, pause, close 뒤)
failed실행이 실패했다. attention에 이유가 있다

admission_state

값뜻
active메시지와 제어를 받는다
pausing → pausedpause 중, 그리고 checkpoint를 남기고 worker가 내려간 상태. 메시지는 409 SESSION_PAUSED
resumingresume 중. 새 worker가 checkpoint에서 복원한다
stopping → stoppedterminate 중, 그리고 멈춘 상태
recovery_required결과를 알 수 없는 turn이 남아 사람이 복구 결정을 해야 한다(닫기)
closing → closedclose 중, 그리고 닫힌 상태. 되돌릴 수 없다

turn 상태

for await (const turn of werft.sessions.turns.list(sessionId)) {
  console.log(turn.turn_id, turn.status);
}

queued·running·needs_input은 진행 중이고, completed·failed·interrupted·cancelled·outcome_unknown은 끝난 상태다. completed는 엔진이 turn을 정상 종료했다는 뜻이지, 요청한 일을 다 했다는 뜻은 아니다. 일이 끝났는지는 이벤트의 assistant·tool_result를 읽고 판단한다. 이벤트의 result도 완료 판정이 아니다.

interrupt

돌고 있는 turn 하나만 멈춘다. worker는 남아 다음 메시지를 받는다. 아래 대본은 답을 5분 늦게 보내서, 그동안 turn이 running에 머문다. 이 지연은 개발용 서버의 fake 모델에서만 된다. 실제 모델에서는 오래 걸리는 일을 시킨 turn으로 해 본다.

const sent = await werft.sessions.messages.send(sessionId, {
  message:
    'GATE-SPEC {"id":"slow","steps":[],"final":"too late","finalDelayMs":300000}',
});
while ((await werft.sessions.turns.get(sessionId, sent.turn_id)).status !== "running") {
  await new Promise((resolve) => setTimeout(resolve, 1000));
}
const accepted = await werft.sessions.interrupt(sessionId, {
  target_turn_id: sent.turn_id,
});
console.log(accepted.receipt_status);
const turn = await waitForTurn(werft, sessionId, sent.turn_id);
console.log(`turn ${turn.turn_id}: ${turn.status}`);
accepted
turn 2: interrupted

아직 시작하지 않은 turn은 409 TURN_NOT_STARTED, 이미 끝난 turn은 아무것도 하지 않는 성공(no_op: true)이다. scope는 sessions:control이다.

attention

세션이 사람의 손을 기다릴 때 attention에 code와 이유가 붙는다. 평소에는 null이다.

code뜻
PAUSE_BLOCKED, CLOSE_BLOCKEDpause·close가 60초 넘게 drain하지 못했다. 이유는 long_turn, pending_request 등
BUDGET_EXCEEDED세션 비용이나 token 상한을 넘었다
TRANSCRIPT_LIMIT대화 기록이 상한에 다가갔거나 넘었다. 새 세션으로 옮길 때다
STARTUP_FAILED, RESTORE_FAILEDworker가 시작하거나 checkpoint를 복원하지 못한다
CATALOG_MISMATCH세션을 만든 뒤 운영자가 프로필이나 저장소 설정을 바꿨다
PROFILE_BLOCKEDowner가 세션의 프로필(reason_code: blocked)이나 그 바탕 프로필(base_blocked)을 차단했다. profile_id가 그 프로필이다. 풀릴 때까지 새 메시지는 409 PROFILE_BLOCKED로 거절된다
CONTEXT_GAP복구 결정으로 이전 대화의 일부가 빠졌다

사용량

const usage = await werft.sessions.usage(sessionId);
console.log(usage.cost.amount_usd, usage.cost.complete, usage.budget_exceeded);
0.000063 true false

비용은 청구액이 아니라 가격표로 계산한 추정치다. complete가 false면 합계가 아직 정해지지 않았다. 열린 turn이 있거나(open_turn_count), 끝난 turn 가운데 비용이 보고되지 않은 것이 있다(unreported_turn_count). 뒤의 경우는 모든 turn이 끝나도 false로 남는다.

기간 동안 내가 시작한 세션을 모두 더하려면 GET /v1/usage/summary를 쓴다. 이 합계와 GET /v1/limits?scope=me는 SDK 메서드가 없어 HTTP API로 읽는다. SDK의 limits.get()은 설치 전체 한도(GET /v1/limits)를 읽는데, 이것은 로그인한 workspace owner만 읽으므로 API key로 부르면 403이다. 레퍼런스: GET /v1/usage/summary, GET /v1/limits since는 날짜(그 날 00:00 UTC)나 offset이 붙은 시각이고, until을 빼면 지금까지다.

curl -sS "$API/v1/usage/summary?scope=me&since=2026-10-01" -H "Authorization: Bearer $KEY" \
  | jq '{session_count, cost_usd, cost_complete, token_count, run_seconds}'

기간 안에 만든 세션이 대상이고, cost_usd는 그 세션들 /usage의 cost.amount_usd 합이다. 그 가운데 하나라도 complete: false면 cost_complete도 false다. 개인 key는 만든 사람의 세션을 더한다. scope=installation(모든 owner의 세션)과 GET /v1/limits는 로그인한 workspace owner만 읽고, 다른 구성원과 모든 API key는 403이다. scope=agent&agent_id=…는 누가 시작했든 그 에이전트의 어느 release로든 시작한 세션을 더한다. workspace owner·admin과 그 에이전트의 관리자가 읽고, 그 밖의 구성원과 모든 API key는 403, 다른 workspace의 에이전트나 없는 에이전트는 누구에게나 404다. group_by와 함께 쓸 수 없다.

GET /v1/limits의 usage.outputs는 세션 성과물(out/ 아래 파일)이 object store에 차지하는 바이트다(used_bytes). 뒤 checkpoint가 앞 checkpoint의 성과물을 그대로 이어받으면 한 번만 세고, 너무 커서 올리지 않은 파일은 세지 않으며, checkpoint 정리가 객체를 지우면 그만큼 줄어든다. updated_at은 값이 마지막으로 바뀐 시각이고 바뀐 적 없으면 null이다. 이 값은 usage.storage(입력 메시지)와 따로이고 storage_limit_bytes 집행에 들어가지 않는다. 성과물을 기록하기 전에 만든 checkpoint가 남아 있으면 그 성과물은 빠지고 complete가 false다.

에이전트별·사용자별로 나눠 보려면 group_by=agent 또는 group_by=user를 더한다. 응답에 groups[{id, name, session_count, run_seconds, cost_usd, cost_complete}]가 붙고, 그룹들의 session_count·cost_usd·run_seconds를 더하면 위 합계와 같다. 에이전트가 없는 세션, 사람이 만들지 않은 key가 시작한 세션은 id가 null인 그룹이다. name은 에이전트 이름이나 사용자 표시 이름이고 이메일은 싣지 않는다. scope=me에서 호출자가 아닌 사용자의 name은 null이다. 프로필별로 보려면 group_by=profile을 쓴다. 세션의 현재 프로필(sessions.profile_id)로 묶으므로 중간에 프로필을 바꾼 세션은 비용 전체가 지금 프로필에 잡힌다. 프로필에는 표시 이름이 없어 id와 name 모두 프로필 id이고, 프로필 없는 세션은 id가 null인 그룹이다.

GET /v1/limits?scope=me는 로그인한 구성원 누구나 읽는다. 세션 단위 한도 네 개(queued_input_limit_per_session, max_turn_seconds, session_cost_limit_usd, session_token_limit)만 주고 usage는 없다. API key는 403이다.

curl -sS "$API/v1/usage/summary?scope=me&since=2026-10-01&group_by=agent" -H "Authorization: Bearer $KEY" \
  | jq '.groups[] | {name, session_count, cost_usd}'

다음 단계

이 페이지