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