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