supercode-harness 0.4.13

The optional native Supercode agent and tool harness
Documentation
//! P4c (COMPOSABLE-HARNESS-DESIGN.md §5.2 P4 "doom-loop breaker", oc UNIQUE
//! `doom_loop` row, catalog D3): a repeated-identical-tool-call counter that
//! refuses the call once it reaches `core.doom_loop_threshold` consecutive
//! repeats. Scripts a `Provider` that repeats the same tool call a fixed
//! number of times, mirroring `tests/model_switch.rs`'s `RecordingProvider`
//! idiom.

use std::sync::atomic::{AtomicUsize, Ordering};

use async_trait::async_trait;
use supercode_harness::{
    Agent, ChatMessage, ChatRequest, Config, FunctionCall, Provider, Role, ToolCall, Usage,
};

/// Scripts `n_repeats` identical `bash` calls (same command every time),
/// then a final plain-text answer.
struct RepeatingToolCallScript {
    call_index: AtomicUsize,
    n_repeats: usize,
    command: String,
}

#[async_trait]
impl Provider for RepeatingToolCallScript {
    async fn complete(
        &self,
        _req: &ChatRequest,
        _on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
    ) -> supercode_harness::Result<(ChatMessage, Usage)> {
        let n = self.call_index.fetch_add(1, Ordering::SeqCst);
        if n < self.n_repeats {
            Ok((
                ChatMessage {
                    role: Role::Assistant,
                    content: None,
                    content_parts: None,
                    tool_calls: Some(vec![ToolCall {
                        id: format!("call_{n}"),
                        kind: "function".into(),
                        function: FunctionCall {
                            name: "bash".into(),
                            arguments: serde_json::json!({"command": self.command}).to_string(),
                        },
                    }]),
                    tool_call_id: None,
                    name: None,
                    metadata: Default::default(),
                },
                Usage::default(),
            ))
        } else {
            Ok((ChatMessage::assistant("done"), Usage::default()))
        }
    }
}

