Files
ai-app/server/src/session/echo.rs
T
irisandClaude Opus 5 c12ab7f098 Take rustfmt's defaults
The code was hand-formatted -- close to rustfmt's output but not it, mostly
in keeping chains and call arguments on one line where the formatter would
break them. That is a per-line decision every future change has to make
again, and reproducing it would mean a config whose only job is to preserve
how the code already looks.

So this is `cargo fmt` at its defaults, with no rustfmt.toml, which is
where the sibling dev-updater checkout already sits: it is clean at the
defaults today, so the two repos now agree on layout without either of them
configuring it.

Formatting only -- no behaviour, no renames, nothing reordered. Verified
after: cargo test (35 pass), cargo clippy --all-targets clean, cargo fmt
--check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
2026-08-28 03:13:36 -04:00

166 lines
5.5 KiB
Rust

//! The phase-1 fake driver: no child process, just events. It exists to
//! prove the whole pipe -- spawn, transcript, SSE cursors, questions,
//! interrupts -- before any AI is involved, and stays useful afterwards as
//! a connectivity check that costs no tokens.
//!
//! Behavior: every message is echoed back as a few streamed text deltas. A
//! message starting with `/tool` also emits a fake tool run, and one
//! starting with `/question` asks one (exercising the answer path). This is
//! exactly the event vocabulary the real drivers produce, so a UI that
//! renders echo sessions correctly renders the real thing.
use std::sync::Mutex;
use std::time::Duration;
use super::driver::{Driver, Event, EventSink, ImageRef, SessionStatus};
/// Delay between streamed deltas -- long enough that streaming is visibly
/// streaming in the UI, short enough that tests waiting on a full turn
/// stay fast.
const DELTA_DELAY: Duration = Duration::from_millis(50);
pub struct EchoDriver {
sink: EventSink,
/// Id of the question currently awaiting an answer, if any. One at a
/// time is all the echo behavior ever produces.
pending_question: Mutex<Option<String>>,
}
impl EchoDriver {
pub fn new(sink: EventSink) -> Self {
let driver = Self {
sink,
pending_question: Mutex::new(None),
};
driver.emit(Event::Status {
state: SessionStatus::Idle,
});
driver
}
/// Sends are infallible from the driver's point of view: a closed sink
/// means the session is being torn down, and there is nobody left to
/// report to.
fn emit(&self, event: Event) {
let _ = self.sink.send(event);
}
}
impl Driver for EchoDriver {
fn send_user_message(&self, text: String, _images: Vec<ImageRef>) {
let sink = self.sink.clone();
if let Some(rest) = text.strip_prefix("/question") {
let id = format!("q-{}", super::random_hex());
let prompt = if rest.trim().is_empty() {
"Echo asks: proceed?".to_string()
} else {
format!("Echo asks: {}", rest.trim())
};
*self.pending_question.lock().unwrap() = Some(id.clone());
self.emit(Event::Status {
state: SessionStatus::Running,
});
self.emit(Event::Question {
id,
prompt,
options: vec!["Yes".to_string(), "No".to_string()],
});
self.emit(Event::Status {
state: SessionStatus::AwaitingInput,
});
return;
}
let run_tool = text
.strip_prefix("/tool")
.map(|rest| rest.trim().to_string());
tokio::spawn(async move {
let send = |event: Event| {
let _ = sink.send(event);
};
send(Event::Status {
state: SessionStatus::Running,
});
if let Some(input) = run_tool {
let id = format!("t-{}", super::random_hex());
send(Event::ToolStart {
id: id.clone(),
tool: "echo-tool".to_string(),
input: serde_json::json!({ "input": input }),
});
tokio::time::sleep(DELTA_DELAY).await;
send(Event::ToolUpdate {
id: id.clone(),
output: "working...".to_string(),
});
tokio::time::sleep(DELTA_DELAY).await;
send(Event::ToolEnd {
id,
output: format!("echoed: {input}"),
});
}
// Word-at-a-time so streaming is visibly streaming.
for word in format!("You said: {text}").split_inclusive(' ') {
send(Event::AssistantText {
delta: word.to_string(),
});
tokio::time::sleep(DELTA_DELAY).await;
}
send(Event::UsageDelta {
tokens: text.split_whitespace().count() as u64,
});
send(Event::Status {
state: SessionStatus::Idle,
});
});
}
fn answer_question(&self, id: &str, answer: &str) {
let mut pending = self.pending_question.lock().unwrap();
match pending.as_deref() {
Some(expected) if expected == id => {
*pending = None;
self.emit(Event::AssistantText {
delta: format!("You answered: {answer}"),
});
self.emit(Event::Status {
state: SessionStatus::Idle,
});
}
_ => self.emit(Event::Error {
message: format!("no question {id} is awaiting an answer"),
}),
}
}
fn interrupt(&self) {
// Nothing real to stop; a pending question is abandoned so the
// session isn't stuck awaiting input forever.
*self.pending_question.lock().unwrap() = None;
self.emit(Event::Status {
state: SessionStatus::Idle,
});
}
fn set_model(&self, model: &str) {
self.emit(Event::Error {
message: format!("echo sessions have no model to change to {model}"),
});
}
fn compact(&self) {
self.emit(Event::Error {
message: "echo sessions have nothing to compact".to_string(),
});
}
fn shutdown(&self) {
self.emit(Event::Status {
state: SessionStatus::Exited,
});
}
}