An agent's subagent daemon publishes each subagent's output as terminal
rows on `$SWARM.term.<agent>.sub.<subagent>`, as the agent, into a
per-agent stream it creates itself; swarm-controller lists an agent's
subagents from that stream's subjects and relays one subagent's rows as
SSE; the swarm UI lists them under the agent's terminal preview and
reuses AgentTermPreview, full-screen tab included, with no input.
- swarm-nats.nix: the agent token may also publish
`$SWARM.term.{agent}.sub.>` and `$JS.API.STREAM.CREATE|INFO` on
`term-sub-{agent}`, and nothing else of JetStream. A module-eval arm
pins the agent-token grant as an exact list.
- mcp.nix: hive-subagent-daemon loads the agent's store identity
(`hive-agent-bao-cert/-key/-server-ca`, the ones hive-agent loads)
whenever the agent has a store, not only on the opencode preset. The
agent's own queue secret lives in the store, so this is the credential
the harness connects with.
- hive-subagent-mcp: `swarm_term` reads the agent's queue secret under
that identity, connects with the agent token, opens or creates
`term-sub-<agent>` (max_age 24h), and publishes classified rows from
the sink every subagent line already passes through. The sink only
queues (bounded, drop-and-count); a missing store, refused credential,
failed stream create or failed publish is a log line.
- The stream-json classifier (`stream_enrich`) and the `TermMsg` row
types plus `fit` move from the hive-agent binary into hive-sh4re, so
the subagent daemon publishes the rows AgentTermPreview already
renders. hive-agent keeps its LiveEvent classifier on top.
- swarm-controller: `GET /api/agents/{name}/subagents` and
`GET /api/agents/{name}/subagents/{subagent}/term/stream`.
- docs/swarm: what the UI shows and what the queue carries.
Closes #4827
76 lines
2.7 KiB
TypeScript
76 lines
2.7 KiB
TypeScript
// <SubagentTerms> — an agent's subagents, under its own terminal preview in
|
|
// `AgentsPage`'s detail panel. The list is `GET /api/agents/{name}/subagents`:
|
|
// the subagents the swarm queue holds terminal rows for. Subagents are
|
|
// spawned on demand, so it is refetched every `REFRESH_MS`. Picking one
|
|
// shows its terminal in `AgentTermPreview`, expand-to-tab included. Read-only
|
|
// like the agent's own: subagents take no input.
|
|
//
|
|
// Mount it keyed by agent name: the refresh timer is armed once per mount.
|
|
import { useState } from "preact/hooks";
|
|
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
|
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
|
import { Badge } from "@hive/shared/badge.js";
|
|
import { AgentTermPreview } from "./AgentTermPreview.js";
|
|
import { useRefreshInterval } from "../../ui/refresh-interval/RefreshInterval.js";
|
|
import "./SubagentTerms.css";
|
|
|
|
const REFRESH_MS = 10_000;
|
|
|
|
export function SubagentTerms({ agentName }: { agentName: string }) {
|
|
const [names, setNames] = useState<string[] | null>(null);
|
|
const [error, setError] = useState<ProblemDetails | null>(null);
|
|
const [selected, setSelected] = useState<string | null>(null);
|
|
|
|
async function refresh() {
|
|
const res = await fetch(
|
|
`/api/agents/${encodeURIComponent(agentName)}/subagents`,
|
|
);
|
|
if (!res.ok) {
|
|
setError(await readApiError(res));
|
|
return;
|
|
}
|
|
setNames((await res.json()) as string[]);
|
|
setError(null);
|
|
}
|
|
|
|
useRefreshInterval(REFRESH_MS, () => {
|
|
refresh().catch((e: unknown) => setError({ detail: String(e) }));
|
|
});
|
|
|
|
return (
|
|
<section class="ui-subagent-terms" aria-label={`${agentName}'s subagents`}>
|
|
<div class="ui-subagent-terms-list">
|
|
<span class="ui-subagent-terms-heading">subagents</span>
|
|
{names === null && !error && <Badge value="loading…" />}
|
|
{names?.length === 0 && (
|
|
<span class="ui-subagent-terms-empty">
|
|
none with output on record
|
|
</span>
|
|
)}
|
|
{names?.map((name) => (
|
|
<Badge
|
|
key={name}
|
|
variant={name === selected ? "default" : "quiet"}
|
|
value={name}
|
|
expanded={name === selected}
|
|
title={
|
|
name === selected
|
|
? `hide ${name}'s terminal`
|
|
: `show ${name}'s terminal`
|
|
}
|
|
onClick={() => setSelected(name === selected ? null : name)}
|
|
/>
|
|
))}
|
|
</div>
|
|
{error && (
|
|
<ApiErrorPanel
|
|
problem={error}
|
|
context={`failed to list ${agentName}'s subagents`}
|
|
/>
|
|
)}
|
|
{selected && (
|
|
<AgentTermPreview agentName={agentName} subagent={selected} />
|
|
)}
|
|
</section>
|
|
);
|
|
}
|