MTP-0.2.0 migration

This commit is contained in:
Alex Emmet 2026-07-20 19:00:12 +02:00
commit f1c7317fab
14 changed files with 1309 additions and 281 deletions

View file

@ -1,2 +1,2 @@
[env] [env]
MTP_TYPE_MAPS = { value = "type-maps.yaml", relative = true } MTP_TYPE_MAPS = { value = "../type-maps.yaml", relative = true }

658
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,9 +10,12 @@ chrono = { version = "0.4", features = ["serde"] }
thiserror = "2" thiserror = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
mtp = { git = "https://git@git.methanium.net/methanium/mtp.git", features = [] } mtp = { git = "https://git@git.methanium.net/methanium/mtp.git", features = [
"crypto",
] }
tokio = "1" tokio = "1"
async-trait = "0.1" async-trait = "0.1"
url = "2"
[dev-dependencies] [dev-dependencies]
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }

View file

@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::task::SessionBudget;
/* ExploreAgent is intentionally limited to read-only tools so it can be /* ExploreAgent is intentionally limited to read-only tools so it can be
dispatched without approval for safe, unrestricted codebase exploration. */ dispatched without approval for safe, unrestricted codebase exploration. */
@ -37,6 +38,35 @@ impl AgentType {
_ => None, _ => None,
} }
} }
pub fn default_session_budget(&self) -> SessionBudget {
match self {
AgentType::BuildAgent => SessionBudget {
max_tool_calls: 50,
max_tokens: 100_000,
},
AgentType::Planner => SessionBudget {
max_tool_calls: 30,
max_tokens: 50_000,
},
AgentType::ExploreAgent => SessionBudget {
max_tool_calls: 10,
max_tokens: 5_000,
},
AgentType::DocAgent => SessionBudget {
max_tool_calls: 20,
max_tokens: 20_000,
},
AgentType::TestJudge => SessionBudget {
max_tool_calls: 10,
max_tokens: 10_000,
},
AgentType::ChatAgent => SessionBudget {
max_tool_calls: 5,
max_tokens: 5_000,
},
}
}
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -46,6 +76,24 @@ pub enum ApprovalMode {
PerTool, PerTool,
} }
/// Controls what happens when a tool call requires approval and the user defers it.
/// Orthogonal to `ApprovalMode` (which controls *how* approvals are collected).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalTurnMode {
/// Block the agent turn; resume by injecting the approval result into the same turn.
Wait,
/// Return a placeholder result to the agent immediately ("Approval pending");
/// the actual approval result is injected in a subsequent turn.
Defer,
}
impl Default for ApprovalTurnMode {
fn default() -> Self {
ApprovalTurnMode::Wait
}
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlannedAction { pub struct PlannedAction {
pub tool_name: String, pub tool_name: String,
@ -70,6 +118,10 @@ pub struct AgentPolicy {
pub max_tokens: u32, pub max_tokens: u32,
pub allowed_tools: Vec<String>, pub allowed_tools: Vec<String>,
pub forbidden_mutations: Vec<String>, pub forbidden_mutations: Vec<String>,
/// Controls what happens when a tool call is deferred by the user.
/// `Wait` pauses the turn; `Defer` returns a placeholder immediately.
#[serde(default)]
pub turn_mode: ApprovalTurnMode,
} }
impl AgentPolicy { impl AgentPolicy {
@ -84,6 +136,7 @@ impl AgentPolicy {
max_tokens: 4096, max_tokens: 4096,
allowed_tools: vec!["read_file".into(), "write_file".into(), "edit_file".into()], allowed_tools: vec!["read_file".into(), "write_file".into(), "edit_file".into()],
forbidden_mutations: vec!["Cargo.toml".into(), "package.json".into()], forbidden_mutations: vec!["Cargo.toml".into(), "package.json".into()],
turn_mode: ApprovalTurnMode::Defer,
}, },
AgentType::TestJudge => Self { AgentType::TestJudge => Self {
agent_type: agent_type.clone(), agent_type: agent_type.clone(),
@ -94,6 +147,7 @@ impl AgentPolicy {
max_tokens: 4096, max_tokens: 4096,
allowed_tools: vec![], allowed_tools: vec![],
forbidden_mutations: vec![], forbidden_mutations: vec![],
turn_mode: ApprovalTurnMode::Defer,
}, },
AgentType::BuildAgent => Self { AgentType::BuildAgent => Self {
agent_type: agent_type.clone(), agent_type: agent_type.clone(),
@ -104,6 +158,7 @@ impl AgentPolicy {
max_tokens: 131072, max_tokens: 131072,
allowed_tools: vec![], allowed_tools: vec![],
forbidden_mutations: vec!["Cargo.toml".into(), "package.json".into()], forbidden_mutations: vec!["Cargo.toml".into(), "package.json".into()],
turn_mode: ApprovalTurnMode::Wait,
}, },
AgentType::Planner => Self { AgentType::Planner => Self {
agent_type: agent_type.clone(), agent_type: agent_type.clone(),
@ -114,6 +169,7 @@ impl AgentPolicy {
max_tokens: 131072, max_tokens: 131072,
allowed_tools: vec![], allowed_tools: vec![],
forbidden_mutations: vec![], forbidden_mutations: vec![],
turn_mode: ApprovalTurnMode::Defer,
}, },
AgentType::ChatAgent => Self { AgentType::ChatAgent => Self {
agent_type: agent_type.clone(), agent_type: agent_type.clone(),
@ -124,6 +180,7 @@ impl AgentPolicy {
max_tokens: 4096, max_tokens: 4096,
allowed_tools: vec![], allowed_tools: vec![],
forbidden_mutations: vec![], forbidden_mutations: vec![],
turn_mode: ApprovalTurnMode::Defer,
}, },
/* ExploreAgent: auto-approved, read-only tools only, short timeout. /* ExploreAgent: auto-approved, read-only tools only, short timeout.
Forbidden mutations set to wildcard to block all writes. */ Forbidden mutations set to wildcard to block all writes. */
@ -143,6 +200,7 @@ impl AgentPolicy {
"web_fetch".into(), "web_fetch".into(),
], ],
forbidden_mutations: vec!["*".into()], forbidden_mutations: vec!["*".into()],
turn_mode: ApprovalTurnMode::Defer,
}, },
} }
} }

