use std::{
collections::BTreeSet,
sync::{Arc, RwLock},
};
use rho_sdk::{
model::ToolSpec,
tool::{Tool, ToolContext, ToolError, ToolErrorKind, ToolVisibility},
ToolHost,
};
use serde::Serialize;
use crate::config::CodemodeMode;
use super::{
search::search_entries,
tool::CodeModeTool,
tool_search::{ToolSearchHit, ToolSearchTool, TOOL_SEARCH_NAME},
CODEMODE_TOOL_NAME,
};
#[derive(Debug, PartialEq, Eq, Serialize)]
pub(crate) struct ToolCatalogEntry {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
pub returns: Option<serde_json::Value>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ContextExposure {
Direct,
Hidden,
}
impl ContextExposure {
fn declared_in(self, mode: CodemodeMode) -> bool {
match self {
Self::Direct => mode == CodemodeMode::On,
Self::Hidden => false,
}
}
}
#[derive(Clone)]
pub(crate) struct ScriptTool {
tool: Arc<dyn Tool>,
name: String,
exposure: ContextExposure,
}
impl ScriptTool {
pub(crate) fn new(name: String, tool: Arc<dyn Tool>, exposure: ContextExposure) -> Self {
Self {
tool,
name,
exposure,
}
}
pub(super) fn name(&self) -> &str {
&self.name
}
pub(super) fn catalog_entry(&self) -> ToolCatalogEntry {
let spec = self.tool.spec();
ToolCatalogEntry {
name: spec.name,
description: spec.description,
parameters: spec.input_schema,
returns: self.tool.output_schema(),
}
}
}
#[derive(Default)]
struct SurfaceState {
tools: Vec<ScriptTool>,
loaded: BTreeSet<String>,
mode: CodemodeMode,
}
#[derive(Default)]
pub(crate) struct CodeModeSurface {
state: RwLock<SurfaceState>,
}
impl CodeModeSurface {
pub(crate) fn orchestration_tools(self: &Arc<Self>) -> [Arc<dyn Tool>; 2] {
[
Arc::new(CodeModeTool::new(self.clone())),
Arc::new(ToolSearchTool::new(self.clone())),
]
}
#[cfg(test)]
pub(crate) fn sync(&self, tools: &[Arc<dyn Tool>]) {
self.sync_entries(
tools.iter().map(|tool| {
ScriptTool::new(tool.spec().name, tool.clone(), ContextExposure::Direct)
}),
);
}
pub(crate) fn sync_entries(&self, tools: impl IntoIterator<Item = ScriptTool>) {
let mut siblings: Vec<_> = tools
.into_iter()
.filter(|sibling| !is_orchestration_tool(&sibling.name))
.collect();
siblings.sort_by(|a, b| a.name.cmp(&b.name));
self.state.write().expect("codemode surface").tools = siblings;
}
pub(super) fn snapshot(
&self,
context: &ToolContext,
) -> Result<(ToolHost, Vec<ScriptTool>), ToolError> {
let state = self.state.read().expect("codemode surface");
let host = state
.tools
.iter()
.fold(ToolHost::child_builder(context), |builder, sibling| {
builder.tool_shared(sibling.tool.clone())
})
.max_parallel_tools(crate::app::sdk_config::parallel_tool_limit())
.build()
.map_err(|error| ToolError::new(ToolErrorKind::Execution, error.to_string()))?;
Ok((host, state.tools.clone()))
}
pub(crate) fn search_and_load(&self, query: &str, limit: usize) -> Vec<ToolSearchHit> {
let mut state = self.state.write().expect("codemode surface");
let entries: Vec<_> = state
.tools
.iter()
.map(|sibling| {
let spec = sibling.tool.spec();
ToolCatalogEntry {
name: spec.name,
description: spec.description,
parameters: spec.input_schema,
returns: None,
}
})
.collect();
let hits = search_entries(&entries, query, limit);
state
.loaded
.extend(hits.iter().map(|entry| entry.name.clone()));
hits.into_iter().map(ToolSearchHit::from).collect()
}
pub(crate) fn mode(&self) -> CodemodeMode {
self.state.read().expect("codemode surface").mode
}
pub(crate) fn set_mode(&self, mode: CodemodeMode) {
self.state.write().expect("codemode surface").mode = mode;
}
pub(crate) fn decorate(&self, snapshot: rho_sdk::SessionSnapshot) -> rho_sdk::SessionSnapshot {
let (key, value) = self.checkpoint();
snapshot.with_metadata(key, value)
}
pub(crate) fn checkpoint(&self) -> (&'static str, String) {
let state = self.state.read().expect("codemode surface");
(
LOADED_TOOLS_KEY,
serde_json::to_string(&state.loaded).expect("serializable tool names"),
)
}
pub(crate) fn restore(&self, snapshot: &rho_sdk::SessionSnapshot) {
let loaded = snapshot
.metadata()
.get(LOADED_TOOLS_KEY)
.and_then(|value| serde_json::from_str(value).ok())
.unwrap_or_default();
self.state.write().expect("codemode surface").loaded = loaded;
}
pub(crate) fn clear_loaded(&self) {
self.state.write().expect("codemode surface").loaded.clear();
}
}
const LOADED_TOOLS_KEY: &str = "rho.codemode.loaded_tools";
fn is_orchestration_tool(name: &str) -> bool {
matches!(name, CODEMODE_TOOL_NAME | TOOL_SEARCH_NAME)
}
impl ToolVisibility for CodeModeSurface {
fn is_advertised(&self, name: &str) -> bool {
if is_orchestration_tool(name) {
return true;
}
let state = self.state.read().expect("codemode surface");
state
.tools
.binary_search_by(|sibling| sibling.name.as_str().cmp(name))
.is_ok_and(|index| {
state.loaded.contains(name) || state.tools[index].exposure.declared_in(state.mode)
})
}
fn describe(&self, spec: &ToolSpec) -> Option<String> {
let state = self.state.read().expect("codemode surface");
if state.mode != CodemodeMode::On {
return None;
}
state
.tools
.binary_search_by(|sibling| sibling.name.cmp(&spec.name))
.ok()?;
Some(format!(
"{}\n\nCodemode: `call_tool(\"{}\", args)` returns `{{ is_error, content, data }}`; batch independent calls with `call_tools`.",
spec.description.trim_end(), spec.name,
))
}
}