use std::{path::PathBuf, sync::Arc};
use nanocodex_oai_api::{
responses::CustomToolFormat,
tools::{ToolContext, ToolDefinition, ToolInput, ToolOutput, ToolOutputBody},
};
use super::{
CodeModeExecution, CodeModeHost, CodeModeNotification, CodeModeObserver, CodeModeUpdate,
NestedToolCall, OwnedToolContext,
};
use crate::runtime_config::{ImageGenerationConfig, WebSearchConfig};
const EXEC_GRAMMAR: &str = r"start: /[\s\S]+/";
const EXEC_DESCRIPTION: &str = r"Run JavaScript in the embedded host.
- `tools` contains the application-defined async tools listed below.
- `text(value)` and `image(value)` append output for the model.
- `generatedImage(result)` appends an image-generation result for the model.
- `store(key, value)` and `load(key)` retain serializable values across calls.
- JavaScript runs inside the Node or browser host supplied by the embedding application.";
#[derive(Clone, Default)]
pub struct HostedTools {
host: Option<Arc<dyn CodeModeHost>>,
}
impl HostedTools {
#[must_use]
pub fn new(host: impl CodeModeHost) -> Self {
Self {
host: Some(Arc::new(host)),
}
}
#[must_use]
pub const fn web_search_enabled(&self) -> bool {
false
}
#[must_use]
pub const fn image_generation_enabled(&self) -> bool {
false
}
#[must_use]
pub const fn for_session(self, _session_id: &str) -> Self {
self
}
pub const fn start_providers(&self) {}
}
pub struct HostedToolRuntime {
working_directory: Arc<str>,
host: Option<Arc<dyn CodeModeHost>>,
}
#[derive(Clone, Copy)]
pub struct HostedToolRuntimeControl;
impl HostedToolRuntime {
pub fn new(
workspace: impl Into<PathBuf>,
_web_search: Option<WebSearchConfig>,
_image_generation: Option<ImageGenerationConfig>,
) -> Self {
let workspace = workspace.into();
Self {
working_directory: Arc::from(workspace.to_string_lossy().into_owned()),
host: None,
}
}
#[must_use]
pub fn new_with_tools(
workspace: impl Into<PathBuf>,
web_search: Option<WebSearchConfig>,
image_generation: Option<ImageGenerationConfig>,
tools: &HostedTools,
) -> Self {
Self::new(workspace, web_search, image_generation).with_tools(tools)
}
#[must_use]
pub fn with_tools(mut self, tools: &HostedTools) -> Self {
self.host.clone_from(&tools.host);
self
}
#[must_use]
pub const fn default_shell_name(&self) -> &'static str {
"javascript"
}
#[must_use]
pub fn working_directory(&self) -> &str {
&self.working_directory
}
#[must_use]
pub const fn control(&self) -> HostedToolRuntimeControl {
HostedToolRuntimeControl
}
#[must_use]
pub fn model_specs(&self, session_id: &str) -> Vec<ToolDefinition> {
self.model_contract(session_id).0
}
pub(crate) fn model_contract(
&self,
session_id: &str,
) -> (Vec<ToolDefinition>, Vec<(String, String)>) {
let mut definitions = self.host.as_ref().map_or_else(Vec::new, |host| {
match host.tool_definitions(session_id) {
Ok(definitions) => definitions,
Err(error) => {
tracing::warn!(
target: "nanocodex_tools",
%error,
"hosted Code Mode tool discovery failed"
);
Vec::new()
}
}
});
definitions.sort_by(|left, right| left.name().cmp(right.name()));
let code_mode_tool_names = definitions
.iter()
.map(|definition| {
(
normalize_identifier(definition.name()),
definition.name().to_owned(),
)
})
.collect();
let mut description = EXEC_DESCRIPTION.to_owned();
for definition in definitions {
description.push_str("\n\n- `tools.");
description.push_str(definition.name());
description.push_str("`: ");
description.push_str(definition.description().trim());
}
(
vec![ToolDefinition::custom(
"exec",
description,
CustomToolFormat::grammar("lark", EXEC_GRAMMAR),
)],
code_mode_tool_names,
)
}
#[must_use]
pub const fn supports_parallel_tool_calls(&self, _name: &str) -> bool {
false
}
#[must_use]
pub const fn contains(&self, _name: &str) -> bool {
false
}
#[allow(
clippy::unused_async,
reason = "matches the native tool-runtime contract"
)]
pub async fn execute_tool(
&self,
name: &str,
_input: ToolInput,
_context: ToolContext<'_>,
) -> ToolOutput {
ToolOutput::error(format!(
"direct tool `{name}` is unavailable in a hosted runtime"
))
}
pub async fn execute_code(&self, source: &str, context: ToolContext<'_>) -> CodeModeExecution {
let Some(host) = &self.host else {
return failed("no hosted Code Mode adapter is configured");
};
match host.execute(source, context).await {
Ok(execution) => execution,
Err(error) => failed(&error.to_string()),
}
}
pub async fn execute_code_owned(
&self,
source: &str,
context: OwnedToolContext,
) -> CodeModeExecution {
self.execute_code(source, context.as_context()).await
}
pub async fn execute_code_owned_with_updates(
&self,
source: &str,
context: OwnedToolContext,
observer: &mut dyn CodeModeObserver,
) -> CodeModeExecution {
let execution = self.execute_code_owned(source, context).await;
replay_nested_updates(&execution, observer);
execution
}
#[allow(
clippy::unused_async,
reason = "matches the native tool-runtime contract"
)]
pub async fn wait_for_code(
&self,
_input: &str,
_context: ToolContext<'_>,
) -> CodeModeExecution {
failed("background code-mode cells are unavailable in a hosted runtime")
}
pub async fn wait_for_code_with_updates(
&self,
input: &str,
context: ToolContext<'_>,
observer: &mut dyn CodeModeObserver,
) -> CodeModeExecution {
let execution = self.wait_for_code(input, context).await;
replay_nested_updates(&execution, observer);
execution
}
}
fn normalize_identifier(name: &str) -> String {
let mut identifier = String::new();
for (index, character) in name.chars().enumerate() {
let valid = if index == 0 {
character == '_' || character == '$' || character.is_ascii_alphabetic()
} else {
character == '_' || character == '$' || character.is_ascii_alphanumeric()
};
identifier.push(if valid { character } else { '_' });
}
if identifier.is_empty() {
"_".to_owned()
} else {
identifier
}
}
impl HostedToolRuntimeControl {
pub const fn begin_turn(&self) {}
#[allow(
clippy::unused_async,
reason = "matches the native tool-runtime control contract"
)]
pub async fn cancel_turn(&self) {}
#[allow(
clippy::unused_async,
reason = "matches the native tool-runtime control contract"
)]
pub async fn cancel(&self) {}
}
fn replay_nested_updates(execution: &CodeModeExecution, observer: &mut dyn CodeModeObserver) {
for call in &execution.nested_calls {
observer.update(CodeModeUpdate::NestedCallStarted {
call_id: &call.call_id,
name: &call.name,
input: &call.input,
});
observer.update(CodeModeUpdate::NestedCallCompleted(call));
}
}
fn failed(message: &str) -> CodeModeExecution {
CodeModeExecution {
output: ToolOutputBody::Text(format!("Script failed\nOutput:\n{message}")),
success: false,
nested_calls: Vec::<NestedToolCall>::new(),
notifications: Vec::<CodeModeNotification>::new(),
}
}
#[cfg(all(test, not(target_family = "wasm")))]
mod tests {
use nanocodex_oai_api::tools::ToolOutputBody;
use serde_json::json;
use super::{HostedToolRuntime, HostedTools};
use crate::{
ToolContext, ToolDefinition,
hosted::{CodeModeExecution, CodeModeHost, CodeModeHostError, HostFuture, NestedToolCall},
};
struct EchoHost;
impl CodeModeHost for EchoHost {
fn tool_definitions(
&self,
session_id: &str,
) -> Result<Vec<ToolDefinition>, CodeModeHostError> {
assert_eq!(session_id, "session-1");
Ok(vec![
ToolDefinition::function("zeta", "Zeta.", json!({"type": "object"})),
ToolDefinition::function("alpha", "Alpha.", json!({"type": "object"})),
])
}
fn execute<'a>(
&'a self,
source: &'a str,
context: ToolContext<'a>,
) -> HostFuture<'a, Result<CodeModeExecution, CodeModeHostError>> {
Box::pin(async move {
Ok(CodeModeExecution {
output: ToolOutputBody::Text(format!(
"{source}:{}:{}",
context.session_id(),
context.call_id()
)),
success: true,
nested_calls: Vec::<NestedToolCall>::new(),
notifications: Vec::new(),
})
})
}
}
#[test]
fn model_description_orders_host_definitions() {
let tools = HostedTools::new(EchoHost).for_session("session-1");
tools.start_providers();
let specs =
HostedToolRuntime::new_with_tools(".", None, None, &tools).model_specs("session-1");
let description = specs[0].description();
assert!(description.find("tools.alpha").unwrap() < description.find("tools.zeta").unwrap());
}
#[tokio::test]
async fn execution_receives_the_standard_tool_context() {
let tools = HostedTools::new(EchoHost);
let runtime = HostedToolRuntime::new_with_tools(".", None, None, &tools);
let execution = runtime
.execute_code(
"echo",
ToolContext::new("gpt-5", "session-1", "call-1", &[], 1_000),
)
.await;
let ToolOutputBody::Text(output) = execution.output else {
panic!("expected text output");
};
assert_eq!(output, "echo:session-1:call-1");
}
}