Skip to main content

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}