aion_integrations/spec.rs
1//! The neutral run identity handed to a harness at [`crate::AgentHarness::start`].
2
3use aion_core::{ActivityId, Payload, RunId, WorkflowId};
4
5/// The neutral identity + input for one agent run (one activity attempt).
6///
7/// Carries only what every harness needs to run an attempt: the
8/// `(workflow, run, activity, attempt)` key, the activity-type name the engine dispatched, and
9/// the input [`Payload`]. It names **no** harness-specific configuration — an adapter holds any
10/// harness-specific settings itself (constructed when the [`crate::AgentHarness`] is built),
11/// keeping this spec harness-blind.
12///
13/// # The run axis is what the adapters stamp onto every event
14///
15/// Each adapter stamps this identity onto every [`aion_core::ActivityEvent`] it emits, and the
16/// transcript keyspace is keyed on the same quad. The run therefore has to arrive here from the
17/// dispatch that started the attempt; an adapter has no other source for it and must never
18/// synthesise one.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct AgentRunSpec {
21 /// The workflow this activity attempt belongs to.
22 pub workflow_id: WorkflowId,
23 /// The concrete run of that workflow this attempt belongs to. Required: a continue-as-new
24 /// chain reuses one `workflow_id` while ordinals and attempts restart per generation.
25 pub run_id: RunId,
26 /// The activity within the workflow.
27 pub activity_id: ActivityId,
28 /// The attempt number of the activity being run.
29 pub attempt: u32,
30 /// The activity-type name the engine dispatched to this run (e.g. `"dev"`).
31 ///
32 /// Neutral run identity, not harness configuration: it is the same type name the workflow
33 /// definition routed on, so an adapter can label or parameterise a run by what the engine
34 /// asked for without the spec learning anything harness-specific.
35 pub activity_type: String,
36 /// The activity input handed to the agent.
37 pub input: Payload,
38}
39
40impl AgentRunSpec {
41 /// Builds a run spec from the neutral run identity and input payload.
42 #[must_use]
43 pub fn new(
44 workflow_id: WorkflowId,
45 run_id: RunId,
46 activity_id: ActivityId,
47 attempt: u32,
48 activity_type: impl Into<String>,
49 input: Payload,
50 ) -> Self {
51 Self {
52 workflow_id,
53 run_id,
54 activity_id,
55 attempt,
56 activity_type: activity_type.into(),
57 input,
58 }
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use aion_core::{ActivityId, ContentType, Payload, RunId, WorkflowId};
65
66 use super::AgentRunSpec;
67
68 #[test]
69 fn spec_carries_run_identity_and_input() {
70 let workflow_id = WorkflowId::new_v4();
71 let run_id = RunId::new_v4();
72 let activity_id = ActivityId::from_sequence_position(4);
73 let input = Payload::new(ContentType::Json, b"{}".to_vec());
74
75 let spec = AgentRunSpec::new(
76 workflow_id.clone(),
77 run_id.clone(),
78 activity_id.clone(),
79 2,
80 "dev",
81 input.clone(),
82 );
83
84 assert_eq!(spec.workflow_id, workflow_id);
85 assert_eq!(spec.run_id, run_id);
86 assert_eq!(spec.activity_id, activity_id);
87 assert_eq!(spec.attempt, 2);
88 assert_eq!(spec.activity_type, "dev");
89 assert_eq!(spec.input, input);
90 }
91
92 /// Two generations of one continue-as-new chain: same workflow, same
93 /// activity ordinal, same attempt number — distinguished only by the run.
94 #[test]
95 fn two_generations_of_one_chain_are_distinct_specs() {
96 let workflow_id = WorkflowId::new_v4();
97 let activity_id = ActivityId::from_sequence_position(0);
98 let input = Payload::new(ContentType::Json, b"{}".to_vec());
99 let generation = |run_id: RunId| {
100 AgentRunSpec::new(
101 workflow_id.clone(),
102 run_id,
103 activity_id.clone(),
104 1,
105 "dev",
106 input.clone(),
107 )
108 };
109
110 let first = generation(RunId::new_v4());
111 let second = generation(RunId::new_v4());
112 assert_ne!(first, second);
113 assert_eq!(first.workflow_id, second.workflow_id);
114 assert_eq!(first.activity_id, second.activity_id);
115 assert_eq!(first.attempt, second.attempt);
116 }
117}