agents
Running instances. You start by name and then address by instance — one agent can have many live instances, so a verb taking a name has no answer for "which one".
interface AgentsApi {
// Reading.
targets(): Promise<readonly AgentTarget[]> // everything that can be started
list(): readonly AgentInstance[] // running, in spawn order
get(id: string): AgentInstance | null
focused(): AgentInstance | null
// Lifecycle. Throws INSTANCE_NOT_RUNNING on an id nothing answers to.
spawn(name: string): Promise<AgentInstance> // always NEW, in the background
focus(id: string): void
stop(id?: string): Promise<void> // omit id for the focused instance
reboot(id?: string): Promise<void> // omit id for the focused instance
// Conversation.
send(content: string): Promise<void> // to the FOCUSED instance. Yields nothing.
interrupt(): boolean
}
type AgentInstance = {
readonly id: string // stable; every verb addresses by this
readonly name: string // the project — several instances may share one
readonly focused: boolean
readonly activity: "booting" | "rebooting" | "shutting-down" | "working" | null
}
type AgentTarget = {
readonly name: string
readonly kind: "local" | "deployed"
readonly instances: readonly AgentInstance[] // most recently focused first
}
Spawn, focus, send
const reviewer = await agents.spawn("@axon/reviewer")
agents.focus(reviewer.id)
await agents.send("review main")
spawn runs in the background and does not take the screen — a config booting two
agents would otherwise have them race for it, and neither asked to be looked at. It
returns the instance because a.id !== b.id for two spawns of the same agent: "the one I
just started" cannot be recovered from the name.
send targets whatever is focused, which is why the middle line is required.
send yields nothing, deliberately
const reply = await agents.send("what changed?") // void
An extension drives a conversation and never consumes one. Branching on model output is an agent's job — build a cognet. Enforced by the type.
A send during a switch is queued for the agent arriving, not delivered to the one being left.
stop vs reboot
await agents.reboot(id) // rescan + hot-swap. The conversation survives.
await agents.stop(id) // the only verb here that ends a conversation.
Focusing never stops what you were on.
Acting on what is running
commands.register("stop all", {
async run() {
const running = agents.list()
if (!running.length) return tui.info("nothing running")
if (!await palette.confirm(`Stop ${running.length} instances?`)) return
for (const i of running) await agents.stop(i.id)
},
description: "Shut down every instance",
})
list() is spawn order and stable — a list you navigate must not reorder under the
cursor.
interrupt
keys.register("ctrl+alt+x", () => {
if (!agents.interrupt()) tui.warn("nothing to interrupt")
})
Stops the focused instance's current wake, as Escape does. Interrupted is a settled
outcome, not an error — nothing is lost from the conversation.
An id is not a guarantee
if (!agents.get(savedId)) return // it may have stopped on its own
agents.focus(savedId)