use super::{
Arc, Error, Lua, LuaSectionHandle, LuaToolHandle, MultiValue, Mutex, Result, ToolBindings,
ToolCallCounts, ToolPhase, ToolRuntime, Value, Variadic, validate_alias,
};
pub(crate) fn install_lua_tool_calls(
lua: &Lua,
counts: &ToolCallCounts,
declared: &[String],
) -> Result<()> {
let globals = lua.globals();
let tools: mlua::Table = globals.raw_get("tools").map_err(Error::lua)?;
let calls_inner = lua.create_table().map_err(Error::lua)?;
let meta = lua.create_table().map_err(Error::lua)?;
let counts_for_index = counts.clone();
let declared: Vec<String> = declared.to_vec();
let index = lua
.create_function(move |_, (_table, key): (mlua::Table, String)| {
let value = counts_for_index.get(&key).map_err(mlua::Error::external)?;
if let Some(count) = value {
Ok(count)
} else {
let in_scope = counts_for_index.aliases().map_err(mlua::Error::external)?;
let declared_unscoped = declared.iter().any(|alias| alias == &key);
Err(mlua::Error::external(format!(
"tools.calls: {key:?} is not in this section's tool scope; \
in-scope aliases: {in_scope:?}{}",
if declared_unscoped {
" (alias was declared by tools.need but not added to this section's scope)"
} else if in_scope.is_empty() {
""
} else {
" - check for typos or add it via tools.add"
}
)))
}
})
.map_err(Error::lua)?;
meta.set("__index", index).map_err(Error::lua)?;
let newindex_err = lua
.create_function(|_, _: MultiValue| -> mlua::Result<()> {
Err(mlua::Error::external("tools.calls is read-only"))
})
.map_err(Error::lua)?;
meta.set("__newindex", newindex_err).map_err(Error::lua)?;
calls_inner.set_metatable(Some(meta));
tools.set("calls", calls_inner).map_err(Error::lua)?;
Ok(())
}
pub(crate) struct ToolsAddEntry {
alias: String,
description_override: Option<String>,
}
pub(crate) fn push_tools_add_entry(
entries: &mut Vec<ToolsAddEntry>,
value: Value,
) -> mlua::Result<()> {
match value {
Value::String(s) => {
entries.push(ToolsAddEntry {
alias: s.to_string_lossy(),
description_override: None,
});
Ok(())
}
Value::UserData(ud) => {
let handle = ud.borrow::<LuaToolHandle>()?;
entries.push(ToolsAddEntry {
alias: handle.name().to_owned(),
description_override: handle.model_description_override().map(str::to_owned),
});
Ok(())
}
Value::Table(table) => {
for item in table.sequence_values::<Value>() {
match item? {
Value::String(s) => entries.push(ToolsAddEntry {
alias: s.to_string_lossy(),
description_override: None,
}),
Value::UserData(ud) => {
let handle = ud.borrow::<LuaToolHandle>()?;
entries.push(ToolsAddEntry {
alias: handle.name().to_owned(),
description_override: handle
.model_description_override()
.map(str::to_owned),
});
}
_ => {
return Err(mlua::Error::external(
"tools.add array elements must be strings or Tool objects",
));
}
}
}
Ok(())
}
_ => Err(mlua::Error::external(
"tools.add expects strings, Tool objects, or arrays of either",
)),
}
}
pub(crate) fn collect_tools_add_entries(args: Variadic<Value>) -> mlua::Result<Vec<ToolsAddEntry>> {
let mut entries = Vec::new();
for value in args {
push_tools_add_entry(&mut entries, value)?;
}
Ok(entries)
}
pub(crate) fn install_h2_tools(
lua: &Lua,
globals: &mlua::Table,
bindings: &ToolBindings,
runtime: &Arc<Mutex<ToolRuntime>>,
) -> Result<()> {
{
let state = runtime
.lock()
.map_err(|_| Error::Lua("tool declaration runtime was poisoned".to_owned()))?;
if state.phase != ToolPhase::H2 {
return Err(Error::Lua(
"tool scope is not open for H2 recording".to_owned(),
));
}
}
let tools = lua.create_table().map_err(Error::lua)?;
for name in ["need", "always"] {
let operation = name;
let forbidden = lua
.create_function(move |_, _: MultiValue| -> mlua::Result<()> {
Err(mlua::Error::external(format!(
"tools.{operation} is only available during live H1 execution"
)))
})
.map_err(Error::lua)?;
tools.set(name, forbidden).map_err(Error::lua)?;
}
let frozen = bindings.clone();
let state = Arc::clone(runtime);
let add = lua
.create_function(move |_, args: Variadic<Value>| {
let entries = collect_tools_add_entries(args)?;
let mut state = state
.lock()
.map_err(|_| mlua::Error::external("tool declaration runtime was poisoned"))?;
if state.phase != ToolPhase::H2 {
return Err(mlua::Error::external(
"tools.add is only available before the H2 tool scope closes",
));
}
for entry in &entries {
validate_alias(&entry.alias).map_err(mlua::Error::external)?;
if frozen.binding(&entry.alias).is_none() {
return Err(mlua::Error::external(format!(
"tools.add alias {:?} was not declared by tools.need",
entry.alias
)));
}
}
let mut changed = false;
for entry in entries {
if let Some(description) = entry.description_override {
let override_changed = match state.description_overrides.get(&entry.alias) {
Some(existing) => existing != &description,
None => true,
};
if override_changed {
state
.description_overrides
.insert(entry.alias.clone(), description);
changed = true;
}
}
if frozen
.always
.iter()
.any(|existing| existing == &entry.alias)
{
continue;
}
if !state.added.iter().any(|existing| existing == &entry.alias) {
state.added.push(entry.alias);
changed = true;
}
}
if changed {
state.generation = state.generation.saturating_add(1);
}
Ok(())
})
.map_err(Error::lua)?;
tools.set("add", add).map_err(Error::lua)?;
globals.raw_set("tools", tools).map_err(Error::lua)
}
pub(crate) fn install_tasks_table(lua: &Lua, tasks: &[LuaSectionHandle]) -> Result<()> {
let table = lua
.create_table_with_capacity(0, tasks.len())
.map_err(Error::lua)?;
for handle in tasks {
let userdata = lua.create_userdata(handle.clone()).map_err(Error::lua)?;
table
.raw_set(handle.heading(), userdata)
.map_err(Error::lua)?;
}
lua.globals().raw_set("tasks", table).map_err(Error::lua)
}