pub struct Conversation {
pub messages: Vec<Message>,
pub taint: Taint,
pub rewritten: Vec<Vec<Message>>,
pub pressure: ContextTracker,
}Expand description
A conversation, and what has entered it.
The taint lives here, with the messages, because that is what it is a property of. Tracking it per run meant the lethal trifecta was defeated by pressing Enter: fetch a hostile page on one turn, read a secret and send on the next, and the interlock saw a clean slate both times — while the attacker’s text sat in the model’s context the whole while, still able to steer it. A turn boundary is not a security boundary.
Bundling the two makes the right thing the default rather than something
each caller has to remember. Keep the history and you keep the taint; start
a new conversation — a batch item, a subagent, an eval case — and you get a
clean one, because you built a new Conversation to do it.
Fields§
§messages: Vec<Message>§taint: TaintWhat has entered this conversation so far. Grows, never shrinks: there is no way to un-read a page.
rewritten: Vec<Vec<Message>>Full states of messages that an in-place rewrite replaced during the
current run, oldest first — compaction, eviction, thinning. The loop
snapshots the list before each rewrite pass and clears at run start;
Session::record_run walks these before the final state, so turns a
mid-run rewrite dropped still reach the file. Without this, a run long
enough to compact itself lost its own head: the front-end records at
run end, and the rewrite record carries only what survived.
On the conversation rather than the outcome for the same reason taint is: it is a fact about what the messages went through, and bundling it with them makes the right thing the default — the recording call receives the conversation and cannot skip what it carries.
pressure: ContextTrackerWhat the last requests on this conversation cost, so the next one can
be predicted. Here rather than on the run for the reason taint is —
see ContextTracker::carry_into, which also explains when it resets.
Implementations§
Source§impl Conversation
impl Conversation
pub fn new() -> Self
Sourcepub fn resumed(messages: Vec<Message>, taint: Taint) -> Self
pub fn resumed(messages: Vec<Message>, taint: Taint) -> Self
Resume a transcript whose taint is known — from a session file that recorded it.
pub fn push(&mut self, message: Message)
Sourcepub fn roll_back_failed_turn(&mut self, before: Vec<Message>)
pub fn roll_back_failed_turn(&mut self, before: Vec<Message>)
Roll a failed run back to the messages the request found, minus the
user message that triggered it — restore the snapshot, then pop,
in that order. run_in mutates the list in place and does not roll
back on Err, so a bare pop is wrong twice over: after a failure
mid-tool-turn the tail is a tool-result message, and popping it
orphans the assistant’s tool_use — every later request on the
session 400s (“a tool result must exist for every tool_use id”),
each failure then eating the user’s newly typed message; and after a
mid-run compaction the list is shorter than the snapshot, so the
pop keeps the very message it exists to drop.
Here rather than in any one front-end because four of them need it
(the chat REPL, the TUI, the web surface, the voice facade), and the
fourth was found missing the fix precisely because the first three
each carried their own copy. Deliberately touches messages and
nothing else: taint stays — a failed turn that read a hostile page
still read it.
A caller that writes a transcript must also record the rolled-back
state (Session::record_run with the pre-run snapshot expresses it
as a rewrite), or the failure survives a resume — the file otherwise
keeps the user turn memory just dropped.
The pop is conditional on the tail being the person’s own text
(is_plain_user_text), not on its role — because there are two
ways a turn begins, and they earn different failure outcomes. A
plain submit pushes a user message, and the snapshot ends with it:
popped, or the next request resends the dangling trigger. A submit
that folded into a tool-round tail (the barge-in shape — see
append_user_text’s callers, all of which record the fold at submit
and snapshot after it) leaves the snapshot ending with the tool
results carrying the folded text: popping would orphan that
round’s tool_use, so the utterance survives the failed turn inside
an already-valid tail and simply waits for the next attempt.
Asymmetric on purpose — a popped trigger prevents a verbatim resend,
a kept fold is the owner’s words already on the record inside a turn
the next request may legally carry — and one rule serves both, so a
caller does not carry a flag from its push site to its error arm.
There is a third shape, and its outcome is chosen, not accidental: a
fold into a plain user tail (an interrupt before the first token
leaves the previous prompt unanswered; the next submit merges into
it, since pushing beside it is the invalid shape). On failure the
snapshot’s tail is that merged message — plain user text — so the
pop removes both prompts. Deliberate: they were two unanswered
requests awaiting the same never-produced reply, and a resend of
either without the other misquotes the person. The recorded rewrite
removes them from the loadable state only; messages_ever still
unions them into the corpus, so recall keeps what was said.
Two costs of that recording, known and accepted: the rewrite carries the whole conversation, so a long-lived surface riding out a flapping provider appends one full history copy per failure — the only way the format can express a rollback, and failures are rare; and the rewrite drops the taint timeline’s earlier checkpoints, so the trailing taint record covers the whole rolled-back list with the run’s cumulative taint — a clean early turn in a session that later read a hostile page and failed classifies untrusted. Over-taint, never under; the safe direction, deliberately.
pub fn is_empty(&self) -> bool
pub fn len(&self) -> usize
Trait Implementations§
Source§impl Clone for Conversation
impl Clone for Conversation
Source§fn clone(&self) -> Conversation
fn clone(&self) -> Conversation
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more