Skip to main content

nika_core/ast/raw/
action.rs

1//! Raw task action variants (verbs).
2//!
3//! Each verb (infer, exec, fetch, invoke, agent) has its own params struct
4//! with full span tracking for error reporting.
5
6use indexmap::IndexMap;
7
8use crate::source::{Span, Spanned};
9
10/// The action a task performs - one of the 5 verbs.
11#[derive(Debug, Clone)]
12pub enum RawTaskAction {
13    /// LLM inference: infer: { prompt: "..." }
14    Infer(Spanned<RawInferAction>),
15
16    /// Shell command: exec: { command: "..." }
17    Exec(Spanned<RawExecAction>),
18
19    /// HTTP fetch: fetch: { url: "..." }
20    Fetch(Spanned<RawFetchAction>),
21
22    /// MCP tool invocation: invoke: tool_name or invoke: { tool: name, params: {...} }
23    Invoke(Spanned<RawInvokeAction>),
24
25    /// Autonomous agent: agent: { prompt: "..." }
26    Agent(Box<Spanned<RawAgentAction>>),
27}
28
29impl Default for RawTaskAction {
30    fn default() -> Self {
31        RawTaskAction::Infer(Spanned::dummy(RawInferAction::default()))
32    }
33}
34
35impl RawTaskAction {
36    /// Get the verb name as a string.
37    pub fn verb_name(&self) -> &'static str {
38        match self {
39            RawTaskAction::Infer(_) => "infer",
40            RawTaskAction::Exec(_) => "exec",
41            RawTaskAction::Fetch(_) => "fetch",
42            RawTaskAction::Invoke(_) => "invoke",
43            RawTaskAction::Agent(_) => "agent",
44        }
45    }
46
47    /// Get the span of the action.
48    pub fn span(&self) -> Span {
49        match self {
50            RawTaskAction::Infer(a) => a.span,
51            RawTaskAction::Exec(a) => a.span,
52            RawTaskAction::Fetch(a) => a.span,
53            RawTaskAction::Invoke(a) => a.span,
54            RawTaskAction::Agent(a) => a.span,
55        }
56    }
57}
58
59/// Parameters for the `infer` verb (LLM inference).
60#[derive(Debug, Clone, Default)]
61pub struct RawInferAction {
62    /// The prompt to send to the LLM (optional when `content` is present)
63    pub prompt: Spanned<String>,
64
65    /// System prompt override
66    pub system: Option<Spanned<String>>,
67
68    /// Temperature (0.0 - 2.0)
69    pub temperature: Option<Spanned<f64>>,
70
71    /// Maximum tokens to generate
72    pub max_tokens: Option<Spanned<u32>>,
73
74    /// Enable extended thinking (Claude)
75    pub extended_thinking: Option<Spanned<bool>>,
76
77    /// Thinking budget tokens
78    pub thinking_budget: Option<Spanned<u32>>,
79
80    /// Multimodal content parts (text + images) for vision models
81    pub content: Option<Spanned<Vec<crate::ast::content::RawContentPart>>>,
82
83    /// Expected response format: text, json, markdown
84    pub response_format: Option<Spanned<String>>,
85
86    /// Guardrails for validating infer output
87    pub guardrails: Vec<crate::ast::guardrails::GuardrailConfig>,
88}
89
90/// Parameters for the `exec` verb (shell command execution).
91#[derive(Debug, Clone, Default)]
92pub struct RawExecAction {
93    /// Command to execute (string or array)
94    pub command: Spanned<String>,
95
96    /// Run through shell (sh -c) - defaults to false for security
97    pub shell: Option<Spanned<bool>>,
98
99    /// Working directory
100    pub cwd: Option<Spanned<String>>,
101
102    /// Environment variables
103    pub env: Option<Spanned<IndexMap<Spanned<String>, Spanned<String>>>>,
104
105    /// Timeout in milliseconds
106    pub timeout_ms: Option<Spanned<u64>>,
107}
108
109/// Parameters for the `fetch` verb (HTTP requests).
110#[derive(Debug, Clone, Default)]
111pub struct RawFetchAction {
112    /// URL to fetch
113    pub url: Spanned<String>,
114
115    /// HTTP method (GET, POST, PUT, DELETE, etc.)
116    pub method: Option<Spanned<String>>,
117
118    /// HTTP headers
119    pub headers: Option<Spanned<IndexMap<Spanned<String>, Spanned<String>>>>,
120
121    /// Request body (for POST/PUT)
122    pub body: Option<Spanned<String>>,
123
124    /// Request body as JSON
125    pub json: Option<Spanned<serde_json::Value>>,
126
127    /// Timeout in milliseconds
128    pub timeout_ms: Option<Spanned<u64>>,
129
130    /// Follow redirects
131    pub follow_redirects: Option<Spanned<bool>>,
132
133    /// Response mode: "full" (status + headers + body) or "binary" (CAS store)
134    pub response: Option<Spanned<String>>,
135
136    /// Extraction mode: markdown, article, text, selector, metadata, links, feed, jsonpath, llm_txt
137    pub extract: Option<Spanned<String>>,
138
139    /// CSS selector or JSONPath expression (used with extract: selector, text, jsonpath)
140    pub selector: Option<Spanned<String>>,
141}
142
143/// Parameters for the `invoke` verb (MCP tool invocation).
144#[derive(Debug, Clone, Default)]
145pub struct RawInvokeAction {
146    /// MCP tool name: "tool_name" or "server::tool_name"
147    /// Required unless `resource` is set.
148    pub tool: Option<Spanned<String>>,
149
150    /// MCP resource URI (alternative to tool call)
151    pub resource: Option<Spanned<String>>,
152
153    /// Tool parameters (validated against MCP schema)
154    pub params: Option<Spanned<serde_json::Value>>,
155
156    /// Optional MCP server to use (if not in tool name)
157    pub mcp: Option<Spanned<String>>,
158
159    /// Timeout for tool execution
160    pub timeout_ms: Option<Spanned<u64>>,
161}
162
163impl RawInvokeAction {
164    /// Parse server and tool name from the tool field.
165    /// Returns (server, tool_name) where server may be None.
166    /// Returns None if no tool is specified (resource-only invoke).
167    pub fn parse_tool_name(&self) -> Option<(Option<&str>, &str)> {
168        let tool = self.tool.as_ref()?;
169        let tool_str = &tool.value;
170        if let Some((server, name)) = tool_str.split_once("::") {
171            Some((Some(server), name))
172        } else {
173            Some((None, tool_str.as_str()))
174        }
175    }
176}
177
178/// Parameters for the `agent` verb (autonomous agent execution).
179#[derive(Debug, Clone, Default)]
180pub struct RawAgentAction {
181    /// The prompt for the agent to execute
182    pub prompt: Spanned<String>,
183
184    /// Available tools for the agent
185    pub tools: Option<Spanned<Vec<Spanned<String>>>>,
186
187    /// Maximum turns before stopping
188    pub max_turns: Option<Spanned<u32>>,
189
190    /// Maximum tokens per response
191    pub max_tokens: Option<Spanned<u32>>,
192
193    /// Agent definition reference (agents: section)
194    pub from: Option<Spanned<String>>,
195
196    /// Skills to inject into system prompt
197    pub skills: Option<Spanned<Vec<Spanned<String>>>>,
198
199    /// Provider override (inside agent: block)
200    pub provider: Option<Spanned<String>>,
201
202    /// Model override (inside agent: block)
203    pub model: Option<Spanned<String>>,
204
205    /// MCP servers for tool access
206    pub mcp: Option<Spanned<Vec<Spanned<String>>>>,
207
208    /// Temperature for LLM sampling
209    pub temperature: Option<Spanned<f64>>,
210
211    /// Token budget for the agent
212    pub token_budget: Option<Spanned<u32>>,
213
214    /// System prompt (agent persona)
215    pub system: Option<Spanned<String>>,
216
217    /// Enable extended thinking (Claude)
218    pub extended_thinking: Option<Spanned<bool>>,
219
220    /// Thinking budget tokens
221    pub thinking_budget: Option<Spanned<u32>>,
222
223    /// Max spawn_agent recursion depth
224    pub depth_limit: Option<Spanned<u32>>,
225
226    /// Tool choice behavior: auto, required, none
227    pub tool_choice: Option<Spanned<String>>,
228
229    /// Sequences that stop generation (passed to LLM)
230    pub stop_sequences: Option<Spanned<Vec<Spanned<String>>>>,
231
232    /// Scope preset (full, minimal, debug)
233    pub scope: Option<Spanned<String>>,
234    /// Guardrails for validating agent outputs.
235    pub guardrails: Vec<crate::ast::guardrails::GuardrailConfig>,
236    /// Completion behavior configuration.
237    pub completion: Option<crate::ast::completion::CompletionConfig>,
238    /// Execution limits for cost control.
239    pub limits: Option<crate::ast::limits::LimitsConfig>,
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::source::FileId;
246
247    fn make_span(start: u32, end: u32) -> Span {
248        Span::new(FileId(0), start, end)
249    }
250
251    #[test]
252    fn test_action_verb_names() {
253        let infer = RawTaskAction::Infer(Spanned::dummy(RawInferAction::default()));
254        assert_eq!(infer.verb_name(), "infer");
255
256        let exec = RawTaskAction::Exec(Spanned::dummy(RawExecAction::default()));
257        assert_eq!(exec.verb_name(), "exec");
258
259        let fetch = RawTaskAction::Fetch(Spanned::dummy(RawFetchAction::default()));
260        assert_eq!(fetch.verb_name(), "fetch");
261
262        let invoke = RawTaskAction::Invoke(Spanned::dummy(RawInvokeAction::default()));
263        assert_eq!(invoke.verb_name(), "invoke");
264
265        let agent = RawTaskAction::Agent(Box::new(Spanned::dummy(RawAgentAction::default())));
266        assert_eq!(agent.verb_name(), "agent");
267    }
268
269    #[test]
270    fn test_invoke_parse_tool_name() {
271        // Simple tool name
272        let simple = RawInvokeAction {
273            tool: Some(Spanned::new("my_tool".to_string(), make_span(0, 7))),
274            ..Default::default()
275        };
276        let (server, name) = simple.parse_tool_name().unwrap();
277        assert_eq!(server, None);
278        assert_eq!(name, "my_tool");
279
280        // Server-qualified name
281        let qualified = RawInvokeAction {
282            tool: Some(Spanned::new("novanet::query".to_string(), make_span(0, 14))),
283            ..Default::default()
284        };
285        let (server, name) = qualified.parse_tool_name().unwrap();
286        assert_eq!(server, Some("novanet"));
287        assert_eq!(name, "query");
288    }
289
290    #[test]
291    fn test_infer_action_fields() {
292        let infer = RawInferAction {
293            prompt: Spanned::new("Hello, world!".to_string(), make_span(0, 13)),
294            temperature: Some(Spanned::new(0.7, make_span(20, 23))),
295            max_tokens: Some(Spanned::new(1000, make_span(30, 34))),
296            ..Default::default()
297        };
298
299        assert_eq!(infer.prompt.value, "Hello, world!");
300        assert_eq!(infer.temperature.as_ref().unwrap().value, 0.7);
301        assert_eq!(infer.max_tokens.as_ref().unwrap().value, 1000);
302    }
303}