holon 0.14.1

A headless, event-driven runtime for long-lived agents
Documentation
use anyhow::{anyhow, Result};
use serde::Serialize;
use serde_json::Value;

use crate::{
    runtime::RuntimeHandle,
    tool::{apply_patch::ApplyPatchSurface, ToolCall, ToolResult, ToolSpec},
    types::{ToolCapabilityFamily, TrustLevel},
};

pub(crate) mod agent_get;
pub(crate) mod apply_patch_tool;
pub(crate) mod cancel_external_trigger;
pub(crate) mod complete_work_item;
pub(crate) mod create_external_trigger;
pub(crate) mod create_work_item;
pub(crate) mod enqueue;
pub(crate) mod exec_command;
pub(crate) mod exec_command_batch;
pub(crate) mod get_work_item;
pub(crate) mod list_work_items;
pub(crate) mod memory_get;
pub(crate) mod memory_search;
pub(crate) mod notify_operator;
pub(crate) mod pick_work_item;
pub(crate) mod sleep;
pub(crate) mod spawn_agent;
pub(crate) mod task_input;
pub(crate) mod task_list;
pub(crate) mod task_output;
pub(crate) mod task_status;
pub(crate) mod task_stop;
pub(crate) mod update_work_item;
pub(crate) mod use_workspace;
pub(crate) mod web_fetch;
pub(crate) mod web_search;
pub(crate) mod work_item_action;
pub(crate) mod work_item_query;

pub(crate) struct BuiltinToolDefinition {
    pub(crate) family: ToolCapabilityFamily,
    pub(crate) spec: ToolSpec,
}

pub(crate) fn builtin_tool_definitions() -> Result<Vec<BuiltinToolDefinition>> {
    Ok(vec![
        sleep::definition()?,
        agent_get::definition()?,
        notify_operator::definition()?,
        enqueue::definition()?,
        spawn_agent::definition()?,
        task_list::definition()?,
        task_status::definition()?,
        task_input::definition()?,
        task_output::definition()?,
        task_stop::definition()?,
        create_work_item::definition()?,
        pick_work_item::definition()?,
        get_work_item::definition()?,
        list_work_items::definition()?,
        update_work_item::definition()?,
        complete_work_item::definition()?,
        memory_search::definition()?,
        memory_get::definition()?,
        apply_patch_tool::definition()?,
        exec_command::definition()?,
        exec_command_batch::definition()?,
        use_workspace::definition()?,
        web_fetch::definition()?,
        web_search::definition()?,
    ])
}

pub(crate) fn builtin_tool_definitions_for_apply_patch_surface(
    surface: ApplyPatchSurface,
) -> Result<Vec<BuiltinToolDefinition>> {
    builtin_tool_definitions()?
        .into_iter()
        .map(|definition| {
            if definition.spec.name == apply_patch_tool::NAME {
                apply_patch_tool::definition_for_surface(surface)
            } else {
                Ok(definition)
            }
        })
        .collect()
}

