use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanningView {
pub goal: String,
pub budget: PlanningBudget,
pub screen: PlanningScreen,
pub elements: Vec<PlanningElement>,
#[serde(default)]
pub adapter_facts: Vec<AdapterFactRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub adapter_actions: Vec<AdapterActionRef>,
#[serde(default)]
pub capabilities: Vec<CapabilityRef>,
#[serde(default)]
pub run_progress: RunProgress,
#[serde(default)]
pub memories: Vec<MemoryRef>,
#[serde(default)]
pub knowledge: Vec<KnowledgeRef>,
#[serde(default)]
pub recent_events: Vec<EventRef>,
#[serde(default)]
pub blockers: Vec<Blocker>,
#[serde(default)]
pub anomalies: Vec<AnomalyRef>,
#[serde(default)]
pub evidence: Vec<EvidenceRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub selection_rationale: Option<String>,
pub omitted_counts: OmittedCounts,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub adapter_actions_prompt: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanningBudget {
pub max_tokens: u32,
pub max_elements: u32,
pub max_memories: u32,
pub max_adapter_facts: u32,
#[serde(default = "default_max_knowledge")]
pub max_knowledge: u32,
#[serde(default = "default_max_recent_events")]
pub max_recent_events: u32,
}
fn default_max_knowledge() -> u32 {
8
}
fn default_max_recent_events() -> u32 {
10
}
impl Default for PlanningBudget {
fn default() -> Self {
Self {
max_tokens: 8000,
max_elements: 80,
max_memories: 8,
max_adapter_facts: 12,
max_knowledge: 8,
max_recent_events: 10,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RunProgress {
pub steps_used: u32,
pub max_steps: u32,
}
impl RunProgress {
pub fn steps_remaining(&self) -> u32 {
self.max_steps.saturating_sub(self.steps_used)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PlanningScreen {
pub active_app: String,
#[serde(default)]
pub window: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanningElement {
pub id: String,
pub element_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
pub state: PlanningElementState,
#[serde(default)]
pub clickable: bool,
#[serde(default)]
pub settable: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub select_options: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PlanningElementState {
pub focused: bool,
pub selected: bool,
pub enabled: bool,
pub checked: bool,
pub expanded: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryRef {
pub id: i64,
pub kind: String,
pub summary: String,
pub content: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeRef {
pub id: i64,
pub source: String,
pub content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdapterFactRef {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub adapter: String,
pub kind: String,
pub payload: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdapterActionRef {
pub adapter: String,
pub action: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub params_schema: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
#[serde(default)]
pub mutates_state: bool,
#[serde(default)]
pub requires_verification: bool,
#[serde(default)]
pub returns_data: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityRef {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventRef {
pub id: String,
pub kind: String,
pub summary: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub at: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvidenceRef {
pub source: String,
pub id: String,
pub summary: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyRef {
pub kind: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Blocker {
pub kind: String,
pub description: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub element_id: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OmittedCounts {
pub elements: u32,
pub memories: u32,
pub knowledge: u32,
pub adapter_facts: u32,
pub recent_events: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn budget_default_keeps_typical_prompts_under_common_windows() {
let b = PlanningBudget::default();
assert!(b.max_tokens <= 8000);
assert!(b.max_elements <= 100);
assert!(b.max_memories <= 16);
}
#[test]
fn empty_view_serializes_compactly() {
let view = PlanningView {
goal: "test".into(),
budget: PlanningBudget::default(),
screen: PlanningScreen {
active_app: "TestApp".into(),
window: "main".into(),
summary: None,
url: None,
},
elements: vec![],
adapter_facts: vec![],
adapter_actions: vec![],
capabilities: vec![],
memories: vec![],
knowledge: vec![],
recent_events: vec![],
blockers: vec![],
anomalies: vec![],
evidence: vec![],
selection_rationale: None,
omitted_counts: OmittedCounts::default(),
run_progress: RunProgress::default(),
adapter_actions_prompt: None,
};
let json = serde_json::to_string(&view).unwrap();
assert!(
json.len() < 700,
"empty view should serialize compactly, got {} chars",
json.len()
);
}
#[test]
fn view_roundtrips_through_json() {
let view = PlanningView {
goal: "submit form".into(),
budget: PlanningBudget {
max_tokens: 4000,
max_elements: 40,
max_memories: 4,
max_adapter_facts: 6,
max_knowledge: 4,
max_recent_events: 5,
},
screen: PlanningScreen {
active_app: "Browser".into(),
window: "Concur — Expenses".into(),
summary: Some("Expense form, partially filled".into()),
url: Some("https://concur.example.com/expenses".into()),
},
elements: vec![PlanningElement {
id: "dom:submit".into(),
element_type: "button".into(),
label: Some("Submit for Approval".into()),
value: None,
state: PlanningElementState {
focused: false,
selected: false,
enabled: true,
checked: false,
expanded: false,
},
clickable: true,
settable: false,
select_options: None,
}],
adapter_facts: vec![],
adapter_actions: vec![],
capabilities: vec![CapabilityRef {
id: "cdp_bound".into(),
detail: Some("Google Chrome — concur.example.com".into()),
}],
memories: vec![],
knowledge: vec![],
recent_events: vec![],
blockers: vec![],
anomalies: vec![],
evidence: vec![],
selection_rationale: Some("Submit-related element + cdp capability".into()),
omitted_counts: OmittedCounts {
elements: 431,
..Default::default()
},
run_progress: RunProgress {
steps_used: 7,
max_steps: 80,
},
adapter_actions_prompt: None,
};
let json = serde_json::to_string(&view).unwrap();
let back: PlanningView = serde_json::from_str(&json).unwrap();
assert_eq!(back.goal, view.goal);
assert_eq!(back.elements.len(), 1);
assert_eq!(back.capabilities[0].id, "cdp_bound");
assert_eq!(back.omitted_counts.elements, 431);
assert_eq!(back.run_progress.steps_remaining(), 73);
}
#[test]
fn run_progress_remaining_saturates_at_zero() {
let p = RunProgress {
steps_used: 100,
max_steps: 80,
};
assert_eq!(p.steps_remaining(), 0);
}
}