Auto deployments
This commit is contained in:
parent
212fea579f
commit
cc92cf681c
8 changed files with 484 additions and 6 deletions
|
|
@ -1,6 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AgentType {
|
||||
Planner,
|
||||
|
|
@ -39,3 +39,86 @@ pub enum ApprovalMode {
|
|||
Batch,
|
||||
PerTool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PlannedAction {
|
||||
pub tool_name: String,
|
||||
pub arguments: serde_json::Value,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SimulationResult {
|
||||
pub planned_actions: Vec<PlannedAction>,
|
||||
pub estimated_tokens: u32,
|
||||
pub estimated_duration: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentPolicy {
|
||||
pub agent_type: AgentType,
|
||||
pub auto_approve: bool,
|
||||
pub max_depth: u32,
|
||||
pub require_preview: bool,
|
||||
pub timeout_seconds: u32,
|
||||
pub max_tokens: u32,
|
||||
pub allowed_tools: Vec<String>,
|
||||
pub forbidden_mutations: Vec<String>,
|
||||
}
|
||||
|
||||
impl AgentPolicy {
|
||||
pub fn defaults_for(agent_type: &AgentType) -> Self {
|
||||
match agent_type {
|
||||
AgentType::DocAgent => Self {
|
||||
agent_type: agent_type.clone(),
|
||||
auto_approve: true,
|
||||
max_depth: 2,
|
||||
require_preview: false,
|
||||
timeout_seconds: 30,
|
||||
max_tokens: 4096,
|
||||
allowed_tools: vec!["read_file".into(), "write_file".into(), "edit_file".into()],
|
||||
forbidden_mutations: vec!["Cargo.toml".into(), "package.json".into()],
|
||||
},
|
||||
AgentType::TestJudge => Self {
|
||||
agent_type: agent_type.clone(),
|
||||
auto_approve: true,
|
||||
max_depth: 3,
|
||||
require_preview: true,
|
||||
timeout_seconds: 60,
|
||||
max_tokens: 4096,
|
||||
allowed_tools: vec![],
|
||||
forbidden_mutations: vec![],
|
||||
},
|
||||
AgentType::BuildAgent => Self {
|
||||
agent_type: agent_type.clone(),
|
||||
auto_approve: false,
|
||||
max_depth: 5,
|
||||
require_preview: false,
|
||||
timeout_seconds: 300,
|
||||
max_tokens: 16384,
|
||||
allowed_tools: vec![],
|
||||
forbidden_mutations: vec!["Cargo.toml".into(), "package.json".into()],
|
||||
},
|
||||
AgentType::Planner => Self {
|
||||
agent_type: agent_type.clone(),
|
||||
auto_approve: false,
|
||||
max_depth: 3,
|
||||
require_preview: true,
|
||||
timeout_seconds: 120,
|
||||
max_tokens: 16384,
|
||||
allowed_tools: vec![],
|
||||
forbidden_mutations: vec![],
|
||||
},
|
||||
AgentType::ChatAgent => Self {
|
||||
agent_type: agent_type.clone(),
|
||||
auto_approve: true,
|
||||
max_depth: 1,
|
||||
require_preview: false,
|
||||
timeout_seconds: 60,
|
||||
max_tokens: 4096,
|
||||
allowed_tools: vec![],
|
||||
forbidden_mutations: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ 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>,
|
||||
}
|
||||
|
|
@ -34,6 +35,17 @@ impl StreamEvent {
|
|||
Self {
|
||||
event_type: "content".to_string(),
|
||||
content: Some(content),
|
||||
thinking: None,
|
||||
error: None,
|
||||
message_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn thinking(thinking: String) -> Self {
|
||||
Self {
|
||||
event_type: "thinking".to_string(),
|
||||
content: None,
|
||||
thinking: Some(thinking),
|
||||
error: None,
|
||||
message_id: None,
|
||||
}
|
||||
|
|
@ -43,6 +55,7 @@ impl StreamEvent {
|
|||
Self {
|
||||
event_type: "error".to_string(),
|
||||
content: None,
|
||||
thinking: None,
|
||||
error: Some(error),
|
||||
message_id: None,
|
||||
}
|
||||
|
|
@ -52,6 +65,7 @@ impl StreamEvent {
|
|||
Self {
|
||||
event_type: "done".to_string(),
|
||||
content: None,
|
||||
thinking: None,
|
||||
error: None,
|
||||
message_id: Some(message_id),
|
||||
}
|
||||
|
|
|
|||
46
src/conclusion.rs
Normal file
46
src/conclusion.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConclusionCard {
|
||||
pub id: Uuid,
|
||||
pub project_id: Uuid,
|
||||
pub title: String, // e.g., "Auth Module Implementation"
|
||||
pub summary: String, // LLM-generated or template
|
||||
pub child_run_ids: Vec<Uuid>,
|
||||
pub artifacts: Vec<Artifact>, // files, docs, test results
|
||||
pub metrics: ConclusionMetrics, // tokens, duration, agent counts
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub trigger: ConclusionTrigger, // AutoChainComplete | UserMilestone | BoardFinalize | ManualMerge
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Artifact {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub artifact_type: ArtifactType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ArtifactType {
|
||||
File,
|
||||
Documentation,
|
||||
TestResult,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConclusionMetrics {
|
||||
pub total_tokens: u32,
|
||||
pub duration_seconds: u64,
|
||||
pub agent_count: u32,
|
||||
pub tool_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ConclusionTrigger {
|
||||
AutoChainComplete,
|
||||
UserMilestone,
|
||||
BoardFinalize,
|
||||
ManualMerge,
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ pub enum ToDoStatus {
|
|||
Delegated,
|
||||
Failed,
|
||||
PendingApproval,
|
||||
Draft,
|
||||
}
|
||||
|
||||
impl Default for ToDoStatus {
|
||||
|
|
@ -29,6 +30,7 @@ impl fmt::Display for ToDoStatus {
|
|||
ToDoStatus::ReadyForAgent => write!(f, "ready_for_agent"),
|
||||
ToDoStatus::Delegated => write!(f, "delegated"),
|
||||
ToDoStatus::Failed => write!(f, "failed"),
|
||||
ToDoStatus::Draft => write!(f, "draft"),
|
||||
ToDoStatus::PendingApproval => write!(f, "pending_approval"),
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +47,7 @@ impl std::str::FromStr for ToDoStatus {
|
|||
"ready_for_agent" => Ok(ToDoStatus::ReadyForAgent),
|
||||
"delegated" => Ok(ToDoStatus::Delegated),
|
||||
"failed" => Ok(ToDoStatus::Failed),
|
||||
"draft" => Ok(ToDoStatus::Draft),
|
||||
"pending_approval" => Ok(ToDoStatus::PendingApproval),
|
||||
_ => Err(format!("unknown status: {}", s)),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,24 +4,29 @@ pub use uuid::Uuid;
|
|||
|
||||
pub mod agents;
|
||||
pub mod ai_response;
|
||||
pub mod conclusion;
|
||||
pub mod coral;
|
||||
pub mod enums;
|
||||
pub mod errors;
|
||||
pub mod krill;
|
||||
pub mod project;
|
||||
pub mod sync;
|
||||
pub mod task;
|
||||
pub mod todo;
|
||||
pub mod tools;
|
||||
|
||||
pub use agents::{AgentType, ApprovalMode};
|
||||
pub use agents::{AgentPolicy, AgentType, ApprovalMode, PlannedAction, SimulationResult};
|
||||
pub use ai_response::{Conversation, Message};
|
||||
pub use conclusion::{
|
||||
Artifact, ArtifactType, ConclusionCard, ConclusionMetrics, ConclusionTrigger,
|
||||
};
|
||||
pub use coral::{Coral, CoralId};
|
||||
pub use enums::*;
|
||||
pub use errors::{ShoalError, ValidationError};
|
||||
pub use krill::{KrillConfig, KrillDescriptor, KrillId};
|
||||
pub use project::{Project, ProjectFile, ProjectId, ProjectSettings};
|
||||
pub use task::{Task, TaskResult};
|
||||
pub use todo::ToDo;
|
||||
pub use todo::{Dependency, ToDo};
|
||||
pub use tools::{
|
||||
format_tool_error, format_tool_result, parse_tool_call_blocks, parse_tool_call_stream,
|
||||
tool_definitions, CompositionStep, ExecutionContext, MarketplaceSearchResults, ParsedToolCall,
|
||||
|
|
|
|||
159
src/sync.rs
Normal file
159
src/sync.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ConflictResolution {
|
||||
Required,
|
||||
ResolvedLocal,
|
||||
ResolvedRemote,
|
||||
ResolvedManual,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FileConflict {
|
||||
pub file: String,
|
||||
pub base_hash: String,
|
||||
pub local_hash: String,
|
||||
pub remote_hash: String,
|
||||
pub resolution: ConflictResolution,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConflictManifest {
|
||||
pub conflicts: Vec<FileConflict>,
|
||||
pub unconflicted_remote: Vec<String>,
|
||||
pub unconflicted_local: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TrunkVersionSummary {
|
||||
pub version_id: String,
|
||||
pub parent_version: Option<String>,
|
||||
pub snapshot_hash: String,
|
||||
pub merged_at: String,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum TopicState {
|
||||
Working,
|
||||
AwaitingReview,
|
||||
Approved,
|
||||
Merged,
|
||||
Rejected,
|
||||
Conflict,
|
||||
}
|
||||
|
||||
impl TopicState {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
TopicState::Working => "working",
|
||||
TopicState::AwaitingReview => "awaiting_review",
|
||||
TopicState::Approved => "approved",
|
||||
TopicState::Merged => "merged",
|
||||
TopicState::Rejected => "rejected",
|
||||
TopicState::Conflict => "conflict",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"working" => Some(TopicState::Working),
|
||||
"awaiting_review" => Some(TopicState::AwaitingReview),
|
||||
"approved" => Some(TopicState::Approved),
|
||||
"merged" => Some(TopicState::Merged),
|
||||
"rejected" => Some(TopicState::Rejected),
|
||||
"conflict" => Some(TopicState::Conflict),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TopicSummary {
|
||||
pub topic_id: String,
|
||||
pub parent_topic_id: Option<String>,
|
||||
pub root_task_id: String,
|
||||
pub project_id: String,
|
||||
pub state: TopicState,
|
||||
pub trunk_version_before: Option<String>,
|
||||
pub change_set_hash: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TopicChange {
|
||||
pub change_id: String,
|
||||
pub topic_id: String,
|
||||
pub task_id: String,
|
||||
pub parent_task_id: Option<String>,
|
||||
pub file_path: String,
|
||||
pub operation: TopicChangeOperation,
|
||||
pub blob_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum TopicChangeOperation {
|
||||
Create,
|
||||
Modify,
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl TopicChangeOperation {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
TopicChangeOperation::Create => "create",
|
||||
TopicChangeOperation::Modify => "modify",
|
||||
TopicChangeOperation::Delete => "delete",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"create" => Some(TopicChangeOperation::Create),
|
||||
"modify" => Some(TopicChangeOperation::Modify),
|
||||
"delete" => Some(TopicChangeOperation::Delete),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum MergeQueueStatus {
|
||||
Queued,
|
||||
Merging,
|
||||
Conflict,
|
||||
Completed,
|
||||
Deferred,
|
||||
}
|
||||
|
||||
impl MergeQueueStatus {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
MergeQueueStatus::Queued => "queued",
|
||||
MergeQueueStatus::Merging => "merging",
|
||||
MergeQueueStatus::Conflict => "conflict",
|
||||
MergeQueueStatus::Completed => "completed",
|
||||
MergeQueueStatus::Deferred => "deferred",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"queued" => Some(MergeQueueStatus::Queued),
|
||||
"merging" => Some(MergeQueueStatus::Merging),
|
||||
"conflict" => Some(MergeQueueStatus::Conflict),
|
||||
"completed" => Some(MergeQueueStatus::Completed),
|
||||
"deferred" => Some(MergeQueueStatus::Deferred),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MergeQueueEntry {
|
||||
pub request_id: String,
|
||||
pub topic_id: Option<String>,
|
||||
pub dock_session_id: Option<String>,
|
||||
pub status: MergeQueueStatus,
|
||||
pub created_at: String,
|
||||
}
|
||||
97
src/todo.rs
97
src/todo.rs
|
|
@ -5,6 +5,7 @@ use uuid::Uuid;
|
|||
use crate::enums::{ToDoSource, ToDoStatus};
|
||||
use crate::errors::ValidationError;
|
||||
use crate::{CommunicationType, CommunicationValue, DataTypes, DataValue};
|
||||
use std::collections::HashSet;
|
||||
|
||||
const MAX_TITLE_LENGTH: usize = 500;
|
||||
const MAX_DESCRIPTION_LENGTH: usize = 10000;
|
||||
|
|
@ -12,14 +13,22 @@ const MIN_PRIORITY: u32 = 1;
|
|||
const MAX_PRIORITY: u32 = 1000;
|
||||
const DEFAULT_PRIORITY: u32 = 100;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Dependency {
|
||||
pub todo_id: Uuid,
|
||||
pub board_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ToDo {
|
||||
pub id: Uuid,
|
||||
pub board_id: Uuid,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub status: ToDoStatus,
|
||||
pub priority: u32,
|
||||
pub depends_on: Vec<Uuid>,
|
||||
pub cross_board_depends_on: Vec<Dependency>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub created_by: Uuid,
|
||||
|
|
@ -30,6 +39,7 @@ pub struct ToDo {
|
|||
pub source: ToDoSource,
|
||||
pub automation_chain_id: Option<Uuid>,
|
||||
pub tags: Vec<String>,
|
||||
pub estimated_tokens: u32,
|
||||
}
|
||||
|
||||
impl Default for ToDo {
|
||||
|
|
@ -37,11 +47,13 @@ impl Default for ToDo {
|
|||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
board_id: Uuid::nil(),
|
||||
title: String::new(),
|
||||
description: String::new(),
|
||||
status: ToDoStatus::default(),
|
||||
priority: DEFAULT_PRIORITY,
|
||||
depends_on: Vec::new(),
|
||||
cross_board_depends_on: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by: Uuid::nil(),
|
||||
|
|
@ -52,6 +64,7 @@ impl Default for ToDo {
|
|||
source: ToDoSource::User,
|
||||
automation_chain_id: None,
|
||||
tags: Vec::new(),
|
||||
estimated_tokens: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,11 +74,13 @@ impl ToDo {
|
|||
let now = Utc::now();
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
board_id: Uuid::nil(),
|
||||
title: title.trim().to_string(),
|
||||
description,
|
||||
status: ToDoStatus::default(),
|
||||
priority: DEFAULT_PRIORITY,
|
||||
depends_on: Vec::new(),
|
||||
cross_board_depends_on: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by,
|
||||
|
|
@ -76,6 +91,7 @@ impl ToDo {
|
|||
source: ToDoSource::User,
|
||||
automation_chain_id: None,
|
||||
tags: Vec::new(),
|
||||
estimated_tokens: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,12 +125,56 @@ impl ToDo {
|
|||
}
|
||||
|
||||
fn has_circular_dependency(&self) -> bool {
|
||||
if self.depends_on.is_empty() {
|
||||
return false;
|
||||
// Direct self-dependency
|
||||
if self.depends_on.contains(&self.id) {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Performs full graph cycle detection given a map of todo_id -> dependencies.
|
||||
/// Returns the ID of the first todo that would participate in a cycle, or None.
|
||||
pub fn detect_cycle_in_graph(deps: &std::collections::HashMap<Uuid, Vec<Uuid>>) -> Option<Uuid> {
|
||||
let mut visited: HashSet<Uuid> = HashSet::new();
|
||||
let mut in_stack: HashSet<Uuid> = HashSet::new();
|
||||
|
||||
fn dfs(
|
||||
node: Uuid,
|
||||
deps: &std::collections::HashMap<Uuid, Vec<Uuid>>,
|
||||
visited: &mut HashSet<Uuid>,
|
||||
in_stack: &mut HashSet<Uuid>,
|
||||
) -> Option<Uuid> {
|
||||
if in_stack.contains(&node) {
|
||||
return Some(node);
|
||||
}
|
||||
if visited.contains(&node) {
|
||||
return None;
|
||||
}
|
||||
visited.insert(node);
|
||||
in_stack.insert(node);
|
||||
|
||||
if let Some(children) = deps.get(&node) {
|
||||
for child in children {
|
||||
if let Some(cycle_node) = dfs(*child, deps, visited, in_stack) {
|
||||
return Some(cycle_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
in_stack.remove(&node);
|
||||
None
|
||||
}
|
||||
|
||||
for node in deps.keys() {
|
||||
if !visited.contains(node) {
|
||||
if let Some(cycle_node) = dfs(*node, deps, &mut visited, &mut in_stack) {
|
||||
return Some(cycle_node);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: ToDoStatus) -> Result<(), ValidationError> {
|
||||
let valid_transition = match (&self.status, &status) {
|
||||
(ToDoStatus::Pending, ToDoStatus::InProgress) => true,
|
||||
|
|
@ -350,4 +410,37 @@ mod tests {
|
|||
let result = todo.update(None, None, Some(0));
|
||||
assert!(matches!(result, Err(ValidationError::InvalidPriority)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_circular_dependency_self_reference() {
|
||||
let mut todo = ToDo::default();
|
||||
todo.depends_on = vec![todo.id];
|
||||
assert!(todo.has_circular_dependency());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_circular_dependency_none() {
|
||||
let todo = ToDo::default();
|
||||
assert!(!todo.has_circular_dependency());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_cycle_in_graph_simple_cycle() {
|
||||
let id_a = Uuid::new_v4();
|
||||
let id_b = Uuid::new_v4();
|
||||
let mut deps = std::collections::HashMap::new();
|
||||
deps.insert(id_a, vec![id_b]);
|
||||
deps.insert(id_b, vec![id_a]);
|
||||
assert!(ToDo::detect_cycle_in_graph(&deps).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_cycle_in_graph_no_cycle() {
|
||||
let id_a = Uuid::new_v4();
|
||||
let id_b = Uuid::new_v4();
|
||||
let mut deps = std::collections::HashMap::new();
|
||||
deps.insert(id_a, vec![id_b]);
|
||||
deps.insert(id_b, vec![]);
|
||||
assert!(ToDo::detect_cycle_in_graph(&deps).is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
77
src/tools.rs
77
src/tools.rs
|
|
@ -106,6 +106,20 @@ impl ToolDefinition {
|
|||
}
|
||||
}
|
||||
|
||||
/// Strip the board_id parameter from the tool's JSON schema.
|
||||
/// This is used to hide the board context from agents that shouldn't
|
||||
/// have to manage it manually (similar to project_id).
|
||||
pub fn strip_board_id(&mut self) {
|
||||
if let Some(obj) = self.parameters.as_object_mut() {
|
||||
if let Some(properties) = obj.get_mut("properties").and_then(|p| p.as_object_mut()) {
|
||||
properties.remove("board_id");
|
||||
}
|
||||
if let Some(required) = obj.get_mut("required").and_then(|r| r.as_array_mut()) {
|
||||
required.retain(|v| v.as_str() != Some("board_id"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this tool version is deprecated
|
||||
pub fn is_deprecated(&self) -> bool {
|
||||
self.deprecated
|
||||
|
|
@ -1077,7 +1091,8 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
|||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": { "type": "string", "description": "Project ID" }
|
||||
"project_id": { "type": "string", "description": "Project ID" },
|
||||
"board_id": { "type": "string", "description": "Board ID (defaults to project's primary board)" }
|
||||
},
|
||||
"required": ["project_id"]
|
||||
})
|
||||
|
|
@ -1091,6 +1106,7 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": { "type": "string", "description": "Project ID" },
|
||||
"board_id": { "type": "string", "description": "Board ID (defaults to project's primary board)" },
|
||||
"column_id": { "type": "string", "description": "Column ID to place the todo" },
|
||||
"title": { "type": "string", "description": "Title of the todo" },
|
||||
"description": { "type": "string", "description": "Optional description" },
|
||||
|
|
@ -1208,6 +1224,7 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": { "type": "string", "description": "Project ID" },
|
||||
"board_id": { "type": "string", "description": "Board ID (defaults to project's primary board)" },
|
||||
"name": { "type": "string", "description": "Column name" },
|
||||
"column_order": { "type": "number", "description": "Display order" }
|
||||
},
|
||||
|
|
@ -1516,6 +1533,7 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"plan_id": { "type": "string", "description": "Auto-generated plan ID" },
|
||||
"todo_id": { "type": "string", "description": "The todo ID this plan belongs to" },
|
||||
"steps": { "type": "array", "description": "List of mutation steps", "items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -2016,4 +2034,61 @@ Done with tools.
|
|||
assert_eq!(ToolCategory::Agent.as_str(), "agent");
|
||||
assert_eq!(ToolCategory::Other.as_str(), "other");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_streaming_rejects_tool_result_at_start() {
|
||||
let chunks = vec![
|
||||
"```tool-result:read_file\n",
|
||||
"{\"call_id\": \"call_01\"}\n",
|
||||
"```\nSome trailing text.",
|
||||
];
|
||||
let results = parse_tool_call_stream(chunks.into_iter());
|
||||
assert!(results.is_empty(), "tool-result: at stream start should be rejected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_streaming_rejects_multiple_tool_results() {
|
||||
let chunks = vec![
|
||||
"```tool-result:read_file\n{\"call_id\": \"c1\"}\n```\n",
|
||||
"```tool-result:write_file\n{\"call_id\": \"c2\"}\n```\n",
|
||||
];
|
||||
let results = parse_tool_call_stream(chunks.into_iter());
|
||||
assert!(results.is_empty(), "multiple tool-result: blocks should all be rejected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_streaming_mixed_tool_and_tool_result() {
|
||||
let chunks = vec![
|
||||
"```tool:read_file\n{\"call_id\": \"c1\"}\n```\n",
|
||||
"```tool-result:write_file\n{\"call_id\": \"c2\"}\n```\n",
|
||||
"```tool:search_files\n{\"call_id\": \"c3\"}\n```\n",
|
||||
];
|
||||
let results = parse_tool_call_stream(chunks.into_iter());
|
||||
assert_eq!(results.len(), 2, "should only parse the tool: blocks, not tool-result:");
|
||||
assert_eq!(results[0].call_id, "c1");
|
||||
assert_eq!(results[1].call_id, "c3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_streaming_tool_result_only_chunk() {
|
||||
let results = parse_tool_call_stream(vec!["```tool-result:read_file\n{\"call_id\": \"c1\"}\n```\n"].into_iter());
|
||||
assert!(results.is_empty(), "tool-result: as only chunk content should be rejected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_streaming_tool_result_in_middle_of_text() {
|
||||
let chunks = vec![
|
||||
"Some text before.\n```tool-result:read_file\n",
|
||||
"{\"call_id\": \"c1\"}\n```\n",
|
||||
"Some text after.",
|
||||
];
|
||||
let results = parse_tool_call_stream(chunks.into_iter());
|
||||
assert!(results.is_empty(), "tool-result: in middle of text should be rejected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_streaming_finish_rejects_tool_result_no_closing_fence() {
|
||||
let results = parse_tool_call_stream(vec!["```tool-result:read_file\n{\"call_id\": \"c1\"}\n"].into_iter());
|
||||
assert!(results.is_empty(), "tool-result: without closing fence should be rejected by finish()");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue