ask_operator tool: non-blocking; operator answer arrives as helper event

new mcp tool on the manager surface that queues a question on the
dashboard and returns the question id immediately. operator submits an
answer via /answer-question/<id>; the dashboard fires
HelperEvent::OperatorAnswered { id, question, answer } into the manager
inbox so the next turn picks it up.

also: fix async-form button stuck on spinner after successful submit
(refreshState skipped re-rendering, so the button was never re-enabled).
This commit is contained in:
müde 2026-05-15 18:44:42 +02:00
parent abfd2cce4b
commit 2770630f33
17 changed files with 426 additions and 79 deletions

View file

@ -228,6 +228,15 @@ pub enum HelperEvent {
/// A sub-agent's container was torn down (container removed; state
/// dirs preserved per `destroy` semantics).
Destroyed { agent: 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 {
id: i64,
question: String,
answer: String,
},
}
/// Requests on the manager socket. Manager has the agent surface (send/recv)
@ -265,14 +274,35 @@ pub enum ManagerRequest {
agent: String,
commit_ref: String,
},
/// Ask the operator a question. The host-side handler blocks until the
/// operator answers via the dashboard; the answer is then returned as the
/// response. `options` is advisory: an empty list means free-text.
AskOperator {
question: String,
#[serde(default)]
options: Vec<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ManagerResponse {
Ok,
Err { message: String },
Message { from: String, body: String },
Err {
message: String,
},
Message {
from: String,
body: String,
},
Empty,
Status { unread: u64 },
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.
QuestionQueued {
id: i64,
},
}