starweaver-runtime 0.3.0

Agent-loop graph and runtime executor primitives for Starweaver
Documentation
#![allow(missing_docs, clippy::unwrap_used)]

use std::sync::Arc;

use starweaver_core::{ConversationId, RunId};
use starweaver_model::{
    ModelMessage, ModelRequest, ModelRequestPart, ModelResponse, ModelResponseStreamEvent,
    PartDelta, PartEnd, PartStart, TestModel, ToolCallPart,
};
use starweaver_runtime::{DirectModelRequest, model_request, model_request_stream, tool_call};
use starweaver_tools::{FunctionTool, ToolContext, ToolRegistry, ToolResult};

fn user_message(text: &str) -> ModelMessage {
    ModelMessage::Request(ModelRequest {
        parts: vec![ModelRequestPart::UserPrompt {
            content: vec![starweaver_model::ContentPart::Text {
                text: text.to_string(),
            }],
            name: None,
            metadata: serde_json::Map::new(),
        }],
        timestamp: None,
        instructions: None,
        run_id: None,
        conversation_id: None,
        metadata: serde_json::Map::new(),
    })
}

#[tokio::test]
async fn direct_model_request_returns_response() {
    let model = TestModel::with_text("direct");
    let response = model_request(
        &model,
        DirectModelRequest::new(vec![user_message("hello")]).with_ids(
            RunId::from_string("run_direct"),
            ConversationId::from_string("conv_direct"),
        ),
    )
    .await
    .unwrap();

    assert_eq!(response.text_output(), "direct");
    assert_eq!(model.captured_messages().len(), 1);
}

#[tokio::test]
async fn direct_model_request_uses_stream_final_events() {
    let model = TestModel::with_stream_events(vec![vec![ModelResponseStreamEvent::FinalResult(
        Box::new(ModelResponse::text("direct streamed")),
    )]]);

    let response = model_request(&model, DirectModelRequest::new(vec![user_message("hello")]))
        .await
        .unwrap();

    assert_eq!(response.text_output(), "direct streamed");
    assert_eq!(model.captured_messages().len(), 1);
}

#[tokio::test]
async fn direct_model_stream_falls_back_to_final_result() {
    let model = TestModel::with_text("stream");
    let events = model_request_stream(&model, DirectModelRequest::new(vec![user_message("hello")]))
        .await
        .unwrap();

    assert_eq!(
        events,
        vec![ModelResponseStreamEvent::FinalResult(Box::new(
            ModelResponse::text("stream")
        ))]
    );
}

#[tokio::test]
async fn direct_tool_call_executes_registry_tool() {
    let tool = FunctionTool::new(
        "echo",
        Some("Echo arguments".to_string()),
        serde_json::json!({"type": "object"}),
        |_ctx: ToolContext, args: serde_json::Value| async move { Ok(ToolResult::new(args)) },
    );
    let tools = ToolRegistry::new().with_tool(Arc::new(tool));
    let context = ToolContext::new(
        RunId::from_string("run_tool"),
        ConversationId::from_string("conv_tool"),
        0,
    );
    let call = ToolCallPart {
        id: "call_1".to_string(),
        name: "echo".to_string(),
        arguments: serde_json::json!({"value": 42}).into(),
    };

    let result = tool_call(&tools, context, &call).await;

    assert_eq!(result.name, "echo");
    assert!(!result.is_error);
    assert_eq!(result.content["value"], 42);
}

#[test]
fn model_stream_events_remain_replay_serializable() {
    let events = vec![
        ModelResponseStreamEvent::PartStart(PartStart {
            index: 0,
            part_kind: "text".to_string(),
        }),
        ModelResponseStreamEvent::PartDelta(PartDelta::text(0, "ok")),
        ModelResponseStreamEvent::PartEnd(PartEnd::with_kind(0, "text")),
        ModelResponseStreamEvent::FinalResult(Box::new(ModelResponse::text("ok"))),
    ];

    let encoded = serde_json::to_value(&events).unwrap();
    let decoded: Vec<ModelResponseStreamEvent> = serde_json::from_value(encoded).unwrap();
    assert_eq!(decoded, events);
}