**Why the model became fable.** `spawn_session` fell back to the
provider's first listed model when none was given. That list is a shortcut
for the spawn screen, written in whatever order somebody typed it, and its
first entry is `fable` -- so every session spawned without a model, which
is every import, silently became a fable session. It looked like a default
and was an artefact of list order. Absent now means absent: no `--model`
flag, and the CLI uses whatever the person configured for themselves.
**Model and permission mode are now visible and changeable** from the
session, as buttons that read as their current value rather than labels
beside one. The mode was spawn-only; the CLI turns out to accept
`control_request{subtype:set_permission_mode}` and echo the mode back,
probed against 2.1.237 the same way the rest of the protocol record was.
Both default to `auto` -- on a phone every ask is a round trip to a
question card, which is how "allow Bash?" became the most-answered
question in the app.
The mode is reported by the API so the picker shows what the session is
actually set to, and it is kept in the live session beside the model for
the reason the model already was: `meta` is the shape a session was
*launched* with, so reporting from it shows the value a change replaced.
**And an imported session keeps itself level with its source file**, so
work done at a terminal arrives without a button. `--resume` appends to
the same transcript rather than forking -- measured, not assumed -- so the
only hard question is which new lines came from here.
Answered by counting the events this session has recorded. Status is the
obvious signal and is wrong, which cost a round trip to find: a turn that
starts and finishes between two polls reads as idle at both, so its output
is replayed on top of itself. It showed up on screen as `donedone`, and
only because the reply was one word -- with a longer answer it would have
looked like the model repeating itself.
Verified against both halves: text appended to the source file the way a
terminal writes it appears within one interval, and a message sent through
the app appears exactly once, before and after a turn.
118 lines
4.0 KiB
Rust
118 lines
4.0 KiB
Rust
//! 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<String>,
|
|
},
|
|
/// 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<Event>;
|
|
|
|
/// 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<ImageRef>);
|
|
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);
|
|
}
|