use crate::format::OutputFormat;
use anyhow::{Result, anyhow};
use heck::{ToKebabCase, ToLowerCamelCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
pub const DEFAULT_UI_PORT: u16 = 31994;
pub const DEFAULT_ID_WORDS: u8 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum IdCase {
#[default]
KebabCase,
SnakeCase,
CamelCase,
PascalCase,
Lowercase,
Uppercase,
TitleCase,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdsConfig {
#[serde(default = "default_id_words")]
pub task_id_words: u8,
#[serde(default = "default_id_words")]
pub agent_id_words: u8,
#[serde(default)]
pub id_case: IdCase,
}
fn default_id_words() -> u8 {
DEFAULT_ID_WORDS
}
impl Default for IdsConfig {
fn default() -> Self {
Self {
task_id_words: DEFAULT_ID_WORDS,
agent_id_words: DEFAULT_ID_WORDS,
id_case: IdCase::default(),
}
}
}
impl IdCase {
pub fn convert(&self, input: &str) -> String {
match self {
IdCase::KebabCase => input.to_kebab_case(),
IdCase::SnakeCase => input.to_snake_case(),
IdCase::CamelCase => input.to_lower_camel_case(),
IdCase::PascalCase => input.to_upper_camel_case(),
IdCase::Lowercase => input.replace('-', "").to_lowercase(),
IdCase::Uppercase => input.replace('-', "").to_uppercase(),
IdCase::TitleCase => input.to_title_case(),
}
}
pub fn separator(&self) -> Option<&'static str> {
match self {
IdCase::KebabCase => Some("-"),
IdCase::SnakeCase => Some("_"),
IdCase::TitleCase => Some(" "),
IdCase::CamelCase | IdCase::PascalCase | IdCase::Lowercase | IdCase::Uppercase => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UiMode {
#[default]
None,
Web,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UiConfig {
#[serde(default)]
pub mode: UiMode,
#[serde(default = "default_ui_port")]
pub port: u16,
#[serde(default = "default_retry_initial_ms")]
pub retry_initial_ms: u64,
#[serde(default = "default_retry_jitter_ms")]
pub retry_jitter_ms: u64,
#[serde(default = "default_retry_max_ms")]
pub retry_max_ms: u64,
#[serde(default = "default_retry_multiplier")]
pub retry_multiplier: f64,
}
impl Default for UiConfig {
fn default() -> Self {
Self {
mode: UiMode::default(),
port: default_ui_port(),
retry_initial_ms: default_retry_initial_ms(),
retry_jitter_ms: default_retry_jitter_ms(),
retry_max_ms: default_retry_max_ms(),
retry_multiplier: default_retry_multiplier(),
}
}
}
fn default_ui_port() -> u16 {
DEFAULT_UI_PORT
}
fn default_retry_initial_ms() -> u64 {
15_000 }
fn default_retry_jitter_ms() -> u64 {
5_000 }
fn default_retry_max_ms() -> u64 {
240_000 }
fn default_retry_multiplier() -> f64 {
2.0
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AutoAdvanceConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub target_state: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum UnknownKeyBehavior {
Allow,
#[default]
Warn,
Reject,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum GateEnforcement {
Allow,
#[default]
Warn,
Reject,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateDefinition {
#[serde(rename = "type")]
pub gate_type: String,
#[serde(default)]
pub enforcement: GateEnforcement,
#[serde(default)]
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttachmentKeyDefinition {
pub mime: String,
#[serde(default = "default_append_mode")]
pub mode: String,
}
fn default_append_mode() -> String {
"append".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttachmentsConfig {
#[serde(default)]
pub unknown_key: UnknownKeyBehavior,
#[serde(default = "AttachmentsConfig::default_definitions")]
pub definitions: HashMap<String, AttachmentKeyDefinition>,
}
impl Default for AttachmentsConfig {
fn default() -> Self {
Self {
unknown_key: UnknownKeyBehavior::default(),
definitions: Self::default_definitions(),
}
}
}
impl AttachmentsConfig {
pub fn default_definitions() -> HashMap<String, AttachmentKeyDefinition> {
let mut defs = HashMap::new();
defs.insert(
"commit".to_string(),
AttachmentKeyDefinition {
mime: "text/git.hash".to_string(),
mode: "append".to_string(),
},
);
defs.insert(
"checkin".to_string(),
AttachmentKeyDefinition {
mime: "text/p4.changelist".to_string(),
mode: "append".to_string(),
},
);
defs.insert(
"meta".to_string(),
AttachmentKeyDefinition {
mime: "application/json".to_string(),
mode: "replace".to_string(),
},
);
defs.insert(
"note".to_string(),
AttachmentKeyDefinition {
mime: "text/plain".to_string(),
mode: "append".to_string(),
},
);
defs.insert(
"log".to_string(),
AttachmentKeyDefinition {
mime: "text/plain".to_string(),
mode: "append".to_string(),
},
);
defs.insert(
"error".to_string(),
AttachmentKeyDefinition {
mime: "text/plain".to_string(),
mode: "append".to_string(),
},
);
defs.insert(
"output".to_string(),
AttachmentKeyDefinition {
mime: "text/plain".to_string(),
mode: "append".to_string(),
},
);
defs.insert(
"diff".to_string(),
AttachmentKeyDefinition {
mime: "text/x-diff".to_string(),
mode: "append".to_string(),
},
);
defs.insert(
"changelist".to_string(),
AttachmentKeyDefinition {
mime: "text/plain".to_string(),
mode: "append".to_string(),
},
);
defs.insert(
"plan".to_string(),
AttachmentKeyDefinition {
mime: "text/markdown".to_string(),
mode: "replace".to_string(),
},
);
defs.insert(
"result".to_string(),
AttachmentKeyDefinition {
mime: "application/json".to_string(),
mode: "replace".to_string(),
},
);
defs.insert(
"context".to_string(),
AttachmentKeyDefinition {
mime: "text/plain".to_string(),
mode: "replace".to_string(),
},
);
defs.insert(
"gate/tests".to_string(),
AttachmentKeyDefinition {
mime: "text/plain".to_string(),
mode: "append".to_string(),
},
);
defs.insert(
"gate/commit".to_string(),
AttachmentKeyDefinition {
mime: "text/plain".to_string(),
mode: "append".to_string(),
},
);
defs.insert(
"gate/review".to_string(),
AttachmentKeyDefinition {
mime: "text/plain".to_string(),
mode: "append".to_string(),
},
);
defs
}
pub fn get_definition(&self, key: &str) -> Option<&AttachmentKeyDefinition> {
self.definitions.get(key)
}
pub fn is_known_key(&self, key: &str) -> bool {
self.definitions.contains_key(key)
}
pub fn get_mime_default(&self, key: &str) -> &str {
self.definitions
.get(key)
.map(|d| d.mime.as_str())
.unwrap_or("text/plain")
}
pub fn get_mode_default(&self, key: &str) -> &str {
self.definitions
.get(key)
.map(|d| d.mode.as_str())
.unwrap_or("append")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TagDefinition {
#[serde(default)]
pub category: Option<String>,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TagsConfig {
#[serde(default)]
pub unknown_tag: UnknownKeyBehavior,
#[serde(default)]
pub definitions: HashMap<String, TagDefinition>,
}
impl Default for TagsConfig {
fn default() -> Self {
Self {
unknown_tag: UnknownKeyBehavior::default(),
definitions: HashMap::new(),
}
}
}
impl TagsConfig {
pub fn is_known_tag(&self, tag: &str) -> bool {
self.definitions.contains_key(tag)
}
pub fn tag_names(&self) -> Vec<&str> {
self.definitions.keys().map(|s| s.as_str()).collect()
}
pub fn tags_in_category(&self, category: &str) -> Vec<&str> {
self.definitions
.iter()
.filter(|(_, def)| def.category.as_deref() == Some(category))
.map(|(name, _)| name.as_str())
.collect()
}
pub fn categories(&self) -> Vec<&str> {
let mut cats: Vec<&str> = self
.definitions
.values()
.filter_map(|def| def.category.as_deref())
.collect();
cats.sort();
cats.dedup();
cats
}
pub fn validate_tag(&self, tag: &str) -> Result<Option<String>> {
if self.is_known_tag(tag) {
return Ok(None);
}
match self.unknown_tag {
UnknownKeyBehavior::Allow => Ok(None),
UnknownKeyBehavior::Warn => Ok(Some(format!(
"Unknown tag '{}'. Known tags: {:?}",
tag,
self.tag_names()
))),
UnknownKeyBehavior::Reject => Err(anyhow!(
"Unknown tag '{}'. Configure in tags.definitions or set unknown_tag to 'allow' or 'warn'. Known tags: {:?}",
tag,
self.tag_names()
)),
}
}
pub fn validate_tags(&self, tags: &[String]) -> Result<Vec<String>> {
let mut warnings = Vec::new();
for tag in tags {
if let Some(warning) = self.validate_tag(tag)? {
warnings.push(warning);
}
}
Ok(warnings)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
#[serde(default)]
pub server: ServerConfig,
#[serde(default)]
pub paths: PathsConfig,
#[serde(default)]
pub states: StatesConfig,
#[serde(default)]
pub dependencies: DependenciesConfig,
#[serde(default)]
pub auto_advance: AutoAdvanceConfig,
#[serde(default)]
pub attachments: AttachmentsConfig,
#[serde(default)]
pub phases: PhasesConfig,
#[serde(default)]
pub tags: TagsConfig,
#[serde(default)]
pub ids: IdsConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerPaths {
pub db_path: PathBuf,
pub media_dir: PathBuf,
pub log_dir: PathBuf,
pub config_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
#[serde(default = "default_db_path")]
pub db_path: PathBuf,
#[serde(default = "default_media_dir")]
pub media_dir: PathBuf,
#[serde(default = "default_claim_limit")]
pub claim_limit: i32,
#[serde(default = "default_stale_timeout")]
pub stale_timeout_seconds: i64,
#[serde(default)]
pub default_format: OutputFormat,
#[serde(default = "default_skills_dir")]
pub skills_dir: PathBuf,
#[serde(default = "default_log_dir")]
pub log_dir: PathBuf,
#[serde(default)]
pub ui: UiConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_workflow: Option<String>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
db_path: default_db_path(),
media_dir: default_media_dir(),
claim_limit: default_claim_limit(),
stale_timeout_seconds: default_stale_timeout(),
default_format: OutputFormat::default(),
skills_dir: default_skills_dir(),
log_dir: default_log_dir(),
ui: UiConfig::default(),
default_workflow: None,
}
}
}
fn default_db_path() -> PathBuf {
PathBuf::from("task-graph/tasks.db")
}
fn default_media_dir() -> PathBuf {
PathBuf::from("task-graph/media")
}
fn default_skills_dir() -> PathBuf {
PathBuf::from("task-graph/skills")
}
fn default_log_dir() -> PathBuf {
PathBuf::from("task-graph/logs")
}
fn default_paths_root() -> String {
".".to_string()
}
fn default_claim_limit() -> i32 {
5
}
fn default_stale_timeout() -> i64 {
900 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathsConfig {
#[serde(default = "default_paths_root")]
pub root: String,
#[serde(default)]
pub style: PathStyle,
#[serde(default)]
pub map_windows_drives: bool,
#[serde(default)]
pub mappings: HashMap<String, String>,
}
impl Default for PathsConfig {
fn default() -> Self {
Self {
root: default_paths_root(),
style: PathStyle::Relative,
map_windows_drives: false,
mappings: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum PathStyle {
#[default]
Relative,
ProjectPrefixed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatesConfig {
#[serde(default = "default_initial_state")]
pub initial: String,
#[serde(default = "default_disconnect_state")]
pub disconnect_state: String,
#[serde(default = "default_blocking_states")]
pub blocking_states: Vec<String>,
#[serde(default = "default_state_definitions")]
pub definitions: HashMap<String, StateDefinition>,
}
impl Default for StatesConfig {
fn default() -> Self {
Self {
initial: default_initial_state(),
disconnect_state: default_disconnect_state(),
blocking_states: default_blocking_states(),
definitions: default_state_definitions(),
}
}
}
fn default_initial_state() -> String {
"pending".to_string()
}
fn default_disconnect_state() -> String {
"pending".to_string()
}
fn default_blocking_states() -> Vec<String> {
vec![
"pending".to_string(),
"assigned".to_string(),
"working".to_string(),
]
}
fn default_state_definitions() -> HashMap<String, StateDefinition> {
let mut defs = HashMap::new();
defs.insert(
"pending".to_string(),
StateDefinition {
exits: vec![
"assigned".to_string(),
"working".to_string(),
"cancelled".to_string(),
],
timed: false,
},
);
defs.insert(
"assigned".to_string(),
StateDefinition {
exits: vec![
"working".to_string(),
"pending".to_string(),
"cancelled".to_string(),
],
timed: false,
},
);
defs.insert(
"working".to_string(),
StateDefinition {
exits: vec![
"completed".to_string(),
"failed".to_string(),
"pending".to_string(),
],
timed: true,
},
);
defs.insert(
"completed".to_string(),
StateDefinition {
exits: vec!["pending".to_string()],
timed: false,
},
);
defs.insert(
"failed".to_string(),
StateDefinition {
exits: vec!["pending".to_string()],
timed: false,
},
);
defs.insert(
"cancelled".to_string(),
StateDefinition {
exits: vec![],
timed: false,
},
);
defs
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateDefinition {
#[serde(default)]
pub exits: Vec<String>,
#[serde(default)]
pub timed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependenciesConfig {
#[serde(default = "default_dependency_definitions")]
pub definitions: HashMap<String, DependencyDefinition>,
}
impl Default for DependenciesConfig {
fn default() -> Self {
Self {
definitions: default_dependency_definitions(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyDefinition {
pub display: DependencyDisplay,
pub blocks: BlockTarget,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DependencyDisplay {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BlockTarget {
None,
Start,
Completion,
}
fn default_dependency_definitions() -> HashMap<String, DependencyDefinition> {
let mut defs = HashMap::new();
defs.insert(
"blocks".to_string(),
DependencyDefinition {
display: DependencyDisplay::Horizontal,
blocks: BlockTarget::Start,
},
);
defs.insert(
"follows".to_string(),
DependencyDefinition {
display: DependencyDisplay::Horizontal,
blocks: BlockTarget::Start,
},
);
defs.insert(
"contains".to_string(),
DependencyDefinition {
display: DependencyDisplay::Vertical,
blocks: BlockTarget::Completion,
},
);
defs.insert(
"duplicate".to_string(),
DependencyDefinition {
display: DependencyDisplay::Horizontal,
blocks: BlockTarget::None,
},
);
defs.insert(
"see-also".to_string(),
DependencyDefinition {
display: DependencyDisplay::Horizontal,
blocks: BlockTarget::None,
},
);
defs.insert(
"relates-to".to_string(),
DependencyDefinition {
display: DependencyDisplay::Horizontal,
blocks: BlockTarget::None,
},
);
defs
}
impl DependenciesConfig {
pub fn is_valid_dep_type(&self, dep_type: &str) -> bool {
self.definitions.contains_key(dep_type)
}
pub fn get_definition(&self, dep_type: &str) -> Option<&DependencyDefinition> {
self.definitions.get(dep_type)
}
pub fn start_blocking_types(&self) -> Vec<&str> {
self.definitions
.iter()
.filter(|(_, def)| def.blocks == BlockTarget::Start)
.map(|(name, _)| name.as_str())
.collect()
}
pub fn completion_blocking_types(&self) -> Vec<&str> {
self.definitions
.iter()
.filter(|(_, def)| def.blocks == BlockTarget::Completion)
.map(|(name, _)| name.as_str())
.collect()
}
pub fn vertical_types(&self) -> Vec<&str> {
self.definitions
.iter()
.filter(|(_, def)| def.display == DependencyDisplay::Vertical)
.map(|(name, _)| name.as_str())
.collect()
}
pub fn dep_type_names(&self) -> Vec<&str> {
self.definitions.keys().map(|s| s.as_str()).collect()
}
pub fn validate(&self) -> anyhow::Result<()> {
if self.definitions.is_empty() {
return Err(anyhow::anyhow!(
"At least one dependency type must be defined"
));
}
let has_start_blocking = self
.definitions
.values()
.any(|d| d.blocks == BlockTarget::Start);
if !has_start_blocking {
return Err(anyhow::anyhow!(
"At least one dependency type with blocks: start must be defined"
));
}
Ok(())
}
}
impl StatesConfig {
pub fn is_valid_state(&self, state: &str) -> bool {
self.definitions.contains_key(state)
}
pub fn is_valid_transition(&self, from: &str, to: &str) -> bool {
if let Some(def) = self.definitions.get(from) {
def.exits.contains(&to.to_string())
} else {
false
}
}
pub fn is_timed_state(&self, state: &str) -> bool {
self.definitions
.get(state)
.map(|d| d.timed)
.unwrap_or(false)
}
pub fn is_terminal_state(&self, state: &str) -> bool {
self.definitions
.get(state)
.map(|d| d.exits.is_empty())
.unwrap_or(false)
}
pub fn is_blocking_state(&self, state: &str) -> bool {
self.blocking_states.contains(&state.to_string())
}
pub fn state_names(&self) -> Vec<&str> {
self.definitions.keys().map(|s| s.as_str()).collect()
}
pub fn get_exits(&self, state: &str) -> Vec<&str> {
self.definitions
.get(state)
.map(|d| d.exits.iter().map(|s| s.as_str()).collect())
.unwrap_or_default()
}
pub fn untimed_state_names(&self) -> Vec<&str> {
self.definitions
.iter()
.filter(|(_, def)| !def.timed)
.map(|(name, _)| name.as_str())
.collect()
}
pub fn validate(&self) -> Result<()> {
if !self.definitions.contains_key(&self.initial) {
return Err(anyhow!(
"Initial state '{}' is not defined in state definitions",
self.initial
));
}
if !self.definitions.contains_key(&self.disconnect_state) {
return Err(anyhow!(
"Disconnect state '{}' is not defined in state definitions",
self.disconnect_state
));
}
if self.is_timed_state(&self.disconnect_state) {
return Err(anyhow!(
"Disconnect state '{}' must not be a timed state",
self.disconnect_state
));
}
for state in &self.blocking_states {
if !self.definitions.contains_key(state) {
return Err(anyhow!(
"Blocking state '{}' is not defined in state definitions",
state
));
}
}
for (state_name, def) in &self.definitions {
for exit in &def.exits {
if !self.definitions.contains_key(exit) {
return Err(anyhow!(
"State '{}' has exit '{}' which is not defined",
state_name,
exit
));
}
}
}
let has_terminal = self.definitions.values().any(|d| d.exits.is_empty());
if !has_terminal {
return Err(anyhow!(
"At least one terminal state (with empty exits) must be defined"
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhasesConfig {
#[serde(default)]
pub unknown_phase: UnknownKeyBehavior,
#[serde(default = "default_phases")]
pub definitions: HashSet<String>,
}
impl Default for PhasesConfig {
fn default() -> Self {
Self {
unknown_phase: UnknownKeyBehavior::Warn,
definitions: default_phases(),
}
}
}
fn default_phases() -> HashSet<String> {
[
"deliver", "triage", "explore", "diagnose", "design", "plan", "implement", "test", "review", "security", "doc", "integrate", "deploy", "monitor", "optimize", ]
.iter()
.map(|s| s.to_string())
.collect()
}
impl PhasesConfig {
pub fn is_known_phase(&self, phase: &str) -> bool {
self.definitions.contains(phase)
}
pub fn phase_names(&self) -> Vec<&str> {
self.definitions.iter().map(|s| s.as_str()).collect()
}
pub fn check_phase(&self, phase: &str) -> Result<Option<String>> {
if self.is_known_phase(phase) {
return Ok(None);
}
match self.unknown_phase {
UnknownKeyBehavior::Allow => Ok(None),
UnknownKeyBehavior::Warn => Ok(Some(format!(
"Unknown phase '{}'. Known phases: {:?}",
phase,
self.phase_names()
))),
UnknownKeyBehavior::Reject => Err(anyhow!(
"Unknown phase '{}'. Known phases: {:?}. Configure in phases.definitions or set unknown_phase to 'allow' or 'warn'.",
phase,
self.phase_names()
)),
}
}
}
impl Config {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let config: Config = serde_yaml::from_str(&content)?;
Ok(config)
}
pub fn load_or_default() -> Self {
if let Ok(config_path) = std::env::var("TASK_GRAPH_CONFIG_PATH")
&& let Ok(config) = Self::load(&config_path)
{
return config;
}
if let Ok(config) = Self::load("task-graph/config.yaml") {
return config;
}
if let Ok(config) = Self::load(".task-graph/config.yaml") {
return config;
}
let mut config = Self::default();
if let Ok(db_path) = std::env::var("TASK_GRAPH_DB_PATH") {
config.server.db_path = PathBuf::from(db_path);
}
if let Ok(media_dir) = std::env::var("TASK_GRAPH_MEDIA_DIR") {
config.server.media_dir = PathBuf::from(media_dir);
}
if let Ok(log_dir) = std::env::var("TASK_GRAPH_LOG_DIR") {
config.server.log_dir = PathBuf::from(log_dir);
}
config
}
pub fn ensure_db_dir(&self) -> Result<()> {
if let Some(parent) = self.server.db_path.parent() {
std::fs::create_dir_all(parent)?;
}
Ok(())
}
pub fn ensure_media_dir(&self) -> Result<()> {
std::fs::create_dir_all(&self.server.media_dir)?;
Ok(())
}
pub fn ensure_log_dir(&self) -> Result<()> {
std::fs::create_dir_all(&self.server.log_dir)?;
Ok(())
}
pub fn media_dir(&self) -> &Path {
&self.server.media_dir
}
pub fn log_dir(&self) -> &Path {
&self.server.log_dir
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolPrompt {
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Prompts {
pub instructions: Option<String>,
#[serde(default)]
pub tools: HashMap<String, ToolPrompt>,
}
impl Prompts {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let prompts: Option<Prompts> = serde_yaml::from_str(&content)?;
Ok(prompts.unwrap_or_default())
}
pub fn load_or_default() -> Self {
if let Ok(prompts) = Self::load("task-graph/prompts.yaml") {
return prompts;
}
if let Ok(prompts) = Self::load(".task-graph/prompts.yaml") {
return prompts;
}
Self::default()
}
pub fn get_tool_description(&self, name: &str) -> Option<&str> {
self.tools.get(name).map(|t| t.description.as_str())
}
}