Tell the model when a message was a steer
A message typed during a turn reaches the model at the next model call if the turn has one left, and otherwise as the opening line of the next turn -- Claude's read out of the fifo after the turn ended, Codex's requeued when turn/steer is refused. Read there it is indistinguishable from a reply, so the model treats the answer it just gave as seen. Both drivers now compose the text the CLI receives through driver::message_body, which prefixes a note saying the message was written without having seen the rest of that turn. The transcript still holds the words that were typed; only the CLI's copy carries the note. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
59ebd75b46
commit
3af2502982
4 files changed
+107
-18
No files matched your search
@@ -36,6 +36,52 @@ pub(in crate::session) fn store_image(
|
||||
Some(name)
|
||||
}
|
||||
|
||||
/// Prefixed to a message that was typed while a turn was already running.
|
||||
///
|
||||
/// A steer goes to the CLI the moment it arrives, but *when the model reads
|
||||
/// it* is not ours to decide: it lands at the next model call if there is
|
||||
/// one, and a turn that ends first delivers it as the opening line of the
|
||||
/// next turn instead -- Claude's from the fifo, Codex's requeued after
|
||||
/// `activeTurnNotSteerable`. Read there it looks like a reply to the answer
|
||||
/// just given, so the model acts as if the person had seen that answer, which
|
||||
/// is exactly what they had not. Nothing else distinguishes the two cases by
|
||||
/// the time the model sees them, so the note is the only thing that can carry
|
||||
/// the fact.
|
||||
const STEERING_NOTE: &str = "[Sent while you were still working, so it was written without \
|
||||
having seen the rest of that turn. Treat it as steering the work in progress, not as a \
|
||||
reply to anything you said after it was sent.]";
|
||||
|
||||
/// The text a CLI receives for one message: the note above when this is a
|
||||
/// steer, the typed words, and the paths of any attachment the model has to
|
||||
/// read from disk rather than being handed inline.
|
||||
///
|
||||
/// The paths come after the words because a model reading a list of files
|
||||
/// before the request treats the list as the request.
|
||||
pub(in crate::session) fn message_body(
|
||||
text: &str,
|
||||
files: &[std::path::PathBuf],
|
||||
steering: bool,
|
||||
) -> String {
|
||||
let mut body = String::new();
|
||||
if steering {
|
||||
body.push_str(STEERING_NOTE);
|
||||
}
|
||||
for part in std::iter::once(text.to_string()).chain(
|
||||
files
|
||||
.iter()
|
||||
.map(|path| format!("Attached file: {}", path.display())),
|
||||
) {
|
||||
if part.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !body.is_empty() {
|
||||
body.push_str("\n\n");
|
||||
}
|
||||
body.push_str(&part);
|
||||
}
|
||||
body
|
||||
}
|
||||
|
||||
/// The name an upload is stored and served under: an image is
|
||||
/// `<hex>.<extension>` and is an [`ImageRef`] like any other; any other file
|
||||
/// keeps its own name after the hex, `<hex>-<name>`, because the name is what
|
||||
@@ -651,6 +697,23 @@ pub trait Driver: Send + Sync {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A steer says so to the model and nowhere else: the note goes only into
|
||||
/// what the CLI is handed, while the transcript keeps what was typed.
|
||||
#[test]
|
||||
fn only_a_steer_carries_the_note() {
|
||||
let files = [std::path::PathBuf::from("/tmp/x/trace.txt")];
|
||||
let plain = message_body("do the thing", &files, false);
|
||||
assert_eq!(plain, "do the thing\n\nAttached file: /tmp/x/trace.txt");
|
||||
let steer = message_body("do the thing", &files, true);
|
||||
assert_eq!(steer, format!("{STEERING_NOTE}\n\n{plain}"));
|
||||
// Attachments with nothing typed still need the note, and must not
|
||||
// arrive with a blank line where the words would have been.
|
||||
assert_eq!(
|
||||
message_body("", &files, true),
|
||||
format!("{STEERING_NOTE}\n\nAttached file: /tmp/x/trace.txt")
|
||||
);
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
Reference in new issue
Block a user