use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use oino_ai::{Model, ProviderEvent};
use tokio_util::sync::CancellationToken;
use crate::guest::{CompletionItem, InitInput, ToolInvocation};
use crate::json::RawJson;
use crate::manifest::Manifest;
use crate::ops::{
ControlOp, ControlReply, DeviceOp, EditorOp, HostContextInfo, ModelsOp, ModelsReply, PanelOp,
ProviderHook, ProviderHookReply, SecretOp, SessionQuery, SessionWrite, SessionsIndexQuery,
SettingsLayer, SettingsScope, TaskOp,
};
use crate::registry::{
ArbiterRoles, CommandSpec, EventKind, FlagSpec, Registration, RendererSpec, ShortcutSpec,
ToolSpec,
};
use crate::types::{CallContext, ExtensionError, ExtensionId, HostError};
pub type BoxFut<'a, T> = BoxFuture<'a, T>;
pub struct DetachedBash {
pub command: String,
pub pid: Option<u32>,
pub output_so_far: String,
pub full_output_path: Option<String>,
pub output: BoxStream<'static, Vec<u8>>,
pub exit: BoxFuture<'static, Option<i32>>,
pub kill: CancellationToken,
}
impl std::fmt::Debug for DetachedBash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DetachedBash")
.field("command", &self.command)
.field("pid", &self.pid)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub enum ComponentSource {
Bytes(Arc<[u8]>),
Path(PathBuf),
}
#[derive(Clone, Debug)]
pub struct LoadedPackage {
pub manifest: Manifest,
pub component: ComponentSource,
pub root: Option<PathBuf>,
pub bundled: bool,
}
pub trait ExtensionRuntime: Send + Sync {
fn load(
&self,
package: LoadedPackage,
host: Arc<dyn HostServices>,
) -> BoxFut<'_, Result<Arc<dyn ExtensionInstance>, ExtensionError>>;
}
pub trait ExtensionInstance: Send + Sync {
fn id(&self) -> &ExtensionId;
fn manifest(&self) -> &Manifest;
fn init(
&self,
input: InitInput,
cx: CallContext,
) -> BoxFut<'_, Result<Registration, ExtensionError>>;
fn on_event(
&self,
kind: EventKind,
payload: RawJson,
cx: CallContext,
) -> BoxFut<'_, Result<Option<RawJson>, ExtensionError>>;
fn run_tool(
&self,
inv: ToolInvocation,
cancel: CancellationToken,
cx: CallContext,
) -> BoxFut<'_, Result<RawJson, ExtensionError>>;
fn prepare_arguments(&self, tool: &str, raw: RawJson) -> Result<RawJson, ExtensionError>;
fn prepare_loadout(
&self,
tool: &str,
loadout: RawJson,
) -> Result<Option<RawJson>, ExtensionError>;
fn reaches_tools(&self, tool: &str, input: RawJson) -> Result<RawJson, ExtensionError>;
fn run_command(
&self,
name: &str,
args: &str,
cancel: CancellationToken,
cx: CallContext,
) -> BoxFut<'_, Result<Option<RawJson>, ExtensionError>>;
fn complete_command(
&self,
name: &str,
prefix: &str,
cx: CallContext,
) -> BoxFut<'_, Vec<CompletionItem>>;
fn run_shortcut(&self, id: &str, cx: CallContext) -> BoxFut<'_, Result<(), ExtensionError>>;
fn handle_service(
&self,
name: &str,
payload: RawJson,
caller: &ExtensionId,
cx: CallContext,
) -> BoxFut<'_, Result<RawJson, ExtensionError>>;
fn propose_nudge(&self, cx: CallContext) -> BoxFut<'_, Option<RawJson>>;
fn nudge_delivered(&self, id: &str, cx: CallContext);
fn wait_reason(&self, cx: CallContext) -> BoxFut<'_, Option<String>>;
fn render(&self, request: RawJson, cx: CallContext) -> Option<RawJson>;
fn route_model(
&self,
request: RawJson,
cx: CallContext,
) -> BoxFut<'_, Result<RawJson, ExtensionError>>;
fn complete_input(&self, request: RawJson, cx: CallContext) -> BoxFut<'_, Option<RawJson>>;
fn provider(&self) -> Option<&dyn ProviderInstance>;
fn shutdown(&self, reason: &str) -> BoxFut<'_, ()>;
}
pub type ProviderEventStream = BoxStream<'static, Result<ProviderEvent, ExtensionError>>;
pub trait ProviderInstance: Send + Sync {
fn stream(&self, request: RawJson) -> BoxFut<'_, Result<ProviderEventStream, ExtensionError>>;
fn list_models(
&self,
provider_id: &str,
credential: Option<RawJson>,
) -> BoxFut<'_, Result<Vec<Model>, ExtensionError>>;
fn oauth_login(
&self,
provider_id: &str,
cx: CallContext,
) -> BoxFut<'_, Result<RawJson, ExtensionError>>;
fn oauth_refresh(
&self,
provider_id: &str,
credential: RawJson,
) -> BoxFut<'_, Result<RawJson, ExtensionError>>;
}
pub trait AgentHandle: Send + Sync {
fn id(&self) -> String;
fn session_file(&self) -> Option<String>;
fn prompt(
&self,
text: String,
delivery: Option<String>,
) -> BoxFut<'_, Result<String, HostError>>;
fn steer(&self, text: String) -> BoxFut<'_, Result<(), HostError>>;
fn follow_up(&self, text: String) -> BoxFut<'_, Result<(), HostError>>;
fn abort(&self);
fn set_thinking(&self, level: &str) -> Result<String, HostError>;
fn last_assistant_text(&self) -> Option<String>;
fn wait_idle(&self) -> BoxFut<'_, Result<(), HostError>>;
fn usage(&self) -> RawJson;
fn events(&self) -> BoxStream<'static, String>;
fn close(&self, grace: Duration) -> BoxFut<'_, ()>;
}
macro_rules! unsupported {
($name:literal) => {
Err(HostError::unsupported($name))
};
}
#[allow(unused_variables, reason = "default bodies ignore their arguments")]
pub trait HostServices: Send + Sync {
fn subscribe(&self, ext: &ExtensionId, events: &[EventKind]) {}
fn unsubscribe(&self, ext: &ExtensionId, events: &[EventKind]) {}
fn register_tool(&self, ext: &ExtensionId, spec: ToolSpec) -> Result<(), HostError> {
unsupported!("registry.register-tool")
}
fn unregister_tool(&self, ext: &ExtensionId, name: &str) {}
fn register_command(&self, ext: &ExtensionId, spec: CommandSpec) -> Result<String, HostError> {
unsupported!("registry.register-command")
}
fn unregister_command(&self, ext: &ExtensionId, name: &str) {}
fn register_shortcut(&self, ext: &ExtensionId, spec: ShortcutSpec) -> Result<(), HostError> {
unsupported!("registry.register-shortcut")
}
fn unregister_shortcut(&self, ext: &ExtensionId, id: &str) {}
fn register_flag(&self, ext: &ExtensionId, spec: FlagSpec) -> Result<(), HostError> {
unsupported!("registry.register-flag")
}
fn get_flag(&self, ext: &ExtensionId, name: &str) -> Option<RawJson> {
None
}
fn register_renderer(&self, ext: &ExtensionId, spec: RendererSpec) {}
fn register_service(&self, ext: &ExtensionId, name: &str) -> Result<(), HostError> {
unsupported!("registry.register-service")
}
fn set_arbiter_roles(&self, ext: &ExtensionId, roles: ArbiterRoles) {}
fn register_settings(&self, ext: &ExtensionId, ns: RawJson) -> Result<(), HostError> {
unsupported!("registry.register-settings")
}
fn extension_settings(
&self,
ext: &ExtensionId,
namespace: &str,
layer: SettingsLayer,
) -> Result<RawJson, HostError> {
unsupported!("registry.extension-settings")
}
fn set_extension_setting(
&self,
ext: &ExtensionId,
namespace: &str,
key: &str,
value: Option<RawJson>,
scope: SettingsScope,
) -> Result<(), HostError> {
unsupported!("registry.set-extension-setting")
}
fn register_provider(&self, ext: &ExtensionId, cfg: RawJson) -> Result<(), HostError> {
unsupported!("registry.register-provider")
}
fn unregister_provider(&self, ext: &ExtensionId, id: &str) {}
fn register_virtual_model(&self, ext: &ExtensionId, def: RawJson) -> Result<(), HostError> {
unsupported!("registry.register-virtual-model")
}
fn unregister_virtual_model(&self, ext: &ExtensionId, provider: &str, id: &str) {}
fn session_read(&self, cx: &CallContext, q: SessionQuery) -> Result<RawJson, HostError> {
unsupported!("session reads")
}
fn session_write(
&self,
ext: &ExtensionId,
cx: &CallContext,
w: SessionWrite,
) -> BoxFut<'_, Result<Option<String>, HostError>> {
Box::pin(async { unsupported!("session writes") })
}
fn sessions_index(
&self,
ext: &ExtensionId,
q: SessionsIndexQuery,
) -> BoxFut<'_, Result<RawJson, HostError>> {
Box::pin(async { unsupported!("session.sessions-list") })
}
fn control(
&self,
ext: &ExtensionId,
cx: &CallContext,
op: ControlOp,
) -> BoxFut<'_, Result<ControlReply, HostError>> {
let name = op.name();
Box::pin(async move { Err(HostError::unsupported(&format!("control.{name}"))) })
}
fn tool_progress(&self, cx: &CallContext, tool_call_id: &str, partial: RawJson) {}
fn models(
&self,
ext: &ExtensionId,
cx: &CallContext,
op: ModelsOp,
) -> BoxFut<'_, Result<ModelsReply, HostError>> {
Box::pin(async { unsupported!("models") })
}
fn ui_dialog(
&self,
ext: &ExtensionId,
cx: &CallContext,
req: RawJson,
cancel: Option<CancellationToken>,
) -> BoxFut<'_, Result<RawJson, HostError>> {
Box::pin(async { unsupported!("ui.dialog") })
}
fn ui_fire(&self, ext: &ExtensionId, cx: &CallContext, req: RawJson) -> Result<(), HostError> {
unsupported!("ui.fire")
}
fn ui_panel(
&self,
ext: &ExtensionId,
cx: &CallContext,
op: PanelOp,
) -> Result<Option<String>, HostError> {
unsupported!("ui panels")
}
fn ui_editor(&self, cx: &CallContext, op: EditorOp) -> Result<Option<String>, HostError> {
unsupported!("ui editor")
}
fn ui_device(
&self,
ext: &ExtensionId,
cx: &CallContext,
op: DeviceOp,
) -> BoxFut<'_, Result<Option<RawJson>, HostError>> {
Box::pin(async { unsupported!("ui device") })
}
fn agent_spawn(
&self,
ext: &ExtensionId,
cx: &CallContext,
spec: RawJson,
) -> BoxFut<'_, Result<Arc<dyn AgentHandle>, HostError>> {
Box::pin(async { unsupported!("agents") })
}
fn agents_list(&self, cx: &CallContext) -> RawJson {
RawJson::from_json_value(&serde_json::Value::Array(Vec::new()))
}
fn take_bash(
&self,
ext: &ExtensionId,
cx: &CallContext,
tool_call_id: &str,
task_id: &str,
reason: &str,
) -> BoxFut<'_, Result<DetachedBash, HostError>> {
Box::pin(async { unsupported!("bash detach") })
}
fn tasks(&self, ext: &ExtensionId, cx: &CallContext, op: TaskOp) -> Result<RawJson, HostError> {
unsupported!("tasks")
}
fn bus_publish(
&self,
ext: &ExtensionId,
cx: &CallContext,
topic: &str,
data: RawJson,
sticky: bool,
) {
}
fn bus_subscribe(&self, ext: &ExtensionId, cx: &CallContext, pattern: &str, on: bool) {}
fn bus_get(&self, cx: &CallContext, topic: &str) -> Option<RawJson> {
None
}
fn service_call(
&self,
ext: &ExtensionId,
cx: &CallContext,
target: Option<&ExtensionId>,
name: &str,
payload: RawJson,
timeout: Duration,
) -> BoxFut<'_, Result<RawJson, HostError>> {
Box::pin(async { unsupported!("services.call") })
}
fn services_list(&self) -> Vec<(ExtensionId, String)> {
Vec::new()
}
fn secret(&self, ext: &ExtensionId, op: SecretOp) -> Result<Option<String>, HostError> {
unsupported!("secrets")
}
fn provider_hook(
&self,
ext: &ExtensionId,
hook: ProviderHook,
) -> BoxFut<'_, ProviderHookReply> {
let reply = hook.passthrough();
Box::pin(async move { reply })
}
fn host_context(&self, cx: &CallContext) -> HostContextInfo {
HostContextInfo::default()
}
fn host_service(
&self,
ext: &ExtensionId,
cx: &CallContext,
name: &str,
op: &str,
payload: RawJson,
cancel: CancellationToken,
) -> BoxFut<'_, Result<RawJson, HostError>> {
Box::pin(async { unsupported!("host-service.call") })
}
fn host_services(&self) -> Vec<String> {
Vec::new()
}
fn deliver_event(
&self,
ext: &ExtensionId,
session_id: &str,
kind: EventKind,
payload: RawJson,
) {
}
}