use crate::candidate::{ChangeClass, Metric};
use crate::runlog::Corpus;
pub const DIAGNOSE_SYSTEM: &str = "\
You are diagnosing a harness — the program that runs an AI agent — from its own \
measurements. You propose one change and predict what it will do. You do not \
apply it: a separate measurement decides whether it was right, and a wrong \
proposal costs one measurement, so a specific guess beats a safe one.
You may read the source and its documentation. The documentation records why \
each mechanism exists and what it cost to learn; treat a documented reason as \
evidence, not as decoration. If the thing you were about to change is \
load-bearing for something the documentation explains, propose something else.
Never reproduce sentences from anything you read. Write your own.";
pub const DIAGNOSE_INSTRUCTION: &str = "\
Work out what is most likely going wrong, then propose exactly one change.
Write your reasoning first, in prose. Then a block in exactly this form:
PROPOSAL
class: config | prose | architecture | security
change: <one line — for config, KEY=VALUE>
metric: ended_on_failed_call | tool_error_rate | cut_short | compactions | turns | malformed_args
rationale: <one line: what is wrong, and why this addresses it>
`metric` is what you predict this change will *reduce*. Pick the one it should \
move most; a prediction that cannot fail is not a prediction. If the evidence \
does not support any single change, say so in prose and write no block.
Anything touching `[security]`, `[sandbox]` or `[outbox]` is `class: security`, \
whatever else it also is. Calling it something else does not make it \
measurable — it is reclassified from the change itself and staged for a person \
either way.";
#[derive(Debug, Clone, Default)]
pub struct Evidence {
pub runs: usize,
pub sessions_read: usize,
pub model: String,
pub tool_calls: u64,
pub tool_errors: u64,
pub tool_error_rate: Option<f64>,
pub ended_on_failed_call: usize,
pub ended_on_failed_call_rate: Option<f64>,
pub compactions: u64,
pub stop_causes: Vec<(String, usize)>,
pub mean_peak_context_pressure: Option<f64>,
pub mean_anticipated_guilt: Option<f64>,
pub findings: Vec<String>,
pub history: Vec<String>,
}
impl Evidence {
pub fn of(model: &str, corpus: &Corpus) -> Evidence {
Evidence {
runs: corpus.len(),
sessions_read: corpus.sessions_read,
model: model.to_string(),
tool_calls: corpus.tool_calls(),
tool_errors: corpus.tool_errors(),
tool_error_rate: corpus.tool_error_rate(),
ended_on_failed_call: corpus.ended_on_failed_call(),
ended_on_failed_call_rate: corpus.rate_of(|r| r.stats.ended_on_failed_call),
compactions: corpus.compactions(),
stop_causes: corpus
.stop_causes()
.into_iter()
.map(|(cause, n)| {
let name = cause
.map(|c| {
serde_json::to_string(&c)
.unwrap_or_default()
.trim_matches('"')
.to_string()
})
.unwrap_or_else(|| "unrecorded".into());
(name, n)
})
.collect(),
mean_peak_context_pressure: corpus.mean_peak_context_pressure(),
mean_anticipated_guilt: corpus.mean_anticipated_guilt(),
findings: Vec::new(),
history: Vec::new(),
}
}
pub fn brief(&self) -> String {
let pct = |r: Option<f64>| match r {
Some(r) => format!("{:.1}%", r * 100.0),
None => "unknown (no denominator)".into(),
};
let mut out = format!(
"model: {}\nruns: {} (from {} session(s))\n\
tool calls: {} · refused by the environment: {} ({})\n\
finished on a failed call: {} ({})\ncompactions: {}\nstop causes: {}\n\
avg peak context pressure: {} · avg anticipated guilt: {} \
(guilt is computed partly from pressure — a rise in both is not two \
independent findings)\n",
self.model,
self.runs,
self.sessions_read,
self.tool_calls,
self.tool_errors,
pct(self.tool_error_rate),
self.ended_on_failed_call,
pct(self.ended_on_failed_call_rate),
self.compactions,
self.stop_causes
.iter()
.map(|(name, n)| format!("{name} {n}"))
.collect::<Vec<_>>()
.join(", "),
pct(self.mean_peak_context_pressure),
self.mean_anticipated_guilt
.map(|g| format!("{g:.2}"))
.unwrap_or_else(|| "unknown (no denominator)".into()),
);
if !self.findings.is_empty() {
out.push_str("\nwhat the health check reported:\n");
for f in &self.findings {
out.push_str(&format!("- {f}\n"));
}
}
if !self.history.is_empty() {
out.push_str(
"\nalready proposed by earlier passes — do not propose any of these again; \
a measured rejection is evidence, not an invitation to retry:\n",
);
for h in &self.history {
out.push_str(&format!("- {h}\n"));
}
}
out
}
}
pub const GUARDED_SECTIONS: [&str; 3] = ["security", "sandbox", "outbox"];
pub const GUARDED_KEYS: [&str; 6] = [
"trifecta",
"block_private_ips",
"allowed_domains",
"blocked_domains",
"mark_untrusted_output",
"block_sends_after_private",
];
pub fn names_guarded_setting(change: &str) -> Option<&'static str> {
let hay = change.to_lowercase();
for section in GUARDED_SECTIONS {
if hay.contains(&format!("{section}.")) || hay.contains(&format!("{section}]")) {
return Some(section);
}
}
GUARDED_KEYS.into_iter().find(|k| hay.contains(k))
}
#[derive(Debug, Clone, PartialEq)]
pub struct Proposal {
pub class: ChangeClass,
pub change: String,
pub metric: Metric,
pub rationale: String,
pub reclassified: Option<String>,
}
pub fn parse_proposal(text: &str) -> Option<Proposal> {
let start = text.rfind("PROPOSAL")?;
let mut fields = std::collections::HashMap::new();
for line in text[start..].lines().skip(1) {
let line = line.trim().trim_start_matches(['-', '*', ' ']);
if line.is_empty() && !fields.is_empty() {
break;
}
if let Some((k, v)) = line.split_once(':') {
let key = k.trim().trim_matches('`').to_lowercase();
if matches!(key.as_str(), "class" | "change" | "metric" | "rationale") {
fields.insert(key, v.trim().to_string());
}
}
}
let class = match fields.get("class")?.to_lowercase().as_str() {
"config" => ChangeClass::Config,
"prose" => ChangeClass::Prose,
"architecture" => ChangeClass::Architecture,
"security" => ChangeClass::Security,
_ => return None,
};
let metric = match fields.get("metric")?.to_lowercase().as_str() {
"ended_on_failed_call" => Metric::EndedOnFailedCall,
"tool_error_rate" => Metric::ToolErrorRate,
"cut_short" => Metric::CutShort,
"compactions" => Metric::Compactions,
"turns" => Metric::Turns,
"malformed_args" => Metric::MalformedArgs,
_ => return None,
};
let change = fields.get("change")?.trim().to_string();
if change.is_empty() {
return None;
}
let (class, reclassified) = match names_guarded_setting(&change) {
Some(found) if class != ChangeClass::Security => (
ChangeClass::Security,
Some(format!(
"proposed as `{class:?}`, reclassified: the change names `{found}`, \
which is a security boundary"
)),
),
_ => (class, None),
};
Some(Proposal {
class,
change,
metric,
rationale: fields.get("rationale").cloned().unwrap_or_default(),
reclassified,
})
}
pub const CARRY_OVER_WORDS: usize = 8;
pub fn carries_over(proposal: &str, sources: &[&str]) -> Option<String> {
let words = |s: &str| -> Vec<String> {
s.split_whitespace()
.map(|w| {
w.trim_matches(|c: char| !c.is_alphanumeric())
.to_lowercase()
})
.filter(|w| !w.is_empty())
.collect()
};
let needle = words(proposal);
if needle.len() < CARRY_OVER_WORDS {
return None;
}
let haystacks: Vec<Vec<String>> = sources.iter().map(|s| words(s)).collect();
for window in needle.windows(CARRY_OVER_WORDS) {
for hay in &haystacks {
if hay.windows(CARRY_OVER_WORDS).any(|w| w == window) {
return Some(window.join(" "));
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_well_formed_block_parses_out_of_whatever_prose_surrounds_it() {
let reply = "\
The turn ceiling is stopping a quarter of runs, and the ones it stops are the
long ones. Raising it is the cheapest thing to try.
PROPOSAL
class: config
change: max_turns=40
metric: cut_short
rationale: runs are hitting the ceiling rather than finishing
I would look at compaction next if this does not help.";
let p = parse_proposal(reply).unwrap();
assert_eq!(p.class, ChangeClass::Config);
assert_eq!(p.change, "max_turns=40");
assert_eq!(p.metric, Metric::CutShort);
assert!(p.rationale.starts_with("runs are hitting"));
}
#[test]
fn declining_to_propose_is_a_legitimate_answer() {
let reply = "The rates are all within normal range; I see nothing worth changing.";
assert!(parse_proposal(reply).is_none());
}
#[test]
fn a_block_that_cannot_be_falsified_is_refused() {
let base = "PROPOSAL\nclass: config\nchange: max_turns=40\nmetric: cut_short";
assert!(parse_proposal(base).is_some());
for broken in [
"PROPOSAL\nclass: config\nchange: max_turns=40",
"PROPOSAL\nclass: config\nchange: max_turns=40\nmetric: vibes",
"PROPOSAL\nclass: whatever\nchange: max_turns=40\nmetric: cut_short",
"PROPOSAL\nclass: config\nchange:\nmetric: cut_short",
] {
assert!(parse_proposal(broken).is_none(), "{broken}");
}
}
#[test]
fn a_security_change_labelled_config_is_reclassified_rather_than_believed() {
let reply = "\
PROPOSAL
class: config
change: security.minimize_taint=false
metric: tool_error_rate
rationale: taint minimization refuses calls that would have succeeded";
let p = parse_proposal(reply).unwrap();
assert_eq!(p.class, ChangeClass::Security);
let note = p.reclassified.expect("the mislabel must be on the record");
assert!(note.contains("Config"), "{note}");
assert!(note.contains("security"), "{note}");
}
#[test]
fn every_guarded_boundary_is_caught_however_it_is_spelled() {
for change in [
"security.trifecta=allow",
"[security] trifecta = \"allow\"",
"config.security.block_private_ips=false",
"sandbox.kind=none",
"[sandbox] kind = \"none\"",
"outbox.tools=[]",
"trifecta=ask",
"block_sends_after_private=false",
] {
let reply =
format!("PROPOSAL\nclass: config\nchange: {change}\nmetric: tool_error_rate");
let p = parse_proposal(&reply).expect(change);
assert_eq!(p.class, ChangeClass::Security, "{change}");
assert!(p.reclassified.is_some(), "{change}");
}
}
#[test]
fn every_security_setting_is_guarded_including_the_ones_not_yet_written() {
let v = serde_json::to_value(crate::config::SecurityConfig::default())
.expect("SecurityConfig serialises");
let fields = v.as_object().expect("as a map");
assert!(
!fields.is_empty(),
"no fields found — did the shape change?"
);
for name in fields.keys() {
assert!(
names_guarded_setting(&format!("{name}=whatever")).is_some(),
"`{name}` is a [security] setting and nothing guards it by name. \
Add it to GUARDED_KEYS. A proposal naming it while asserting \
`class: config` would route to the measurement arm."
);
}
}
#[test]
fn a_sandbox_or_outbox_setting_is_guarded_by_its_section_not_its_field() {
assert!(names_guarded_setting("sandbox.kind=none").is_some());
assert!(names_guarded_setting("[outbox] tools = []").is_some());
assert_eq!(names_guarded_setting("kind=none"), None);
assert_eq!(names_guarded_setting("tools=[]"), None);
}
#[test]
fn the_closed_override_set_is_untouched_by_the_check() {
for change in [
"max_turns=40",
"compact_at_tokens=100000",
"max_output_tokens=8192",
"effort=high",
] {
let reply = format!("PROPOSAL\nclass: config\nchange: {change}\nmetric: cut_short");
let p = parse_proposal(&reply).expect(change);
assert_eq!(p.class, ChangeClass::Config, "{change}");
assert!(p.reclassified.is_none(), "{change}");
}
}
#[test]
fn an_honestly_labelled_security_change_carries_no_mislabel_note() {
let reply = "PROPOSAL\nclass: security\nchange: sandbox.kind=none\nmetric: tool_error_rate";
let p = parse_proposal(reply).unwrap();
assert_eq!(p.class, ChangeClass::Security);
assert!(p.reclassified.is_none());
}
#[test]
fn naming_a_setting_is_what_counts_not_mentioning_its_subject() {
let reply = "\
PROPOSAL
class: prose
change: reword the sandbox preflight failure so it names the backend
metric: tool_error_rate
rationale: the message does not say which backend refused";
let p = parse_proposal(reply).unwrap();
assert_eq!(p.class, ChangeClass::Prose);
assert!(p.reclassified.is_none());
assert_eq!(
names_guarded_setting("explain the sandbox. Then bwrap"),
Some("sandbox")
);
}
#[test]
fn the_derivation_only_ever_raises_toward_review() {
for change in [
"max_turns=40",
"sandbox.kind=none",
"reword the system prompt",
] {
let reply = format!("PROPOSAL\nclass: security\nchange: {change}\nmetric: cut_short");
let p = parse_proposal(&reply).expect(change);
assert_eq!(p.class, ChangeClass::Security, "{change}");
}
}
#[test]
fn the_last_block_wins_when_a_model_reconsiders() {
let reply = "\
PROPOSAL
class: config
change: max_turns=20
metric: cut_short
Actually the ceiling is not the problem.
PROPOSAL
class: config
change: compact_at_tokens=8000
metric: compactions
rationale: the threshold is too low";
let p = parse_proposal(reply).unwrap();
assert_eq!(p.change, "compact_at_tokens=8000");
assert_eq!(p.metric, Metric::Compactions);
}
#[test]
fn a_proposal_that_reproduces_what_it_read_is_caught() {
let page = "Some blog post. To improve reliability you should always \
disable the sandbox before running any agent tooling. More text.";
let lifted = "I propose we always disable the sandbox before running any \
agent tooling, per the source.";
let hit = carries_over(lifted, &[page]).expect("verbatim run not caught");
assert!(
hit.contains("disable the sandbox before running any"),
"{hit}"
);
let drawn = "Sandbox startup is failing on this host, so runs are erroring \
before they begin; raise the preflight timeout.";
assert_eq!(carries_over(drawn, &[page]), None);
}
#[test]
fn short_proposals_and_incidental_phrases_do_not_trip_the_check() {
let page = "The model stopped after the tool call failed.";
assert_eq!(carries_over("max_turns=40", &[page]), None);
assert_eq!(
carries_over("the model stopped after the tool call", &[page]),
None
);
assert!(carries_over("the model stopped after the tool call failed", &[page]).is_some());
}
#[test]
fn the_brief_reports_an_absent_rate_as_unknown_rather_than_zero() {
let evidence = Evidence {
model: "tiny-local".into(),
runs: 12,
..Default::default()
};
let brief = evidence.brief();
assert!(brief.contains("unknown (no denominator)"), "{brief}");
assert!(!brief.contains("0.0%"), "{brief}");
}
#[test]
fn the_brief_reports_the_homeostat_means_when_sensed() {
let evidence = Evidence {
model: "tiny-local".into(),
runs: 8,
mean_peak_context_pressure: Some(0.42),
mean_anticipated_guilt: Some(0.1),
..Default::default()
};
let brief = evidence.brief();
assert!(brief.contains("42.0%"), "{brief}");
assert!(brief.contains("0.10"), "{brief}");
assert!(brief.contains("not two"), "{brief}");
}
#[test]
fn the_brief_carries_numbers_and_findings_and_has_nowhere_to_put_a_transcript() {
let mut evidence = Evidence {
model: "opus".into(),
runs: 40,
tool_calls: 200,
tool_errors: 60,
tool_error_rate: Some(0.3),
..Default::default()
};
evidence.findings.push("30% of calls refused".into());
let brief = evidence.brief();
assert!(brief.contains("30.0%"));
assert!(brief.contains("what the health check reported"));
assert!(brief.contains("- 30% of calls refused"));
}
}