637 lines
26 KiB
Rust
637 lines
26 KiB
Rust
//! Wire types shared between `hive-c0re` and the in-container harness.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Host admin socket — /run/hyperhive/host.sock
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/// Requests on the host admin socket.
|
|
///
|
|
/// Wire format: one JSON object per line.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "cmd", rename_all = "snake_case")]
|
|
pub enum HostRequest {
|
|
/// Create and start a sub-agent container directly (no approval). Use
|
|
/// this from privileged contexts (operator on the host); it bypasses the
|
|
/// approval queue intentionally so test scripts and one-off recoveries
|
|
/// don't need a separate approve step.
|
|
Spawn { name: String },
|
|
/// Submit a spawn request for the user to approve. On approval the host
|
|
/// creates and starts the container. Mirrors the manager's
|
|
/// `RequestSpawn` — exposed on the admin socket so the dashboard and CLI
|
|
/// can also queue spawns through the approval flow.
|
|
RequestSpawn { name: String },
|
|
/// Stop a managed container (graceful).
|
|
Kill { name: String },
|
|
/// Tear down a sub-agent container: stop + remove + drop the systemd
|
|
/// drop-in, purge pending approvals. Persistent state (proposed/applied
|
|
/// repos, Claude credentials) is KEPT by default — recreating the agent
|
|
/// with the same name reuses prior config + login. With `purge=true`
|
|
/// the agent's `/var/lib/hyperhive/{agents,applied}/<name>/` trees are
|
|
/// also wiped (config history + creds + notes gone forever). Manager
|
|
/// not destroyable.
|
|
Destroy {
|
|
name: String,
|
|
#[serde(default)]
|
|
purge: bool,
|
|
},
|
|
/// Apply pending config to a managed container.
|
|
Rebuild { name: String },
|
|
/// List managed containers.
|
|
List,
|
|
/// List pending approval requests.
|
|
Pending,
|
|
/// Approve a pending request by id; the action runs immediately.
|
|
Approve { id: i64 },
|
|
/// Deny a pending request by id.
|
|
Deny { id: i64 },
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct HostResponse {
|
|
pub ok: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub error: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub agents: Option<Vec<String>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub approvals: Option<Vec<Approval>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Approval {
|
|
pub id: i64,
|
|
pub agent: String,
|
|
#[serde(default)]
|
|
pub kind: ApprovalKind,
|
|
/// For `ApplyCommit`: the git sha the manager submitted. For `Spawn`:
|
|
/// empty. Note that this is the manager's *claimed* ref — the
|
|
/// canonical, hive-c0re-vouched sha after the proposal fetch lives
|
|
/// in `fetched_sha`.
|
|
pub commit_ref: String,
|
|
/// The sha hive-c0re fetched from the proposed repo into applied at
|
|
/// submission time, then tagged `proposal/<id>`. Stable for the
|
|
/// lifetime of the approval — manager amends in proposed don't
|
|
/// change what gets built. Only set for `ApplyCommit` after the
|
|
/// successful fetch.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub fetched_sha: Option<String>,
|
|
pub requested_at: i64,
|
|
pub status: ApprovalStatus,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub resolved_at: Option<i64>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub note: Option<String>,
|
|
/// Optional free-text description the manager attached at submission
|
|
/// time — shown on the dashboard approval card so the operator can
|
|
/// understand the change without opening the diff.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
/// What action the approval, when granted, will trigger.
|
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ApprovalKind {
|
|
/// Apply a manager-proposed config commit (existing flow).
|
|
#[default]
|
|
ApplyCommit,
|
|
/// Create + start a new sub-agent container with the given name.
|
|
Spawn,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ApprovalStatus {
|
|
Pending,
|
|
Approved,
|
|
Denied,
|
|
Failed,
|
|
}
|
|
|
|
impl HostResponse {
|
|
pub fn success() -> Self {
|
|
Self {
|
|
ok: true,
|
|
error: None,
|
|
agents: None,
|
|
approvals: None,
|
|
}
|
|
}
|
|
|
|
pub fn error(message: impl Into<String>) -> Self {
|
|
Self {
|
|
ok: false,
|
|
error: Some(message.into()),
|
|
agents: None,
|
|
approvals: None,
|
|
}
|
|
}
|
|
|
|
pub fn list(agents: Vec<String>) -> Self {
|
|
Self {
|
|
ok: true,
|
|
error: None,
|
|
agents: Some(agents),
|
|
approvals: None,
|
|
}
|
|
}
|
|
|
|
pub fn pending(approvals: Vec<Approval>) -> Self {
|
|
Self {
|
|
ok: true,
|
|
error: None,
|
|
agents: None,
|
|
approvals: Some(approvals),
|
|
}
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Per-agent socket — /run/hyperhive/agents/<name>/mcp.sock on the host,
|
|
// bind-mounted into the container at /run/hive/mcp.sock.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/// A logical message between agents.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Message {
|
|
pub from: String,
|
|
pub to: String,
|
|
pub body: String,
|
|
}
|
|
|
|
/// One row of a broker inbox query — what the dashboard renders in
|
|
/// its operator-inbox section and what a per-agent web UI returns
|
|
/// from a `Recent` request. Lives in `hive_sh4re` so it can travel
|
|
/// over both the dashboard's `/api/state` and the agent socket
|
|
/// without an internal-to-wire conversion.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct InboxRow {
|
|
pub id: i64,
|
|
pub from: String,
|
|
pub body: String,
|
|
pub at: i64,
|
|
}
|
|
|
|
/// Reminder timing: either relative (wait N seconds) or absolute (at unix
|
|
/// timestamp).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "timing_type", rename_all = "snake_case")]
|
|
pub enum ReminderTiming {
|
|
/// Remind after this many seconds from now.
|
|
InSeconds { seconds: u64 },
|
|
/// Remind at this unix timestamp (seconds since epoch).
|
|
At { unix_timestamp: i64 },
|
|
}
|
|
|
|
/// One row in the response to `GetOpenThreads`. Tagged enum so new
|
|
/// thread kinds (forge PRs, long-running approvals from a privileged
|
|
/// bot, etc) can land later without breaking existing handlers. The
|
|
/// caller (claude in the agent harness) is expected to render these
|
|
/// as a short bulleted list — the per-row fields are all the context
|
|
/// needed without a follow-up fetch.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
pub enum OpenThread {
|
|
/// A pending approval. For agent-flavour `GetOpenThreads` calls
|
|
/// this only surfaces when the agent itself is the manager
|
|
/// (sub-agents don't submit approvals). For manager-flavour calls
|
|
/// it lists every pending approval in the swarm. `agent` is the
|
|
/// affected agent (target of the spawn / config commit).
|
|
Approval {
|
|
id: i64,
|
|
agent: String,
|
|
commit_ref: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
description: Option<String>,
|
|
/// Wall-clock seconds since `requested_at`. Saturates at zero on
|
|
/// any clock anomaly (back-step etc).
|
|
age_seconds: u64,
|
|
},
|
|
/// An unanswered question. For agent-flavour calls: only threads
|
|
/// where the agent is `asker` OR `target`. For manager-flavour
|
|
/// calls: every unanswered question in the swarm. `target = None`
|
|
/// means the question is addressed to the operator (dashboard
|
|
/// path); `Some(agent)` is a peer-to-peer thread.
|
|
Question {
|
|
id: i64,
|
|
asker: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
target: Option<String>,
|
|
question: String,
|
|
/// Wall-clock seconds since `asked_at`. Saturates at zero.
|
|
age_seconds: u64,
|
|
},
|
|
}
|
|
|
|
/// Requests on a per-agent socket. The agent's identity is the socket
|
|
/// it came in on; `Send.from` is filled in by the server, not the client.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "cmd", rename_all = "snake_case")]
|
|
pub enum AgentRequest {
|
|
/// Send a message to another agent.
|
|
Send { to: String, body: String },
|
|
/// Pop one pending message from this agent's inbox. Long-polls
|
|
/// up to `wait_seconds` (capped at 60s server-side, default 30s
|
|
/// when None) before returning `Empty`.
|
|
Recv {
|
|
#[serde(default)]
|
|
wait_seconds: Option<u64>,
|
|
},
|
|
/// Non-mutating: how many pending messages are addressed to me?
|
|
/// Used by the harness to render a status line after each tool call.
|
|
Status,
|
|
/// Operator-injected message TO this agent (from this agent's own web
|
|
/// UI). Recipient is implicit — `from` is `"operator"`. Effectively the
|
|
/// per-agent equivalent of the old dashboard T4LK form, but scoped to
|
|
/// the agent whose page the operator is on.
|
|
OperatorMsg { body: String },
|
|
/// Wake-up event injected from inside the container — typically an
|
|
/// extra MCP server (matrix, scraper, webhook bridge) signalling
|
|
/// that external work has arrived for this agent. Recipient is
|
|
/// implicit (this agent); `from` is caller-chosen so the wake
|
|
/// prompt can label the source ("matrix: new message in
|
|
/// #general"). Identity = socket means anything that can connect
|
|
/// to `/run/hive/mcp.sock` is implicitly trusted to inject these,
|
|
/// which is fine: the bind-mount is restricted to the agent's
|
|
/// own container.
|
|
Wake { from: String, body: String },
|
|
/// Last `limit` messages addressed to this agent, newest-first.
|
|
/// Non-mutating — pulls from the broker without delivering. The
|
|
/// per-agent web UI uses this to render its own inbox section.
|
|
Recent { limit: u64 },
|
|
/// Surface a question to either the operator or another agent.
|
|
/// `to = None` (or `Some("operator")`) routes the question to the
|
|
/// dashboard's operator-question queue (legacy `AskOperator`
|
|
/// behaviour). `to = Some(<agent>)` routes it to that agent's
|
|
/// inbox as a `HelperEvent::QuestionAsked` so the recipient can
|
|
/// answer back via `AgentRequest::Answer` (or
|
|
/// `ManagerRequest::Answer`); the answer threads back to the asker
|
|
/// as a `HelperEvent::QuestionAnswered` event. Either way the
|
|
/// response shape is `QuestionQueued { id }` — the asker uses the
|
|
/// id to correlate the asynchronous answer event.
|
|
Ask {
|
|
question: String,
|
|
#[serde(default)]
|
|
options: Vec<String>,
|
|
#[serde(default)]
|
|
multi: bool,
|
|
#[serde(default)]
|
|
ttl_seconds: Option<u64>,
|
|
/// Recipient of the question. `None` or `Some("operator")` =
|
|
/// the human operator (dashboard); `Some(<agent_name>)` = a
|
|
/// peer agent (their inbox).
|
|
#[serde(default)]
|
|
to: Option<String>,
|
|
},
|
|
/// Answer a question previously routed to this agent via
|
|
/// `HelperEvent::QuestionAsked`. The caller is implicitly the
|
|
/// answerer; only the question's `target` agent (or the operator,
|
|
/// via the dashboard) is authorised. Wires through to
|
|
/// `HelperEvent::QuestionAnswered` in the asker's inbox.
|
|
Answer { id: i64, answer: String },
|
|
/// Schedule a reminder message to be delivered to this agent at a
|
|
/// future time. The reminder lands in the agent's inbox as an auto-sent
|
|
/// message from `"reminder"`. Use for agent follow-ups (e.g. check task
|
|
/// status, retry failed operation). Message length is limited; pass
|
|
/// `file_path` to store in a file and get a path-reference message
|
|
/// instead.
|
|
Remind {
|
|
message: String,
|
|
timing: ReminderTiming,
|
|
#[serde(default)]
|
|
file_path: Option<String>,
|
|
},
|
|
/// Loose-ends view: pending approvals + unanswered questions
|
|
/// pending against THIS agent. Approvals only surface if this
|
|
/// agent submitted them (which only ever happens for the
|
|
/// manager); questions surface where the agent is `asker` or
|
|
/// `target`. Cheap O(n) sweep server-side — no caching.
|
|
GetOpenThreads,
|
|
}
|
|
|
|
/// Responses on a per-agent socket.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
pub enum AgentResponse {
|
|
/// `Send` succeeded.
|
|
Ok,
|
|
/// Either `Send` failed or `Recv` errored.
|
|
Err { message: String },
|
|
/// `Recv` produced a message.
|
|
Message { from: String, body: String },
|
|
/// `Recv` found nothing pending.
|
|
Empty,
|
|
/// `Status` result: how many pending messages are in this agent's inbox.
|
|
Status { unread: u64 },
|
|
/// `Recent` result: newest-first inbox rows.
|
|
Recent { rows: Vec<InboxRow> },
|
|
/// `Ask` result: the queued question id. The answer lands later
|
|
/// as `HelperEvent::QuestionAnswered` in this agent's inbox.
|
|
QuestionQueued { id: i64 },
|
|
/// `GetOpenThreads` result: list of loose ends pending against
|
|
/// this agent. Ordered newest-first within each kind.
|
|
OpenThreads { threads: Vec<OpenThread> },
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Manager socket — /run/hyperhive/manager/mcp.sock on the host, bind-mounted
|
|
// into the manager container at /run/hive/mcp.sock.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/// Logical name the broker uses for the manager.
|
|
pub const MANAGER_AGENT: &str = "manager";
|
|
|
|
/// Logical name the broker uses for the human operator. Messages with
|
|
/// `to = OPERATOR_RECIPIENT` accumulate in sqlite and surface on the
|
|
/// dashboard's inbox view — they are never `recv`'d by an agent harness.
|
|
pub const OPERATOR_RECIPIENT: &str = "operator";
|
|
|
|
/// Sender hive-c0re uses for events it pushes into the manager's inbox.
|
|
/// Manager harness recognises this and parses the body as a `HelperEvent`.
|
|
pub const SYSTEM_SENDER: &str = "system";
|
|
|
|
/// Out-of-band events the host-side daemon pushes to the manager's inbox.
|
|
/// Serialised as JSON in `Message::body` (sender = `SYSTEM_SENDER`).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "event", rename_all = "snake_case")]
|
|
pub enum HelperEvent {
|
|
/// An approval was approved/denied/failed; if approved, the underlying
|
|
/// action (rebuild or spawn) has already run by the time this lands.
|
|
ApprovalResolved {
|
|
id: i64,
|
|
agent: String,
|
|
commit_ref: String,
|
|
status: ApprovalStatus,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
note: Option<String>,
|
|
/// Canonical sha hive-c0re fetched into applied at submission
|
|
/// time. `git show <sha>` against `/agents/<n>/applied.git`
|
|
/// inside the manager container yields the exact tree being
|
|
/// referenced.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
sha: Option<String>,
|
|
/// Terminal tag name in the applied repo for this approval —
|
|
/// `deployed/<id>`, `failed/<id>`, or `denied/<id>` (and
|
|
/// `approved/<id>` for the rare bare-approval case where
|
|
/// no underlying action runs).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
tag: Option<String>,
|
|
},
|
|
/// A new container was spawned (post-approval or via the admin CLI
|
|
/// bypass path). `ok=false` means the spawn failed.
|
|
Spawned {
|
|
agent: String,
|
|
ok: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
note: Option<String>,
|
|
/// Sha of the `deployed/0` commit seeded by hive-c0re on
|
|
/// first spawn (Some on success, None on failure).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
sha: Option<String>,
|
|
},
|
|
/// A container was rebuilt (auto-update on flake rev change, or a
|
|
/// manual rebuild from CLI/dashboard).
|
|
Rebuilt {
|
|
agent: String,
|
|
ok: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
note: Option<String>,
|
|
/// Sha that ended up at `deployed/<id>` on success, or the
|
|
/// proposal sha that just got tagged `failed/<id>` on
|
|
/// failure. None for the (rare) rebuild path that doesn't go
|
|
/// through an approval (e.g. `auto_update::rebuild_agent`
|
|
/// reapplying the existing main).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
sha: Option<String>,
|
|
/// `deployed/<id>` or `failed/<id>` for approval-driven
|
|
/// rebuilds; None for auto-update / dashboard rebuilds that
|
|
/// don't change the deployed commit.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
tag: Option<String>,
|
|
},
|
|
/// A sub-agent's container was stopped (the systemd unit is down;
|
|
/// persistent state is unchanged).
|
|
Killed { agent: String },
|
|
/// A sub-agent's container was torn down (container removed; state
|
|
/// dirs preserved per `destroy` semantics).
|
|
Destroyed { agent: String },
|
|
/// A sub-agent's container has no claude session yet (first
|
|
/// spawn, or `--purge` wiped creds). Manager can't do anything
|
|
/// about it directly — login is interactive OAuth — but it
|
|
/// surfaces so the manager knows the agent is in partial-run
|
|
/// mode and can flag the operator.
|
|
NeedsLogin { agent: String },
|
|
/// An agent successfully completed claude login — the session
|
|
/// dir now contains creds. Transition fires once per login.
|
|
LoggedIn { agent: String },
|
|
/// An agent's recorded flake rev is stale relative to the
|
|
/// current hyperhive rev. The manager has the `update` tool to
|
|
/// trigger a rebuild without operator approval (it's a no-op
|
|
/// when nothing actually changed).
|
|
NeedsUpdate { agent: String },
|
|
/// Container exited without an operator-initiated stop. Fired by
|
|
/// the crash watcher when an agent's container transitions from
|
|
/// running → stopped and no `Stopping` / `Restarting` /
|
|
/// `Destroying` transient was set, so the operator (or the
|
|
/// manager) knows it crashed rather than was killed on purpose.
|
|
ContainerCrash {
|
|
agent: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
note: Option<String>,
|
|
},
|
|
/// A question queued via `Ask` was answered (by the operator via
|
|
/// the dashboard, or by another agent via `Answer`). `id` matches
|
|
/// the `QuestionQueued.id` returned to the asker; `question`
|
|
/// echoes the original prompt so the asker can stitch the answer
|
|
/// back to context across compactions; `answerer` is who answered
|
|
/// (`"operator"` or a peer agent name).
|
|
QuestionAnswered {
|
|
id: i64,
|
|
question: String,
|
|
answer: String,
|
|
answerer: String,
|
|
},
|
|
/// A peer (or the manager) asked this agent a question via
|
|
/// `Ask { to: Some(<this-agent>), ... }`. The recipient should
|
|
/// answer via `Answer { id, answer }` on their socket; the answer
|
|
/// will route back to the asker as a `QuestionAnswered` event.
|
|
/// `options` + `multi` mirror the original `Ask` args so the
|
|
/// answerer knows what shape of reply is expected.
|
|
QuestionAsked {
|
|
id: i64,
|
|
asker: String,
|
|
question: String,
|
|
#[serde(default)]
|
|
options: Vec<String>,
|
|
#[serde(default)]
|
|
multi: bool,
|
|
},
|
|
}
|
|
|
|
/// Requests on the manager socket. Manager has the agent surface (send/recv)
|
|
/// plus privileged lifecycle verbs.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "cmd", rename_all = "snake_case")]
|
|
pub enum ManagerRequest {
|
|
Send {
|
|
to: String,
|
|
body: String,
|
|
},
|
|
/// Same shape as `AgentRequest::Recv` — caller-tunable long-poll
|
|
/// duration, capped at 60s server-side, default 30s when None.
|
|
Recv {
|
|
#[serde(default)]
|
|
wait_seconds: Option<u64>,
|
|
},
|
|
/// Non-mutating: pending message count, used to render a status line
|
|
/// after each MCP tool call (mirrors `AgentRequest::Status`).
|
|
Status,
|
|
/// Operator-injected message TO the manager (from the manager's own web
|
|
/// UI). Same shape as `AgentRequest::OperatorMsg`.
|
|
OperatorMsg {
|
|
body: String,
|
|
},
|
|
/// Last `limit` messages addressed to the manager, newest-first.
|
|
/// Non-mutating; mirror of `AgentRequest::Recent`.
|
|
Recent {
|
|
limit: u64,
|
|
},
|
|
/// Submit a spawn request for the user to approve. On approval the host
|
|
/// creates and starts the container. Brand-new agent names only — if an
|
|
/// agent of the same name already exists, the approval will fail.
|
|
RequestSpawn {
|
|
name: String,
|
|
/// Optional description shown on the dashboard approval card.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
description: Option<String>,
|
|
},
|
|
/// Stop a sub-agent (graceful).
|
|
Kill {
|
|
name: String,
|
|
},
|
|
/// Start a previously-stopped sub-agent container.
|
|
Start {
|
|
name: String,
|
|
},
|
|
/// Restart a sub-agent container (stop + start).
|
|
Restart {
|
|
name: String,
|
|
},
|
|
/// Rebuild a sub-agent: re-applies the current hyperhive flake +
|
|
/// agent.nix, restarts the container. No approval required —
|
|
/// it's idempotent and the manager owns its own update cadence.
|
|
Update {
|
|
name: String,
|
|
},
|
|
/// Submit a config commit for the user to approve. `commit_ref` is opaque
|
|
/// to the host (typically a git sha pointing into the agent's config repo).
|
|
/// On approval the host applies the change via `nixos-container update`.
|
|
RequestApplyCommit {
|
|
agent: String,
|
|
commit_ref: String,
|
|
/// Optional description shown on the dashboard approval card so the
|
|
/// operator knows what the change does without opening the diff.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
description: Option<String>,
|
|
},
|
|
/// Surface a question to either the operator or another agent.
|
|
/// Mirrors `AgentRequest::Ask` exactly — see that doc for the
|
|
/// routing semantics (operator = dashboard queue; agent = the
|
|
/// peer's inbox via `HelperEvent::QuestionAsked`).
|
|
///
|
|
/// - `options` is advisory: empty = free-text only; non-empty = the
|
|
/// dashboard renders the choices alongside a free-text fallback
|
|
/// ("Other…") so the operator is never trapped.
|
|
/// - `multi=true` lets the operator pick multiple options (rendered
|
|
/// as checkboxes). The answer is returned as a single string with
|
|
/// selections joined by ", ".
|
|
/// - `ttl_seconds`: optional auto-cancel after that many seconds. On
|
|
/// expiry the question is resolved with answer `[expired]` and the
|
|
/// asker gets the usual `QuestionAnswered` event. None = wait
|
|
/// forever for an answer (or manual cancel).
|
|
/// - `to`: recipient (None / `Some("operator")` = operator;
|
|
/// `Some(<agent>)` = peer agent).
|
|
Ask {
|
|
question: String,
|
|
#[serde(default)]
|
|
options: Vec<String>,
|
|
#[serde(default)]
|
|
multi: bool,
|
|
#[serde(default)]
|
|
ttl_seconds: Option<u64>,
|
|
#[serde(default)]
|
|
to: Option<String>,
|
|
},
|
|
/// Answer a question previously routed to the manager via
|
|
/// `HelperEvent::QuestionAsked` (i.e. an agent asked the manager
|
|
/// for input). Mirror of `AgentRequest::Answer`.
|
|
Answer { id: i64, answer: String },
|
|
/// Fetch recent journal lines for a sub-agent container. hive-c0re
|
|
/// runs `journalctl -M <agent> -n <lines> --no-pager` and returns
|
|
/// the output as a string. Useful for diagnosing MCP registration
|
|
/// failures, startup crashes, and harness errors.
|
|
///
|
|
/// `lines` defaults to 50 when omitted.
|
|
GetLogs {
|
|
agent: String,
|
|
#[serde(default)]
|
|
lines: Option<u32>,
|
|
},
|
|
/// Mirror of `AgentRequest::Remind` on the manager surface — schedule
|
|
/// a reminder addressed to the manager itself. Same semantics: body
|
|
/// soft-caps at 4 KiB, oversize bodies auto-persist to
|
|
/// `/state/reminders/auto-<ts>.md` (the manager container's own state
|
|
/// mount) and the inbox sees a pointer.
|
|
Remind {
|
|
message: String,
|
|
timing: ReminderTiming,
|
|
#[serde(default)]
|
|
file_path: Option<String>,
|
|
},
|
|
/// Hive-wide loose-ends view: EVERY pending approval + EVERY
|
|
/// unanswered question in the swarm. Used by the manager to scan
|
|
/// for stalled coordination — the per-agent equivalent on the
|
|
/// sub-agent surface is `AgentRequest::GetOpenThreads` which
|
|
/// only returns rows where the agent itself is asker / target.
|
|
GetOpenThreads,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
pub enum ManagerResponse {
|
|
Ok,
|
|
Err {
|
|
message: String,
|
|
},
|
|
Message {
|
|
from: String,
|
|
body: String,
|
|
},
|
|
Empty,
|
|
Status {
|
|
unread: u64,
|
|
},
|
|
/// Result of `Ask`: the queued question id. The actual answer
|
|
/// arrives later as a `HelperEvent::QuestionAnswered` in the
|
|
/// asker's inbox, so this returns immediately rather than blocking
|
|
/// the turn.
|
|
QuestionQueued {
|
|
id: i64,
|
|
},
|
|
/// `Recent` result: mirror of `AgentResponse::Recent`.
|
|
Recent {
|
|
rows: Vec<InboxRow>,
|
|
},
|
|
/// `GetLogs` result: journal lines for the requested container.
|
|
Logs {
|
|
content: String,
|
|
},
|
|
/// `GetOpenThreads` result: hive-wide loose ends (approvals +
|
|
/// unanswered questions). Same `OpenThread` variants as the
|
|
/// agent surface; the manager's view is unfiltered.
|
|
OpenThreads {
|
|
threads: Vec<OpenThread>,
|
|
},
|
|
}
|