TypeScript-фреймворк для AI agents с tool calling, structured output (Zod), agentic loops, multi-turn conversations и встроенным observability (Langfuse). Используется в BloodGPT для портирования Python-сервисов на TS-агенты в edge worktree bloodgpt-for-business-mastra-agents (ветка feat/mastra-agents).

Использование в BloodGPT

5 Mastra-агентов реализованы (или в процессе) в worktree feat/mastra-agents:

AgentDomainPatternStatusSource session
loincMappingAgentLOINC harmonization3-step decomposed workflow (agent-vs-workflow)Architectural parity достигнут с Python (BG-1140)833ec924, 82806132
normalizationAgentV2Parameter name normalizationSingle agent с словарь-fallbackProduction-ready под USE_LOCAL_NORMALIZATION flagfc708f24
surveyAgent (medical-context)Patient questionnaireMulti-turn agent (medical-context-survey)Реализован, BG-10507d777edb
patientSummaryAgentIPS Patient Summary retrievalAgent-for-retrieval — 4-phase agentic loopРеализован, BG-1049 (6/6 scenarios на SmartHealthIT)3d5c475c
healthChatAgentPatient chat interfaceSingle agent с FHIR write toolsПодключён к patient-portal /api/chatf09d1111

5 smart FHIR write tools (recordSymptom/recordMedication/recordAllergy/recordProcedure/recordFamilyHistory) — shared между chat / survey (V1.5) / TextToFHIR (V2). См. fhir-meta-tagging (как помечают write-events) + llm-numeric-codes-policy (как LLM кодирует terms) + clinical-record-reconciliation (dedup существующих).

Все агенты + tools перенесены в packages/analysis-core для shared usage (session f09d1111).

Workflows — декларативный примитив рядом с агентами

Mastra — это не только агенты. Второй примитив — workflow: ты описываешь конвейер декларативно (узлы-шаги + как они соединены), а Mastra даёт под него runtime, типизацию входов/выходов на каждом шаге и — главное, ради чего его и хочется брать — визуализацию: workflow рисуется графом в Mastra Studio (видно шаги, ветвления, что куда течёт; и трейс конкретного прогона поверх этого графа — § «Mastra Studio» ниже). Логика выгоды: один раз разложил пайплайн визуально, настроил, прогнал — и дальше его легко «перенести», потому что та же декларация и есть то, что побежит в проде (через @mastra/inngest она компилируется в Inngest-функцию — § «В проде» ниже). То есть workflow — не новый рантайм, это design surface: нативное моделирование пайплайна + визуализация + «настроил один раз — работает как есть».

Workflow vs агент: workflow — статичный граф, форму задаёшь вперёд; агент (agent.generate() — LLM в цикле с tools) — динамический loop, форму решает на лету сама модель. Граница «когда decompose в workflow, когда loop в агенте» — agent-vs-workflow (там же open: workflow-примитив под наш собственный use case ещё не пробовали — все 5 наших агентов либо single-agent, либо agentic loop; loincMappingAgent декомпозирован, но руками, не через createWorkflow).

Модель: workflow, шаги, композиция

  • createWorkflow({ id, inputSchema, outputSchema }) — определяет workflow с типизированным входом и выходом (Zod-схемы на границах). Дальше на нём строится цепочка, в конце — .commit() (финализирует определение).
  • createStep({ id, inputSchema, outputSchema, execute }) — один шаг: типизированная единица работы. execute({ inputData, ... }) возвращает что-то, что валидируется против outputSchema; выход одного шага = вход следующего (схемы должны стыковаться). Внутри execute — что угодно: LLM-вызов, обращение к БД/FHIR, трансформация, вызов tool’а.
  • .then(step) — выполнить шаг последовательно после предыдущего, прокинув output → input.
  • .parallel([a, b]) — выполнить независимые шаги конкурентно, собрать оба результата (вход у обоих — выход предыдущего шага).
  • .branch([[cond, step], [cond2, step2], ...]) — выбрать шаг по предикату над текущими данными (аналог switch): первый сматчившийся cond решает, какая ветка бежит.
  • .dowhile(step, cond) / .dountil(step, cond) — крутить шаг, пока (или: пока не) предикат над данными истинен. Типичное — «уточняй, пока качество ниже порога», «обрабатывай очередь, пока не пуста».
  • .foreach(step, { concurrency }) — выполнить шаг по разу на каждый элемент массива-входа; concurrency — сколько элементов параллельно.
  • .map(fn) — чистая трансформация данных между шагами (переформатировать, переименовать, выбрать поле). Не единица работы — клей.
  • Nested workflow — целый workflow можно подставить как шаг в другой. Композиция и переиспользование: общий под-конвейер описывается раз, вставляется в несколько мест.

