mlua_swarm_schema/lib.rs
1//! Blueprint schema — Swarm IF SoT (= the core type set that defines "how a Blueprint object is written").
2//!
3//! This crate provides **schema types + serde derives only** as a pure IF crate. Execution
4//! layers (SpawnerFactory / EngineDispatcher / Compiler) are not included here; consumers
5//! (the `mlua-swarm` crate) own them. External consumers, sibling worktrees, and
6//! future bundles can read/write Blueprints by depending on this single crate.
7//!
8//! # Versioning contract
9//!
10//! `Blueprint.schema_version` is tied to this crate's semver. It is fixed at 0.1.0 for now;
11//! during 0.x breaking changes are free, and 1.0 will freeze the schema.
12//!
13//! # IN-immutability (extension discipline)
14//!
15//! This crate is the IN side of the swarm layering and stays **plain serde
16//! data**: no compile pass, no field the engine macro-expands, no DSL
17//! dialect. Flow conds are written literally against the Flow.ir Expr set
18//! (`Eq($.<step>.verdict, Lit("blocked"))` — domain verdicts are plain
19//! strings in step output). Authoring sugar (builders) lives OUT on the
20//! consumer side; runtime behavior extension lives in the engine's
21//! `SpawnerLayer` middleware.
22//!
23//! # AgentKind handling (= internal SoT)
24//!
25//! [`AgentKind`] is the SoT for the SpawnerAdapter offering axis. It is a closed enum managed
26//! inside Swarm, extended by variant addition through **explicit maintenance**. String lookup
27//! or a `Custom` escape hatch is deliberately avoided (= structurally eliminates the "silly
28//! runtime typos" class of failures).
29//!
30//! # Examples
31//!
32//! Build a minimal [`Blueprint`] with a single [`AgentDef`] via struct literal:
33//!
34//! ```
35//! use mlua_swarm_schema::{
36//! AgentDef, AgentKind, Blueprint, current_schema_version,
37//! };
38//! use mlua_flow_ir::{Expr, Node};
39//! use serde_json::json;
40//!
41//! let bp = Blueprint {
42//! schema_version: current_schema_version(),
43//! id: "hello".into(),
44//! flow: Node::Step {
45//! ref_: "greeter".into(),
46//! in_: Expr::Lit { value: json!({"name": "world"}) },
47//! out: Expr::Path { at: "$.greeting".into() },
48//! },
49//! agents: vec![AgentDef {
50//! name: "greeter".into(),
51//! kind: AgentKind::RustFn,
52//! spec: json!({"fn_id": "hello_world"}),
53//! profile: None,
54//! meta: None,
55//! }],
56//! operators: vec![],
57//! hints: Default::default(),
58//! strategy: Default::default(),
59//! metadata: Default::default(),
60//! spawner_hints: Default::default(),
61//! default_agent_kind: AgentKind::Operator,
62//! default_operator_kind: None,
63//! };
64//!
65//! assert_eq!(bp.id, "hello");
66//! assert_eq!(bp.agents.len(), 1);
67//! assert_eq!(bp.strategy.strict_refs, true);
68//! ```
69//!
70//! Round-trip a [`Blueprint`] through JSON (= confirms `serde` derives and the
71//! `deny_unknown_fields` contract):
72//!
73//! ```
74//! use mlua_swarm_schema::{AgentKind, Blueprint, BlueprintMetadata};
75//! use mlua_flow_ir::{Expr, Node};
76//! use serde_json::json;
77//!
78//! let bp = Blueprint {
79//! schema_version: mlua_swarm_schema::current_schema_version(),
80//! id: "roundtrip".into(),
81//! flow: Node::Seq { children: vec![] },
82//! agents: vec![],
83//! operators: vec![],
84//! hints: Default::default(),
85//! strategy: Default::default(),
86//! metadata: BlueprintMetadata {
87//! description: Some("roundtrip smoke".into()),
88//! default_run_ttl_secs: Some(1800),
89//! ..Default::default()
90//! },
91//! spawner_hints: Default::default(),
92//! default_agent_kind: AgentKind::Operator,
93//! default_operator_kind: None,
94//! };
95//!
96//! let json = serde_json::to_string(&bp).unwrap();
97//! let back: Blueprint = serde_json::from_str(&json).unwrap();
98//! assert_eq!(bp, back);
99//! assert_eq!(back.metadata.default_run_ttl_secs, Some(1800));
100//! ```
101
102#![warn(missing_docs)]
103
104use mlua_flow_ir::Node as FlowNode;
105use schemars::JsonSchema;
106use serde::{Deserialize, Serialize};
107use serde_json::Value;
108use std::collections::HashMap;
109
110// ──────────────────────────────────────────────────────────────────────────
111// Versioning
112// ──────────────────────────────────────────────────────────────────────────
113
114/// Current Blueprint schema version. Tied to this crate's semver.
115pub const CURRENT_SCHEMA_VERSION: &str = "0.1.0";
116
117fn default_schema_version() -> semver::Version {
118 current_schema_version()
119}
120
121/// Blueprint construction helper: returns the semver of the current schema version.
122/// Callers can write `schema_version: current_schema_version(),`.
123pub fn current_schema_version() -> semver::Version {
124 semver::Version::parse(CURRENT_SCHEMA_VERSION)
125 .expect("CURRENT_SCHEMA_VERSION must be valid semver")
126}
127
128// ──────────────────────────────────────────────────────────────────────────
129// Blueprint (top-level package)
130// ──────────────────────────────────────────────────────────────────────────
131
132/// Unified package of flow.ir + Swarm extension layers. The entry-point type of Swarm.
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
134#[serde(deny_unknown_fields)]
135pub struct Blueprint {
136 /// Schema version (= tied to this crate's semver). Default = `CURRENT_SCHEMA_VERSION`.
137 /// Serialized as a semver string (e.g. `"0.1.0"`).
138 #[serde(default = "default_schema_version")]
139 #[schemars(with = "String")]
140 pub schema_version: semver::Version,
141 /// Blueprint identifier (= unique key within the caller's namespace).
142 pub id: String,
143 /// Embeds the flow.ir Node verbatim (= keeps flow.ir side unpolluted).
144 /// Opaque in the JSON Schema (the Node shape is owned by the `mlua-flow-ir`
145 /// crate, a separate repo; see its docs for the Node / Expr grammar).
146 #[schemars(with = "Value")]
147 pub flow: FlowNode,
148 /// Swarm extension layer: agent → backend mapping.
149 #[serde(default)]
150 pub agents: Vec<AgentDef>,
151 /// Swarm extension layer: **design-time definition** of Operator roles (first-class).
152 ///
153 /// `AgentDef.spec.operator_ref` references an `OperatorDef.name` (logical role name) in
154 /// this vec. Embedding runtime-generated IDs such as sid into the BP is forbidden
155 /// (= collapses the design-time vs runtime boundary). Runtime backend bindings are
156 /// established via the attach / register path; the BP side holds only logical names.
157 ///
158 /// Every `kind = Operator` agent must have its `spec.operator_ref` present in this
159 /// list — the compiler validates it at `compile()` time. May be `[]` only when the
160 /// Blueprint declares no Operator agents.
161 #[serde(default)]
162 pub operators: Vec<OperatorDef>,
163 /// Swarm extension layer: per-agent hints (interpreted by the Compiler).
164 #[serde(default)]
165 pub hints: CompilerHints,
166 /// Swarm extension layer: Compiler behavior strategy (strict / lenient).
167 #[serde(default)]
168 pub strategy: CompilerStrategy,
169 /// Blueprint metadata (description / origin / tags / ttl / version label / alias).
170 #[serde(default)]
171 pub metadata: BlueprintMetadata,
172 /// Swarm extension layer: hint keys of the layers to wrap around the SpawnerStack.
173 /// Resolved by the LayerRegistry at engine bind time (= unregistered keys are silently
174 /// skipped). Flow / Blueprint do not hold middleware implementations (e.g. MainAIMiddleware)
175 /// directly; they only declare required capabilities as string keys (= implementations
176 /// live in the engine-side LayerRegistry).
177 #[serde(default)]
178 pub spawner_hints: SpawnerHints,
179 /// BP-wide default `AgentKind` (= fallback when `AgentDef.kind` is omitted).
180 /// Four-layer cascade: (1) Schema impl Default = Operator, (2) CLI
181 /// `--default-agent-kind`, (3) this field (BP JSON literal), (4) `AgentDef.kind`
182 /// (per-agent literal). (5) `CompilerHints.kind_override` allows runtime override.
183 /// All default resolution flows through this path.
184 #[serde(default = "default_global_agent_kind")]
185 pub default_agent_kind: AgentKind,
186 /// BP-wide default `OperatorKind` (= the "BP Global" tier of the 4-tier
187 /// `OperatorKind` cascade). `None` when the Blueprint author does not
188 /// declare a default; the caller-side resolver then falls through to
189 /// the hardcoded `OperatorKind::default()` (Automate).
190 ///
191 /// # 4-tier cascade (highest to lowest priority)
192 ///
193 /// 1. Runtime Agent-level (per-agent override supplied at task-launch time)
194 /// 2. Runtime Global (the launch-time `operator_kind` request)
195 /// 3. BP Agent-level (`OperatorDef.kind`, resolved via `AgentDef.spec.operator_ref`)
196 /// 4. BP Global (this field)
197 /// 5. Default Fallback (`OperatorKind::default()` = Automate)
198 ///
199 /// The collapse itself is implemented once on the engine side and consumed
200 /// per-agent when resolving operator info.
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub default_operator_kind: Option<OperatorKind>,
203}
204
205/// Global default `AgentKind` at the Schema impl Default layer. Bottom of the 4-layer cascade.
206pub fn default_global_agent_kind() -> AgentKind {
207 AgentKind::Operator
208}
209
210/// Set of **capability hint keys** for the SpawnerLayer required by a Blueprint.
211///
212/// # Design rationale (= for the person who will reconstruct this later)
213///
214/// A Blueprint is a pure layer of flow.ir + agent name binding and holds no middleware
215/// **implementation**. Nevertheless there are cases where the caller must be told the BP
216/// needs certain **capabilities** — e.g. "MainAI hook required", "Operator delegate path
217/// required", operator role mode switching, presence/absence of senior escalation, and
218/// so on.
219///
220/// `spawner_hints.layers` is the place where those capabilities are declared as **string
221/// keys**. The engine-side `LayerRegistry` (= consumer crate) resolves key → factory and
222/// wraps the compiled routes with a `SpawnerStack`. The Blueprint does not import the
223/// concrete `MainAIMiddleware` type; it exposes intent through strings such as `"main_ai"`
224/// (= separates the pure Flow layer from implementation details).
225///
226/// # Canonical hint keys
227///
228/// - `"main_ai"` → `MainAIMiddleware` (= fires SpawnHook before/after when kind is MainAi/Composite)
229/// - `"senior_escalation"` → `SeniorEscalationMiddleware` (= fires SeniorBridge.ask on worker ok=false)
230/// - `"operator_delegate"` → `OperatorDelegateMiddleware` (= delegates the entire spawn to an external Operator.execute)
231///
232/// # Behavior of unregistered keys
233///
234/// If the engine-side LayerRegistry has no matching factory, the key is **silently skipped**
235/// (= lenient default). This preserves Blueprint portability (= an unsupported capability in
236/// another deployment falls back gracefully).
237#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
238#[serde(deny_unknown_fields)]
239pub struct SpawnerHints {
240 /// Ordered list of layer hint keys to wrap around the SpawnerStack.
241 #[serde(default)]
242 pub layers: Vec<String>,
243}
244
245// ──────────────────────────────────────────────────────────────────────────
246// AgentDef / AgentKind / AgentProfile / AgentMeta
247// ──────────────────────────────────────────────────────────────────────────
248
249/// Maps an agent name to a Worker IMPL kind and its configuration. Referenced from flow.ir
250/// `Step.ref` by name.
251///
252/// # Design
253///
254/// `AgentDef.kind` directly expresses the **Worker IMPL axis** (= not the old Spawner axis).
255/// Dispatching to a host Spawner adapter (`InProcSpawner` / `ProcessSpawner` /
256/// `OperatorSpawner`) is done by an internal Resolver on the compiler side. The design goal
257/// is "do not make the caller aware of which Spawner hosts the Worker IMPL"; the caller
258/// (Blueprint author) sees only the WorkerIMPL viewpoint.
259///
260/// A Spawner-axis hint (= "which adapter would you prefer running this Worker on", as a
261/// priority list) will be added via a future `spawner_hint: Vec<Spawner>` field as a carry.
262/// The current internal Resolver is a fixed 1:1 mapping, so the field is unnecessary today.
263#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
264#[serde(deny_unknown_fields)]
265pub struct AgentDef {
266 /// Agent name (= referenced from flow.ir `Step.ref`).
267 pub name: String,
268 /// Worker IMPL kind (= see [`AgentKind`]).
269 pub kind: AgentKind,
270 /// Free-form schema per kind. Interpreted by the SpawnerFactory.
271 #[serde(default)]
272 pub spec: Value,
273 /// Agent persona information (system_prompt / model / tools, etc.). Orthogonal to the
274 /// backend kind and is a first-class field. Expected to be populated by
275 /// `agent_md_loader` from the frontmatter + body of an `agent.md`. `None` = an agent
276 /// without a profile (= backend built solely from `spec`).
277 #[serde(default)]
278 pub profile: Option<AgentProfile>,
279 /// Agent-level metadata (description / version / tags).
280 #[serde(default)]
281 pub meta: Option<AgentMeta>,
282}
283
284/// Agent persona information. Orthogonal to the backend kind (Shell / InProc / Operator).
285///
286/// Populated by `agent_md_loader::load_dir` from the frontmatter and Markdown body of
287/// `agents/*.md` in agent-profiles. The backend (e.g. AgentBlockOperator) receives this
288/// struct at construction / dispatch time and consumes `system_prompt` as the LLM API
289/// system message and `model` / `tools` as configuration.
290///
291/// C-C-specific fields (`permissionMode` / `memory` / `abtest`, etc.) are dumped into
292/// `extras: Value`, and consumers that need them read them out. This is the escape hatch
293/// that keeps the schema future-proof rather than making it strict.
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
295#[serde(deny_unknown_fields)]
296pub struct AgentProfile {
297 /// Markdown body (= system prompt content).
298 #[serde(default)]
299 pub system_prompt: String,
300 /// LLM model identifier (e.g. `"sonnet"` / `"haiku"` / `"opus"`).
301 #[serde(default)]
302 pub model: Option<String>,
303 /// Reasoning effort (e.g. `"low"` / `"medium"` / `"high"`).
304 #[serde(default)]
305 pub effort: Option<String>,
306 /// List of available tool names (normalized from the CSV form in frontmatter).
307 #[serde(default)]
308 pub tools: Vec<String>,
309 /// Frontmatter `description`. A short one-line description.
310 #[serde(default)]
311 pub description: Option<String>,
312 /// C-C-specific / future-proof fields (permissionMode / memory / abtest / ...).
313 /// Shape is the leftover keys of the agent.md frontmatter dumped as a JSON object.
314 #[serde(default)]
315 pub extras: Value,
316 /// Content hash (blake3 32-byte hex) of the agent body (= `system_prompt`).
317 ///
318 /// # Purpose
319 ///
320 /// When the Enhance loop receives a Patch that replaces
321 /// `/agents/N/profile/system_prompt`, the post-hook in `patch_applier.lua`
322 /// recomputes this field (= new blake3 of the body) and updates it automatically.
323 /// This is the field that structurally prevents a Blueprint carrying a stale hash
324 /// from being committed.
325 ///
326 /// - `None` = hash not computed (= manually built agent, or a Blueprint predating this field)
327 /// - `Some(hex)` = latest hash at agent-profiles seed time or after PatchApplier
328 ///
329 /// Planned to be used as the cache-index key in `AgentStore`.
330 #[serde(default)]
331 pub version_hash: Option<String>,
332}
333
334/// SoT of the **Worker IMPL axis**. A closed enum managed inside Swarm and extended by
335/// variant addition through **explicit maintenance**. String lookup / escape hatches are
336/// deliberately not adopted.
337///
338/// This enum **expresses Worker IMPL directly**; dispatching to a host Spawner adapter is
339/// resolved by an internal Resolver on the compiler side (= callers see only the Worker
340/// IMPL viewpoint).
341///
342/// # Internal Resolver mapping (= currently a fixed 1:1, carry: priority list form)
343///
344/// | AgentKind | Host Spawner adapter |
345/// |---|---|
346/// | `Lua` | `InProcSpawner` (mlua VM eval) |
347/// | `RustFn` | `InProcSpawner` (Rust closure) |
348/// | `AgentBlock` | `InProcSpawner` (agent-block-core SDK in-process) |
349/// | `Subprocess` | `ProcessSpawner` (child process launch) |
350/// | `Operator` | `OperatorSpawner` (interactive role / Human-MainAI delegation) |
351#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, JsonSchema)]
352#[serde(rename_all = "snake_case")]
353pub enum AgentKind {
354 /// Lua script eval through the mlua VM (= factory-side registry looked up by `spec.fn_id`).
355 Lua,
356 /// Rust closure (= factory-side registry looked up by `spec.fn_id`).
357 RustFn,
358 /// Headless LLM agent via the agent-block-core SDK (in-process).
359 AgentBlock,
360 /// Child-process launch (= `spec.program` + `args`, via the ProcessSpawner path).
361 Subprocess,
362 /// Interactive Operator role (= MainAI / Human delegation, `spec.operator_ref`).
363 Operator,
364}
365
366// ──────────────────────────────────────────────────────────────────────────
367// OperatorDef / OperatorKind
368// ──────────────────────────────────────────────────────────────────────────
369
370/// Kind axis of an Operator role (= "in which mode does this Operator run").
371/// Corresponds 1:1 with the engine's runtime `OperatorKind`. Kept as a schema
372/// duplicate so that BPs can be authored while depending only on this crate.
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
374#[serde(rename_all = "snake_case")]
375pub enum OperatorKind {
376 /// MainAI (= interactive AI Operator via WS client or SDK).
377 MainAi,
378 /// Automate (= normal spawn path, without human interception).
379 #[default]
380 Automate,
381 /// Composite (= MainAi + Automate running side by side).
382 Composite,
383}
384
385/// Design-time definition of an Operator role (first-class).
386///
387/// `AgentDef.spec.operator_ref` references this struct's `name` as a logical role name.
388/// Binding to a runtime backend (WS session / SDK / pool, etc.) is established via the
389/// attach path; the BP side only declares "under this logical name we expect an Operator
390/// of this Kind".
391///
392/// `spec` is an escape hatch for kind-specific config (WS endpoint / SDK profile / pool
393/// binding, etc.). Even when empty, declaring `name` + `kind` alone is enough for
394/// compile-time validation to succeed (= it guarantees that agent `operator_ref` values
395/// reference an existing definition).
396#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
397#[serde(deny_unknown_fields)]
398pub struct OperatorDef {
399 /// Logical role name (= design-time symbol referenced from `AgentDef.spec.operator_ref`).
400 pub name: String,
401 /// Display name for UI / docs (optional).
402 #[serde(default)]
403 pub display_name: Option<String>,
404 /// Kind axis of the Operator (MainAi / Automate / Composite) — the "BP
405 /// Agent-level" tier of the 4-tier `OperatorKind` cascade (see
406 /// `Blueprint.default_operator_kind` for the full tier list). `None`
407 /// when this `OperatorDef` does not declare a kind; the resolver then
408 /// falls through to BP Global / Default Fallback for agents referencing
409 /// this role via `AgentDef.spec.operator_ref`.
410 #[serde(default)]
411 pub kind: Option<OperatorKind>,
412 /// Kind-specific config (WS endpoint / SDK profile / pool binding, etc.). Interpreted
413 /// by the factory.
414 #[serde(default)]
415 pub spec: Value,
416 /// Operator persona information (e.g. system_prompt template). Same shape as
417 /// `AgentDef.profile`. Used as a template when the Operator itself plays a "role".
418 /// If `None`, the agent-side profile is used instead.
419 #[serde(default)]
420 pub profile: Option<AgentProfile>,
421 /// Operator-level metadata (description / version / tags).
422 #[serde(default)]
423 pub meta: Option<AgentMeta>,
424}
425
426/// Agent / Operator level metadata (description / version / tags).
427#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
428#[serde(deny_unknown_fields)]
429pub struct AgentMeta {
430 /// Short human-readable description.
431 #[serde(default)]
432 pub description: Option<String>,
433 /// Free-form version label.
434 #[serde(default)]
435 pub version: Option<String>,
436 /// Tag list for classification / routing.
437 #[serde(default)]
438 pub tags: Vec<String>,
439}
440
441// ──────────────────────────────────────────────────────────────────────────
442// Compiler hints / strategy
443// ──────────────────────────────────────────────────────────────────────────
444
445/// Per-agent overrides / hints. Interpreted by the Compiler / SpawnerFactory; not required.
446#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
447#[serde(deny_unknown_fields)]
448pub struct CompilerHints {
449 /// Agent name → per-agent hint (= passed to `SpawnerFactory.build`).
450 #[serde(default)]
451 pub per_agent: HashMap<String, Value>,
452 /// Global hints (= e.g. parallel limit, default timeout, ...).
453 #[serde(default)]
454 pub global: Value,
455}
456
457/// Compiler behavior rules. Controls strict / lenient handling and default fallback.
458#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
459#[serde(deny_unknown_fields)]
460pub struct CompilerStrategy {
461 /// If `true` (default), an unresolved `Step.ref` is an error; if `false`, it falls
462 /// through to the default Spawner.
463 #[serde(default = "default_true")]
464 pub strict_refs: bool,
465 /// If `true` (default), an `AgentKind` missing from the registry is an error; if
466 /// `false`, it is skipped.
467 #[serde(default = "default_true")]
468 pub strict_kind: bool,
469}
470
471fn default_true() -> bool {
472 true
473}
474
475impl Default for CompilerStrategy {
476 fn default() -> Self {
477 Self {
478 strict_refs: true,
479 strict_kind: true,
480 }
481 }
482}
483
484// ──────────────────────────────────────────────────────────────────────────
485// Blueprint metadata / origin
486// ──────────────────────────────────────────────────────────────────────────
487
488/// Blueprint-level metadata (description / origin / tags / ttl / version label / alias).
489#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
490#[serde(deny_unknown_fields)]
491pub struct BlueprintMetadata {
492 /// Short human-readable description of the Blueprint.
493 #[serde(default)]
494 pub description: Option<String>,
495 /// Provenance record (inline / file / algocline).
496 #[serde(default)]
497 pub origin: BlueprintOrigin,
498 /// Tag list for classification / routing.
499 #[serde(default)]
500 pub tags: Vec<String>,
501 /// Optional SemVer label (= match target for `TaskPipeline VersionSelector::SemVerReq`).
502 /// Example: `"1.2.3"`. Rewritten by `EnhanceAdapter` on PATCH/MINOR/MAJOR bumps.
503 #[serde(default, skip_serializing_if = "Option::is_none")]
504 pub version_label: Option<String>,
505 /// Optional LDS session alias label. The Swarm engine itself does not apply this
506 /// (= it is free-form content); the value is expanded into the Spawn directive and
507 /// reaches the MainAI. The MainAI is expected to establish a task session via
508 /// `mcp__lds__session_create(root=..., alias=<this>)`, and to inject
509 /// `LDS Session Alias: <this>` verbatim into the SubAgent dispatch prompt body.
510 /// The SubAgent body then calls `mcp__lds__session_start(alias=<this>)` with the
511 /// received alias. Worktree ownership is thereby unified under a single session, and
512 /// cross-SubAgent / cross-worktree ownership blocks (= `not owned by this session`)
513 /// cannot fire structurally.
514 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub project_name_alias: Option<String>,
516 /// Optional default TTL (seconds) for tasks dispatched via this BP. Estimated by the
517 /// Blueprint author from the flow shape (agent count × expected duration per agent).
518 /// If `POST /v1/tasks` supplies `ttl_secs` explicitly, the body value wins; otherwise
519 /// this metadata field is used as the default; if both are absent, the server global
520 /// default (`default_run_ttl()` = 1800s) applies. Not needed for short chains (~5 min);
521 /// recommended for long chains (14 agents × several minutes = 30-60 min).
522 #[serde(default, skip_serializing_if = "Option::is_none")]
523 pub default_run_ttl_secs: Option<u64>,
524}
525
526/// Provenance record of a Blueprint.
527#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, JsonSchema)]
528#[serde(tag = "kind", rename_all = "snake_case")]
529pub enum BlueprintOrigin {
530 /// Inline construction, e.g. via a Rust struct literal or test code.
531 #[default]
532 Inline,
533 /// Loaded from a file.
534 File {
535 /// Source file path.
536 path: String,
537 },
538 /// Emitted by an algocline strategy (traced by `session_id`).
539 Algo {
540 /// Algocline session identifier.
541 session_id: String,
542 },
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn schema_version_default_parses() {
551 let v = default_schema_version();
552 assert_eq!(v.to_string(), "0.1.0");
553 }
554
555 #[test]
556 fn current_schema_version_const_matches() {
557 assert_eq!(CURRENT_SCHEMA_VERSION, "0.1.0");
558 }
559
560 #[test]
561 fn blueprint_json_schema_exports_key_properties() {
562 let schema = schemars::schema_for!(Blueprint);
563 let v = serde_json::to_value(&schema).expect("schema serializes");
564 let props = v["properties"].as_object().expect("object schema");
565 for key in [
566 "schema_version",
567 "id",
568 "flow",
569 "agents",
570 "operators",
571 "hints",
572 "strategy",
573 "metadata",
574 "spawner_hints",
575 "default_agent_kind",
576 "default_operator_kind",
577 ] {
578 assert!(props.contains_key(key), "missing property: {key}");
579 }
580 // semver override lands as a plain string
581 assert_eq!(v["properties"]["schema_version"]["type"], "string");
582 // enum variants (snake_case) survive into the schema (LLM author axis)
583 let dump = v.to_string();
584 assert!(dump.contains("agent_block"), "AgentKind variants in schema");
585 assert!(dump.contains("main_ai"), "OperatorKind variants in schema");
586 // nested defs are referenced (AgentDef reachable from agents[])
587 assert!(dump.contains("AgentDef"), "AgentDef definition in schema");
588 }
589}