autumn-plugin-agent 0.4.0

Autumn plugin: provider-agnostic, always-on LLM agent harness (tool calling, sessions, memory, approvals, heartbeats) for OpenAI-compatible and Anthropic APIs.
Documentation
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]

use super::*;
use crate::error::ErrorKind;
use serde_json::json;

#[tokio::test]
async fn fn_tool_executes() {
    let tool = FnTool::new(
        "echo",
        "Echoes its input.",
        json!({"type": "object"}),
        |input: serde_json::Value| async move { Ok(input) },
    );
    assert_eq!(tool.name(), "echo");
    assert_eq!(tool.description(), "Echoes its input.");
    assert_eq!(tool.input_schema(), json!({"type": "object"}));
    let out = tool
        .execute(json!({"hi": 1}), &ToolContext::detached("c1"))
        .await
        .unwrap();
    assert_eq!(out, json!({"hi": 1}));

    let def = tool.definition();
    assert_eq!(def.name, "echo");
    assert_eq!(def.description, "Echoes its input.");
}

#[tokio::test]
async fn fn_tool_propagates_domain_errors() {
    let tool = FnTool::new(
        "broken",
        "Always fails.",
        json!({"type": "object"}),
        |_input: serde_json::Value| async move { Err(AgentError::new(ErrorKind::Tool, "kaput")) },
    );
    let err = tool
        .execute(json!({}), &ToolContext::detached("c1"))
        .await
        .unwrap_err();
    assert_eq!(err.kind(), ErrorKind::Tool);
}

#[test]
fn fn_tool_debug_hides_the_closure() {
    let tool = FnTool::new("t", "d", json!({}), |input: serde_json::Value| async move {
        Ok(input)
    });
    let debug = format!("{tool:?}");
    assert!(debug.contains("FnTool"), "{debug}");
    assert!(debug.contains("\"t\""), "{debug}");
}

#[tokio::test]
async fn shared_tool_is_object_safe() {
    let tool: Arc<dyn Tool> =
        FnTool::new("t", "d", json!({}), |input: serde_json::Value| async move {
            Ok(input)
        })
        .shared();
    assert_eq!(tool.definition().name, "t");
}

#[tokio::test]
async fn context_tool_sees_the_call_context() {
    let tool = FnTool::with_context(
        "ctx",
        "Echoes the call id.",
        json!({"type": "object"}),
        |_input: serde_json::Value, ctx: ToolContext| async move {
            Ok(json!({"run": ctx.run_id.as_str(), "call": ctx.call_id, "step": ctx.step}))
        },
    );
    let ctx = ToolContext {
        run_id: RunId::new("run-1"),
        call_id: "call-9".to_owned(),
        session_id: Some(SessionId::new("s")),
        step: 3,
    };
    let out = tool.execute(json!({}), &ctx).await.unwrap();
    assert_eq!(out, json!({"run": "run-1", "call": "call-9", "step": 3}));
}

#[test]
fn effects_default_to_write_and_order_by_impact() {
    let tool = FnTool::new("t", "d", json!({}), |input: serde_json::Value| async move {
        Ok(input)
    });
    assert_eq!(Tool::effect(&tool), ToolEffect::Write);
    let tool = tool.effect(ToolEffect::ReadOnly);
    assert_eq!(Tool::effect(&tool), ToolEffect::ReadOnly);
    assert!(ToolEffect::ReadOnly < ToolEffect::Internal);
    assert!(ToolEffect::Internal < ToolEffect::Write);
    assert!(ToolEffect::Write < ToolEffect::External);
    assert_eq!(
        serde_json::to_value(ToolEffect::ReadOnly).unwrap(),
        json!("read_only")
    );
}