435 lines
17 KiB
Rust
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,
|
|
}
|