fn temp_cwd() -> std::path::PathBuf {
    let dir = std::env::temp_dir().join(format!(
        "sc-p4c-doomloop-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ));
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

#[tokio::test]
async fn default_off_repeated_identical_calls_all_succeed() {
    let config = Config::builder().cwd(temp_cwd()).build();
    assert_eq!(config.doom_loop_threshold, None);
    let script = RepeatingToolCallScript {
        call_index: AtomicUsize::new(0),
        n_repeats: 5,
        command: "true".to_string(),
    };
    let mut agent = Agent::with_provider(config, Box::new(script));
    let answer = agent.send("go").await.unwrap();
    assert_eq!(answer, "done");
    // Every one of the 5 identical calls actually ran (none blocked): the
    // history has 5 tool-result messages, none of them an error.
    let errors = agent
        .history()
        .iter()
        .filter(|m| m.role == Role::Tool)
        .filter(|m| {
            m.content
                .as_deref()
                .map(|c| c.starts_with("Error:"))
                .unwrap_or(false)
        })
        .count();
    assert_eq!(
        errors, 0,
        "no call should be blocked with the threshold unset"
    );
    let tool_count = agent
        .history()
        .iter()
        .filter(|m| m.role == Role::Tool)
        .count();
    assert_eq!(tool_count, 5);
}

#[tokio::test]
async fn threshold_blocks_the_nth_consecutive_identical_call() {
    let config = Config::builder()
        .cwd(temp_cwd())
        .doom_loop_threshold(3)
        .build();
    let script = RepeatingToolCallScript {
        call_index: AtomicUsize::new(0),
        n_repeats: 4,
        command: "true".to_string(),
    };
    let mut agent = Agent::with_provider(config, Box::new(script));
    agent.send("go").await.unwrap();

    let tool_msgs: Vec<&ChatMessage> = agent
        .history()
        .iter()
        .filter(|m| m.role == Role::Tool)
        .collect();
    assert_eq!(tool_msgs.len(), 4, "all 4 calls still produce a result");
    // Calls 1-2 succeed; call 3 (the 3rd consecutive identical call) is
    // blocked; call 4 is STILL blocked (the streak carries forward, it does
    // not silently reset after one block).
    let is_err = |m: &ChatMessage| {
        m.content
            .as_deref()
            .map(|c| c.starts_with("Error:"))
            .unwrap_or(false)
    };
    assert!(!is_err(tool_msgs[0]), "{:?}", tool_msgs[0].content);
    assert!(!is_err(tool_msgs[1]), "{:?}", tool_msgs[1].content);
    assert!(is_err(tool_msgs[2]), "{:?}", tool_msgs[2].content);
    assert!(
        tool_msgs[2]
            .content
            .as_deref()
            .unwrap()
            .contains("doom-loop"),
        "{:?}",
        tool_msgs[2].content
    );
    assert!(is_err(tool_msgs[3]), "{:?}", tool_msgs[3].content);
}

#[tokio::test]
async fn a_different_call_in_between_resets_the_streak() {
    // Scripts: bash "true", bash "echo hi", bash "true", bash "echo hi" —
    // with threshold 2, no two CONSECUTIVE calls are ever identical (each
    // one differs from its immediate predecessor), so nothing should block
    // even though "true" and "echo hi" each occur twice non-consecutively.
    struct AlternatingScript {
        call_index: AtomicUsize,
    }
    #[async_trait]
    impl Provider for AlternatingScript {
        async fn complete(
            &self,
            _req: &ChatRequest,
            _on_delta: &(dyn for<'a> Fn(&'a str) + Send + Sync),
        ) -> supercode_harness::Result<(ChatMessage, Usage)> {
            let n = self.call_index.fetch_add(1, Ordering::SeqCst);
            let commands = ["true", "echo hi", "true", "echo hi"];
            if n < commands.len() {
                Ok((
                    ChatMessage {
                        role: Role::Assistant,
                        content: None,
                        content_parts: None,
                        tool_calls: Some(vec![ToolCall {
                            id: format!("call_{n}"),
                            kind: "function".into(),
                            function: FunctionCall {
                                name: "bash".into(),
                                arguments: serde_json::json!({"command": commands[n]}).to_string(),
                            },
                        }]),
                        tool_call_id: None,
                        name: None,
                        metadata: Default::default(),
                    },
                    Usage::default(),
                ))
            } else {
                Ok((ChatMessage::assistant("done"), Usage::default()))
            }
        }
    }

    let config = Config::builder()
        .cwd(temp_cwd())
        .doom_loop_threshold(2)
        .build();
    let mut agent = Agent::with_provider(
        config,
        Box::new(AlternatingScript {
            call_index: AtomicUsize::new(0),
        }),
    );
    agent.send("go").await.unwrap();
    let errors = agent
        .history()
        .iter()
        .filter(|m| m.role == Role::Tool)
        .filter(|m| {
            m.content
                .as_deref()
                .map(|c| c.starts_with("Error:"))
                .unwrap_or(false)
        })
        .count();
    assert_eq!(
        errors, 0,
        "no two CONSECUTIVE calls are identical, so the streak counter never reaches 2"
    );
}

#[tokio::test]
async fn threshold_of_one_never_fires_boundary() {
    // §3.1 doc contract: a threshold below 2 can never fire (the FIRST call
    // already "repeats zero times") — proven directly via the boundary.
    let config = Config::builder()
        .cwd(temp_cwd())
        .doom_loop_threshold(1)
        .build();
    let script = RepeatingToolCallScript {
        call_index: AtomicUsize::new(0),
        n_repeats: 3,
        command: "true".to_string(),
    };
    let mut agent = Agent::with_provider(config, Box::new(script));
    agent.send("go").await.unwrap();
    let errors = agent
        .history()
        .iter()
        .filter(|m| m.role == Role::Tool)
        .filter(|m| {
            m.content
                .as_deref()
                .map(|c| c.starts_with("Error:"))
                .unwrap_or(false)
        })
        .count();
    assert_eq!(errors, 0, "threshold=1 must never block anything");
}