Паузы и human-in-the-loop

Workflow умеет durable-паузы — прогон переживает паузу любой длины, не держа процесс:

  • step.sleep("id", "1h") / step.sleepUntil("id", date) — durable задержка (отложить следующий шаг на час / до даты).
  • step.waitForEvent("id", { event, timeout, ... }) — пауза до прихода внешнего события (например — дождаться, что врач закрыл review; timeout: "7d").
  • suspend() внутри execute + resumeSchema у шага — workflow приостанавливается, отдаёт управление наружу и позже возобновляется новым входом по resumeSchema. Классический HITL: показать человеку промежуточный результат → ждать его ответ → продолжить с этим ответом. Снаружи workflow, растянутый на минуты-дни, пишется как обычная синхронная функция.

В проде: компиляция в Inngest-функцию

In-process-раннер Mastra годится для дева/Studio. В проде workflow гоняется на Inngest: @mastra/inngestinit(inngest) отдаёт createWorkflow / createStep, привязанные к Inngest, и тогда workflow компилируется в Inngest-функцию, а каждый Mastra-шаг — в step.run под капотом. То есть бесплатно получаешь durability / retries / observability Inngest, и в проде бежит ровно то, что разложил и видел в Studio. Мапинг прямой:

MastraInngest
createWorkflow(...) (один workflow)inngest.createFunction(...) — одна function = один run
createStep(...) в .then(...)step.run("id", ...) — memoized retry-checkpoint
.parallel([a, b])несколько step.run, выполняемых конкурентно
.branch([[cond, step], ...])обычный if/switch вокруг step.run
.dowhile(step, cond) / .dountilwhile-цикл вокруг step.run с пронумерованными ключами итераций
.foreach(step, { concurrency })цикл step.run по массиву (или fan-out через sendEvent, если упёрся в лимит шагов)
.map(fn)трансформация данных между шагами (не checkpoint)
nested workflow как .then-шагstep.invoke() дочерней функции — TBD verify: компилит ли @mastra/inngest вложенный workflow в отдельную Inngest-функцию или инлайнит его шаги
suspend() + resume (HITL)step.waitForEvent()
.sleep() / .sleepUntil()step.sleep() / step.sleepUntil()
agent.generate() (агентский loop)не workflow-примитив — тело одного step.run (так и в проде: normalizeAndHarmonizeStep оборачивает Mastra-агента)

Что такое сама Inngest-функция и шаг (substrate, на который это компилится) — Function vs Step — два уровня. Коротко: function — это job целиком (привязана к триггеру event/cron, на ней живёт config — concurrency / retries / idempotency / debounce / rateLimit, она извне вызываема и планируема); step — durable checkpoint внутри function (step.run("id", fn): при crash шаги 0..N-1 проигрываются из кэша, заново бежит только упавший; id — стабильный ключ мемоизации; своего retry-счётчика у шага нет — это retry функции).

Что наследуется от Inngest — и где граница пользы Mastra

