use std::{
collections::VecDeque,
path::Path,
sync::{Arc, Mutex},
time::Duration,
};
use async_trait::async_trait;
use basis::{
AllowAll, ApprovalAnswer, ApprovalRequest, Approver, Bound, ChildContext, ChildSpec,
CollectingSink, SpawnTool, ToolRoster, TurnOptions, approval::ApprovalGate,
run::prepare_with_session, tools::SPAWN,
};
use mentra::{
BuiltinProvider, ContentBlock, ModelInfo, Role, Runtime, RuntimePolicy, Session, TokenUsage,
agent::{AgentConfig, ToolProfile, WorkspaceConfig},
provider::{
Provider, ProviderDescriptor, ProviderError, ProviderEventStream, Request, Response,
provider_event_stream_from_response,
},
runtime::VolatileRuntimeStore,
};
use serde_json::json;
const NOT_STUCK: Duration = Duration::from_secs(20);
#[derive(Debug, Clone)]
struct Turn {
content: Vec<ContentBlock>,
tokens: u64,
}
impl Turn {
fn calling(id: &str, input: &str) -> Self {
Self::calling_tool(id, SPAWN, json!({ "input": input }))
}
fn calling_tool(id: &str, name: &str, input: serde_json::Value) -> Self {
Self {
content: vec![ContentBlock::ToolUse {
id: id.to_string(),
name: name.to_string(),
input,
}],
tokens: 0,
}
}
fn saying(text: &str) -> Self {
Self {
content: vec![ContentBlock::text(text)],
tokens: 0,
}
}
fn costing(self, tokens: u64) -> Self {
Self { tokens, ..self }
}
fn usage(&self) -> Option<TokenUsage> {
(self.tokens > 0).then(|| TokenUsage {
input_tokens: Some(self.tokens),
output_tokens: Some(0),
total_tokens: Some(self.tokens),
..TokenUsage::default()
})
}
}
#[derive(Debug, Clone)]
struct Asked {
model: String,
tools: Vec<String>,
system: String,
}
struct ScriptedProvider {
id: BuiltinProvider,
models: Vec<ModelInfo>,
turns: Mutex<VecDeque<Turn>>,
asked: Arc<Mutex<Vec<Asked>>>,
}
impl ScriptedProvider {
fn new(
id: BuiltinProvider,
models: Vec<ModelInfo>,
turns: Vec<Turn>,
) -> (Self, Arc<Mutex<Vec<Asked>>>) {
let asked = Arc::new(Mutex::new(Vec::new()));
let provider = Self {
id,
models,
turns: Mutex::new(turns.into()),
asked: Arc::clone(&asked),
};
(provider, asked)
}
}
#[async_trait]
impl Provider for ScriptedProvider {
fn descriptor(&self) -> ProviderDescriptor {
ProviderDescriptor::new(self.id)
}
async fn list_models(&self) -> Result<Vec<ModelInfo>, ProviderError> {
Ok(self.models.clone())
}
async fn stream(&self, request: Request<'_>) -> Result<ProviderEventStream, ProviderError> {
self.asked.lock().expect("not poisoned").push(Asked {
model: request.model.to_string(),
tools: request.tools.iter().map(|tool| tool.name.clone()).collect(),
system: request
.system
.as_deref()
.map(str::to_string)
.unwrap_or_default(),
});
let turn = self
.turns
.lock()
.expect("not poisoned")
.pop_front()
.unwrap_or_else(|| Turn::saying("done"));
Ok(provider_event_stream_from_response(Response {
id: "scripted".to_string(),
model: request.model.to_string(),
role: Role::Assistant,
usage: turn.usage(),
content: turn.content,
stop_reason: None,
}))
}
}
struct Requests(Arc<Mutex<Vec<Asked>>>);
impl Requests {
fn all(&self) -> Vec<Asked> {
self.0.lock().expect("not poisoned").clone()
}
fn nth(&self, index: usize) -> Asked {
self.all()
.get(index)
.unwrap_or_else(|| panic!("no request at index {index}"))
.clone()
}
}
struct Recording {
seen: Arc<Mutex<Vec<ApprovalRequest>>>,
}
#[async_trait]
impl Approver for Recording {
async fn approve(&mut self, request: &ApprovalRequest) -> ApprovalAnswer {
self.seen
.lock()
.expect("not poisoned")
.push(request.clone());
AllowAll.approve(request).await
}
}
fn parent_model() -> ModelInfo {
ModelInfo::new("parent-model", BuiltinProvider::OpenAI)
}
fn cheap_model() -> ModelInfo {
ModelInfo::new("cheap-model", BuiltinProvider::Anthropic)
}
fn triage_policy(child: &ChildContext<'_>) -> ChildSpec {
if child.prompt().starts_with("triage:") {
ChildSpec::inherit()
.with_roster(ToolRoster::only(["read", SPAWN]))
.with_model(cheap_model())
.with_system("You are a triage gate. Answer yes or no.")
} else {
ChildSpec::inherit()
}
}
fn runtime(
workspace: &Path,
parent_turns: Vec<Turn>,
child_turns: Vec<Turn>,
policy: impl Fn(&ChildContext<'_>) -> ChildSpec + Send + Sync + 'static,
) -> (Runtime, Requests, Requests) {
let (parent, parent_asked) =
ScriptedProvider::new(BuiltinProvider::OpenAI, vec![parent_model()], parent_turns);
let (cheap, cheap_asked) =
ScriptedProvider::new(BuiltinProvider::Anthropic, vec![cheap_model()], child_turns);
let runtime = Runtime::builder()
.with_provider_instance(parent)
.with_provider_instance(cheap)
.with_store(VolatileRuntimeStore::new())
.with_policy(RuntimePolicy::workspace_bounded(workspace))
.with_file_tools(mentra::FileToolProfile::Split)
.with_tool_authorizer(ApprovalGate::new())
.with_tool(SpawnTool::new().with_child_policy(policy))
.build()
.expect("runtime builds");
(runtime, Requests(parent_asked), Requests(cheap_asked))
}
fn session(runtime: &Runtime, workspace: &Path) -> Session {
runtime
.create_session_with_config(
"test",
parent_model(),
AgentConfig {
tool_profile: ToolProfile::hide(["shell", "background_run", "task"]),
workspace: WorkspaceConfig {
base_dir: workspace.to_path_buf(),
..Default::default()
},
..Default::default()
},
)
.expect("session")
}
fn context() -> basis::ContextConfig {
basis::ContextConfig {
file_name: "AGENTS.md".to_string(),
global_dir: None,
walk_parents: false,
}
}
struct Run {
asked: Vec<ApprovalRequest>,
stopped_by: Option<Bound>,
total_tokens: u64,
}
async fn drive(workspace: &Path, runtime: &Runtime, options: TurnOptions) -> Run {
let session = session(runtime, workspace);
let seen = Arc::new(Mutex::new(Vec::new()));
let mut prepared = prepare_with_session(
session,
workspace,
"do the thing",
&context(),
"openai",
"parent-model",
)
.expect("prepared");
let report = tokio::time::timeout(
NOT_STUCK,
prepared.execute_with_approver_and_options(
CollectingSink::new(),
Recording {
seen: Arc::clone(&seen),
},
options,
),
)
.await
.expect("the run must not hang waiting on an unanswered approval")
.expect("the run completes");
let asked = seen.lock().expect("not poisoned").clone();
Run {
asked,
stopped_by: report.stopped_by,
total_tokens: report.usage.total_tokens(),
}
}
#[tokio::test]
async fn a_triage_child_runs_on_the_cheap_provider_with_the_narrow_roster() {
let workspace = tempfile::tempdir().expect("tempdir");
let (runtime, parent, cheap) = runtime(
workspace.path(),
vec![
Turn::calling("call-0", "triage: is this bug report real?"),
Turn::saying("parent done"),
],
vec![Turn::saying("yes, real")],
triage_policy,
);
drive(workspace.path(), &runtime, TurnOptions::default()).await;
assert_eq!(cheap.all().len(), 1, "the triage child asks once");
assert_eq!(parent.all().len(), 2, "the parent's rounds stay its own");
let child = cheap.nth(0);
assert_eq!(child.model, "cheap-model");
let mut tools = child.tools.clone();
tools.sort();
assert_eq!(tools, vec!["read".to_string(), SPAWN.to_string()]);
assert!(
child.system.contains("You are a triage gate."),
"{}",
child.system
);
assert!(
child.system.contains("subagent"),
"mentra's subagent instructions still apply to an overridden child: {}",
child.system
);
let parent_request = parent.nth(0);
assert_eq!(parent_request.model, "parent-model");
assert!(
!parent_request.system.contains("You are a triage gate."),
"the child's voice must not leak into the parent: {}",
parent_request.system
);
}
#[tokio::test]
async fn the_approver_reads_what_the_child_will_be() {
let workspace = tempfile::tempdir().expect("tempdir");
let (runtime, _parent, _cheap) = runtime(
workspace.path(),
vec![
Turn::calling("call-0", "triage: is this bug report real?"),
Turn::saying("parent done"),
],
vec![Turn::saying("yes, real")],
triage_policy,
);
let run = drive(workspace.path(), &runtime, TurnOptions::default()).await;
assert_eq!(run.asked.len(), 1, "one delegation, one question");
let input = &run.asked[0].input;
assert_eq!(input["mode"], "agent");
assert_eq!(
input["child"],
json!({
"model": { "id": "cheap-model", "provider": "anthropic" },
"roster": { "offered": ["read", SPAWN] },
"system": "replaced",
}),
"a remembered rule can match on what the child will be"
);
assert!(
!input.to_string().contains("triage gate"),
"the system prompt's text never travels in a preview: {input}"
);
}
#[tokio::test]
async fn a_policy_that_answers_inherit_changes_nothing_observable() {
let workspace = tempfile::tempdir().expect("tempdir");
let (runtime, parent, cheap) = runtime(
workspace.path(),
vec![
Turn::calling("call-0", "summarise the README"),
Turn::saying("child done"),
Turn::saying("parent done"),
],
Vec::new(),
triage_policy,
);
let run = drive(workspace.path(), &runtime, TurnOptions::default()).await;
assert_eq!(cheap.all().len(), 0, "no override, no cheap provider");
assert_eq!(
parent.all().len(),
3,
"parent round, inherited child round, parent round"
);
assert_eq!(parent.nth(1).model, "parent-model");
let input = &run.asked[0].input;
assert!(
input.get("child").is_none(),
"an inherited child leaves the preview byte-identical: {input}"
);
}
#[tokio::test]
async fn the_bounds_still_bind_a_child_the_policy_reshaped() {
let workspace = tempfile::tempdir().expect("tempdir");
let (runtime, parent, _cheap) = runtime(
workspace.path(),
vec![
Turn::calling("call-0", "triage: is this bug report real?").costing(10),
Turn::saying("parent done").costing(10),
],
vec![Turn::saying("yes, real").costing(200)],
triage_policy,
);
let run = drive(
workspace.path(),
&runtime,
TurnOptions::default().with_token_budget(100),
)
.await;
assert_eq!(
run.stopped_by,
Some(Bound::TokenBudget),
"what the reshaped child spent has to be what stops the parent"
);
assert_eq!(
parent.all().len(),
1,
"the parent's second round must never have been started"
);
assert_eq!(
run.total_tokens, 210,
"a run that stopped on 210 tokens must not report having spent 10"
);
}
#[tokio::test]
async fn a_narrowed_child_is_not_offered_a_siblings_tools() {
let sibling = tempfile::tempdir().expect("tempdir");
let mine = tempfile::tempdir().expect("tempdir");
let program = sibling.path().join("jenkins");
std::fs::write(&program, "#!/bin/sh\nprintf ok\n").expect("write program");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755))
.expect("make it executable");
}
std::fs::create_dir_all(sibling.path().join(".basis")).expect("dir");
let manifest = json!({
"schema": 1,
"tools": {
"jenkins_job": {
"description": "Trigger a job.",
"input_schema": {"type": "object", "properties": {}},
"command": [program],
},
},
});
std::fs::write(
sibling.path().join(".basis/tools.json"),
serde_json::to_vec(&manifest).expect("serialize manifest"),
)
.expect("write manifest");
let (provider, asked) = ScriptedProvider::new(
BuiltinProvider::OpenAI,
vec![parent_model()],
vec![
Turn::calling("call-0", "triage: is this real?"),
Turn::saying("child done"),
Turn::saying("parent done"),
],
);
let shared = Arc::new(
basis::Runtime::builder()
.with_provider_instance(provider)
.with_ephemeral_history()
.with_child_policy(|child: &ChildContext<'_>| {
if child.prompt().starts_with("triage:") {
ChildSpec::inherit().with_roster(ToolRoster::hide(["write"]))
} else {
ChildSpec::inherit()
}
})
.build()
.expect("builds offline"),
);
let _declaring = offline_workspace(sibling.path(), Arc::clone(&shared))
.open()
.await
.expect("the sibling opens and claims its tool");
let workspace = offline_workspace(mine.path(), shared)
.open()
.await
.expect("opens");
let report = workspace
.prepare(basis::RunSpec::new("do the thing"))
.expect("mints")
.execute_with_approver(CollectingSink::new(), AllowAll)
.await
.expect("the run completes");
drop(report);
let rosters: Vec<Vec<String>> = asked
.lock()
.expect("not poisoned")
.iter()
.map(|request| request.tools.clone())
.collect();
assert_eq!(rosters.len(), 3, "parent, child, parent");
for (round, roster) in rosters.iter().enumerate() {
assert!(
!roster.contains(&"jenkins_job".to_string()),
"round {round} was offered a sibling repository's tool: {roster:?}"
);
}
assert!(
rosters[1].contains(&SPAWN.to_string()),
"the narrowed child keeps everything its parent had: {:?}",
rosters[1]
);
assert!(
!rosters[1].contains(&"write".to_string()),
"and loses exactly what the policy hid: {:?}",
rosters[1]
);
}
#[cfg(feature = "mcp")]
#[tokio::test]
async fn a_narrowed_child_keeps_every_hide_its_parent_was_minted_with() {
const PROD_DB_QUERY: &str = "mcp__prod-db__query";
struct ProdDbQuery;
impl mentra::tool::ToolDefinition for ProdDbQuery {
fn descriptor(&self) -> mentra::tool::RuntimeToolDescriptor {
mentra::tool::RuntimeToolDescriptor::builder(PROD_DB_QUERY)
.description("query the production database")
.input_schema(json!({"type": "object"}))
.build()
}
}
#[async_trait]
impl mentra::tool::ToolExecutor for ProdDbQuery {
async fn execute(
&self,
_ctx: mentra::tool::ParallelToolContext,
_input: serde_json::Value,
) -> mentra::tool::ToolResult {
Ok("every row".to_string())
}
}
let root = tempfile::tempdir().expect("tempdir");
let (provider, asked) = ScriptedProvider::new(
BuiltinProvider::OpenAI,
vec![parent_model()],
vec![
Turn::calling("call-0", "triage: is this real?"),
Turn::saying("child done"),
Turn::saying("stranger done"),
Turn::calling("call-1", "triage: is this real?"),
Turn::saying("child done"),
Turn::saying("owner done"),
],
);
let shared = Arc::new(
basis::Runtime::builder()
.with_provider_instance(provider)
.with_ephemeral_history()
.with_tool(ProdDbQuery)
.with_child_policy(|child: &ChildContext<'_>| {
if child.prompt().starts_with("triage:") {
ChildSpec::inherit().with_roster(ToolRoster::hide(["write"]))
} else {
ChildSpec::inherit()
}
})
.build()
.expect("builds offline"),
);
let owner = offline_workspace(root.path(), Arc::clone(&shared))
.with_mcp(basis::McpConfig {
workspace_file: std::path::PathBuf::new(),
global_dir: None,
supplied: vec![basis::McpServer::Stdio(basis::McpServerConfig {
name: "prod-db".to_string(),
command: "basis-test-no-such-mcp-server".to_string(),
args: Vec::new(),
env: std::collections::HashMap::new(),
cwd: None,
})],
})
.open()
.await
.expect("opens even though the server does not come up");
assert_eq!(owner.mcp_servers(), ["prod-db"]);
let stranger = offline_workspace(root.path(), shared)
.with_mcp(basis::McpConfig {
workspace_file: std::path::PathBuf::new(),
global_dir: None,
supplied: Vec::new(),
})
.open()
.await
.expect("the same directory opens again");
assert!(stranger.mcp_servers().is_empty());
for workspace in [&stranger, &owner] {
let report = workspace
.prepare(basis::RunSpec::new("do the thing"))
.expect("mints")
.execute_with_approver(CollectingSink::new(), AllowAll)
.await
.expect("the run completes");
drop(report);
}
let rosters: Vec<Vec<String>> = asked
.lock()
.expect("not poisoned")
.iter()
.map(|request| request.tools.clone())
.collect();
assert_eq!(rosters.len(), 6, "two runs of parent, child, parent");
for (round, roster) in rosters[..3].iter().enumerate() {
assert!(
!roster.contains(&PROD_DB_QUERY.to_string()),
"round {round} of the open that configured no servers was offered one: {roster:?}"
);
}
assert!(
rosters[1].contains(&SPAWN.to_string()),
"the narrowed child keeps everything its parent had: {:?}",
rosters[1]
);
assert!(
!rosters[1].contains(&"write".to_string()),
"and loses exactly what the policy hid: {:?}",
rosters[1]
);
assert!(
rosters[4].contains(&PROD_DB_QUERY.to_string()),
"a narrowed child of the open that *did* configure `prod-db` still has it: {:?}",
rosters[4]
);
}
#[cfg(feature = "mcp")]
#[tokio::test]
async fn a_delegated_child_cannot_call_a_later_siblings_bridged_tool_either() {
const PROD_DB_QUERY: &str = "mcp__prod-db__query";
struct ProdDbQuery(Arc<std::sync::atomic::AtomicBool>);
impl mentra::tool::ToolDefinition for ProdDbQuery {
fn descriptor(&self) -> mentra::tool::RuntimeToolDescriptor {
mentra::tool::RuntimeToolDescriptor::builder(PROD_DB_QUERY)
.description("query the production database")
.input_schema(json!({"type": "object"}))
.build()
}
}
#[async_trait]
impl mentra::tool::ToolExecutor for ProdDbQuery {
async fn execute(
&self,
_ctx: mentra::tool::ParallelToolContext,
_input: serde_json::Value,
) -> mentra::tool::ToolResult {
self.0.store(true, std::sync::atomic::Ordering::SeqCst);
Ok("every row".to_string())
}
}
let root = tempfile::tempdir().expect("tempdir");
let (provider, asked) = ScriptedProvider::new(
BuiltinProvider::OpenAI,
vec![parent_model()],
vec![
Turn::calling("call-0", "triage: is this real?"),
Turn::calling_tool("call-1", PROD_DB_QUERY, json!({})),
Turn::saying("child done"),
Turn::saying("parent done"),
],
);
let shared = Arc::new(
basis::Runtime::builder()
.with_provider_instance(provider)
.with_ephemeral_history()
.with_child_policy(|child: &ChildContext<'_>| {
if child.prompt().starts_with("triage:") {
ChildSpec::inherit().with_roster(ToolRoster::hide(["write"]))
} else {
ChildSpec::inherit()
}
})
.build()
.expect("builds offline"),
);
let stranger = offline_workspace(root.path(), shared)
.with_mcp(basis::McpConfig {
workspace_file: std::path::PathBuf::new(),
global_dir: None,
supplied: Vec::new(),
})
.open()
.await
.expect("opens");
let mut run = stranger
.prepare(basis::RunSpec::new("do the thing"))
.expect("mints");
let ran = Arc::new(std::sync::atomic::AtomicBool::new(false));
let _bridged = stranger
.mentra_runtime()
.try_register_tool_for_audience(
mentra::tool::ToolAudience::new(basis::store::runtime_identifier(root.path())),
ProdDbQuery(Arc::clone(&ran)),
)
.expect("nothing answers to that name yet");
run.execute_with_approver(CollectingSink::new(), AllowAll)
.await
.expect("the run completes — a denial is an answer, not an error");
assert!(
!ran.load(std::sync::atomic::Ordering::SeqCst),
"a narrowed child reached a server its workspace never configured"
);
let rosters: Vec<Vec<String>> = asked
.lock()
.expect("not poisoned")
.iter()
.map(|request| request.tools.clone())
.collect();
assert_eq!(rosters.len(), 4, "parent, child, child, parent");
assert!(
rosters[1].contains(&PROD_DB_QUERY.to_string()),
"a mint that happened first cannot hide a name registered after it — which is \
exactly why the refusal has to come from somewhere else: {:?}",
rosters[1]
);
}
#[cfg(feature = "mcp")]
#[tokio::test]
async fn a_child_of_a_resumed_parent_inherits_the_hides_the_resume_computed() {
const PROD_DB_QUERY: &str = "mcp__prod-db__query";
struct ProdDbQuery;
impl mentra::tool::ToolDefinition for ProdDbQuery {
fn descriptor(&self) -> mentra::tool::RuntimeToolDescriptor {
mentra::tool::RuntimeToolDescriptor::builder(PROD_DB_QUERY)
.description("query the production database")
.input_schema(json!({"type": "object"}))
.build()
}
}
#[async_trait]
impl mentra::tool::ToolExecutor for ProdDbQuery {
async fn execute(
&self,
_ctx: mentra::tool::ParallelToolContext,
_input: serde_json::Value,
) -> mentra::tool::ToolResult {
Ok("every row".to_string())
}
}
let root = tempfile::tempdir().expect("tempdir");
let (provider, asked) = ScriptedProvider::new(
BuiltinProvider::OpenAI,
vec![parent_model()],
vec![
Turn::saying("minted"),
Turn::calling("call-0", "triage: is this real?"),
Turn::saying("child done"),
Turn::saying("parent done"),
],
);
let shared = Arc::new(
basis::Runtime::builder()
.with_provider_instance(provider)
.with_ephemeral_history()
.with_child_policy(|child: &ChildContext<'_>| {
if child.prompt().starts_with("triage:") {
ChildSpec::inherit().with_roster(ToolRoster::hide(["write"]))
} else {
ChildSpec::inherit()
}
})
.build()
.expect("builds offline"),
);
let stranger = offline_workspace(root.path(), shared)
.with_mcp(basis::McpConfig {
workspace_file: std::path::PathBuf::new(),
global_dir: None,
supplied: Vec::new(),
})
.open()
.await
.expect("opens");
let mut minted = stranger
.prepare(basis::RunSpec::new("do the thing"))
.expect("mints");
let agent_id = minted.agent_id().to_string();
minted
.execute_with_approver(CollectingSink::new(), AllowAll)
.await
.expect("the run completes");
drop(minted);
stranger
.mentra_runtime()
.try_register_tool(ProdDbQuery)
.expect("nothing answers to that name yet");
stranger
.resume(&agent_id, basis::RunSpec::new("keep going"))
.expect("its own workspace resumes it")
.execute_with_approver(CollectingSink::new(), AllowAll)
.await
.expect("the run completes");
let rosters: Vec<Vec<String>> = asked
.lock()
.expect("not poisoned")
.iter()
.map(|request| request.tools.clone())
.collect();
assert_eq!(
rosters.len(),
4,
"mint, resumed parent, child, resumed parent"
);
assert!(
!rosters[0].contains(&PROD_DB_QUERY.to_string()),
"nothing answered to that name when the conversation was minted: {:?}",
rosters[0]
);
assert!(
rosters[1].contains(&PROD_DB_QUERY.to_string()),
"a resume restates no tool profile onto the agent, so the parent keeps the roster \
its first mint froze — which is why the child's answer has to come from the \
ledger: {:?}",
rosters[1]
);
assert!(
!rosters[2].contains(&PROD_DB_QUERY.to_string()),
"the narrowed child of a resumed parent must be judged by what is foreign now, \
not by what the mint saw: {:?}",
rosters[2]
);
assert!(
rosters[2].contains(&SPAWN.to_string()) && !rosters[2].contains(&"write".to_string()),
"and it still keeps everything its parent had except what the policy hid: {:?}",
rosters[2]
);
}
fn offline_workspace(path: &Path, runtime: Arc<basis::Runtime>) -> basis::WorkspaceBuilder {
basis::Workspace::builder(path)
.with_runtime(runtime)
.with_model(basis::ModelSelector::Id("parent-model".to_string()))
.with_context(basis::ContextConfig {
file_name: "AGENTS.md".to_string(),
global_dir: None,
walk_parents: false,
})
.with_skills(basis::skills::SkillsConfig {
workspace_subdir: Some(std::path::PathBuf::from(".basis/skills")),
shared_workspace_dir: true,
global_dir: None,
shared_home_dir: false,
})
.with_templates(basis::templates::TemplatesConfig {
workspace_subdir: std::path::PathBuf::from(".basis/templates"),
global_dir: None,
})
.with_hooks(basis::hooks::HooksConfig {
workspace_file: std::path::PathBuf::from(".basis/hooks.json"),
global_dir: None,
supplied: Vec::new(),
})
.with_tools(basis::tools::declared::ToolsConfig {
workspace_file: std::path::PathBuf::from(".basis/tools.json"),
global_dir: None,
supplied: Vec::new(),
})
.with_memory(basis::MemoryConfig::disabled())
}
#[tokio::test]
async fn a_child_of_a_child_still_sees_one_door() {
let workspace = tempfile::tempdir().expect("tempdir");
let (runtime, _parent, cheap) = runtime(
workspace.path(),
vec![
Turn::calling("call-0", "triage: level one"),
Turn::saying("parent done"),
],
vec![
Turn::calling("call-1", "triage: level two"),
Turn::saying("grandchild: yes"),
Turn::saying("child: yes"),
],
triage_policy,
);
drive(workspace.path(), &runtime, TurnOptions::default()).await;
assert_eq!(
cheap.all().len(),
3,
"child round, grandchild round, child round — all on the cheap model"
);
let grandchild = cheap.nth(1);
assert_eq!(grandchild.model, "cheap-model");
assert!(
grandchild.tools.contains(&SPAWN.to_string()),
"the one door is still on the grandchild's roster: {:?}",
grandchild.tools
);
for replaced in ["shell", "background_run", "task"] {
assert!(
!grandchild.tools.contains(&replaced.to_string()),
"{replaced} came back at depth two: {:?}",
grandchild.tools
);
}
}