use std::path::Path;
use crate::multi_agent::audit_writer::InvocationAuditPaths;
use crate::multi_agent::types::{AgentInvocation, EntityRef};
use crate::CorpFinanceResult;
use uuid::Uuid;
pub const REGISTERED_CFA_AGENTS: &[&str] = &[
"cfa-chief-analyst",
"cfa-equity-analyst",
"cfa-credit-analyst",
"cfa-fixed-income-analyst",
"cfa-derivatives-analyst",
"cfa-quant-risk-analyst",
"cfa-macro-analyst",
"cfa-private-markets-analyst",
"cfa-esg-regulatory-analyst",
];
pub fn validate_target_agent(slug: &str) -> bool {
REGISTERED_CFA_AGENTS.contains(&slug)
}
pub fn no_cycles_in_chain(invocation: &AgentInvocation, history: &[AgentInvocation]) -> bool {
let mut current = invocation.parent_invocation_id;
let max_walks = history.len() + 1;
for _ in 0..max_walks {
let parent_id = match current {
Some(id) => id,
None => return true,
};
let parent = match history.iter().find(|h| h.invocation_id == parent_id) {
Some(p) => p,
None => return true, };
if parent.target_agent == invocation.target_agent {
return false;
}
current = parent.parent_invocation_id;
}
false
}
pub fn record_invocation(invocation: &AgentInvocation) -> CorpFinanceResult<()> {
if !validate_target_agent(&invocation.target_agent) {
return Err(crate::error::CorpFinanceError::InvalidInput {
field: "target_agent".to_string(),
reason: format!(
"agent slug '{}' is not in the registered CFA specialist set",
invocation.target_agent
),
});
}
#[cfg(feature = "audit")]
{
let manifest = crate::audit::surface_audit::SurfaceManifest {
surface: crate::surface::Surface::Mcp,
surface_event_id: invocation.invocation_id.to_string(),
command_args: serde_json::json!({
"event": "agent_invocation_started",
"target_agent": invocation.target_agent,
"input_summary": invocation.input_summary,
"tenant_id": invocation.tenant_id,
"parent_invocation_id": invocation
.parent_invocation_id
.map(|u| u.to_string()),
}),
output_paths: Vec::new(),
};
let _hash = crate::audit::surface_audit::compute_surface_audit_hash(&manifest);
}
Ok(())
}
pub fn complete_invocation(
invocation_id: Uuid,
output_summary: &str,
entities: &[EntityRef],
) -> CorpFinanceResult<()> {
#[cfg(feature = "audit")]
{
let entity_summary: Vec<serde_json::Value> = entities
.iter()
.map(|e| {
serde_json::json!({
"kind": format!("{:?}", e.kind).to_lowercase(),
"value": e.value,
})
})
.collect();
let manifest = crate::audit::surface_audit::SurfaceManifest {
surface: crate::surface::Surface::Mcp,
surface_event_id: invocation_id.to_string(),
command_args: serde_json::json!({
"event": "agent_invocation_completed",
"output_summary": output_summary,
"entities": entity_summary,
}),
output_paths: Vec::new(),
};
let _hash = crate::audit::surface_audit::compute_surface_audit_hash(&manifest);
}
#[cfg(not(feature = "audit"))]
let _ = (invocation_id, output_summary, entities);
Ok(())
}
pub fn record_invocation_with_audit(
invocation: &AgentInvocation,
manifest_root: &Path,
) -> CorpFinanceResult<InvocationAuditPaths> {
record_invocation(invocation)?;
crate::multi_agent::audit_writer::write_invocation_started(manifest_root, invocation)
}
pub fn complete_invocation_with_audit(
invocation_id: Uuid,
output_summary: &str,
entities: &[EntityRef],
tenant_id: Option<&str>,
manifest_root: &Path,
) -> CorpFinanceResult<InvocationAuditPaths> {
complete_invocation(invocation_id, output_summary, entities)?;
crate::multi_agent::audit_writer::write_invocation_completed(
manifest_root,
invocation_id,
output_summary,
entities,
tenant_id,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::multi_agent::types::InvocationStatus;
use chrono::Utc;
fn mk(slug: &str, parent: Option<Uuid>) -> AgentInvocation {
AgentInvocation {
invocation_id: Uuid::now_v7(),
target_agent: slug.into(),
parent_invocation_id: parent,
input_summary: "x".into(),
tenant_id: Some("local".into()),
ts: Utc::now(),
status: InvocationStatus::Pending,
}
}
#[test]
fn validates_each_registered_agent() {
for slug in REGISTERED_CFA_AGENTS {
assert!(validate_target_agent(slug));
}
}
#[test]
fn rejects_unknown_slug() {
assert!(!validate_target_agent("not-a-real-specialist"));
}
#[test]
fn no_cycles_with_empty_history() {
let inv = mk("cfa-equity-analyst", None);
assert!(no_cycles_in_chain(&inv, &[]));
}
#[test]
fn detects_cycle_when_ancestor_shares_slug() {
let root = mk("cfa-equity-analyst", None);
let child = AgentInvocation {
parent_invocation_id: Some(root.invocation_id),
..mk("cfa-credit-analyst", Some(root.invocation_id))
};
let grand = AgentInvocation {
parent_invocation_id: Some(child.invocation_id),
..mk("cfa-equity-analyst", Some(child.invocation_id))
};
let history = vec![root.clone(), child.clone()];
assert!(!no_cycles_in_chain(&grand, &history));
}
#[test]
fn record_invocation_rejects_unknown_slug() {
let mut inv = mk("cfa-equity-analyst", None);
inv.target_agent = "ghost-analyst".into();
let res = record_invocation(&inv);
assert!(res.is_err());
}
#[test]
fn record_invocation_accepts_known_slug() {
let inv = mk("cfa-private-markets-analyst", None);
assert!(record_invocation(&inv).is_ok());
}
#[cfg(feature = "audit")]
#[test]
fn record_invocation_computes_audit_hash_when_audit_feature_on() {
let inv = mk("cfa-equity-analyst", None);
let result = record_invocation(&inv);
assert!(result.is_ok());
}
#[cfg(feature = "audit")]
#[test]
fn complete_invocation_computes_audit_hash_when_audit_feature_on() {
let inv = mk("cfa-equity-analyst", None);
let entities: Vec<EntityRef> = Vec::new();
let result = complete_invocation(inv.invocation_id, "summary", &entities);
assert!(result.is_ok());
}
#[test]
fn record_invocation_with_audit_validates_target_agent() {
let dir = tempfile::tempdir().unwrap();
let mut inv = mk("cfa-equity-analyst", None);
inv.target_agent = "not-a-cfa-agent".into();
let result = record_invocation_with_audit(&inv, dir.path());
assert!(result.is_err(), "unknown slug must return Err");
}
#[test]
fn record_invocation_with_audit_writes_files_for_known_slug() {
let dir = tempfile::tempdir().unwrap();
let inv = mk("cfa-fixed-income-analyst", None);
let paths = record_invocation_with_audit(&inv, dir.path()).unwrap();
assert!(paths.event_path.exists(), "event file must exist");
assert!(paths.audit_path.exists(), "audit companion must exist");
}
#[test]
fn complete_invocation_with_audit_writes_completed_phase() {
let dir = tempfile::tempdir().unwrap();
let inv = mk("cfa-macro-analyst", None);
let entities: Vec<EntityRef> = Vec::new();
let paths = complete_invocation_with_audit(
inv.invocation_id,
"macro summary",
&entities,
inv.tenant_id.as_deref(),
dir.path(),
)
.unwrap();
let name = paths.event_path.file_name().unwrap().to_string_lossy();
assert!(
name.contains(".completed."),
"filename must contain .completed. infix"
);
}
}