Compare commits
4 changed files with 33 additions and 200 deletions
|
|
@ -88,7 +88,7 @@ impl AgentPolicy {
|
|||
AgentType::TestJudge => Self {
|
||||
agent_type: agent_type.clone(),
|
||||
auto_approve: true,
|
||||
max_depth: 2,
|
||||
max_depth: 3,
|
||||
require_preview: true,
|
||||
timeout_seconds: 60,
|
||||
max_tokens: 4096,
|
||||
|
|
@ -98,7 +98,7 @@ impl AgentPolicy {
|
|||
AgentType::BuildAgent => Self {
|
||||
agent_type: agent_type.clone(),
|
||||
auto_approve: false,
|
||||
max_depth: 3,
|
||||
max_depth: 5,
|
||||
require_preview: false,
|
||||
timeout_seconds: 300,
|
||||
max_tokens: 16384,
|
||||
|
|
|
|||
86
src/todo.rs
86
src/todo.rs
|
|
@ -39,7 +39,6 @@ pub struct ToDo {
|
|||
pub automation_chain_id: Option<Uuid>,
|
||||
pub tags: Vec<String>,
|
||||
pub estimated_tokens: u32,
|
||||
pub tools_json: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ToDo {
|
||||
|
|
@ -65,7 +64,6 @@ impl Default for ToDo {
|
|||
automation_chain_id: None,
|
||||
tags: Vec::new(),
|
||||
estimated_tokens: 0,
|
||||
tools_json: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +91,6 @@ impl ToDo {
|
|||
automation_chain_id: None,
|
||||
tags: Vec::new(),
|
||||
estimated_tokens: 0,
|
||||
tools_json: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +99,7 @@ impl ToDo {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn validate(&self, deps: &std::collections::HashMap<Uuid, Vec<Uuid>>) -> Result<(), ValidationError> {
|
||||
pub fn validate(&self) -> Result<(), ValidationError> {
|
||||
if self.title.is_empty() {
|
||||
return Err(ValidationError::EmptyTitle);
|
||||
}
|
||||
|
|
@ -119,13 +116,21 @@ impl ToDo {
|
|||
return Err(ValidationError::InvalidPriority);
|
||||
}
|
||||
|
||||
if let Some(cycle_node) = Self::detect_cycle_in_graph(deps) {
|
||||
return Err(ValidationError::CircularDependency(cycle_node.to_string()));
|
||||
if self.has_circular_dependency() {
|
||||
return Err(ValidationError::CircularDependency(self.id.to_string()));
|
||||
}
|
||||
|
||||
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
|
||||
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> {
|
||||
|
|
@ -191,7 +196,6 @@ impl ToDo {
|
|||
(ToDoStatus::Delegated, ToDoStatus::Completed) => true,
|
||||
(ToDoStatus::Delegated, ToDoStatus::Blocked) => true,
|
||||
(ToDoStatus::Failed, ToDoStatus::Pending) => true,
|
||||
(ToDoStatus::Failed, ToDoStatus::ReadyForAgent) => true,
|
||||
(ToDoStatus::PendingApproval, ToDoStatus::InProgress) => true,
|
||||
(ToDoStatus::PendingApproval, ToDoStatus::Blocked) => true,
|
||||
_ => self.status == status,
|
||||
|
|
@ -209,25 +213,6 @@ impl ToDo {
|
|||
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(
|
||||
&mut self,
|
||||
title: Option<String>,
|
||||
|
|
@ -285,42 +270,40 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_todo_validate_empty_title() {
|
||||
let todo = ToDo::default();
|
||||
let deps = std::collections::HashMap::new();
|
||||
assert!(matches!(todo.validate(&deps), Err(ValidationError::EmptyTitle)));
|
||||
let mut todo = ToDo::default();
|
||||
todo.title = "".to_string();
|
||||
assert!(matches!(todo.validate(), Err(ValidationError::EmptyTitle)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_todo_validate_title_too_long() {
|
||||
let mut todo = ToDo::default();
|
||||
todo.title = "a".repeat(MAX_TITLE_LENGTH + 1);
|
||||
let deps = std::collections::HashMap::new();
|
||||
assert!(matches!(
|
||||
todo.validate(&deps),
|
||||
todo.validate(),
|
||||
Err(ValidationError::TitleTooLong)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
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());
|
||||
todo.priority = 0;
|
||||
assert!(matches!(
|
||||
todo.validate(&deps),
|
||||
todo.validate(),
|
||||
Err(ValidationError::InvalidPriority)
|
||||
));
|
||||
|
||||
let mut todo = ToDo::new("Test".to_string(), "desc".to_string(), Uuid::new_v4());
|
||||
todo.priority = 1001;
|
||||
assert!(matches!(
|
||||
todo.validate(&deps),
|
||||
todo.validate(),
|
||||
Err(ValidationError::InvalidPriority)
|
||||
));
|
||||
|
||||
let mut todo = ToDo::new("Test".to_string(), "desc".to_string(), Uuid::new_v4());
|
||||
todo.priority = 500;
|
||||
assert!(todo.validate(&deps).is_ok());
|
||||
assert!(todo.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -392,19 +375,16 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_cycle_in_graph_self_reference() {
|
||||
let id = Uuid::new_v4();
|
||||
let mut deps = std::collections::HashMap::new();
|
||||
deps.insert(id, vec![id]);
|
||||
assert!(ToDo::detect_cycle_in_graph(&deps).is_some());
|
||||
fn test_has_circular_dependency_self_reference() {
|
||||
let mut todo = ToDo::default();
|
||||
todo.depends_on = vec![todo.id];
|
||||
assert!(todo.has_circular_dependency());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_cycle_in_graph_none() {
|
||||
let id = Uuid::new_v4();
|
||||
let mut deps = std::collections::HashMap::new();
|
||||
deps.insert(id, vec![]);
|
||||
assert!(ToDo::detect_cycle_in_graph(&deps).is_none());
|
||||
fn test_has_circular_dependency_none() {
|
||||
let todo = ToDo::default();
|
||||
assert!(!todo.has_circular_dependency());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -426,20 +406,4 @@ mod tests {
|
|||
deps.insert(id_b, vec![]);
|
||||
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,11 +97,6 @@ pub struct ToolDefinition {
|
|||
/* Caller surface (chat/planner/agent) in which this tool may appear. */
|
||||
#[serde(default)]
|
||||
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 {
|
||||
|
|
@ -124,7 +119,6 @@ impl Default for ToolDefinition {
|
|||
deprecation_message: String::new(),
|
||||
execution_context: ExecutionContext::default(),
|
||||
context_visibility: ContextVisibility::default(),
|
||||
required_depth: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -834,7 +828,6 @@ pub struct ToolDocumentation {
|
|||
pub const PLANNER_ONLY_TOOLS: &[&str] = &[
|
||||
"change_planner_mode",
|
||||
"propose_strategic_item",
|
||||
"split_task",
|
||||
"audit_assumptions",
|
||||
"identify_blind_spots",
|
||||
"check_dependencies",
|
||||
|
|
@ -859,16 +852,13 @@ 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
|
||||
Reef via MTP and are NOT in this list — they have no local implementation. */
|
||||
pub const REEF_PROXY_TOOLS: &[&str] = &[
|
||||
"read_file",
|
||||
"file_read",
|
||||
"write_file",
|
||||
"file_write",
|
||||
"read_file", "file_read",
|
||||
"write_file", "file_write",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"search_files",
|
||||
"grep",
|
||||
"bash",
|
||||
"execute",
|
||||
"bash", "execute",
|
||||
"delete_workspace",
|
||||
"list_files",
|
||||
];
|
||||
|
|
@ -1549,34 +1539,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
|||
"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!(
|
||||
"change_planner_mode",
|
||||
"Switch the Planner's working mode. Modes: cooperative (fast shipping), critical (audit), red_team (pre-mortem), socratic (discovery), execution (task decomposition).",
|
||||
|
|
@ -1738,81 +1700,6 @@ 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
|
||||
}
|
||||
|
||||
|
|
@ -1834,7 +1721,7 @@ pub fn tool_ids_for_agent_type(agent_type: &crate::agents::AgentType) -> Option<
|
|||
.collect::<Vec<String>>()
|
||||
};
|
||||
|
||||
let ids = Some(match agent_type {
|
||||
Some(match agent_type {
|
||||
crate::agents::AgentType::BuildAgent => curated(&[
|
||||
"read_file",
|
||||
"write_file",
|
||||
|
|
@ -1890,25 +1777,7 @@ pub fn tool_ids_for_agent_type(agent_type: &crate::agents::AgentType) -> Option<
|
|||
"web_search",
|
||||
"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. */
|
||||
|
|
|
|||
Loading…
Reference in a new issue