//! The common event model and the `Driver` trait -- the one abstraction //! everything hangs off (see PLAN.md). //! //! A driver translates its child process's JSONL dialect into [`Event`]s //! and accepts the small inbound vocabulary below. The transcript, the SSE //! stream, and the phone UI work purely in this model; nothing downstream //! of a driver may branch on the session kind. use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; /// The name a session's image is stored and served under -- returned by /// `POST /attachments` for an upload, minted by a driver for one a tool /// produced, and fetched back from `/sessions/{id}/files/{ref}`. Both /// directions use the one id so the transcript renders them identically. pub type ImageRef = String; /// Everything a session can tell the outside world. Every event is /// appended to the session's transcript with a sequence number, then fanned /// out to SSE subscribers; the phone renders purely from this stream, so /// reconnecting is just "events after seq N" -- no separate history path /// to drift from the live one. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum Event { /// What the user sent, echoed into the transcript by the manager (not /// by drivers) so every device renders the full conversation from the /// one stream. UserMessage { text: String, }, /// Streaming assistant text; the phone renders the concatenation as /// markdown. AssistantText { delta: String, }, ToolStart { id: String, tool: String, input: serde_json::Value, }, ToolUpdate { id: String, output: String, }, ToolEnd { id: String, output: String, }, /// An image the session produced or was sent, saved under the session /// dir and referenced by id; the phone fetches it by URL. Image { #[serde(rename = "ref")] image: ImageRef, }, /// Anything the session needs a human for: AskUserQuestion, and /// permission requests, are the same shape with different options. Question { id: String, prompt: String, options: Vec, }, /// The manager's record of a question being answered, so a rendered /// question card resolves on every device, not just the one that /// answered it. Answered { id: String, answer: String, }, Status { state: SessionStatus, }, /// Per-turn token counts, where the dialect reports them. UsageDelta { tokens: u64, }, Error { message: String, }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum SessionStatus { Idle, Running, AwaitingInput, Compacting, Exited, } /// Where a driver reports events. Unbounded because producers are child /// processes a slow phone must never be able to stall; the transcript file /// is the backpressure-free buffer of record. pub type EventSink = mpsc::UnboundedSender; /// The inbound half of a session. Deliberately small; see PLAN.md for the /// per-driver mapping of each method onto its dialect. /// /// `send_user_message` during a run is the point of the whole app: both /// real dialects queue it for injection at the next tool boundary rather /// than the end of the turn. pub trait Driver: Send + Sync { fn send_user_message(&self, text: String, images: Vec); fn answer_question(&self, id: &str, answer: &str); /// Stop mid-run; the session survives. fn interrupt(&self); fn set_model(&self, model: &str); /// How much the session asks about before acting. Live rather than /// spawn-only: the answer changes with what is being done, and a phone /// is the worst place to answer "may I run this?" forty times. fn set_permission_mode(&self, mode: &str); /// pi: native compaction; claude: `/compact`. fn compact(&self); /// Graceful process exit. fn shutdown(&self); }