Skip to main content

af_workflow/
node.rs

1//! Node trait + context. Port of `platform/node_protocol.py`.
2//!
3//! A spec has two node kinds. Ingress nodes are pure event sources and are
4//! handled by the runner (next phase); this module models the **step node** —
5//! a pure transformer: one event in, one [`StepResult`] out. State is the only
6//! cross-run channel, scoped to the branch by id prefix.
7
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use serde_json::Value;
12
13use crate::event::Event;
14use crate::recorder::{noop_recorder, RunRecorder};
15use crate::result::StepResult;
16use crate::state::State;
17
18/// The execution context handed to every step node.
19///
20/// Port of the subset of `WorkflowContext` a node legitimately touches:
21/// branch id (for state scoping), instance config + metadata (for template
22/// resolution), and the state handle. I/O tool calls (`ctx.do(...)`) are the
23/// next porting phase.
24pub struct WorkflowContext {
25    pub branch_id: String,
26    pub config: Value,
27    pub instance_metadata: Value,
28    pub state: Arc<dyn State>,
29    pub recorder: Arc<dyn RunRecorder>,
30    pub trigger_kind: String,
31}
32
33impl WorkflowContext {
34    pub fn new(branch_id: impl Into<String>, state: Arc<dyn State>) -> Self {
35        Self {
36            branch_id: branch_id.into(),
37            config: Value::Object(Default::default()),
38            instance_metadata: Value::Object(Default::default()),
39            state,
40            recorder: noop_recorder(),
41            trigger_kind: "event".into(),
42        }
43    }
44
45    pub fn with_config(mut self, config: Value) -> Self {
46        self.config = config;
47        self
48    }
49
50    pub fn with_recorder(mut self, recorder: Arc<dyn RunRecorder>) -> Self {
51        self.recorder = recorder;
52        self
53    }
54
55    pub fn with_trigger_kind(mut self, trigger_kind: impl Into<String>) -> Self {
56        self.trigger_kind = trigger_kind.into();
57        self
58    }
59
60    /// Prefix a state key with the branch id (R6 scoping).
61    pub fn scoped(&self, key: &str) -> String {
62        format!("{}.{}", self.branch_id, key)
63    }
64}
65
66/// A pure transformer node. One event in, one result out.
67#[async_trait]
68pub trait StepNode: Send + Sync {
69    /// Whether `process` may return [`StepResult::FanOut`]. Default `false`.
70    fn produces_fan_out(&self) -> bool {
71        false
72    }
73
74    async fn process(&self, event: &Event, ctx: &WorkflowContext) -> StepResult;
75}