use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::intent::{AgentId, Authority, Intent, ToolCall};
use crate::object::ObjectId;
pub const MAX_DESCRIPTION_LEN: usize = 1024;
pub const MAX_REASONING_LEN: usize = 8192;
pub const MAX_AGENT_ID_LEN: usize = 128;
pub const MAX_TOOL_CALLS: usize = 256;
fn truncate_chars(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
s.chars().take(max).collect()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Commit {
pub id: ObjectId,
pub state_root: ObjectId,
pub parents: Vec<ObjectId>,
pub timestamp: DateTime<Utc>,
pub agent_id: AgentId,
pub authority: Authority,
pub intent: Intent,
pub reasoning: Option<String>,
pub confidence: Option<f64>,
pub tool_calls: Vec<ToolCall>,
}
impl Commit {
pub fn enforce_caps(&mut self) {
if self.agent_id.chars().count() > MAX_AGENT_ID_LEN {
self.agent_id = truncate_chars(&self.agent_id, MAX_AGENT_ID_LEN);
}
if self.intent.description.chars().count() > MAX_DESCRIPTION_LEN {
self.intent.description = truncate_chars(&self.intent.description, MAX_DESCRIPTION_LEN);
}
if let Some(reasoning) = self.reasoning.as_mut()
&& reasoning.chars().count() > MAX_REASONING_LEN
{
*reasoning = truncate_chars(reasoning, MAX_REASONING_LEN);
}
if self.tool_calls.len() > MAX_TOOL_CALLS {
self.tool_calls.truncate(MAX_TOOL_CALLS);
}
}
}
pub struct CommitBuilder {
state_root: ObjectId,
parents: Vec<ObjectId>,
agent_id: AgentId,
authority: Authority,
intent: Intent,
reasoning: Option<String>,
confidence: Option<f64>,
tool_calls: Vec<ToolCall>,
}
impl CommitBuilder {
pub fn new(
state_root: ObjectId,
agent_id: impl Into<AgentId>,
authority: Authority,
intent: Intent,
) -> Self {
Self {
state_root,
parents: Vec::new(),
agent_id: agent_id.into(),
authority,
intent,
reasoning: None,
confidence: None,
tool_calls: Vec::new(),
}
}
pub fn parents(mut self, parents: Vec<ObjectId>) -> Self {
self.parents = parents;
self
}
pub fn parent(mut self, parent: ObjectId) -> Self {
self.parents = vec![parent];
self
}
pub fn reasoning(mut self, reasoning: impl Into<String>) -> Self {
self.reasoning = Some(reasoning.into());
self
}
pub fn confidence(mut self, confidence: f64) -> Self {
self.confidence = Some(confidence.clamp(0.0, 1.0));
self
}
pub fn tool_calls(mut self, calls: Vec<ToolCall>) -> Self {
self.tool_calls = calls;
self
}
pub fn build(self) -> Commit {
let timestamp = Utc::now();
let agent_id = truncate_chars(&self.agent_id, MAX_AGENT_ID_LEN);
let mut intent = self.intent;
intent.description = truncate_chars(&intent.description, MAX_DESCRIPTION_LEN);
let reasoning = self
.reasoning
.map(|r| truncate_chars(&r, MAX_REASONING_LEN));
let mut tool_calls = self.tool_calls;
if tool_calls.len() > MAX_TOOL_CALLS {
tool_calls.truncate(MAX_TOOL_CALLS);
}
let mut commit = Commit {
id: ObjectId::from_bytes([0u8; 32]), state_root: self.state_root,
parents: self.parents,
timestamp,
agent_id,
authority: self.authority,
intent,
reasoning,
confidence: self.confidence,
tool_calls,
};
let bytes = serde_json::to_vec(&CommitHashInput {
state_root: &commit.state_root,
parents: &commit.parents,
timestamp: &commit.timestamp,
agent_id: &commit.agent_id,
authority: &commit.authority,
intent: &commit.intent,
reasoning: &commit.reasoning,
confidence: &commit.confidence,
tool_calls: &commit.tool_calls,
})
.expect("commit serialization should never fail");
commit.id = ObjectId::hash(&bytes);
commit
}
}
#[derive(Serialize)]
struct CommitHashInput<'a> {
state_root: &'a ObjectId,
parents: &'a Vec<ObjectId>,
timestamp: &'a DateTime<Utc>,
agent_id: &'a AgentId,
authority: &'a Authority,
intent: &'a Intent,
reasoning: &'a Option<String>,
confidence: &'a Option<f64>,
tool_calls: &'a Vec<ToolCall>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::intent::IntentCategory;
fn test_intent() -> Intent {
Intent::new(IntentCategory::Checkpoint, "test commit")
}
fn test_authority() -> Authority {
Authority::simple("test-agent")
}
#[test]
fn test_commit_builder() {
let state_root = ObjectId::hash(b"test-state");
let commit = CommitBuilder::new(state_root, "agent/test", test_authority(), test_intent())
.reasoning("this is a test")
.confidence(0.95)
.build();
assert_eq!(commit.state_root, state_root);
assert_eq!(commit.agent_id, "agent/test");
assert_eq!(commit.reasoning, Some("this is a test".to_string()));
assert_eq!(commit.confidence, Some(0.95));
assert!(commit.parents.is_empty());
}
#[test]
fn test_commit_id_is_content_addressed() {
let state_root = ObjectId::hash(b"test-state");
let commit1 = CommitBuilder::new(
state_root,
"agent/test",
test_authority(),
Intent::new(IntentCategory::Explore, "exploring option A"),
)
.build();
let commit2 = CommitBuilder::new(
state_root,
"agent/test",
test_authority(),
Intent::new(IntentCategory::Fix, "fixing a bug"),
)
.build();
assert_ne!(
commit1.id, commit2.id,
"different commits should have different IDs"
);
}
#[test]
fn test_commit_with_parent() {
let parent_id = ObjectId::hash(b"parent-commit");
let state_root = ObjectId::hash(b"child-state");
let commit = CommitBuilder::new(state_root, "agent/test", test_authority(), test_intent())
.parent(parent_id)
.build();
assert_eq!(commit.parents, vec![parent_id]);
}
#[test]
fn test_description_truncated() {
let state_root = ObjectId::hash(b"test");
let long = "x".repeat(MAX_DESCRIPTION_LEN + 500);
let commit = CommitBuilder::new(
state_root,
"agent/test",
test_authority(),
Intent::new(IntentCategory::Checkpoint, long),
)
.build();
assert_eq!(
commit.intent.description.chars().count(),
MAX_DESCRIPTION_LEN
);
}
#[test]
fn test_reasoning_truncated() {
let state_root = ObjectId::hash(b"test");
let long = "r".repeat(MAX_REASONING_LEN + 10);
let commit = CommitBuilder::new(state_root, "agent/test", test_authority(), test_intent())
.reasoning(long)
.build();
assert_eq!(
commit.reasoning.as_ref().unwrap().chars().count(),
MAX_REASONING_LEN
);
}
#[test]
fn test_agent_id_truncated() {
let state_root = ObjectId::hash(b"test");
let long = "a".repeat(MAX_AGENT_ID_LEN + 50);
let commit = CommitBuilder::new(state_root, long, test_authority(), test_intent()).build();
assert_eq!(commit.agent_id.chars().count(), MAX_AGENT_ID_LEN);
}
#[test]
fn test_tool_calls_truncated() {
let state_root = ObjectId::hash(b"test");
let calls: Vec<ToolCall> = (0..(MAX_TOOL_CALLS + 20))
.map(|i| ToolCall {
tool_name: format!("tool_{}", i),
arguments: serde_json::json!({}),
result: None,
timestamp: Utc::now(),
})
.collect();
let commit = CommitBuilder::new(state_root, "agent/test", test_authority(), test_intent())
.tool_calls(calls)
.build();
assert_eq!(commit.tool_calls.len(), MAX_TOOL_CALLS);
}
#[test]
fn test_truncation_preserves_utf8_boundaries() {
let state_root = ObjectId::hash(b"test");
let emoji_flood = "🎉".repeat(MAX_DESCRIPTION_LEN + 10);
let commit = CommitBuilder::new(
state_root,
"agent/test",
test_authority(),
Intent::new(IntentCategory::Checkpoint, emoji_flood),
)
.build();
assert_eq!(
commit.intent.description.chars().count(),
MAX_DESCRIPTION_LEN
);
assert!(
commit
.intent
.description
.is_char_boundary(commit.intent.description.len())
);
}
#[test]
fn test_enforce_caps_truncates_bypassed_builder() {
let big_reasoning = "r".repeat(100_000); let big_desc = "d".repeat(MAX_DESCRIPTION_LEN * 4);
let big_agent = "a".repeat(MAX_AGENT_ID_LEN * 10);
let big_tools: Vec<ToolCall> = (0..(MAX_TOOL_CALLS * 2))
.map(|i| ToolCall {
tool_name: format!("t{}", i),
arguments: serde_json::json!({}),
result: None,
timestamp: Utc::now(),
})
.collect();
let mut commit = Commit {
id: ObjectId::hash(b"test"),
state_root: ObjectId::hash(b"state"),
parents: vec![],
timestamp: Utc::now(),
agent_id: big_agent,
authority: test_authority(),
intent: Intent::new(IntentCategory::Checkpoint, big_desc),
reasoning: Some(big_reasoning),
confidence: Some(0.5),
tool_calls: big_tools,
};
commit.enforce_caps();
assert_eq!(commit.agent_id.chars().count(), MAX_AGENT_ID_LEN);
assert_eq!(
commit.intent.description.chars().count(),
MAX_DESCRIPTION_LEN
);
assert_eq!(
commit.reasoning.as_ref().unwrap().chars().count(),
MAX_REASONING_LEN
);
assert_eq!(commit.tool_calls.len(), MAX_TOOL_CALLS);
}
#[test]
fn test_enforce_caps_is_idempotent() {
let commit_orig = CommitBuilder::new(
ObjectId::hash(b"s"),
"agent/test",
test_authority(),
test_intent(),
)
.reasoning("short")
.build();
let mut commit = commit_orig.clone();
commit.enforce_caps();
assert_eq!(commit.agent_id, commit_orig.agent_id);
assert_eq!(commit.intent.description, commit_orig.intent.description);
assert_eq!(commit.reasoning, commit_orig.reasoning);
assert_eq!(commit.tool_calls.len(), commit_orig.tool_calls.len());
}
#[test]
fn test_confidence_clamped() {
let state_root = ObjectId::hash(b"test");
let commit = CommitBuilder::new(state_root, "agent/test", test_authority(), test_intent())
.confidence(1.5) .build();
assert_eq!(commit.confidence, Some(1.0));
}
}