fabro/lib/components/fabro-agent/tests/it/compaction.rs
Bryan Helmkamp e557774b0b
Rebuild fabro-llm on lithos-llm and adopt its stream contract in fabro-agent
fabro-llm is now a thin integration crate: catalog construction from the
lithos built-ins, the Fabro policy layer, and operator overlays; client
construction from catalog plus credentials; a Fabro `ModelResolver` that
enforces `metadata.fabro` policy; model selection; a server gateway
adapter; attachment inlining middleware; reasoning normalization;
one-shot structured output; probe wiring; and catalog API views. The
in-house codecs, transports, providers, tool loop, retry, cost, and
token-count code are deleted along with the wire snapshots that covered
them.

fabro-agent consumes lithos `StreamEvent`s and `Response`s directly.
Retry is split: lithos's retry middleware handles failures before any
visible output, and the agent replays the turn after.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 17:26:57 -06:00

106 lines
3.7 KiB
Rust

use std::path::Path;
use std::sync::Arc;
use fabro_agent::{AgentProfile, LocalSandbox, OpenAiProfile, Session, SessionOptions};
use fabro_llm::test_support::client_from_env;
use fabro_llm::{Client, ClientOptions};
use fabro_test::{TwinScenario, TwinScenarios, TwinToolCall, twin_openai};
use tokio::fs::read_to_string;
const MODEL: &str = "gpt-5.4-mini";
#[expect(
clippy::disallowed_methods,
reason = "e2e_openai! expands live-mode environment lookups even for twin-only tests"
)]
#[fabro_macros::e2e_test(twin)]
async fn openai_twin_compaction_preserves_tool_call_pairs() {
let tmp = tempfile::tempdir().expect("failed to create tempdir");
let (base_url, api_key) = fabro_test::e2e_openai!();
load_compaction_scenarios(&api_key).await;
let mut session = make_openai_session(tmp.path(), base_url, api_key).await;
session.initialize().await.unwrap();
let result = session
.process_input(
"Trigger the compaction regression by writing four small files, then say done.",
)
.await;
assert!(
result.is_ok(),
"session should complete without sending an orphaned function_call_output: {result:?}"
);
assert_eq!(
read_to_string(tmp.path().join("four.txt"))
.await
.expect("four.txt should be written"),
"four"
);
}
async fn make_openai_session(cwd: &Path, base_url: String, api_key: String) -> Session {
let client = openai_client(base_url, api_key).await;
let profile: Arc<dyn AgentProfile> = Arc::new(OpenAiProfile::new(MODEL));
let sandbox = Arc::new(LocalSandbox::new(cwd.to_path_buf()));
let options = SessionOptions {
enable_context_compaction: true,
compaction_threshold_percent: 80,
compaction_preserve_turns: 6,
..SessionOptions::default()
};
Session::new(client, profile, sandbox, options, None)
}
async fn load_compaction_scenarios(namespace: &str) {
TwinScenarios::new(namespace.to_string())
.scenario(
TwinScenario::responses(MODEL)
.stream(true)
.input_contains("Trigger the compaction regression")
.tool_call(TwinToolCall::write_file("one.txt", "one")),
)
.scenario(
TwinScenario::responses(MODEL)
.stream(true)
.tool_call(TwinToolCall::write_file("two.txt", "two")),
)
.scenario(
TwinScenario::responses(MODEL)
.stream(true)
.tool_call(TwinToolCall::write_file("three.txt", "three")),
)
.scenario(
TwinScenario::responses(MODEL)
.stream(true)
.tool_call(TwinToolCall::write_file("four.txt", "four"))
.usage(180_000, 5),
)
.scenario(
TwinScenario::responses(MODEL)
.stream(false)
.input_contains("Here is the conversation to summarize")
.text("short summary"),
)
.scenario(TwinScenario::responses(MODEL).stream(true).text("Done."))
.load(twin_openai().await)
.await;
}
/// A client whose `openai` provider points at `base_url` and authenticates
/// with `api_key`, the way the twin expects.
async fn openai_client(base_url: String, api_key: String) -> Client {
let catalog = fabro_llm::build_catalog(&fabro_config::LlmLayer::default(), &move |name| {
(name == fabro_static::EnvVars::OPENAI_BASE_URL).then(|| base_url.clone())
})
.expect("catalog should build");
client_from_env(
catalog,
move |name| (name == fabro_static::EnvVars::OPENAI_API_KEY).then(|| api_key.clone()),
ClientOptions::standard(),
)
.await
}