1use tracing::warn;
2
3use claw10_domain::{
4 Agent, Evidence, Lineage, Memory, Mission, PolicyBundle, Skill, Task, Worker,
5};
6use claw10_toon::{ToonContext, ToonEncoder};
7
8#[derive(Debug, thiserror::Error)]
9pub enum ContextError {
10 #[error("Token budget exhausted: needed {needed}, available {available}")]
11 TokenBudgetExhausted { needed: usize, available: usize },
12 #[error("No suitable context sources available")]
13 NoSources,
14}
15
16#[derive(Debug, Default)]
18pub struct ContextSources<'a> {
19 pub task: Option<&'a Task>,
20 pub mission: Option<&'a Mission>,
21 pub memories: &'a [Memory],
22 pub policies: &'a [PolicyBundle],
23 pub skills: &'a [Skill],
24 pub history: &'a [String],
25 pub tools: &'a [String],
26 pub agents: &'a [Agent],
27 pub lineage: Option<&'a Lineage>,
28 pub workers: &'a [Worker],
29 pub evidence: &'a [Evidence],
30}
31
32#[derive(Debug, Clone)]
33pub struct PipelineConfig {
34 pub max_token_budget: usize,
35 pub include_task: bool,
36 pub include_mission: bool,
37 pub include_memories: bool,
38 pub include_policy: bool,
39 pub include_skills: bool,
40 pub include_history: bool,
41 pub include_tools: bool,
42 pub include_agent: bool,
43 pub include_lineage: bool,
44 pub include_workers: bool,
45 pub include_evidence: bool,
46}
47
48impl Default for PipelineConfig {
49 fn default() -> Self {
50 Self {
51 max_token_budget: 4096,
52 include_task: true,
53 include_mission: true,
54 include_memories: true,
55 include_policy: true,
56 include_skills: true,
57 include_history: true,
58 include_tools: true,
59 include_agent: true,
60 include_lineage: true,
61 include_workers: false,
62 include_evidence: true,
63 }
64 }
65}
66
67pub struct ContextPipeline {
68 config: PipelineConfig,
69}
70
71impl ContextPipeline {
72 #[must_use]
73 pub fn new(config: PipelineConfig) -> Self {
74 Self { config }
75 }
76
77 pub async fn build_context(&self, sources: ContextSources<'_>) -> Result<String, ContextError> {
80 let mut ctx = ToonContext::new();
81
82 if self.config.include_task {
83 if let Some(task) = sources.task {
84 ctx.add_section("task", ToonEncoder::encode_task(task));
85 }
86 }
87
88 if self.config.include_mission {
89 if let Some(mission) = sources.mission {
90 ctx.add_section("mission", ToonEncoder::encode_mission(mission));
91 }
92 }
93
94 if self.config.include_memories && !sources.memories.is_empty() {
95 ctx.add_section("memory", ToonEncoder::encode_memories(sources.memories));
96 }
97
98 if self.config.include_policy && !sources.policies.is_empty() {
99 ctx.add_section(
100 "policy",
101 ToonEncoder::encode_policy_summary(sources.policies),
102 );
103 }
104
105 if self.config.include_skills && !sources.skills.is_empty() {
106 ctx.add_section("skills", ToonEncoder::encode_skills(sources.skills));
107 }
108
109 if self.config.include_history && !sources.history.is_empty() {
110 ctx.add_section("history", ToonEncoder::encode_history(sources.history));
111 }
112
113 if self.config.include_tools && !sources.tools.is_empty() {
114 ctx.add_section("tools", ToonEncoder::encode_tools(sources.tools));
115 }
116
117 if self.config.include_agent && !sources.agents.is_empty() {
118 ctx.add_section("agents", ToonEncoder::encode_agent_roster(sources.agents));
119 }
120
121 if self.config.include_lineage {
122 if let Some(lineage) = sources.lineage {
123 ctx.add_section("lineage", ToonEncoder::encode_lineage(lineage));
124 }
125 }
126
127 if self.config.include_workers && !sources.workers.is_empty() {
128 ctx.add_section("workers", ToonEncoder::encode_workers(sources.workers));
129 }
130
131 if self.config.include_evidence && !sources.evidence.is_empty() {
132 ctx.add_section("evidence", ToonEncoder::encode_evidence(sources.evidence));
133 }
134
135 let context = ctx.build();
136 let estimated_tokens = estimate_tokens(&context);
137
138 if estimated_tokens > self.config.max_token_budget {
139 warn!(
140 estimated_tokens,
141 max_budget = self.config.max_token_budget,
142 "Context exceeds token budget, trimming"
143 );
144 Ok(self.trim_to_budget(&context, self.config.max_token_budget))
145 } else {
146 Ok(context)
147 }
148 }
149
150 fn trim_to_budget(&self, context: &str, max_tokens: usize) -> String {
152 let sections: Vec<&str> = context.split("\n---\n").collect();
153 let mut selected = Vec::new();
154 let mut running_tokens = 0usize;
155
156 for section in sections.iter().rev() {
157 let section_tokens = estimate_tokens(section);
158 if running_tokens + section_tokens <= max_tokens {
159 selected.push(*section);
160 running_tokens += section_tokens;
161 } else {
162 let available = max_tokens.saturating_sub(running_tokens);
163 if available > 10 {
164 let chars_per_token = 4;
165 let max_chars = available * chars_per_token;
166 let truncated: String = section.chars().take(max_chars).collect();
167 selected.push(&*Box::leak(truncated.into_boxed_str()));
168 }
169 break;
170 }
171 }
172
173 selected.reverse();
174 selected.join("\n---\n")
175 }
176}
177
178fn estimate_tokens(text: &str) -> usize {
180 (text.len() + 3) / 4
181}
182
183#[cfg(test)]
184#[path = "lib_test.rs"]
185mod tests;
186