blob: 02968b939bd6b0eb8946f0c7f27dc7c9a6194c7c [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.
*/
use crate::support::context as fixture;
use maka_agent_engine::RunWork;
use maka_agent_facts::context::ModelRequestContext;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::ToolOutcome;
use maka_agent_facts::tools::ToolExecutor;
use maka_agent_facts::tools::ToolFuture;
use maka_event_log::EventLog;
use maka_tools::{
ToolCatalog, ToolDefinition, ToolHandler, ToolNesting, ToolRegistration, ToolSemantics,
};
use serde_json::{Value, json};
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use tokio::{
io::AsyncWriteExt,
net::{TcpListener, TcpStream},
};
use tokio_util::sync::CancellationToken;
struct Large(Arc<AtomicUsize>, usize);
impl ToolExecutor for Large {
fn names(&self) -> Vec<String> {
vec!["Read".into()]
}
fn invoke(&self, _: String, _: Value, _: CancellationToken) -> ToolFuture {
self.0.fetch_add(1, Ordering::SeqCst);
let repeats = self.1;
Box::pin(async move { Ok(json!({"content": "visible-original-marker".repeat(repeats)})) })
}
}
fn catalog(count: Arc<AtomicUsize>, repeats: usize) -> ToolCatalog {
ToolCatalog::new([ToolRegistration {
definition: ToolDefinition {
freeform: None,
output_schema: None,
provider: None,
name: "Read".into(),
description: "fixture".into(),
input_schema: json!({"type":"object","properties":{"path":{"type":"string"}}}),
},
nesting: ToolNesting::Nestable,
semantics: ToolSemantics::Parallel,
handler: ToolHandler::Immediate(Arc::new(Large(count, repeats))),
}])
.unwrap()
}
async fn tool(socket: &mut TcpStream, count: usize) {
let calls: Vec<_> = (0..count).map(|index| json!({"index":index,"id":format!("call-{index}"),"type":"function","function":{"name":"Read","arguments":"{\"path\":\"file\"}"}})).collect();
let call = json!({"id":"response","object":"chat.completion.chunk","created":1,"model":"test",
"choices":[{"index":0,"delta":{"tool_calls":calls},"finish_reason":null}]});
let done = json!({"id":"response","object":"chat.completion.chunk","created":1,"model":"test",
"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":200,"completion_tokens":10,"total_tokens":210}});
let body = format!("data: {call}\n\ndata: {done}\n\ndata: [DONE]\n\n");
socket.write_all(format!("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",body.len()).as_bytes()).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn settled_results_are_pruned_before_context_limits_and_survive_failed_summary() {
for (spent, interrupted, call_count, repeats) in [
(false, false, 40, 10_000),
(true, false, 1, 600),
(true, true, 1, 600),
] {
let directory = tempfile::tempdir().unwrap();
let log = Arc::new(
EventLog::open(&directory.path().join("events.sqlite"))
.await
.unwrap(),
);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://{}/v1", listener.local_addr().unwrap());
let observed = log.clone();
let server = tokio::spawn(async move {
if interrupted {
for text in ["old-answer", fixture::SUMMARY] {
let (mut socket, _) = listener.accept().await.unwrap();
fixture::read_request(&mut socket).await;
fixture::respond(&mut socket, text, "stop").await;
}
}
let (mut socket, _) = listener.accept().await.unwrap();
let first = fixture::read_request(&mut socket).await;
assert_eq!(first["max_tokens"], 4000);
tool(&mut socket, call_count).await;
let mut main = None;
let mut summaries = 0;
let expected_summaries = if !spent { 0 } else { 1 };
while summaries < expected_summaries || main.is_none() {
let (mut socket, _) = listener.accept().await.unwrap();
let request = fixture::read_request(&mut socket).await;
if !request["messages"].to_string().contains(
maka_agent_facts::context::default_compaction_prompt(
maka_agent_facts::language::Language::En,
)
.lines()
.next()
.unwrap(),
) {
assert!(main.is_none());
main = Some((socket, request));
continue;
}
summaries += 1;
assert!(summaries <= expected_summaries);
assert_eq!(
request["max_tokens"], 8000,
"Summary budget is independent of the Main output budget"
);
assert!(
request["messages"]
.to_string()
.contains("visible-original-marker")
);
if interrupted {
let delta = json!({"id":"summary","object":"chat.completion.chunk","created":1,"model":"test",
"choices":[{"index":0,"delta":{"content":"partial-summary-must-not-replay"},"finish_reason":null}]});
let failure =
json!({"error":{"message":"summary stream failed","type":"server_error"}});
let body = format!("data: {delta}\n\ndata: {failure}\n\n");
socket.write_all(format!("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",body.len()).as_bytes()).await.unwrap();
} else {
fixture::respond(&mut socket, "incomplete summary", "length").await;
}
}
let (mut socket, request) = main.unwrap();
// Do not cancel the last summary merely because Main has finished:
// this case verifies a genuinely failed summary, not terminal abort.
let mut commits = observed.subscribe_commits();
loop {
// Forty calls also retain their input start/delta/end observations;
// together with dispatch, settlement and archive facts they exceed 200.
let prefix = observed.prefix(512, 16 * 1024 * 1024).await.unwrap();
let summary_steps: Vec<_> = prefix
.events
.iter()
.filter_map(|stored| match &stored.event.fact {
Fact::ModelRequested {
step_id,
purpose: maka_agent_facts::context::ModelPurpose::Summary,
..
} if stored.event.invocation.invocation_id == "invocation-first" => {
Some(step_id)
}
_ => None,
})
.collect();
let settled = prefix
.events
.iter()
.filter(|stored| match &stored.event.fact {
Fact::ModelCompleted { step_id, .. }
| Fact::ModelInterrupted { step_id, .. } => {
summary_steps.contains(&step_id)
}
_ => false,
})
.count();
if settled == expected_summaries {
break;
}
commits.changed().await.unwrap();
}
let messages = request["messages"].to_string();
assert_eq!(
request["max_tokens"], 4000,
"All Main steps use the admitted limit"
);
assert!(messages.contains("maka.archived_tool_result"));
assert!(
messages.contains("visible-original-marker"),
"bounded first page is visible"
);
for message in request["messages"]
.as_array()
.unwrap()
.iter()
.filter(|m| m["role"] == "tool")
{
assert!(message["content"].as_str().unwrap().encode_utf16().count() <= 7500);
}
assert!(!messages.contains("partial-summary-must-not-replay"));
assert_eq!(messages.contains("summary-marker"), interrupted);
let prefix = observed.prefix(512, 16 * 1024 * 1024).await.unwrap();
let archives: Vec<_> = prefix
.events
.iter()
.filter(|e| matches!(e.event.fact, Fact::ToolResultArchived { .. }))
.collect();
assert_eq!(archives.len(), call_count);
assert!(prefix.events.iter().any(|e| matches!(&e.event.fact,
Fact::ToolSettled { outcome: ToolOutcome::Succeeded { model_projection, .. }, .. }
if serde_json::to_string(model_projection).unwrap().contains("visible-original-marker")
)));
fixture::respond(&mut socket, "done", "stop").await;
});
let count = Arc::new(AtomicUsize::new(0));
let mut input = fixture::input(&base, "first", false);
input.main_output_limit = Some(4000);
input.context = Some(ModelRequestContext {
provider_id: "openai".into(),
// The archived pages fit; the original multi-megabyte tool output
// does not. The separate usage threshold selects summary attempts.
context_window: Some(256_000),
model_context_window: None,
declared_window: spent.then_some(210),
});
input.work = RunWork::Message {
allow_prior_unknown: false,
source_messages: Vec::new(),
message: "read".into(),
tools: catalog(count.clone(), repeats),
};
let worker = fixture::engine(log.clone());
if interrupted {
worker
.run(
fixture::input(&base, "old", false),
CancellationToken::new(),
)
.await
.unwrap();
worker
.run(
fixture::input(&base, "baseline", true),
CancellationToken::new(),
)
.await
.unwrap();
}
let previous = log
.read_model_context("session", None)
.await
.unwrap()
.baseline
.map(|baseline| baseline.event_id);
worker.run(input, CancellationToken::new()).await.unwrap();
worker.drain().await;
server.await.unwrap();
assert_eq!(count.load(Ordering::SeqCst), call_count);
let prefix = log.prefix(512, 16 * 1024 * 1024).await.unwrap();
assert_eq!(
prefix
.events
.iter()
.filter(|stored| matches!(
stored.event.fact,
Fact::ContextCheckpointRecorded { .. }
))
.count(),
usize::from(interrupted)
);
let next = log
.read_model_context("session", None)
.await
.unwrap()
.baseline
.map(|baseline| baseline.event_id);
assert_eq!(previous, next);
assert_eq!(
prefix
.events
.iter()
.filter(|stored| matches!(stored.event.fact, Fact::ModelInterrupted { .. }))
.count(),
usize::from(interrupted)
);
if interrupted {
assert!(prefix.events.iter().any(|stored| matches!(&stored.event.fact,
Fact::ModelObserved { event, .. } if serde_json::to_string(event).unwrap().contains("partial-summary-must-not-replay"))));
assert_eq!(
prefix
.events
.iter()
.filter(|stored| stored.event.invocation.invocation_id == "invocation-first")
.filter(|stored| matches!(
stored.event.fact,
Fact::ModelRequested {
purpose: maka_agent_facts::context::ModelPurpose::Summary,
..
}
))
.count(),
1
);
}
for stored in prefix.events {
if let Fact::ModelRequested {
effective_source_digest,
..
} = stored.event.fact
{
assert!(
effective_source_digest.is_some(),
"every request must retain its effective source for bundle validation"
);
}
}
}
}