exec와 wait: 다 못 돈 프로그램은 어떻게 끝나나
도구 호출을 스무 번 보내는 대신, 모델에게 JavaScript 한 편을 쓰게 하세요. 어디서 도는지, 무엇을 할 수 있는지, 열 초 안에 못 끝내면 어떻게 되는지 — 이번 레슨은 그 세 질문 뒤의 아이디어 두 가지입니다.
- 모델은 첫 줄에 pragma 한 줄을 써서, 이번 양보 시간을 선언할 수 있어요description.rs L22
- 실행 입구는 그 밀리초를 “닿으면 한 번 관찰” 모드로 바꿉니다service.rs L77
- 10초를 넘는 예산에는 유예 1초를 더 주고, 서버 상한에도 막혀요service.rs L198
- 타이머가 닿으면 모아 둔 출력을 한 묶음으로 넘기고, 버퍼는 동시에 비워요cell_actor/mod.rs L242
- 양보는 모델이 읽을 수 있는 문장으로 번역되고, cell 번호가 붙어요code_mode/mod.rs L283
- 모델은 번호를 들고 돌아와 이어 돌릴 수 있고, 예산을 고치거나 길이를 제한하거나 바로 멈출 수도 있어요wait_handler.rs L24
- 스크립트가 끝나면 결과를 돌려주고 이 cell을 닫아요runtime.rs L24
파일 다섯을 읽고 모으면, 일반 도구 호출은 왕복 다섯이 필요해요. 왕복마다 파일 전문이 컨텍스트에 들어가고, 다음 턴에 모델은 커진 이력을 다시 읽어요. 진짜 비싼 것은 왕복의 리듬이지, 도구 자체가 아닙니다.
그럼 모델에게 프로그램을 쓰게 하면 되지요. 문제는 그 프로그램을 한 줄도 심사한 사람이 없고, 모델이 지금 써서 바로 낸다는 점이에요. 파일을 읽고 네트워크를 칠 수 있는 런타임을 주면, 호스트의 능력을 통째로 내주는 셈입니다.
Codex는 모델에게 도구 둘, exec와 wait를 줘요. exec는 JavaScript 소스를 받아 새 V8 isolate에서 async module로 평가해요. 도구는 모두 전역 tools에 달리고, 이름은 적법한 JS 식별자로 정규화되어 await tools.exec_command(...)처럼 씁니다. 루프도, 분기도 되고, 중간 값은 변수에 남으며, 직접 넘긴 부분만 모델로 돌아와요.
핵심은 이 런타임이 아주 얇게 깎였다는 점이에요. 도구 설명서는 모델에게, 무엇이 없는지를 바로 말합니다.
- Runs raw JavaScript -- no Node, no file system, no network access, no console.
- Accepts raw JavaScript source text, not JSON, quoted strings, or markdown code fences.
- You may optionally start the tool input with a first-line pragma like `// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}`.
- `yield_time_ms` asks `exec` to yield early if the script is still running. Defaults to 10000 ms.
- `max_output_tokens` sets the token budget for direct `exec` results. Defaults to 10000 tokens.
- When the JS code is fully evaluated, the isolate's lifetime ends and unawaited promises are silently discarded.
openai/codex 기준, 확인 파일 codex-rs/code-mode-protocol/src/description.rs, commit 4f39251a01, 확인일 2026-08-22. 코드 블록은 소스 원문을 유지합니다. 이 텍스트 자체가 모델에게 보내는 도구 설명입니다.Node도, 파일 시스템도, 네트워크도, console조차 없어요. 잊은 게 아니라 일부러 안 준 거예요. 부작용을 내려면 길 하나, tools뿐이에요. 그 길에는 승인과 샌드박스가 그대로 있고, 띄울 창은 띄우고 막을 것은 막아요.
덤으로 심사할 면적이 줄어요. isolate가 파일을 직접 읽으면 이 층이 파일 권한을 또 만들어야 해요. 지금은 아무것도 못 하니, 권한은 아래 층에 두면 되고 코드를 두 번 쓰지 않아도 됩니다.
왕복은 비싸고 배치는 싸다 — 수십 년의 옛 장부예요. 데이터베이스에는 일괄 쓰기가 있고, RPC 프레임워크는 batch를 모아요. 모델 샘플링 한 라운드는 네트워크 왕복보다 훨씬 비싸니, N번을 한 번으로 합치면 이득은 더 커집니다.
빼기의 절반은 더 보편적이에요. 믿지 못할 코드에 가능한 한 작은 환경을 주어, 나쁜 짓을 하고 싶어도 인터페이스가 없게 하는 것 — 그게 보안 설계의 보편 모양이고, V8이라는 구체 기술과는 상관없어요. 언어를 바꾸든 샌드박스를 바꾸든 물을 질문은 같아요. 이 코드에 정말 필요한 능력은 몇 가지이며, 나머지는 하나도 안 줄 수 있는가.
프로그램이 3분 걸려요. 타임아웃은 얼마로 둘까요.
3분으로 두면 사용자는 3분 동안 움직임도 못 보고 멈출 곳도 없어요. 10초로 두면 긴 일은 끝나지 않고, 더 괴로운 건 앞 9초 성과가 함께 사라진다는 점이에요. 모델은 타임아웃 하나만 받고 처음부터 다시 해야 합니다. 양쪽 다 틀렸고, 문제는 “아직 안 끝남”을 실패로 본 데 있어요.
Codex는 도는 중인 스크립트를 정체 있는 것으로 만들어요. 이름이 cell. yield_time_ms가 닿아도 cell은 죽지 않고, 그동안 모은 출력을 한 묶음으로 넘긴 뒤 버퍼를 비우고 계속 돌아요. exec는 그때 한 문장을 돌려, 스크립트가 아직 돌고 번호가 얼마인지 말합니다.
번호를 받은 모델에게는 선택 셋이 있어요. wait으로 시간을 더 사거나, terminate: true로 멈추거나, 아니면 다른 일부터. wait는 지난 양보 이후의 새 출력만 돌려줘요. 넘길 때 버퍼를 비웠으니, 같은 내용이 컨텍스트를 두 번 차지하지 않습니다.
작은 디테일이 설계자가 무엇을 생각했는지 잘 보여 줘요. 양보 시간이 10초를 넘으면 Codex는 유예 1초를 더 준 뒤에야 정말 관찰합니다.출처:codex-rs/code-mode-runtime/src/service.rs 198–210행경계에 딱 맞춰 끝난 스크립트는, 몇 밀리초 차이로 왕복을 한 번 더 낭비하지 않아요.
비대칭 한 곳도 적어 둘 만해요. 모델에게 보내는 설명서에서 wait는 인자 넷이에요. cell 번호, 양보 시간, 반환 길이 상한, 종료 여부. 그런데 프로토콜 층 요청 구조체는 앞의 둘만 들고, 뒤의 둘은 핸들러 층에 머물러요. 종료는 다른 길을 타고, 길이 상한은 결과를 받은 뒤에야 자릅니다. 소스를 읽을 때 이 두 층은 한 층으로 섞이기 쉬워요.
“아직 안 끝남”을 일등 상태로 만드는 것은 긴 작업 인터페이스의 보편 모양이에요. HTTP는 202와 폴링, 작업 큐는 job id와 poll, 큰 파일 내보내기도 번호를 먼저 줘요. 공통점은 호출자에게 “계속 기다리기”와 “실패로 보기” 중 하나를 강요하지 않고, 다시 물어볼 손잡이를 준다는 점입니다.
Agent 위에 두면 이 손잡이는 한 층 더 가치가 있어요. 중간 출력을 본 모델은 마음을 바꿀 수 있고, 앞 네 스텝이 틀렸으면 바로 terminate해서 남은 8분을 함께 달리지 않아도 돼요. 제어권은 생각하는 쪽으로 돌아갑니다.
타임아웃: DeepSeek Harness는 죽이기를 고름
DSH의 run_code는 장부 두 권이에요. 한 권은 바쁜 시간으로, worker 이벤트 루프 이용률을 폴링해 핫 루프를 숨기지 못하게 하고, 느린 도구를 기다리는 시간은 억울하게 과금하지 않아요. 다른 한 권은 벽시계로, 닿으면 worker를 바로 종료해요. 기본값은 6만 밀리초와 60만 밀리초입니다.
대가는 분명해요. run_code 한 번은 예산 안에 끝나야 하고, 타임아웃은 실패이며, “같은 프로그램이 이어 돈다”는 일등 상태는 없어요. 대신 구현이 단순하고, 호스트가 살아있는 cell 더미를 유지하지 않아도 됩니다. Codex는 반대예요. 모델은 wait 프로토콜을 하나 더 배워야 하고, cell은 끝나거나 멈추거나 세션이 끝날 때까지 자원을 차지합니다.
상태: 일회용 세계인가, 남겨 두는 서랍인가
DSH 설계 노트는 아주 직설적이에요. 프로그램이 사는 세계는 worker와 함께 끝나고, 풀링도 실행 간 상태도 없어요. 다음 실행에 넘길 것은 도구 결과에 쓰거나 작업 공간 파일에 떨어뜨리세요. 좋은 점은 실행마다 깨끗한 새 세계이고, 문제가 나면 재현하기 쉽다는 것입니다.
Codex는 store와 load를 줘요. 같은 세션의 exec 여러 번은 데이터를 공유하고, 세션을 넘기면 서로 못 봐요. 오케스트레이션은 편하고, 대가는 정리 책임이 다시 자기에게 온다는 점이에요. 이 서랍은 항목별 크기 상한이 없고, JSON에 못 들어가는 값만 거절하며, TTL도 없어서 세션 전체가 끝나야 런타임과 함께 풀립니다.
양보 예산을 어떻게 잡아야 손해 보지 않나
프로그램이 40초 걸리고, 양보 예산 기본값은 10초예요. 추론해 보세요. 모델은 wait을 몇 번 보내야 하고, 매번 받는 것은 전체 출력인지 새로 생긴 구간인지, 기본값을 30초로 바꾸는 일이 왜 언제나 더 이득은 아닌지.
심화 한 질문: 35초에 프로그램이 아주 큰 객체를 store에 넣었고, 이어서 모델이 terminate를 결정하면, 그 객체는 언제 지워지고 크기는 누가 관리하나요.