use std::sync::Arc;
use agent_client_protocol_schema::SessionId;
use defect_agent::fs::{FsBackend, NoopFsBackend};
use defect_agent::llm::{
Capabilities, CompletionRequest, FeatureSupport, HostedCapabilities, LlmProvider, ModelInfo,
ProtocolId, ProviderError, ProviderInfo, ProviderStream, ThinkingEcho,
};
use defect_agent::session::{
AgentCore, AgentError, DefaultAgentCore, Frontend, SessionCapabilitiesConfig, SessionInitError,
TurnConfig, WebSearchCapabilityConfig, WebSearchCapabilityMode, new_session_id,
};
use defect_agent::shell::{NoopShellBackend, ShellBackend};
use futures::future::BoxFuture;
use tokio_util::sync::CancellationToken;
fn unsupported_caps() -> Capabilities {
Capabilities {
tool_calls: FeatureSupport::Supported,
parallel_tool_calls: FeatureSupport::Supported,
thinking: FeatureSupport::Unsupported,
vision: FeatureSupport::Unsupported,
prompt_cache: FeatureSupport::Unsupported,
thinking_echo: ThinkingEcho::Forbidden,
}
}
struct NoHostedProvider;
impl LlmProvider for NoHostedProvider {
fn info(&self) -> ProviderInfo {
ProviderInfo {
vendor: "no-hosted".to_string(),
protocol: ProtocolId::AnthropicMessages,
display_name: "No Hosted Provider".to_string(),
}
}
fn capabilities(&self) -> Capabilities {
unsupported_caps()
}
fn list_models(&self) -> BoxFuture<'_, Result<Vec<ModelInfo>, ProviderError>> {
Box::pin(async { Ok(Vec::new()) })
}
fn model_info(&self, _: &str) -> Option<ModelInfo> {
None
}
fn complete(
&self,
_: CompletionRequest,
_: CancellationToken,
) -> BoxFuture<'_, Result<ProviderStream, ProviderError>> {
unimplemented!("not exercised")
}
}
struct HostedSearchProvider;
impl LlmProvider for HostedSearchProvider {
fn info(&self) -> ProviderInfo {
ProviderInfo {
vendor: "hosted".to_string(),
protocol: ProtocolId::AnthropicMessages,
display_name: "Hosted Web Search Provider".to_string(),
}
}
fn capabilities(&self) -> Capabilities {
unsupported_caps()
}
fn hosted_capabilities(&self) -> HostedCapabilities {
HostedCapabilities::with_web_search(true)
}
fn list_models(&self) -> BoxFuture<'_, Result<Vec<ModelInfo>, ProviderError>> {
Box::pin(async { Ok(Vec::new()) })
}
fn model_info(&self, _: &str) -> Option<ModelInfo> {
None
}
fn complete(
&self,
_: CompletionRequest,
_: CancellationToken,
) -> BoxFuture<'_, Result<ProviderStream, ProviderError>> {
unimplemented!("not exercised")
}
}
fn build_core(
provider: Arc<dyn LlmProvider>,
capabilities: SessionCapabilitiesConfig,
) -> DefaultAgentCore {
DefaultAgentCore::builder()
.provider(provider)
.config(TurnConfig {
model: "test-001".to_string(),
..TurnConfig::default()
})
.capabilities(capabilities)
.build()
}
#[tokio::test]
async fn delegate_with_unsupported_provider_fails_session_init() {
let core = build_core(
Arc::new(NoHostedProvider) as Arc<dyn LlmProvider>,
SessionCapabilitiesConfig::with_web_search(WebSearchCapabilityConfig::new(
WebSearchCapabilityMode::Delegate,
)),
);
let cwd = std::env::current_dir().expect("cwd");
let result = core
.create_session(
SessionId::new(new_session_id()),
cwd,
vec![],
Arc::new(NoopFsBackend) as Arc<dyn FsBackend>,
Arc::new(NoopShellBackend) as Arc<dyn ShellBackend>,
Frontend::Headless,
)
.await;
match result {
Ok(_) => panic!("expected CapabilityUnsatisfied; got Ok"),
Err(AgentError::Init(SessionInitError::CapabilityUnsatisfied {
capability,
provider,
})) => {
assert_eq!(capability, "web_search");
assert_eq!(provider, "no-hosted");
}
Err(other) => panic!("unexpected error: {other:?}"),
}
}
#[tokio::test]
async fn delegate_with_supported_provider_creates_session() {
let core = build_core(
Arc::new(HostedSearchProvider) as Arc<dyn LlmProvider>,
SessionCapabilitiesConfig::with_web_search(WebSearchCapabilityConfig::new(
WebSearchCapabilityMode::Delegate,
)),
);
let cwd = std::env::current_dir().expect("cwd");
let session = core
.create_session(
SessionId::new(new_session_id()),
cwd,
vec![],
Arc::new(NoopFsBackend) as Arc<dyn FsBackend>,
Arc::new(NoopShellBackend) as Arc<dyn ShellBackend>,
Frontend::Headless,
)
.await
.expect("create session");
let _ = session.id();
}
#[tokio::test]
async fn disabled_mode_succeeds_regardless_of_provider() {
for provider in [
Arc::new(NoHostedProvider) as Arc<dyn LlmProvider>,
Arc::new(HostedSearchProvider) as Arc<dyn LlmProvider>,
] {
let core = build_core(
provider,
SessionCapabilitiesConfig::with_web_search(WebSearchCapabilityConfig::new(
WebSearchCapabilityMode::Disabled,
)),
);
let cwd = std::env::current_dir().expect("cwd");
core.create_session(
SessionId::new(new_session_id()),
cwd,
vec![],
Arc::new(NoopFsBackend) as Arc<dyn FsBackend>,
Arc::new(NoopShellBackend) as Arc<dyn ShellBackend>,
Frontend::Headless,
)
.await
.expect("disabled mode should always succeed");
}
}