Watch
1
0
Fork
You've already forked souveraine
0
souveraine/src/api/models.rs

435 lines
17 KiB
Rust

#![allow(dead_code)] // WIP scaffolding not yet wired
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSummary {
pub id: String,
pub name: String,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentState {
pub id: String,
pub name: String,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub llm_config: LlmConfig,
pub memory: MemoryConfig,
pub memory_blocks: Vec<MemoryBlock>,
pub tools: Vec<String>,
pub tags: Vec<String>,
/// Public key (hex) of the instance that created this agent.
/// `None` for agents created before this field existed.
pub owner_seed_id: Option<String>,
#[serde(rename = "_souveraine")]
pub souveraine: SouveraineConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmConfig {
pub model: String,
#[serde(default = "default_context_window")]
pub context_window: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
/// Maximum tool-calling rounds before forcing a text response.
/// Configurable per-agent; 0 disables tools entirely.
#[serde(default = "default_max_tool_rounds")]
pub max_tool_rounds: u32,
/// Milliseconds to wait between tool rounds to avoid rate-limit cascades.
/// The follow-up LLM call after a tool executes can trigger rate limits
/// if it arrives too quickly. Default 500ms.
#[serde(default = "default_inter_round_delay")]
pub inter_round_delay_ms: u64,
/// Whether this model supports image inputs (vision).
/// When false, images are stripped to text markers before sending.
#[serde(default = "default_supports_images")]
pub supports_images: bool,
/// How many tool rounds between subconscious mid-turn checkpoints.
/// 0 disables checkpointing entirely.
#[serde(default = "default_checkpoint_interval")]
pub checkpoint_interval: u32,
}
fn default_supports_images() -> bool { true }
fn default_checkpoint_interval() -> u32 { 10 }
fn default_context_window() -> u32 {
128000
}
fn default_max_tool_rounds() -> u32 {
10
}
fn default_inter_round_delay() -> u64 {
500
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryConfig {
pub git_enabled: bool,
pub auto_commit: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_window: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryBlock {
pub label: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SouveraineConfig {
pub n1_enabled: bool,
pub reflection_enabled: bool,
pub archivist_enabled: bool,
pub archivist_threshold: f32,
pub sensorium_bandwidth: String,
/// Per-agent model override for the subconscious (N+1) pass. When set,
/// it wins over the global `[subconscious] model`. None falls back to
/// the global setting, then the agent's own primary model.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subconscious_model: Option<String>,
/// Per-agent model override for the reflection (N+25) pass.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reflection_model: Option<String>,
/// Per-agent model override for the archivist (N+100) synthesis pass.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub archivist_model: Option<String>,
/// Per-agent inference provider name (e.g. "zai"). When set, the agent's
/// requests route through the named provider instead of the global
/// `[inference] provider`. None falls back to the global default.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct CreateAgentRequest {
pub name: String,
#[serde(default)]
pub description: Option<String>,
pub llm_config: LlmConfig,
#[serde(default)]
pub memory_blocks: Vec<MemoryBlock>,
#[serde(default)]
pub tools: Vec<String>,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct UpdateAgentRequest {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub llm_config: Option<LlmConfig>,
#[serde(default)]
pub memory_blocks: Option<Vec<MemoryBlock>>,
#[serde(default)]
pub tools: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub struct AgentFilters {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub tags: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conversation {
pub id: String,
pub agent_id: String,
pub created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Deserialize)]
pub struct CreateConversationRequest {
pub agent_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: String,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
impl Message {
/// Convert API Message to internal ConversationMessage
pub fn to_conversation_message(&self) -> crate::core::session::ConversationMessage {
use crate::core::session::{ConversationMessage, MessageRole, ContentBlock};
let role = match self.role.as_str() {
"system" => MessageRole::System,
"user" => MessageRole::User,
"assistant" => MessageRole::Assistant,
"tool" => MessageRole::Tool,
_ => MessageRole::User,
};
ConversationMessage {
role,
blocks: vec![ContentBlock::Text { text: self.content.clone() }],
usage: None,
timestamp: Some(chrono::Utc::now()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub function: ToolFunction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolFunction {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Deserialize)]
pub struct SendMessageRequest {
pub messages: Vec<Message>,
#[serde(default)]
pub stream: bool,
/// Ambient context from the sending surface — what the environment
/// senses at the moment of speaking: active window, open apps, cursor
/// position, device sensors. Injected as a system note before the user
/// message so the agent perceives the room she is being spoken to in.
#[serde(default)]
pub ambient: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct InterjectRequest {
pub text: String,
}
/// The full wire mirror of [`crate::backend::BackendEvent`].
///
/// Every engine event crosses the SSE boundary — no silent skips. The
/// exhaustive `From` impls in both directions mean a new `BackendEvent`
/// variant is a compile error here, not an invisible hole in every
/// non-TUI surface. Tag values are the wire contract; never rename.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "message_type")]
pub enum StreamEvent {
#[serde(rename = "assistant_message")]
AssistantMessage { content: String },
#[serde(rename = "reasoning_message")]
ReasoningMessage { content: String },
#[serde(rename = "tool_call_message")]
ToolCallMessage {
tool_call: ToolCall,
#[serde(default)]
round: u32,
},
#[serde(rename = "tool_return_message")]
ToolReturnMessage { tool_return: ToolReturn },
#[serde(rename = "souveraine_surfacing")]
Surfacing { source: String, content: String, priority: String },
#[serde(rename = "souveraine_reflection")]
Reflection { content: String },
#[serde(rename = "souveraine_archivist")]
Archivist { synthesis: String, pressure: f32 },
#[serde(rename = "compaction_warning")]
CompactionWarning { pressure: f32, tier: u8 },
#[serde(rename = "context_pressure")]
ContextPressure { pressure: f32, tokens: usize },
#[serde(rename = "inference_strain")]
InferenceStrain { attempt: u32, status: u16, model: String },
#[serde(rename = "schedule_active")]
ScheduleActive { name: String },
#[serde(rename = "schedule_complete")]
ScheduleComplete { name: String, silent: bool },
#[serde(rename = "subconscious_token")]
SubconsciousToken { content: String },
#[serde(rename = "subconscious_tool_call")]
SubconsciousToolCall { name: String, arguments: String },
#[serde(rename = "subconscious_tool_result")]
SubconsciousToolResult { name: String, output: String, is_error: bool },
#[serde(rename = "subconscious_halt")]
SubconsciousHalt { reason: String, severity: String },
#[serde(rename = "subconscious_pass")]
SubconsciousPass { active: bool },
#[serde(rename = "atmosphere")]
Atmosphere { preset: String },
#[serde(rename = "itinerary")]
Itinerary { route: String },
#[serde(rename = "outfit")]
Outfit { name: String },
#[serde(rename = "interstitial")]
Interstitial { text: String, register: crate::backend::Register },
#[serde(rename = "primary_complete")]
PrimaryComplete,
/// The turn failed in the substrate. Mirrors BackendEvent::Error so a
/// dead turn is never a silently-ended stream.
#[serde(rename = "error")]
Error { message: String },
#[serde(rename = "done")]
Done,
#[serde(rename = "ping")]
Ping,
}
impl StreamEvent {
pub fn message_type(&self) -> &'static str {
match self {
StreamEvent::AssistantMessage { .. } => "message",
StreamEvent::ReasoningMessage { .. } => "reasoning",
StreamEvent::ToolCallMessage { .. } => "tool_call",
StreamEvent::ToolReturnMessage { .. } => "tool_return",
StreamEvent::Surfacing { .. } => "souveraine_surfacing",
StreamEvent::Reflection { .. } => "souveraine_reflection",
StreamEvent::Archivist { .. } => "souveraine_archivist",
StreamEvent::CompactionWarning { .. } => "compaction_warning",
StreamEvent::ContextPressure { .. } => "context_pressure",
StreamEvent::InferenceStrain { .. } => "inference_strain",
StreamEvent::ScheduleActive { .. } => "schedule_active",
StreamEvent::ScheduleComplete { .. } => "schedule_complete",
StreamEvent::SubconsciousToken { .. } => "subconscious_token",
StreamEvent::SubconsciousToolCall { .. } => "subconscious_tool_call",
StreamEvent::SubconsciousToolResult { .. } => "subconscious_tool_result",
StreamEvent::SubconsciousHalt { .. } => "subconscious_halt",
StreamEvent::SubconsciousPass { .. } => "subconscious_pass",
StreamEvent::Atmosphere { .. } => "atmosphere",
StreamEvent::Itinerary { .. } => "itinerary",
StreamEvent::Outfit { .. } => "outfit",
StreamEvent::Interstitial { .. } => "interstitial",
StreamEvent::PrimaryComplete => "primary_complete",
StreamEvent::Error { .. } => "error",
StreamEvent::Done => "done",
StreamEvent::Ping => "ping",
}
}
}
impl From<crate::backend::BackendEvent> for StreamEvent {
fn from(be: crate::backend::BackendEvent) -> Self {
use crate::backend::BackendEvent as BE;
match be {
BE::Token(content) => Self::AssistantMessage { content },
BE::Reasoning(content) => Self::ReasoningMessage { content },
BE::Surfacing { source, content, priority } => Self::Surfacing { source, content, priority },
BE::Reflection(content) => Self::Reflection { content },
BE::Archivist { synthesis, pressure } => Self::Archivist { synthesis, pressure },
BE::CompactionWarning { pressure, tier } => Self::CompactionWarning { pressure, tier },
BE::ContextPressure(pressure, tokens) => Self::ContextPressure { pressure, tokens },
BE::InferenceStrain { attempt, status, model } => Self::InferenceStrain { attempt, status, model },
BE::ScheduleActive { name } => Self::ScheduleActive { name },
BE::ScheduleComplete { name, silent } => Self::ScheduleComplete { name, silent },
BE::ToolCall { id, name, arguments, round } => Self::ToolCallMessage {
tool_call: ToolCall { id, function: ToolFunction { name, arguments } },
round,
},
BE::ToolResult { id, name, output, is_error } => Self::ToolReturnMessage {
tool_return: ToolReturn {
status: if is_error { "error".into() } else { "success".into() },
output,
id,
name,
},
},
BE::SubconsciousToken(content) => Self::SubconsciousToken { content },
BE::SubconsciousToolCall { name, arguments } => Self::SubconsciousToolCall { name, arguments },
BE::SubconsciousToolResult { name, output, is_error } => {
Self::SubconsciousToolResult { name, output, is_error }
}
BE::SubconsciousHalt { reason, severity } => Self::SubconsciousHalt { reason, severity },
BE::SubconsciousPass(active) => Self::SubconsciousPass { active },
BE::Atmosphere(preset) => Self::Atmosphere { preset },
BE::Itinerary(route) => Self::Itinerary { route },
BE::Outfit(name) => Self::Outfit { name },
BE::Interstitial { text, register } => Self::Interstitial { text, register },
BE::Keepalive => Self::Ping,
BE::PrimaryComplete => Self::PrimaryComplete,
BE::Error { message } => Self::Error { message },
BE::Done => Self::Done,
}
}
}
impl From<StreamEvent> for crate::backend::BackendEvent {
fn from(se: StreamEvent) -> Self {
use crate::backend::BackendEvent as BE;
match se {
StreamEvent::AssistantMessage { content } => BE::Token(content),
StreamEvent::ReasoningMessage { content } => BE::Reasoning(content),
StreamEvent::Surfacing { source, content, priority } => BE::Surfacing { source, content, priority },
StreamEvent::Reflection { content } => BE::Reflection(content),
StreamEvent::Archivist { synthesis, pressure } => BE::Archivist { synthesis, pressure },
StreamEvent::CompactionWarning { pressure, tier } => BE::CompactionWarning { pressure, tier },
StreamEvent::ContextPressure { pressure, tokens } => BE::ContextPressure(pressure, tokens),
StreamEvent::InferenceStrain { attempt, status, model } => BE::InferenceStrain { attempt, status, model },
StreamEvent::ScheduleActive { name } => BE::ScheduleActive { name },
StreamEvent::ScheduleComplete { name, silent } => BE::ScheduleComplete { name, silent },
StreamEvent::ToolCallMessage { tool_call, round } => BE::ToolCall {
id: tool_call.id,
name: tool_call.function.name,
arguments: tool_call.function.arguments,
round,
},
StreamEvent::ToolReturnMessage { tool_return } => BE::ToolResult {
id: tool_return.id,
name: tool_return.name,
is_error: tool_return.status == "error",
output: tool_return.output,
},
StreamEvent::SubconsciousToken { content } => BE::SubconsciousToken(content),
StreamEvent::SubconsciousToolCall { name, arguments } => BE::SubconsciousToolCall { name, arguments },
StreamEvent::SubconsciousToolResult { name, output, is_error } => {
BE::SubconsciousToolResult { name, output, is_error }
}
StreamEvent::SubconsciousHalt { reason, severity } => BE::SubconsciousHalt { reason, severity },
StreamEvent::SubconsciousPass { active } => BE::SubconsciousPass(active),
StreamEvent::Atmosphere { preset } => BE::Atmosphere(preset),
StreamEvent::Itinerary { route } => BE::Itinerary(route),
StreamEvent::Outfit { name } => BE::Outfit(name),
StreamEvent::Interstitial { text, register } => BE::Interstitial { text, register },
StreamEvent::Ping => BE::Keepalive,
StreamEvent::PrimaryComplete => BE::PrimaryComplete,
StreamEvent::Error { message } => BE::Error { message },
StreamEvent::Done => BE::Done,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolReturn {
pub status: String,
pub output: String,
/// Tool-call id this return answers. Empty on frames from pre-widening servers.
#[serde(default)]
pub id: String,
/// Tool name. Empty on frames from pre-widening servers.
#[serde(default)]
pub name: String,
}
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub error: String,
pub message: String,
}