use super::{dict_get, vm_to_json};
use crate::value::VmValue;
pub(super) fn plan_artifact_from_result(result: &VmValue) -> Option<serde_json::Value> {
if let Some(VmValue::String(rendered)) = dict_get(result, "result") {
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(rendered) {
if parsed.is_object() {
return Some(parsed);
}
}
}
if let Some(value) = dict_get(result, "result") {
let json = vm_to_json(value);
if json.is_object() {
return Some(json);
}
}
let tool_name = dict_get(result, "tool_name")
.or_else(|| dict_get(result, "name"))
.map(|v| v.display())
.unwrap_or_default();
let arguments = dict_get(result, "arguments").map(vm_to_json)?;
Some(crate::llm::plan::normalize_plan_tool_call(
&tool_name, &arguments,
))
}
fn persisted_plan_document_events(
session_id: &str,
) -> Result<Vec<crate::llm::plan::PlanDocumentEvent>, crate::llm::plan::PlanDocumentError> {
let Some(transcript) = crate::agent_sessions::transcript(session_id) else {
return Ok(Vec::new());
};
vm_to_json(&transcript)
.get("events")
.and_then(serde_json::Value::as_array)
.into_iter()
.flatten()
.filter_map(|event| event.pointer("/metadata/plan_document_event"))
.map(|event| {
serde_json::from_value(event.clone()).map_err(|error| {
crate::llm::plan::PlanDocumentError::Invalid(format!(
"invalid persisted plan document event: {error}"
))
})
})
.collect()
}
pub(super) fn next_plan_document_event(
session_id: &str,
tool_name: &str,
result: &VmValue,
plan_value: serde_json::Value,
created_at: String,
event_id: String,
) -> Result<crate::llm::plan::PlanDocumentEvent, crate::llm::plan::PlanDocumentError> {
let events = persisted_plan_document_events(session_id)?;
if tool_name != crate::llm::plan::UPDATE_PLAN_TOOL || events.is_empty() {
return crate::llm::plan::create_plan_document_event(
plan_value, session_id, tool_name, created_at, event_id,
);
}
let mut store = if matches!(
events.first(),
Some(crate::llm::plan::PlanDocumentEvent::Created { .. })
) {
crate::llm::plan::PlanDocumentStore::replay(&events)?
} else {
crate::llm::plan::PlanDocumentStore::resume(
events
.last()
.expect("non-empty plan document event list")
.document()
.clone(),
)?
};
let arguments = dict_get(result, "arguments")
.map(vm_to_json)
.unwrap_or_else(|| serde_json::json!({}));
if let Some(document_id) = arguments
.get("document_id")
.and_then(serde_json::Value::as_str)
{
if document_id != store.current().document_id {
return Err(crate::llm::plan::PlanDocumentError::Invalid(format!(
"update_plan document_id {document_id} does not match current document {}",
store.current().document_id
)));
}
}
let expected_revision_id = arguments
.get("expected_revision_id")
.and_then(serde_json::Value::as_str)
.unwrap_or(&store.current().current_revision.revision_id)
.to_string();
let plan: crate::llm::plan::PlanArtifact =
serde_json::from_value(plan_value).map_err(|error| {
crate::llm::plan::PlanDocumentError::Invalid(format!(
"invalid executable plan: {error}"
))
})?;
let markdown = arguments
.get("markdown")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned)
.unwrap_or_else(|| {
crate::llm::plan::render_plan(
&serde_json::to_value(&plan).unwrap_or_else(|_| serde_json::json!({})),
)
});
store.edit(crate::llm::plan::EditPlanDocument {
expected_revision_id,
markdown,
plan,
author: crate::llm::plan::PlanAuthor {
id: session_id.to_string(),
display_name: None,
},
source: crate::llm::plan::PlanSource {
kind: tool_name.to_string(),
uri: None,
},
created_at,
event_id,
})?;
Ok(store
.events()
.last()
.expect("edit appends a plan document event")
.clone())
}