use crate::blueprint::{AgentMeta, Blueprint};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)]
pub struct CtxKeyResolution {
pub value: Value,
pub winning_tier: CtxTier,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CtxTier {
AgentInline,
MetaRef,
BpGlobal,
}
pub fn explain_agent_ctx(
bp: &Blueprint,
agent: &str,
) -> Option<BTreeMap<String, CtxKeyResolution>> {
let agent_def = bp.agents.iter().find(|ad| ad.name == agent)?;
let meta: Option<&AgentMeta> = agent_def.meta.as_ref();
let mut out: BTreeMap<String, CtxKeyResolution> = BTreeMap::new();
if let Some(Value::Object(obj)) = bp.default_agent_ctx.as_ref() {
for (k, v) in obj {
out.insert(
k.clone(),
CtxKeyResolution {
value: v.clone(),
winning_tier: CtxTier::BpGlobal,
},
);
}
}
if let Some(meta_ref) = meta.and_then(|m| m.meta_ref.as_ref()) {
if let Some(meta_def) = bp.metas.iter().find(|m| &m.name == meta_ref) {
if let Value::Object(obj) = &meta_def.ctx {
for (k, v) in obj {
out.insert(
k.clone(),
CtxKeyResolution {
value: v.clone(),
winning_tier: CtxTier::MetaRef,
},
);
}
}
}
}
if let Some(Value::Object(obj)) = meta.and_then(|m| m.ctx.as_ref()) {
for (k, v) in obj {
out.insert(
k.clone(),
CtxKeyResolution {
value: v.clone(),
winning_tier: CtxTier::AgentInline,
},
);
}
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::blueprint::{
current_schema_version, AgentDef, AgentKind, BlueprintMetadata, CompilerHints,
CompilerStrategy, MetaDef,
};
use mlua_flow_ir::{Expr, Node as FlowNode};
use serde_json::json;
fn path(s: &str) -> Expr {
Expr::Path {
at: s.parse().expect("literal test path"),
}
}
fn noop_flow() -> FlowNode {
FlowNode::Step {
ref_: "noop".to_string(),
in_: path("$.input"),
out: path("$.out"),
}
}
fn agent(name: &str, meta: Option<AgentMeta>) -> AgentDef {
AgentDef {
name: name.to_string(),
kind: AgentKind::RustFn,
spec: json!({ "fn_id": name }),
profile: None,
meta,
}
}
fn bp(
agents: Vec<AgentDef>,
metas: Vec<MetaDef>,
default_agent_ctx: Option<Value>,
) -> Blueprint {
Blueprint {
schema_version: current_schema_version(),
id: "explain-test".into(),
flow: noop_flow(),
agents,
operators: vec![],
metas,
hints: CompilerHints::default(),
strategy: CompilerStrategy::default(),
metadata: BlueprintMetadata::default(),
spawner_hints: Default::default(),
default_agent_kind: AgentKind::Operator,
default_operator_kind: None,
default_init_ctx: None,
default_agent_ctx,
default_context_policy: None,
projection_placement: None,
audits: vec![],
degradation_policy: None,
}
}
#[test]
fn bp_global_only() {
let blueprint = bp(
vec![agent("scout", None)],
vec![],
Some(json!({ "work_dir": "/bp-global" })),
);
let resolved = explain_agent_ctx(&blueprint, "scout").expect("agent present");
assert_eq!(resolved.len(), 1);
assert_eq!(resolved["work_dir"].value, json!("/bp-global"));
assert_eq!(resolved["work_dir"].winning_tier, CtxTier::BpGlobal);
}
#[test]
fn agent_inline_overrides_bp_global() {
let blueprint = bp(
vec![agent(
"scout",
Some(AgentMeta {
ctx: Some(json!({ "work_dir": "/inline" })),
..Default::default()
}),
)],
vec![],
Some(json!({ "work_dir": "/bp-global", "extra": "kept" })),
);
let resolved = explain_agent_ctx(&blueprint, "scout").expect("agent present");
assert_eq!(resolved["work_dir"].value, json!("/inline"));
assert_eq!(resolved["work_dir"].winning_tier, CtxTier::AgentInline);
assert_eq!(resolved["extra"].value, json!("kept"));
assert_eq!(resolved["extra"].winning_tier, CtxTier::BpGlobal);
}
#[test]
fn meta_ref_wins_over_bp_global_and_agent_inline_wins_over_meta_ref() {
let blueprint = bp(
vec![agent(
"scout",
Some(AgentMeta {
ctx: Some(json!({ "work_dir": "/inline-wins" })),
meta_ref: Some("heavy-scan".to_string()),
..Default::default()
}),
)],
vec![MetaDef {
name: "heavy-scan".to_string(),
ctx: json!({ "work_dir": "/meta-ref", "budget": "high" }),
}],
Some(json!({ "work_dir": "/bp-global", "budget": "low", "extra": "kept" })),
);
let resolved = explain_agent_ctx(&blueprint, "scout").expect("agent present");
assert_eq!(resolved["work_dir"].value, json!("/inline-wins"));
assert_eq!(resolved["work_dir"].winning_tier, CtxTier::AgentInline);
assert_eq!(resolved["budget"].value, json!("high"));
assert_eq!(resolved["budget"].winning_tier, CtxTier::MetaRef);
assert_eq!(resolved["extra"].value, json!("kept"));
assert_eq!(resolved["extra"].winning_tier, CtxTier::BpGlobal);
}
#[test]
fn agent_not_in_blueprint_returns_none() {
let blueprint = bp(vec![agent("scout", None)], vec![], None);
assert!(explain_agent_ctx(&blueprint, "no-such-agent").is_none());
}
#[test]
fn explain_agent_ctx_matches_derive_agent_ctx_semantics() {
use crate::service::task_launch::derive_agent_ctx;
let blueprint = bp(
vec![
agent(
"scout",
Some(AgentMeta {
ctx: Some(json!({ "work_dir": "/inline-wins" })),
meta_ref: Some("heavy-scan".to_string()),
..Default::default()
}),
),
agent("plain", None),
],
vec![MetaDef {
name: "heavy-scan".to_string(),
ctx: json!({ "work_dir": "/meta-ref", "budget": "high" }),
}],
Some(json!({ "work_dir": "/bp-global", "budget": "low", "extra": "kept" })),
);
let (global, per_agent) = derive_agent_ctx(&blueprint);
for agent_name in ["scout", "plain"] {
let mut derived_merged = serde_json::Map::new();
if let Some(Value::Object(g)) = &global {
derived_merged.extend(g.clone());
}
if let Some(Value::Object(pa)) = per_agent.get(agent_name) {
for (k, v) in pa {
derived_merged.insert(k.clone(), v.clone());
}
}
let explained = explain_agent_ctx(&blueprint, agent_name).expect("agent present");
let explained_values: serde_json::Map<String, Value> = explained
.into_iter()
.map(|(k, resolution)| (k, resolution.value))
.collect();
assert_eq!(
explained_values, derived_merged,
"explain_agent_ctx must resolve to the exact same merged \
result as derive_agent_ctx for agent '{agent_name}'"
);
}
}
}