use std::collections::BTreeMap;
use std::sync::atomic::AtomicU32;
use std::sync::{Arc, Mutex};
use crate::client::ToolSchema;
use crate::debug::DebugCapture;
use crate::lua::{
LiveBindingProducer, ModelInferHook, SectionVm, ToolBindings, ToolCallCounts, ToolRuntime,
ToolScope, install_lua_tool_calls, snapshot_tool_scope,
};
use crate::model::ModelBinding;
use crate::observe::Observer;
use crate::tools::{SharedTools, ToolId, ToolRegistry};
use crate::{Error, Result};
use super::gateway::GatewaySource;
use super::scope::{ToolAnalysis, prepare_scoped_tools, validate_effective_scope_inner};
use super::support::bridge_blocking;
use super::tool_loop::{SectionProgress, run_tool_loop};
struct CachedToolState {
generation: u64,
scope: ToolScope,
schemas: Vec<ToolSchema>,
dispatch: BTreeMap<String, ToolId>,
}
pub(crate) struct PreparedTools {
pub(crate) scope: ToolScope,
pub(crate) schemas: Vec<ToolSchema>,
pub(crate) dispatch: BTreeMap<String, ToolId>,
#[cfg(test)]
pub(crate) reused: bool,
}
pub(crate) struct ToolBag {
bindings: ToolBindings,
runtime: Arc<Mutex<ToolRuntime>>,
cached: Option<CachedToolState>,
}
impl ToolBag {
#[must_use]
pub(crate) fn new(bindings: ToolBindings, runtime: Arc<Mutex<ToolRuntime>>) -> Self {
Self {
bindings,
runtime,
cached: None,
}
}
#[must_use]
pub(crate) fn bindings(&self) -> &ToolBindings {
&self.bindings
}
pub(crate) fn prepare(&mut self, registry: &ToolRegistry<'_>) -> Result<PreparedTools> {
let generation = {
let runtime = self
.runtime
.lock()
.map_err(|_| Error::Lua("tool declaration runtime was poisoned".to_owned()))?;
runtime.generation()
};
if let Some(cached) = &self.cached
&& cached.generation == generation
{
return Ok(PreparedTools {
scope: cached.scope.clone(),
schemas: cached.schemas.clone(),
dispatch: cached.dispatch.clone(),
#[cfg(test)]
reused: true,
});
}
let scope = snapshot_tool_scope(&self.bindings, &self.runtime)?;
let (schemas, dispatch) = prepare_scoped_tools(&scope, registry)?;
self.cached = Some(CachedToolState {
generation,
scope: scope.clone(),
schemas: schemas.clone(),
dispatch: dispatch.clone(),
});
Ok(PreparedTools {
scope,
schemas,
dispatch,
#[cfg(test)]
reused: false,
})
}
}
pub(crate) struct InferContext {
client: GatewaySource,
shared_tools: SharedTools,
observer: Arc<dyn Observer>,
debug: Option<Arc<dyn DebugCapture>>,
execution: String,
section: String,
max_tool_iterations: usize,
turns: Arc<AtomicU32>,
analysis: Option<ToolAnalysis>,
live_bindings: Option<LiveBindingProducer>,
tool_bag: Mutex<ToolBag>,
counts_slot: Arc<Mutex<Option<ToolCallCounts>>>,
sys_live: Arc<Mutex<Option<serde_json::Value>>>,
}
impl InferContext {
fn prepare_tools(
&self,
registry: &ToolRegistry<'_>,
) -> mlua::Result<(PreparedTools, Vec<String>)> {
if let Some(live) = &self.live_bindings {
let bindings = live.bindings().map_err(mlua::Error::external)?.0;
let scope = ToolScope::from_bindings(
bindings
.always()
.iter()
.filter_map(|alias| {
bindings
.bindings()
.iter()
.find(|binding| binding.alias() == alias)
.cloned()
})
.collect(),
);
let (schemas, dispatch) =
prepare_scoped_tools(&scope, registry).map_err(mlua::Error::external)?;
let declared = bindings
.bindings()
.iter()
.map(|binding| binding.alias().to_owned())
.collect();
return Ok((
PreparedTools {
scope,
schemas,
dispatch,
#[cfg(test)]
reused: false,
},
declared,
));
}
let mut bag = self
.tool_bag
.lock()
.map_err(|_| mlua::Error::external("tool bag mutex was poisoned"))?;
let prepared = bag.prepare(registry).map_err(mlua::Error::external)?;
if let Some(analysis) = &self.analysis {
validate_effective_scope_inner(analysis, &prepared.scope)
.map_err(mlua::Error::external)?;
}
let declared = bag
.bindings()
.bindings()
.iter()
.map(|binding| binding.alias().to_owned())
.collect();
Ok((prepared, declared))
}
fn infer(
self: &Arc<Self>,
lua: &mlua::Lua,
binding: &ModelBinding,
prompt: &str,
) -> mlua::Result<String> {
let registry = self.shared_tools.registry();
let (prepared, declared) = self.prepare_tools(®istry)?;
let counts = {
let mut slot = self
.counts_slot
.lock()
.map_err(|_| mlua::Error::external("tool call counts mutex was poisoned"))?;
if let Some(existing) = slot.as_ref() {
for tool in prepared.scope.bindings() {
existing
.ensure(tool.alias())
.map_err(mlua::Error::external)?;
}
existing.clone()
} else {
let created = ToolCallCounts::new(
prepared
.scope
.bindings()
.iter()
.map(|b| b.alias().to_owned()),
);
*slot = Some(created.clone());
created
}
};
install_lua_tool_calls(lua, &counts, &declared).map_err(mlua::Error::external)?;
let completion_options = binding.completion_options();
let client = self.client.resolve().map_err(mlua::Error::external)?;
let (text, finish_reason) = bridge_blocking(run_tool_loop(
&client,
&prepared.schemas,
&prepared.dispatch,
®istry,
prompt.to_owned(),
self.max_tool_iterations,
SectionProgress {
execution: &self.execution,
observer: self.observer.as_ref(),
section: &self.section,
turns: self.turns.as_ref(),
debug: self.debug.as_deref(),
completion_options: &completion_options,
},
Some(&counts),
Some(&prepared.dispatch),
))
.map_err(mlua::Error::external)?;
lua.globals()
.raw_set("reply", text.as_str())
.map_err(mlua::Error::external)?;
{
let mut live = self
.sys_live
.lock()
.map_err(|_| mlua::Error::external("sys live slot was poisoned"))?;
if let Some(sys) = live.as_mut() {
*sys = crate::lua::enrich_sys_reply_finish_reason(sys, finish_reason.as_deref());
let table = crate::lua::seal_sys(lua, sys).map_err(mlua::Error::external)?;
lua.globals()
.raw_set("sys", table)
.map_err(mlua::Error::external)?;
}
}
Ok(text)
}
}
#[expect(
clippy::too_many_arguments,
reason = "infer hook installation threads the same borrowed run context fanout already carries"
)]
pub(crate) fn attach_infer_hook(
vm: &SectionVm,
client: GatewaySource,
shared_tools: &SharedTools,
observer: Arc<dyn Observer>,
debug: Option<Arc<dyn DebugCapture>>,
execution: &str,
section: &str,
max_tool_iterations: usize,
turns: &Arc<AtomicU32>,
analysis: Option<&ToolAnalysis>,
live_bindings: Option<LiveBindingProducer>,
) {
let (tool_bindings, tool_runtime) = vm.tool_bag_handles();
let ctx = Arc::new(InferContext {
client,
shared_tools: shared_tools.clone(),
observer,
debug,
execution: execution.to_owned(),
section: section.to_owned(),
max_tool_iterations,
turns: Arc::clone(turns),
analysis: analysis.cloned(),
live_bindings,
tool_bag: Mutex::new(ToolBag::new(tool_bindings, tool_runtime)),
counts_slot: vm.counts_slot(),
sys_live: vm.sys_live_handle(),
});
let hook: ModelInferHook =
Arc::new(move |lua, binding, prompt| ctx.infer(lua, binding, prompt));
vm.set_infer_hook(hook);
}