Compare commits
4 changed files with 33 additions and 200 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: 2,
|
max_depth: 3,
|
||||||
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: 3,
|
max_depth: 5,
|
||||||
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,7 +39,6 @@ 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 {
|
||||||
|
|
@ -65,7 +64,6 @@ 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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -93,7 +91,6 @@ 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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,7 +99,7 @@ impl ToDo {
|
||||||
self
|
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() {
|
if self.title.is_empty() {
|
||||||
return Err(ValidationError::EmptyTitle);
|
return Err(ValidationError::EmptyTitle);
|
||||||
}
|
}
|
||||||
|
|
@ -119,13 +116,21 @@ impl ToDo {
|
||||||
return Err(ValidationError::InvalidPriority);
|
return Err(ValidationError::InvalidPriority);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(cycle_node) = Self::detect_cycle_in_graph(deps) {
|
if self.has_circular_dependency() {
|
||||||
return Err(ValidationError::CircularDependency(cycle_node.to_string()));
|
return Err(ValidationError::CircularDependency(self.id.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> {
|
||||||
|
|
@ -191,7 +196,6 @@ 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,
|
||||||
|
|
@ -209,25 +213,6 @@ 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>,
|
||||||
|
|
@ -285,42 +270,40 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_todo_validate_empty_title() {
|
fn test_todo_validate_empty_title() {
|
||||||
let todo = ToDo::default();
|
let mut todo = ToDo::default();
|
||||||
let deps = std::collections::HashMap::new();
|
todo.title = "".to_string();
|
||||||
assert!(matches!(todo.validate(&deps), Err(ValidationError::EmptyTitle)));
|
assert!(matches!(todo.validate(), 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(&deps),
|
todo.validate(),
|
||||||
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(&deps),
|
todo.validate(),
|
||||||
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(&deps),
|
todo.validate(),
|
||||||
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(&deps).is_ok());
|
assert!(todo.validate().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -392,19 +375,16 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_detect_cycle_in_graph_self_reference() {
|
fn test_has_circular_dependency_self_reference() {
|
||||||
let id = Uuid::new_v4();
|
let mut todo = ToDo::default();
|
||||||
let mut deps = std::collections::HashMap::new();
|
todo.depends_on = vec![todo.id];
|
||||||
deps.insert(id, vec![id]);
|
assert!(todo.has_circular_dependency());
|
||||||
assert!(ToDo::detect_cycle_in_graph(&deps).is_some());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_detect_cycle_in_graph_none() {
|
fn test_has_circular_dependency_none() {
|
||||||
let id = Uuid::new_v4();
|
let todo = ToDo::default();
|
||||||
let mut deps = std::collections::HashMap::new();
|
assert!(!todo.has_circular_dependency());
|
||||||
deps.insert(id, vec![]);
|
|
||||||
assert!(ToDo::detect_cycle_in_graph(&deps).is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -426,20 +406,4 @@ 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,11 +97,6 @@ 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 {
|
||||||
|
|
@ -124,7 +119,6 @@ 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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -834,7 +828,6 @@ 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",
|
||||||
|
|
@ -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
|
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",
|
"read_file", "file_read",
|
||||||
"file_read",
|
"write_file", "file_write",
|
||||||
"write_file",
|
|
||||||
"file_write",
|
|
||||||
"edit_file",
|
"edit_file",
|
||||||
"list_directory",
|
"list_directory",
|
||||||
"search_files",
|
"search_files",
|
||||||
"grep",
|
"grep",
|
||||||
"bash",
|
"bash", "execute",
|
||||||
"execute",
|
|
||||||
"delete_workspace",
|
"delete_workspace",
|
||||||
"list_files",
|
"list_files",
|
||||||
];
|
];
|
||||||
|
|
@ -1549,34 +1539,6 @@ 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).",
|
||||||
|
|
@ -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
|
tools
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1834,7 +1721,7 @@ pub fn tool_ids_for_agent_type(agent_type: &crate::agents::AgentType) -> Option<
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
};
|
};
|
||||||
|
|
||||||
let ids = Some(match agent_type {
|
Some(match agent_type {
|
||||||
crate::agents::AgentType::BuildAgent => curated(&[
|
crate::agents::AgentType::BuildAgent => curated(&[
|
||||||
"read_file",
|
"read_file",
|
||||||
"write_file",
|
"write_file",
|
||||||
|
|
@ -1890,25 +1777,7 @@ 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