warble_claude_code/ir.rs
1//! Typed view of the Warble IR (`warble_ir_version: 0.7`) that this back-end consumes.
2//!
3//! Mirrors [`ir-schema.md`][spec-ir] field-for-field. The IR JSON is the language-neutral seam
4//! between the front-end compiler and any back-end: this module depends on the schema doc, not on
5//! the front-end's Rust types. Enum arms cover every schema-valid value; arms this target does not
6//! yet realize are rejected at emit time (a "wall-hit"), not at deserialization.
7//!
8//! [spec-ir]: https://github.com/Canner/Warble/blob/main/docs/spec/ir-schema.md
9
10use crate::error::DispatchError;
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13
14/// IR version this back-end understands. Copied (not shared via a `core` dependency — dispatchers
15/// never depend on `warble`) from the same source of truth as `docs/spec/ir-schema.md`'s title and
16/// the TS back-end's `SUPPORTED_IR_VERSIONS` in `dispatcher/claude-agent-sdk/src/ir.ts`. The sole
17/// cross-target lockstep owner is `core/tests/ir_version_lockstep_tests.rs`; this target's
18/// `ir_version_tests.rs` covers only its own rejection behavior. An unrecognized
19/// `warble_ir_version` is a loud-fail — see [`validate_ir_version`] — never a silent best-effort
20/// read.
21pub const SUPPORTED_IR_VERSION: &str = "0.7";
22
23/// The one version gate every IR-consuming entry point in this crate (and the `cli` binary, at IR
24/// parse time) must call before doing anything else with `ir`: `emit_claude_code_with_realization`
25/// calls this as its first statement, and `cli::load_ir` calls it right after deserializing, so
26/// every subcommand that reads an `ir.json` — `dispatch` and `manifest` alike — is covered by the
27/// same check instead of each reimplementing (and potentially forgetting) it.
28pub fn validate_ir_version(ir: &WarbleIr) -> Result<(), DispatchError> {
29 if ir.warble_ir_version != SUPPORTED_IR_VERSION {
30 return Err(DispatchError::new(format!(
31 "unsupported warble_ir_version '{}' (this back-end understands: {SUPPORTED_IR_VERSION})",
32 ir.warble_ir_version
33 )));
34 }
35 Ok(())
36}
37
38/// A step's tier is an **open string**, not a fixed enum. Warble ships a standard core vocabulary
39/// (`strong`, `cheap`) that keeps components portable, but a component may name a custom tier; the
40/// dispatch-time model config (see `ModelConfig`) maps each tier name to a concrete model, and a
41/// tier with no mapping is a loud-fail. Kept as `String` so the seam never constrains the vocabulary.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "kebab-case")]
44pub enum RealizationKind {
45 Skill,
46 Tool,
47 GatedTool,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "lowercase")]
52pub enum ComponentType {
53 Analytical,
54 Assertive,
55 Mutating,
56 Constitutive,
57 Orchestrating,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum TriggerKind {
63 OneShot,
64 Scheduled,
65 Event,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "lowercase")]
70pub enum OutcomeKind {
71 None,
72 Assertion,
73 Mutation,
74 Dispatch,
75}
76
77impl RealizationKind {
78 pub fn as_str(&self) -> &'static str {
79 match self {
80 RealizationKind::Skill => "skill",
81 RealizationKind::Tool => "tool",
82 RealizationKind::GatedTool => "gated-tool",
83 }
84 }
85}
86
87impl TriggerKind {
88 pub fn as_str(&self) -> &'static str {
89 match self {
90 TriggerKind::OneShot => "one_shot",
91 TriggerKind::Scheduled => "scheduled",
92 TriggerKind::Event => "event",
93 }
94 }
95}
96
97impl OutcomeKind {
98 pub fn as_str(&self) -> &'static str {
99 match self {
100 OutcomeKind::None => "none",
101 OutcomeKind::Assertion => "assertion",
102 OutcomeKind::Mutation => "mutation",
103 OutcomeKind::Dispatch => "dispatch",
104 }
105 }
106}
107
108impl ComponentType {
109 pub fn as_str(&self) -> &'static str {
110 match self {
111 ComponentType::Analytical => "analytical",
112 ComponentType::Assertive => "assertive",
113 ComponentType::Mutating => "mutating",
114 ComponentType::Constitutive => "constitutive",
115 ComponentType::Orchestrating => "orchestrating",
116 }
117 }
118}
119
120#[derive(Debug, Clone, Deserialize)]
121pub struct ContextBinding {
122 pub project: String,
123 pub binding_mode: String,
124 /// Fine-grained resolved binding (IR v0.3): metrics/dimensions/grains/lineage summary the
125 /// front-end learned from the bound semantic layer. Carried through and tolerated here; the
126 /// Claude Code back-end does not yet consume it (it still drives off the coarse project path).
127 #[serde(default)]
128 pub resolved: Option<serde_json::Value>,
129}
130
131/// The IR's profile-level `config` block. Empty since IR `0.6` removed `tier_policy` (an inert
132/// field no back-end read); kept as a struct so future profile-level config is an additive change.
133#[derive(Debug, Clone, Deserialize)]
134pub struct IrConfig {}
135
136#[derive(Debug, Clone, Deserialize)]
137pub struct LlmCall {
138 pub name: String,
139 /// Tier name (conventionally `strong`/`cheap`; custom names allowed — see [`RealizationKind`]
140 /// docs and `ModelConfig`). Resolved to a concrete model at dispatch.
141 pub tier: String,
142 #[serde(default)]
143 pub consumes: Vec<String>,
144 #[serde(default)]
145 pub produces: Option<String>,
146 pub prompt: String,
147 /// Whether this step only runs under a runtime condition (e.g. a prior step's outcome).
148 #[serde(default)]
149 pub conditional: bool,
150 /// The closed-vocabulary guard deciding whether a `conditional` step runs (IR v0.3+; see
151 /// [`ir-schema.md`][spec-ir]). This back-end's actual emission still treats `conditional` as an
152 /// opaque flag — the static Claude Code CLI target has no deterministic runtime, so the guard is
153 /// judged emergently by the live CLI agent from the emitted prompt text, not evaluated here.
154 /// `conditional.rs` carries a deterministic evaluator for the same closed vocabulary, but it is
155 /// conformance-only (tested against a fixture shared with the SDK back-end's evaluator, not
156 /// wired into emission). This field is tolerated either way so the seam stays forward-compatible.
157 ///
158 /// [spec-ir]: https://github.com/Canner/Warble/blob/main/docs/spec/ir-schema.md
159 #[serde(default)]
160 pub when: Option<WhenGuard>,
161}
162
163/// A closed-vocabulary guard on a conditional `llm_call`: `guard` is one of `on_failure` /
164/// `on_flag` / `on_missing`, `target` is the guard-specific argument. See
165/// [`ir-schema.md`][spec-ir].
166///
167/// [spec-ir]: https://github.com/Canner/Warble/blob/main/docs/spec/ir-schema.md
168#[derive(Debug, Clone, Deserialize)]
169pub struct WhenGuard {
170 pub guard: String,
171 pub target: String,
172}
173
174#[derive(Debug, Clone, Deserialize)]
175pub struct Guardrail {
176 pub name: String,
177 pub locked: bool,
178 #[serde(default)]
179 pub scope: Option<String>,
180 /// Optional numeric or structured threshold the guardrail enforces (shape is guardrail-specific).
181 #[serde(default)]
182 pub threshold: Option<serde_json::Value>,
183}
184
185#[derive(Debug, Clone, Deserialize)]
186pub struct Trigger {
187 pub kind: TriggerKind,
188}
189
190/// A typed render block: a block type plus its field-name → field-type schema. The field-type
191/// strings are echoed into the agent's render contract verbatim; the back-end never interprets
192/// them. `BTreeMap` keeps field order stable (deterministic output).
193#[derive(Debug, Clone, Deserialize)]
194pub struct RenderBlock {
195 #[serde(rename = "type")]
196 pub block_type: String,
197 #[serde(default)]
198 pub fields: BTreeMap<String, String>,
199}
200
201#[derive(Debug, Clone, Deserialize)]
202pub struct Outcome {
203 pub kind: OutcomeKind,
204 /// Free-form verdict classification for `assertion`-kind outcomes.
205 #[serde(default)]
206 pub verdict_type: Option<String>,
207 /// Event/signal names this outcome emits (e.g. for `dispatch`-kind outcomes).
208 #[serde(default)]
209 pub emits: Option<Vec<String>>,
210 /// The mutation/dispatch target, when applicable.
211 #[serde(default)]
212 pub target: Option<String>,
213 /// The kind of change made, for `mutation`-kind outcomes.
214 #[serde(default)]
215 pub change_type: Option<String>,
216 /// Routing scope for `dispatch`-kind outcomes (shape is dispatch-target-specific).
217 #[serde(default)]
218 pub routable_scope: Option<serde_json::Value>,
219}
220
221#[derive(Debug, Clone, Deserialize)]
222pub struct Effect {
223 #[serde(default)]
224 pub render_blocks: Vec<RenderBlock>,
225 pub outcome: Outcome,
226}
227
228#[derive(Debug, Clone, Deserialize)]
229pub struct PreconditionResult {
230 pub status: String,
231 /// Per-predicate evaluation results (IR v0.3): each `{predicate, outcome}`. In v0.2 this was a
232 /// free-form string list; v0.3 makes it structured now that predicates are really evaluated.
233 #[serde(default)]
234 pub checks: Vec<PreconditionCheck>,
235}
236
237/// One evaluated `context_precondition` and its outcome (`pass` in emitted IR — a failing
238/// predicate loud-fails the compile before any IR is emitted).
239#[derive(Debug, Clone, Deserialize)]
240pub struct PreconditionCheck {
241 pub predicate: String,
242 pub outcome: String,
243}
244
245/// A context precondition a component requires to hold before it runs (e.g. `has_metric`).
246#[derive(Debug, Clone, Deserialize)]
247pub struct Precondition {
248 pub predicate: String,
249 #[serde(default)]
250 pub args: Option<serde_json::Value>,
251}
252
253/// A component parameter, either bound at dispatch time (`bind`, with an optional `default`) or
254/// sourced from context (`source`). Exactly one of `bind`/`source` is expected to be present per
255/// the schema, but both are optional here since this is a Deserialize-only view.
256#[derive(Debug, Clone, Deserialize)]
257pub struct Param {
258 pub name: String,
259 #[serde(default)]
260 pub bind: Option<String>,
261 #[serde(default)]
262 pub source: Option<String>,
263 #[serde(default)]
264 pub default: Option<serde_json::Value>,
265}
266
267/// An authored evaluation spec: which eval template to run and which metrics it scores.
268#[derive(Debug, Clone, Deserialize)]
269pub struct EvalSpec {
270 pub template_ref: String,
271 #[serde(default)]
272 pub metrics: Vec<String>,
273}
274
275#[derive(Debug, Clone, Deserialize)]
276pub struct ComponentNode {
277 pub id: String,
278 pub verb: String,
279 #[serde(rename = "type")]
280 pub component_type: ComponentType,
281 pub realization_kind: RealizationKind,
282 pub context_binding: ContextBinding,
283 pub precondition_result: PreconditionResult,
284 pub prompt_fragment: String,
285 pub llm_calls: Vec<LlmCall>,
286 pub guardrails: Vec<Guardrail>,
287 pub trigger: Trigger,
288 #[serde(default)]
289 pub required_capabilities: Vec<String>,
290 #[serde(default)]
291 pub borrowed_actions: Vec<String>,
292 pub eval_ref: String,
293 pub effect: Effect,
294 /// Free-text context capabilities this component requires (e.g. "a wren project ...").
295 #[serde(default)]
296 pub context_requirements: Vec<String>,
297 /// Structured preconditions the bound context must satisfy (e.g. `has_metric`).
298 #[serde(default)]
299 pub context_precondition: Vec<Precondition>,
300 /// Component parameters, either context-sourced or dispatch-bound.
301 #[serde(default)]
302 pub params: Vec<Param>,
303 /// Authored evaluation spec, when the component declares one.
304 #[serde(default)]
305 pub eval: Option<EvalSpec>,
306 /// Optional free-form framing shared by every step of this component (see
307 /// `docs/spec/ir-schema.md`).
308 #[serde(default)]
309 pub brief: Option<String>,
310 /// What this component is for, authored for a reader choosing between components rather than for
311 /// the agent doing the work. Absent when the author wrote none, in which case the back-end
312 /// synthesizes a shape line instead.
313 #[serde(default)]
314 pub description: Option<String>,
315 /// Example requests this component is the right destination for.
316 #[serde(default)]
317 pub examples: Vec<String>,
318}
319
320#[derive(Debug, Clone, Deserialize)]
321pub struct WarbleIr {
322 pub warble_ir_version: String,
323 pub profile: String,
324 pub context_binding: ContextBinding,
325 pub config: IrConfig,
326 pub components: Vec<ComponentNode>,
327}