| 1 | //! Shared route-sized inline output with recoverable session spillover. |
| 2 | |
| 3 | use std::path::PathBuf; |
| 4 | |
| 5 | use crate::tools::spec::{ToolContext, ToolError}; |
| 6 | |
| 7 | #[derive(Debug)] |
| 8 | pub(crate) struct OverflowArtifact { |
| 9 | pub(crate) session_id: String, |
| 10 | pub(crate) absolute_path: PathBuf, |
| 11 | pub(crate) relative_path: PathBuf, |
| 12 | pub(crate) byte_size: u64, |
| 13 | pub(crate) preview: String, |
| 14 | } |
| 15 | |
| 16 | #[derive(Debug)] |
| 17 | pub(crate) struct BoundedText { |
| 18 | pub(crate) content: String, |
| 19 | pub(crate) artifact: Option<OverflowArtifact>, |
| 20 | } |
| 21 | |
| 22 | /// The route's inline budget for one tool result; the same number the engine |
| 23 | /// measures every tool result against (#6508). |
| 24 | pub(crate) fn inline_char_budget(context: &ToolContext) -> usize { |
| 25 | crate::route_budget::route_inline_char_budget(context.route_context_window) |
| 26 | } |
| 27 | |
| 28 | pub(crate) fn bound_text<F>( |
| 29 | content: String, |
| 30 | context: &ToolContext, |
| 31 | artifact_id: F, |
| 32 | subject: &str, |
| 33 | ) -> Result<BoundedText, ToolError> |
| 34 | where |
| 35 | F: FnOnce(&str) -> String, |
| 36 | { |
| 37 | let budget = inline_char_budget(context); |
| 38 | if content.chars().count() <= budget { |
| 39 | return Ok(BoundedText { |
| 40 | content, |
| 41 | artifact: None, |
| 42 | }); |
| 43 | } |
| 44 | |
| 45 | let artifact_id = artifact_id(&content); |
| 46 | let (absolute_path, relative_path) = |
| 47 | crate::artifacts::write_session_artifact(&context.state_namespace, &artifact_id, &content) |
| 48 | .map_err(|error| { |
| 49 | ToolError::execution_failed(format!( |
| 50 | "failed to preserve {subject} content artifact: {error}" |
| 51 | )) |
| 52 | })?; |
| 53 | let relative = crate::artifacts::format_artifact_relative_path(&relative_path); |
| 54 | let absolute = absolute_path.display().to_string(); |
| 55 | let inline = crate::tools::truncate::fit_to_inline_budget( |
| 56 | &content, |
| 57 | budget, |
| 58 | Some(&absolute), |
| 59 | Some(&relative), |
| 60 | ); |
| 61 | let preview = content.chars().take(200).collect(); |
| 62 | |
| 63 | Ok(BoundedText { |
| 64 | content: inline, |
| 65 | artifact: Some(OverflowArtifact { |
| 66 | session_id: context.state_namespace.clone(), |
| 67 | absolute_path, |
| 68 | relative_path, |
| 69 | byte_size: content.len() as u64, |
| 70 | preview, |
| 71 | }), |
| 72 | }) |
| 73 | } |
| 74 |