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:
6 files changed
+339
-35
No files matched your search
@@ -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
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user