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};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum AgentType {
|
pub enum AgentType {
|
||||||
Planner,
|
Planner,
|
||||||
|
|
@ -39,3 +39,86 @@ pub enum ApprovalMode {
|
||||||
Batch,
|
Batch,
|
||||||
PerTool,
|
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")]
|
#[serde(rename = "type")]
|
||||||
pub event_type: String,
|
pub event_type: String,
|
||||||
pub content: Option<String>,
|
pub content: Option<String>,
|
||||||
|
pub thinking: Option<String>,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
pub message_id: Option<String>,
|
pub message_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +35,17 @@ impl StreamEvent {
|
||||||
Self {
|
Self {
|
||||||
event_type: "content".to_string(),
|
event_type: "content".to_string(),
|
||||||
content: Some(content),
|
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,
|
error: None,
|
||||||
message_id: None,
|
message_id: None,
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +55,7 @@ impl StreamEvent {
|
||||||
Self {
|
Self {
|
||||||
event_type: "error".to_string(),
|
event_type: "error".to_string(),
|
||||||
content: None,
|
content: None,
|
||||||
|
thinking: None,
|
||||||
error: Some(error),
|
error: Some(error),
|
||||||
message_id: None,
|
message_id: None,
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +65,7 @@ impl StreamEvent {
|
||||||
Self {
|
Self {
|
||||||
event_type: "done".to_string(),
|
event_type: "done".to_string(),
|
||||||
content: None,
|
content: None,
|
||||||
|
thinking: None,
|
||||||
error: None,
|
error: None,
|
||||||
message_id: Some(message_id),
|
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,
|
Delegated,
|
||||||
Failed,
|
Failed,
|
||||||
PendingApproval,
|
PendingApproval,
|
||||||
|
Draft,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ToDoStatus {
|
impl Default for ToDoStatus {
|
||||||
|
|
@ -29,6 +30,7 @@ impl fmt::Display for ToDoStatus {
|
||||||
ToDoStatus::ReadyForAgent => write!(f, "ready_for_agent"),
|
ToDoStatus::ReadyForAgent => write!(f, "ready_for_agent"),
|
||||||
ToDoStatus::Delegated => write!(f, "delegated"),
|
ToDoStatus::Delegated => write!(f, "delegated"),
|
||||||
ToDoStatus::Failed => write!(f, "failed"),
|
ToDoStatus::Failed => write!(f, "failed"),
|
||||||
|
ToDoStatus::Draft => write!(f, "draft"),
|
||||||
ToDoStatus::PendingApproval => write!(f, "pending_approval"),
|
ToDoStatus::PendingApproval => write!(f, "pending_approval"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +47,7 @@ impl std::str::FromStr for ToDoStatus {
|
||||||
"ready_for_agent" => Ok(ToDoStatus::ReadyForAgent),
|
"ready_for_agent" => Ok(ToDoStatus::ReadyForAgent),
|
||||||
"delegated" => Ok(ToDoStatus::Delegated),
|
"delegated" => Ok(ToDoStatus::Delegated),
|
||||||
"failed" => Ok(ToDoStatus::Failed),
|
"failed" => Ok(ToDoStatus::Failed),
|
||||||
|
"draft" => Ok(ToDoStatus::Draft),
|
||||||
"pending_approval" => Ok(ToDoStatus::PendingApproval),
|
"pending_approval" => Ok(ToDoStatus::PendingApproval),
|
||||||
_ => Err(format!("unknown status: {}", s)),
|
_ => Err(format!("unknown status: {}", s)),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,29 @@ pub use uuid::Uuid;
|
||||||
|
|
||||||
pub mod agents;
|
pub mod agents;
|
||||||
pub mod ai_response;
|
pub mod ai_response;
|
||||||
|
pub mod conclusion;
|
||||||
pub mod coral;
|
pub mod coral;
|
||||||
pub mod enums;
|
pub mod enums;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod krill;
|
pub mod krill;
|
||||||
pub mod project;
|
pub mod project;
|
||||||
|
pub mod sync;
|
||||||
pub mod task;
|
pub mod task;
|
||||||
pub mod todo;
|
pub mod todo;
|
||||||
pub mod tools;
|
pub mod tools;
|
||||||
|
|
||||||
pub use agents::{AgentType, ApprovalMode};
|
pub use agents::{AgentPolicy, AgentType, ApprovalMode, PlannedAction, SimulationResult};
|
||||||
pub use ai_response::{Conversation, Message};
|
pub use ai_response::{Conversation, Message};
|
||||||
|
pub use conclusion::{
|
||||||
|
Artifact, ArtifactType, ConclusionCard, ConclusionMetrics, ConclusionTrigger,
|
||||||
|
};
|
||||||
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};
|
||||||
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::ToDo;
|
pub use todo::{Dependency, ToDo};
|
||||||
pub use tools::{
|
pub use tools::{
|
||||||
format_tool_error, format_tool_result, parse_tool_call_blocks, parse_tool_call_stream,
|
format_tool_error, format_tool_result, parse_tool_call_blocks, parse_tool_call_stream,
|
||||||
tool_definitions, CompositionStep, ExecutionContext, MarketplaceSearchResults, ParsedToolCall,
|
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::enums::{ToDoSource, ToDoStatus};
|
||||||
use crate::errors::ValidationError;
|
use crate::errors::ValidationError;
|
||||||
use crate::{CommunicationType, CommunicationValue, DataTypes, DataValue};
|
use crate::{CommunicationType, CommunicationValue, DataTypes, DataValue};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
const MAX_TITLE_LENGTH: usize = 500;
|
const MAX_TITLE_LENGTH: usize = 500;
|
||||||
const MAX_DESCRIPTION_LENGTH: usize = 10000;
|
const MAX_DESCRIPTION_LENGTH: usize = 10000;
|
||||||
|
|
@ -12,14 +13,22 @@ const MIN_PRIORITY: u32 = 1;
|
||||||
const MAX_PRIORITY: u32 = 1000;
|
const MAX_PRIORITY: u32 = 1000;
|
||||||
const DEFAULT_PRIORITY: u32 = 100;
|
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)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct ToDo {
|
pub struct ToDo {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
pub board_id: Uuid,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub status: ToDoStatus,
|
pub status: ToDoStatus,
|
||||||
pub priority: u32,
|
pub priority: u32,
|
||||||
pub depends_on: Vec<Uuid>,
|
pub depends_on: Vec<Uuid>,
|
||||||
|
pub cross_board_depends_on: Vec<Dependency>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
pub created_by: Uuid,
|
pub created_by: Uuid,
|
||||||
|
|
@ -30,6 +39,7 @@ pub struct ToDo {
|
||||||
pub source: ToDoSource,
|
pub source: ToDoSource,
|
||||||
pub automation_chain_id: Option<Uuid>,
|
pub automation_chain_id: Option<Uuid>,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
pub estimated_tokens: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ToDo {
|
impl Default for ToDo {
|
||||||
|
|
@ -37,11 +47,13 @@ impl Default for ToDo {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
Self {
|
Self {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
|
board_id: Uuid::nil(),
|
||||||
title: String::new(),
|
title: String::new(),
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
status: ToDoStatus::default(),
|
status: ToDoStatus::default(),
|
||||||
priority: DEFAULT_PRIORITY,
|
priority: DEFAULT_PRIORITY,
|
||||||
depends_on: Vec::new(),
|
depends_on: Vec::new(),
|
||||||
|
cross_board_depends_on: Vec::new(),
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
created_by: Uuid::nil(),
|
created_by: Uuid::nil(),
|
||||||
|
|
@ -52,6 +64,7 @@ impl Default for ToDo {
|
||||||
source: ToDoSource::User,
|
source: ToDoSource::User,
|
||||||
automation_chain_id: None,
|
automation_chain_id: None,
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
|
estimated_tokens: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,11 +74,13 @@ impl ToDo {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
Self {
|
Self {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
|
board_id: Uuid::nil(),
|
||||||
title: title.trim().to_string(),
|
title: title.trim().to_string(),
|
||||||
description,
|
description,
|
||||||
status: ToDoStatus::default(),
|
status: ToDoStatus::default(),
|
||||||
priority: DEFAULT_PRIORITY,
|
priority: DEFAULT_PRIORITY,
|
||||||
depends_on: Vec::new(),
|
depends_on: Vec::new(),
|
||||||
|
cross_board_depends_on: Vec::new(),
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
created_by,
|
created_by,
|
||||||
|
|
@ -76,6 +91,7 @@ impl ToDo {
|
||||||
source: ToDoSource::User,
|
source: ToDoSource::User,
|
||||||
automation_chain_id: None,
|
automation_chain_id: None,
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
|
estimated_tokens: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,12 +125,56 @@ impl ToDo {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_circular_dependency(&self) -> bool {
|
fn has_circular_dependency(&self) -> bool {
|
||||||
if self.depends_on.is_empty() {
|
// Direct self-dependency
|
||||||
return false;
|
if self.depends_on.contains(&self.id) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
false
|
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> {
|
pub fn set_status(&mut self, status: ToDoStatus) -> Result<(), ValidationError> {
|
||||||
let valid_transition = match (&self.status, &status) {
|
let valid_transition = match (&self.status, &status) {
|
||||||
(ToDoStatus::Pending, ToDoStatus::InProgress) => true,
|
(ToDoStatus::Pending, ToDoStatus::InProgress) => true,
|
||||||
|
|
@ -350,4 +410,37 @@ mod tests {
|
||||||
let result = todo.update(None, None, Some(0));
|
let result = todo.update(None, None, Some(0));
|
||||||
assert!(matches!(result, Err(ValidationError::InvalidPriority)));
|
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
|
/// Check if this tool version is deprecated
|
||||||
pub fn is_deprecated(&self) -> bool {
|
pub fn is_deprecated(&self) -> bool {
|
||||||
self.deprecated
|
self.deprecated
|
||||||
|
|
@ -1077,7 +1091,8 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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"]
|
"required": ["project_id"]
|
||||||
})
|
})
|
||||||
|
|
@ -1091,6 +1106,7 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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)" },
|
||||||
"column_id": { "type": "string", "description": "Column ID to place the todo" },
|
"column_id": { "type": "string", "description": "Column ID to place the todo" },
|
||||||
"title": { "type": "string", "description": "Title of the todo" },
|
"title": { "type": "string", "description": "Title of the todo" },
|
||||||
"description": { "type": "string", "description": "Optional description" },
|
"description": { "type": "string", "description": "Optional description" },
|
||||||
|
|
@ -1208,6 +1224,7 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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)" },
|
||||||
"name": { "type": "string", "description": "Column name" },
|
"name": { "type": "string", "description": "Column name" },
|
||||||
"column_order": { "type": "number", "description": "Display order" }
|
"column_order": { "type": "number", "description": "Display order" }
|
||||||
},
|
},
|
||||||
|
|
@ -1516,6 +1533,7 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"plan_id": { "type": "string", "description": "Auto-generated plan ID" },
|
"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": {
|
"steps": { "type": "array", "description": "List of mutation steps", "items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -2016,4 +2034,61 @@ Done with tools.
|
||||||
assert_eq!(ToolCategory::Agent.as_str(), "agent");
|
assert_eq!(ToolCategory::Agent.as_str(), "agent");
|
||||||
assert_eq!(ToolCategory::Other.as_str(), "other");
|
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