supercode-core 0.2.1

A lightweight, fully-customizable AI coding agent SDK in Rust. Talks to any model via OpenRouter or any OpenAI-compatible endpoint.
Documentation
//! TR-7 (T20) dev/06 — Tier-2 demo: an archived, side-by-side comparison of
//! the deterministic `[turns cleared]` stub against an LLM-summarized stub
//! over the SAME cleared span of the SAME session, plus a scripted live-agent
//! run proving the summarized view lets the model answer a question
//! DIRECTLY from the summary — no `expand_reduction`/`sidecar_search` round
//! trip needed, unlike the deterministic stub (compare
//! `rehydrate.rs`'s own TR-1 dev/06 demo, which *requires* that round trip
//! because its stub carries no content, only "content existed here").
//!
//! **Scope note (per D14/D15 measurement discipline):** the summarizer here
//! is a small, deterministic, in-process CANNED fake — never a real model
//! call — because qualitative real-model summary quality is the project
//! owner's separate end-to-end differentiator demo, run outside this test
//! suite. What THIS test demonstrates and archives is the mechanism: the
//! summarized placeholder is what actually reaches the model's request body,
//! it carries the fact a later turn needs, and `expand_reduction` on its
//! `reduction_id` still resolves to the byte-exact original — proving TR-7's
//! "materially better continuation quality at ~equal token cost, and the
//! model can still drill into the verbatim original" claim mechanically,
//! ahead of the real-model qualitative pass.
//!
//! The archived artifact (git-ignored `target/`, same convention
//! `rehydrate.rs`'s TR-1 dev/06 demo uses) is a JSON file containing: the
//! shared `reduction_id`, the deterministic stub text, the summarized stub
//! text, the exact request body the scripted model turn saw, its answer, and
//! this scope note verbatim.

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

use async_trait::async_trait;
use supercode::reduce::rehydrate::expand_reduction;
use supercode::reduce::summarize::SpanSummarizer;
use supercode::reduce::{
    project_messages, reduction_id, ReductionKind, ReductionLog, ReductionPolicy,
};
use supercode::session::Session;
use supercode::sidecar::SidecarWriter;
use supercode::{Agent, ChatMessage, ChatRequest, Config, Provider, Usage};

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

const SECRET_FACT: &str = "the rollback runbook is at runbooks/rollback-v3.md";

/// A small, deterministic, CANNED summarizer standing in for a real model —
/// scope note above explains why a real model is deliberately out of scope
/// here. Its output deliberately embeds `SECRET_FACT` verbatim, exactly the
/// property a real summarizer is expected (not guaranteed) to have for
/// content actually present in the span.
struct CannedSummarizer;
impl SpanSummarizer for CannedSummarizer {
    fn summarize(&self, _span_text: &str) -> supercode::Result<String> {
        Ok(format!(
            "The user shared an operational fact for later use: {SECRET_FACT}. The assistant \
             acknowledged it, then a few filler turns followed."
        ))
    }
    fn model_id(&self) -> &str {
        "canned-tr7-demo-summarizer-v1"
    }
}

/// Turn 0: user shares the fact, model acks. Turns 1-3: filler (grows history
/// past the A10 threshold, clearing turn 0 out of view). Turn 4 (the
/// question): with TR-7 summaries ON, the fact is directly visible in the
/// projected request body via the summarized placeholder — the model
/// answers immediately, with NO `expand_reduction`/`sidecar_search` call.
struct SummaryAwareDemo {
    calls: AtomicUsize,
    captured_question_turn_body: std::sync::Mutex<Option<String>>,
}
#[async_trait]
impl Provider for SummaryAwareDemo {
    async fn complete(
        &self,
        req: &ChatRequest,
        _on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
    ) -> supercode::Result<(ChatMessage, Usage)> {
        let n = self.calls.fetch_add(1, Ordering::SeqCst);
        match n {
            0 => Ok((
                ChatMessage::assistant("Noted, I'll remember that."),
                Usage::default(),
            )),
            1..=3 => Ok((ChatMessage::assistant(format!("ack {n}")), Usage::default())),
            _ => {
                let body = serde_json::to_string(&req.messages).unwrap();
                // The differentiator, proven mechanically: the fact is
                // ALREADY present in the request — no tool round trip
                // required to answer, unlike the deterministic-stub case.
                assert!(
                    body.contains(SECRET_FACT),
                    "TR-7: the summarized placeholder must carry the fact directly: {body}"
                );
                *self.captured_question_turn_body.lock().unwrap() = Some(body);
                Ok((
                    ChatMessage::assistant(
                        "The rollback runbook is at runbooks/rollback-v3.md \
                         (answered directly from the TR-7 summary, no expand_reduction needed)."
                            .to_string(),
                    ),
                    Usage::default(),
                ))
            }
        }
    }
}

