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}