use std::path::PathBuf;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use oino_agent::{AgentToolResult, ToolInvocation, UpdateSink};
use oino_ai::{JsonObject, JsonValue, Model, ProviderEvent};
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;
pub type ExtensionId = String;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExtensionKind {
Provider,
#[default]
Extension,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExtensionMode {
Tui,
Rpc,
Json,
#[default]
Print,
Acp,
Desktop,
Mobile,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CallKind {
#[default]
Event,
Tool,
Command,
Shortcut,
Service,
Render,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallContext {
pub session_id: String,
pub mode: ExtensionMode,
pub has_ui: bool,
pub cwd: PathBuf,
pub is_child: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub child_role: Option<String>,
pub call_kind: CallKind,
}
impl CallContext {
#[must_use]
pub fn with_kind(&self, call_kind: CallKind) -> Self {
Self {
call_kind,
..self.clone()
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{message}")]
pub struct ExtensionCallError {
pub message: String,
pub trap: bool,
}
impl ExtensionCallError {
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
trap: false,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ToolExposure {
#[default]
Direct,
ModelOnly,
Codemode,
Deferred,
Hidden,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolSpec {
pub name: String,
#[serde(default)]
pub label: String,
pub description: String,
pub parameters: JsonValue,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_snippet: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub prompt_guidelines: Vec<String>,
#[serde(default)]
pub exposure: ToolExposure,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_active: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub execution_mode: Option<oino_agent::ExecutionMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reaches_tools: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_schema: Option<JsonValue>,
#[serde(default)]
pub prepare_arguments: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandSpec {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub hidden: bool,
#[serde(default)]
pub completions: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShortcutSpec {
pub id: String,
#[serde(default)]
pub default_keys: Vec<String>,
#[serde(default)]
pub description: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FlagType {
Boolean,
String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FlagSpec {
pub name: String,
#[serde(rename = "type")]
pub kind: FlagType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<JsonValue>,
#[serde(default)]
pub description: String,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Registration {
#[serde(default)]
pub tools: Vec<ToolSpec>,
#[serde(default)]
pub commands: Vec<CommandSpec>,
#[serde(default)]
pub shortcuts: Vec<ShortcutSpec>,
#[serde(default)]
pub flags: Vec<FlagSpec>,
#[serde(default)]
pub events: Vec<String>,
#[serde(default)]
pub providers: Vec<JsonValue>,
#[serde(default)]
pub virtual_models: Vec<JsonValue>,
#[serde(default)]
pub renderers: Vec<JsonValue>,
#[serde(default)]
pub apis: Vec<String>,
#[serde(default)]
pub arbiter: ArbiterRoles,
#[serde(default)]
pub settings: Vec<JsonValue>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArbiterRoles {
#[serde(default)]
pub nudge_provider: bool,
#[serde(default)]
pub wait_source: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompletionItem {
pub value: String,
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
pub trait ProviderInstance: Send + Sync {
fn stream(
&self,
request: JsonValue,
cancel: CancellationToken,
) -> BoxFuture<
'_,
Result<BoxStream<'static, Result<ProviderEvent, ExtensionCallError>>, ExtensionCallError>,
>;
fn list_models(
&self,
provider_id: &str,
credential: Option<JsonValue>,
) -> BoxFuture<'_, Result<Vec<Model>, ExtensionCallError>>;
fn oauth_login(
&self,
provider_id: &str,
cx: CallContext,
) -> BoxFuture<'_, Result<JsonValue, ExtensionCallError>> {
let _ = (provider_id, cx);
Box::pin(async { Err(ExtensionCallError::new("OAuth sign-in is not supported")) })
}
fn oauth_refresh(
&self,
provider_id: &str,
credential: JsonValue,
) -> BoxFuture<'_, Result<JsonValue, ExtensionCallError>> {
let _ = (provider_id, credential);
Box::pin(async { Err(ExtensionCallError::new("OAuth refresh is not supported")) })
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PackageResourceDirs {
pub skills: Vec<PathBuf>,
pub prompts: Vec<PathBuf>,
pub themes: Vec<PathBuf>,
}
pub trait ExtensionInstance: Send + Sync {
fn id(&self) -> &str;
fn kind(&self) -> ExtensionKind {
ExtensionKind::Extension
}
fn path(&self) -> String {
self.id().to_owned()
}
fn resource_dirs(&self) -> PackageResourceDirs {
PackageResourceDirs::default()
}
fn init(&self, cx: CallContext) -> BoxFuture<'_, Result<Registration, ExtensionCallError>>;
fn on_event(
&self,
kind: &str,
payload: JsonValue,
cx: CallContext,
) -> BoxFuture<'_, Result<Option<JsonValue>, ExtensionCallError>>;
fn run_tool(
&self,
tool: &str,
invocation: ToolInvocation,
updates: UpdateSink,
cancel: CancellationToken,
cx: CallContext,
) -> BoxFuture<'_, Result<AgentToolResult, ExtensionCallError>>;
fn prepare_arguments(&self, _tool: &str, raw: JsonObject) -> JsonObject {
raw
}
fn run_command(
&self,
name: &str,
args: &str,
cancel: CancellationToken,
cx: CallContext,
) -> BoxFuture<'_, Result<Option<JsonValue>, ExtensionCallError>>;
fn complete_command(
&self,
_name: &str,
_prefix: &str,
_cx: CallContext,
) -> BoxFuture<'_, Vec<CompletionItem>> {
Box::pin(async { Vec::new() })
}
fn run_shortcut(
&self,
_id: &str,
_cx: CallContext,
) -> BoxFuture<'_, Result<(), ExtensionCallError>> {
Box::pin(async { Ok(()) })
}
fn complete_input(
&self,
_request: JsonValue,
_cx: CallContext,
) -> BoxFuture<'_, Option<JsonValue>> {
Box::pin(async { None })
}
fn services(&self) -> Vec<String> {
Vec::new()
}
fn handle_service(
&self,
name: &str,
_payload: JsonValue,
_caller: &str,
_cx: CallContext,
) -> BoxFuture<'_, Result<JsonValue, ExtensionCallError>> {
let message = format!("service {name} is not implemented");
Box::pin(async move { Err(ExtensionCallError::new(message)) })
}
fn tool_overrides(&self) -> Vec<String> {
Vec::new()
}
fn render(&self, _request: JsonValue, _cx: CallContext) -> Option<JsonValue> {
None
}
fn route_model(
&self,
_request: JsonValue,
_cx: CallContext,
) -> BoxFuture<'_, Result<JsonValue, ExtensionCallError>> {
Box::pin(async { Err(ExtensionCallError::new("route-model is not implemented")) })
}
fn propose_nudge(&self, _cx: CallContext) -> BoxFuture<'_, Option<JsonValue>> {
Box::pin(async { None })
}
fn nudge_delivered(&self, _id: &str, _cx: CallContext) {}
fn wait_reason(&self, _cx: CallContext) -> BoxFuture<'_, Option<String>> {
Box::pin(async { None })
}
fn hook_deadline(&self, _kind: &str) -> Option<std::time::Duration> {
None
}
fn provider(&self) -> Option<&dyn ProviderInstance> {
None
}
fn shutdown(&self, _reason: &str) -> BoxFuture<'_, ()> {
Box::pin(async {})
}
}