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

+1 -1
View File
@@ -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);
}
+63 -20
View File
@@ -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<String>,
pub dispatch_agent: Option<String>,
pub prompt: Option<String>,
pub variables: Option<AgentVariables>,
}
impl TaskNode {
@@ -31,6 +33,7 @@ impl TaskNode {
description: String,
dispatch_agent: Option<String>,
prompt: Option<String>,
variables: Option<AgentVariables>,
) -> 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<String>,
prompt: Option<String>,
variables: Option<AgentVariables>,
) -> 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());