use serde::{Deserialize, Serialize};
use crate::error::ValidationError;
use crate::layout::ContextLayout;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextTransform {
pub from_blueprint: String,
pub to_blueprint: String,
pub mappings: Vec<RegionMapping>,
}
impl ContextTransform {
pub(super) fn validate(
&self,
layout: &ContextLayout,
) -> std::result::Result<(), ValidationError> {
for mapping in &self.mappings {
if layout.get_region(&mapping.to_region).is_none() {
return Err(ValidationError::Region {
region: mapping.to_region.clone(),
message: "transform target region not found in layout".to_string(),
});
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegionMapping {
pub from_region: String,
pub to_region: String,
pub transform: Option<ContentTransform>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransitionEdge {
pub target: String,
#[serde(default)]
pub condition: TransitionCondition,
pub hint: Option<String>,
#[serde(default)]
pub transform: EdgeTransform,
#[serde(default)]
pub gate: Option<TransitionGate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stuck: Option<StuckConfig>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct StuckConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after_iterations: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after_minutes: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after_same_file_edits: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after_tool_calls: Option<usize>,
}
impl StuckConfig {
pub fn is_armed(&self) -> bool {
self.after_iterations.is_some()
|| self.after_minutes.is_some()
|| self.after_same_file_edits.is_some()
|| self.after_tool_calls.is_some()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TransitionGate {
#[serde(default)]
pub require_modifications: bool,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub region: Option<String>,
#[serde(default)]
pub tools: Vec<String>,
#[serde(default)]
pub max_attempts: Option<usize>,
#[serde(default)]
pub require_region_updated: Option<String>,
#[serde(default)]
pub require_regions: Vec<String>,
#[serde(default)]
pub require_no_open_items: Option<String>,
}
pub const DEFAULT_GATE_ATTEMPTS: usize = 3;
pub const MODIFYING_TOOLS: &[&str] = &["write_file", "edit_file"];
pub const SUBMIT_OUTPUT_TOOL: &str = "submit_output";
pub const DEFAULT_OUTPUT_REENTRY_CAP: usize = 3;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct NudgeConfig {
#[serde(default)]
pub enabled: Option<bool>,
#[serde(default)]
pub max: Option<usize>,
#[serde(default)]
pub text: Option<String>,
}
pub const DEFAULT_NUDGE_TEXT: &str = "You have tools available. Please use them to complete the task. Start by reading the relevant files in the working directory.";
pub const DEFAULT_MAX_NUDGES: usize = 3;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedNudge {
pub enabled: bool,
pub max: usize,
pub text: String,
}
pub fn resolve_nudge(
global: Option<&NudgeConfig>,
agent: Option<&NudgeConfig>,
stage: Option<&NudgeConfig>,
stage_is_reviewed: bool,
) -> ResolvedNudge {
fn field<T: Clone>(
global: Option<&NudgeConfig>,
agent: Option<&NudgeConfig>,
stage: Option<&NudgeConfig>,
get: impl Fn(&NudgeConfig) -> Option<T>,
) -> Option<T> {
stage
.and_then(&get)
.or_else(|| agent.and_then(&get))
.or_else(|| global.and_then(&get))
}
ResolvedNudge {
enabled: field(global, agent, stage, |c| c.enabled).unwrap_or(!stage_is_reviewed),
max: field(global, agent, stage, |c| c.max).unwrap_or(DEFAULT_MAX_NUDGES),
text: field(global, agent, stage, |c| c.text.clone())
.unwrap_or_else(|| DEFAULT_NUDGE_TEXT.to_string()),
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TransitionCondition {
#[default]
Always,
Error,
MaxIterations,
LlmChoice,
DeadEnd,
Stuck,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EdgeTransform {
#[default]
Direct,
Clear,
Compact {
#[serde(default)]
prompt: Option<String>,
},
Custom {
carry: Vec<String>,
compact: Vec<String>,
clear: Vec<String>,
compact_prompt: Option<String>,
},
}
impl PartialEq for EdgeTransform {
#[inline(never)]
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Direct, Self::Direct) | (Self::Clear, Self::Clear) => true,
(Self::Compact { prompt: a }, Self::Compact { prompt: b }) => a == b,
(
Self::Custom {
carry: ca,
compact: coa,
clear: cla,
compact_prompt: cpa,
},
Self::Custom {
carry: cb,
compact: cob,
clear: clb,
compact_prompt: cpb,
},
) => ca == cb && coa == cob && cla == clb && cpa == cpb,
_ => false,
}
}
}
impl Eq for EdgeTransform {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ContentTransform {
Direct,
Summarize,
Extract {
fields: Vec<String>,
},
}