Skip to main content

synaptic_deep/middleware/
filesystem.rs

1use async_trait::async_trait;
2use serde_json::Value;
3use std::sync::Arc;
4use synaptic_core::SynapticError;
5use synaptic_middleware::{AgentMiddleware, ToolCallRequest, ToolCaller};
6
7use crate::backend::Backend;
8
9/// Middleware that evicts large tool results to files in the backend.
10///
11/// After a tool call, if the result exceeds `eviction_threshold` tokens (~4 chars/token),
12/// the full result is written to `.evicted/{tool_call_id}.txt` and replaced with a
13/// preview showing the first and last 5 lines.
14pub struct FilesystemMiddleware {
15    backend: Arc<dyn Backend>,
16    eviction_threshold_chars: usize,
17}
18
19impl FilesystemMiddleware {
20    pub fn new(backend: Arc<dyn Backend>, eviction_threshold_tokens: usize) -> Self {
21        Self {
22            backend,
23            eviction_threshold_chars: eviction_threshold_tokens * 4,
24        }
25    }
26}
27
28#[async_trait]
29impl AgentMiddleware for FilesystemMiddleware {
30    async fn wrap_tool_call(
31        &self,
32        request: ToolCallRequest,
33        next: &dyn ToolCaller,
34    ) -> Result<Value, SynapticError> {
35        let tool_call_id = request.call.id.clone();
36        let result = next.call(request).await?;
37
38        let result_str = match &result {
39            Value::String(s) => s.clone(),
40            other => serde_json::to_string(other).unwrap_or_default(),
41        };
42
43        if result_str.len() > self.eviction_threshold_chars {
44            let eviction_path = format!(".evicted/{}.txt", tool_call_id);
45            let _ = self.backend.write_file(&eviction_path, &result_str).await;
46
47            let lines: Vec<&str> = result_str.lines().collect();
48            let preview = if lines.len() <= 10 {
49                result_str
50            } else {
51                let first = &lines[..5];
52                let last = &lines[lines.len() - 5..];
53                format!(
54                    "{}\n\n... ({} lines omitted, full result in {}) ...\n\n{}",
55                    first.join("\n"),
56                    lines.len() - 10,
57                    eviction_path,
58                    last.join("\n")
59                )
60            };
61            Ok(Value::String(preview))
62        } else {
63            Ok(result)
64        }
65    }
66}