//! 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. //! //! The event model itself -- [`Event`], [`QuestionOption`], [`SessionStatus`], //! [`AttachmentRef`], `ImageRef`, [`context_tokens`] and [`context_after`] -- //! moved to the `event-model` crate on 2026-09-04, so `client-core` can share //! one definition with this server instead of a hand-kept Kotlin mirror. //! Re-exported here so nothing downstream of this module had to change; what //! stayed behind is the *driver* abstraction, which is how this server runs //! a session rather than part of what a client reads off the wire. pub use event_model::{ AttachmentRef, Event, QuestionOption, SessionStatus, context_after, context_tokens, }; use tokio::sync::mpsc; /// Something a session can be asked to do to itself. /// /// A closed set rather than a string, because the two that are not /// dialect-specific have to reach every provider: compaction is a capability /// an llama session may one day have, and a name is this server's own. `Raw` /// is the escape for a dialect's own commands, which only the thing running /// the session can interpret. #[derive(Debug, Clone, PartialEq)] pub enum SessionCommand { Compact, Clear, SetTitle(String), Raw(String), } impl SessionCommand { /// What a person would have typed to ask for this, which is what a phone /// shows while it waits. pub fn label(&self) -> String { match self { Self::Compact => "/compact".to_string(), Self::Clear => "/clear".to_string(), Self::SetTitle(title) => format!("/rename {title}"), Self::Raw(text) => text.clone(), } } /// Runs it. Called only at a boundary -- see [`Event::CommandQueued`]. pub fn apply(&self, driver: &dyn Driver) { match self { Self::Compact => driver.compact(), Self::Clear => driver.clear(), Self::SetTitle(title) => driver.set_title(title), Self::Raw(text) => driver.run_command(text), } } } /// What became of a request to take a queued message back. /// /// Three states rather than a bool because the two failures are not the same /// fact. A driver that writes into its session the moment a message arrives /// -- which is what `ClaudeDriver` does, so a steer reaches the model at the /// next tool boundary -- can never take one back, and a phone told only "no" /// would have to guess whether it asked too late or asked about nothing. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Unqueued { /// Out of the queue; the session will never read it. Dropped, /// Already handed to the session, so there is nothing left to take back. AlreadySent, /// Nothing is waiting under that id. Unknown, } /// 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 { /// Takes a message, now or once the session is free for it. /// /// Every driver owes exactly one `MessageTaken` per message, at the moment /// it actually starts reading it: that event is what puts the message in /// the transcript, so a driver that never sends it drops the message from /// the conversation entirely. fn send_user_message(&self, text: String, attachments: Vec); /// Takes back a message that is still waiting, named by the id its /// [`Event::MessageQueued`] carried. /// /// Answering is the whole of the contract: a driver that drops the message /// owes an [`Event::MessageDropped`], and one that cannot must say which /// of the two reasons it is -- "the session has already been told" is /// worth knowing, and "there is nothing under that id" means the bubble on /// screen is stale. The default is the honest answer for a driver with no /// queue at all. fn unqueue(&self, _id: &str) -> Unqueued { Unqueued::Unknown } /// Answers one question with everything that was chosen, in the order it /// was offered. A driver whose dialect takes a single value joins them /// where it writes it. fn answer_question(&self, id: &str, answers: &[String]); /// 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); // Both of the above are requests, and neither reports the outcome by // returning. A driver that changes the setting owes an [`Event::Settings`] // once it has -- that event, not the request, is what the manager and the // phone read. One that cannot owes an [`Event::Error`] saying why. /// Tells the process what this conversation is called, when it has /// somewhere to put it. /// /// Unlike the two above, this is not a request that can fail: the rename /// has already happened in this server's config, which is what a phone /// lists. So a driver whose process has no notion of a name does nothing /// and says nothing. Claude Code has one: `--name` at creation and /// `/rename` afterwards, which is what puts the same name in its own /// session picker and in what other agents see. fn set_title(&self, title: &str); /// Runs a command this session's own dialect understands, verbatim -- /// `/context`, `/usage`, anything a CLI adds next month. A driver with no /// such vocabulary says so with an [`Event::Error`] rather than sending it /// as a message, which would put a line meant for the session in front of /// the model. /// /// Called only when the session is between turns; the waiting is done /// above, once, for every driver. fn run_command(&self, text: &str); /// llama: not built, and refused; claude: `/compact`. fn compact(&self); /// Drops the conversation so far without ending the session. /// /// The cheap half of managing a long session, and why it is a driver /// operation rather than a manager one: compaction *reads* the whole /// conversation in order to summarise it, so on a large context it is /// itself one of the most expensive requests the session will make -- /// measured at 1.7 million tokens for one automatic compaction on /// 2026-08-29. Clearing costs nothing, because nothing is sent. /// /// Every implementation emits [`Event::Cleared`] so the transcript carries /// the divider whatever the dialect did behind it. fn clear(&self); /// Stop attending to the process but leave it running, because this /// server is going away and means to adopt it again. /// /// Deliberately not a shutdown: a backend restart must not end a turn that /// is in flight, so a session's process outlives the server that started /// it and is found again through `session::process`. /// /// Its counterpart is [`Driver::stop`]. Every driver owes exactly one of /// the two on the way out, and which one is the difference between "back /// shortly" and "this conversation is over". /// Whether a line written *now* would start a turn of its own, rather than /// landing inside one already in flight. /// /// Asked of the driver because the driver is the only thing that knows: it /// updates this the instant it writes rather than when output returns. The /// manager's `SessionStatus` is built from what has been *recorded*, so /// between writing a line and the CLI's first output it still reads idle, /// and a second line sent in that gap lands inside the turn the first one /// started. For a command that is the difference between being executed /// and being read to the model as text, which is silent both ways. /// /// Defaults to true for a driver with no turn of its own to be inside. fn between_turns(&self) -> bool { true } fn detach(&self); /// End the process for good, because it must not survive this. The path /// out for everything [`Driver::detach`] preserves. /// /// Two callers, differing only in what is being ended: a session being /// deleted, whose conversation goes with it, and a throwaway session at a /// server's exit, whose transcript stays and whose process does not. fn stop(&self); } #[cfg(test)] mod tests { use super::*; /// A tripwire for the wire format, not for serde. The app reads these /// names, and getting one wrong does not fail loudly: a field the app /// cannot find reads as a field the server chose not to send, which /// several of them are allowed to be. #[test] fn multi_word_fields_go_out_in_camel_case() { let json = serde_json::to_value(Event::Compacted { pre_tokens: Some(28719), post_tokens: Some(1125), trigger: Some("manual".to_string()), }) .expect("serialize"); assert_eq!( json, serde_json::json!({ "type": "compacted", "preTokens": 28719, "postTokens": 1125, "trigger": "manual", }) ); } /// The two events that take the context *down* are the point of the fold: /// a figure measured before a compaction or a clear stopped being true at /// that moment, and carrying it forward is how a session that had just /// been cleared went on reporting the context it no longer had. #[test] fn a_compaction_and_a_clear_move_the_context_a_turn_cannot() { let after = |current, event| context_after(current, &event); assert_eq!( after( Some(500), Event::UsageDelta { tokens: 12, context: Some(30_100), } ), Some(30_100) ); assert_eq!( after( Some(128_402), Event::Compacted { pre_tokens: Some(128_402), post_tokens: Some(9_617), trigger: Some("auto".to_string()), } ), Some(9_617) ); assert_eq!(after(Some(9_617), Event::Cleared), None); // A compaction that did not say how much it recovered leaves the // context unknown rather than stale: it definitely moved. assert_eq!( after( Some(128_402), Event::Compacted { pre_tokens: None, post_tokens: None, trigger: None, } ), None ); // A turn the dialect reported no context for is stale by a turn, // which every context figure is, rather than unknown. assert_eq!( after( Some(30_100), Event::UsageDelta { tokens: 12, context: None, } ), Some(30_100) ); // Everything else leaves it alone. assert_eq!( after( Some(30_100), Event::Status { state: SessionStatus::Idle, } ), Some(30_100) ); } }