pub(crate) async fn execute_builtin_tool(
    runtime: &RuntimeHandle,
    agent_id: &str,
    trust: &TrustLevel,
    call: &ToolCall,
) -> Result<ToolResult> {
    match call.name.as_str() {
        sleep::NAME => sleep::execute(runtime, agent_id, trust, &call.input).await,
        agent_get::NAME => agent_get::execute(runtime, agent_id, trust, &call.input).await,
        notify_operator::NAME => {
            notify_operator::execute(runtime, agent_id, trust, &call.input).await
        }
        enqueue::NAME => enqueue::execute(runtime, agent_id, trust, &call.input).await,
        spawn_agent::NAME => spawn_agent::execute(runtime, agent_id, trust, &call.input).await,
        task_list::NAME => task_list::execute(runtime, agent_id, trust, &call.input).await,
        task_status::NAME => task_status::execute(runtime, agent_id, trust, &call.input).await,
        task_input::NAME => task_input::execute(runtime, agent_id, trust, &call.input).await,
        task_output::NAME => task_output::execute(runtime, agent_id, trust, &call.input).await,
        task_stop::NAME => task_stop::execute(runtime, agent_id, trust, &call.input).await,
        create_work_item::NAME => {
            create_work_item::execute(runtime, agent_id, trust, &call.input).await
        }
        pick_work_item::NAME => {
            pick_work_item::execute(runtime, agent_id, trust, &call.input).await
        }
        get_work_item::NAME => get_work_item::execute(runtime, agent_id, trust, &call.input).await,
        list_work_items::NAME => {
            list_work_items::execute(runtime, agent_id, trust, &call.input).await
        }
        update_work_item::NAME => {
            update_work_item::execute(runtime, agent_id, trust, &call.input).await
        }
        complete_work_item::NAME => {
            complete_work_item::execute(runtime, agent_id, trust, &call.input).await
        }
        memory_search::NAME => memory_search::execute(runtime, agent_id, trust, &call.input).await,
        memory_get::NAME => memory_get::execute(runtime, agent_id, trust, &call.input).await,
        create_external_trigger::NAME => {
            create_external_trigger::execute(runtime, agent_id, trust, &call.input).await
        }
        cancel_external_trigger::NAME => {
            cancel_external_trigger::execute(runtime, agent_id, trust, &call.input).await
        }
        apply_patch_tool::NAME => {
            apply_patch_tool::execute(runtime, agent_id, trust, &call.input).await
        }
        exec_command::NAME => exec_command::execute(runtime, agent_id, trust, &call.input).await,
        exec_command_batch::NAME => {
            exec_command_batch::execute(runtime, agent_id, trust, &call.input).await
        }
        use_workspace::NAME => use_workspace::execute(runtime, agent_id, trust, &call.input).await,
        web_fetch::NAME => web_fetch::execute(runtime, agent_id, trust, &call.input).await,
        web_search::NAME => web_search::execute(runtime, agent_id, trust, &call.input).await,
        _ => Err(anyhow!("unknown builtin tool {}", call.name)),
    }
}

pub(crate) fn render_tool_result_for_model(result: &ToolResult) -> Result<String> {
    if result.is_error() {
        let error = result
            .tool_error()
            .ok_or_else(|| anyhow!("tool error result missing error payload"))?;
        return Ok(error.render_for_model(Some(&result.envelope.tool_name)));
    }

    match result.envelope.tool_name.as_str() {
        apply_patch_tool::NAME => apply_patch_tool::render_for_model(result),
        exec_command::NAME => exec_command::render_for_model(result),
        exec_command_batch::NAME => exec_command_batch::render_for_model(result),
        task_output::NAME => task_output::render_for_model(result),
        _ => canonical_json_render(result),
    }
}

pub(crate) fn canonical_json_render(result: &ToolResult) -> Result<String> {
    serde_json::to_string(&result.envelope).map_err(Into::into)
}

pub(crate) fn serialize_success<T: Serialize>(tool_name: &str, value: &T) -> Result<ToolResult> {
    let value = serde_json::to_value(value)?;
    Ok(success_from_value(tool_name, value))
}

pub(crate) fn success_from_value(tool_name: &str, mut value: Value) -> ToolResult {
    let summary_text = match &mut value {
        Value::Object(map) => map
            .remove("summary_text")
            .and_then(|value| value.as_str().map(ToString::to_string)),
        _ => None,
    };
    ToolResult::success(tool_name, value, summary_text)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn non_command_tools_default_to_canonical_json_render() {
        let result = ToolResult::success(
            "AgentGet",
            serde_json::json!({"agent": {"id": "default"}}),
            None,
        );
        let rendered = render_tool_result_for_model(&result).unwrap();
        assert!(rendered.starts_with("{\"tool_name\":\"AgentGet\""));
    }

    #[test]
    fn tool_errors_use_shared_model_visible_receipt() {
        let result = ToolResult::error(
            "ExecCommand",
            crate::tool::ToolError::new("invalid_tool_input", "missing required field")
                .with_details(serde_json::json!({ "field": "cmd" }))
                .with_recovery_hint("provide `cmd`"),
        );
        let rendered = render_tool_result_for_model(&result).unwrap();
        let receipt: serde_json::Value = serde_json::from_str(&rendered).unwrap();

        assert_eq!(receipt["ok"], false);
        assert_eq!(receipt["tool_name"], "ExecCommand");
        assert_eq!(receipt["kind"], "invalid_tool_input");
        assert_eq!(receipt["field"], "cmd");
        assert_eq!(receipt["hint"], "provide `cmd`");
    }
}