Skip to Content
Card SDKAPI referenceAgents & terminals

Agents & terminals

Listing agents

Needs agents.read.

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):

FieldTypeMeaning
idstringThe card’s id on the canvas.
namestringWhat the user sees.
harnessstringclaude-code, codex-cli, opencode, qwen-code, shell-bash, shell-powershell, shell-cmd…
kind'ai' or 'shell'An AI agent or a terminal.
statusAgentStatusworking, needs-input, finished, idle or exited.
connectedbooleanAn arrow connects it with your card.
sessionTitlestring?The title the agent gave its session.
turnStartedAtnumber?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.
modelstring?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

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, and an arrow between your card and the agent or terminal.

// 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 and an arrow to an AI agent (terminals take commands instead).

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 }):

OptionDefaultMeaning
submittruefalse 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 (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: 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 with your card’s name.
  • To an agent in 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 and an arrow to a terminal card (bash, PowerShell or cmd).

// 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? })
DoesRuns one command and waits for the prompt to come backTypes text; submit: true presses Enter after it
Returns{ exitCode, output, truncated, timedOut }nothing
Limitstimeout 1–120 s (default 120), output ≤ 64 KB; 30 commands a minute per card, shared with the terminal’s command port60 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.