workflow / schedule / plan / todo: 오케스트레이션 원어의 취사선택
네 오케스트레이션 원어가 각자 무엇을 담당하는지, 왜 하나로 합치지 않았는지. 핵심 문서: docs/subsystems/workflow.zh.md 등 네 편.
실제 시나리오 네 개, 장면마다 원어를 하나 고르세요. 틀려도 괜찮아요—판정이 이유를 설명해 줘요. “재생”을 누르면 자동 해설을 보고, 카드를 직접 눌러 언제든 답할 수 있어요.
docs/subsystems/workflow.zh.md, schedule.zh.md, plan.zh.md, packages/todo/tool-todo/README.zh.md에서 나왔고, 확인일 2026-08-13.결론부터: 이 네 원어는 태스크 엔진을 공유하지 않고, 지속화 형태조차 달라요. workflow는 일회성이에요. 모델이 JS 스크립트를 쓰면 엔진이 node:worker_threads의 vm에서 실행하고, 스크립트 안 agent()가 호스트로 돌아가 서브 Agent를 띄워요. 끝나면 결과와 표시 기록만 남고, 로직 자체는 지속 상태 기계가 되지 않아요. schedule은 지속형이에요. create·dispatch·delete가 모두 schedule/change 세션 이벤트라, 로그를 재생하면 알림 상태를 전부 복구할 수 있어요. plan은 더 가벼워요—그냥 plan/mode 불리언 이벤트의 로그 폴드예요. todo는 스냅샷이에요. 매번 todo_write가 표 전체를 바꾸고, UI는 최신본을 투영해 그려요.
아웃라인의 질문 “다단계 오케스트레이션은 모델이 스크립트를 써야 하나, 프레임워크 상태 기계여야 하나”에 DSH의 답은 둘 다, 다만 역할이 분명하다는 거예요. 실행 오케스트레이션은 모델 스크립트에 맡기세요—로직이 워낙 다양해서 프레임워크가 미리 다 정의할 수 없어요. 시간·자세·표시는 프레임워크 상태 기계에 맡기세요—턴을 넘기고 재시작까지 이어지는 결정성이 필요하고, 모델 스크립트로는 못 줘요. workflow 문서도 자신의 meta 필드 어휘가 Claude Code의 dynamic workflows와 맞다고 말해요(workflow.zh.md 제 41, 49행). 뿌리는 같고 착지는 달라요.
놓치기 쉬운 규율이 하나 더 있어요. workflow 스크립트에서 agent() 옵션을 하나 잘못 쓰면 fatal: true인 WorkflowError가 나고, parallel() 조합기는 그걸 그대로 다시 던져 스크립트 전체를 끝냅니다. 서브 Agent의 실제 실행 실패만 항목별 null로 매핑돼요(workflow.zh.md 제 116행). 잘못된 코드와 실행 실패는 다른 오류 유형이에요—섞이면 스크립트를 디버그할 수 없어요.
plan은 권한이 아니에요plan mode는 소프트 지침이에요. 활성일 때 시스템 프롬프트에 plan:policy 한 단락을 넣고, 도구 목록은 그대로예요(요청 캐시 안정용). 쓰기를 실제로 막는 건 샌드박스와 승인이에요—둘 다 plan 상태를 읽지 않으니 따로 설정해야 해요.
schedule은 세션을 나가지 않아요알림은 원래 세션의 followup 턴으로만 돌아와요. 푸시도, 외부 알림 채널도 없고, 차가운 세션은 일하지 않아요. 전달 의미는 최소 한 번: 수락 후 dispatch 전에 크래시하면, 복구 때 알림이 한 번 더 가요.
todo는 실행을 구동하지 않아요todo_write는 순수 표시 상태예요. 전체 교체, 로그 기록, UI 투영. 부분 갱신도, 회수 도구도, 안정 id도 없어요. 이걸 태스크 엔진으로 쓰는 게 이 원어에 대한 가장 흔한 오독이에요.
먼저 schedule의 고정 주기 결정—이 강의에서 통째로 볼 만한 유일한 코드예요. 세션이 오프라인이라 만료 시각 N개를 놓쳤다면, 복구 후 하나씩 보완하지 않고 나눗셈 한 번으로 가장 최근 만료를 구한 뒤 기록을 미래로 밀어요. 열거·재생·적체 없음:
const steps = Math.floor((acceptedAt - target) / interval)
const occurrence = target + steps * interval
/* v8 ignore next -- bounded operands and a quotient-derived product stay safe. */
if (!Number.isSafeInteger(occurrence) || occurrence < target || occurrence > acceptedAt) {
throw new ScheduleLogError('every occurrence arithmetic must stay within the accepted interval')
}
const occurrenceAt = new Date(occurrence).toISOString()
const next = occurrence + interval
packages/schedule/schedule/src/domain.ts, 확인일 2026-08-13. 코드 블록은 소스 원문을 유지합니다.두 번째 경계는 plan mode가 언제 먹히는지예요—글로 설명할게요. 모델이 스트리밍 중일 때 사용자가 전환을 누르면, 플러그인은 바로 로그를 쓰지 않고 프로세스 메모리의 pending에 걸어 둔 뒤, 다음 턴 내 pre-step 경계에서야 손대요. 순서가 까다로워요. 리스너가 먼저 await next()로 하류가 이 스텝을 받을지 묻고, 하류가 거절했거나·시그널이 이미 취소됐거나·pending이 없으면 그대로 통과해요. 세 관문을 다 지나야 선택을 로그에 추가해요. 추가가 실패하면 warn만 남기고 이 스텝은 통과시켜요—자세 전환 한 번 실패했다고 턴 전체를 막지 않아요. 크래시 의미도 여기서 나와요. pending은 프로세스 메모리에만 살아요. 전환이 로그에 닿기 전에 죽으면, 재시작 후 plan mode는 전환 전 상태를 유지해요.
출처: packages/plan/plan-mode/src/index.ts 제 205–218행의 agent/pre-step 리스너, 확인일 2026-08-13.
todo에서 가장 태도 있는 설계는 코드를 붙일 필요가 없어요. allowParallelInProgress는 필수 설정이고, 스키마에 z.boolean().required()로 쓰여 기본값이 없어요(packages/todo/tool-todo/src/index.ts 제 41–43행). 여러 작업이 동시에 진행 중일 수 있는지는 이 배포가 동시 서브 Agent를 도는지에 달렸고, 도구 스스로는 관측할 수 없으니 배포 쪽에 표명을 강제해요. false로 두면 모델이 진행 중을 하나 더 찍는 순간 Error: invalid todos: at most one task may be in_progress를 먹어요(제 107–109행).
Claude Code는 집약 노선이에요. 일곱 종류의 비동기 작업(셸 명령, 로컬 서브 Agent, 원격 Agent, Teammate, 워크플로, MCP 모니터, 메모리 통합)을 하나의 Task 프레임 아래 걸고 registerTask·updateTaskState·kill를 한 수명주기로 공유해요(원고 study/chapters/06-task-system.md 제 27–47행이 tasks/types.ts를 인용). DSH는 반대예요. subagent 문서가 이어가기 경로는 “Task를 만들지 않고, 중간 결과를 담는 래퍼 층도 만들지 않는다”고 명시하고, 네 오케스트레이션 원어는 저마다 지속화 형태가 달라요. 집약은 통일된 진행 UI와 관리 입구를 사고, 분리는 각 원어가 자기 의미를 끝까지 말할 공간을 사요—schedule의 놓침 합치기, plan의 pending 전환처럼, 통합 프레임에 넣으면 타협이 필요해요.
작은 todo 도구에서의 갈라짐이 두 집의 성격을 가장 잘 보여 줘요. Claude Code의 TodoWrite는 프롬프트에 규율을 하드코딩해요. “Exactly ONE task must be in_progress at any time (not less, not more)”. 항목은 content에 activeForm 이중 형태까지 요구하고, 실행 중에는 진행형 문장을 보여 줘요(원고 study/chapters/14-all-prompts.md 제 1243–1293행이 TodoWriteTool/prompt.ts 원문을 인용). DSH는 같은 규율을 필수 배포 설정으로 만들어요. 동시 서브 Agent 조합은 true, 단일 스레드 규율은 false. false면 프롬프트 권고가 아니라 코드가 거절하고, 항목 모양은 content와 세 상태 status만으로 최소예요. 하나는 프롬프트로 모델을, 하나는 스키마로 배포를 묶은 뒤 코드가 집행해요.
두 경계를 추측해 보세요
첫째: 모델이 긴 방안을 스트리밍 중인데 사용자가 지금 “plan mode 들어가기”를 눌렀어요. 이 선택은 언제 로그에 쓰이고, 언제부터 모델 요청에 영향을 주나요? 이 턴이 끝나기 전에 프로세스가 죽으면, 재시작 후 plan mode는 켜져 있나요 꺼져 있나요? (힌트: pending은 프로세스 메모리에만 있어요.) 둘째: every_seconds: 3600 알림이 있고 세션이 5시간 오프라인 뒤 복구되면, 복구 순간에 알림이 몇 번 가고 다음 목표는 어디인가요? 이 강의 첫 소스의 steps 식으로 손으로 밀어 보세요.