1use std::sync::Arc;
15
16use leviath_core::{EntryKind, Region, RegionEntry};
17use leviath_scripting::region_hook::{RegionScript, run_on_overflow, run_on_write, run_render};
18
19#[derive(Debug, Clone, Default)]
23pub struct AssembleMeta {
24 pub stage_name: String,
26 pub stage_iterations: usize,
28 pub model: String,
30}
31
32pub(crate) enum OnWriteOutcome {
34 Accept(String, usize),
36 Drop,
38}
39
40fn entry_to_json(entry: &RegionEntry) -> serde_json::Value {
44 let mut obj = serde_json::json!({
45 "content": entry.content,
46 "tokens": entry.tokens,
47 "timestamp": entry.timestamp,
48 "key": entry.key,
49 });
50 let (kind, extra) = match &entry.kind {
51 EntryKind::Text => ("text", None),
52 EntryKind::UserMessage => ("user_message", None),
53 EntryKind::AssistantTurn { tool_calls } => (
54 "assistant_turn",
55 Some((
56 "tool_calls",
57 serde_json::to_value(tool_calls).unwrap_or_default(),
58 )),
59 ),
60 EntryKind::ToolResult {
61 tool_call_id,
62 tool_name,
63 is_error,
64 } => {
65 obj["tool_call_id"] = serde_json::json!(tool_call_id);
66 obj["tool_name"] = serde_json::json!(tool_name);
67 obj["is_error"] = serde_json::json!(is_error);
68 ("tool_result", None)
69 }
70 };
71 obj["kind"] = serde_json::json!(kind);
72 if let Some((k, v)) = extra {
73 obj[k] = v;
74 }
75 obj
76}
77
78fn region_to_json(region: &Region) -> serde_json::Value {
80 serde_json::json!({
81 "name": region.name,
82 "budget": region.max_tokens,
83 "current_tokens": region.current_tokens,
84 "entry_count": region.content.len(),
85 })
86}
87
88fn fallback_block(region: &Region) -> leviath_providers::SystemBlock {
92 let text = region
93 .content
94 .iter()
95 .map(|e| e.content.as_str())
96 .collect::<Vec<_>>()
97 .join("\n\n");
98 leviath_providers::SystemBlock {
99 text: format!("[{}]:\n{}", region.name, text),
100 cache_hint: leviath_core::CacheHint::Never,
101 }
102}
103
104#[allow(clippy::too_many_arguments)]
110pub(crate) fn render_custom_region(
111 region: &Region,
112 script: Option<&Arc<RegionScript>>,
113 persistent: bool,
114 meta: &AssembleMeta,
115 window_current: usize,
116 window_max: usize,
117 system_blocks: &mut Vec<leviath_providers::SystemBlock>,
118 messages: &mut Vec<leviath_providers::Message>,
119) {
120 let Some(script) = script else {
121 if !region.content.is_empty() {
124 tracing::warn!(
125 region = %region.name,
126 "custom region has no compiled script; rendering fallback block"
127 );
128 system_blocks.push(fallback_block(region));
129 }
130 return;
131 };
132
133 let ctx = serde_json::json!({
134 "region": region_to_json(region),
135 "entries": region.content.iter().map(entry_to_json).collect::<Vec<_>>(),
136 "stage_name": meta.stage_name,
137 "stage_iterations": meta.stage_iterations,
138 "model": meta.model,
139 "window": { "total_tokens": window_current, "max_tokens": window_max },
140 });
141
142 let rendered = match run_render(script, ctx) {
143 Ok(value) => value,
144 Err(e) => {
145 tracing::warn!(
146 region = %region.name,
147 script = %script.path,
148 error = %e,
149 "custom region render failed; using fallback block"
150 );
151 if !region.content.is_empty() {
152 system_blocks.push(fallback_block(region));
153 }
154 return;
155 }
156 };
157
158 match parse_render_output(&rendered, persistent) {
159 Ok((blocks, msgs)) => {
160 let emitted_tokens: usize = blocks
161 .iter()
162 .map(|b| leviath_core::estimate_tokens(&b.text))
163 .chain(msgs.iter().map(|m| {
164 match &m.content {
165 leviath_providers::MessageContent::Text(t) => {
166 leviath_core::estimate_tokens(t)
167 }
168 leviath_providers::MessageContent::Blocks(bs) => bs
169 .iter()
170 .map(|b| match b {
171 leviath_providers::ContentBlock::Text { text } => {
172 leviath_core::estimate_tokens(text)
173 }
174 leviath_providers::ContentBlock::ToolUse { input, .. } => {
175 leviath_core::estimate_tokens(&input.to_string())
176 }
177 leviath_providers::ContentBlock::ToolResult { content, .. } => {
178 leviath_core::estimate_tokens(content)
179 }
180 })
181 .sum(),
182 }
183 }))
184 .sum();
185 if emitted_tokens > region.max_tokens {
186 tracing::warn!(
187 region = %region.name,
188 script = %script.path,
189 emitted_tokens,
190 budget = region.max_tokens,
191 "custom region render exceeds its budget; sending anyway \
192 (enable exact_token_counting for a hard guard)"
193 );
194 }
195 system_blocks.extend(blocks);
196 messages.extend(msgs);
197 }
198 Err(reason) => {
199 tracing::warn!(
200 region = %region.name,
201 script = %script.path,
202 reason = %reason,
203 "custom region render returned an invalid shape; using fallback block"
204 );
205 if !region.content.is_empty() {
206 system_blocks.push(fallback_block(region));
207 }
208 }
209 }
210}
211
212fn parse_render_output(
217 value: &serde_json::Value,
218 persistent: bool,
219) -> Result<
220 (
221 Vec<leviath_providers::SystemBlock>,
222 Vec<leviath_providers::Message>,
223 ),
224 String,
225> {
226 let hint = if persistent {
228 leviath_core::CacheHint::Always
229 } else {
230 leviath_core::CacheHint::UntilChanged
231 };
232 let block = |text: &str| leviath_providers::SystemBlock {
233 text: text.to_string(),
234 cache_hint: hint,
235 };
236
237 match value {
238 serde_json::Value::String(s) => {
239 let blocks = if s.is_empty() { vec![] } else { vec![block(s)] };
240 Ok((blocks, vec![]))
241 }
242 serde_json::Value::Object(obj) => {
243 let mut blocks = Vec::new();
244 match obj.get("system") {
245 None | Some(serde_json::Value::Null) => {}
246 Some(serde_json::Value::String(s)) => {
247 if !s.is_empty() {
248 blocks.push(block(s));
249 }
250 }
251 Some(serde_json::Value::Array(items)) => {
252 for item in items {
253 match item {
254 serde_json::Value::String(s) if !s.is_empty() => blocks.push(block(s)),
255 serde_json::Value::String(_) => {}
256 other => {
257 return Err(format!(
258 "system array items must be strings, found {other}"
259 ));
260 }
261 }
262 }
263 }
264 Some(other) => {
265 return Err(format!(
266 "system must be a string or array of strings, found {other}"
267 ));
268 }
269 }
270 let mut messages = Vec::new();
271 match obj.get("messages") {
272 None | Some(serde_json::Value::Null) => {}
273 Some(serde_json::Value::Array(items)) => {
274 for item in items {
275 messages.push(message_from_json(item)?);
276 }
277 }
278 Some(other) => return Err(format!("messages must be an array, found {other}")),
279 }
280 Ok((blocks, messages))
281 }
282 other => Err(format!(
283 "render must return a string or #{{ system, messages }} map, found {other}"
284 )),
285 }
286}
287
288fn message_from_json(value: &serde_json::Value) -> Result<leviath_providers::Message, String> {
296 let obj = value
297 .as_object()
298 .ok_or_else(|| format!("each message must be a map, found {value}"))?;
299 let role = obj
300 .get("role")
301 .and_then(|r| r.as_str())
302 .ok_or("each message needs a role of \"user\" or \"assistant\"")?;
303 if role != "user" && role != "assistant" {
304 return Err(format!(
305 "message role must be user or assistant, found {role}"
306 ));
307 }
308
309 let content_str = match obj.get("content") {
310 None | Some(serde_json::Value::Null) => None,
311 Some(serde_json::Value::String(s)) => Some(s.clone()),
312 Some(other) => return Err(format!("message content must be a string, found {other}")),
313 };
314
315 if let Some(calls) = obj.get("tool_calls") {
316 if role != "assistant" {
317 return Err("tool_calls are only valid on an assistant message".to_string());
318 }
319 let calls = calls
320 .as_array()
321 .ok_or_else(|| format!("tool_calls must be an array, found {calls}"))?;
322 let mut blocks = Vec::new();
323 if let Some(text) = content_str.filter(|s| !s.is_empty()) {
324 blocks.push(leviath_providers::ContentBlock::Text { text });
325 }
326 for call in calls {
327 let call = call
328 .as_object()
329 .ok_or_else(|| format!("each tool_call must be a map, found {call}"))?;
330 let id = call
331 .get("id")
332 .and_then(|v| v.as_str())
333 .ok_or("each tool_call needs a string id")?;
334 let name = call
335 .get("name")
336 .and_then(|v| v.as_str())
337 .ok_or("each tool_call needs a string name")?;
338 blocks.push(leviath_providers::ContentBlock::ToolUse {
339 id: id.to_string(),
340 name: name.to_string(),
341 input: call
342 .get("arguments")
343 .cloned()
344 .unwrap_or(serde_json::Value::Object(Default::default())),
345 thought_signature: call
346 .get("thought_signature")
347 .and_then(|v| v.as_str())
348 .map(String::from),
349 });
350 }
351 return Ok(leviath_providers::Message {
352 role: "assistant".to_string(),
353 content: leviath_providers::MessageContent::Blocks(blocks),
354 cache_breakpoint: false,
355 });
356 }
357
358 if let Some(results) = obj.get("tool_results") {
359 if role != "user" {
360 return Err("tool_results are only valid on a user message".to_string());
361 }
362 let results = results
363 .as_array()
364 .ok_or_else(|| format!("tool_results must be an array, found {results}"))?;
365 let mut blocks = Vec::new();
366 for result in results {
367 let result = result
368 .as_object()
369 .ok_or_else(|| format!("each tool_result must be a map, found {result}"))?;
370 let id = result
371 .get("tool_call_id")
372 .and_then(|v| v.as_str())
373 .ok_or("each tool_result needs a string tool_call_id")?;
374 let content = result
375 .get("content")
376 .and_then(|v| v.as_str())
377 .ok_or("each tool_result needs string content")?;
378 blocks.push(leviath_providers::ContentBlock::ToolResult {
379 tool_use_id: id.to_string(),
380 content: content.to_string(),
381 is_error: result
382 .get("is_error")
383 .and_then(|v| v.as_bool())
384 .unwrap_or(false),
385 });
386 }
387 return Ok(leviath_providers::Message {
388 role: "user".to_string(),
389 content: leviath_providers::MessageContent::Blocks(blocks),
390 cache_breakpoint: false,
391 });
392 }
393
394 let content =
395 content_str.ok_or("a message without tool_calls/tool_results needs string content")?;
396 Ok(leviath_providers::Message {
397 role: role.to_string(),
398 content: content.into(),
399 cache_breakpoint: false,
400 })
401}
402
403pub(crate) fn apply_on_write(
406 script: &RegionScript,
407 region: &Region,
408 content: String,
409 tokens: usize,
410 kind: &EntryKind,
411) -> OnWriteOutcome {
412 let kind_str = match kind {
413 EntryKind::Text => "text",
414 EntryKind::UserMessage => "user_message",
415 EntryKind::AssistantTurn { .. } => "assistant_turn",
416 EntryKind::ToolResult { .. } => "tool_result",
417 };
418 let ctx = serde_json::json!({
419 "region": region_to_json(region),
420 "entry": { "content": content, "kind": kind_str, "tokens": tokens },
421 });
422 match run_on_write(script, ctx) {
423 Ok(serde_json::Value::String(replacement)) => {
424 let tokens = leviath_core::estimate_tokens(&replacement);
425 OnWriteOutcome::Accept(replacement, tokens)
426 }
427 Ok(serde_json::Value::Bool(false)) => OnWriteOutcome::Drop,
428 Ok(serde_json::Value::Bool(true)) | Ok(serde_json::Value::Null) => {
429 OnWriteOutcome::Accept(content, tokens)
430 }
431 Ok(other) => {
432 tracing::warn!(
433 region = %region.name,
434 script = %script.path,
435 returned = %other,
436 "on_write must return a string, true/false, or unit; accepting entry unchanged"
437 );
438 OnWriteOutcome::Accept(content, tokens)
439 }
440 Err(e) => {
441 tracing::warn!(
442 region = %region.name,
443 script = %script.path,
444 error = %e,
445 "on_write failed; accepting entry unchanged"
446 );
447 OnWriteOutcome::Accept(content, tokens)
448 }
449 }
450}
451
452pub(crate) fn apply_overflow(
456 script: &RegionScript,
457 region: &mut Region,
458 needed_tokens: usize,
459) -> usize {
460 let ctx = serde_json::json!({
461 "region": region_to_json(region),
462 "entries": region.content.iter().map(entry_to_json).collect::<Vec<_>>(),
463 "needed_tokens": needed_tokens,
464 });
465 let value = match run_on_overflow(script, ctx) {
466 Ok(v) => v,
467 Err(e) => {
468 tracing::warn!(
469 region = %region.name,
470 script = %script.path,
471 error = %e,
472 "on_overflow failed; falling back to oldest-first eviction"
473 );
474 return 0;
475 }
476 };
477 let Some(indices) = valid_drop_indices(&value, region.content.len()) else {
478 tracing::warn!(
479 region = %region.name,
480 script = %script.path,
481 returned = %value,
482 "on_overflow must return an array of in-range entry indices; \
483 falling back to oldest-first eviction"
484 );
485 return 0;
486 };
487
488 let mut freed = 0;
489 for index in indices.into_iter().rev() {
491 let entry = region.content.remove(index);
492 freed += entry.tokens;
493 }
494 region.current_tokens = region.current_tokens.saturating_sub(freed);
495 freed
496}
497
498fn valid_drop_indices(value: &serde_json::Value, len: usize) -> Option<Vec<usize>> {
501 let items = value.as_array()?;
502 let mut indices = Vec::with_capacity(items.len());
503 for item in items {
504 let index = item.as_u64()? as usize;
505 if index >= len {
506 return None;
507 }
508 indices.push(index);
509 }
510 indices.sort_unstable();
511 indices.dedup();
512 Some(indices)
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use crate::test_support::with_tracing;
519 use leviath_core::RegionKind;
520 use leviath_scripting::region_hook::compile;
521 use serde_json::json;
522
523 fn script(src: &str) -> Arc<RegionScript> {
524 Arc::new(compile("test.rhai", src).unwrap())
525 }
526
527 fn region_with(entries: &[(&str, EntryKind)]) -> Region {
528 let mut region = Region::new(
529 "brain".to_string(),
530 RegionKind::Custom {
531 script: "test.rhai".to_string(),
532 persistent: false,
533 },
534 1000,
535 );
536 for (content, kind) in entries {
537 region
538 .add_typed_entry(content.to_string(), 10, kind.clone())
539 .unwrap();
540 }
541 region
542 }
543
544 fn render(
545 region: &Region,
546 script: Option<&Arc<RegionScript>>,
547 persistent: bool,
548 ) -> (
549 Vec<leviath_providers::SystemBlock>,
550 Vec<leviath_providers::Message>,
551 ) {
552 let mut blocks = Vec::new();
553 let mut messages = Vec::new();
554 with_tracing(|| {
555 render_custom_region(
556 region,
557 script,
558 persistent,
559 &AssembleMeta {
560 stage_name: "plan".to_string(),
561 stage_iterations: 2,
562 model: "m1".to_string(),
563 },
564 50,
565 2000,
566 &mut blocks,
567 &mut messages,
568 )
569 });
570 (blocks, messages)
571 }
572
573 #[test]
576 fn entry_to_json_serializes_all_kinds() {
577 let mut region = region_with(&[
578 ("plain", EntryKind::Text),
579 ("hi", EntryKind::UserMessage),
580 (
581 "calling",
582 EntryKind::AssistantTurn {
583 tool_calls: vec![leviath_core::SerializedToolCall {
584 id: "c1".to_string(),
585 name: "shell".to_string(),
586 arguments: json!({"command": "ls"}),
587 thought_signature: None,
588 }],
589 },
590 ),
591 (
592 "result",
593 EntryKind::ToolResult {
594 tool_call_id: "c1".to_string(),
595 tool_name: "shell".to_string(),
596 is_error: true,
597 },
598 ),
599 ]);
600 region.content[0].key = Some("k".to_string());
601
602 let entries: Vec<_> = region.content.iter().map(entry_to_json).collect();
603 assert_eq!(entries[0]["kind"], json!("text"));
604 assert_eq!(entries[0]["key"], json!("k"));
605 assert_eq!(entries[0]["tokens"], json!(10));
606 assert_eq!(entries[1]["kind"], json!("user_message"));
607 assert_eq!(entries[2]["kind"], json!("assistant_turn"));
608 assert_eq!(entries[2]["tool_calls"][0]["id"], json!("c1"));
609 assert_eq!(entries[3]["kind"], json!("tool_result"));
610 assert_eq!(entries[3]["tool_call_id"], json!("c1"));
611 assert_eq!(entries[3]["is_error"], json!(true));
612 }
613
614 #[test]
617 fn render_string_becomes_one_block_with_persistence_hint() {
618 let region = region_with(&[("x", EntryKind::Text)]);
619 let s = script("fn render(ctx) { `<${ctx.region.name}>` }");
620
621 let (blocks, messages) = render(®ion, Some(&s), false);
622 assert_eq!(blocks.len(), 1);
623 assert_eq!(blocks[0].text, "<brain>");
624 assert_eq!(blocks[0].cache_hint, leviath_core::CacheHint::UntilChanged);
625 assert!(messages.is_empty());
626
627 let (blocks, _) = render(®ion, Some(&s), true);
628 assert_eq!(blocks[0].cache_hint, leviath_core::CacheHint::Always);
629 }
630
631 #[test]
632 fn render_map_emits_system_array_and_typed_messages() {
633 let src = r#"
636 fn render(ctx) {
637 #{
638 system: ["s1", "", "s2"],
639 messages: [
640 #{ role: "user", content: "hello" },
641 #{ role: "assistant", content: "thinking", tool_calls: [
642 #{ id: "c1", name: "shell", arguments: #{ command: "ls" } },
643 ] },
644 #{ role: "user", tool_results: [
645 #{ tool_call_id: "c1", content: "file_a", is_error: false },
646 ] },
647 ],
648 }
649 }
650 "#;
651 let region = region_with(&[("x", EntryKind::Text)]);
652 let (blocks, messages) = render(®ion, Some(&script(src)), false);
653
654 assert_eq!(
655 blocks.iter().map(|b| b.text.as_str()).collect::<Vec<_>>(),
656 vec!["s1", "s2"],
657 "empty system strings are skipped"
658 );
659 assert_eq!(messages.len(), 3);
660 assert_eq!(messages[0].role, "user");
661 let assistant = serde_json::to_value(&messages[1].content).unwrap();
664 assert_eq!(assistant[0], json!({ "type": "text", "text": "thinking" }));
665 assert_eq!(assistant[1]["type"], json!("tool_use"));
666 assert_eq!(assistant[1]["id"], json!("c1"));
667 assert_eq!(assistant[1]["name"], json!("shell"));
668 let results = serde_json::to_value(&messages[2].content).unwrap();
669 assert_eq!(results[0]["type"], json!("tool_result"));
670 assert_eq!(results[0]["tool_use_id"], json!("c1"));
671 assert_eq!(results[0]["content"], json!("file_a"));
672 assert_eq!(results[0]["is_error"], json!(false));
673 }
674
675 #[test]
676 fn render_map_accepts_single_system_string_and_null_fields() {
677 let src = r#"fn render(ctx) { #{ system: "solo", messages: () } }"#;
678 let region = region_with(&[("x", EntryKind::Text)]);
679 let (blocks, messages) = render(®ion, Some(&script(src)), false);
680 assert_eq!(blocks.len(), 1);
681 assert_eq!(blocks[0].text, "solo");
682 assert!(messages.is_empty());
683 }
684
685 #[test]
686 fn render_empty_map_and_empty_string_emit_nothing() {
687 let region = region_with(&[("x", EntryKind::Text)]);
688 for src in ["fn render(ctx) { #{} }", "fn render(ctx) { \"\" }"] {
689 let (blocks, messages) = render(®ion, Some(&script(src)), false);
690 assert!(blocks.is_empty(), "src: {src}");
691 assert!(messages.is_empty());
692 }
693 }
694
695 #[test]
696 fn render_sees_stage_meta_and_window_fields() {
697 let src = r#"
698 fn render(ctx) {
699 `${ctx.stage_name}|${ctx.stage_iterations}|${ctx.model}|${ctx.window.total_tokens}|${ctx.window.max_tokens}`
700 }
701 "#;
702 let region = region_with(&[("x", EntryKind::Text)]);
703 let (blocks, _) = render(®ion, Some(&script(src)), false);
704 assert_eq!(blocks[0].text, "plan|2|m1|50|2000");
705 }
706
707 #[test]
708 fn render_over_budget_warns_but_still_emits() {
709 let src = r#"fn render(ctx) { let s = "x"; s.pad(8000, 'x'); s }"#;
712 let region = region_with(&[("x", EntryKind::Text)]);
713 let (blocks, _) = render(®ion, Some(&script(src)), false);
714 assert_eq!(blocks.len(), 1);
715 assert_eq!(blocks[0].text.len(), 8000);
716 }
717
718 #[test]
721 fn render_missing_script_falls_back_to_temporary_style() {
722 let region = region_with(&[("a", EntryKind::Text), ("b", EntryKind::Text)]);
723 let (blocks, messages) = render(®ion, None, false);
724 assert_eq!(blocks.len(), 1);
725 assert_eq!(blocks[0].text, "[brain]:\na\n\nb");
726 assert_eq!(blocks[0].cache_hint, leviath_core::CacheHint::Never);
727 assert!(messages.is_empty());
728 }
729
730 #[test]
731 fn render_missing_script_on_empty_region_emits_nothing() {
732 let region = region_with(&[]);
733 let (blocks, messages) = render(®ion, None, false);
734 assert!(blocks.is_empty());
735 assert!(messages.is_empty());
736 }
737
738 #[test]
739 fn render_runtime_error_falls_back() {
740 let region = region_with(&[("kept", EntryKind::Text)]);
741 let s = script("fn render(ctx) { throw \"broken\" }");
742 let (blocks, _) = render(®ion, Some(&s), false);
743 assert_eq!(blocks.len(), 1);
744 assert_eq!(blocks[0].text, "[brain]:\nkept");
745 assert_eq!(blocks[0].cache_hint, leviath_core::CacheHint::Never);
746 }
747
748 #[test]
749 fn render_error_on_empty_region_emits_nothing() {
750 let region = region_with(&[]);
751 let s = script("fn render(ctx) { throw \"broken\" }");
752 let (blocks, _) = render(®ion, Some(&s), false);
753 assert!(blocks.is_empty());
754 }
755
756 #[test]
757 fn render_invalid_shapes_fall_back() {
758 let region = region_with(&[("kept", EntryKind::Text)]);
759 for src in [
760 "fn render(ctx) { 42 }",
761 "fn render(ctx) { true }",
762 "fn render(ctx) { [1, 2] }",
763 "fn render(ctx) { }",
764 "fn render(ctx) { #{ system: 42 } }",
765 "fn render(ctx) { #{ system: [1] } }",
766 "fn render(ctx) { #{ messages: \"not an array\" } }",
767 "fn render(ctx) { #{ messages: [42] } }",
768 "fn render(ctx) { #{ messages: [#{ content: \"no role\" }] } }",
769 "fn render(ctx) { #{ messages: [#{ role: \"system\", content: \"bad role\" }] } }",
770 "fn render(ctx) { #{ messages: [#{ role: \"user\", content: 42 }] } }",
771 "fn render(ctx) { #{ messages: [#{ role: \"user\" }] } }",
772 "fn render(ctx) { #{ messages: [#{ role: \"user\", tool_calls: [] }] } }",
773 "fn render(ctx) { #{ messages: [#{ role: \"assistant\", tool_calls: 42 }] } }",
774 "fn render(ctx) { #{ messages: [#{ role: \"assistant\", tool_calls: [42] }] } }",
775 "fn render(ctx) { #{ messages: [#{ role: \"assistant\", tool_calls: [#{ name: \"n\" }] }] } }",
776 "fn render(ctx) { #{ messages: [#{ role: \"assistant\", tool_calls: [#{ id: \"i\" }] }] } }",
777 "fn render(ctx) { #{ messages: [#{ role: \"assistant\", tool_results: [] }] } }",
778 "fn render(ctx) { #{ messages: [#{ role: \"user\", tool_results: 42 }] } }",
779 "fn render(ctx) { #{ messages: [#{ role: \"user\", tool_results: [42] }] } }",
780 "fn render(ctx) { #{ messages: [#{ role: \"user\", tool_results: [#{ content: \"c\" }] }] } }",
781 "fn render(ctx) { #{ messages: [#{ role: \"user\", tool_results: [#{ tool_call_id: \"i\" }] }] } }",
782 ] {
783 let (blocks, messages) = render(®ion, Some(&script(src)), false);
784 assert_eq!(blocks.len(), 1, "src must fall back: {src}");
785 assert_eq!(blocks[0].text, "[brain]:\nkept", "src: {src}");
786 assert!(messages.is_empty(), "src: {src}");
787 }
788 }
789
790 #[test]
791 fn render_invalid_shape_on_empty_region_emits_nothing() {
792 let region = region_with(&[]);
795 let (blocks, messages) = render(®ion, Some(&script("fn render(ctx) { 42 }")), false);
796 assert!(blocks.is_empty());
797 assert!(messages.is_empty());
798 }
799
800 #[test]
801 fn render_empty_single_system_string_is_skipped() {
802 let src = r#"fn render(ctx) { #{ system: "" } }"#;
803 let region = region_with(&[("x", EntryKind::Text)]);
804 let (blocks, messages) = render(®ion, Some(&script(src)), false);
805 assert!(blocks.is_empty());
806 assert!(messages.is_empty());
807 }
808
809 #[test]
810 fn render_tool_call_passes_thought_signature_through() {
811 let src = r#"
812 fn render(ctx) {
813 #{ messages: [#{ role: "assistant", tool_calls: [
814 #{ id: "c", name: "n", thought_signature: "sig123" },
815 ] }] }
816 }
817 "#;
818 let region = region_with(&[("x", EntryKind::Text)]);
819 let (_, messages) = render(®ion, Some(&script(src)), false);
820 let blocks = serde_json::to_value(&messages[0].content).unwrap();
821 assert_eq!(blocks[0]["thought_signature"], json!("sig123"));
822 }
823
824 #[test]
825 fn on_write_ctx_reports_every_entry_kind() {
826 let src = r#"
828 fn render(ctx) { "" }
829 fn on_write(ctx) { ctx.entry.kind }
830 "#;
831 for (kind, expected) in [
832 (EntryKind::Text, "text"),
833 (EntryKind::UserMessage, "user_message"),
834 (
835 EntryKind::AssistantTurn { tool_calls: vec![] },
836 "assistant_turn",
837 ),
838 (
839 EntryKind::ToolResult {
840 tool_call_id: "c".to_string(),
841 tool_name: "t".to_string(),
842 is_error: false,
843 },
844 "tool_result",
845 ),
846 ] {
847 let replaced = on_write_kind(src, "x", &kind);
848 assert_eq!(
849 replaced.map(|(content, _)| content),
850 Some(expected.to_string())
851 );
852 }
853 }
854
855 #[test]
856 fn render_assistant_tool_call_defaults_arguments_and_signature() {
857 let src = r#"
858 fn render(ctx) {
859 #{ messages: [#{ role: "assistant", tool_calls: [#{ id: "c", name: "n" }] }] }
860 }
861 "#;
862 let region = region_with(&[("x", EntryKind::Text)]);
863 let (_, messages) = render(®ion, Some(&script(src)), false);
864 let blocks = serde_json::to_value(&messages[0].content).unwrap();
865 assert_eq!(blocks[0]["type"], json!("tool_use"));
866 assert_eq!(blocks[0]["input"], json!({}));
867 assert_eq!(blocks[0]["thought_signature"], serde_json::Value::Null);
870 }
871
872 fn on_write_kind(src: &str, content: &str, kind: &EntryKind) -> Option<(String, usize)> {
878 let region = region_with(&[]);
879 let outcome =
880 with_tracing(|| apply_on_write(&script(src), ®ion, content.to_string(), 5, kind));
881 match outcome {
882 OnWriteOutcome::Accept(content, tokens) => Some((content, tokens)),
883 OnWriteOutcome::Drop => None,
884 }
885 }
886
887 fn on_write_of(src: &str, content: &str) -> Option<(String, usize)> {
888 on_write_kind(src, content, &EntryKind::Text)
889 }
890
891 #[test]
892 fn on_write_replaces_accepts_and_drops() {
893 let replaced = on_write_of(
894 "fn render(ctx) { \"\" }\nfn on_write(ctx) { ctx.entry.content.to_upper() }",
895 "hi",
896 );
897 assert_eq!(
898 replaced,
899 Some(("HI".to_string(), leviath_core::estimate_tokens("HI")))
900 );
901
902 for accept_body in ["true", ""] {
903 let src = format!("fn render(ctx) {{ \"\" }}\nfn on_write(ctx) {{ {accept_body} }}");
904 assert_eq!(
905 on_write_of(&src, "orig"),
906 Some(("orig".to_string(), 5)),
907 "body {accept_body:?} accepts unchanged with original tokens"
908 );
909 }
910
911 assert_eq!(
912 on_write_of("fn render(ctx) { \"\" }\nfn on_write(ctx) { false }", "x"),
913 None,
914 "false drops the entry"
915 );
916 }
917
918 #[test]
919 fn on_write_invalid_return_and_error_accept_unchanged() {
920 for src in [
921 "fn render(ctx) { \"\" }\nfn on_write(ctx) { 42 }",
922 "fn render(ctx) { \"\" }\nfn on_write(ctx) { throw \"bad\" }",
923 ] {
924 assert_eq!(
925 on_write_of(src, "keep"),
926 Some(("keep".to_string(), 5)),
927 "src: {src}"
928 );
929 }
930 }
931
932 #[test]
935 fn apply_overflow_drops_chosen_indices() {
936 let mut region = region_with(&[
937 ("a", EntryKind::Text),
938 ("b", EntryKind::Text),
939 ("c", EntryKind::Text),
940 ]);
941 let s = script("fn render(ctx) { \"\" }\nfn on_overflow(ctx) { [2, 0, 2] }");
943 let freed = with_tracing(|| apply_overflow(&s, &mut region, 15));
944 assert_eq!(freed, 20);
945 assert_eq!(region.content.len(), 1);
946 assert_eq!(region.content[0].content, "b");
947 assert_eq!(region.current_tokens, 10);
948 }
949
950 #[test]
951 fn apply_overflow_error_and_invalid_shapes_free_nothing() {
952 for src in [
953 "fn render(ctx) { \"\" }\nfn on_overflow(ctx) { throw \"bad\" }",
954 "fn render(ctx) { \"\" }\nfn on_overflow(ctx) { \"not an array\" }",
955 "fn render(ctx) { \"\" }\nfn on_overflow(ctx) { [\"x\"] }",
956 "fn render(ctx) { \"\" }\nfn on_overflow(ctx) { [99] }",
957 ] {
958 let mut region = region_with(&[("a", EntryKind::Text)]);
959 let freed = with_tracing(|| apply_overflow(&script(src), &mut region, 5));
960 assert_eq!(freed, 0, "src: {src}");
961 assert_eq!(region.content.len(), 1, "content untouched: {src}");
962 }
963 }
964
965 #[test]
966 fn overflow_ctx_carries_needed_tokens_and_entries() {
967 let src = r#"
968 fn render(ctx) { "" }
969 fn on_overflow(ctx) {
970 if ctx.needed_tokens == 7 && ctx.entries.len() == 2 { [0] } else { [] }
971 }
972 "#;
973 let mut region = region_with(&[("a", EntryKind::Text), ("b", EntryKind::Text)]);
974 let freed = with_tracing(|| apply_overflow(&script(src), &mut region, 7));
975 assert_eq!(freed, 10);
976 }
977}