글 목록으로
2026년 9월 13일
6분 소요

DeepSeek Harness 코드 분석: 에이전트 루프를 플러그인 경계로 나눈 구조

DeepSeek Harness는 모델·도구·세션뿐 아니라 에이전트가 한 바퀴 도는 루프도 플러그인이다. Cordis가 서비스를 ctx 키로 붙이고, 기본 드라이버는 ctx.agentLoop다. 2026년 9월 10일 커밋을 기준으로 무엇이 커널이고 무엇을 설정으로 바꾸는지 나눈다.

코딩 에이전트를 조금 고치려다 실행기 전체를 포크하게 되는 경우가 많다. 샌드박스만 바꾸고 싶은데 루프와 도구 호출이 한 파일에 붙어 있고, 세션 로그 형식을 바꾸려면 모델 호출부까지 손댄다.

DeepSeek Harness(dsh)는 그 부품을 플러그인으로 나눈 오픈소스 실행기다. 슬로건은 “Everything is a Plugin”이다. 모델 어댑터, 도구, 세션, 샌드박스, 스케줄, UI뿐 아니라 기본 에이전트 루프도 dsh-agent-loop 패키지다. MIT 라이선스고, 문서는 developer preview라 호환이 깨지는 변경이 있다고 못 박는다.

이 글은 2026년 9월 10일 c291e79(0.1.5 동기화) 기준으로 docs/architecture.mdpackages/core를 읽은 분석이다. npx @deepseek-ai/dsh web으로 돌려 본 사용 기록은 아니다. 하네스 엔지니어링에서 말한 “모델 바깥을 설계한다”를, 루프 자체를 교체 단위로 만든 구현에서 확인한다. 같은 날 OpenViking은 기억 위치를 경로로 다루는 쪽이다.

슬로건과 커널을 나누어 본다

Cordis는 플러그인을 올리고, 의존을 맞추고, 효과를 되돌리는 프레임워크다. 플러그인은 Serviceapply(ctx)에서 올라가고, 다른 코드는 구현 클래스가 아니라 ctx.tools·ctx.llm·ctx.sessions 키로 찾는다. inject가 필요한 서비스를 기다리므로 부팅 순서를 손으로 짜지 않는다. 통신은 emit·waterfall 같은 타입 있는 이벤트고, ctx.effect()/ctx.on()으로 붙인 등록은 플러그인을 내리면 풀린다.

그래서 “특권 코어를 패치하지 않는다”는 말은, 실행기 기능을 소스 트리에서 고치지 말고 옆에 플러그인을 얹으라는 뜻에 가깝다. Cordis 로더와 컨텍스트 자체는 그 아래에 있다. 슬로건을 글자 그대로 읽으면 커널이 없는 것처럼 보이지만, 실제 교체 단위는 Cordis 위의 서비스 키와 프로필 패치다.

packages/core가 이 경계를 분명히 한다. ctx.agents는 에이전트 핸들과 레지스트리다. ctx.agentLoop는 기본 드라이버다. 확장 플러그인은 agent에 의존하고, 드라이버는 갈아끼울 수 있게 남겨 둔다.

루프가 하는 일과 바꾸면 안 되는 일

기본 구현은 ReactLoopAgent다. README는 표준 수명주기 — 모델을 호출하고, 도구를 실행하고, 반복 — 가 부족할 때만 커스텀 Agent를 쓰라고 한다.

한 step은 모델 요청 하나와 그것이 부른 도구다. 한 turn은 step이 0개 이상이다. 흐름은 대략 이렇다.

turn/start
  → agent/pre-step (입력 수락·거절)
  → step/start
  → 프롬프트·도구 스키마 조립
  → 모델 스트림
  → tools/pre-execute → execute → post-execute
  → step/end
turn/end

