agentic_contracts/hydra.rs
1//! Hydra integration placeholder traits.
2//!
3//! These traits define how sisters connect to the Hydra orchestrator.
4//! They are PLACEHOLDERS — the real implementations will come when
5//! Hydra is built. For now, they establish the contract shape so
6//! sisters can prepare.
7//!
8//! # Architecture
9//!
10//! ```text
11//! ┌─────────────────────────────────────────┐
12//! │ HYDRA │
13//! │ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
14//! │ │Execution│ │Capability│ │ Receipt │ │
15//! │ │ Gate │ │ Engine │ │ Ledger │ │
16//! │ └────┬────┘ └────┬─────┘ └────┬─────┘ │
17//! │ │ │ │ │
18//! │ ┌────┴───────────┴────────────┴──────┐ │
19//! │ │ HydraBridge trait │ │
20//! │ └────────────────────────────────────┘ │
21//! └───────────────┬───────────────────────────┘
22//! │
23//! ┌────────────┼────────────┐
24//! ▼ ▼ ▼
25//! Memory Vision Codebase ...
26//! ```
27
28use crate::context::SessionContext;
29use crate::errors::SisterResult;
30use crate::types::{Metadata, SisterType};
31use chrono::{DateTime, Utc};
32use serde::{Deserialize, Serialize};
33
34// ═══════════════════════════════════════════════════════════════════
35// HYDRA BRIDGE — How sisters connect to Hydra
36// ═══════════════════════════════════════════════════════════════════
37
38/// Summary of a sister's current state (for Hydra's context window).
39///
40/// This is the token-efficient summary Hydra uses to understand
41/// what each sister is doing without loading full state.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct SisterSummary {
44 /// Which sister
45 pub sister_type: SisterType,
46
47 /// Brief status line for LLM context
48 pub status_line: String,
49
50 /// Item count (memories, captures, nodes, etc.)
51 pub item_count: usize,
52
53 /// Active session/workspace name
54 pub active_context: Option<String>,
55
56 /// Additional metadata
57 #[serde(default)]
58 pub metadata: Metadata,
59}
60
61/// A command from Hydra to a sister
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct HydraCommand {
64 /// Command type (sister interprets this)
65 pub command_type: String,
66
67 /// Command parameters
68 #[serde(default)]
69 pub params: Metadata,
70
71 /// Hydra run ID (for receipt chain)
72 pub run_id: String,
73
74 /// Step ID within the run
75 pub step_id: u64,
76}
77
78/// Result of executing a Hydra command
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct CommandResult {
81 /// Whether the command succeeded
82 pub success: bool,
83
84 /// Result data
85 pub data: serde_json::Value,
86
87 /// Error message (if failed)
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub error: Option<String>,
90
91 /// Evidence IDs produced by this command
92 #[serde(default)]
93 pub evidence_ids: Vec<String>,
94}
95
96/// The bridge between Hydra and individual sisters.
97///
98/// This is a PLACEHOLDER trait. Sisters should not implement it yet.
99/// It establishes the expected contract shape for when Hydra arrives.
100///
101/// When Hydra is built, this trait will require:
102/// `Sister + SessionManagement/WorkspaceManagement + Grounding + EventEmitter + Queryable`
103pub trait HydraBridge {
104 /// Get a token-efficient summary of current sister state.
105 /// Hydra calls this to build its context window
106 fn session_context(&self) -> SisterResult<SessionContext>;
107
108 /// Restore sister state from a previous session context.
109 /// Used when Hydra resumes a run
110 fn restore_session(&mut self, context: SessionContext) -> SisterResult<()>;
111
112 /// Get a brief summary for Hydra's context
113 fn summary(&self) -> SisterResult<SisterSummary>;
114
115 /// Execute a command from Hydra.
116 /// This is the escape hatch for Hydra-specific operations
117 fn execute(&mut self, command: HydraCommand) -> SisterResult<CommandResult>;
118}
119
120// ═══════════════════════════════════════════════════════════════════
121// EXECUTION GATE — Hydra's safety core (placeholder types)
122// ═══════════════════════════════════════════════════════════════════
123
124/// Risk level for an action
125#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
126#[serde(rename_all = "snake_case")]
127pub enum RiskLevel {
128 /// Low risk (0.0-0.3): auto-approve
129 Low,
130
131 /// Medium risk (0.3-0.6): log and proceed
132 Medium,
133
134 /// High risk (0.6-0.8): require confirmation
135 High,
136
137 /// Critical risk (0.8-1.0): block and escalate
138 Critical,
139}
140
141/// An action that needs to pass through the execution gate
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct GatedAction {
144 /// What sister is requesting this action
145 pub sister_type: SisterType,
146
147 /// Action type
148 pub action_type: String,
149
150 /// Assessed risk level
151 pub risk_level: RiskLevel,
152
153 /// Risk score (0.0-1.0)
154 pub risk_score: f64,
155
156 /// Required capability
157 pub capability: String,
158
159 /// When the action was requested
160 pub requested_at: DateTime<Utc>,
161
162 /// Action parameters
163 #[serde(default)]
164 pub params: Metadata,
165}
166
167/// Result of passing through the execution gate
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct GateDecision {
170 /// Whether the action is approved
171 pub approved: bool,
172
173 /// Reason for the decision
174 pub reason: String,
175
176 /// Approval ID (for receipt chain)
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub approval_id: Option<String>,
179
180 /// Conditions imposed on the action
181 #[serde(default)]
182 pub conditions: Vec<String>,
183}
184
185/// The Execution Gate trait (placeholder).
186///
187/// Hydra implements this, NOT sisters. Sisters submit actions
188/// to the gate; Hydra decides whether to approve.
189pub trait ExecutionGate {
190 /// Submit an action for approval
191 fn check(&self, action: GatedAction) -> SisterResult<GateDecision>;
192
193 /// Quick check if a capability is available
194 fn has_capability(&self, capability: &str) -> bool;
195
196 /// Get current risk threshold
197 fn risk_threshold(&self) -> RiskLevel;
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_risk_level_ordering() {
206 assert!(RiskLevel::Low < RiskLevel::Medium);
207 assert!(RiskLevel::Medium < RiskLevel::High);
208 assert!(RiskLevel::High < RiskLevel::Critical);
209 }
210
211 #[test]
212 fn test_sister_summary() {
213 let summary = SisterSummary {
214 sister_type: SisterType::Memory,
215 status_line: "590 nodes, session 42 active".into(),
216 item_count: 590,
217 active_context: Some("session_42".into()),
218 metadata: Metadata::new(),
219 };
220
221 assert_eq!(summary.sister_type, SisterType::Memory);
222 assert_eq!(summary.item_count, 590);
223 }
224
225 #[test]
226 fn test_command_result() {
227 let result = CommandResult {
228 success: true,
229 data: serde_json::json!({"added": 5}),
230 error: None,
231 evidence_ids: vec!["ev_1".into()],
232 };
233
234 assert!(result.success);
235 assert_eq!(result.evidence_ids.len(), 1);
236 }
237
238 #[test]
239 fn test_gate_decision() {
240 let decision = GateDecision {
241 approved: true,
242 reason: "Low risk action, auto-approved".into(),
243 approval_id: Some("approval_123".into()),
244 conditions: vec![],
245 };
246
247 assert!(decision.approved);
248 }
249}