Skip to main content

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}