Skip to main content

heddle_object_model/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 bytes, preserved exactly from the source identity.
10    #[serde(with = "serde_bytes")]
11    pub name: Vec<u8>,
12    /// Email address bytes, preserved exactly from the source identity.
13    #[serde(with = "serde_bytes")]
14    pub email: Vec<u8>,
15}
16
17impl Principal {
18    /// Create a new principal.
19    pub fn new(name: impl AsRef<[u8]>, email: impl AsRef<[u8]>) -> Self {
20        Self {
21            name: name.as_ref().to_vec(),
22            email: email.as_ref().to_vec(),
23        }
24    }
25
26    /// Return the name as text for display or text-only API boundaries.
27    pub fn name_lossy(&self) -> std::borrow::Cow<'_, str> {
28        String::from_utf8_lossy(&self.name)
29    }
30
31    /// Return the email as text for display or text-only API boundaries.
32    pub fn email_lossy(&self) -> std::borrow::Cow<'_, str> {
33        String::from_utf8_lossy(&self.email)
34    }
35
36    /// Create from environment variables.
37    pub fn from_env() -> Option<Self> {
38        let name = std::env::var("HEDDLE_PRINCIPAL_NAME").ok()?;
39        let email = std::env::var("HEDDLE_PRINCIPAL_EMAIL").ok()?;
40        if name.trim().is_empty() || email.trim().is_empty() {
41            return None;
42        }
43        Some(Self {
44            name: name.into_bytes(),
45            email: email.into_bytes(),
46        })
47    }
48}
49
50impl std::fmt::Display for Principal {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(f, "{} <{}>", self.name_lossy(), self.email_lossy())
53    }
54}
55
56/// AI agent identity that performed changes on behalf of a principal.
57#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
58pub struct Agent {
59    /// Provider name (e.g., "anthropic", "openai").
60    pub provider: String,
61    /// Model identifier (e.g., "claude-opus-4-5-20250120").
62    pub model: String,
63    /// Session identifier (opt-in, links to Session.id).
64    pub session_id: Option<String>,
65    /// Segment identifier (opt-in, links to SessionSegment.id).
66    pub segment_id: Option<String>,
67    /// Policy or prompt template identifier.
68    pub policy_id: Option<String>,
69    /// Frozen harness thought_level (ACP name: Claude effort, Codex reasoning, OpenCode variant).
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub thought_level: Option<String>,
72    /// Frozen harness parent actor / agent_id (ACP name).
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub parent: Option<String>,
75}
76
77impl Agent {
78    /// Create a new agent.
79    pub fn new(provider: impl Into<String>, model: impl Into<String>) -> Self {
80        Self {
81            provider: provider.into(),
82            model: model.into(),
83            session_id: None,
84            segment_id: None,
85            policy_id: None,
86            thought_level: None,
87            parent: None,
88        }
89    }
90
91    /// Create with session linkage.
92    pub fn with_session(
93        mut self,
94        session_id: impl Into<String>,
95        segment_id: impl Into<String>,
96    ) -> Self {
97        self.session_id = Some(session_id.into());
98        self.segment_id = Some(segment_id.into());
99        self
100    }
101
102    /// Create with policy ID.
103    pub fn with_policy(mut self, policy_id: impl Into<String>) -> Self {
104        self.policy_id = Some(policy_id.into());
105        self
106    }
107
108    /// Create from environment variables.
109    pub fn from_env() -> Option<Self> {
110        let provider = std::env::var("HEDDLE_AGENT_PROVIDER").ok()?;
111        let model = std::env::var("HEDDLE_AGENT_MODEL").ok()?;
112        let session_id = std::env::var("HEDDLE_SESSION_ID").ok();
113        let segment_id = std::env::var("HEDDLE_SESSION_SEGMENT").ok();
114        let policy_id = std::env::var("HEDDLE_AGENT_POLICY").ok();
115        Some(Self {
116            provider,
117            model,
118            session_id,
119            segment_id,
120            policy_id,
121            thought_level: None,
122            parent: None,
123        })
124    }
125
126    /// Freeze a published thought_level onto this agent.
127    pub fn with_thought_level(mut self, thought_level: impl Into<String>) -> Self {
128        self.thought_level = Some(thought_level.into());
129        self
130    }
131
132    /// Freeze a published parent actor onto this agent.
133    pub fn with_parent(mut self, parent: impl Into<String>) -> Self {
134        self.parent = Some(parent.into());
135        self
136    }
137}
138
139impl std::fmt::Display for Agent {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        write!(f, "{}/{}", self.provider, self.model)
142    }
143}
144
145/// Attribution for a change (who did it).
146#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
147pub struct Attribution {
148    /// Human accountable for the change.
149    pub principal: Principal,
150    /// AI agent that performed the change (if any).
151    pub agent: Option<Agent>,
152}
153
154impl Attribution {
155    /// Create attribution for a human-only change.
156    pub fn human(principal: Principal) -> Self {
157        Self {
158            principal,
159            agent: None,
160        }
161    }
162
163    /// Create attribution for an agent-assisted change.
164    pub fn with_agent(principal: Principal, agent: Agent) -> Self {
165        Self {
166            principal,
167            agent: Some(agent),
168        }
169    }
170}
171
172impl std::fmt::Display for Attribution {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        if let Some(agent) = &self.agent {
175            write!(f, "{} (via {})", self.principal, agent)
176        } else {
177            write!(f, "{}", self.principal)
178        }
179    }
180}