From 7ec537c4bea1ee5fd2e4c2f61925b68b3cbb1aab Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:15:42 +0800 Subject: [PATCH] test(agent): lock in aggregate gating of a parallel tool batch (#192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #192 asked for a pre-send context gate between a parallel tool batch and its continuation request, plus a regression test. The gate already exists: runTurn's #193 pre-send check sits at the top of the loop — after stepResponses appends the whole batch and before the continuation request — and inputOverCompactThreshold re-estimates over the full messages, including the freshly appended outputs. So a parallel batch that crosses compactAt() only in aggregate triggers compaction before it is sent, which is exactly #192's scenario. This adds the missing regression test (the issue's ask #4): one function_call_output stays under the window; a six-output batch (each individually under the per-output cap) crosses compactAt() in aggregate and trips the gate. No code change is needed on top of #193; the #203 prefill baseline (in the in-flight .202 follow-ups) only makes the gate fire earlier/more conservatively. Refs #192 Co-Authored-By: blackfloofie <265516171+blackfloofie@users.noreply.github.com> --- src/agent_request.zig | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/agent_request.zig b/src/agent_request.zig index 6f26327d..245c2e69 100644 --- a/src/agent_request.zig +++ b/src/agent_request.zig @@ -542,6 +542,39 @@ test "inputOverCompactThreshold (#193): local estimate gates a pre-send compact" try std.testing.expect(!inputOverCompactThreshold(&agent)); } +// (#192) A parallel tool batch must be gated as an AGGREGATE. runTurn's #193 +// pre-send gate sits at the top of the loop — after stepResponses appends the +// whole batch and before the continuation request (agent.zig) — so +// inputOverCompactThreshold re-estimates over every freshly appended output. +// Six parallel read_file outputs, each individually well under the window (and +// the 16 KB per-output cap), still trip the gate together, so compaction runs +// before the continuation instead of overflowing the model. +test "inputOverCompactThreshold (#192): a parallel tool batch is gated as an aggregate" { + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const a = arena_state.allocator(); + var msgs = std.json.Array.init(a); + var agent: Agent = undefined; + // compactAt() = 10_000/10*8 = 8_000 tokens ≈ 32_000 serialized bytes. + agent.provider = .{ .id = "codex", .kind = .responses, .auth = .bearer, .url = "", .api_key = "", .model = "gpt-5", .context = 10_000 }; + + // One ~6 KB output — individually far under the window — must not trip it. + const one = "{\"type\":\"function_call_output\",\"call_id\":\"c0\",\"output\":\"" ++ ("x" ** 6000) ++ "\"}"; + try msgs.append(try std.json.parseFromSliceLeaky(Value, a, one, .{})); + agent.messages = msgs; + try std.testing.expect(!inputOverCompactThreshold(&agent)); // one output: under + + // Five more (six total — the reported six parallel read_file calls) push the + // aggregate serialized input past compactAt() (~36 KB ≈ 9k est tokens). + var i: usize = 0; + while (i < 5) : (i += 1) { + const item = "{\"type\":\"function_call_output\",\"call_id\":\"c\",\"output\":\"" ++ ("x" ** 6000) ++ "\"}"; + try msgs.append(try std.json.parseFromSliceLeaky(Value, a, item, .{})); + } + agent.messages = msgs; + try std.testing.expect(inputOverCompactThreshold(&agent)); // full batch: over → compact before continuation +} + pub fn recordUsageResponses(self: *Agent, response: std.json.ObjectMap, req_body_len: usize) void { self.last_cache_read = 0; // Fallback estimate (~4 bytes/token) from the serialized request body,