agents-kit 0.0.1

Rust agent control Kit for Codex, OpenCode, and provider-backed Custom runtimes
Documentation
use agents_kit::KitError;
use agents_kit::error::{DeadlineKind, ErrorDetail, ErrorKind, ErrorPhase, Operation};
use std::error::Error;
use std::time::Duration;
use tools_kit::ToolError;
use tools_kit::declaration::ToolName;

#[test]
fn tool_timeout_remains_a_timeout() {
    let error = KitError::from(ToolError::Timeout {
        tool: ToolName::new("private-tool-sentinel").expect("valid tool name"),
        timeout_secs: 7,
    });
    assert_eq!(error.kind(), ErrorKind::Timeout, "{error:?}");
    assert_eq!(
        error.detail(),
        Some(&ErrorDetail::DeadlineElapsed {
            kind: DeadlineKind::Request,
            timeout: Some(Duration::from_secs(7)),
        })
    );
    assert!(!format!("{error:?}").contains("private-tool-sentinel"));
}

#[test]
fn normalized_sources_and_cleanup_preserve_safe_evidence() {
    let error = KitError::from(ToolError::BuildCleanup {
        primary: Box::new(ToolError::Timeout {
            tool: ToolName::new("secret-tool").unwrap(),
            timeout_secs: 3,
        }),
        cleanup: Box::new(ToolError::Transport {
            tool: None,
            message: "secret-credential".into(),
        }),
    })
    .with_context(Operation::Build, ErrorPhase::InventoryVerification)
    .with_cause(
        KitError::integration("normalized failure").with_detail(ErrorDetail::ServiceUnavailable),
    );
    let copied = error.clone();
    assert_eq!(copied.kind(), ErrorKind::Timeout);
    assert_eq!(copied.context().operation, Operation::Build);
    assert_eq!(copied.context().phase, ErrorPhase::InventoryVerification);
    assert_eq!(copied.supplementary().len(), 1);
    assert_eq!(
        copied.supplementary()[0].context().phase,
        ErrorPhase::Cleanup
    );
    assert_eq!(
        copied.supplementary()[0].detail(),
        Some(&ErrorDetail::TransportFailure)
    );
    let source = copied.source().expect("normalized source");
    assert!(source.downcast_ref::<KitError>().is_some());
    assert!(source.downcast_ref::<ToolError>().is_none());
    for text in [
        copied.to_string(),
        format!("{copied:?}"),
        source.to_string(),
    ] {
        assert!(!text.contains("secret-"));
    }
}

#[test]
fn dependency_closure_and_cancellation_do_not_invent_agent_terminals() {
    let closed = KitError::from(ToolError::Closed {
        operation: "private-route".into(),
    });
    assert_eq!(closed.kind(), ErrorKind::Integration);
    assert_eq!(closed.detail(), Some(&ErrorDetail::DependencyClosed));
    let cancelled = KitError::from(ToolError::Cancelled {
        tool: ToolName::new("secret-tool").unwrap(),
        reason: Some("secret-reason".into()),
    });
    assert_eq!(cancelled.kind(), ErrorKind::Integration);
    assert_eq!(cancelled.detail(), Some(&ErrorDetail::DependencyCancelled));
}

#[test]
fn retained_trees_are_bounded_and_clones_keep_truncation() {
    let mut deep = KitError::integration("leaf");
    for _ in 0..32 {
        deep = KitError::integration("parent").with_cause(deep);
    }
    let mut depth = 1;
    let mut node = &deep;
    while let Some(cause) = node.cause() {
        depth += 1;
        node = cause;
    }
    assert_eq!(depth, 8);
    assert!(deep.clone().diagnostics_truncated());

    let mut wide = KitError::integration("primary");
    for _ in 0..40 {
        wide = wide.with_supplementary(KitError::integration("secondary"));
    }
    assert_eq!(wide.supplementary().len(), 31);
    assert!(wide.diagnostics_truncated());
    assert_eq!(wide.message(), "primary");

    let error = KitError::integration("é".repeat(4097));
    assert_eq!(error.message().len(), 4096);
    assert!(error.diagnostics_truncated());
    let mut total = error;
    for _ in 0..25 {
        total = total.with_supplementary(KitError::integration("x".repeat(4096)));
    }
    let size = total.message().len()
        + total
            .supplementary()
            .iter()
            .map(|error| error.message().len())
            .sum::<usize>();
    assert_eq!(size, 64 * 1024);
    assert!(total.diagnostics_truncated());
}

#[test]
fn caller_construction_is_clone_send_sync_and_preserves_observations() {
    fn assert_traits<T: Clone + Send + Sync + std::error::Error>() {}
    assert_traits::<KitError>();
    let error = KitError::new(ErrorKind::Integration, "authentication rejected")
        .with_context(Operation::Prompt, ErrorPhase::ModelGeneration)
        .with_detail(ErrorDetail::AuthenticationRejected)
        .with_http_status(401);
    assert_eq!(error.clone().http_status(), Some(401));
    assert!(error.to_string().contains("AuthenticationRejected"));
    assert!(error.to_string().contains("Prompt/ModelGeneration"));
}

struct TimedOutStore;
impl storage_kit::StateStoreBackend for TimedOutStore {
    type Transaction = storage_kit::state::transaction::StateTransaction;
    async fn load(
        &self,
        _key: storage_kit::state::record::StateKey,
    ) -> Result<Option<storage_kit::state::record::StateRecordSnapshot>, storage_kit::StateStoreError>
    {
        Err(storage_kit::StateStoreError::TimedOut {
            operation: "secret-store-operation".into(),
            timeout_ms: 73,
        })
    }
    async fn list(
        &self,
        _request: storage_kit::state::record::StateListRequest,
    ) -> Result<storage_kit::state::record::StateListResponse, storage_kit::StateStoreError> {
        Err(storage_kit::StateStoreError::TimedOut {
            operation: "secret-store-operation".into(),
            timeout_ms: 73,
        })
    }
    async fn begin_transaction(&self) -> Result<Self::Transaction, storage_kit::StateStoreError> {
        Err(storage_kit::StateStoreError::TimedOut {
            operation: "secret-store-operation".into(),
            timeout_ms: 73,
        })
    }
}

#[tokio::test]
async fn storage_timeouts_preserve_duration_and_public_read_operation() {
    let storage = agents_kit::AgentStorage::new(TimedOutStore);
    let error = storage.list_agents().await.expect_err("store timeout");
    assert_eq!(error.kind(), ErrorKind::Timeout);
    assert_eq!(error.context().operation, Operation::ListAgents);
    assert_eq!(error.context().phase, ErrorPhase::StateRead);
    assert_eq!(
        error.detail(),
        Some(&ErrorDetail::DeadlineElapsed {
            kind: DeadlineKind::Operation,
            timeout: Some(Duration::from_millis(73))
        })
    );
    assert!(!format!("{error:?}").contains("secret-store-operation"));
}