diff --git a/CLAUDE.md b/CLAUDE.md index 9b29051f..d1a69727 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,7 +32,7 @@ hand-maintained per-file tree drifts out of sync with the code. user). `src/main.rs` is the `hive-c0re` binary — **daemon-only** (`serve` + the periodic vacuum/sweep loops); the operator CLI lives in the separate `hivectl` crate, which talks to the daemon over the host - admin socket. Owns the sqlite broker, approval + reminder + + admin socket. Owns the sqlite broker, approval + question + reminder + schedule queues, the meta flake, lifecycle (`nixos-container` shellouts), gateway / forge / matrix provisioning, per-container stats, and the axum operator dashboard (`dashboard.rs`). Largest crate. @@ -120,7 +120,7 @@ hand-maintained per-file tree drifts out of sync with the code. `forge_notify` are the built-in ones, but any user-configured MCP server can push todos here too, nothing restricts the `subsystem` set. Carries the loose-ends-v2 **todo** ops plus - harness-local reminders. ⚠️ Distinct from + harness-local reminders + questions. ⚠️ Distinct from `hive-core-agent-sock` above: **this socket never leaves the container** and `hive-c0re` is not in the path at all — no broker round-trip, no long-poll, no marker files. diff --git a/README.md b/README.md index 8b5b5766..f84d9096 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ durable identity, and stop them from eating production. hyperhive is the substrate. - identity = unix socket -- communication = sqlite-backed broker (`send` / `recv` / `remind`) +- communication = sqlite-backed broker (`send` / `recv` / `ask` / + `answer` / `remind`) - config = git (manager proposes, operator approves, deploys land as tagged commits) - blast radius = container diff --git a/docs/agent-hierarchy.md b/docs/agent-hierarchy.md index ff08bcf2..c54b1ca4 100644 --- a/docs/agent-hierarchy.md +++ b/docs/agent-hierarchy.md @@ -94,7 +94,7 @@ umount-old / mount-new / restart-cascade step. | `request_init_config` (spawn a new child) | any agent, child added under self | | config change via forge PR (any descendant's config) | any ancestor | | `get_logs` (any descendant) | any ancestor | -| moderate reminders (cancel any open thread of a descendant) | any ancestor | +| moderate questions / reminders (cancel any open thread of a descendant) | any ancestor | | `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else | | `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) | @@ -149,17 +149,16 @@ other agents don't: - **Scattered ownership checks** — a handful of independent manager-only overrides exist across `hive-c0re` today: loose-ends visibility (manager sees hive-wide, sub-agents only their own), - `destroy` refusing to act on the manager, and crash-watch skipping - the manager (it auto-restarts via systemd instead of going through - the crash-watch loop). Each is planned to become an + "manager can cancel any question/reminder" overrides on the owner + check, `destroy` refusing to act on the manager, and crash-watch + skipping the manager (it auto-restarts via systemd instead of going + through the crash-watch loop). Each is planned to become an ancestor/descendant check instead of a manager-name check — see the - module docs for `loose_ends.rs`, `stores/broker.rs`, `actions.rs`, - and `workers/crash_watch.rs` for the current owner-check logic in - each. (Question/answer routing and its own manager-override cancel - path — formerly `hive-c0re/src/questions.rs` and - `stores/operator_questions.rs` — has been removed entirely; reminder - cancellation is now handled fully in-agent, see the note on - `CancelLooseEndKind::Reminder` in `hive-c0re/src/socket_server/mod.rs`.) + module docs for `loose_ends.rs`, `stores/operator_questions.rs`, + `stores/broker.rs`, `actions.rs`, and `workers/crash_watch.rs` for + the current owner-check logic in each. (Reminder cancellation has + since moved fully in-agent — see the note on + `CancelLooseEndKind::Reminder` in `hive-c0re/src/questions.rs`.) None of the above is a stable interface — treat the module doc comments as the source of truth for exactly which checks exist today. diff --git a/docs/approvals.md b/docs/approvals.md index 5ed06c2c..88f7c63b 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -598,6 +598,12 @@ renders only for the root agent. The root agent's approval-gating behaviour comes from its CLAUDE.md / agent-specific instructions, not the system prompt template. +Any agent (root or not) can also ask a structured question of the +operator or a peer agent via the `ask`/`answer` MCP tools, independent +of the approval flow above — see +`docs/conventions.md#question-routing-ask--answer` for the routing +rules, `ttl_seconds` expiry, and cancellation. + ## Helper events to the submitting agent `Coordinator::notify_submitter(approval_id, &HelperEvent)` routes the @@ -629,6 +635,17 @@ root agent. Variants (`hive_sh4re::manager::HelperEvent`): - `NeedsUpdate { agent }` — sub-agent's recorded flake rev is stale. The root agent calls `update(name)` to rebuild — idempotent, no approval required. +- `QuestionAnswered { id, question, answer, answerer }` — peer + `Answer` request (answerer = agent name) or ttl watchdog expiry + (answerer = `"ttl-watchdog"`, answer = `"[expired]"`). The dashboard's + own `/answer-question/{id}` trigger (answerer = `"operator"`) is + gone — removed along with the rest of the dashboard's ask/answer + surface, so an operator-targeted question now has no path to an + answer at all. +- `QuestionAsked { id, asker, question, options, multi }` — + fired when an agent calls `Ask { to: Some(), ... }`. + The recipient responds via `Answer { id, answer }` and the + asker sees the matching `QuestionAnswered`. The remaining lower-urgency lifecycle notices — `Rebuilt`, `Killed`, `Destroyed`, `NeedsLogin`, `LoggedIn`, `ConfigReady` — are "FYI, check @@ -651,11 +668,11 @@ or the dashboard `↻ R3BU1LD` button when the lock didn't move). When set, bootstrap container yields the exact tree that was referenced. To add a new lifecycle notice: if it needs to drive an immediate turn -(something genuinely urgent, like `ContainerCrash`), add a -`HelperEvent` variant + call sites + update `prompts/system.md`'s -message-event list. If it's "FYI, check when convenient," call -`push_todo`/`push_todo_submitter` directly instead — no new wire type -needed. +(a live conversation like `Ask`/`Answer`, or something genuinely +urgent), add a `HelperEvent` variant + call sites + update +`prompts/system.md`'s message-event list. If it's "FYI, check when +convenient," call `push_todo`/`push_todo_submitter` directly instead — +no new wire type needed. ## Auto-update on startup diff --git a/docs/conventions.md b/docs/conventions.md index 7b06bcc2..472c6075 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -171,6 +171,46 @@ cutoff so a later `AckTurn` doesn't double-update and a stale redelivery tag can't outlive its row. The operator-side sibling is the dashboard's "mark all read" (unbounded, per-agent). +### Question routing (Ask / Answer) + +`AgentRequest::Ask` (and the manager-flavour mirror) surfaces a +structured question that either lands in the operator-question 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's whole ask/answer + surface (the UI that used to render this queue and let the operator + reply) has been removed — an operator-targeted question currently + has no reader anywhere; this routing case is being removed too as + the same effort continues. 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 — a list is never enforced; peer-agent + recipients see it in their `QuestionAsked` event and can return any + string regardless. +- `multi = true` is a hint that the answerer should treat `options` as + a multi-select rather than pick-one. 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, is permitted to reply; an answer attempt from anyone else +fails the wire-side check. (The operator's only path to invoke this +was the now-removed dashboard — see the note above; the +authorisation rule itself is unchanged.) + ### Loose-ends wire shape `LooseEnd` is the per-row response shape for `GetLooseEnds` (both @@ -180,12 +220,15 @@ privileged bot, etc.) can land later without breaking existing handlers. Each row carries enough context that the caller renders it directly as a bulleted list, no follow-up fetch needed. -Per-flavour scoping is uniform across the two cancellable variants: +Per-flavour scoping is uniform across the three variants: - **agent-flavour** `GetLooseEnds` only surfaces rows the calling agent has standing in. `Approval` rows only appear when the calling agent is the manager (sub-agents don't submit - approvals). `Reminder` rows are scoped to `owner == self`. + approvals). `Question` rows surface where the agent is `asker` + OR `target` (the routing semantics from the Ask/Answer + subsection above). `Reminder` rows are scoped to `owner == + self`. - **manager-flavour** `GetLooseEnds` lists every pending row in the swarm — full audit view. @@ -197,6 +240,9 @@ Per-variant fields: blurb shown on the dashboard card. `commit_ref` is the kind-specific payload (see `docs/approvals.md::Approval kinds (wire shapes)`). +- `Question { id, asker, target?, question, age_seconds }` — + `target = None` = operator-routed (dashboard); `Some(agent)` = + peer-to-peer thread. - `Reminder { id, owner, message, due_at, age_seconds }` — `due_at` is the absolute time the scheduler is targeting (RFC 3339 on the wire, see *Timestamps on the wire* below); clients @@ -219,9 +265,10 @@ unsynchronised wall clock, etc.) so the bulleted list never shows nonsense ages. `CancelLooseEnd { kind, id }` is the matching write surface. The -`kind` enum (`Reminder` / `Approval`) selects which underlying store -the dispatcher reaches into. `Reminder` cancels from either surface -subject to an ownership check (the scheduling agent). +`kind` enum (`Question` / `Reminder` / `Approval`) selects which +underlying store the dispatcher reaches into. `Question` and +`Reminder` cancel from either surface subject to ownership +checks (asker for the question, scheduler for the reminder). `Approval` is manager-only — sub-agents don't submit approvals so they have nothing of their own to withdraw; their wire surface returns a clear error if they try. Cancelling an approval diff --git a/docs/persistence.md b/docs/persistence.md index b7f3ae00..e0e65ba9 100644 --- a/docs/persistence.md +++ b/docs/persistence.md @@ -20,8 +20,8 @@ keeps its state, purging it doesn't.** Beyond that: -- **Approvals are kept forever** — they're an audit trail, not a - cache. Nothing about them ever ages out. +- **Approvals and questions are kept forever** — they're audit trails, + not caches. Nothing about them ever ages out. - **Broker messages**: acked ones vacuum after 30 days; anything undelivered or delivered-but-not-yet-acked is always kept, however old. @@ -55,11 +55,20 @@ power-intent registry: ⚠️ The `mcp__hyperhive__remind` queue is **not** here any more: it moved to a harness-local, per-agent store as part of the loose-ends-v2 migration — see [`/harness/` contents - below](#state-dirs-per-agent) for where reminders (and todos) - actually live now. + below](#state-dirs-per-agent) for where reminders (and todos, and + the questions mirror) actually live now. - `approvals` — the queue. `agent / kind (merge_config_pr | spawn | init_config | update_meta_inputs | schedule_prompt) / commit_ref / requested_at / status / resolved_at / note`. +- `operator_questions` — `ask` / `answer` queue (despite the + table 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 / target`. `target IS + NULL` = operator path (dashboard); `target = ''` = peer + Q&A (`HelperEvent::QuestionAsked` pushed into target's inbox, + answered via `Answer` request). Migrated via `ALTER TABLE ADD + COLUMN` against `pragma_table_info`. - `scheduled_prompts` — recurring + one-shot prompt queue. `owner / body / interval_seconds (NULL = one-shot) / next_fire_at_unix / created_at_unix / source ("operator" or @@ -89,8 +98,9 @@ Retention: rows are always kept — the harness `ack_turn`s only after a successful turn, so an unacked row can still be requeued via `requeue_inflight` on a crash. -- Approvals are kept indefinitely — an audit trail. `actions::destroy` - rows stay visible to anything that queries by id. +- Approvals and questions are kept indefinitely — both are + audit trails. `actions::destroy` and answered questions stay + visible to anything that queries by id. - Scheduled prompts: one-shot rows are deleted on fire by the worker; recurring rows live until the operator cancels them (`cancel_schedule` MCP / dashboard ✗) which tombstones via @@ -291,9 +301,9 @@ Under `/var/lib/hyperhive/agents//`: hours; non-terminal (still-running) tasks are never deleted. This used to be a host-side `hive-c0re` vacuum, moved in-container for the same privsep-ownership reason as the events vacuum above. - - `hyperhive-state.sqlite` — consolidated loose-ends-v2 store: todos - and reminders, one small table each in a single file (in-container - daemons — `hive-bash-daemon`, `hive-matrix-daemon`, + - `hyperhive-state.sqlite` — consolidated loose-ends-v2 store: todos, + reminders, and a questions mirror, one small table each in a single + file (in-container daemons — `hive-bash-daemon`, `hive-matrix-daemon`, `hive-forge-notify` — upsert keyed todos here over the harness's in-agent socket, `HIVE_AGENT_SOCKET`; the harness merges them into `get_loose_ends` output and clears a row on `mark_todo_done`). diff --git a/docs/terminal-rendering.md b/docs/terminal-rendering.md index d8d43be4..f1da4c63 100644 --- a/docs/terminal-rendering.md +++ b/docs/terminal-rendering.md @@ -180,6 +180,8 @@ tools (Read, Write, etc.) keep their name as-is. | **Core hyperhive** | | | `send*` | rich renderer: `send* → to · NL` (default-open body) | | `recv*` | `recv*()` · `recv* wait Ns` · `recv* max N` | +| `ask*` | rich renderer: `ask* → to` (no inline answer form — see [Inline ask-operator answer](#inline-ask-operator-answer)) | +| `answer*` | rich renderer: `answer* #id` | | `remind*` | `remind* +Xm "preview"` or `remind* at HH:MMZ "preview"` | | `set_status*` | `set_status* "text"` | | `get_loose_ends*` | `get_loose_ends*()` or `get_loose_ends* [agent]` | @@ -240,6 +242,15 @@ This keeps less-frequent tools that don't have a specific `fmt_tool_use` case from dumping raw JSON. Common matrix and hyperhive tools have their own cases and skip this path. +## Inline ask-operator answer + +An `mcp__hyperhive__ask(to: "operator", ...)` row has no inline +answer form in this terminal — it renders like any other tool call. +The dashboard's own question-surfacing UI that used to let the +operator answer from the Y3R C4LL tab is gone (hyperhive#3721); there +is currently no UI path to answer an operator-targeted `ask()` at +all. + ## Dashboard side (not covered here) The main dashboard's message-flow pane is a different diff --git a/docs/turn-loop/mcp.md b/docs/turn-loop/mcp.md index 42026548..35d8b3bb 100644 --- a/docs/turn-loop/mcp.md +++ b/docs/turn-loop/mcp.md @@ -23,7 +23,8 @@ preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and ## Core tools (always available) **Messaging** (`messaging` group): `send(to, body, in_reply_to?)`, -`recv(max?)`, `ack_until(up_to)`. +`recv(max?)`, `ask(question, options?, multi?, +ttl_seconds?, to?)`, `answer(id, answer)`, `ack_until(up_to)`. - `send` — message a peer (logical name) or the operator (`to: "operator"`). Use `to: ""` to address the topology @@ -47,6 +48,13 @@ preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and other turn: if the context crossed the watermark the harness runs a notes-checkpoint turn and then `/compact`. Compacting before shutdown keeps a later cold start cheap instead of re-uploading a huge transcript. +- `ask` — surface a structured question to the operator (default) or + a peer agent (`to: ""`). Non-blocking — returns a question + id; the answer arrives as a `question_answered` system event in the + asker's inbox. `options` is advisory; `multi=true` renders as + checkboxes; `ttl_seconds` auto-cancels with answer `[expired]`. +- `answer` — respond to a `question_asked` event routed to this + agent. Strict authorisation: only the declared target can answer. - `ack_until(up_to)` — bulk-mark inbox rows handled: every row with broker id `<= up_to` is stamped as acked in a single UPDATE. Recipient-scoped (agents can only ack their own rows). Use when a @@ -56,12 +64,14 @@ preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and Transient pings (sentinel id 0) have nothing to ack and show no marker. **System messages** (from sender `system`): the higher-urgency -lifecycle events (`hive_sh4re::manager::HelperEvent`) are +lifecycle + Q&A events (`hive_sh4re::manager::HelperEvent`) are delivered as regular inbox messages (same `recv` path; body is a JSON object with an `event` discriminant field). The **submitting agent** (the root agent for top-level containers; an agent with the `approvals` tool group for its own subtree) receives `container_crash`, -`needs_update`, and `approval_resolved` this way. The remaining, lower-urgency lifecycle +`needs_update`, and `approval_resolved` this way. Any agent receives +Q&A events when it is the declared target (`question_asked`) or the +asker (`question_answered`). The remaining, lower-urgency lifecycle notices — `spawned`, `rebuilt`, `killed`, `destroyed`, `needs_login`, `logged_in`, `config_ready` — skip the inbox entirely: they land as todos on the submitting agent's in-container todo socket instead @@ -76,19 +86,18 @@ payload shapes and routing logic in `cancel_loose_end(kind, id)`, `remind(message, delay_seconds? | at_unix_timestamp?)`. -- `get_loose_ends(agent?)` — list scheduled reminders, pending - approvals you submitted, and active local tasks published by - external MCP daemons (e.g. running bash tasks from - `hive-bash-daemon`). Each row carries an id + kind for - `cancel_loose_end`. Omit `agent` to list your own threads. Pass - `agent: ""` to inspect a direct child agent (always accessible - per topology enforcement); non-children require the - `query_agent_state` capability. The `"*"` hive-wide query is not - available on the agent socket. -- `cancel_loose_end` — hard-delete a `reminder`, cancel a pending - `approval` row, or clear a `todo` row (loose-ends-v2). Agents may - only cancel rows they own; the `approval` kind is further restricted - to the root agent (`ruth`) server-side. +- `get_loose_ends(agent?)` — list pending questions (asked/owed), + scheduled reminders, and active local tasks published by external MCP + daemons (e.g. running bash tasks from `hive-bash-daemon`). Each row + carries an id + kind for `cancel_loose_end`. Omit `agent` to list + your own threads. Pass `agent: ""` to inspect a direct child + agent (always accessible per topology enforcement); non-children + require the `query_agent_state` capability. The `"*"` hive-wide + query is not available on the agent socket. +- `cancel_loose_end` — withdraw a `question` (posts `[cancelled by + ]`), hard-delete a `reminder`, or cancel a pending `approval` + row. Agents may only cancel rows they own; the `approval` kind is + further restricted to the root agent (`ruth`) server-side. - `remind` — schedule a reminder in this agent's own inbox. Large payloads spill to `/agents//state/reminders/`. Pending count capped at 50 per agent (`HIVE_REMIND_MAX_PENDING_PER_AGENT`). diff --git a/docs/web-ui/agent.md b/docs/web-ui/agent.md index 6d802daa..c7654775 100644 --- a/docs/web-ui/agent.md +++ b/docs/web-ui/agent.md @@ -170,11 +170,14 @@ from the harness-local store (same effect as `cancel_loose_end(kind: "todo")`, just from the web UI instead of the agent's own tool calls). The todos flyout is the only per-agent flyout — there is no separate -"loose-ends" or "tasks" list. There used to be a note here about the -`ask`/`answer` MCP tools having no inline answer form in this -terminal — that whole mechanism (the tools, the dashboard's questions -pane, the wire protocol) has since been removed entirely, so there's -nothing left to render a form for. +"loose-ends" or "tasks" list. There is also no inline answer form for +`ask` tool calls in this terminal: an `ask` renders like any other +tool call (see +[`terminal-rendering.md`](../terminal-rendering.md#inline-ask-operator-answer)). +The dashboard's own Y3R C4LL questions pane that used to let the +operator answer from there is gone (hyperhive#3721) — an +operator-targeted `ask()` currently has no UI path to an answer at +all until the removal finishes or a replacement lands. ## Live view diff --git a/hive-agent-mcp/README.md b/hive-agent-mcp/README.md index 171992cc..482d9fba 100644 --- a/hive-agent-mcp/README.md +++ b/hive-agent-mcp/README.md @@ -9,7 +9,7 @@ would hit. HTTP is the sole transport; there is no stdio mode here. ## When to use it This is where the core hyperhive tool surface lives: `send`, `recv`, -`remind`, `get_loose_ends`, `set_status`, +`ask`/`answer`, `remind`, `get_loose_ends`, `set_status`, `get_agent_meta`, lifecycle (`kill`/`start`/`restart`/`update` on direct children), scheduling, and the approval-request tools. Reach for this crate when you're adding or changing a built-in tool rather @@ -25,7 +25,7 @@ or their own daemon instead of living here. in-agent socket the `hive-agent` harness serves. - **`client.rs`** — socket client to the hyperhive broker. - **`send_allow.rs`** — enforces the per-agent - `hyperhive.allowedRecipients` allow-list on `send`. + `hyperhive.allowedRecipients` allow-list on `send`/`ask`. - **`paths.rs`** — socket + state path resolution shared with the harness's own `paths.rs` conventions. diff --git a/hive-agent-mcp/src/mcp/args.rs b/hive-agent-mcp/src/mcp/args.rs index 5a68820c..f64ac080 100644 --- a/hive-agent-mcp/src/mcp/args.rs +++ b/hive-agent-mcp/src/mcp/args.rs @@ -159,13 +159,14 @@ pub struct UpdateArgs { #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct CancelLooseEndArgs { - /// Which kind of thread to cancel — `"reminder"` for a scheduled - /// `remind` that hasn't fired yet, `"approval"` for a pending - /// approval you submitted, or `"todo"` for a loose-ends-v2 todo - /// (bash/matrix/forge). Use the `kind` field straight off the - /// `get_loose_ends` row. + /// Which kind of thread to cancel — `"question"` for an open + /// `ask` that's still waiting on an answer, `"reminder"` for a + /// scheduled `remind` that hasn't fired yet, or `"todo"` for a + /// loose-ends-v2 todo (bash/matrix/forge). Use the `kind` + /// field straight off the `get_loose_ends` row. pub kind: String, - /// Row id from the matching `get_loose_ends` entry. + /// Row id from the matching `get_loose_ends` entry (or the + /// `question_queued` reply when you submitted it). pub id: i64, } diff --git a/hive-agent-mcp/src/mcp/render.rs b/hive-agent-mcp/src/mcp/render.rs index 7656c0a8..bb6b3466 100644 --- a/hive-agent-mcp/src/mcp/render.rs +++ b/hive-agent-mcp/src/mcp/render.rs @@ -152,7 +152,7 @@ fn msg_id_tag(id: i64) -> String { } /// Hard cap on how many individual `Todo` lines `render_loose_ends` will -/// emit. Approvals/reminders stay naturally bounded (they're +/// emit. Approvals/questions/reminders stay naturally bounded (they're /// triaged interactively and don't self-multiply), but todos are pushed by /// unattended producers — matrix/bash/forge are the built-in ones, but any /// user-configured MCP server can push its own too — an agent that goes a long @@ -194,6 +194,19 @@ fn render_one_loose_end(out: &mut String, t: &hive_sh4re::inbox::LooseEnd) { "- approval #{id} ({agent} @ {commit_ref}, {age_seconds}s old){desc}" ); } + hive_sh4re::inbox::LooseEnd::Question { + id, + asker, + target, + question, + age_seconds, + } => { + let to = target.as_deref().unwrap_or("operator"); + let _ = writeln!( + out, + "- question #{id} ({asker} → {to}, {age_seconds}s old): {question}" + ); + } hive_sh4re::inbox::LooseEnd::Reminder { id, owner, @@ -446,20 +459,23 @@ pub(super) fn parse_loose_end_kind( raw: &str, ) -> Result { match raw.trim().to_ascii_lowercase().as_str() { + "question" | "q" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Question), "reminder" | "r" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Reminder), "approval" | "a" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Approval), other => Err(format!( "cancel_loose_end: unknown kind '{other}' \ - (expected \"reminder\", \"approval\", or \"todo\")" + (expected \"question\", \"reminder\", \"approval\", or \"todo\")" )), } } /// Canonical user-facing label for a `CancelLooseEndKind` — used in -/// the success ack so the caller always sees `"reminder"` instead of -/// whatever alias they passed in (`"r"`). +/// the success ack so the caller always sees `"question"` / +/// `"reminder"` instead of whatever alias they passed in (`"q"` / +/// `"r"`). pub(super) fn loose_end_kind_label(kind: hive_sh4re::inbox::CancelLooseEndKind) -> &'static str { match kind { + hive_sh4re::inbox::CancelLooseEndKind::Question => "question", hive_sh4re::inbox::CancelLooseEndKind::Reminder => "reminder", hive_sh4re::inbox::CancelLooseEndKind::Approval => "approval", } diff --git a/hive-agent/prompts/system.md b/hive-agent/prompts/system.md index 9297b2fc..44e114dd 100644 --- a/hive-agent/prompts/system.md +++ b/hive-agent/prompts/system.md @@ -2,7 +2,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity Tools (hyperhive surface). Full signature + behavior for each comes from the tool's own MCP description (you already received it via the MCP tool schema) — this is just the map of what exists and which ones are gated, so you know where to look: -- **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__mark_todos_done`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. One habit worth internalizing beyond the tool descriptions themselves: prefer ending the turn over repeatedly polling `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint). For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time — there's no blind range-clear, only ids you've actually looked at. +- **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__ask`, `mcp__hyperhive__answer`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__mark_todos_done`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. Two habits worth internalizing beyond the tool descriptions themselves: prefer ending the turn over repeatedly polling `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint); and `ask`/`answer` are async — `ask` returns immediately with a question id, the reply lands later as a `question_answered` system event, never block a turn waiting on it inline. For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time — there's no blind range-clear, only ids you've actually looked at. - **Extra MCP tools** (some agents only): `mcp____` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time. - **Lifecycle** (_requires `lifecycle` tool group_, direct children only, no approval needed): `restart`, `kill`, `start`, `update`, `list_containers`. - **Approvals** (_requires `approvals` tool group_, queues an operator approval): `request_init_config`, `request_apply_commit`, `request_update_meta_inputs`. @@ -15,11 +15,11 @@ Your config repo is mounted **read-only** at `/agents/{label}/config/` — `agen Approval boundary: lifecycle ops on _existing_ direct children (`kill`, `start`, `restart`) are at your discretion — no operator approval needed (requires `lifecycle` tool group). _Creating_ a new agent (two-step: `request_init_config` + `request_apply_commit`) and _changing_ any agent's config (`request_apply_commit`) both go through the approval queue (requires `approvals` tool group). The operator only signs off on changes; you run the day-to-day. -Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `container_crash`, `needs_update`. Use these to react to lifecycle changes: +Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `container_crash`, `needs_update`, `question_asked`, `question_answered`. Use these to react to lifecycle changes: - `needs_update` — agent's flake rev is stale. Call `update(name)` to rebuild — it's idempotent and doesn't need approval. - `container_crash` — restart with `start(name)`. If it crashes again, ask the operator. -- `approval_resolved` — one of your own submitted approvals (`request_init_config`, `request_apply_commit`, `request_update_meta_inputs`, a scheduled prompt, …) was approved, denied, or failed; the body carries the resolution. +- otherwise pick up answers to questions you asked. Lifecycle notices that don't need an immediate turn — a new agent spawned, its config repo seeded, a container rebuilt/killed/destroyed, or its login state changing — surface as todos instead of messages now. Call `get_loose_ends` to see them. diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index a0ecf104..d59d6205 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -104,9 +104,9 @@ async fn main() -> Result<()> { // ---------- shared turn helpers ---------- /// Surface a `SYSTEM_SENDER` message in the live event bus + tracing -/// log. Both agents and the manager receive `ContainerCrash`, -/// reparent notifications, and friends; the parse and log path is -/// identical. Quiet no-op when `from` isn't +/// log. Both agents and the manager receive `QuestionAnswered`, +/// `ContainerCrash`, reparent notifications, and friends; the parse +/// and log path is identical. Quiet no-op when `from` isn't /// `SYSTEM_SENDER`. fn log_system_event(bus: &Bus, from: &str, body: &str) { if from != SYSTEM_SENDER { diff --git a/hive-agent/src/prompt.rs b/hive-agent/src/prompt.rs index 3e0393ac..e0f5f5c2 100644 --- a/hive-agent/src/prompt.rs +++ b/hive-agent/src/prompt.rs @@ -255,7 +255,7 @@ shared closer assert!(!rendered.contains("