Раз workflow → Inngest-функция, на него ложатся ограничения Inngest, и дизайн пайплайна под них подстраивается:

  • Config — на уровне функции, не отдельного шага. Когда @mastra/inngest сворачивает workflow в одну Inngest-функцию, ручки concurrency / retries / idempotency стоят на этой функции и действуют на весь прогон workflow целиком. Нельзя сказать «вот этому шагу retries: 1, остальным 4» или «ограничь этот шаг до одного прогона на testId». Нужно такое — этот шаг придётся вынести в отдельную Inngest-функцию (или nested workflow, который компилится в свою функцию). Та же дыра, что у голого Inngest — per-step options у него в roadmap.
  • Лимит 1000 шагов на функцию распространяется на скомпилированный workflow. .foreach по 500 элементам × 2 шага каждый = 1000 — упрёшься; дизайнить вокруг (батчить тело цикла либо fan-out через события). См. Лимиты.
  • Лимиты размера state (4 MB на step output, 32 MB на run state) — данные между Mastra-шагами едут через Inngest run state. Поэтому workflow, обрабатывающий тяжёлые payload’ы (FHIR-ресурсы, PDF), проектируется так, чтобы между шагами гонять ID/handle, а байты держать во внешнем store — ограничение формирует дизайн, не наоборот. См. Передача данных между шагами — через store, не через step output.
  • Динамическая форма пайплайна — слабое место Mastra. Builder eager: граф пишется один раз, на этапе определения; он не может сослаться на самого себя и не может отрастить новые ветки по данным, которые видны только в рантайме. Если форма работы зависит от данных — скажем, шаг разбивает большой вход на куски, и сколько кусков выяснится только при прогоне; или обход дерева неизвестной глубины — это уже не граф, который рисуется заранее. Такое сворачивается в один шаг с worklist + .dowhile-циклом внутри либо в императивный код в createStep — и тогда Mastra видит один непрозрачный шаг, и визуализация (то, ради чего Mastra и брали) динамическую часть не показывает. Отсюда граница: выигрыш Mastra — на статичном скелете пайплайна (часть, которую раскладываешь и видишь графом один раз); по-настоящему динамический control-flow — это тот же код, который пишешь и против голого Inngest SDK. Контекст — no-self-rolled-queues, biomarker-actuality-integration § Inngest-декомпозиция.

Mastra Studio + Direct mode

Два режима для testing:

  • Mastra Studio (localhost:4111) — UI для chat-интерактивного тестирования agent. Полезно для debug одного scenario с visual real-time tool call traces. Читает STUDIO_* env vars при старте — env-changes требуют restart.
  • Direct mode через agent.generate() + tsx --env-file=.env <script> — для batch testing без UI. Patient-context передаётся per-request через setPatientContext(). Без рестарта между сценариями.

Gotchas / debugging patterns

Zod inputSchema стрипает unknown fields в createTool

Critical pattern обнаруженный в session 82806132 (BG-1140 LOINC closeout):

Mastra createTool({ inputSchema: z.object({...}) }) валидирует input через zod. Поля не описанные в схеме — стрипаются до того как tool execute их видит. TypeScript as any cast компилируется, но runtime валидация удаляет.

Симптом: tool ведёт себя как будто получает другой input. Score не меняется после fix логики; “console.log внутри tool не видит ожидаемое поле”.

Fix: явно добавлять каждое expected поле в inputSchema zod schema.

Это применимо ко всем Mastra tools — общий issue.

STUDIO_FHIR_PATIENT_ID (и подобные STUDIO_* env) загружается один раз при старте

Studio не перечитывает env между requests. Чтобы сменить patient — restart Studio. Direct mode (agent.generate()) обходит — patient-context передаётся per-call.

tsx не загружает .env автоматически

Studio и Inngest dev-server делают это сами. Standalone tsx <script> — нет. Решение: tsx --env-file=.env <script> (Node.js native flag).

Tool format mismatch между Studio API и agent.generate()

Парсинг result отличается:

  • Studio API: step.toolCalls[i].toolName
  • Direct (agent.generate()): step.toolCalls[i].payload.toolName

