Skip to Content
Card SDKСправочник APIПорты: карточки говорят с карточками

Порты: карточки говорят с карточками

Порты позволяют карточке обмениваться типизированными данными с карточками, соединёнными с ней стрелками: ваш радар тестов отправляет упавшие тесты в список задач, заметка передаёт свой текст вашему резюмирующему инструменту, две свои карточки договариваются о собственном формате. Порты объявляются в манифесте; приложение само маршрутизирует, преобразует и проверяет каждое значение.

1/6Своя карточка Test radar объявляет в манифесте два выхода: failures (ns:tasks) и summary (ns:markdown).

Направление

Стрелка от карточки A к карточке B несёт выходы A на входы B. Если карточки соединены в обе стороны, данные идут в обе стороны. (Инструментам и разрешениям агентов направление не важно, портам — важно.)

card.ports.peers перечисляет все карточки, соединённые с вашей, с полем direction:

  • downstream — ваша карточка → соседняя: ваши выходы доходят до её входов;
  • upstream — соседняя → ваша: её выходы доходят до ваших входов;
  • both — в обе стороны.

Типы

У каждого порта есть тип: общеизвестный ns:* или свой <package-name>/<type-name> с JSON Schema.

ТипЗначение
ns:anyЛюбой JSON. Как вход принимает любой тип выхода без изменений.
ns:textСтрока (до 1 000 000).
ns:markdownСтрока Markdown (до 1 000 000).
ns:numberЧисло.
ns:booleantrue или false.
ns:jsonОбъект или массив.
ns:urlСтрока URI.
ns:image{ mimeType, data, alt? } — PNG, JPEG, WebP или GIF в base64, не больше ~700 КБ.
ns:file-ref{ path, line? } — файл в папке воркспейса. Получение такого значения не даёт доступа к файлам.
ns:task{ text, id?, status?, column?, assignee? } — status: pending, active, done или error.
ns:tasksМассив до 200 ns:task.
ns:task-patch{ ref, text?, status?, column? } — изменить одну задачу; ref — её идентификатор или точный текст.
ns:table{ columns: string[], rows: (string, number, boolean or null)[][] }
ns:event{ type, data?, at? }
ns:triggerПустой сигнал: {} или null.

Свой тип называется по имени вашего пакета — test-radar/coverage — и обязан иметь schema (безопасное подмножество, без pattern). Другая карточка может объявить тот же тип, чтобы работать с вашей. Любой порт может добавить schema поверх своего типа; значение должно подходить под обе.

Отправка: emit

const delivered = await card.ports.emit('failures', [ { text: 'checkout › pays with a saved card', status: 'error' }, { text: 'auth › logs in with SSO', status: 'error' } ]) if (delivered === 0) card.ui.toast('Connect me to a todo list to track these')

Значение проверяется по схемам выхода и доставляется каждой соседней карточке ниже по стрелке, у которой есть совместимый вход. У каждой из них приложение выбирает вход: помеченный default, иначе вход ровно того же типа, иначе первый совместимый (входы-запросы через emit не выбираются никогда). Если типы различаются, значение преобразуется (см. ниже) и проверяется по схемам этого входа. Вы получаете число карточек, которые его приняли.

Чтобы отправить одной конкретной карточке и, при желании, в конкретный вход:

await card.ports.send(noteId, 'summary', '## Nightly run\n\nAll green.', { input: 'replace' })

Приём

card.ports.onMessage<string>((text, message) => { render(`${message.fromKind} card ${message.from} sent ${message.type} on ${message.output}`, text) }, { input: 'notes' })

message — это { from, fromKind, output, input, type, sourceType, data, at }: type — тип вашего входа (после преобразования), sourceType — объявленный тип выхода отправителя до преобразования (в старых версиях приложения отсутствует). Без input вы получаете значения со всех входов. Сообщения, пришедшие до подписки, сохраняются (до 100) и доставляются первому слушателю.

Преобразование типов

Выход доходит до входа другого типа, только если для этой пары есть преобразование:

Тип входаПринимает выходы типаКак
тот же типтот же типбез изменений
ns:anyлюбыебез изменений
ns:textmarkdown, urlбез изменений
number, booleanString(value)
jsonотформатированный JSON
ns:markdowntext, urlбез изменений
numberString(value)
jsonблок кода json
tableтаблица Markdown
tasksчек-лист (- [x] done, - [ ] open)
ns:taskstaskоборачивается в массив
text, markdownпо задаче на каждую непустую строку (маркеры списков и чекбоксы убираются)
ns:jsontable, tasks, task, event, file-ref, task-patchбез изменений
ns:triggerevent, text, number, boolean, jsonстановится {} — «что-то произошло»

Свои типы совпадают только с тем же своим типом (или с ns:any). Помощники portsCompatible, portCoercion, coercePortValue и pickInputFor экспортируются из SDK, если хотите рассуждать об этом сами.

Запросы: спросить и ответить

Вход с "mode": "request" отвечает на вопросы. Тип ответа объявляется в response:

{ "id": "lookup", "label": "Look up", "type": "ns:text", "mode": "request", "response": { "type": "ns:json" } }
// The answering card: card.ports.onRequest<string>('lookup', async (query, request) => { const hits = await search(query, request.signal) // request.signal aborts at the deadline return { query, hits } // checked against response.type }) // The asking card (connected to it by an arrow, either direction): const answer = await card.ports.request<{ hits: string[] }>(peerId, 'lookup', 'flaky tests', { timeoutMs: 10_000 }) render(answer.hits) declare function search(q: string, signal: AbortSignal): Promise<string[]> declare const peerId: string

