nemo_flow_adaptive/acg/profile.rs
1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Behavioral profile types for the Adaptive Cache Governor (ACG) system.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::acg::prompt_ir::SpanId;
10use crate::acg::types::AgentIdentity;
11
12/// Stability bucket assigned to a prompt block.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum StabilityClass {
16 /// The block is highly consistent across observations.
17 Stable,
18 /// The block is somewhat consistent but still changes meaningfully.
19 SemiStable,
20 /// The block changes enough that it should be treated as variable.
21 Variable,
22}
23
24/// Stability analysis result for one prompt block.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct BlockStabilityScore {
27 /// Span identifier of the analyzed prompt block.
28 pub span_id: SpanId,
29 /// Stability bucket assigned to the block.
30 pub classification: StabilityClass,
31 /// Effective stability score in the range `[0.0, 1.0]`.
32 pub score: f64,
33 /// Confidence score in the range `[0.0, 1.0]`.
34 pub confidence: f64,
35 /// Number of observations that contained this block.
36 pub observation_count: u32,
37}
38
39/// Percentile summary for a duration-like metric.
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub struct DistributionSummary {
42 /// 50th percentile value.
43 pub p50: f64,
44 /// 90th percentile value.
45 pub p90: f64,
46 /// 99th percentile value.
47 pub p99: f64,
48 /// Number of samples summarized by the distribution.
49 pub sample_count: u32,
50}
51
52/// Coarse behavioral archetype inferred from observed runs.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum SessionArchetype {
56 /// Short sessions dominated by direct answers.
57 FastAnswer,
58 /// Sessions that repeatedly call tools in loops or fan-outs.
59 ToolHeavyLoop,
60 /// Longer workflows with extended execution lifetime.
61 LongRunningWorkflow,
62 /// Multi-turn diagnostic or debugging sessions.
63 MultiTurnTroubleshooting,
64}
65
66/// Summary of observed parallelism behavior across runs.
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68pub struct ParallelismPattern {
69 /// Whether any tool fan-outs were observed.
70 pub has_fanouts: bool,
71 /// Typical observed fan-out width, when fan-outs were present.
72 #[serde(skip_serializing_if = "Option::is_none")]
73 #[serde(default)]
74 pub typical_fanout_width: Option<u32>,
75 /// Whether the workflow was mostly serial despite any fan-outs.
76 pub predominantly_serial: bool,
77}
78
79/// One phase of tool usage observed in an agent workflow.
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub struct ToolUsagePhase {
82 /// Human-readable label for the phase.
83 pub phase_label: String,
84 /// Tool names commonly observed in the phase.
85 pub tools: Vec<String>,
86 /// Fraction of runs that reached this phase.
87 pub phase_reach_rate: f64,
88}
89
90/// Aggregated behavioral profile derived from observed runs.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct BehavioralProfile {
93 /// Agent identity the profile applies to.
94 pub agent_identity: AgentIdentity,
95 /// Version string for the profile schema or derivation pipeline.
96 pub profile_version: String,
97 /// Stability summary for prompt blocks observed across runs.
98 pub block_stability: Vec<BlockStabilityScore>,
99 /// Observed session-duration distribution, when available.
100 #[serde(skip_serializing_if = "Option::is_none")]
101 #[serde(default)]
102 pub session_duration: Option<DistributionSummary>,
103 /// Observed inter-call-gap distribution, when available.
104 #[serde(skip_serializing_if = "Option::is_none")]
105 #[serde(default)]
106 pub inter_call_gap: Option<DistributionSummary>,
107 /// Observed parallelism behavior, when available.
108 #[serde(skip_serializing_if = "Option::is_none")]
109 #[serde(default)]
110 pub parallelism: Option<ParallelismPattern>,
111 /// Observed tool-usage phases.
112 pub tool_usage_phases: Vec<ToolUsagePhase>,
113 /// Dominant session archetype, when enough data exists to infer one.
114 #[serde(skip_serializing_if = "Option::is_none")]
115 #[serde(default)]
116 pub dominant_archetype: Option<SessionArchetype>,
117 /// Number of observations used to derive the profile.
118 pub observation_count: u32,
119 /// Minimum number of observations required for the profile to be considered usable.
120 pub minimum_observations: u32,
121 /// Timestamp when the profile was last updated.
122 pub updated_at: DateTime<Utc>,
123}
124
125impl BehavioralProfile {
126 /// Report whether the profile has enough data to be trusted.
127 ///
128 /// # Returns
129 /// `true` when [`Self::observation_count`] is at least
130 /// [`Self::minimum_observations`] and `false` otherwise.
131 pub fn has_sufficient_data(&self) -> bool {
132 self.observation_count >= self.minimum_observations
133 }
134}