#[tokio::test]
async fn dev06_tier2_demo_summarized_stub_vs_deterministic_stub_side_by_side() {
    let dir = temp_dir("dev06");
    let sidecar_path = dir.join("demo.sidecar.jsonl");

    let config = Config::builder()
        .cwd(dir.clone())
        .compact_after_messages(6) // keep_recent = max(6/2, 2) = 3
        .build();
    let demo_provider = std::sync::Arc::new(SummaryAwareDemo {
        calls: AtomicUsize::new(0),
        captured_question_turn_body: std::sync::Mutex::new(None),
    });
    let mut agent = Agent::with_provider_arc(config, demo_provider.clone());

    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 {
        summarize_cleared_turns: true,
        // Low floor: this demo's point is the mechanism, not re-proving the
        // cost-guard boundary (`reduce_summaries.rs`'s dev/05 already does).
        expected_summary_bytes: 10,
        summary_cost_floor_multiple: 2,
        ..ReductionPolicy::default()
    };
    agent.set_reduction_policy(policy.clone());
    agent.set_span_summarizer(CannedSummarizer);

    agent
        .send(format!("Please remember this: {SECRET_FACT}"))
        .await
        .unwrap();
    for i in 0..3 {
        agent
            .send(format!("filler turn {i}, just say ack"))
            .await
            .unwrap();
    }
    assert!(
        agent
            .reduction_log()
            .reductions
            .iter()
            .any(|r| matches!(r.kind, ReductionKind::TurnsCleared { .. })),
        "expected a TurnsCleared reduction before the question turn: {:?}",
        agent.reduction_log()
    );

    let reply = agent.send("Where is the rollback runbook?").await.unwrap();
    assert!(
        reply.contains("runbooks/rollback-v3.md"),
        "the model must answer correctly using the summary: {reply}"
    );

    let cleared = agent
        .reduction_log()
        .reductions
        .iter()
        .find(|r| matches!(r.kind, ReductionKind::TurnsCleared { .. }))
        .cloned()
        .expect("a TurnsCleared reduction must exist");
    let (first, last) = match cleared.kind {
        ReductionKind::TurnsCleared { first, last, .. } => (first, last),
        _ => unreachable!(),
    };
    let summarized_stub = cleared.placeholder.clone();
    assert!(summarized_stub.contains(SECRET_FACT));
    assert!(summarized_stub.contains(&format!("expand_reduction(\"{}\")", cleared.id)));

    // --- Side-by-side: project the IDENTICAL messages/threshold with
    // summaries OFF (default) to get the deterministic stub. `TurnsCleared`
    // is a prefix-stable singleton (SPEC.md A5): once established at some
    // history length `L`, it never widens as the conversation keeps growing
    // — so reprojecting fresh over `full_history` truncated back down to
    // that SAME length `L` recomputes the IDENTICAL `(first, last)` range
    // (and therefore the identical content hash/id) that the live,
    // summaries-on run established. `L = last + 1 + keep_recent` falls
    // straight out of `compute_clear_range`'s own math (`cut = len -
    // keep_recent`, `last = cut - 1`) — reused here rather than
    // re-deriving/guessing which turn first triggered it.
    let full_history = agent.history()[1..].to_vec();
    // `Agent::maybe_compact` derives `clear_turns_older_than` from
    // `compact_after_messages` onto the AGENT's own internal policy copy
    // (never mutating this test's local `policy` binding) — read it back
    // from the live agent so the deterministic re-projection below uses the
    // exact same threshold that actually fired.
    let live_threshold = agent
        .reduction_policy()
        .and_then(|p| p.clear_turns_older_than)
        .expect("compact_after_messages must have populated a threshold by now");
    let keep_recent = (live_threshold / 2).max(2);
    let establishment_len = last + 1 + keep_recent;
    let messages_at_establishment = &full_history[..establishment_len];
    let deterministic_policy = ReductionPolicy {
        clear_turns_older_than: Some(live_threshold),
        ..ReductionPolicy::default()
    };
    let (_, deterministic_log) = project_messages(
        messages_at_establishment,
        &deterministic_policy,
        &ReductionLog::default(),
    );
    let deterministic = deterministic_log
        .reductions
        .iter()
        .find(|r| matches!(r.kind, ReductionKind::TurnsCleared { .. }))
        .expect("the deterministic (summaries-off) projection must clear the same span");
    assert_eq!(
        deterministic.id, cleared.id,
        "the deterministic and summarized projections of the identical span must mint the \
         SAME reduction_id"
    );
    let deterministic_stub = deterministic.placeholder.clone();
    assert!(!deterministic_stub.contains(SECRET_FACT));
    assert!(!deterministic_stub.contains("sc-summary"));

    // `expand_reduction` still resolves the summarized stub's id to the
    // byte-exact original cleared turns — TR-7 dev/02's invert-purity
    // guarantee, exercised here through the model-invocable intrinsic
    // (mirrors `rehydrate.rs`'s TR-1 demo).
    let view_with_stub: Vec<ChatMessage> = agent.history()[1..].to_vec();
    // Reconstruct a reduced view carrying the summarized placeholder at
    // `first`/`last`'s address the same way `project_messages` produced it
    // live: read it back off the live reduction log/history directly rather
    // than re-deriving, so this exercises exactly what the agent actually
    // saw.
    let _ = view_with_stub;
    let outcome = expand_reduction(
        agent.reduction_log(),
        &full_history,
        None,
        &cleared.id,
        None,
    )
    .unwrap_or_else(|e| panic!("expand_reduction({}) failed: {e}", cleared.id));
    assert!(
        outcome.content.contains(SECRET_FACT),
        "expand_reduction must still recover the byte-exact original span: {}",
        outcome.content
    );

    let question_turn_body = demo_provider
        .captured_question_turn_body
        .lock()
        .unwrap()
        .clone()
        .expect("the question turn must have run");

    // Archive the side-by-side comparison (git-ignored `target/`, mirrors
    // `rehydrate.rs`'s TR-1 dev/06 convention).
    let workspace_target = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target");
    std::fs::create_dir_all(&workspace_target).ok();
    let artifact_path = workspace_target.join("tr7-summary-demo-side-by-side.json");
    let artifact = serde_json::json!({
        "scope_note": "The summarizer used here is a small, deterministic, in-process CANNED \
            fake, not a real model — qualitative real-model summary quality is deferred to the \
            project owner's separate end-to-end differentiator demo. This artifact demonstrates \
            and archives the MECHANISM: the summarized placeholder (not the deterministic stub) \
            reaches the model's request body, carries the fact a later turn needs, and its \
            reduction_id still resolves to the byte-exact original via expand_reduction.",
        "cleared_range": {"first": first, "last": last},
        "reduction_id": cleared.id,
        "deterministic_stub_summaries_off": deterministic_stub,
        "summarized_stub_summaries_on": summarized_stub,
        "question_turn_request_body_contains_fact_directly": question_turn_body.contains(SECRET_FACT),
        "model_answer": reply,
        "expand_reduction_recovers_original": outcome.content.contains(SECRET_FACT),
    });
    std::fs::write(
        &artifact_path,
        serde_json::to_string_pretty(&artifact).unwrap(),
    )
    .unwrap();
    assert!(artifact_path.exists());
    eprintln!(
        "TR-7 dev/06 demo artifact archived at {}",
        artifact_path.display()
    );

    // Sanity: the PROJECTED view (never `agent.history()` itself, which stays
    // full-fidelity/unreduced by design, A5) still carries the placeholder
    // message with this exact `reduction_id`, never drifted across the
    // comparison re-projection above.
    let (current_view, _) = project_messages(
        &full_history,
        agent.reduction_policy().unwrap(),
        agent.reduction_log(),
    );
    let placeholder_msg = current_view
        .iter()
        .find(|m| reduction_id(m) == Some(cleared.id.as_str()))
        .expect("the projected view must still carry the placeholder message");
    assert_eq!(reduction_id(placeholder_msg), Some(cleared.id.as_str()));

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