use std::sync::Arc;
use juncture_core::{config::RunnableConfig, state::State, store::Store, stream::ToolsEvent};
pub struct ToolRuntime<S: State> {
pub state: S,
pub tool_call_id: String,
pub config: RunnableConfig,
pub store: Option<Arc<dyn Store>>,
tools_event_tx: Option<tokio::sync::mpsc::UnboundedSender<ToolsEvent>>,
}
impl<S: State> ToolRuntime<S> {
#[must_use]
pub fn new(
state: S,
tool_call_id: String,
config: RunnableConfig,
store: Option<Arc<dyn Store>>,
) -> Self {
Self {
state,
tool_call_id,
config,
store,
tools_event_tx: None,
}
}
#[must_use]
pub fn new_without_store(state: S, tool_call_id: String, config: RunnableConfig) -> Self {
Self {
state,
tool_call_id,
config,
store: None,
tools_event_tx: None,
}
}
#[must_use]
pub fn with_tools_event_tx(
mut self,
tx: tokio::sync::mpsc::UnboundedSender<ToolsEvent>,
) -> Self {
self.tools_event_tx = Some(tx);
self
}
#[must_use]
pub const fn state(&self) -> &S {
&self.state
}
#[must_use]
pub fn tool_call_id(&self) -> &str {
&self.tool_call_id
}
#[must_use]
pub const fn config(&self) -> &RunnableConfig {
&self.config
}
#[must_use]
pub const fn store(&self) -> Option<&Arc<dyn Store>> {
self.store.as_ref()
}
pub fn emit_output_delta(&self, delta: &str) {
if let Some(ref tx) = self.tools_event_tx {
let event = ToolsEvent::ToolOutputDelta {
tool_call_id: self.tool_call_id.clone(),
delta: delta.to_string(),
};
let _ = tx.send(event);
}
tracing::debug!(
tool_call_id = %self.tool_call_id,
delta_len = delta.len(),
"tool output delta emitted"
);
}
}
impl<S: State> Clone for ToolRuntime<S> {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
tool_call_id: self.tool_call_id.clone(),
config: self.config.clone(),
store: self.store.clone(),
tools_event_tx: self.tools_event_tx.clone(),
}
}
}
impl<S: State> std::fmt::Debug for ToolRuntime<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ToolRuntime")
.field("tool_call_id", &self.tool_call_id)
.field("config", &self.config)
.field(
"store",
&self.store.as_ref().map_or("None", |_| "Some(...)"),
)
.field(
"tools_event_tx",
&self.tools_event_tx.as_ref().map_or("None", |_| "Some(...)"),
)
.field("state", &std::any::type_name::<S>())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use juncture_core::state::FieldsChanged;
#[derive(Clone, Debug, Default)]
struct TestState;
impl juncture_core::State for TestState {
type Update = TestStateUpdate;
type FieldVersions = juncture_core::state::FieldVersions;
fn apply(&mut self, _update: Self::Update) -> FieldsChanged {
FieldsChanged(0)
}
fn reset_ephemeral(&mut self) {}
}
#[derive(Clone, Debug, Default)]
struct TestStateUpdate;
#[test]
fn test_tool_runtime_new() {
let state = TestState;
let config = RunnableConfig::default();
let runtime = ToolRuntime::new_without_store(state, "call_123".to_string(), config);
assert_eq!(runtime.tool_call_id, "call_123");
}
#[test]
fn test_tool_runtime_accessors() {
let state = TestState;
let config = RunnableConfig::default();
let runtime = ToolRuntime::new_without_store(state, "call_123".to_string(), config);
assert_eq!(runtime.tool_call_id(), "call_123");
}
#[test]
fn test_tool_runtime_clone() {
let state = TestState;
let config = RunnableConfig::default();
let runtime = ToolRuntime::new_without_store(state, "call_123".to_string(), config);
let cloned = runtime.clone();
assert_eq!(cloned.tool_call_id, runtime.tool_call_id);
}
#[test]
fn test_tool_runtime_debug() {
let state = TestState;
let config = RunnableConfig::default();
let runtime = ToolRuntime::new_without_store(state, "call_123".to_string(), config);
let debug_str = format!("{runtime:?}");
assert!(debug_str.contains("ToolRuntime"));
assert!(debug_str.contains("call_123"));
}
#[test]
fn test_emit_output_delta_does_not_panic() {
let state = TestState;
let config = RunnableConfig::default();
let runtime = ToolRuntime::new_without_store(state, "call_456".to_string(), config);
runtime.emit_output_delta("partial result chunk");
runtime.emit_output_delta("");
runtime.emit_output_delta("another delta");
}
#[test]
fn test_emit_output_delta_with_tx_sends_event() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let state = TestState;
let config = RunnableConfig::default();
let runtime = ToolRuntime::new_without_store(state, "call_delta".to_string(), config)
.with_tools_event_tx(tx);
runtime.emit_output_delta("delta_chunk");
let event = rx.try_recv().expect("should receive ToolOutputDelta");
match event {
juncture_core::stream::ToolsEvent::ToolOutputDelta {
tool_call_id,
delta,
} => {
assert_eq!(tool_call_id, "call_delta");
assert_eq!(delta, "delta_chunk");
}
other => panic!("expected ToolOutputDelta, got {other:?}"),
}
}
}