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

@ -221,11 +221,17 @@ pub enum AgentRequest {
/// Non-mutating — pulls from the broker without delivering. The
/// per-agent web UI uses this to render its own inbox section.
Recent { limit: u64 },
/// Surface a question to the operator on the dashboard. Same
/// shape as `ManagerRequest::AskOperator` — any agent can ask;
/// the answer routes back to the asker's inbox as a
/// `HelperEvent::OperatorAnswered`.
AskOperator {
/// Surface a question to either the operator or another agent.
/// `to = None` (or `Some("operator")`) routes the question to the
/// dashboard's operator-question queue (legacy `AskOperator`
/// behaviour). `to = Some(<agent>)` routes it to that agent's
/// inbox as a `HelperEvent::QuestionAsked` so the recipient can
/// answer back via `AgentRequest::Answer` (or
/// `ManagerRequest::Answer`); the answer threads back to the asker
/// as a `HelperEvent::QuestionAnswered` event. Either way the
/// response shape is `QuestionQueued { id }` — the asker uses the
/// id to correlate the asynchronous answer event.
Ask {
question: String,
#[serde(default)]
options: Vec<String>,
@ -233,7 +239,18 @@ pub enum AgentRequest {
multi: bool,
#[serde(default)]
ttl_seconds: Option<u64>,
/// Recipient of the question. `None` or `Some("operator")` =
/// the human operator (dashboard); `Some(<agent_name>)` = a
/// peer agent (their inbox).
#[serde(default)]
to: Option<String>,
},
/// Answer a question previously routed to this agent via
/// `HelperEvent::QuestionAsked`. The caller is implicitly the
/// answerer; only the question's `target` agent (or the operator,
/// via the dashboard) is authorised. Wires through to
/// `HelperEvent::QuestionAnswered` in the asker's inbox.
Answer { id: i64, answer: String },
/// Schedule a reminder message to be delivered to this agent at a
/// future time. The reminder lands in the agent's inbox as an auto-sent
/// message from `"reminder"`. Use for agent follow-ups (e.g. check task
@ -264,8 +281,8 @@ pub enum AgentResponse {
Status { unread: u64 },
/// `Recent` result: newest-first inbox rows.
Recent { rows: Vec<InboxRow> },
/// `AskOperator` result: the queued question id. The answer lands
/// later as `HelperEvent::OperatorAnswered` in this agent's inbox.
/// `Ask` result: the queued question id. The answer lands later
/// as `HelperEvent::QuestionAnswered` in this agent's inbox.
QuestionQueued { id: i64 },
}
@ -375,14 +392,32 @@ pub enum HelperEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
note: Option<String>,
},
/// The operator answered a question that was queued via
/// `AskOperator`. `id` matches the `QuestionQueued.id` returned to the
/// asker; `question` echoes the original prompt so the manager can
/// stitch the answer back to context across compactions.
OperatorAnswered {
/// A question queued via `Ask` was answered (by the operator via
/// the dashboard, or by another agent via `Answer`). `id` matches
/// the `QuestionQueued.id` returned to the asker; `question`
/// echoes the original prompt so the asker can stitch the answer
/// back to context across compactions; `answerer` is who answered
/// (`"operator"` or a peer agent name).
QuestionAnswered {
id: i64,
question: String,
answer: String,
answerer: String,
},
/// A peer (or the manager) asked this agent a question via
/// `Ask { to: Some(<this-agent>), ... }`. The recipient should
/// answer via `Answer { id, answer }` on their socket; the answer
/// will route back to the asker as a `QuestionAnswered` event.
/// `options` + `multi` mirror the original `Ask` args so the
/// answerer knows what shape of reply is expected.
QuestionAsked {
id: i64,
asker: String,
question: String,
#[serde(default)]
options: Vec<String>,
#[serde(default)]
multi: bool,
},
}
@ -452,9 +487,10 @@ pub enum ManagerRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
/// Ask the operator a question. Returns immediately with the queued
/// question id; the operator's answer arrives later as a
/// `HelperEvent::OperatorAnswered` in the manager inbox.
/// Surface a question to either the operator or another agent.
/// Mirrors `AgentRequest::Ask` exactly — see that doc for the
/// routing semantics (operator = dashboard queue; agent = the
/// peer's inbox via `HelperEvent::QuestionAsked`).
///
/// - `options` is advisory: empty = free-text only; non-empty = the
/// dashboard renders the choices alongside a free-text fallback
@ -464,9 +500,11 @@ pub enum ManagerRequest {
/// selections joined by ", ".
/// - `ttl_seconds`: optional auto-cancel after that many seconds. On
/// expiry the question is resolved with answer `[expired]` and the
/// manager gets the usual `OperatorAnswered` event. None = wait
/// forever for an operator answer (or manual cancel).
AskOperator {
/// asker gets the usual `QuestionAnswered` event. None = wait
/// forever for an answer (or manual cancel).
/// - `to`: recipient (None / `Some("operator")` = operator;
/// `Some(<agent>)` = peer agent).
Ask {
question: String,
#[serde(default)]
options: Vec<String>,
@ -474,7 +512,13 @@ pub enum ManagerRequest {
multi: bool,
#[serde(default)]
ttl_seconds: Option<u64>,
#[serde(default)]
to: Option<String>,
},
/// Answer a question previously routed to the manager via
/// `HelperEvent::QuestionAsked` (i.e. an agent asked the manager
/// for input). Mirror of `AgentRequest::Answer`.
Answer { id: i64, answer: String },
/// Fetch recent journal lines for a sub-agent container. hive-c0re
/// runs `journalctl -M <agent> -n <lines> --no-pager` and returns
/// the output as a string. Useful for diagnosing MCP registration
@ -514,9 +558,10 @@ pub enum ManagerResponse {
Status {
unread: u64,
},
/// Result of `AskOperator`: the queued question id. The actual answer
/// arrives later as a `HelperEvent::OperatorAnswered` in the manager
/// inbox, so this returns immediately rather than blocking the turn.
/// Result of `Ask`: the queued question id. The actual answer
/// arrives later as a `HelperEvent::QuestionAnswered` in the
/// asker's inbox, so this returns immediately rather than blocking
/// the turn.
QuestionQueued {
id: i64,
},