Skip to main content

tandem_server/automation_v2/
types.rs

1use std::collections::HashMap;
2
3use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use tandem_plan_compiler::api::{
7    ContextObject, PlanScopeSnapshot, PlanValidationReport,
8    ProjectedAutomationContextMaterialization, ProjectedRoutineContextPartition,
9    ProjectedStepContextBindings,
10};
11
12use crate::routines::types::RoutineMisfirePolicy;
13
14pub type AutomationV2Schedule =
15    tandem_workflows::plan_package::AutomationV2Schedule<RoutineMisfirePolicy>;
16pub use tandem_workflows::plan_package::AutomationV2ScheduleType;
17
18pub type WorkflowPlanStep = tandem_workflows::plan_package::WorkflowPlanStep<
19    AutomationFlowInputRef,
20    AutomationFlowOutputContract,
21>;
22pub type WorkflowPlan =
23    tandem_workflows::plan_package::WorkflowPlan<AutomationV2Schedule, WorkflowPlanStep>;
24pub use tandem_workflows::plan_package::{WorkflowPlanChatMessage, WorkflowPlanConversation};
25pub type WorkflowPlanDraftRecord =
26    tandem_workflows::plan_package::WorkflowPlanDraftRecord<WorkflowPlan>;
27pub type AutomationRuntimeContextMaterialization = ProjectedAutomationContextMaterialization;
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30#[serde(rename_all = "snake_case")]
31pub enum AutomationV2Status {
32    Active,
33    Paused,
34    Draft,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct AutomationAgentToolPolicy {
39    #[serde(default)]
40    pub allowlist: Vec<String>,
41    #[serde(default)]
42    pub denylist: Vec<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct AutomationAgentMcpPolicy {
47    #[serde(default)]
48    pub allowed_servers: Vec<String>,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub allowed_tools: Option<Vec<String>>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct AutomationAgentProfile {
55    pub agent_id: String,
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub template_id: Option<String>,
58    pub display_name: String,
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub avatar_url: Option<String>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub model_policy: Option<Value>,
63    #[serde(default)]
64    pub skills: Vec<String>,
65    pub tool_policy: AutomationAgentToolPolicy,
66    pub mcp_policy: AutomationAgentMcpPolicy,
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub approval_policy: Option<String>,
69}
70
71impl From<tandem_plan_compiler::api::ProjectedAutomationAgentProfile> for AutomationAgentProfile {
72    fn from(value: tandem_plan_compiler::api::ProjectedAutomationAgentProfile) -> Self {
73        Self {
74            agent_id: value.agent_id,
75            template_id: value.template_id,
76            display_name: value.display_name,
77            avatar_url: None,
78            model_policy: value.model_policy,
79            skills: Vec::new(),
80            tool_policy: AutomationAgentToolPolicy {
81                allowlist: value.tool_allowlist,
82                denylist: Vec::new(),
83            },
84            mcp_policy: AutomationAgentMcpPolicy {
85                allowed_servers: value.allowed_mcp_servers,
86                allowed_tools: None,
87            },
88            approval_policy: None,
89        }
90    }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
94#[serde(rename_all = "snake_case")]
95pub enum AutomationNodeStageKind {
96    Orchestrator,
97    Workstream,
98    Review,
99    Test,
100    Approval,
101}
102
103impl From<tandem_plan_compiler::api::ProjectedAutomationStageKind> for AutomationNodeStageKind {
104    fn from(value: tandem_plan_compiler::api::ProjectedAutomationStageKind) -> Self {
105        match value {
106            tandem_plan_compiler::api::ProjectedAutomationStageKind::Workstream => Self::Workstream,
107            tandem_plan_compiler::api::ProjectedAutomationStageKind::Review => Self::Review,
108            tandem_plan_compiler::api::ProjectedAutomationStageKind::Test => Self::Test,
109            tandem_plan_compiler::api::ProjectedAutomationStageKind::Approval => Self::Approval,
110        }
111    }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct AutomationApprovalGate {
116    #[serde(default)]
117    pub required: bool,
118    #[serde(default)]
119    pub decisions: Vec<String>,
120    #[serde(default)]
121    pub rework_targets: Vec<String>,
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub instructions: Option<String>,
124}
125
126impl From<tandem_plan_compiler::api::ProjectedAutomationApprovalGate> for AutomationApprovalGate {
127    fn from(value: tandem_plan_compiler::api::ProjectedAutomationApprovalGate) -> Self {
128        Self {
129            required: value.required,
130            decisions: value.decisions,
131            rework_targets: value.rework_targets,
132            instructions: value.instructions,
133        }
134    }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct AutomationFlowNode {
139    pub node_id: String,
140    pub agent_id: String,
141    pub objective: String,
142    #[serde(default)]
143    pub depends_on: Vec<String>,
144    #[serde(default)]
145    pub input_refs: Vec<AutomationFlowInputRef>,
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub output_contract: Option<AutomationFlowOutputContract>,
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub retry_policy: Option<Value>,
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub timeout_ms: Option<u64>,
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub stage_kind: Option<AutomationNodeStageKind>,
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub gate: Option<AutomationApprovalGate>,
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub metadata: Option<Value>,
158}
159
160impl<I, O> From<tandem_plan_compiler::api::ProjectedAutomationNode<I, O>> for AutomationFlowNode
161where
162    I: Into<AutomationFlowInputRef>,
163    O: Into<AutomationFlowOutputContract>,
164{
165    fn from(value: tandem_plan_compiler::api::ProjectedAutomationNode<I, O>) -> Self {
166        Self {
167            node_id: value.node_id,
168            agent_id: value.agent_id,
169            objective: value.objective,
170            depends_on: value.depends_on,
171            input_refs: value.input_refs.into_iter().map(Into::into).collect(),
172            output_contract: value.output_contract.map(Into::into),
173            retry_policy: value.retry_policy,
174            timeout_ms: value.timeout_ms,
175            stage_kind: value.stage_kind.map(Into::into),
176            gate: value.gate.map(Into::into),
177            metadata: value.metadata,
178        }
179    }
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize, Default)]
183pub struct AutomationFlowInputRef {
184    pub from_step_id: String,
185    pub alias: String,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize, Default)]
189pub struct AutomationFlowOutputContract {
190    pub kind: String,
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub validator: Option<AutomationOutputValidatorKind>,
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub enforcement: Option<AutomationOutputEnforcement>,
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub schema: Option<Value>,
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub summary_guidance: Option<String>,
199}
200
201impl From<tandem_plan_compiler::api::ProjectedMissionInputRef> for AutomationFlowInputRef {
202    fn from(value: tandem_plan_compiler::api::ProjectedMissionInputRef) -> Self {
203        Self {
204            from_step_id: value.from_step_id,
205            alias: value.alias,
206        }
207    }
208}
209
210impl tandem_plan_compiler::api::WorkflowInputRefLike for AutomationFlowInputRef {
211    fn from_step_id(&self) -> &str {
212        self.from_step_id.as_str()
213    }
214}
215
216impl From<tandem_plan_compiler::api::OutputContractSeed> for AutomationFlowOutputContract {
217    fn from(value: tandem_plan_compiler::api::OutputContractSeed) -> Self {
218        Self {
219            kind: value.kind,
220            validator: value.validator_kind.map(|kind| match kind {
221                tandem_plan_compiler::api::ProjectedOutputValidatorKind::ResearchBrief => {
222                    AutomationOutputValidatorKind::ResearchBrief
223                }
224                tandem_plan_compiler::api::ProjectedOutputValidatorKind::ReviewDecision => {
225                    AutomationOutputValidatorKind::ReviewDecision
226                }
227                tandem_plan_compiler::api::ProjectedOutputValidatorKind::StructuredJson => {
228                    AutomationOutputValidatorKind::StructuredJson
229                }
230                tandem_plan_compiler::api::ProjectedOutputValidatorKind::CodePatch => {
231                    AutomationOutputValidatorKind::CodePatch
232                }
233                tandem_plan_compiler::api::ProjectedOutputValidatorKind::GenericArtifact => {
234                    AutomationOutputValidatorKind::GenericArtifact
235                }
236            }),
237            enforcement: value
238                .enforcement
239                .and_then(|raw| serde_json::from_value(raw).ok()),
240            schema: value.schema,
241            summary_guidance: value.summary_guidance,
242        }
243    }
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
247pub struct AutomationOutputEnforcement {
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub validation_profile: Option<String>,
250    #[serde(default)]
251    pub required_tools: Vec<String>,
252    #[serde(default)]
253    pub required_evidence: Vec<String>,
254    #[serde(default)]
255    pub required_sections: Vec<String>,
256    #[serde(default)]
257    pub prewrite_gates: Vec<String>,
258    #[serde(default)]
259    pub retry_on_missing: Vec<String>,
260    #[serde(default)]
261    pub terminal_on: Vec<String>,
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub repair_budget: Option<u32>,
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub session_text_recovery: Option<String>,
266}
267
268#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
269#[serde(rename_all = "snake_case")]
270pub enum AutomationOutputValidatorKind {
271    CodePatch,
272    ResearchBrief,
273    ReviewDecision,
274    StructuredJson,
275    GenericArtifact,
276}
277
278impl AutomationOutputValidatorKind {
279    pub fn stable_key(self) -> &'static str {
280        match self {
281            Self::CodePatch => "code_patch",
282            Self::ResearchBrief => "research_brief",
283            Self::ReviewDecision => "review_decision",
284            Self::StructuredJson => "structured_json",
285            Self::GenericArtifact => "generic_artifact",
286        }
287    }
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct AutomationFlowSpec {
292    #[serde(default)]
293    pub nodes: Vec<AutomationFlowNode>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct AutomationExecutionPolicy {
298    #[serde(default, skip_serializing_if = "Option::is_none")]
299    pub max_parallel_agents: Option<u32>,
300    #[serde(default, skip_serializing_if = "Option::is_none")]
301    pub max_total_runtime_ms: Option<u64>,
302    #[serde(default, skip_serializing_if = "Option::is_none")]
303    pub max_total_tool_calls: Option<u32>,
304    #[serde(default, skip_serializing_if = "Option::is_none")]
305    pub max_total_tokens: Option<u64>,
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub max_total_cost_usd: Option<f64>,
308}
309
310impl From<tandem_plan_compiler::api::ProjectedAutomationExecutionPolicy>
311    for AutomationExecutionPolicy
312{
313    fn from(value: tandem_plan_compiler::api::ProjectedAutomationExecutionPolicy) -> Self {
314        Self {
315            max_parallel_agents: value.max_parallel_agents,
316            max_total_runtime_ms: value.max_total_runtime_ms,
317            max_total_tool_calls: value.max_total_tool_calls,
318            max_total_tokens: value.max_total_tokens,
319            max_total_cost_usd: value.max_total_cost_usd,
320        }
321    }
322}
323
324impl AutomationV2Spec {
325    fn metadata_value<T>(&self, key: &str) -> Option<T>
326    where
327        T: DeserializeOwned,
328    {
329        self.metadata
330            .as_ref()
331            .and_then(|metadata| metadata.get(key).cloned())
332            .and_then(|value| serde_json::from_value(value).ok())
333    }
334
335    pub fn runtime_context_materialization(
336        &self,
337    ) -> Option<AutomationRuntimeContextMaterialization> {
338        self.metadata_value("context_materialization")
339    }
340
341    pub fn approved_plan_runtime_context_materialization(
342        &self,
343    ) -> Option<AutomationRuntimeContextMaterialization> {
344        let approved_plan = self.approved_plan_materialization()?;
345        let scope_snapshot = self.plan_scope_snapshot_materialization()?;
346        let context_objects = scope_snapshot
347            .context_objects
348            .into_iter()
349            .map(|context_object: ContextObject| {
350                (context_object.context_object_id.clone(), context_object)
351            })
352            .collect::<HashMap<_, _>>();
353        let routines = approved_plan
354            .routines
355            .into_iter()
356            .map(|routine| ProjectedRoutineContextPartition {
357                routine_id: routine.routine_id,
358                visible_context_objects: routine
359                    .visible_context_object_ids
360                    .into_iter()
361                    .filter_map(|context_object_id| {
362                        context_objects.get(&context_object_id).cloned()
363                    })
364                    .collect(),
365                step_context_bindings: routine
366                    .step_context_bindings
367                    .into_iter()
368                    .map(|binding| ProjectedStepContextBindings {
369                        step_id: binding.step_id,
370                        context_reads: binding.context_reads,
371                        context_writes: binding.context_writes,
372                    })
373                    .collect(),
374            })
375            .collect();
376        Some(AutomationRuntimeContextMaterialization { routines })
377    }
378
379    pub fn plan_scope_snapshot_materialization(&self) -> Option<PlanScopeSnapshot> {
380        self.metadata
381            .as_ref()
382            .and_then(|metadata| metadata.get("plan_package_bundle"))
383            .and_then(|bundle| bundle.get("scope_snapshot"))
384            .cloned()
385            .and_then(|value| serde_json::from_value(value).ok())
386    }
387
388    pub(crate) fn plan_package_validation_report(&self) -> Option<PlanValidationReport> {
389        self.metadata_value("plan_package_validation")
390    }
391
392    pub(crate) fn approved_plan_materialization(
393        &self,
394    ) -> Option<tandem_plan_compiler::api::ApprovedPlanMaterialization> {
395        self.metadata_value("approved_plan_materialization")
396    }
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct AutomationV2Spec {
401    pub automation_id: String,
402    pub name: String,
403    #[serde(default, skip_serializing_if = "Option::is_none")]
404    pub description: Option<String>,
405    pub status: AutomationV2Status,
406    pub schedule: AutomationV2Schedule,
407    #[serde(default)]
408    pub agents: Vec<AutomationAgentProfile>,
409    pub flow: AutomationFlowSpec,
410    pub execution: AutomationExecutionPolicy,
411    #[serde(default)]
412    pub output_targets: Vec<String>,
413    pub created_at_ms: u64,
414    pub updated_at_ms: u64,
415    pub creator_id: String,
416    #[serde(default, skip_serializing_if = "Option::is_none")]
417    pub workspace_root: Option<String>,
418    #[serde(default, skip_serializing_if = "Option::is_none")]
419    pub metadata: Option<Value>,
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub next_fire_at_ms: Option<u64>,
422    #[serde(default, skip_serializing_if = "Option::is_none")]
423    pub last_fired_at_ms: Option<u64>,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct AutomationNodeOutput {
428    pub contract_kind: String,
429    #[serde(default, skip_serializing_if = "Option::is_none")]
430    pub validator_kind: Option<AutomationOutputValidatorKind>,
431    #[serde(default, skip_serializing_if = "Option::is_none")]
432    pub validator_summary: Option<AutomationValidatorSummary>,
433    pub summary: String,
434    pub content: Value,
435    pub created_at_ms: u64,
436    pub node_id: String,
437    #[serde(default, skip_serializing_if = "Option::is_none")]
438    pub status: Option<String>,
439    #[serde(default, skip_serializing_if = "Option::is_none")]
440    pub blocked_reason: Option<String>,
441    #[serde(default, skip_serializing_if = "Option::is_none")]
442    pub approved: Option<bool>,
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub workflow_class: Option<String>,
445    #[serde(default, skip_serializing_if = "Option::is_none")]
446    pub phase: Option<String>,
447    #[serde(default, skip_serializing_if = "Option::is_none")]
448    pub failure_kind: Option<String>,
449    #[serde(default, skip_serializing_if = "Option::is_none")]
450    pub tool_telemetry: Option<Value>,
451    #[serde(default, skip_serializing_if = "Option::is_none")]
452    pub preflight: Option<Value>,
453    #[serde(default, skip_serializing_if = "Option::is_none")]
454    pub capability_resolution: Option<Value>,
455    #[serde(default, skip_serializing_if = "Option::is_none")]
456    pub attempt_evidence: Option<Value>,
457    #[serde(default, skip_serializing_if = "Option::is_none")]
458    pub blocker_category: Option<String>,
459    #[serde(default, skip_serializing_if = "Option::is_none")]
460    pub receipt_timeline: Option<Value>,
461    #[serde(default, skip_serializing_if = "Option::is_none")]
462    pub quality_mode: Option<String>,
463    #[serde(default, skip_serializing_if = "Option::is_none")]
464    pub requested_quality_mode: Option<String>,
465    #[serde(default, skip_serializing_if = "Option::is_none")]
466    pub emergency_rollback_enabled: Option<bool>,
467    #[serde(default, skip_serializing_if = "Option::is_none")]
468    pub fallback_used: Option<bool>,
469    #[serde(default, skip_serializing_if = "Option::is_none")]
470    pub artifact_validation: Option<Value>,
471    #[serde(default, skip_serializing_if = "Option::is_none")]
472    pub provenance: Option<AutomationNodeOutputProvenance>,
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct AutomationValidatorSummary {
477    pub kind: AutomationOutputValidatorKind,
478    pub outcome: String,
479    #[serde(default, skip_serializing_if = "Option::is_none")]
480    pub reason: Option<String>,
481    #[serde(default)]
482    pub unmet_requirements: Vec<String>,
483    #[serde(default)]
484    pub warning_requirements: Vec<String>,
485    #[serde(default)]
486    pub warning_count: u32,
487    #[serde(default, skip_serializing_if = "Option::is_none")]
488    pub accepted_candidate_source: Option<String>,
489    #[serde(default, skip_serializing_if = "Option::is_none")]
490    pub verification_outcome: Option<String>,
491    #[serde(default, skip_serializing_if = "Option::is_none")]
492    pub validation_basis: Option<Value>,
493    #[serde(default)]
494    pub repair_attempted: bool,
495    #[serde(default)]
496    pub repair_attempt: u32,
497    #[serde(default)]
498    pub repair_attempts_remaining: u32,
499    #[serde(default)]
500    pub repair_succeeded: bool,
501    #[serde(default)]
502    pub repair_exhausted: bool,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct AutomationNodeOutputFreshness {
507    pub current_run: bool,
508    pub current_attempt: bool,
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct AutomationNodeOutputProvenance {
513    pub session_id: String,
514    pub node_id: String,
515    #[serde(default, skip_serializing_if = "Option::is_none")]
516    pub run_id: Option<String>,
517    #[serde(default, skip_serializing_if = "Option::is_none")]
518    pub output_path: Option<String>,
519    #[serde(default, skip_serializing_if = "Option::is_none")]
520    pub content_digest: Option<String>,
521    #[serde(default, skip_serializing_if = "Option::is_none")]
522    pub accepted_candidate_source: Option<String>,
523    #[serde(default, skip_serializing_if = "Option::is_none")]
524    pub validation_outcome: Option<String>,
525    #[serde(default, skip_serializing_if = "Option::is_none")]
526    pub repair_attempt: Option<u64>,
527    #[serde(default, skip_serializing_if = "Option::is_none")]
528    pub repair_succeeded: Option<bool>,
529    #[serde(default, skip_serializing_if = "Option::is_none")]
530    pub reuse_allowed: Option<bool>,
531    pub freshness: AutomationNodeOutputFreshness,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
535#[serde(rename_all = "snake_case")]
536pub enum AutomationRunStatus {
537    Queued,
538    Running,
539    Pausing,
540    Paused,
541    AwaitingApproval,
542    Completed,
543    Blocked,
544    Failed,
545    Cancelled,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct AutomationPendingGate {
550    pub node_id: String,
551    pub title: String,
552    #[serde(default, skip_serializing_if = "Option::is_none")]
553    pub instructions: Option<String>,
554    #[serde(default)]
555    pub decisions: Vec<String>,
556    #[serde(default)]
557    pub rework_targets: Vec<String>,
558    pub requested_at_ms: u64,
559    #[serde(default)]
560    pub upstream_node_ids: Vec<String>,
561}
562
563#[derive(Debug, Clone, Serialize, Deserialize)]
564pub struct AutomationGateDecisionRecord {
565    pub node_id: String,
566    pub decision: String,
567    #[serde(default, skip_serializing_if = "Option::is_none")]
568    pub reason: Option<String>,
569    pub decided_at_ms: u64,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
573#[serde(rename_all = "snake_case")]
574pub enum AutomationStopKind {
575    Cancelled,
576    OperatorStopped,
577    GuardrailStopped,
578    Panic,
579    Shutdown,
580    ServerRestart,
581}
582
583#[derive(Debug, Clone, Serialize, Deserialize)]
584pub struct AutomationLifecycleRecord {
585    pub event: String,
586    pub recorded_at_ms: u64,
587    #[serde(default, skip_serializing_if = "Option::is_none")]
588    pub reason: Option<String>,
589    #[serde(default, skip_serializing_if = "Option::is_none")]
590    pub stop_kind: Option<AutomationStopKind>,
591    #[serde(default, skip_serializing_if = "Option::is_none")]
592    pub metadata: Option<Value>,
593}
594
595#[derive(Debug, Clone, Serialize, Deserialize)]
596pub struct AutomationFailureRecord {
597    pub node_id: String,
598    pub reason: String,
599    pub failed_at_ms: u64,
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct AutomationRunCheckpoint {
604    #[serde(default)]
605    pub completed_nodes: Vec<String>,
606    #[serde(default)]
607    pub pending_nodes: Vec<String>,
608    #[serde(default)]
609    pub node_outputs: std::collections::HashMap<String, Value>,
610    #[serde(default)]
611    pub node_attempts: std::collections::HashMap<String, u32>,
612    #[serde(default)]
613    pub blocked_nodes: Vec<String>,
614    #[serde(default, skip_serializing_if = "Option::is_none")]
615    pub awaiting_gate: Option<AutomationPendingGate>,
616    #[serde(default)]
617    pub gate_history: Vec<AutomationGateDecisionRecord>,
618    #[serde(default)]
619    pub lifecycle_history: Vec<AutomationLifecycleRecord>,
620    #[serde(default, skip_serializing_if = "Option::is_none")]
621    pub last_failure: Option<AutomationFailureRecord>,
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize)]
625pub struct AutomationV2RunRecord {
626    pub run_id: String,
627    pub automation_id: String,
628    pub trigger_type: String,
629    pub status: AutomationRunStatus,
630    pub created_at_ms: u64,
631    pub updated_at_ms: u64,
632    #[serde(default, skip_serializing_if = "Option::is_none")]
633    pub started_at_ms: Option<u64>,
634    #[serde(default, skip_serializing_if = "Option::is_none")]
635    pub finished_at_ms: Option<u64>,
636    #[serde(default)]
637    pub active_session_ids: Vec<String>,
638    #[serde(default, skip_serializing_if = "Option::is_none")]
639    pub latest_session_id: Option<String>,
640    #[serde(default)]
641    pub active_instance_ids: Vec<String>,
642    pub checkpoint: AutomationRunCheckpoint,
643    #[serde(default, skip_serializing_if = "Option::is_none")]
644    pub runtime_context: Option<AutomationRuntimeContextMaterialization>,
645    #[serde(default, skip_serializing_if = "Option::is_none")]
646    pub automation_snapshot: Option<AutomationV2Spec>,
647    #[serde(default, skip_serializing_if = "Option::is_none")]
648    pub pause_reason: Option<String>,
649    #[serde(default, skip_serializing_if = "Option::is_none")]
650    pub resume_reason: Option<String>,
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub detail: Option<String>,
653    #[serde(default, skip_serializing_if = "Option::is_none")]
654    pub stop_kind: Option<AutomationStopKind>,
655    #[serde(default, skip_serializing_if = "Option::is_none")]
656    pub stop_reason: Option<String>,
657    #[serde(default)]
658    pub prompt_tokens: u64,
659    #[serde(default)]
660    pub completion_tokens: u64,
661    #[serde(default)]
662    pub total_tokens: u64,
663    #[serde(default)]
664    pub estimated_cost_usd: f64,
665    #[serde(default, skip_serializing_if = "Option::is_none")]
666    pub scheduler: Option<crate::app::state::automation::scheduler::SchedulerMetadata>,
667}