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
This commit is contained in:
Dark-Alex-17 committed 2026-09-12 01:37:20 -06:00
1 parent 3599b0a861
commit f97a83f165
6 files changed
+339 -35

No files matched your search

+2 -1
View File
@@ -329,6 +329,7 @@ impl Agent {
agent_variables: &[AgentVariable], agent_variables: &[AgentVariable],
pre_set_variables: Option<&AgentVariables>, pre_set_variables: Option<&AgentVariables>,
no_interaction: bool, no_interaction: bool,
interactive: bool,
) -> Result<AgentVariables> { ) -> Result<AgentVariables> {
let mut output = IndexMap::new(); let mut output = IndexMap::new();
if agent_variables.is_empty() { if agent_variables.is_empty() {
@@ -349,7 +350,7 @@ impl Agent {
if no_interaction { if no_interaction {
continue; continue;
} }
if *IS_STDOUT_TERMINAL { if interactive && *IS_STDOUT_TERMINAL {
if !printed { if !printed {
println!("⚙ Init agent variables..."); println!("⚙ Init agent variables...");
printed = true; printed = true;
+2 -2
View File
@@ -84,13 +84,13 @@ pub(in crate::config) const DEFAULT_SPAWN_INSTRUCTIONS: &str = indoc! {"
| Tool | Purpose | | 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__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__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_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__list_running` | List all subagents YOU have spawned, with their status. |
| `agent__cancel` | Cancel a running agent by ID. | | `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_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_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. | | `agent__task_fail` | Mark a task as failed. Dependents remain blocked. |
+2
View File
@@ -1191,6 +1191,7 @@ impl RequestContext {
agent.defined_variables(), agent.defined_variables(),
self.agent_variables.as_ref(), self.agent_variables.as_ref(),
self.info_flag, self.info_flag,
self.self_agent_id.is_none(),
)?; )?;
agent.set_shared_variables(new_variables); agent.set_shared_variables(new_variables);
} }
@@ -1213,6 +1214,7 @@ impl RequestContext {
agent.defined_variables(), agent.defined_variables(),
self.agent_variables.as_ref(), self.agent_variables.as_ref(),
self.info_flag, self.info_flag,
self.self_agent_id.is_none(),
)?; )?;
agent.set_shared_variables(new_variables.clone()); agent.set_shared_variables(new_variables.clone());
new_variables new_variables
+269 -11
View File
@@ -2,8 +2,9 @@ use super::todo::TODO_FUNCTION_PREFIX;
use super::{FunctionDeclaration, JsonSchema}; use super::{FunctionDeclaration, JsonSchema};
use crate::client::{Model, ModelType, call_chat_completions}; use crate::client::{Model, ModelType, call_chat_completions};
use crate::config::{ use crate::config::{
Agent, AppState, Input, RequestContext, Role, RoleLike, default_max_agent_depth, Agent, AgentVariable, AgentVariables, AppState, Input, RequestContext, Role, RoleLike,
effective_max_concurrent_jobs, jobs_enabled, list_agents_with_descriptions, 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::mailbox::{Envelope, EnvelopePayload, Inbox, PeerRegistry, graph_agent_id};
use crate::supervisor::notification::agent_notification; 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<Option<AgentVariables>, 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::<Vec<_>>()
.join(", ");
Err(format!(
"Unknown variable(s) {unknown:?} for agent '{agent_name}'. Declared variables: {declared_list}"
))
}
fn merge_spawn_variables(inherited: &mut Option<AgentVariables>, 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<RwLock<Supervisor>>>, id: &str) -> bool { fn is_job_task(supervisor: Option<&Arc<RwLock<Supervisor>>>, id: &str) -> bool {
id.starts_with("job_") || supervisor.is_some_and(|sup| sup.read().has_job(id)) id.starts_with("job_") || supervisor.is_some_and(|sup| sup.read().has_job(id))
} }
@@ -265,6 +333,14 @@ pub fn agent_function_declarations() -> Vec<FunctionDeclaration> {
..Default::default() ..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()]), required: Some(vec!["agent".to_string(), "prompt".to_string()]),
..Default::default() ..Default::default()
@@ -410,6 +486,14 @@ pub fn agent_function_declarations() -> Vec<FunctionDeclaration> {
..Default::default() ..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()]), required: Some(vec!["subject".to_string()]),
..Default::default() ..Default::default()
@@ -918,6 +1002,15 @@ async fn handle_spawn(ctx: &mut RequestContext, args: &Value) -> Result<Value> {
.ok_or_else(|| anyhow!("'prompt' is required"))? .ok_or_else(|| anyhow!("'prompt' is required"))?
.to_string(); .to_string();
let _task_id = args.get("task_id").and_then(Value::as_str); 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() if let Some(parent) = ctx.agent.as_ref()
&& !agent_permitted(parent.spawnable_agents(), &agent_name) && !agent_permitted(parent.spawnable_agents(), &agent_name)
@@ -995,6 +1088,16 @@ async fn handle_spawn(ctx: &mut RequestContext, args: &Value) -> Result<Value> {
) )
.await?; .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 agent_mcp_servers = agent.mcp_server_names().to_vec();
let session = agent.agent_session().map(|v| v.to_string()); let session = agent.agent_session().map(|v| v.to_string());
let child_jobs_enabled = jobs_enabled(Some(&agent), app_config.as_ref()); 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<Value> {
Supervisor::new(max_concurrent_agents, max_depth).with_max_concurrent_jobs(max_jobs), 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 { if let Some(session) = session {
child_ctx child_ctx
@@ -1605,6 +1711,16 @@ fn handle_task_create(ctx: &mut RequestContext, args: &Value) -> Result<Value> {
bail!("'prompt' is required when 'agent' is set"); 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 let supervisor = ctx
.supervisor .supervisor
.as_ref() .as_ref()
@@ -1617,6 +1733,7 @@ fn handle_task_create(ctx: &mut RequestContext, args: &Value) -> Result<Value> {
description.to_string(), description.to_string(),
dispatch_agent.clone(), dispatch_agent.clone(),
task_prompt, task_prompt,
variables,
); );
let mut dep_errors = vec![]; let mut dep_errors = vec![];
@@ -1664,6 +1781,7 @@ fn handle_task_list(ctx: &mut RequestContext) -> Result<Value> {
"blocks": t.blocks.iter().collect::<Vec<_>>(), "blocks": t.blocks.iter().collect::<Vec<_>>(),
"agent": t.dispatch_agent, "agent": t.dispatch_agent,
"prompt": t.prompt, "prompt": t.prompt,
"variables": t.variables,
}) })
}) })
.collect(); .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 newly_runnable_ids = sup.task_queue_mut().complete(task_id);
let mut newly_runnable = Vec::new(); 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<AgentVariables>)> = Vec::new();
for id in &newly_runnable_ids { for id in &newly_runnable_ids {
if let Some(t) = sup.task_queue().get(id) { 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) { 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(); 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}")) { 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(); let mut spawned = Vec::new();
for (agent, prompt) in &dispatchable { for (agent, prompt, variables) in &dispatchable {
let spawn_args = json!({ let spawn_args = dispatch_spawn_args(agent, prompt, variables.as_ref());
"agent": agent,
"prompt": prompt,
});
match handle_spawn(ctx, &spawn_args).await { match handle_spawn(ctx, &spawn_args).await {
Ok(result) => { Ok(result) => {
let agent_id = result let agent_id = result
@@ -2935,6 +3055,30 @@ mod tests {
assert_eq!(result["auto_dispatch"], true); 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] #[test]
fn handle_task_create_agent_without_prompt_errors() { fn handle_task_create_agent_without_prompt_errors() {
let mut ctx = ctx_with_supervisor(4, 3); 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] #[test]
fn handle_spawn_at_capacity_errors() { fn handle_spawn_at_capacity_errors() {
let mut ctx = ctx_with_supervisor(1, 3); let mut ctx = ctx_with_supervisor(1, 3);
+1 -1
View File
@@ -504,7 +504,7 @@ mod tests {
let mut sup = Supervisor::new(4, 3); let mut sup = Supervisor::new(4, 3);
let id = sup let id = sup
.task_queue_mut() .task_queue_mut()
.create("task".into(), "desc".into(), None, None); .create("task".into(), "desc".into(), None, None, None);
assert!(!id.is_empty()); assert!(!id.is_empty());
assert_eq!(sup.task_queue().list().len(), 1); assert_eq!(sup.task_queue().list().len(), 1);
} }
+63 -20
View File
@@ -1,3 +1,4 @@
use crate::config::AgentVariables;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
@@ -22,6 +23,7 @@ pub struct TaskNode {
pub blocks: HashSet<String>, pub blocks: HashSet<String>,
pub dispatch_agent: Option<String>, pub dispatch_agent: Option<String>,
pub prompt: Option<String>, pub prompt: Option<String>,
pub variables: Option<AgentVariables>,
} }
impl TaskNode { impl TaskNode {
@@ -31,6 +33,7 @@ impl TaskNode {
description: String, description: String,
dispatch_agent: Option<String>, dispatch_agent: Option<String>,
prompt: Option<String>, prompt: Option<String>,
variables: Option<AgentVariables>,
) -> Self { ) -> Self {
Self { Self {
id, id,
@@ -42,6 +45,7 @@ impl TaskNode {
blocks: HashSet::new(), blocks: HashSet::new(),
dispatch_agent, dispatch_agent,
prompt, prompt,
variables,
} }
} }
@@ -70,10 +74,18 @@ impl TaskQueue {
description: String, description: String,
dispatch_agent: Option<String>, dispatch_agent: Option<String>,
prompt: Option<String>, prompt: Option<String>,
variables: Option<AgentVariables>,
) -> String { ) -> String {
let id = self.next_id.to_string(); let id = self.next_id.to_string();
self.next_id += 1; 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); self.tasks.insert(id.clone(), task);
id id
} }
@@ -193,8 +205,15 @@ mod tests {
"Research auth patterns".into(), "Research auth patterns".into(),
None, None,
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!(id1, "1");
assert_eq!(id2, "2"); assert_eq!(id2, "2");
@@ -204,8 +223,8 @@ mod tests {
#[test] #[test]
fn test_dependency_and_completion() { fn test_dependency_and_completion() {
let mut queue = TaskQueue::new(); let mut queue = TaskQueue::new();
let id1 = queue.create("Step 1".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); let id2 = queue.create("Step 2".into(), "".into(), None, None, None);
queue.add_dependency(&id2, &id1).unwrap(); queue.add_dependency(&id2, &id1).unwrap();
@@ -221,9 +240,9 @@ mod tests {
#[test] #[test]
fn test_fan_in_dependency() { fn test_fan_in_dependency() {
let mut queue = TaskQueue::new(); 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 id2 = queue.create("B".into(), "".into(), 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); let id3 = queue.create("C (needs A and B)".into(), "".into(), None, None, None);
queue.add_dependency(&id3, &id1).unwrap(); queue.add_dependency(&id3, &id1).unwrap();
queue.add_dependency(&id3, &id2).unwrap(); queue.add_dependency(&id3, &id2).unwrap();
@@ -242,8 +261,8 @@ mod tests {
#[test] #[test]
fn test_cycle_detection() { fn test_cycle_detection() {
let mut queue = TaskQueue::new(); 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 id2 = queue.create("B".into(), "".into(), None, None); let id2 = queue.create("B".into(), "".into(), None, None, None);
queue.add_dependency(&id2, &id1).unwrap(); queue.add_dependency(&id2, &id1).unwrap();
let result = queue.add_dependency(&id1, &id2); let result = queue.add_dependency(&id1, &id2);
@@ -254,7 +273,7 @@ mod tests {
#[test] #[test]
fn test_self_dependency_rejected() { fn test_self_dependency_rejected() {
let mut queue = TaskQueue::new(); 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); let result = queue.add_dependency(&id1, &id1);
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -262,7 +281,7 @@ mod tests {
#[test] #[test]
fn test_claim() { fn test_claim() {
let mut queue = TaskQueue::new(); 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-1"));
assert!(!queue.claim(&id1, "worker-2")); assert!(!queue.claim(&id1, "worker-2"));
@@ -272,7 +291,7 @@ mod tests {
#[test] #[test]
fn test_fail_sets_status() { fn test_fail_sets_status() {
let mut queue = TaskQueue::new(); 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); queue.fail(&id);
assert_eq!(queue.get(&id).unwrap().status, TaskStatus::Failed); assert_eq!(queue.get(&id).unwrap().status, TaskStatus::Failed);
} }
@@ -291,17 +310,41 @@ mod tests {
"desc".into(), "desc".into(),
Some("coder".into()), Some("coder".into()),
Some("implement feature".into()), Some("implement feature".into()),
None,
); );
let task = queue.get(&id).unwrap(); let task = queue.get(&id).unwrap();
assert_eq!(task.dispatch_agent.as_deref(), Some("coder")); assert_eq!(task.dispatch_agent.as_deref(), Some("coder"));
assert_eq!(task.prompt.as_deref(), Some("implement feature")); 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] #[test]
fn test_claim_blocked_task_fails() { fn test_claim_blocked_task_fails() {
let mut queue = TaskQueue::new(); 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 id2 = queue.create("B".into(), "".into(), None, None); let id2 = queue.create("B".into(), "".into(), None, None, None);
queue.add_dependency(&id2, &id1).unwrap(); queue.add_dependency(&id2, &id1).unwrap();
assert!(!queue.claim(&id2, "worker")); assert!(!queue.claim(&id2, "worker"));
} }
@@ -309,9 +352,9 @@ mod tests {
#[test] #[test]
fn test_list_sorted_by_id() { fn test_list_sorted_by_id() {
let mut queue = TaskQueue::new(); let mut queue = TaskQueue::new();
queue.create("Third".into(), "".into(), None, None); queue.create("Third".into(), "".into(), None, None, None);
queue.create("First".into(), "".into(), None, None); queue.create("First".into(), "".into(), None, None, None);
queue.create("Second".into(), "".into(), None, None); queue.create("Second".into(), "".into(), None, None, None);
let tasks = queue.list(); let tasks = queue.list();
let ids: Vec<&str> = tasks.iter().map(|t| t.id.as_str()).collect(); let ids: Vec<&str> = tasks.iter().map(|t| t.id.as_str()).collect();
assert_eq!(ids, vec!["1", "2", "3"]); assert_eq!(ids, vec!["1", "2", "3"]);
@@ -326,7 +369,7 @@ mod tests {
#[test] #[test]
fn test_dependency_on_nonexistent_task_errors() { fn test_dependency_on_nonexistent_task_errors() {
let mut queue = TaskQueue::new(); 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"); let result = queue.add_dependency(&id1, "nonexistent");
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -340,13 +383,13 @@ mod tests {
#[test] #[test]
fn test_task_node_is_runnable() { 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()); assert!(node.is_runnable());
} }
#[test] #[test]
fn test_task_node_not_runnable_when_blocked() { 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.blocked_by.insert("2".into());
node.status = TaskStatus::Blocked; node.status = TaskStatus::Blocked;
assert!(!node.is_runnable()); assert!(!node.is_runnable());