Исключение в обработчике возвращает ошибку; промис спрашивающего отклоняется. Запросы, пришедшие до регистрации обработчика, ждут почти до своего дедлайна, а потом получают «no handler». Тайм-аут по умолчанию 30 с, максимум 120 с (TIMEOUT). Запрос к усыплённой карточке будит её (до 10 с) или падает с UNAVAILABLE.

Сохраняемые значения

Пометьте выход "retain": true, и приложение будет хранить его последнее значение (до 256 КБ):

  • карточка, подключённая позже, сразу получает его один раз;
  • любая подключённая карточка может прочитать его в любой момент, куда бы ни смотрела стрелка:
const last = await card.ports.read<{ text: string }[]>(todoId, 'items') if (last) render(`${last.data.length} items, as of ${new Date(last.at).toLocaleTimeString()}`) declare const todoId: string

Именно через сохраняемые значения карточка догоняет события после усыпления — потоковые сообщения, отправленные, пока её страница была выгружена, в очередь не ставятся.

Кто подключён: обнаружение

Карточка может узнать, что выдают и принимают подключённые к ней карточки — включая встроенные, — и подстроиться:

import type { PeerInfo } from '@neurosquad/card-sdk' function describePeer(peer: PeerInfo): string { const ins = peer.inputs.map((p) => `${p.id}:${p.type}`).join(', ') || 'none' const outs = peer.outputs.map((p) => `${p.id}:${p.type}`).join(', ') || 'none' return `${peer.name} (${peer.kind}, ${peer.direction}) — in: ${ins}; out: ${outs}` } card.ports.peers.forEach((peer) => render(describePeer(peer))) card.ports.onPeersChanged((peers) => render(peers.map(describePeer))) // Is there a checklist downstream that takes tasks? const taskSink = card.ports.peers.find( (p) => p.direction !== 'upstream' && p.inputs.some((i) => i.type === 'ns:tasks') ) render(taskSink?.name ?? 'no task list connected')

PeerInfo — это { cardId, kind, name, type?, direction, inputs, outputs }. kind — custom, note, todo, kanban, sticky, agent, terminal или other (карточка без портов, например браузер). type — харнесс для агентов и терминалов, имя пакета для своих карточек. Каждый порт — это PortInfo: { id, label, description?, type, schema?, mode, response?, retain, default, permission? }; permission задан у портов встроенных карточек и называет, что нужно вашей карточке, чтобы им пользоваться.

Ваши собственные порты: card.ports.inputs, card.ports.outputs или card.ports.describe().

Три помощника покрывают то, что нужно большинству карточек перед отправкой:

import { hasDownstreamPeer, permissionForPeer, requestPermissions } from '@neurosquad/card-sdk' async function sendSummary(text: string): Promise<void> { if (!hasDownstreamPeer(card.ports.peers)) { card.ui.toast('Draw an arrow from this card to a note') return } // Which permission does sending Markdown to each peer need? (cards.connected for a note) const needed = card.ports.peers .map((peer) => permissionForPeer(peer, { outputType: 'ns:markdown' })) .filter((id) => id !== null) // Asks only for what is missing; never throws — resolves with what is usable now. const usable = await requestPermissions(card, ...needed) if (usable.length === needed.length) await card.ports.emit('summary', text) }

Встроенные карточки

Собственные карточки приложения участвуют через адаптеры. Для работы с ними нужно разрешение из последней колонки — у вашей карточки.

КарточкаВходыВыходыНужно
Заметкаappend (ns:markdown, по умолчанию): добавляет абзац в конец · replace (ns:markdown): заменяет всю заметкуtext (ns:markdown, сохраняется): заметка, при каждом измененииcards.connected
Список задачadd (ns:tasks, по умолчанию): добавляет пункты · update (ns:task-patch): меняет текст или статус одного пунктаitems (ns:tasks, сохраняется): все пункты, при каждом измененииcards.connected
Доска задачadd (ns:tasks, по умолчанию): добавляет задачи без исполнителя · update (ns:task-patch): переносит или правит одну задачуtasks (ns:tasks, сохраняется): все задачи с их колонкамиcards.connected
Стикерtitle (ns:text, по умолчанию): задаёт заголовокtitle (ns:text, сохраняется)cards.connected
ИИ-агентprompt (ns:text, по умолчанию): отправляет промпт — ровно как agents.prompt с параметрами по умолчаниюstatus (ns:event, сохраняется): { type: "status", data: { status } }agents.prompt / agents.read
reply (ns:markdown, сохраняется): итоговое сообщение в конце хода — Claude Code и Hermes Agentagents.output
Терминалcommand (ns:text, по умолчанию): выполняет одну командуexit (ns:event): { type: "exit", data: { command, exitCode } } после каждой командыterminals.write / agents.output

Так, если от вашей карточки к заметке идёт стрелка, card.ports.emit('summary', '# Done') допишет в неё абзац; если от списка задач к вашей карточке, ваш вход ns:tasks будет получать каждое изменение списка.

Лимиты

Значение до 1 МБ (сохраняемое — до 256 КБ); не больше 20 сообщений в секунду на выход. Значение, не прошедшее схему, отклоняется с INVALID_PARAMS и точным путём — приложение никогда не доставит то, что не соответствует объявленному получателем.

Между двумя своими карточками порты не требуют разрешений: согласие — это стрелка, которую провёл пользователь. Данные идут только по стрелкам, только из объявленных выходов в объявленные входы и только в направлении стрелки.