use pretty_assertions::assert_eq;
use rho_sdk::{tool::tool_progress_channel, CancellationToken};
use super::*;
use crate::agent::{
AgentId, AgentRuntimeSpec, ModelPolicy, ModelSelection, PromptPolicy, ToolPolicy,
};
fn context() -> ToolContext {
let (progress, _receiver) = tool_progress_channel(std::num::NonZeroUsize::MIN);
ToolContext::new( None, CancellationToken::new(), progress)
}
#[tokio::test]
async fn host_operations_require_the_complete_named_host_contract() {
for (server, remote_tools, connects) in [
(
"t3-code",
vec!["delegate_task", "task_status", "task_cancel"],
true,
),
(
"another-host",
vec!["delegate_task", "task_status", "task_cancel"],
false,
),
("t3-code", vec!["task_status", "task_cancel"], false),
("t3-code", vec!["delegate_task", "task_cancel"], false),
("t3-code", vec!["delegate_task", "task_status"], false),
("t3-code", vec![], false),
] {
let mut tools = AppToolSet::disabled();
tools.add_bundle(crate::tools::sdk_registry::StaticToolBundle::new(
remote_tools
.iter()
.map(|name| {
Arc::new(rho_sdk::tool::ScriptedTool::new(
rho_sdk::model::ToolSpec {
name: namespaced_tool_name(server, name),
description: String::new(),
input_schema: json!({"type": "object"}),
},
rho_sdk::tool::ScriptedToolOutcome::Success(ToolOutput::text("unused")),
)) as Arc<dyn Tool>
})
.collect(),
));
let orchestrator = HostOrchestrator::connect(&tools);
let id = || ToolCallId::from_string("host-call").unwrap();
let expected = if connects {
Ok(())
} else {
Err(ToolErrorKind::Execution)
};
for result in [
orchestrator.status(id(), "task-1", context()).await,
orchestrator
.cancel(id(), "task-1", None, context())
.await,
] {
assert_eq!(
result.map(drop).map_err(|error| error.kind()),
expected,
"{server} {remote_tools:?}"
);
}
}
}
#[test]
fn delegation_arguments_route_through_the_child_rebind() {
let definition = AgentDefinition {
id: AgentId::new("host-worker").unwrap(),
description: "Implementation".into(),
prompt: PromptPolicy::Extend("Use the definition's prompt".into()),
runtime: AgentRuntimeSpec::Rho {
tools: ToolPolicy::All,
model: ModelPolicy::Require(ModelSelection {
provider: Some("openai-codex".into()),
model: "gpt-6-luna".into(),
auth: None,
fast: false,
}),
reasoning: None,
},
};
let long_prompt = "x".repeat(513);
for prompt in [
"\n \nImplement the parser\nKeep public APIs stable",
long_prompt.as_str(),
] {
assert_eq!(
delegate_arguments(&definition, prompt, None),
json!({
"task": format!("/agent:host-worker {prompt}"),
"mode": "async",
})
);
}
assert_eq!(
delegate_arguments(
&definition,
"--model is literal task text",
None
),
json!({"task": "/agent:host-worker -- --model is literal task text", "mode": "async"}),
);
}