From f97a83f165b3d3f9216c2d108e87fe4edf6da84b Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 11 Sep 2026 23:08:41 -0600 Subject: [PATCH] feat(agents): allow passing agent variables to agent__spawn and agent__task_create - agent__spawn accepts an optional variables object (string->string); values overlay the parent-inherited agent variables per key, with inheritance preserved as the fallback layer - unknown variable names, non-string values, and non-object shapes return self-correcting status:error JSON listing the agent's declared variables - spawned children can no longer trigger the interactive variable prompt: a new interactive gate in Agent::init_agent_variables routes missing required variables to the existing bail path while leaving CLI/REPL prompting and --info skip semantics unchanged - agent__task_create persists variables on the task, agent__task_list echoes them, and task auto-dispatch threads them into the spawn - tool schemas and built-in spawn docs updated; unit tests extended in agents.rs and taskqueue.rs --- src/config/agent.rs | 3 +- src/config/prompts.rs | 4 +- src/config/request_context.rs | 2 + src/function/agents.rs | 280 ++++++++++++++++++++++++++++++++-- src/supervisor/mod.rs | 2 +- src/supervisor/taskqueue.rs | 83 +++++++--- 6 files changed, 339 insertions(+), 35 deletions(-) diff --git a/src/config/agent.rs b/src/config/agent.rs index 757100e..617c206 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -329,6 +329,7 @@ impl Agent { agent_variables: &[AgentVariable], pre_set_variables: Option<&AgentVariables>, no_interaction: bool, + interactive: bool, ) -> Result { let mut output = IndexMap::new(); if agent_variables.is_empty() { @@ -349,7 +350,7 @@ impl Agent { if no_interaction { continue; } - if *IS_STDOUT_TERMINAL { + if interactive && *IS_STDOUT_TERMINAL { if !printed { println!("⚙ Init agent variables..."); printed = true; diff --git a/src/config/prompts.rs b/src/config/prompts.rs index bc9818e..ce3c077 100644 --- a/src/config/prompts.rs +++ b/src/config/prompts.rs @@ -84,13 +84,13 @@ pub(in crate::config) const DEFAULT_SPAWN_INSTRUCTIONS: &str = indoc! {" | Tool | Purpose | |------|----------| - | `agent__spawn` | Spawn a subagent in the background. Returns an `id` immediately. | + | `agent__spawn` | Spawn a subagent in the background. Returns an `id` immediately. Optional `variables` (string map) sets the target agent's declared variables, overriding inherited values. | | `agent__check` | Non-blocking status probe: running or finished. Never returns/consumes the result — use `agent__collect`. | | `agent__collect` | Blocking wait: wait for an agent to finish, return its output. | | `agent__list_available` | List all agent types you can spawn (name + description). Use this to discover specialists before calling `agent__spawn`. | | `agent__list_running` | List all subagents YOU have spawned, with their status. | | `agent__cancel` | Cancel a running agent by ID. | - | `agent__task_create` | Create a task in the dependency-aware task queue. | + | `agent__task_create` | Create a task in the dependency-aware task queue. Optional `variables` (string map) is passed to the auto-dispatched agent. | | `agent__task_list` | List all tasks and their status/dependencies. | | `agent__task_complete` | Mark a task done; returns any newly unblocked tasks. Auto-dispatches agents for tasks with a designated agent. | | `agent__task_fail` | Mark a task as failed. Dependents remain blocked. | diff --git a/src/config/request_context.rs b/src/config/request_context.rs index 285db31..3b6f5d9 100644 --- a/src/config/request_context.rs +++ b/src/config/request_context.rs @@ -1191,6 +1191,7 @@ impl RequestContext { agent.defined_variables(), self.agent_variables.as_ref(), self.info_flag, + self.self_agent_id.is_none(), )?; agent.set_shared_variables(new_variables); } @@ -1213,6 +1214,7 @@ impl RequestContext { agent.defined_variables(), self.agent_variables.as_ref(), self.info_flag, + self.self_agent_id.is_none(), )?; agent.set_shared_variables(new_variables.clone()); new_variables diff --git a/src/function/agents.rs b/src/function/agents.rs index 071bcb2..b9d1cd9 100644 --- a/src/function/agents.rs +++ b/src/function/agents.rs @@ -2,8 +2,9 @@ use super::todo::TODO_FUNCTION_PREFIX; use super::{FunctionDeclaration, JsonSchema}; use crate::client::{Model, ModelType, call_chat_completions}; use crate::config::{ - Agent, AppState, Input, RequestContext, Role, RoleLike, default_max_agent_depth, - effective_max_concurrent_jobs, jobs_enabled, list_agents_with_descriptions, + Agent, AgentVariable, AgentVariables, AppState, Input, RequestContext, Role, RoleLike, + default_max_agent_depth, effective_max_concurrent_jobs, jobs_enabled, + list_agents_with_descriptions, }; use crate::supervisor::mailbox::{Envelope, EnvelopePayload, Inbox, PeerRegistry, graph_agent_id}; use crate::supervisor::notification::agent_notification; @@ -39,6 +40,73 @@ fn agent_permitted(whitelist: Option<&[String]>, target: &str) -> bool { } } +fn parse_variables_arg(args: &Value) -> Result, String> { + let Some(value) = args.get("variables") else { + return Ok(None); + }; + let Some(obj) = value.as_object() else { + return Err( + "'variables' must be a JSON object mapping variable names to string values, e.g. {\"pr\": \"1234\"}".to_string(), + ); + }; + let mut variables = AgentVariables::new(); + for (key, val) in obj { + let Some(val) = val.as_str() else { + return Err(format!("'variables.{key}' must be a string value")); + }; + variables.insert(key.clone(), val.to_string()); + } + Ok(Some(variables)) +} + +fn validate_spawn_variables( + agent_name: &str, + declared: &[AgentVariable], + provided: &AgentVariables, +) -> Result<(), String> { + let unknown: Vec<&str> = provided + .keys() + .filter(|key| !declared.iter().any(|v| v.name == **key)) + .map(String::as_str) + .collect(); + if unknown.is_empty() { + return Ok(()); + } + if declared.is_empty() { + return Err(format!( + "Agent '{agent_name}' declares no variables; remove the 'variables' argument." + )); + } + let declared_list = declared + .iter() + .map(|v| format!("{} ({})", v.name, v.description)) + .collect::>() + .join(", "); + Err(format!( + "Unknown variable(s) {unknown:?} for agent '{agent_name}'. Declared variables: {declared_list}" + )) +} + +fn merge_spawn_variables(inherited: &mut Option, provided: AgentVariables) { + if provided.is_empty() { + return; + } + inherited + .get_or_insert_with(AgentVariables::new) + .extend(provided); +} + +fn dispatch_spawn_args(agent: &str, prompt: &str, variables: Option<&AgentVariables>) -> Value { + let mut spawn_args = json!({ + "agent": agent, + "prompt": prompt, + }); + if let Some(variables) = variables { + spawn_args["variables"] = json!(variables); + } + spawn_args +} + fn is_job_task(supervisor: Option<&Arc>>, id: &str) -> bool { id.starts_with("job_") || supervisor.is_some_and(|sup| sup.read().has_job(id)) } @@ -265,6 +333,14 @@ pub fn agent_function_declarations() -> Vec { ..Default::default() }, ), + ( + "variables".to_string(), + JsonSchema { + type_value: Some("object".to_string()), + description: Some("Values for the target agent's declared variables (its config/graph `variables:` list), e.g. {\"pr\": \"1234\"}. Overrides values inherited from the parent. String values only. Note: if the target agent resumes an existing shared session, the session's stored variable values take precedence and these are ignored.".into()), + ..Default::default() + }, + ), ])), required: Some(vec!["agent".to_string(), "prompt".to_string()]), ..Default::default() @@ -410,6 +486,14 @@ pub fn agent_function_declarations() -> Vec { ..Default::default() }, ), + ( + "variables".to_string(), + JsonSchema { + type_value: Some("object".to_string()), + description: Some("Values for the auto-spawned agent's declared variables, e.g. {\"pr\": \"1234\"}. Passed to `agent__spawn` at dispatch time. String values only.".into()), + ..Default::default() + }, + ), ])), required: Some(vec!["subject".to_string()]), ..Default::default() @@ -918,6 +1002,15 @@ async fn handle_spawn(ctx: &mut RequestContext, args: &Value) -> Result { .ok_or_else(|| anyhow!("'prompt' is required"))? .to_string(); let _task_id = args.get("task_id").and_then(Value::as_str); + let variables = match parse_variables_arg(args) { + Ok(v) => v, + Err(message) => { + return Ok(json!({ + "status": "error", + "message": message, + })); + } + }; if let Some(parent) = ctx.agent.as_ref() && !agent_permitted(parent.spawnable_agents(), &agent_name) @@ -995,6 +1088,16 @@ async fn handle_spawn(ctx: &mut RequestContext, args: &Value) -> Result { ) .await?; + if let Some(provided) = variables.as_ref() + && let Err(message) = + validate_spawn_variables(&agent_name, agent.defined_variables(), provided) + { + return Ok(json!({ + "status": "error", + "message": message, + })); + } + let agent_mcp_servers = agent.mcp_server_names().to_vec(); let session = agent.agent_session().map(|v| v.to_string()); let child_jobs_enabled = jobs_enabled(Some(&agent), app_config.as_ref()); @@ -1020,6 +1123,9 @@ async fn handle_spawn(ctx: &mut RequestContext, args: &Value) -> Result { Supervisor::new(max_concurrent_agents, max_depth).with_max_concurrent_jobs(max_jobs), ))); } + if let Some(provided) = variables { + merge_spawn_variables(&mut child_ctx.agent_variables, provided); + } if let Some(session) = session { child_ctx @@ -1605,6 +1711,16 @@ fn handle_task_create(ctx: &mut RequestContext, args: &Value) -> Result { bail!("'prompt' is required when 'agent' is set"); } + let variables = match parse_variables_arg(args) { + Ok(v) => v, + Err(message) => { + return Ok(json!({ + "status": "error", + "message": message, + })); + } + }; + let supervisor = ctx .supervisor .as_ref() @@ -1617,6 +1733,7 @@ fn handle_task_create(ctx: &mut RequestContext, args: &Value) -> Result { description.to_string(), dispatch_agent.clone(), task_prompt, + variables, ); let mut dep_errors = vec![]; @@ -1664,6 +1781,7 @@ fn handle_task_list(ctx: &mut RequestContext) -> Result { "blocks": t.blocks.iter().collect::>(), "agent": t.dispatch_agent, "prompt": t.prompt, + "variables": t.variables, }) }) .collect(); @@ -1688,7 +1806,7 @@ async fn handle_task_complete(ctx: &mut RequestContext, args: &Value) -> Result< let newly_runnable_ids = sup.task_queue_mut().complete(task_id); let mut newly_runnable = Vec::new(); - let mut to_dispatch: Vec<(String, String, String)> = Vec::new(); + let mut to_dispatch: Vec<(String, String, String, Option)> = Vec::new(); for id in &newly_runnable_ids { if let Some(t) = sup.task_queue().get(id) { @@ -1700,15 +1818,20 @@ async fn handle_task_complete(ctx: &mut RequestContext, args: &Value) -> Result< })); if let (Some(agent), Some(prompt)) = (&t.dispatch_agent, &t.prompt) { - to_dispatch.push((id.clone(), agent.clone(), prompt.clone())); + to_dispatch.push(( + id.clone(), + agent.clone(), + prompt.clone(), + t.variables.clone(), + )); } } } let mut dispatchable = Vec::new(); - for (tid, agent, prompt) in to_dispatch { + for (tid, agent, prompt, variables) in to_dispatch { if sup.task_queue_mut().claim(&tid, &format!("auto:{agent}")) { - dispatchable.push((agent, prompt)); + dispatchable.push((agent, prompt, variables)); } } @@ -1716,11 +1839,8 @@ async fn handle_task_complete(ctx: &mut RequestContext, args: &Value) -> Result< }; let mut spawned = Vec::new(); - for (agent, prompt) in &dispatchable { - let spawn_args = json!({ - "agent": agent, - "prompt": prompt, - }); + for (agent, prompt, variables) in &dispatchable { + let spawn_args = dispatch_spawn_args(agent, prompt, variables.as_ref()); match handle_spawn(ctx, &spawn_args).await { Ok(result) => { let agent_id = result @@ -2935,6 +3055,30 @@ mod tests { assert_eq!(result["auto_dispatch"], true); } + #[test] + fn handle_task_create_stores_variables() { + let mut ctx = ctx_with_supervisor(4, 3); + let result = handle_task_create( + &mut ctx, + &json!({"subject": "Auto task", "agent": "coder", "prompt": "do it", "variables": {"pr": "1234"}}), + ) + .unwrap(); + assert_eq!(result["status"], "ok"); + + let list = handle_task_list(&mut ctx).unwrap(); + let tasks = list["tasks"].as_array().unwrap(); + assert_eq!(tasks[0]["variables"]["pr"], "1234"); + } + + #[test] + fn handle_task_create_rejects_non_object_variables() { + let mut ctx = ctx_with_supervisor(4, 3); + let result = + handle_task_create(&mut ctx, &json!({"subject": "Bad", "variables": ["pr"]})).unwrap(); + assert_eq!(result["status"], "error"); + assert!(result["message"].as_str().unwrap().contains("JSON object")); + } + #[test] fn handle_task_create_agent_without_prompt_errors() { let mut ctx = ctx_with_supervisor(4, 3); @@ -3635,6 +3779,120 @@ mod tests { ); } + #[test] + fn handle_spawn_non_object_variables_errors() { + let mut ctx = ctx_with_supervisor(4, 3); + let result = run_async(handle_spawn( + &mut ctx, + &json!({"agent": "x", "prompt": "p", "variables": "pr=1234"}), + )) + .unwrap(); + assert_eq!(result["status"], "error"); + assert!(result["message"].as_str().unwrap().contains("JSON object")); + } + + #[test] + fn handle_spawn_non_string_variable_value_errors() { + let mut ctx = ctx_with_supervisor(4, 3); + let result = run_async(handle_spawn( + &mut ctx, + &json!({"agent": "x", "prompt": "p", "variables": {"pr": 1234}}), + )) + .unwrap(); + assert_eq!(result["status"], "error"); + assert!( + result["message"] + .as_str() + .unwrap() + .contains("'variables.pr'") + ); + } + + #[test] + fn parse_variables_arg_absent_returns_none() { + assert_eq!(parse_variables_arg(&json!({})).unwrap(), None); + } + + #[test] + fn parse_variables_arg_accepts_object_of_strings() { + let parsed = parse_variables_arg(&json!({"variables": {"pr": "1234", "repo": "coyote"}})) + .unwrap() + .unwrap(); + assert_eq!(parsed.get("pr").map(String::as_str), Some("1234")); + assert_eq!(parsed.get("repo").map(String::as_str), Some("coyote")); + } + + #[test] + fn validate_spawn_variables_unknown_name_lists_declared() { + let declared = vec![AgentVariable { + name: "pr".into(), + description: "PR number".into(), + ..Default::default() + }]; + let provided = AgentVariables::from([("typo".to_string(), "x".to_string())]); + let err = validate_spawn_variables("coder", &declared, &provided).unwrap_err(); + assert!(err.contains("typo")); + assert!(err.contains("pr (PR number)")); + } + + #[test] + fn validate_spawn_variables_none_declared_errors() { + let provided = AgentVariables::from([("pr".to_string(), "1".to_string())]); + let err = validate_spawn_variables("coder", &[], &provided).unwrap_err(); + assert!(err.contains("declares no variables")); + } + + #[test] + fn validate_spawn_variables_known_names_ok() { + let declared = vec![AgentVariable { + name: "pr".into(), + description: "PR number".into(), + ..Default::default() + }]; + let provided = AgentVariables::from([("pr".to_string(), "1".to_string())]); + assert!(validate_spawn_variables("coder", &declared, &provided).is_ok()); + } + + #[test] + fn merge_spawn_variables_overrides_inherited_per_key() { + let mut inherited = Some(AgentVariables::from([ + ("pr".to_string(), "1".to_string()), + ("repo".to_string(), "coyote".to_string()), + ])); + merge_spawn_variables( + &mut inherited, + AgentVariables::from([("pr".to_string(), "2".to_string())]), + ); + let merged = inherited.unwrap(); + assert_eq!(merged.get("pr").map(String::as_str), Some("2")); + assert_eq!(merged.get("repo").map(String::as_str), Some("coyote")); + } + + #[test] + fn merge_spawn_variables_seeds_when_nothing_inherited() { + let mut inherited = None; + merge_spawn_variables( + &mut inherited, + AgentVariables::from([("pr".to_string(), "2".to_string())]), + ); + assert_eq!(inherited.unwrap().get("pr").map(String::as_str), Some("2")); + } + + #[test] + fn dispatch_spawn_args_includes_variables() { + let variables = AgentVariables::from([("pr".to_string(), "1234".to_string())]); + let args = dispatch_spawn_args("coder", "do it", Some(&variables)); + assert_eq!(args["agent"], "coder"); + assert_eq!(args["prompt"], "do it"); + assert_eq!(args["variables"]["pr"], "1234"); + } + + #[test] + fn dispatch_spawn_args_omits_absent_variables() { + let args = dispatch_spawn_args("coder", "do it", None); + assert!(args.get("variables").is_none()); + } + #[test] fn handle_spawn_at_capacity_errors() { let mut ctx = ctx_with_supervisor(1, 3); diff --git a/src/supervisor/mod.rs b/src/supervisor/mod.rs index d3e0487..044635c 100644 --- a/src/supervisor/mod.rs +++ b/src/supervisor/mod.rs @@ -504,7 +504,7 @@ mod tests { let mut sup = Supervisor::new(4, 3); let id = sup .task_queue_mut() - .create("task".into(), "desc".into(), None, None); + .create("task".into(), "desc".into(), None, None, None); assert!(!id.is_empty()); assert_eq!(sup.task_queue().list().len(), 1); } diff --git a/src/supervisor/taskqueue.rs b/src/supervisor/taskqueue.rs index e60f676..5bebed4 100644 --- a/src/supervisor/taskqueue.rs +++ b/src/supervisor/taskqueue.rs @@ -1,3 +1,4 @@ +use crate::config::AgentVariables; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -22,6 +23,7 @@ pub struct TaskNode { pub blocks: HashSet, pub dispatch_agent: Option, pub prompt: Option, + pub variables: Option, } impl TaskNode { @@ -31,6 +33,7 @@ impl TaskNode { description: String, dispatch_agent: Option, prompt: Option, + variables: Option, ) -> Self { Self { id, @@ -42,6 +45,7 @@ impl TaskNode { blocks: HashSet::new(), dispatch_agent, prompt, + variables, } } @@ -70,10 +74,18 @@ impl TaskQueue { description: String, dispatch_agent: Option, prompt: Option, + variables: Option, ) -> String { let id = self.next_id.to_string(); self.next_id += 1; - let task = TaskNode::new(id.clone(), subject, description, dispatch_agent, prompt); + let task = TaskNode::new( + id.clone(), + subject, + description, + dispatch_agent, + prompt, + variables, + ); self.tasks.insert(id.clone(), task); id } @@ -193,8 +205,15 @@ mod tests { "Research auth patterns".into(), None, None, + None, + ); + let id2 = queue.create( + "Implement".into(), + "Write the code".into(), + None, + None, + None, ); - let id2 = queue.create("Implement".into(), "Write the code".into(), None, None); assert_eq!(id1, "1"); assert_eq!(id2, "2"); @@ -204,8 +223,8 @@ mod tests { #[test] fn test_dependency_and_completion() { let mut queue = TaskQueue::new(); - let id1 = queue.create("Step 1".into(), "".into(), None, None); - let id2 = queue.create("Step 2".into(), "".into(), None, None); + let id1 = queue.create("Step 1".into(), "".into(), None, None, None); + let id2 = queue.create("Step 2".into(), "".into(), None, None, None); queue.add_dependency(&id2, &id1).unwrap(); @@ -221,9 +240,9 @@ mod tests { #[test] fn test_fan_in_dependency() { let mut queue = TaskQueue::new(); - let id1 = queue.create("A".into(), "".into(), None, None); - let id2 = queue.create("B".into(), "".into(), None, None); - let id3 = queue.create("C (needs A and B)".into(), "".into(), None, None); + let id1 = queue.create("A".into(), "".into(), None, None, None); + let id2 = queue.create("B".into(), "".into(), None, None, None); + let id3 = queue.create("C (needs A and B)".into(), "".into(), None, None, None); queue.add_dependency(&id3, &id1).unwrap(); queue.add_dependency(&id3, &id2).unwrap(); @@ -242,8 +261,8 @@ mod tests { #[test] fn test_cycle_detection() { let mut queue = TaskQueue::new(); - let id1 = queue.create("A".into(), "".into(), None, None); - let id2 = queue.create("B".into(), "".into(), None, None); + let id1 = queue.create("A".into(), "".into(), None, None, None); + let id2 = queue.create("B".into(), "".into(), None, None, None); queue.add_dependency(&id2, &id1).unwrap(); let result = queue.add_dependency(&id1, &id2); @@ -254,7 +273,7 @@ mod tests { #[test] fn test_self_dependency_rejected() { let mut queue = TaskQueue::new(); - let id1 = queue.create("A".into(), "".into(), None, None); + let id1 = queue.create("A".into(), "".into(), None, None, None); let result = queue.add_dependency(&id1, &id1); assert!(result.is_err()); } @@ -262,7 +281,7 @@ mod tests { #[test] fn test_claim() { let mut queue = TaskQueue::new(); - let id1 = queue.create("Task".into(), "".into(), None, None); + let id1 = queue.create("Task".into(), "".into(), None, None, None); assert!(queue.claim(&id1, "worker-1")); assert!(!queue.claim(&id1, "worker-2")); @@ -272,7 +291,7 @@ mod tests { #[test] fn test_fail_sets_status() { let mut queue = TaskQueue::new(); - let id = queue.create("Task".into(), "".into(), None, None); + let id = queue.create("Task".into(), "".into(), None, None, None); queue.fail(&id); assert_eq!(queue.get(&id).unwrap().status, TaskStatus::Failed); } @@ -291,17 +310,41 @@ mod tests { "desc".into(), Some("coder".into()), Some("implement feature".into()), + None, ); let task = queue.get(&id).unwrap(); assert_eq!(task.dispatch_agent.as_deref(), Some("coder")); assert_eq!(task.prompt.as_deref(), Some("implement feature")); } + #[test] + fn test_variables_stored() { + let mut queue = TaskQueue::new(); + let id = queue.create( + "Auto task".into(), + "desc".into(), + Some("coder".into()), + Some("implement feature".into()), + Some(AgentVariables::from([( + "pr".to_string(), + "1234".to_string(), + )])), + ); + let task = queue.get(&id).unwrap(); + assert_eq!( + task.variables + .as_ref() + .and_then(|v| v.get("pr")) + .map(String::as_str), + Some("1234") + ); + } + #[test] fn test_claim_blocked_task_fails() { let mut queue = TaskQueue::new(); - let id1 = queue.create("A".into(), "".into(), None, None); - let id2 = queue.create("B".into(), "".into(), None, None); + let id1 = queue.create("A".into(), "".into(), None, None, None); + let id2 = queue.create("B".into(), "".into(), None, None, None); queue.add_dependency(&id2, &id1).unwrap(); assert!(!queue.claim(&id2, "worker")); } @@ -309,9 +352,9 @@ mod tests { #[test] fn test_list_sorted_by_id() { let mut queue = TaskQueue::new(); - queue.create("Third".into(), "".into(), None, None); - queue.create("First".into(), "".into(), None, None); - queue.create("Second".into(), "".into(), None, None); + queue.create("Third".into(), "".into(), None, None, None); + queue.create("First".into(), "".into(), None, None, None); + queue.create("Second".into(), "".into(), None, None, None); let tasks = queue.list(); let ids: Vec<&str> = tasks.iter().map(|t| t.id.as_str()).collect(); assert_eq!(ids, vec!["1", "2", "3"]); @@ -326,7 +369,7 @@ mod tests { #[test] fn test_dependency_on_nonexistent_task_errors() { let mut queue = TaskQueue::new(); - let id1 = queue.create("A".into(), "".into(), None, None); + let id1 = queue.create("A".into(), "".into(), None, None, None); let result = queue.add_dependency(&id1, "nonexistent"); assert!(result.is_err()); } @@ -340,13 +383,13 @@ mod tests { #[test] fn test_task_node_is_runnable() { - let node = TaskNode::new("1".into(), "t".into(), "d".into(), None, None); + let node = TaskNode::new("1".into(), "t".into(), "d".into(), None, None, None); assert!(node.is_runnable()); } #[test] fn test_task_node_not_runnable_when_blocked() { - let mut node = TaskNode::new("1".into(), "t".into(), "d".into(), None, None); + let mut node = TaskNode::new("1".into(), "t".into(), "d".into(), None, None, None); node.blocked_by.insert("2".into()); node.status = TaskStatus::Blocked; assert!(!node.is_runnable());