agents-kit 0.0.2

Rust agent control Kit for Codex, OpenCode, and provider-backed Custom runtimes
Documentation
use std::time::Duration;

use agents_kit::error::{ErrorKind, ErrorPhase, Operation};
use agents_kit::model::ModelInfo;
use agents_kit::workspace::WorkspaceDefaults;
use agents_kit::{AgentBackend, AgentBuilder, KitError, WorkspaceBackend};
use serde_json::{Value, json};

use crate::OpenCodeEventProbe;
use crate::native_cleanup::NativeCleanup;

impl OpenCodeEventProbe {
    pub(super) fn provider_inventory() -> Value {
        crate::opencode_release::ReleaseFixture::default().inventory()
    }

    async fn discover_inventories(payloads: Vec<Value>) -> Vec<Result<Vec<ModelInfo>, KitError>> {
        let probe = Self::start(0).await;
        let built = AgentBuilder::create(
            probe.config(Duration::from_secs(2), Duration::from_secs(3)),
            Self::storage().await,
        )
        .build()
        .await;
        let mut cleanup = NativeCleanup::new(Duration::from_secs(3));
        let observation = NativeCleanup::observe(
            tokio::time::Instant::now() + Duration::from_secs(10),
            async {
                let agent = built.as_ref().expect("inventory Agent must build");
                let mut results = Vec::new();
                for payload in payloads {
                    assert!(
                        payload.to_string().len() < 65_536,
                        "fixture body stays bounded"
                    );
                    probe.stats.release.lock().unwrap().inventory = Some(payload);
                    results.push(agent.list_models().await);
                }
                results
            },
        )
        .await;
        if let Ok(agent) = built {
            cleanup.settle(agent.shutdown()).await;
        }
        cleanup
            .settle(async {
                probe.shutdown().await;
                Ok::<(), KitError>(())
            })
            .await;
        cleanup.finish(observation)
    }

    fn provider_pair() -> Value {
        let mut inventory = Self::provider_inventory();
        let mut disconnected = inventory["all"][0].clone();
        disconnected["id"] = json!("disconnected");
        disconnected["source"] = json!("api");
        disconnected["key"] = json!("PRIVATE_KEY");
        disconnected["models"]["model"]["providerID"] = json!("disconnected");
        disconnected["models"]["model"]["name"] = json!("Disconnected Model");
        inventory["all"].as_array_mut().unwrap().push(disconnected);
        inventory["default"]["disconnected"] = json!("model");
        inventory["all"][0]["source"] = json!("env");
        inventory
    }
}

#[tokio::test]
async fn provider_inventory_accepts_boolean_and_arbitrary_string_interleaving() {
    let fields = [
        json!(false),
        json!(true),
        json!({"field":"reasoning"}),
        json!({"field":"reasoning_content"}),
        json!({"field":"reasoning_details"}),
        json!({"field":"reasoning_text"}),
        json!({"field":"PRIVATE_CUSTOM_FIELD"}),
        json!({"field":""}),
        json!({"field":" \t\n"}),
    ];
    let mut payloads = Vec::new();
    let mut labels = Vec::new();
    for field in fields {
        for provider in 0..2 {
            let mut inventory = OpenCodeEventProbe::provider_pair();
            inventory["all"][provider]["models"]["model"]["capabilities"]["interleaved"] =
                field.clone();
            labels.push((provider, field.clone()));
            payloads.push(inventory);
        }
    }
    let results = OpenCodeEventProbe::discover_inventories(payloads).await;
    let failures: Vec<_> = labels
        .iter()
        .zip(&results)
        .filter(|(_, result)| result.is_err())
        .collect();
    assert!(
        failures.is_empty(),
        "source-valid inventory rejected: {failures:?}"
    );
    for models in results {
        let models = models.unwrap();
        assert_eq!(
            models.len(),
            1,
            "only reported connected providers are selectable"
        );
        assert_eq!(models[0].display_name(), Some("Probe Model"));
        assert!(!models[0].is_default());
        assert!(models[0].reasoning_efforts().is_empty());
        assert!(models[0].personalities().is_empty());
        for secret in [
            "PRIVATE_KEY",
            "PRIVATE_CUSTOM_FIELD",
            "interleaved",
            "source",
            "https://example.invalid",
        ] {
            assert!(!format!("{models:?}").contains(secret));
        }
    }
}

