Skip to main content

runx_parser/graph/
types.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use runx_contracts::{JsonObject, JsonValue};
6
7#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
8#[serde(deny_unknown_fields)]
9pub struct RawGraphIr {
10    pub document: JsonObject,
11}
12
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase", deny_unknown_fields)]
15pub struct GraphContextEdge {
16    pub input: String,
17    pub from_step: String,
18    pub output: String,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase", deny_unknown_fields)]
23pub struct GraphRetryPolicy {
24    pub max_attempts: u64,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub backoff_ms: Option<u64>,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum FanoutSyncStrategy {
32    All,
33    Any,
34    Quorum,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum FanoutBranchFailurePolicy {
40    Halt,
41    Continue,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum FanoutThresholdAction {
47    Pause,
48    Escalate,
49}
50
51#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub struct FanoutThresholdGate {
54    pub step: String,
55    pub field: String,
56    pub above: f64,
57    pub action: FanoutThresholdAction,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum FanoutConflictAction {
63    Pause,
64    Escalate,
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase", deny_unknown_fields)]
69pub struct FanoutConflictGate {
70    pub field: String,
71    pub steps: Vec<String>,
72    pub action: FanoutConflictAction,
73}
74
75#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase", deny_unknown_fields)]
77pub struct FanoutGroupPolicy {
78    pub group_id: String,
79    pub strategy: FanoutSyncStrategy,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub min_success: Option<u64>,
82    pub on_branch_failure: FanoutBranchFailurePolicy,
83    pub threshold_gates: Vec<FanoutThresholdGate>,
84    pub conflict_gates: Vec<FanoutConflictGate>,
85}
86
87#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase", deny_unknown_fields)]
89pub struct GraphGuard {
90    pub step: String,
91    pub field: String,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub equals: Option<JsonValue>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub not_equals: Option<JsonValue>,
96}
97
98#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
99#[serde(deny_unknown_fields)]
100pub struct GraphPolicy {
101    pub guards: Vec<GraphGuard>,
102}
103
104/// Per-step conditional selection. When present and the condition does not hold
105/// (or the field is unresolved), the step is skipped and the graph continues;
106/// sibling steps with complementary conditions form a branch. Unlike a guard,
107/// a `when` never blocks the run, it selects.
108#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
109#[serde(rename_all = "camelCase", deny_unknown_fields)]
110pub struct GraphWhen {
111    pub field: String,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub equals: Option<JsonValue>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub not_equals: Option<JsonValue>,
116}
117
118/// Where [`MintAuthorityDirective`] draws the requested child scope from when the
119/// runtime computes the attenuation off the model path. The two cases are
120/// mutually exclusive by construction, so a step can never feed the mint from two
121/// sources at once.
122#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum MintScopeSource {
125    /// Derive the child from the step's static `scopes:` list (the common
126    /// in-graph case). The declared ceiling and the minted term share one source,
127    /// so they cannot drift.
128    StaticScopes,
129    /// Derive the child from a runtime input named by `requested_scope_from` (the
130    /// dynamic case, e.g. an ops-desk-chosen scope). The mint fail-closes if the
131    /// requested scope exceeds the charter.
132    RequestedScope,
133}
134
135/// Declarative request to MINT (compute) the step's child authority term from the
136/// graph charter, off the model path, rather than receive a pre-built term. This
137/// is the compute path; the act-declaration `authority_term_from` /
138/// `authority_parent_from` / `authority_subset_proof_from` keys remain the
139/// explicit pre-built path. A directive is only coherent when the graph (or
140/// runner) declares `charter_from`, since the mint narrows that parent charter.
141#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase", deny_unknown_fields)]
143pub struct MintAuthorityDirective {
144    pub source: MintScopeSource,
145}
146
147#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase", deny_unknown_fields)]
149pub struct GraphStep {
150    pub id: String,
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub label: Option<String>,
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub skill: Option<String>,
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub tool: Option<String>,
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub run: Option<JsonObject>,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub instructions: Option<String>,
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub artifacts: Option<JsonObject>,
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub runner: Option<String>,
165    pub inputs: JsonObject,
166    pub context: BTreeMap<String, String>,
167    pub context_edges: Vec<GraphContextEdge>,
168    #[serde(default, skip_serializing_if = "Vec::is_empty")]
169    pub context_skills: Vec<String>,
170    pub scopes: Vec<String>,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub allowed_tools: Option<Vec<String>>,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub retry: Option<GraphRetryPolicy>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub policy: Option<JsonObject>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub fanout_group: Option<String>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub when: Option<GraphWhen>,
181    pub mutating: bool,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub idempotency_key: Option<String>,
184    /// Compute path: when present, the runtime mints this step's child authority
185    /// term from the graph charter (named by `ExecutionGraph::charter_from`) off
186    /// the model path, instead of receiving a pre-built term via the act
187    /// declaration's `authority_*_from` keys.
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub mint_authority: Option<MintAuthorityDirective>,
190    /// The input key carrying the requested child scope, used only when
191    /// `mint_authority.source` is [`MintScopeSource::RequestedScope`]. The static
192    /// `scopes:` list is the source for [`MintScopeSource::StaticScopes`]; the two
193    /// are mutually exclusive.
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub requested_scope_from: Option<String>,
196}
197
198#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase", deny_unknown_fields)]
200pub struct ExecutionGraph {
201    pub name: String,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub owner: Option<String>,
204    /// The input key carrying the parent charter authority term that steps with a
205    /// `mint_authority` directive attenuate from. Declared once at the graph (or
206    /// runner) level, replacing per-skill re-threading of the parent authority. A
207    /// step's `mint_authority` is only coherent when this is set.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub charter_from: Option<String>,
210    pub steps: Vec<GraphStep>,
211    pub fanout_groups: BTreeMap<String, FanoutGroupPolicy>,
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub policy: Option<GraphPolicy>,
214    pub raw: RawGraphIr,
215}