DeepSeek Harness · 오케스트레이션과 서브 Agent

workflow / schedule / plan / todo: 오케스트레이션 원어의 취사선택

네 오케스트레이션 원어가 각자 무엇을 담당하는지, 왜 하나로 합치지 않았는지. 핵심 문서: docs/subsystems/workflow.zh.md 등 네 편.

강의 목표이 강의를 마치면 세 가지를 말할 수 있어요. DSH가 다단계 오케스트레이션을 나눈 네 원어의 적용 경계—workflow는 실행, schedule은 시간, plan은 협업 자세, todo는 진행 표시; 모델이 스크립트를 쓰는 방식과 프레임워크 상태 기계가 네 원어에서 어떻게 역할을 나누는지; 그리고 이 넷을 왜 일부러 하나의 통합 태스크 시스템으로 합치지 않았는지.
인터랙티브 데모 · 원어 선택기

실제 시나리오 네 개, 장면마다 원어를 하나 고르세요. 틀려도 괜찮아요—판정이 이유를 설명해 줘요. “재생”을 누르면 자동 해설을 보고, 카드를 직접 눌러 언제든 답할 수 있어요.

시나리오 1 / 4
첫 시나리오를 기다리는 중이에요.
데모는 수업용 정리예요. 각 원어의 경계 사실은 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행). 잘못된 코드와 실행 실패는 다른 오류 유형이에요—섞이면 스크립트를 디버그할 수 없어요.

workflow · 실행 오케스트레이션 모델이 JS 스크립트 작성 · worker + vm 실행 agent()가 호스트로 돌아가 서브 Agent · 일회성, 상태 기계 없음 누가 핸들을 잡나: 모델 schedule · 시간 schedule/change 이벤트 지속화 · 이 세션 안에서만 전달 놓친 간격은 한 번으로 합침 · followup은 현재 턴을 끊지 않음 누가 핸들을 잡나: 프레임워크 상태 기계 plan · 협업 자세 plan/mode 로그 이벤트 폴드 · 활성 시 지침 문단 주입 소프트 지침, 하드 제한은 샌드박스·승인 · 종료는 사람 검토 누가 핸들을 잡나: 프레임워크 상태 기계 todo · 진행 표시 todo_write 전체 교체 · todo/write 이벤트 + 투영 렌더 사람용, 실행 비구동 · 단일 소유자, 서브 Agent는 공유 안 함 누가 핸들을 잡나: 프레임워크 상태 기계 네 원어 · 네 지속화 형태 · 모두 선택 기능, agent loop는 어느 것에도 의존하지 않음 하나로 합치면 네 수명주기가 상태를 억지로 공유해야 하고, 각자 무엇인지 말할 수 없게 돼요
수업용 구조도: 노드와 연결은 소스 관계를 설명하며, 내용은 수업에 맞게 정리했습니다.
plan은 권한이 아니에요

plan mode는 소프트 지침이에요. 활성일 때 시스템 프롬프트에 plan:policy 한 단락을 넣고, 도구 목록은 그대로예요(요청 캐시 안정용). 쓰기를 실제로 막는 건 샌드박스와 승인이에요—둘 다 plan 상태를 읽지 않으니 따로 설정해야 해요.

schedule은 세션을 나가지 않아요

알림은 원래 세션의 followup 턴으로만 돌아와요. 푸시도, 외부 알림 채널도 없고, 차가운 세션은 일하지 않아요. 전달 의미는 최소 한 번: 수락 후 dispatch 전에 크래시하면, 복구 때 알림이 한 번 더 가요.

todo는 실행을 구동하지 않아요

todo_write는 순수 표시 상태예요. 전체 교체, 로그 기록, UI 투영. 부분 갱신도, 회수 도구도, 안정 id도 없어요. 이걸 태스크 엔진으로 쓰는 게 이 원어에 대한 가장 흔한 오독이에요.

핵심 증거 · 놓침 합치기와 pending 전환

먼저 schedule의 고정 주기 결정—이 강의에서 통째로 볼 만한 유일한 코드예요. 세션이 오프라인이라 만료 시각 N개를 놓쳤다면, 복구 후 하나씩 보완하지 않고 나눗셈 한 번으로 가장 최근 만료를 구한 뒤 기록을 미래로 밀어요. 열거·재생·적체 없음:

packages/schedule/schedule/src/domain.ts발췌, 제 536–543행
  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
소스 스냅샷 안내: 로컬 저장소 deepseek-harness-master 기준, 확인 파일 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행).

가로 비교 · 통합 Task 프레임 vs 네 독립 원어

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만으로 최소예요. 하나는 프롬프트로 모델을, 하나는 스키마로 배포를 묶은 뒤 코드가 집행해요.

수업 실습
01

두 경계를 추측해 보세요

첫째: 모델이 긴 방안을 스트리밍 중인데 사용자가 지금 “plan mode 들어가기”를 눌렀어요. 이 선택은 언제 로그에 쓰이고, 언제부터 모델 요청에 영향을 주나요? 이 턴이 끝나기 전에 프로세스가 죽으면, 재시작 후 plan mode는 켜져 있나요 꺼져 있나요? (힌트: pending은 프로세스 메모리에만 있어요.) 둘째: every_seconds: 3600 알림이 있고 세션이 5시간 오프라인 뒤 복구되면, 복구 순간에 알림이 몇 번 가고 다음 목표는 어디인가요? 이 강의 첫 소스의 steps 식으로 손으로 밀어 보세요.

Takeaway: 실행 오케스트레이션은 모델 스크립트에, 시간·자세·표시는 프레임워크 상태 기계에. 네 원어, 네 지속화 형태—누가 누구를 사칭하지 않아요. 원어를 고를 때 먼저 물어보세요. 이 상태는 얼마나 오래 살아야 하나요? 한 번 run이면 workflow, 세션 재시작 뒤까지면 schedule과 plan, 사람 눈용만이면 todo.