fn last_recorded_source_state_for_current(
workspace_root: &Path,
task_id: &TaskId,
current_state: &str,
machine: &rhei_validator::StateMachine,
) -> MietteResult<Option<String>> {
let path = workspace_root.join("runtime").join("state-transitions.log");
if !path.exists() {
return Ok(None);
}
let content = fs::read_to_string(&path)
.map_err(|err| file_io_report(&path, "failed to read task transition history", err))?;
let task_id_str = task_id.to_string();
let mut found = None;
for line in content.lines() {
let Some((entry_task, transition)) = line.trim().split_once(' ') else {
continue;
};
if entry_task != task_id_str {
continue;
}
let Some((from, to)) = transition.split_once('@') else {
continue;
};
let from = normalized_state_name(from.trim(), machine);
let to = normalized_state_name(to.trim(), machine);
if machine.is_valid_state(&from) && to == current_state {
found = Some(from);
}
}
Ok(found)
}
fn resolve_source_handoff_path(
render_context: &RuntimeTemplateContext<'_>,
artifact: &rhei_validator::StateArtifactDef,
source_state: &str,
visit_count: Option<u64>,
identity: &TransitionInvocationContext<'_>,
) -> (String, PathBuf) {
let (target, model, model_provider, model_name, agent, agent_mode) = *identity;
resolve_artifact_path(
render_context.workspace_root,
artifact,
&render_context.task.id.to_string(),
source_state,
visit_count,
target,
model,
model_provider,
model_name,
agent,
agent_mode,
)
}
fn source_handoff_candidate_paths(
render_context: &RuntimeTemplateContext<'_>,
source_def: &rhei_validator::StateDef,
artifact: &rhei_validator::StateArtifactDef,
source_state: &str,
visit_count: Option<u64>,
) -> Vec<String> {
transition_contexts_for_state(source_def, &[])
.iter()
.map(|identity| {
let (_, path) = resolve_source_handoff_path(
render_context,
artifact,
source_state,
visit_count,
identity,
);
format!(" {}", path.display())
})
.collect()
}
fn read_source_state_handoff(
render_context: &RuntimeTemplateContext<'_>,
source_def: &rhei_validator::StateDef,
artifact: &rhei_validator::StateArtifactDef,
source_state: &str,
visit_count: Option<u64>,
) -> MietteResult<Option<String>> {
for identity in transition_contexts_for_state(source_def, &[]) {
let (_, path) = resolve_source_handoff_path(
render_context,
artifact,
source_state,
visit_count,
&identity,
);
if !path.exists() {
continue;
}
let content = fs::read_to_string(&path)
.map_err(|err| file_io_report(&path, "failed to read state handoff", err))?;
if content.trim().is_empty() {
continue;
}
return Ok(Some(content.trim().to_string()));
}
Ok(None)
}
fn resolve_state_handoff_sections(
render_context: &RuntimeTemplateContext<'_>,
) -> MietteResult<Vec<PromptHandoffSection>> {
let Some(state_def) = render_context.machine.states.get(render_context.state_name) else {
return Ok(Vec::new());
};
let Some(handoff) = state_def.handoff.as_ref() else {
return Ok(Vec::new());
};
let mut sections = Vec::new();
for inherit in &handoff.inherit {
if inherit.from_axis != "transition.previous" {
continue;
}
let Some(source_state) = last_recorded_source_state_for_current(
render_context.workspace_root,
&render_context.task.id,
render_context.state_name,
render_context.machine,
)?
else {
if inherit.required {
return Err(miette!(
help = handoff_missing_source_help(),
"state '{}' requires a handoff from the previous transition, but no transition into this state was recorded for task {}",
render_context.state_name,
render_context.task.id
));
}
continue;
};
let Some(source_def) = render_context.machine.states.get(&source_state) else {
if inherit.required {
return Err(miette!(
help = handoff_missing_source_help(),
"state '{}' requires a handoff from previous state '{}', but that state is not in the machine",
render_context.state_name,
source_state
));
}
continue;
};
let mut artifacts = source_def
.outputs
.iter()
.filter(|artifact| artifact.kind.as_deref() == Some("handoff"))
.filter(|artifact| inherit.name.as_ref().is_none_or(|name| &artifact.name == name))
.collect::<Vec<_>>();
if artifacts.is_empty() {
if inherit.required {
return Err(miette!(
help = handoff_no_output_help(),
"state '{}' requires a handoff from previous state '{}', but no matching handoff output was declared",
render_context.state_name,
source_state
));
}
continue;
}
if artifacts.len() > 1 && inherit.merge.as_deref() != Some("all") {
return Err(miette!(
help = handoff_ambiguous_help(),
"state '{}' handoff from previous state '{}' is ambiguous; select a name or set merge: all",
render_context.state_name,
source_state
));
}
for artifact in artifacts.drain(..) {
let source_visit_count = Some(render_visit_count(
render_context.metadata,
&render_context.task.id,
&source_state,
&source_state,
render_context.machine,
));
let Some(content) = read_source_state_handoff(
render_context,
source_def,
artifact,
&source_state,
source_visit_count,
)?
else {
if inherit.required {
return Err(miette!(
help = handoff_empty_artifact_help(),
"state '{}' requires handoff '{}' from previous state '{}', but no \
handoff artifact with content was found. Looked for:\n{}",
render_context.state_name,
artifact.name,
source_state,
source_handoff_candidate_paths(
render_context,
source_def,
artifact,
&source_state,
source_visit_count,
)
.join("\n")
));
}
continue;
};
sections.push(PromptHandoffSection { source_state: source_state.clone(), content });
}
}
Ok(sections)
}