diff --git a/docs/conventions.md b/docs/conventions.md index 383f84ea..7d380ade 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -108,6 +108,42 @@ each id in a per-recipient in-memory set so the next `Recv` can tag the row with `redelivered: true`. Idempotent + cheap when there's nothing in flight, so the at-boot fire is unconditional. +### Question routing (Ask / Answer) + +`AgentRequest::Ask` (and the manager-flavour mirror) surfaces a +structured question that either lands in the operator's dashboard +queue or in a peer agent's inbox. The recipient is the `to` field: + +- `to = None` or `to = Some("operator")` — routes to the + operator-question queue. The dashboard renders the question with + any `options` as a chip strip plus a free-text fallback (`Other…`) + so the operator is never trapped by an incomplete list. The + legacy `AskOperator` variant collapses into this case. +- `to = Some()` — peer Q&A. The target agent receives a + `HelperEvent::QuestionAsked { id, asker, question, options, multi }` + in their inbox. They reply via `AgentRequest::Answer` (or + `ManagerRequest::Answer` if they're the manager); the answer + threads back to the asker as a `HelperEvent::QuestionAnswered` + event. + +Shape fields are uniform across both targets: + +- `options` is advisory — the dashboard chips are decoration over a + free-text fallback; peer-agent recipients see the list in their + `QuestionAsked` event and can return any string. +- `multi = true` lets the answerer pick multiple options (checkboxes + in the dashboard, a hint in the peer-agent event). The answer + comes back as a single string with selections joined by `", "`. +- `ttl_seconds` auto-cancels with answer `[expired]` (and `answerer: + "ttl-watchdog"`) when the wait becomes moot. `None` = wait + indefinitely or until manual cancel. + +Response shape is always `QuestionQueued { id }` — the asker stores +the id and correlates the asynchronous answer event when it lands. +Authorisation on `Answer`: only the question's `target` agent (or +the operator via the dashboard) is permitted to reply; an answer +attempt from anyone else fails the wire-side check. + ### Loose-ends wire shape `LooseEnd` is the per-row response shape for `GetLooseEnds` (both diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index c970f4f7..2c5d95d0 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -337,15 +337,8 @@ pub enum AgentRequest { /// per-agent web UI uses this to render its own inbox section. Recent { limit: u64 }, /// 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()` 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. + /// Routing + shape: see + /// `docs/conventions.md::Question routing (Ask / Answer)`. Ask { question: String, #[serde(default)] @@ -354,17 +347,13 @@ pub enum AgentRequest { multi: bool, #[serde(default)] ttl_seconds: Option, - /// Recipient of the question. `None` or `Some("operator")` = - /// the human operator (dashboard); `Some()` = a - /// peer agent (their inbox). #[serde(default)] to: Option, }, /// 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. + /// `HelperEvent::QuestionAsked`. Authorised callers + threading + /// back via `HelperEvent::QuestionAnswered`: see + /// `docs/conventions.md::Question routing (Ask / Answer)`. 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 @@ -723,22 +712,8 @@ pub enum ManagerRequest { description: Option, }, /// 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 - /// ("Other…") so the operator is never trapped. - /// - `multi=true` lets the operator pick multiple options (rendered - /// as checkboxes). The answer is returned as a single string with - /// selections joined by ", ". - /// - `ttl_seconds`: optional auto-cancel after that many seconds. On - /// expiry the question is resolved with answer `[expired]` and the - /// asker gets the usual `QuestionAnswered` event. None = wait - /// forever for an answer (or manual cancel). - /// - `to`: recipient (None / `Some("operator")` = operator; - /// `Some()` = peer agent). + /// Manager-flavour mirror of `AgentRequest::Ask` — routing + shape + /// docs in `docs/conventions.md::Question routing (Ask / Answer)`. Ask { question: String, #[serde(default)] @@ -751,8 +726,8 @@ pub enum ManagerRequest { to: Option, }, /// Answer a question previously routed to the manager via - /// `HelperEvent::QuestionAsked` (i.e. an agent asked the manager - /// for input). Mirror of `AgentRequest::Answer`. + /// `HelperEvent::QuestionAsked`. Mirror of `AgentRequest::Answer`; + /// see `docs/conventions.md::Question routing (Ask / Answer)`. Answer { id: i64, answer: String }, /// Fetch recent journal lines for a sub-agent container. `agent` /// is the logical agent name; hive-c0re resolves it to the