aethershell 7.0.0

The world's first multi-agent shell with typed functional pipelines and multi-modal AI
//! Return shapes: what a builtin gives back, advertised before it is called.
//!
//! The agent-facing surface has always described *inputs* (`json_schema`) and,
//! since the effect work, *danger* (`x-effect`). It has never described the
//! **result**. With ~1,300 builtins an agent meeting an unfamiliar one has no
//! choice but to run it to discover the shape, read the output, and only then
//! write the pipeline it wanted — a wasted round-trip on the common case, and
//! the reason a typed pipeline language still gets driven like a text shell.
//!
//! A declared shape turns exploration into composition: knowing `git_status`
//! returns `array<record{path:str,staged:bool,…}>` is enough to write
//! `git_status() | where(fn(f) => f.staged) | select("path")` correctly, first
//! try, without the intermediate rows ever entering the context window.
//!
//! # Only proven shapes are advertised
//!
//! Declaring a shape from a builtin's *name* is precisely the reasoning that
//! produced 28 misclassified effects and then 306 more (see `safety::effect_of`
//! and `tests/effect_ratchet.rs`). So this module refuses to guess: every entry
//! in [`DECLARED`] must be reproduced by actually calling the builtin in
//! `tests/return_shapes.rs`, and that test fails both on a declared shape with
//! no proof and on a proof that disagrees with what was declared.
//!
//! The consequence is that [`DECLARED`] is *small and true* rather than large
//! and aspirational. It grows by adding a probe, not by adding a claim.

use crate::value::Value;

/// Fields listed before a record shape is elided. A shape is a hint for
/// composition, not a schema dump; past a dozen fields it costs more tokens
/// than the round-trip it saves.
const MAX_FIELDS: usize = 12;

/// The shape of a value as actually observed, in the compact notation used by
/// [`DECLARED`].
///
/// Scalars render as their type name. An array renders as `array<T>` when every
/// element agrees and `array<any>` when they do not — a ragged array is a real
/// property of the result, so it is reported rather than smoothed over. An
/// empty array is `array<any>`: nothing was observed, and claiming an element
/// type from zero elements would be a guess.
pub fn observe(v: &Value) -> String {
    match v {
        Value::Null => "null".to_string(),
        Value::Bool(_) => "bool".to_string(),
        Value::Int(_) => "int".to_string(),
        Value::Float(_) => "float".to_string(),
        Value::Str(_) => "str".to_string(),
        Value::Uri(_) => "uri".to_string(),
        Value::Table(_) => "table".to_string(),
        Value::Array(items) => {
            let mut it = items.iter().map(observe);
            match it.next() {
                None => "array<any>".to_string(),
                Some(first) => {
                    if it.all(|s| s == first) {
                        format!("array<{first}>")
                    } else {
                        "array<any>".to_string()
                    }
                }
            }
        }
        Value::Record(map) => {
            let mut parts = Vec::new();
            for (k, val) in map.iter().take(MAX_FIELDS) {
                parts.push(format!("{k}:{}", observe(val)));
            }
            let more = map.len().saturating_sub(MAX_FIELDS);
            if more > 0 {
                parts.push(format!("+{more}"));
            }
            format!("record{{{}}}", parts.join(","))
        }
        // Everything else is a value an agent cannot usefully destructure.
        _ => "any".to_string(),
    }
}

/// The advertised return shape of a builtin, or `None` when none has been
/// proven. `None` means "not established", never "returns nothing" — the
/// absence of a claim is deliberate and is not evidence about the builtin.
pub fn shape_of(name: &str) -> Option<&'static str> {
    DECLARED
        .iter()
        .find(|(n, _)| *n == name)
        .map(|(_, shape)| *shape)
}

/// How many builtins currently carry a proven shape. Surfaced so the coverage
/// is visible rather than assumed — the same reason `effect_coverage` reports
/// its fall-through count.
pub fn declared_count() -> usize {
    DECLARED.len()
}

