supercode-harness 0.4.13

The optional native Supercode agent and tool harness
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
//! Acceptance tests for TR-12 (`.volter/tracker/markdown/TR-12.md`): the
//! `cap_tool_output` supersession land-blocker fixed at
//! `Agent::run_loop`'s tool-result push site (SPEC.md D6/A7).
//!
//! THE DEFECT (pre-fix): under the default config
//! (`max_tool_output_bytes = Some(100_000)`), `cap_tool_output` ran
//! unconditionally at the history push site even with a recorder + policy
//! active. The recorder got the full bytes; `history` got the capped copy.
//! A10 `TurnsCleared` (and A7 `ToolOutputTruncated`) reduction hashes are
//! minted in `project_messages` from `history` — so any reduction covering a
//! tool output over 100KB was minted from an already-capped copy, which
//! could never recompute the same way once the sidecar was reloaded from
//! disk (`verify_log`/`invert`, the CLI's offline path). Existing
//! `invert_project_is_identity*` tests mint AND invert against the same
//! in-memory `Session`, so they structurally could not catch this.
//!
//! THE FIX: gate `cap_tool_output` off exactly when a recorder AND a
//! `ReductionPolicy` are both installed — the only combination under which a
//! reduction is ever minted over `history` with a durable sidecar behind it.
//! `history` then holds full bytes by construction, so `history[1..] ≡
//! sidecar.messages`, and every hash minted matches what a reloaded sidecar
//! recomputes.

use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};

use async_trait::async_trait;
use supercode_harness::reduce::{
    invert, project_messages, reduction_id, verify_log, ReductionPolicy,
};
use supercode_harness::session::Session;
use supercode_harness::sidecar::SidecarWriter;
use supercode_harness::store::SessionStore;
use supercode_harness::{
    Agent, ChatMessage, ChatRequest, Config, FunctionCall, Provider, Role, ToolCall, Usage,
};

