tools & tasks
This commit is contained in:
parent
475e3fc4b0
commit
507a04a5ff
4 changed files with 200 additions and 33 deletions
|
|
@ -88,7 +88,7 @@ impl AgentPolicy {
|
||||||
AgentType::TestJudge => Self {
|
AgentType::TestJudge => Self {
|
||||||
agent_type: agent_type.clone(),
|
agent_type: agent_type.clone(),
|
||||||
auto_approve: true,
|
auto_approve: true,
|
||||||
max_depth: 3,
|
max_depth: 2,
|
||||||
require_preview: true,
|
require_preview: true,
|
||||||
timeout_seconds: 60,
|
timeout_seconds: 60,
|
||||||
max_tokens: 4096,
|
max_tokens: 4096,
|
||||||
|
|
@ -98,7 +98,7 @@ impl AgentPolicy {
|
||||||
AgentType::BuildAgent => Self {
|
AgentType::BuildAgent => Self {
|
||||||
agent_type: agent_type.clone(),
|
agent_type: agent_type.clone(),
|
||||||
auto_approve: false,
|
auto_approve: false,
|
||||||
max_depth: 5,
|
max_depth: 3,
|
||||||
require_preview: false,
|
require_preview: false,
|
||||||
timeout_seconds: 300,
|
timeout_seconds: 300,
|
||||||
max_tokens: 16384,
|
max_tokens: 16384,
|
||||||
|
|
|
||||||
86
src/todo.rs
86
src/todo.rs
|
|
@ -39,6 +39,7 @@ pub struct ToDo {
|
||||||
pub automation_chain_id: Option<Uuid>,
|
pub automation_chain_id: Option<Uuid>,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub estimated_tokens: u32,
|
pub estimated_tokens: u32,
|
||||||
|
pub tools_json: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ToDo {
|
impl Default for ToDo {
|
||||||
|
|
@ -64,6 +65,7 @@ impl Default for ToDo {
|
||||||
automation_chain_id: None,
|
automation_chain_id: None,
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
estimated_tokens: 0,
|
estimated_tokens: 0,
|
||||||
|
tools_json: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +93,7 @@ impl ToDo {
|
||||||
automation_chain_id: None,
|
automation_chain_id: None,
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
estimated_tokens: 0,
|
estimated_tokens: 0,
|
||||||
|
tools_json: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,7 +102,7 @@ impl ToDo {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate(&self) -> Result<(), ValidationError> {
|
pub fn validate(&self, deps: &std::collections::HashMap<Uuid, Vec<Uuid>>) -> Result<(), ValidationError> {
|
||||||
if self.title.is_empty() {
|
if self.title.is_empty() {
|
||||||
return Err(ValidationError::EmptyTitle);
|
return Err(ValidationError::EmptyTitle);
|
||||||
}
|
}
|
||||||
|
|
@ -116,21 +119,13 @@ impl ToDo {
|
||||||
return Err(ValidationError::InvalidPriority);
|
return Err(ValidationError::InvalidPriority);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.has_circular_dependency() {
|
if let Some(cycle_node) = Self::detect_cycle_in_graph(deps) {
|
||||||
return Err(ValidationError::CircularDependency(self.id.to_string()));
|
return Err(ValidationError::CircularDependency(cycle_node.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_circular_dependency(&self) -> bool {
|
|
||||||
// Direct self-dependency
|
|
||||||
if self.depends_on.contains(&self.id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/* DFS cycle detection over a full dependency graph. Returns the first
|
/* DFS cycle detection over a full dependency graph. Returns the first
|
||||||
todo_id found in a cycle, or None when the graph is acyclic. */
|
todo_id found in a cycle, or None when the graph is acyclic. */
|
||||||
pub fn detect_cycle_in_graph(deps: &std::collections::HashMap<Uuid, Vec<Uuid>>) -> Option<Uuid> {
|
pub fn detect_cycle_in_graph(deps: &std::collections::HashMap<Uuid, Vec<Uuid>>) -> Option<Uuid> {
|
||||||
|
|
@ -196,6 +191,7 @@ impl ToDo {
|
||||||
(ToDoStatus::Delegated, ToDoStatus::Completed) => true,
|
(ToDoStatus::Delegated, ToDoStatus::Completed) => true,
|
||||||
(ToDoStatus::Delegated, ToDoStatus::Blocked) => true,
|
(ToDoStatus::Delegated, ToDoStatus::Blocked) => true,
|
||||||
(ToDoStatus::Failed, ToDoStatus::Pending) => true,
|
(ToDoStatus::Failed, ToDoStatus::Pending) => true,
|
||||||
|
(ToDoStatus::Failed, ToDoStatus::ReadyForAgent) => true,
|
||||||
(ToDoStatus::PendingApproval, ToDoStatus::InProgress) => true,
|
(ToDoStatus::PendingApproval, ToDoStatus::InProgress) => true,
|
||||||
(ToDoStatus::PendingApproval, ToDoStatus::Blocked) => true,
|
(ToDoStatus::PendingApproval, ToDoStatus::Blocked) => true,
|
||||||
_ => self.status == status,
|
_ => self.status == status,
|
||||||
|
|
@ -213,6 +209,25 @@ impl ToDo {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Walk the parent_todo_id chain and return the depth (1 = root, 2 = child, etc.)
|
||||||
|
pub fn calculate_depth(
|
||||||
|
todo_id: Uuid,
|
||||||
|
get_parent: impl Fn(Uuid) -> Option<Uuid>,
|
||||||
|
) -> usize {
|
||||||
|
let mut depth = 1usize;
|
||||||
|
let mut current_id = get_parent(todo_id);
|
||||||
|
let mut visited = HashSet::new();
|
||||||
|
|
||||||
|
while let Some(pid) = current_id {
|
||||||
|
if !visited.insert(pid) {
|
||||||
|
break; // cycle detected
|
||||||
|
}
|
||||||
|
depth += 1;
|
||||||
|
current_id = get_parent(pid);
|
||||||
|
}
|
||||||
|
depth
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update(
|
pub fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
|
|
@ -270,40 +285,42 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_todo_validate_empty_title() {
|
fn test_todo_validate_empty_title() {
|
||||||
let mut todo = ToDo::default();
|
let todo = ToDo::default();
|
||||||
todo.title = "".to_string();
|
let deps = std::collections::HashMap::new();
|
||||||
assert!(matches!(todo.validate(), Err(ValidationError::EmptyTitle)));
|
assert!(matches!(todo.validate(&deps), Err(ValidationError::EmptyTitle)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_todo_validate_title_too_long() {
|
fn test_todo_validate_title_too_long() {
|
||||||
let mut todo = ToDo::default();
|
let mut todo = ToDo::default();
|
||||||
todo.title = "a".repeat(MAX_TITLE_LENGTH + 1);
|
todo.title = "a".repeat(MAX_TITLE_LENGTH + 1);
|
||||||
|
let deps = std::collections::HashMap::new();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
todo.validate(),
|
todo.validate(&deps),
|
||||||
Err(ValidationError::TitleTooLong)
|
Err(ValidationError::TitleTooLong)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_todo_validate_priority() {
|
fn test_todo_validate_priority() {
|
||||||
|
let deps = std::collections::HashMap::new();
|
||||||
let mut todo = ToDo::new("Test".to_string(), "desc".to_string(), Uuid::new_v4());
|
let mut todo = ToDo::new("Test".to_string(), "desc".to_string(), Uuid::new_v4());
|
||||||
todo.priority = 0;
|
todo.priority = 0;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
todo.validate(),
|
todo.validate(&deps),
|
||||||
Err(ValidationError::InvalidPriority)
|
Err(ValidationError::InvalidPriority)
|
||||||
));
|
));
|
||||||
|
|
||||||
let mut todo = ToDo::new("Test".to_string(), "desc".to_string(), Uuid::new_v4());
|
let mut todo = ToDo::new("Test".to_string(), "desc".to_string(), Uuid::new_v4());
|
||||||
todo.priority = 1001;
|
todo.priority = 1001;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
todo.validate(),
|
todo.validate(&deps),
|
||||||
Err(ValidationError::InvalidPriority)
|
Err(ValidationError::InvalidPriority)
|
||||||
));
|
));
|
||||||
|
|
||||||
let mut todo = ToDo::new("Test".to_string(), "desc".to_string(), Uuid::new_v4());
|
let mut todo = ToDo::new("Test".to_string(), "desc".to_string(), Uuid::new_v4());
|
||||||
todo.priority = 500;
|
todo.priority = 500;
|
||||||
assert!(todo.validate().is_ok());
|
assert!(todo.validate(&deps).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -375,16 +392,19 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_has_circular_dependency_self_reference() {
|
fn test_detect_cycle_in_graph_self_reference() {
|
||||||
let mut todo = ToDo::default();
|
let id = Uuid::new_v4();
|
||||||
todo.depends_on = vec![todo.id];
|
let mut deps = std::collections::HashMap::new();
|
||||||
assert!(todo.has_circular_dependency());
|
deps.insert(id, vec![id]);
|
||||||
|
assert!(ToDo::detect_cycle_in_graph(&deps).is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_has_circular_dependency_none() {
|
fn test_detect_cycle_in_graph_none() {
|
||||||
let todo = ToDo::default();
|
let id = Uuid::new_v4();
|
||||||
assert!(!todo.has_circular_dependency());
|
let mut deps = std::collections::HashMap::new();
|
||||||
|
deps.insert(id, vec![]);
|
||||||
|
assert!(ToDo::detect_cycle_in_graph(&deps).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -406,4 +426,20 @@ mod tests {
|
||||||
deps.insert(id_b, vec![]);
|
deps.insert(id_b, vec![]);
|
||||||
assert!(ToDo::detect_cycle_in_graph(&deps).is_none());
|
assert!(ToDo::detect_cycle_in_graph(&deps).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_catches_indirect_cycle() {
|
||||||
|
let id_a = Uuid::new_v4();
|
||||||
|
let id_b = Uuid::new_v4();
|
||||||
|
let mut todo = ToDo::new("Test".to_string(), "desc".to_string(), Uuid::new_v4());
|
||||||
|
todo.id = id_a;
|
||||||
|
todo.depends_on = vec![id_b];
|
||||||
|
let mut deps = std::collections::HashMap::new();
|
||||||
|
deps.insert(id_a, vec![id_b]);
|
||||||
|
deps.insert(id_b, vec![id_a]);
|
||||||
|
assert!(matches!(
|
||||||
|
todo.validate(&deps),
|
||||||
|
Err(ValidationError::CircularDependency(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
141
src/tools.rs
141
src/tools.rs
|
|
@ -97,6 +97,11 @@ pub struct ToolDefinition {
|
||||||
/* Caller surface (chat/planner/agent) in which this tool may appear. */
|
/* Caller surface (chat/planner/agent) in which this tool may appear. */
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub context_visibility: ContextVisibility,
|
pub context_visibility: ContextVisibility,
|
||||||
|
/* Minimum agent depth required for this tool to be visible.
|
||||||
|
0 = visible to all agents.
|
||||||
|
1+ = only visible to agents with max_depth >= this value. */
|
||||||
|
#[serde(default)]
|
||||||
|
pub required_depth: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn default_timeout_ms() -> u64 {
|
const fn default_timeout_ms() -> u64 {
|
||||||
|
|
@ -119,6 +124,7 @@ impl Default for ToolDefinition {
|
||||||
deprecation_message: String::new(),
|
deprecation_message: String::new(),
|
||||||
execution_context: ExecutionContext::default(),
|
execution_context: ExecutionContext::default(),
|
||||||
context_visibility: ContextVisibility::default(),
|
context_visibility: ContextVisibility::default(),
|
||||||
|
required_depth: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -828,6 +834,7 @@ pub struct ToolDocumentation {
|
||||||
pub const PLANNER_ONLY_TOOLS: &[&str] = &[
|
pub const PLANNER_ONLY_TOOLS: &[&str] = &[
|
||||||
"change_planner_mode",
|
"change_planner_mode",
|
||||||
"propose_strategic_item",
|
"propose_strategic_item",
|
||||||
|
"split_task",
|
||||||
"audit_assumptions",
|
"audit_assumptions",
|
||||||
"identify_blind_spots",
|
"identify_blind_spots",
|
||||||
"check_dependencies",
|
"check_dependencies",
|
||||||
|
|
@ -852,13 +859,16 @@ This is the Krill → Reef proxy set, not the full set of Reef-only tools.
|
||||||
Tools like kanban_*, document_*, and strategic items are always executed on
|
Tools like kanban_*, document_*, and strategic items are always executed on
|
||||||
Reef via MTP and are NOT in this list — they have no local implementation. */
|
Reef via MTP and are NOT in this list — they have no local implementation. */
|
||||||
pub const REEF_PROXY_TOOLS: &[&str] = &[
|
pub const REEF_PROXY_TOOLS: &[&str] = &[
|
||||||
"read_file", "file_read",
|
"read_file",
|
||||||
"write_file", "file_write",
|
"file_read",
|
||||||
|
"write_file",
|
||||||
|
"file_write",
|
||||||
"edit_file",
|
"edit_file",
|
||||||
"list_directory",
|
"list_directory",
|
||||||
"search_files",
|
"search_files",
|
||||||
"grep",
|
"grep",
|
||||||
"bash", "execute",
|
"bash",
|
||||||
|
"execute",
|
||||||
"delete_workspace",
|
"delete_workspace",
|
||||||
"list_files",
|
"list_files",
|
||||||
];
|
];
|
||||||
|
|
@ -1539,6 +1549,34 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
||||||
"required": ["project_id", "title", "description", "rationale", "proposer_id"]
|
"required": ["project_id", "title", "description", "rationale", "proposer_id"]
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
tool!(
|
||||||
|
"split_task",
|
||||||
|
"Split a Pending todo into multiple subtasks with proper dependency tracking, depth validation, and cycle detection. Each subtask should be independently deployable to an agent.",
|
||||||
|
"strategic",
|
||||||
|
false, false, true,
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_id": { "type": "string", "description": "Project ID" },
|
||||||
|
"parent_todo_id": { "type": "string", "description": "ID of the todo to split (must be in Pending status)" },
|
||||||
|
"subtasks": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of subtask definitions",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": { "type": "string", "description": "Title of the subtask" },
|
||||||
|
"description": { "type": "string", "description": "Detailed description" },
|
||||||
|
"depends_on": { "type": "array", "items": { "type": "integer" }, "description": "Indices (0-based) of subtasks this one depends on" },
|
||||||
|
"agent_type_hint": { "type": "string", "description": "Optional agent type hint (build_agent, doc_agent, explore_agent, test_judge)" }
|
||||||
|
},
|
||||||
|
"required": ["title"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["project_id", "parent_todo_id", "subtasks"]
|
||||||
|
})
|
||||||
|
),
|
||||||
tool!(
|
tool!(
|
||||||
"change_planner_mode",
|
"change_planner_mode",
|
||||||
"Switch the Planner's working mode. Modes: cooperative (fast shipping), critical (audit), red_team (pre-mortem), socratic (discovery), execution (task decomposition).",
|
"Switch the Planner's working mode. Modes: cooperative (fast shipping), critical (audit), red_team (pre-mortem), socratic (discovery), execution (task decomposition).",
|
||||||
|
|
@ -1700,6 +1738,81 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Assign required_depth per tool. Depth 0 = visible to all agents.
|
||||||
|
Depth 1 = needs one planning cycle (bash, write/edits, build tools).
|
||||||
|
Depth 2 = needs moderate depth (kanban mutation, workspace creation).
|
||||||
|
Depth 3 = needs deep reasoning (strategic, batch planning). */
|
||||||
|
let depth_map: std::collections::HashMap<&str, u32> = [
|
||||||
|
// Depth 0 — always visible
|
||||||
|
("read_file", 0),
|
||||||
|
("file_read", 0),
|
||||||
|
("list_directory", 0),
|
||||||
|
("search_files", 0),
|
||||||
|
("list_files", 0),
|
||||||
|
("grep", 0),
|
||||||
|
("web_search", 0),
|
||||||
|
("web_fetch", 0),
|
||||||
|
("web_api", 0),
|
||||||
|
("workspace_info", 0),
|
||||||
|
("kanban_list_board", 0),
|
||||||
|
("kanban_add_tag", 0),
|
||||||
|
("git_status", 0),
|
||||||
|
("git_diff", 0),
|
||||||
|
("git_log", 0),
|
||||||
|
("git_branch", 0),
|
||||||
|
("get_documentation_context", 0),
|
||||||
|
("documentation_tree", 0),
|
||||||
|
("verify_documentation", 0),
|
||||||
|
("find_references", 0),
|
||||||
|
("file_dependencies", 0),
|
||||||
|
("audit_assumptions", 0),
|
||||||
|
("identify_blind_spots", 0),
|
||||||
|
("check_dependencies", 0),
|
||||||
|
("evaluate_plan_risk", 0),
|
||||||
|
("compare_project_patterns", 0),
|
||||||
|
("find_similar_risks", 0),
|
||||||
|
("report_completion", 0),
|
||||||
|
// Depth 1 — needs one planning cycle
|
||||||
|
("write_file", 1),
|
||||||
|
("file_write", 1),
|
||||||
|
("edit_file", 1),
|
||||||
|
("bash", 1),
|
||||||
|
("execute", 1),
|
||||||
|
("cargo_check", 1),
|
||||||
|
("npm_build", 1),
|
||||||
|
("python_check", 1),
|
||||||
|
("test_runner", 1),
|
||||||
|
("delete_workspace", 1),
|
||||||
|
("create_venv", 1),
|
||||||
|
("install_dependencies", 1),
|
||||||
|
("document_file", 1),
|
||||||
|
("document_project", 1),
|
||||||
|
("document_folder", 1),
|
||||||
|
("store_file_doc", 1),
|
||||||
|
// Depth 2 — needs moderate depth
|
||||||
|
("kanban_create_todo", 2),
|
||||||
|
("kanban_update_todo", 2),
|
||||||
|
("kanban_delete_todo", 2),
|
||||||
|
("kanban_move_todo", 2),
|
||||||
|
("kanban_create_task", 2),
|
||||||
|
("kanban_update_task", 2),
|
||||||
|
("create_workspace", 2),
|
||||||
|
("split_task", 2),
|
||||||
|
// Depth 3 — needs deep reasoning
|
||||||
|
("submit_batch_plan", 3),
|
||||||
|
("change_planner_mode", 3),
|
||||||
|
("propose_strategic_item", 3),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for tool in tools.iter_mut() {
|
||||||
|
if let Some(&depth) = depth_map.get(tool.id.as_str()) {
|
||||||
|
tool.required_depth = depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tools
|
tools
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1721,7 +1834,7 @@ pub fn tool_ids_for_agent_type(agent_type: &crate::agents::AgentType) -> Option<
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(match agent_type {
|
let ids = Some(match agent_type {
|
||||||
crate::agents::AgentType::BuildAgent => curated(&[
|
crate::agents::AgentType::BuildAgent => curated(&[
|
||||||
"read_file",
|
"read_file",
|
||||||
"write_file",
|
"write_file",
|
||||||
|
|
@ -1777,7 +1890,25 @@ pub fn tool_ids_for_agent_type(agent_type: &crate::agents::AgentType) -> Option<
|
||||||
"web_search",
|
"web_search",
|
||||||
"web_fetch",
|
"web_fetch",
|
||||||
]),
|
]),
|
||||||
})
|
});
|
||||||
|
|
||||||
|
// Filter by depth: remove tools whose required_depth exceeds the agent's max_depth
|
||||||
|
let policy = crate::agents::AgentPolicy::defaults_for(agent_type);
|
||||||
|
let max_depth = policy.max_depth;
|
||||||
|
let all_tools = tool_definitions();
|
||||||
|
let depth_map: std::collections::HashMap<&str, u32> = all_tools
|
||||||
|
.iter()
|
||||||
|
.map(|t| (t.id.as_str(), t.required_depth))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
ids.retain(|id| {
|
||||||
|
depth_map
|
||||||
|
.get(id.as_str())
|
||||||
|
.map(|&req| req <= max_depth)
|
||||||
|
.unwrap_or(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Formats tool definitions as OpenAI-compatible function-calling JSON. */
|
/* Formats tool definitions as OpenAI-compatible function-calling JSON. */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue