Skip to main content

dpm_model/run/
record.rs

1//! The immutable facts a run records when it starts.
2
3use crate::{
4    ActorId, ArtifactId, AssetId, Key, LineageId, OperationId, RunId, WorkContract, WorkItemId,
5    WorkspaceId,
6};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// The external runtime session a run belongs to, named by the provider's own identifiers.
11///
12/// Identifiers are opaque to DPM and are never a place for secrets: they appear in shared views.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct RunSession {
16    /// Lowercase provider word such as `codex` or `claude`; DPM keeps no table of providers.
17    pub provider: String,
18    /// The provider's session or thread identifier.
19    pub session: String,
20    /// The provider's turn or request identifier within the session, if it has one.
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub turn: Option<String>,
23    /// The runtime's identity and configuration as the recorder attests them when the run starts.
24    /// Fixed for the life of the run and compared when a start is resent. Boxed so that a start
25    /// request without it stays small.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub provenance: Option<Box<RunProvenance>>,
28}
29
30/// What the recorder attests about the runtime a run executed in, fixed when the run starts.
31///
32/// The requested and the observed model are kept apart: the first is what the recorder asked the
33/// runtime to use, the second what the runtime announced it used. Neither is a place for secrets.
34#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct RunProvenance {
37    /// The model the recorder asked for; absent when it left the choice to the runtime.
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub requested_model: Option<String>,
40    /// The model the runtime announced, when it announced one.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub observed_model: Option<String>,
43    /// The runtime's version as it announced it.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub runtime_version: Option<String>,
46    /// SHA-256, in lowercase hex, of the canonical public configuration the runtime was started
47    /// with: permission mode, tools, allowed rules and confinement. The configuration itself is
48    /// not stored, only what identifies it.
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub configuration_digest: Option<String>,
51}
52
53/// How completely DPM observes the executor.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum Observation {
57    /// A service hosts or watches the executor, so silence is evidence of a problem.
58    Managed,
59    /// The executor reports itself through the CLI or agent tools; absent reports prove nothing,
60    /// so a silent run is stale, never idle or finished.
61    ReportedOnly,
62}
63
64impl Observation {
65    /// The lowercase word every adapter spells the mode with.
66    #[must_use]
67    pub fn word(self) -> &'static str {
68        match self {
69            Self::Managed => "managed",
70            Self::ReportedOnly => "reported_only",
71        }
72    }
73}
74
75/// An observation word no run has.
76#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
77#[error("unknown observation {0:?}; expected managed or reported_only")]
78pub struct ObservationParseError(String);
79
80impl std::str::FromStr for Observation {
81    type Err = ObservationParseError;
82
83    fn from_str(word: &str) -> Result<Self, Self::Err> {
84        [Self::Managed, Self::ReportedOnly]
85            .into_iter()
86            .find(|mode| mode.word() == word)
87            .ok_or_else(|| ObservationParseError(word.into()))
88    }
89}
90
91/// An exact, immutable reference to the source a run executes against.
92///
93/// A branch or path is mutable, so it is not a source reference: name the commit.
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
96pub enum RunSource {
97    /// A commit of a repository asset the work requires.
98    GitCommit {
99        /// Repository asset the task's contract requires.
100        asset: AssetId,
101        /// Full lowercase hexadecimal object name, 40 (SHA-1) or 64 (SHA-256) digits.
102        commit: String,
103    },
104    /// Git commit evidence attached to this task, as `attach-git-head` records it: a `GitCommit`
105    /// artifact whose `commit` and `asset_id` metadata and `git:asset:ASSET@COMMIT` URI name an
106    /// exact commit of a repository the task requires. Evidence that names only a branch, or a
107    /// different asset than its metadata, is not an exact source and is refused.
108    Artifact {
109        /// An artifact attached to the task.
110        artifact: ArtifactId,
111    },
112}
113
114/// The exact, immutable source content a run's source references resolved to when it started.
115///
116/// The engine fills this from the plan, so the record holds the commit itself and not only a
117/// pointer to evidence.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(deny_unknown_fields)]
120pub struct ExactSource {
121    /// Repository asset the commit belongs to; a repository the task's contract requires.
122    pub asset: AssetId,
123    /// Full lowercase hexadecimal object name, 40 or 64 digits.
124    pub commit: String,
125    /// The attached evidence artifact that attested the commit, when the source named one.
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub artifact: Option<ArtifactId>,
128}
129
130/// What a caller asks to record when a run starts; the application adds what it observes.
131///
132/// Two requests with equal fields are one request, which is how a retry is told from a conflict.
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(deny_unknown_fields)]
135pub struct RunStart {
136    /// Run identity and idempotency key, a version 7 UUID.
137    pub id: RunId,
138    /// The task being executed.
139    pub work: WorkItemId,
140    /// The principal doing the work; the task's owner when the run starts.
141    pub executor: ActorId,
142    /// Run that spawned this one, such as a parent agent's run.
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub parent: Option<RunId>,
145    /// Provider session, absent for an executor with none.
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub session: Option<RunSession>,
148    /// How completely the executor is observed.
149    pub observation: Observation,
150    /// Exact source references; empty when none are known.
151    #[serde(default, skip_serializing_if = "Vec::is_empty")]
152    pub sources: Vec<RunSource>,
153    /// When the executor says the run began. It is a claim, never used to judge freshness.
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub observed_at: Option<DateTime<Utc>>,
156}
157
158/// The contract a run executes against, as observed at its start.
159///
160/// This is typed immutable content: later reviewed plan changes edit the plan, not this record.
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162#[serde(deny_unknown_fields)]
163pub struct ContractObservation {
164    /// Workspace the contract was read from.
165    pub workspace_id: WorkspaceId,
166    /// Writable history the revision belongs to.
167    pub lineage_id: LineageId,
168    /// Plan revision the contract was read at, meaningful within the lineage.
169    pub revision: u64,
170    /// Human key of the work at that revision.
171    pub work_key: Key,
172    /// Submissions the work already had, so a rework run is told from the first attempt.
173    pub prior_submissions: u32,
174    /// The authored contract exactly as it stood at that revision.
175    pub contract: WorkContract,
176}
177
178/// A run as recorded when it started.
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
180#[serde(deny_unknown_fields)]
181pub struct RunRecord {
182    /// Run identity.
183    pub id: RunId,
184    /// The task being executed.
185    pub work: WorkItemId,
186    /// The principal doing the work.
187    pub executor: ActorId,
188    /// The principal that recorded the run, the executor itself or a service acting for it.
189    pub recorded_by: ActorId,
190    /// Run that spawned this one.
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub parent: Option<RunId>,
193    /// Provider session.
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub session: Option<RunSession>,
196    /// How completely the executor is observed.
197    pub observation: Observation,
198    /// Exact source references as requested.
199    #[serde(default, skip_serializing_if = "Vec::is_empty")]
200    pub sources: Vec<RunSource>,
201    /// The exact commits those references resolved to, captured immutably at the start.
202    #[serde(default, skip_serializing_if = "Vec::is_empty")]
203    pub exact_sources: Vec<ExactSource>,
204    /// The contract observed at the start.
205    pub contract: ContractObservation,
206    /// When the executor says the run began.
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub observed_started_at: Option<DateTime<Utc>>,
209    /// When DPM recorded the start; this is the receipt time, from DPM's own clock.
210    pub started_at: DateTime<Utc>,
211}
212
213impl RunRecord {
214    /// The request fields this record answers, for comparing a resent start with the recorded one.
215    #[must_use]
216    pub fn request(&self) -> RunStart {
217        RunStart {
218            id: self.id,
219            work: self.work,
220            executor: self.executor.clone(),
221            parent: self.parent,
222            session: self.session.clone(),
223            observation: self.observation,
224            sources: self.sources.clone(),
225            observed_at: self.observed_started_at,
226        }
227    }
228}
229
230/// An explicit association of a committed project operation with the run that performed it.
231///
232/// Linking never changes the operation: the semantic log is unaware of runs.
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234#[serde(deny_unknown_fields)]
235pub struct RunLink {
236    /// The run that performed the operation.
237    pub run: RunId,
238    /// The committed operation.
239    pub operation: OperationId,
240    /// The task the operation targeted.
241    pub work: WorkItemId,
242    /// The principal that recorded the association.
243    pub linked_by: ActorId,
244    /// When DPM recorded the association.
245    pub linked_at: DateTime<Utc>,
246}