Code Mode: 코드 한 편이 여러 툴 라운드를 대체
모델이 작은 프로그램을 쓰고, run_code가 worker 샌드박스로 보내면, 툴 콜 다섯 번을 한 라운드로 끝냅니다. 이번 레슨은 그 설계 뒤의 아이디어 두 가지를 다룹니다.
먼저 이름을 분명히 할게요. 공식 발표는 PTC, 즉 프로그램형 도구 호출이라고 부르고, preset 메타데이터에도 name: PTC 模式라고 적혀 있어요(출처: apps/cli/config/agent-presets/code/preset.yml 1행). 소스에서 PTC를 찾아도 안 나와요. 내부 이름은 처음부터 끝까지 code mode예요: 설정 mode: code, 도구 이름 run_code. 이름 두 개, 같은 메커니즘.
네이티브 도구 호출은 모델에게 리모컨을 쥐여 주는 것과 같아요. 한 번 누르면 파일 하나 읽고, 결과가 오면 또 누릅니다. 누를 때마다 완전한 샘플링 라운드이고, 모델은 커진 컨텍스트를 다시 읽어야 다음을 정할 수 있어요. 로그 파일 3개를 읽고 보고서를 쓰면 이미 5라운드이고, 50개로 늘리면 예산과 인내가 함께 타 버립니다. 비용 병목은 도구 자체가 아니라, 한 스텝에 한 번 샘플링하는 리듬에 있어요.
한 샘플링 라운드에서 모델이 TypeScript 작은 프로그램을 바로 쓰고, 프로그램 안에서 await tools.name(args)를 원하는 만큼 호출해요. 루프와 분기도 괜찮아요. 프로그램이 샌드박스에서 일을 끝내면 중간 결과는 전부 샌드박스 변수에 남고, 끝난 뒤에는 print와 return 내용만 모델로 돌아갑니다. 도구 설명 원문은 “Only what you print or return comes back — curate it.”예요(출처: packages/core/tools/src/code-mode.ts 52행).
이 문장에는 출처가 있어요. preset 파일 머리의 설계 의도 주석 원문이 “five round trips becomes one”이고, 이번 레슨 제목도 여기서 나왔어요:
# The `code` agent preset: the standard coding agent, presented as Code Mode.
#
# Everything in `standard` is here unchanged. What is added is the `tool-presentation`
# row: instead of one tool call per action, the model writes a TypeScript
# program against a generated SDK and `run_code` executes it, so a sequence
# that would be five round trips becomes one.
deepseek-harness-master 기준, 확인 파일 apps/cli/config/agent-presets/code/agent.cordis.yml, 확인일 2026-08-13. 코드 블록은 소스 원문을 유지합니다.주석 후반에는 복선도 있어요. 이 preset이 바꾸는 건 도구 제시 방식뿐이고, 레지스트리 자체는 호스트에 남습니다(출처: 같은 파일 8–11행). 아이디어 2에서 말하는 권한 추종도 이 배치에서 출발해요.
이건 네트워크 프로그래밍 수십 년의 오래된 이치예요. 왕복은 비싸고, 배치는 싸요. DB에는 일괄 쓰기가 있고, RPC 프레임워크도 배치를 모읍니다. 모델 샘플링 한 라운드는 네트워크 왕복 한 번보다 훨씬 비싸서, N번 왕복을 한 번으로 합치면 이득은 더 커져요. 언젠가 DSH를 다른 언어로 다시 써도 이 장부는 그대로 성립합니다.
아이디어 1에는 풀리지 않은 전제가 있어요. 샌드박스에서 도는 건 모델이 방금 쓴 코드고, 한 줄도 심사받지 않았어요. 편하자고 호스트 프로세스에서 바로 돌리면 사고 방식이 널려 있어요. 환경 변수의 API key를 집어 가고, while (true) 하나가 메모리를 다 먹으면 호스트도 같이 죽고, 프로그램이 메시지를 위조해 도구 결과인 척 상위 계층을 속일 수도 있어요. 그래서 호스트는 첫날부터 상대가 나쁜 짓을 한다고 가정해야 하고, 그 가정이 서면 나머지는 엔지니어링 문제입니다.
DSH의 방식은 방어선 세 겹에, 권한 규칙 하나예요.
첫째, 자원을 용접해 닫아요. 실행마다 새 worker를 띄우고 쓰면 버립니다. 시작 인자로 길을 막아요. 환경 변수를 비워 프로그램이 호스트 자격증명을 못 갖게 하고, 상속 로더 플래그를 끊고, 힙 상한을 용접해 두면 프로그램이 힙을 터뜨려도 worker가 바로 종료합니다(출처: packages/code-runtime/code-runtime-worker-thread/src/index.ts 378–387행).
둘째, 통신은 구조화 메시지만 인정해요. 호스트와 worker 사이에는 메시지 포트 하나뿐이고 메모리는 공유하지 않아요. 업스트림 메시지는 call, log, output-limit, done 네 가지뿐이고, 인바운드마다 먼저 모양을 검사한 뒤 필드별로 깨끗한 사본을 다시 만들며, 쓰레기는 조용히 버립니다(출처: 같은 파일 142–165행). worker 쪽 tools 네임스페이스는 null-prototype으로 만들어, __proto__ 같은 위조 이름으로는 아무것도 만질 수 없어요(출처: bootstrap.ts 324–326행).
셋째, 시간과 바이트를 양쪽에서 기장하고, worker가 스스로 보고한 숫자는 믿지 않아요. 시간은 장부 두 권이에요. computeMs는 worker의 실측 바쁜 시간을 폴링해서 핫 루프를 숨기지 못하게 하고, 느린 도구를 기다리는 시간은 억울하게 과금하지 않아요. maxWallMs는 최후 보루로, 예산 두 개가 한도에 닿으면 바로 강제 종료합니다(출처: index.ts 534–545행). 바이트는 worker가 보내기 전 자체 사전 검사하고, 호스트는 받은 뒤 OutputLedger로 다시 적어요(출처: index.ts 169–229행). 듣기 좋은 숫자만 보고해서는 안 됩니다.
그다음이 권한 규칙이에요. 프로그램이 샌드박스에 들어가도 승인은 한 치도 풀리지 않아요. 매번 await tools.xxx는 호스트가 하위 디스패치로 감싸, 부모 호출의 token을 달고 네이티브 모드와 같은 pre-execute 승인 폭포를 탑니다. 하위 호출 id는 callId:code:n 형태고, 이벤트에 처음부터 끝까지 흔적이 남아요(출처: packages/core/tools/src/code-mode.ts 545, 477, 470행). 프로그램이 바인딩할 수 있는 도구는 System Prompt에 선언된 것들뿐이고, 제한된 도구는 명단에서 바로 사라집니다(출처: 같은 파일 601–608행). 반대로 mode: code에서 run_code를 우회해 네이티브 호출을 바로 보내면, 정책 파이프라인에 들어가기 전에 UNKNOWN_TOOL로 거부됩니다(출처: docs/subsystems/tools.zh.md). 입구는 좁혔지만, 권한의 문은 바꾸지 않았어요.
DSH 스스로도 이 격리의 위치를 냉정하게 잡아요. README는 바로 이렇게 시작합니다:
“이것은 격리 조치이지 보안 경계가 아닙니다: 신뢰 입장은 의도적으로 bash와 동등…하지만 bash에 없는 격리를 제공합니다: 독립 isolate, 빈 환경, 힙 상한, 강제 종료.”
출처: packages/code-runtime/code-runtime-worker-thread/README.zh.md, 줄임표는 원문 인용 생략불신 경계는 보안 설계의 보편 형태이고, worker_threads라는 구체 기술과 상관없어요. 컨테이너, V8 isolate, 다른 언어의 서브프로세스로 바꿔도 할 일은 이 네 가지예요. 깨끗한 환경을 새로 열고, 자원을 상한에 두고, 좁은 인터페이스로 통신하며 메시지마다 검증하고, 장부는 양쪽이 각자 적습니다. 브라우저가 웹페이지에, OS가 프로세스에 쓰는 것과 같은 생각이고, 모델 코드는 명단에 새로 온 한 명일 뿐 대우는 그대로예요.
Claude Code
등가물이 없어요. bash 도구는 범용 비상구라 모델이 스크립트를 써 실행할 수 있지만, Read·Edit 같은 도구 API는 스크립트에 프로그래밍 가능한 바인딩으로 노출되지 않고, 스크립트 안의 동작도 각 도구 자신의 파이프라인을 타지 않아요. Anthropic 공식 블로그 “Code execution with MCP”가 같은 아이디어를 냈지만, 확인일 기준 Claude Code 제품에 내장된 동급 run_code 메커니즘은 보이지 않았어요.
Grok Build
이 메커니즘은 없어요. 공개 증거 기준입니다. 로컬 grok-build-main 저장소 전체에서 run_code와 code mode를 검색하면, 텔레메트리 이벤트 이름이 글자만 겹칠 뿐이에요. 도구 체계는 네이티브 호출과 toolset preset 조합이고, 모델이 프로그램을 써서 샌드박스가 도구 API를 오케스트레이션하는 통로는 없습니다.
Codex CLI와 Cloudflare
아이디어는 통하지만, 이번 레슨은 두 곳 소스를 대조하지 않았고 사실만 말해요. DSH 설계 노트는 Cloudflare 블로그를 명시적으로 인용하며, 핵심 관찰은 모델이 코드를 쓰는 능력이 도구 호출을 연달아 보내는 것보다 낫다는 점입니다(출처: .agents/notes/implemented/feature/2026-06-15-code-mode.zh.md). Codex CLI 쪽에도 비슷한 공개 논의가 있지만, 역시 아이디어만 언급합니다.
악의적 프로그램 두 편, 각각 어느 예산에 부딪히나?
프로그램 A는 동기 핫 루프 while (true) {}이고, 프로그램 B는 영원히 resolve되지 않는 await new Promise(() => {})예요. 아이디어 2의 세 번째 방어선에 맞춰 추론해 보세요. A와 B는 각각 computeMs와 maxWallMs 중 무엇에 종료되나요? A가 미완료 도구 호출 하나를 걸어 과금을 피할 수 없는 이유는 뭔가요?