View file

@ -21,53 +21,144 @@ pub struct AiConversation {
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamEvent { #[serde(tag = "type")]
#[serde(rename = "type")] pub enum StreamEvent {
pub event_type: String, #[serde(rename = "content")]
pub content: Option<String>, Content { content: String },
pub thinking: Option<String>,
pub error: Option<String>, #[serde(rename = "thinking")]
pub message_id: Option<String>, Thinking { content: String },
#[serde(rename = "error")]
Error { error: String },
#[serde(rename = "done")]
Done { message_id: String },
#[serde(rename = "tool_call")]
ToolCall {
tool_name: String,
tool_id: String,
arguments: String,
},
#[serde(rename = "tool_executing")]
ToolExecuting { tool_name: String, tool_id: String },
#[serde(rename = "tool_result")]
ToolResult {
tool_name: String,
tool_id: String,
status: String,
result: String,
},
#[serde(rename = "checkpoint")]
Checkpoint {
checkpoint_id: String,
data: serde_json::Value,
},
#[serde(rename = "simulation_result")]
SimulationResult { simulation: serde_json::Value },
#[serde(rename = "debug")]
Debug { content: String },
#[serde(rename = "handshake")]
Handshake {
protocol_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
warmup: Option<bool>,
},
#[serde(rename = "task_complete")]
TaskComplete,
} }
impl StreamEvent { impl StreamEvent {
pub fn content(content: String) -> Self { pub fn content(content: String) -> Self {
Self { Self::Content { content }
event_type: "content".to_string(),
content: Some(content),
thinking: None,
error: None,
message_id: None,
}
} }
pub fn thinking(thinking: String) -> Self { pub fn thinking(content: String) -> Self {
Self { Self::Thinking { content }
event_type: "thinking".to_string(),
content: None,
thinking: Some(thinking),
error: None,
message_id: None,
}
} }
pub fn error(error: String) -> Self { pub fn error(error: String) -> Self {
Self { Self::Error { error }
event_type: "error".to_string(),
content: None,
thinking: None,
error: Some(error),
message_id: None,
}
} }
pub fn done(message_id: String) -> Self { pub fn done(message_id: String) -> Self {
Self { Self::Done { message_id }
event_type: "done".to_string(), }
content: None,
thinking: None, pub fn tool_call(tool_name: String, tool_id: String, arguments: String) -> Self {
error: None, Self::ToolCall {
message_id: Some(message_id), tool_name,
tool_id,
arguments,
}
}
pub fn tool_executing(tool_name: String, tool_id: String) -> Self {
Self::ToolExecuting { tool_name, tool_id }
}
pub fn tool_result(
tool_name: String,
tool_id: String,
status: String,
result: String,
) -> Self {
Self::ToolResult {
tool_name,
tool_id,
status,
result,
}
}
pub fn checkpoint(checkpoint_id: String, data: serde_json::Value) -> Self {
Self::Checkpoint {
checkpoint_id,
data,
}
}
pub fn simulation_result(simulation: serde_json::Value) -> Self {
Self::SimulationResult { simulation }
}
pub fn debug(content: String) -> Self {
Self::Debug { content }
}
pub fn handshake(protocol_version: String, warmup: Option<bool>) -> Self {
Self::Handshake {
protocol_version,
warmup,
}
}
pub fn task_complete() -> Self {
Self::TaskComplete
}
pub fn event_type(&self) -> &str {
match self {
Self::Content { .. } => "content",
Self::Thinking { .. } => "thinking",
Self::Error { .. } => "error",
Self::Done { .. } => "done",
Self::ToolCall { .. } => "tool_call",
Self::ToolExecuting { .. } => "tool_executing",
Self::ToolResult { .. } => "tool_result",
Self::Checkpoint { .. } => "checkpoint",
Self::SimulationResult { .. } => "simulation_result",
Self::Debug { .. } => "debug",
Self::Handshake { .. } => "handshake",
Self::TaskComplete => "task_complete",
} }
} }
} }

View file