fn temp_dir(tag: &str) -> PathBuf {
    static N: AtomicUsize = AtomicUsize::new(0);
    let dir = std::env::temp_dir().join(format!(
        "supercode-tr12-{tag}-{}-{}",
        std::process::id(),
        N.fetch_add(1, Ordering::SeqCst)
    ));
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

/// A tool ("big_tool") that returns a fixed, distinctive payload of exactly
/// `bytes` length — big enough to trip `Config::max_tool_output_bytes`'s
/// default 100 KB cap.
struct BigTool(String);
#[async_trait]
impl supercode_harness::tools::Tool for BigTool {
    fn name(&self) -> &str {
        "big_tool"
    }
    fn description(&self) -> &str {
        "x"
    }
    fn parameters(&self) -> serde_json::Value {
        serde_json::json!({"type": "object"})
    }
    async fn execute(
        &self,
        _a: serde_json::Value,
        _c: &supercode_harness::tools::ToolContext,
    ) -> supercode_harness::Result<String> {
        Ok(self.0.clone())
    }
}

fn tool_call_msg(id: &str) -> ChatMessage {
    ChatMessage {
        role: Role::Assistant,
        content: None,
        content_parts: None,
        tool_calls: Some(vec![ToolCall {
            id: id.to_string(),
            kind: "function".to_string(),
            function: FunctionCall {
                name: "big_tool".to_string(),
                arguments: "{}".to_string(),
            },
        }]),
        tool_call_id: None,
        name: None,
        metadata: Default::default(),
    }
}

/// Turn 0: call `big_tool`. Every turn after: a plain text reply (no tool
/// calls), so `Agent::send` returns immediately and the caller drives how
/// many turns run by how many times it calls `send`. Every request's message
/// vector is captured so a test can inspect the wire body of any turn.
struct ToolThenPlainCapturing {
    calls: AtomicUsize,
    requests: Arc<Mutex<Vec<Vec<ChatMessage>>>>,
}
#[async_trait]
impl Provider for ToolThenPlainCapturing {
    async fn complete(
        &self,
        req: &ChatRequest,
        _on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
    ) -> supercode_harness::Result<(ChatMessage, Usage)> {
        let n = self.calls.fetch_add(1, Ordering::SeqCst);
        self.requests.lock().unwrap().push(req.messages.clone());
        if n == 0 {
            Ok((tool_call_msg("c1"), Usage::default()))
        } else {
            Ok((
                ChatMessage::assistant(format!("reply {n}")),
                Usage::default(),
            ))
        }
    }
}

/// Wire-serialized byte size of a request's message vector, the same
/// approximation `reduce_loop.rs`'s `wire_bytes` uses for its A7 acceptance
/// assertion.
fn wire_bytes(msgs: &[ChatMessage]) -> usize {
    serde_json::to_string(msgs).map(|s| s.len()).unwrap_or(0)
}

/// dev/01 (GUARANTOR REGRESSION, required verbatim by Control) + dev/04
/// (whole-suite invariant): a live agent run with a recorder + policy BOTH
/// active produces a >100KB tool output that lands inside an established
/// `clear_turns_older_than`/`TurnsCleared` range; `verify_log` AND `invert`
/// then run OFFLINE against the sidecar and reduction log **reloaded from
/// disk** (via `SessionStore`, the exact primitives
/// `cli/main.rs:1520,2864,3012`'s `show-reductions`/`convert`/`inspect` use)
/// — never the live `Agent`/in-memory `Session`. Both must pass clean, and
/// `invert` must restore the full original bytes byte-exact.
///
/// MUTATION CHECK (see the TR-12 builder report): this test was verified to
/// FAIL on the pre-fix gate (temporarily reverting `Agent::run_loop`'s
/// `for_history` gate to always call `self.cap_tool_output(output)`) with a
/// `verify_log`/hash-mismatch error, and to pass with the fix restored.
#[tokio::test]
async fn dev01_dev04_guarantor_regression_offline_verify_and_invert() {
    let dir = temp_dir("dev01-dev04");
    let store_dir = dir.join("store");
    let store = SessionStore::open(&store_dir).unwrap();
    let name = "tr12-dev01";
    let sidecar_path = store.sidecar_path(name);

    // A distinctive >100KB payload (150,000 bytes) with a needle so a
    // byte-exact restore can be checked precisely, not just by length.
    let mut original = "Q".repeat(120_000);
    original.push_str("TR12-NEEDLE-DEV01");
    original.push_str(&"y".repeat(150_000 - original.len()));
    assert_eq!(original.len(), 150_000);
    assert!(original.len() > 100_000, "must trip the default cap");

    let requests = Arc::new(Mutex::new(Vec::new()));
    let config = Config::builder()
        .cwd(dir.clone())
        .compact_after_messages(8)
        .build();
    let mut reg = supercode_harness::tools::ToolRegistry::new();
    reg.register(BigTool(original.clone()));
    let mut agent = Agent::with_parts(
        config,
        Box::new(ToolThenPlainCapturing {
            calls: AtomicUsize::new(0),
            requests: requests.clone(),
        }),
        reg,
    );

    let empty_session = Session::from_claude_code_str("").unwrap();
    let writer = SidecarWriter::create(&sidecar_path, &empty_session).unwrap();
    agent.set_recorder(writer);
    let policy = ReductionPolicy::default();
    agent.set_reduction_policy(policy.clone());

    // Turn 0 fires the big tool call. Turns 1..5 are plain text — enough for
    // `compact_after_messages(8)` to trigger a `TurnsCleared` range covering
    // the tool call/result from turn 0 (established once `history[1..].len()`
    // first exceeds 8, then reproduced verbatim forever after).
    for i in 0..6 {
        let reply = agent.send(format!("turn {i}")).await.unwrap();
        assert!(
            reply.starts_with("reply"),
            "unexpected reply at turn {i}: {reply}"
        );
    }

    let log = agent.reduction_log().clone();
    let cleared = log
        .reductions
        .iter()
        .find_map(|r| match r.kind {
            supercode_harness::reduce::ReductionKind::TurnsCleared { first, last, .. } => {
                Some((first, last))
            }
            _ => None,
        })
        .expect("a TurnsCleared reduction must have been established");
    // The tool call (index 1) and its tool result (index 2) in
    // `history[1..]` must fall inside the cleared range — otherwise this
    // test isn't exercising the scenario TR-12 requires.
    assert!(
        cleared.0 <= 1 && cleared.1 >= 2,
        "the big tool output must land inside the TurnsCleared range, got {cleared:?}"
    );

    // dev/04: `history[1..]` is byte-identical to the reloaded sidecar's
    // messages (direct assertion, not implied) — the property the D6/A7 gate
    // exists to guarantee.
    let sidecar_raw_live = std::fs::read_to_string(&sidecar_path).unwrap();
    let sidecar_live_reload = Session::from_native_str(&sidecar_raw_live).unwrap();
    assert_eq!(
        sidecar_live_reload.messages.len(),
        agent.history().len() - 1
    );
    for (a, b) in sidecar_live_reload
        .messages
        .iter()
        .zip(&agent.history()[1..])
    {
        assert_eq!(a.role, b.role);
        assert_eq!(
            a.content, b.content,
            "history[1..] must equal the sidecar, byte for byte"
        );
    }

    // Persist the reduction log the way the CLI does (`sessions
    // show-reductions`/`convert`/`inspect` all call `store.save_reduction_log`
    // via the resume/chat path; here we do it explicitly so this test's
    // offline reload is genuine).
    store.save_reduction_log(name, &log).unwrap();

    // --- OFFLINE from here: fresh reads from disk, no live `Agent`/Session ---
    let sidecar_jsonl = store
        .load_sidecar(name)
        .unwrap()
        .expect("sidecar must exist on disk");
    let sidecar = Session::from_sidecar_str(&sidecar_jsonl).unwrap();
    let log_reloaded = store
        .load_reduction_log(name)
        .unwrap()
        .expect("reduction log must exist on disk");

    // The exact primitive `cli/main.rs:1520` (`show-reductions`),
    // `:2864` (`convert`), and `:3012` (`inspect`) all call before doing
    // anything else with a reduced session.
    verify_log(&log_reloaded, &sidecar)
        .expect("verify_log must pass clean against the reloaded-from-disk sidecar");

    // Reconstruct the final projected view exactly as it was established
    // (prefix stability, A5: passing `log_reloaded` as `prior` reapplies the
    // existing `TurnsCleared` range verbatim rather than recomputing a new
    // one against the reloaded sidecar's now-final length).
    let (final_view, reprojected_log) = project_messages(&sidecar.messages, &policy, &log_reloaded);
    assert_eq!(
        reprojected_log, log_reloaded,
        "re-projecting from the reloaded sidecar with its own log must not invent new reductions"
    );

    let inverted = invert(&final_view, &log_reloaded, &sidecar)
        .expect("invert must pass clean against the reloaded-from-disk sidecar");
    assert_eq!(inverted.len(), sidecar.messages.len());
    for (a, b) in inverted.iter().zip(&sidecar.messages) {
        assert_eq!(a.role, b.role);
        assert_eq!(a.content, b.content);
    }

    // Byte-exact restore of the big tool output specifically.
    let restored_tool_msg = inverted
        .iter()
        .find(|m| m.role == Role::Tool && m.tool_call_id.as_deref() == Some("c1"))
        .and_then(|m| m.content.clone())
        .expect("the big tool result must be present after invert");
    assert_eq!(
        restored_tool_msg, original,
        "invert must restore the full 150,000-byte original byte-exact"
    );

    std::fs::remove_dir_all(&dir).ok();
}

/// dev/02: gate correctness. With a recorder + policy both active, the
/// history message pushed for a >cap tool result carries the FULL bytes (no
/// cap notice). With no reduction machinery at all (no policy — the legacy
/// path), `cap_tool_output` still runs and the notice still appears,
/// byte-identical to before TR-12 (pre-existing legacy-cap tests —
/// `agent_loop.rs::oversized_tool_output_is_capped`,
/// `agent_records_full_fidelity_while_capping_view` — are left unmodified
/// and still pass; this test adds the explicit side-by-side contrast the AC
/// calls for).
#[tokio::test]
async fn dev02_gate_correctness_full_bytes_with_machinery_capped_without() {
    let big = "N".repeat(150_000);

    // With machinery: recorder + policy both active.
    {
        let dir = temp_dir("dev02-with-machinery");
        let sidecar_path = dir.join("sess.sidecar.jsonl");
        let config = Config::builder().cwd(dir.clone()).build();
        let mut reg = supercode_harness::tools::ToolRegistry::new();
        reg.register(BigTool(big.clone()));
        let requests = Arc::new(Mutex::new(Vec::new()));
        let mut agent = Agent::with_parts(
            config,
            Box::new(ToolThenPlainCapturing {
                calls: AtomicUsize::new(0),
                requests,
            }),
            reg,
        );
        let empty_session = Session::from_claude_code_str("").unwrap();
        let writer = SidecarWriter::create(&sidecar_path, &empty_session).unwrap();
        agent.set_recorder(writer);
        agent.set_reduction_policy(ReductionPolicy::default());

        agent.send("go").await.unwrap();

        let history_copy = agent
            .history()
            .iter()
            .find(|m| m.role == Role::Tool)
            .and_then(|m| m.content.clone())
            .unwrap();
        assert_eq!(
            history_copy, big,
            "with machinery, history must keep the FULL bytes"
        );
        assert!(
            !history_copy.contains("bytes total, showing first"),
            "with machinery, no cap notice may ever appear in history"
        );
        std::fs::remove_dir_all(&dir).ok();
    }

    // Without machinery: no policy at all (recorder absent too, the plainest
    // legacy configuration) — capping stays exactly as before TR-12.
    {
        let dir = temp_dir("dev02-legacy");
        let config = Config::builder().cwd(dir.clone()).build();
        let mut reg = supercode_harness::tools::ToolRegistry::new();
        reg.register(BigTool(big.clone()));
        let requests = Arc::new(Mutex::new(Vec::new()));
        let mut agent = Agent::with_parts(
            config,
            Box::new(ToolThenPlainCapturing {
                calls: AtomicUsize::new(0),
                requests,
            }),
            reg,
        );

        agent.send("go").await.unwrap();

        let history_copy = agent
            .history()
            .iter()
            .find(|m| m.role == Role::Tool)
            .and_then(|m| m.content.clone())
            .unwrap();
        assert!(
            history_copy.len() < big.len(),
            "without machinery, legacy capping must still run"
        );
        assert!(
            history_copy.contains("bytes total, showing first"),
            "without machinery, the legacy cap notice must still appear: {history_copy}"
        );
        assert!(
            history_copy.contains("full output not retained"),
            "without a recorder, the notice must honestly say so: {history_copy}"
        );
        std::fs::remove_dir_all(&dir).ok();
    }
}

/// dev/03: safety net. Even with the D6/A7 gate off (recorder + policy both
/// active, so `history`/the sidecar keep a `>>100KB` tool output in full),
/// the very next request's WIRE payload is still bounded — by A7's own
/// projection pass, in the same loop (the next `build_request_messages`
/// call, before the model ever sees the raw output) — and carries a
/// `reduction_id` stub (the TR-1 contract every rehydration intrinsic
/// depends on). ROADMAP P7's runaway-output/RSS concern is covered by this,
/// not by reintroducing lossy capping.
#[tokio::test]
async fn dev03_safety_net_wire_bounded_via_a7_with_gate_off() {
    let dir = temp_dir("dev03");
    let sidecar_path = dir.join("sess.sidecar.jsonl");

    // A full order of magnitude past the legacy 100KB cap.
    let huge = "H".repeat(1_000_000);
    assert!(huge.len() >= 100_000 * 10);

    let config = Config::builder().cwd(dir.clone()).build();
    let mut reg = supercode_harness::tools::ToolRegistry::new();
    reg.register(BigTool(huge.clone()));
    let requests = Arc::new(Mutex::new(Vec::new()));
    let mut agent = Agent::with_parts(
        config,
        Box::new(ToolThenPlainCapturing {
            calls: AtomicUsize::new(0),
            requests: requests.clone(),
        }),
        reg,
    );
    let empty_session = Session::from_claude_code_str("").unwrap();
    let writer = SidecarWriter::create(&sidecar_path, &empty_session).unwrap();
    agent.set_recorder(writer);
    agent.set_reduction_policy(ReductionPolicy {
        tool_output_keep_bytes: 4096,
        tool_output_trigger_bytes: 8192,
        protect_last_n_tool_results: 0,
        ..ReductionPolicy::default()
    });

    agent.send("go").await.unwrap();

    // history/the sidecar hold the full 1,000,000-byte output (the gate is
    // off — this is the accepted in-memory/durable-storage tradeoff).
    let history_copy = agent
        .history()
        .iter()
        .find(|m| m.role == Role::Tool)
        .and_then(|m| m.content.clone())
        .unwrap();
    assert_eq!(history_copy.len(), huge.len());

    // The SECOND request (built at the top of the next loop iteration, same
    // `send` call, immediately after the tool result was pushed) must carry
    // a bounded wire payload, not the raw 1,000,000-byte output.
    let reqs = requests.lock().unwrap().clone();
    let second_request = &reqs[1];
    let bytes = wire_bytes(second_request);
    assert!(
        bytes < 50_000,
        "the wire payload for the very next request must be bounded by A7, got {bytes} bytes"
    );
    let body = serde_json::to_string(second_request).unwrap();
    assert!(
        !body.contains(&huge),
        "the wire payload must never contain the raw huge output"
    );

    // A7's stub carries a `reduction_id` (TR-1 contract): every rehydration
    // intrinsic (`expand_reduction`/`sidecar_search`) locates a placeholder
    // by this id.
    assert!(
        second_request.iter().any(|m| reduction_id(m).is_some()),
        "the reduced view must carry a message with a reduction id"
    );

    std::fs::remove_dir_all(&dir).ok();
}