self-run: each claim gets its own section, uncut; a mind query failure fails the run; a claimed cut spec is named as the task; drive loses its unused parameters
Closes Soul's findings on 0e18223 (cut_spec cut-cut-self-run-start-brief.r1).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0e182237fd
commit
04b8ef0251
2 changed files with 72 additions and 56 deletions
|
|
@ -107,44 +107,41 @@ pub fn start_message(instance: &Slug, run: &PipelineRef) -> String {
|
|||
)
|
||||
}
|
||||
|
||||
/// The most of the task that reaches the start message, in bytes.
|
||||
const TASK_LIMIT: usize = 8 * 1024;
|
||||
|
||||
/// The task section of the start message: each claimed follow-up's id,
|
||||
/// campaign and `item` text verbatim, because the brief lives in the claim and a
|
||||
/// run that cannot reach its mind would never see its job. A claim that cannot
|
||||
/// be read as a follow-up is named, never dropped and never replaced by an
|
||||
/// invented task. The section is cut at a character boundary to `TASK_LIMIT`,
|
||||
/// marked when cut.
|
||||
fn task_section(mind: &dyn MindPort, claims: &[PipelineRef]) -> String {
|
||||
/// The task section of the start message: one section per claim, in claim
|
||||
/// order. A claimed follow-up gives its id, campaign and `item` verbatim,
|
||||
/// because the brief lives in the claim and a run that cannot reach its mind
|
||||
/// would never see its job; a claimed cut spec is named as the task, to be read
|
||||
/// with `huginn view`; a claim the mind does not hold, or of another kind, is
|
||||
/// named by id. The claims are bounded by their types, so nothing is cut. A mind
|
||||
/// that cannot answer is an error: the run is not started on a guessed task.
|
||||
fn task_section(mind: &dyn MindPort, claims: &[PipelineRef]) -> Result<String> {
|
||||
let mut task = String::new();
|
||||
for claim in claims {
|
||||
let one = Selection { keys: Some(vec![claim.id.0.clone()]), projection: "document".into(), ..Selection::default() };
|
||||
let read = mind.query(&one).ok().and_then(|page| match page.items {
|
||||
PipelinePageItems::Documents(views) => views.into_iter().next().map(|view| view.document),
|
||||
_ => None,
|
||||
});
|
||||
match read {
|
||||
let page = mind.query(&one).with_context(|| format!("the claim `{}` could not be read from the mind", claim.id.0))?;
|
||||
let PipelinePageItems::Documents(views) = page.items else { bail!("the mind answered headers to a document selection") };
|
||||
let id = &claim.id.0;
|
||||
match views.into_iter().next().map(|view| view.document) {
|
||||
Some(PipelineDocument::FollowUp(follow_up)) => {
|
||||
task.push_str(&format!("\n\nTask, claimed follow-up `{}` (campaign `{}`):\n{}", claim.id.0, follow_up.campaign.0, follow_up.item.0));
|
||||
}
|
||||
_ => task.push_str(&format!("\n\nThe run claims `{}`, which could not be read as a follow-up: its task is unknown.", claim.id.0)),
|
||||
}
|
||||
}
|
||||
cut_at(task, TASK_LIMIT)
|
||||
}
|
||||
task.push_str(&format!("
|
||||
|
||||
/// `text` cut at a character boundary to `limit` bytes, marked when cut.
|
||||
fn cut_at(mut text: String, limit: usize) -> String {
|
||||
if text.len() > limit {
|
||||
let mut end = limit;
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
Task, claimed follow-up `{id}` (campaign `{}`):
|
||||
{}", follow_up.campaign.0, follow_up.item.0));
|
||||
}
|
||||
Some(PipelineDocument::CutSpec(spec)) => {
|
||||
task.push_str(&format!("
|
||||
|
||||
Task, claimed cut spec `{id}` (campaign `{}`): the spec is the task; read it with `huginn view`.", spec.campaign.0));
|
||||
}
|
||||
Some(_) => task.push_str(&format!("
|
||||
|
||||
The run claims `{id}` ({:?}), which is no brief: its task is unknown.", claim.kind)),
|
||||
None => task.push_str(&format!("
|
||||
|
||||
The run claims `{id}`, which the mind does not hold: its task is unknown.")),
|
||||
}
|
||||
text.truncate(end);
|
||||
text.push_str("\n[task truncated]");
|
||||
}
|
||||
text
|
||||
Ok(task)
|
||||
}
|
||||
|
||||
/// What the child is told once, at the context ceiling.
|
||||
|
|
@ -491,7 +488,7 @@ fn withdraw_misconfigured(mind: &dyn MindPort, clock: &dyn Clock, run: &Pipeline
|
|||
/// error, and the launch that opens the next run closes the run left open.
|
||||
pub fn carry(mind: &dyn MindPort, clock: &dyn Clock, lender: &dyn Lender, spawner: &dyn Spawner, run_cap_s: u64, out: &mut dyn Write) -> Result<SelfRunOutcome> {
|
||||
let Some((run, budget, claims)) = her_run(mind)? else { return Ok(SelfRunOutcome::NoRun) };
|
||||
let start = format!("{}{}", start_message(mind.instance(), &run), task_section(mind, &claims));
|
||||
let start = format!("{}{}", start_message(mind.instance(), &run), task_section(mind, &claims)?);
|
||||
let mut spent = Spent::default();
|
||||
let ran = lend_and_drive(mind, clock, lender, spawner, run_cap_s, out, (&run, &budget, &start), &mut spent);
|
||||
let closed = close_if_open(mind, clock, &run, &ran, &spent);
|
||||
|
|
@ -524,7 +521,7 @@ fn lend_and_drive(mind: &dyn MindPort, clock: &dyn Clock, lender: &dyn Lender, s
|
|||
return Err(error);
|
||||
}
|
||||
};
|
||||
let outcome = drive(mind, clock, child, run, start, spent, out)?;
|
||||
let outcome = drive(clock, child, start, spent, out)?;
|
||||
// A child that could not even be killed may still be spending: that lease is
|
||||
// dropped, and the connector bills the reservation.
|
||||
if !matches!(outcome, SelfRunOutcome::Ended { cause: Cause::Unstoppable, .. }) {
|
||||
|
|
@ -574,7 +571,7 @@ fn write_result(line: &str, out: &mut dyn Write) -> std::io::Result<()> {
|
|||
}
|
||||
|
||||
/// The loop: facts in, one clock read per fact, actions out.
|
||||
fn drive(mind: &dyn MindPort, clock: &dyn Clock, mut child: Box<dyn Child>, run: &PipelineRef, start: &str, spent: &mut Spent, out: &mut dyn Write) -> Result<SelfRunOutcome> {
|
||||
fn drive(clock: &dyn Clock, mut child: Box<dyn Child>, start: &str, spent: &mut Spent, out: &mut dyn Write) -> Result<SelfRunOutcome> {
|
||||
let user_line = |text: &str| json!({"type": "user", "message": {"role": "user", "content": text}}).to_string();
|
||||
child.write_line(&user_line(start));
|
||||
|
||||
|
|
|
|||
|
|
@ -271,12 +271,14 @@ struct Seen<'a> {
|
|||
instance: Slug,
|
||||
doubled: bool,
|
||||
admit_fails: bool,
|
||||
/// A query for this key fails.
|
||||
query_fails_on: Option<String>,
|
||||
admits: Cell<usize>,
|
||||
}
|
||||
|
||||
impl<'a> Seen<'a> {
|
||||
fn of(inner: &'a TestMind) -> Self {
|
||||
Self { inner, instance: inner.instance().clone(), doubled: false, admit_fails: false, admits: Cell::new(0) }
|
||||
Self { inner, instance: inner.instance().clone(), doubled: false, admit_fails: false, query_fails_on: None, admits: Cell::new(0) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -286,6 +288,11 @@ impl MindPort for Seen<'_> {
|
|||
}
|
||||
|
||||
fn query(&self, selection: &Selection) -> Result<PipelineSelectionPage> {
|
||||
if let (Some(key), Some(keys)) = (&self.query_fails_on, &selection.keys)
|
||||
&& keys.contains(key)
|
||||
{
|
||||
return Err(anyhow!("mind-down-canary"));
|
||||
}
|
||||
let mut page = self.inner.query(selection)?;
|
||||
if self.doubled
|
||||
&& let PipelinePageItems::Headers(items) = &mut page.items
|
||||
|
|
@ -1355,29 +1362,50 @@ fn follow_up_campaign() -> String {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn a_claim_that_cannot_be_read_as_a_follow_up_is_named_not_dropped() {
|
||||
// A cut spec is claimable but is no brief.
|
||||
fn a_claim_that_is_no_brief_is_named_not_dropped() {
|
||||
// A claimed cut spec is the task: named with its id, to be read with huginn view.
|
||||
let mind = claiming(&[spec_ref("x")], vec![]);
|
||||
let ran = carry_with(&Seen::of(&mind), vec![Out(result_line())], Some(0), false);
|
||||
let first = &ran.written()[0];
|
||||
assert!(first.contains(&spec_ref("x").id.0) && first.contains("its task is unknown"), "{first}");
|
||||
assert!(first.contains(&format!("claimed cut spec `{}`", spec_ref("x").id.0)) && first.contains("huginn view"), "{first}");
|
||||
assert!(!first.contains("unknown"), "{first}");
|
||||
// A claim the mind does not hold at all.
|
||||
let absent = reference(K::FollowUp, &id("follow_up", "absent"));
|
||||
let task = task_section(&mind, std::slice::from_ref(&absent));
|
||||
assert!(task.contains(&absent.id.0) && task.contains("its task is unknown"), "{task}");
|
||||
let task = task_section(&mind, std::slice::from_ref(&absent)).unwrap();
|
||||
assert!(task.contains(&absent.id.0) && task.contains("does not hold"), "{task}");
|
||||
// A document of another kind is named by id and kind.
|
||||
let other = run_ref("selfrun-a");
|
||||
let task = task_section(&mind, std::slice::from_ref(&other)).unwrap();
|
||||
assert!(task.contains(&other.id.0) && task.contains("no brief"), "{task}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_task_is_bounded_and_marked_when_cut() {
|
||||
let big = "\u{e9}".repeat(480);
|
||||
let docs: Vec<_> = (0..40).map(|n| follow_up_saying(&format!("big-{n}"), &big)).collect();
|
||||
fn every_claim_of_sixty_four_full_items_is_named_by_id() {
|
||||
let big = "\u{e9}".repeat(500);
|
||||
let docs: Vec<_> = (0..64).map(|n| follow_up_saying(&format!("big-{n}"), &big)).collect();
|
||||
let claims: Vec<_> = docs.iter().map(|(_, claim)| claim.clone()).collect();
|
||||
let mind = claiming(&claims, docs.into_iter().map(|(document, _)| document).collect());
|
||||
let task = task_section(&mind, &claims);
|
||||
assert!(task.len() <= TASK_LIMIT + "\n[task truncated]".len(), "{}", task.len());
|
||||
assert!(task.ends_with("\n[task truncated]"));
|
||||
let small = task_section(&mind, &claims[..2]);
|
||||
assert!(!small.contains("truncated") && small.contains(&claims[1].id.0));
|
||||
let task = task_section(&mind, &claims).unwrap();
|
||||
for claim in &claims {
|
||||
assert!(task.contains(&claim.id.0), "{}", claim.id.0);
|
||||
}
|
||||
assert_eq!(task.matches(&big).count(), 64, "every item verbatim");
|
||||
assert!(!task.contains("truncated"));
|
||||
// Claim order is kept.
|
||||
assert!(task.find(&claims[0].id.0) < task.find(&claims[63].id.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mind_query_failure_on_a_claim_fails_the_run_with_the_cause_and_starts_no_child() {
|
||||
let (brief, claim) = follow_up_saying("brief-a", "Fix the thing.");
|
||||
let mind = claiming(std::slice::from_ref(&claim), vec![brief]);
|
||||
let mut seen = Seen::of(&mind);
|
||||
seen.query_fails_on = Some(claim.id.0.clone());
|
||||
let ran = carry_with(&seen, vec![Out(result_line())], Some(0), false);
|
||||
let error = format!("{:#}", ran.outcome.as_ref().unwrap_err());
|
||||
assert!(error.contains("mind-down-canary") && error.contains(&claim.id.0), "{error}");
|
||||
assert!(ran.spawns.is_empty() && ran.events.is_empty() && !ran.all.iter().any(|event| matches!(event, Event::Lent)), "no lease, no child");
|
||||
assert!(!ran.out.contains("unknown"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1385,12 +1413,3 @@ fn a_run_without_claims_gets_the_identity_lines_alone() {
|
|||
let ran = go(vec![Out(result_line())]);
|
||||
assert_eq!(ran.written()[0], start_message(&Slug("yggdrasil".into()), &run_ref("selfrun-a")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cut_keeps_exactly_the_limit_whole_characters_and_marks_only_a_real_cut() {
|
||||
assert_eq!(cut_at("abcd".into(), 4), "abcd");
|
||||
assert_eq!(cut_at("abcde".into(), 4), "abcd\n[task truncated]");
|
||||
// "\u{e9}" is two bytes: a limit inside one keeps the characters before it.
|
||||
assert_eq!(cut_at("a\u{e9}\u{e9}".into(), 4), "a\u{e9}\n[task truncated]");
|
||||
assert_eq!(cut_at("a\u{e9}\u{e9}".into(), 5), "a\u{e9}\u{e9}");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue