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:
| Agent | Domain | Pattern | Status | Source session |
|---|---|---|---|---|
loincMappingAgent | LOINC harmonization | 3-step decomposed workflow (agent-vs-workflow) | Architectural parity достигнут с Python (BG-1140) | 833ec924, 82806132 |
normalizationAgentV2 | Parameter name normalization | Single agent с словарь-fallback | Production-ready под USE_LOCAL_NORMALIZATION flag | fc708f24 |
surveyAgent (medical-context) | Patient questionnaire | Multi-turn agent (medical-context-survey) | Реализован, BG-1050 | 7d777edb |
patientSummaryAgent | IPS Patient Summary retrieval | Agent-for-retrieval — 4-phase agentic loop | Реализован, BG-1049 (6/6 scenarios на SmartHealthIT) | 3d5c475c |
healthChatAgent | Patient chat interface | Single agent с FHIR write tools | Подключён к patient-portal /api/chat | f09d1111 |
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/inngest — init(inngest) отдаёт createWorkflow / createStep, привязанные к Inngest, и тогда workflow компилируется в Inngest-функцию, а каждый Mastra-шаг — в step.run под капотом. То есть бесплатно получаешь durability / retries / observability Inngest, и в проде бежит ровно то, что разложил и видел в Studio. Мапинг прямой:
| Mastra | Inngest |
|---|---|
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) / .dountil | while-цикл вокруг 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.tssuffix — где есть оба варианта параллельно
Не общая 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 слоя — оверхед без пользы.
Маркеры выбора:
| Признак сервиса | Mastra | AI 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 output | overkill | ✓ (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.
Сноски
-
Mastra docs, accessed 2026-05-17, https://mastra.ai/docs — TBD verify URL). ↩
-
Mastra workflows +
@mastra/inngestintegration, 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». ↩ -
Mastra Workspace + Sandbox, accessed 2026-05-17, https://mastra.ai/reference/workspace/sandbox — https://mastra.ai/reference/workspace/local-sandbox. ↩
-
Mastra Code Mode RFC (open), accessed 2026-05-17, https://github.com/mastra-ai/mastra/issues/11036. ↩
-
SmartHealthIT FHIR sandbox, accessed 2026-05-17, https://r4.smarthealthit.org — test data). ↩
-
Сессия
ildar/833ec924, 2026-03-24 — ` (LOINC port). ↩ -
Сессия
ildar/82806132, 2026-04-05 — ` (LOINC closeout BG-1140). ↩ -
Сессия
ildar/7d777edb, 2026-03-23 — ` (Survey BG-1050). ↩ -
Сессия
ildar/3d5c475c, 2026-03-23 — ` (PatientSummaryAgent BG-1049). ↩ -
Сессия
ildar/f09d1111, 2026-03-26 — ` (shared package + chat). ↩ -
Сессия
ildar/7ff79368, 2026-03-27 — ` (FHIR write tools). ↩ -
Сессия
ildar/fc708f24, 2026-03-26 — ` (Inngest single step). ↩ -
Сессия
ildar/4791d030, 2026-04-10 — ` (V2 param analysis BG-1191 — Mastra hidden layers debugging). ↩ -
Сессия
ildar/18b47185, 2026-04-15 —(V2 расширение —. ↩ -
Сессия
ildar/dd17b3fd, 2026-04-22 — ` (V2. ↩ -
Сессия
ildar/7cc1d514, 2026-04-23 — ` (V2. ↩