use std::sync::Arc;
use std::time::Duration;
use adk_agent::codeact::{
CodeRuntime, PendingCall, ResumeWith, RunStep, RuntimeCapabilities, RuntimeError,
};
use adk_code::embedded_python::monty::{FunctionCall, MontyRun, RunProgress};
use adk_code::embedded_python::monty_types::{
CompileOptions, ExcType, ExtFunctionResult, LimitedTracker, MontyException, MontyObject,
NameLookupResult, PrintWriter, ResourceLimits,
};
use adk_code::embedded_python::{json_to_monty, monty_to_json};
use adk_core::Tool;
use serde_json::Value;
use crate::os_access::{OsAccess, OsAccessBuilder, PathAccess};
use crate::prompt::{MONTY_PROMPT, TOOL_DISPATCH_FN, tool_entry};
type Tracker = LimitedTracker;
type Progress = RunProgress<Tracker>;
pub struct MontyRuntime {
limits: ResourceLimits,
extra_prompt: Option<String>,
os: Arc<OsAccess>,
}
fn default_resource_limits() -> ResourceLimits {
ResourceLimits::new().max_duration(Duration::from_secs(5)).max_memory(256 * 1024 * 1024)
}
impl MontyRuntime {
#[must_use]
pub fn new() -> Self {
Self::builder().build()
}
#[must_use]
pub fn builder() -> MontyRuntimeBuilder {
MontyRuntimeBuilder::new()
}
fn tracker(&self) -> Tracker {
LimitedTracker::new(self.limits.clone())
}
}
impl Default for MontyRuntime {
fn default() -> Self {
Self::new()
}
}
pub struct MontyRuntimeBuilder {
limits: ResourceLimits,
extra_prompt: Option<String>,
os: OsAccessBuilder,
}
impl MontyRuntimeBuilder {
#[must_use]
pub fn new() -> Self {
Self { limits: default_resource_limits(), extra_prompt: None, os: OsAccessBuilder::new() }
}
#[must_use]
pub fn resource_limits(mut self, limits: ResourceLimits) -> Self {
self.limits = limits;
self
}
#[must_use]
pub fn unlimited(mut self) -> Self {
self.limits = ResourceLimits::new();
self
}
#[must_use]
pub fn max_duration(mut self, duration: Duration) -> Self {
self.limits.max_duration = Some(duration);
self
}
#[must_use]
pub fn max_memory(mut self, bytes: usize) -> Self {
self.limits.max_memory = Some(bytes);
self
}
#[must_use]
pub fn additional_prompt(mut self, text: impl Into<String>) -> Self {
self.extra_prompt = Some(text.into());
self
}
#[must_use]
pub fn os_access(mut self, access: OsAccess) -> Self {
self.os = access.into_builder();
self
}
#[must_use]
pub fn allow_path(
mut self,
virtual_path: impl Into<String>,
host_path: impl Into<std::path::PathBuf>,
access: PathAccess,
) -> Self {
self.os = self.os.allow_path(virtual_path, host_path, access);
self
}
#[must_use]
pub fn environ<K, V>(mut self, vars: impl IntoIterator<Item = (K, V)>) -> Self
where
K: Into<String>,
V: Into<String>,
{
self.os = self.os.environ(vars);
self
}
#[must_use]
pub fn environ_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.os = self.os.environ_var(key, value);
self
}
#[must_use]
pub fn system_clock(mut self, enabled: bool) -> Self {
self.os = self.os.system_clock(enabled);
self
}
#[must_use]
pub fn build(self) -> MontyRuntime {
MontyRuntime {
limits: self.limits,
extra_prompt: self.extra_prompt,
os: Arc::new(self.os.build()),
}
}
}
impl Default for MontyRuntimeBuilder {
fn default() -> Self {
Self::new()
}
}
impl CodeRuntime for MontyRuntime {
fn start(&self, script: &str, script_name: &str) -> Result<RunStep, RuntimeError> {
let run = match MontyRun::new(
script.to_string(),
script_name,
Vec::new(),
CompileOptions::default(),
) {
Ok(run) => run,
Err(exc) => return Ok(RunStep::raised(render_exception(&exc))),
};
let mut stdout = String::new();
match run.start(Vec::new(), self.tracker(), PrintWriter::collect_string(&mut stdout)) {
Ok(progress) => drive(progress, stdout, &self.os),
Err(exc) => Ok(RunStep::raised(render_exception(&exc)).with_stdout(stdout)),
}
}
fn resume(&self, snapshot: &[u8], with: ResumeWith) -> Result<RunStep, RuntimeError> {
let progress =
Progress::load(snapshot).map_err(|err| RuntimeError::Snapshot(err.to_string()))?;
let call = progress.into_function_call().ok_or_else(|| {
RuntimeError::Snapshot("snapshot is not a paused external function call".to_string())
})?;
resume_call(call, with, &self.os)
}
fn capabilities(&self) -> RuntimeCapabilities {
let mut prompt = MONTY_PROMPT.to_string();
prompt.push_str("\n\n");
prompt.push_str(&self.os.prompt_section());
if let Some(extra) = &self.extra_prompt {
prompt.push_str("\n\n");
prompt.push_str(extra);
}
RuntimeCapabilities::new(true, prompt)
}
fn render_tools(&self, tools: &[Arc<dyn Tool>]) -> String {
let mut entries = String::new();
for tool in tools {
if tool.is_builtin() {
continue;
}
entries.push_str(&tool_entry(tool.as_ref()));
}
if entries.trim().is_empty() {
return String::new();
}
format!(
"The following tools are available. Invoke each one with the built-in \
`{TOOL_DISPATCH_FN}` function — the first argument is the tool name and \
the rest are passed by keyword; a tool is never callable as a bare name. \
Each returns a JSON-compatible value.\n\n```python\n{entries}```"
)
}
}
fn drive(
mut progress: Progress,
mut stdout: String,
os: &Arc<OsAccess>,
) -> Result<RunStep, RuntimeError> {
let mut mounts = os.build_mount_table()?;
loop {
match progress {
RunProgress::Complete(value) => {
return Ok(RunStep::complete(monty_to_json(&value)).with_stdout(stdout));
}
RunProgress::FunctionCall(call) => match resolve_dispatch(&call) {
Ok((name, keyword)) => {
let pending = MontyPendingCall::from_call(call, name, keyword, os.clone());
return Ok(RunStep::call(Box::new(pending)).with_stdout(stdout));
}
Err(message) => {
progress = match call.resume(
ExtFunctionResult::Error(monty_error(&message)),
PrintWriter::collect_string(&mut stdout),
) {
Ok(next) => next,
Err(exc) => {
return Ok(RunStep::raised(render_exception(&exc)).with_stdout(stdout));
}
};
}
},
RunProgress::OsCall(call) => {
progress = match call
.resume_with(PrintWriter::collect_string(&mut stdout), |call| {
os.resolve(call, &mut mounts)
}) {
Ok(next) => next,
Err(exc) => {
return Ok(RunStep::raised(render_exception(&exc)).with_stdout(stdout));
}
};
}
RunProgress::NameLookup(lookup) => {
progress = match lookup
.resume(NameLookupResult::Undefined, PrintWriter::collect_string(&mut stdout))
{
Ok(next) => next,
Err(exc) => {
return Ok(RunStep::raised(render_exception(&exc)).with_stdout(stdout));
}
};
}
RunProgress::ResolveFutures(futures) => {
let denied: Vec<(u32, ExtFunctionResult)> = futures
.pending_call_ids()
.iter()
.map(|id| {
(
*id,
ExtFunctionResult::Error(monty_error(
"asynchronous external calls are not supported; call tools synchronously, without `await`",
)),
)
})
.collect();
progress = match futures.resume(denied, PrintWriter::collect_string(&mut stdout)) {
Ok(next) => next,
Err(exc) => {
return Ok(RunStep::raised(render_exception(&exc)).with_stdout(stdout));
}
};
}
}
}
}
struct MontyPendingCall {
name: String,
keyword: Vec<(String, Value)>,
call_id: u64,
progress: Progress,
os: Arc<OsAccess>,
}
impl MontyPendingCall {
fn from_call(
call: FunctionCall<Tracker>,
name: String,
keyword: Vec<(String, Value)>,
os: Arc<OsAccess>,
) -> Self {
let call_id = u64::from(call.call_id);
Self { name, keyword, call_id, progress: RunProgress::FunctionCall(call), os }
}
}
fn resolve_dispatch(
call: &FunctionCall<Tracker>,
) -> Result<(String, Vec<(String, Value)>), String> {
if call.function_name != TOOL_DISPATCH_FN {
return Err(format!(
"'{}' is not defined. Call tools only via {TOOL_DISPATCH_FN}(\"<tool-name>\", {{...}}).",
call.function_name
));
}
let Some(MontyObject::String(name)) = call.args.first() else {
return Err(format!(
"{TOOL_DISPATCH_FN}(...) needs the tool name as the first positional string argument, \
e.g. {TOOL_DISPATCH_FN}(\"my_tool\", {{\"arg\": value}})."
));
};
let name = name.clone();
if !call.kwargs.is_empty() {
return Err(format!(
"{TOOL_DISPATCH_FN}(...) takes the tool name and a single arguments dict; put tool \
arguments inside the dict, not as keyword arguments: \
{TOOL_DISPATCH_FN}(\"{name}\", {{\"arg\": value}})."
));
}
if call.args.len() > 2 {
return Err(format!(
"{TOOL_DISPATCH_FN}(...) takes exactly the tool name and one arguments dict: \
{TOOL_DISPATCH_FN}(\"{name}\", {{\"arg\": value}})."
));
}
let keyword = match call.args.get(1) {
None => Vec::new(),
Some(MontyObject::Dict(pairs)) => {
let mut keyword = Vec::with_capacity(pairs.len());
for (key, value) in pairs {
let MontyObject::String(key) = key else {
return Err(format!(
"{TOOL_DISPATCH_FN}(\"{name}\", ...) argument keys must be strings; \
pass arguments as {{\"arg\": value}}."
));
};
keyword.push((key.clone(), monty_to_json(value)));
}
keyword
}
Some(_) => {
return Err(format!(
"{TOOL_DISPATCH_FN}(\"{name}\", ...) needs a single arguments dict, \
e.g. {TOOL_DISPATCH_FN}(\"{name}\", {{\"arg\": value}})."
));
}
};
Ok((name, keyword))
}
impl PendingCall for MontyPendingCall {
fn function_name(&self) -> &str {
&self.name
}
fn positional_args(&self) -> &[Value] {
&[]
}
fn keyword_args(&self) -> &[(String, Value)] {
&self.keyword
}
fn call_id(&self) -> u64 {
self.call_id
}
fn dump(&self) -> Result<Vec<u8>, RuntimeError> {
self.progress.dump().map_err(|err| RuntimeError::Snapshot(err.to_string()))
}
fn resume(self: Box<Self>, with: ResumeWith) -> Result<RunStep, RuntimeError> {
let os = self.os.clone();
let call = self
.progress
.into_function_call()
.expect("MontyPendingCall always wraps a function call");
resume_call(call, with, &os)
}
}
fn resume_call(
call: FunctionCall<Tracker>,
with: ResumeWith,
os: &Arc<OsAccess>,
) -> Result<RunStep, RuntimeError> {
let result = match with {
ResumeWith::Value(value) => ExtFunctionResult::Return(json_to_monty(value)),
ResumeWith::Raise(message) => ExtFunctionResult::Error(monty_error(&message)),
};
let mut stdout = String::new();
match call.resume(result, PrintWriter::collect_string(&mut stdout)) {
Ok(progress) => drive(progress, stdout, os),
Err(exc) => Ok(RunStep::raised(render_exception(&exc)).with_stdout(stdout)),
}
}
fn monty_error(message: &str) -> MontyException {
MontyException::new(ExcType::RuntimeError, Some(message.to_string()))
}
fn render_exception(exc: &MontyException) -> String {
exc.to_string()
}