mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
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>
106 lines
3.7 KiB
Rust
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
|
|
}
|