Skip to main content

harn_vm/composition/
hosts.rs

1use std::collections::BTreeMap;
2use std::rc::Rc;
3
4use serde_json::Value;
5
6use crate::agent_events::ToolCallErrorCategory;
7use crate::value::VmValue;
8use crate::vm::Vm;
9use crate::VmClosure;
10
11use super::manifest::BindingManifestEntry;
12use super::types::{CompositionToolHost, CompositionToolOutput};
13
14pub struct StaticCompositionToolHost {
15    outputs: BTreeMap<String, Value>,
16}
17
18impl StaticCompositionToolHost {
19    pub fn new(outputs: BTreeMap<String, Value>) -> Self {
20        Self { outputs }
21    }
22}
23
24#[async_trait::async_trait(?Send)]
25impl CompositionToolHost for StaticCompositionToolHost {
26    async fn call(&self, binding: &BindingManifestEntry, input: Value) -> CompositionToolOutput {
27        if let Some(value) = self.outputs.get(&binding.name) {
28            return CompositionToolOutput::ok(value.clone());
29        }
30        if let Some(value) = binding.metadata.get("mock_output") {
31            return CompositionToolOutput::ok(value.clone());
32        }
33        CompositionToolOutput::ok(serde_json::json!({
34            "tool": binding.name,
35            "input": input,
36        }))
37    }
38}
39
40/// Dispatches every binding call to a caller-supplied Harn closure.
41///
42/// The closure is compiled in the calling VM and receives
43/// `(binding_name: string, input: dict)`. Returning a value yields a successful
44/// child result; raising an error fails the child call. Each invocation runs
45/// on a fresh clone of the outer VM so closure-side builtins (`host_call`,
46/// pipeline imports, etc.) resolve normally — the inner composition VM only
47/// sees the manifest bindings plus pure helpers.
48pub struct ClosureCompositionToolHost {
49    closure: VmClosure,
50    outer_vm: Vm,
51}
52
53impl ClosureCompositionToolHost {
54    pub fn new(closure: VmClosure, outer_vm: Vm) -> Self {
55        Self { closure, outer_vm }
56    }
57}
58
59#[async_trait::async_trait(?Send)]
60impl CompositionToolHost for ClosureCompositionToolHost {
61    async fn call(&self, binding: &BindingManifestEntry, input: Value) -> CompositionToolOutput {
62        let mut vm = self.outer_vm.child_vm();
63        let args = vec![
64            VmValue::String(Rc::from(binding.name.as_str())),
65            crate::json_to_vm_value(&input),
66        ];
67        match vm.call_closure_pub(&self.closure, &args).await {
68            Ok(value) => {
69                let json = crate::llm::vm_value_to_json(&value);
70                CompositionToolOutput::ok(json)
71            }
72            Err(error) => {
73                CompositionToolOutput::error(error.to_string(), ToolCallErrorCategory::ToolError)
74            }
75        }
76    }
77}