converge_pack/formation.rs
1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! Formation-kind tag exposed to suggestors.
5//!
6//! The full `Formation` enum (with its variant payloads — `StaticFormation`,
7//! `ScoredFormation`, `DeliberatedFormation`, `OpenClawFormation`) lives in
8//! `converge-core::formation`. That crate owns formation *configuration* and
9//! orchestration. Pack only needs the **tag** so a `Suggestor` running inside
10//! a formation can ask "what kind of formation am I in?" without depending
11//! on core.
12//!
13//! This split keeps the layer rule intact (pack does not depend on core)
14//! while letting suggestors adapt behavior to formation context. A formation
15//! harness that orchestrates inner suggestors is responsible for setting
16//! this tag on the `Context` it passes down via
17//! [`Context::formation_kind`](crate::Context::formation_kind). Default is
18//! `None`, meaning the suggestor is running outside any formation harness
19//! and should fall back to its standalone behavior.
20
21use serde::{Deserialize, Serialize};
22
23/// The kind of formation orchestrating a suggestor's current execution.
24///
25/// Exposed to `Suggestor` implementations via
26/// [`Context::formation_kind`](crate::Context::formation_kind). Suggestors
27/// should treat this as advisory; pure-context determinism is still the
28/// primary contract.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum FormationKind {
32 /// Fixed ordered set of suggestors; all run every cycle.
33 Static,
34 /// Ranked candidates; top-N by score participate.
35 Scored,
36 /// Multi-cycle huddle with confidence threshold.
37 Deliberated,
38 /// Adaptive variant selection with extra-loop budget.
39 OpenClaw,
40}
41
42impl std::fmt::Display for FormationKind {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::Static => write!(f, "static"),
46 Self::Scored => write!(f, "scored"),
47 Self::Deliberated => write!(f, "deliberated"),
48 Self::OpenClaw => write!(f, "open_claw"),
49 }
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn formation_kind_display() {
59 assert_eq!(FormationKind::Static.to_string(), "static");
60 assert_eq!(FormationKind::Scored.to_string(), "scored");
61 assert_eq!(FormationKind::Deliberated.to_string(), "deliberated");
62 assert_eq!(FormationKind::OpenClaw.to_string(), "open_claw");
63 }
64
65 #[test]
66 fn formation_kind_serde_roundtrip() {
67 for kind in [
68 FormationKind::Static,
69 FormationKind::Scored,
70 FormationKind::Deliberated,
71 FormationKind::OpenClaw,
72 ] {
73 let json = serde_json::to_string(&kind).unwrap();
74 let back: FormationKind = serde_json::from_str(&json).unwrap();
75 assert_eq!(back, kind);
76 }
77 }
78}