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:
parent
abfd2cce4b
commit
2770630f33
17 changed files with 426 additions and 79 deletions
|
|
@ -37,6 +37,7 @@ pub enum SocketReply {
|
|||
Message { from: String, body: String },
|
||||
Empty,
|
||||
Status(u64),
|
||||
QuestionQueued(i64),
|
||||
}
|
||||
|
||||
impl From<hive_sh4re::AgentResponse> for SocketReply {
|
||||
|
|
@ -59,6 +60,7 @@ impl From<hive_sh4re::ManagerResponse> for SocketReply {
|
|||
hive_sh4re::ManagerResponse::Message { from, body } => Self::Message { from, body },
|
||||
hive_sh4re::ManagerResponse::Empty => Self::Empty,
|
||||
hive_sh4re::ManagerResponse::Status { unread } => Self::Status(unread),
|
||||
hive_sh4re::ManagerResponse::QuestionQueued { id } => Self::QuestionQueued(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -209,6 +211,16 @@ pub struct KillArgs {
|
|||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct AskOperatorArgs {
|
||||
/// The question to surface on the dashboard.
|
||||
pub question: String,
|
||||
/// Optional fixed-choice answers. If empty, the dashboard renders a
|
||||
/// free-text input. Otherwise renders a select list of these options.
|
||||
#[serde(default)]
|
||||
pub options: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct RequestApplyCommitArgs {
|
||||
/// Agent whose config repo the commit lives in (use `"hm1nd"` for the
|
||||
|
|
@ -310,6 +322,36 @@ impl ManagerServer {
|
|||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Surface a question to the operator on the dashboard. Returns immediately \
|
||||
with a question id — do NOT wait inline. When the operator answers, a system message \
|
||||
with event `operator_answered { id, question, answer }` lands in your inbox; handle it \
|
||||
on a future turn. Use this when a decision needs human signal (ambiguous sub-agent \
|
||||
request, policy call, scope clarification). `options` is advisory: pass a short \
|
||||
fixed-choice list when applicable, otherwise leave empty for free text."
|
||||
)]
|
||||
async fn ask_operator(&self, Parameters(args): Parameters<AskOperatorArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("ask_operator", log, async move {
|
||||
let resp = self
|
||||
.dispatch(hive_sh4re::ManagerRequest::AskOperator {
|
||||
question: args.question,
|
||||
options: args.options,
|
||||
})
|
||||
.await;
|
||||
match resp {
|
||||
Ok(SocketReply::QuestionQueued(id)) => format!(
|
||||
"question queued (id={id}); operator's answer will arrive as a system \
|
||||
`operator_answered` event in your inbox"
|
||||
),
|
||||
Ok(SocketReply::Err(m)) => format!("ask_operator failed: {m}"),
|
||||
Ok(other) => format!("ask_operator unexpected response: {other:?}"),
|
||||
Err(e) => format!("ask_operator transport error: {e:#}"),
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Submit a config change for operator approval. Pass the agent name \
|
||||
(e.g. `alice` or `hm1nd` for the manager's own config) and a commit sha in that \
|
||||
|
|
@ -322,23 +364,19 @@ impl ManagerServer {
|
|||
let log = format!("{args:?}");
|
||||
let agent = args.agent.clone();
|
||||
let commit_ref = args.commit_ref.clone();
|
||||
run_tool_envelope(
|
||||
"request_apply_commit",
|
||||
log,
|
||||
async move {
|
||||
let resp = self
|
||||
.dispatch(hive_sh4re::ManagerRequest::RequestApplyCommit {
|
||||
agent: args.agent,
|
||||
commit_ref: args.commit_ref,
|
||||
})
|
||||
.await;
|
||||
format_ack(
|
||||
resp,
|
||||
"request_apply_commit",
|
||||
format!("apply approval queued for {agent} @ {commit_ref}"),
|
||||
)
|
||||
},
|
||||
)
|
||||
run_tool_envelope("request_apply_commit", log, async move {
|
||||
let resp = self
|
||||
.dispatch(hive_sh4re::ManagerRequest::RequestApplyCommit {
|
||||
agent: args.agent,
|
||||
commit_ref: args.commit_ref,
|
||||
})
|
||||
.await;
|
||||
format_ack(
|
||||
resp,
|
||||
"request_apply_commit",
|
||||
format!("apply approval queued for {agent} @ {commit_ref}"),
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
|
@ -348,7 +386,8 @@ impl ManagerServer {
|
|||
relay between them and the operator. Use `send` to talk to agents/operator, `recv` \
|
||||
to drain your inbox. Privileged: `request_spawn` (new agent, gated on operator \
|
||||
approval), `kill` (graceful stop), `request_apply_commit` (config change for \
|
||||
any agent including yourself). The manager's own config lives at \
|
||||
any agent including yourself), `ask_operator` (block on a human answer via the \
|
||||
dashboard). The manager's own config lives at \
|
||||
`/agents/hm1nd/config/agent.nix`."
|
||||
)]
|
||||
impl ServerHandler for ManagerServer {}
|
||||
|
|
@ -388,6 +427,7 @@ pub fn allowed_mcp_tools(flavor: Flavor) -> Vec<String> {
|
|||
"request_spawn",
|
||||
"kill",
|
||||
"request_apply_commit",
|
||||
"ask_operator",
|
||||
],
|
||||
};
|
||||
names
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue