Tool Output Limits
Tool Output Limits
이 문서에서는 ToolOutputLimits capability를 소개해요. 컨텍스트 창을 지배할 만큼 큰 도구 반환을 줄여요. 도구 반환은 히스토리에 ToolReturnPart로 영속되므로, 지나치게 큰 반환은 이후 모든 모델 요청에 재전송되어 실행 내내 토큰 비용을 지불해요. 이 capability는 반환이 생성될 때 가로채 한 번 줄이고, 줄어든 형태가 영속되도록 하죠 — 감소는 요청마다 재계산되지 않아요.
출처: 문서
본문
ToolOutputLimits는 컨텍스트 창을 지배할 만큼 큰 도구 반환을 줄여요. 도구 반환은 ToolReturnPart로 히스토리에 영속되므로, 지나치게 큰 반환은 이후 모든 모델 요청에 재전송되어 실행 내내 토큰 비용을 지불해요. 이 capability는 반환이 생성될 때 가로채 한 번 줄이고, 줄어든 형태가 영속되도록 하죠 — 감소는 요청마다 재계산되지 않아요.
Pydantic AI Harness가 0.x 릴리스인 동안에는 minor 릴리스 사이에 API가 바뀔 수 있어요. 그럴 때는 deprecation 경고와 릴리스 노트 마이그레이션 안내가 (당신이나 당신의 에이전트에게) 정확히 업그레이드 방법을 알려줘요. 버전 정책을 참고하세요.
The problem
도구가 컨텍스트 창을 지배할 만큼 큰 페이로드를 반환할 수 있어요. 큰 파일 읽기, 장황한 로그, 큰 JSON 문서 같은 것이죠. 도구 반환은 히스토리에 영속되므로, 지나치게 큰 반환은 이후 모든 모델 요청에 재전송되어 실행 내내 토큰 비용을 지불해요.
이것은 compaction capability가 범위 밖으로 명명한 overflow-to-file 후속 작업이에요. 이 capability는 이미 창 안에 있는 컨텍스트를 압축하거나 버리는 대신, 큰 도구 출력을 생성 시점에 창 밖으로 옮겨요.
The three modes
Mode
Cost
Lossy?
What the model gets
Truncate
zero-LLM
yes
텍스트의 head / tail / head+tail 클램프
Spill
zero-LLM
no
핸들 + 미리보기 + 모양 스케치. 전체 페이로드는 요청 시 다시 읽음
Summarize
LLM 호출 한 번
yes
크기 제한 요약(기본적으로 실행의 모델 상속)
Spill은 무손실이에요. 전체 페이로드가 영속되고 모델은 등록된 read_tool_result(handle, offset, limit, from_end, pattern) 도구를 통해 그 조각을 읽어요(Claude Code 패턴, core #4352 설계). 그 도구는 제한적이에요. offset >= 0, limit는 내장 줄 상한으로 클램프되고, 결합된 출력이 상한되며, pattern은 리터럴 부분 문자열(정규식 아님)이라 모델이 공급한 값이 치명적인 역추적(backtracking)으로 호스트를 멈추게 하지 않아요.
Usage
ToolOutputLimits()를 capabilities에 넣어 Agent를 구성하세요. 인자 없이 기본 밴드를 사용해요. 10,000자 이상의 반환을 spill하며, 저장소가 쓰기를 받아들이지 못하면 제한된 truncation 폴백을 사용해요.
from pydantic_ai import Agent
from pydantic_ai_harness import ToolOutputLimits
agent = Agent('openai:gpt-4o', capabilities=[ToolOutputLimits()])
이 capability는 모델이 spill된 어떤 페이로드로도 페이지를 다시 넘길 수 있도록 단일 read_tool_result 도구를 등록해요. 해당 도구의 자체 반환은 감소 면제예요.
Bands: combine the modes
정렬된 크기 bands 목록을 구성하세요. 각 밴드는 (over, action) 쌍이에요. 반환의 측정된 크기가 over에 도달하면 그 action이 실행돼요. 맞는 임계값이 가장 큰 밴드가 승리하고, 가장 작은 임계값 아래의 것은 통과해요.
from pydantic_ai import Agent
from pydantic_ai_harness import ToolOutputLimits
from pydantic_ai_harness.tool_output_limits import Band, Spill, Summarize, Truncate
agent = Agent(
'openai:gpt-4o',
capabilities=[
ToolOutputLimits(
bands=[
Band(over=100_000, action=Spill()), # huge: keep losslessly, read back on demand
Band(over=20_000, action=Summarize()), # large: compress with the run's model
Band(over=5_000, action=Truncate()), # medium: cheap clamp
],
# below 5,000: passthrough
)
],
)
bands를 전달하지 않을 때의 기본 밴드는 10,000자 임계값의 Spill(then=Truncate())이에요. 저장소가 쓰기를 받아들이면 무손실, 아니면 제한된 truncation — LLM 비용 0, 조용한 버림 없음.
Passthrough()는 bands 또는 per_tool 목록을 위한 명시적 no-op action으로, 일치하는 반환을 그대로 남겨요.
Fallbacks with then
모든 action은 선택적 then을 취하며, action이 실행될 수 없을 때 적용돼요. Spill의 저장소 오류, 바이너리 페이로드의 Truncate/Summarize, 모델 호출이 예외를 발생시키는 Summarize 등이에요. then은 체인을 이루므로, Summarize(then=Spill(then=Truncate()))는 summarize -> spill -> truncate로 저하돼요.
Per-tool overrides and filtering
per_tool은 명명된 도구에 대해 전역 밴드 목록을 대체하고(파일 읽기는 head, 로그는 tail), tool_filter(a ToolSelector)는 capability가 건드리는 도구의 범위를 한정해요.
from pydantic_ai import Agent
from pydantic_ai_harness import ToolOutputLimits
from pydantic_ai_harness.tool_output_limits import Band, Truncate, TruncationStrategy
agent = Agent(
'openai:gpt-4o',
capabilities=[
ToolOutputLimits(
per_tool={
'read_file': [Band(over=8_000, action=Truncate(strategy=TruncationStrategy.head))],
'run_shell': [Band(over=8_000, action=Truncate(strategy=TruncationStrategy.tail))],
},
tool_filter=['read_file', 'run_shell', 'search'],
)
],
)
TruncationStrategy에는 세 멤버가 있어요. head(첫 문자 보존, 헤더와 스키마에 좋음), tail(마지막 문자 보존, 오류가 마지막에 오는 빌드·테스트 출력에 좋음), head_tail(양 끝 보존, 중간 생략 — 기본값).
Prefer complete tail lines
Truncate(keep_tail_lines=N)을 설정해 나머지 문자 예산을 할당하기 전에 마지막 N줄을 예약해요. 기본값은 0이며, 기존 truncation 동작을 바꾸지 않아요.
from pydantic_ai_harness.tool_output_limits import Band, ToolOutputLimits, Truncate, TruncationStrategy
truncate = Truncate(max_chars=4_000, strategy=TruncationStrategy.head, keep_tail_lines=2)
limits = ToolOutputLimits(bands=[], per_tool={'run_command': [Band(over=4_000, action=truncate)]})
head에서는 남은 콘텐츠 예산이 출력의 시작을 유지해요. head_tail에서는 시작과 예약된 줄 바로 앞 텍스트 사이에 2:3으로 나눠요. tail은 계속 끝을 유지해요. 표식과 유지된 모든 문자는 max_chars에 포함돼요.
상한이 우선해요. 요청된 줄이 그 상한을 초과하면 truncation이 평소의 tail 전략으로 폴백하고, 맞지만 전체 표식이 그것을 밀어내면 결과는 상한 안의 순수 tail 조각이에요. 이는 then을 호출하거나 지나치게 큰 제어 트레일러가 온전히 유지된다는 것을 보장하지 않아요.
줄은 LF 또는 CRLF로 구분되고 기존 종결이 유지돼요. 종결 개행은 줄을 추가하지 않지만, 빈 마지막 줄은 세어요. 존재하는 줄보다 많이 요청하면 전체 텍스트를 선택하며 같은 상한을 적용해요. 음수 keep_tail_lines 값은 거부돼요.
이는 직렬화와 (선택적) ANSI 제거 후 텍스트에 적용되며, ToolReturn.return_value와 텍스트 content에 대해 독립적으로 적용돼요. 바이너리 폴백은 변하지 않아요. 꼬리 줄 선택은 텔레메트리 스팬을 추가하지 않아요. 이는 별도 연산이 아니라 기존 도구 결과 감소 안의 슬라이스 선택이기 때문이에요.
Layering with Shell
Shell의 네이티브 max_output_chars를 ToolOutputLimits 임계값 위로 유지하세요. 적당한 명령 출력에는 tail truncation을, 큰 출력에는 Spill을 사용하세요:
from pydantic_ai import Agent
from pydantic_ai_harness.shell import Shell
from pydantic_ai_harness.tool_output_limits import (
Band,
Spill,
ToolOutputLimits,
Truncate,
TruncationStrategy,
)
tail = Truncate(max_chars=4_000, strategy=TruncationStrategy.tail)
agent = Agent(
'openai:gpt-5.6-sol',
capabilities=[
Shell(allowed_commands=['git', 'rg', 'pytest'], max_output_chars=100_000),
ToolOutputLimits(
bands=[],
per_tool={
'run_command': [
Band(over=20_000, action=Spill(then=tail)),
Band(over=4_000, action=tail),
],
},
),
],
)
run_command만 이 밴드를 사용해요. bands=[]는 다른 도구를 네이티브 한도에 맡겨요. tail 조각은 유지된 접미사 안에 맞으면 종료 코드 트레일러를 보존해요. 작은 예산에서 전체 줄이 살아남는다는 것을 보장하지 않으며, head는 트레일러를 제거할 수 있어요.
Shell은 ToolOutputLimits가 결과를 보기 전에 네이티브 상한을 적용해요. spill 임계값이 그 상한 아래라면, 저장소가 실패하지 않는 한 네이티브로 잘린 결과가 다시 잘리는 대신 저장돼요. spill 미리보기는 양 끝을 보여주고 모델은 read_tool_result로 저장된 텍스트를 검사할 수 있어요.
Spilling은 Shell에서 받은 결과만 보존해요. 네이티브 상한이 이미 제거한 콘텐츠는 복구할 수 없어요. 미리보기는 spill과 네이티브 truncation 공지 둘 다를 포함할 수 있어요. 네이티브 공지는 저장된 결과를 설명하지, 더 짧은 미리보기를 설명하지 않아요. 양수 Spill.preview_chars 값은 미리보기의 콘텐츠 예산을 제어하며, 그 헤더와 생략 표식이 그 길이에 추가돼요.
Both return_value and content are reduced
ToolReturn은 return_value와 선택적 content를 지니며, core는 content를 별도의 모델이 보는 부분으로 렌더링하고 그것도 히스토리에 영속돼요. 이 capability는 둘 다 같은 밴드 논리로 측정·감소해요(별도의 핸들로 spill). 텍스트 content는 제자리에서 감소하고, 넘쳐나는 비텍스트 content(멀티모달 부분)는 안전하게 잘릴 수 없으므로 warnings.warn과 함께 감소되지 않은 채 남아요.
ToolReturn.metadata is preserved
Spill 키(overflow_handle, overflow_bytes, overflow_content_handle)는 도구가 넣은 것과 함께 ToolReturn.metadata에 살아요. 기존 매핑은 문자열화된 키로 복사돼고, 기존 비매핑 값(문자열, dataclass, Mapping이 아닌 것)은 버리는 대신 original_metadata 아래에 유지돼요. 어느 쪽이든 호출자의 메타데이터는 spill에서 살아남고 Agent.run 후 결과 ToolReturnPart의 metadata에서 읽을 수 있어요.
Size unit
임계값은 기본적으로 문자로 측정돼요. over_tokens=True를 설정하면 추정 토큰으로 측정해요(compaction과 같은 ~4자-당-토큰 휴리스틱). 정확성을 위해 tokenizer 콜러블을 전달하세요. Truncate.max_chars는 항상 문자예요 — truncation은 임계값 단위와 무관하게 문자 연산이에요. 상한은 truncation 표식을 포함하고 각 감소 텍스트 값에 별도로 적용돼요. 예산이 유지된 콘텐츠와 완전한 표식 둘 다에 맞지 않으면 truncation은 표식 없이 선택된 슬라이스를 유지해요. 비양수 상한은 빈 문자열을 반환해요. strip_ansi=True를 설정하면 측정·감소 전 텍스트 반환에서 ANSI 이스케이프 시퀀스를 제거해요.
Pageable structured spills
spill된 구조화 반환은 컴팩트 JSON으로 저장돼요. 한 줄짜리 긴 줄이죠. read_tool_result는 줄 단위로 페이지를 넘기므로 페이지 1이 전체 페이로드를 반환하고 페이지 2는 비어요. serializer를 설정하면 값을 실제 줄이 있는 레이아웃으로 저장해요:
from pydantic_ai_harness.tool_output_limits import ToolOutputLimits, indented_json, json_lines
ToolOutputLimits(serializer=indented_json) # one field per line
ToolOutputLimits(serializer=json_lines) # one record per line
레코드 목록을 반환하는 도구에는 json_lines를 사용하세요. N번째 줄은 N번째 레코드이므로 페이지 오프셋과 pattern 매치가 전체 레코드와 정렬돼요. 목록 같은 시퀀스가 아닌 것은 indented_json으로 폴백해요 — dict로 감싼 목록도 포함하므로, 레코드별 페이지를 원하면 목록을 직접 반환하세요. 그 외의 모든 것에는 indented_json을 사용하세요.
어떤 (value) -> str 콜러블도 동작하지만 사전 설정을 선호하세요. 사전 설정은 read-back 오프셋을 줄 격자에서 밀어낼 수 있는 유니코드 줄 구분자(U+0085/U+2028/U+2029)를 이스케이프해요. 직렬화된 텍스트도 측정 대상이므로, 들여쓰기 레이아웃은 컴팩트 JSON이 넘지 않는 크기 밴드를 넘을 수 있어요. 문자열과 바이너리 반환은 절대 직렬화되지 않고, 가장 작은 밴드 아래의 반환은 그대로 통과하며, 예외를 발생시키거나 비텍스트를 반환하는 serializer는 도구 출력을 잃는 대신 경고하고 컴팩트 JSON으로 폴백해요.
Spill store
spill된 페이로드는 좁은 OverflowStore protocol을 거쳐요. 기본 LocalFileStore는 안정적인 루트 디렉터리 아래 (run_id, tool_call_id, retry)당 파일 하나를 쓰고 실행 후에도 유지하므로, 이후의 read_tool_result — 이 실행이나 이후 agent/run에서 — 가 여전히 그 파일에 도달할 수 있어요. 핸들은 백엔드 주소 지정 가능(상대 키)이지 절대 로컬 경로가 아니므로, 영속 백엔드(Temporal, 블롭 저장소, #4352가 랜딩되면 core ExecutionEnvironment 워크스페이스)는 다른 프로세스에서도 같은 핸들을 해석할 수 있어요. 자체 백엔드는 store=...로 공급하세요.
from typing import Protocol
class OverflowStore(Protocol):
async def write(self, key: str, data: bytes) -> str: ... # returns a handle
async def read(self, handle: str) -> bytes: ...
Security model (shared root, not isolation)
저장소 루트는 의도적으로 안정적이고 공유 가능해요 — spill된 파일은 이후 에이전트나 실행이 읽을 수 있어야 하므로 — 보안은 인스턴스별 격리에서 오지 않아요. 두 메커니즘에서 와요. 루트는 0700(소유자 전용) 권한으로 생성되고, read는 대상(심볼릭 링크 따라감)을 해석하고 심볼릭 링크, .., 절대 경로로 루트를 벗어나는 것을 거부해요. 핸들 세그먼트도 정화되어 조작된 핸들이 바깥으로 순회할 수 없어요.
Cleanup: keep-forever by default, opt-in TTL pruning
기본적으로 저장소는 spill된 파일을 영원히 유지해요 — 실행 종료 시 삭제하면 여전히 spill을 읽고 싶은 이후 에이전트를 깨뜨리죠. 디스크 사용을 제한하려면 연령 기반 정리를 옵트인하세요:
from datetime import timedelta
from pydantic_ai import Agent
from pydantic_ai_harness import ToolOutputLimits
from pydantic_ai_harness.tool_output_limits import LocalFileStore
store = LocalFileStore(cleanup_after=timedelta(hours=6)) # default: None = keep forever
agent = Agent('openai:gpt-4o', capabilities=[ToolOutputLimits(store=store)])
설정되면 write는 수정 시간(st_mtime)이 cleanup_after보다 오래된 파일을 삭제하는 백그라운드 정리(핫 경로 밖의 데몬 스레드)를 예약해요. 정리는 비차단이며 오류를 만들지 않아요. 모든 실패는 잡혀서 warnings.warn으로 표면화되고, 에이전트 실행으로 전파되지 않으므로 정리가 실행을 실패시키거나 핫 경로를 막지 않아요. 마지막 읽기 시간(st_atime)은 noatime/relatime 마운트에서 신뢰할 수 없으므로 사용되지 않아요.
in-process TTL보다 외부 정리(cron, sweeper)를 선호하나요? 저장소 루트를 가리키고 mtime으로 삭제하세요:
import time
from pathlib import Path
root = Path('/tmp/pyai_harness_overflow') # or your configured base_dir
cutoff = time.time() - 6 * 3600
for path in root.rglob('*'):
if path.is_file() and path.stat().st_mtime < cutoff:
path.unlink(missing_ok=True)
Usage accounting
내장 Summarize 호출은 모델에 대한 실제 요청이므로, 그 전체 사용량(토큰과 요청 자체)이 SummarizingCompaction과 정확히 같이 실행의 ctx.usage에 접혀 들어가요. 중첩 실행은 유한 요청 한도가 보류 중인 부모 요청을 위해 요청 한 개를 예약한다는 점 외에는 부모 한도를 그대로 받아요.
기본적으로 Summarize는 실행 중인 에이전트의 모델(ctx.model)을 상속해요. Summarize(model=...)에 모델 id나 인스턴스를 전달해 재정의하거나, summarize 콜러블을 전달해 내장 프롬프트를 완전히 우회할 수 있어요. capability의 summary_prompt 템플릿은 {tool_name}과 {output} 자리 표시자를 모두 포함해야 해요.
영속 실행 capability가 붙어 있으면 내장 모델 요약은 저널링된 capability 연산이에요. ToolOutputLimits는 안정적인 기본 id='tool_output_limits'를 지니므로, 영속 복구가 구성 없이 동작해요.
durability 아래에서 밴드를 선택할 때 두 가지 세부사항이 중요해요. 요약되는 텍스트는 저널링된 연산 입력의 일부이므로, 엔진의 페이로드 한도 근처 출력에는 내장 Summarize보다 Spill을 선호하세요. 그리고 커스텀 summarize 콜러블은 영속 연산이 아닌 직접 실행돼요 — 임의의 콜러블은 워커 쪽에서 재구성할 수 없으므로 — 재생 시 다시 호출될 수 있어요.
Edge cases
- 바이너리 반환은 그대로 spill되고 절대 문자열화 잘림되지 않아요. 바이너리의
Truncate/Summarize는then으로 폴스루해요. - 구조화/중첩 반환은
Spill(또는 summarize)을 선호해요. — JSON을 잘라내면 잘못된 JSON이 되니까요.Spill은 최상위의 한 줄 모양 스케치를 포함해요. ModelRetry와 도구 오류는 이 훅에 절대 도달하지 않아요(발생되지 반환되지 않으므로), 모델은 항상 복구에 필요한 전체 오류를 받아요.- 큰
ToolReturn.content는return_value와 같은 밴드로 감소돼요. 넘쳐나는 비텍스트 콘텐츠는 경고와 함께 감소되지 않은 채 남아요. - 한 단계의 여러 과대 반환은 별도 핸들을 얻고(도구 호출별로 키), 재시도도 별도 핸들을 얻어요(재시도별로 키), 그래서 재시도된 호출이 이전 시도의 spill을 덮어쓰지 않아요.
Relationship to other capabilities
- 창 안의 컨텍스트를 압축하거나 버리는 compaction과 구별돼요. 이 capability는 생성 시점에 큰 도구 출력을 창 밖으로 옮겨요.
- 랜딩되면
OverflowStore이음새를 통해 core #4352(표준 쿼리 가능 파일 원시형)를 소비해요. - 도구 반환이 아니라 폭주하는 모델 응답을 클램프하는
ClampOversizedMessages와 구별돼요.
API reference
ToolOutputLimits
Bases: AbstractCapability[AgentDepsT]
과대 도구 반환이 생성될 때 줄이고 감소를 영속.
도구가 컨텍스트 창을 지배할 만큼 큰 페이로드를 반환할 수 있어요. 도구 반환은 히스토리에 영속되므로, 과대 반환은 이후 모든 요청에 재전송돼요. 이 capability는 after_tool_execute에서 반환을 가로채 한 번 줄이고, 줄어든 형태를 영속시켜요 — 요청마다 재계산되지 않아요.
정렬된 크기 bands 목록을 통해 자유롭게 결합되는 세 감소 모드:
Truncate: 문자 예산으로 클램프. 손실 있음, 제로 비용.Spill: 전체 페이로드 영속, 모델에read_tool_result핸들 + 미리보기. 무손실.Summarize: 크기 제한 LLM 요약. 기본적으로 실행의 모델 상속.
측정된 크기가 over 임계값을 충족하는 첫 밴드가 승리하고, 더 작은 반환은 통과해요. per_tool은 명명된 도구의 밴드 목록을 대체하고, tool_filter는 건드리는 도구의 범위를 한정해요. 기본은 Spill(then=Truncate())이에요. 저장소가 쓰기를 받아들이면 무손실, 아니면 제한된 truncation.
ModelRetry와 다른 오류는 이 훅에 절대 도달하지 않아요(발생되지 반환되지 않으므로), 모델이 복구에 필요한 오류 페이로드는 절대 spill되거나 요약되지 않아요.
Attributes
bands
정렬된 크기 밴드. over 임계값이 충족되는 첫 밴드가 승리.
Type: Sequence[Band] Default: field(default_factory=_default_bands)
per_tool
명명된 도구에 대해 bands를 대체하는 도구별 밴드 목록.
Type: Mapping[str, Sequence[Band]] Default: field(default_factory=(dict[str, Sequence[Band]]))
tool_filter
이 capability가 건드리는 도구. 일치하지 않는 도구는 항상 통과.
Type: ToolSelector[AgentDepsT] Default: 'all'
over_tokens
밴드 임계값을 문자 대신 추정 토큰으로 측정.
Type: bool Default: False
tokenizer
over_tokens용 선택적 (str) -> int 토크나이저. 기본값은 ~4자 휴리스틱.
Type: Callable[[str], int] | None Default: None
store
spill된 페이로드의 백엔드. 기본값은 LocalFileStore.
Type: OverflowStore | None Default: None
strip_ansi
측정·감소 전 텍스트 반환에서 ANSI 이스케이프 시퀀스 제거.
Type: bool Default: False
summary_prompt
Summarize용 프롬프트 템플릿. {tool_name}과 {output}을 포함해야 해요.
Type: str Default: _DEFAULT_SUMMARY_PROMPT
serializer
구조화(비문자열, 비바이너리) 반환을 측정·미리보기·spill·재읽기 되는 텍스트로 렌더링. 설정하지 않으면 구조화 반환은 컴팩트 JSON으로 렌더링되어 전체 값이 한 줄에 들어가요. indented_json과 json_lines 사전 설정은 큰 spill을 read_tool_result로 줄 단위 페이징 가능하게 해요. 모든 밴드 임계값 아래에 머무는 반환은 원래 객체로 통과하므로, 직렬화된 텍스트는 밴드가 트리거된 후에만 모델이 볼 수 있어요.
Type: Serializer | None Default: None
Methods
get_toolset
def get_toolset() -> AgentToolset[AgentDepsT] | None
요청 시 spill된 페이로드를 읽는 read_tool_result 도구를 등록.
Returns
AgentToolset[AgentDepsT] | None
after_tool_execute
@async
def after_tool_execute(
ctx: RunContext[AgentDepsT],
*,
call: ToolCallPart,
tool_def: ToolDefinition,
args: dict[str, Any],
result: Any,
) -> Any
도구 결과 — return_value와 모델이 보는 content 둘 다 —를 줄여요.