Skip to main content

meerkat_workgraph/
types.rs

1use std::collections::BTreeSet;
2use std::fmt;
3use std::str::FromStr;
4
5use chrono::{DateTime, Utc};
6use meerkat_core::SessionId;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use uuid::Uuid;
10
11use crate::WorkGraphError;
12pub use crate::machines::work_attention_lifecycle::WorkAttentionLifecycleMachineState as WorkAttentionMachineState;
13use crate::machines::workgraph_lifecycle as wg_dsl;
14pub use crate::machines::workgraph_lifecycle::WorkGraphLifecycleMachineState as WorkGraphMachineState;
15
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18#[serde(transparent)]
19pub struct WorkItemId(String);
20
21impl WorkItemId {
22    pub fn new(value: impl Into<String>) -> Result<Self, WorkGraphError> {
23        validate_token("work item id", value.into()).map(Self)
24    }
25
26    pub fn generated() -> Self {
27        Self(format!("work_{}", Uuid::now_v7()))
28    }
29
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
36#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
37#[serde(transparent)]
38pub struct WorkAttentionBindingId(String);
39
40impl WorkAttentionBindingId {
41    pub fn new(value: impl Into<String>) -> Result<Self, WorkGraphError> {
42        validate_token("work attention binding id", value.into()).map(Self)
43    }
44
45    pub fn generated() -> Self {
46        Self(format!("attention_{}", Uuid::now_v7()))
47    }
48
49    pub fn as_str(&self) -> &str {
50        &self.0
51    }
52}
53
54impl fmt::Display for WorkAttentionBindingId {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.write_str(self.as_str())
57    }
58}
59
60impl FromStr for WorkAttentionBindingId {
61    type Err = WorkGraphError;
62
63    fn from_str(value: &str) -> Result<Self, Self::Err> {
64        Self::new(value)
65    }
66}
67
68impl fmt::Display for WorkItemId {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.write_str(self.as_str())
71    }
72}
73
74impl FromStr for WorkItemId {
75    type Err = WorkGraphError;
76
77    fn from_str(value: &str) -> Result<Self, Self::Err> {
78        Self::new(value)
79    }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
83#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
84#[serde(transparent)]
85pub struct WorkNamespace(String);
86
87impl WorkNamespace {
88    pub fn new(value: impl Into<String>) -> Result<Self, WorkGraphError> {
89        validate_token("work namespace", value.into()).map(Self)
90    }
91
92    pub fn default_namespace() -> Self {
93        Self("default".to_string())
94    }
95
96    pub fn as_str(&self) -> &str {
97        &self.0
98    }
99}
100
101impl Default for WorkNamespace {
102    fn default() -> Self {
103        Self::default_namespace()
104    }
105}
106
107impl fmt::Display for WorkNamespace {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        f.write_str(self.as_str())
110    }
111}
112
113impl FromStr for WorkNamespace {
114    type Err = WorkGraphError;
115
116    fn from_str(value: &str) -> Result<Self, Self::Err> {
117        Self::new(value)
118    }
119}
120
121fn validate_token(name: &str, value: String) -> Result<String, WorkGraphError> {
122    let trimmed = value.trim();
123    if trimmed.is_empty() {
124        return Err(WorkGraphError::InvalidInput(format!(
125            "{name} must not be empty"
126        )));
127    }
128    if trimmed.chars().any(char::is_control) {
129        return Err(WorkGraphError::InvalidInput(format!(
130            "{name} must not contain control characters"
131        )));
132    }
133    Ok(trimmed.to_string())
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
137#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
138#[serde(rename_all = "snake_case")]
139pub enum WorkStatus {
140    #[default]
141    Open,
142    InProgress,
143    Blocked,
144    Completed,
145    Cancelled,
146    Failed,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
150#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
151#[serde(rename_all = "snake_case")]
152pub enum WorkPriority {
153    Low,
154    #[default]
155    Medium,
156    High,
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
160#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
161#[serde(rename_all = "snake_case")]
162pub enum WorkEdgeKind {
163    Blocks,
164    Parent,
165    Related,
166    Supersedes,
167    DerivedFrom,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[serde(rename_all = "snake_case")]
173pub enum WorkOwnerKind {
174    Principal,
175    Agent,
176    Session,
177    Mob,
178    Label,
179}
180
181impl WorkOwnerKind {
182    pub fn as_str(self) -> &'static str {
183        match self {
184            Self::Principal => "principal",
185            Self::Agent => "agent",
186            Self::Session => "session",
187            Self::Mob => "mob",
188            Self::Label => "label",
189        }
190    }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
194#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
195pub struct WorkOwnerKey {
196    pub kind: WorkOwnerKind,
197    pub id: String,
198}
199
200impl WorkOwnerKey {
201    pub fn new(kind: WorkOwnerKind, id: impl Into<String>) -> Result<Self, WorkGraphError> {
202        Ok(Self {
203            kind,
204            id: validate_token("work owner id", id.into())?,
205        })
206    }
207
208    pub fn principal(id: impl Into<String>) -> Result<Self, WorkGraphError> {
209        Self::new(WorkOwnerKind::Principal, id)
210    }
211
212    pub fn agent(id: impl Into<String>) -> Result<Self, WorkGraphError> {
213        Self::new(WorkOwnerKind::Agent, id)
214    }
215
216    pub fn session(id: impl Into<String>) -> Result<Self, WorkGraphError> {
217        Self::new(WorkOwnerKind::Session, id)
218    }
219
220    pub fn mob(id: impl Into<String>) -> Result<Self, WorkGraphError> {
221        Self::new(WorkOwnerKind::Mob, id)
222    }
223
224    pub fn label(id: impl Into<String>) -> Result<Self, WorkGraphError> {
225        Self::new(WorkOwnerKind::Label, id)
226    }
227
228    pub fn canonical(&self) -> String {
229        format!("{}:{}", self.kind.as_str(), self.id)
230    }
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
235pub struct WorkOwner {
236    pub key: WorkOwnerKey,
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    pub display_name: Option<String>,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
243#[serde(tag = "kind", rename_all = "snake_case")]
244pub enum WorkCompletionPolicy {
245    #[default]
246    SelfAttest,
247    HostConfirmed,
248    PrincipalConfirmed,
249    Supervisor {
250        owner_key: WorkOwnerKey,
251    },
252    ReviewerQuorum {
253        #[cfg_attr(feature = "schema", schemars(range(min = 1, max = 64)))]
254        threshold: u16,
255    },
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
260#[serde(tag = "kind", rename_all = "snake_case")]
261pub enum PublicGoalCompletionPolicy {
262    #[default]
263    SelfAttest,
264}
265
266impl From<PublicGoalCompletionPolicy> for WorkCompletionPolicy {
267    fn from(policy: PublicGoalCompletionPolicy) -> Self {
268        match policy {
269            PublicGoalCompletionPolicy::SelfAttest => Self::SelfAttest,
270        }
271    }
272}
273
274impl WorkCompletionPolicy {
275    pub fn requires_trusted_principal(&self) -> bool {
276        matches!(
277            self,
278            Self::PrincipalConfirmed | Self::Supervisor { .. } | Self::ReviewerQuorum { .. }
279        )
280    }
281
282    pub(crate) fn to_machine(&self) -> wg_dsl::WorkCompletionPolicy {
283        match self {
284            Self::SelfAttest => wg_dsl::WorkCompletionPolicy::SelfAttest,
285            Self::HostConfirmed => wg_dsl::WorkCompletionPolicy::HostConfirmed,
286            Self::PrincipalConfirmed => wg_dsl::WorkCompletionPolicy::PrincipalConfirmed,
287            Self::Supervisor { .. } => wg_dsl::WorkCompletionPolicy::Supervisor,
288            Self::ReviewerQuorum { .. } => wg_dsl::WorkCompletionPolicy::ReviewerQuorum,
289        }
290    }
291
292    pub(crate) fn supervisor_owner_key(&self) -> Option<wg_dsl::WorkOwnerKey> {
293        match self {
294            Self::Supervisor { owner_key } => Some(work_owner_key_to_machine(owner_key)),
295            _ => None,
296        }
297    }
298
299    pub(crate) fn reviewer_quorum_threshold(&self) -> Option<u64> {
300        match self {
301            Self::ReviewerQuorum { threshold } => Some(u64::from(*threshold)),
302            _ => None,
303        }
304    }
305
306    pub(crate) fn from_machine(
307        policy: wg_dsl::WorkCompletionPolicy,
308        supervisor_owner_key: Option<wg_dsl::WorkOwnerKey>,
309        reviewer_quorum_threshold: Option<u64>,
310    ) -> Self {
311        match policy {
312            wg_dsl::WorkCompletionPolicy::SelfAttest => Self::SelfAttest,
313            wg_dsl::WorkCompletionPolicy::HostConfirmed => Self::HostConfirmed,
314            wg_dsl::WorkCompletionPolicy::PrincipalConfirmed => Self::PrincipalConfirmed,
315            wg_dsl::WorkCompletionPolicy::Supervisor => Self::Supervisor {
316                owner_key: supervisor_owner_key
317                    .map(work_owner_key_from_machine)
318                    .unwrap_or_else(|| WorkOwnerKey {
319                        kind: WorkOwnerKind::Principal,
320                        id: "supervisor".to_string(),
321                    }),
322            },
323            wg_dsl::WorkCompletionPolicy::ReviewerQuorum => Self::ReviewerQuorum {
324                threshold: reviewer_quorum_threshold
325                    .and_then(|threshold| u16::try_from(threshold).ok())
326                    .unwrap_or(1),
327            },
328        }
329    }
330}
331
332impl WorkOwner {
333    pub fn new(key: WorkOwnerKey) -> Self {
334        Self {
335            key,
336            display_name: None,
337        }
338    }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
343pub struct WorkClaim {
344    pub owner: WorkOwner,
345    pub claimed_at: DateTime<Utc>,
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    pub lease_expires_at: Option<DateTime<Utc>>,
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
351#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
352pub struct ExternalWorkRef {
353    pub kind: String,
354    pub id: String,
355    #[serde(default, skip_serializing_if = "Option::is_none")]
356    pub url: Option<String>,
357}
358
359/// Typed classification of confirmation evidence.
360///
361/// This is the canonical signal the `WorkGraphLifecycleMachine` consumes to
362/// decide completion-policy satisfaction. The producer
363/// (`confirmation_evidence_for_policy`) sets this field; the raw
364/// [`WorkEvidenceRef::kind`] string remains only as opaque provenance/display
365/// and is never re-read to classify evidence for the satisfaction decision.
366#[derive(
367    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
368)]
369#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
370#[serde(rename_all = "snake_case")]
371pub enum WorkEvidenceKind {
372    /// Generic / self-attested evidence that does not satisfy any
373    /// confirmation policy on its own.
374    #[default]
375    SelfAttest,
376    HostConfirmation,
377    PrincipalConfirmation,
378    SupervisorConfirmation,
379    ReviewerConfirmation,
380}
381
382impl WorkEvidenceKind {
383    pub(crate) fn to_machine(self) -> wg_dsl::WorkEvidenceKind {
384        match self {
385            Self::SelfAttest => wg_dsl::WorkEvidenceKind::SelfAttest,
386            Self::HostConfirmation => wg_dsl::WorkEvidenceKind::HostConfirmation,
387            Self::PrincipalConfirmation => wg_dsl::WorkEvidenceKind::PrincipalConfirmation,
388            Self::SupervisorConfirmation => wg_dsl::WorkEvidenceKind::SupervisorConfirmation,
389            Self::ReviewerConfirmation => wg_dsl::WorkEvidenceKind::ReviewerConfirmation,
390        }
391    }
392
393    /// Parse a reserved confirmation classification out of the opaque
394    /// provenance/display [`WorkEvidenceRef::kind`] string at the ingress
395    /// boundary. The recognized reserved literals map 1:1 onto a confirmation
396    /// variant; the generic `"self_attest"` literal and every other string
397    /// (including the empty string) carry no reserved confirmation and yield
398    /// `None`. This is the single place the opaque string is classified — every
399    /// downstream decision reads the typed classification, not the string.
400    pub(crate) fn from_kind_str(kind: &str) -> Option<Self> {
401        match kind {
402            "host_confirmation" => Some(Self::HostConfirmation),
403            "principal_confirmation" => Some(Self::PrincipalConfirmation),
404            "supervisor_confirmation" => Some(Self::SupervisorConfirmation),
405            "reviewer_confirmation" => Some(Self::ReviewerConfirmation),
406            _ => None,
407        }
408    }
409
410    /// Whether this classification is a reserved confirmation that may only be
411    /// stamped by the trusted goal-confirm producer. Generic
412    /// [`WorkEvidenceKind::SelfAttest`] evidence is never reserved.
413    pub(crate) fn is_reserved_confirmation(self) -> bool {
414        !matches!(self, Self::SelfAttest)
415    }
416
417    /// Project the typed classification into the machine-owned confirmation
418    /// observation the `WorkGraphLifecycleMachine` consumes. Generic
419    /// self-attested evidence projects to the `Other` observation; the
420    /// empty-display case is handled separately by the caller.
421    pub(crate) fn to_confirmation_observation(self) -> wg_dsl::WorkConfirmationEvidenceObservation {
422        match self {
423            Self::SelfAttest => wg_dsl::WorkConfirmationEvidenceObservation::Other,
424            Self::HostConfirmation => wg_dsl::WorkConfirmationEvidenceObservation::HostConfirmation,
425            Self::PrincipalConfirmation => {
426                wg_dsl::WorkConfirmationEvidenceObservation::PrincipalConfirmation
427            }
428            Self::SupervisorConfirmation => {
429                wg_dsl::WorkConfirmationEvidenceObservation::SupervisorConfirmation
430            }
431            Self::ReviewerConfirmation => {
432                wg_dsl::WorkConfirmationEvidenceObservation::ReviewerConfirmation
433            }
434        }
435    }
436}
437
438#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
439#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
440pub struct WorkEvidenceRef {
441    /// Opaque provenance/display label for the evidence. It is parsed into the
442    /// typed confirmation classification at the ingress boundary only (see
443    /// [`WorkEvidenceRef::confirmation_classification`]); no completion-policy
444    /// satisfaction decision re-reads this string. The typed
445    /// [`WorkEvidenceRef::confirmation_kind`] is the authoritative carrier.
446    pub kind: String,
447    pub id: String,
448    #[serde(default, skip_serializing_if = "Option::is_none")]
449    pub label: Option<String>,
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    pub summary: Option<String>,
452    /// Typed confirmation classification set by the trusted producer. Drives the
453    /// machine-owned completion-policy satisfaction decision. Generic evidence
454    /// leaves this unset (treated as `SelfAttest`).
455    #[serde(default, skip_serializing_if = "Option::is_none")]
456    pub confirmation_kind: Option<WorkEvidenceKind>,
457    /// Typed identity of the confirming owner for supervisor/reviewer
458    /// confirmations. Set by the trusted producer; the machine records distinct
459    /// owners per confirmation kind.
460    #[serde(default, skip_serializing_if = "Option::is_none")]
461    pub confirming_owner_key: Option<WorkOwnerKey>,
462}
463
464impl WorkEvidenceRef {
465    /// The effective typed confirmation classification carried by this evidence,
466    /// considering BOTH carriers: the typed [`WorkEvidenceRef::confirmation_kind`]
467    /// field (authoritative when set) and the reserved confirmation literals that
468    /// may be encoded only in the opaque [`WorkEvidenceRef::kind`] string at
469    /// ingress. Returns `None` for generic self-attested evidence.
470    ///
471    /// This is the single typed read every confirmation decision uses, so a
472    /// reserved classification surfaces regardless of which carrier the caller
473    /// supplied — closing the gap where the machine honored a forged
474    /// `confirmation_kind` while the guards inspected only the `kind` string.
475    pub(crate) fn confirmation_classification(&self) -> Option<WorkEvidenceKind> {
476        self.confirmation_kind
477            .filter(|kind| kind.is_reserved_confirmation())
478            .or_else(|| WorkEvidenceKind::from_kind_str(&self.kind))
479    }
480}
481
482#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
483#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
484pub struct WorkItemRef {
485    pub realm_id: String,
486    pub namespace: WorkNamespace,
487    pub item_id: WorkItemId,
488}
489
490#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
491#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
492#[serde(tag = "kind", rename_all = "snake_case")]
493pub enum WorkAttentionTarget {
494    Session { session_id: SessionId },
495    LoweredOwner { owner_key: WorkOwnerKey },
496}
497
498impl WorkAttentionTarget {
499    pub fn owner_key(&self) -> Result<WorkOwnerKey, WorkGraphError> {
500        match self {
501            Self::Session { session_id } => WorkOwnerKey::session(session_id.to_string()),
502            Self::LoweredOwner { owner_key } => Ok(owner_key.clone()),
503        }
504    }
505
506    /// Canonical query/uniqueness key for an attention target. This string is
507    /// persisted as an indexed store column and backs the
508    /// active-binding-per-target invariant, so it must be stable and
509    /// injective over the target vocabulary.
510    pub fn target_key(&self) -> String {
511        match self {
512            Self::Session { session_id } => format!("session:{session_id}"),
513            Self::LoweredOwner { owner_key } => {
514                format!("owner:{}:{}", owner_key.kind.as_str(), owner_key.id)
515            }
516        }
517    }
518}
519
520#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
521#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
522#[serde(tag = "kind", rename_all = "snake_case")]
523pub enum GoalAttentionTarget {
524    Session { session_id: SessionId },
525    Owner { owner_key: WorkOwnerKey },
526}
527
528impl GoalAttentionTarget {
529    pub fn to_attention_target(&self) -> WorkAttentionTarget {
530        match self {
531            Self::Session { session_id } => WorkAttentionTarget::Session {
532                session_id: session_id.clone(),
533            },
534            Self::Owner { owner_key } => WorkAttentionTarget::LoweredOwner {
535                owner_key: owner_key.clone(),
536            },
537        }
538    }
539}
540
541#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
542#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
543#[serde(rename_all = "snake_case")]
544pub enum WorkAttentionMode {
545    #[default]
546    Pursue,
547    Coordinate,
548    Review,
549    Falsify,
550    Judge,
551    Observe,
552}
553
554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
555#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
556#[serde(tag = "state", rename_all = "snake_case")]
557// `status_key()` below mirrors this serde tag vocabulary; keep them in sync.
558pub enum WorkAttentionStatus {
559    #[default]
560    Active,
561    Paused {
562        #[serde(default, skip_serializing_if = "Option::is_none")]
563        until: Option<DateTime<Utc>>,
564    },
565    Superseded,
566    Stopped,
567}
568
569impl WorkAttentionStatus {
570    /// Canonical status key persisted as an indexed store column (SQL query
571    /// pushdown + the active-binding-per-target occupancy guard). Mirrors the
572    /// serde `state` tag vocabulary.
573    pub fn status_key(&self) -> &'static str {
574        match self {
575            Self::Active => "active",
576            Self::Paused { .. } => "paused",
577            Self::Superseded => "superseded",
578            Self::Stopped => "stopped",
579        }
580    }
581
582    /// Terminal statuses are eligible for store pruning: no lifecycle
583    /// transition leads out of them.
584    pub fn is_terminal(&self) -> bool {
585        matches!(self, Self::Superseded | Self::Stopped)
586    }
587}
588
589#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
590#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
591#[serde(rename_all = "snake_case")]
592pub enum AttentionDelegatedAuthority {
593    #[default]
594    AddEvidence,
595    CloseOwnReviewItem,
596    RequestClosure,
597    CloseIfPolicyAllows,
598}
599
600#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
601#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
602pub struct AttentionProjectionPolicy {
603    #[serde(default = "default_projection_max_text_chars")]
604    pub max_text_chars: u32,
605    #[serde(default = "default_include_parent_context")]
606    pub include_parent_context: bool,
607}
608
609fn default_include_parent_context() -> bool {
610    true
611}
612
613impl Default for AttentionProjectionPolicy {
614    fn default() -> Self {
615        Self {
616            max_text_chars: default_projection_max_text_chars(),
617            include_parent_context: true,
618        }
619    }
620}
621
622fn default_projection_max_text_chars() -> u32 {
623    4096
624}
625
626#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
627#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
628pub struct WorkAttentionBinding {
629    pub binding_id: WorkAttentionBindingId,
630    pub work_ref: WorkItemRef,
631    pub target: WorkAttentionTarget,
632    pub mode: WorkAttentionMode,
633    pub status: WorkAttentionStatus,
634    #[serde(default = "default_work_attention_machine_state")]
635    #[cfg_attr(feature = "schema", schemars(with = "WorkAttentionMachineStateSchema"))]
636    pub machine_state: WorkAttentionMachineState,
637    pub delegated_authority: AttentionDelegatedAuthority,
638    #[serde(default)]
639    pub projection_policy: AttentionProjectionPolicy,
640    pub created_at: DateTime<Utc>,
641    pub updated_at: DateTime<Utc>,
642}
643
644#[cfg(feature = "schema")]
645#[derive(schemars::JsonSchema)]
646#[allow(dead_code)]
647struct WorkAttentionMachineStateSchema {
648    lifecycle_phase: String,
649    revision: u64,
650    paused_until_utc_ms: Option<u64>,
651    superseded_by_binding_key: Option<String>,
652    terminal_at_utc_ms: Option<u64>,
653}
654
655fn default_work_attention_machine_state() -> WorkAttentionMachineState {
656    WorkAttentionMachineState::default()
657}
658
659#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
660pub struct WorkItem {
661    pub id: WorkItemId,
662    pub realm_id: String,
663    pub namespace: WorkNamespace,
664    pub title: String,
665    #[serde(default, skip_serializing_if = "Option::is_none")]
666    pub description: Option<String>,
667    pub status: WorkStatus,
668    #[serde(default)]
669    pub completion_policy: WorkCompletionPolicy,
670    pub priority: WorkPriority,
671    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
672    pub labels: BTreeSet<String>,
673    #[serde(default, skip_serializing_if = "Option::is_none")]
674    pub owner: Option<WorkOwner>,
675    #[serde(default, skip_serializing_if = "Option::is_none")]
676    pub claim: Option<WorkClaim>,
677    pub machine_state: WorkGraphMachineState,
678    pub revision: u64,
679    #[serde(default, skip_serializing_if = "Option::is_none")]
680    pub due_at: Option<DateTime<Utc>>,
681    #[serde(default, skip_serializing_if = "Option::is_none")]
682    pub not_before: Option<DateTime<Utc>>,
683    #[serde(default, skip_serializing_if = "Option::is_none")]
684    pub snoozed_until: Option<DateTime<Utc>>,
685    pub created_at: DateTime<Utc>,
686    pub updated_at: DateTime<Utc>,
687    #[serde(default, skip_serializing_if = "Option::is_none")]
688    pub terminal_at: Option<DateTime<Utc>>,
689    #[serde(default, skip_serializing_if = "Vec::is_empty")]
690    pub external_refs: Vec<ExternalWorkRef>,
691    #[serde(default, skip_serializing_if = "Vec::is_empty")]
692    pub evidence_refs: Vec<WorkEvidenceRef>,
693}
694
695#[derive(Deserialize)]
696struct WorkItemWire {
697    id: WorkItemId,
698    realm_id: String,
699    namespace: WorkNamespace,
700    title: String,
701    #[serde(default)]
702    description: Option<String>,
703    status: WorkStatus,
704    #[serde(default)]
705    completion_policy: WorkCompletionPolicy,
706    priority: WorkPriority,
707    #[serde(default)]
708    labels: BTreeSet<String>,
709    #[serde(default)]
710    owner: Option<WorkOwner>,
711    #[serde(default)]
712    claim: Option<WorkClaim>,
713    #[serde(default)]
714    machine_state: Option<WorkGraphMachineState>,
715    revision: u64,
716    #[serde(default)]
717    due_at: Option<DateTime<Utc>>,
718    #[serde(default)]
719    not_before: Option<DateTime<Utc>>,
720    #[serde(default)]
721    snoozed_until: Option<DateTime<Utc>>,
722    created_at: DateTime<Utc>,
723    updated_at: DateTime<Utc>,
724    #[serde(default)]
725    terminal_at: Option<DateTime<Utc>>,
726    #[serde(default)]
727    external_refs: Vec<ExternalWorkRef>,
728    #[serde(default)]
729    evidence_refs: Vec<WorkEvidenceRef>,
730}
731
732impl<'de> Deserialize<'de> for WorkItem {
733    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
734    where
735        D: serde::Deserializer<'de>,
736    {
737        let mut wire = WorkItemWire::deserialize(deserializer)?;
738        let machine_state = wire.machine_state.take().ok_or_else(|| {
739            serde::de::Error::custom(
740                "WorkItem is missing `machine_state`: lifecycle/revision authority is machine-owned \
741                 and cannot be reconstructed from projected fields",
742            )
743        })?;
744        Ok(Self {
745            id: wire.id,
746            realm_id: wire.realm_id,
747            namespace: wire.namespace,
748            title: wire.title,
749            description: wire.description,
750            status: wire.status,
751            completion_policy: wire.completion_policy,
752            priority: wire.priority,
753            labels: wire.labels,
754            owner: wire.owner,
755            claim: wire.claim,
756            machine_state,
757            revision: wire.revision,
758            due_at: wire.due_at,
759            not_before: wire.not_before,
760            snoozed_until: wire.snoozed_until,
761            created_at: wire.created_at,
762            updated_at: wire.updated_at,
763            terminal_at: wire.terminal_at,
764            external_refs: wire.external_refs,
765            evidence_refs: wire.evidence_refs,
766        })
767    }
768}
769
770#[cfg(feature = "schema")]
771impl schemars::JsonSchema for WorkItem {
772    // NOTE (K21): this manual schema inlines the composite field shapes
773    // (`owner`, `claim`, `completion_policy`, `external_refs`,
774    // `evidence_refs`). The SDK generator's inline-object promotion pass
775    // (tools/sdk-codegen/generate.py) dedupes them by structural content
776    // against the derived sibling schemas (`WorkOwnerKey`,
777    // `WorkCompletionPolicy`, `WorkEvidenceRef`); keep these inline copies
778    // structurally identical to the derived shapes or the generator will
779    // mint `WorkItem*`-named twins (visible in the regen diff and the
780    // promotion report).
781    fn schema_name() -> std::borrow::Cow<'static, str> {
782        "WorkItem".into()
783    }
784
785    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
786        schemars::json_schema!({
787            "type": "object",
788            "required": [
789                "id",
790                "realm_id",
791                "namespace",
792                "title",
793                "status",
794                "completion_policy",
795                "priority",
796                "machine_state",
797                "revision",
798                "created_at",
799                "updated_at"
800            ],
801            "properties": {
802                "id": { "type": "string" },
803                "realm_id": { "type": "string" },
804                "namespace": { "type": "string" },
805                "title": { "type": "string" },
806                "description": { "type": ["string", "null"] },
807                "status": {
808                    "type": "string",
809                    "enum": ["open", "in_progress", "blocked", "completed", "cancelled", "failed"]
810                },
811                "completion_policy": {
812                    "oneOf": [
813                        {
814                            "type": "object",
815                            "required": ["kind"],
816                            "properties": { "kind": { "type": "string", "const": "self_attest" } }
817                        },
818                        {
819                            "type": "object",
820                            "required": ["kind"],
821                            "properties": { "kind": { "type": "string", "const": "host_confirmed" } }
822                        },
823                        {
824                            "type": "object",
825                            "required": ["kind"],
826                            "properties": { "kind": { "type": "string", "const": "principal_confirmed" } }
827                        },
828                        {
829                            "type": "object",
830                            "required": ["kind", "owner_key"],
831                            "properties": {
832                                "kind": { "type": "string", "const": "supervisor" },
833                                "owner_key": {
834                                    "type": "object",
835                                    "required": ["kind", "id"],
836                                    "properties": {
837                                        "kind": {
838                                            "type": "string",
839                                            "enum": ["principal", "agent", "session", "mob", "label"]
840                                        },
841                                        "id": { "type": "string" }
842                                    }
843                                }
844                            }
845                        },
846                        {
847                            "type": "object",
848                            "required": ["kind", "threshold"],
849                            "properties": {
850                                "kind": { "type": "string", "const": "reviewer_quorum" },
851                                "threshold": { "type": "integer", "format": "uint16", "minimum": 1, "maximum": 64 }
852                            }
853                        }
854                    ]
855                },
856                "priority": {
857                    "type": "string",
858                    "enum": ["low", "medium", "high"]
859                },
860                "labels": {
861                    "type": "array",
862                    "uniqueItems": true,
863                    "items": { "type": "string" }
864                },
865                "owner": {
866                    "anyOf": [
867                        {
868                            "type": "object",
869                            "required": ["key"],
870                            "properties": {
871                                "key": {
872                                    "type": "object",
873                                    "required": ["kind", "id"],
874                                    "properties": {
875                                        "kind": {
876                                            "type": "string",
877                                            "enum": ["principal", "agent", "session", "mob", "label"]
878                                        },
879                                        "id": { "type": "string" }
880                                    }
881                                },
882                                "display_name": { "type": ["string", "null"] }
883                            }
884                        },
885                        { "type": "null" }
886                    ]
887                },
888                "claim": {
889                    "anyOf": [
890                        {
891                            "type": "object",
892                            "required": ["owner", "claimed_at"],
893                            "properties": {
894                                "owner": {
895                                    "type": "object",
896                                    "required": ["key"],
897                                    "properties": {
898                                        "key": {
899                                            "type": "object",
900                                            "required": ["kind", "id"],
901                                            "properties": {
902                                                "kind": {
903                                                    "type": "string",
904                                                    "enum": ["principal", "agent", "session", "mob", "label"]
905                                                },
906                                                "id": { "type": "string" }
907                                            }
908                                        },
909                                        "display_name": { "type": ["string", "null"] }
910                                    }
911                                },
912                                "claimed_at": { "type": "string", "format": "date-time" },
913                                "lease_expires_at": { "type": ["string", "null"], "format": "date-time" }
914                            }
915                        },
916                        { "type": "null" }
917                    ]
918                },
919                "machine_state": {
920                    "type": "object",
921                    "description": "Catalog-generated WorkGraphLifecycleMachine state projection."
922                },
923                "revision": { "type": "integer", "format": "uint64", "minimum": 0 },
924                "due_at": { "type": ["string", "null"], "format": "date-time" },
925                "not_before": { "type": ["string", "null"], "format": "date-time" },
926                "snoozed_until": { "type": ["string", "null"], "format": "date-time" },
927                "created_at": { "type": "string", "format": "date-time" },
928                "updated_at": { "type": "string", "format": "date-time" },
929                "terminal_at": { "type": ["string", "null"], "format": "date-time" },
930                "external_refs": {
931                    "type": "array",
932                    "items": {
933                        "type": "object",
934                        "required": ["kind", "id"],
935                        "properties": {
936                            "kind": { "type": "string" },
937                            "id": { "type": "string" },
938                            "url": { "type": ["string", "null"] }
939                        }
940                    }
941                },
942                "evidence_refs": {
943                    "type": "array",
944                    "items": {
945                        "type": "object",
946                        "required": ["kind", "id"],
947                        "properties": {
948                            "kind": { "type": "string" },
949                            "id": { "type": "string" },
950                            "label": { "type": ["string", "null"] },
951                            "summary": { "type": ["string", "null"] },
952                            "confirmation_kind": {
953                                "anyOf": [
954                                    {
955                                        "oneOf": [
956                                            {
957                                                "type": "string",
958                                                "enum": [
959                                                    "host_confirmation",
960                                                    "principal_confirmation",
961                                                    "supervisor_confirmation",
962                                                    "reviewer_confirmation"
963                                                ]
964                                            },
965                                            { "type": "string", "const": "self_attest" }
966                                        ]
967                                    },
968                                    { "type": "null" }
969                                ]
970                            },
971                            "confirming_owner_key": {
972                                "anyOf": [
973                                    {
974                                        "type": "object",
975                                        "required": ["kind", "id"],
976                                        "properties": {
977                                            "kind": {
978                                                "type": "string",
979                                                "enum": ["principal", "agent", "session", "mob", "label"]
980                                            },
981                                            "id": { "type": "string" }
982                                        }
983                                    },
984                                    { "type": "null" }
985                                ]
986                            }
987                        }
988                    }
989                }
990            }
991        })
992    }
993}
994
995pub(crate) fn work_lifecycle_state_from_status(status: WorkStatus) -> wg_dsl::WorkLifecycleState {
996    match status {
997        WorkStatus::Open => wg_dsl::WorkLifecycleState::Open,
998        WorkStatus::InProgress => wg_dsl::WorkLifecycleState::InProgress,
999        WorkStatus::Blocked => wg_dsl::WorkLifecycleState::Blocked,
1000        WorkStatus::Completed => wg_dsl::WorkLifecycleState::Completed,
1001        WorkStatus::Cancelled => wg_dsl::WorkLifecycleState::Cancelled,
1002        WorkStatus::Failed => wg_dsl::WorkLifecycleState::Failed,
1003    }
1004}
1005
1006pub(crate) fn work_owner_kind_to_machine(kind: WorkOwnerKind) -> wg_dsl::WorkOwnerKind {
1007    match kind {
1008        WorkOwnerKind::Principal => wg_dsl::WorkOwnerKind::Principal,
1009        WorkOwnerKind::Agent => wg_dsl::WorkOwnerKind::Agent,
1010        WorkOwnerKind::Session => wg_dsl::WorkOwnerKind::Session,
1011        WorkOwnerKind::Mob => wg_dsl::WorkOwnerKind::Mob,
1012        WorkOwnerKind::Label => wg_dsl::WorkOwnerKind::Label,
1013    }
1014}
1015
1016pub(crate) fn work_owner_key_to_machine(owner: &WorkOwnerKey) -> wg_dsl::WorkOwnerKey {
1017    wg_dsl::WorkOwnerKey {
1018        kind: work_owner_kind_to_machine(owner.kind),
1019        id: owner.id.clone(),
1020    }
1021}
1022
1023fn work_owner_key_from_machine(owner: wg_dsl::WorkOwnerKey) -> WorkOwnerKey {
1024    let kind = match owner.kind {
1025        wg_dsl::WorkOwnerKind::Principal => WorkOwnerKind::Principal,
1026        wg_dsl::WorkOwnerKind::Agent => WorkOwnerKind::Agent,
1027        wg_dsl::WorkOwnerKind::Session => WorkOwnerKind::Session,
1028        wg_dsl::WorkOwnerKind::Mob => WorkOwnerKind::Mob,
1029        wg_dsl::WorkOwnerKind::Label => WorkOwnerKind::Label,
1030    };
1031    WorkOwnerKey { kind, id: owner.id }
1032}
1033
1034#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1035#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1036pub struct WorkEdge {
1037    pub realm_id: String,
1038    pub namespace: WorkNamespace,
1039    pub kind: WorkEdgeKind,
1040    pub from_id: WorkItemId,
1041    pub to_id: WorkItemId,
1042    pub created_at: DateTime<Utc>,
1043}
1044
1045#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1046#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1047#[serde(rename_all = "snake_case")]
1048pub enum WorkGraphEventKind {
1049    Created,
1050    Updated,
1051    Claimed,
1052    Released,
1053    Blocked,
1054    Closed,
1055    Linked,
1056    EvidenceAdded,
1057    AttentionCreated,
1058    AttentionUpdated,
1059}
1060
1061#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1062#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1063pub struct WorkGraphEvent {
1064    #[serde(default, skip_serializing_if = "Option::is_none")]
1065    pub seq: Option<i64>,
1066    pub realm_id: String,
1067    pub namespace: WorkNamespace,
1068    #[serde(default, skip_serializing_if = "Option::is_none")]
1069    pub item_id: Option<WorkItemId>,
1070    pub kind: WorkGraphEventKind,
1071    pub at: DateTime<Utc>,
1072    #[serde(default, skip_serializing_if = "Value::is_null")]
1073    pub payload: Value,
1074}
1075
1076impl WorkGraphEvent {
1077    pub fn item(
1078        realm_id: String,
1079        namespace: WorkNamespace,
1080        item_id: WorkItemId,
1081        kind: WorkGraphEventKind,
1082        at: DateTime<Utc>,
1083        payload: Value,
1084    ) -> Self {
1085        Self {
1086            seq: None,
1087            realm_id,
1088            namespace,
1089            item_id: Some(item_id),
1090            kind,
1091            at,
1092            payload,
1093        }
1094    }
1095
1096    pub fn graph(
1097        realm_id: String,
1098        namespace: WorkNamespace,
1099        kind: WorkGraphEventKind,
1100        at: DateTime<Utc>,
1101        payload: Value,
1102    ) -> Self {
1103        Self {
1104            seq: None,
1105            realm_id,
1106            namespace,
1107            item_id: None,
1108            kind,
1109            at,
1110            payload,
1111        }
1112    }
1113}
1114
1115#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1116#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1117pub struct CreateWorkItemRequest {
1118    #[serde(default, skip_serializing_if = "Option::is_none")]
1119    pub realm_id: Option<String>,
1120    #[serde(default, skip_serializing_if = "Option::is_none")]
1121    pub namespace: Option<WorkNamespace>,
1122    pub title: String,
1123    #[serde(default, skip_serializing_if = "Option::is_none")]
1124    pub description: Option<String>,
1125    #[serde(default)]
1126    pub priority: WorkPriority,
1127    #[serde(default)]
1128    pub completion_policy: WorkCompletionPolicy,
1129    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1130    pub labels: BTreeSet<String>,
1131    #[serde(default, skip_serializing_if = "Option::is_none")]
1132    pub due_at: Option<DateTime<Utc>>,
1133    #[serde(default, skip_serializing_if = "Option::is_none")]
1134    pub not_before: Option<DateTime<Utc>>,
1135    #[serde(default, skip_serializing_if = "Option::is_none")]
1136    pub snoozed_until: Option<DateTime<Utc>>,
1137    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1138    pub external_refs: Vec<ExternalWorkRef>,
1139    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1140    pub evidence_refs: Vec<WorkEvidenceRef>,
1141    #[serde(default, skip_serializing_if = "Option::is_none")]
1142    pub status: Option<WorkStatus>,
1143}
1144
1145#[derive(Debug, Clone, Serialize, Deserialize)]
1146#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1147pub struct UpdateWorkItemRequest {
1148    pub id: WorkItemId,
1149    #[serde(default, skip_serializing_if = "Option::is_none")]
1150    pub realm_id: Option<String>,
1151    #[serde(default, skip_serializing_if = "Option::is_none")]
1152    pub namespace: Option<WorkNamespace>,
1153    pub expected_revision: u64,
1154    #[serde(default, skip_serializing_if = "Option::is_none")]
1155    pub title: Option<String>,
1156    #[serde(default, skip_serializing_if = "Option::is_none")]
1157    pub description: Option<String>,
1158    #[serde(default, skip_serializing_if = "Option::is_none")]
1159    pub priority: Option<WorkPriority>,
1160    #[serde(default, skip_serializing_if = "Option::is_none")]
1161    pub completion_policy: Option<WorkCompletionPolicy>,
1162    #[serde(default, skip_serializing_if = "Option::is_none")]
1163    pub labels: Option<BTreeSet<String>>,
1164    #[serde(default, skip_serializing_if = "Option::is_none")]
1165    pub due_at: Option<DateTime<Utc>>,
1166    #[serde(default, skip_serializing_if = "Option::is_none")]
1167    pub not_before: Option<DateTime<Utc>>,
1168    #[serde(default, skip_serializing_if = "Option::is_none")]
1169    pub snoozed_until: Option<DateTime<Utc>>,
1170    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1171    pub external_refs: Vec<ExternalWorkRef>,
1172}
1173
1174#[derive(Debug, Clone, Serialize, Deserialize)]
1175#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1176pub struct PolicyEscalateRequest {
1177    pub id: WorkItemId,
1178    #[serde(default, skip_serializing_if = "Option::is_none")]
1179    pub realm_id: Option<String>,
1180    #[serde(default, skip_serializing_if = "Option::is_none")]
1181    pub namespace: Option<WorkNamespace>,
1182    pub expected_revision: u64,
1183    pub authority_projection: AttentionContextProjection,
1184    pub completion_policy: WorkCompletionPolicy,
1185}
1186
1187#[derive(Debug, Clone, Serialize, Deserialize)]
1188#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1189pub struct ClaimWorkItemRequest {
1190    pub id: WorkItemId,
1191    #[serde(default, skip_serializing_if = "Option::is_none")]
1192    pub realm_id: Option<String>,
1193    #[serde(default, skip_serializing_if = "Option::is_none")]
1194    pub namespace: Option<WorkNamespace>,
1195    pub expected_revision: u64,
1196    pub owner: WorkOwner,
1197    #[serde(default, skip_serializing_if = "Option::is_none")]
1198    pub lease_seconds: Option<u64>,
1199    #[serde(default, skip_serializing_if = "Option::is_none")]
1200    pub lease_expires_at: Option<DateTime<Utc>>,
1201}
1202
1203#[derive(Debug, Clone, Serialize, Deserialize)]
1204#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1205pub struct ReleaseWorkItemRequest {
1206    pub id: WorkItemId,
1207    #[serde(default, skip_serializing_if = "Option::is_none")]
1208    pub realm_id: Option<String>,
1209    #[serde(default, skip_serializing_if = "Option::is_none")]
1210    pub namespace: Option<WorkNamespace>,
1211    pub expected_revision: u64,
1212}
1213
1214#[derive(Debug, Clone, Serialize, Deserialize)]
1215#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1216pub struct CloseWorkItemRequest {
1217    pub id: WorkItemId,
1218    #[serde(default, skip_serializing_if = "Option::is_none")]
1219    pub realm_id: Option<String>,
1220    #[serde(default, skip_serializing_if = "Option::is_none")]
1221    pub namespace: Option<WorkNamespace>,
1222    pub expected_revision: u64,
1223    #[serde(default = "default_terminal_status")]
1224    pub status: WorkStatus,
1225}
1226
1227fn default_terminal_status() -> WorkStatus {
1228    WorkStatus::Completed
1229}
1230
1231#[derive(Debug, Clone, Serialize, Deserialize)]
1232#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1233pub struct LinkWorkItemsRequest {
1234    #[serde(default, skip_serializing_if = "Option::is_none")]
1235    pub realm_id: Option<String>,
1236    #[serde(default, skip_serializing_if = "Option::is_none")]
1237    pub namespace: Option<WorkNamespace>,
1238    pub kind: WorkEdgeKind,
1239    pub from_id: WorkItemId,
1240    pub to_id: WorkItemId,
1241}
1242
1243#[derive(Debug, Clone, Serialize, Deserialize)]
1244#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1245pub struct AddEvidenceRequest {
1246    pub id: WorkItemId,
1247    #[serde(default, skip_serializing_if = "Option::is_none")]
1248    pub realm_id: Option<String>,
1249    #[serde(default, skip_serializing_if = "Option::is_none")]
1250    pub namespace: Option<WorkNamespace>,
1251    pub expected_revision: u64,
1252    pub evidence: WorkEvidenceRef,
1253}
1254
1255#[derive(Debug, Clone, Serialize, Deserialize)]
1256#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1257pub struct GoalCreateRequest {
1258    #[serde(default, skip_serializing_if = "Option::is_none")]
1259    pub realm_id: Option<String>,
1260    #[serde(default, skip_serializing_if = "Option::is_none")]
1261    pub namespace: Option<WorkNamespace>,
1262    pub title: String,
1263    #[serde(default, skip_serializing_if = "Option::is_none")]
1264    pub description: Option<String>,
1265    pub target: GoalAttentionTarget,
1266    #[serde(default)]
1267    pub mode: WorkAttentionMode,
1268    #[serde(default)]
1269    pub completion_policy: WorkCompletionPolicy,
1270    #[serde(default)]
1271    pub delegated_authority: AttentionDelegatedAuthority,
1272    #[serde(default)]
1273    pub projection_policy: AttentionProjectionPolicy,
1274}
1275
1276#[derive(Debug, Clone, Serialize, Deserialize)]
1277#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1278pub struct PublicGoalCreateRequest {
1279    #[serde(default, skip_serializing_if = "Option::is_none")]
1280    pub realm_id: Option<String>,
1281    #[serde(default, skip_serializing_if = "Option::is_none")]
1282    pub namespace: Option<WorkNamespace>,
1283    pub title: String,
1284    #[serde(default, skip_serializing_if = "Option::is_none")]
1285    pub description: Option<String>,
1286    pub target: GoalAttentionTarget,
1287    #[serde(default)]
1288    pub mode: WorkAttentionMode,
1289    #[serde(default)]
1290    pub completion_policy: PublicGoalCompletionPolicy,
1291    #[serde(default)]
1292    pub delegated_authority: AttentionDelegatedAuthority,
1293    #[serde(default)]
1294    pub projection_policy: AttentionProjectionPolicy,
1295}
1296
1297impl From<PublicGoalCreateRequest> for GoalCreateRequest {
1298    fn from(request: PublicGoalCreateRequest) -> Self {
1299        Self {
1300            realm_id: request.realm_id,
1301            namespace: request.namespace,
1302            title: request.title,
1303            description: request.description,
1304            target: request.target,
1305            mode: request.mode,
1306            completion_policy: request.completion_policy.into(),
1307            delegated_authority: request.delegated_authority,
1308            projection_policy: request.projection_policy,
1309        }
1310    }
1311}
1312
1313#[derive(Debug, Clone, Serialize, Deserialize)]
1314#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1315pub struct GoalCreateResult {
1316    pub item: WorkItem,
1317    pub attention: WorkAttentionBinding,
1318}
1319
1320#[derive(Debug, Clone, Serialize, Deserialize)]
1321#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1322pub struct GoalStatusRequest {
1323    pub binding_id: WorkAttentionBindingId,
1324    #[serde(default, skip_serializing_if = "Option::is_none")]
1325    pub realm_id: Option<String>,
1326    #[serde(default, skip_serializing_if = "Option::is_none")]
1327    pub namespace: Option<WorkNamespace>,
1328}
1329
1330#[derive(Debug, Clone, Serialize, Deserialize)]
1331#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1332pub struct GoalStatusResult {
1333    pub item: WorkItem,
1334    pub attention: WorkAttentionBinding,
1335}
1336
1337#[derive(Debug, Clone, Serialize, Deserialize)]
1338#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1339pub struct GoalConfirmRequest {
1340    pub binding_id: WorkAttentionBindingId,
1341    #[serde(default, skip_serializing_if = "Option::is_none")]
1342    pub realm_id: Option<String>,
1343    #[serde(default, skip_serializing_if = "Option::is_none")]
1344    pub namespace: Option<WorkNamespace>,
1345    pub expected_revision: u64,
1346    pub evidence: WorkEvidenceRef,
1347    #[serde(skip)]
1348    #[cfg_attr(feature = "schema", schemars(skip))]
1349    pub principal: Option<WorkOwnerKey>,
1350    #[serde(skip)]
1351    #[cfg_attr(feature = "schema", schemars(skip))]
1352    pub trusted_principal: Option<WorkOwnerKey>,
1353}
1354
1355impl GoalConfirmRequest {
1356    /// Promote an already-authenticated host principal into the service authority field.
1357    pub fn with_trusted_principal(mut self, principal: Option<WorkOwnerKey>) -> Self {
1358        if self.trusted_principal.is_none() {
1359            self.trusted_principal = principal;
1360        }
1361        self
1362    }
1363}
1364
1365#[derive(Debug, Clone, Serialize, Deserialize)]
1366#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1367pub struct GoalConfirmResult {
1368    pub item: WorkItem,
1369    pub attention: WorkAttentionBinding,
1370}
1371
1372#[derive(Debug, Clone, Serialize, Deserialize)]
1373#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1374pub struct GoalRequestCloseRequest {
1375    pub binding_id: WorkAttentionBindingId,
1376    #[serde(default, skip_serializing_if = "Option::is_none")]
1377    pub realm_id: Option<String>,
1378    #[serde(default, skip_serializing_if = "Option::is_none")]
1379    pub namespace: Option<WorkNamespace>,
1380    pub expected_revision: u64,
1381    #[serde(default)]
1382    pub status: GoalTerminalStatus,
1383}
1384
1385#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1386#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1387#[serde(rename_all = "snake_case")]
1388pub enum GoalTerminalStatus {
1389    #[default]
1390    Completed,
1391    Cancelled,
1392    Failed,
1393}
1394
1395impl From<GoalTerminalStatus> for WorkStatus {
1396    fn from(status: GoalTerminalStatus) -> Self {
1397        match status {
1398            GoalTerminalStatus::Completed => Self::Completed,
1399            GoalTerminalStatus::Cancelled => Self::Cancelled,
1400            GoalTerminalStatus::Failed => Self::Failed,
1401        }
1402    }
1403}
1404
1405#[derive(Debug, Clone, Serialize, Deserialize)]
1406#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1407pub struct PublicGoalRequestCloseRequest {
1408    pub binding_id: WorkAttentionBindingId,
1409    #[serde(default, skip_serializing_if = "Option::is_none")]
1410    pub realm_id: Option<String>,
1411    #[serde(default, skip_serializing_if = "Option::is_none")]
1412    pub namespace: Option<WorkNamespace>,
1413    pub expected_revision: u64,
1414    #[serde(default)]
1415    pub status: GoalTerminalStatus,
1416}
1417
1418impl From<PublicGoalRequestCloseRequest> for GoalRequestCloseRequest {
1419    fn from(request: PublicGoalRequestCloseRequest) -> Self {
1420        Self {
1421            binding_id: request.binding_id,
1422            realm_id: request.realm_id,
1423            namespace: request.namespace,
1424            expected_revision: request.expected_revision,
1425            status: request.status,
1426        }
1427    }
1428}
1429
1430#[derive(Debug, Clone, Serialize, Deserialize)]
1431#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1432pub struct GoalRequestCloseResult {
1433    pub item: WorkItem,
1434    pub attention: WorkAttentionBinding,
1435}
1436
1437#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1438#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1439pub struct AttentionListRequest {
1440    #[serde(default, skip_serializing_if = "Option::is_none")]
1441    pub realm_id: Option<String>,
1442    #[serde(default, skip_serializing_if = "Option::is_none")]
1443    pub namespace: Option<WorkNamespace>,
1444    #[serde(default, skip_serializing_if = "Option::is_none")]
1445    pub target: Option<WorkAttentionTarget>,
1446    #[serde(default, skip_serializing_if = "Option::is_none")]
1447    pub status: Option<WorkAttentionStatus>,
1448}
1449
1450#[derive(Debug, Clone, Serialize, Deserialize)]
1451#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1452pub struct AttentionListResult {
1453    pub attention: Vec<WorkAttentionBinding>,
1454}
1455
1456#[derive(Debug, Clone, Serialize, Deserialize)]
1457#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1458pub struct AttentionBindingRequest {
1459    pub binding_id: WorkAttentionBindingId,
1460    #[serde(default, skip_serializing_if = "Option::is_none")]
1461    pub realm_id: Option<String>,
1462    #[serde(default, skip_serializing_if = "Option::is_none")]
1463    pub namespace: Option<WorkNamespace>,
1464}
1465
1466#[derive(Debug, Clone, Serialize, Deserialize)]
1467#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1468pub struct AttentionPauseRequest {
1469    pub binding_id: WorkAttentionBindingId,
1470    #[serde(default, skip_serializing_if = "Option::is_none")]
1471    pub realm_id: Option<String>,
1472    #[serde(default, skip_serializing_if = "Option::is_none")]
1473    pub namespace: Option<WorkNamespace>,
1474    pub expected_revision: u64,
1475    #[serde(default, skip_serializing_if = "Option::is_none")]
1476    pub until: Option<DateTime<Utc>>,
1477}
1478
1479#[derive(Debug, Clone, Serialize, Deserialize)]
1480#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1481pub struct AttentionResumeRequest {
1482    pub binding_id: WorkAttentionBindingId,
1483    #[serde(default, skip_serializing_if = "Option::is_none")]
1484    pub realm_id: Option<String>,
1485    #[serde(default, skip_serializing_if = "Option::is_none")]
1486    pub namespace: Option<WorkNamespace>,
1487    pub expected_revision: u64,
1488}
1489
1490#[derive(Debug, Clone, Serialize, Deserialize)]
1491#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1492pub struct AttentionReassignRequest {
1493    pub binding_id: WorkAttentionBindingId,
1494    #[serde(default, skip_serializing_if = "Option::is_none")]
1495    pub realm_id: Option<String>,
1496    #[serde(default, skip_serializing_if = "Option::is_none")]
1497    pub namespace: Option<WorkNamespace>,
1498    pub expected_revision: u64,
1499    pub authority_projection: AttentionContextProjection,
1500    pub target: GoalAttentionTarget,
1501}
1502
1503#[derive(Debug, Clone, Serialize, Deserialize)]
1504#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1505pub struct AttentionBindingResult {
1506    pub attention: WorkAttentionBinding,
1507}
1508
1509#[derive(Debug, Clone, Serialize, Deserialize)]
1510#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1511pub struct AttentionReassignResult {
1512    pub previous: WorkAttentionBinding,
1513    pub attention: WorkAttentionBinding,
1514}
1515
1516/// Break-glass host-plane reassignment (host API only — never exposed on the
1517/// agent tool surface or any wire catalog). WorkGraphs are agent-operated;
1518/// the agent-native transfer is a coordinate-mode agent executing the move.
1519/// This request exists for the one case the graph cannot heal agent-natively:
1520/// a binding stuck on a wedged/retired agent with no coordinator holding
1521/// authority over it. It carries mandatory attribution and is audit-logged in
1522/// the workgraph event stream.
1523#[derive(Debug, Clone, Serialize, Deserialize)]
1524pub struct BreakGlassAttentionReassignRequest {
1525    pub binding_id: WorkAttentionBindingId,
1526    #[serde(default, skip_serializing_if = "Option::is_none")]
1527    pub realm_id: Option<String>,
1528    #[serde(default, skip_serializing_if = "Option::is_none")]
1529    pub namespace: Option<WorkNamespace>,
1530    pub expected_revision: u64,
1531    pub target: GoalAttentionTarget,
1532    /// Authenticated principal performing the break-glass move. Recorded in
1533    /// the audit event; must identify a human/host operator, not an agent.
1534    pub principal: String,
1535    /// Operator-supplied justification. Recorded in the audit event.
1536    pub reason: String,
1537}
1538
1539/// Prune request for TERMINAL (superseded/stopped) attention bindings. The
1540/// event stream keeps the full audit history; pruning removes only the
1541/// binding rows, which otherwise grow monotonically with reassignment churn.
1542#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1543pub struct AttentionPruneRequest {
1544    #[serde(default, skip_serializing_if = "Option::is_none")]
1545    pub realm_id: Option<String>,
1546    #[serde(default, skip_serializing_if = "Option::is_none")]
1547    pub namespace: Option<WorkNamespace>,
1548    /// Only prune bindings last updated strictly before this instant; `None`
1549    /// prunes every terminal binding in scope.
1550    #[serde(default, skip_serializing_if = "Option::is_none")]
1551    pub updated_before: Option<DateTime<Utc>>,
1552}
1553
1554#[derive(Debug, Clone, Serialize, Deserialize)]
1555pub struct AttentionPruneResult {
1556    pub pruned: u64,
1557}
1558
1559#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1560#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1561#[serde(rename_all = "snake_case")]
1562pub enum AttentionContinueOutcome {
1563    Accepted,
1564    Deduplicated,
1565    Rejected,
1566}
1567
1568#[derive(Debug, Clone, Serialize, Deserialize)]
1569#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1570pub struct AttentionContinueResult {
1571    pub outcome: AttentionContinueOutcome,
1572    #[serde(default, skip_serializing_if = "Option::is_none")]
1573    pub input_id: Option<String>,
1574    #[serde(default, skip_serializing_if = "Option::is_none")]
1575    pub existing_id: Option<String>,
1576    #[serde(default, skip_serializing_if = "Option::is_none")]
1577    pub reason: Option<String>,
1578}
1579
1580#[derive(Debug, Clone, Serialize, Deserialize)]
1581#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1582pub struct AttentionProjectionRequest {
1583    pub binding_id: WorkAttentionBindingId,
1584    #[serde(default, skip_serializing_if = "Option::is_none")]
1585    pub realm_id: Option<String>,
1586    #[serde(default, skip_serializing_if = "Option::is_none")]
1587    pub namespace: Option<WorkNamespace>,
1588}
1589
1590#[derive(Debug, Clone, Serialize, Deserialize)]
1591#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1592pub struct AttentionProjectionResult {
1593    pub projection: AttentionContextProjection,
1594}
1595
1596#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1597#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1598pub struct AttentionContextProjection {
1599    pub binding_id: WorkAttentionBindingId,
1600    pub work_ref: WorkItemRef,
1601    pub mode: WorkAttentionMode,
1602    pub binding_revision: u64,
1603    pub item_revision: u64,
1604    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1605    pub parent_refs: Vec<WorkItemRef>,
1606    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1607    pub parent_context: Vec<AttentionProjectionParentContext>,
1608    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1609    pub evidence_refs: Vec<WorkEvidenceRef>,
1610    pub authority: ProjectedAttentionAuthority,
1611    pub text: AttentionProjectionText,
1612}
1613
1614#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1615#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1616pub struct AttentionProjectionParentContext {
1617    pub work_ref: WorkItemRef,
1618    pub status: WorkStatus,
1619    pub revision: u64,
1620}
1621
1622#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1623#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1624pub struct ProjectedAttentionAuthority {
1625    pub can_get: bool,
1626    pub can_add_evidence: bool,
1627    pub can_release: bool,
1628    pub can_update: bool,
1629    pub can_block: bool,
1630    pub can_create: bool,
1631    pub can_link: bool,
1632    pub can_link_parent: bool,
1633    pub can_link_related: bool,
1634    pub can_link_derived_from: bool,
1635    #[serde(default)]
1636    pub can_close_own_review_item: bool,
1637    pub can_close_if_policy_allows: bool,
1638}
1639
1640#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1641#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1642pub struct AttentionProjectionText {
1643    pub title: String,
1644    pub rendered: String,
1645    pub truncated: bool,
1646}
1647
1648#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1649#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1650pub struct WorkItemFilter {
1651    #[serde(default, skip_serializing_if = "Option::is_none")]
1652    pub realm_id: Option<String>,
1653    #[serde(default, skip_serializing_if = "Option::is_none")]
1654    pub namespace: Option<WorkNamespace>,
1655    #[serde(default)]
1656    pub all_namespaces: bool,
1657    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1658    pub statuses: Vec<WorkStatus>,
1659    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1660    pub labels: Vec<String>,
1661    #[serde(default)]
1662    pub include_terminal: bool,
1663    #[serde(default, skip_serializing_if = "Option::is_none")]
1664    pub limit: Option<usize>,
1665}
1666
1667#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1668#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1669pub struct ReadyWorkFilter {
1670    #[serde(default, skip_serializing_if = "Option::is_none")]
1671    pub realm_id: Option<String>,
1672    #[serde(default, skip_serializing_if = "Option::is_none")]
1673    pub namespace: Option<WorkNamespace>,
1674    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1675    pub labels: Vec<String>,
1676    #[serde(default, skip_serializing_if = "Option::is_none")]
1677    pub limit: Option<usize>,
1678}
1679
1680#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1681#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1682pub struct WorkGraphSnapshotFilter {
1683    #[serde(default, skip_serializing_if = "Option::is_none")]
1684    pub realm_id: Option<String>,
1685    #[serde(default, skip_serializing_if = "Option::is_none")]
1686    pub namespace: Option<WorkNamespace>,
1687    #[serde(default)]
1688    pub all_namespaces: bool,
1689    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1690    pub statuses: Vec<WorkStatus>,
1691    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1692    pub labels: Vec<String>,
1693    #[serde(default)]
1694    pub include_terminal: bool,
1695    #[serde(default, skip_serializing_if = "Option::is_none")]
1696    pub limit: Option<usize>,
1697}
1698
1699/// Parameters identifying a single WorkGraph item by id within an optional
1700/// realm/namespace scope (`workgraph/get`).
1701#[derive(Debug, Clone, Serialize, Deserialize)]
1702#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1703pub struct WorkGraphIdParams {
1704    pub id: WorkItemId,
1705    #[serde(default, skip_serializing_if = "Option::is_none")]
1706    pub realm_id: Option<String>,
1707    #[serde(default, skip_serializing_if = "Option::is_none")]
1708    pub namespace: Option<WorkNamespace>,
1709}
1710
1711#[derive(Debug, Clone, Serialize, Deserialize)]
1712#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1713pub struct WorkGraphSnapshot {
1714    pub realm_id: String,
1715    #[serde(default, skip_serializing_if = "Option::is_none")]
1716    pub namespace: Option<WorkNamespace>,
1717    pub all_namespaces: bool,
1718    pub captured_at: DateTime<Utc>,
1719    #[serde(default, skip_serializing_if = "Option::is_none")]
1720    pub event_high_water_mark: Option<i64>,
1721    pub items: Vec<WorkItem>,
1722    pub edges: Vec<WorkEdge>,
1723    #[serde(default)]
1724    pub attention: Vec<WorkAttentionBinding>,
1725    pub ready_item_ids: Vec<WorkItemId>,
1726}
1727
1728#[derive(Debug, Clone, Serialize, Deserialize)]
1729#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1730pub struct WorkGraphItemsResponse {
1731    pub items: Vec<WorkItem>,
1732}
1733
1734#[derive(Debug, Clone, Serialize, Deserialize)]
1735#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1736pub struct WorkGraphEventsResponse {
1737    pub events: Vec<WorkGraphEvent>,
1738}
1739
1740#[cfg(test)]
1741#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
1742mod tests {
1743    use super::*;
1744    use crate::machine::WorkGraphMachine;
1745
1746    fn machine_item() -> WorkItem {
1747        WorkGraphMachine::create_item(
1748            CreateWorkItemRequest {
1749                title: "deserialize-authority".to_string(),
1750                ..Default::default()
1751            },
1752            "realm".to_string(),
1753            WorkNamespace::default(),
1754            Utc::now(),
1755        )
1756        .expect("machine create_item")
1757        .0
1758    }
1759
1760    #[test]
1761    fn work_item_round_trip_preserves_machine_state() {
1762        let item = machine_item();
1763        let json = serde_json::to_string(&item).expect("serialize work item");
1764        let decoded: WorkItem = serde_json::from_str(&json).expect("deserialize work item");
1765        assert_eq!(
1766            decoded, item,
1767            "round-trip must preserve the whole work item"
1768        );
1769        assert_eq!(
1770            decoded.machine_state, item.machine_state,
1771            "round-trip must preserve machine-owned lifecycle authority verbatim"
1772        );
1773    }
1774
1775    #[test]
1776    fn work_item_without_machine_state_fails_closed() {
1777        let item = machine_item();
1778        let mut value = serde_json::to_value(&item).expect("serialize work item to value");
1779        value
1780            .as_object_mut()
1781            .expect("work item json object")
1782            .remove("machine_state");
1783
1784        let result: Result<WorkItem, _> = serde_json::from_value(value);
1785        let err = result.expect_err(
1786            "deserializing a WorkItem without machine_state must fail closed, \
1787             never fabricate lifecycle/revision authority from projected fields",
1788        );
1789        assert!(
1790            err.to_string().contains("machine_state"),
1791            "fail-closed error must cite the missing machine_state authority, got: {err}"
1792        );
1793    }
1794}