#[tokio::test]
async fn provider_inventory_rejects_malformed_interleaving_before_connected_projection() {
    let fields = [
        Value::Null,
        json!(7),
        json!([]),
        json!(["reasoning"]),
        json!(["reasoning_text"]),
        json!([false]),
        json!({"field":"reasoning", "extra":true}),
        json!("reasoning_text"),
        json!({}),
        json!({"field":null}),
        json!({"field":false}),
        json!({"field":7}),
        json!({"field":[]}),
        json!({"field":{}}),
    ];
    let mut payloads = Vec::new();
    let mut labels = Vec::new();
    for field in fields {
        for provider in 0..2 {
            let mut inventory = OpenCodeEventProbe::provider_pair();
            inventory["all"][provider]["models"]["model"]["capabilities"]["interleaved"] =
                field.clone();
            labels.push((provider, Some(field.clone())));
            payloads.push(inventory);
        }
    }
    for provider in 0..2 {
        let mut inventory = OpenCodeEventProbe::provider_pair();
        inventory["all"][provider]["models"]["model"]["capabilities"]
            .as_object_mut()
            .unwrap()
            .remove("interleaved");
        labels.push((provider, None));
        payloads.push(inventory);
    }
    let results = OpenCodeEventProbe::discover_inventories(payloads).await;
    assert_eq!(
        results.len(),
        labels.len(),
        "every malformed inventory is observed"
    );
    for ((provider, interleaved), result) in labels.into_iter().zip(results) {
        let error = result.expect_err(&format!(
            "invalid HTTP shapes cannot become model inventories: provider={provider}, interleaved={interleaved:?}"
        ));
        assert_eq!(error.kind(), ErrorKind::Protocol);
        assert_eq!(error.context().operation, Operation::ListModels);
        assert_eq!(error.context().phase, ErrorPhase::ResponseDecode);
    }
}

#[tokio::test]
async fn provider_inventory_rejects_inconsistent_indexes_and_disconnected_models() {
    let mut payloads = Vec::new();
    for connected in [json!(["provider", "provider"]), json!(["missing"])] {
        let mut inventory = OpenCodeEventProbe::provider_pair();
        inventory["connected"] = connected;
        payloads.push(inventory);
    }
    let mut duplicate = OpenCodeEventProbe::provider_pair();
    let provider = duplicate["all"][0].clone();
    duplicate["all"].as_array_mut().unwrap().push(provider);
    payloads.push(duplicate);
    for provider in ["provider", "disconnected"] {
        let mut missing_default = OpenCodeEventProbe::provider_pair();
        missing_default["default"]
            .as_object_mut()
            .unwrap()
            .remove(provider);
        payloads.push(missing_default);
        let mut invalid_default = OpenCodeEventProbe::provider_pair();
        invalid_default["default"][provider] = json!("missing");
        payloads.push(invalid_default);
    }
    let mut foreign_default = OpenCodeEventProbe::provider_pair();
    foreign_default["default"]["missing"] = json!("model");
    payloads.push(foreign_default);
    for provider in 0..2 {
        for (field, value) in [("id", "mismatched-key"), ("providerID", "foreign")] {
            let mut inventory = OpenCodeEventProbe::provider_pair();
            inventory["all"][provider]["models"]["model"][field] = json!(value);
            payloads.push(inventory);
        }
    }
    for result in OpenCodeEventProbe::discover_inventories(payloads).await {
        let error = result.expect_err("contradictory complete inventory must fail");
        assert_eq!(error.kind(), ErrorKind::Protocol);
        assert_eq!(error.context().operation, Operation::ListModels);
        assert_eq!(error.context().phase, ErrorPhase::InventoryVerification);
    }
}

