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