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,4 +1,5 @@
use serde::{Deserialize, Serialize};
use crate::task::SessionBudget;
/* ExploreAgent is intentionally limited to read-only tools so it can be
dispatched without approval for safe, unrestricted codebase exploration. */
@ -37,6 +38,35 @@ impl AgentType {
_ => 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)]
@ -46,6 +76,24 @@ pub enum ApprovalMode {
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)]
pub struct PlannedAction {
pub tool_name: String,
@ -70,6 +118,10 @@ pub struct AgentPolicy {
pub max_tokens: u32,
pub allowed_tools: 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 {
@ -84,6 +136,7 @@ impl AgentPolicy {
max_tokens: 4096,
allowed_tools: vec!["read_file".into(), "write_file".into(), "edit_file".into()],
forbidden_mutations: vec!["Cargo.toml".into(), "package.json".into()],
turn_mode: ApprovalTurnMode::Defer,
},
AgentType::TestJudge => Self {
agent_type: agent_type.clone(),
@ -94,6 +147,7 @@ impl AgentPolicy {
max_tokens: 4096,
allowed_tools: vec![],
forbidden_mutations: vec![],
turn_mode: ApprovalTurnMode::Defer,
},
AgentType::BuildAgent => Self {
agent_type: agent_type.clone(),
@ -104,6 +158,7 @@ impl AgentPolicy {
max_tokens: 131072,
allowed_tools: vec![],
forbidden_mutations: vec!["Cargo.toml".into(), "package.json".into()],
turn_mode: ApprovalTurnMode::Wait,
},
AgentType::Planner => Self {
agent_type: agent_type.clone(),
@ -114,6 +169,7 @@ impl AgentPolicy {
max_tokens: 131072,
allowed_tools: vec![],
forbidden_mutations: vec![],
turn_mode: ApprovalTurnMode::Defer,
},
AgentType::ChatAgent => Self {
agent_type: agent_type.clone(),
@ -124,6 +180,7 @@ impl AgentPolicy {
max_tokens: 4096,
allowed_tools: vec![],
forbidden_mutations: vec![],
turn_mode: ApprovalTurnMode::Defer,
},
/* ExploreAgent: auto-approved, read-only tools only, short timeout.
Forbidden mutations set to wildcard to block all writes. */
@ -143,6 +200,7 @@ impl AgentPolicy {
"web_fetch".into(),
],
forbidden_mutations: vec!["*".into()],
turn_mode: ApprovalTurnMode::Defer,
},
}
}

View file

@ -21,53 +21,144 @@ pub struct AiConversation {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamEvent {
#[serde(rename = "type")]
pub event_type: String,
pub content: Option<String>,
pub thinking: Option<String>,
pub error: Option<String>,
pub message_id: Option<String>,
#[serde(tag = "type")]
pub enum StreamEvent {
#[serde(rename = "content")]
Content { content: String },
#[serde(rename = "thinking")]
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 {
pub fn content(content: String) -> Self {
Self {
event_type: "content".to_string(),
content: Some(content),
thinking: None,
error: None,
message_id: None,
}
Self::Content { content }
}
pub fn thinking(thinking: String) -> Self {
Self {
event_type: "thinking".to_string(),
content: None,
thinking: Some(thinking),
error: None,
message_id: None,
}
pub fn thinking(content: String) -> Self {
Self::Thinking { content }
}
pub fn error(error: String) -> Self {
Self {
event_type: "error".to_string(),
content: None,
thinking: None,
error: Some(error),
message_id: None,
}
Self::Error { error }
}
pub fn done(message_id: String) -> Self {
Self {
event_type: "done".to_string(),
content: None,
thinking: None,
error: None,
message_id: Some(message_id),
Self::Done { message_id }
}
pub fn tool_call(tool_name: String, tool_id: String, arguments: String) -> Self {
Self::ToolCall {
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 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)]
pub enum ToDoStatus {
Pending,

View file

@ -20,6 +20,21 @@ pub enum ValidationError {
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)]
pub enum ShoalError {
#[error("storage error: {0}")]
@ -38,6 +53,20 @@ pub enum ShoalError {
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 {
fn from(e: ValidationError) -> Self {
ShoalError::ValidationError(e.to_string())

View file

@ -3,6 +3,15 @@ use uuid::Uuid;
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;
#[derive(Debug, Clone)]
@ -76,12 +85,14 @@ impl KrillDescriptor {
#[derive(Debug, Clone, Default)]
pub struct KrillConfig {
pub settings: serde_json::Value,
pub pod_protocol_version: PodProtocolVersion,
}
impl KrillConfig {
pub fn new() -> Self {
Self {
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 enums::*;
pub use errors::{ShoalError, ValidationError};
pub use krill::{KrillConfig, KrillDescriptor, KrillId};
pub use krill::{KrillConfig, KrillDescriptor, KrillId, PodProtocolVersion};
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, 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,
ApprovalRequirement, CompositionStep, ContextVisibility, ExecutionContext,
MarketplaceSearchResults, ParsedToolCall, TestAction, ToolComposition, ToolDefinition,
ToolDependency, ToolDocumentation, ToolExample, ToolExecutor, ToolListing, ToolParser,
ToolPlugin, ToolRegistry, ToolTest, ToolTestResult, ToolTestSuite, ToolTestSuiteResult,
ToolVersion, GLOBAL_TOOLS, REEF_PROXY_TOOLS,
ToolPlugin, ToolRegistry, ToolState, ToolTest, ToolTestResult, ToolTestSuite,
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 build_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)]
pub extra: serde_json::Value,
}
@ -120,6 +122,7 @@ impl ProjectSettings {
language: None,
build_command: None,
run_command: None,
git_commit_template: None,
extra: serde_json::json!({}),
}
}
@ -139,6 +142,11 @@ impl ProjectSettings {
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) {
if let Some(obj) = self.extra.as_object_mut() {
obj.insert(key.to_string(), value);

View file

@ -183,6 +183,61 @@ pub struct ConnectorConfig {
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)]
pub struct AgentTaskSpec {
pub task_id: Uuid,
@ -200,6 +255,10 @@ pub struct AgentTaskSpec {
pub pre_approved_plan_id: Option<Uuid>,
pub execution_token: Option<Uuid>,
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>,
#[serde(default)]
pub suggested_test_files: Vec<String>,
@ -209,10 +268,19 @@ pub struct AgentTaskSpec {
pub connector_config: Option<ConnectorConfig>,
#[serde(default)]
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
precedence in the resolution chain (above DB overrides and defaults). */
#[serde(default)]
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 {
@ -238,12 +306,15 @@ impl AgentTaskSpec {
pre_approved_plan_id: None,
execution_token: None,
session_budget: None,
task_budget: None,
agent_type: None,
suggested_test_files: Vec::new(),
project_id: None,
connector_config: None,
tools_json: None,
tools_hash: None,
system_prompt: None,
resume_from_checkpoint: None,
}
}
}

View file

@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::enums::{ToDoSource, ToDoStatus};
use crate::enums::{ToDoMode, ToDoSource, ToDoStatus};
use crate::errors::ValidationError;
use std::collections::HashSet;
@ -36,6 +36,7 @@ pub struct ToDo {
pub subtask_ids: Vec<Uuid>,
pub affected_files: Vec<String>,
pub source: ToDoSource,
pub mode: ToDoMode,
pub automation_chain_id: Option<Uuid>,
pub tags: Vec<String>,
pub estimated_tokens: u32,
@ -62,6 +63,7 @@ impl Default for ToDo {
subtask_ids: Vec::new(),
affected_files: Vec::new(),
source: ToDoSource::User,
mode: ToDoMode::default(),
automation_chain_id: None,
tags: Vec::new(),
estimated_tokens: 0,
@ -90,6 +92,7 @@ impl ToDo {
subtask_ids: Vec::new(),
affected_files: Vec::new(),
source: ToDoSource::User,
mode: ToDoMode::default(),
automation_chain_id: None,
tags: Vec::new(),
estimated_tokens: 0,
@ -199,6 +202,8 @@ impl ToDo {
(ToDoStatus::Failed, ToDoStatus::ReadyForAgent) => true,
(ToDoStatus::PendingApproval, ToDoStatus::InProgress) => true,
(ToDoStatus::PendingApproval, ToDoStatus::Blocked) => true,
(ToDoStatus::Draft, ToDoStatus::Pending) => true,
(ToDoStatus::Pending, ToDoStatus::Draft) => true,
_ => 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. */
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[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
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"];
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
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 the content of a file with optional pagination",
"filesystem",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -914,7 +964,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"write_file",
"Create or overwrite a file with content",
"filesystem",
true, true, false,
true,
true,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -929,7 +981,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"list_directory",
"List directory contents with optional filtering",
"filesystem",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -944,7 +998,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"edit_file",
"Edit file contents with line-based operations (replace, insert, delete)",
"filesystem",
true, true, false,
true,
true,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -971,7 +1027,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"search_files",
"Search for text patterns in files (grep-like)",
"filesystem",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -982,7 +1040,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["path", "pattern"]
})
),
// ========================
// Execution Tools
// ========================
@ -990,7 +1047,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"bash",
"Execute a shell command and capture output",
"execution",
true, true, false,
true,
true,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1001,7 +1060,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["command"]
})
),
// ========================
// Git Tools
// ========================
@ -1009,7 +1067,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"git_status",
"Show working tree status in a git repository",
"git",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1022,7 +1082,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"git_diff",
"Show changes between commits, commit and working tree, etc.",
"git",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1036,7 +1098,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"git_log",
"Show commit history in a git repository",
"git",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1049,7 +1113,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"git_branch",
"List or manage git branches",
"git",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1058,7 +1124,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
}
})
),
// ========================
// Build & Test Tools
// ========================
@ -1066,7 +1131,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"cargo_check",
"Run cargo check on a Rust project to verify compilation",
"build",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1079,7 +1146,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"npm_build",
"Run npm build script in a JavaScript/TypeScript project",
"build",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1092,7 +1161,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"python_check",
"Check Python syntax or run linting on a Python file",
"build",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1105,7 +1176,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"test_runner",
"Run project tests with a test runner like pytest",
"build",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1115,7 +1188,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
}
})
),
// ========================
// Network Tools
// ========================
@ -1123,7 +1195,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"web_search",
"Search the web for information using a search provider",
"network",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1138,7 +1212,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"web_fetch",
"Fetch and parse web pages from URLs",
"network",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1152,7 +1228,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"web_api",
"Make HTTP API requests to external services",
"network",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1163,7 +1241,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["url", "method"]
})
),
// ========================
// Workspace Tools
// ========================
@ -1171,7 +1248,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"create_workspace",
"Create an ephemeral workspace directory for isolated operations",
"workspace",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1184,7 +1263,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"delete_workspace",
"Delete an ephemeral workspace and all its contents",
"workspace",
true, true, false,
true,
true,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1198,7 +1279,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"create_venv",
"Create a Python virtual environment",
"workspace",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1212,7 +1295,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"install_dependencies",
"Install Python dependencies from requirements.txt or specified packages",
"workspace",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1226,7 +1311,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"workspace_info",
"Get information and statistics about a workspace directory",
"workspace",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1235,7 +1322,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["path"]
})
),
// ========================
// Kanban Tools
// ========================
@ -1243,7 +1329,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"kanban_list_board",
"List all columns and todos in the Kanban board",
"kanban",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1255,10 +1343,12 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
),
tool!(
"kanban_create_todo",
"Create a new todo item in a Kanban board",
"kanban",
false, false, true,
serde_json::json!({
"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" },
@ -1280,7 +1370,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"kanban_update_todo",
"Update a todo item's title, description, status, or priority",
"kanban",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1298,7 +1390,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"kanban_delete_todo",
"Delete a todo item from the Kanban board",
"kanban",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1310,25 +1404,29 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
),
tool!(
"kanban_move_todo",
"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"]
})
"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",
"Create a sub-task within a todo",
"kanban",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1344,7 +1442,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"kanban_update_task",
"Update a sub-task's title, description, or status",
"kanban",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1357,11 +1457,64 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"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!(
"kanban_add_tag",
"Add a tag to a todo item",
"kanban",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1380,7 +1533,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"document_file",
"Analyze and generate documentation for a file",
"documentation",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1394,7 +1549,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"document_project",
"Generate a comprehensive summary of all project files",
"documentation",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1407,7 +1564,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"find_references",
"Find all references to a symbol across the project",
"documentation",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1423,7 +1582,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"file_dependencies",
"Get incoming or outgoing dependencies for a file",
"documentation",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1438,7 +1599,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"documentation_tree",
"Build a directory tree showing documentation coverage",
"documentation",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1452,7 +1615,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"verify_documentation",
"Verify documentation freshness and compute coverage stats",
"documentation",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1465,7 +1630,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"document_folder",
"Document all files within a folder",
"documentation",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1479,7 +1646,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"store_file_doc",
"Store documentation for a file",
"documentation",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1502,7 +1671,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"get_documentation_context",
"Get the documentation context for a file",
"documentation",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1512,7 +1683,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["project_id", "file_path"]
})
),
// ========================
// File Tools
// ========================
@ -1520,7 +1690,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"list_files",
"List all files in a project",
"files",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1530,7 +1702,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"required": ["project_id"]
})
),
// ========================
// Strategic Tools
// ========================
@ -1538,7 +1709,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"propose_strategic_item",
"Propose a new high-level strategic goal or todo",
"strategic",
false, true, true,
false,
true,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1555,7 +1728,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"split_task",
"Split a Pending todo into multiple subtasks with proper dependency tracking, depth validation, and cycle detection. Each subtask should be independently deployable to an agent.",
"strategic",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1583,7 +1758,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"change_planner_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,
false,
true,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1598,7 +1775,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"audit_assumptions",
"List the user's current assumptions about the project and identify the weakest ones",
"strategic",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1611,7 +1790,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"identify_blind_spots",
"Query project data for missing dependency chains, undocumented risks, and recurring blockers",
"strategic",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1624,7 +1805,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"check_dependencies",
"Analyze todo dependency chains for circular or missing dependencies",
"strategic",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1637,7 +1820,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"evaluate_plan_risk",
"Score the current plan for feasibility, edge-case coverage, and alignment",
"strategic",
false, false, true,
false,
false,
true,
serde_json::json!({
"type": "object",
"properties": {
@ -1650,7 +1835,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"compare_project_patterns",
"Compare kanban structures across projects to detect inconsistencies",
"strategic",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1662,7 +1849,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"find_similar_risks",
"Search across projects for similar risk patterns",
"strategic",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1671,7 +1860,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
}
})
),
// ========================
// Agent Tools
// ========================
@ -1679,7 +1867,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"report_completion",
"Report completion status of a todo back to the Bridge",
"agent",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1697,7 +1887,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"submit_batch_plan",
"Submit a batch plan of mutations for pre-approval before execution",
"agent",
false, false, false,
false,
false,
false,
serde_json::json!({
"type": "object",
"properties": {
@ -1798,6 +1990,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
("kanban_move_todo", 2),
("kanban_create_task", 2),
("kanban_update_task", 2),
("kanban_add_task", 1),
("kanban_complete_task", 1),
("kanban_remove_task", 1),
("create_workspace", 2),
("split_task", 2),
// Depth 3 — needs deep reasoning
@ -1933,11 +2128,37 @@ pub fn format_tools_json(tools: &[ToolDefinition]) -> Option<String> {
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)]
mod tests {
use super::*;
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]
fn test_parse_single_block() {
let text = r#"
@ -2213,10 +2434,12 @@ Done with tools.
let results = parse_tool_call_blocks(text);
assert_eq!(results.len(), 1);
assert_eq!(results[0].call_id, "call_01");
assert!(results[0].payload["content"]
.as_str()
.unwrap_or("")
.contains("🎉"));
assert!(
results[0].payload["content"]
.as_str()
.unwrap_or("")
.contains("🎉")
);
}
#[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]
fn test_category_backstop_keeps_sensitive_tools_fail_closed() {
/* Invariant enforced by the category backstop: no strategic tool is ever