blob: bb5042c7333bd5fc6cc736f4c82fed01a1cc84c7 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Durable, bounded replacements of immutable model evidence.
mod encoding;
mod reference;
use crate::{
read::{MAX_PAGE_CHARS, ReadInput, ReadPage},
tool_output::DurableToolProjection,
};
pub use encoding::{encode_projection, outcome_projection, projection_digest};
pub use reference::ToolResultAddress;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
const ARCHIVE_READ_INSTRUCTIONS: &str = "Read the page below; pass page.next to Read to continue. The address is a Session resource, not a workspace file.";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PruneReason {
ToolResultPruned,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ArchiveIdentity {
pub runtime_event_id: String,
pub tool_call_id: String,
pub tool_name: String,
pub source_projection_digest: String,
pub body_sha256: String,
pub original_bytes: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ArchivedPlaceholder {
pub identity: ArchiveIdentity,
pub original_estimated_tokens: u64,
pub reason: PruneReason,
pub page: ReadPage,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CanonicalHeader<'a> {
kind: &'static str,
resource_ref: String,
tool_name: &'a str,
original_bytes: u64,
reason: PruneReason,
read_instructions: &'static str,
}
#[derive(Serialize)]
struct CanonicalPlaceholder<'a> {
#[serde(flatten)]
header: CanonicalHeader<'a>,
page: &'a ReadPage,
}
pub(crate) fn identity_string(value: &str) -> bool {
!value.is_empty()
}
pub(crate) fn hash(value: &str) -> bool {
value.len() == 64
&& value
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
pub fn valid_projection_digest(value: &str) -> bool {
value.strip_prefix("sha256:").is_some_and(hash)
}
impl ArchivedPlaceholder {
/// One size rule for current and historical results. Media stays materializable.
pub fn prepare(
event_id: String,
tool_call_id: String,
tool_name: String,
projection: &DurableToolProjection,
) -> Result<Option<Self>, &'static str> {
if matches!(projection, DurableToolProjection::Content { parts }
if parts.iter().any(|part| part.media().is_some()))
|| matches!(projection, DurableToolProjection::Json { value }
if value.get("kind").and_then(serde_json::Value::as_str) == Some("maka.archived_tool_result"))
{
return Ok(None);
}
let bytes = encode_projection(projection)?;
let body = std::str::from_utf8(&bytes).map_err(|_| "invalid archive text")?;
let chars = body.encode_utf16().count();
if chars <= MAX_PAGE_CHARS {
return Ok(None);
}
let identity = ArchiveIdentity {
runtime_event_id: event_id,
tool_call_id,
tool_name,
source_projection_digest: projection_digest(projection)?,
body_sha256: hex::encode(Sha256::digest(&bytes)),
original_bytes: bytes.len() as u64,
};
identity.validate()?;
let header = canonical_header(&identity)?;
// Inserting ,"page": into the header adds eight characters.
let overhead = serde_json::to_string(&header)
.map_err(|_| "invalid archive header")?
.encode_utf16()
.count()
+ 8;
let request = ReadInput {
path: header.resource_ref.clone(),
offset: None,
limit: None,
}
.resolve()
.map_err(|_| "invalid archive address")?;
let page = request
.tool_result_page(
&identity.tool_name,
body,
MAX_PAGE_CHARS.saturating_sub(overhead).max(1),
)
.map_err(|_| "archive page cannot fit the model response")?;
let placeholder = Self {
identity,
original_estimated_tokens: (chars as u64).div_ceil(4),
reason: PruneReason::ToolResultPruned,
page,
};
placeholder.validate()?;
Ok(Some(placeholder))
}
pub fn validate(&self) -> Result<(), &'static str> {
self.identity.validate()?;
if self.original_estimated_tokens > 9_007_199_254_740_991 {
return Err("archive exceeds token limit");
}
Ok(())
}
/// Canonical evidence codec; Engine projects current guidance separately.
/// Preserve the frozen page and header for source verification.
pub fn to_canonical_projection(&self) -> Result<DurableToolProjection, &'static str> {
self.validate()?;
Ok(DurableToolProjection::Json {
value: self.canonical_value()?,
})
}
fn canonical_value(&self) -> Result<serde_json::Value, &'static str> {
serde_json::to_value(CanonicalPlaceholder {
header: canonical_header(&self.identity)?,
page: &self.page,
})
.map_err(|_| "invalid archive projection")
}
}
fn canonical_header(identity: &ArchiveIdentity) -> Result<CanonicalHeader<'_>, &'static str> {
Ok(CanonicalHeader {
kind: "maka.archived_tool_result",
resource_ref: identity.short_ref()?,
tool_name: &identity.tool_name,
original_bytes: identity.original_bytes,
reason: PruneReason::ToolResultPruned,
read_instructions: ARCHIVE_READ_INSTRUCTIONS,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn archive_preserves_complete_wide_deep_and_ordered_projection_encoding() {
let wide = DurableToolProjection::Json {
value: serde_json::Value::Array(vec![serde_json::json!(7); 20_257]),
};
let expected = format!(
"[{}]",
std::iter::repeat_n("7", 20_257)
.collect::<Vec<_>>()
.join(",")
);
assert_eq!(encode_projection(&wide).unwrap(), expected.as_bytes());
let mut deep = serde_json::json!("tail");
for _ in 0..40 {
deep = serde_json::Value::Array(vec![deep]);
}
let projection = DurableToolProjection::Json { value: deep };
assert_eq!(
encode_projection(&projection).unwrap(),
format!("{}\"tail\"{}", "[".repeat(40), "]".repeat(40)).as_bytes()
);
let content = DurableToolProjection::Content {
parts: (0..65)
.map(|index| crate::tool_output::ProjectionPart::Text {
text: format!("part-{index:02}"),
})
.collect(),
};
let expected: Vec<_> = (0..65)
.map(|index| serde_json::json!({"type":"text","text":format!("part-{index:02}")}))
.collect();
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&encode_projection(&content).unwrap())
.unwrap(),
serde_json::json!(expected)
);
assert_eq!(
projection_digest(&projection).unwrap(),
crate::artifact::content_digest(
format!(
"{{\"kind\":\"json\",\"value\":{}\"tail\"{}}}",
"[".repeat(40),
"]".repeat(40)
)
.as_bytes()
)
);
}
#[test]
fn archive_body_can_exceed_four_mib_while_model_page_stays_bounded() {
let text = "x".repeat(5 * 1024 * 1024);
let projection = DurableToolProjection::Text { text: text.clone() };
let encoded = encode_projection(&projection).unwrap();
assert_eq!(encoded.len(), text.len() + 2);
assert_eq!(serde_json::from_slice::<String>(&encoded).unwrap(), text);
let placeholder =
ArchivedPlaceholder::prepare("event".into(), "call".into(), "Read".into(), &projection)
.unwrap()
.unwrap();
assert_eq!(placeholder.identity.original_bytes, 5 * 1024 * 1024 + 2);
let model = placeholder.to_canonical_projection().unwrap();
let DurableToolProjection::Json { value } = model else {
panic!()
};
assert!(value.to_string().encode_utf16().count() <= MAX_PAGE_CHARS);
assert!(placeholder.page.next.is_some());
}
}