use aion_core::{Event, RunId, WorkflowId, WorkflowSummary};
use aion_mcp::tools::service::{ToolCall, ToolFailure};
use aion_proto::{ProtoDescribeWorkflowRequest, ProtoWorkflowId, WireError};
use serde_json::json;
use crate::mcp::args::{optional_run_id, required_str, required_workflow_id};
use crate::mcp::run_scope::RunScope;
use crate::{CallerIdentity, ServerState, api::handlers};
use super::errors::tool_failure;
pub(crate) struct RunView {
pub(crate) namespace: String,
pub(crate) workflow_id: WorkflowId,
pub(crate) run_id: RunId,
pub(crate) history: Vec<Event>,
pub(crate) summary: WorkflowSummary,
pub(crate) scope: RunScope,
}
impl RunView {
pub(crate) async fn read(
state: &ServerState,
caller: &CallerIdentity,
call: &ToolCall,
) -> Result<Self, ToolFailure> {
let namespace = required_str(call, "namespace")?;
let workflow_id = required_workflow_id(call, "workflow_id")?;
let requested_run = optional_run_id(call, "run_id")?;
Self::read_parts(
state,
caller,
&namespace,
&workflow_id,
requested_run.as_ref(),
)
.await
}
pub(crate) async fn read_parts(
state: &ServerState,
caller: &CallerIdentity,
namespace: &str,
workflow_id: &WorkflowId,
requested_run: Option<&RunId>,
) -> Result<Self, ToolFailure> {
let request = ProtoDescribeWorkflowRequest {
namespace: namespace.to_owned(),
workflow_id: Some(ProtoWorkflowId {
uuid: workflow_id.to_string(),
}),
run_id: requested_run.map(|run| run.clone().into()),
include_history: false,
};
let outcome = handlers::describe(state.namespace_guard(), caller, request)
.await
.map_err(|error| tool_failure(&error))?;
let summary = WorkflowSummary::from_history(&outcome.history).ok_or_else(|| {
tool_failure(&WireError::not_found(format!(
"workflow {workflow_id} has no recorded start event"
)))
})?;
let scope = RunScope::from_history(&outcome.history);
let run_id = match requested_run {
Some(run) => {
if !scope.knows_run(run) {
return Err(ToolFailure::new(
format!(
"run {run} is not a run of workflow {workflow_id}. Call describe_run \
without a run_id to get this workflow's current run, and never \
construct a run id."
),
json!({
"code": "unknown_run",
"workflow_id": workflow_id.to_string(),
"run_id": run.to_string(),
}),
));
}
run.clone()
}
None => latest_run(&outcome.history).ok_or_else(|| {
tool_failure(&WireError::not_found(format!(
"workflow {workflow_id} has no recorded run"
)))
})?,
};
Ok(Self {
namespace: outcome.namespace,
workflow_id: outcome.workflow_id,
run_id,
history: outcome.history,
summary,
scope,
})
}
pub(crate) fn history_head_seq(&self) -> u64 {
self.history.last().map_or(0, Event::seq)
}
pub(crate) fn run_segment(&self) -> &[Event] {
aion_core::run_segment(&self.history, &self.run_id)
}
}
fn latest_run(history: &[Event]) -> Option<RunId> {
history.iter().rev().find_map(|event| match event {
Event::WorkflowStarted { run_id, .. } => Some(run_id.clone()),
_ => None,
})
}