diff --git a/docs/conventions.md b/docs/conventions.md index a837bfcb..d91da1e3 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -322,7 +322,7 @@ binary flavor. |---|---| | `messaging` | `send`, `recv`, `ask`, `answer` | | `meta` | `get_agent_meta` (`set_status` is always-on, see below) | -| `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind`, `request_next_turn` | +| `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` | | `execution` | vestigial — `mcp__bash__run` / `mcp__bash__status` are always available unconditionally via `extraMcpServers`; this group's entries expand to non-existent `mcp__hyperhive__run` / `mcp__hyperhive__status` and have no effect. See `docs/tools/bash.md`. | | `lifecycle` | `kill`, `start`, `restart`, `update` *(privileged)* | | `approvals` | `request_init_config`, `request_update_meta_inputs` *(privileged)* | diff --git a/docs/turn-loop.md b/docs/turn-loop.md index 1d786d81..0ed7f03e 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -12,10 +12,8 @@ agents) runs: loop does nothing but re-stat it every 5 s — no broker poll, no claude process. Because step 1 is never reached, messages stay queued and unacked, so a resume drains the backlog instead of - losing it; reminders and todo wakes buffer in their channels. The - check runs before the self-continue slot is consumed, so a pending - `request_next_turn` survives the pause. Set it with - `hivectl agent pause` or the dashboard toggle; see + losing it; reminders and todo wakes buffer in their channels. Set + it with `hivectl agent pause` or the dashboard toggle; see [persistence](persistence.md#-harnesspaused-per-agent). 1. Long-poll `Recv` on its socket. The host-side broker (`broker.rs::recv_blocking_batch`) returns immediately if there's @@ -141,21 +139,17 @@ only complete output silence for the window trips it. The harness sets the window from `HIVE_TURN_IDLE_SECS` (`0` disables) and maps the driver's `Error::IdleTimeout` onto `TurnError::ApiStall`. -After the outcome handler, the stats sink records a row and the -`hyperhive-continue` sentinel (dropped by the `request_next_turn` -MCP tool) is consumed if present. `handle_turn` reports the result -to `serve_loop` via `TurnControl { auth_failed, continue_requested, -pending }`. When a continue was requested, the turn did not -auth-fail, and the inbox is empty (`pending == 0`), `serve_loop` -drives the next turn in-process with a synthetic -`{ from: "self", body: "continue" }` message (`synthetic_continue`) -— it never goes through the broker, so the self-continue doesn't -persist to sqlite or show up as a recv'able inbox message. If real -messages are already pending the continue is dropped: those messages -drive the next turn(s) via `recv_next`, so an explicit self-wake -isn't needed (this is the `request_next_turn` contract — "no effect -if a new inbox message arrives before this turn ends"). The -`should_self_continue` predicate encodes exactly that decision. +After the outcome handler, the stats sink records a row. `handle_turn` +reports the result to `serve_loop` via `TurnControl { auth_failed }` — +on auth failure the loop parks in `wait_for_login`; otherwise it loops +straight back to the idle wait (step 1). There is no same-turn +self-continue tool (removed — see forge #2777): every multi-step +continuation rides an external wake instead — a new inbox message, a +`remind`, or an in-container todo wake (bash-task completion, forge +notification, matrix activity). Ending the turn and letting one of +those drive the next one is strictly better than parking in-process: +it checkpoints the session and observes wakes that only reach the +harness between turns. ## Sub-pages diff --git a/docs/turn-loop/mcp.md b/docs/turn-loop/mcp.md index 4a1942f6..55a568ea 100644 --- a/docs/turn-loop/mcp.md +++ b/docs/turn-loop/mcp.md @@ -80,7 +80,7 @@ shapes and routing logic in **Inbox** (`inbox` group): `get_loose_ends(agent?)`, `cancel_loose_end(kind, id)`, `remind(message, delay_seconds? | -at_unix_timestamp?)`, `request_next_turn()`. +at_unix_timestamp?)`. - `get_loose_ends(agent?)` — list pending questions (asked/owed), scheduled reminders, and active local tasks published by external MCP @@ -97,9 +97,15 @@ at_unix_timestamp?)`, `request_next_turn()`. - `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`). -- `request_next_turn` — ask the harness to start another turn - immediately after this one ends, even if the inbox is empty. - Next turn fires with `from: "self"` and `body: "continue"`. + +There is no same-turn self-continue tool (`request_next_turn` was +removed — unanimous consensus across every agent that used the harness +that ending the turn and letting an external wake drive the next one +is strictly better: it checkpoints the session and observes wakes that +only reach the harness between turns, forge #2777). Multi-step work +rides `remind` for a durable self-wake, or an in-container todo wake +(bash-task completion, forge notification, matrix activity) for +work already in flight. **Meta** (`meta` group): `set_status(text)`, `get_agent_meta(name?)`. diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index 0dcad96a..a65de0f5 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -626,30 +626,6 @@ impl AgentServer { .await } - #[tool( - description = "Ask the harness to start another turn immediately after this one \ - completes, even if the inbox is empty. Use this when you have ongoing work that \ - spans multiple turns (long builds, multi-step tasks) and you want to continue \ - without waiting for an external message. The next turn will start with \ - `from: \"self\"` and `body: \"continue\"`. Has no effect if a new inbox message \ - arrives before this turn ends — the harness already loops immediately on pending \ - messages. No args." - )] - async fn request_next_turn(&self) -> String { - run_tool_envelope("request_next_turn", String::new(), async move { - let sentinel = crate::paths::state_dir().join("hyperhive-continue"); - match std::fs::write(&sentinel, b"") { - Ok(()) => "ok — harness will start another turn immediately after this one", - Err(e) => { - tracing::warn!(error = %e, path = %sentinel.display(), "request_next_turn: write failed"); - return format!("request_next_turn failed: {e}"); - } - } - .to_string() - }) - .await - } - #[tool( description = "Compact the current session's context, mirroring the operator's \ dashboard `/compact` button. Gated: only honoured when this agent's last \ diff --git a/hive-agent/prompts/system.md b/hive-agent/prompts/system.md index fa084b48..b4bddaea 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__ask`, `mcp__hyperhive__answer`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`, `mcp__hyperhive__request_next_turn`. Two habits worth internalizing beyond the tool descriptions themselves: prefer ending the turn over parking in `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. +- **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__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. Two habits worth internalizing beyond the tool descriptions themselves: prefer ending the turn over parking in `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. - **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`. @@ -42,6 +42,6 @@ Keep messages short — a few sentences each. For anything big (file listings, l When your inbox has a message, handle it and stop. Don't narrate intent — act. -**Turns are your checkpoint.** The harness runs one claude turn per inbox message; when you stop, it acknowledges that message and your `--continue` session is saved to disk. Ending the turn is how you commit progress — both the session and the inbox acknowledgement. If the container restarts _while a turn is still running_, the message that drove it was never acknowledged, so it gets redelivered on the next boot, prefixed `[redelivered after harness restart — may already be handled]`. A long single turn that does step after step widens the window where a restart loses work and forces that redelivery, so prefer short turns: do a unit of work, write anything durable under `/agents/{label}/state/`, and end. +**Turns are your checkpoint.** The harness runs one claude turn per inbox message; when you stop, it acknowledges that message and your `--continue` session is saved to disk. Ending the turn is how you commit progress — both the session and the inbox acknowledgement. If the container restarts _while a turn is still running_, the message that drove it was never acknowledged, so it gets redelivered on the next boot, prefixed `[redelivered after harness restart — may already be handled]`. A long single turn that does step after step widens the window where a restart loses work and forces that redelivery, so prefer short turns: do a unit of work, write anything durable under `/agents/{label}/state/` **at every natural boundary, not just when a turn happens to end**, and stop. -**To keep working without waiting for a new message, call `request_next_turn()`** before you stop. The harness immediately starts a fresh turn with `from: "self"`, `body: "continue"` — the supported way to run multi-step work (long builds, sequential edits) as a series of checkpointed turns rather than one monolithic turn. Don't busy-wait inside a turn for a condition to resolve: end the turn and let the next wake drive the continuation — a `remind` you scheduled, an external event, a backgrounded bash task's completion, or `request_next_turn()`. (A long-poll `recv(wait_seconds: …)` blocks _within_ the current turn — it parks for new inbox messages but does not end the turn or checkpoint, so it isn't a substitute for ending the turn.) +For multi-step work (long builds, sequential edits) that spans more than one turn: end the turn and let an external wake drive the next one — a new inbox message, a `remind` you scheduled, or a backgrounded bash task's completion. Ending the turn is never a same-turn continuation: it's the checkpoint itself, and the only place an in-container todo wake (bash-task completion, matrix unread, forge activity) can reach you — a long-poll `recv(wait_seconds: …)` blocks _within_ the current turn instead and misses exactly those wakes, so it isn't a substitute for ending the turn. **Keep `set_status` current whenever the work changes** — a stale status is what actually loses context across a restart, not turn length; a status set at the start of a task and never touched again is a bug, not a shortcut. diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index f3a0f53c..6d578de8 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -188,72 +188,19 @@ fn format_turn_failure(err: &anyhow::Error) -> String { format!("[system] `{who}` claude turn failed:\n{err:#}") } -/// Check for the `hyperhive-continue` sentinel under the state dir -/// (dropped by the `request_next_turn` MCP tool). Returns true and -/// consumes the file when present; false otherwise. Caller fires -/// the role-specific `Wake` request — the sentinel itself is wire- -/// agnostic so this helper lives outside both surfaces. -fn consume_continue_sentinel() -> bool { - let sentinel = crate::paths::state_dir().join("hyperhive-continue"); - if !sentinel.exists() { - return false; - } - if let Err(e) = std::fs::remove_file(&sentinel) { - tracing::warn!(error = %e, "consume_continue_sentinel: remove sentinel failed"); - return false; - } - true -} - -/// What a finished turn tells the serve loop to do next. Replaces the -/// bare `auth_failed` bool so the loop can also act on a pending -/// `request_next_turn` without round-tripping a synthetic message -/// through the broker. +/// What a finished turn tells the serve loop to do next. struct TurnControl { /// The turn ended in `AuthFailed` — caller parks on login. auth_failed: bool, - /// `request_next_turn` was called during the turn (the - /// `hyperhive-continue` sentinel was dropped + consumed). - continue_requested: bool, - /// Inbox unread count observed right after the turn. Used to - /// decide whether a self-continue is actually needed. - pending: u64, -} - -/// Decide whether the serve loop should drive a self-continue turn -/// in-process. A continue is only "needed" when nothing else will -/// wake the agent: if real messages are already pending they drive -/// the next turn(s) and the continue is dropped (matches the -/// `request_next_turn` contract — "no effect if a new inbox message -/// arrives before this turn ends"). Auth-failed parks the loop on -/// login, so it suppresses the continue too. -fn should_self_continue(ctrl: &TurnControl) -> bool { - ctrl.continue_requested && !ctrl.auth_failed && ctrl.pending == 0 -} - -/// Synthesize the `from: "self"` / `body: "continue"` message that a -/// `request_next_turn` self-continue drives. Built in-process rather -/// than fetched from the broker — it never touches the send/recv -/// path, so it doesn't persist to sqlite or pollute the inbox. -/// `id = 0` is a non-broker sentinel: the synthetic message -/// has no DB row, and `AckTurn` keys off the recipient's in-flight -/// list (which is empty here) rather than this id. -fn synthetic_continue() -> hive_sh4re::DeliveredMessage { - hive_sh4re::DeliveredMessage { - from: "self".into(), - body: "continue".into(), - id: 0, - redelivered: false, - in_reply_to: None, - } } /// Synthesize the message that drives a turn when an in-container producer /// upserted a new/changed *todo* over the in-agent socket (loose-ends v2). /// The harness owns the todo store locally and signals the serve loop /// directly — so this wake never touches the broker (no long-poll, no -/// marker file). `id = 0` is the same non-broker sentinel as -/// [`synthetic_continue`]. +/// marker file). `id = 0` is a non-broker sentinel: the synthetic message +/// has no DB row, and `AckTurn` keys off the recipient's in-flight list +/// (which is empty here) rather than this id. fn synthetic_todo_message() -> hive_sh4re::DeliveredMessage { hive_sh4re::DeliveredMessage { from: "todo".into(), @@ -678,20 +625,12 @@ async fn serve_loop( // The durable claude session, built once and reused for every turn + // idle compaction below (it's effectively stateless). let session = turn::make_session(&bus); - // Set when a turn calls `request_next_turn` and no real work is - // pending — the next iteration drives this synthetic message - // in-process instead of long-polling the broker. Never - // persisted: it lives entirely in this loop's stack. - let mut self_continue: Option = None; // Tracks the last observed pause state so the transitions get logged // once each instead of twelve lines a minute while parked. let mut was_paused = false; loop { // Pause gate. While the marker is present this loop drives no - // turns at all — deliberately *before* the `self_continue.take()` - // below, so a `request_next_turn` that raced the pause is still - // waiting when the agent resumes rather than being consumed by - // a turn that never runs. + // turns at all. // // Nothing here touches the broker: not calling `S::recv_next` is // exactly the "messages queue unacked, resume drains the @@ -722,78 +661,75 @@ async fn serve_loop( }); was_paused = false; } - let next = match self_continue.take() { - Some(msg) => msg, - None => match { - // Idle wait: race the broker long-poll against a local - // todo signal so an in-container producer's upsert drives a - // turn without any broker round-trip. `biased` polls the - // broker recv first, so a genuinely-ready inbox message is - // never dropped in favour of the todo wake. - tokio::select! { - biased; - o = S::recv_next(socket) => o, - () = todo_wake.notified() => RecvOutcome::LocalTodo, - Some(dm) = reminder_rx.recv() => RecvOutcome::Message(dm), - } - } { - RecvOutcome::Message(first) => first, - RecvOutcome::LocalTodo => { - // Gate on `has_any()` before spawning a turn: a burst of - // same-turn upserts can arm a second `Notify` permit that - // outlives the turn which already drained its payload - // (the phantom-todo-wake issue) — `notify_one` doesn't - // coalesce once the first permit's been consumed, so the surplus wake - // fires the instant the loop is back here even though - // there's nothing left to show. Fail open (drive a turn - // anyway) on a `has_any` error so a flaky sqlite read - // never silently swallows a real wake. - let has_any = todos_store - .as_ref() - .is_none_or(|store| store.has_any().unwrap_or(true)); - if !has_any { - tracing::debug!("todo wake fired against an empty store — stale, skipping"); - continue; - } - tracing::debug!("todo wake consumed, sending synthetic todo message"); - synthetic_todo_message() - } - RecvOutcome::Empty => { - // Idle: no message this poll. Service a queued operator - // `/compact` here so it runs even when no turn is driving - // (the in-flight case is handled at the end of drive_turn). - let compacted = turn::run_pending_compact(files, &bus, &session).await; - if !compacted { - tokio::time::sleep(interval).await; - } + let next = match { + // Idle wait: race the broker long-poll against a local + // todo signal so an in-container producer's upsert drives a + // turn without any broker round-trip. `biased` polls the + // broker recv first, so a genuinely-ready inbox message is + // never dropped in favour of the todo wake. + tokio::select! { + biased; + o = S::recv_next(socket) => o, + () = todo_wake.notified() => RecvOutcome::LocalTodo, + Some(dm) = reminder_rx.recv() => RecvOutcome::Message(dm), + } + } { + RecvOutcome::Message(first) => first, + RecvOutcome::LocalTodo => { + // Gate on `has_any()` before spawning a turn: a burst of + // same-turn upserts can arm a second `Notify` permit that + // outlives the turn which already drained its payload + // (the phantom-todo-wake issue) — `notify_one` doesn't + // coalesce once the first permit's been consumed, so the surplus wake + // fires the instant the loop is back here even though + // there's nothing left to show. Fail open (drive a turn + // anyway) on a `has_any` error so a flaky sqlite read + // never silently swallows a real wake. + let has_any = todos_store + .as_ref() + .is_none_or(|store| store.has_any().unwrap_or(true)); + if !has_any { + tracing::debug!("todo wake fired against an empty store — stale, skipping"); continue; } - RecvOutcome::TransportError => { - // `recv_next` already logged the detail; just retry. - // No backoff: the long-poll wait is itself the throttle. - continue; + tracing::debug!("todo wake consumed, sending synthetic todo message"); + synthetic_todo_message() + } + RecvOutcome::Empty => { + // Idle: no message this poll. Service a queued operator + // `/compact` here so it runs even when no turn is driving + // (the in-flight case is handled at the end of drive_turn). + let compacted = turn::run_pending_compact(files, &bus, &session).await; + if !compacted { + tokio::time::sleep(interval).await; } - RecvOutcome::GracefulStop => { - // c0re fenced our inbox and wants a clean stop. Run one - // checkpoint turn so the agent flushes durable /state, - // report completion, then exit the loop → the harness - // process ends and the container can be stopped. - tracing::info!( - "graceful stop signalled — running stop-checkpoint turn, then exiting" - ); - let _ = handle_turn::( - socket, - &bus, - stats.as_ref(), - files, - &session, - graceful_stop_message(), - ) - .await; - S::graceful_stop_complete(socket).await; - return Ok(()); - } - }, + continue; + } + RecvOutcome::TransportError => { + // `recv_next` already logged the detail; just retry. + // No backoff: the long-poll wait is itself the throttle. + continue; + } + RecvOutcome::GracefulStop => { + // c0re fenced our inbox and wants a clean stop. Run one + // checkpoint turn so the agent flushes durable /state, + // report completion, then exit the loop → the harness + // process ends and the container can be stopped. + tracing::info!( + "graceful stop signalled — running stop-checkpoint turn, then exiting" + ); + let _ = handle_turn::( + socket, + &bus, + stats.as_ref(), + files, + &session, + graceful_stop_message(), + ) + .await; + S::graceful_stop_complete(socket).await; + return Ok(()); + } }; let ctrl = handle_turn::(socket, &bus, stats.as_ref(), files, &session, next).await; if ctrl.auth_failed { @@ -805,19 +741,14 @@ async fn serve_loop( u64::try_from(interval.as_millis()).unwrap_or(2000), ) .await; - } else if should_self_continue(&ctrl) { - tracing::info!("request_next_turn: driving self-continue turn in-process"); - self_continue = Some(synthetic_continue()); } } } /// Drive a single turn: emit boot-of-turn events, run claude, ack on /// success / requeue on rate-limit-or-401 / notify parent on failure, -/// record stats, then pick up the `request_next_turn` sentinel if it's -/// been dropped during the turn. Returns a `TurnControl` carrying the -/// auth-failed flag, whether a self-continue was requested, and the -/// post-turn inbox count — the serve loop decides what to do next. +/// record stats. Returns a `TurnControl` carrying the auth-failed flag — +/// the serve loop decides what to do next. async fn handle_turn( socket: &Path, bus: &Bus, @@ -933,54 +864,5 @@ async fn handle_turn( } TurnControl { auth_failed: matches!(outcome, Err(turn::TurnError::AuthFailed)), - continue_requested: consume_continue_sentinel(), - pending, - } -} - -#[cfg(test)] -mod continue_tests { - use super::{TurnControl, should_self_continue, synthetic_continue}; - - fn ctrl(auth_failed: bool, continue_requested: bool, pending: u64) -> TurnControl { - TurnControl { - auth_failed, - continue_requested, - pending, - } - } - - #[test] - fn self_continue_when_requested_and_inbox_empty() { - assert!(should_self_continue(&ctrl(false, true, 0))); - } - - #[test] - fn no_self_continue_when_not_requested() { - assert!(!should_self_continue(&ctrl(false, false, 0))); - } - - #[test] - fn no_self_continue_when_real_messages_pending() { - // A real message will drive the next turn via recv — the - // continue is superseded, not needed (request_next_turn contract). - assert!(!should_self_continue(&ctrl(false, true, 3))); - } - - #[test] - fn no_self_continue_when_auth_failed() { - // Auth-failed parks the loop on login; a queued continue must - // not jump the gate. - assert!(!should_self_continue(&ctrl(true, true, 0))); - } - - #[test] - fn synthetic_continue_shape() { - let m = synthetic_continue(); - assert_eq!(m.from, "self"); - assert_eq!(m.body, "continue"); - assert_eq!(m.id, 0); - assert!(!m.redelivered); - assert!(m.in_reply_to.is_none()); } } diff --git a/hive-agent/src/stream_enrich.rs b/hive-agent/src/stream_enrich.rs index 9241b49f..2a877874 100644 --- a/hive-agent/src/stream_enrich.rs +++ b/hive-agent/src/stream_enrich.rs @@ -362,7 +362,6 @@ fn tool_icon(name: &str) -> &'static str { "mcp__hyperhive__cancel_loose_end" => "✂️", "mcp__hyperhive__ack_until" => "✅", "mcp__hyperhive__get_agent_meta" => "ℹ️", - "mcp__hyperhive__request_next_turn" => "⏩", "mcp__hyperhive__restart" => "↻", "mcp__hyperhive__kill" => "⏹️", "mcp__hyperhive__start" => "▶️", diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 22ce93e6..8ca7570c 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -594,7 +594,7 @@ pub enum ToolGroup { Messaging, /// `get_agent_meta` (`set_status` is always-on — see `ALWAYS_ON_TOOLS`) Meta, - /// `get_loose_ends`, `cancel_loose_end`, `remind`, `request_next_turn` + /// `get_loose_ends`, `cancel_loose_end`, `remind` Inbox, /// `kill`, `start`, `restart`, `update` - *(privileged)* Lifecycle, @@ -628,12 +628,7 @@ impl ToolGroup { match self { Self::Messaging => &["send", "recv", "ack_until", "ask", "answer"], Self::Meta => &["get_agent_meta"], - Self::Inbox => &[ - "get_loose_ends", - "cancel_loose_end", - "remind", - "request_next_turn", - ], + Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"], Self::Lifecycle => &["kill", "start", "restart", "update", "list_containers"], Self::Approvals => &["request_init_config", "request_update_meta_inputs"], Self::Scheduling => &[ @@ -735,9 +730,7 @@ impl ToolGroup { Self::Meta => { "get_agent_meta — identity introspection (set_status is always available)" } - Self::Inbox => { - "get_loose_ends, cancel_loose_end, remind, request_next_turn — self-scheduling" - } + Self::Inbox => "get_loose_ends, cancel_loose_end, remind — self-scheduling", Self::Lifecycle => { "kill, start, restart, update, list_containers — container lifecycle (privileged)" }