use super::gate::{build_description, build_tools, resolve_render_gate, synthesized_description};
use super::sections::{build_assertion_section, build_mutation_section, build_render_section};
use super::support::{
has_data_access_capability, is_assertion, is_mutation, tier_collapse_comment,
};
use super::types::{ContextInjection, RenderFlavor};
use crate::error::DispatchError;
use crate::ir::ComponentNode;
use crate::models::ModelConfig;
use crate::provider::ToolMap;
use crate::resolve::ResolutionReport;
use serde::Serialize;
#[derive(Serialize)]
pub(super) struct AgentFrontmatter {
pub(super) name: String,
pub(super) description: String,
pub(super) tools: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) model: Option<String>,
}
pub(super) fn to_yaml(fm: &impl Serialize) -> String {
serde_yaml::to_string(fm)
.expect("frontmatter serializes")
.trim_end()
.to_string()
}
#[allow(clippy::too_many_arguments)]
pub(super) fn build_agent_markdown(
node: &ComponentNode,
report: &ResolutionReport,
flavor: RenderFlavor,
models: &ModelConfig,
context: &ContextInjection,
tool_map: &ToolMap,
include_setup_recovery_tool: bool,
include_dashboard_save_tool: bool,
) -> Result<String, DispatchError> {
build_agent_markdown_named(
&node.verb,
node,
report,
flavor,
models,
context,
tool_map,
include_setup_recovery_tool,
include_dashboard_save_tool,
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn build_agent_markdown_named(
name: &str,
node: &ComponentNode,
report: &ResolutionReport,
flavor: RenderFlavor,
models: &ModelConfig,
context: &ContextInjection,
tool_map: &ToolMap,
include_setup_recovery_tool: bool,
include_dashboard_save_tool: bool,
) -> Result<String, DispatchError> {
let gate = resolve_render_gate(node, report, flavor);
let model = (!node.llm_calls.is_empty())
.then(|| models.collapsed_model(&node.llm_calls).map(str::to_string))
.transpose()?;
let mut tools = build_tools(node, &gate, tool_map);
if include_setup_recovery_tool {
tools.push("mcp__genbi_session__report_setup_recovery".to_string());
}
if include_dashboard_save_tool {
tools.push("mcp__genbi_session__save_dashboard".to_string());
}
let frontmatter = AgentFrontmatter {
name: name.to_string(),
description: if name == node.verb {
build_description(node)
} else {
synthesized_description(node)
},
tools,
model: model.clone(),
};
let yaml_block = to_yaml(&frontmatter);
let data_access_line =
if has_data_access_capability(&node.required_capabilities) || is_mutation(node) {
"All data access MUST go through the `wren` CLI (e.g. `wren --sql ...`, `wren cube \
list`, `wren genbi build ...`) — never raw SQL clients, never filesystem tricks against the \
underlying warehouse."
} else {
"You hold no data-access tools: this agent never queries that project itself. Report \
what your own tools return and nothing beyond it."
};
let binding_line = if context.introspected() {
format!(
"You are bound to the wren project at `{}`.",
node.context_binding.project
)
} else {
format!(
"You are bound to the semantic layer `{}`, which lives elsewhere and was not read here.",
node.context_binding.project
)
};
let preamble = if node.context_binding.absent {
String::new()
} else {
[binding_line, data_access_line.to_string()].join("\n")
};
let mut parts: Vec<String> = vec!["---".to_string(), yaml_block, "---".to_string()];
if !node.context_binding.absent {
parts.extend([
String::new(),
preamble,
String::new(),
context.prompt_section(),
]);
}
if let Some(comment) = model
.as_deref()
.and_then(|model| tier_collapse_comment(&node.llm_calls, model))
{
parts.push(String::new());
parts.push(comment);
}
if let Some(brief) = &node.brief {
parts.push(String::new());
parts.push(brief.clone());
}
parts.push(String::new());
parts.push(node.prompt_fragment.clone());
if let Some(section) = build_render_section(node, &gate) {
parts.push(String::new());
parts.push(section);
}
if is_assertion(node) {
parts.push(String::new());
parts.push(build_assertion_section(node));
}
if is_mutation(node) {
parts.push(String::new());
parts.push(build_mutation_section(node));
}
Ok(format!("{}\n", parts.join("\n")))
}