Skip to main content

onemessagebus_agent/
event.rs

1//! The agent stack's envelope, labels, sources, phases and filter: the core's
2//! generic types over the [`Agent`] vocabulary, and the closed enums that
3//! vocabulary is made of.
4//!
5//! Every type here serializes to the bytes the stack's producers write today —
6//! `crates/onemessagebus-agent/tests/recorded/` holds a stream from each and
7//! round-trips it unchanged.
8
9use std::fmt;
10
11use onemessagebus::{Message, SchemaId, Vocabulary};
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use serde_json::{Map, Value};
15
16use crate::Agent;
17
18pub use onemessagebus::ArtifactRef;
19
20/// One event of the agent stack: the core's envelope over the agent vocabulary.
21pub type Envelope = onemessagebus::Envelope<Agent>;
22
23/// Which envelopes pass: the core's filter over the agent vocabulary.
24pub type EventFilter = onemessagebus::Filter<Agent>;
25
26/// One matcher of an [`EventFilter`]: `source`, `kind`, and the
27/// [`MatchFields`] — `phase` and the five reserved labels a matcher may name.
28pub type Matcher = onemessagebus::Matcher<Agent>;
29
30/// The core's emitter over the agent vocabulary.
31pub type Emitter = onemessagebus::Emitter<Agent>;
32
33/// The core's reader over the agent vocabulary.
34pub type Reader = onemessagebus::Reader<Agent>;
35
36/// The core's merge over the agent vocabulary.
37pub type Merge = onemessagebus::Merge<Agent>;
38
39/// The library that produced an event.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
41#[serde(rename_all = "lowercase")]
42pub enum Source {
43    /// `oneagentgraph`.
44    Agentgraph,
45    /// `onevcs`.
46    Vcs,
47    /// `onepipeline`.
48    Pipeline,
49}
50
51impl Source {
52    /// Every source, in the order a refusal lists them.
53    #[must_use]
54    pub const fn every() -> [Source; 3] {
55        [Source::Agentgraph, Source::Vcs, Source::Pipeline]
56    }
57
58    /// The word this source travels as.
59    #[must_use]
60    pub const fn as_str(self) -> &'static str {
61        match self {
62            Source::Agentgraph => "agentgraph",
63            Source::Vcs => "vcs",
64            Source::Pipeline => "pipeline",
65        }
66    }
67
68    /// The envelope version a producer of this source writes against.
69    #[must_use]
70    pub fn write_version(self) -> u32 {
71        Agent::write_version(&self)
72    }
73}
74
75impl fmt::Display for Source {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        f.write_str(self.as_str())
78    }
79}
80
81/// Which part of a change's life an event belongs to.
82///
83/// Four phases over one change: the work is made, it is brought together with
84/// the base it is going onto, it is proposed and ruled on, and what carries it
85/// is released. Stamped by the producer, never derived by a reader: one kind's
86/// phase is not always a fact about the kind.
87#[derive(
88    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
89)]
90#[serde(rename_all = "kebab-case")]
91pub enum Phase {
92    /// The work is being made.
93    Development,
94    /// The work is being brought together with the base.
95    Integrate,
96    /// The change request is open and being ruled on.
97    Review,
98    /// What carries the landed change is being released.
99    Release,
100}
101
102impl Phase {
103    /// The word this phase is spelled with, in a filter and in a rendering.
104    #[must_use]
105    pub const fn as_str(self) -> &'static str {
106        match self {
107            Phase::Development => "development",
108            Phase::Integrate => "integrate",
109            Phase::Review => "review",
110            Phase::Release => "release",
111        }
112    }
113
114    /// Every phase, in the order a refusal lists them.
115    #[must_use]
116    pub const fn every() -> [Phase; 4] {
117        [
118            Phase::Development,
119            Phase::Integrate,
120            Phase::Review,
121            Phase::Release,
122        ]
123    }
124}
125
126impl fmt::Display for Phase {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        f.write_str(self.as_str())
129    }
130}
131
132/// The agent envelope's reserved top-level dimensions: `phase`, optional and
133/// omitted from the wire when absent, exactly as `onepipeline` relays it and
134/// `onevcs` stamps it.
135///
136/// Carried between `kind` and `labels` on the wire. Refuses any other
137/// top-level key, which is what makes an agent envelope reject an unknown
138/// field by name.
139#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
140#[serde(deny_unknown_fields)]
141pub struct Dimensions {
142    /// Which part of a change's life the event belongs to, as its producer
143    /// classified it.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub phase: Option<Phase>,
146}
147
148impl Dimensions {
149    /// No phase.
150    #[must_use]
151    pub const fn none() -> Self {
152        Self { phase: None }
153    }
154
155    /// At `phase`.
156    #[must_use]
157    pub const fn at(phase: Phase) -> Self {
158        Self { phase: Some(phase) }
159    }
160}
161
162impl From<Phase> for Dimensions {
163    fn from(phase: Phase) -> Self {
164        Self::at(phase)
165    }
166}
167
168impl From<Option<Phase>> for Dimensions {
169    fn from(phase: Option<Phase>) -> Self {
170        Self { phase }
171    }
172}
173
174/// The reserved label keys, plus whatever else a producer stamped.
175///
176/// Reserved keys are absent rather than empty when unknown, so an enricher can
177/// tell "not stamped" from "stamped empty". The extras are flattened beside
178/// them, in the order they were stamped.
179#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
180pub struct Labels {
181    /// The run this event belongs to.
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub run_id: Option<String>,
184    /// The round within the run.
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    pub round: Option<u64>,
187    /// The graph node being executed.
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub node: Option<String>,
190    /// The step within a node that runs several in sequence.
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub step: Option<String>,
193    /// Which member of a conversation produced the event.
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub member: Option<String>,
196    /// The persona that member is running under.
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub persona: Option<String>,
199    /// Free-form extras beyond the reserved keys above, carried untouched.
200    #[serde(flatten)]
201    pub extra: Map<String, Value>,
202}
203
204impl Message for Labels {
205    const SCHEMA: SchemaId = SchemaId::literal("agent", "labels", 1);
206}
207
208/// What an agent matcher may name beside `source` and `kind`: `phase` and the
209/// reserved labels, each by exact equality against what the envelope carries.
210/// `round` is deliberately not among them, as the grammar says.
211#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
212#[serde(deny_unknown_fields)]
213pub struct MatchFields {
214    /// The phase the envelope was stamped at.
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub phase: Option<Phase>,
217    /// The `run_id` label.
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub run_id: Option<String>,
220    /// The `node` label.
221    #[serde(default, skip_serializing_if = "Option::is_none")]
222    pub node: Option<String>,
223    /// The `step` label.
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub step: Option<String>,
226    /// The `member` label.
227    #[serde(default, skip_serializing_if = "Option::is_none")]
228    pub member: Option<String>,
229    /// The `persona` label.
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub persona: Option<String>,
232}
233
234/// What the agent envelope carries that the core's field names do not spell:
235/// the phase, by name.
236pub trait AgentEnvelope {
237    /// The phase the envelope was stamped at, if any.
238    fn phase(&self) -> Option<Phase>;
239}
240
241impl AgentEnvelope for Envelope {
242    fn phase(&self) -> Option<Phase> {
243        self.dimensions.phase
244    }
245}
246
247/// [`EventFilter::allows`] spelled in the agent vocabulary's own terms.
248pub trait AgentFilter {
249    /// Whether an envelope of `source`, `kind`, `labels` and `phase` passes.
250    fn admits(&self, source: Source, kind: &str, labels: &Labels, phase: Option<Phase>) -> bool;
251}
252
253impl AgentFilter for EventFilter {
254    fn admits(&self, source: Source, kind: &str, labels: &Labels, phase: Option<Phase>) -> bool {
255        self.allows(&source, kind, &Dimensions { phase }, labels)
256    }
257}