Аналогично для usage:

  • Studio: result.usage
  • Direct: result.totalUsage

Если переиспользуем парсер между UI и batch — нужны два варианта.

Mastra скрывает 4 слоя default-инъекций

Critical pattern обнаруженный в session 4791d030 (BG-1191 V2 param analysis):

agent.generate(msg)
  ↓
Mastra loop (memory, tool retry, step-counter)
  ↓
ai-sdk generateText (provider abstraction, retries, stream-vs-non-stream)
  ↓
@ai-sdk/openai provider (model-specific defaults, tokenizer config)
  ↓
OpenAI SDK (HTTP client, headers, base URL)

Каждый слой инжектирует defaults. Для parity с Python (openai.AsyncOpenAI.chat.completions.create(**kwargs) — no hidden decisions) нужно знать или обойти каждый default. Это уровень глубже чем zod stripping.

Symptom: payload содержит другие поля чем ожидаешь; performance отличается; iteration count не такой как в reference.

Debugging path: dump actual payload что идёт по wire (через bifrost trace или direct OpenAI client wrap), сравнить с reference Python.

Escape hatch: для one-shot prompts с structured output — обойти Mastra и вызвать ai-sdk/openai напрямую (без agentic loop). Сохраняет ai-sdk benefits без Mastra overhead. См. примечание в agent-vs-workflow про when-to-use AI SDK direct.

Уроки портажа Python→TS, которые всплыли на Mastra-агентах, но к самой Mastra не относятся (валидация только через реальный LLM-вызов, обязательный production-reference output, snake_case↔camelCase boundary conversion, точное воспроизведение порядка полей схемы, Promise.all-advantage, side-by-side VERBOSE comparison) — вынесены в python-to-ts-portage.

Langfuse integration

Mastra поддерживает Langfuse tracing out-of-box — каждый tool call, prompt, response записывается с latency через LANGFUSE_* env vars.

Langfuse-проект уже создан, но Mastra-агенты к нему пока не подключены — поэтому fallback на manual log из result.steps[].toolCalls/toolResults, сохраняемый в файл. Carry-over: подключить агентов к существующему проекту; статус Langfuse-интеграции стоит расписать отдельной заметкой.

.mastra.ts naming convention для co-existence

Когда V1 (Python-style logic в TS, либо ai-sdk direct) и V2 (Mastra) сосуществуют в codebase до finalize migration — .mastra.ts suffix маркирует Mastra-вариант. Из 18b47185 (V2 param analysis):

  • retriever.ts / generator.ts — primary (когда parity достигнут и Mastra становится default)
  • <service>.mastra.ts suffix — где есть оба варианта параллельно

Не общая convention (loincMappingAgent etc уже без suffix). Используется только в transition periods. После V2 deploy — V1-варианты удаляются, suffix снимается.

Adapter pattern для transition к FHIR-only storage

V2 enrichment пишет typed Mastra output. Текущий downstream ожидает Prisma rows. Pattern:

V2 Mastra output (Zod-typed)
   ↓
adapter (services/parameter-analysis-to-prisma.ts) — pure mapping function
   ↓
Prisma row shape — Prisma calls в отдельной step

Adapter без I/O, без Prisma вызовов внутри — testable отдельно. Прежде чем full FHIR-only read path готов (см. fhir-modeling-ai-content [О1]) — adapter сохраняет backward compatibility.

Это transition pattern — adapter удаляется, когда все consumers переедут на FHIR-as-source-of-truth. Carry-over: проверить статус — возможно read path на FHIR уже готов и adapter снят (см. docs/STORAGE_ARCHITECTURE.md migration path / fhir-modeling-ai-content).

AI SDK direct vs Mastra agentic loop — когда какое

Это архитектурная дилемма, которая всплывает на каждом новом LLM-сервисе: использовать Mastra agent или зайти ниже, через ai-sdk/openai напрямую. Mastra добавляет 4 слоя abstraction (см. gotchas) — это плата за agent-loop возможности. Без agent-loop эти 4 слоя — оверхед без пользы.

