self: a briefless turn is open from a request with nothing echoed until its result; the start message states the routing rules
A request with no line of ours echoed is a turn that took in no brief, notification or not; until its result only the conversation it continues, or the notification's task's, is written. Nothing told yet means no other conversation exists. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
08a80697d3
commit
8bdb84d94b
2 changed files with 62 additions and 6 deletions
|
|
@ -605,6 +605,9 @@ struct SelfInbox<'a> {
|
|||
/// The `tool_use_id`s of task notifications whose turn has not yet ended in a result that took in no brief. While any is pending, a
|
||||
/// brief is written only when it is of the conversation the notification is attributed to.
|
||||
notified: Vec<String>,
|
||||
/// A turn that took in no brief is open: a request came with nothing echoed, and no line of ours has joined it. Until its result, a brief
|
||||
/// is written only when it is of the conversation that turn is attributed to (the notification'"'"'s task, else the turn before).
|
||||
briefless_open: bool,
|
||||
/// The flush was written and the CLI has not yet taken it.
|
||||
flush_written: bool,
|
||||
/// The CLI took the flush and its result has not come: that result is the session's note to its successor, never a reply to a brief.
|
||||
|
|
@ -625,7 +628,7 @@ impl<'a> SelfInbox<'a> {
|
|||
carried.push(brief.id.clone());
|
||||
}
|
||||
}
|
||||
Self { mind, start, plan: render_plan(plan_quota, now_ms), told, waiting: Vec::new(), heard, run: run.clone(), carried, echoed: Vec::new(), conversation, last_answers: Vec::new(), tasks: HashMap::new(), notified: Vec::new(), flush_written: false, flushing: false, replies: 0, unadmitted: Vec::new() }
|
||||
Self { mind, start, plan: render_plan(plan_quota, now_ms), told, waiting: Vec::new(), heard, run: run.clone(), carried, echoed: Vec::new(), conversation, last_answers: Vec::new(), tasks: HashMap::new(), notified: Vec::new(), briefless_open: false, flush_written: false, flushing: false, replies: 0, unadmitted: Vec::new() }
|
||||
}
|
||||
|
||||
/// Admits each reply the mind has not taken, in order; one try each. A refusal or an unreachable mind keeps the reply and never ends the session.
|
||||
|
|
@ -647,6 +650,7 @@ impl<'a> SelfInbox<'a> {
|
|||
let Ok(Value::Object(event)) = serde_json::from_str::<Value>(line) else { return };
|
||||
let text = result_text(&event);
|
||||
let flush = std::mem::take(&mut self.flushing);
|
||||
let took = self.echoed.first().and_then(|id| self.conversation_of(id));
|
||||
// The flush result is the session's note to its successor, in the journal: it is no reply, so nothing ever points at it.
|
||||
if ok && !flush && !text.trim().is_empty() {
|
||||
self.replies += 1;
|
||||
|
|
@ -665,10 +669,12 @@ impl<'a> SelfInbox<'a> {
|
|||
text: cut_text(text, REPLY_TEXT_LIMIT).as_str().into(),
|
||||
});
|
||||
}
|
||||
// A turn that took in no brief has ended: it was the turn of the notifications pending, which are now answered.
|
||||
if !flush && self.echoed.is_empty() {
|
||||
// The turn has ended. A turn that took in no brief answered the notifications pending; one that took in the notification's own conversation's briefs
|
||||
// took the notification in with them. A notification of another conversation stays pending: its turn is still to come.
|
||||
if !flush && (took.is_none() || (ok && !text.trim().is_empty() && self.turn_conversation() == took)) {
|
||||
self.notified.clear();
|
||||
}
|
||||
self.briefless_open = false;
|
||||
self.admit_unadmitted();
|
||||
self.conversation = self.carried.first().or(self.echoed.first()).and_then(|id| self.conversation_of(id));
|
||||
}
|
||||
|
|
@ -695,8 +701,8 @@ impl<'a> SelfInbox<'a> {
|
|||
answers
|
||||
}
|
||||
|
||||
/// The one conversation the pending notifications are attributed to; none when they name two, or one could not be attributed.
|
||||
fn notified_conversation(&self) -> Option<Conversation> {
|
||||
/// The one conversation the open turn is attributed to: its notifications' tasks', else the turn before. None when they name two, or one could not be attributed.
|
||||
fn turn_conversation(&self) -> Option<Conversation> {
|
||||
let answers = self.briefless_answers();
|
||||
let mut conversations = answers.iter().map(|id| self.conversation_of(id));
|
||||
let first = conversations.next()??;
|
||||
|
|
@ -735,10 +741,16 @@ impl<'a> SelfInbox<'a> {
|
|||
self.told.iter().find(|brief| brief.id == id).map(|brief| brief.conversation.clone())
|
||||
}
|
||||
|
||||
/// Whether the open turn lets this brief in: while a task notification or another turn that took in no brief is open, only the conversation
|
||||
/// it is attributed to (none when it cannot be placed), unless nothing has been told yet and no other conversation exists.
|
||||
fn turn_allows(&self, brief: &Brief) -> bool {
|
||||
(self.notified.is_empty() && (!self.briefless_open || self.told.is_empty())) || self.turn_conversation().as_ref() == Some(&brief.conversation)
|
||||
}
|
||||
|
||||
/// Whether a waiting brief may be told now: any when every brief told has been answered, else only the unanswered ones' conversation.
|
||||
/// While a task notification's turn is open, only the conversation the notification is attributed to may be told (none when it cannot be placed).
|
||||
fn may_tell(&self, brief: &Brief) -> bool {
|
||||
(self.notified.is_empty() || self.notified_conversation().as_ref() == Some(&brief.conversation)) && self.conversation.as_ref().is_none_or(|conversation| *conversation == brief.conversation)
|
||||
self.turn_allows(brief) && self.conversation.as_ref().is_none_or(|conversation| *conversation == brief.conversation)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -777,9 +789,15 @@ impl SessionInbox for SelfInbox<'_> {
|
|||
fn line(&mut self, line: &str) {
|
||||
self.heard.hear(line);
|
||||
self.note_task(line);
|
||||
if matches!(read_usage(line), Usage::Request { .. }) && self.echoed.is_empty() {
|
||||
self.briefless_open = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn echoed(&mut self) {
|
||||
if !self.carried.is_empty() {
|
||||
self.briefless_open = false;
|
||||
}
|
||||
self.echoed.append(&mut self.carried);
|
||||
self.flushing |= std::mem::take(&mut self.flush_written);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -338,3 +338,41 @@ fn a_restart_with_a_resolved_claim_and_no_open_brief_holds_the_gate_for_the_clai
|
|||
assert!(ran.outcome.is_ok());
|
||||
assert_eq!((answered(&mind, 1), answered(&mind, 2)), (vec![id_of("a1")], vec![id_of("l1")]));
|
||||
}
|
||||
|
||||
/// A turn that took in no brief is running (a request with nothing echoed) and the stream gave no notification event: it continues the turn
|
||||
/// before, so another conversation's brief is not written into it.
|
||||
#[test]
|
||||
fn another_conversations_brief_waits_for_any_turn_that_took_in_no_brief() {
|
||||
let before = |mind: &Rc<TestMind>| {
|
||||
vec![
|
||||
Out(echo()),
|
||||
Out(saying("nothing yet")),
|
||||
briefed(mind, 31, vec![aqua("a1")]),
|
||||
Out(echo()),
|
||||
Out(saying("aqua turn: started a background task")),
|
||||
Out(at("n1", 10)),
|
||||
briefed(mind, 31, vec![libby("l1")]),
|
||||
Idle(31),
|
||||
]
|
||||
};
|
||||
let mind = live_mind();
|
||||
let ran = mind_run(&mind, before(&mind));
|
||||
assert!(ran.outcome.is_ok());
|
||||
assert!(!written_with(&ran, &id_of("l1")), "{:?}", ran.written());
|
||||
|
||||
let mind = live_mind();
|
||||
let ran = mind_run(&mind, [before(&mind), vec![Out(saying("SYNTHETIC aqua task outcome")), Idle(31), Out(echo()), Out(saying("libby done"))]].concat());
|
||||
assert!(ran.outcome.is_ok());
|
||||
assert_eq!(answered(&mind, 3), vec![id_of("a1")], "the outcome continues aqua's turn, not libby's");
|
||||
assert_eq!(answered(&mind, 4), vec![id_of("l1")]);
|
||||
}
|
||||
|
||||
/// The start message states the routing rules her Self works under, in place of the old continuation sentence.
|
||||
#[test]
|
||||
fn the_start_message_states_the_routing_rules() {
|
||||
let text = start_message(&Slug("yggdrasil".into()), &run_ref("selfrun-a"), &[], 0);
|
||||
for rule in ["one conversation at a time", "the conversation of the turn that started it", "no other conversation's brief is told into that turn", "pointer to it by id, without its text"] {
|
||||
assert!(text.contains(rule), "{rule}: {text}");
|
||||
}
|
||||
assert!(!text.contains("continues the one before"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue