1use indexmap::IndexMap;
7
8use super::ids::TaskId;
9use crate::ast::artifact::ArtifactSpec;
10use crate::ast::decompose::DecomposeSpec;
11use crate::ast::logging::LogConfig;
12use crate::ast::structured::StructuredOutputSpec;
13use crate::binding::WithSpec;
14use crate::source::Span;
15
16#[derive(Debug, Clone)]
21pub struct AnalyzedTask {
22 pub id: TaskId,
24
25 pub name: String,
27
28 pub description: Option<String>,
30
31 pub action: AnalyzedTaskAction,
33
34 pub provider: Option<String>,
36
37 pub model: Option<String>,
39
40 pub with_spec: WithSpec,
45
46 pub depends_on: Vec<TaskId>,
51
52 pub implicit_deps: Vec<TaskId>,
58
59 pub output: Option<AnalyzedOutput>,
61
62 pub for_each: Option<AnalyzedForEach>,
64
65 pub retry: Option<AnalyzedRetry>,
67
68 pub decompose: Option<DecomposeSpec>,
70
71 pub concurrency: Option<u32>,
73
74 pub fail_fast: Option<bool>,
76
77 pub artifact: Option<ArtifactSpec>,
79
80 pub log: Option<LogConfig>,
82
83 pub structured: Option<StructuredOutputSpec>,
85
86 pub span: Span,
88}
89
90#[derive(Debug, Clone)]
92pub enum AnalyzedTaskAction {
93 Infer(AnalyzedInferAction),
95
96 Exec(AnalyzedExecAction),
98
99 Fetch(AnalyzedFetchAction),
101
102 Invoke(AnalyzedInvokeAction),
104
105 Agent(Box<AnalyzedAgentAction>),
107}
108
109impl Default for AnalyzedTaskAction {
110 fn default() -> Self {
111 AnalyzedTaskAction::Infer(AnalyzedInferAction::default())
112 }
113}
114
115impl AnalyzedTaskAction {
116 pub fn verb_name(&self) -> &'static str {
118 match self {
119 AnalyzedTaskAction::Infer(_) => "infer",
120 AnalyzedTaskAction::Exec(_) => "exec",
121 AnalyzedTaskAction::Fetch(_) => "fetch",
122 AnalyzedTaskAction::Invoke(_) => "invoke",
123 AnalyzedTaskAction::Agent(_) => "agent",
124 }
125 }
126}
127
128#[derive(Debug, Clone, Default)]
130pub struct AnalyzedInferAction {
131 pub prompt: String,
133
134 pub system: Option<String>,
136
137 pub temperature: Option<f64>,
139
140 pub max_tokens: Option<u32>,
142
143 pub extended_thinking: Option<bool>,
145
146 pub thinking_budget: Option<u32>,
148
149 pub content: Option<Vec<crate::ast::content::AnalyzedContentPart>>,
151
152 pub response_format: Option<String>,
154
155 pub guardrails: Vec<crate::ast::guardrails::GuardrailConfig>,
157
158 pub span: Span,
160}
161
162#[derive(Debug, Clone, Default)]
164pub struct AnalyzedExecAction {
165 pub command: String,
167
168 pub shell: bool,
170
171 pub cwd: Option<String>,
173
174 pub env: IndexMap<String, String>,
176
177 pub timeout_ms: Option<u64>,
179
180 pub span: Span,
182}
183
184#[derive(Debug, Clone, Default)]
186pub struct AnalyzedFetchAction {
187 pub url: String,
189
190 pub method: HttpMethod,
192
193 pub headers: IndexMap<String, String>,
195
196 pub body: Option<String>,
198
199 pub json: Option<serde_json::Value>,
201
202 pub timeout_ms: Option<u64>,
204
205 pub follow_redirects: bool,
207
208 pub response: Option<String>,
210
211 pub extract: Option<String>,
213
214 pub selector: Option<String>,
216
217 pub span: Span,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
223pub enum HttpMethod {
224 #[default]
225 Get,
226 Post,
227 Put,
228 Patch,
229 Delete,
230 Head,
231 Options,
232}
233
234impl HttpMethod {
235 pub fn parse(s: &str) -> Option<Self> {
237 match s.to_uppercase().as_str() {
238 "GET" => Some(Self::Get),
239 "POST" => Some(Self::Post),
240 "PUT" => Some(Self::Put),
241 "PATCH" => Some(Self::Patch),
242 "DELETE" => Some(Self::Delete),
243 "HEAD" => Some(Self::Head),
244 "OPTIONS" => Some(Self::Options),
245 _ => None,
246 }
247 }
248
249 pub fn as_str(&self) -> &'static str {
251 match self {
252 Self::Get => "GET",
253 Self::Post => "POST",
254 Self::Put => "PUT",
255 Self::Patch => "PATCH",
256 Self::Delete => "DELETE",
257 Self::Head => "HEAD",
258 Self::Options => "OPTIONS",
259 }
260 }
261}
262
263#[derive(Debug, Clone, Default)]
265pub struct AnalyzedInvokeAction {
266 pub server: Option<String>,
268
269 pub tool: String,
271
272 pub resource: Option<String>,
274
275 pub params: Option<serde_json::Value>,
277
278 pub timeout_ms: Option<u64>,
280
281 pub span: Span,
283}
284
285#[derive(Debug, Clone, Default)]
287pub struct AnalyzedAgentAction {
288 pub prompt: String,
290
291 pub tools: Vec<String>,
293
294 pub max_turns: Option<u32>,
296
297 pub max_tokens: Option<u32>,
299
300 pub from: Option<String>,
302
303 pub skills: Vec<String>,
305
306 pub mcp: Vec<String>,
308
309 pub system: Option<String>,
311
312 pub temperature: Option<f64>,
314
315 pub token_budget: Option<u32>,
317
318 pub extended_thinking: Option<bool>,
320
321 pub thinking_budget: Option<u32>,
323
324 pub depth_limit: Option<u32>,
326
327 pub tool_choice: Option<String>,
329
330 pub stop_sequences: Vec<String>,
332
333 pub scope: Option<String>,
335 pub guardrails: Vec<crate::ast::guardrails::GuardrailConfig>,
337 pub completion: Option<crate::ast::completion::CompletionConfig>,
339 pub limits: Option<crate::ast::limits::LimitsConfig>,
341
342 pub span: Span,
344}
345
346#[derive(Debug, Clone)]
348pub struct AnalyzedOutput {
349 pub format: OutputFormat,
351
352 pub schema: Option<serde_json::Value>,
354
355 pub schema_ref: Option<String>,
357
358 pub max_retries: Option<u32>,
360
361 pub span: Span,
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
367pub enum OutputFormat {
368 #[default]
369 Text,
370 Json,
371 Yaml,
372}
373
374impl OutputFormat {
375 pub fn parse(s: &str) -> Option<Self> {
377 match s.to_lowercase().as_str() {
378 "text" => Some(Self::Text),
379 "json" => Some(Self::Json),
380 "yaml" => Some(Self::Yaml),
381 _ => None,
382 }
383 }
384
385 pub fn as_str(&self) -> &'static str {
387 match self {
388 Self::Text => "text",
389 Self::Json => "json",
390 Self::Yaml => "yaml",
391 }
392 }
393}
394
395#[derive(Debug, Clone)]
397pub struct AnalyzedForEach {
398 pub items: String,
400
401 pub as_var: String,
403
404 pub concurrency: Option<u32>,
406
407 pub fail_fast: bool,
409
410 pub span: Span,
412}
413
414impl Default for AnalyzedForEach {
415 fn default() -> Self {
416 Self {
417 items: String::new(),
418 as_var: "item".to_string(),
419 concurrency: Some(1), fail_fast: true,
421 span: Span::dummy(),
422 }
423 }
424}
425
426impl AnalyzedForEach {
427 pub fn is_binding(&self) -> bool {
429 self.items.starts_with("{{") || self.items.starts_with("$")
430 }
431
432 pub fn is_array(&self) -> bool {
434 self.items.starts_with('[')
435 }
436
437 pub fn parse_items(&self) -> Option<Vec<serde_json::Value>> {
439 if self.is_array() {
440 serde_json::from_str(&self.items).ok()
441 } else {
442 None
443 }
444 }
445}
446
447#[derive(Debug, Clone)]
449pub struct AnalyzedRetry {
450 pub max_attempts: u32,
452
453 pub delay_ms: u64,
455
456 pub backoff: Option<f64>,
458
459 pub span: Span,
461}
462
463impl Default for AnalyzedRetry {
464 fn default() -> Self {
465 Self {
466 max_attempts: 3,
467 delay_ms: 1000,
468 backoff: None,
469 span: Span::dummy(),
470 }
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn test_http_method_parse() {
480 assert_eq!(HttpMethod::parse("GET"), Some(HttpMethod::Get));
481 assert_eq!(HttpMethod::parse("get"), Some(HttpMethod::Get));
482 assert_eq!(HttpMethod::parse("POST"), Some(HttpMethod::Post));
483 assert_eq!(HttpMethod::parse("UNKNOWN"), None);
484 }
485
486 #[test]
487 fn test_output_format_parse() {
488 assert_eq!(OutputFormat::parse("text"), Some(OutputFormat::Text));
489 assert_eq!(OutputFormat::parse("JSON"), Some(OutputFormat::Json));
490 assert_eq!(OutputFormat::parse("yaml"), Some(OutputFormat::Yaml));
491 assert_eq!(OutputFormat::parse("unknown"), None);
492 }
493
494 #[test]
495 fn test_analyzed_task_action_verb() {
496 let infer = AnalyzedTaskAction::Infer(AnalyzedInferAction::default());
497 assert_eq!(infer.verb_name(), "infer");
498
499 let exec = AnalyzedTaskAction::Exec(AnalyzedExecAction::default());
500 assert_eq!(exec.verb_name(), "exec");
501 }
502
503 #[test]
504 fn test_analyzed_task_with_spec() {
505 use crate::binding::types::{BindingPath, BindingSource, PathSegment};
506 use crate::binding::{WithEntry, WithSpec};
507
508 let mut with_spec = WithSpec::default();
509 with_spec.insert(
510 "data".to_string(),
511 WithEntry::simple(BindingPath {
512 source: BindingSource::Task("step1".into()),
513 segments: vec![PathSegment::Field("result".into())],
514 }),
515 );
516
517 assert_eq!(with_spec.len(), 1);
518 let entry = with_spec.get("data").unwrap();
519 assert_eq!(entry.task_id(), Some("step1"));
520 }
521
522 #[test]
523 fn test_analyzed_for_each_default() {
524 let for_each = AnalyzedForEach::default();
525 assert_eq!(for_each.as_var, "item");
526 assert_eq!(for_each.concurrency, Some(1)); assert!(for_each.fail_fast);
528 }
529
530 #[test]
531 fn test_analyzed_for_each_is_binding() {
532 let for_each = AnalyzedForEach {
533 items: "{{with.items}}".to_string(),
534 ..Default::default()
535 };
536 assert!(for_each.is_binding());
537
538 let for_each = AnalyzedForEach {
539 items: "$items".to_string(),
540 ..Default::default()
541 };
542 assert!(for_each.is_binding());
543
544 let for_each = AnalyzedForEach {
545 items: r#"["a", "b", "c"]"#.to_string(),
546 ..Default::default()
547 };
548 assert!(!for_each.is_binding());
549 }
550
551 #[test]
552 fn test_analyzed_for_each_is_array() {
553 let for_each = AnalyzedForEach {
554 items: r#"["a", "b", "c"]"#.to_string(),
555 ..Default::default()
556 };
557 assert!(for_each.is_array());
558
559 let for_each = AnalyzedForEach {
560 items: "{{with.items}}".to_string(),
561 ..Default::default()
562 };
563 assert!(!for_each.is_array());
564 }
565
566 #[test]
567 fn test_analyzed_for_each_parse_items() {
568 let for_each = AnalyzedForEach {
569 items: r#"["a", "b", "c"]"#.to_string(),
570 ..Default::default()
571 };
572 let items = for_each.parse_items().unwrap();
573 assert_eq!(items.len(), 3);
574 assert_eq!(items[0], serde_json::Value::String("a".to_string()));
575
576 let for_each = AnalyzedForEach {
577 items: "{{with.items}}".to_string(),
578 ..Default::default()
579 };
580 assert!(for_each.parse_items().is_none());
581 }
582
583 #[test]
584 fn test_analyzed_retry_default() {
585 let retry = AnalyzedRetry::default();
586 assert_eq!(retry.max_attempts, 3);
587 assert_eq!(retry.delay_ms, 1000);
588 assert!(retry.backoff.is_none());
589 }
590}