Маркеры выбора:

Признак сервисаMastraAI SDK direct
Multi-turn / dialog state✓ (Survey, health-chat)
Agentic loop с tool calls + reasoning между ними✓ (PatientSummaryAgent retrieval)
Interactive debugging через Studio
One-shot LLM call: typed input → typed outputoverkill✓ (generateObject)
Performance критичен (parity с Python)latency hit от wrapper
Reproducibility (hidden defaults не ОК)плохо — defaults внутри Mastra layers✓ — control в одном месте

Эвристика: сервис делает один LLM-вызов (recognition, single classifier, fact-extraction) — AI SDK direct. Сервис ведёт диалог или ходит в инструменты между шагами reasoning — Mastra.

Carry-over: эта дилемма заслуживает отдельной decision-page (technical/mastra-vs-ai-sdk-direct.md) — здесь она embedded внутри Mastra-entity, но сам вопрос архитектурный и применяется к каждому новому LLM-сервису. Сейчас она читается как «escape hatch внутри Mastra», что недо-отражает значимость.

  • Pipeline-step как часть Inngest function, не agent-loop

В session 4791d030 (V2 param analysis) обе версии разрабатывались параллельно — final выбор open. См. agent-vs-workflow для broader pattern discussion.

Cascade fallback

Mastra не имеет явного cascade primitive (Gemini → GPT-5.2) per-step — нужно реализовывать вручную через try/catch + retry-with-different-model. В Python production cascade простой и явный, см. loinc про production model config.

Это архитектурная разница между Python production и TS Mastra — повлияла на parity gap в LOINC pipeline (часть, не основная).

Workspace + Sandbox primitive (доступно, не используется)

@mastra/core@1.1.0 (наша версия) шипит Workspace + WorkspaceSandbox interface с готовой реализацией LocalSandbox. Изоляция:

  • Linux — через bubblewrap (bwrap), kernel namespace isolation: mount / PID / network / user. Userspace, не требует root.
  • macOS — через sandbox-exec / seatbelt.

API — sandbox.executeCommand(cmd, args, { timeout, env, cwd, onStdout, onStderr }){ stdout, stderr, exitCode, executionTimeMs, timedOut }. Pluggable providers через ComputeSDK: E2BSandbox (Firecracker microVM), DockerSandbox, ModalSandbox, BlaxelSandbox — переключение одной строкой конфига.

При подключении к Mastra-агенту автоматически появляются tools: execute_command, get_process_output, kill_process. Не in-process JS-eval (как isolated-vm) — это shell-команда в изолированном namespace, поэтому работает одинаково для TS и Python (executeCommand('python3', ['-c', code])).

В наших 5 production-агентах не используется: ни один не исполняет LLM-сгенерённый код. Вопрос актуализировался в апреле 2026 для FHIR-агента Никиты в Realai-plus/fhir-services (ветка feature/medagentbench-agent) — у него execute_python_code тул через голый exec() без изоляции. Mastra LocalSandbox — один из путей закрытия; обзор runtime-категорий — code-execution-sandbox; emerging industry-идея про code-emitting агентов (Cloudflare Code Mode / Anthropic code execution with MCP) — code-mode-pattern.

