Skip to main content

swarm_engine_core/learn/
scenario_profile.rs

1//! ScenarioProfile - 永続的に改善されるシナリオの実体
2//!
3//! ## 設計思想
4//!
5//! Scenario(静的定義)に対して、学習結果を蓄積・管理する Entity。
6//! Bootstrap で活性化し、Runtime で継続的に最適化される。
7//!
8//! ## Lifecycle
9//!
10//! ```text
11//! 1. Register
12//!    swarm profile add scenarios/deep_search.toml
13//!    → state: Draft
14//!
15//! 2. Bootstrap
16//!    swarm profile bootstrap deep_search
17//!    → with_graph で N 回実行
18//!    → DepGraph 学習
19//!    → state: Active
20//!
21//! 3. Use
22//!    swarm run --profile deep_search "タスク"
23//!    → Profile から DepGraph/Params 適用
24//!    → stats 更新
25//!
26//! 4. Optimize (自動)
27//!    → パフォーマンス監視
28//!    → 閾値割れで再チューニング
29//!    → state: Optimizing → Active
30//! ```
31//!
32//! ## Storage
33//!
34//! ```text
35//! ~/.swarm-engine/profiles/troubleshooting/
36//! ├── profile.json           # ScenarioProfile (metadata)
37//! ├── dep_graph.json         # LearnedDepGraph
38//! ├── exploration.json       # LearnedExploration
39//! ├── strategy.json          # LearnedStrategy
40//! └── sessions/              # 学習セッションログ
41//! ```
42
43use std::path::{Path, PathBuf};
44use std::time::{SystemTime, UNIX_EPOCH};
45
46use serde::{Deserialize, Serialize};
47
48use super::learned_component::{
49    LearnedComponent, LearnedDepGraph, LearnedExploration, LearnedStrategy,
50};
51use super::session_group::LearningPhase;
52
53// ============================================================================
54// ScenarioProfileId
55// ============================================================================
56
57/// ScenarioProfile 識別子
58#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
59pub struct ScenarioProfileId(pub String);
60
61impl ScenarioProfileId {
62    /// 新規作成
63    pub fn new(id: impl Into<String>) -> Self {
64        Self(id.into())
65    }
66}
67
68impl std::fmt::Display for ScenarioProfileId {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        write!(f, "{}", self.0)
71    }
72}
73
74// ============================================================================
75// ProfileState
76// ============================================================================
77
78/// Profile のライフサイクル状態
79#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
80#[serde(rename_all = "snake_case")]
81pub enum ProfileState {
82    /// 定義のみ、未 Bootstrap
83    #[default]
84    Draft,
85    /// Bootstrap 進行中
86    Bootstrapping,
87    /// 使用可能
88    Active,
89    /// Active + 継続チューニング中
90    Optimizing,
91}
92
93impl std::fmt::Display for ProfileState {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        match self {
96            Self::Draft => write!(f, "draft"),
97            Self::Bootstrapping => write!(f, "bootstrapping"),
98            Self::Active => write!(f, "active"),
99            Self::Optimizing => write!(f, "optimizing"),
100        }
101    }
102}
103
104// ============================================================================
105// ScenarioSource
106// ============================================================================
107
108/// シナリオのソース参照
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(tag = "type", rename_all = "snake_case")]
111pub enum ScenarioSource {
112    /// ファイルパス参照
113    File { path: PathBuf },
114    /// インライン定義(将来用)
115    Inline { content: String },
116}
117
118impl ScenarioSource {
119    /// ファイルパスから作成
120    pub fn from_path(path: impl AsRef<Path>) -> Self {
121        Self::File {
122            path: path.as_ref().to_path_buf(),
123        }
124    }
125}
126
127// ============================================================================
128// BootstrapData
129// ============================================================================
130
131/// Bootstrap 完了時のデータ
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct BootstrapData {
134    /// 完了日時
135    pub completed_at: u64,
136    /// 実行セッション数
137    pub session_count: usize,
138    /// 成功率
139    pub success_rate: f64,
140    /// 使用した variant(例: "with_graph")
141    pub source_variant: String,
142    /// Bootstrap フェーズ
143    pub phase: LearningPhase,
144}
145
146impl BootstrapData {
147    /// 新規作成
148    pub fn new(session_count: usize, success_rate: f64, source_variant: impl Into<String>) -> Self {
149        Self {
150            completed_at: SystemTime::now()
151                .duration_since(UNIX_EPOCH)
152                .map(|d| d.as_secs())
153                .unwrap_or(0),
154            session_count,
155            success_rate,
156            source_variant: source_variant.into(),
157            phase: LearningPhase::Bootstrap,
158        }
159    }
160}
161
162// ============================================================================
163// ProfileStats
164// ============================================================================
165
166/// Profile の統計情報
167#[derive(Debug, Clone, Default, Serialize, Deserialize)]
168pub struct ProfileStats {
169    /// 総実行回数
170    pub total_runs: usize,
171    /// 成功率
172    pub success_rate: f64,
173    /// 平均実行時間(ミリ秒)
174    pub avg_duration_ms: u64,
175    /// 最終実行日時
176    pub last_run_at: Option<u64>,
177}
178
179impl ProfileStats {
180    /// 実行結果を記録
181    pub fn record_run(&mut self, success: bool, duration_ms: u64) {
182        let prev_total = self.total_runs as f64;
183        let prev_success = self.success_rate * prev_total;
184
185        self.total_runs += 1;
186
187        // 成功率を更新
188        let new_success = if success {
189            prev_success + 1.0
190        } else {
191            prev_success
192        };
193        self.success_rate = new_success / self.total_runs as f64;
194
195        // 平均実行時間を更新(移動平均)
196        self.avg_duration_ms = ((self.avg_duration_ms as f64 * prev_total + duration_ms as f64)
197            / self.total_runs as f64) as u64;
198
199        self.last_run_at = Some(
200            SystemTime::now()
201                .duration_since(UNIX_EPOCH)
202                .map(|d| d.as_secs())
203                .unwrap_or(0),
204        );
205    }
206}
207
208// ============================================================================
209// ScenarioProfile
210// ============================================================================
211
212/// 永続的に改善されるシナリオの実体
213///
214/// Scenario(静的定義)に対する学習結果を管理する Entity。
215/// 各学習コンポーネントは独立して更新可能。
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct ScenarioProfile {
218    // ============================================
219    // Identity
220    // ============================================
221    /// Profile ID
222    pub id: ScenarioProfileId,
223
224    /// シナリオソース参照
225    pub scenario_source: ScenarioSource,
226
227    /// ライフサイクル状態
228    pub state: ProfileState,
229
230    // ============================================
231    // Learned Components (全て明示的に型付け)
232    // ============================================
233    /// 学習済み依存グラフ
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    pub dep_graph: Option<LearnedDepGraph>,
236
237    /// 学習済み探索パラメータ
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    pub exploration: Option<LearnedExploration>,
240
241    /// 学習済み戦略設定
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub strategy: Option<LearnedStrategy>,
244
245    // ============================================
246    // Bootstrap Data
247    // ============================================
248    /// Bootstrap 完了データ
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub bootstrap: Option<BootstrapData>,
251
252    // ============================================
253    // Metadata
254    // ============================================
255    /// 統計情報
256    #[serde(default)]
257    pub stats: ProfileStats,
258
259    /// 作成日時
260    pub created_at: u64,
261
262    /// 更新日時
263    pub updated_at: u64,
264}
265
266impl ScenarioProfile {
267    /// 新規作成(Draft 状態)
268    pub fn new(id: impl Into<String>, source: ScenarioSource) -> Self {
269        let now = SystemTime::now()
270            .duration_since(UNIX_EPOCH)
271            .map(|d| d.as_secs())
272            .unwrap_or(0);
273
274        Self {
275            id: ScenarioProfileId::new(id),
276            scenario_source: source,
277            state: ProfileState::Draft,
278            dep_graph: None,
279            exploration: None,
280            strategy: None,
281            bootstrap: None,
282            stats: ProfileStats::default(),
283            created_at: now,
284            updated_at: now,
285        }
286    }
287
288    /// ファイルパスから作成
289    pub fn from_file(id: impl Into<String>, path: impl AsRef<Path>) -> Self {
290        Self::new(id, ScenarioSource::from_path(path))
291    }
292
293    // ============================================
294    // State Management
295    // ============================================
296
297    /// Bootstrap 開始
298    pub fn start_bootstrap(&mut self) {
299        self.state = ProfileState::Bootstrapping;
300        self.touch();
301    }
302
303    /// Bootstrap 完了
304    pub fn complete_bootstrap(&mut self, data: BootstrapData) {
305        self.bootstrap = Some(data);
306        self.state = ProfileState::Active;
307        self.touch();
308    }
309
310    /// Optimizing 開始
311    pub fn start_optimizing(&mut self) {
312        if self.state == ProfileState::Active {
313            self.state = ProfileState::Optimizing;
314            self.touch();
315        }
316    }
317
318    /// Optimizing 完了
319    pub fn finish_optimizing(&mut self) {
320        if self.state == ProfileState::Optimizing {
321            self.state = ProfileState::Active;
322            self.touch();
323        }
324    }
325
326    /// 使用可能か
327    pub fn is_usable(&self) -> bool {
328        matches!(self.state, ProfileState::Active | ProfileState::Optimizing)
329    }
330
331    // ============================================
332    // Component Management
333    // ============================================
334
335    /// DepGraph を更新
336    pub fn update_dep_graph(&mut self, dep_graph: LearnedDepGraph) {
337        if let Some(existing) = &mut self.dep_graph {
338            existing.merge(&dep_graph);
339        } else {
340            self.dep_graph = Some(dep_graph);
341        }
342        self.touch();
343    }
344
345    /// Exploration を更新
346    pub fn update_exploration(&mut self, exploration: LearnedExploration) {
347        if let Some(existing) = &mut self.exploration {
348            existing.merge(&exploration);
349        } else {
350            self.exploration = Some(exploration);
351        }
352        self.touch();
353    }
354
355    /// Strategy を更新
356    pub fn update_strategy(&mut self, strategy: LearnedStrategy) {
357        if let Some(existing) = &mut self.strategy {
358            existing.merge(&strategy);
359        } else {
360            self.strategy = Some(strategy);
361        }
362        self.touch();
363    }
364
365    // ============================================
366    // Stats
367    // ============================================
368
369    /// 実行結果を記録
370    pub fn record_run(&mut self, success: bool, duration_ms: u64) {
371        self.stats.record_run(success, duration_ms);
372        self.touch();
373    }
374
375    /// 全コンポーネントの最小信頼度
376    pub fn min_confidence(&self) -> f64 {
377        [
378            self.dep_graph.as_ref().map(|c| c.confidence()),
379            self.exploration.as_ref().map(|c| c.confidence()),
380            self.strategy.as_ref().map(|c| c.confidence()),
381        ]
382        .into_iter()
383        .flatten()
384        .fold(1.0, f64::min)
385    }
386
387    /// 全コンポーネントの平均信頼度
388    pub fn avg_confidence(&self) -> f64 {
389        let confidences: Vec<f64> = [
390            self.dep_graph.as_ref().map(|c| c.confidence()),
391            self.exploration.as_ref().map(|c| c.confidence()),
392            self.strategy.as_ref().map(|c| c.confidence()),
393        ]
394        .into_iter()
395        .flatten()
396        .collect();
397
398        if confidences.is_empty() {
399            0.0
400        } else {
401            confidences.iter().sum::<f64>() / confidences.len() as f64
402        }
403    }
404
405    // ============================================
406    // Internal
407    // ============================================
408
409    fn touch(&mut self) {
410        self.updated_at = SystemTime::now()
411            .duration_since(UNIX_EPOCH)
412            .map(|d| d.as_secs())
413            .unwrap_or(0);
414    }
415}
416
417// ============================================================================
418// Tests
419// ============================================================================
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn test_profile_creation() {
427        let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
428
429        assert_eq!(profile.id.0, "test");
430        assert_eq!(profile.state, ProfileState::Draft);
431        assert!(profile.dep_graph.is_none());
432        assert!(!profile.is_usable());
433    }
434
435    #[test]
436    fn test_profile_lifecycle() {
437        let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
438
439        // Draft -> Bootstrapping
440        profile.start_bootstrap();
441        assert_eq!(profile.state, ProfileState::Bootstrapping);
442        assert!(!profile.is_usable());
443
444        // Bootstrapping -> Active
445        let bootstrap_data = BootstrapData::new(10, 0.9, "with_graph");
446        profile.complete_bootstrap(bootstrap_data);
447        assert_eq!(profile.state, ProfileState::Active);
448        assert!(profile.is_usable());
449
450        // Active -> Optimizing
451        profile.start_optimizing();
452        assert_eq!(profile.state, ProfileState::Optimizing);
453        assert!(profile.is_usable());
454
455        // Optimizing -> Active
456        profile.finish_optimizing();
457        assert_eq!(profile.state, ProfileState::Active);
458    }
459
460    #[test]
461    fn test_profile_stats() {
462        let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
463
464        profile.record_run(true, 100);
465        profile.record_run(true, 200);
466        profile.record_run(false, 150);
467
468        assert_eq!(profile.stats.total_runs, 3);
469        assert!((profile.stats.success_rate - 2.0 / 3.0).abs() < 0.001);
470        assert_eq!(profile.stats.avg_duration_ms, 150);
471    }
472
473    #[test]
474    fn test_component_update() {
475        use crate::exploration::DependencyGraph;
476
477        let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
478
479        let dep_graph = LearnedDepGraph::new(DependencyGraph::new(), vec!["A".to_string()])
480            .with_confidence(0.8);
481
482        profile.update_dep_graph(dep_graph);
483        assert!(profile.dep_graph.is_some());
484        assert_eq!(profile.min_confidence(), 0.8);
485    }
486
487    #[test]
488    fn test_serialization() {
489        let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
490        let json = serde_json::to_string(&profile).unwrap();
491        let restored: ScenarioProfile = serde_json::from_str(&json).unwrap();
492        assert_eq!(restored.id.0, "test");
493    }
494}