pub mod process;
pub mod restart;
pub use restart::RestartPolicy;
use self::process::{
NotificationFrame, ProviderCompleteParams, ProviderCompleteResult, ProviderStreamEvent,
};
use crate::extensions::commands::CommandOutputEvent;
use crate::extensions::hooks::events::{HookEvent, HookResult};
use crate::extensions::info::PluginInfo;
use crate::extensions::tasks::TaskEvent;
use async_trait::async_trait;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq)]
pub enum InvokeCommandEvent {
Output(CommandOutputEvent),
Task(TaskEvent),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExtensionHealth {
Loaded,
FailedValidation,
FailedInitialize,
Running,
Restarting,
Degraded,
Failed,
}
impl ExtensionHealth {
pub fn as_str(self) -> &'static str {
match self {
Self::Loaded => "loaded",
Self::FailedValidation => "failed_validation",
Self::FailedInitialize => "failed_initialize",
Self::Running => "running",
Self::Restarting => "restarting",
Self::Degraded => "degraded",
Self::Failed => "failed",
}
}
}
#[cfg(test)]
#[allow(clippy::items_after_test_module)]
mod health_tests {
use super::ExtensionHealth;
#[test]
fn as_str_covers_all_variants() {
assert_eq!(ExtensionHealth::Loaded.as_str(), "loaded");
assert_eq!(
ExtensionHealth::FailedValidation.as_str(),
"failed_validation"
);
assert_eq!(
ExtensionHealth::FailedInitialize.as_str(),
"failed_initialize"
);
assert_eq!(ExtensionHealth::Running.as_str(), "running");
assert_eq!(ExtensionHealth::Restarting.as_str(), "restarting");
assert_eq!(ExtensionHealth::Degraded.as_str(), "degraded");
assert_eq!(ExtensionHealth::Failed.as_str(), "failed");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExtensionLifecycle {
pub generation: u64,
pub health: ExtensionHealth,
}
#[async_trait]
pub trait ExtensionHandler: Send + Sync {
fn id(&self) -> &str;
fn lifecycle_snapshot(&self) -> Option<ExtensionLifecycle> {
None
}
async fn handle(&self, event: &HookEvent) -> HookResult;
async fn handle_no_spawn(&self, event: &HookEvent) -> HookResult {
self.handle(event).await
}
async fn call_tool_for(
&self,
name: &str,
input: Value,
session_id: Option<&str>,
cwd: Option<&std::path::Path>,
) -> Result<Value, String> {
let _ = (name, input, session_id, cwd);
Err("extension runtime does not support tool.call".to_string())
}
async fn call_tool_as(
&self,
name: &str,
input: Value,
session_id: Option<&str>,
cwd: Option<&std::path::Path>,
caller: Option<&agent_core::memory::author::Author>,
) -> Result<Value, String> {
let _ = caller;
self.call_tool_for(name, input, session_id, cwd).await
}
#[cfg(any(test, feature = "testing"))]
async fn call_tool_no_chat(&self, name: &str, input: Value) -> Result<Value, String> {
self.call_tool_for(name, input, None, None).await
}
async fn provider_complete(
&self,
_params: ProviderCompleteParams,
) -> Result<ProviderCompleteResult, String> {
Err("extension runtime does not support provider.complete".to_string())
}
async fn provider_stream(
&self,
_params: ProviderCompleteParams,
_sink: tokio::sync::mpsc::Sender<ProviderStreamEvent>,
) -> Result<ProviderCompleteResult, String> {
Err("provider.stream is not supported by this extension".to_string())
}
async fn invoke_command(
&self,
_command: &str,
_args: Vec<String>,
_request_id: &str,
_sink: crate::extensions::invoke_output::InvokeEventSink,
) -> Result<Value, String> {
Err("extension runtime does not support command.invoke".to_string())
}
async fn get_info(&self) -> Result<PluginInfo, String> {
Err("extension runtime does not support info.get".to_string())
}
async fn settings_editor_open(&self, _category: &str, _field: &str) -> Result<Value, String> {
Err("extension runtime does not support settings.editor.open".to_string())
}
async fn settings_editor_key(
&self,
_category: &str,
_field: &str,
_key: &str,
) -> Result<Value, String> {
Err("extension runtime does not support settings.editor.key".to_string())
}
async fn settings_editor_commit(
&self,
_category: &str,
_field: &str,
_value: Value,
) -> Result<Value, String> {
Err("extension runtime does not support settings.editor.commit".to_string())
}
async fn shutdown(&self);
async fn subscribe_notifications(
&self,
) -> (usize, tokio::sync::mpsc::Receiver<NotificationFrame>) {
let (_tx, rx) = tokio::sync::mpsc::channel(1);
(0, rx)
}
async fn health(&self) -> ExtensionHealth {
ExtensionHealth::Running
}
async fn restart_count(&self) -> usize {
0
}
}
#[cfg(test)]
mod lifecycle_tests {
use super::*;
struct UnsupportedHandler;
#[async_trait]
impl ExtensionHandler for UnsupportedHandler {
fn id(&self) -> &str {
"unsupported-lifecycle"
}
async fn handle(&self, _event: &HookEvent) -> HookResult {
unreachable!("snapshot must not dispatch a hook")
}
async fn shutdown(&self) {}
}
#[test]
fn default_lifecycle_snapshot_is_unavailable_through_trait_object() {
let handler: &dyn ExtensionHandler = &UnsupportedHandler;
assert_eq!(handler.lifecycle_snapshot(), None);
}
#[tokio::test]
async fn default_call_tool_for_serves_no_tool() {
let handler: &dyn ExtensionHandler = &UnsupportedHandler;
let chat = std::env::temp_dir().join("chat-folder");
for (session_id, cwd) in [(Some("chat-A"), Some(chat.as_path())), (None, None)] {
let err = handler
.call_tool_for("any", serde_json::json!({}), session_id, cwd)
.await
.unwrap_err();
assert!(err.contains("does not support tool.call"), "{err}");
}
}
#[derive(Default)]
struct Callers(std::sync::Mutex<Vec<(Option<String>, Option<std::path::PathBuf>)>>);
#[async_trait]
impl ExtensionHandler for Callers {
fn id(&self) -> &str {
"callers"
}
async fn handle(&self, _event: &HookEvent) -> HookResult {
HookResult::default()
}
async fn shutdown(&self) {}
async fn call_tool_for(
&self,
_name: &str,
_input: Value,
session_id: Option<&str>,
cwd: Option<&std::path::Path>,
) -> Result<Value, String> {
self.0.lock().unwrap().push((
session_id.map(str::to_string),
cwd.map(std::path::Path::to_path_buf),
));
Ok(Value::Null)
}
}
#[tokio::test]
async fn only_the_named_no_chat_call_omits_the_caller() {
let handler = Callers::default();
let chat = std::env::temp_dir().join("chat-folder");
handler
.call_tool_for("t", serde_json::json!({}), Some("chat-A"), Some(&chat))
.await
.unwrap();
handler
.call_tool_no_chat("t", serde_json::json!({}))
.await
.unwrap();
assert_eq!(
*handler.0.lock().unwrap(),
vec![(Some("chat-A".to_string()), Some(chat)), (None, None)]
);
}
}
#[cfg(test)]
pub mod tests {
use super::*;
pub struct DummyHandler;
#[async_trait]
impl ExtensionHandler for DummyHandler {
fn id(&self) -> &str {
"dummy"
}
fn lifecycle_snapshot(&self) -> Option<ExtensionLifecycle> {
Some(ExtensionLifecycle {
health: ExtensionHealth::Running,
generation: 0,
})
}
async fn handle(&self, _event: &HookEvent) -> HookResult {
HookResult::default()
}
async fn shutdown(&self) {}
}
}