Uploads, Sync, Changetracking, Pods throttle
This commit is contained in:
parent
89e939d50f
commit
ae6375a1d9
2 changed files with 327 additions and 54 deletions
|
|
@ -32,8 +32,8 @@ pub use task::{Task, TaskResult};
|
|||
pub use todo::{Dependency, ToDo};
|
||||
pub use tools::{
|
||||
format_tool_error, format_tools_json, format_tool_result, parse_tool_call_blocks,
|
||||
parse_tool_call_stream, tool_definitions, tool_ids_for_agent_type, CompositionStep,
|
||||
ExecutionContext, MarketplaceSearchResults, ParsedToolCall,
|
||||
parse_tool_call_stream, tool_definitions, tool_ids_for_agent_type, ApprovalRequirement,
|
||||
CompositionStep, ContextVisibility, ExecutionContext, MarketplaceSearchResults, ParsedToolCall,
|
||||
TestAction, ToolComposition, ToolDefinition, ToolDependency, ToolDocumentation, ToolExample,
|
||||
ToolListing, ToolParser, ToolPlugin, ToolTest, ToolTestResult, ToolTestSuite,
|
||||
ToolTestSuiteResult, ToolVersion,
|
||||
|
|
|
|||
369
src/tools.rs
369
src/tools.rs
|
|
@ -16,6 +16,53 @@ impl Default for ExecutionContext {
|
|||
}
|
||||
}
|
||||
|
||||
/* Which caller surface a tool may appear in, independent of where it executes.
|
||||
`ExecutionContext` decides which runtime runs a tool; `ContextVisibility`
|
||||
decides which surface (direct chat, planner session, or agent run) is allowed
|
||||
to see and invoke it. */
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ContextVisibility {
|
||||
/* Visible everywhere: chat, planner, and agent runs. */
|
||||
Everywhere,
|
||||
/* Only visible inside a planner session (strategic tooling). */
|
||||
PlannerOnly,
|
||||
/* Only visible to automated agent runs; hidden from direct chat and planner. */
|
||||
AgentOnly,
|
||||
}
|
||||
|
||||
impl Default for ContextVisibility {
|
||||
fn default() -> Self {
|
||||
Self::Everywhere
|
||||
}
|
||||
}
|
||||
|
||||
/* How a tool call is gated before it runs. `None` runs immediately; `Policy`
|
||||
consults the per-agent Kelp policy and precedent gate (a human or agent-flow
|
||||
approval may follow); `Human` always reaches a person. This replaces the legacy
|
||||
`dangerous`/`requires_approval` bool pair. Whether a tool *mutates* durable
|
||||
state is a separate axis exposed by `ToolDefinition::mutates()`. */
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ApprovalRequirement {
|
||||
#[default]
|
||||
None,
|
||||
Policy,
|
||||
Human,
|
||||
}
|
||||
|
||||
impl ApprovalRequirement {
|
||||
/* Bridges callers that still describe a tool with the old two-bool intent
|
||||
(dangerous, requires_approval): either flag means the call is policy-gated. */
|
||||
pub fn from_flags(dangerous: bool, requires_approval: bool) -> Self {
|
||||
if dangerous || requires_approval {
|
||||
ApprovalRequirement::Policy
|
||||
} else {
|
||||
ApprovalRequirement::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolDefinition {
|
||||
#[serde(default)]
|
||||
|
|
@ -25,10 +72,9 @@ pub struct ToolDefinition {
|
|||
#[serde(default)]
|
||||
pub category: String,
|
||||
pub parameters: serde_json::Value,
|
||||
/* How this tool is gated before it runs. Defaults to `None` (runs freely). */
|
||||
#[serde(default)]
|
||||
pub dangerous: bool,
|
||||
#[serde(default)]
|
||||
pub requires_approval: bool,
|
||||
pub approval_requirement: ApprovalRequirement,
|
||||
#[serde(default)]
|
||||
pub version: String,
|
||||
/* Whether this tool requires a project context to function.
|
||||
|
|
@ -47,6 +93,9 @@ pub struct ToolDefinition {
|
|||
/* Execution context indicating where this tool should be available */
|
||||
#[serde(default)]
|
||||
pub execution_context: ExecutionContext,
|
||||
/* Caller surface (chat/planner/agent) in which this tool may appear. */
|
||||
#[serde(default)]
|
||||
pub context_visibility: ContextVisibility,
|
||||
}
|
||||
|
||||
const fn default_timeout_ms() -> u64 {
|
||||
|
|
@ -61,19 +110,34 @@ impl Default for ToolDefinition {
|
|||
description: String::new(),
|
||||
category: String::new(),
|
||||
parameters: serde_json::Value::Null,
|
||||
dangerous: false,
|
||||
requires_approval: false,
|
||||
approval_requirement: ApprovalRequirement::None,
|
||||
version: String::new(),
|
||||
project_scoped: false,
|
||||
timeout_ms: default_timeout_ms(),
|
||||
deprecated: false,
|
||||
deprecation_message: String::new(),
|
||||
execution_context: ExecutionContext::default(),
|
||||
context_visibility: ContextVisibility::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolDefinition {
|
||||
/* Tools that mutate durable state (filesystem or shell). Kept as a name-derived
|
||||
axis separate from the approval taxonomy: it drives the Lookout "dangerous"
|
||||
badge and the Phase 8 snapshot-before-mutation gate. */
|
||||
pub fn mutates(&self) -> bool {
|
||||
matches!(
|
||||
self.name.as_str(),
|
||||
"write_file" | "edit_file" | "bash" | "delete_workspace"
|
||||
)
|
||||
}
|
||||
|
||||
/* True when the call passes through any approval gate at all. */
|
||||
pub fn requires_approval(&self) -> bool {
|
||||
self.approval_requirement != ApprovalRequirement::None
|
||||
}
|
||||
|
||||
pub fn to_openai_format(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "function",
|
||||
|
|
@ -557,14 +621,14 @@ macro_rules! tool {
|
|||
description: $desc.to_string(),
|
||||
category: $cat.to_string(),
|
||||
parameters: $params,
|
||||
dangerous: $dangerous,
|
||||
requires_approval: $approval,
|
||||
approval_requirement: ApprovalRequirement::from_flags($dangerous, $approval),
|
||||
version: "1.0.0".to_string(),
|
||||
project_scoped: $scoped,
|
||||
timeout_ms: default_timeout_ms(),
|
||||
deprecated: false,
|
||||
deprecation_message: String::new(),
|
||||
execution_context: ExecutionContext::default(),
|
||||
context_visibility: ContextVisibility::default(),
|
||||
}
|
||||
};
|
||||
($name:expr, $desc:expr, $cat:expr, $dangerous:expr, $approval:expr, $scoped:expr, $params:expr, $timeout:expr) => {
|
||||
|
|
@ -574,14 +638,14 @@ macro_rules! tool {
|
|||
description: $desc.to_string(),
|
||||
category: $cat.to_string(),
|
||||
parameters: $params,
|
||||
dangerous: $dangerous,
|
||||
requires_approval: $approval,
|
||||
approval_requirement: ApprovalRequirement::from_flags($dangerous, $approval),
|
||||
version: "1.0.0".to_string(),
|
||||
project_scoped: $scoped,
|
||||
timeout_ms: $timeout,
|
||||
deprecated: false,
|
||||
deprecation_message: String::new(),
|
||||
execution_context: ExecutionContext::default(),
|
||||
context_visibility: ContextVisibility::default(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -595,9 +659,13 @@ pub trait ToolPlugin: Send + Sync {
|
|||
fn schema(&self) -> serde_json::Value;
|
||||
fn execute(&self, args: serde_json::Value) -> Result<serde_json::Value, String>;
|
||||
/* Defaults to true (dangerous) — safe default for unknown plugins. */
|
||||
fn dangerous(&self) -> bool { true }
|
||||
fn dangerous(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/* Defaults to true; callers must explicitly opt out of approval prompts. */
|
||||
fn requires_approval(&self) -> bool { true }
|
||||
fn requires_approval(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/* A marketplace listing for a published tool. */
|
||||
|
|
@ -734,9 +802,27 @@ pub struct ToolDocumentation {
|
|||
pub examples: Vec<ToolExample>,
|
||||
}
|
||||
|
||||
/* Tools visible only inside a planner session (strategic oversight). */
|
||||
pub const PLANNER_ONLY_TOOLS: &[&str] = &[
|
||||
"change_planner_mode",
|
||||
"propose_strategic_item",
|
||||
"audit_assumptions",
|
||||
"identify_blind_spots",
|
||||
"check_dependencies",
|
||||
"evaluate_plan_risk",
|
||||
"compare_project_patterns",
|
||||
"find_similar_risks",
|
||||
];
|
||||
|
||||
/* Tools visible only to automated agent runs, hidden from direct chat and planner.
|
||||
Mutating tools (write_file, edit_file, bash, delete_workspace) are intentionally
|
||||
NOT here: they stay Everywhere so direct chat can call them under approval routing
|
||||
(Phase 4). Only genuinely agent-internal collaboration tools are agent-only. */
|
||||
pub const AGENT_ONLY_TOOLS: &[&str] = &["submit_batch_plan", "report_completion"];
|
||||
|
||||
/* Returns all built-in tool definitions across all categories. */
|
||||
pub fn tool_definitions() -> Vec<ToolDefinition> {
|
||||
vec![
|
||||
let mut tools = vec![
|
||||
// ========================
|
||||
// Filesystem Tools
|
||||
// ========================
|
||||
|
|
@ -1398,14 +1484,14 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
|||
),
|
||||
tool!(
|
||||
"change_planner_mode",
|
||||
"Suggest switching the Planner's personality mode",
|
||||
"Switch the Planner's working mode. Modes: cooperative (fast shipping), critical (audit), red_team (pre-mortem), socratic (discovery), execution (task decomposition).",
|
||||
"strategic",
|
||||
false, true, true,
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": { "type": "string", "description": "Project ID" },
|
||||
"mode": { "type": "string", "enum": ["cooperative", "critical", "red_team", "socratic"], "description": "The planner mode to switch to" },
|
||||
"mode": { "type": "string", "enum": ["cooperative", "critical", "red_team", "socratic", "execution"], "description": "The planner mode to switch to" },
|
||||
"rationale": { "type": "string", "description": "Why this mode change is beneficial" }
|
||||
},
|
||||
"required": ["project_id", "mode", "rationale"]
|
||||
|
|
@ -1534,45 +1620,106 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
|||
"required": ["plan_id", "steps", "rationale"]
|
||||
})
|
||||
),
|
||||
]
|
||||
];
|
||||
|
||||
/* Apply context visibility. The explicit id lists are the primary source,
|
||||
but a category backstop makes classification fail-closed: any strategic or
|
||||
agent-collaboration tool added later is restricted by its category even if
|
||||
nobody remembered to list it here, so a new sensitive tool cannot silently
|
||||
leak into the chat surface as the macro's Everywhere default. Every current
|
||||
strategic tool is already in PLANNER_ONLY_TOOLS and every agent tool in
|
||||
AGENT_ONLY_TOOLS, so this changes no existing tool's classification. */
|
||||
for tool in tools.iter_mut() {
|
||||
if PLANNER_ONLY_TOOLS.contains(&tool.id.as_str()) {
|
||||
tool.context_visibility = ContextVisibility::PlannerOnly;
|
||||
} else if AGENT_ONLY_TOOLS.contains(&tool.id.as_str()) {
|
||||
tool.context_visibility = ContextVisibility::AgentOnly;
|
||||
} else {
|
||||
tool.context_visibility = match tool.category.as_str() {
|
||||
"strategic" => ContextVisibility::PlannerOnly,
|
||||
"agent" => ContextVisibility::AgentOnly,
|
||||
_ => tool.context_visibility,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
tools
|
||||
}
|
||||
|
||||
/* Returns the tool IDs appropriate for a given agent type.
|
||||
Used by the dispatcher to embed tool definitions in task specs. */
|
||||
pub fn tool_ids_for_agent_type(agent_type: &crate::agents::AgentType) -> Option<Vec<&'static str>> {
|
||||
Used by the dispatcher to embed tool definitions in task specs.
|
||||
|
||||
Planner and ChatAgent derive their sets from `ContextVisibility` so the tool
|
||||
list stays a single source of truth: the Planner sees Everywhere + PlannerOnly
|
||||
tools, ChatAgent sees Everywhere tools only (strategic PlannerOnly and
|
||||
agent-internal AgentOnly tools stay hidden). The remaining agent types keep a
|
||||
curated subset that is narrower than any visibility class allows. */
|
||||
pub fn tool_ids_for_agent_type(agent_type: &crate::agents::AgentType) -> Option<Vec<String>> {
|
||||
let curated = |ids: &[&str]| ids.iter().map(|s| s.to_string()).collect::<Vec<String>>();
|
||||
let by_visibility = |allowed: &[ContextVisibility]| {
|
||||
tool_definitions()
|
||||
.into_iter()
|
||||
.filter(|t| allowed.contains(&t.context_visibility))
|
||||
.map(|t| t.id)
|
||||
.collect::<Vec<String>>()
|
||||
};
|
||||
|
||||
Some(match agent_type {
|
||||
crate::agents::AgentType::BuildAgent => vec![
|
||||
"read_file", "write_file", "edit_file", "list_directory", "search_files",
|
||||
"bash", "cargo_check", "npm_build", "python_check", "test_runner",
|
||||
"create_workspace", "kanban_create_todo", "kanban_update_todo",
|
||||
"kanban_create_task", "list_files", "submit_batch_plan", "report_completion",
|
||||
],
|
||||
crate::agents::AgentType::DocAgent => vec![
|
||||
"read_file", "search_files", "list_files", "list_directory",
|
||||
"find_references", "file_dependencies", "get_documentation_context",
|
||||
"documentation_tree", "store_file_doc",
|
||||
],
|
||||
crate::agents::AgentType::TestJudge => vec![
|
||||
"read_file", "search_files", "list_files", "list_directory",
|
||||
"bash", "kanban_create_todo", "kanban_update_todo", "report_completion",
|
||||
],
|
||||
crate::agents::AgentType::Planner => vec![
|
||||
"kanban_list_board", "kanban_create_todo", "kanban_update_todo",
|
||||
"kanban_delete_todo", "kanban_move_todo", "kanban_create_task",
|
||||
"kanban_update_task", "kanban_add_tag", "document_file", "document_project",
|
||||
"find_references", "file_dependencies", "documentation_tree",
|
||||
"verify_documentation", "document_folder", "get_documentation_context",
|
||||
"list_files", "propose_strategic_item", "change_planner_mode",
|
||||
"audit_assumptions", "identify_blind_spots", "check_dependencies",
|
||||
"evaluate_plan_risk", "compare_project_patterns", "find_similar_risks",
|
||||
"store_file_doc", "read_file",
|
||||
],
|
||||
crate::agents::AgentType::ChatAgent => return None,
|
||||
crate::agents::AgentType::BuildAgent => curated(&[
|
||||
"read_file",
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"list_directory",
|
||||
"search_files",
|
||||
"bash",
|
||||
"cargo_check",
|
||||
"npm_build",
|
||||
"python_check",
|
||||
"test_runner",
|
||||
"create_workspace",
|
||||
"kanban_create_todo",
|
||||
"kanban_update_todo",
|
||||
"kanban_create_task",
|
||||
"list_files",
|
||||
"submit_batch_plan",
|
||||
"report_completion",
|
||||
]),
|
||||
/*
|
||||
* The DocAgent documents the file handed to it inline in the user message,
|
||||
* so it needs no raw codebase access (read_file/search/list/deps). It gets
|
||||
* only the doc-context read helpers plus store_file_doc — no write or exec
|
||||
* tools — which is why the prompt no longer needs a negative "you must not
|
||||
* edit" instruction.
|
||||
*/
|
||||
crate::agents::AgentType::DocAgent => curated(&[
|
||||
"get_documentation_context",
|
||||
"documentation_tree",
|
||||
"store_file_doc",
|
||||
]),
|
||||
crate::agents::AgentType::TestJudge => curated(&[
|
||||
"read_file",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"list_directory",
|
||||
"bash",
|
||||
"kanban_create_todo",
|
||||
"kanban_update_todo",
|
||||
"report_completion",
|
||||
]),
|
||||
crate::agents::AgentType::Planner => by_visibility(&[
|
||||
ContextVisibility::Everywhere,
|
||||
ContextVisibility::PlannerOnly,
|
||||
]),
|
||||
crate::agents::AgentType::ChatAgent => by_visibility(&[ContextVisibility::Everywhere]),
|
||||
/* ExploreAgent only receives read-only tools — no write, edit, or bash. */
|
||||
crate::agents::AgentType::ExploreAgent => vec![
|
||||
"read_file", "list_directory", "search_files", "list_files",
|
||||
"web_search", "web_fetch",
|
||||
],
|
||||
crate::agents::AgentType::ExploreAgent => curated(&[
|
||||
"read_file",
|
||||
"list_directory",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"web_search",
|
||||
"web_fetch",
|
||||
]),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -2060,6 +2207,132 @@ Done with tools.
|
|||
assert_eq!(ExecutionContext::default(), ExecutionContext::Both);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_visibility_default() {
|
||||
assert_eq!(ContextVisibility::default(), ContextVisibility::Everywhere);
|
||||
assert_eq!(
|
||||
ToolDefinition::default().context_visibility,
|
||||
ContextVisibility::Everywhere
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_definitions_context_visibility_classification() {
|
||||
let tools = tool_definitions();
|
||||
let visibility_of = |id: &str| {
|
||||
tools
|
||||
.iter()
|
||||
.find(|t| t.id == id)
|
||||
.map(|t| t.context_visibility)
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
visibility_of("change_planner_mode"),
|
||||
Some(ContextVisibility::PlannerOnly)
|
||||
);
|
||||
assert_eq!(
|
||||
visibility_of("propose_strategic_item"),
|
||||
Some(ContextVisibility::PlannerOnly)
|
||||
);
|
||||
assert_eq!(
|
||||
visibility_of("submit_batch_plan"),
|
||||
Some(ContextVisibility::AgentOnly)
|
||||
);
|
||||
assert_eq!(
|
||||
visibility_of("report_completion"),
|
||||
Some(ContextVisibility::AgentOnly)
|
||||
);
|
||||
/* Mutating tools stay Everywhere so chat can call them under approval routing. */
|
||||
assert_eq!(
|
||||
visibility_of("write_file"),
|
||||
Some(ContextVisibility::Everywhere)
|
||||
);
|
||||
assert_eq!(visibility_of("bash"), Some(ContextVisibility::Everywhere));
|
||||
assert_eq!(
|
||||
visibility_of("read_file"),
|
||||
Some(ContextVisibility::Everywhere)
|
||||
);
|
||||
assert_eq!(
|
||||
visibility_of("kanban_list_board"),
|
||||
Some(ContextVisibility::Everywhere)
|
||||
);
|
||||
|
||||
for id in PLANNER_ONLY_TOOLS {
|
||||
assert_eq!(
|
||||
visibility_of(id),
|
||||
Some(ContextVisibility::PlannerOnly),
|
||||
"{id} should be PlannerOnly"
|
||||
);
|
||||
}
|
||||
for id in AGENT_ONLY_TOOLS {
|
||||
assert_eq!(
|
||||
visibility_of(id),
|
||||
Some(ContextVisibility::AgentOnly),
|
||||
"{id} should be AgentOnly"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_category_backstop_keeps_sensitive_tools_fail_closed() {
|
||||
/* Invariant enforced by the category backstop: no strategic tool is ever
|
||||
chat-visible and no agent-collaboration tool is planner/chat-visible,
|
||||
regardless of whether it was added to the explicit id lists. A new tool
|
||||
in either category that leaks as Everywhere fails this test. */
|
||||
for tool in tool_definitions() {
|
||||
match tool.category.as_str() {
|
||||
"strategic" => assert_eq!(
|
||||
tool.context_visibility,
|
||||
ContextVisibility::PlannerOnly,
|
||||
"strategic tool {} must not be chat-visible",
|
||||
tool.id
|
||||
),
|
||||
"agent" => assert_eq!(
|
||||
tool.context_visibility,
|
||||
ContextVisibility::AgentOnly,
|
||||
"agent tool {} must not be chat/planner-visible",
|
||||
tool.id
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_ids_for_agent_type_derived_from_visibility() {
|
||||
use crate::agents::AgentType;
|
||||
|
||||
let planner = tool_ids_for_agent_type(&AgentType::Planner).unwrap();
|
||||
assert!(planner.iter().any(|id| id == "change_planner_mode"));
|
||||
assert!(planner.iter().any(|id| id == "read_file"));
|
||||
assert!(planner.iter().any(|id| id == "write_file"));
|
||||
/* Agent-internal tools stay hidden from the planner. */
|
||||
assert!(!planner.iter().any(|id| id == "submit_batch_plan"));
|
||||
assert!(!planner.iter().any(|id| id == "report_completion"));
|
||||
|
||||
let chat = tool_ids_for_agent_type(&AgentType::ChatAgent).unwrap();
|
||||
/* Mutating tools remain visible to chat (approval-gated). */
|
||||
assert!(chat.iter().any(|id| id == "write_file"));
|
||||
assert!(chat.iter().any(|id| id == "bash"));
|
||||
/* Strategic and agent-internal tools are hidden from chat. */
|
||||
assert!(!chat.iter().any(|id| id == "change_planner_mode"));
|
||||
assert!(!chat.iter().any(|id| id == "submit_batch_plan"));
|
||||
assert!(!chat.iter().any(|id| id == "report_completion"));
|
||||
|
||||
/* Curated agent types are unchanged. */
|
||||
let build = tool_ids_for_agent_type(&AgentType::BuildAgent).unwrap();
|
||||
assert!(build.iter().any(|id| id == "submit_batch_plan"));
|
||||
assert_eq!(build.len(), 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_visibility_serde_snake_case() {
|
||||
let json = serde_json::to_string(&ContextVisibility::PlannerOnly).unwrap();
|
||||
assert_eq!(json, "\"planner_only\"");
|
||||
let parsed: ContextVisibility = serde_json::from_str("\"agent_only\"").unwrap();
|
||||
assert_eq!(parsed, ContextVisibility::AgentOnly);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toolcategory_as_str() {
|
||||
assert_eq!(ToolCategory::Filesystem.as_str(), "filesystem");
|
||||
|
|
|
|||
Loading…
Reference in a new issue