supercode-harness 0.5.45

The optional native Volter Harness agent and tool harness
Documentation
//! Deterministic tool-schema tiering (TR-8 / T5).
//!
//! Shrinks what's ADVERTISED to the model, never what's stored: tool
//! definitions are config, never session content (SPEC ground rule 4), so
//! this operates purely on the wire-shape `description`/`parameters` pair
//! built fresh at request time ([`crate::agent::Agent::schema_for`]) — there
//! is nothing here to leak into an export or sidecar, since a tier is never
//! persisted anywhere and is recomputed from the registry + [`crate::Config`]
//! on every call.
//!
//! [`SchemaTier::Medium`] and [`SchemaTier::Minimal`] are deterministic,
//! rule-based text transforms — no LLM in the loop, so the same
//! (registry, tier) pair always produces byte-identical output (dev/05). The
//! model must never see an INVALID schema: `required`, every property's
//! `type`, `enum`, and the `properties`/`items` structure itself are never
//! touched by [`minify`] — only prose (`description`) and the
//! `examples`/`title` metadata keys are stripped or trimmed.

use serde_json::Value;

/// How verbose an advertised tool schema is. `Full` is today's behavior —
/// byte-identical to the tool's own `description()`/`parameters()`. Builtins
/// default to `Full` (small, load-bearing); the win target is fat activated
/// MCP tools (set via the global knob or a per-tool override, see
/// [`crate::Config::tool_schema_tier`] / [`crate::config::ToolOverride::schema_tier`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub enum SchemaTier {
    /// As-shipped: `description()`/`parameters()` verbatim.
    #[default]
    Full,
    /// Trimmed descriptions (top-level and per-param, truncated at a
    /// sentence boundary); `examples`/`title` stripped everywhere.
    /// `required` and every param's `type` are untouched.
    Medium,
    /// One-sentence top-level description; only *required* params keep a
    /// (one-sentence) description — optional params keep name+type only,
    /// with their `description` dropped. `required` and every param's
    /// `type` are untouched, so the schema stays valid and fully typed.
    Minimal,
}

impl SchemaTier {
    /// Parse a config/CLI string form (`"full"` / `"medium"` / `"minimal"`).
    pub fn parse(s: &str) -> Option<Self> {
        match s {
            "full" => Some(SchemaTier::Full),
            "medium" => Some(SchemaTier::Medium),
            "minimal" => Some(SchemaTier::Minimal),
            _ => None,
        }
    }

    /// The canonical string form (round-trips through [`Self::parse`]).
    pub fn as_str(&self) -> &'static str {
        match self {
            SchemaTier::Full => "full",
            SchemaTier::Medium => "medium",
            SchemaTier::Minimal => "minimal",
        }
    }
}

/// Apply `tier` to a tool's advertised `description`/`parameters`, returning
/// the (possibly) minified pair. Deterministic and LLM-free: the same inputs
/// always produce the same output.
///
/// Per-tool byte floor: if the minified wire form (description + serialized
/// parameters) is not smaller than the original, the original is returned
/// unchanged — a tool that's already terse is advertised as-is at any tier,
/// never bloated by the transform.
pub fn minify(description: &str, parameters: &Value, tier: SchemaTier) -> (String, Value) {
    if tier == SchemaTier::Full {
        return (description.to_string(), parameters.clone());
    }
    let budget = if tier == SchemaTier::Minimal { 1 } else { 2 };
    let new_description = truncate_sentences(description, budget);
    let mut new_parameters = parameters.clone();
    minify_node(&mut new_parameters, tier);

    let orig_bytes = description.len()
        + serde_json::to_string(parameters)
            .map(|s| s.len())
            .unwrap_or(0);
    let new_bytes = new_description.len()
        + serde_json::to_string(&new_parameters)
            .map(|s| s.len())
            .unwrap_or(0);
    if new_bytes >= orig_bytes {
        // Byte floor: never grow, and skip the churn on already-terse tools.
        (description.to_string(), parameters.clone())
    } else {
        (new_description, new_parameters)
    }
}

