use futures::future::BoxFuture;
use oino_ai::{JsonValue, ThinkingLevel, UserContentField};
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;
use super::api::{ArbiterRoles, CallContext, CommandSpec, FlagSpec, ShortcutSpec, ToolSpec};
use crate::agents::ChildAgent;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Delivery {
Steer,
FollowUp,
NextTurn,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CustomMessageDraft {
pub custom_type: String,
#[serde(default)]
pub content: UserContentField,
#[serde(default)]
pub display: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<JsonValue>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "query", rename_all = "kebab-case")]
pub enum SessionQuery {
Header,
Entries,
Branch,
Entry {
id: String,
},
Tree,
Projection,
ContextEntries,
Label {
id: String,
},
Name,
Leaf,
Id,
File,
}
#[allow(
clippy::large_enum_variant,
reason = "operations are built once per call and moved, never stored"
)]
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum HostOp {
Subscribe(Vec<String>),
Unsubscribe(Vec<String>),
RegisterTool(ToolSpec),
UnregisterTool(String),
RegisterCommand(CommandSpec),
UnregisterCommand(String),
RegisterShortcut(ShortcutSpec),
UnregisterShortcut(String),
RegisterFlag(FlagSpec),
GetFlag(String),
RegisterRenderer(JsonValue),
RegisterProvider(JsonValue),
UnregisterProvider(String),
RegisterVirtualModel(JsonValue),
UnregisterVirtualModel {
provider: String,
id: String,
},
SessionRead(SessionQuery),
AppendEntry {
custom_type: String,
data: Option<JsonValue>,
},
SendMessage {
message: CustomMessageDraft,
trigger_turn: bool,
deliver_as: Option<Delivery>,
},
SendUserMessage {
content: UserContentField,
deliver_as: Option<Delivery>,
},
SetSessionName(String),
SetLabel {
entry_id: String,
label: Option<String>,
},
GetActiveTools,
GetAllTools,
SetActiveTools(Vec<String>),
GetCommands,
SetModel {
provider: String,
id: String,
},
SetDefaultModel {
provider: String,
id: String,
thinking: Option<ThinkingLevel>,
},
GetModel,
GetThinkingLevel,
SetThinkingLevel(ThinkingLevel),
IsIdle,
Abort,
HasPendingMessages,
GetContextUsage,
Compact {
custom_instructions: Option<String>,
},
GetSystemPrompt,
GetSettings,
GetAvailableModels,
GetScopedModels,
GetRoutedModel,
GetSessionStats,
IsProjectTrusted,
ModelComplete {
model: Option<String>,
context: JsonValue,
options: JsonValue,
cancel: Option<CancellationToken>,
},
SummarizeDefault {
messages: JsonValue,
custom_instructions: Option<String>,
previous_summary: Option<String>,
},
WaitForIdle,
NavigateTree {
target_id: String,
summarize: bool,
custom_instructions: Option<String>,
replace_instructions: bool,
label: Option<String>,
},
UiDialog {
method: String,
params: oino_ai::JsonObject,
cancel: Option<CancellationToken>,
},
UiFire {
method: String,
params: oino_ai::JsonObject,
},
ExecuteTool {
parent_tool_call_id: String,
name: String,
args: oino_ai::JsonObject,
},
InvokeCommand {
name: String,
args: String,
},
SetArbiterRoles(ArbiterRoles),
ListAgents,
GetRunState,
Shutdown,
GetSystemPromptOptions,
NewSession {
parent_session: Option<std::path::PathBuf>,
},
Fork {
entry_id: String,
at: bool,
},
SwitchSession(std::path::PathBuf),
Reload,
RegisterSettings(JsonValue),
GetExtensionSettings {
namespace: String,
layer: crate::extensions::settings::SettingsLayer,
},
SetExtensionSetting {
namespace: String,
key: String,
value: Option<JsonValue>,
scope: oino_settings::SettingsScope,
},
GetAllExtensionSettings,
EditorText,
CredentialFor(String),
BusPublish {
topic: String,
data: JsonValue,
sticky: bool,
},
BusGet(String),
RunningBash,
GetFrontends,
}
impl HostOp {
#[must_use]
pub fn is_bus_op(&self) -> bool {
matches!(self, Self::BusPublish { .. } | Self::BusGet(_))
}
#[must_use]
pub fn is_settings_op(&self) -> bool {
matches!(
self,
Self::RegisterSettings(_)
| Self::GetExtensionSettings { .. }
| Self::SetExtensionSetting { .. }
| Self::GetAllExtensionSettings
| Self::GetSettings
)
}
}
#[derive(Clone, Debug, Default)]
pub struct ModelRequest {
pub model: Option<String>,
pub context: JsonValue,
pub options: JsonValue,
pub cancel: Option<CancellationToken>,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum HostError {
#[error("{0}")]
StaleContext(String),
#[error("This operation is only available in command and shortcut contexts")]
CommandContextRequired,
#[error("{0}")]
Invalid(String),
#[error("{0}")]
Conflict(String),
#[error("{0}")]
Failed(String),
}
pub trait HostApi: Send + Sync {
fn call<'a>(
&'a self,
extension: &'a str,
cx: &'a CallContext,
op: HostOp,
) -> BoxFuture<'a, Result<JsonValue, HostError>>;
fn model_stream<'a>(
&'a self,
_extension: &'a str,
_cx: &'a CallContext,
_request: ModelRequest,
) -> BoxFuture<'a, Result<futures::stream::BoxStream<'static, JsonValue>, HostError>> {
Box::pin(async {
Err(HostError::Failed(
"models.stream is not supported by this host".into(),
))
})
}
fn detach_bash<'a>(
&'a self,
_extension: &'a str,
_cx: &'a CallContext,
_tool_call_id: &'a str,
_task_id: &'a str,
_reason: &'a str,
) -> BoxFuture<'a, Result<crate::session::DetachedBash, HostError>> {
Box::pin(async {
Err(HostError::Failed(
"bash detach is not supported by this host".into(),
))
})
}
fn spawn_agent<'a>(
&'a self,
_extension: &'a str,
_cx: &'a CallContext,
_spec: JsonValue,
) -> BoxFuture<'a, Result<std::sync::Arc<ChildAgent>, HostError>> {
Box::pin(async {
Err(HostError::Failed(
"agents is not supported by this host".into(),
))
})
}
}