Carry-over: Mastra Code Mode RFC (mastra#11036) — типизированные RPC-биндинги к тулам внутри sandbox, аналог Cloudflare Code Mode. Висит без implementation, ETA нет. Если зарелизят — Mastra сможет хостить code-execution agents без attribution к Cloudflare ecosystem; см. code-mode-pattern.

Subprocess-модель Workspace плохо ложится на in-process isolate. Если решим идти в isolated-vm или workerd — Mastra Workspace не помогает, проще воткнуть как обычный Mastra Tool с самописной начинкой.

Связано

  • agent-vs-workflow — pattern для structured-LLM tasks (LOINC). Mastra используется как execution engine, decomposed-логика выше уровня агента.
  • tool-calling — pattern, на котором построены все наши Mastra-агенты; Mastra-specific gotchas описаны здесь (этой странице), концепт самого pattern’а — там
  • python-to-ts-portage — методология/уроки портажа Python→TS (валидация через real-LLM, production-reference, snake_case↔camelCase boundary, порядок полей схемы, Promise.all) — не Mastra-specific, вынесено отдельно
  • large-data-files-storage — большие data-файлы агентов (LOINC ~60MB + словари): git-lfs vs GCS-download vs bake-in-image — не решено (status: draft); вынесено отдельно
  • code-execution-sandbox — обзор всех runtime-категорий (in-process isolate / OS namespace / sidecar / managed cloud) с tradeoff’ами; технологии общие, текущий триггер — LLM-агент
  • code-mode-pattern — emerging industry-идея про code-emitting агентов (Cloudflare Code Mode / Anthropic code execution with MCP); Mastra Workspace+LocalSandbox subprocess-модели для этой идеи плохо подходит, нужен RPC-bridge
  • medical-context-survey — multi-turn agent на Mastra
  • loinc — LOINC pipeline на Mastra (3-step workflow + tools)
  • fhir-meta-tagging — Mastra write tools маркируют resources через этот pattern
  • llm-numeric-codes-policy — Mastra-агенты возвращают English term, не SNOMED code
  • clinical-record-reconciliation — Mastra write tools должны dedup’ить против existing FHIR resources
  • inngest — Mastra-агенты вызываются как Inngest functions в production (single step normalizeAndHarmonizeStep); workflow-примитивы Mastra ↔ Inngest function/step — § «Workflows» выше + inngest § «Function vs Step»
  • google-healthcare-api — primary FHIR backend для production agent calls
  • hapi-fhir — alternative FHIR backend, локальный test setup

Источники

Источники: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16.

Сноски

  1. Mastra docs, accessed 2026-05-17, https://mastra.ai/docs — TBD verify URL).

  2. Mastra workflows + @mastra/inngest integration, accessed 2026-05-17, https://mastra.ai/docs/workflows — TBD verify URL) — workflow primitives (.then / .parallel / .branch / .dowhile / .foreach / nested / suspend-resume) и компиляция в Inngest-функцию; function/step-сторона — inngest § «Function vs Step».

  3. Mastra Workspace + Sandbox, accessed 2026-05-17, https://mastra.ai/reference/workspace/sandboxhttps://mastra.ai/reference/workspace/local-sandbox.

  4. Mastra Code Mode RFC (open), accessed 2026-05-17, https://github.com/mastra-ai/mastra/issues/11036.

  5. SmartHealthIT FHIR sandbox, accessed 2026-05-17, https://r4.smarthealthit.org — test data).

  6. Сессия ildar/833ec924, 2026-03-24 — ` (LOINC port).

  7. Сессия ildar/82806132, 2026-04-05 — ` (LOINC closeout BG-1140).

  8. Сессия ildar/7d777edb, 2026-03-23 — ` (Survey BG-1050).

  9. Сессия ildar/3d5c475c, 2026-03-23 — ` (PatientSummaryAgent BG-1049).

  10. Сессия ildar/f09d1111, 2026-03-26 — ` (shared package + chat).

  11. Сессия ildar/7ff79368, 2026-03-27 — ` (FHIR write tools).

  12. Сессия ildar/fc708f24, 2026-03-26 — ` (Inngest single step).

  13. Сессия ildar/4791d030, 2026-04-10 — ` (V2 param analysis BG-1191 — Mastra hidden layers debugging).

  14. Сессия ildar/18b47185, 2026-04-15 — (V2 расширение —.

  15. Сессия ildar/dd17b3fd, 2026-04-22 — ` (V2.

  16. Сессия ildar/7cc1d514, 2026-04-23 — ` (V2.