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:
@@ -905,6 +905,60 @@ impl UiWriter for ConsoleUiWriter {
|
|||||||
true
|
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) {
|
fn print_tool_timing(&self, duration_str: &str, tokens_delta: u32, context_percentage: f32) {
|
||||||
let color_code = duration_color(duration_str);
|
let color_code = duration_color(duration_str);
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ pub fn is_compact_tool(tool_name: &str) -> bool {
|
|||||||
pub fn is_self_handled_tool(tool_name: &str) -> bool {
|
pub fn is_self_handled_tool(tool_name: &str) -> bool {
|
||||||
matches!(tool_name,
|
matches!(tool_name,
|
||||||
"todo_read" | "todo_write" |
|
"todo_read" | "todo_write" |
|
||||||
"plan_read" | "plan_write"
|
"plan_read" | "plan_write" |
|
||||||
|
"write_envelope"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use crate::ToolCall;
|
|||||||
|
|
||||||
use super::executor::ToolContext;
|
use super::executor::ToolContext;
|
||||||
use super::invariants::{
|
use super::invariants::{
|
||||||
format_envelope_markdown, get_envelope_path, read_envelope, read_rulespec,
|
read_envelope, read_rulespec,
|
||||||
write_envelope, ActionEnvelope, Rulespec,
|
write_envelope, ActionEnvelope, Rulespec,
|
||||||
};
|
};
|
||||||
use super::datalog::{
|
use super::datalog::{
|
||||||
@@ -231,13 +231,32 @@ pub fn verify_token(session_id: &str, working_dir: &Path) -> Result<bool> {
|
|||||||
Ok(stored_token == expected_token)
|
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
|
// Tool Implementation
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Execute the `write_envelope` tool.
|
/// 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>(
|
pub async fn execute_write_envelope<W: UiWriter>(
|
||||||
tool_call: &ToolCall,
|
tool_call: &ToolCall,
|
||||||
ctx: &mut ToolContext<'_, W>,
|
ctx: &mut ToolContext<'_, W>,
|
||||||
@@ -246,28 +265,25 @@ pub async fn execute_write_envelope<W: UiWriter>(
|
|||||||
|
|
||||||
let session_id = match ctx.session_id {
|
let session_id = match ctx.session_id {
|
||||||
Some(id) => 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
|
// Get the facts YAML from args
|
||||||
let facts_yaml = match tool_call.args.get("facts").and_then(|v| v.as_str()) {
|
let facts_yaml = match tool_call.args.get("facts").and_then(|v| v.as_str()) {
|
||||||
Some(f) => f,
|
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
|
// Parse the YAML into an ActionEnvelope
|
||||||
let envelope: ActionEnvelope = match serde_yaml::from_str(facts_yaml) {
|
let envelope: ActionEnvelope = match serde_yaml::from_str(facts_yaml) {
|
||||||
Ok(e) => e,
|
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
|
// Validate that facts is non-empty
|
||||||
// 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.
|
|
||||||
if envelope.facts.is_empty() {
|
if envelope.facts.is_empty() {
|
||||||
return Ok(
|
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\
|
```yaml\n\
|
||||||
facts:\n\
|
facts:\n\
|
||||||
\x20 my_feature:\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)
|
// Write the envelope to disk (without verified token initially)
|
||||||
if let Err(e) = write_envelope(session_id, &envelope) {
|
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 fact_groups = envelope.facts.len();
|
||||||
let mut output = format!(
|
|
||||||
"✅ Envelope written: {}\n{}",
|
|
||||||
envelope_path.display(),
|
|
||||||
format_envelope_markdown(&envelope),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Run verification against rulespec (shadow mode)
|
// Run verification pipeline
|
||||||
let effective_wd = ctx.working_dir
|
let effective_wd = ctx.working_dir
|
||||||
.map(Path::new)
|
.map(Path::new)
|
||||||
.unwrap_or_else(|| Path::new("."));
|
.unwrap_or_else(|| Path::new("."));
|
||||||
let verification_note = verify_envelope(session_id, effective_wd);
|
let vr = verify_envelope(session_id, effective_wd);
|
||||||
output.push_str(&verification_note);
|
|
||||||
|
|
||||||
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.
|
/// Verify the action envelope against the compiled rulespec using datalog.
|
||||||
///
|
///
|
||||||
/// This is the core verification step that:
|
/// Returns a `VerifyResult` with pipeline stages and verification counts.
|
||||||
/// 1. Reads `analysis/rulespec.yaml` from the working directory
|
/// Stages are displayed as compact lines with flat icons.
|
||||||
/// 2. Compiles it into datalog relations
|
pub fn verify_envelope(session_id: &str, working_dir: &Path) -> VerifyResult {
|
||||||
/// 3. Loads the envelope from the session
|
let mut stages: Vec<(String, String)> = Vec::new();
|
||||||
/// 4. Extracts facts and runs datalog rules
|
|
||||||
/// 5. Writes results to session artifacts (shadow mode - stderr + files)
|
// Stage 1: envelope written
|
||||||
/// 6. If all predicates pass, stamps the envelope with a verification token
|
stages.push(("✎".into(), "envelope written".into()));
|
||||||
///
|
|
||||||
/// Returns a short status string for inclusion in tool output.
|
|
||||||
pub fn verify_envelope(session_id: &str, working_dir: &Path) -> String {
|
|
||||||
// Read rulespec from analysis/rulespec.yaml
|
// Read rulespec from analysis/rulespec.yaml
|
||||||
let rulespec = match read_rulespec(working_dir) {
|
let rulespec = match read_rulespec(working_dir) {
|
||||||
Ok(Some(rs)) => rs,
|
Ok(Some(rs)) => rs,
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
eprintln!("\nℹ️ No analysis/rulespec.yaml found - skipping datalog verification");
|
eprintln!(" -- no analysis/rulespec.yaml found, skipping verification");
|
||||||
return "\nℹ️ No rulespec found — skipping invariant verification.\n".to_string();
|
return VerifyResult {
|
||||||
|
stages,
|
||||||
|
passed: None, total: None, failed: 0,
|
||||||
|
llm_summary: "Envelope written. No rulespec — skipping verification.".into(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("\n⚠️ Failed to read analysis/rulespec.yaml: {}", e);
|
eprintln!(" !! failed to read rulespec: {}", e);
|
||||||
return format!("\n⚠️ Failed to read rulespec: {}\n", 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) {
|
let compiled = match compile_rulespec(&rulespec, "envelope-verify", 0) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("\n⚠️ Failed to compile rulespec: {}", e);
|
eprintln!(" !! failed to compile rulespec: {}", e);
|
||||||
return format!("\n⚠️ Failed to compile rulespec: {}\n", e);
|
return VerifyResult {
|
||||||
|
stages,
|
||||||
|
passed: None, total: None, failed: 0,
|
||||||
|
llm_summary: format!("Envelope written. Failed to compile rulespec: {}", e),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if compiled.is_empty() {
|
if compiled.is_empty() {
|
||||||
eprintln!("\nℹ️ Rulespec has no predicates - skipping datalog verification");
|
eprintln!(" -- rulespec has no predicates, skipping verification");
|
||||||
return "\nℹ️ Rulespec has no predicates — skipping invariant verification.\n".to_string();
|
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) {
|
let envelope = match read_envelope(session_id) {
|
||||||
Ok(Some(e)) => e,
|
Ok(Some(e)) => e,
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
eprintln!("\n⚠️ No envelope found - skipping datalog verification");
|
eprintln!(" !! no envelope found after write");
|
||||||
return "\n⚠️ No envelope found — skipping invariant verification.\n".to_string();
|
return VerifyResult {
|
||||||
|
stages,
|
||||||
|
passed: None, total: None, failed: 0,
|
||||||
|
llm_summary: "Envelope written but could not be re-read for verification.".into(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("\n⚠️ Failed to load envelope: {}", e);
|
eprintln!(" !! failed to load envelope: {}", e);
|
||||||
return format!("\n⚠️ Failed to load envelope: {}\n", 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);
|
let facts = extract_facts(&envelope, &compiled);
|
||||||
|
|
||||||
// Execute datalog rules
|
|
||||||
let result = execute_rules(&compiled, &facts);
|
let result = execute_rules(&compiled, &facts);
|
||||||
|
|
||||||
// Format results
|
// Write artifacts to session dir (shadow mode)
|
||||||
let output = format_datalog_results(&result);
|
|
||||||
|
|
||||||
let session_dir = get_session_logs_dir(session_id);
|
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 dl_path = session_dir.join("rulespec.compiled.dl");
|
||||||
let datalog_program = format_datalog_program(&compiled, &facts);
|
let datalog_program = format_datalog_program(&compiled, &facts);
|
||||||
if let Err(e) = std::fs::write(&dl_path, &datalog_program) {
|
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);
|
||||||
}
|
}
|
||||||
|
let eval_output = format_datalog_results(&result);
|
||||||
// Write evaluation report
|
|
||||||
let eval_path = session_dir.join("datalog_evaluation.txt");
|
let eval_path = session_dir.join("datalog_evaluation.txt");
|
||||||
match std::fs::write(&eval_path, &output) {
|
if let Err(e) = std::fs::write(&eval_path, &eval_output) {
|
||||||
Ok(_) => {
|
eprintln!(" !! failed to write evaluation report: {}", e);
|
||||||
eprintln!("📊 Compiled rules: {}", dl_path.display());
|
|
||||||
eprintln!("📊 Evaluation report: {}", eval_path.display());
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("⚠️ Failed to write datalog evaluation: {}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If all predicates passed, stamp the envelope with a verification token
|
// Stage 3: verification result
|
||||||
if result.failed_count == 0 && result.passed_count > 0 {
|
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) {
|
match stamp_envelope(session_id, &envelope, &rulespec) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
eprintln!("🔏 Envelope stamped with verification token");
|
stages.push(("\u{2235}".into(), "token stamped".into()));
|
||||||
|
eprintln!(" -- envelope stamped with verification token");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
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)
|
// LLM summary (token value intentionally omitted)
|
||||||
let total = result.passed_count + result.failed_count;
|
let llm_summary = if failed == 0 {
|
||||||
if result.failed_count == 0 {
|
format!("Envelope written. Verification: {}/{} passed.", passed, total)
|
||||||
format!(
|
|
||||||
"\n✅ Invariant verification: {}/{} passed\n", result.passed_count, total,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!("Envelope written. Verification: {}/{} passed, {} failed.", passed, total, failed)
|
||||||
"\n⚠️ Invariant verification: {}/{} passed, {} failed\n", result.passed_count, total, result.failed_count,
|
};
|
||||||
)
|
|
||||||
|
VerifyResult {
|
||||||
|
stages,
|
||||||
|
passed: Some(passed as usize),
|
||||||
|
total: Some(total as usize),
|
||||||
|
failed: failed as usize,
|
||||||
|
llm_summary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,17 @@ pub trait UiWriter: Send + Sync {
|
|||||||
false
|
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
|
/// Print tool execution timing
|
||||||
fn print_tool_timing(&self, duration_str: &str, tokens_delta: u32, context_percentage: f32);
|
fn print_tool_timing(&self, duration_str: &str, tokens_delta: u32, context_percentage: f32);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user