Merge origin/main
This commit is contained in:
commit
b932bb696d
10 changed files with 2004 additions and 385 deletions
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[env]
|
||||
MTP_TYPE_MAPS = { value = "type-maps.yaml", relative = true }
|
||||
1484
Cargo.lock
generated
1484
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,7 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||
thiserror = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
stp-core = { git = "https://git.methanium.net/shoal/stp.git" }
|
||||
mtp = { path = "../mtp-npm", features = [] }
|
||||
tokio = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
171
src/agents.rs
171
src/agents.rs
|
|
@ -1,5 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/* ExploreAgent is intentionally limited to read-only tools so it can be
|
||||
dispatched without approval for safe, unrestricted codebase exploration. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AgentType {
|
||||
|
|
@ -8,6 +10,8 @@ pub enum AgentType {
|
|||
BuildAgent,
|
||||
ChatAgent,
|
||||
TestJudge,
|
||||
/* Lightweight read-only agent for exploration tasks. Never mutates files. */
|
||||
ExploreAgent,
|
||||
}
|
||||
|
||||
impl AgentType {
|
||||
|
|
@ -18,6 +22,7 @@ impl AgentType {
|
|||
AgentType::BuildAgent => "build_agent",
|
||||
AgentType::ChatAgent => "chat",
|
||||
AgentType::TestJudge => "test_judge",
|
||||
AgentType::ExploreAgent => "explore_agent",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -28,6 +33,7 @@ impl AgentType {
|
|||
"build_agent" => Some(AgentType::BuildAgent),
|
||||
"chat" => Some(AgentType::ChatAgent),
|
||||
"test_judge" => Some(AgentType::TestJudge),
|
||||
"explore_agent" => Some(AgentType::ExploreAgent),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -119,6 +125,171 @@ impl AgentPolicy {
|
|||
allowed_tools: vec![],
|
||||
forbidden_mutations: vec![],
|
||||
},
|
||||
/* ExploreAgent: auto-approved, read-only tools only, short timeout.
|
||||
Forbidden mutations set to wildcard to block all writes. */
|
||||
AgentType::ExploreAgent => Self {
|
||||
agent_type: agent_type.clone(),
|
||||
auto_approve: true,
|
||||
max_depth: 1,
|
||||
require_preview: false,
|
||||
timeout_seconds: 30,
|
||||
max_tokens: 4096,
|
||||
allowed_tools: vec![
|
||||
"read_file".into(),
|
||||
"list_directory".into(),
|
||||
"search_files".into(),
|
||||
"list_files".into(),
|
||||
"web_search".into(),
|
||||
"web_fetch".into(),
|
||||
],
|
||||
forbidden_mutations: vec!["*".into()],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Per-agent prompt configuration. Stored in the DB and resolved on task
|
||||
dispatch. Resolution order: task instructions > DB override > this config
|
||||
> compiled-in default > tool protocol. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentPromptConfig {
|
||||
pub agent_type: AgentType,
|
||||
/* Replaces or extends the compiled-in default prompt when set. */
|
||||
pub system_prompt: Option<String>,
|
||||
/* Rules appended after the main prompt, one per item. */
|
||||
pub append_rules: Vec<String>,
|
||||
/* When true, system_prompt fully replaces the default rather than appending. */
|
||||
pub override_defaults: bool,
|
||||
}
|
||||
|
||||
impl AgentPromptConfig {
|
||||
pub fn new(agent_type: AgentType) -> Self {
|
||||
Self {
|
||||
agent_type,
|
||||
system_prompt: None,
|
||||
append_rules: Vec::new(),
|
||||
override_defaults: false,
|
||||
}
|
||||
}
|
||||
|
||||
/* Applies this config on top of a base prompt. Returns the merged result. */
|
||||
pub fn apply(&self, base: &str) -> String {
|
||||
let body = if self.override_defaults {
|
||||
self.system_prompt.as_deref().unwrap_or(base).to_string()
|
||||
} else {
|
||||
match &self.system_prompt {
|
||||
Some(p) => format!("{}\n\n{}", base, p),
|
||||
None => base.to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
if self.append_rules.is_empty() {
|
||||
body
|
||||
} else {
|
||||
format!("{}\n\n{}", body, self.append_rules.join("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_agent_type_as_str_roundtrip() {
|
||||
let types = [
|
||||
AgentType::Planner,
|
||||
AgentType::DocAgent,
|
||||
AgentType::BuildAgent,
|
||||
AgentType::ChatAgent,
|
||||
AgentType::TestJudge,
|
||||
AgentType::ExploreAgent,
|
||||
];
|
||||
for agent_type in &types {
|
||||
let s = agent_type.as_str();
|
||||
let parsed = AgentType::from_str(s);
|
||||
assert_eq!(parsed.as_ref(), Some(agent_type), "roundtrip failed for {:?}", agent_type);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_type_from_str_unknown() {
|
||||
assert!(AgentType::from_str("unknown_agent").is_none());
|
||||
assert!(AgentType::from_str("").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_defaults_for_all_types() {
|
||||
let types = [
|
||||
AgentType::Planner,
|
||||
AgentType::DocAgent,
|
||||
AgentType::BuildAgent,
|
||||
AgentType::ChatAgent,
|
||||
AgentType::TestJudge,
|
||||
AgentType::ExploreAgent,
|
||||
];
|
||||
for agent_type in &types {
|
||||
let policy = AgentPolicy::defaults_for(agent_type);
|
||||
assert_eq!(&policy.agent_type, agent_type);
|
||||
assert!(policy.timeout_seconds > 0);
|
||||
assert!(policy.max_tokens > 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_explore_agent_is_auto_approved_read_only() {
|
||||
let policy = AgentPolicy::defaults_for(&AgentType::ExploreAgent);
|
||||
assert!(policy.auto_approve);
|
||||
assert_eq!(policy.max_depth, 1);
|
||||
assert!(policy.allowed_tools.contains(&"read_file".to_string()));
|
||||
assert!(policy.allowed_tools.contains(&"search_files".to_string()));
|
||||
assert!(!policy.allowed_tools.contains(&"write_file".to_string()));
|
||||
assert!(!policy.allowed_tools.contains(&"edit_file".to_string()));
|
||||
assert!(policy.forbidden_mutations.contains(&"*".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_agent_requires_approval() {
|
||||
let policy = AgentPolicy::defaults_for(&AgentType::BuildAgent);
|
||||
assert!(!policy.auto_approve);
|
||||
assert!(policy.max_tokens > 8192);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_prompt_config_apply_override() {
|
||||
let mut config = AgentPromptConfig::new(AgentType::BuildAgent);
|
||||
config.system_prompt = Some("Custom prompt".to_string());
|
||||
config.override_defaults = true;
|
||||
|
||||
let result = config.apply("Original base");
|
||||
assert_eq!(result, "Custom prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_prompt_config_apply_append() {
|
||||
let mut config = AgentPromptConfig::new(AgentType::DocAgent);
|
||||
config.system_prompt = Some("Extension".to_string());
|
||||
config.override_defaults = false;
|
||||
|
||||
let result = config.apply("Base prompt");
|
||||
assert!(result.starts_with("Base prompt"));
|
||||
assert!(result.contains("Extension"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_prompt_config_apply_rules() {
|
||||
let mut config = AgentPromptConfig::new(AgentType::Planner);
|
||||
config.append_rules = vec!["Rule 1".to_string(), "Rule 2".to_string()];
|
||||
|
||||
let result = config.apply("Base");
|
||||
assert!(result.contains("Rule 1"));
|
||||
assert!(result.contains("Rule 2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_prompt_config_no_overrides() {
|
||||
let config = AgentPromptConfig::new(AgentType::ChatAgent);
|
||||
let result = config.apply("Base prompt");
|
||||
assert_eq!(result, "Base prompt");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
src/lib.rs
12
src/lib.rs
|
|
@ -1,5 +1,6 @@
|
|||
pub use chrono::{DateTime, Utc};
|
||||
pub use stp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue};
|
||||
pub use mtp::codec::{CommunicationValue, DataValue};
|
||||
pub use mtp::type_map::{CommunicationType, DataType};
|
||||
pub use uuid::Uuid;
|
||||
|
||||
pub mod agents;
|
||||
|
|
@ -9,13 +10,14 @@ pub mod coral;
|
|||
pub mod enums;
|
||||
pub mod errors;
|
||||
pub mod krill;
|
||||
pub mod mutations;
|
||||
pub mod project;
|
||||
pub mod sync;
|
||||
pub mod task;
|
||||
pub mod todo;
|
||||
pub mod tools;
|
||||
|
||||
pub use agents::{AgentPolicy, AgentType, ApprovalMode, PlannedAction, SimulationResult};
|
||||
pub use agents::{AgentPolicy, AgentPromptConfig, AgentType, ApprovalMode, PlannedAction, SimulationResult};
|
||||
pub use ai_response::{Conversation, Message};
|
||||
pub use conclusion::{
|
||||
Artifact, ArtifactType, ConclusionCard, ConclusionMetrics, ConclusionTrigger,
|
||||
|
|
@ -24,12 +26,14 @@ pub use coral::{Coral, CoralId};
|
|||
pub use enums::*;
|
||||
pub use errors::{ShoalError, ValidationError};
|
||||
pub use krill::{KrillConfig, KrillDescriptor, KrillId};
|
||||
pub use mutations::FileMutation;
|
||||
pub use project::{Project, ProjectFile, ProjectId, ProjectSettings};
|
||||
pub use task::{Task, TaskResult};
|
||||
pub use todo::{Dependency, ToDo};
|
||||
pub use tools::{
|
||||
format_tool_error, format_tool_result, parse_tool_call_blocks, parse_tool_call_stream,
|
||||
tool_definitions, CompositionStep, ExecutionContext, MarketplaceSearchResults, ParsedToolCall,
|
||||
format_tool_error, format_tools_json, format_tool_result, parse_tool_call_blocks,
|
||||
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,
|
||||
|
|
|
|||
10
src/mutations.rs
Normal file
10
src/mutations.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A single file change inferred from a sandbox diff: `hash_before` absent means the file
|
||||
/// was created, `hash_after` absent means it was deleted, both present means it was modified.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FileMutation {
|
||||
pub file: String,
|
||||
pub hash_before: Option<String>,
|
||||
pub hash_after: Option<String>,
|
||||
}
|
||||
23
src/task.rs
23
src/task.rs
|
|
@ -174,6 +174,15 @@ pub struct FileRange {
|
|||
pub line_end: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConnectorConfig {
|
||||
pub api_key: String,
|
||||
pub base_url: String,
|
||||
pub model: String,
|
||||
pub max_tokens: Option<i32>,
|
||||
pub temperature: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentTaskSpec {
|
||||
pub task_id: Uuid,
|
||||
|
|
@ -194,6 +203,16 @@ pub struct AgentTaskSpec {
|
|||
pub agent_type: Option<AgentType>,
|
||||
#[serde(default)]
|
||||
pub suggested_test_files: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub project_id: Option<Uuid>,
|
||||
#[serde(default)]
|
||||
pub connector_config: Option<ConnectorConfig>,
|
||||
#[serde(default)]
|
||||
pub tools_json: Option<String>,
|
||||
/* Overrides the agent-type default system prompt when set. Takes highest
|
||||
precedence in the resolution chain (above DB overrides and defaults). */
|
||||
#[serde(default)]
|
||||
pub system_prompt: Option<String>,
|
||||
}
|
||||
|
||||
impl AgentTaskSpec {
|
||||
|
|
@ -221,6 +240,10 @@ impl AgentTaskSpec {
|
|||
session_budget: None,
|
||||
agent_type: None,
|
||||
suggested_test_files: Vec::new(),
|
||||
project_id: None,
|
||||
connector_config: None,
|
||||
tools_json: None,
|
||||
system_prompt: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
41
src/todo.rs
41
src/todo.rs
|
|
@ -4,7 +4,6 @@ use uuid::Uuid;
|
|||
|
||||
use crate::enums::{ToDoSource, ToDoStatus};
|
||||
use crate::errors::ValidationError;
|
||||
use crate::{CommunicationType, CommunicationValue, DataTypes, DataValue};
|
||||
use std::collections::HashSet;
|
||||
|
||||
const MAX_TITLE_LENGTH: usize = 500;
|
||||
|
|
@ -132,8 +131,8 @@ impl ToDo {
|
|||
false
|
||||
}
|
||||
|
||||
/// Performs full graph cycle detection given a map of todo_id -> dependencies.
|
||||
/// Returns the ID of the first todo that would participate in a cycle, or None.
|
||||
/* 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> {
|
||||
let mut visited: HashSet<Uuid> = HashSet::new();
|
||||
let mut in_stack: HashSet<Uuid> = HashSet::new();
|
||||
|
|
@ -250,42 +249,6 @@ impl ToDo {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<ToDo> for CommunicationValue {
|
||||
fn from(todo: ToDo) -> Self {
|
||||
let mut cv = CommunicationValue::new(CommunicationType::todo);
|
||||
cv = cv.add_data(DataTypes::id, DataValue::Str(todo.id.to_string()));
|
||||
cv = cv.add_data(DataTypes::title, DataValue::Str(todo.title));
|
||||
cv = cv.add_data(DataTypes::description, DataValue::Str(todo.description));
|
||||
cv = cv.add_data(DataTypes::status, DataValue::Str(todo.status.to_string()));
|
||||
cv = cv.add_data(DataTypes::todo_id, DataValue::Number(todo.priority as i64));
|
||||
|
||||
let depends: Vec<DataValue> = todo
|
||||
.depends_on
|
||||
.iter()
|
||||
.map(|u| DataValue::Str(u.to_string()))
|
||||
.collect();
|
||||
cv = cv.add_data(DataTypes::depends_on, DataValue::Array(depends));
|
||||
|
||||
cv = cv.add_data(
|
||||
DataTypes::created_at,
|
||||
DataValue::Str(todo.created_at.to_rfc3339()),
|
||||
);
|
||||
cv = cv.add_data(
|
||||
DataTypes::updated_at,
|
||||
DataValue::Str(todo.updated_at.to_rfc3339()),
|
||||
);
|
||||
cv = cv.add_data(
|
||||
DataTypes::user_id,
|
||||
DataValue::Str(todo.created_by.to_string()),
|
||||
);
|
||||
|
||||
if let Some(pid) = todo.project_id {
|
||||
cv = cv.add_data(DataTypes::project_id, DataValue::Str(pid.to_string()));
|
||||
}
|
||||
|
||||
cv
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
|
|
|||
578
src/tools.rs
578
src/tools.rs
|
|
@ -1,6 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Context in which a tool should be available
|
||||
/* Context in which a tool should be available */
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExecutionContext {
|
||||
|
|
@ -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,28 +72,30 @@ 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.
|
||||
/// Project-scoped tools are filtered out when no project is selected.
|
||||
/* Whether this tool requires a project context to function.
|
||||
Project-scoped tools are filtered out when no project is selected. */
|
||||
#[serde(default)]
|
||||
pub project_scoped: bool,
|
||||
/// Maximum execution time in milliseconds. Defaults to 30000 (30s).
|
||||
/* Maximum execution time in milliseconds. Defaults to 30000 (30s). */
|
||||
#[serde(default = "default_timeout_ms")]
|
||||
pub timeout_ms: u64,
|
||||
/// Whether this tool version is deprecated
|
||||
/* Whether this tool version is deprecated */
|
||||
#[serde(default)]
|
||||
pub deprecated: bool,
|
||||
/// Message shown when tool is deprecated, explaining migration path
|
||||
/* Message shown when tool is deprecated, explaining migration path */
|
||||
#[serde(default)]
|
||||
pub deprecation_message: String,
|
||||
/// Execution context indicating where this tool should be available
|
||||
/* 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",
|
||||
|
|
@ -86,15 +150,12 @@ impl ToolDefinition {
|
|||
}
|
||||
|
||||
pub fn to_short_doc(&self) -> String {
|
||||
format!(
|
||||
"- {}: {}\n",
|
||||
self.name, self.description
|
||||
)
|
||||
format!("- {}: {}\n", self.name, self.description)
|
||||
}
|
||||
|
||||
/// Strip the project_id parameter from the tool's JSON schema.
|
||||
/// This is used to hide the project context from agents that shouldn't
|
||||
/// have to manage it manually.
|
||||
/* Strip the project_id parameter from the tool's JSON schema.
|
||||
This is used to hide the project context from agents that shouldn't
|
||||
have to manage it manually. */
|
||||
pub fn strip_project_id(&mut self) {
|
||||
if let Some(obj) = self.parameters.as_object_mut() {
|
||||
if let Some(properties) = obj.get_mut("properties").and_then(|p| p.as_object_mut()) {
|
||||
|
|
@ -106,9 +167,9 @@ impl ToolDefinition {
|
|||
}
|
||||
}
|
||||
|
||||
/// Strip the board_id parameter from the tool's JSON schema.
|
||||
/// This is used to hide the board context from agents that shouldn't
|
||||
/// have to manage it manually (similar to project_id).
|
||||
/* Strip the board_id parameter from the tool's JSON schema.
|
||||
This is used to hide the board context from agents that shouldn't
|
||||
have to manage it manually (similar to project_id). */
|
||||
pub fn strip_board_id(&mut self) {
|
||||
if let Some(obj) = self.parameters.as_object_mut() {
|
||||
if let Some(properties) = obj.get_mut("properties").and_then(|p| p.as_object_mut()) {
|
||||
|
|
@ -120,12 +181,12 @@ impl ToolDefinition {
|
|||
}
|
||||
}
|
||||
|
||||
/// Check if this tool version is deprecated
|
||||
/* Check if this tool version is deprecated */
|
||||
pub fn is_deprecated(&self) -> bool {
|
||||
self.deprecated
|
||||
}
|
||||
|
||||
/// Get deprecation message with migration guidance
|
||||
/* Get deprecation message with migration guidance */
|
||||
pub fn deprecation_notice(&self) -> Option<&str> {
|
||||
if self.deprecated && !self.deprecation_message.is_empty() {
|
||||
Some(&self.deprecation_message)
|
||||
|
|
@ -134,40 +195,41 @@ impl ToolDefinition {
|
|||
}
|
||||
}
|
||||
|
||||
/// Compare tool versions. Returns true if self is newer than other.
|
||||
/// Uses simple semver comparison (major.minor.patch).
|
||||
/* Compare tool versions. Returns true if self is newer than other.
|
||||
Uses simple semver comparison (major.minor.patch). */
|
||||
pub fn is_newer_than(&self, other: &Self) -> bool {
|
||||
compare_versions(&self.version, &other.version) == std::cmp::Ordering::Greater
|
||||
}
|
||||
|
||||
/// Filter tools by execution context
|
||||
pub fn filter_by_context(tools: &[ToolDefinition], context: ExecutionContext) -> Vec<ToolDefinition> {
|
||||
/* Filter tools by execution context */
|
||||
pub fn filter_by_context(
|
||||
tools: &[ToolDefinition],
|
||||
context: ExecutionContext,
|
||||
) -> Vec<ToolDefinition> {
|
||||
match context {
|
||||
ExecutionContext::Both => tools.to_vec(),
|
||||
context => tools
|
||||
.iter()
|
||||
.filter(|t| t.execution_context == context || t.execution_context == ExecutionContext::Both)
|
||||
.filter(|t| {
|
||||
t.execution_context == context || t.execution_context == ExecutionContext::Both
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter tools to only include non-deprecated ones
|
||||
/* Filter tools to only include non-deprecated ones */
|
||||
pub fn filter_active(tools: &[ToolDefinition]) -> Vec<ToolDefinition> {
|
||||
tools
|
||||
.iter()
|
||||
.filter(|t| !t.deprecated)
|
||||
.cloned()
|
||||
.collect()
|
||||
tools.iter().filter(|t| !t.deprecated).cloned().collect()
|
||||
}
|
||||
|
||||
/// Check compatibility with another tool definition
|
||||
/* Check compatibility with another tool definition */
|
||||
pub fn is_compatible_with(&self, other: &Self) -> bool {
|
||||
self.name == other.name && !self.deprecated && !other.deprecated
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple semver comparison (major.minor.patch)
|
||||
/* Simple semver comparison (major.minor.patch) */
|
||||
fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
|
||||
let parse_version = |v: &str| -> Vec<u64> {
|
||||
v.split('.')
|
||||
|
|
@ -409,8 +471,8 @@ pub struct ToolErrorInfo {
|
|||
pub message: String,
|
||||
}
|
||||
|
||||
/// Extended version tracking for tool definitions.
|
||||
/// Provides schema versioning, deprecation notices, and migration guidance.
|
||||
/* Extended version tracking for tool definitions.
|
||||
Provides schema versioning, deprecation notices, and migration guidance. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolVersion {
|
||||
pub version: String,
|
||||
|
|
@ -433,8 +495,8 @@ impl Default for ToolVersion {
|
|||
}
|
||||
}
|
||||
|
||||
/// Execution context for tool filtering.
|
||||
/// Determines which tools are available in which execution environment.
|
||||
/* Execution context for tool filtering. Determines which tools are
|
||||
available in which execution environment (Reef vs. Krill vs. both). */
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ToolContext {
|
||||
|
|
@ -449,7 +511,7 @@ impl Default for ToolContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// Tool category classification for UI and filtering.
|
||||
/* Tool category classification for UI grouping and filtering. */
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ToolCategory {
|
||||
|
|
@ -484,8 +546,8 @@ impl ToolCategory {
|
|||
}
|
||||
}
|
||||
|
||||
/// A composed workflow of multiple tool steps executed in sequence
|
||||
/// with dependency-based ordering.
|
||||
/* A composed workflow of multiple tool steps executed in sequence
|
||||
with dependency-based ordering. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolComposition {
|
||||
pub id: String,
|
||||
|
|
@ -501,21 +563,20 @@ const fn default_composition_timeout_ms() -> u64 {
|
|||
60000
|
||||
}
|
||||
|
||||
/// A single step within a tool composition.
|
||||
/* A single step within a tool composition. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CompositionStep {
|
||||
pub tool: String,
|
||||
pub arguments: serde_json::Value,
|
||||
/// Indices of steps this step depends on (0-based).
|
||||
/// If None, the step has no dependencies and can run immediately.
|
||||
/* Indices (0-based) of steps this step depends on. None = no dependencies. */
|
||||
#[serde(default)]
|
||||
pub depends_on: Option<Vec<usize>>,
|
||||
/// Optional label for referencing step outputs
|
||||
/* Optional label for referencing step outputs. */
|
||||
#[serde(default)]
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
/// Declares a dependency of one tool on another tool version.
|
||||
/* Declares a dependency of one tool on another tool version. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolDependency {
|
||||
pub tool: String,
|
||||
|
|
@ -560,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) => {
|
||||
|
|
@ -577,29 +638,37 @@ 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(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Trait for implementing custom tool plugins.
|
||||
/// Plugins can be registered at runtime to extend the tool ecosystem.
|
||||
/* Trait for implementing custom tool plugins.
|
||||
Plugins register at runtime to extend the tool ecosystem. */
|
||||
pub trait ToolPlugin: Send + Sync {
|
||||
fn name(&self) -> &str;
|
||||
fn description(&self) -> &str;
|
||||
fn category(&self) -> &str;
|
||||
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
|
||||
}
|
||||
/* Defaults to true; callers must explicitly opt out of approval prompts. */
|
||||
fn requires_approval(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A marketplace listing for a published tool.
|
||||
/* A marketplace listing for a published tool. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolListing {
|
||||
pub id: String,
|
||||
|
|
@ -626,7 +695,7 @@ pub struct ToolListing {
|
|||
pub published_at: String,
|
||||
}
|
||||
|
||||
/// Results from a marketplace search.
|
||||
/* Results from a marketplace search. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MarketplaceSearchResults {
|
||||
pub listings: Vec<ToolListing>,
|
||||
|
|
@ -635,7 +704,7 @@ pub struct MarketplaceSearchResults {
|
|||
pub page_size: usize,
|
||||
}
|
||||
|
||||
/// A documented example for a tool.
|
||||
/* A documented example for a tool. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolExample {
|
||||
pub id: String,
|
||||
|
|
@ -654,7 +723,7 @@ pub struct ToolExample {
|
|||
pub author: String,
|
||||
}
|
||||
|
||||
/// A single test case for a tool.
|
||||
/* A single test case for a tool. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolTest {
|
||||
pub name: String,
|
||||
|
|
@ -671,7 +740,7 @@ const fn default_test_timeout() -> u64 {
|
|||
30000
|
||||
}
|
||||
|
||||
/// A suite of tests for a specific tool.
|
||||
/* A suite of tests for a specific tool. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolTestSuite {
|
||||
pub tool_name: String,
|
||||
|
|
@ -682,14 +751,14 @@ pub struct ToolTestSuite {
|
|||
pub teardown_actions: Vec<TestAction>,
|
||||
}
|
||||
|
||||
/// A setup or teardown action for a test suite.
|
||||
/* A setup or teardown action for a test suite. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TestAction {
|
||||
pub tool: String,
|
||||
pub arguments: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Result of running a single tool test.
|
||||
/* Result of running a single tool test. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolTestResult {
|
||||
pub test_name: String,
|
||||
|
|
@ -701,7 +770,7 @@ pub struct ToolTestResult {
|
|||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
/// Results of running a full test suite.
|
||||
/* Results of running a full test suite. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolTestSuiteResult {
|
||||
pub tool_name: String,
|
||||
|
|
@ -712,7 +781,7 @@ pub struct ToolTestSuiteResult {
|
|||
pub total_duration_ms: u64,
|
||||
}
|
||||
|
||||
/// Generated documentation for a tool.
|
||||
/* Generated documentation for a tool. */
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolDocumentation {
|
||||
pub tool_name: String,
|
||||
|
|
@ -733,9 +802,27 @@ pub struct ToolDocumentation {
|
|||
pub examples: Vec<ToolExample>,
|
||||
}
|
||||
|
||||
/// Returns all built-in tool definitions across all categories.
|
||||
/* 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
|
||||
// ========================
|
||||
|
|
@ -1099,23 +1186,25 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
|||
),
|
||||
tool!(
|
||||
"kanban_create_todo",
|
||||
"Create a new todo item in a Kanban column",
|
||||
"kanban",
|
||||
false, false, true,
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": { "type": "string", "description": "Project ID" },
|
||||
"board_id": { "type": "string", "description": "Board ID (defaults to project's primary board)" },
|
||||
"column_id": { "type": "string", "description": "Column ID to place the todo" },
|
||||
"Create a new todo item in a Kanban board",
|
||||
"kanban",
|
||||
false, false, true,
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": { "type": "string", "description": "Project ID" },
|
||||
"board_id": { "type": "string", "description": "Board ID (defaults to project's primary board)" },
|
||||
"status": { "type": "string", "enum": ["pending","in_progress","completed","blocked","ready_for_agent","delegated","failed","pending_approval"], "description": "Initial status of the todo" },
|
||||
|
||||
"title": { "type": "string", "description": "Title of the todo" },
|
||||
"description": { "type": "string", "description": "Optional description" },
|
||||
"priority": { "type": "number", "description": "Priority (1-1000)" },
|
||||
"deploy_agent": { "type": "boolean", "description": "Whether to deploy an agent for this todo" },
|
||||
"agent_prompt": { "type": "string", "description": "Optional agent prompt if deploy_agent is true" },
|
||||
"agent_task_details": { "type": "string", "description": "Optional agent task details" }
|
||||
"agent_task_details": { "type": "string", "description": "Optional agent task details" },
|
||||
"parent_todo_id": { "type": "string", "description": "Optional parent todo ID" }
|
||||
},
|
||||
"required": ["project_id", "column_id", "title"]
|
||||
"required": ["project_id", "title"]
|
||||
})
|
||||
),
|
||||
tool!(
|
||||
|
|
@ -1130,7 +1219,7 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
|||
"todo_id": { "type": "string", "description": "ID of the todo to update" },
|
||||
"title": { "type": "string", "description": "New title" },
|
||||
"description": { "type": "string", "description": "New description" },
|
||||
"status": { "type": "string", "description": "New status (pending, in_progress, completed, blocked)" },
|
||||
"status": { "type": "string", "enum": ["pending","in_progress","completed","blocked","ready_for_agent","delegated","failed","pending_approval"], "description": "New status" },
|
||||
"priority": { "type": "number", "description": "New priority" }
|
||||
},
|
||||
"required": ["project_id", "todo_id"]
|
||||
|
|
@ -1152,19 +1241,19 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
|||
),
|
||||
tool!(
|
||||
"kanban_move_todo",
|
||||
"Move a todo to a different column or reorder it",
|
||||
"kanban",
|
||||
false, false, true,
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": { "type": "string", "description": "Project ID" },
|
||||
"todo_id": { "type": "string", "description": "ID of the todo to move" },
|
||||
"column_id": { "type": "string", "description": "Target column ID" },
|
||||
"task_order": { "type": "number", "description": "New order position" }
|
||||
},
|
||||
"required": ["project_id", "todo_id", "column_id"]
|
||||
})
|
||||
"Move a todo item to a different status",
|
||||
"kanban",
|
||||
false, false, true,
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": { "type": "string", "description": "Project ID" },
|
||||
"todo_id": { "type": "string", "description": "ID of the todo to move" },
|
||||
"status": { "type": "string", "enum": ["pending","in_progress","completed","blocked","ready_for_agent","delegated","failed","pending_approval"], "description": "Target status" },
|
||||
"task_order": { "type": "number", "description": "New order position" }
|
||||
},
|
||||
"required": ["project_id", "todo_id"]
|
||||
})
|
||||
),
|
||||
tool!(
|
||||
"kanban_create_task",
|
||||
|
|
@ -1215,23 +1304,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
|||
"required": ["project_id", "todo_id", "tag_name"]
|
||||
})
|
||||
),
|
||||
tool!(
|
||||
"kanban_create_column",
|
||||
"Create a new column in the Kanban board",
|
||||
"kanban",
|
||||
false, false, true,
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": { "type": "string", "description": "Project ID" },
|
||||
"board_id": { "type": "string", "description": "Board ID (defaults to project's primary board)" },
|
||||
"name": { "type": "string", "description": "Column name" },
|
||||
"column_order": { "type": "number", "description": "Display order" }
|
||||
},
|
||||
"required": ["project_id", "name"]
|
||||
})
|
||||
),
|
||||
|
||||
// ========================
|
||||
// Documentation Tools
|
||||
// ========================
|
||||
|
|
@ -1412,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"]
|
||||
|
|
@ -1548,7 +1620,125 @@ 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.
|
||||
|
||||
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 => 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 => curated(&[
|
||||
"read_file",
|
||||
"list_directory",
|
||||
"search_files",
|
||||
"list_files",
|
||||
"web_search",
|
||||
"web_fetch",
|
||||
]),
|
||||
})
|
||||
}
|
||||
|
||||
/* Formats tool definitions as OpenAI-compatible function-calling JSON. */
|
||||
pub fn format_tools_json(tools: &[ToolDefinition]) -> Option<String> {
|
||||
let formatted: Vec<serde_json::Value> = tools
|
||||
.iter()
|
||||
.map(|t| {
|
||||
serde_json::json!({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": t.name,
|
||||
"description": t.description,
|
||||
"parameters": t.parameters,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
serde_json::to_string(&formatted).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -2014,12 +2204,135 @@ Done with tools.
|
|||
|
||||
#[test]
|
||||
fn test_execution_context_default() {
|
||||
assert_eq!(ExecutionContext::default(), ExecutionContext::Both);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_visibility_default() {
|
||||
assert_eq!(ContextVisibility::default(), ContextVisibility::Everywhere);
|
||||
assert_eq!(
|
||||
ExecutionContext::default(),
|
||||
ExecutionContext::Both
|
||||
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");
|
||||
|
|
@ -2043,7 +2356,10 @@ Done with tools.
|
|||
"```\nSome trailing text.",
|
||||
];
|
||||
let results = parse_tool_call_stream(chunks.into_iter());
|
||||
assert!(results.is_empty(), "tool-result: at stream start should be rejected");
|
||||
assert!(
|
||||
results.is_empty(),
|
||||
"tool-result: at stream start should be rejected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2053,7 +2369,10 @@ Done with tools.
|
|||
"```tool-result:write_file\n{\"call_id\": \"c2\"}\n```\n",
|
||||
];
|
||||
let results = parse_tool_call_stream(chunks.into_iter());
|
||||
assert!(results.is_empty(), "multiple tool-result: blocks should all be rejected");
|
||||
assert!(
|
||||
results.is_empty(),
|
||||
"multiple tool-result: blocks should all be rejected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2064,15 +2383,24 @@ Done with tools.
|
|||
"```tool:search_files\n{\"call_id\": \"c3\"}\n```\n",
|
||||
];
|
||||
let results = parse_tool_call_stream(chunks.into_iter());
|
||||
assert_eq!(results.len(), 2, "should only parse the tool: blocks, not tool-result:");
|
||||
assert_eq!(
|
||||
results.len(),
|
||||
2,
|
||||
"should only parse the tool: blocks, not tool-result:"
|
||||
);
|
||||
assert_eq!(results[0].call_id, "c1");
|
||||
assert_eq!(results[1].call_id, "c3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_streaming_tool_result_only_chunk() {
|
||||
let results = parse_tool_call_stream(vec!["```tool-result:read_file\n{\"call_id\": \"c1\"}\n```\n"].into_iter());
|
||||
assert!(results.is_empty(), "tool-result: as only chunk content should be rejected");
|
||||
let results = parse_tool_call_stream(
|
||||
vec!["```tool-result:read_file\n{\"call_id\": \"c1\"}\n```\n"].into_iter(),
|
||||
);
|
||||
assert!(
|
||||
results.is_empty(),
|
||||
"tool-result: as only chunk content should be rejected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2083,12 +2411,20 @@ Done with tools.
|
|||
"Some text after.",
|
||||
];
|
||||
let results = parse_tool_call_stream(chunks.into_iter());
|
||||
assert!(results.is_empty(), "tool-result: in middle of text should be rejected");
|
||||
assert!(
|
||||
results.is_empty(),
|
||||
"tool-result: in middle of text should be rejected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_streaming_finish_rejects_tool_result_no_closing_fence() {
|
||||
let results = parse_tool_call_stream(vec!["```tool-result:read_file\n{\"call_id\": \"c1\"}\n"].into_iter());
|
||||
assert!(results.is_empty(), "tool-result: without closing fence should be rejected by finish()");
|
||||
let results = parse_tool_call_stream(
|
||||
vec!["```tool-result:read_file\n{\"call_id\": \"c1\"}\n"].into_iter(),
|
||||
);
|
||||
assert!(
|
||||
results.is_empty(),
|
||||
"tool-result: without closing fence should be rejected by finish()"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
118
type-maps.yaml
Normal file
118
type-maps.yaml
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
protocol_version: "1.0"
|
||||
|
||||
type_maps:
|
||||
"1.0":
|
||||
CommunicationTypes:
|
||||
ReservoirHeartbeat: 32
|
||||
ReservoirRequest: 33
|
||||
ReservoirStore: 34
|
||||
AuthRequest: 35
|
||||
AuthResponse: 36
|
||||
ProjectRequest: 37
|
||||
ProjectResponse: 38
|
||||
KanbanRequest: 39
|
||||
KanbanResponse: 40
|
||||
DiscussRequest: 41
|
||||
DiscussResponse: 42
|
||||
DiscussStreamChunk: 43
|
||||
DiscussStreamDone: 44
|
||||
ToolRequest: 45
|
||||
ToolResponse: 46
|
||||
AgentRequest: 47
|
||||
AgentResponse: 48
|
||||
AdminRequest: 49
|
||||
AdminResponse: 50
|
||||
SyncRequest: 51
|
||||
SyncResponse: 52
|
||||
PodRequest: 53
|
||||
PodResponse: 54
|
||||
LookoutEvent: 55
|
||||
EventBroadcast: 56
|
||||
ErrorResponse: 57
|
||||
KrillIoInitialize: 58
|
||||
KrillIoInitialized: 59
|
||||
KrillIoAdvertiseSkills: 60
|
||||
KrillIoSkillsAdvertised: 61
|
||||
KrillIoQuerySkills: 62
|
||||
KrillIoQuerySkillsResponse: 63
|
||||
KrillIoMessage: 64
|
||||
KrillIoMessageResponse: 65
|
||||
KrillIoToolCallRequest: 66
|
||||
KrillIoToolCallResponse: 67
|
||||
KrillIoListTools: 68
|
||||
KrillIoListToolsResponse: 69
|
||||
KrillIoPauseSession: 70
|
||||
KrillIoResumeSession: 71
|
||||
KrillIoTerminateSession: 72
|
||||
KrillIoSessionStateChanged: 73
|
||||
KrillIoHeartbeat: 74
|
||||
KrillIoHeartbeatResponse: 75
|
||||
KrillIoLoadModel: 76
|
||||
KrillIoModelLoaded: 77
|
||||
KrillIoUnloadModel: 78
|
||||
KrillIoMemoryReport: 79
|
||||
KrillIoImmediateKill: 80
|
||||
KrillIoKilled: 81
|
||||
PodTaskAvailable: 82
|
||||
PodDiscussAbort: 83
|
||||
DataTypes:
|
||||
ProjectId: 32
|
||||
RequestType: 33
|
||||
ResponseData: 34
|
||||
Data: 35
|
||||
FilePath: 36
|
||||
FileData: 37
|
||||
SessionId: 38
|
||||
KrillId: 39
|
||||
KrillIoVersion: 40
|
||||
MaxHistoryLength: 41
|
||||
DefaultTimeoutMs: 42
|
||||
Enabled: 43
|
||||
ToolError: 44
|
||||
SkillsJson: 45
|
||||
Content: 46
|
||||
MessageRole: 47
|
||||
Response: 48
|
||||
TokensUsed: 49
|
||||
ToolId: 50
|
||||
ToolCallId: 51
|
||||
ToolArguments: 52
|
||||
Timeout: 53
|
||||
ToolSuccess: 54
|
||||
ExecutionTimeMs: 55
|
||||
ToolResult: 56
|
||||
ToolDefinitionsList: 57
|
||||
Graceful: 58
|
||||
SessionState: 59
|
||||
ModelPath: 60
|
||||
LoadIntoVram: 61
|
||||
WeightsPath: 62
|
||||
Path: 63
|
||||
RamUsageMb: 64
|
||||
VramMb: 65
|
||||
KillReason: 66
|
||||
SaveCheckpoint: 67
|
||||
AuthToken: 68
|
||||
UserId: 69
|
||||
Username: 70
|
||||
Email: 71
|
||||
Password: 72
|
||||
BoardId: 73
|
||||
TodoId: 74
|
||||
TaskId: 75
|
||||
TagId: 76
|
||||
ChatId: 77
|
||||
MessageId: 78
|
||||
AgentRunId: 79
|
||||
Payload: 80
|
||||
EventType: 81
|
||||
EventPayload: 82
|
||||
StreamSessionId: 83
|
||||
ContentChunk: 84
|
||||
StreamComplete: 85
|
||||
FileHash: 86
|
||||
FileSize: 87
|
||||
FileContent: 88
|
||||
ErrorCode: 89
|
||||
PageOffset: 90
|
||||
PageLimit: 91
|
||||
Loading…
Reference in a new issue