self-run: the result events text, subtype and error flag are written to the unit's stdout, bounded to 4 KiB and marked when cut, so what claude concluded reaches the journal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d98a9db5fe
commit
f896fbaa75
3 changed files with 51 additions and 5 deletions
|
|
@ -444,6 +444,33 @@ pub fn carry(mind: &dyn MindPort, clock: &dyn Clock, lender: &dyn Lender, spawne
|
|||
Ok(outcome)
|
||||
}
|
||||
|
||||
/// The most of a result's text that reaches the journal.
|
||||
const RESULT_TEXT_LIMIT: usize = 4096;
|
||||
|
||||
/// What `claude` concluded: a result event's text, with its subtype and error
|
||||
/// flag when it has them, written to the unit's stdout so it reaches the journal
|
||||
/// before the transcript goes with the runtime directory. Bounded, marked when cut.
|
||||
fn write_result(line: &str, out: &mut dyn Write) -> std::io::Result<()> {
|
||||
let Ok(Value::Object(event)) = serde_json::from_str::<Value>(line) else { return Ok(()) };
|
||||
if event.get("type").and_then(Value::as_str) != Some("result") {
|
||||
return Ok(());
|
||||
}
|
||||
let mut head = String::from("result");
|
||||
if let Some(subtype) = event.get("subtype").and_then(Value::as_str) {
|
||||
head.push_str(&format!(" subtype={subtype}"));
|
||||
}
|
||||
if let Some(is_error) = event.get("is_error").and_then(Value::as_bool) {
|
||||
head.push_str(&format!(" is_error={is_error}"));
|
||||
}
|
||||
let text = event.get("result").and_then(Value::as_str).unwrap_or("");
|
||||
let mut end = text.len().min(RESULT_TEXT_LIMIT);
|
||||
while !text.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
let marker = if end < text.len() { "\n[truncated]" } else { "" };
|
||||
writeln!(out, "{head}: {}{marker}", &text[..end])
|
||||
}
|
||||
|
||||
/// 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, spent: &mut Spent, out: &mut dyn Write) -> Result<SelfRunOutcome> {
|
||||
let user_line = |text: &str| json!({"type": "user", "message": {"role": "user", "content": text}}).to_string();
|
||||
|
|
@ -456,6 +483,7 @@ fn drive(mind: &dyn MindPort, clock: &dyn Clock, mut child: Box<dyn Child>, run:
|
|||
let fact = match child.next(wait) {
|
||||
Read::Line(line) => {
|
||||
spent.hear(&line);
|
||||
write_result(&line, out)?;
|
||||
match read_usage(&line) {
|
||||
Usage::Request { id, prefix } => Event::Request { id, prefix },
|
||||
Usage::Result => Event::Result,
|
||||
|
|
|
|||
|
|
@ -622,6 +622,24 @@ mod seed_links {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_result_events_text_reaches_the_journal_with_its_subtype_and_error_flag() {
|
||||
let line = json!({"type": "result", "subtype": "success", "is_error": false, "result": "all done"}).to_string();
|
||||
let ran = go(vec![Out(line)]);
|
||||
assert!(ran.out.contains("result subtype=success is_error=false: all done\n"), "{}", ran.out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_result_is_cut_at_the_limit_on_a_character_boundary_and_marked() {
|
||||
// One ASCII byte first, so the limit falls in the middle of a two-byte character.
|
||||
let text = format!("a{}", "é".repeat(RESULT_TEXT_LIMIT));
|
||||
let line = json!({"type": "result", "result": text}).to_string();
|
||||
let ran = go(vec![Out(line)]);
|
||||
let written = ran.out.lines().find(|l| l.starts_with("result")).unwrap();
|
||||
assert_eq!(written, format!("result: a{}", "é".repeat(RESULT_TEXT_LIMIT / 2 - 1)));
|
||||
assert!(ran.out.contains("\n[truncated]\n"), "{}", ran.out);
|
||||
}
|
||||
|
||||
// ---- what is read -----------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
|
@ -805,7 +823,7 @@ fn a_jump_past_the_ceiling_plus_the_headroom_writes_the_flush_and_the_running_tu
|
|||
// The headroom is measured from the prefix the flush was asked at (400k) and counts the requests after the
|
||||
// first result only: 459_999 does not interrupt; 460_000 does, once, and m6 is never read.
|
||||
assert_eq!(jump.count(&Event::Stopped(Stop::Interrupt)), 1, "{kinds:?}");
|
||||
assert_eq!(jump.out, "request 1 prefix 290000\nrequest 2 prefix 400000\nrequest 3 prefix 470000\nrequest 4 prefix 459999\nrequest 5 prefix 460000\nsettled: billed 7 micro-USD\n");
|
||||
assert_eq!(jump.out, "request 1 prefix 290000\nrequest 2 prefix 400000\nrequest 3 prefix 470000\nresult subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed\nrequest 4 prefix 459999\nrequest 5 prefix 460000\nsettled: billed 7 micro-USD\n");
|
||||
assert_eq!(jump.ended().0, Cause::Signalled(Stop::Interrupt));
|
||||
assert_eq!(flushes(&jump), 1);
|
||||
}
|
||||
|
|
@ -1014,7 +1032,7 @@ fn unreadable_and_foreign_lines_are_skipped_and_never_printed() {
|
|||
Out(at("m1", 10)),
|
||||
Out(result_line()),
|
||||
]);
|
||||
assert_eq!(ran.out, "request 1 prefix 10\nsettled: billed 7 micro-USD\n");
|
||||
assert_eq!(ran.out, "request 1 prefix 10\nresult subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed\nsettled: billed 7 micro-USD\n");
|
||||
let outcome = ran.outcome.as_ref().unwrap().to_string();
|
||||
assert!(!ran.out.contains(canary) && !outcome.contains(canary));
|
||||
assert!(!ran.written().iter().any(|text| text.contains(canary)), "nothing from the stream is written back");
|
||||
|
|
@ -1214,7 +1232,7 @@ mod process {
|
|||
let mut out = Vec::new();
|
||||
let outcome = carry(&Seen::of(&mind), &SystemClock, &FakeLender::new(Answer::Grant), &Spawn(program), TEST_CAP_S, &mut out).unwrap();
|
||||
assert_eq!(outcome, SelfRunOutcome::Ended { cause: Cause::FlushRequested, code: Some(0), requests: 2, prefix: 300_000 });
|
||||
assert_eq!(String::from_utf8(out).unwrap(), "request 1 prefix 10\nrequest 2 prefix 300000\nsettled: billed 7 micro-USD\n");
|
||||
assert_eq!(String::from_utf8(out).unwrap(), "request 1 prefix 10\nrequest 2 prefix 300000\nresult subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed\nsettled: billed 7 micro-USD\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -218,9 +218,9 @@ fn the_journal_says_what_was_billed_and_that_a_failed_settle_leaves_the_reservat
|
|||
(outcome, String::from_utf8(out).unwrap())
|
||||
};
|
||||
let (outcome, out) = run(Answer::Grant);
|
||||
assert_eq!(out, "request 1 prefix 10\nsettled: billed 7 micro-USD\n");
|
||||
assert_eq!(out, "request 1 prefix 10\nresult subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed\nsettled: billed 7 micro-USD\n");
|
||||
let (failed, out) = run(Answer::GrantSettleFails);
|
||||
assert_eq!(out, "request 1 prefix 10\nsettle failed: the connector bills the reservation\n", "the connector's words are not echoed");
|
||||
assert_eq!(out, "request 1 prefix 10\nresult subtype=success is_error=true: Failed to authenticate: OAuth session expired and could not be refreshed\nsettle failed: the connector bills the reservation\n", "the connector's words are not echoed");
|
||||
assert_eq!(failed, outcome, "the run's own outcome stands");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue