//! The vocabulary of runtime control traffic: model-visible text the runtime //! itself authors, as opposed to text a user or the model wrote. //! //! Every layer that needs to tell "the user said this" from "the runtime said //! this" — session projection, the desktop and admin clients, channel //! delivery, compaction — reads this one module. Before it existed each layer //! kept its own hand-copied list of text prefixes, and the lists drifted: a //! nudge added to the agent loop was hidden by the server but shown by the //! client as a message from the user, and the client's turn count then //! disagreed with the server's, displacing every later turn. //! //! Three classes, each with a different lifetime: //! //! * [`ControlKind`] — a synthetic *user-role message the agent loop appends to //! the ledger* (repair nudges, stop guards). Persisted, and tagged //! structurally at creation (`MessageMeta::control`). That tag is the only //! way anything recognises one: there is no text sniffing to drift. //! * [`CONTEXT_BLOCK_TAGS`] — a `…` block the runtime *derives into a //! message* when it assembles model input (compaction summary, intent note, //! work contract, conversation thread). Never persisted as its own message. //! * [`InlineHint`] — a marker *line inside other text* (a tool result, a stop //! guard's reason). Not a message, so it is stripped line by line. use serde::{Deserialize, Serialize}; /// A synthetic user-role message the agent loop appends to the ledger. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ControlKind { /// A stop hook blocked the turn from ending. StopHook, /// The stop gate (verification, goal, managed work) blocked completion. StopGuard, /// A search/fetch result was ignored by the answer that followed it. GroundingCheck, /// A `vak` fence in the answer did not parse. FenceCheck, /// The answer restated a card that a tool call had already shown. DuplicateCardCheck, /// The answer reads as a card but was written as prose. PresentationCheck, /// A current value was asked for and nothing was retrieved this turn /// (docs/design/68-context-engine.md §7). FreshnessCheck, /// The response carried neither text nor a tool call (a thinking-only /// completion): act on the plan, or answer. EmptyStep, /// Model drift (docs/design/68-context-engine.md §7): the step served a /// different directive than the current one — a mismatched-domain tool /// call, or a verbatim repeat of a past answer. SteeringDrift, /// An `emit_*_card` call after a successful retrieval THIS run whose /// own payload shares no topic word with either the directive or what /// was just retrieved — never checked when nothing was retrieved this /// run, since a card built from the model's own reasoning or from data /// already in the directive routinely has no vocabulary overlap with /// either and would otherwise be gated for being right /// (docs/design/68-context-engine.md §7). TopicMismatchCheck, /// Correctable tool failures went unrepaired across steps: the admitted /// schema of each failing tool, re-surfaced with the retries left. RepairDirective, } impl ControlKind { pub const ALL: [ControlKind; 11] = [ ControlKind::StopHook, ControlKind::StopGuard, ControlKind::GroundingCheck, ControlKind::FenceCheck, ControlKind::DuplicateCardCheck, ControlKind::PresentationCheck, ControlKind::FreshnessCheck, ControlKind::EmptyStep, ControlKind::SteeringDrift, ControlKind::TopicMismatchCheck, ControlKind::RepairDirective, ]; /// The literal the message body begins with, for the model's benefit. /// Nothing else reads it: consumers use the structural tag. pub const fn marker(self) -> &'static str { match self { ControlKind::StopHook => "[stop-hook]", ControlKind::StopGuard => "[stop-guard]", ControlKind::GroundingCheck => "[grounding-check]", ControlKind::FenceCheck => "[fence-check]", ControlKind::DuplicateCardCheck => "[duplicate-card-check]", ControlKind::PresentationCheck => "[presentation-check]", ControlKind::FreshnessCheck => "[freshness-check]", ControlKind::EmptyStep => "[empty-step]", ControlKind::SteeringDrift => "[steering-drift]", ControlKind::TopicMismatchCheck => "[topic-mismatch]", ControlKind::RepairDirective => "[repair-directive]", } } /// Whether this asks the model to redo the answer it just gave, so a card /// the model emits afterwards replaces the earlier attempt rather than /// adding a second one to the same answer. pub const fn retries_answer(self) -> bool { matches!( self, ControlKind::GroundingCheck | ControlKind::FenceCheck | ControlKind::DuplicateCardCheck | ControlKind::PresentationCheck | ControlKind::FreshnessCheck | ControlKind::EmptyStep | ControlKind::TopicMismatchCheck ) } } /// A `…` block the runtime derives into a model-visible message. pub const CONTEXT_BLOCK_TAGS: [&str; 10] = [ "conversation_thread", "context_summary", "intent", "work_contract", "managed_work", "context_packet", "system_reminder", "runtime_guidance", "scratchpad", "workspace_delta", ]; /// A marker line inside other text (a tool result or a stop guard's reason). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum InlineHint { Recovery, PostToolUseHook, } impl InlineHint { pub const ALL: [InlineHint; 2] = [InlineHint::Recovery, InlineHint::PostToolUseHook]; pub const fn marker(self) -> &'static str { match self { InlineHint::Recovery => "[recovery]", InlineHint::PostToolUseHook => "[post-tool-use hook]", } } /// Everything from the marker to the end of the text is the hint (its /// body can run over several lines), as opposed to a hint that ends at /// "Please continue.". pub const fn runs_to_end(self) -> bool { matches!(self, InlineHint::Recovery) } } /// Whether one line is an inline runtime hint rather than conversation. pub fn is_control_line(line: &str) -> bool { let trimmed = line.trim(); InlineHint::ALL .into_iter() .any(|hint| trimmed.starts_with(hint.marker())) } /// Every inline marker a text-based recogniser (one working on message text /// rather than the structural tag, such as the desktop client) must know. The /// TypeScript side is checked against this list in `vak-server`'s tests. pub fn inline_markers() -> Vec<&'static str> { InlineHint::ALL .into_iter() .map(InlineHint::marker) .collect() } /// Lines of runtime narration that are not conversation either: the surface /// stamp, the outcome banner, and the like. Not control *messages* (they have /// no kind), so they are matched by text alone. const NARRATION_PREFIXES: [&str; 5] = [ "Surface:", "Outcome:", "primary deliverable:", "contract_id:", "I will write and execute this within the sandbox", ]; /// Whether one line is scaffolding a reader should never see: an inline /// runtime hint or runtime narration. pub fn is_scaffolding_line(line: &str) -> bool { let trimmed = line.trim(); NARRATION_PREFIXES .iter() .any(|prefix| trimmed.starts_with(prefix)) || trimmed.eq_ignore_ascii_case("completed") || trimmed.eq_ignore_ascii_case("vak") || is_control_line(trimmed) } /// Removes `…` context blocks (an unterminated one runs to the end /// of the text) and the `[marker]: … Please continue.` spans that stop hooks, /// stop guards and tool-result hints embed in other text. pub fn strip_control_blocks(text: &str) -> String { let mut out = text.to_string(); for tag in CONTEXT_BLOCK_TAGS { let close_pattern = format!(""); while let Some(start) = find_open_tag(&out, tag) { if let Some(end_offset) = out[start..].find(&close_pattern) { let end = start + end_offset + close_pattern.len(); out.replace_range(start..end, ""); } else { out.truncate(start); break; } } } // (marker, runs to the end of the text). Persisted control messages are // never embedded in other text, so only the inline hints appear here. let embedded: [(String, bool); 2] = [ ( InlineHint::Recovery.marker().to_string(), InlineHint::Recovery.runs_to_end(), ), (format!("{}:", InlineHint::PostToolUseHook.marker()), false), ]; for (prefix, runs_to_end) in &embedded { while let Some(start) = out.find(prefix.as_str()) { let remainder = &out[start..]; if *runs_to_end { out.truncate(start); break; } if let Some(end_offset) = remainder.find("Please continue.") { let end = start + end_offset + "Please continue.".len(); out.replace_range(start..end, ""); } else if let Some(end_offset) = remainder.find("Please continue") { let end = start + end_offset + "Please continue".len(); out.replace_range(start..end, ""); } else if let Some(newline_offset) = remainder.find('\n') { let end = start + newline_offset + 1; out.replace_range(start..end, ""); } else { out.truncate(start); break; } } } out } /// Position of the first `` / `` opener, as a whole tag name. /// /// A bare prefix match (`` and, finding /// no ``, truncated the rest of the text. fn find_open_tag(text: &str, tag: &str) -> Option { let prefix = format!("<{tag}"); let mut from = 0; while let Some(offset) = text[from..].find(&prefix) { let start = from + offset; let after = text[start + prefix.len()..].chars().next(); if matches!(after, Some('>') | Some('/')) || after.is_some_and(char::is_whitespace) { return Some(start); } from = start + prefix.len(); } None } /// Whether a line opens or closes a fenced code block. fn is_fence(line: &str) -> bool { let trimmed = line.trim_start(); trimmed.starts_with("```") || trimmed.starts_with("~~~") } /// The text with all scaffolding removed and outer blank lines trimmed. /// /// Fenced code is content, whatever it contains, and passes through /// verbatim: a block that shows an `` element, a line reading `vak` /// or `Outcome: ok` is part of the answer, and removing it silently /// corrupts code a person will copy. Only the prose between fences is /// cleaned; an unterminated fence runs to the end of the text. pub fn clean_scaffolding(text: &str) -> String { let had_trailing_newline = text.ends_with('\n'); let mut lines: Vec = Vec::new(); let mut prose = String::new(); let mut in_code = false; let flush = |prose: &mut String, lines: &mut Vec| { let stripped = strip_control_blocks(prose); lines.extend( stripped .lines() .filter(|line| !is_scaffolding_line(line)) .map(str::to_string), ); prose.clear(); }; for line in text.lines() { if in_code { lines.push(line.to_string()); if is_fence(line) { in_code = false; } } else if is_fence(line) { flush(&mut prose, &mut lines); lines.push(line.to_string()); in_code = true; } else { prose.push_str(line); prose.push('\n'); } } flush(&mut prose, &mut lines); while let Some(first) = lines.first() { if first.trim().is_empty() { lines.remove(0); } else { break; } } while let Some(last) = lines.last() { if last.trim().is_empty() { lines.pop(); } else { break; } } let mut out = lines.join("\n"); if had_trailing_newline && !out.is_empty() { out.push('\n'); } out } #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { use super::*; #[test] fn every_marker_is_unique_and_bracketed() { let mut markers: Vec<&str> = ControlKind::ALL .into_iter() .map(ControlKind::marker) .collect(); markers.extend(inline_markers()); let mut sorted = markers.clone(); sorted.sort_unstable(); sorted.dedup(); assert_eq!( sorted.len(), markers.len(), "duplicate marker in {markers:?}" ); assert!( markers .iter() .all(|m| m.starts_with('[') && m.ends_with(']')) ); } /// Code a person will copy is never edited: inside a fence, a line that /// looks like runtime narration or a context tag is content. #[test] fn fenced_code_passes_through_verbatim() { let text = "Run this:\n```\nvak\nOutcome: ok\n\n```\nSurface: cli\nDone."; assert_eq!( clean_scaffolding(text), "Run this:\n```\nvak\nOutcome: ok\n\n```\nDone." ); // An unterminated fence runs to the end, and the prose before it is // still cleaned. let open = "noteAnswer:\n~~~html\n"; assert_eq!( clean_scaffolding(open), "Answer:\n~~~html\n" ); } #[test] fn ordinary_text_is_never_a_control_line() { for text in [ "how did the market do", "[ERROR]: it broke", "[note] remember this", ] { assert!(!is_control_line(text), "{text}"); } } #[test] fn inline_hints_are_control_lines() { for hint in InlineHint::ALL { assert!(is_control_line(&format!(" {} something", hint.marker()))); } } #[test] fn only_answer_retries_arm_the_card_supersede() { let armed: Vec<_> = ControlKind::ALL .into_iter() .filter(|k| k.retries_answer()) .collect(); assert_eq!( armed, vec![ ControlKind::GroundingCheck, ControlKind::FenceCheck, ControlKind::DuplicateCardCheck, ControlKind::PresentationCheck, ControlKind::FreshnessCheck, ControlKind::EmptyStep, ControlKind::TopicMismatchCheck, ] ); } #[test] fn serialises_as_snake_case() { assert_eq!( serde_json::to_string(&ControlKind::DuplicateCardCheck).unwrap(), "\"duplicate_card_check\"" ); } #[test] fn inline_hints_are_scaffolding_and_clean_to_nothing() { for marker in inline_markers() { let line = format!("{marker}: something the runtime said"); assert!(is_scaffolding_line(&line), "{line}"); assert_eq!(clean_scaffolding(&line), "", "{line}"); } } #[test] fn real_answer_text_survives_around_embedded_hints() { let text = "selectdFinal result."; assert_eq!(clean_scaffolding(text), "Final result."); let text = "Answer body.\n[recovery] retry the failing call"; assert_eq!(clean_scaffolding(text), "Answer body."); let text = "Tool output\n[post-tool-use hook]: blocked\nMore output"; assert_eq!(clean_scaffolding(text), "Tool output\nMore output"); } }