#[tokio::test]
async fn provider_inventory_uses_connected_membership_and_stable_portable_order() {
    let mut inventory = OpenCodeEventProbe::provider_pair();
    let mut second = inventory["all"][0].clone();
    second["id"] = json!("another");
    second["source"] = json!("config");
    second["models"]["model"]["providerID"] = json!("another");
    second["models"]["model"]["name"] = json!("Probe Model");
    inventory["all"].as_array_mut().unwrap().push(second);
    inventory["connected"] = json!(["provider", "another"]);
    inventory["default"]["another"] = json!("model");
    let mut reversed = inventory.clone();
    reversed["all"].as_array_mut().unwrap().reverse();
    reversed["connected"].as_array_mut().unwrap().reverse();
    let results = OpenCodeEventProbe::discover_inventories(vec![inventory, reversed]).await;
    let mut selections = Vec::new();
    for result in results {
        let models = result.expect("connected membership is independent of keys and source");
        assert_eq!(models.len(), 2);
        assert!(
            models
                .windows(2)
                .all(|pair| pair[0].selector() < pair[1].selector())
        );
        assert!(
            models
                .iter()
                .all(|model| model.display_name() == Some("Probe Model")),
            "equal display names do not merge distinct provider/model identities"
        );
        assert!(models.iter().all(|model| !model.is_default()));
        selections.push(
            models
                .into_iter()
                .map(|model| model.selector().clone())
                .collect::<Vec<_>>(),
        );
    }
    assert_eq!(selections[0], selections[1]);
}

#[tokio::test]
async fn provider_inventory_preserves_exact_selection_without_default_fallback() {
    let probe = OpenCodeEventProbe::start(0).await;
    let built = AgentBuilder::create(
        probe.config(Duration::from_secs(2), Duration::from_secs(3)),
        OpenCodeEventProbe::storage().await,
    )
    .build()
    .await;
    let mut workspace = None;
    let observation = NativeCleanup::observe(
        tokio::time::Instant::now() + Duration::from_secs(10),
        async {
            let agent = built.as_ref().expect("inventory Agent must build");
            let mut inventory = OpenCodeEventProbe::provider_pair();
            let mut selected = inventory["all"][0]["models"]["model"].clone();
            selected["id"] = json!("selected");
            selected["name"] = json!("Configured Model");
            inventory["all"][0]["models"]["selected"] = selected;
            probe.stats.release.lock().unwrap().inventory = Some(inventory.clone());
            let models = agent.list_models().await.unwrap();
            let selector = models
                .iter()
                .find(|model| model.display_name() == Some("Configured Model"))
                .expect("configured model must be present exactly")
                .selector()
                .clone();
            let defaults = WorkspaceDefaults::new(Some(selector), None, None).unwrap();
            workspace = Some(
                agent
                    .new_workspace(OpenCodeEventProbe::workspace_request_with_defaults(
                        defaults,
                    ))
                    .await
                    .unwrap(),
            );
            workspace.as_ref().unwrap().list_tools().await.unwrap();
            assert_eq!(
                *probe.stats.tool_models.lock().unwrap(),
                vec![("provider".to_owned(), "selected".to_owned())]
            );
            let mut disconnected = inventory.clone();
            disconnected["connected"] = json!(["disconnected"]);
            probe.stats.release.lock().unwrap().inventory = Some(disconnected);
            let remaining = agent.list_models().await.unwrap();
            assert_eq!(remaining.len(), 1);
            assert_eq!(remaining[0].display_name(), Some("Disconnected Model"));
            let error =
                workspace.as_ref().unwrap().list_tools().await.expect_err(
                    "disconnected selected provider cannot fall back to another provider",
                );
            assert_eq!(error.kind(), ErrorKind::Unsupported);
            assert_eq!(error.context().operation, Operation::ListTools);
            assert_eq!(
                probe.stats.tool_models.lock().unwrap().len(),
                1,
                "disconnected selection cannot describe a fallback model"
            );

            inventory["all"][0]["models"]
                .as_object_mut()
                .unwrap()
                .remove("selected");
            probe.stats.release.lock().unwrap().inventory = Some(inventory);
            let error = workspace
                .as_ref()
                .unwrap()
                .list_tools()
                .await
                .expect_err("missing selected model cannot fall back to inventory default");
            assert_eq!(error.kind(), ErrorKind::Unsupported);
            assert_eq!(error.context().operation, Operation::ListTools);
            assert_eq!(
                probe.stats.tool_models.lock().unwrap().len(),
                1,
                "failed selection cannot describe a fallback model"
            );
        },
    )
    .await;
    let mut cleanup = NativeCleanup::new(Duration::from_secs(3));
    if let Some(workspace) = workspace {
        cleanup.settle(workspace.close()).await;
    }
    if let Ok(agent) = built {
        cleanup.settle(agent.shutdown()).await;
    }
    cleanup
        .settle(async {
            probe.shutdown().await;
            Ok::<(), KitError>(())
        })
        .await;
    cleanup.finish(observation);
}