비동기 태스크(SEP-1686) — 오래 걸리는 작업을 지금 호출하고 나중에 가져오기
실제 에이전트(agent) 작업은 몇 분에서 몇 시간이 걸립니다. 지속적 통합(CI; Continuous Integration) 실행, 심층 리서치 종합(deep-research synthesis), 배치 내보내기(batch export)가 그렇습니다. 동기 도구 호출(synchronous tool call)은 연결이 끊기거나, 타임아웃되거나, UI를 막습니다. 2025-11-25에 병합된 SEP-1686은 태스크(Tasks) 프리미티브(primitive)를 추가합니다. 어떤 요청이든 태스크가 되도록 보강할 수 있고, 결과는 나중에 가져오거나 상태 알림(state notification)으로 스트리밍할 수 있습니다. 드리프트(drift) 위험 메모: Tasks는 2026년 상반기까지 실험적이며, SDK 표면(surface)은 아직 스펙(spec)을 중심으로 설계되는 중입니다.
유형: Build
언어: Python (표준 라이브러리, 비동기 태스크 상태 머신)
선수 학습: Phase 13 · 07 (MCP 서버), Phase 13 · 09 (전송 방식)
소요 시간: 약 75분
학습 목표
- 도구(tool)를 동기 실행에서 태스크 보강(task-augmented) 방식으로 승격(promote)해야 하는 시점을 식별합니다. 기준은 서버 측 작업이 30초를 넘는 경우입니다.
- 태스크 생명주기(lifecycle)를 따라갑니다.
working → input_required → completed / failed / cancelled입니다.
- 크래시(crash)가 진행 중인 작업을 잃어버리지 않도록 태스크 상태를 영속화(persist)합니다.
tasks/status를 폴링(polling)하고 tasks/result를 올바르게 가져옵니다.
문제
generate_report 도구가 몇 분짜리 추출 파이프라인(pipeline)을 실행합니다. 동기 모델에서의 선택지는 다음과 같습니다.
- 연결(connection)을 3분 동안 열어 둡니다. 원격 전송 방식(remote transport)은 연결을 끊고, 클라이언트는 타임아웃(timeout)되며, UI는 멈춥니다.
- 즉시 플레이스홀더(placeholder)를 반환하고 클라이언트가 사용자 정의 엔드포인트(endpoint)를 폴링하게 합니다. MCP의 일관성(uniformity)을 깨뜨립니다.
- 실행만 하고 잊어버립니다(fire-and-forget). 결과가 없습니다.
어느 것도 좋지 않습니다. SEP-1686은 네 번째 선택지를 추가합니다. 태스크 보강(task augmentation)입니다. 어떤 요청, 보통 tools/call이 태스크로 태그(tag)될 수 있습니다. 서버는 즉시 태스크 ID(task id)를 반환합니다. 클라이언트는 tasks/status를 폴링하고, 끝나면 tasks/result를 가져옵니다. 서버 측 상태는 재시작 후에도 살아남습니다.
개념
태스크 보강(task augmentation)
요청은 params._meta.task.required: true를 설정하면 태스크가 됩니다. 또는 optional: true를 설정하고 서버가 결정하게 할 수 있습니다. 서버는 즉시 다음을 응답합니다.
{
"jsonrpc": "2.0", "id": 1,
"result": {
"_meta": {
"task": {
"id": "tsk_9f7b...",
"state": "working",
"ttl": 900000
}
}
}
}
ttl은 서버가 상태를 보관하겠다고 약속하는 시간입니다. ttl 이후 태스크 결과는 폐기됩니다.
도구 어노테이션(tool annotations)은 태스크 지원을 선언할 수 있습니다.
taskSupport: "forbidden" — 이 도구는 항상 동기적으로 실행됩니다. 빠른 도구에 안전합니다.
taskSupport: "optional" — 클라이언트가 태스크 보강을 요청할 수 있습니다.
taskSupport: "required" — 클라이언트는 반드시 태스크 보강을 사용해야 합니다.
generate_report 도구는 required일 것입니다. notes_search 도구는 forbidden일 것입니다.
상태(states)
working -> input_required -> working (elicitation을 통한 루프)
working -> completed
working -> failed
working -> cancelled
상태 머신(state machine)은 추가만 가능합니다(append-only). 한 번 completed, failed, cancelled가 되면 태스크는 종료 상태(terminal state)입니다.
메서드(methods)
tasks/status {taskId} — 현재 상태와 진행 힌트(progress hint)를 반환합니다.
tasks/result {taskId} — 완료되지 않았다면 막히거나 404를 반환합니다.
tasks/cancel {taskId} — 멱등적(idempotent)입니다. 종료 상태는 무시합니다.
tasks/list — 선택 사항입니다. 활성 태스크와 최근 완료 태스크를 열거합니다.
상태 변경 스트리밍(streaming state changes)
서버가 지원한다면 클라이언트는 상태 알림을 구독(subscribe)할 수 있습니다.
server -> notifications/tasks/updated {taskId, state, progress?}
폴링 대신 스트리밍하는 클라이언트는 더 나은 사용자 경험(UX; User Experience)을 제공합니다. 폴링은 최소 인터페이스(minimal surface)로 항상 지원됩니다.
영속 상태(durable state)
스펙은 태스크 지원을 선언한 서버가 상태를 영속화하도록 요구합니다. 크래시가 ttl 안의 완료 결과를 잃어버리면 안 됩니다. 저장소(store)는 SQLite, Redis, 파일시스템(filesystem) 등 다양할 수 있습니다. Lesson 13 하네스(harness)는 파일시스템을 사용합니다.
취소 의미론(cancellation semantics)
tasks/cancel은 멱등적입니다. 태스크가 실행 중이면 서버는 중지를 시도합니다. 실행기(executor)가 협력적 취소(executor-cooperative cancellation)를 확인해야 합니다. 이미 종료 상태라면 요청은 아무 일도 하지 않습니다(no-op).
크래시 복구(crash recovery)
서버 프로세스(process)가 재시작되면 다음을 수행합니다.
- 영속화된 모든 태스크 상태를 로드(load)합니다.
- 프로세스가 죽어 있던
working 태스크를 CRASH_RECOVERY 오류와 함께 failed로 표시합니다.
completed / failed / cancelled 상태는 ttl 동안 보존합니다.
비동기 태스크와 샘플링(sampling)
태스크 자체가 sampling/createMessage를 호출(call)할 수 있습니다. 오래 걸리는 리서치(research) 태스크는 이런 방식으로 동작합니다. 서버의 태스크 스레드(thread)는 필요할 때 클라이언트의 모델에 샘플링을 요청하고, 클라이언트 UI는 주기적인 진행 업데이트(progress update)와 함께 태스크를 working으로 표시합니다.
이것이 실험적(experimental)인 이유
SEP-1686은 2025-11-25에 배포(ship)되었지만, 더 넓은 로드맵(roadmap)은 세 가지 열린 이슈를 짚습니다. 영속 구독 프리미티브(durable subscription primitives), 서브태스크(subtasks; parent-child task relationships), 결과 TTL 표준화(result-TTL standardization)입니다. 2026년 동안 스펙이 진화할 것으로 예상해야 합니다. 프로덕션(production) 코드는 일반 사례에 대해서만 Tasks를 안정적이라고 보고, 서브태스크에 대한 향후 SDK 변경에 대비해야 합니다.
사용해 보기
code/main.py는 영속 태스크 저장소(filesystem-backed)와 백그라운드 스레드(background thread)에서 실행되는 generate_report 도구를 구현합니다. 클라이언트는 도구를 호출하고 즉시 태스크 ID를 받습니다. 워커(worker)가 진행률(progress)을 업데이트하는 동안 tasks/status를 폴링하고, 완료되면 tasks/result를 가져옵니다. 취소가 동작하며, 워커 스레드를 죽이고(kill) 상태를 다시 로드하는 방식으로 크래시 복구를 시뮬레이션합니다.
살펴볼 지점은 다음과 같습니다.
- 태스크 상태 JSON은
/tmp/lesson-13-tasks/<id>.json에 영속화됩니다.
- 워커 스레드는
progress 필드를 업데이트합니다. 폴링하면 값이 증가하는 것을 볼 수 있습니다.
- 클라이언트 측 취소는 이벤트(event)를 설정합니다. 워커는 이를 확인하고 일찍 종료합니다.
- "크래시" 이후 상태를 다시 로드하면 진행 중이던 태스크가
CRASH_RECOVERY와 함께 failed로 표시됩니다.
산출물 만들기
이 lesson은 outputs/skill-task-store-designer.md를 만듭니다. 리서치(research), 빌드(build), 내보내기(export)처럼 오래 걸리는 도구가 주어지면, 이 스킬(skill)은 태스크 저장소의 상태 형태(state shape), ttl, 영속성(durability), 적절한 taskSupport 플래그(flag)를 설계하고 진행 알림(progress notification)을 스케치(sketch)합니다.
연습문제
-
code/main.py를 실행합니다. generate_report 태스크를 시작하고, 상태를 폴링한 뒤, 결과를 가져옵니다.
-
실행 중간에 tasks/cancel 호출을 추가합니다. 워커가 이를 존중하고 상태가 cancelled가 되는지 확인합니다.
-
크래시 복구를 시뮬레이션합니다. 워커 스레드를 죽이고 로더(loader)를 재시작한 뒤, CRASH_RECOVERY 실패 모드를 관찰합니다.
-
저장소를 SQLite로 확장합니다. 영속성의 장점은 동일하지만, 쿼리 옵션(query option)이 열립니다. 예를 들어 세션 X의 모든 태스크를 나열할 수 있습니다.
-
2026년 MCP 로드맵 글을 읽습니다. 다음 해 SDK API 설계에 가장 큰 영향을 줄 가능성이 있는 Tasks 관련 열린 이슈 하나를 찾습니다.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 태스크(Task) | "오래 걸리는 도구 호출" | 비동기 실행을 위해 _meta.task로 보강된 요청 |
| SEP-1686 | "Tasks 스펙" | 2025-11-25에 Tasks를 추가한 Spec Evolution Proposal |
_meta.task | "태스크 봉투" | ID, 상태, ttl을 담은 요청별 메타데이터 |
| taskSupport | "도구 플래그" | 도구별 forbidden / optional / required |
tasks/status | "폴링 메서드" | 현재 상태와 선택적 진행 힌트를 가져옴 |
tasks/result | "결과 가져오기" | 완료된 payload를 반환하거나 아직 완료 전이면 404 |
tasks/cancel | "멈추기" | 멱등적 취소 요청 |
| ttl | "보관 예산" | 서버가 태스크 상태를 보관하겠다고 약속하는 밀리초 |
notifications/tasks/updated | "상태 푸시" | 서버가 시작하는 상태 변경 이벤트 |
| 영속 저장소(Durable store) | "크래시 안전 상태" | 파일시스템 / SQLite / Redis 영속 계층 |
더 읽을거리