use serde::Serialize;
use crate::cli::workspace::PolicyConfig;
fn rand_u64() -> u64 {
use std::hash::{BuildHasher, Hasher};
std::collections::hash_map::RandomState::new()
.build_hasher()
.finish()
}
#[derive(Debug, Serialize)]
pub struct DeliberationRequest {
pub room_id: String,
pub user_query: String,
pub deliberation_rounds: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_names: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub policy_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout_seconds: Option<u64>,
}
pub fn build_request_raw_policy_id(policy_id: &str, task: &str) -> DeliberationRequest {
let nonce: u64 = rand_u64();
let room_id = format!("adhoc_{nonce:016x}");
DeliberationRequest {
room_id,
user_query: task.to_string(),
deliberation_rounds: 3,
agent_names: None,
policy_id: Some(policy_id.to_string()),
effort: None,
scope: None,
timeout_seconds: None,
}
}
pub fn build_request(
room_name: &str,
policy: &PolicyConfig,
task: &str,
) -> Result<DeliberationRequest, String> {
let (agent_names, policy_id) = if let Some(agents) = &policy.agents {
if agents.len() < 2 {
return Err("policy must specify at least two agents for deliberation".to_string());
}
(Some(agents.clone()), None)
} else if policy.roles.is_some() {
let id = policy.policy_id();
(None, Some(id))
} else {
return Err("policy must specify either agents or roles".to_string());
};
let timeout_seconds = policy
.sla
.as_ref()
.and_then(|sla| sla.job_timeout())
.map(|d| d.as_secs());
let nonce: u64 = rand_u64();
let room_id = format!("{room_name}_{nonce:016x}");
Ok(DeliberationRequest {
room_id,
user_query: task.to_string(),
deliberation_rounds: policy.max_rounds,
agent_names,
policy_id,
effort: Some(policy.effort),
scope: None,
timeout_seconds,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::workspace::PolicyConfig;
use crate::scheduling::PolicySla;
fn static_policy() -> PolicyConfig {
PolicyConfig {
agents: Some(vec!["agent-a".into(), "agent-b".into()]),
roles: None,
max_rounds: 3,
effort: 0.85,
sla: None,
capabilities: None,
tags: None,
mode: Default::default(),
}
}
fn roles_policy() -> PolicyConfig {
PolicyConfig {
agents: None,
roles: Some(vec![crate::cli::workspace::RoleConfig {
role: "reviewer".into(),
count: 2,
capabilities: vec!["lang:rust".into()],
context: None,
pinned_agents: None,
moderator: false,
}]),
max_rounds: 3,
effort: 0.85,
sla: None,
capabilities: None,
tags: None,
mode: Default::default(),
}
}
#[test]
fn build_request_static_agents() {
let req = build_request("my-room", &static_policy(), "audit this code").unwrap();
assert!(
req.room_id.starts_with("my-room_"),
"room_id should be prefixed with room name, got: {}",
req.room_id
);
assert_eq!(req.user_query, "audit this code");
assert_eq!(req.deliberation_rounds, 3);
assert_eq!(
req.agent_names.as_deref(),
Some(&["agent-a".to_string(), "agent-b".to_string()][..])
);
assert!(req.policy_id.is_none(), "static should not send policy_id");
assert_eq!(req.effort, Some(0.85));
assert!(req.scope.is_none());
assert!(req.timeout_seconds.is_none());
}
#[test]
fn build_request_single_agent_rejected() {
let mut policy = static_policy();
policy.agents = Some(vec!["only-one".into()]);
let err = build_request("room", &policy, "task").unwrap_err();
assert!(
err.contains("at least two"),
"expected min-agents error, got: {err}"
);
}
#[test]
fn build_request_roles_sends_policy_id() {
let policy = roles_policy();
let req = build_request("room", &policy, "task").unwrap();
assert!(
req.agent_names.is_none(),
"role-based should not send agent_names"
);
assert!(req.policy_id.is_some(), "role-based should send policy_id");
assert_eq!(req.policy_id.unwrap(), policy.policy_id());
}
#[test]
fn build_request_effort_passthrough() {
let mut policy = static_policy();
policy.effort = 0.42;
let req = build_request("room", &policy, "task").unwrap();
assert_eq!(req.effort, Some(0.42));
}
#[test]
fn build_request_sla_maps_to_timeout() {
let mut policy = static_policy();
policy.sla = Some(PolicySla {
job_timeout_secs: 600,
response_sla_secs: None,
max_tokens: None,
});
let req = build_request("room", &policy, "task").unwrap();
assert_eq!(req.timeout_seconds, Some(600));
}
#[test]
fn build_request_timeout_does_not_scale_with_max_rounds() {
let sla = PolicySla {
job_timeout_secs: 300,
response_sla_secs: None,
max_tokens: None,
};
let mut policy_1 = static_policy();
policy_1.max_rounds = 1;
policy_1.sla = Some(sla.clone());
let mut policy_10 = static_policy();
policy_10.max_rounds = 10;
policy_10.sla = Some(sla);
let req_1 = build_request("r1", &policy_1, "task").unwrap();
let req_10 = build_request("r10", &policy_10, "task").unwrap();
assert_eq!(req_1.timeout_seconds, Some(300));
assert_eq!(req_10.timeout_seconds, Some(300));
assert_eq!(req_1.timeout_seconds, req_10.timeout_seconds);
}
#[test]
fn build_request_timeout_is_none_when_sla_job_timeout_is_zero() {
let mut policy = static_policy();
policy.sla = Some(PolicySla {
job_timeout_secs: 0,
response_sla_secs: None,
max_tokens: None,
});
let req = build_request("room", &policy, "task").unwrap();
assert_eq!(req.timeout_seconds, None);
}
#[test]
fn build_request_timeout_handles_u64_max_without_overflow() {
let mut policy = static_policy();
policy.sla = Some(PolicySla {
job_timeout_secs: u64::MAX,
response_sla_secs: None,
max_tokens: None,
});
let req = build_request("room", &policy, "task").unwrap();
assert_eq!(req.timeout_seconds, Some(u64::MAX));
}
#[test]
fn build_request_serializes_to_expected_json() {
let req = build_request("room", &static_policy(), "task").unwrap();
let json = serde_json::to_value(&req).unwrap();
assert!(json["room_id"].as_str().unwrap().starts_with("room_"));
assert_eq!(json["agent_names"][0], "agent-a");
assert!(json.get("effort").is_some(), "effort field must be present");
let effort_val = json["effort"].as_f64().unwrap();
assert!(
(effort_val - 0.85).abs() < 1e-6,
"effort should be ~0.85, got {effort_val}"
);
assert!(json.get("policy_id").is_none());
assert!(json.get("scope").is_none());
assert!(json.get("timeout_seconds").is_none());
}
#[test]
fn build_request_roles_serializes_policy_id() {
let req = build_request("room", &roles_policy(), "task").unwrap();
let json = serde_json::to_value(&req).unwrap();
assert!(
json.get("agent_names").is_none(),
"role-based should omit agent_names"
);
assert!(
json["policy_id"].is_string(),
"role-based should have policy_id"
);
}
#[test]
fn build_request_raw_policy_id_creates_adhoc_room() {
let hash = "a".repeat(64);
let req = build_request_raw_policy_id(&hash, "audit this");
assert!(
req.room_id.starts_with("adhoc_"),
"room_id should start with 'adhoc_', got: {}",
req.room_id
);
assert_eq!(req.user_query, "audit this");
assert_eq!(req.policy_id.as_deref(), Some(hash.as_str()));
assert!(req.agent_names.is_none());
assert_eq!(req.deliberation_rounds, 3);
assert!(req.effort.is_none());
}
#[test]
fn build_request_adhoc_named_policy() {
let policy = roles_policy();
let req = build_request("adhoc", &policy, "review this").unwrap();
assert!(req.room_id.starts_with("adhoc_"));
assert!(req.policy_id.is_some());
assert_eq!(req.policy_id.unwrap(), policy.policy_id());
}
}