返回 CodeWhale
profile.rs
根目录 / crates / tui / src / core / runtime_contract / profile.rs
1 use std::collections::BTreeSet;
2
3 use serde::{Deserialize, Serialize};
4
5 use super::{RUNTIME_CONTRACT_SCHEMA_VERSION, terminal::TerminalProcessPolicy};
6
7 /// Candidate profiles are implementation experiments, not public Codewhale
8 /// modes. Plan/Act/Operate and permission posture remain independent axes.
9 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10 #[serde(rename_all = "snake_case")]
11 pub enum AgentProfileCandidate {
12 CurrentFull,
13 ConsolidatedCore,
14 SpecializedCore,
15 AdaptiveCore,
16 }
17
18 /// Semantic abilities a profile promises regardless of model-facing tool
19 /// names. This lets paired trials compare combined and specialized schemas
20 /// without changing the task contract.
21 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
22 #[serde(rename_all = "snake_case")]
23 pub enum SemanticCapability {
24 CommandExecution,
25 FileRead,
26 FileSearch,
27 FileEdit,
28 ActiveChecklist,
29 TypedTermination,
30 Verification,
31 Delegation,
32 Network,
33 Mcp,
34 Knowledge,
35 Media,
36 Release,
37 }
38
39 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40 #[serde(rename_all = "snake_case")]
41 pub enum ToolActivationPolicy {
42 Static,
43 DeferredSearch,
44 ExplicitCapability,
45 Adaptive,
46 }
47
48 /// Exact tool/profile manifest supplied to a model for one run.
49 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50 pub struct ToolProfileManifest {
51 pub schema_version: u32,
52 pub candidate: AgentProfileCandidate,
53 pub activation_policy: ToolActivationPolicy,
54 pub terminal_policy: TerminalProcessPolicy,
55 pub capabilities: BTreeSet<SemanticCapability>,
56 pub active_tools: BTreeSet<String>,
57 pub deferred_tools: BTreeSet<String>,
58 pub max_steps: u32,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub max_wall_time_seconds: Option<u64>,
61 }
62
63 impl ToolProfileManifest {
64 #[must_use]
65 pub fn new(
66 candidate: AgentProfileCandidate,
67 activation_policy: ToolActivationPolicy,
68 terminal_policy: TerminalProcessPolicy,
69 max_steps: u32,
70 ) -> Self {
71 Self {
72 schema_version: RUNTIME_CONTRACT_SCHEMA_VERSION,
73 candidate,
74 activation_policy,
75 terminal_policy,
76 capabilities: BTreeSet::new(),
77 active_tools: BTreeSet::new(),
78 deferred_tools: BTreeSet::new(),
79 max_steps,
80 max_wall_time_seconds: None,
81 }
82 }
83
84 #[must_use]
85 pub fn with_capability(mut self, capability: SemanticCapability) -> Self {
86 self.capabilities.insert(capability);
87 self
88 }
89
90 #[must_use]
91 pub fn with_active_tool(mut self, tool: impl Into<String>) -> Self {
92 self.active_tools.insert(tool.into());
93 self
94 }
95
96 #[must_use]
97 pub fn with_deferred_tool(mut self, tool: impl Into<String>) -> Self {
98 self.deferred_tools.insert(tool.into());
99 self
100 }
101
102 pub fn validate(&self) -> Result<(), String> {
103 if self.schema_version != RUNTIME_CONTRACT_SCHEMA_VERSION {
104 return Err(format!(
105 "unsupported runtime profile schema {}; expected {}",
106 self.schema_version, RUNTIME_CONTRACT_SCHEMA_VERSION
107 ));
108 }
109 if self.max_steps == 0 {
110 return Err("runtime profile max_steps must be greater than zero".to_string());
111 }
112 if let Some(overlap) = self.active_tools.intersection(&self.deferred_tools).next() {
113 return Err(format!(
114 "tool `{overlap}` cannot be both active and deferred"
115 ));
116 }
117 if !self
118 .capabilities
119 .contains(&SemanticCapability::TypedTermination)
120 {
121 return Err("runtime profile must promise typed termination".to_string());
122 }
123 Ok(())
124 }
125 }
126
127 #[cfg(test)]
128 mod tests {
129 use super::*;
130
131 #[test]
132 fn candidate_profile_keeps_public_modes_out_of_the_contract() {
133 let json = serde_json::to_string(&AgentProfileCandidate::AdaptiveCore).unwrap();
134 assert_eq!(json, "\"adaptive_core\"");
135 assert!(!json.contains("plan"));
136 assert!(!json.contains("operate"));
137 }
138
139 #[test]
140 fn manifest_rejects_active_deferred_overlap() {
141 let manifest = ToolProfileManifest::new(
142 AgentProfileCandidate::AdaptiveCore,
143 ToolActivationPolicy::DeferredSearch,
144 TerminalProcessPolicy::Hybrid,
145 64,
146 )
147 .with_capability(SemanticCapability::TypedTermination)
148 .with_active_tool("tool_search")
149 .with_deferred_tool("tool_search");
150
151 assert!(
152 manifest
153 .validate()
154 .unwrap_err()
155 .contains("both active and deferred")
156 );
157 }
158
159 #[test]
160 fn manifest_requires_typed_termination() {
161 let manifest = ToolProfileManifest::new(
162 AgentProfileCandidate::SpecializedCore,
163 ToolActivationPolicy::Static,
164 TerminalProcessPolicy::Isolated,
165 32,
166 );
167 assert!(
168 manifest
169 .validate()
170 .unwrap_err()
171 .contains("typed termination")
172 );
173 }
174 }
175
175 lines RUST