use std::sync::Arc;
use adk_core::Tool;
use serde_json::{Map, Value};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum RuntimeError {
#[error("snapshot error: {0}")]
Snapshot(String),
#[error("interpreter error: {0}")]
Internal(String),
}
#[derive(Debug, Clone)]
pub enum ResumeWith {
Value(Value),
Raise(String),
}
pub enum RunStep {
Call {
call: Box<dyn PendingCall>,
stdout: String,
},
Complete {
value: Value,
stdout: String,
},
Raised {
message: String,
stdout: String,
},
}
impl RunStep {
#[must_use]
pub fn call(call: Box<dyn PendingCall>) -> Self {
Self::Call { call, stdout: String::new() }
}
#[must_use]
pub fn complete(value: Value) -> Self {
Self::Complete { value, stdout: String::new() }
}
#[must_use]
pub fn raised(message: impl Into<String>) -> Self {
Self::Raised { message: message.into(), stdout: String::new() }
}
#[must_use]
pub fn with_stdout(mut self, stdout: impl Into<String>) -> Self {
let slot = match &mut self {
Self::Call { stdout, .. } => stdout,
Self::Complete { stdout, .. } => stdout,
Self::Raised { stdout, .. } => stdout,
};
*slot = stdout.into();
self
}
}
impl std::fmt::Debug for RunStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Call { call, stdout } => f
.debug_struct("Call")
.field("function_name", &call.function_name())
.field("call_id", &call.call_id())
.field("stdout", stdout)
.finish(),
Self::Complete { value, stdout } => {
f.debug_struct("Complete").field("value", value).field("stdout", stdout).finish()
}
Self::Raised { message, stdout } => {
f.debug_struct("Raised").field("message", message).field("stdout", stdout).finish()
}
}
}
}
pub trait PendingCall: Send {
fn function_name(&self) -> &str;
fn positional_args(&self) -> &[Value];
fn keyword_args(&self) -> &[(String, Value)];
fn call_id(&self) -> u64;
fn dump(&self) -> Result<Vec<u8>, RuntimeError>;
fn resume(self: Box<Self>, with: ResumeWith) -> Result<RunStep, RuntimeError>;
}
pub trait CodeRuntime: Send + Sync {
fn start(&self, script: &str, script_name: &str) -> Result<RunStep, RuntimeError>;
fn resume(&self, snapshot: &[u8], with: ResumeWith) -> Result<RunStep, RuntimeError>;
fn capabilities(&self) -> RuntimeCapabilities {
RuntimeCapabilities::default()
}
fn render_tools(&self, tools: &[Arc<dyn Tool>]) -> String {
let catalog = default_tool_catalog(tools);
if catalog.trim().is_empty() {
return String::new();
}
format!("Available tools:\n{catalog}")
}
}
pub fn bind_call_args(tool: &dyn Tool, positional: &[Value], keyword: &[(String, Value)]) -> Value {
let mut map = Map::new();
for (name, value) in keyword {
map.insert(name.clone(), value.clone());
}
if !positional.is_empty() {
let names = ordered_parameter_names(tool);
for (index, value) in positional.iter().enumerate() {
let key = names.get(index).cloned().unwrap_or_else(|| format!("arg{index}"));
map.entry(key).or_insert_with(|| value.clone());
}
}
Value::Object(map)
}
fn ordered_parameter_names(tool: &dyn Tool) -> Vec<String> {
let Some(schema) = tool.parameters_schema() else {
return Vec::new();
};
let required: Vec<String> = schema
.get("required")
.and_then(Value::as_array)
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(str::to_string)).collect())
.unwrap_or_default();
let mut ordered = required.clone();
if let Some(props) = schema.get("properties").and_then(Value::as_object) {
for key in props.keys() {
if !ordered.contains(key) {
ordered.push(key.clone());
}
}
}
ordered
}
pub fn default_tool_catalog(tools: &[Arc<dyn Tool>]) -> String {
let mut out = String::new();
for tool in tools {
if tool.is_builtin() {
continue;
}
let decl = tool.declaration();
let params = decl
.get("parameters")
.and_then(|p| p.get("properties"))
.and_then(|p| p.as_object())
.map(|props| props.keys().cloned().collect::<Vec<_>>().join(", "))
.unwrap_or_default();
let annotation = if tool.is_long_running() { " [long-running]" } else { "" };
out.push_str(&format!(
"- {}({}): {}{}\n",
tool.name(),
params,
tool.description(),
annotation
));
}
out
}
#[derive(Debug, Clone, Default)]
pub struct RuntimeCapabilities {
pub supports_suspension: bool,
pub prompt: String,
}
impl RuntimeCapabilities {
pub fn new(supports_suspension: bool, prompt: impl Into<String>) -> Self {
Self { supports_suspension, prompt: prompt.into() }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_capabilities_do_not_claim_suspension() {
assert!(!RuntimeCapabilities::default().supports_suspension);
assert!(RuntimeCapabilities::default().prompt.is_empty());
}
#[test]
fn new_carries_prompt_and_flag() {
let caps = RuntimeCapabilities::new(true, "Monty: no class/match");
assert!(caps.supports_suspension);
assert_eq!(caps.prompt, "Monty: no class/match");
}
#[test]
fn default_catalog_lists_function_tools() {
let catalog = default_tool_catalog(&[crate::codeact::test_support::echo_tool()]);
assert!(catalog.contains("echo"));
assert!(catalog.contains("echoes its arguments"));
}
#[test]
fn default_catalog_omits_builtin_tools() {
use crate::codeact::test_support::{builtin_tool, echo_tool};
let catalog = default_tool_catalog(&[echo_tool(), builtin_tool()]);
assert!(catalog.contains("echo"));
assert!(!catalog.contains("web_search"));
}
#[test]
fn default_catalog_annotates_long_running() {
let catalog = default_tool_catalog(&[crate::codeact::test_support::long_running_tool()]);
assert!(catalog.contains("slow"));
assert!(catalog.contains("[long-running]"));
}
#[test]
fn render_tools_empty_when_only_builtin_or_none() {
use crate::codeact::test_support::{ScriptedRuntime, builtin_tool, echo_tool};
let rt = ScriptedRuntime::new(vec![]);
assert!(rt.render_tools(&[]).is_empty());
assert!(rt.render_tools(&[builtin_tool()]).is_empty());
assert!(rt.render_tools(&[echo_tool()]).starts_with("Available tools:"));
}
#[test]
fn bind_call_args_maps_positional_and_prefers_keyword() {
use crate::codeact::test_support::echo_tool;
use serde_json::json;
let tool = echo_tool();
let bound =
bind_call_args(tool.as_ref(), &[json!(1)], &[("label".to_string(), json!("x"))]);
assert_eq!(bound, json!({"arg0": 1, "label": "x"}));
}
#[test]
fn run_step_constructors_and_with_stdout() {
use serde_json::json;
let step = RunStep::complete(json!(1)).with_stdout("hi");
match step {
RunStep::Complete { value, stdout } => {
assert_eq!(value, json!(1));
assert_eq!(stdout, "hi");
}
other => panic!("expected Complete, got {other:?}"),
}
}
}