pub struct ToolResultPolicy {
pub max_bytes: Option<usize>,
pub dedupe_repeats: bool,
pub spill: bool,
}Expand description
A ceiling on how much of one tool result reaches the context.
A single call can return more than the whole conversation: a lock file, a
SELECT *, an MCP tool the framework does not control. Measured on a real
run, “search these files for a word” cost 53,487 input tokens because one
read_file returned a lock file — the model then paid for it on every
subsequent turn, and compaction started throwing away real history to make
room. A per-result ceiling is the only place to stop that: the tools cannot
all be trusted (third-party MCP), and the compactor only runs after the
damage is in the context.
Fields§
§max_bytes: Option<usize>Max serialized bytes of a single tool result. None disables the guard.
Default ~24 KiB — roughly 6k tokens of English, generous for a file page
or a query result, far below what blows a window.
dedupe_repeats: boolReplace the payload of a read-only call that exactly repeats an earlier one in the same run, when nothing has modified the world in between.
StuckPolicy only sees consecutive identical rounds. Reading a file
at iteration 1 and again at iteration 5 is not that, and looks like
progress — but the same bytes land in the context twice and the model
learns nothing the second time. Measured on a real run, model wait was
36.7s against 3ms of tool execution: a repeat costs context, not time,
so what is suppressed is the payload, not the call.
Only ToolRisk::ReadOnly qualifies (Network is a separate risk, and an
external endpoint may answer differently), and any non-read-only call
clears the record — after a write, re-reading is the correct move.
Off by default, on the evidence. Measured on the completion benchmark: ceilings alone solved 6/6 tasks for 160k effective tokens; adding repeat-suppression cut that to 32k — and lost a task. An agent working through a file larger than one page re-reads it because it cannot hold it, gets told it already has the answer, and does not: the content was paged away. Five times cheaper is not worth a task a framework could otherwise do, so this is opt-in for callers who would rather have the tokens. (Suppressing from the third identical call rather than the second would likely keep most of the saving without the failure — it needs measuring before it becomes the default.)
spill: boolWhen a result exceeds max_bytes, save the full payload to a file
under .harness/spill/ in the workspace and inline a bounded preview
plus the path, instead of cutting the tail off and throwing it away.
Truncation destroys information: the model is told “narrow your
request”, but the bytes it needed may be exactly the ones dropped, and
the only recovery is re-running the call to be truncated again.
Measured on the completion benchmark, that loop is visible as a single
task paying 184k input tokens. Spilling keeps the ceiling — the context
gets a preview, never the flood — while the whole result stays
retrievable through the file tools the agent already has (read_file
with offset/limit, grep on the spill path), because the spill lives
inside the workspace jail. Borrowed from DeepSeek Harness’s spill
family (preview + retrieval locator), which is the same judgement.
Costs nothing until it fires: the write happens only on the oversized path, which the default ceiling makes rare. When the write fails (e.g. read-only workspace) the guard falls back to plain truncation.
Trait Implementations§
Source§impl Clone for ToolResultPolicy
impl Clone for ToolResultPolicy
Source§fn clone(&self) -> ToolResultPolicy
fn clone(&self) -> ToolResultPolicy
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more