use std::path::PathBuf;
use oneagentgraph::note::Accepted;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub use oneagentgraph::note::{
Addressee, Criterion, Note, NoteRefused, NoteText, Party, Undelivered,
};
use crate::channel::{Author, Command, Deliver, Reply, REPLY_ENVELOPE_VERSION};
use crate::error::{Error, Result};
use crate::event::Envelope;
use crate::views::RunPaths;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "reached", rename_all = "kebab-case")]
pub enum Reached {
Queued,
Worker,
Supervisor,
JudgedWith {
completion_reason: String,
},
Carried,
}
impl Reached {
#[must_use]
pub fn a_conversation_read_it(&self) -> bool {
!matches!(self, Self::Carried)
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Queued => "queued",
Self::Worker => "worker",
Self::Supervisor => "supervisor",
Self::JudgedWith { .. } => "judged-with",
Self::Carried => "carried",
}
}
#[must_use]
pub fn shown_at_delivery(&self) -> &'static [Party] {
match self {
Self::Supervisor | Self::JudgedWith { .. } => &[Party::Supervisor],
Self::Queued | Self::Worker | Self::Carried => &[],
}
}
#[must_use]
pub fn routed_to(&self) -> &'static [Party] {
match self {
Self::Worker | Self::Queued => &[Party::Worker, Party::Supervisor],
Self::Supervisor => &[Party::Worker],
Self::JudgedWith { .. } | Self::Carried => &[],
}
}
}
impl From<&Accepted> for Reached {
fn from(accepted: &Accepted) -> Self {
match accepted {
Accepted::Queued => Self::Queued,
Accepted::Interrupted {
party: Party::Worker,
} => Self::Worker,
Accepted::Interrupted {
party: Party::Supervisor,
} => Self::Supervisor,
Accepted::JudgedWith { completion_reason } => Self::JudgedWith {
completion_reason: completion_reason.clone(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Delivered {
To(Reached),
Queued,
}
pub fn deliver(run: &RunPaths, node: &str, note: &Note) -> Result<Delivered> {
deliver_with(run, node, note, Deliver::Live, true)
}
pub fn deliver_with(
run: &RunPaths,
node: &str,
note: &Note,
deliver: Deliver,
persist: bool,
) -> Result<Delivered> {
let envelope = Reply {
version: Some(REPLY_ENVELOPE_VERSION),
author: Author::planner(),
commands: vec![Command::Note {
id: node.to_string(),
addressee: note.addressee,
text: note.text.clone(),
criterion: note.criterion.clone(),
deliver,
persist,
}],
..Reply::default()
};
crate::driver::deliver_note_envelope(run, &envelope)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Reach {
LiveOnly,
LiveThenNext,
NextOnly,
}
impl Reach {
pub(crate) fn of(node: &str, deliver: Deliver, persist: bool) -> Result<Self> {
match (deliver, persist) {
(Deliver::Live, false) => Ok(Self::LiveOnly),
(Deliver::Live, true) => Ok(Self::LiveThenNext),
(Deliver::Next, true) => Ok(Self::NextOnly),
(Deliver::Next, false) => Err(reaches_nobody(
node,
"`deliver: next` attempts no live delivery and `persist: false` composes it \
into no dispatch, so this note reaches nobody whatever the run does",
)),
}
}
pub(crate) fn attempts_a_live_turn(self) -> bool {
!matches!(self, Self::NextOnly)
}
pub(crate) fn composes_forward(self) -> bool {
!matches!(self, Self::LiveOnly)
}
}
pub(crate) fn reaches_nobody(node: &str, why: &str) -> Error {
Error::Refused(format!("note: node '{node}': {why}"))
}
pub(crate) fn of(
addressee: Addressee,
text: &NoteText,
criterion: Option<&Criterion>,
) -> std::result::Result<Note, NoteRefused> {
let note = Note::new(addressee, text.as_str())?;
match criterion {
None => Ok(note),
Some(criterion) => note.binding(criterion.as_str()),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct RecordedNote {
pub addressee: Addressee,
pub text: NoteText,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub criterion: Option<Criterion>,
#[serde(flatten)]
pub reached: Reached,
}
impl RecordedNote {
pub(crate) fn of_delivery(operation: &crate::edits::Operation) -> Option<Self> {
let crate::edits::Operation::NoteDelivered {
addressee,
text,
criterion,
reached,
..
} = operation
else {
return None;
};
Some(Self {
addressee: *addressee,
text: text.clone(),
criterion: criterion.clone(),
reached: reached.clone(),
})
}
}
pub(crate) const CARRIED_KEY: &str = "notes_carried";
pub(crate) const SPENT_KEY: &str = "notes_spent";
pub(crate) fn payload_of(notes: &[RecordedNote]) -> Value {
serde_json::to_value(notes).unwrap_or_else(|_| Value::Array(Vec::new()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Placement {
ComposedIntoIt,
DeliveredSince,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Held {
pub note: RecordedNote,
pub placement: Placement,
}
impl Held {
fn read(&self) -> bool {
match self.placement {
Placement::ComposedIntoIt => true,
Placement::DeliveredSince => self.note.reached.a_conversation_read_it(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct Standing {
pub held: Vec<Held>,
}
impl Standing {
pub(crate) fn notes(&self) -> Vec<RecordedNote> {
self.held.iter().map(|held| held.note.clone()).collect()
}
pub(crate) fn carried(&self) -> Vec<RecordedNote> {
self.held
.iter()
.filter(|held| !held.read())
.map(|held| held.note.clone())
.collect()
}
pub(crate) fn read(&self) -> Vec<RecordedNote> {
self.held
.iter()
.filter(|held| held.read())
.map(|held| held.note.clone())
.collect()
}
}
pub(crate) fn standing(journal: &[Envelope], node: &str) -> Result<Standing> {
let mut standing = Standing::default();
for envelope in journal {
if envelope.kind.0 == crate::event::PipelineKind::NodeDispatched.as_str() {
if envelope.labels.node.as_deref() != Some(node) {
continue;
}
let composed: Vec<RecordedNote> = match envelope.payload.get(CARRIED_KEY) {
None => Vec::new(),
Some(carried) => serde_json::from_value(carried.clone())
.map_err(|error| unreadable_record(envelope, CARRIED_KEY, &error))?,
};
standing.held = composed
.into_iter()
.map(|note| Held {
note,
placement: Placement::ComposedIntoIt,
})
.collect();
continue;
}
let is_a_commit = [
crate::event::PipelineKind::EditCommitted,
crate::event::PipelineKind::CommandAccepted,
]
.iter()
.any(|kind| envelope.kind.0 == kind.as_str());
if !is_a_commit || envelope.source != crate::event::Source::Pipeline {
continue;
}
let operations: Vec<crate::edits::Operation> = serde_json::from_value(
envelope
.payload
.get("operations")
.cloned()
.unwrap_or(Value::Null),
)
.map_err(|error| unreadable_record(envelope, "operations", &error))?;
for operation in operations {
let crate::edits::Operation::NoteDelivered { node: whose, .. } = &operation else {
continue;
};
if whose == node {
if let Some(note) = RecordedNote::of_delivery(&operation) {
standing.held.push(Held {
note,
placement: Placement::DeliveredSince,
});
}
}
}
}
Ok(standing)
}
fn unreadable_record(envelope: &Envelope, field: &str, error: &serde_json::Error) -> Error {
Error::Invalid(format!(
"the run's record of the notes delivered to its nodes cannot be read: `{}` record \
{}/{} carries a `{field}` this build cannot read ({error}), so which notes a \
dispatch is composed with cannot be decided from it",
envelope.kind.0, envelope.stream, envelope.seq
))
}
pub(crate) fn standing_for(paths: &RunPaths, node: &str) -> Result<Standing> {
standing(&crate::journal::read(&paths.journal()), node)
}
pub(crate) const CARRY_DIR: &str = "notes";
pub(crate) fn carry_store(paths: &RunPaths, node: &str) -> PathBuf {
let name: String = node
.bytes()
.map(|byte| match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' | b'-' => char::from(byte).to_string(),
other => format!("%{other:02X}"),
})
.collect();
paths
.dir
.join(CARRY_DIR)
.join(format!("{name}.carried.jsonl"))
}
pub(crate) fn carry(
paths: &RunPaths,
node: &str,
addressee: Addressee,
text: &NoteText,
criterion: Option<&Criterion>,
) -> Result<()> {
let store = carry_store(paths, node);
let dir = paths.dir.join(CARRY_DIR);
std::fs::create_dir_all(&dir).map_err(|source| Error::Ledger { path: dir, source })?;
of(addressee, text, criterion)
.map_err(|why| why.to_string())
.and_then(|note| {
onemessagebus::Carry::sender::<Note, Accepted>(&store)
.send(note)
.map(drop)
.map_err(|why| why.to_string())
})
.map_err(|why| {
Error::Invalid(format!(
"the note for node '{node}' could not be carried to its next dispatch in {}: {why}",
store.display()
))
})
}
pub(crate) fn drain_carried(paths: &RunPaths, node: &str, standing: Standing) -> Result<Standing> {
let store = carry_store(paths, node);
let inbox = onemessagebus::Inbox::<Note, Accepted>::new();
inbox.adopt_carried(&store).map_err(|why| {
Error::Invalid(format!(
"the notes carried to node '{node}' cannot be read from {}: {why}",
store.display()
))
})?;
while inbox.take().is_some() {}
Ok(standing)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WorkerThenJudge {
AwaitingWorker,
AwaitingJudge { worker_turn: u64 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Routing {
ReopenedWorkerTurn(WorkerThenJudge),
ComposedIntoTheTask(WorkerThenJudge),
RidesTheDecision,
NextTurnToOpen {
worker_shown: bool,
judge_shown: bool,
},
PresentedOutsideTheSeam(WorkerThenJudge),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) enum Evidence {
DeliveredOrigin,
OpeningTask,
InstructionText,
AnsweringTurn,
}
impl Evidence {
fn party(self) -> Party {
match self {
Self::DeliveredOrigin | Self::OpeningTask | Self::InstructionText => Party::Worker,
Self::AnsweringTurn => Party::Supervisor,
}
}
}
impl Routing {
fn of(reached: &Reached) -> Option<Self> {
match reached {
Reached::Worker => Some(Self::ReopenedWorkerTurn(WorkerThenJudge::AwaitingWorker)),
Reached::Supervisor => Some(Self::RidesTheDecision),
Reached::Queued => Some(Self::NextTurnToOpen {
worker_shown: false,
judge_shown: false,
}),
Reached::Carried => Some(Self::PresentedOutsideTheSeam(
WorkerThenJudge::AwaitingWorker,
)),
Reached::JudgedWith { .. } => None,
}
}
fn presented_by(
&mut self,
party: Party,
opened: &oneagentgraph::event::TurnStarted,
text: &str,
) -> Option<Evidence> {
let turn = opened.turn;
let carries_this_note = opened.instruction.contains(text);
let delivered =
opened.origin == Some(oneagentgraph::event::Origin::Delivered) && carries_this_note;
let shown_to_both = Self::NextTurnToOpen {
worker_shown: true,
judge_shown: true,
};
match (party, *self) {
(Party::Worker, Self::ReopenedWorkerTurn(WorkerThenJudge::AwaitingWorker))
if delivered =>
{
*self =
Self::ReopenedWorkerTurn(WorkerThenJudge::AwaitingJudge { worker_turn: turn });
Some(Evidence::DeliveredOrigin)
}
(Party::Worker, Self::ComposedIntoTheTask(WorkerThenJudge::AwaitingWorker)) => {
*self =
Self::ComposedIntoTheTask(WorkerThenJudge::AwaitingJudge { worker_turn: turn });
Some(Evidence::OpeningTask)
}
(Party::Worker, Self::PresentedOutsideTheSeam(WorkerThenJudge::AwaitingWorker))
if carries_this_note =>
{
*self = Self::PresentedOutsideTheSeam(WorkerThenJudge::AwaitingJudge {
worker_turn: turn,
});
Some(Evidence::InstructionText)
}
(
Party::Supervisor,
Self::ReopenedWorkerTurn(WorkerThenJudge::AwaitingJudge { worker_turn })
| Self::ComposedIntoTheTask(WorkerThenJudge::AwaitingJudge { worker_turn })
| Self::PresentedOutsideTheSeam(WorkerThenJudge::AwaitingJudge { worker_turn }),
) if turn >= worker_turn => {
*self = shown_to_both;
Some(Evidence::AnsweringTurn)
}
(Party::Worker, Self::RidesTheDecision) if delivered => {
*self = shown_to_both;
Some(Evidence::DeliveredOrigin)
}
(
Party::Worker,
Self::NextTurnToOpen {
worker_shown: false,
judge_shown,
},
) if delivered => {
*self = Self::NextTurnToOpen {
worker_shown: true,
judge_shown,
};
Some(Evidence::DeliveredOrigin)
}
(
Party::Supervisor,
Self::NextTurnToOpen {
worker_shown,
judge_shown: false,
},
) => {
*self = Self::NextTurnToOpen {
worker_shown,
judge_shown: true,
};
Some(Evidence::AnsweringTurn)
}
_ => None,
}
}
fn settled(self) -> bool {
matches!(
self,
Self::NextTurnToOpen {
worker_shown: true,
judge_shown: true,
}
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Routed {
note: RecordedNote,
routing: Routing, routed_at: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Shown {
pub turn: u64,
pub evidence: Evidence,
pub note: RecordedNote,
}
impl Shown {
pub(crate) fn payload(&self) -> serde_json::Map<String, Value> {
let mut payload = match serde_json::to_value(&self.note) {
Ok(Value::Object(note)) => note,
_ => serde_json::Map::new(),
};
payload.insert(
"party".into(),
serde_json::to_value(self.evidence.party()).unwrap_or(Value::Null),
);
payload.insert("turn".into(), Value::from(self.turn));
payload.insert(
"evidence".into(),
serde_json::to_value(self.evidence).unwrap_or(Value::Null),
);
payload
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct Presentations {
routed: Vec<Routed>,
}
impl Presentations {
pub(crate) fn delivered_while_live(&mut self, note: RecordedNote, at: u64) {
let Some(routing) = Routing::of(¬e.reached) else {
return;
};
self.routed.push(Routed {
note,
routing,
routed_at: at,
});
}
pub(crate) fn composed_into_the_task(&mut self, note: RecordedNote, at: u64) {
self.routed.push(Routed {
note,
routing: Routing::ComposedIntoTheTask(WorkerThenJudge::AwaitingWorker),
routed_at: at,
});
}
pub(crate) fn observe(&mut self, envelope: &Envelope) -> Vec<Shown> {
if self.routed.is_empty()
|| envelope.source != crate::event::Source::Agentgraph
|| envelope.kind.0 != oneagentgraph::event::EventKind::TurnStarted.as_str()
{
return Vec::new();
}
let Ok(opened) = serde_json::from_value::<oneagentgraph::event::TurnStarted>(
Value::Object(envelope.payload.clone()),
) else {
return Vec::new();
};
let Some(started_at) = crate::projection::millis_of(&opened.started_at) else {
return Vec::new();
};
let party = if opened.role == oneagentgraph::event::Party::Assistant.as_str() {
Party::Worker
} else if opened.role == oneagentgraph::event::Party::User.as_str() {
Party::Supervisor
} else {
return Vec::new();
};
let mut shown = Vec::new();
for routed in &mut self.routed {
if started_at < routed.routed_at {
continue;
}
let Some(evidence) =
routed
.routing
.presented_by(party, &opened, routed.note.text.as_str())
else {
continue;
};
shown.push(Shown {
turn: opened.turn,
evidence,
note: routed.note.clone(),
});
}
self.routed.retain(|routed| !routed.routing.settled());
shown
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::edits::Operation;
use crate::event::{Labels, Source, ENVELOPE_VERSION};
use crate::journal::{self, labels, payload};
use serde_json::json;
#[test]
fn a_carried_note_waits_in_its_nodes_store_and_one_dispatch_takes_it() {
let root =
std::env::temp_dir().join(format!("onepipeline-note-carry-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&root);
let paths = RunPaths::under(&root, "demo");
paths.create().expect("the run directory");
let texts = |standing: &Standing| -> Vec<String> {
standing
.carried()
.iter()
.map(|note| note.text.as_str().to_owned())
.collect()
};
let stored = |node: &str| {
onemessagebus::Carry::read(&carry_store(&paths, node)).map(|entries| entries.len())
};
let text = |said: &str| -> NoteText { said.parse().expect("a readable note") };
carry(&paths, "later", Addressee::Worker, &text("first"), None).expect("carried");
carry(&paths, "later", Addressee::Worker, &text("second"), None).expect("carried");
carry(
&paths,
"a/../b",
Addressee::Worker,
&text("elsewhere"),
None,
)
.expect("carried");
assert_eq!(stored("later").expect("a store"), 2);
assert_eq!(
carry_store(&paths, "a/../b")
.file_name()
.and_then(|name| name.to_str()),
Some("a%2F%2E%2E%2Fb.carried.jsonl"),
"a node id that navigates was not kept to one segment"
);
let journal = vec![
delivered(1, "later", "first", Reached::Carried),
delivered(2, "later", "second", Reached::Carried),
];
let owed = standing(&journal, "later").expect("the fold reads");
let adopted = drain_carried(&paths, "later", owed.clone()).expect("the store reads");
assert_eq!(texts(&adopted), vec!["first", "second"]);
assert_eq!(
adopted, owed,
"the store decided what a dispatch is composed with"
);
assert_eq!(
stored("later").expect("a store"),
0,
"the store was not drained"
);
assert_eq!(
stored("a/../b").expect("a store"),
1,
"another node's store was drained"
);
let again = drain_carried(&paths, "later", owed).expect("an empty store reads");
assert_eq!(texts(&again), vec!["first", "second"]);
let none = drain_carried(&paths, "never", Standing::default()).expect("no store");
assert!(none.held.is_empty());
std::fs::write(carry_store(&paths, "later"), "not a carry store\n").expect("written");
let refused = drain_carried(&paths, "later", Standing::default())
.expect_err("a store this build cannot read is refused");
assert!(
refused.to_string().contains("node 'later'")
&& refused.to_string().contains("later.carried.jsonl"),
"{refused}"
);
std::fs::create_dir_all(carry_store(&paths, "blocked"))
.expect("a directory where the store would go");
let refused = carry(&paths, "blocked", Addressee::Worker, &text("held"), None)
.expect_err("a store that is a directory is refused");
assert!(
refused.to_string().contains("node 'blocked'")
&& refused.to_string().contains("blocked.carried.jsonl"),
"{refused}"
);
let filed = RunPaths::under(&root, "filed");
filed.create().expect("the run directory");
std::fs::write(filed.dir.join(CARRY_DIR), "not a directory").expect("written");
let refused = carry(&filed, "later", Addressee::Worker, &text("held"), None)
.expect_err("a notes directory that cannot be made is refused");
assert!(
matches!(&refused, Error::Ledger { path, .. } if path == &filed.dir.join(CARRY_DIR)),
"{refused}"
);
let _ = std::fs::remove_dir_all(&root);
}
fn pipeline(
kind: journal::PipelineKind,
seq: u64,
node: Option<&str>,
fields: &[(&str, Value)],
) -> Envelope {
Envelope {
v: ENVELOPE_VERSION,
ts: crate::sys::rfc3339_from_millis(1_786_000_000_000 + seq * 1_000),
stream: "s".into(),
seq,
source: Source::Pipeline,
kind: kind.into(),
dimensions: Default::default(),
labels: Labels {
node: node.map(str::to_string),
..labels("demo", None)
},
payload: payload(fields),
artifacts: Vec::new(),
}
}
fn delivered(seq: u64, node: &str, text: &str, reached: Reached) -> Envelope {
pipeline(
journal::PipelineKind::EditCommitted,
seq,
None,
&[(
"operations",
json!([Operation::NoteDelivered {
node: node.into(),
addressee: Addressee::Worker,
text: text.parse().expect("a usable note"),
criterion: None,
shown_to: reached.shown_at_delivery().to_vec(),
routed_to: reached.routed_to().to_vec(),
reached,
}]),
)],
)
}
fn texts(notes: &[RecordedNote]) -> Vec<&str> {
notes.iter().map(|note| note.text.as_str()).collect()
}
#[test]
fn what_stands_for_a_node_starts_at_its_last_dispatch_and_reads_forward() {
let journal = vec![
pipeline(
journal::PipelineKind::NodeDispatched,
1,
Some("build"),
&[("attempt", json!(1))],
),
delivered(2, "build", "first ruling", Reached::Worker),
delivered(3, "other", "not yours", Reached::Worker),
delivered(4, "build", "landed nowhere", Reached::Carried),
];
let before = standing(&journal, "build").expect("the record reads");
assert_eq!(texts(&before.notes()), ["first ruling", "landed nowhere"]);
assert_eq!(texts(&before.read()), ["first ruling"]);
let composed = payload_of(&before.notes());
assert_eq!(composed[1]["reached"], json!("carried"));
assert!(
composed[0].get("shown_to").is_none(),
"a dispatch's record claimed a presentation it has not made: {composed}"
);
let mut continued = journal.clone();
continued.push(pipeline(
journal::PipelineKind::NodeDispatched,
5,
Some("build"),
&[("attempt", json!(2)), (CARRIED_KEY, composed)],
));
continued.push(delivered(6, "build", "second ruling", Reached::Supervisor));
let after = standing(&continued, "build").expect("the record reads");
assert_eq!(
texts(&after.notes()),
["first ruling", "landed nowhere", "second ruling"]
);
assert_eq!(
texts(&after.read()),
["first ruling", "landed nowhere", "second ruling"]
);
let mut fresh = continued.clone();
fresh.push(pipeline(
journal::PipelineKind::NodeDispatched,
7,
Some("build"),
&[("attempt", json!(1))],
));
assert_eq!(
standing(&fresh, "build").expect("the record reads"),
Standing::default()
);
}
#[test]
fn a_record_the_fold_cannot_read_is_refused_by_name_rather_than_read_past() {
let carried_wrong = vec![pipeline(
journal::PipelineKind::NodeDispatched,
1,
Some("build"),
&[("attempt", json!(2)), (CARRIED_KEY, json!("a ruling"))],
)];
let refused =
standing(&carried_wrong, "build").expect_err("a string is not a list of notes");
let said = refused.to_string();
assert!(
said.contains("node-dispatched") && said.contains("s/1") && said.contains(CARRIED_KEY),
"the refusal does not name the record or the field: {said}"
);
let operations_wrong = vec![pipeline(
journal::PipelineKind::EditCommitted,
2,
None,
&[("operations", json!([{"kind": "from-the-future"}]))],
)];
let refused = standing(&operations_wrong, "build")
.expect_err("an operation this build does not know is not read past");
assert!(
refused.to_string().contains("`operations`"),
"the refusal does not name the field: {refused}"
);
let mut foreign = pipeline(
journal::PipelineKind::EditCommitted,
3,
None,
&[("operations", json!("not ours"))],
);
foreign.source = Source::Agentgraph;
assert_eq!(
standing(&[foreign], "build").expect("a sibling's record is passed over"),
Standing::default()
);
}
#[test]
fn each_disposition_tells_a_confirmed_presentation_from_a_routed_one() {
assert!(Reached::Worker.shown_at_delivery().is_empty());
assert_eq!(
Reached::Worker.routed_to(),
[Party::Worker, Party::Supervisor]
);
assert_eq!(Reached::Supervisor.shown_at_delivery(), [Party::Supervisor]);
assert_eq!(Reached::Supervisor.routed_to(), [Party::Worker]);
let judged = Reached::JudgedWith {
completion_reason: "done".into(),
};
assert_eq!(judged.shown_at_delivery(), [Party::Supervisor]);
assert!(judged.routed_to().is_empty());
assert!(Reached::Queued.shown_at_delivery().is_empty());
assert_eq!(
Reached::Queued.routed_to(),
[Party::Worker, Party::Supervisor]
);
assert!(Reached::Carried.shown_at_delivery().is_empty());
assert!(Reached::Carried.routed_to().is_empty());
let record = |reached: Reached| Operation::NoteDelivered {
node: "build".into(),
addressee: Addressee::Both,
text: "ship it".parse().expect("a usable note"),
criterion: None,
shown_to: reached.shown_at_delivery().to_vec(),
routed_to: reached.routed_to().to_vec(),
reached,
};
let wire = serde_json::to_value(record(Reached::Carried)).expect("it serializes");
assert!(
wire.get("shown_to").is_none() && wire.get("routed_to").is_none(),
"{wire}"
);
let wire = serde_json::to_value(record(Reached::Worker)).expect("it serializes");
assert!(wire.get("shown_to").is_none(), "{wire}");
assert_eq!(wire["routed_to"], json!(["worker", "supervisor"]), "{wire}");
let wire = serde_json::to_value(record(Reached::Supervisor)).expect("it serializes");
assert_eq!(wire["shown_to"], json!(["supervisor"]), "{wire}");
assert_eq!(wire["routed_to"], json!(["worker"]), "{wire}");
assert_eq!(
serde_json::from_value::<Operation>(wire).expect("it reads back"),
record(Reached::Supervisor)
);
}
fn turn(seq: u64, at: u64, role: &str, turn: u64, origin: Option<&str>) -> Envelope {
turn_on(seq, at, role, turn, origin, "do it")
}
fn turn_on(
seq: u64,
at: u64,
role: &str,
turn: u64,
origin: Option<&str>,
instruction: &str,
) -> Envelope {
let mut payload = payload(&[
("turn", json!(turn)),
("role", json!(role)),
("instruction", json!(instruction)),
("started_at", json!(crate::sys::rfc3339_from_millis(at))),
]);
if let Some(origin) = origin {
payload.insert("origin".into(), json!(origin));
}
Envelope {
v: 1,
ts: crate::sys::rfc3339_from_millis(at),
stream: "node-scope-1".into(),
seq,
source: Source::Agentgraph,
kind: crate::event::EventKind(
oneagentgraph::event::EventKind::TurnStarted.as_str().into(),
),
dimensions: Default::default(),
labels: labels("demo", Some("build")),
payload,
artifacts: Vec::new(),
}
}
fn recorded(text: &str, reached: Reached) -> RecordedNote {
RecordedNote {
addressee: Addressee::Both,
text: text.parse().expect("a usable note"),
criterion: None,
reached,
}
}
#[test]
fn a_presentation_is_recorded_when_the_stream_shows_it_and_in_the_producers_order() {
let mut watch = Presentations::default();
watch.delivered_while_live(recorded("stop", Reached::Worker), 1_000);
assert!(watch.observe(&turn(1, 900, "user", 1, None)).is_empty());
assert!(watch
.observe(&turn(2, 950, "assistant", 1, Some("task")))
.is_empty());
assert!(watch
.observe(&turn(3, 1_100, "assistant", 2, Some("supervisor")))
.is_empty());
assert!(watch.observe(&turn(4, 1_200, "user", 2, None)).is_empty());
assert!(watch
.observe(&turn_on(
5,
1_250,
"assistant",
3,
Some("delivered"),
"## Notes delivered to you during this run\n\n- carry on"
))
.is_empty());
let shown = watch.observe(&turn_on(
5,
1_300,
"assistant",
3,
Some("delivered"),
"## Notes delivered to you during this run\n\n- stop",
));
assert_eq!(shown.len(), 1, "{shown:?}");
assert_eq!(shown[0].evidence.party(), Party::Worker);
assert_eq!(shown[0].turn, 3);
assert_eq!(shown[0].evidence, Evidence::DeliveredOrigin);
assert_eq!(shown[0].payload()["party"], json!("worker"));
assert_eq!(shown[0].payload()["evidence"], json!("delivered-origin"));
assert_eq!(shown[0].payload()["text"], json!("stop"));
assert_eq!(shown[0].payload()["reached"], json!("worker"));
let shown = watch.observe(&turn(6, 1_400, "user", 3, None));
assert_eq!(shown.len(), 1, "{shown:?}");
assert_eq!(shown[0].evidence.party(), Party::Supervisor);
assert!(watch.observe(&turn(7, 1_500, "user", 4, None)).is_empty());
let mut watch = Presentations::default();
watch.delivered_while_live(recorded("ruling", Reached::Supervisor), 2_000);
assert!(watch.observe(&turn(8, 2_100, "user", 5, None)).is_empty());
let shown = watch.observe(&turn_on(
9,
2_200,
"assistant",
6,
Some("delivered"),
"the supervisor was told: ruling",
));
assert_eq!(shown.len(), 1);
assert_eq!(shown[0].evidence.party(), Party::Worker);
let mut watch = Presentations::default();
watch.composed_into_the_task(recorded("carried in", Reached::Carried), 3_000);
let shown = watch.observe(&turn(10, 3_100, "assistant", 1, Some("task")));
assert_eq!(shown.len(), 1);
assert_eq!(shown[0].evidence.party(), Party::Worker);
assert_eq!(shown[0].evidence, Evidence::OpeningTask);
let shown = watch.observe(&turn(11, 3_200, "user", 1, None));
assert_eq!(shown.len(), 1);
assert_eq!(shown[0].evidence.party(), Party::Supervisor);
let mut watch = Presentations::default();
watch.delivered_while_live(
recorded("stop re-running the tier", Reached::Carried),
5_000,
);
let mut cut = turn_on(
13,
5_100,
"assistant",
2,
Some("supervisor"),
"The manager says: stop re-running",
);
cut.payload
.insert("instruction_truncated".into(), json!(true));
assert!(
watch.observe(&cut).is_empty(),
"an instruction cut short of the text was read as carrying it"
);
let mut other = turn(14, 5_200, "assistant", 3, Some("supervisor"));
other
.payload
.insert("instruction".into(), json!("carry on as you were"));
assert!(watch.observe(&other).is_empty());
let mut read_aloud = turn(15, 5_300, "assistant", 4, Some("supervisor"));
read_aloud.payload.insert(
"instruction".into(),
json!("The manager says: stop re-running the tier. Do that."),
);
let shown = watch.observe(&read_aloud);
assert_eq!(shown.len(), 1, "{shown:?}");
assert_eq!(shown[0].evidence.party(), Party::Worker);
assert_eq!(shown[0].evidence, Evidence::InstructionText);
assert_eq!(shown[0].payload()["evidence"], json!("instruction-text"));
let shown = watch.observe(&turn(16, 5_400, "user", 4, None));
assert_eq!(shown.len(), 1, "{shown:?}");
assert_eq!(shown[0].evidence.party(), Party::Supervisor);
assert_eq!(shown[0].evidence, Evidence::AnsweringTurn);
let mut watch = Presentations::default();
watch.delivered_while_live(recorded("first ruling", Reached::Worker), 6_000);
watch.delivered_while_live(recorded("second ruling", Reached::Worker), 6_000);
let shown = watch.observe(&turn_on(
17,
6_100,
"assistant",
2,
Some("delivered"),
"## Notes delivered to you during this run\n\n- first ruling",
));
assert_eq!(
shown
.iter()
.map(|shown| shown.note.text.as_str())
.collect::<Vec<_>>(),
["first ruling"],
"{shown:?}"
);
let shown = watch.observe(&turn_on(
18,
6_200,
"assistant",
3,
Some("delivered"),
"## Notes delivered to you during this run\n\n- second ruling",
));
assert_eq!(
shown
.iter()
.map(|shown| (shown.note.text.as_str(), shown.turn))
.collect::<Vec<_>>(),
[("second ruling", 3)],
"{shown:?}"
);
let shown = watch.observe(&turn(19, 6_300, "user", 3, None));
assert_eq!(
shown.len(),
2,
"both were carried by turns the judge's answer follows: {shown:?}"
);
let mut watch = Presentations::default();
watch.delivered_while_live(recorded("queued ruling", Reached::Queued), 7_000);
assert!(watch.observe(&turn(20, 6_900, "user", 1, None)).is_empty());
assert!(watch
.observe(&turn(21, 7_050, "assistant", 2, Some("supervisor")))
.is_empty());
let shown = watch.observe(&turn_on(
22,
7_100,
"assistant",
3,
Some("delivered"),
"## Notes delivered to you during this run\n\n- queued ruling",
));
assert_eq!(shown.len(), 1, "{shown:?}");
assert_eq!(shown[0].evidence.party(), Party::Worker);
assert_eq!(shown[0].evidence, Evidence::DeliveredOrigin);
assert_eq!(shown[0].payload()["reached"], json!("queued"));
let shown = watch.observe(&turn(23, 7_200, "user", 3, None));
assert_eq!(shown.len(), 1, "{shown:?}");
assert_eq!(shown[0].evidence.party(), Party::Supervisor);
assert_eq!(shown[0].evidence, Evidence::AnsweringTurn);
assert!(watch.observe(&turn(24, 7_300, "user", 4, None)).is_empty());
let mut watch = Presentations::default();
watch.delivered_while_live(recorded("queued ruling", Reached::Queued), 8_000);
let shown = watch.observe(&turn(25, 8_100, "user", 5, None));
assert_eq!(shown.len(), 1, "{shown:?}");
assert_eq!(shown[0].evidence.party(), Party::Supervisor);
let shown = watch.observe(&turn_on(
26,
8_200,
"assistant",
6,
Some("delivered"),
"## Notes delivered to you during this run\n\n- queued ruling",
));
assert_eq!(shown.len(), 1, "{shown:?}");
assert_eq!(shown[0].evidence.party(), Party::Worker);
assert!(watch
.observe(&turn_on(
27,
8_300,
"assistant",
7,
Some("delivered"),
"## Notes delivered to you during this run\n\n- queued ruling",
))
.is_empty());
let mut watch = Presentations::default();
watch.delivered_while_live(
recorded(
"passed",
Reached::JudgedWith {
completion_reason: "done".into(),
},
),
4_000,
);
assert!(watch
.observe(&turn(12, 4_100, "assistant", 7, Some("delivered")))
.is_empty());
}
#[test]
fn the_names_this_module_writes_are_the_ones_the_divergence_record_states() {
let record = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("docs/contract-divergences.md"),
)
.expect("the divergence record ships");
let entry = record
.split("\n## ")
.find(|entry| entry.starts_with("70."))
.expect("the record carries entry 70");
let block: Value = entry
.split("```json")
.nth(1)
.and_then(|rest| rest.split("```").next())
.map(|block| serde_json::from_str(block).expect("entry 70's block is JSON"))
.expect("entry 70 carries the json block this test drives");
assert_eq!(
block["node_dispatched_keys"],
json!([CARRIED_KEY, SPENT_KEY]),
"the keys a `node-dispatched` carries are not the ones entry 70 names"
);
assert_eq!(
block["heading"],
json!(crate::plan::MANAGER_NOTES_HEADING),
"the heading a re-dispatch renders the notes under is not the one entry 70 names"
);
let written = serde_json::to_value(Operation::NoteDelivered {
node: "build".into(),
addressee: Addressee::Worker,
text: "ship it".parse().expect("a usable note"),
criterion: None,
shown_to: Reached::Supervisor.shown_at_delivery().to_vec(),
routed_to: Reached::Supervisor.routed_to().to_vec(),
reached: Reached::Supervisor,
})
.expect("it serializes");
let fields: Vec<String> = serde_json::from_value(block["note_delivered_fields"].clone())
.expect("entry 70 names the fields");
for field in &fields {
assert!(
written.get(field).is_some(),
"a delivery is not stamped under the field entry 70 names (`{field}`): {written}"
);
}
assert_eq!(
block["event_kinds"],
json!([crate::event::PipelineKind::NoteShown.as_str()]),
"the kind a presentation is recorded under is not the one entry 70 names"
);
let every = |evidence: Evidence| match evidence {
Evidence::DeliveredOrigin
| Evidence::OpeningTask
| Evidence::InstructionText
| Evidence::AnsweringTurn => serde_json::to_value(evidence).expect("it serializes"),
};
assert_eq!(
block["note_shown_evidence"],
json!([
every(Evidence::DeliveredOrigin),
every(Evidence::OpeningTask),
every(Evidence::InstructionText),
every(Evidence::AnsweringTurn),
]),
"the evidence a `note-shown` can name is not what entry 70 states"
);
}
#[test]
fn a_recorded_note_is_the_deliverys_own_fields_less_the_node_and_the_presentations() {
let delivery = Operation::NoteDelivered {
node: "build".into(),
addressee: Addressee::Both,
text: "ship it".parse().expect("a usable note"),
criterion: Some(
"`version.txt` holds `v: 2`"
.parse()
.expect("a usable criterion"),
),
shown_to: Reached::Supervisor.shown_at_delivery().to_vec(),
routed_to: Reached::Supervisor.routed_to().to_vec(),
reached: Reached::Supervisor,
};
let note = RecordedNote::of_delivery(&delivery).expect("a delivery carries a note");
let mut written = serde_json::to_value(&delivery).expect("it serializes");
let written = written.as_object_mut().expect("an object");
for not_the_notes_own in ["kind", "node", "shown_to", "routed_to"] {
assert!(
written.remove(not_the_notes_own).is_some(),
"the delivery no longer writes `{not_the_notes_own}`; this gate is stale"
);
}
assert_eq!(
serde_json::to_value(¬e).expect("it serializes"),
Value::Object(written.clone()),
"a recorded note and the delivery it came from no longer share the note's fields"
);
assert!(RecordedNote::of_delivery(&Operation::HumanAttested {
node: "approve".into()
})
.is_none());
}
}