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}