use super::fence::strip_optional_json_fence_label;
use super::types::{
AgentReplyPlanV1, PlanArtifactError, normalize_agent_reply_plan_v1_acceptance_in_place,
};
use super::validate::{
validate_agent_reply_plan_v1, validate_agent_reply_plan_v1_with_validate_only_binding_ids,
};
pub fn parse_agent_reply_plan_v1(content: &str) -> Result<AgentReplyPlanV1, PlanArtifactError> {
for slice in collect_json_candidates(content) {
let Ok(plan) = serde_json::from_str::<AgentReplyPlanV1>(&slice) else {
continue;
};
if validate_agent_reply_plan_v1(&plan).is_ok() {
let mut plan = plan;
finalize_agent_reply_plan_v1_acceptance_in_place(&mut plan);
return Ok(plan);
}
}
Err(PlanArtifactError::NotFound)
}
pub fn parse_agent_reply_plan_v1_with_validate_only_binding_ids(
content: &str,
validate_only_binding_ids: Option<&[String]>,
) -> Result<AgentReplyPlanV1, PlanArtifactError> {
for slice in collect_json_candidates(content) {
let Ok(plan) = serde_json::from_str::<AgentReplyPlanV1>(&slice) else {
continue;
};
if validate_agent_reply_plan_v1_with_validate_only_binding_ids(
&plan,
validate_only_binding_ids,
)
.is_ok()
{
let mut plan = plan;
finalize_agent_reply_plan_v1_acceptance_in_place(&mut plan);
return Ok(plan);
}
}
Err(PlanArtifactError::NotFound)
}
fn finalize_agent_reply_plan_v1_acceptance_in_place(plan: &mut AgentReplyPlanV1) {
normalize_agent_reply_plan_v1_acceptance_in_place(plan);
for step in &mut plan.steps {
crate::cm_agent::acceptance::apply_executor_kind_acceptance_defaults(step);
}
}
pub fn assistant_merged_text_for_plan_artifact_parse(msg: &crate::cm_types::Message) -> String {
let mut m = msg.clone();
crate::cm_types::merge_reasoning_details_into_reasoning_content(&mut m);
let c = crate::cm_types::message_content_as_str(&m.content).unwrap_or("");
let r = m.reasoning_content.as_deref().unwrap_or("");
let c = c.trim();
let r = r.trim();
match (r.is_empty(), c.is_empty()) {
(true, true) => String::new(),
(true, false) => c.to_string(),
(false, true) => r.to_string(),
(false, false) => format!("{r}\n\n{c}"),
}
}
pub fn parse_agent_reply_plan_v1_from_assistant_message(
msg: &crate::cm_types::Message,
) -> Result<AgentReplyPlanV1, PlanArtifactError> {
let merged = assistant_merged_text_for_plan_artifact_parse(msg);
parse_agent_reply_plan_v1(&merged)
}
pub fn parse_agent_reply_plan_v1_from_assistant_message_with_validate_only_binding_ids(
msg: &crate::cm_types::Message,
validate_only_binding_ids: Option<&[String]>,
) -> Result<AgentReplyPlanV1, PlanArtifactError> {
let merged = assistant_merged_text_for_plan_artifact_parse(msg);
parse_agent_reply_plan_v1_with_validate_only_binding_ids(&merged, validate_only_binding_ids)
}
fn collect_json_candidates(content: &str) -> Vec<String> {
let mut out = Vec::new();
let parts: Vec<&str> = content.split("```").collect();
for i in (1..parts.len()).step_by(2) {
let raw = parts[i].trim();
if raw.is_empty() {
continue;
}
let body = strip_optional_json_fence_label(raw);
if body.starts_with('{') {
out.push(body);
}
}
let all = content.trim();
if all.starts_with('{') && !out.iter().any(|s| s.as_str() == all) {
out.push(all.to_string());
}
out
}