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

@ -248,16 +248,25 @@ package legitimacy, cheaper alternative, blast radius) before
committing and calling `request_apply_commit`.
For ambiguous cases or anything that needs human signal, the
manager calls `ask_operator(question, options?, multi?,
ttl_seconds?)` — queues the question on the dashboard and returns
the id immediately. The operator's answer arrives later as
`HelperEvent::OperatorAnswered` in the manager inbox. Storage is
`hive-c0re::operator_questions` (sqlite); the answer flow is:
manager calls `ask(question, options?, multi?, ttl_seconds?, to?)`
queues the question and returns the id immediately. When `to` is
omitted (or `"operator"`) the question shows up on the dashboard;
when `to` is a sub-agent's name, the recipient receives a
`HelperEvent::QuestionAsked` and answers via their own `answer`
tool. Either way the answer arrives back as
`HelperEvent::QuestionAnswered { id, question, answer, answerer }`
in the asker's inbox. Storage is `hive-c0re::operator_questions`
(sqlite) — same table, with a nullable `target` column
(NULL = operator). Dispatch goes through
`hive-c0re/src/questions.rs::{handle_ask, handle_answer}` so both
the agent + manager surfaces stay aligned. The answer flow is:
```
POST /answer-question/{id}
→ OperatorQuestions::answer
→ notify_manager(OperatorAnswered { id, question, answer })
POST /answer-question/{id} agent: Answer { id, answer }
→ OperatorQuestions::answer(_, _, "operator") → questions::handle_answer
→ notify_agent(asker, QuestionAnswered { → OperatorQuestions::answer(_, _, agent)
answerer: "operator", ... }) → notify_agent(asker, QuestionAnswered {
answerer: agent, ... })
```
Two more paths resolve a pending question with a sentinel answer:
@ -301,9 +310,14 @@ regular claude turn so the manager can react. Variants
- `NeedsUpdate { agent }` — sub-agent's recorded flake rev is
stale. Manager calls `update(name)` to rebuild — idempotent,
no approval required.
- `OperatorAnswered { id, question, answer }` — dashboard
`/answer-question/{id}` after the operator submits the answer
form.
- `QuestionAnswered { id, question, answer, answerer }`
dashboard `/answer-question/{id}` (answerer = `"operator"`),
peer `Answer` request (answerer = agent name), or ttl watchdog
expiry (answerer = `"ttl-watchdog"`, answer = `"[expired]"`).
- `QuestionAsked { id, asker, question, options, multi }`
fired when an agent calls `Ask { to: Some(<this-agent>), ... }`.
The recipient responds via `Answer { id, answer }` and the
asker sees the matching `QuestionAnswered`.
To add a new event: new `HelperEvent` variant + call sites + update
`prompts/manager.md` so the manager knows the new shape.

View file

@ -12,10 +12,15 @@ Three tables, all in one file:
`sender / recipient / body / sent_at / delivered_at`.
- `approvals` — the queue. `agent / kind (apply_commit | spawn) /
commit_ref / requested_at / status / resolved_at / note`.
- `operator_questions``ask_operator` queue.
- `operator_questions``ask` / `answer` queue (despite the
file name, stores both operator-targeted + agent-to-agent
questions since the `ask` rename).
`asker / question / options_json / multi / asked_at /
deadline_at (ttl) / answered_at / answer`. Migrated via
`ALTER TABLE ADD COLUMN` against `pragma_table_info`.
deadline_at (ttl) / answered_at / answer / target`. `target IS
NULL` = operator path (dashboard); `target = '<agent>'` = peer
Q&A (`HelperEvent::QuestionAsked` pushed into target's inbox,
answered via `Answer` request). Migrated via `ALTER TABLE ADD
COLUMN` against `pragma_table_info`.
Retention:

View file

@ -107,10 +107,18 @@ it as a stdio child via `--mcp-config`. The hyperhive socket name is
"anything pending?" peek. Positive value parks the turn up
to that many seconds (cap 180) — incoming messages wake
instantly, otherwise returns empty at the timeout.
- `ask_operator(question, options?, multi?, ttl_seconds?)`
surface a question on the dashboard. Same shape as the manager's;
answer routes back to the asker's own inbox as
`HelperEvent::OperatorAnswered` via `coord.notify_agent`.
- `ask(question, options?, multi?, ttl_seconds?, to?)`
surface a structured question. Same shape as the manager's;
recipient defaults to the operator (dashboard) but can be set
to a peer agent name via `to: "<agent>"`. Answer routes back
to the asker's own inbox as `HelperEvent::QuestionAnswered`
via `coord.notify_agent`. For peer questions the recipient
sees a `HelperEvent::QuestionAsked` event and replies with
`answer(id, answer)`.
- `answer(id, answer)` — respond to a `question_asked` event
routed to this agent. Authorisation is strict: only the
declared target (or the operator via the dashboard) can
answer.
### Waking the agent from inside the container
@ -167,16 +175,22 @@ meta's.
- `request_apply_commit(agent, commit_ref)` — submit a config
change for any agent (`hm1nd` for the manager's own config) for
operator approval.
- `ask_operator(question, options?, multi?, ttl_seconds?)`
surface a question on the dashboard. Non-blocking — returns the
queued question id; the operator's answer arrives later as
`HelperEvent::OperatorAnswered` in the manager inbox. Options
always render alongside a free-text fallback; `multi=true`
renders options as checkboxes. `ttl_seconds` auto-cancels with
answer `[expired]` after the deadline (useful for time-sensitive
decisions that become moot if the operator hasn't responded).
The operator can also manually cancel with `[cancelled]` via the
dashboard.
- `ask(question, options?, multi?, ttl_seconds?, to?)`
surface a structured question to the operator (default) or a
sub-agent (`to: "<agent>"`). Non-blocking — returns the
queued question id; the answer arrives later as
`HelperEvent::QuestionAnswered { id, question, answer,
answerer }` in the asker's inbox. Options always render
alongside a free-text fallback; `multi=true` renders options
as checkboxes. `ttl_seconds` auto-cancels with answer
`[expired]` (and `answerer: "ttl-watchdog"`) after the
deadline (useful for time-sensitive decisions that become moot
if no one has responded). The operator can also manually
cancel with `[cancelled]` via the dashboard.
- `answer(id, answer)` — respond to a `question_asked` event
that was routed to the manager (a sub-agent did
`ask(to: "manager", ...)`). Surfaces in the asker's inbox as
the same `question_answered` event.
The boundary: lifecycle ops on *existing* sub-agents
(`kill`/`start`/`restart`) are at the manager's discretion — no

View file

@ -61,7 +61,9 @@ the previous process's socket release resolves itself.
age + claude-creds badge). Two actions: `⊕ R3V1V3` (queues a
Spawn approval; existing state is reused), `PURG3` (wipes
state + applied dirs; `POST /purge-tombstone/{name}`).
4. **M1ND H4S QU3STI0NS** — pending `ask_operator` questions
4. **M1ND H4S QU3STI0NS** — pending operator-targeted `ask`
questions, i.e. rows with `target IS NULL` (peer-to-peer
questions live in the same table but never surface here)
(amber pulsing border). Free-text fallback always rendered
alongside any option list; `multi=true` renders options as
checkboxes; submit merges selections + free text comma-joined.