# Agents & terminals

> Listing agents and their live status, turn events, reading the screens of connected agents, sending prompts, and running commands in connected terminals.

Source: https://docs.neurosquad.ai/en/card-sdk/api/agents

## Listing agents

Needs [`agents.read`](https://docs.neurosquad.ai/en/card-sdk/permissions#agents-read).

```ts
import type { AgentInfo } from '@neurosquad/card-sdk'

const agents: AgentInfo[] = await card.agents.list()
const waiting = agents.filter((a) => a.kind === 'ai' && a.status === 'needs-input')
const one = await card.agents.get(agents[0].id)
render(waiting.map((a) => a.name), one.model ?? 'default model')
```

Every AI agent and terminal in the card's workspace (other cards are not agents):

| Field | Type | Meaning |
| --- | --- | --- |
| `id` | `string` | The card's id on the canvas. |
| `name` | `string` | What the user sees. |
| `harness` | `string` | `claude-code`, `codex-cli`, `opencode`, `qwen-code`, `shell-bash`, `shell-powershell`, `shell-cmd`… |
| `kind` | `'ai'` or `'shell'` | An AI agent or a terminal. |
| `status` | `AgentStatus` | `working`, `needs-input`, `finished`, `idle` or `exited`. |
| `connected` | `boolean` | An arrow connects it with your card. |
| `sessionTitle` | `string?` | The title the agent gave its session. |
| `turnStartedAt` | `number?` | Epoch ms the current turn started, while `working`. |
| `lastTurn` | `{ startedAt, endedAt, endedAs }?` | The last finished turn since the app started: epoch ms (`startedAt` may be `null`), and `endedAs` `finished` or `needs-input` — to catch up on turns your card missed while it was not running. |
| `model` | `string?` | The model it runs on, when known. |

Statuses come from the same tracker as the rest of the app — for Claude Code from its own hooks
(for OpenCode and Kilo Code from their NeuroSquad plugin, for Hermes Agent from its lifecycle webhooks),
so `needs-input` ("waiting for your permission") and `finished` are told apart exactly.

## Events

```ts
card.agents.onStatus(({ agentId, status }) => render(agentId, status))
card.agents.onTurn(({ agentId, phase, at }) => render(agentId, phase, new Date(at)))
card.agents.onChanged((agents) => render(agents.length))   // added, removed or renamed

// Only one agent:
card.agents.onStatus((e) => render(e.status), agentId)
```

- `agents.status` — `{ agentId, status, at }` whenever a status changes.
- `agents.turn` — `{ agentId, phase: 'start' | 'end', at }`. A turn starts when a prompt is
submitted and ends when the agent finishes or needs input.
- `agents.changed` — `{ agents }`, the whole new list.

The SDK subscribes while you have listeners. Each returns a function that removes it.

## Reading output

Needs [`agents.output`](https://docs.neurosquad.ai/en/card-sdk/permissions#agents-output), and an **arrow** between your card
and the agent or terminal.

```ts
// The visible screen, as text (up to 500 lines).
const screen = await card.agents.readScreen(agentId, 80)

// The last reply, from the agent's own session log (Claude Code, OpenCode, Kilo Code, Hermes Agent). null for others.
const { text, at } = await card.agents.lastReply(agentId)

// Live output: ANSI colours stripped, delivered in chunks about every 100 ms.
const stop = card.agents.onOutput([agentId, shellId], ({ agentId: from, text: chunk }) => {
  if (chunk.includes('FAIL')) render(`${from} printed a failure`)
})
stop() // when you no longer need it

render(screen, text, at)
```

Calling these for an agent without an arrow fails with `NOT_CONNECTED`. `readScreen` of an agent
that is not running fails with `UNAVAILABLE`.

## Prompting agents

Needs [`agents.prompt`](https://docs.neurosquad.ai/en/card-sdk/permissions#agents-prompt) and an arrow to an **AI** agent
(terminals take [commands](#terminals) instead).

```ts
import { isCardSdkError } from '@neurosquad/card-sdk'

try {
  const delivered = await card.agents.prompt(agentId, 'Run the checkout tests and fix what fails.')
  // 'sent'    — submitted now
  // 'queued'  — the agent is working; it goes out when the turn ends
  // 'inserted'— typed in only (submit: false); the user presses Enter
  card.ui.toast(delivered === 'queued' ? 'Queued after the current turn' : 'Sent')
} catch (error) {
  if (isCardSdkError(error, 'NOT_CONNECTED')) card.ui.toast('Draw an arrow from me to an agent')
  else if (isCardSdkError(error, 'BUDGET_PAUSED')) card.ui.toast('The workspace is over its budget')
  else if (isCardSdkError(error, 'USER_CANCELLED')) card.ui.toast('Not sent')
  else throw error
}
```

Options — `card.agents.prompt(agentId, text, { submit, whenBusy })`:

| Option | Default | Meaning |
| --- | --- | --- |
| `submit` | `true` | `false` types the text into the agent's input without pressing Enter — the user reviews and sends it. Result `inserted`. |
| `whenBusy` | `'queue'` | While the agent is working: `queue` puts it in the agent's [prompt queue](https://docs.neurosquad.ai/en/agents/queue) (`queued`), `send` submits anyway (`sent`), `fail` refuses with `BUSY`. |

What the app does with every prompt:

- It goes through the same path as the prompt queue and the [budget](https://docs.neurosquad.ai/en/cards/budget): over the
workspace's budget, it fails with `BUDGET_PAUSED` (a person typing is never limited — your card
is).
- It shows on the arrow and in its [arrow log](https://docs.neurosquad.ai/en/canvas/arrows#arrow-log) with your card's name.
- To an agent in [dangerous mode](https://docs.neurosquad.ai/en/agents/dangerous-mode), the app first shows the prompt in its own
window-wide dialog and waits for **Send prompt**; if the user declines — `USER_CANCELLED`; if your card is not on
screen — `NOT_VISIBLE`.
- At most 6 prompts a minute per card — counted together with prompts sent through the agent's
`prompt` port — ≤ 20 000 characters each.
- Queued prompts from cards are capped at 10 per agent, show which card they came from, and are
dropped before they go out if the arrow, the permission, the card or its package is gone, or the
agent was switched to dangerous mode.

> A prompt is an instruction to something that can edit files and run commands. Never forward text
> from the web, a file or another card into a prompt without showing it to the user first — that is
> how prompt injection happens. Prefer `submit: false` when the text is not yours.

## Terminals

Needs [`terminals.write`](https://docs.neurosquad.ai/en/card-sdk/permissions#terminals-write) and an arrow to a terminal card
(bash, PowerShell or cmd).

```ts
// Run a command and wait for it to finish.
const result = await card.terminals.run(shellId, 'npm test -- --reporter=dot', { timeoutMs: 120_000 })
if (result.timedOut) render('still running after 2 minutes')
else render(result.exitCode === 0 ? 'passed' : `failed with ${result.exitCode}`, result.output)

// Or just type into it (and press Enter).
await card.terminals.write(shellId, 'git status', { submit: true })
```

| | `run(agentId, command, { timeoutMs? })` | `write(agentId, text, { submit? })` |
| --- | --- | --- |
| Does | Runs one command and waits for the prompt to come back | Types text; `submit: true` presses Enter after it |
| Returns | `{ exitCode, output, truncated, timedOut }` | nothing |
| Limits | timeout 1–120 s (default 120), output ≤ 64 KB; 30 commands a minute per card, shared with the terminal's `command` port | 60 a minute |

`exitCode` is `null` in cmd, which does not report one. The user sees every command in the terminal
itself and on the arrow. Commands are not limited by the agents' budget — a terminal is not an AI
agent — but they run with the user's full rights: see the [security checklist](https://docs.neurosquad.ai/en/card-sdk/security).
