Make write_envelope a compact self-handled tool with flat emojis

- Add write_envelope to is_self_handled_tool() to skip normal output
- Add print_envelope_compact() to UiWriter trait with default no-op
- Implement compact pipeline display in ConsoleUiWriter showing stages:
  ✎ envelope written → ⚙ rulespec compiled → ✓ verified → ∵ token stamped
- Refactor verify_envelope() to return structured VerifyResult
- Replace bubbly emojis (📊🔏ℹ️🔒) with flat Unicode throughout
This commit is contained in:
Dhanji R. Prasanna
2026-02-28 14:54:59 +11:00
parent f074d2c1f4
commit 98ca094be7
4 changed files with 188 additions and 79 deletions

View File

@@ -905,6 +905,60 @@ impl UiWriter for ConsoleUiWriter {
true
}
fn print_envelope_compact(&self, fact_groups: usize, stages: &[(&str, &str)], passed: Option<usize>, total: Option<usize>, failed: usize) {
// Clear any streaming hint
self.hint_state.handle_hint(ToolParsingHint::Complete);
let is_agent_mode = self.hint_state.is_agent_mode.load(Ordering::Relaxed);
let tool_color = if is_agent_mode { TOOL_COLOR_AGENT_BOLD } else { TOOL_COLOR_NORMAL_BOLD };
// Add blank line if last output was text
if self.hint_state.last_output_was_text.load(Ordering::Relaxed) {
println!();
}
self.hint_state.last_output_was_text.store(false, Ordering::Relaxed);
self.hint_state.last_output_was_tool.store(true, Ordering::Relaxed);
*self.last_read_file_path.lock().unwrap() = None;
// Header: " ● write_envelope | N fact groups"
let facts_label = if fact_groups == 1 { "fact group" } else { "fact groups" };
println!(
" \x1b[2m●\x1b[0m {}{:<width$}\x1b[0m \x1b[2m|\x1b[0m \x1b[35m{} {}\x1b[0m",
tool_color, "write_envelope", fact_groups, facts_label, width = TOOL_NAME_PADDING
);
// Pipeline stages
let stages_len = stages.len();
// Determine if we need to show a verification summary line after stages
let has_verification = passed.is_some();
for (i, (icon, desc)) in stages.iter().enumerate() {
let is_last = i == stages_len - 1 && !has_verification;
let prefix = if is_last { "" } else { "" };
println!(" \x1b[2m{}\x1b[0m {} \x1b[2m{}\x1b[0m", prefix, icon, desc);
}
// Verification summary line (if rulespec was present)
if let (Some(p), Some(t)) = (passed, total) {
if failed == 0 {
println!(
" \x1b[2m└\x1b[0m \x1b[32m✓ {}/{} passed\x1b[0m",
p, t
);
} else {
println!(
" \x1b[2m└\x1b[0m \x1b[31m✗ {}/{} passed, {} failed\x1b[0m",
p, t, failed
);
}
}
println!();
// Clear tool state
self.clear_tool_state();
}
fn print_tool_timing(&self, duration_str: &str, tokens_delta: u32, context_percentage: f32) {
let color_code = duration_color(duration_str);

View File

@@ -111,7 +111,8 @@ pub fn is_compact_tool(tool_name: &str) -> bool {
pub fn is_self_handled_tool(tool_name: &str) -> bool {
matches!(tool_name,
"todo_read" | "todo_write" |
"plan_read" | "plan_write"
"plan_read" | "plan_write" |
"write_envelope"
)
}

View File

@@ -26,7 +26,7 @@ use crate::ToolCall;
use super::executor::ToolContext;
use super::invariants::{
format_envelope_markdown, get_envelope_path, read_envelope, read_rulespec,
read_envelope, read_rulespec,
write_envelope, ActionEnvelope, Rulespec,
};
use super::datalog::{
@@ -231,13 +231,32 @@ pub fn verify_token(session_id: &str, working_dir: &Path) -> Result<bool> {
Ok(stored_token == expected_token)
}
// ============================================================================
// Envelope Verification
// ============================================================================
/// Result of the envelope verification pipeline.
pub struct VerifyResult {
/// Pipeline stages completed: (flat_icon, description)
pub stages: Vec<(String, String)>,
/// Number of predicates that passed (None if no rulespec)
pub passed: Option<usize>,
/// Total number of predicates (None if no rulespec)
pub total: Option<usize>,
/// Number of predicates that failed
pub failed: usize,
/// Short summary for LLM context
pub llm_summary: String,
}
// ============================================================================
// Tool Implementation
// ============================================================================
/// Execute the `write_envelope` tool.
///
/// Accepts YAML facts, writes the action envelope, and runs verification.
/// Accepts YAML facts, writes the action envelope, runs verification,
/// and displays a compact pipeline summary via the UI writer.
pub async fn execute_write_envelope<W: UiWriter>(
tool_call: &ToolCall,
ctx: &mut ToolContext<'_, W>,
@@ -246,28 +265,25 @@ pub async fn execute_write_envelope<W: UiWriter>(
let session_id = match ctx.session_id {
Some(id) => id,
None => return Ok(" No active session - envelopes are session-scoped.".to_string()),
None => return Ok("Error: No active session - envelopes are session-scoped.".to_string()),
};
// Get the facts YAML from args
let facts_yaml = match tool_call.args.get("facts").and_then(|v| v.as_str()) {
Some(f) => f,
None => return Ok(" Missing 'facts' argument. Provide the envelope facts as YAML.".to_string()),
None => return Ok("Error: Missing 'facts' argument. Provide the envelope facts as YAML.".to_string()),
};
// Parse the YAML into an ActionEnvelope
let envelope: ActionEnvelope = match serde_yaml::from_str(facts_yaml) {
Ok(e) => e,
Err(e) => return Ok(format!(" Invalid envelope YAML: {}", e)),
Err(e) => return Ok(format!("Error: Invalid envelope YAML: {}", e)),
};
// Validate that facts is non-empty. This catches the common mistake where
// the agent sends a raw YAML map without the required `facts:` top-level key.
// serde silently ignores unknown fields and defaults `facts` to an empty HashMap,
// so we must check explicitly.
// Validate that facts is non-empty
if envelope.facts.is_empty() {
return Ok(
" Envelope has empty facts. The YAML must contain a non-empty `facts` top-level key. Example:\n\n\
"Error: Envelope has empty facts. The YAML must contain a non-empty `facts` top-level key. Example:\n\n\
```yaml\n\
facts:\n\
\x20 my_feature:\n\
@@ -279,134 +295,161 @@ pub async fn execute_write_envelope<W: UiWriter>(
// Write the envelope to disk (without verified token initially)
if let Err(e) = write_envelope(session_id, &envelope) {
return Ok(format!(" Failed to write envelope: {}", e));
return Ok(format!("Error: Failed to write envelope: {}", e));
}
let envelope_path = get_envelope_path(session_id);
let mut output = format!(
"✅ Envelope written: {}\n{}",
envelope_path.display(),
format_envelope_markdown(&envelope),
);
let fact_groups = envelope.facts.len();
// Run verification against rulespec (shadow mode)
// Run verification pipeline
let effective_wd = ctx.working_dir
.map(Path::new)
.unwrap_or_else(|| Path::new("."));
let verification_note = verify_envelope(session_id, effective_wd);
output.push_str(&verification_note);
let vr = verify_envelope(session_id, effective_wd);
Ok(output)
// Display compact pipeline via UI writer
let stage_refs: Vec<(&str, &str)> = vr.stages.iter()
.map(|(icon, desc)| (icon.as_str(), desc.as_str()))
.collect();
ctx.ui_writer.print_envelope_compact(fact_groups, &stage_refs, vr.passed, vr.total, vr.failed);
Ok(vr.llm_summary)
}
// ============================================================================
// Envelope Verification
// Envelope Verification Pipeline
// ============================================================================
/// Verify the action envelope against the compiled rulespec using datalog.
///
/// This is the core verification step that:
/// 1. Reads `analysis/rulespec.yaml` from the working directory
/// 2. Compiles it into datalog relations
/// 3. Loads the envelope from the session
/// 4. Extracts facts and runs datalog rules
/// 5. Writes results to session artifacts (shadow mode - stderr + files)
/// 6. If all predicates pass, stamps the envelope with a verification token
///
/// Returns a short status string for inclusion in tool output.
pub fn verify_envelope(session_id: &str, working_dir: &Path) -> String {
/// Returns a `VerifyResult` with pipeline stages and verification counts.
/// Stages are displayed as compact lines with flat icons.
pub fn verify_envelope(session_id: &str, working_dir: &Path) -> VerifyResult {
let mut stages: Vec<(String, String)> = Vec::new();
// Stage 1: envelope written
stages.push(("".into(), "envelope written".into()));
// Read rulespec from analysis/rulespec.yaml
let rulespec = match read_rulespec(working_dir) {
Ok(Some(rs)) => rs,
Ok(None) => {
eprintln!("\n No analysis/rulespec.yaml found - skipping datalog verification");
return "\n No rulespec found — skipping invariant verification.\n".to_string();
eprintln!(" -- no analysis/rulespec.yaml found, skipping verification");
return VerifyResult {
stages,
passed: None, total: None, failed: 0,
llm_summary: "Envelope written. No rulespec — skipping verification.".into(),
};
}
Err(e) => {
eprintln!("\n⚠️ Failed to read analysis/rulespec.yaml: {}", e);
return format!("\n⚠️ Failed to read rulespec: {}\n", e);
eprintln!(" !! failed to read rulespec: {}", e);
return VerifyResult {
stages,
passed: None, total: None, failed: 0,
llm_summary: format!("Envelope written. Failed to read rulespec: {}", e),
};
}
};
// Compile rulespec on-the-fly
// Stage 2: compile rulespec
let compiled = match compile_rulespec(&rulespec, "envelope-verify", 0) {
Ok(c) => c,
Err(e) => {
eprintln!("\n⚠️ Failed to compile rulespec: {}", e);
return format!("\n⚠️ Failed to compile rulespec: {}\n", e);
eprintln!(" !! failed to compile rulespec: {}", e);
return VerifyResult {
stages,
passed: None, total: None, failed: 0,
llm_summary: format!("Envelope written. Failed to compile rulespec: {}", e),
};
}
};
if compiled.is_empty() {
eprintln!("\n Rulespec has no predicates - skipping datalog verification");
return "\n Rulespec has no predicates — skipping invariant verification.\n".to_string();
eprintln!(" -- rulespec has no predicates, skipping verification");
return VerifyResult {
stages,
passed: None, total: None, failed: 0,
llm_summary: "Envelope written. Rulespec has no predicates.".into(),
};
}
// Load envelope
let pred_count = compiled.predicates.len();
stages.push(("\u{2699}".into(), format!("rulespec compiled ({} predicates)", pred_count)));
// Load envelope back from disk (to verify what was actually written)
let envelope = match read_envelope(session_id) {
Ok(Some(e)) => e,
Ok(None) => {
eprintln!("\n⚠️ No envelope found - skipping datalog verification");
return "\n⚠️ No envelope found — skipping invariant verification.\n".to_string();
eprintln!(" !! no envelope found after write");
return VerifyResult {
stages,
passed: None, total: None, failed: 0,
llm_summary: "Envelope written but could not be re-read for verification.".into(),
};
}
Err(e) => {
eprintln!("\n⚠️ Failed to load envelope: {}", e);
return format!("\n⚠️ Failed to load envelope: {}\n", e);
eprintln!(" !! failed to load envelope: {}", e);
return VerifyResult {
stages,
passed: None, total: None, failed: 0,
llm_summary: format!("Envelope written but failed to re-read: {}", e),
};
}
};
// Extract facts from envelope
// Extract facts and execute datalog rules
let facts = extract_facts(&envelope, &compiled);
// Execute datalog rules
let result = execute_rules(&compiled, &facts);
// Format results
let output = format_datalog_results(&result);
// Write artifacts to session dir (shadow mode)
let session_dir = get_session_logs_dir(session_id);
// Write compiled rules to .dl file
let dl_path = session_dir.join("rulespec.compiled.dl");
let datalog_program = format_datalog_program(&compiled, &facts);
if let Err(e) = std::fs::write(&dl_path, &datalog_program) {
eprintln!("⚠️ Failed to write compiled rules: {}", e);
eprintln!(" !! failed to write compiled rules: {}", e);
}
// Write evaluation report
let eval_output = format_datalog_results(&result);
let eval_path = session_dir.join("datalog_evaluation.txt");
match std::fs::write(&eval_path, &output) {
Ok(_) => {
eprintln!("📊 Compiled rules: {}", dl_path.display());
eprintln!("📊 Evaluation report: {}", eval_path.display());
}
Err(e) => {
eprintln!("⚠️ Failed to write datalog evaluation: {}", e);
}
if let Err(e) = std::fs::write(&eval_path, &eval_output) {
eprintln!(" !! failed to write evaluation report: {}", e);
}
// If all predicates passed, stamp the envelope with a verification token
if result.failed_count == 0 && result.passed_count > 0 {
// Stage 3: verification result
let total = result.passed_count + result.failed_count;
let passed = result.passed_count;
let failed = result.failed_count;
if failed == 0 {
stages.push(("\u{2713}".into(), format!("verified {}/{}", passed, total)));
} else {
stages.push(("\u{2717}".into(), format!("verified {}/{}, {} failed", passed, total, failed)));
}
// Stage 4: stamp if all passed
if failed == 0 && passed > 0 {
match stamp_envelope(session_id, &envelope, &rulespec) {
Ok(_) => {
eprintln!("🔏 Envelope stamped with verification token");
stages.push(("\u{2235}".into(), "token stamped".into()));
eprintln!(" -- envelope stamped with verification token");
}
Err(e) => {
eprintln!("⚠️ Failed to stamp envelope: {}", e);
eprintln!(" !! failed to stamp envelope: {}", e);
}
}
}
// Summary for tool output (token value intentionally omitted — LLM must not see it)
let total = result.passed_count + result.failed_count;
if result.failed_count == 0 {
format!(
"\n✅ Invariant verification: {}/{} passed\n", result.passed_count, total,
)
// LLM summary (token value intentionally omitted)
let llm_summary = if failed == 0 {
format!("Envelope written. Verification: {}/{} passed.", passed, total)
} else {
format!(
"\n⚠️ Invariant verification: {}/{} passed, {} failed\n", result.passed_count, total, result.failed_count,
)
format!("Envelope written. Verification: {}/{} passed, {} failed.", passed, total, failed)
};
VerifyResult {
stages,
passed: Some(passed as usize),
total: Some(total as usize),
failed: failed as usize,
llm_summary,
}
}

View File

@@ -78,6 +78,17 @@ pub trait UiWriter: Send + Sync {
false
}
/// Print a compact write_envelope output showing pipeline stages.
/// Displays: envelope written → rulespec compiled → verification → token stamped
/// fact_groups: number of top-level fact groups in the envelope
/// stages: list of (flat_icon, description) pairs for each completed stage
/// passed: number of predicates that passed (None if no rulespec)
/// total: total number of predicates (None if no rulespec)
/// failed: number of predicates that failed
fn print_envelope_compact(&self, _fact_groups: usize, _stages: &[(& str, &str)], _passed: Option<usize>, _total: Option<usize>, _failed: usize) {
// Default: no-op (NullUiWriter inherits this)
}
/// Print tool execution timing
fn print_tool_timing(&self, duration_str: &str, tokens_delta: u32, context_percentage: f32);