blob: d385910868dbe2727faaadebe53a6bb6e41fd7a0 [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 support;
use maka_agent_facts::context::CompactOutcome;
use maka_agent_facts::event::Fact;
use maka_agent_facts::event::InvocationOutcome;
use maka_event_log::EventLog;
use std::sync::Arc;
use support::*;
use tokio::net::TcpListener;
use tokio_util::sync::CancellationToken;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn compact_next_main_and_reopen_use_durable_baseline_with_complete_tail() {
let engine = |log| {
let model = maka_model::ModelExecutor::new(std::time::Duration::from_secs(10)).unwrap();
maka_agent_engine::Engine::new(
log,
model,
maka_tools::CodeExecutor::new(Default::default()).unwrap(),
)
};
let input = |base: &str, id: &str, compact| {
let mut input = support::input(base, id, compact);
input.configuration.system_prompt = Some(maka_agent_facts::execution::SystemPrompt {
sources: Vec::new(),
text: "MAIN_SYSTEM_PROMPT_EVIDENCE".into(),
policy_revision: 3,
});
input
};
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("events.sqlite");
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://{}/v1", listener.local_addr().unwrap());
let server = tokio::spawn(async move {
let mut requests = Vec::new();
for text in [
"old-answer-private",
SUMMARY,
"tail-answer",
"reopened-answer",
] {
let (mut socket, _) = listener.accept().await.unwrap();
requests.push(read_request(&mut socket).await);
respond(&mut socket, text, "stop").await;
}
requests
});
let log = Arc::new(EventLog::open(&path).await.unwrap());
let worker = engine(log.clone());
worker
.run(input(&base, "old", false), CancellationToken::new())
.await
.unwrap();
worker
.run(input(&base, "compact", true), CancellationToken::new())
.await
.unwrap();
let source = log.read_model_context("session", None).await.unwrap();
let checkpoint = source.baseline.unwrap().event_id;
worker
.run(input(&base, "tail", false), CancellationToken::new())
.await
.unwrap();
worker.drain().await;
drop(worker);
Arc::try_unwrap(log).ok().unwrap().close().await.unwrap();
let log = Arc::new(EventLog::open(&path).await.unwrap());
let worker = engine(log.clone());
worker
.run(input(&base, "reopen", false), CancellationToken::new())
.await
.unwrap();
worker.drain().await;
let requests = server.await.unwrap();
for index in [0, 2, 3] {
assert_eq!(requests[index]["messages"][0]["role"], "system");
assert!(
requests[index]["messages"][0]["content"]
.as_str()
.unwrap()
.starts_with("MAIN_SYSTEM_PROMPT_EVIDENCE")
);
assert_eq!(requests[index]["messages"][0], requests[0]["messages"][0]);
}
assert_eq!(requests[1]["messages"][0], requests[0]["messages"][0]);
assert_eq!(requests[1]["messages"][1], requests[0]["messages"][1]);
assert!(
requests[1]["messages"]
.to_string()
.contains("old-answer-private")
);
assert_eq!(requests[1]["messages"][0]["role"], "system");
assert!(
requests[1]["messages"].as_array().unwrap().last().unwrap()["content"]
.as_str()
.unwrap()
.contains(
maka_agent_facts::context::default_compaction_prompt(
maka_agent_facts::language::Language::En
)
.lines()
.next()
.unwrap()
)
);
assert!(requests[1]["tools"].as_array().is_none_or(Vec::is_empty));
assert_eq!(requests[1]["max_tokens"], 8000);
for request in &requests[2..] {
assert!(
request["messages"][1]["content"]
.as_str()
.unwrap()
.contains(SUMMARY)
);
assert!(
!request["messages"]
.to_string()
.contains("old-answer-private")
);
assert!(
!request["messages"].to_string().contains(
maka_agent_facts::context::default_compaction_prompt(
maka_agent_facts::language::Language::En
)
.lines()
.next()
.unwrap()
)
);
}
assert!(requests[3]["messages"].to_string().contains("tail-answer"));
let prefix = log.prefix(100, 256 * 1024).await.unwrap();
let summary = prefix
.events
.iter()
.find(|stored| {
matches!(
stored.event.fact,
Fact::ModelRequested {
purpose: maka_agent_facts::context::ModelPurpose::Summary,
..
}
)
})
.unwrap();
let composition = log
.request_composition("session", &summary.event.id)
.await
.unwrap()
.unwrap();
assert_eq!(composition.max_output_tokens, Some(8000));
assert_eq!(
composition.system_prompt.as_deref(),
Some("MAIN_SYSTEM_PROMPT_EVIDENCE")
);
let first_request = prefix
.events
.iter()
.find(|stored| matches!(stored.event.fact, Fact::ModelRequested { .. }))
.unwrap();
let original = log
.request_composition("session", &first_request.event.id)
.await
.unwrap()
.unwrap();
assert_eq!(composition.dynamic_context, original.dynamic_context);
let adapter = original
.sources
.iter()
.find(|source| source.kind == maka_agent_facts::composition::SourceKind::ModelAdapter)
.unwrap();
assert_eq!(adapter.name, "chat-completions");
assert!(composition.sources.contains(adapter));
for event in prefix.events.iter().filter(|event| {
matches!(
event.event.invocation.invocation_id.as_str(),
"invocation-tail" | "invocation-reopen"
)
}) {
if let Fact::ModelRequested {
checkpoint_event_id,
..
} = &event.event.fact
{
assert_eq!(checkpoint_event_id.as_ref(), Some(&checkpoint));
}
}
let legacy = maka_agent_engine::project_model_history(
&prefix,
"session",
maka_agent_facts::language::Language::En,
)
.unwrap();
assert!(
!serde_json::to_string(&legacy)
.unwrap()
.contains("summary-marker"),
"summary attempts never become ordinary history"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn arbitrary_completed_summary_is_accepted_without_format_repair() {
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 server = tokio::spawn(async move {
let mut requests = Vec::new();
for text in ["old-answer", "invalid-summary", "next-answer"] {
let (mut socket, _) = listener.accept().await.unwrap();
requests.push(read_request(&mut socket).await);
respond(&mut socket, text, "stop").await;
}
requests
});
let worker = engine(log.clone());
for (id, compact) in [("old", false), ("bad", true), ("next", false)] {
worker
.run(input(&base, id, compact), CancellationToken::new())
.await
.unwrap();
}
let prefix = log.prefix(100, 256 * 1024).await.unwrap();
assert!(prefix.events.iter().any(|event| matches!(&event.event.fact,
Fact::ContextCheckpointRecorded { checkpoint } if checkpoint.summary.text == "invalid-summary")));
let requests = server.await.unwrap();
assert_eq!(requests.len(), 3);
assert!(
requests[2]["messages"]
.to_string()
.contains("invalid-summary")
);
assert!(!requests[2]["messages"].to_string().contains("old-answer"));
worker.drain().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn incomplete_summary_fails_without_repair_and_main_retains_incomplete_text() {
use crate::support::agent_loop;
use serde_json::json;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::io::AsyncWriteExt;
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 server = tokio::spawn(async move {
let mut requests = Vec::new();
for (text, reason) in [
("old", "stop"),
("cut", "length"),
("truncated-main", "length"),
("after-failure", "stop"),
] {
let (mut socket, _) = listener.accept().await.unwrap();
requests.push(read_request(&mut socket).await);
if text == "truncated-main" {
let partial = json!({"id":"partial","object":"chat.completion.chunk","created":1,"model":"test",
"choices":[{"index":0,"delta":{"content":text,"tool_calls":[{"index":0,"id":"unexecuted","type":"function",
"function":{"name":"echo","arguments":"{\"value\":42}"}}]},"finish_reason":null}]});
let end = json!({"id":"partial","object":"chat.completion.chunk","created":1,"model":"test",
"choices":[{"index":0,"delta":{},"finish_reason":reason}]});
let body = format!("data: {partial}\n\ndata: {end}\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();
} else {
respond(&mut socket, text, reason).await;
}
}
requests
});
let worker = engine(log.clone());
worker
.run(input(&base, "old", false), CancellationToken::new())
.await
.unwrap();
worker
.run(input(&base, "shorten", true), CancellationToken::new())
.await
.unwrap();
let effects = Arc::new(AtomicUsize::new(0));
let truncated_input = agent_loop::input(
&base,
"truncated",
Arc::new(agent_loop::Effect {
log: log.clone(),
count: effects.clone(),
}),
);
assert!(matches!(
worker.run(truncated_input, CancellationToken::new()).await,
Err(maka_agent_engine::RunError::ModelIncomplete)
));
assert_eq!(effects.load(Ordering::SeqCst), 0);
worker
.run(
input(&base, "after-failure", false),
CancellationToken::new(),
)
.await
.unwrap();
let requests = server.await.unwrap();
assert!(
requests[3]["messages"]
.to_string()
.contains("truncated-main"),
"valid incomplete text remains available to a new user message"
);
let prefix = log.prefix(100, 256 * 1024).await.unwrap();
assert!(
!prefix
.events
.iter()
.any(|event| matches!(event.event.fact, Fact::ContextCheckpointRecorded { .. }))
);
assert!(prefix.events.iter().any(|event| matches!(
&event.event.fact,
Fact::InvocationEnded {
outcome: InvocationOutcome::ContextCompactFinished {
outcome: CompactOutcome::Failed { .. }
}
}
)));
let truncated: Vec<_> = prefix
.events
.iter()
.filter(|event| event.event.invocation.invocation_id == "invocation-truncated")
.collect();
assert!(truncated.iter().any(|event| matches!(
&event.event.fact,
Fact::ToolRejected {
reason: maka_agent_facts::tool_call::ToolRejection::PreparationFailed { .. },
..
}
)));
assert!(
!truncated
.iter()
.any(|event| matches!(event.event.fact, Fact::ToolDispatched { .. }))
);
assert!(
truncated
.iter()
.any(|event| matches!(&event.event.fact, Fact::InvocationEnded {
outcome: InvocationOutcome::Failed { class, .. }
} if class == "model_incomplete"))
);
assert!(
truncated
.iter()
.any(|event| matches!(event.event.fact, Fact::ModelCompleted { .. }))
);
worker.drain().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn summary_preserves_main_prefix_tools_and_custom_instruction_with_independent_budget() {
use crate::support::agent_loop;
use std::sync::atomic::{AtomicUsize, Ordering};
for (window, main_budget, expected_budget) in [
(None, Some(128), 8000),
(Some(1_000_000), Some(128), 40_000),
(Some(1_000_000), Some(64_000), 64_000),
(Some(128_000), None, 8000),
(Some(128_000), Some(128), 5120),
] {
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 server = tokio::spawn(async move {
let mut requests = vec![];
for text in ["original answer", "自由格式摘要", "next answer", ""] {
let (mut socket, _) = listener.accept().await.unwrap();
requests.push(read_request(&mut socket).await);
respond(&mut socket, text, "stop").await;
}
requests
});
let effects = Arc::new(AtomicUsize::new(0));
let worker = engine(log.clone());
for (id, compact) in [
("first", false),
("compact", true),
("next", false),
("again", true),
] {
let mut run = input(&base, id, compact);
run.main_output_limit = main_budget;
run.context = Some(maka_agent_facts::context::ModelRequestContext {
provider_id: "fixture".into(),
context_window: window,
model_context_window: window,
declared_window: None,
});
run.configuration.system_prompt = Some(maka_agent_facts::execution::SystemPrompt {
text: if compact {
"different compact invocation system"
} else {
"frozen ordinary main system"
}
.into(),
sources: vec![],
policy_revision: 1,
});
if compact {
run.configuration.compaction_prompt = Some("只保留下一步;可使用任意格式。".into());
} else {
run.work = agent_loop::input(
&base,
id,
Arc::new(agent_loop::Effect {
log: log.clone(),
count: effects.clone(),
}),
)
.work;
}
worker.run(run, CancellationToken::new()).await.unwrap();
}
worker.drain().await;
let requests = server.await.unwrap();
for (main, summary) in [(0, 1), (2, 3)] {
let prefix = requests[main]["messages"].as_array().unwrap();
let compact = requests[summary]["messages"].as_array().unwrap();
assert_eq!(&compact[..prefix.len()], prefix);
assert_eq!(
compact.len(),
prefix.len() + 2,
"one completed assistant message and one compaction instruction"
);
assert_eq!(compact.last().unwrap()["role"], "user");
assert_eq!(
compact.last().unwrap()["content"],
"只保留下一步;可使用任意格式。"
);
assert_eq!(requests[summary]["tools"], requests[main]["tools"]);
assert!(!requests[main]["tools"].as_array().unwrap().is_empty());
assert_eq!(requests[summary]["max_tokens"], expected_budget);
assert_eq!(requests[main]["max_tokens"], main_budget.unwrap_or(8000));
}
assert_eq!(effects.load(Ordering::SeqCst), 0);
let context = log.read_model_context("session", None).await.unwrap();
assert_eq!(context.baseline.unwrap().checkpoint.summary.text, "");
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn summary_only_excerpts_after_provider_overflow_with_frozen_composition() {
use tokio::io::AsyncWriteExt;
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 original = "history-marker ".repeat(3000);
let expected = original.clone();
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
read_request(&mut socket).await;
respond(&mut socket, &original, "stop").await;
let (mut socket, _) = listener.accept().await.unwrap();
let full = read_request(&mut socket).await;
let body = r#"{"error":{"message":"request exceeds context","type":"invalid_request_error","code":"context_length_exceeded"}}"#;
socket.write_all(format!("HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).as_bytes()).await.unwrap();
let (mut socket, _) = listener.accept().await.unwrap();
let fitted = read_request(&mut socket).await;
respond(&mut socket, "brief arbitrary summary", "stop").await;
(full, fitted)
});
let worker = engine(log.clone());
worker
.run(input(&base, "old", false), CancellationToken::new())
.await
.unwrap();
let mut compact = input(&base, "compact", true);
compact.main_output_limit = Some(128);
compact.context = Some(maka_agent_facts::context::ModelRequestContext {
provider_id: "fixture".into(),
context_window: Some(10_000),
model_context_window: Some(10_000),
declared_window: None,
});
worker.run(compact, CancellationToken::new()).await.unwrap();
worker.drain().await;
let (full, fitted) = server.await.unwrap();
assert!(full["messages"].to_string().contains(&expected));
assert!(
!full["messages"]
.to_string()
.contains("Conversation excerpts")
);
assert!(
fitted["messages"]
.to_string()
.contains("Conversation excerpts")
);
assert!(fitted.to_string().len() < full.to_string().len());
assert_eq!(full["max_tokens"], 400);
assert_eq!(fitted["max_tokens"], 400);
let events = log.prefix(100, 1024 * 1024).await.unwrap();
let mut compositions = vec![];
for event in events.events.iter().filter(|event| {
matches!(
event.event.fact,
Fact::ModelRequested {
purpose: maka_agent_facts::context::ModelPurpose::Summary,
..
}
)
}) {
compositions.push(
log.request_composition("session", &event.event.id)
.await
.unwrap()
.unwrap(),
);
}
assert_eq!(compositions.len(), 2);
assert_eq!(compositions[0], compositions[1]);
assert!(events.events.iter().any(|event| matches!(&event.event.fact,
Fact::ContextCheckpointRecorded { checkpoint } if checkpoint.summary.text == "brief arbitrary summary")));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn summary_advertised_tools_never_execute() {
use crate::support::agent_loop;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::io::AsyncWriteExt;
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 server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let main = read_request(&mut socket).await;
respond(&mut socket, "ordinary answer", "stop").await;
let (mut socket, _) = listener.accept().await.unwrap();
let summary = read_request(&mut socket).await;
assert_eq!(summary["tools"], main["tools"]);
let call = serde_json::json!({"id":"summary","object":"chat.completion.chunk","created":1,"model":"test",
"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"forbidden","type":"function",
"function":{"name":"echo","arguments":"{}"}}]},"finish_reason":null}]});
let end = serde_json::json!({"id":"summary","object":"chat.completion.chunk","created":1,"model":"test",
"choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]});
let body = format!("data: {call}\n\ndata: {end}\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();
});
let effects = Arc::new(AtomicUsize::new(0));
let worker = engine(log.clone());
let mut main = input(&base, "main", false);
main.work = agent_loop::input(
&base,
"main",
Arc::new(agent_loop::Effect {
log: log.clone(),
count: effects.clone(),
}),
)
.work;
worker.run(main, CancellationToken::new()).await.unwrap();
assert!(
worker
.run(input(&base, "compact", true), CancellationToken::new())
.await
.is_err()
);
worker.drain().await;
server.await.unwrap();
assert_eq!(effects.load(Ordering::SeqCst), 0);
assert!(
log.read_model_context("session", None)
.await
.unwrap()
.baseline
.is_none()
);
let events = log.prefix(100, 256 * 1024).await.unwrap();
assert!(
!events
.events
.iter()
.any(|event| matches!(event.event.fact, Fact::ToolDispatched { .. }))
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn overflow_preserves_system_and_first_goal_or_continuation_summary() {
use tokio::io::AsyncWriteExt;
const GOAL: &str = "UNIQUE_ORIGINAL_GOAL: recover the launch checklist";
const SYSTEM: &str = "Preserve trusted system instructions exactly.";
for baseline in [false, true] {
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 server = tokio::spawn(async move {
let mut responses = vec!["initial answer".to_string()];
if baseline {
responses.push(GOAL.into());
}
responses.extend(
(0..3)
.map(|index| format!("large turn {index}: {}", "recent detail ".repeat(3000))),
);
for response in responses {
let (mut socket, _) = listener.accept().await.unwrap();
read_request(&mut socket).await;
respond(&mut socket, &response, "stop").await;
}
let (mut socket, _) = listener.accept().await.unwrap();
let full = read_request(&mut socket).await;
let body = r#"{"error":{"message":"context exceeded","type":"invalid_request_error","code":"context_length_exceeded"}}"#;
socket.write_all(format!("HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).as_bytes()).await.unwrap();
let (mut socket, _) = listener.accept().await.unwrap();
let fitted = read_request(&mut socket).await;
respond(&mut socket, "final summary", "stop").await;
(full, fitted)
});
let worker = engine(log.clone());
let mut stages = vec![("first", false)];
if baseline {
stages.push(("baseline", true));
}
stages.extend([
("one", false),
("two", false),
("three", false),
("final", true),
]);
for (id, compact) in stages {
let mut run = input(&base, id, compact);
run.configuration.system_prompt = Some(maka_agent_facts::execution::SystemPrompt {
text: SYSTEM.into(),
sources: vec![],
policy_revision: 1,
});
if id == "first"
&& let maka_agent_engine::RunWork::Message { message, .. } = &mut run.work
{
*message = GOAL.into();
}
if id == "final" {
run.context = Some(maka_agent_facts::context::ModelRequestContext {
provider_id: "fixture".into(),
context_window: Some(10_000),
model_context_window: Some(10_000),
declared_window: None,
});
}
worker.run(run, CancellationToken::new()).await.unwrap();
}
worker.drain().await;
let (full, fitted) = server.await.unwrap();
assert_eq!(full["messages"][0], fitted["messages"][0]);
assert_eq!(fitted["messages"][0]["role"], "system");
let system = fitted["messages"][0]["content"].as_str().unwrap();
assert!(system.starts_with(SYSTEM));
let events = log.prefix(100, 1024 * 1024).await.unwrap();
let breakdowns: Vec<_> = events
.events
.iter()
.filter(|event| event.event.invocation.invocation_id == "invocation-final")
.filter_map(|event| match &event.event.fact {
Fact::ModelRequested {
context_breakdown: Some(breakdown),
..
} => Some(breakdown),
_ => None,
})
.collect();
assert_eq!(breakdowns.len(), 2);
for breakdown in breakdowns {
assert_eq!(breakdown.system.content_bytes, system.len() as u64);
}
assert!(full["messages"].to_string().contains(GOAL));
assert!(
!full["messages"]
.to_string()
.contains("Conversation excerpts")
);
let content = &fitted["messages"][1]["content"];
let excerpt = if let Some(text) = content.as_str() {
text.to_owned()
} else {
content
.as_array()
.unwrap()
.iter()
.filter_map(|part| part["text"].as_str())
.collect::<Vec<_>>()
.join("\n")
};
assert!(excerpt.contains("Earlier task or summary:"));
assert!(
excerpt.contains(GOAL),
"first conversational anchor must survive: {excerpt}"
);
assert!(excerpt.contains("large turn 2"));
assert!(fitted.to_string().len() < full.to_string().len());
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn small_window_caps_summary_output_and_compacts_only_for_input_pressure() {
for (manual, large_history, output_limit) in [
(true, false, None),
(false, false, Some(200_000)),
(false, true, Some(200_000)),
] {
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 summarized = manual || large_history;
let server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
read_request(&mut socket).await;
let history = if large_history {
"retained history ".repeat(1000)
} else {
"short history".into()
};
respond(&mut socket, &history, "stop").await;
let mut requests = Vec::new();
for _ in 0..if summarized && !manual { 2 } else { 1 } {
let (mut socket, _) = listener.accept().await.unwrap();
let request = read_request(&mut socket).await;
let is_summary = request["messages"].to_string().contains(
maka_agent_facts::context::default_compaction_prompt(
maka_agent_facts::language::Language::En,
)
.lines()
.next()
.unwrap(),
);
respond(
&mut socket,
if is_summary {
"brief checkpoint"
} else {
"done"
},
"stop",
)
.await;
requests.push(request);
}
requests
});
let worker = engine(log);
worker
.run(input(&base, "old", false), CancellationToken::new())
.await
.unwrap();
let mut next = input(&base, "next", manual);
next.main_output_limit = output_limit;
next.context = Some(maka_agent_facts::context::ModelRequestContext {
provider_id: "fixture".into(),
context_window: Some(4096),
model_context_window: Some(4096),
declared_window: None,
});
worker.run(next, CancellationToken::new()).await.unwrap();
worker.drain().await;
let requests = server.await.unwrap();
let summaries: Vec<_> = requests
.iter()
.filter(|request| {
request["messages"].to_string().contains(
maka_agent_facts::context::default_compaction_prompt(
maka_agent_facts::language::Language::En,
)
.lines()
.next()
.unwrap(),
)
})
.collect();
assert_eq!(summaries.len(), usize::from(summarized));
for request in &requests {
let output = request["max_tokens"].as_u64().unwrap();
assert!((1..4096).contains(&output));
}
if large_history {
assert_eq!(summaries[0]["max_tokens"], 164);
assert!(
summaries[0]["messages"]
.to_string()
.contains("retained history")
);
assert!(
requests[1]["messages"]
.to_string()
.contains("brief checkpoint")
);
} else if manual {
assert!(summaries[0]["max_tokens"].as_u64().unwrap() > 164);
}
}
}
#[tokio::test]
async fn new_summary_uses_captured_language_without_retranslating_the_prior_main_prefix() {
use maka_agent_facts::language::Language;
for (language, summary_instruction, context_label) in [
(
Language::ZhCn,
maka_agent_facts::context::default_compaction_prompt(Language::ZhCn),
"会话上下文",
),
(
Language::ZhTw,
maka_agent_facts::context::default_compaction_prompt(Language::ZhTw),
"工作階段上下文",
),
] {
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 server = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let main = read_request(&mut socket).await;
respond(&mut socket, "opaque prior answer 原文", "stop").await;
let (mut socket, _) = listener.accept().await.unwrap();
let summary = read_request(&mut socket).await;
respond(&mut socket, SUMMARY, "stop").await;
(main, summary)
});
let worker = engine(log.clone());
let mut original = input(&base, "original", false);
original.configuration.system_prompt = Some(maka_agent_facts::execution::SystemPrompt {
text: "Original custom prefix 原文".into(),
policy_revision: 0,
sources: vec![],
});
worker
.run(original, CancellationToken::new())
.await
.unwrap();
let mut compact = input(&base, "translated-summary", true);
compact.configuration.language = language;
compact.configuration.system_prompt = Some(maka_agent_facts::execution::SystemPrompt {
text: "This current prefix must not replace the prior Main surface".into(),
policy_revision: 0,
sources: vec![],
});
worker.run(compact, CancellationToken::new()).await.unwrap();
worker.drain().await;
let (main, summary) = server.await.unwrap();
assert!(
main["messages"][0]["content"]
.as_str()
.unwrap()
.contains("Original custom prefix 原文")
);
let system = summary["messages"][0]["content"].as_str().unwrap();
assert!(system.starts_with("Original custom prefix 原文"));
assert!(system.contains(context_label));
assert!(!system.contains("This current prefix"));
let messages = summary["messages"].as_array().unwrap();
assert!(
messages.last().unwrap()["content"]
.as_str()
.unwrap()
.contains(summary_instruction)
);
assert!(
messages
.iter()
.any(|message| message["content"] == "opaque prior answer 原文")
);
let prefix = log.prefix(100, 128 * 1024).await.unwrap();
let summary_request = prefix
.events
.iter()
.find(|event| {
matches!(
event.event.fact,
Fact::ModelRequested {
purpose: maka_agent_facts::context::ModelPurpose::Summary,
..
}
)
})
.unwrap();
let frozen = log
.request_composition("session", &summary_request.event.id)
.await
.unwrap()
.unwrap();
assert_eq!(
frozen.system_prompt.as_deref(),
Some("Original custom prefix 原文")
);
log.shutdown().await.unwrap();
}
}