Skip to main content

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}