holon 0.14.1

A headless, event-driven runtime for long-lived agents
Documentation
use std::{fs, path::PathBuf, sync::Arc};

use anyhow::Result;
use async_trait::async_trait;
use holon::{
    provider::{
        AgentProvider, ConversationMessage, ModelBlock, ProviderTurnRequest, ProviderTurnResponse,
    },
    types::{MessageBody, MessageEnvelope, MessageKind, MessageOrigin, Priority, TrustLevel},
};
use serde::Deserialize;
use tempfile::tempdir;
use tokio::sync::Mutex;
mod support;

use support::{eventually, RuntimeHarness, TestConfigBuilder};

#[derive(Debug, Deserialize)]
struct RegressionFixture {
    prompt: String,
    steps: Vec<FixtureStep>,
    expected_file: String,
    expected_content: String,
    expected_brief: String,
}

#[derive(Debug, Clone, Deserialize)]
struct FixtureStep {
    blocks: Vec<ModelBlock>,
}

struct FixtureProvider {
    steps: Vec<FixtureStep>,
    index: Mutex<usize>,
}

impl FixtureProvider {
    fn new(steps: Vec<FixtureStep>) -> Self {
        Self {
            steps,
            index: Mutex::new(0),
        }
    }
}

#[async_trait]
impl AgentProvider for FixtureProvider {
    async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
        let mut index = self.index.lock().await;
        let step = self.steps.get(*index).cloned().unwrap_or(FixtureStep {
            blocks: vec![ModelBlock::Text {
                text: "fixture exhausted".into(),
            }],
        });
        *index += 1;

        if *index > 1 {
            let has_exact_tool_results = request
                .conversation
                .iter()
                .any(|message| matches!(message, ConversationMessage::UserToolResults(_)));
            let has_turn_local_recap = request.conversation.iter().any(|message| {
                matches!(
                    message,
                    ConversationMessage::UserText(text)
                        if text.contains("Turn-local recap for older completed rounds")
                )
            });
            assert!(
                has_exact_tool_results || has_turn_local_recap,
                "continuation request should preserve prior tool context via exact tool results or a turn-local recap: {:?}",
                request.conversation
            );
        }

        Ok(ProviderTurnResponse {
            blocks: step.blocks,
            stop_reason: None,
            input_tokens: 100,
            output_tokens: 50,
            cache_usage: None,
            request_diagnostics: None,
        })
    }
}
#[tokio::test]
async fn fixture_coding_loop_regression_stays_green() -> Result<()> {
    let fixture_path =
        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/write_and_verify.json");
    let fixture: RegressionFixture = serde_json::from_str(&fs::read_to_string(&fixture_path)?)?;

    let workspace = tempdir()?.keep();
    let data_dir = tempdir()?.keep();
    fs::create_dir_all(&workspace)?;
    let provider = FixtureProvider::new(fixture.steps.clone());

    let harness = RuntimeHarness::with_config_and_provider(
        TestConfigBuilder::new()
            .with_workspace_dir(workspace.clone())
            .with_data_dir(data_dir)
            .build(),
        Arc::new(provider),
    )
    .await?;
    let runtime = harness.runtime.clone();

    runtime
        .enqueue(MessageEnvelope::new(
            "default",
            MessageKind::OperatorPrompt,
            MessageOrigin::Operator { actor_id: None },
            TrustLevel::TrustedOperator,
            Priority::Normal,
            MessageBody::Text {
                text: fixture.prompt,
            },
        ))
        .await?;

    let expected_path = workspace.join(&fixture.expected_file);
    eventually(|| Ok(expected_path.exists())).await?;

    let content = fs::read_to_string(expected_path)?;
    assert_eq!(content, fixture.expected_content);

    eventually(|| {
        let briefs = runtime.storage().read_recent_briefs(10)?;
        Ok(briefs
            .iter()
            .any(|brief| brief.text.contains(&fixture.expected_brief)))
    })
    .await?;
    Ok(())
}