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}