@ -1,6 +1,38 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToDoMode {
Discussion,
Finalized,
}
impl Default for ToDoMode {
fn default() -> Self {
ToDoMode::Discussion
}
}
impl fmt::Display for ToDoMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ToDoMode::Discussion => write!(f, "discussion"),
ToDoMode::Finalized => write!(f, "finalized"),
}
}
}
impl std::str::FromStr for ToDoMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"discussion" => Ok(ToDoMode::Discussion),
"finalized" => Ok(ToDoMode::Finalized),
_ => Err(format!("unknown mode: {}", s)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToDoStatus { pub enum ToDoStatus {
Pending, Pending,

View file

@ -20,6 +20,21 @@ pub enum ValidationError {
InvalidField(String), InvalidField(String),
} }
impl ValidationError {
pub fn http_status(&self) -> u16 {
match self {
ValidationError::EmptyTitle
| ValidationError::TitleTooLong
| ValidationError::DescriptionTooLong
| ValidationError::InvalidPriority
| ValidationError::InvalidStatusTransition { .. }
| ValidationError::InvalidField(_) => 400,
ValidationError::CircularDependency(_)
| ValidationError::DependencyNotFound(_) => 409,
}
}
}
#[derive(Debug, Clone, Error)] #[derive(Debug, Clone, Error)]
pub enum ShoalError { pub enum ShoalError {
#[error("storage error: {0}")] #[error("storage error: {0}")]
@ -38,6 +53,20 @@ pub enum ShoalError {
InternalError(String), InternalError(String),
} }
impl ShoalError {
pub fn http_status(&self) -> u16 {
match self {
ShoalError::ValidationError(_) => 400,
ShoalError::NotFound(_) => 404,
ShoalError::Unauthorized(_) => 401,
ShoalError::StorageError(_)
| ShoalError::NetworkError(_)
| ShoalError::DatabaseError(_)
| ShoalError::InternalError(_) => 500,
}
}
}
impl From<ValidationError> for ShoalError { impl From<ValidationError> for ShoalError {
fn from(e: ValidationError) -> Self { fn from(e: ValidationError) -> Self {
ShoalError::ValidationError(e.to_string()) ShoalError::ValidationError(e.to_string())

View file

@ -3,6 +3,15 @@ use uuid::Uuid;
use crate::enums::ModelType; use crate::enums::ModelType;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum PodProtocolVersion {
V1,
}
impl Default for PodProtocolVersion {
fn default() -> Self { Self::V1 }
}
pub type KrillId = Uuid; pub type KrillId = Uuid;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -76,12 +85,14 @@ impl KrillDescriptor {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct KrillConfig { pub struct KrillConfig {
pub settings: serde_json::Value, pub settings: serde_json::Value,
pub pod_protocol_version: PodProtocolVersion,
} }
impl KrillConfig { impl KrillConfig {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
settings: serde_json::json!({}), settings: serde_json::json!({}),
pod_protocol_version: PodProtocolVersion::default(),
} }
} }

View file

@ -28,17 +28,44 @@ pub use conclusion::{
pub use coral::{Coral, CoralId}; pub use coral::{Coral, CoralId};
pub use enums::*; pub use enums::*;
pub use errors::{ShoalError, ValidationError}; pub use errors::{ShoalError, ValidationError};
pub use krill::{KrillConfig, KrillDescriptor, KrillId}; pub use krill::{KrillConfig, KrillDescriptor, KrillId, PodProtocolVersion};
pub use mutations::FileMutation; pub use mutations::FileMutation;
pub use project::{Project, ProjectFile, ProjectId, ProjectSettings}; pub use project::{Project, ProjectFile, ProjectId, ProjectSettings};
pub use task::{Task, TaskResult}; pub use task::{Task, TaskResult};
pub use todo::{Dependency, ToDo}; pub use todo::{Dependency, ToDo};
pub use tools::{ pub use tools::{
format_tool_error, format_tool_result, format_tools_json, is_global_tool, is_reef_proxy_tool, compute_tools_hash, format_tool_error, format_tool_result, format_tools_json,
is_global_tool, is_reef_proxy_tool,
parse_tool_call_blocks, parse_tool_call_stream, tool_definitions, tool_ids_for_agent_type, parse_tool_call_blocks, parse_tool_call_stream, tool_definitions, tool_ids_for_agent_type,
ApprovalRequirement, CompositionStep, ContextVisibility, ExecutionContext, ApprovalRequirement, CompositionStep, ContextVisibility, ExecutionContext,
MarketplaceSearchResults, ParsedToolCall, TestAction, ToolComposition, ToolDefinition, MarketplaceSearchResults, ParsedToolCall, TestAction, ToolComposition, ToolDefinition,
ToolDependency, ToolDocumentation, ToolExample, ToolExecutor, ToolListing, ToolParser, ToolDependency, ToolDocumentation, ToolExample, ToolExecutor, ToolListing, ToolParser,
ToolPlugin, ToolRegistry, ToolTest, ToolTestResult, ToolTestSuite, ToolTestSuiteResult, ToolPlugin, ToolRegistry, ToolState, ToolTest, ToolTestResult, ToolTestSuite,
ToolVersion, GLOBAL_TOOLS, REEF_PROXY_TOOLS, ToolTestSuiteResult, ToolVersion, GLOBAL_TOOLS, REEF_PROXY_TOOLS,
}; };
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn typemap_contains_expected_types() {
let tm = mtp::type_map::TypeMap::latest();
assert!(
tm.data_id_enum(DataType::ProjectId).is_some(),
"TypeMap missing ProjectId"
);
assert!(
tm.comm_id_enum(CommunicationType::AuthRequest).is_some(),
"TypeMap missing AuthRequest"
);
assert!(
tm.comm_id_enum(CommunicationType::PasswordAuthRequest).is_some(),
"TypeMap missing PasswordAuthRequest"
);
assert!(
tm.data_id_enum(DataType::DockId).is_some(),
"TypeMap missing DockId"
);
}
}

View file

@ -110,6 +110,8 @@ pub struct ProjectSettings {
pub language: Option<String>, pub language: Option<String>,
pub build_command: Option<String>, pub build_command: Option<String>,
pub run_command: Option<String>, pub run_command: Option<String>,
/// Conventional subject/body template used for generated git commits.
pub git_commit_template: Option<String>,
#[serde(flatten)] #[serde(flatten)]
pub extra: serde_json::Value, pub extra: serde_json::Value,
} }
@ -120,6 +122,7 @@ impl ProjectSettings {
language: None, language: None,
build_command: None, build_command: None,
run_command: None, run_command: None,
git_commit_template: None,
extra: serde_json::json!({}), extra: serde_json::json!({}),
} }
} }
@ -139,6 +142,11 @@ impl ProjectSettings {
self self
} }
pub fn with_git_commit_template(mut self, template: String) -> Self {
self.git_commit_template = Some(template);
self
}
pub fn set_extra(&mut self, key: &str, value: serde_json::Value) { pub fn set_extra(&mut self, key: &str, value: serde_json::Value) {
if let Some(obj) = self.extra.as_object_mut() { if let Some(obj) = self.extra.as_object_mut() {
obj.insert(key.to_string(), value); obj.insert(key.to_string(), value);

View file

@ -183,6 +183,61 @@ pub struct ConnectorConfig {
pub temperature: Option<f32>, pub temperature: Option<f32>,
} }
impl ConnectorConfig {
pub const MIN_API_KEY_LEN: usize = 20;
/// Validate the connector configuration fields. Returns a list of human-readable
/// error messages; an empty vec means the config is valid.
pub fn validate(&self) -> Vec<String> {
let mut errors = Vec::new();
if self.api_key.trim().len() < Self::MIN_API_KEY_LEN {
errors.push(format!(
"api_key must be non-empty and at least {} characters",
Self::MIN_API_KEY_LEN
));
}
match url::Url::parse(&self.base_url) {
Ok(parsed) => {
if parsed.scheme() != "https" && parsed.scheme() != "http" {
errors.push(format!(
"base_url must use https or http scheme, got '{}'",
parsed.scheme()
));
}
}
Err(e) => {
errors.push(format!("base_url is not a valid URL: {}", e));
}
}
if self.model.trim().is_empty() {
errors.push("model must be non-empty".to_string());
}
if let Some(tokens) = self.max_tokens {
if tokens <= 0 {
errors.push(format!(
"max_tokens must be > 0, got {}",
tokens
));
}
}
if let Some(temp) = self.temperature {
if !(0.0..=2.0).contains(&temp) {
errors.push(format!(
"temperature must be in [0.0, 2.0], got {}",
temp
));
}
}
errors
}
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentTaskSpec { pub struct AgentTaskSpec {
pub task_id: Uuid, pub task_id: Uuid,
@ -200,6 +255,10 @@ pub struct AgentTaskSpec {
pub pre_approved_plan_id: Option<Uuid>, pub pre_approved_plan_id: Option<Uuid>,
pub execution_token: Option<Uuid>, pub execution_token: Option<Uuid>,
pub session_budget: Option<SessionBudget>, pub session_budget: Option<SessionBudget>,
/// Maximum number of checklist tasks this agent run may add. `None` leaves
/// task creation unconstrained for backwards-compatible callers.
#[serde(default)]
pub task_budget: Option<u32>,
pub agent_type: Option<AgentType>, pub agent_type: Option<AgentType>,
#[serde(default)] #[serde(default)]
pub suggested_test_files: Vec<String>, pub suggested_test_files: Vec<String>,
@ -209,10 +268,19 @@ pub struct AgentTaskSpec {
pub connector_config: Option<ConnectorConfig>, pub connector_config: Option<ConnectorConfig>,
#[serde(default)] #[serde(default)]
pub tools_json: Option<String>, pub tools_json: Option<String>,
/// blake3-style hash of `tools_json` for deduplication across dispatches.
/// When present, the receiver may skip the full payload if the hash is cached.
#[serde(default)]
pub tools_hash: Option<String>,
/* Overrides the agent-type default system prompt when set. Takes highest /* Overrides the agent-type default system prompt when set. Takes highest
precedence in the resolution chain (above DB overrides and defaults). */ precedence in the resolution chain (above DB overrides and defaults). */
#[serde(default)] #[serde(default)]
pub system_prompt: Option<String>, pub system_prompt: Option<String>,
/// When set, the Pod reads the latest checkpoint from SQLite and injects the
/// completed tool calls into the system message so the LLM resumes instead
/// of restarting from scratch on retry.
#[serde(default)]
pub resume_from_checkpoint: Option<Uuid>,
} }
impl AgentTaskSpec { impl AgentTaskSpec {
@ -238,12 +306,15 @@ impl AgentTaskSpec {
pre_approved_plan_id: None, pre_approved_plan_id: None,
execution_token: None, execution_token: None,
session_budget: None, session_budget: None,
task_budget: None,
agent_type: None, agent_type: None,
suggested_test_files: Vec::new(), suggested_test_files: Vec::new(),
project_id: None, project_id: None,
connector_config: None, connector_config: None,
tools_json: None, tools_json: None,
tools_hash: None,
system_prompt: None, system_prompt: None,
resume_from_checkpoint: None,
} }
} }
} }

View file

@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::enums::{ToDoSource, ToDoStatus}; use crate::enums::{ToDoMode, ToDoSource, ToDoStatus};
use crate::errors::ValidationError; use crate::errors::ValidationError;
use std::collections::HashSet; use std::collections::HashSet;
@ -36,6 +36,7 @@ pub struct ToDo {
pub subtask_ids: Vec<Uuid>, pub subtask_ids: Vec<Uuid>,
pub affected_files: Vec<String>, pub affected_files: Vec<String>,
pub source: ToDoSource, pub source: ToDoSource,
pub mode: ToDoMode,
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,
@ -62,6 +63,7 @@ impl Default for ToDo {
subtask_ids: Vec::new(), subtask_ids: Vec::new(),
affected_files: Vec::new(), affected_files: Vec::new(),
source: ToDoSource::User, source: ToDoSource::User,
mode: ToDoMode::default(),
automation_chain_id: None, automation_chain_id: None,
tags: Vec::new(), tags: Vec::new(),
estimated_tokens: 0, estimated_tokens: 0,
@ -90,6 +92,7 @@ impl ToDo {
subtask_ids: Vec::new(), subtask_ids: Vec::new(),
affected_files: Vec::new(), affected_files: Vec::new(),
source: ToDoSource::User, source: ToDoSource::User,
mode: ToDoMode::default(),
automation_chain_id: None, automation_chain_id: None,
tags: Vec::new(), tags: Vec::new(),
estimated_tokens: 0, estimated_tokens: 0,
@ -199,6 +202,8 @@ impl ToDo {
(ToDoStatus::Failed, ToDoStatus::ReadyForAgent) => 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,
(ToDoStatus::Draft, ToDoStatus::Pending) => true,
(ToDoStatus::Pending, ToDoStatus::Draft) => true,
_ => self.status == status, _ => self.status == status,
}; };

View file

@ -516,6 +516,48 @@ impl Default for ToolVersion {
} }
} }
/* Dynamic runtime state of a tool, managed per-project by `ToolStateManager`.
This replaces the static `deprecated` bool on `ToolDefinition` with a richer
taxonomy that operators can toggle without redeploying. */
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolState {
Enabled,
Disabled,
Deprecated,
Experimental,
}
impl Default for ToolState {
fn default() -> Self {
Self::Enabled
}
}
impl ToolState {
pub fn as_str(&self) -> &'static str {
match self {
Self::Enabled => "enabled",
Self::Disabled => "disabled",
Self::Deprecated => "deprecated",
Self::Experimental => "experimental",
}
}
pub fn from_str(s: &str) -> Self {
match s {
"disabled" => Self::Disabled,
"deprecated" => Self::Deprecated,
"experimental" => Self::Experimental,
_ => Self::Enabled,
}
}
pub fn is_available(&self) -> bool {
matches!(self, Self::Enabled | Self::Experimental)
}
}
/* Tool category classification for UI grouping and filtering. */ /* Tool category classification for UI grouping and filtering. */
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
@ -849,7 +891,13 @@ pub const PLANNER_ONLY_TOOLS: &[&str] = &[
Mutating tools (write_file, edit_file, bash, delete_workspace) are intentionally 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 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. */ (Phase 4). Only genuinely agent-internal collaboration tools are agent-only. */
pub const AGENT_ONLY_TOOLS: &[&str] = &["submit_batch_plan", "report_completion"]; pub const AGENT_ONLY_TOOLS: &[&str] = &[
"submit_batch_plan",
"report_completion",
"kanban_add_task",
"kanban_complete_task",
"kanban_remove_task",
];
/* Tools that proxy to the Reef API when a `REEF_URL` / `REEF_API_URL` is /* Tools that proxy to the Reef API when a `REEF_URL` / `REEF_API_URL` is
configured. These tools have both a local implementation and a Reef-backed configured. These tools have both a local implementation and a Reef-backed
@ -899,7 +947,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"read_file", "read_file",
"Read the content of a file with optional pagination", "Read the content of a file with optional pagination",
"filesystem", "filesystem",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -914,7 +964,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"write_file", "write_file",
"Create or overwrite a file with content", "Create or overwrite a file with content",
"filesystem", "filesystem",
true, true, false, true,
true,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -929,7 +981,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"list_directory", "list_directory",
"List directory contents with optional filtering", "List directory contents with optional filtering",
"filesystem", "filesystem",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -944,7 +998,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"edit_file", "edit_file",
"Edit file contents with line-based operations (replace, insert, delete)", "Edit file contents with line-based operations (replace, insert, delete)",
"filesystem", "filesystem",
true, true, false, true,
true,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -971,7 +1027,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"search_files", "search_files",
"Search for text patterns in files (grep-like)", "Search for text patterns in files (grep-like)",
"filesystem", "filesystem",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -982,7 +1040,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["path", "pattern"] "required": ["path", "pattern"]
}) })
), ),
// ======================== // ========================
// Execution Tools // Execution Tools
// ======================== // ========================
@ -990,7 +1047,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"bash", "bash",
"Execute a shell command and capture output", "Execute a shell command and capture output",
"execution", "execution",
true, true, false, true,
true,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1001,7 +1060,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["command"] "required": ["command"]
}) })
), ),
// ======================== // ========================
// Git Tools // Git Tools
// ======================== // ========================
@ -1009,7 +1067,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"git_status", "git_status",
"Show working tree status in a git repository", "Show working tree status in a git repository",
"git", "git",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1022,7 +1082,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"git_diff", "git_diff",
"Show changes between commits, commit and working tree, etc.", "Show changes between commits, commit and working tree, etc.",
"git", "git",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1036,7 +1098,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"git_log", "git_log",
"Show commit history in a git repository", "Show commit history in a git repository",
"git", "git",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1049,7 +1113,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"git_branch", "git_branch",
"List or manage git branches", "List or manage git branches",
"git", "git",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1058,7 +1124,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
} }
}) })
), ),
// ======================== // ========================
// Build & Test Tools // Build & Test Tools
// ======================== // ========================
@ -1066,7 +1131,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"cargo_check", "cargo_check",
"Run cargo check on a Rust project to verify compilation", "Run cargo check on a Rust project to verify compilation",
"build", "build",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1079,7 +1146,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"npm_build", "npm_build",
"Run npm build script in a JavaScript/TypeScript project", "Run npm build script in a JavaScript/TypeScript project",
"build", "build",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1092,7 +1161,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"python_check", "python_check",
"Check Python syntax or run linting on a Python file", "Check Python syntax or run linting on a Python file",
"build", "build",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1105,7 +1176,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"test_runner", "test_runner",
"Run project tests with a test runner like pytest", "Run project tests with a test runner like pytest",
"build", "build",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1115,7 +1188,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
} }
}) })
), ),
// ======================== // ========================
// Network Tools // Network Tools
// ======================== // ========================
@ -1123,7 +1195,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"web_search", "web_search",
"Search the web for information using a search provider", "Search the web for information using a search provider",
"network", "network",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1138,7 +1212,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"web_fetch", "web_fetch",
"Fetch and parse web pages from URLs", "Fetch and parse web pages from URLs",
"network", "network",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1152,7 +1228,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"web_api", "web_api",
"Make HTTP API requests to external services", "Make HTTP API requests to external services",
"network", "network",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1163,7 +1241,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["url", "method"] "required": ["url", "method"]
}) })
), ),
// ======================== // ========================
// Workspace Tools // Workspace Tools
// ======================== // ========================
@ -1171,7 +1248,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"create_workspace", "create_workspace",
"Create an ephemeral workspace directory for isolated operations", "Create an ephemeral workspace directory for isolated operations",
"workspace", "workspace",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1184,7 +1263,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"delete_workspace", "delete_workspace",
"Delete an ephemeral workspace and all its contents", "Delete an ephemeral workspace and all its contents",
"workspace", "workspace",
true, true, false, true,
true,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1198,7 +1279,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"create_venv", "create_venv",
"Create a Python virtual environment", "Create a Python virtual environment",
"workspace", "workspace",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1212,7 +1295,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"install_dependencies", "install_dependencies",
"Install Python dependencies from requirements.txt or specified packages", "Install Python dependencies from requirements.txt or specified packages",
"workspace", "workspace",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1226,7 +1311,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"workspace_info", "workspace_info",
"Get information and statistics about a workspace directory", "Get information and statistics about a workspace directory",
"workspace", "workspace",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1235,7 +1322,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["path"] "required": ["path"]
}) })
), ),
// ======================== // ========================
// Kanban Tools // Kanban Tools
// ======================== // ========================
@ -1243,7 +1329,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"kanban_list_board", "kanban_list_board",
"List all columns and todos in the Kanban board", "List all columns and todos in the Kanban board",
"kanban", "kanban",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1255,10 +1343,12 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
), ),
tool!( tool!(
"kanban_create_todo", "kanban_create_todo",
"Create a new todo item in a Kanban board", "Create a new todo item in a Kanban board",
"kanban", "kanban",
false, false, true, false,
serde_json::json!({ false,
true,
serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
"project_id": { "type": "string", "description": "Project ID" }, "project_id": { "type": "string", "description": "Project ID" },
@ -1280,7 +1370,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"kanban_update_todo", "kanban_update_todo",
"Update a todo item's title, description, status, or priority", "Update a todo item's title, description, status, or priority",
"kanban", "kanban",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1298,7 +1390,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"kanban_delete_todo", "kanban_delete_todo",
"Delete a todo item from the Kanban board", "Delete a todo item from the Kanban board",
"kanban", "kanban",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1310,25 +1404,29 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
), ),
tool!( tool!(
"kanban_move_todo", "kanban_move_todo",
"Move a todo item to a different status", "Move a todo item to a different status",
"kanban", "kanban",
false, false, true, false,
serde_json::json!({ false,
"type": "object", true,
"properties": { serde_json::json!({
"project_id": { "type": "string", "description": "Project ID" }, "type": "object",
"todo_id": { "type": "string", "description": "ID of the todo to move" }, "properties": {
"status": { "type": "string", "enum": ["pending","in_progress","completed","blocked","ready_for_agent","delegated","failed","pending_approval"], "description": "Target status" }, "project_id": { "type": "string", "description": "Project ID" },
"task_order": { "type": "number", "description": "New order position" } "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" },
"required": ["project_id", "todo_id"] "task_order": { "type": "number", "description": "New order position" }
}) },
"required": ["project_id", "todo_id"]
})
), ),
tool!( tool!(
"kanban_create_task", "kanban_create_task",
"Create a sub-task within a todo", "Create a sub-task within a todo",
"kanban", "kanban",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1344,7 +1442,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"kanban_update_task", "kanban_update_task",
"Update a sub-task's title, description, or status", "Update a sub-task's title, description, or status",
"kanban", "kanban",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1357,11 +1457,64 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["project_id", "task_id"] "required": ["project_id", "task_id"]
}) })
), ),
tool!(
"kanban_add_task",
"Add a checklist task to the todo owned by this agent run",
"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 parent todo" },
"title": { "type": "string", "description": "Checklist task title" },
"description": { "type": "string", "description": "Optional description" }
},
"required": ["project_id", "todo_id", "title"]
})
),
tool!(
"kanban_complete_task",
"Mark a checklist task complete or incomplete on the todo owned by this agent run",
"kanban",
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
"project_id": { "type": "string", "description": "Project ID" },
"task_id": { "type": "string", "description": "Checklist task ID" },
"status": { "type": "string", "enum": ["completed", "pending"], "description": "Target status (defaults to completed)" }
},
"required": ["project_id", "task_id"]
})
),
tool!(
"kanban_remove_task",
"Remove a checklist task from the todo owned by this agent run",
"kanban",
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
"project_id": { "type": "string", "description": "Project ID" },
"task_id": { "type": "string", "description": "Checklist task ID" }
},
"required": ["project_id", "task_id"]
})
),
tool!( tool!(
"kanban_add_tag", "kanban_add_tag",
"Add a tag to a todo item", "Add a tag to a todo item",
"kanban", "kanban",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1380,7 +1533,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"document_file", "document_file",
"Analyze and generate documentation for a file", "Analyze and generate documentation for a file",
"documentation", "documentation",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1394,7 +1549,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"document_project", "document_project",
"Generate a comprehensive summary of all project files", "Generate a comprehensive summary of all project files",
"documentation", "documentation",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1407,7 +1564,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"find_references", "find_references",
"Find all references to a symbol across the project", "Find all references to a symbol across the project",
"documentation", "documentation",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1423,7 +1582,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"file_dependencies", "file_dependencies",
"Get incoming or outgoing dependencies for a file", "Get incoming or outgoing dependencies for a file",
"documentation", "documentation",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1438,7 +1599,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"documentation_tree", "documentation_tree",
"Build a directory tree showing documentation coverage", "Build a directory tree showing documentation coverage",
"documentation", "documentation",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1452,7 +1615,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"verify_documentation", "verify_documentation",
"Verify documentation freshness and compute coverage stats", "Verify documentation freshness and compute coverage stats",
"documentation", "documentation",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1465,7 +1630,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"document_folder", "document_folder",
"Document all files within a folder", "Document all files within a folder",
"documentation", "documentation",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1479,7 +1646,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"store_file_doc", "store_file_doc",
"Store documentation for a file", "Store documentation for a file",
"documentation", "documentation",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1502,7 +1671,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"get_documentation_context", "get_documentation_context",
"Get the documentation context for a file", "Get the documentation context for a file",
"documentation", "documentation",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1512,7 +1683,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["project_id", "file_path"] "required": ["project_id", "file_path"]
}) })
), ),
// ======================== // ========================
// File Tools // File Tools
// ======================== // ========================
@ -1520,7 +1690,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"list_files", "list_files",
"List all files in a project", "List all files in a project",
"files", "files",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1530,7 +1702,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["project_id"] "required": ["project_id"]
}) })
), ),
// ======================== // ========================
// Strategic Tools // Strategic Tools
// ======================== // ========================
@ -1538,7 +1709,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"propose_strategic_item", "propose_strategic_item",
"Propose a new high-level strategic goal or todo", "Propose a new high-level strategic goal or todo",
"strategic", "strategic",
false, true, true, false,
true,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1555,7 +1728,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"split_task", "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.", "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", "strategic",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1583,7 +1758,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"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).",
"strategic", "strategic",
false, true, true, false,
true,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1598,7 +1775,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"audit_assumptions", "audit_assumptions",
"List the user's current assumptions about the project and identify the weakest ones", "List the user's current assumptions about the project and identify the weakest ones",
"strategic", "strategic",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1611,7 +1790,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"identify_blind_spots", "identify_blind_spots",
"Query project data for missing dependency chains, undocumented risks, and recurring blockers", "Query project data for missing dependency chains, undocumented risks, and recurring blockers",
"strategic", "strategic",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1624,7 +1805,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"check_dependencies", "check_dependencies",
"Analyze todo dependency chains for circular or missing dependencies", "Analyze todo dependency chains for circular or missing dependencies",
"strategic", "strategic",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1637,7 +1820,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"evaluate_plan_risk", "evaluate_plan_risk",
"Score the current plan for feasibility, edge-case coverage, and alignment", "Score the current plan for feasibility, edge-case coverage, and alignment",
"strategic", "strategic",
false, false, true, false,
false,
true,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1650,7 +1835,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"compare_project_patterns", "compare_project_patterns",
"Compare kanban structures across projects to detect inconsistencies", "Compare kanban structures across projects to detect inconsistencies",
"strategic", "strategic",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1662,7 +1849,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"find_similar_risks", "find_similar_risks",
"Search across projects for similar risk patterns", "Search across projects for similar risk patterns",
"strategic", "strategic",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1671,7 +1860,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
} }
}) })
), ),
// ======================== // ========================
// Agent Tools // Agent Tools
// ======================== // ========================
@ -1679,7 +1867,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"report_completion", "report_completion",
"Report completion status of a todo back to the Bridge", "Report completion status of a todo back to the Bridge",
"agent", "agent",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1697,7 +1887,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"submit_batch_plan", "submit_batch_plan",
"Submit a batch plan of mutations for pre-approval before execution", "Submit a batch plan of mutations for pre-approval before execution",
"agent", "agent",
false, false, false, false,
false,
false,
serde_json::json!({ serde_json::json!({
"type": "object", "type": "object",
"properties": { "properties": {
@ -1798,6 +1990,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
("kanban_move_todo", 2), ("kanban_move_todo", 2),
("kanban_create_task", 2), ("kanban_create_task", 2),
("kanban_update_task", 2), ("kanban_update_task", 2),
("kanban_add_task", 1),
("kanban_complete_task", 1),
("kanban_remove_task", 1),
("create_workspace", 2), ("create_workspace", 2),
("split_task", 2), ("split_task", 2),
// Depth 3 — needs deep reasoning // Depth 3 — needs deep reasoning
@ -1933,11 +2128,37 @@ pub fn format_tools_json(tools: &[ToolDefinition]) -> Option<String> {
serde_json::to_string(&formatted).ok() serde_json::to_string(&formatted).ok()
} }
/// Deterministic hash of a tools JSON string using std DefaultHasher.
/// The hex-encoded result is used as a deduplication key across dispatch/pod.
pub fn compute_tools_hash(tools_json: &str) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
tools_json.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use serde_json::json; use serde_json::json;
#[test]
fn test_compute_tools_hash_deterministic() {
let input = r#"[{"type":"function","function":{"name":"foo","description":"bar","parameters":{}}}]"#;
let h1 = compute_tools_hash(input);
let h2 = compute_tools_hash(input);
assert_eq!(h1, h2);
assert_eq!(h1.len(), 16);
}
#[test]
fn test_compute_tools_hash_different_inputs() {
let a = compute_tools_hash(r#"aaa"#);
let b = compute_tools_hash(r#"bbb"#);
assert_ne!(a, b);
}
#[test] #[test]
fn test_parse_single_block() { fn test_parse_single_block() {
let text = r#" let text = r#"
@ -2213,10 +2434,12 @@ Done with tools.
let results = parse_tool_call_blocks(text); let results = parse_tool_call_blocks(text);
assert_eq!(results.len(), 1); assert_eq!(results.len(), 1);
assert_eq!(results[0].call_id, "call_01"); assert_eq!(results[0].call_id, "call_01");
assert!(results[0].payload["content"] assert!(
.as_str() results[0].payload["content"]
.unwrap_or("") .as_str()
.contains("🎉")); .unwrap_or("")
.contains("🎉")
);
} }
#[test] #[test]
@ -2465,6 +2688,22 @@ Done with tools.
} }
} }
#[test]
fn checklist_task_tools_are_agent_only_at_depth_one() {
for id in [
"kanban_add_task",
"kanban_complete_task",
"kanban_remove_task",
] {
let tool = tool_definitions()
.into_iter()
.find(|tool| tool.id == id)
.expect("checklist task tool must be registered");
assert_eq!(tool.context_visibility, ContextVisibility::AgentOnly);
assert_eq!(tool.required_depth, 1);
}
}
#[test] #[test]
fn test_category_backstop_keeps_sensitive_tools_fail_closed() { fn test_category_backstop_keeps_sensitive_tools_fail_closed() {
/* Invariant enforced by the category backstop: no strategic tool is ever /* Invariant enforced by the category backstop: no strategic tool is ever

View file

@ -1,118 +0,0 @@
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