ask: rename ask_operator → ask + optional 'to' for agent-to-agent Q&A

This commit is contained in:
damocles 2026-05-17 12:10:49 +02:00
parent 87f8f8a123
commit 82b0877c47
21 changed files with 640 additions and 266 deletions

View file

@ -97,34 +97,7 @@ fn recv_timeout(wait_seconds: Option<u64>) -> std::time::Duration {
async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) -> AgentResponse {
let broker = &coord.broker;
match req {
AgentRequest::Send { to, body } => {
if let Err(message) = crate::limits::check_size("send", body) {
return AgentResponse::Err { message };
}
// Handle broadcast sends (recipient = "*")
if to == "*" {
let errors = coord.broadcast_send(agent, body);
if errors.is_empty() {
AgentResponse::Ok
} else {
AgentResponse::Err {
message: format!("broadcast failed for agents: {}", errors.join(", ")),
}
}
} else {
// Normal unicast send
match broker.send(&Message {
from: agent.to_owned(),
to: to.clone(),
body: body.clone(),
}) {
Ok(()) => AgentResponse::Ok,
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
},
}
}
}
AgentRequest::Send { to, body } => handle_send(coord, agent, to, body),
AgentRequest::Recv { wait_seconds } => match broker
.recv_blocking(agent, recv_timeout(*wait_seconds))
.await
@ -170,12 +143,32 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
message: format!("{e:#}"),
},
},
AgentRequest::AskOperator {
AgentRequest::Ask {
question,
options,
multi,
ttl_seconds,
} => handle_ask_operator(coord, agent, question, options, *multi, *ttl_seconds),
to,
} => crate::questions::handle_ask(
coord,
agent,
question,
options,
*multi,
*ttl_seconds,
to.as_deref(),
)
.map_or_else(
|message| AgentResponse::Err { message },
|id| AgentResponse::QuestionQueued { id },
),
AgentRequest::Answer { id, answer } => crate::questions::handle_answer(
coord, agent, *id, answer,
)
.map_or_else(
|message| AgentResponse::Err { message },
|()| AgentResponse::Ok,
),
AgentRequest::Remind {
message,
timing,
@ -184,36 +177,31 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
}
}
fn handle_ask_operator(
coord: &Arc<Coordinator>,
agent: &str,
question: &str,
options: &[String],
multi: bool,
ttl_seconds: Option<u64>,
) -> AgentResponse {
if let Err(message) = crate::limits::check_size("question", question) {
/// Common Send handler shared between dispatch arms. Applies the
/// 1 KiB body cap, then routes broadcast (`to == "*"`) vs unicast
/// through their respective broker calls. Pulled out of `dispatch`
/// to keep that function under the clippy too-many-lines limit; the
/// behaviour is identical to inlining.
fn handle_send(coord: &Arc<Coordinator>, agent: &str, to: &str, body: &str) -> AgentResponse {
if let Err(message) = crate::limits::check_size("send", body) {
return AgentResponse::Err { message };
}
let deadline_at = ttl_seconds.and_then(|s| {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.and_then(|d| i64::try_from(d.as_secs()).ok())
.unwrap_or(0);
i64::try_from(s).ok().map(|s| now + s)
});
match coord
.questions
.submit(agent, question, options, multi, deadline_at)
{
Ok(id) => {
tracing::info!(%id, %agent, ?deadline_at, "agent question queued");
if let Some(ttl) = ttl_seconds {
crate::manager_server::spawn_question_watchdog(coord, id, ttl);
if to == "*" {
let errors = coord.broadcast_send(agent, body);
return if errors.is_empty() {
AgentResponse::Ok
} else {
AgentResponse::Err {
message: format!("broadcast failed for agents: {}", errors.join(", ")),
}
AgentResponse::QuestionQueued { id }
}
};
}
match coord.broker.send(&Message {
from: agent.to_owned(),
to: to.to_owned(),
body: body.to_owned(),
}) {
Ok(()) => AgentResponse::Ok,
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
},