agent/pre-steptools/*는 waterfall이다. 리스너가 next()를 호출해야 다음으로 넘어가고, 호출하지 않으면 그 자리에서 끊는다. 정책 플러그인이 도구를 막을 때 루프 소스를 고치지 않아도 되는 지점이다.

세션 로그가 모델이 보는 역사의 원본이다. 아키텍처 문서의 문장 그대로, 모델에 보이는 것은 로그에 남아 있어야 한다. 새 입력을 모델에 보여 주려면 세션 이벤트 타입을 늘리고 로그에서 다시 그려야 한다. 런타임 메모리에만 있는 힌트는 재개·포크·텔레메트리에서 사라진다.

따라서 루프를 교체하는 이유는 “토큰을 아끼고 싶어서”가 되기 어렵다. 토큰 절감은 프롬프트 조립(ctx.systemPrompt)과 도구 결과 정리, 세션 프로젝션 쪽에 가깝다. 루프를 바꾸는 경우는 수명주기 자체가 다를 때다. 예: 모델 호출 없이 사람 명령만 처리하는 드라이버, 턴 경계를 다른 제품에 위임하는 드라이버. 기본 create()/resume()과 세션 로그 계약을 유지해야 ctx.agents를 쓰는 UI·SDK가 그대로 붙는다.

Tofu가 같은 모델에 하네스만 바꿔 점수를 올린 이야기라면, 여기는 그 하네스의 루프 모듈을 설정 행 하나로 빼는 구조다. Prime Agent처럼 루프가 스스로를 고치는 제품도 아니다. dsh는 교체 가능한 드라이버를 제공할 뿐, 자동으로 더 나은 루프를 탐색하지 않는다.

실행 세계를 같이 옮긴다

능력은 문서에서 말하는 능력 경계(seam)로 묶인다. 인터페이스 정의, 제공자, 그 제공자를 쓰는 도구가 한 세트다. 파일 시스템과 서브프로세스 제공자는 같은 실행 세계를 공유한다. 둘을 원격 샌드박스로 돌리면 Bash·PTY·LSP가 함께 움직이게 만드는 것이 설계 목표다. 샌드박스만 포크해서 루프에 특수 분기를 넣는 방식과 반대다.

OpenSandbox가 실행 환경 API를 맞추는 층이라면, dsh는 그 API를 ctx.fs / ctx.subprocess / ctx.sandbox 키 뒤에 두는 층이다. 로컬에서 쓰던 도구 스키마를 유지한 채 실행 위치만 바꾸는 것이 맞는지, 도입 실험에서 확인할 항목이다.

서브에이전트도 한 인터페이스 뒤에 여러 제공자를 둔다. 자식 에이전트를 새로 띄우는 구현과, 다른 제품의 턴에 일을 맡기는 구현을 같은 호출로 바꿀 수 있다고 적혀 있다. Agent Teams는 선택 기능이다.

설정으로 조립한다

실행 중인 dsh는 프로필이 고른 번들을 순서대로 쌓은 플러그인 트리다.

  • 프로필web, headless, sdk, sdk-minimal, acp 같은 이름. 번들 목록과 cordis.patch.yml을 가진다.
  • 번들 — 플러그인 행과 코드. dsh-base가 web/headless/sdk/acp의 공통 첫 층이다. 모델, 도구, 저장, 샌드박스, 승인, 자격 증명이 여기 들어간다.
  • 패치 — 행 id를 지정해 설정을 통째로 바꾼다. 번들 → 프로필 패치 → 홈 패치 → --patch 순이다.

dsh --profile web --dump-config이 실제 트리를 보여 준다. 출력된 행은 패치로 교체할 수 있다. 테스트 트리의 헤드리스 패치가 id: agent-loop만 집어 설정을 덮는 방식이다.

# 예: $DSH_HOME/profiles/web/cordis.patch.yml
# dump-config에 보이는 행 id를 지정하면 그 행의 설정이 통째로 바뀐다.
- id: agent-loop
  config:
    agents:
      - id: main
        provider: deepseek
        model: deepseek-chat

루프 구현 자체를 바꾸려면 같은 id에 다른 name을 넣는 식이다. 아키텍처 문서는 출력된 행을 패치로 교체할 수 있다고 적는다. 아래는 그 규칙을 따른 설계 예시다. 이 글에서 실행해 본 결과는 아니다.

- id: agent-loop
  name: './plugins/my-agent-loop'

커스텀 프로필은 기본적으로 패치를 다시 읽고, headless·sdk·acp는 시작 때 한 번만 적용한다. 한 번 일을 맡은 프로세스의 의존을 중간에 바꾸면 수명주기가 깨진다는 이유다.

sdk-minimaldsh-base를 쓰지 않는다. 작은 명시 트리를 통째로 가진다. SDK로 임베드할 때 실행기 전체를 끌고 오지 않으려는 예외다.

새 모델 제공자는 ctx.llm에 어댑터를 등록한다. 새 도구는 ctx.tools다. 루프를 만지지 않는 확장이 기본값이다.

기존 실행기에서 손볼 순서

이미 Claude Code, Codex, 자체 루프를 쓰는 경우의 실험 순서다. 검증된 마이그레이션이 아니라 제안이다.

  1. 기준선. 지금 루프·도구·샌드박스·세션 저장을 기록하고, 대표 과제 하나의 완료 기준을 정한다.
  2. 루프는 유지하고 능력 경계만. 파일·셸 실행만 다른 제공자로 바꿔 본다. 도구 이름과 완료 기준은 그대로다.
  3. 모델 어댑터. 같은 루프에 제공자만 바꾼다. 프롬프트 조립이 로그에서 재현되는지 확인한다.
  4. 루프 교체. 1~3으로 안 되는 수명주기일 때만 Agent 구현을 교체한다. create/resume와 세션 이벤트를 유지한다.

확인할 결과도 나눈다. 샌드박스 교체는 금지된 경로가 막히는지, 허용된 명령이 성공하는지다. 루프 교체는 재개·취소·도구 실패 후 다음 step이 로그와 일치하는지가 먼저다. 토큰과 지연은 그다음이다.

프리뷰 단계라 프로필 필드와 세션 포맷 버전이 바뀐다. 세션은 session.vN.jsonl 세대 파일로 남기고, 마이그레이션은 옆 파일로 쓴다. 장기 저장을 이 형식에 맡기려면 세대 정책을 문서와 같이 봐야 한다. 실행 전 SAFETY.md를 읽으라고 저장소가 요구한다.

이 블로그에서 어디에 두나

dsh는 DeepSeek 모델 전용 제품이 아니다. 기본 예시는 DeepSeek 모델을 쓰지만, 설계의 중심은 루프와 도구와 세션을 키와 패치로 나누는 일이다. OpenClaw·Hermes처럼 “에이전트 앱”을 비교한 이전 글과 겹치지 않게, 여기서는 실행기 내부의 교체 단위만 본다.

지금 쓸 곳은 샌드박스나 모델만 바꾸고 싶은데 루프 파일을 건드리고 있는 팀이다. 루프를 더 똑똑하게 만들고 싶다면 이 저장소보다 평가와 자기 수정 하네스 쪽이 가깝다. 부품을 나누는 일과 부품을 스스로 고치는 일은 같이 가지 않는다.