/// Builtin → return shape, every entry proven by `tests/return_shapes.rs`.
///
/// Sorted by name. To add one, add a probe to that test; a claim without a
/// probe fails the build.
/// Note what is *absent*. `first`, `values`, `unique`, `reverse` and `sum` are
/// all probed and all refused: each returns whatever type it was handed
/// (`sum` yields `int` or `float` depending on its operands), so no fixed shape
/// is true of them. A single probe would have "proven" whichever type the test
/// happened to use. They are polymorphic, and this notation cannot yet say so —
/// silence is the honest answer until it can.
pub const DECLARED: &[(&str, &str)] = &[
    ("aecon", "str"),
    ("keys", "array<str>"),
    ("len", "int"),
    // The element shape describes a *populated* listing; an empty directory
    // yields `array<any>`, since nothing was observed to describe.
    (
        "ls",
        "array<record{ext:str,is_dir:bool,modified:int,name:str,path:str,size:int}>",
    ),
    (
        "ontology_manifest",
        "record{categories:array<record{builtins:int,category:str,effects:array<str>}>,effect_legend:array<str>,hint:str,ontology:str,total_builtins:int}",
    ),
    ("pwd", "str"),
    ("range", "array<int>"),
    ("split", "array<str>"),
    ("tokens", "int"),
    ("type_of", "str"),
    ("upper", "str"),
];

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::BTreeMap;

    fn rec(pairs: &[(&str, Value)]) -> Value {
        let mut m = BTreeMap::new();
        for (k, v) in pairs {
            m.insert(k.to_string(), v.clone());
        }
        Value::Record(m)
    }

    #[test]
    fn scalars_observe_as_their_type() {
        assert_eq!(observe(&Value::Int(1)), "int");
        assert_eq!(observe(&Value::Str("a".into())), "str");
        assert_eq!(observe(&Value::Bool(true)), "bool");
        assert_eq!(observe(&Value::Null), "null");
    }

    #[test]
    fn a_uniform_array_reports_its_element_type() {
        let v = Value::Array(vec![Value::Int(1), Value::Int(2)]);
        assert_eq!(observe(&v), "array<int>");
    }

    #[test]
    fn a_ragged_array_is_reported_as_ragged_not_smoothed_over() {
        // Claiming `array<int>` here would be a shape the agent could not rely
        // on — raggedness is a real property of the result.
        let v = Value::Array(vec![Value::Int(1), Value::Str("a".into())]);
        assert_eq!(observe(&v), "array<any>");
    }

    #[test]
    fn an_empty_array_claims_nothing_about_its_elements() {
        assert_eq!(observe(&Value::Array(vec![])), "array<any>");
    }

    #[test]
    fn records_list_fields_with_their_types() {
        let v = rec(&[("b", Value::Str("x".into())), ("a", Value::Int(1))]);
        // BTreeMap ordering makes the rendering stable, which is what lets a
        // declared shape be compared for equality at all.
        assert_eq!(observe(&v), "record{a:int,b:str}");
    }

    #[test]
    fn nested_records_nest() {
        let v = rec(&[("inner", rec(&[("n", Value::Int(1))]))]);
        assert_eq!(observe(&v), "record{inner:record{n:int}}");
    }

    #[test]
    fn a_wide_record_is_elided_with_a_count_rather_than_truncated_silently() {
        let mut m = BTreeMap::new();
        for i in 0..(MAX_FIELDS + 3) {
            m.insert(format!("f{i:02}"), Value::Int(i as i64));
        }
        let s = observe(&Value::Record(m));
        assert!(s.ends_with("+3}"), "expected an elision count, got {s}");
    }

    #[test]
    fn shape_of_is_silent_where_nothing_is_proven() {
        assert_eq!(shape_of("pwd"), Some("str"));
        // Polymorphic: proven *not* to have a fixed shape, so nothing is claimed.
        assert_eq!(shape_of("first"), None);
        assert_eq!(shape_of("no_such_builtin"), None);
    }
}