use indexmap::IndexMap;
use super::ids::TaskId;
use crate::ast::artifact::ArtifactSpec;
use crate::ast::decompose::DecomposeSpec;
use crate::ast::logging::LogConfig;
use crate::ast::structured::StructuredOutputSpec;
use crate::binding::WithSpec;
use crate::source::Span;
#[derive(Debug, Clone)]
pub struct AnalyzedTask {
pub id: TaskId,
pub name: String,
pub description: Option<String>,
pub action: AnalyzedTaskAction,
pub provider: Option<String>,
pub model: Option<String>,
pub with_spec: WithSpec,
pub depends_on: Vec<TaskId>,
pub implicit_deps: Vec<TaskId>,
pub output: Option<AnalyzedOutput>,
pub for_each: Option<AnalyzedForEach>,
pub retry: Option<AnalyzedRetry>,
pub decompose: Option<DecomposeSpec>,
pub concurrency: Option<u32>,
pub fail_fast: Option<bool>,
pub artifact: Option<ArtifactSpec>,
pub log: Option<LogConfig>,
pub structured: Option<StructuredOutputSpec>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub enum AnalyzedTaskAction {
Infer(AnalyzedInferAction),
Exec(AnalyzedExecAction),
Fetch(AnalyzedFetchAction),
Invoke(AnalyzedInvokeAction),
Agent(AnalyzedAgentAction),
}
impl Default for AnalyzedTaskAction {
fn default() -> Self {
AnalyzedTaskAction::Infer(AnalyzedInferAction::default())
}
}
impl AnalyzedTaskAction {
pub fn verb_name(&self) -> &'static str {
match self {
AnalyzedTaskAction::Infer(_) => "infer",
AnalyzedTaskAction::Exec(_) => "exec",
AnalyzedTaskAction::Fetch(_) => "fetch",
AnalyzedTaskAction::Invoke(_) => "invoke",
AnalyzedTaskAction::Agent(_) => "agent",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AnalyzedInferAction {
pub prompt: String,
pub system: Option<String>,
pub temperature: Option<f64>,
pub max_tokens: Option<u32>,
pub thinking: Option<bool>,
pub thinking_budget: Option<u32>,
pub content: Option<Vec<crate::ast::content::AnalyzedContentPart>>,
pub response_format: Option<String>,
pub guardrails: Vec<crate::ast::guardrails::GuardrailConfig>,
pub span: Span,
}
#[derive(Debug, Clone, Default)]
pub struct AnalyzedExecAction {
pub command: String,
pub shell: bool,
pub working_dir: Option<String>,
pub env: IndexMap<String, String>,
pub timeout_ms: Option<u64>,
pub span: Span,
}
#[derive(Debug, Clone, Default)]
pub struct AnalyzedFetchAction {
pub url: String,
pub method: HttpMethod,
pub headers: IndexMap<String, String>,
pub body: Option<String>,
pub json: Option<serde_json::Value>,
pub timeout_ms: Option<u64>,
pub follow_redirects: bool,
pub response: Option<String>,
pub extract: Option<String>,
pub selector: Option<String>,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HttpMethod {
#[default]
Get,
Post,
Put,
Patch,
Delete,
Head,
Options,
}
impl HttpMethod {
pub fn parse(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"GET" => Some(Self::Get),
"POST" => Some(Self::Post),
"PUT" => Some(Self::Put),
"PATCH" => Some(Self::Patch),
"DELETE" => Some(Self::Delete),
"HEAD" => Some(Self::Head),
"OPTIONS" => Some(Self::Options),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Get => "GET",
Self::Post => "POST",
Self::Put => "PUT",
Self::Patch => "PATCH",
Self::Delete => "DELETE",
Self::Head => "HEAD",
Self::Options => "OPTIONS",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AnalyzedInvokeAction {
pub server: Option<String>,
pub tool: String,
pub params: Option<serde_json::Value>,
pub timeout_ms: Option<u64>,
pub span: Span,
}
#[derive(Debug, Clone, Default)]
pub struct AnalyzedAgentAction {
pub prompt: String,
pub tools: Vec<String>,
pub max_iterations: Option<u32>,
pub max_tokens: Option<u32>,
pub from: Option<String>,
pub skills: Vec<String>,
pub mcp: Vec<String>,
pub system: Option<String>,
pub temperature: Option<f64>,
pub token_budget: Option<u32>,
pub extended_thinking: Option<bool>,
pub thinking_budget: Option<u32>,
pub depth_limit: Option<u32>,
pub tool_choice: Option<String>,
pub stop_sequences: Vec<String>,
pub scope: Option<String>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct AnalyzedOutput {
pub format: OutputFormat,
pub schema: Option<serde_json::Value>,
pub schema_ref: Option<String>,
pub max_retries: Option<u32>,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputFormat {
#[default]
Text,
Json,
Yaml,
}
impl OutputFormat {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"text" => Some(Self::Text),
"json" => Some(Self::Json),
"yaml" => Some(Self::Yaml),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Text => "text",
Self::Json => "json",
Self::Yaml => "yaml",
}
}
}
#[derive(Debug, Clone)]
pub struct AnalyzedForEach {
pub items: String,
pub as_var: String,
pub parallel: Option<u32>,
pub fail_fast: bool,
pub span: Span,
}
impl Default for AnalyzedForEach {
fn default() -> Self {
Self {
items: String::new(),
as_var: "item".to_string(),
parallel: Some(1), fail_fast: true,
span: Span::dummy(),
}
}
}
impl AnalyzedForEach {
pub fn is_binding(&self) -> bool {
self.items.starts_with("{{") || self.items.starts_with("$")
}
pub fn is_array(&self) -> bool {
self.items.starts_with('[')
}
pub fn parse_items(&self) -> Option<Vec<serde_json::Value>> {
if self.is_array() {
serde_json::from_str(&self.items).ok()
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct AnalyzedRetry {
pub max_attempts: u32,
pub delay_ms: u64,
pub backoff: Option<f64>,
pub span: Span,
}
impl Default for AnalyzedRetry {
fn default() -> Self {
Self {
max_attempts: 3,
delay_ms: 1000,
backoff: None,
span: Span::dummy(),
}
}
}
impl AnalyzedRetry {
pub fn delay_for_attempt(&self, attempt: u32) -> u64 {
if attempt == 0 {
return 0; }
match self.backoff {
Some(multiplier) => {
let factor = multiplier.powi(attempt as i32 - 1);
(self.delay_ms as f64 * factor) as u64
}
None => self.delay_ms,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::source::FileId;
fn make_span(start: u32, end: u32) -> Span {
Span::new(FileId(0), start, end)
}
#[test]
fn test_http_method_parse() {
assert_eq!(HttpMethod::parse("GET"), Some(HttpMethod::Get));
assert_eq!(HttpMethod::parse("get"), Some(HttpMethod::Get));
assert_eq!(HttpMethod::parse("POST"), Some(HttpMethod::Post));
assert_eq!(HttpMethod::parse("UNKNOWN"), None);
}
#[test]
fn test_output_format_parse() {
assert_eq!(OutputFormat::parse("text"), Some(OutputFormat::Text));
assert_eq!(OutputFormat::parse("JSON"), Some(OutputFormat::Json));
assert_eq!(OutputFormat::parse("yaml"), Some(OutputFormat::Yaml));
assert_eq!(OutputFormat::parse("unknown"), None);
}
#[test]
fn test_analyzed_task_action_verb() {
let infer = AnalyzedTaskAction::Infer(AnalyzedInferAction::default());
assert_eq!(infer.verb_name(), "infer");
let exec = AnalyzedTaskAction::Exec(AnalyzedExecAction::default());
assert_eq!(exec.verb_name(), "exec");
}
#[test]
fn test_analyzed_task_with_spec() {
use crate::binding::types::{BindingPath, BindingSource, PathSegment};
use crate::binding::{WithEntry, WithSpec};
let mut with_spec = WithSpec::default();
with_spec.insert(
"data".to_string(),
WithEntry::simple(BindingPath {
source: BindingSource::Task("step1".into()),
segments: vec![PathSegment::Field("result".into())],
}),
);
assert_eq!(with_spec.len(), 1);
let entry = with_spec.get("data").unwrap();
assert_eq!(entry.task_id(), Some("step1"));
}
#[test]
fn test_analyzed_for_each_default() {
let for_each = AnalyzedForEach::default();
assert_eq!(for_each.as_var, "item");
assert_eq!(for_each.parallel, Some(1)); assert!(for_each.fail_fast);
}
#[test]
fn test_analyzed_for_each_is_binding() {
let for_each = AnalyzedForEach {
items: "{{with.items}}".to_string(),
..Default::default()
};
assert!(for_each.is_binding());
let for_each = AnalyzedForEach {
items: "$items".to_string(),
..Default::default()
};
assert!(for_each.is_binding());
let for_each = AnalyzedForEach {
items: r#"["a", "b", "c"]"#.to_string(),
..Default::default()
};
assert!(!for_each.is_binding());
}
#[test]
fn test_analyzed_for_each_is_array() {
let for_each = AnalyzedForEach {
items: r#"["a", "b", "c"]"#.to_string(),
..Default::default()
};
assert!(for_each.is_array());
let for_each = AnalyzedForEach {
items: "{{with.items}}".to_string(),
..Default::default()
};
assert!(!for_each.is_array());
}
#[test]
fn test_analyzed_for_each_parse_items() {
let for_each = AnalyzedForEach {
items: r#"["a", "b", "c"]"#.to_string(),
..Default::default()
};
let items = for_each.parse_items().unwrap();
assert_eq!(items.len(), 3);
assert_eq!(items[0], serde_json::Value::String("a".to_string()));
let for_each = AnalyzedForEach {
items: "{{with.items}}".to_string(),
..Default::default()
};
assert!(for_each.parse_items().is_none());
}
#[test]
fn test_analyzed_retry_default() {
let retry = AnalyzedRetry::default();
assert_eq!(retry.max_attempts, 3);
assert_eq!(retry.delay_ms, 1000);
assert!(retry.backoff.is_none());
}
#[test]
fn test_analyzed_retry_delay_for_attempt() {
let retry = AnalyzedRetry {
max_attempts: 3,
delay_ms: 1000,
backoff: None,
span: make_span(0, 10),
};
assert_eq!(retry.delay_for_attempt(0), 0); assert_eq!(retry.delay_for_attempt(1), 1000); assert_eq!(retry.delay_for_attempt(2), 1000);
let retry = AnalyzedRetry {
max_attempts: 5,
delay_ms: 1000,
backoff: Some(2.0),
span: make_span(0, 10),
};
assert_eq!(retry.delay_for_attempt(0), 0); assert_eq!(retry.delay_for_attempt(1), 1000); assert_eq!(retry.delay_for_attempt(2), 2000); assert_eq!(retry.delay_for_attempt(3), 4000); }
}