use super::manager::{DEFAULT_REQUEST_TIMEOUT_MS, ExtensionProcess, ExtensionProcessSpec};
use super::package::{ExtensionPackage, ToolDefinition, extension_capability_id};
use async_trait::async_trait;
use everruns_core::capabilities::{Capability, SystemPromptContext};
use everruns_core::tools::{Tool, ToolExecutionResult};
use serde_json::Value;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Duration;
const CONFIG_REQUEST_TIMEOUT_MS: &str = "request_timeout_ms";
pub struct ExtensionCapability {
id: String,
package: ExtensionPackage,
workspace_root: PathBuf,
process: Mutex<Option<(Value, Arc<ExtensionProcess>)>>,
}
impl ExtensionCapability {
pub fn new(package: ExtensionPackage, workspace_root: PathBuf) -> Self {
Self {
id: extension_capability_id(&package.manifest.name),
package,
workspace_root,
process: Mutex::new(None),
}
}
fn process_for(&self, config: &Value) -> Arc<ExtensionProcess> {
let mut slot = self.process.lock().expect("extension process lock");
if let Some((cached_config, process)) = slot.as_ref()
&& cached_config == config
{
return process.clone();
}
let timeout_ms = config
.get(CONFIG_REQUEST_TIMEOUT_MS)
.and_then(Value::as_u64)
.unwrap_or(DEFAULT_REQUEST_TIMEOUT_MS)
.clamp(1_000, 600_000);
let process = Arc::new(ExtensionProcess::new(ExtensionProcessSpec {
manifest: self.package.manifest.clone(),
package_dir: self.package.dir.clone(),
workspace_root: self.workspace_root.clone(),
config: config.clone(),
request_timeout: Duration::from_millis(timeout_ms),
}));
*slot = Some((config.clone(), process.clone()));
process
}
pub fn never_defer_tools(&self) -> Vec<String> {
const NEVER_DEFER_BUDGET: usize = 8;
self.package
.manifest
.tools
.iter()
.filter(|tool| tool.never_defer)
.take(NEVER_DEFER_BUDGET)
.map(|tool| tool.name.clone())
.collect()
}
}
#[async_trait]
impl Capability for ExtensionCapability {
fn id(&self) -> &str {
&self.id
}
fn name(&self) -> &str {
&self.package.manifest.name
}
fn description(&self) -> &str {
&self.package.manifest.description
}
fn category(&self) -> Option<&str> {
Some("Extensions")
}
fn config_schema(&self) -> Option<Value> {
self.package.manifest.config_schema.clone()
}
fn validate_config(&self, config: &Value) -> Result<(), String> {
if config.is_null() || config.is_object() {
Ok(())
} else {
Err(format!(
"config for `{}` must be a table of keys, got {config}",
self.id
))
}
}
async fn system_prompt_contribution(&self, _ctx: &SystemPromptContext) -> Option<String> {
self.system_prompt_contribution_with_config(_ctx, &Value::Null)
.await
}
async fn system_prompt_contribution_with_config(
&self,
_ctx: &SystemPromptContext,
config: &Value,
) -> Option<String> {
if !self.package.manifest.prompt {
return None;
}
let text = self.process_for(config).prompt_contribution().await?;
Some(format!(
"<capability id=\"{}\">\n{}\n</capability>",
self.id, text
))
}
fn tools(&self) -> Vec<Box<dyn Tool>> {
self.tools_with_config(&Value::Null)
}
fn tools_with_config(&self, config: &Value) -> Vec<Box<dyn Tool>> {
let process = self.process_for(config);
self.package
.manifest
.tools
.iter()
.map(|definition| {
Box::new(ExtensionTool {
definition: definition.clone(),
process: process.clone(),
}) as Box<dyn Tool>
})
.collect()
}
}
struct ExtensionTool {
definition: ToolDefinition,
process: Arc<ExtensionProcess>,
}
#[async_trait]
impl Tool for ExtensionTool {
fn name(&self) -> &str {
&self.definition.name
}
fn description(&self) -> &str {
&self.definition.description
}
fn parameters_schema(&self) -> Value {
self.definition.schema.clone()
}
async fn execute(&self, arguments: Value) -> ToolExecutionResult {
match self
.process
.call_tool(&self.definition.name, &self.definition.name, arguments)
.await
{
Ok(result) => ToolExecutionResult::Success(result),
Err(err) => ToolExecutionResult::ToolError(err.to_string()),
}
}
}