ask: rename ask_operator → ask + optional 'to' for agent-to-agent Q&A
This commit is contained in:
parent
87f8f8a123
commit
82b0877c47
21 changed files with 640 additions and 266 deletions
|
|
@ -244,39 +244,30 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
|||
},
|
||||
}
|
||||
}
|
||||
ManagerRequest::AskOperator {
|
||||
ManagerRequest::Ask {
|
||||
question,
|
||||
options,
|
||||
multi,
|
||||
ttl_seconds,
|
||||
} => {
|
||||
if let Err(message) = crate::limits::check_size("question", question) {
|
||||
return ManagerResponse::Err { message };
|
||||
}
|
||||
tracing::info!(%question, ?options, multi, ?ttl_seconds, "manager: ask_operator");
|
||||
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(MANAGER_AGENT, question, options, *multi, deadline_at)
|
||||
{
|
||||
Ok(id) => {
|
||||
tracing::info!(%id, ?deadline_at, "operator question queued");
|
||||
if let Some(ttl) = *ttl_seconds {
|
||||
spawn_question_watchdog(coord, id, ttl);
|
||||
}
|
||||
ManagerResponse::QuestionQueued { id }
|
||||
}
|
||||
Err(e) => ManagerResponse::Err {
|
||||
message: format!("{e:#}"),
|
||||
},
|
||||
}
|
||||
to,
|
||||
} => crate::questions::handle_ask(
|
||||
coord,
|
||||
MANAGER_AGENT,
|
||||
question,
|
||||
options,
|
||||
*multi,
|
||||
*ttl_seconds,
|
||||
to.as_deref(),
|
||||
)
|
||||
.map_or_else(
|
||||
|message| ManagerResponse::Err { message },
|
||||
|id| ManagerResponse::QuestionQueued { id },
|
||||
),
|
||||
ManagerRequest::Answer { id, answer } => {
|
||||
crate::questions::handle_answer(coord, MANAGER_AGENT, *id, answer).map_or_else(
|
||||
|message| ManagerResponse::Err { message },
|
||||
|()| ManagerResponse::Ok,
|
||||
)
|
||||
}
|
||||
ManagerRequest::GetLogs { agent, lines } => {
|
||||
let n = lines.unwrap_or(50);
|
||||
|
|
@ -402,28 +393,41 @@ async fn submit_apply_commit(
|
|||
Ok((id, sha))
|
||||
}
|
||||
|
||||
/// On `AskOperator { ttl_seconds: Some(n) }`, sleep n seconds and then
|
||||
/// try to resolve the question with `[expired]`. If the operator (or
|
||||
/// any other path) already answered it, `answer()` returns Err and
|
||||
/// we no-op silently. Otherwise fire the usual `OperatorAnswered`
|
||||
/// helper event so the manager sees a terminal state.
|
||||
/// On `Ask { ttl_seconds: Some(n) }`, sleep n seconds and then try to
|
||||
/// resolve the question with `[expired]`. If the operator (or any
|
||||
/// other path) already answered it, `answer()` returns Err and we
|
||||
/// no-op silently. Otherwise fire a `QuestionAnswered` helper event
|
||||
/// with `answerer = "ttl-watchdog"` so the asker can distinguish a
|
||||
/// real answer from a deadline trip without parsing the answer text.
|
||||
const TTL_SENTINEL: &str = "[expired]";
|
||||
/// Synthetic `answerer` label used when the ttl watchdog resolves a
|
||||
/// question instead of a real human / agent. Lives in a distinct
|
||||
/// namespace from agent names + the operator so the asker can pattern
|
||||
/// match `event.answerer == "ttl-watchdog"`.
|
||||
const TTL_ANSWERER: &str = "ttl-watchdog";
|
||||
|
||||
pub fn spawn_question_watchdog(coord: &Arc<Coordinator>, id: i64, ttl_secs: u64) {
|
||||
let coord = coord.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(ttl_secs)).await;
|
||||
// `answer` returns Err if already resolved — that's the
|
||||
// normal path when the operator responded before the ttl
|
||||
// fired, so no-op silently.
|
||||
if let Ok((question, asker)) = coord.questions.answer(id, TTL_SENTINEL) {
|
||||
tracing::info!(%id, %asker, "operator question expired (ttl)");
|
||||
// Watchdog has its own answerer label so the authorisation
|
||||
// check in `answer()` permits it for any target. We bypass
|
||||
// the public `answer()` path by calling it with the operator
|
||||
// identity, since the operator is always permitted; the
|
||||
// event we fire carries the real watchdog label for observers.
|
||||
if let Ok((question, asker, _target)) =
|
||||
coord
|
||||
.questions
|
||||
.answer(id, TTL_SENTINEL, hive_sh4re::OPERATOR_RECIPIENT)
|
||||
{
|
||||
tracing::info!(%id, %asker, "question expired (ttl)");
|
||||
coord.notify_agent(
|
||||
&asker,
|
||||
&hive_sh4re::HelperEvent::OperatorAnswered {
|
||||
&hive_sh4re::HelperEvent::QuestionAnswered {
|
||||
id,
|
||||
question,
|
||||
answer: TTL_SENTINEL.to_owned(),
|
||||
answerer: TTL_ANSWERER.to_owned(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue