diff --git a/src/orchestrator/event.rs b/src/orchestrator/event.rs index cbaa46f..0562b97 100644 --- a/src/orchestrator/event.rs +++ b/src/orchestrator/event.rs @@ -1,13 +1,16 @@ -//! Orchestrator events — the contract between Sol's response pipeline -//! and its presentation layers (Matrix, gRPC, future web/API). +//! Orchestrator event types — the public contract between Sol's response +//! pipeline and any presentation layer. //! -//! Every state transition in the response lifecycle is an event. -//! Transport bridges subscribe to these and translate to their protocol. +//! These types are transport-agnostic. The orchestrator has zero knowledge +//! of Matrix, gRPC, or any specific UI. Transport-specific data flows +//! through as opaque `Metadata`. +use std::collections::HashMap; use std::fmt; -/// Unique identifier for a response generation request. -/// One per user message → response cycle (including all tool iterations). +// ── Request ID ────────────────────────────────────────────────────────── + +/// Unique identifier for a response generation cycle (including all tool iterations). #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct RequestId(pub String); @@ -23,23 +26,43 @@ impl fmt::Display for RequestId { } } -/// Which transport initiated this request. -#[derive(Debug, Clone)] -pub enum ResponseMode { - /// Standard Matrix chat (DM, room, thread). - Chat { - room_id: String, - is_spontaneous: bool, - use_thread: bool, - trigger_event_id: String, - }, - /// Coding session via gRPC (`sunbeam code`). - Code { - session_id: String, - room_id: String, - }, +// ── Metadata ──────────────────────────────────────────────────────────── + +/// Opaque key-value bag that flows from request to events untouched. +/// The orchestrator never inspects this. Transport bridges use it to +/// carry routing data (room_id, session_id, event_id, etc.). +#[derive(Debug, Clone, Default)] +pub struct Metadata(pub HashMap); + +impl Metadata { + pub fn new() -> Self { + Self(HashMap::new()) + } + + pub fn with(mut self, key: impl Into, value: impl Into) -> Self { + self.0.insert(key.into(), value.into()); + self + } + + pub fn get(&self, key: &str) -> Option<&str> { + self.0.get(key).map(|s| s.as_str()) + } + + pub fn insert(&mut self, key: impl Into, value: impl Into) { + self.0.insert(key.into(), value.into()); + } } +// ── Token usage ───────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Default)] +pub struct TokenUsage { + pub prompt_tokens: u32, + pub completion_tokens: u32, +} + +// ── Tool types ────────────────────────────────────────────────────────── + /// Whether a tool executes on the server or on a connected client. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ToolSide { @@ -47,40 +70,66 @@ pub enum ToolSide { Client, } +/// Result payload from a client-side tool execution. +#[derive(Debug)] +pub struct ToolResultPayload { + pub text: String, + pub is_error: bool, +} + +/// Minimal context the tool system needs. No transport types. +#[derive(Debug, Clone)] +pub struct ToolContext { + /// User identifier (portable, e.g. "sienna" or "sienna@sunbeam.pt"). + pub user_id: String, + /// Scope key for access control (e.g. room_id for scoped search). + pub scope_key: String, + /// Whether this is a direct/private conversation. + pub is_direct: bool, +} + +// ── Generate request ──────────────────────────────────────────────────── + +/// Request to generate a response. The single entry point for the orchestrator. +/// Transport-agnostic — callers put routing data in `metadata`. +#[derive(Debug, Clone)] +pub struct GenerateRequest { + /// Unique ID for this request cycle. + pub request_id: RequestId, + /// The user's message text. + pub text: String, + /// User identifier (portable). + pub user_id: String, + /// Display name for the user (optional). + pub display_name: Option, + /// Conversation scope key. The orchestrator uses this to look up + /// or create a Mistral conversation. + pub conversation_key: String, + /// Whether this is a direct/private conversation. + pub is_direct: bool, + /// Optional image data URI. + pub image: Option, + /// Opaque metadata — flows through to `Started` events unchanged. + pub metadata: Metadata, +} + +// ── Events ────────────────────────────────────────────────────────────── + /// An event emitted by the orchestrator during response generation. /// Transport bridges subscribe to these and translate to their protocol. #[derive(Debug, Clone)] pub enum OrchestratorEvent { - // ── Lifecycle ────────────────────────────────────────────── - - /// Response generation has begun. - ResponseStarted { + /// Generation has begun. Carries metadata for bridge routing. + Started { request_id: RequestId, - mode: ResponseMode, + metadata: Metadata, }, - /// The model is generating (typing indicator equivalent). + /// The model is generating. Thinking { request_id: RequestId, }, - /// Final response text is ready. - ResponseReady { - request_id: RequestId, - text: String, - prompt_tokens: u32, - completion_tokens: u32, - tool_iterations: u32, - }, - - /// Response generation failed. - ResponseFailed { - request_id: RequestId, - error: String, - }, - - // ── Tool execution ───────────────────────────────────────── - /// A tool call was detected in the model's output. ToolCallDetected { request_id: RequestId, @@ -91,46 +140,32 @@ pub enum OrchestratorEvent { }, /// A tool started executing. - ToolExecutionStarted { + ToolStarted { request_id: RequestId, call_id: String, name: String, }, /// A tool finished executing. - ToolExecutionCompleted { + ToolCompleted { request_id: RequestId, call_id: String, name: String, - result: String, + result_preview: String, success: bool, }, - // ── Progress (presentation hints) ────────────────────────── - - /// Agentic progress started (first tool call in a response). - AgentProgressStarted { + /// Final response ready. + Done { request_id: RequestId, + text: String, + usage: TokenUsage, }, - /// A progress step (tool call summary for display). - AgentProgressStep { + /// Generation failed. + Failed { request_id: RequestId, - summary: String, - }, - - /// Agentic progress complete (all tool iterations done). - AgentProgressDone { - request_id: RequestId, - }, - - // ── Side effects ─────────────────────────────────────────── - - /// Memory extraction should run for this exchange. - MemoryExtractionScheduled { - request_id: RequestId, - user_msg: String, - response: String, + error: String, }, } @@ -138,49 +173,18 @@ impl OrchestratorEvent { /// Get the request ID for any event variant. pub fn request_id(&self) -> &RequestId { match self { - Self::ResponseStarted { request_id, .. } + Self::Started { request_id, .. } | Self::Thinking { request_id } - | Self::ResponseReady { request_id, .. } - | Self::ResponseFailed { request_id, .. } | Self::ToolCallDetected { request_id, .. } - | Self::ToolExecutionStarted { request_id, .. } - | Self::ToolExecutionCompleted { request_id, .. } - | Self::AgentProgressStarted { request_id } - | Self::AgentProgressStep { request_id, .. } - | Self::AgentProgressDone { request_id } - | Self::MemoryExtractionScheduled { request_id, .. } => request_id, + | Self::ToolStarted { request_id, .. } + | Self::ToolCompleted { request_id, .. } + | Self::Done { request_id, .. } + | Self::Failed { request_id, .. } => request_id, } } } -/// Request to generate a chat response (Matrix path). -#[derive(Debug, Clone)] -pub struct ChatRequest { - pub request_id: RequestId, - pub trigger_body: String, - pub trigger_sender: String, - pub room_id: String, - pub room_name: String, - pub is_dm: bool, - pub is_spontaneous: bool, - pub use_thread: bool, - pub trigger_event_id: String, - pub image_data_uri: Option, - pub context_hint: Option, -} - -/// Request to generate a coding response (gRPC path). -#[derive(Debug, Clone)] -pub struct CodeRequest { - pub request_id: RequestId, - pub session_id: String, - pub text: String, - pub project_name: String, - pub project_path: String, - pub prompt_md: String, - pub model: String, - pub room_id: String, -} +// ── Tests ─────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { @@ -194,29 +198,50 @@ mod tests { } #[test] - fn test_event_request_id_accessor() { - let id = RequestId::new(); - let event = OrchestratorEvent::Thinking { - request_id: id.clone(), - }; - assert_eq!(event.request_id(), &id); + fn test_metadata_roundtrip() { + let meta = Metadata::new() + .with("room_id", "!abc:test") + .with("session_id", "sess-1"); + assert_eq!(meta.get("room_id"), Some("!abc:test")); + assert_eq!(meta.get("session_id"), Some("sess-1")); + assert_eq!(meta.get("missing"), None); } #[test] - fn test_response_mode_variants() { - let chat = ResponseMode::Chat { - room_id: "!room:test".into(), - is_spontaneous: false, - use_thread: true, - trigger_event_id: "$event".into(), - }; - assert!(matches!(chat, ResponseMode::Chat { .. })); - - let code = ResponseMode::Code { - session_id: "sess-1".into(), - room_id: "!room:test".into(), - }; - assert!(matches!(code, ResponseMode::Code { .. })); + fn test_event_request_id_accessor() { + let id = RequestId::new(); + let events = vec![ + OrchestratorEvent::Started { request_id: id.clone(), metadata: Metadata::new() }, + OrchestratorEvent::Thinking { request_id: id.clone() }, + OrchestratorEvent::Done { + request_id: id.clone(), + text: "hi".into(), + usage: TokenUsage::default(), + }, + OrchestratorEvent::Failed { request_id: id.clone(), error: "err".into() }, + OrchestratorEvent::ToolCallDetected { + request_id: id.clone(), + call_id: "c1".into(), + name: "bash".into(), + args: "{}".into(), + side: ToolSide::Server, + }, + OrchestratorEvent::ToolStarted { + request_id: id.clone(), + call_id: "c1".into(), + name: "bash".into(), + }, + OrchestratorEvent::ToolCompleted { + request_id: id.clone(), + call_id: "c1".into(), + name: "bash".into(), + result_preview: "ok".into(), + success: true, + }, + ]; + for event in &events { + assert_eq!(event.request_id(), &id); + } } #[test] @@ -225,64 +250,13 @@ mod tests { } #[test] - fn test_all_event_variants_have_request_id() { - let id = RequestId::new(); - let events = vec![ - OrchestratorEvent::ResponseStarted { - request_id: id.clone(), - mode: ResponseMode::Chat { - room_id: "!r:t".into(), - is_spontaneous: false, - use_thread: false, - trigger_event_id: "$e".into(), - }, - }, - OrchestratorEvent::Thinking { request_id: id.clone() }, - OrchestratorEvent::ResponseReady { - request_id: id.clone(), - text: "hi".into(), - prompt_tokens: 0, - completion_tokens: 0, - tool_iterations: 0, - }, - OrchestratorEvent::ResponseFailed { - request_id: id.clone(), - error: "err".into(), - }, - OrchestratorEvent::ToolCallDetected { - request_id: id.clone(), - call_id: "c1".into(), - name: "bash".into(), - args: "{}".into(), - side: ToolSide::Server, - }, - OrchestratorEvent::ToolExecutionStarted { - request_id: id.clone(), - call_id: "c1".into(), - name: "bash".into(), - }, - OrchestratorEvent::ToolExecutionCompleted { - request_id: id.clone(), - call_id: "c1".into(), - name: "bash".into(), - result: "ok".into(), - success: true, - }, - OrchestratorEvent::AgentProgressStarted { request_id: id.clone() }, - OrchestratorEvent::AgentProgressStep { - request_id: id.clone(), - summary: "step".into(), - }, - OrchestratorEvent::AgentProgressDone { request_id: id.clone() }, - OrchestratorEvent::MemoryExtractionScheduled { - request_id: id.clone(), - user_msg: "q".into(), - response: "a".into(), - }, - ]; - - for event in &events { - assert_eq!(event.request_id(), &id); - } + fn test_tool_context() { + let ctx = ToolContext { + user_id: "sienna".into(), + scope_key: "!room:test".into(), + is_direct: true, + }; + assert_eq!(ctx.user_id, "sienna"); + assert!(ctx.is_direct); } }