Skip to main content

objects/object/
state_attribution.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Attribution types for states.
3
4use serde::{Deserialize, Serialize};
5
6/// Human identity accountable for changes.
7#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
8pub struct Principal {
9    /// Human-readable name.
10    pub name: String,
11    /// Email address.
12    pub email: String,
13}
14
15impl Principal {
16    /// Create a new principal.
17    pub fn new(name: impl Into<String>, email: impl Into<String>) -> Self {
18        Self {
19            name: name.into(),
20            email: email.into(),
21        }
22    }
23
24    /// Create from environment variables.
25    pub fn from_env() -> Option<Self> {
26        let name = std::env::var("HEDDLE_PRINCIPAL_NAME").ok()?;
27        let email = std::env::var("HEDDLE_PRINCIPAL_EMAIL").ok()?;
28        if name.trim().is_empty() || email.trim().is_empty() {
29            return None;
30        }
31        Some(Self { name, email })
32    }
33}
34
35impl std::fmt::Display for Principal {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(f, "{} <{}>", self.name, self.email)
38    }
39}
40
41/// AI agent identity that performed changes on behalf of a principal.
42#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
43pub struct Agent {
44    /// Provider name (e.g., "anthropic", "openai").
45    pub provider: String,
46    /// Model identifier (e.g., "claude-opus-4-5-20250120").
47    pub model: String,
48    /// Session identifier (opt-in, links to Session.id).
49    pub session_id: Option<String>,
50    /// Segment identifier (opt-in, links to SessionSegment.id).
51    pub segment_id: Option<String>,
52    /// Policy or prompt template identifier.
53    pub policy_id: Option<String>,
54}
55
56impl Agent {
57    /// Create a new agent.
58    pub fn new(provider: impl Into<String>, model: impl Into<String>) -> Self {
59        Self {
60            provider: provider.into(),
61            model: model.into(),
62            session_id: None,
63            segment_id: None,
64            policy_id: None,
65        }
66    }
67
68    /// Create with session linkage.
69    pub fn with_session(
70        mut self,
71        session_id: impl Into<String>,
72        segment_id: impl Into<String>,
73    ) -> Self {
74        self.session_id = Some(session_id.into());
75        self.segment_id = Some(segment_id.into());
76        self
77    }
78
79    /// Create with policy ID.
80    pub fn with_policy(mut self, policy_id: impl Into<String>) -> Self {
81        self.policy_id = Some(policy_id.into());
82        self
83    }
84
85    /// Create from environment variables.
86    pub fn from_env() -> Option<Self> {
87        let provider = std::env::var("HEDDLE_AGENT_PROVIDER").ok()?;
88        let model = std::env::var("HEDDLE_AGENT_MODEL").ok()?;
89        let session_id = std::env::var("HEDDLE_SESSION_ID").ok();
90        let segment_id = std::env::var("HEDDLE_SESSION_SEGMENT").ok();
91        let policy_id = std::env::var("HEDDLE_AGENT_POLICY").ok();
92        Some(Self {
93            provider,
94            model,
95            session_id,
96            segment_id,
97            policy_id,
98        })
99    }
100}
101
102impl std::fmt::Display for Agent {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        write!(f, "{}/{}", self.provider, self.model)
105    }
106}
107
108/// Attribution for a change (who did it).
109#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
110pub struct Attribution {
111    /// Human accountable for the change.
112    pub principal: Principal,
113    /// AI agent that performed the change (if any).
114    pub agent: Option<Agent>,
115}
116
117impl Attribution {
118    /// Create attribution for a human-only change.
119    pub fn human(principal: Principal) -> Self {
120        Self {
121            principal,
122            agent: None,
123        }
124    }
125
126    /// Create attribution for an agent-assisted change.
127    pub fn with_agent(principal: Principal, agent: Agent) -> Self {
128        Self {
129            principal,
130            agent: Some(agent),
131        }
132    }
133}
134
135impl std::fmt::Display for Attribution {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        if let Some(agent) = &self.agent {
138            write!(f, "{} (via {})", self.principal, agent)
139        } else {
140            write!(f, "{}", self.principal)
141        }
142    }
143}