use crate::agentic::traits::PromptBindingCompiler;
use crate::agentic::types::*;
#[derive(Debug, Default)]
pub struct DefaultPromptBindingCompiler;
impl PromptBindingCompiler for DefaultPromptBindingCompiler {
fn compile_bindings(&self, task: &TaskContext) -> Result<PromptBindingSet, AgenticError> {
use crate::agentic::prelude::*;
let span = tracing::debug_span!(
"autonomic.prompt_binding_compilation",
task_id = %task.task_id,
phase = ?task.phase,
risk = ?task.risk_level,
)
.entered();
let role_decision = DefaultRoleSelector.select_role(task)?;
let selected_role = Some(role_decision.selected_role);
let topology_decision = DefaultTaskDecomposer.choose_topology(task)?;
let allowed = TopologyPolicy.allowed_topologies(task)?;
if !allowed.contains(&topology_decision.topology) {
tracing::warn!(
target: "agentic.compile_bindings",
task_id = %task.task_id,
chosen_topology = ?topology_decision.topology,
allowed = ?allowed,
"chosen topology is not policy-legal — falling back to first allowed"
);
}
let topology = Some(topology_decision.topology);
let mut bindings = std::collections::BTreeMap::new();
bindings.insert("task_id".to_string(), task.task_id.clone());
bindings.insert("title".to_string(), task.title.clone());
bindings.insert("phase".to_string(), format!("{:?}", task.phase));
bindings.insert("risk_level".to_string(), format!("{:?}", task.risk_level));
bindings.insert(
"confidence_band".to_string(),
format!("{:?}", task.evidence.confidence_band),
);
bindings.insert(
"drift_status".to_string(),
format!("{:?}", task.evidence.drift_status),
);
if let Some(ref role) = selected_role {
bindings.insert("selected_role".to_string(), format!("{:?}", role));
}
if let Some(ref topo) = topology {
bindings.insert("topology".to_string(), format!("{:?}", topo));
}
bindings.insert(
"topology_policy_legal".to_string(),
allowed.contains(topology.as_ref().unwrap()).to_string(),
);
let evidence_receipts: Vec<ReceiptId> = task
.evidence
.receipt_refs
.iter()
.map(|r| r.id.clone())
.collect();
let recommended_actions: Vec<ActionClass> =
task.policy.allowed_actions.iter().cloned().collect();
let forbidden_actions: Vec<ActionClass> =
task.policy.forbidden_actions.iter().cloned().collect();
let binding_count = bindings.len();
let receipt_count = evidence_receipts.len();
let action_names: Vec<String> = recommended_actions
.iter()
.map(|a| format!("{:?}", a))
.collect();
span.record("binding_count", binding_count);
span.record("role", format!("{:?}", selected_role));
span.record("topology", format!("{:?}", topology));
span.record("receipt_count", receipt_count);
span.record("actions_count", action_names.len());
tracing::debug!(
target: "autonomic.prompt_binding_compilation",
task_id = %task.task_id,
selected_role = ?selected_role,
topology = ?topology,
binding_count,
receipt_count,
action_count = action_names.len(),
"bindings compiled successfully"
);
Ok(PromptBindingSet {
task_id: task.task_id.clone(),
phase: task.phase.clone(),
selected_role,
topology,
recommended_actions,
forbidden_actions,
evidence_receipts,
confidence_band: task.evidence.confidence_band.clone(),
drift_status: task.evidence.drift_status.clone(),
bindings,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_task(phase: WorkflowPhase, risk: RiskLevel) -> TaskContext {
TaskContext {
task_id: "pb-test".to_string(),
title: "Prompt binding test".to_string(),
phase,
risk_level: risk,
..Default::default()
}
}
#[test]
fn compile_bindings_includes_mandatory_keys() {
let compiler = DefaultPromptBindingCompiler;
let task = make_task(WorkflowPhase::Plan, RiskLevel::Medium);
let result = compiler.compile_bindings(&task).unwrap();
for key in &[
"task_id",
"title",
"phase",
"risk_level",
"confidence_band",
"drift_status",
] {
assert!(
result.bindings.contains_key(*key),
"missing binding key: {key}"
);
}
}
#[test]
fn compile_bindings_includes_role_and_topology() {
let compiler = DefaultPromptBindingCompiler;
let task = make_task(WorkflowPhase::Execute, RiskLevel::Low);
let result = compiler.compile_bindings(&task).unwrap();
assert!(result.selected_role.is_some(), "must select a role");
assert!(result.topology.is_some(), "must select a topology");
assert!(result.bindings.contains_key("selected_role"));
assert!(result.bindings.contains_key("topology"));
}
#[test]
fn compile_bindings_topology_policy_legal_key_is_present() {
let compiler = DefaultPromptBindingCompiler;
let task = make_task(WorkflowPhase::Validate, RiskLevel::High);
let result = compiler.compile_bindings(&task).unwrap();
assert!(
result.bindings.contains_key("topology_policy_legal"),
"topology_policy_legal binding must be emitted"
);
}
#[test]
fn compile_bindings_validate_phase_policy_legal() {
let compiler = DefaultPromptBindingCompiler;
let task = make_task(WorkflowPhase::Validate, RiskLevel::Low);
let result = compiler.compile_bindings(&task).unwrap();
assert_eq!(
result
.bindings
.get("topology_policy_legal")
.map(String::as_str),
Some("true"),
"ReviewLoop must be policy-legal for Validate phase"
);
}
#[test]
fn compile_bindings_returns_receipts_from_evidence() {
let compiler = DefaultPromptBindingCompiler;
let mut task = make_task(WorkflowPhase::Intake, RiskLevel::Low);
task.evidence.receipt_refs.push(ReceiptRef {
id: "receipt-001".to_string(),
transition_id: None,
summary: None,
});
let result = compiler.compile_bindings(&task).unwrap();
assert_eq!(result.evidence_receipts, vec!["receipt-001".to_string()]);
}
#[test]
fn compile_bindings_empty_task_does_not_panic() {
let compiler = DefaultPromptBindingCompiler;
let task = TaskContext::default();
let result = compiler.compile_bindings(&task);
assert!(
result.is_ok(),
"default TaskContext must not cause panic: {result:?}"
);
}
}