Skip to main content

dpm_engine/
run.rs

1//! Agent runs: validated starts, lifecycle transitions, operation links and derived observation.
2//!
3//! Nothing here is a project command. A run never appears in the semantic operation log, never
4//! changes the plan revision, and cannot submit, verify, release or reassign the task it executes;
5//! it only records what was observed about an executor. Persistence lives in `dpm-store`.
6
7use chrono::{DateTime, Utc};
8use dpm_model::{
9    ActorId, LineageId, OperationId, RunEventId, RunId, RunState, ValidationError, WorkItemId,
10    WorkStatus,
11};
12use thiserror::Error;
13
14mod link;
15mod projection;
16mod source;
17mod start;
18mod transition;
19
20pub use link::{OperationFacts, check_link};
21pub use projection::{STALE_AFTER, project};
22pub use start::observe_start;
23pub use transition::{authorize, check_transition};
24
25/// Why a run request was refused.
26#[derive(Debug, Error)]
27pub enum RunError {
28    /// The request is malformed.
29    #[error(transparent)]
30    Validation(#[from] ValidationError),
31    /// The task is not in the plan.
32    #[error("work item {0} does not exist")]
33    MissingWork(WorkItemId),
34    /// Only tasks execute; containers and milestones do not.
35    #[error("work item {0} is not a task and cannot be executed")]
36    NotATask(WorkItemId),
37    /// A run starts on claimed or started work, so its executor is already the task's owner.
38    #[error("work item {work} is {status:?}; a run starts only on claimed or started work")]
39    NotExecuting {
40        /// The task.
41        work: WorkItemId,
42        /// Its lifecycle.
43        status: WorkStatus,
44    },
45    /// The run's executor is not the task's owner.
46    #[error("work item {work} is owned by {}, not {executor}", owner.as_ref().map_or_else(|| "no one".to_string(), ToString::to_string))]
47    NotOwner {
48        /// The task.
49        work: WorkItemId,
50        /// The executor the run names.
51        executor: ActorId,
52        /// The task's owner.
53        owner: Option<ActorId>,
54    },
55    /// An agent may record only its own runs; a human or service may record any.
56    #[error("actor {actor} may not {action} run {run} executed by {executor}")]
57    ActorNotAllowed {
58        /// The principal making the request.
59        actor: ActorId,
60        /// The refused action.
61        action: &'static str,
62        /// The run.
63        run: RunId,
64        /// The run's executor.
65        executor: ActorId,
66    },
67    /// Only a service may label a run managed: nothing else can observe the executor.
68    #[error("run {0} cannot be managed: only a service actor can record a managed run")]
69    ManagedNeedsService(RunId),
70    /// A source reference names an asset or artifact the plan does not support.
71    #[error("run source is not supported by the plan: {0}")]
72    UnsupportedSource(String),
73    /// The lifecycle does not allow this transition.
74    #[error("run {run} cannot move from {from} to {to}")]
75    InvalidTransition {
76        /// The run.
77        run: RunId,
78        /// Its current state.
79        from: RunState,
80        /// The requested state.
81        to: RunState,
82    },
83    /// The run already ended; a terminal state is final.
84    #[error("run {run} already ended as {state}; start a new run to try again")]
85    Terminal {
86        /// The run.
87        run: RunId,
88        /// Its terminal state.
89        state: RunState,
90    },
91    /// The operation cannot be attributed to the run.
92    #[error("operation {operation} cannot be linked to run {run}: {reason}")]
93    LinkRefused {
94        /// The run.
95        run: RunId,
96        /// The operation.
97        operation: OperationId,
98        /// Why the attribution would be wrong; kept as the error's source.
99        #[source]
100        reason: LinkRefusal,
101    },
102}
103
104/// Why an operation cannot be attributed to a run.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
106pub enum LinkRefusal {
107    /// The operation was committed to another workspace.
108    #[error("it belongs to another workspace")]
109    OtherWorkspace,
110    /// The operation was committed under a lineage other than the one the run observed.
111    #[error("it was committed under another lineage than the run observed")]
112    OtherLineage,
113    /// The operation does not act on the run's task.
114    #[error("it does not act on the run's task")]
115    OtherWork,
116    /// The operation was made by someone other than the run's executor.
117    #[error("it was made by someone other than the run's executor")]
118    OtherActor,
119    /// The operation was committed before the run started.
120    #[error("it was committed before the run started")]
121    BeforeStart,
122    /// The operation was committed after the run ended.
123    #[error("it was committed after the run ended")]
124    AfterEnd,
125}
126
127/// The transition identity of a run's start, which is the run's own identity.
128#[must_use]
129pub fn start_event(run: RunId) -> RunEventId {
130    RunEventId(run.0)
131}
132
133/// The instant a receipt at `receipt` stops keeping an unfinished run fresh.
134fn stale_after(receipt: DateTime<Utc>) -> Option<DateTime<Utc>> {
135    receipt.checked_add_signed(STALE_AFTER)
136}
137
138/// Whether `current` is the history a run recorded its contract under.
139fn same_history(recorded: LineageId, current: Option<LineageId>) -> bool {
140    current == Some(recorded)
141}
142
143#[cfg(test)]
144mod tests;