use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::json::RawJson;
macro_rules! event_kinds {
($($(#[$doc:meta])* $variant:ident => $wire:literal,)*) => {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum EventKind {
$($(#[$doc])* #[doc = concat!("`", $wire, "`")] $variant,)*
}
impl EventKind {
pub const ALL: &'static [EventKind] = &[$(EventKind::$variant,)*];
#[must_use]
pub fn wire(self) -> &'static str {
match self { $(EventKind::$variant => $wire,)* }
}
}
impl FromStr for EventKind {
type Err = UnknownEventKind;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { $($wire => Ok(EventKind::$variant),)* _ => Err(UnknownEventKind(s.to_owned())) }
}
}
};
}
event_kinds! {
ProjectTrust => "project_trust",
ResourcesDiscover => "resources_discover",
McpServersChange => "mcp_servers_change",
SessionStart => "session_start",
SessionInfoChanged => "session_info_changed",
SessionBeforeSwitch => "session_before_switch",
SessionBeforeFork => "session_before_fork",
SessionBeforeCompact => "session_before_compact",
SessionCompact => "session_compact",
SessionCompactFailed => "session_compact_failed",
SessionShutdown => "session_shutdown",
SessionBeforeTree => "session_before_tree",
SessionTree => "session_tree",
Context => "context",
ContextWithSystem => "context_with_system",
CacheWarmingDecision => "cache_warming_decision",
BeforeProviderHeaders => "before_provider_headers",
BeforeProviderRequest => "before_provider_request",
AfterProviderResponse => "after_provider_response",
ProviderStreamEvent => "provider_stream_event",
BeforeAgentStart => "before_agent_start",
AgentStart => "agent_start",
AgentEnd => "agent_end",
AgentBeforeSettle => "agent_before_settle",
AgentSettled => "agent_settled",
UiPromptStart => "ui_prompt_start",
UiPromptEnd => "ui_prompt_end",
TurnStart => "turn_start",
TurnEnd => "turn_end",
MessageStart => "message_start",
MessageUpdate => "message_update",
MessageEnd => "message_end",
ToolExecutionStart => "tool_execution_start",
ToolExecutionUpdate => "tool_execution_update",
ToolExecutionEnd => "tool_execution_end",
ModelSelect => "model_select",
ThinkingLevelSelect => "thinking_level_select",
ToolCall => "tool_call",
ToolResult => "tool_result",
UserBash => "user_bash",
Input => "input",
OinoTimer => "oino_timer",
OinoBus => "oino_bus",
OinoUiAction => "oino_ui_action",
OinoEditor => "oino_editor",
OinoSettingsChanged => "oino_settings_changed",
OinoProcess => "oino_process",
OinoAgent => "oino_agent",
OinoTask => "oino_task",
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("unknown event kind `{0}`")]
pub struct UnknownEventKind(pub String);
impl EventKind {
#[must_use]
pub fn is_oino(self) -> bool {
self.wire().starts_with("oino_")
}
}
impl fmt::Display for EventKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.wire())
}
}
impl Serialize for EventKind {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.wire())
}
}
impl<'de> Deserialize<'de> for EventKind {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
text.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Exposure {
#[default]
Direct,
ModelOnly,
Deferred,
Hidden,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExecutionMode {
#[default]
Parallel,
Sequential,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Replay {
Never,
Safe,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ToolKind {
Read,
Edit,
Delete,
Move,
Search,
Execute,
Think,
Fetch,
SwitchMode,
#[default]
Other,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolNamespace {
pub name: String,
#[serde(default)]
pub aliases: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
}
#[allow(clippy::struct_field_names, reason = "mirrors the WIT record")]
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolAnnotations {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub read_only: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub destructive: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idempotent: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub open_world: Option<bool>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "names", rename_all = "lowercase")]
pub enum ReachesTools {
#[default]
None,
All,
Names(Vec<String>),
Dynamic,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolSpec {
pub name: String,
pub label: String,
pub description: String,
pub parameters: RawJson,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_schema: Option<RawJson>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_snippet: Option<String>,
#[serde(default)]
pub prompt_guidelines: Vec<String>,
#[serde(default)]
pub exposure: Exposure,
pub default_active: bool,
#[serde(default)]
pub execution_mode: ExecutionMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub replay: Option<Replay>,
#[serde(default)]
pub kind: ToolKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace: Option<ToolNamespace>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
#[serde(default)]
pub reaches_tools: ReachesTools,
#[serde(default)]
pub prepare_arguments: bool,
#[serde(default)]
pub prepare_loadout: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub render: Option<RawJson>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandSpec {
pub name: String,
pub description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub argument_hint: Option<String>,
#[serde(default)]
pub completions: bool,
#[serde(default)]
pub hidden: bool,
}
#[derive(Clone, Debug, Default, 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_value: Option<RawJson>,
#[serde(default)]
pub description: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RendererTarget {
CustomMessage,
CustomEntry,
Tool,
TurnFooter,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RendererSpec {
pub target: RendererTarget,
pub key: String,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArbiterRoles {
pub nudge_provider: bool,
pub wait_source: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Registration {
#[serde(default)]
pub events: Vec<EventKind>,
#[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 renderers: Vec<RendererSpec>,
#[serde(default)]
pub services: Vec<String>,
#[serde(default)]
pub arbiter: ArbiterRoles,
#[serde(default)]
pub settings: Vec<RawJson>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wire_names_round_trip() {
assert_eq!(EventKind::ALL.len(), 49);
assert_eq!(EventKind::ALL.iter().filter(|k| !k.is_oino()).count(), 41);
for kind in EventKind::ALL {
assert_eq!(kind.wire().parse::<EventKind>().unwrap(), *kind);
assert!(!kind.wire().contains('-'));
}
assert_eq!(
serde_json::to_string(&EventKind::SessionBeforeCompact).unwrap(),
"\"session_before_compact\""
);
assert!("nope".parse::<EventKind>().is_err());
}
}