/// Recursively strip `examples`/`title` and trim/drop `description` per
/// `tier`, over a JSON-Schema object node. Applied uniformly at every depth:
/// each object node's own `required` array decides which of ITS OWN
/// `properties` keep a description at [`SchemaTier::Minimal`], so nested
/// object/array schemas get the same required-vs-optional treatment as the
/// tool's direct parameters — never just a single global flag at depth 0.
/// Recursion also follows every JSON-Schema combinator shape a node can
/// hold: `items` (both single-schema and draft-4 tuple-style array-of-
/// schemas), `anyOf`/`oneOf`/`allOf`, `if`/`then`/`else`, and the schema-map
/// keys `$defs`/`definitions`/`patternProperties` — so a schema whose bulk
/// lives inside a combinator gets the same treatment as one that lives
/// directly under `properties`. `required`, `type`, `enum`, and the
/// `properties`/`items` keys themselves are never touched (dev/02:
/// type/required equality across tiers).
fn minify_node(node: &mut Value, tier: SchemaTier) {
    let Some(map) = node.as_object_mut() else {
        return;
    };
    map.remove("examples");
    map.remove("title");

    // Trim this node's own `description` (e.g. an object-typed param's
    // blurb), if present.
    if let Some(Value::String(d)) = map.get("description").cloned() {
        let n = if tier == SchemaTier::Minimal { 1 } else { 2 };
        map.insert(
            "description".to_string(),
            Value::String(truncate_sentences(&d, n)),
        );
    }

    let required: Vec<String> = map
        .get("required")
        .and_then(Value::as_array)
        .map(|a| {
            a.iter()
                .filter_map(|v| v.as_str().map(str::to_string))
                .collect()
        })
        .unwrap_or_default();

    if let Some(props) = map.get_mut("properties").and_then(|p| p.as_object_mut()) {
        let keys: Vec<String> = props.keys().cloned().collect();
        for key in keys {
            let is_required = required.iter().any(|r| r == &key);
            let Some(prop) = props.get_mut(&key) else {
                continue;
            };
            if let Some(pm) = prop.as_object_mut() {
                pm.remove("examples");
                pm.remove("title");
                // `minify_node` is only ever reached for Medium/Minimal
                // (`minify()` returns early for `Full`), so there's no
                // `Full` case to handle here.
                if tier == SchemaTier::Minimal && !is_required {
                    pm.remove("description");
                } else if let Some(Value::String(d)) = pm.get("description").cloned() {
                    pm.insert(
                        "description".to_string(),
                        Value::String(truncate_sentences(&d, 1)),
                    );
                }
            }
            // Recurse into whatever nested schema shape this property
            // holds (nested object `properties`/`required`, array `items`,
            // or a nested combinator) so the same rule applies at every
            // depth, no matter which JSON-Schema shape carries the bulk.
            minify_node(prop, tier);
        }
    }

    // Array `items`: either a single schema, or (draft-4 tuple validation)
    // an array of per-position schemas.
    if let Some(items) = map.get_mut("items") {
        match items {
            Value::Array(items) => {
                for item in items {
                    minify_node(item, tier);
                }
            }
            _ => minify_node(items, tier),
        }
    }

    // Combinator schema lists: each entry is itself a full schema node.
    for key in ["anyOf", "oneOf", "allOf"] {
        if let Some(Value::Array(arr)) = map.get_mut(key) {
            for item in arr {
                minify_node(item, tier);
            }
        }
    }

    // Conditional schema keys: each holds a single schema node.
    for key in ["if", "then", "else"] {
        if let Some(v) = map.get_mut(key) {
            minify_node(v, tier);
        }
    }

    // Schema-map keys: each value is itself a full schema node, keyed by
    // definition name (`$defs`/`definitions`) or regex (`patternProperties`)
    // rather than by required-tracked property name.
    for key in ["$defs", "definitions", "patternProperties"] {
        if let Some(Value::Object(sub)) = map.get_mut(key) {
            for v in sub.values_mut() {
                minify_node(v, tier);
            }
        }
    }
}

/// Common abbreviations whose trailing `.` must not be mistaken for a
/// sentence boundary. Checked as a suffix of the text scanned so far, so
/// multi-period forms like "e.g." are matched whole (the earlier internal
/// `.` in "e.g" is never itself a boundary candidate, since it isn't
/// followed by whitespace).
const ABBREVIATIONS: &[&str] = &["e.g.", "i.e.", "etc.", "Mr.", "Mrs.", "Dr.", "vs.", "cf."];

/// Truncate `s` to at most `n` sentences, cutting only at a sentence
/// boundary (`.`/`!`/`?` immediately followed by whitespace or
/// end-of-string) — never mid-sentence. Abbreviation-aware: a `.` boundary
/// candidate that closes a known abbreviation (see [`ABBREVIATIONS`]), e.g.
/// "e.g." or "Dr.", is not counted as a sentence end. Returns `s` unchanged
/// if fewer than `n` boundaries are found (nothing sensible to cut at).
/// Byte-index-safe: every cut point sits right after a single-byte ASCII
/// punctuation character, which is always a valid UTF-8 boundary regardless
/// of what multi-byte content surrounds it.
fn truncate_sentences(s: &str, n: usize) -> String {
    if n == 0 || s.is_empty() {
        return s.to_string();
    }
    let bytes = s.as_bytes();
    let mut count = 0;
    for (i, &b) in bytes.iter().enumerate() {
        if b == b'.' || b == b'!' || b == b'?' {
            let boundary = i + 1 == bytes.len() || bytes[i + 1] == b' ' || bytes[i + 1] == b'\n';
            if boundary {
                if b == b'.' && ABBREVIATIONS.iter().any(|a| s[..=i].ends_with(a)) {
                    continue;
                }
                count += 1;
                if count >= n {
                    return s[..=i].to_string();
                }
            }
        }
    }
    s.to_string()
}