Skip to main content

ironflow_store/entities/
step.rs

1//! [`Step`] entity and related request/update types.
2
3use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use uuid::Uuid;
8
9use super::{ApprovalRequirement, Assignee, FsmState, StepApproval, StepKind, StepStatus};
10
11/// Attempt number assigned to steps deserialized from payloads predating the
12/// `attempt` field.
13fn default_attempt() -> u32 {
14    1
15}
16
17/// Generate a deterministic trace ID for a step.
18///
19/// Uses UUIDv5 with `NAMESPACE_OID` and the input
20/// `"{run_id}:{name}:{position}"`, so the same run replayed with the same
21/// steps always produces the same trace IDs.
22///
23/// # Examples
24///
25/// ```
26/// use ironflow_store::entities::step_trace_id;
27/// use uuid::Uuid;
28///
29/// let run_id = Uuid::nil();
30/// let id1 = step_trace_id(run_id, "build", 0);
31/// let id2 = step_trace_id(run_id, "build", 0);
32/// assert_eq!(id1, id2);
33///
34/// let id3 = step_trace_id(run_id, "test", 1);
35/// assert_ne!(id1, id3);
36/// ```
37pub fn step_trace_id(run_id: Uuid, name: &str, position: u32) -> Uuid {
38    Uuid::new_v5(
39        &Uuid::NAMESPACE_OID,
40        format!("{run_id}:{name}:{position}").as_bytes(),
41    )
42}
43
44/// Key of the role name (`AgentConfig::role`) in the persisted input of an
45/// agent step.
46///
47/// The engine persists the agent config as the step input; a config built
48/// from an `AgentRole` carries the role name under this key.
49///
50/// # Examples
51///
52/// ```
53/// use ironflow_store::entities::AGENT_ROLE_INPUT_KEY;
54///
55/// assert_eq!(AGENT_ROLE_INPUT_KEY, "role");
56/// ```
57pub const AGENT_ROLE_INPUT_KEY: &str = "role";
58
59/// Role name of an agent step, read from its persisted input.
60///
61/// Returns `Some` only for a [`StepKind::Agent`] step whose input holds a
62/// non-empty string under [`AGENT_ROLE_INPUT_KEY`]. Any other kind, a missing
63/// input or key, or a non-string or empty value gives `None`: steps persisted
64/// before roles were recorded have no role.
65///
66/// # Examples
67///
68/// ```
69/// use ironflow_store::entities::{StepKind, agent_role_from_input};
70/// use serde_json::json;
71///
72/// let input = json!({"type": "agent", "prompt": "p", "role": "Reviewer"});
73/// assert_eq!(
74///     agent_role_from_input(&StepKind::Agent, Some(&input)).as_deref(),
75///     Some("Reviewer")
76/// );
77/// assert_eq!(agent_role_from_input(&StepKind::Shell, Some(&input)), None);
78/// assert_eq!(agent_role_from_input(&StepKind::Agent, None), None);
79/// ```
80pub fn agent_role_from_input(kind: &StepKind, input: Option<&Value>) -> Option<String> {
81    if *kind != StepKind::Agent {
82        return None;
83    }
84    input?
85        .get(AGENT_ROLE_INPUT_KEY)?
86        .as_str()
87        .filter(|role| !role.is_empty())
88        .map(str::to_string)
89}
90
91/// A single operation within a run.
92///
93/// Steps are executed sequentially in order of [`position`](Step::position).
94///
95/// # Examples
96///
97/// ```
98/// use ironflow_store::entities::Step;
99///
100/// // Steps are created by RunStore::create_step, not directly.
101/// ```
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[non_exhaustive]
104pub struct Step {
105    /// Unique identifier (UUIDv7).
106    pub id: Uuid,
107    /// Deterministic trace ID for log correlation.
108    ///
109    /// Generated as `UUIDv5(NAMESPACE_OID, "{run_id}:{name}:{position}")` so
110    /// the same run replayed with the same steps always produces the same IDs.
111    pub trace_id: Uuid,
112    /// The run this step belongs to.
113    pub run_id: Uuid,
114    /// Human-readable step name (e.g. "build", "test", "review").
115    pub name: String,
116    /// The type of operation.
117    pub kind: StepKind,
118    /// Execution wave within the run (0-based).
119    ///
120    /// In linear flows, this strictly increases (0, 1, 2, ...).
121    /// In DAGs with parallel execution, steps at the same wave share
122    /// the same position and execute concurrently. Use
123    /// `step_dependencies` to determine the actual execution order.
124    pub position: u32,
125    /// Current FSM status — embeds state + state_machine_id for SQL-side transitions.
126    pub status: FsmState<StepStatus>,
127    /// Which run attempt produced this step (1-based).
128    ///
129    /// A run retried twice holds steps with `attempt` 1, 2 and 3. Steps from
130    /// earlier attempts are kept for inspection and are never replayed.
131    /// Derived by the store from `Run::retry_count` at creation time. A run
132    /// recovered after a lost worker lease stays in the same attempt, so its
133    /// finished steps are replayed.
134    #[serde(default = "default_attempt")]
135    pub attempt: u32,
136    /// Serialized operation configuration.
137    pub input: Option<Value>,
138    /// Serialized operation output.
139    pub output: Option<Value>,
140    /// Error message if the step failed.
141    pub error: Option<String>,
142    /// Wall-clock execution duration in milliseconds.
143    pub duration_ms: u64,
144    /// Cost in USD (agent steps only).
145    pub cost_usd: Decimal,
146    /// Uncached input token count (agent steps only).
147    pub input_tokens: Option<u64>,
148    /// Input tokens served from the prompt cache (agent steps only).
149    #[serde(default)]
150    pub cache_read_input_tokens: Option<u64>,
151    /// Input tokens written to the prompt cache (agent steps only).
152    #[serde(default)]
153    pub cache_creation_input_tokens: Option<u64>,
154    /// Output token count (agent steps only).
155    pub output_tokens: Option<u64>,
156    /// When the step was created.
157    pub created_at: DateTime<Utc>,
158    /// When the step record was last updated.
159    pub updated_at: DateTime<Utc>,
160    /// When step execution started.
161    pub started_at: Option<DateTime<Utc>>,
162    /// When step execution finished.
163    pub completed_at: Option<DateTime<Utc>>,
164    /// Debug messages (verbose conversation trace), stored as JSON.
165    pub debug_messages: Option<Value>,
166    /// Whether this step is an error handler (`on_error`) rather than a normal step.
167    #[serde(default)]
168    pub is_error_handler: bool,
169    /// When the approval gate on this step expires, if it carries an SLA deadline.
170    ///
171    /// Only ever set while the step is [`StepStatus::AwaitingApproval`]. Cleared
172    /// the moment the gate resolves (approved, rejected, or escalated).
173    #[serde(default)]
174    pub approval_deadline_at: Option<DateTime<Utc>>,
175    /// Index of the next escalation policy to apply when the deadline fires.
176    #[serde(default)]
177    pub approval_stage: u32,
178    /// User or group the approval is currently assigned to, after reassignment.
179    #[serde(default)]
180    pub approval_assignee: Option<Assignee>,
181    /// User the assignee resolved to when the gate was opened or last reassigned.
182    ///
183    /// `None` when the gate is assigned to a group, to nobody, or to a username no
184    /// account held at that moment: such a gate is admin-only, and an account
185    /// created later under that username is never bound to it.
186    #[serde(default)]
187    pub approval_assignee_user_id: Option<Uuid>,
188    /// Approvers the handler required when the gate opened. `None` for a gate
189    /// opened without approvers: one approval resolves it.
190    #[serde(default)]
191    pub approval_requirement: Option<ApprovalRequirement>,
192    /// Votes cast on the approval gate so far, at most one per user.
193    #[serde(default)]
194    pub approvals: Vec<StepApproval>,
195    /// Provider Account the agent step ran under, if any.
196    #[serde(default)]
197    pub account_id: Option<Uuid>,
198    /// Persistent environment the agent step ran in, if any: the id to pass
199    /// to `Agent::resume_environment` to continue in the same workspace.
200    #[serde(default)]
201    pub environment_id: Option<String>,
202    /// Claude Code session the agent step ran in, fixed before the agent
203    /// launched. The engine resumes it when the step was interrupted.
204    #[serde(default)]
205    pub session_id: Option<String>,
206    /// Name of the `AgentRole` the agent step was created from.
207    ///
208    /// Derived by the store from the step input with [`agent_role_from_input`]:
209    /// `None` for any other step, an agent step built by hand, or a step stored
210    /// before roles were recorded.
211    #[serde(default)]
212    pub agent_role: Option<String>,
213}
214
215/// Request to create a new step.
216///
217/// # Examples
218///
219/// ```
220/// use ironflow_store::entities::{NewStep, StepKind, step_trace_id};
221/// use serde_json::json;
222/// use uuid::Uuid;
223///
224/// let run_id = Uuid::nil();
225/// let req = NewStep {
226///     run_id,
227///     trace_id: step_trace_id(run_id, "build", 0),
228///     name: "build".to_string(),
229///     kind: StepKind::Shell,
230///     position: 0,
231///     input: Some(json!({"command": "cargo build"})),
232///     is_error_handler: false,
233/// };
234/// ```
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct NewStep {
237    /// The run this step belongs to.
238    pub run_id: Uuid,
239    /// Deterministic trace ID for log correlation.
240    pub trace_id: Uuid,
241    /// Step name.
242    pub name: String,
243    /// Operation type.
244    pub kind: StepKind,
245    /// Execution order (0-based).
246    pub position: u32,
247    /// Serialized operation configuration.
248    pub input: Option<Value>,
249    /// Whether this step is an error handler (`on_error`).
250    #[serde(default)]
251    pub is_error_handler: bool,
252}
253
254/// Partial update for a step after execution.
255///
256/// Only `Some` fields are applied; `None` fields are left unchanged.
257///
258/// # Examples
259///
260/// ```
261/// use ironflow_store::entities::{StepUpdate, StepStatus};
262/// use serde_json::json;
263///
264/// let update = StepUpdate {
265///     status: Some(StepStatus::Completed),
266///     output: Some(json!({"stdout": "ok"})),
267///     ..StepUpdate::default()
268/// };
269/// ```
270#[derive(Debug, Clone, Default, Serialize, Deserialize)]
271pub struct StepUpdate {
272    /// New status.
273    pub status: Option<StepStatus>,
274    /// Operation output.
275    pub output: Option<Value>,
276    /// Error message.
277    pub error: Option<String>,
278    /// Execution duration.
279    pub duration_ms: Option<u64>,
280    /// Cost in USD.
281    pub cost_usd: Option<Decimal>,
282    /// Uncached input token count.
283    pub input_tokens: Option<u64>,
284    /// Input tokens served from the prompt cache.
285    #[serde(default)]
286    pub cache_read_input_tokens: Option<u64>,
287    /// Input tokens written to the prompt cache.
288    #[serde(default)]
289    pub cache_creation_input_tokens: Option<u64>,
290    /// Output token count.
291    pub output_tokens: Option<u64>,
292    /// When execution started.
293    pub started_at: Option<DateTime<Utc>>,
294    /// When execution completed.
295    pub completed_at: Option<DateTime<Utc>>,
296    /// Debug messages (verbose conversation trace), stored as JSON.
297    pub debug_messages: Option<Value>,
298    /// New approval deadline. `None` leaves it unchanged — use
299    /// `clear_approval_deadline` to remove it.
300    #[serde(default)]
301    pub approval_deadline_at: Option<DateTime<Utc>>,
302    /// New escalation stage index.
303    #[serde(default)]
304    pub approval_stage: Option<u32>,
305    /// New approval assignee.
306    #[serde(default)]
307    pub approval_assignee: Option<Assignee>,
308    /// Approval requirement recorded when the gate opened.
309    #[serde(default)]
310    pub approval_requirement: Option<ApprovalRequirement>,
311    /// Clear the approval deadline (sets it to `NULL`). Wins over
312    /// `approval_deadline_at` when both are set.
313    #[serde(default)]
314    pub clear_approval_deadline: bool,
315    /// Provider Account the agent step ran under.
316    #[serde(default, skip_serializing_if = "Option::is_none")]
317    pub account_id: Option<Uuid>,
318    /// Persistent environment the agent step ran in.
319    #[serde(default, skip_serializing_if = "Option::is_none")]
320    pub environment_id: Option<String>,
321    /// Claude Code session the agent step runs in.
322    #[serde(default, skip_serializing_if = "Option::is_none")]
323    pub session_id: Option<String>,
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329    use serde_json::json;
330
331    #[test]
332    fn newstep_serde_roundtrip() {
333        let new_step = NewStep {
334            run_id: Uuid::nil(),
335            trace_id: step_trace_id(Uuid::nil(), "build", 0),
336            name: "build".to_string(),
337            kind: StepKind::Shell,
338            position: 0,
339            input: Some(json!({"command": "cargo build"})),
340            is_error_handler: false,
341        };
342
343        let json = serde_json::to_string(&new_step).expect("serialize");
344        let back: NewStep = serde_json::from_str(&json).expect("deserialize");
345
346        assert_eq!(back.run_id, new_step.run_id);
347        assert_eq!(back.name, new_step.name);
348        assert_eq!(back.kind, new_step.kind);
349        assert_eq!(back.position, new_step.position);
350        assert_eq!(back.input, new_step.input);
351    }
352
353    #[test]
354    fn step_serde_preserves_all_fields() {
355        use crate::entities::FsmState;
356        use chrono::Utc;
357
358        let now = Utc::now();
359        let run_id = Uuid::now_v7();
360        let step = Step {
361            id: Uuid::now_v7(),
362            trace_id: step_trace_id(run_id, "test-step", 1),
363            run_id,
364            name: "test-step".to_string(),
365            kind: StepKind::Agent,
366            position: 1,
367            status: FsmState::new(StepStatus::Completed, Uuid::now_v7()),
368            attempt: 2,
369            input: Some(json!({"input": "data"})),
370            output: Some(json!({"output": "result"})),
371            error: None,
372            duration_ms: 2500,
373            cost_usd: Decimal::new(150, 2),
374            input_tokens: Some(100),
375            cache_read_input_tokens: Some(4000),
376            cache_creation_input_tokens: Some(300),
377            output_tokens: Some(200),
378            created_at: now,
379            updated_at: now,
380            started_at: Some(now),
381            completed_at: Some(now),
382            debug_messages: None,
383            is_error_handler: false,
384            approval_deadline_at: Some(now),
385            approval_stage: 2,
386            approval_assignee: Some(Assignee::group("sre-oncall")),
387            approval_assignee_user_id: Some(Uuid::now_v7()),
388            approval_requirement: Some(ApprovalRequirement {
389                reason: Some("amount > 10k".to_string()),
390                required_approvers: 2,
391                approver_groups: vec!["finance".to_string()],
392            }),
393            approvals: vec![StepApproval {
394                user_id: Uuid::now_v7(),
395                approved_by: "alice".to_string(),
396                at: now,
397            }],
398            account_id: Some(Uuid::now_v7()),
399            environment_id: Some("ironflow-env-0a1b2c".to_string()),
400            session_id: Some("0192f0c1-7d2e-7a4b-9c3d-1e2f3a4b5c6d".to_string()),
401            agent_role: Some("Reviewer".to_string()),
402        };
403
404        let json = serde_json::to_string(&step).expect("serialize");
405        let back: Step = serde_json::from_str(&json).expect("deserialize");
406
407        assert_eq!(back.id, step.id);
408        assert_eq!(back.run_id, step.run_id);
409        assert_eq!(back.name, step.name);
410        assert_eq!(back.kind, step.kind);
411        assert_eq!(back.position, step.position);
412        assert_eq!(back.status.state, step.status.state);
413        assert_eq!(back.attempt, step.attempt);
414        assert_eq!(back.input, step.input);
415        assert_eq!(back.output, step.output);
416        assert_eq!(back.error, step.error);
417        assert_eq!(back.duration_ms, step.duration_ms);
418        assert_eq!(back.cost_usd, step.cost_usd);
419        assert_eq!(back.input_tokens, step.input_tokens);
420        assert_eq!(back.cache_read_input_tokens, step.cache_read_input_tokens);
421        assert_eq!(
422            back.cache_creation_input_tokens,
423            step.cache_creation_input_tokens
424        );
425        assert_eq!(back.output_tokens, step.output_tokens);
426        assert_eq!(back.account_id, step.account_id);
427        assert_eq!(back.environment_id, step.environment_id);
428        assert_eq!(back.session_id, step.session_id);
429        assert_eq!(back.agent_role, step.agent_role);
430        assert_eq!(back.approval_deadline_at, step.approval_deadline_at);
431        assert_eq!(back.approval_stage, step.approval_stage);
432        assert_eq!(back.approval_assignee, step.approval_assignee);
433        assert_eq!(
434            back.approval_assignee_user_id,
435            step.approval_assignee_user_id
436        );
437        assert_eq!(back.approval_requirement, step.approval_requirement);
438        assert_eq!(back.approvals, step.approvals);
439    }
440
441    #[test]
442    fn step_serde_defaults_approval_fields_when_absent() {
443        let run_id = Uuid::now_v7();
444        let payload = json!({
445            "id": Uuid::now_v7(),
446            "trace_id": step_trace_id(run_id, "legacy", 0),
447            "run_id": run_id,
448            "name": "legacy",
449            "kind": "shell",
450            "position": 0,
451            "status": {"state": "pending", "state_machine_id": Uuid::now_v7()},
452            "input": null,
453            "output": null,
454            "error": null,
455            "duration_ms": 0,
456            "cost_usd": 0.0,
457            "input_tokens": null,
458            "cache_read_input_tokens": null,
459            "cache_creation_input_tokens": null,
460            "output_tokens": null,
461            "created_at": "2026-09-21T12:00:00Z",
462            "updated_at": "2026-09-21T12:00:00Z",
463            "started_at": null,
464            "completed_at": null,
465            "debug_messages": null
466        });
467
468        let step: Step = serde_json::from_value(payload).expect("deserialize");
469
470        assert_eq!(step.approval_stage, 0);
471        assert!(step.cache_read_input_tokens.is_none());
472        assert!(step.cache_creation_input_tokens.is_none());
473        assert!(step.approval_deadline_at.is_none());
474        assert!(step.approval_assignee.is_none());
475        assert!(step.approval_assignee_user_id.is_none());
476        assert!(step.approval_requirement.is_none());
477        assert!(step.approvals.is_empty());
478        assert!(step.environment_id.is_none());
479        assert!(step.session_id.is_none());
480        assert!(step.agent_role.is_none());
481    }
482
483    #[test]
484    fn stepupdate_default_is_no_changes() {
485        let update = StepUpdate::default();
486        assert!(update.status.is_none());
487        assert!(update.output.is_none());
488        assert!(update.error.is_none());
489        assert!(update.duration_ms.is_none());
490        assert!(update.cost_usd.is_none());
491        assert!(update.input_tokens.is_none());
492        assert!(update.cache_read_input_tokens.is_none());
493        assert!(update.cache_creation_input_tokens.is_none());
494        assert!(update.output_tokens.is_none());
495        assert!(update.started_at.is_none());
496        assert!(update.completed_at.is_none());
497        assert!(update.debug_messages.is_none());
498        assert!(update.approval_deadline_at.is_none());
499        assert!(update.approval_stage.is_none());
500        assert!(update.approval_assignee.is_none());
501        assert!(update.approval_requirement.is_none());
502        assert!(!update.clear_approval_deadline);
503        assert!(update.environment_id.is_none());
504        assert!(update.session_id.is_none());
505    }
506
507    #[test]
508    fn stepupdate_serde_roundtrip() {
509        let update = StepUpdate {
510            status: Some(StepStatus::Completed),
511            output: Some(json!({"result": "ok"})),
512            error: None,
513            duration_ms: Some(1000),
514            cost_usd: Some(Decimal::new(50, 2)),
515            input_tokens: Some(50),
516            cache_read_input_tokens: Some(2000),
517            cache_creation_input_tokens: Some(150),
518            output_tokens: Some(75),
519            started_at: None,
520            completed_at: None,
521            debug_messages: None,
522            approval_deadline_at: Some(Utc::now()),
523            approval_stage: Some(1),
524            approval_assignee: Some(Assignee::group("sre-oncall")),
525            approval_requirement: Some(ApprovalRequirement::default()),
526            clear_approval_deadline: false,
527            account_id: Some(Uuid::now_v7()),
528            environment_id: Some("ironflow-env-0a1b2c".to_string()),
529            session_id: Some("0192f0c1-7d2e-7a4b-9c3d-1e2f3a4b5c6d".to_string()),
530        };
531
532        let json = serde_json::to_string(&update).expect("serialize");
533        let back: StepUpdate = serde_json::from_str(&json).expect("deserialize");
534
535        assert_eq!(back.status, update.status);
536        assert_eq!(back.output, update.output);
537        assert_eq!(back.duration_ms, update.duration_ms);
538        assert_eq!(back.account_id, update.account_id);
539        assert_eq!(back.environment_id, update.environment_id);
540        assert_eq!(back.session_id, update.session_id);
541        assert_eq!(back.cost_usd, update.cost_usd);
542        assert_eq!(back.input_tokens, update.input_tokens);
543        assert_eq!(back.cache_read_input_tokens, update.cache_read_input_tokens);
544        assert_eq!(
545            back.cache_creation_input_tokens,
546            update.cache_creation_input_tokens
547        );
548        assert_eq!(back.output_tokens, update.output_tokens);
549        assert_eq!(back.approval_deadline_at, update.approval_deadline_at);
550        assert_eq!(back.approval_stage, update.approval_stage);
551        assert_eq!(back.approval_assignee, update.approval_assignee);
552        assert_eq!(back.approval_requirement, update.approval_requirement);
553        assert_eq!(back.clear_approval_deadline, update.clear_approval_deadline);
554    }
555
556    #[test]
557    fn stepupdate_deserializes_without_cache_fields() {
558        let payload = json!({
559            "status": "completed",
560            "output": null,
561            "error": null,
562            "duration_ms": 10,
563            "cost_usd": null,
564            "input_tokens": 12,
565            "output_tokens": 3,
566            "started_at": null,
567            "completed_at": null,
568            "debug_messages": null
569        });
570
571        let update: StepUpdate = serde_json::from_value(payload).expect("deserialize");
572
573        assert_eq!(update.input_tokens, Some(12));
574        assert!(update.cache_read_input_tokens.is_none());
575        assert!(update.cache_creation_input_tokens.is_none());
576    }
577
578    #[test]
579    fn trace_id_is_deterministic() {
580        let run_id = Uuid::nil();
581        let id1 = step_trace_id(run_id, "build", 0);
582        let id2 = step_trace_id(run_id, "build", 0);
583        assert_eq!(id1, id2);
584    }
585
586    #[test]
587    fn trace_id_differs_for_different_inputs() {
588        let run_id = Uuid::nil();
589        let a = step_trace_id(run_id, "build", 0);
590        let b = step_trace_id(run_id, "test", 0);
591        let c = step_trace_id(run_id, "build", 1);
592        let d = step_trace_id(Uuid::max(), "build", 0);
593
594        assert_ne!(a, b);
595        assert_ne!(a, c);
596        assert_ne!(a, d);
597    }
598
599    #[test]
600    fn trace_id_is_uuid_v5() {
601        let id = step_trace_id(Uuid::nil(), "build", 0);
602        assert_eq!(id.get_version_num(), 5);
603    }
604
605    #[test]
606    fn agent_role_is_read_from_an_agent_step_input() {
607        let input = json!({"type": "agent", "prompt": "p", "role": "Reviewer"});
608        assert_eq!(
609            agent_role_from_input(&StepKind::Agent, Some(&input)).as_deref(),
610            Some("Reviewer")
611        );
612    }
613
614    #[test]
615    fn agent_role_is_none_without_a_usable_role() {
616        for input in [
617            json!({"type": "agent", "prompt": "p"}),
618            json!({"type": "agent", "prompt": "p", "role": null}),
619            json!({"type": "agent", "prompt": "p", "role": 42}),
620            json!({"type": "agent", "prompt": "p", "role": ""}),
621            json!("not an object"),
622        ] {
623            assert_eq!(
624                agent_role_from_input(&StepKind::Agent, Some(&input)),
625                None,
626                "input: {input}"
627            );
628        }
629    }
630
631    #[test]
632    fn agent_role_is_none_for_another_kind() {
633        let input = json!({"type": "shell", "command": "ls", "role": "Reviewer"});
634        assert_eq!(agent_role_from_input(&StepKind::Shell, Some(&input)), None);
635        assert_eq!(
636            agent_role_from_input(&StepKind::Custom("x".to_string()), Some(&input)),
637            None
638        );
639    }
640
641    #[test]
642    fn agent_role_is_none_without_input() {
643        assert_eq!(agent_role_from_input(&StepKind::Agent, None), None);
644    }
645}