use std::path::{Path, PathBuf};
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use basis::{
AllowAll, CollectingSink, ContextConfig, MemoryConfig, ModelSelector, Provider, RunOutcome,
Runtime, Workspace, WorkspaceBuilder, async_trait,
hooks::HooksConfig,
runtime::{
ContentBlock, ModelInfo, ProviderCapabilities, ProviderDescriptor, ProviderError,
ProviderEventStream, Request, Response, Role, provider_event_stream_from_response,
},
skills::SkillsConfig,
templates::TemplatesConfig,
tools::declared::ToolsConfig,
};
struct Scripted {
asked: Arc<AtomicUsize>,
}
#[async_trait]
impl Provider for Scripted {
fn descriptor(&self) -> ProviderDescriptor {
ProviderDescriptor::new("scripted")
}
fn capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities {
supports_model_listing: true,
supports_streaming: true,
supports_tool_calls: true,
..Default::default()
}
}
async fn list_models(&self) -> Result<Vec<ModelInfo>, ProviderError> {
Ok(vec![ModelInfo::new("scripted-model", "scripted")])
}
async fn stream(&self, _request: Request<'_>) -> Result<ProviderEventStream, ProviderError> {
let asked = self.asked.fetch_add(1, Ordering::SeqCst) + 1;
Ok(provider_event_stream_from_response(Response {
id: format!("scripted-{asked}"),
model: "scripted-model".to_string(),
role: Role::Assistant,
content: vec![ContentBlock::text("the scripted answer")],
stop_reason: None,
usage: None,
}))
}
}
fn pinned(workspace: &Path, runtime: Arc<Runtime>) -> WorkspaceBuilder {
Workspace::builder(workspace)
.with_runtime(runtime)
.with_model(ModelSelector::Id("scripted-model".to_string()))
.with_context(ContextConfig {
file_name: "AGENTS.md".to_string(),
global_dir: None,
walk_parents: false,
})
.with_skills(SkillsConfig {
workspace_subdir: Some(PathBuf::from(".basis/skills")),
shared_workspace_dir: true,
global_dir: None,
shared_home_dir: false,
})
.with_templates(TemplatesConfig {
workspace_subdir: PathBuf::from(".basis/templates"),
global_dir: None,
})
.with_hooks(HooksConfig {
workspace_file: PathBuf::from(".basis/hooks.json"),
global_dir: None,
supplied: Vec::new(),
})
.with_tools(ToolsConfig {
workspace_file: PathBuf::from(".basis/tools.json"),
global_dir: None,
supplied: Vec::new(),
})
.with_memory(MemoryConfig::disabled())
}
#[tokio::test]
async fn a_prepared_run_streams_through_a_host_supplied_provider() {
let asked = Arc::new(AtomicUsize::new(0));
let runtime = Arc::new(
Runtime::builder()
.with_provider_instance(Scripted {
asked: Arc::clone(&asked),
})
.with_ephemeral_history()
.build()
.expect("an instance needs no credential, no environment, no network"),
);
assert_eq!(
runtime.provider(),
"scripted",
"the runtime answers to the descriptor's own id"
);
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("AGENTS.md"), "house rules").expect("write");
let workspace = pinned(dir.path(), runtime).open().await.expect("opens");
let report = workspace
.prepare("say the line")
.expect("mints")
.execute_with_approver(CollectingSink::default(), AllowAll)
.await
.expect("completes");
assert!(matches!(report.outcome, RunOutcome::Ok));
assert_eq!(
report.provider, "scripted",
"every run reports the instance's id, not a builtin's"
);
assert_eq!(report.final_message.as_deref(), Some("the scripted answer"));
assert_eq!(
asked.load(Ordering::SeqCst),
1,
"one prepared run is one turn through the instance"
);
}