1use std::collections::BTreeMap;
4use std::num::NonZeroUsize;
5
6use serde::{Deserialize, Serialize};
7use serde_json::{Value, json};
8
9#[path = "waiting_state.rs"]
10pub mod durable;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct WaitingLimits {
16 pub drafts: NonZeroUsize,
19 pub words_bytes: NonZeroUsize,
21 pub identifier_bytes: NonZeroUsize,
23}
24
25#[derive(Clone, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27struct HeldDraft {
28 draft: String,
29 words: String,
30 since: u64,
31}
32
33#[derive(Clone, Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35struct ResolvedDraft {
36 digest: String,
37 outcome: DraftOutcome,
38}
39
40#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct DraftOutcome {
44 pub draft: String,
46 pub outcome: OutcomeKind,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub refusal: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub reason: Option<String>,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum OutcomeKind {
59 Executed,
60 Refused,
61}
62
63impl DraftOutcome {
64 #[must_use]
66 pub fn executed(draft: &str) -> Self {
67 Self {
68 draft: draft.to_owned(),
69 outcome: OutcomeKind::Executed,
70 refusal: None,
71 reason: None,
72 }
73 }
74
75 #[must_use]
77 pub fn refused(draft: &str, refusal: &str, reason: &str) -> Self {
78 Self {
79 draft: draft.to_owned(),
80 outcome: OutcomeKind::Refused,
81 refusal: Some(refusal.to_owned()),
82 reason: Some(reason.to_owned()),
83 }
84 }
85}
86
87#[derive(Clone, Default, Serialize, Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct WaitingRecord {
90 pending: Vec<HeldDraft>,
91 resolved: BTreeMap<String, ResolvedDraft>,
92 last: Option<DraftOutcome>,
93 gone: bool,
94}
95
96impl std::fmt::Debug for WaitingRecord {
97 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 formatter
99 .debug_struct("WaitingRecord")
100 .finish_non_exhaustive()
101 }
102}
103
104#[derive(Debug, thiserror::Error)]
106pub enum WaitingError {
107 #[error("agent_waiting_bound")]
109 Bound,
110 #[error("agent_waiting_draft_conflict")]
112 Conflict,
113 #[error("agent_waiting_draft_unknown")]
115 Unknown,
116 #[error("agent_waiting_store_failed")]
118 Store,
119}
120
121impl WaitingRecord {
122 pub fn hold(
127 &mut self,
128 draft: &str,
129 words: &str,
130 since: u64,
131 limits: WaitingLimits,
132 ) -> Result<bool, WaitingError> {
133 if draft.is_empty()
134 || draft.len() > limits.identifier_bytes.get()
135 || draft.chars().any(char::is_control)
136 || words.is_empty()
137 || words.len() > limits.words_bytes.get()
138 {
139 return Err(WaitingError::Bound);
140 }
141 if let Some(previous) = self.resolved.get(draft) {
142 return if previous.digest == lys_pass::drafts::request_digest(words) {
143 Ok(false)
144 } else {
145 Err(WaitingError::Conflict)
146 };
147 }
148 if let Some(held) = self.pending.iter().find(|held| held.draft == draft) {
149 return if held.words == words {
150 Ok(false)
151 } else {
152 Err(WaitingError::Conflict)
153 };
154 }
155 if self.pending.len().saturating_add(self.resolved.len()) >= limits.drafts.get() {
156 return Err(WaitingError::Bound);
157 }
158 self.pending.push(HeldDraft {
159 draft: draft.to_owned(),
160 words: words.to_owned(),
161 since,
162 });
163 self.pending
164 .sort_by(|left, right| (left.since, &left.draft).cmp(&(right.since, &right.draft)));
165 Ok(true)
166 }
167
168 pub fn resolve(
174 &mut self,
175 outcome: &DraftOutcome,
176 exchange: &str,
177 gone: bool,
178 limits: WaitingLimits,
179 ) -> Result<Option<Value>, WaitingError> {
180 if exchange.len() > limits.identifier_bytes.get()
181 || outcome.draft.len() > limits.identifier_bytes.get()
182 || outcome
183 .refusal
184 .as_ref()
185 .is_some_and(|name| name.len() > limits.identifier_bytes.get())
186 || outcome
187 .reason
188 .as_ref()
189 .is_some_and(|reason| reason.len() > limits.words_bytes.get())
190 {
191 return Err(WaitingError::Bound);
192 }
193 if exchange.is_empty()
194 || exchange.chars().any(char::is_control)
195 || outcome.draft.is_empty()
196 || outcome.draft.chars().any(char::is_control)
197 || outcome
198 .refusal
199 .as_ref()
200 .is_some_and(|name| name.chars().any(char::is_control))
201 || matches!(outcome.outcome, OutcomeKind::Refused)
202 && (outcome.refusal.as_deref().is_none_or(str::is_empty)
203 || outcome.reason.as_deref().is_none_or(str::is_empty))
204 || matches!(outcome.outcome, OutcomeKind::Executed)
205 && (outcome.refusal.is_some() || outcome.reason.is_some())
206 {
207 return Err(WaitingError::Conflict);
208 }
209 if let Some(previous) = self.resolved.get(&outcome.draft) {
210 return if &previous.outcome == outcome {
211 Ok(None)
212 } else {
213 Err(WaitingError::Conflict)
214 };
215 }
216 let position = self
217 .pending
218 .iter()
219 .position(|held| held.draft == outcome.draft)
220 .ok_or(WaitingError::Unknown)?;
221 let mut inbox = serde_json::to_value(outcome).map_err(|_| WaitingError::Store)?;
222 inbox["meta"] = json!({"from":exchange});
223 let held = self.pending.remove(position);
224 self.resolved.insert(
225 outcome.draft.clone(),
226 ResolvedDraft {
227 digest: lys_pass::drafts::request_digest(&held.words),
228 outcome: outcome.clone(),
229 },
230 );
231 self.last = Some(outcome.clone());
232 self.gone = gone;
233 Ok(Some(inbox))
234 }
235
236 pub fn snapshot(&self) -> Result<Value, WaitingError> {
241 if let Some(held) = self.pending.first() {
242 return Ok(json!({"state":"waiting", "draft":held.draft,
243 "words":held.words, "since":held.since, "count":self.pending.len(), "last_outcome":self.last}));
244 }
245 let mut value = match &self.last {
246 Some(last) => serde_json::to_value(last).map_err(|_| WaitingError::Store)?,
247 None => json!({}),
248 };
249 value["state"] = json!(if self.gone { "gone" } else { "present" });
250 Ok(value)
251 }
252}
253
254use crate::agents::runtime::Agents;
255use crate::auth_lys::live::drafts::DraftError;
256use durable::DraftJournal;
257use liminal::channel::ChannelHandle;
258use lys_pass::drafts::{ApprovedDraft, Execution};
259use std::sync::{Arc, Mutex};
260
261#[derive(Clone, Serialize, Deserialize)]
262#[serde(deny_unknown_fields)]
263pub struct Delivery {
264 agent: String,
265 from: Vec<u64>,
266 message: Value,
267 state: Value,
268 delivered: bool,
269}
270pub struct AgentDelivery {
272 agents: Arc<Agents>,
273 journal: Arc<DraftJournal>,
274 inboxes: Mutex<BTreeMap<String, ChannelHandle>>,
275 exchange: String,
276 limits: WaitingLimits,
277 serial: Mutex<()>,
278}
279impl AgentDelivery {
280 pub fn new(
285 owners: &durable::ApprovalAgents,
286 inboxes: BTreeMap<String, ChannelHandle>,
287 exchange: String,
288 limits: WaitingLimits,
289 ) -> Result<Arc<Self>, DraftError> {
290 if inboxes.len() > limits.drafts.get()
291 || exchange.is_empty()
292 || exchange.len() > limits.identifier_bytes.get()
293 || exchange.chars().any(char::is_control)
294 {
295 return Err(DraftError::Bound);
296 }
297 for (name, inbox) in &inboxes {
298 owners
299 .agents()
300 .registry()
301 .record(name)
302 .map_err(|_| DraftError::Contract)?;
303 if inbox.config().name != format!("agents.{name}.inbox") {
304 return Err(DraftError::Contract);
305 }
306 inbox
307 .durable_positions()
308 .map_err(|_| DraftError::Contract)?;
309 }
310 Ok(Arc::new(Self {
311 agents: owners.agents().clone(),
312 journal: owners.journal().clone(),
313 inboxes: Mutex::new(inboxes),
314 exchange,
315 limits,
316 serial: Mutex::new(()),
317 }))
318 }
319 fn message(
320 &self,
321 agent: &str,
322 draft: &ApprovedDraft,
323 receipt: &Execution,
324 from: Vec<u64>,
325 ) -> Result<Delivery, DraftError> {
326 let outcome = match receipt {
327 Execution::Executed { .. } => DraftOutcome::executed(&draft.id),
328 Execution::RefusedOnExecution {
329 refusal, reason, ..
330 } => DraftOutcome::refused(&draft.id, refusal, reason),
331 };
332 let gone = self
333 .agents
334 .presence()
335 .connection(agent)
336 .map_err(|_| DraftError::Store)?
337 .is_none();
338 self.journal.update(|snapshot| {
339 if let Some(old) = snapshot.deliveries.get(&draft.id) {
340 return Ok(old.clone());
341 }
342 let waiting = snapshot
343 .waiting
344 .get_mut(agent)
345 .ok_or(DraftError::Contract)?;
346 let message = waiting
347 .resolve(&outcome, &self.exchange, gone, self.limits)
348 .map_err(|_| DraftError::Contract)?
349 .ok_or(DraftError::Contract)?;
350 let delivery = Delivery {
351 agent: agent.into(),
352 from,
353 message,
354 state: waiting.snapshot().map_err(|_| DraftError::Store)?,
355 delivered: false,
356 };
357 snapshot
358 .deliveries
359 .insert(draft.id.clone(), delivery.clone());
360 Ok(delivery)
361 })
362 }
363 pub fn bind_inbox(&self, name: &str, inbox: ChannelHandle) -> Result<(), DraftError> {
368 self.agents
369 .registry()
370 .record(name)
371 .map_err(|_| DraftError::Contract)?;
372 if inbox.config().name != format!("agents.{name}.inbox") {
373 return Err(DraftError::Contract);
374 }
375 inbox
376 .durable_positions()
377 .map_err(|_| DraftError::Contract)?;
378 let mut inboxes = self.inboxes.lock().map_err(|_| DraftError::Store)?;
379 if inboxes.contains_key(name) {
380 return Ok(());
381 }
382 if inboxes.len() >= self.limits.drafts.get() {
383 return Err(DraftError::Bound);
384 }
385 inboxes.insert(name.into(), inbox);
386 drop(inboxes);
387 Ok(())
388 }
389}
390#[path = "waiting_outcome.rs"]
391mod outcome;
392
393#[cfg(test)]
394#[path = "waiting_tests.rs"]
395pub(crate) mod tests;
396
397impl std::fmt::Debug for AgentDelivery {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 f.debug_struct("AgentDelivery").finish_non_exhaustive()
400 }
401}