Spill: 도구 출력이 너무 클 때
한도를 넘는 출력은 디스크에 보관하고, 모델에게는 회수 자격증명을 남깁니다. 핵심 소스:packages/spill/spill-policy/src/index.ts。
read 도구를 건너뛰는지; 저장 쓰기가 실패해도 결과가 왜 성공으로 남는지.
packages/spill/spill-policy/src/index.ts 190–209행에 대응합니다. 데모의 maxInlineBytes는 50 KB로 Agent Note 예시 배포와 같습니다.grep이 수만 줄을 맞히거나 web_fetch가 문서 한 페이지를 통째로 가져와 결과가 2MB입니다. 이 결과는 어디로 갈까요? 선택지는 세 가지뿐입니다.
선택지 1: 전부 컨텍스트에 쑤셔 넣기. 다음 모델 요청이 바로 가득 차서 지갑과 컨텍스트 윈도우가 함께 타격을 입어요. 선택지 2: 초과분을 자르기. 공간은 아끼지만, 나중에 모델이 필요한 게 잘린 그 에러 줄이면 작업이 멈춥니다. 선택지 3이 DSH 방식입니다: 전문을 디스크에 보관하고, 컨텍스트에는 앞뒤 미리보기와 경로·read/grep으로 되찾을 수 있다는 힌트만 남깁니다. 컨텍스트에서 나간 정보도 언제든 찾을 수 있어요 — 그게 Spill(오버플로 저장)입니다.
이 일을 하는 플러그인이 dsh-spill-policy입니다. 도구 실행 파이프라인의 tools/post-execute 이벤트에 걸려, 도구 결과가 완전히 확정된 뒤에야 움직입니다. 결정은 다섯 단계뿐이고, Agent Note 2026-07-08에 분명합니다: 위임, 순수 텍스트 검사, 바이트 임계값, saveText, 교체. 데모의 체크리스트가 그 다섯 단계를 그대로 옮긴 것입니다.
순수 텍스트만 처리결과에 비텍스트 블록(스크린샷 등)이 하나라도 섞이면 flattenPlainText가 undefined를 반환하고 결과 전체를 그대로 둡니다. 정책은 최종 포맷 텍스트만 알고 도구 내부 구조는 모르니, 차라리 건드리지 않습니다. 출처: index.ts 80–87행.
read는 건너뜀모델을 향한 쪽은 read 도구를 명시적으로 건너뛰어, read 출력이 spill되어 파일이 되고 모델이 다시 read·spill하는 무한 루프를 막습니다. 로그 쪽은 건너뛰지 않습니다 — 로그 사본은 모델 컨텍스트에 들어가지 않아 루프가 성립하지 않아요. 출처: 195–197행과 219–222행 주석.
자격증명은 경로가 아님locator는 불투명 핸들입니다: 로컬 백엔드는 파일 경로를, 원격 백엔드는 URI나 키를 줄 수 있어요. 소비자는 파싱하지 않고 백엔드가 붙인 retrievalHint로 회수 문구를 렌더하며, read가 항상 올바른 회수 방식이라고 가정하지 않습니다. 출처: docs/subsystems/spill.zh.md 70행.
정책 입구는 통과 판단의 연쇄입니다. 건드리지 않을 이유 네 가지를 순서대로 봅니다: 다운스트림 리스너가 이 결과를 받지 않음, 다른 플러그인이 이미 값을 바꿈, 중첩 서브콜이거나 read 도구, 내용에 비텍스트 블록이 섞임 — 하나라도 맞으면 그대로 통과. 모두 아니면 바이트를 재고, maxInlineBytes 이하면 또 통과. 전부 통과해야 spill로 갑니다.
놓치기 쉬운 순서가 있어요: 판단의 첫걸음은 await next() — 먼저 위임. 다운스트림 리스너(내용을 바꾼 hook 등)가 결과를 완전히 정산한 뒤에야 spill이 확정본을 건드립니다. 그래서 다른 플러그인이 내용을 바꿔도, 바뀐 내용 역시 spill의 관리 아래 있어요.
출처: packages/spill/spill-policy/src/index.ts 194–209행, 확인일 2026-08-13. read를 건너뛰는 이유는 소스 주석에 있습니다: read 출력이 spill되어 파일이 되고 모델이 다시 read·spill하는 루프를 피하려는 것.
개요에 있던 그 경계 조건: spill 저장 쓰기가 실패하면 이번 도구 호출은 성공일까요 실패일까요? 답은 catch 분기에 있어요 — 성공이고, 한 글자도 숨기지 않습니다.
let ref: SpillRef
try {
ref = await spillStore.saveText(save)
} catch (error: unknown) {
// Best-effort: a storage failure (permissions, ENOSPC, backend down) must
// never fail the call or hide the content — keep the original inline.
ctx.logger.warn(`spill-policy: saveText failed for ${toolName}: ${String(error)}; keeping the inline content`)
return undefined
}
packages/spill/spill-policy/src/index.ts, 확인일 2026-08-13. 코드 블록은 소스 원문을 유지합니다.디스크가 가득 차거나, 권한이 틀리거나, 백엔드가 마운트되지 않아도 warn 로그만 남기고 원본 결과는 컨텍스트에 인라인으로 남습니다. 설계 문서 원문: “spill 실패는 성공한 도구 호출을 isError 결과로 바꾸거나 인라인 결과를 숨기지 않는다”(Agent Note 2026-07-08 91행). 논리는 단순해요: spill은 비용을 아끼는 최적화이고, 실패해도 컨텍스트가 조금 커질 뿐 — 성공 호출을 실패로 만들거나 정보를 잃게 해서는 안 됩니다.
반대쪽 함정도 막혀 있어요: 설정 검증은 플러그인 로드 때이지 호출마다가 아닙니다. 음수나 소수 maxInlineBytes는 배포 시작을 바로 실패시킵니다(114–119행) — 잘못된 설정은 배포가 터져야지, 어떤 도구 호출이 책임을 져서는 안 됩니다.
교체 후 모델이 보는 텍스트는 세 부분입니다: 남긴 머리 미리보기, 생략 설명과 자격증명, 남긴 꼬리 미리보기. 자격증명 줄은 spillNotice가 만듭니다(104–108행). Agent Note 예시는 “(Omitted N bytes. Full formatted result stored at: /.../session-.../....txt. Use read with offset/limit, or grep this path to search within it.)”. 표현은 일부러 일반적입니다 — 정책은 최종 텍스트만 알고 도구 내부 자원은 모릅니다.
얼마나 빡센지 보여주는 디테일: 자격증명 자체의 바이트를 먼저 maxInlineBytes 예산에서 빼고 나서 미리보기 크기를 잡습니다(171–172행). 안 그러면 미리보기가 예산을 다 쓰고 자격증명을 붙였을 때 교체 텍스트가 한도를 넘을 수 있어요. 자격증명 한 줄이 한도 전체를 넘으면 정책은 spill을 포기하고 인라인을 유지합니다 — 스스로 선언한 한도를 절대 어기지 않아요(183–185행).
보관 파일 자체도 까다롭습니다. 로컬 백엔드는 <root>/session-<hash>/<random>-<safeName>에 쓰고, 루트는 비공개(0700), 쓰기는 open(path, 'wx', 0o600)으로 배타적이며 소유자만 읽을 수 있어 미리 심어 둔 심볼릭 링크로 쓰기를 돌릴 수 없습니다(docs/subsystems/spill.zh.md 85행). 같은 족의 설계로 첨부 시스템이 있어요: 로그에는 참조, 바이트는 외부 store — 본문에는 가벼운 참조만 남깁니다(docs/subsystems/attachment.zh.md).
Claude Code · 한도 + 디스크 저장
공식 블로그 원문: “For Claude Code, we restrict tool responses to 25,000 tokens by default”; 소스는 Tool.ts의 maxResultSizeChars(study/chapters/02-tool-system.md 664–666행). 670행은 한도를 넘는 도구 결과를 디스크에 둔 뒤 경로 설명을 붙인다고 하며 DSH와 방향이 같습니다. 블로그는 잘라낼 때 Agent에게 왜 잘렸는지, 전체 내용을 어떻게 얻는지 알려야 한다는 원칙도 덧붙입니다.
Grok Build · bash 전용 디스크 저장
도구 출력 기본 한도는 20,000바이트(DEFAULT_TOOL_OUTPUT_CHARS, crates/codegen/xai-grok-tools/src/lib.rs 11행)이고, 초과 시 잘라내며 truncated 플래그를 돌려줍니다. bash만 예외: 전체 출력을 먼저 세션 디렉터리의 terminal log에 씁니다(bash/mod.rs 379–381행). 잘린 부분은 파일에서 되찾을 수 있어요. 이 저장은 bash 전용이라, 다른 도구는 잘리면 그냥 사라진 겁니다.
비교의 초점은 범용성입니다. 세 곳 모두 대용량 출력을 모델에 다 넣을 수 없다고 인정하고, 차이는 버린 부분을 되찾을 수 있는지, 그 능력이 몇 도구를 덮는지예요. Grok은 bash만 디스크에 두고, Claude Code와 DSH는 범용 메커니즘으로 만들었습니다. DSH 쪽이 가장 잘게 나뉩니다: 미리보기는 output-retention 라이브러리, 저장은 spillStore seam(메서드 하나짜리 추상 서비스), 정책 플러그인은 언제 spill할지·자격증명을 어떻게 붙일지만 결정. 패키지 세 개가 구간을 나눠 가져, 원격 저장 백엔드를 바꿔도 정책 코드 한 줄 안 건드려도 됩니다. Agent Note 대안 절은 참조 대상까지 찍어 둡니다: 범용 기본 동작으로, “Claude Code식 범용 도구 결과 영속화”를 겨냥한다(187행).
출력 세 개의 운명을 손으로 추적
배포 설정 maxInlineBytes: 50000. 도구 결과 세 개가 차례로 들어옵니다: 60,000바이트 web_fetch 순수 텍스트; image 블록이 섞인 200,000바이트 브라우저 스크린샷 결과; spill 때 디스크가 가득 찬 80,000바이트 순수 텍스트. 194–203행 게이트 로직과 153–161행 catch 분기를 대조해, 각 결과가 모델 컨텍스트에 들어가는 최종 형태와 보관함에 파일이 몇 개 늘었는지 쓰세요.