unified-agent-api-opencode 0.3.5

Async wrapper around the OpenCode CLI for canonical run --format json flows
Documentation
use std::{
    env, fs,
    io::{self, Write},
    thread,
    time::Duration,
};

const STEP_START: &str = include_str!("../../tests/fixtures/run_json/v1/step_start.jsonl");
const TEXT: &str = include_str!("../../tests/fixtures/run_json/v1/text.jsonl");
const STEP_FINISH: &str = include_str!("../../tests/fixtures/run_json/v1/step_finish.jsonl");

fn first_nonempty_line(text: &str) -> &str {
    text.lines()
        .find(|line| !line.chars().all(|ch| ch.is_whitespace()))
        .expect("fixture contains a non-empty line")
}

fn write_line(out: &mut impl Write, line: &str) -> io::Result<()> {
    out.write_all(line.as_bytes())?;
    out.flush()?;
    Ok(())
}

fn capture_invocation() {
    let capture_path = match env::var("FAKE_OPENCODE_CAPTURE") {
        Ok(path) => path,
        Err(_) => return,
    };

    let payload = serde_json::json!({
        "argv": env::args().skip(1).collect::<Vec<_>>(),
        "cwd": env::current_dir().ok().map(|path| path.display().to_string()),
    });
    fs::write(
        capture_path,
        serde_json::to_vec_pretty(&payload).expect("serialize capture"),
    )
    .expect("write capture");
}

fn main() -> io::Result<()> {
    capture_invocation();

    let scenario =
        env::var("FAKE_OPENCODE_SCENARIO").unwrap_or_else(|_| "three_events_delayed".to_string());

    let step_start = first_nonempty_line(STEP_START);
    let text = first_nonempty_line(TEXT);
    let step_finish = first_nonempty_line(STEP_FINISH);
    let mut out = io::stdout().lock();

    match scenario.as_str() {
        "capture_args" => {
            write_line(&mut out, &format!("{step_start}\n"))?;
            write_line(&mut out, &format!("{text}\n"))?;
            write_line(&mut out, &format!("{step_finish}\n"))?;
        }
        "crlf_blank_lines" => {
            write_line(&mut out, "\r\n")?;
            write_line(&mut out, "   \r\n")?;
            write_line(&mut out, &format!("{step_start}\r\n"))?;
            write_line(&mut out, "\r\n")?;
            write_line(&mut out, &format!("{text}\r\n"))?;
            write_line(&mut out, &format!("{step_finish}\r\n"))?;
        }
        "parse_error_redaction" => {
            let secret = "VERY_SECRET_SHOULD_NOT_APPEAR";
            write_line(&mut out, &format!("not json {secret}\n"))?;
            write_line(&mut out, &format!("{step_start}\n"))?;
            write_line(&mut out, &format!("{text}\n"))?;
            write_line(&mut out, &format!("{step_finish}\n"))?;
        }
        "slow_until_killed" => {
            write_line(&mut out, &format!("{step_start}\n"))?;
            thread::sleep(Duration::from_secs(30));
        }
        "session_not_found_last" => {
            eprintln!("NO SESSION found for SECRET_LAST_SESSION_SCOPE");
            std::process::exit(2);
        }
        "session_not_found_id" => {
            eprintln!("session not found: SECRET_SESSION_ID_DO_NOT_LEAK");
            std::process::exit(2);
        }
        "runtime_failure_invalid_model" => {
            eprintln!("invalid model SECRET_MODEL_REJECTION_DO_NOT_LEAK");
            std::process::exit(3);
        }
        _ => {
            write_line(&mut out, &format!("{step_start}\n"))?;
            thread::sleep(Duration::from_millis(150));
            write_line(&mut out, &format!("{text}\n"))?;
            thread::sleep(Duration::from_millis(150));
            write_line(&mut out, &format!("{step_finish}\n"))?;
        }
    }

    Ok(())
}