use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use onemessagebus::{
Allowlist, Asker, Author, ConsumerName, Correlation, Layout, Message, OpWord, Operation,
Policy, Position, Predicate, Pushed, Queue, QueueError, QueueName, QueueSpec, Registry, Router,
SchemaId, Supersede, Transport,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
pub const PLANNER_CHANNEL: &str = "planner-channel";
pub const SURFACES: &str = "surfaces";
pub const REPLIES: &str = "replies";
pub const COMMANDS: &str = "commands";
pub const COMMAND_OUTCOMES: &str = "command-outcomes";
pub const PROJECTION: &str = "queue.json";
pub const ASKER_ENV: &str = "ONEPIPELINE_CHANNEL_ASKER";
pub const REPLY_ENVELOPE_VERSION: u32 = 3;
pub const REPLY_ENVELOPE_VERSIONS_READ: &[u32] = &[REPLY_ENVELOPE_VERSION, 2];
pub const SURFACE_SCHEMA: SchemaId = SchemaId::literal("agent", "planner-surface", 1);
pub const QUEUED_REPLY_SCHEMA: SchemaId = SchemaId::literal("agent", "queued-reply", 1);
pub const QUEUED_COMMANDS_SCHEMA: SchemaId = SchemaId::literal("agent", "queued-commands", 1);
pub const COMMAND_OUTCOME_SCHEMA: SchemaId = SchemaId::literal("agent", "command-outcome", 1);
pub mod source {
pub const CHECK_IN: &str = "check-in";
pub const PROPOSAL: &str = "proposal";
pub const RECONCILER: &str = "reconciler";
pub const MONITOR: &str = "monitor";
}
fn is_false(value: &bool) -> bool {
!*value
}
fn recorded_asker<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Asker>, D::Error> {
Ok(Option::<String>::deserialize(deserializer)?
.and_then(|name| Asker::new(&name, "the recorded asker").ok()))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Surface {
pub id: u64,
pub kind: String,
pub message: String,
pub source: String,
pub blocking: bool,
pub queued_at: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workstream: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub abandoned: bool,
#[serde(
default,
deserialize_with = "recorded_asker",
skip_serializing_if = "Option::is_none"
)]
pub asker: Option<Asker>,
#[serde(
default,
deserialize_with = "recorded_correlation",
skip_serializing_if = "Option::is_none"
)]
pub correlation: Option<Correlation>,
}
fn recorded_correlation<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Correlation>, D::Error> {
Ok(Option::<String>::deserialize(deserializer)?.and_then(|text| text.parse().ok()))
}
impl Message for Surface {
const SCHEMA: SchemaId = SURFACE_SCHEMA;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ChannelAuthor {
#[default]
Planner,
Monitor,
}
impl ChannelAuthor {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Planner => "planner",
Self::Monitor => "monitor",
}
}
fn is_planner(&self) -> bool {
matches!(self, Self::Planner)
}
#[must_use]
pub fn author(self) -> Author {
Author::from(self.as_str())
}
}
fn read_at_a_version_this_build_reads<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<u32>, D::Error> {
Ok(Option::<u32>::deserialize(deserializer)?.map(|version| {
if REPLY_ENVELOPE_VERSIONS_READ.contains(&version) {
REPLY_ENVELOPE_VERSION
} else {
version
}
}))
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReplyEnvelope {
#[serde(
default,
deserialize_with = "read_at_a_version_this_build_reads",
skip_serializing_if = "Option::is_none"
)]
pub version: Option<u32>,
#[serde(default, skip_serializing_if = "ChannelAuthor::is_planner")]
pub author: ChannelAuthor,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completion: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub commands: Vec<Map<String, Value>>,
}
impl ReplyEnvelope {
#[must_use]
pub fn carries_verdict(&self) -> bool {
self.completion.is_some() || self.message.is_some() || self.reason.is_some()
}
#[must_use]
pub fn carries_edits_without_a_verdict(&self) -> bool {
!self.commands.is_empty() && !self.carries_verdict()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct QueuedReply {
pub id: u64,
pub reply: ReplyEnvelope,
pub at: u64,
#[serde(
default,
deserialize_with = "recorded_correlation",
skip_serializing_if = "Option::is_none"
)]
pub correlation: Option<Correlation>,
}
impl Message for QueuedReply {
const SCHEMA: SchemaId = QUEUED_REPLY_SCHEMA;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct QueuedCommands {
pub id: u64,
#[serde(default)]
pub author: ChannelAuthor,
pub commands: Vec<Map<String, Value>>,
}
impl Message for QueuedCommands {
const SCHEMA: SchemaId = QUEUED_COMMANDS_SCHEMA;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum CommandVerdict {
Applied,
Validated,
Delivered,
Refused,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CommandResult {
pub index: u64,
pub op: String,
pub outcome: CommandVerdict,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CommandOutcome {
pub id: u64,
pub applied: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub results: Vec<CommandResult>,
}
impl Message for CommandOutcome {
const SCHEMA: SchemaId = COMMAND_OUTCOME_SCHEMA;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Op {
Add,
Drop,
Reparent,
Retry,
Cancel,
Requeue,
Complete,
Attest,
Finding,
Amend,
Note,
Settle,
}
impl Op {
pub const ALL: [Op; 12] = [
Op::Add,
Op::Drop,
Op::Reparent,
Op::Retry,
Op::Cancel,
Op::Requeue,
Op::Complete,
Op::Attest,
Op::Finding,
Op::Amend,
Op::Note,
Op::Settle,
];
#[must_use]
pub const fn word(self) -> &'static str {
match self {
Op::Add => "add",
Op::Drop => "drop",
Op::Reparent => "reparent",
Op::Retry => "retry",
Op::Cancel => "cancel",
Op::Requeue => "requeue",
Op::Complete => "complete",
Op::Attest => "attest",
Op::Finding => "finding",
Op::Amend => "amend",
Op::Note => "note",
Op::Settle => "settle",
}
}
#[must_use]
pub fn of_word(word: &str) -> Option<Op> {
Op::ALL.into_iter().find(|op| op.word() == word)
}
}
impl Operation for Op {
fn name(&self) -> &str {
self.word()
}
}
pub const MONITOR_OPS: [Op; 5] = [Op::Retry, Op::Requeue, Op::Cancel, Op::Finding, Op::Add];
#[must_use]
pub fn allowlist() -> Allowlist<Op> {
let planner = ChannelAuthor::Planner.author();
let monitor = ChannelAuthor::Monitor.author();
let mut allowlist = Allowlist::new(Op::ALL);
for op in Op::ALL {
allowlist.grant(planner.clone(), op);
}
for op in MONITOR_OPS {
allowlist.grant(monitor.clone(), op);
}
for (op, reason) in [
(
Op::Complete,
"whether the run is finished is the planner's verdict, not an observation",
),
(
Op::Attest,
"a human action is attested by the person who took it, never by a watcher",
),
(
Op::Drop,
"removing work from the graph is a decomposition decision the planner owns",
),
(
Op::Reparent,
"rewiring dependencies is a decomposition decision the planner owns",
),
(
Op::Amend,
"what a node is judged against is a decomposition decision the planner owns",
),
(
Op::Note,
"a note may bind a criterion the node's judge decides against, which is the planner's decision rather than an observation",
),
(
Op::Settle,
"settling a node from evidence declares an outcome this run never observed, which is the planner's decision rather than an observation",
),
] {
allowlist.refuse(monitor.clone(), &op, reason);
}
allowlist
}
pub fn allows<O: Operation>(
allowlist: &Allowlist<O>,
author: &Author,
word: &str,
) -> Result<(), String> {
let Some(op) = allowlist.vocabulary().iter().find(|op| op.name() == word) else {
return Err(format!(
"'{word}' is not an op of the planner channel; the ops are: {}",
Op::ALL.map(Op::word).join(", ")
));
};
allowlist.allows(author, op).map_err(|refusal| {
format!(
"'{}' is not an op the {} may issue: {}. Surface it to the planner instead",
refusal.op, refusal.author, refusal.reason
)
})
}
pub fn allows_completion<O: Operation>(
allowlist: &Allowlist<O>,
author: &Author,
completion: Option<bool>,
) -> Result<(), String> {
if completion != Some(true) {
return Ok(());
}
let Some(complete) = allowlist
.vocabulary()
.iter()
.find(|op| op.name() == Op::Complete.word())
else {
return Ok(());
};
allowlist.allows(author, complete).map_err(|refusal| {
format!(
"declaring the run complete is not something the {} may do: {}. Surface it to the planner instead",
refusal.author, refusal.reason
)
})
}
fn name(text: &str) -> QueueName {
QueueName::try_from(text).unwrap_or_else(|_| unreachable!("{text} is a queue name"))
}
fn field(text: &str) -> onemessagebus::FieldPath {
text.parse()
.unwrap_or_else(|_| unreachable!("{text} is a field path"))
}
fn claims_a_verdict() -> Predicate {
let verdict = Predicate::Any(
["reply.completion", "reply.message", "reply.reason"]
.into_iter()
.map(|path| Predicate::Present {
field: field(path),
present: true,
})
.collect(),
);
Predicate::Not(Box::new(Predicate::All(vec![
Predicate::NonEmpty {
field: field("reply.commands"),
non_empty: true,
},
Predicate::Not(Box::new(verdict)),
])))
}
#[must_use]
pub fn queues() -> Vec<QueueSpec> {
let mut surfaces = QueueSpec::new(
name(SURFACES),
Policy {
supersede_on: Some(Supersede {
key: field("source"),
when: Some(Predicate::equals(field("source"), source::CHECK_IN)),
}),
hold_pending: true,
blocking_first: true,
projection: Some(
PROJECTION
.parse()
.unwrap_or_else(|_| unreachable!("queue.json is a document name")),
),
..Policy::default()
},
);
surfaces.schema = Some(SURFACE_SCHEMA);
surfaces.answers = Some(name(REPLIES));
let mut replies = QueueSpec::new(name(REPLIES), Policy::default());
replies.schema = Some(QUEUED_REPLY_SCHEMA);
replies.claims = Some(claims_a_verdict());
replies.numbered = true;
let mut commands = QueueSpec::new(name(COMMANDS), Policy::default());
commands.schema = Some(QUEUED_COMMANDS_SCHEMA);
commands.numbered = true;
let mut outcomes = QueueSpec::new(name(COMMAND_OUTCOMES), Policy::default());
outcomes.schema = Some(COMMAND_OUTCOME_SCHEMA);
vec![surfaces, replies, commands, outcomes]
}
fn now_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| {
u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX)
})
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PlannerChannel;
impl PlannerChannel {
fn route_envelope<O: Operation>(
envelope: Value,
allowlist: &Allowlist<O>,
) -> Result<Vec<(QueueName, Value)>, String> {
if !envelope.is_object() {
return Err("the reply is malformed: a reply envelope is a JSON object".to_owned());
}
let envelope: ReplyEnvelope = serde_json::from_value(envelope)
.map_err(|failure| format!("the reply is malformed: {failure}"))?;
let author = envelope.author.author();
allows_completion(allowlist, &author, envelope.completion)?;
let mut routed = Vec::new();
if !envelope.commands.is_empty() {
if envelope.version != Some(REPLY_ENVELOPE_VERSION) {
return Err(format!(
"an edit envelope requires version {REPLY_ENVELOPE_VERSION}"
));
}
for command in &envelope.commands {
let word = command
.get("op")
.and_then(Value::as_str)
.ok_or("a command names its `op`")?;
allows(allowlist, &author, word)?;
}
routed.push((
name(COMMANDS),
serde_json::json!({
"id": 0,
"author": envelope.author,
"commands": envelope.commands,
}),
));
}
if envelope.carries_verdict() || envelope.commands.is_empty() {
routed.push((
name(REPLIES),
serde_json::json!({ "id": 0, "reply": envelope, "at": now_millis() }),
));
}
Ok(routed)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ReplyRouter;
impl Router for ReplyRouter {
fn route(
&self,
queue: &QueueName,
record: Value,
allowlist: &Allowlist<OpWord>,
) -> Result<Vec<(QueueName, Value)>, String> {
match record.get("reply") {
Some(envelope) => {
PlannerChannel::route_envelope(envelope.clone(), allowlist)?;
Ok(vec![(queue.clone(), record)])
}
None => PlannerChannel::route_envelope(record, allowlist),
}
}
}
impl Layout for PlannerChannel {
fn name(&self) -> &str {
PLANNER_CHANNEL
}
fn queues(&self) -> Vec<QueueSpec> {
queues()
}
fn allowlist(&self) -> Allowlist<OpWord> {
allowlist().words()
}
fn registry(&self) -> Registry {
crate::registry()
}
fn prepare(
&self,
queue: &QueueName,
record: Value,
allowlist: &Allowlist<OpWord>,
) -> Result<Vec<(QueueName, Value)>, String> {
match queue.as_str() {
SURFACES => {
let Value::Object(fields) = record else {
return Ok(vec![(queue.clone(), record)]);
};
let about = fields.get(onemessagebus::ask::ABOUT).cloned();
let mut fields: Map<String, Value> = fields
.into_iter()
.filter(|(key, _)| key != onemessagebus::ask::ABOUT)
.collect();
if let Some(about) = about {
fields.entry("workstream").or_insert(about);
}
fields
.entry("queued_at")
.or_insert_with(|| Value::from(now_millis()));
Ok(vec![(queue.clone(), Value::Object(fields))])
}
REPLIES => ReplyRouter.route(queue, record, allowlist),
COMMANDS => {
let author: ChannelAuthor = record
.get("author")
.map(|author| serde_json::from_value(author.clone()))
.transpose()
.map_err(|failure| format!("the envelope's author: {failure}"))?
.unwrap_or_default();
for command in record
.get("commands")
.and_then(Value::as_array)
.ok_or("a command envelope carries `commands`")?
{
let word = command
.get("op")
.and_then(Value::as_str)
.ok_or("a command names its `op`")?;
allows(allowlist, &author.author(), word)?;
}
Ok(vec![(queue.clone(), record)])
}
_ => Ok(vec![(queue.clone(), record)]),
}
}
}
#[derive(Debug, Clone)]
pub struct Channel {
surfaces: Queue<Surface>,
replies: Queue<QueuedReply>,
commands: Queue<QueuedCommands>,
outcomes: Queue<CommandOutcome>,
}
impl Channel {
pub fn open(transport: &Arc<dyn Transport>) -> Result<Self, QueueError> {
let mut specs = queues().into_iter();
let mut next = || specs.next().unwrap_or_else(|| unreachable!("four queues"));
Ok(Self {
surfaces: Queue::open(Arc::clone(transport), next())?,
replies: Queue::open(Arc::clone(transport), next())?,
commands: Queue::open(Arc::clone(transport), next())?,
outcomes: Queue::open(Arc::clone(transport), next())?,
})
}
#[must_use]
pub fn surfaces(&self) -> &Queue<Surface> {
&self.surfaces
}
#[must_use]
pub fn replies(&self) -> &Queue<QueuedReply> {
&self.replies
}
#[must_use]
pub fn commands(&self) -> &Queue<QueuedCommands> {
&self.commands
}
#[must_use]
pub fn outcomes(&self) -> &Queue<CommandOutcome> {
&self.outcomes
}
pub fn push(&self, surface: &Surface) -> Result<Pushed<Surface>, QueueError> {
self.surfaces.push(surface)
}
pub fn claim(&self) -> Result<Option<Surface>, QueueError> {
Ok(self
.surfaces
.claim(&ConsumerName::default_consumer())?
.map(|claimed| claimed.record))
}
pub fn pending(&self) -> Result<Option<Surface>, QueueError> {
Ok(self
.surfaces
.pending(&ConsumerName::default_consumer())?
.map(|claimed| claimed.record))
}
pub fn abandon(&self, raised: &[u64]) -> Result<Vec<Surface>, QueueError> {
self.typed_surfaces(self.surfaces.raw().abandon(raised)?)
}
pub fn attend(&self, asker: &Asker) -> Result<Vec<Surface>, QueueError> {
self.typed_surfaces(self.surfaces.raw().attend(asker)?)
}
fn typed_surfaces(&self, records: Vec<Value>) -> Result<Vec<Surface>, QueueError> {
records
.into_iter()
.map(|record| {
serde_json::from_value(record).map_err(|failure| QueueError::Shape {
queue: name(SURFACES),
why: failure.to_string(),
})
})
.collect()
}
pub fn answer(&self, reply: &ReplyEnvelope, at: u64) -> Result<u64, QueueError> {
self.surfaces.raw().answer_pending()?;
let pushed = self.replies.push(&QueuedReply {
id: 0,
reply: reply.clone(),
at,
correlation: None,
})?;
Ok(pushed.id.unwrap_or_default())
}
pub fn answer_if_verdict(
&self,
reply: &ReplyEnvelope,
at: u64,
) -> Result<Option<u64>, QueueError> {
if reply.carries_verdict() {
return self.answer(reply, at).map(Some);
}
Ok(None)
}
pub fn claim_reply(&self) -> Result<Option<QueuedReply>, QueueError> {
Ok(self
.replies
.claim(&ConsumerName::default_consumer())?
.map(|claimed| claimed.record))
}
pub fn submit(
&self,
author: ChannelAuthor,
commands: Vec<Map<String, Value>>,
) -> Result<u64, QueueError> {
let pushed = self.commands.push(&QueuedCommands {
id: 0,
author,
commands,
})?;
Ok(pushed.id.unwrap_or_default())
}
pub fn claim_commands(&self) -> Result<Vec<QueuedCommands>, QueueError> {
let mut claimed = Vec::new();
while let Some(envelope) = self.commands.claim(&ConsumerName::default_consumer())? {
claimed.push(envelope.record);
}
Ok(claimed)
}
pub fn answer_commands(&self, outcome: &CommandOutcome) -> Result<Position, QueueError> {
Ok(self.outcomes.push(outcome)?.position)
}
pub fn outcome_of(&self, id: u64) -> Result<Option<CommandOutcome>, QueueError> {
Ok(self
.outcomes
.raw()
.log(None)?
.into_iter()
.filter_map(|(record, _)| serde_json::from_value::<CommandOutcome>(record).ok())
.find(|outcome| outcome.id == id))
}
}