Architecture
リポジトリ: mastra-ai/mastra 分析日: 2026-02-14
概要
Mastra は AI エージェント・ワークフロー・メモリ・ツールなどの機能を統合する TypeScript フレームワークであり、その全体アーキテクチャは「中央レジストリ + プラガブルな周辺パッケージ」の構造を取る。コアパッケージ (@mastra/core) が抽象インターフェースと登録メカニズムを定義し、ストレージ・ボイス・デプロイヤー・サーバーアダプターなどの具体実装は独立パッケージとして分離される。この分析では、レイヤー構成・依存関係の流れ・結合パターンを横断的に調査し、大規模モノレポにおける拡張可能なフレームワーク設計のプラクティスを抽出する。
背景にある原則
中央ハブによる構成集約(Composition Root): フレームワークの全コンポーネントを1つのエントリポイント(
Mastraクラス)に集約し、依存関係の解決と横断的関心事(ロガー・オブザーバビリティ)の伝播を一箇所で行うべき。これにより、コンポーネント間の暗黙的な結合を排除し、テスト時のモック差し替えが容易になる(根拠:packages/core/src/mastra/index.ts:500-699のコンストラクタで全プリミティブを順序付きで登録し、setLoggerで全コンポーネントに一括伝播する設計)。インターフェースによる実装分離(Dependency Inversion): コアが依存するのは抽象クラス・インターフェースのみとし、具体実装はすべて外部パッケージに委ねるべき。これにより、23種のストレージバックエンド、13種のボイスプロバイダー、4種のデプロイヤーが同一のコアに対してプラグインできる(根拠:
MastraCompositeStore,MastraVoice,MastraDeployer,PubSubなどの抽象クラスがコアに定義され、具体実装はstores/,voice/,deployers/に分離)。ドメイン分割による関心事の分離: ストレージを機能ドメイン(memory, workflows, scores, observability, agents 等)ごとに独立したインターフェースに分割し、異なるバックエンドを組み合わせ可能にすべき。単一のストレージアダプターが全ドメインをカバーする必要はない(根拠:
MastraCompositeStoreのdomainsオプションにより、メモリは PostgreSQL、ワークフローは LibSQL のように混合構成が可能)。NoOp パターンによるオプショナル機能の安全な無効化: オプショナルな横断的機能(オブザーバビリティ等)には NoOp 実装を提供し、null チェックの散在を防ぐべき(根拠:
NoOpObservabilityクラスがすべてのメソッドを空実装で提供し、利用側はthis.#observability.getDefaultInstance()を null チェックなしで呼び出せる)。
実例と分析
レイヤー構成と依存方向
Mastra のアーキテクチャは明確な4層構造を持つ。
- Core Layer (
packages/core): 抽象インターフェース、型定義、Mastraクラス(中央レジストリ) - Feature Packages (
packages/memory,packages/mcp,packages/rag,packages/server): コアを拡張する機能パッケージ - Adapter Layer (
stores/,voice/,deployers/,server-adapters/,pubsub/,auth/): プラガブルな具体実装 - Client Layer (
client-sdks/): クライアント向け SDK
依存方向は常に「外側 → 内側」であり、コアは一切の具体実装に依存しない。アダプターパッケージは peerDependencies でコアを参照する(例: "@mastra/core": ">=1.4.0-0 <2.0.0-0")。
中央レジストリパターン(Mastra クラス)
Mastra クラスは10種以上のジェネリック型パラメータを持つ中央レジストリであり、全コンポーネントの登録・取得・ライフサイクル管理を行う。
// packages/core/src/mastra/index.ts:292-306
export class Mastra<
TAgents extends Record<string, Agent<any>> = Record<string, Agent<any>>,
TWorkflows extends Record<string, AnyWorkflow> = Record<string, AnyWorkflow>,
TVectors extends Record<string, MastraVector<any>> = Record<string, MastraVector<any>>,
TTTS extends Record<string, MastraTTS> = Record<string, MastraTTS>,
TLogger extends IMastraLogger = IMastraLogger,
TMCPServers extends Record<string, MCPServerBase<any>> = Record<string, MCPServerBase<any>>,
TScorers extends Record<string, MastraScorer<any, any, any, any>> = Record<string, MastraScorer<any, any, any, any>>,
TTools extends Record<string, ToolAction<any, any, any, any, any, any>> = ...,
TProcessors extends Record<string, Processor<any>> = ...,
TMemory extends Record<string, MastraMemory> = ...,
> {コンストラクタでの登録順序は意図的に制御されている。ツールとプロセッサが先に登録され、それらを参照する MCP サーバーとエージェントが後から登録される。
// packages/core/src/mastra/index.ts:596-677
// Now add primitives - order matters for auto-registration
// Tools and processors should be added before agents and MCP servers that might use them依存注入の伝播メカニズム
Mastra クラスは、コンポーネント登録時にロガー・ストレージ・他のプリミティブを __registerPrimitives / __registerMastra / __setLogger メソッドで注入する。これはフレームワーク内部の「プッシュ型 DI」であり、コンテナベースの DI ではなく明示的なメソッド呼び出しで行われる。
// packages/core/src/mastra/index.ts:867-875
mastraAgent.__setLogger(this.#logger);
mastraAgent.__registerMastra(this);
mastraAgent.__registerPrimitives({
logger: this.getLogger(),
storage: this.getStorage(),
agents: agents,
tts: this.#tts,
vectors: this.#vectors,
});setLogger では全コンポーネントに対してロガーを一括伝播する(packages/core/src/mastra/index.ts:2337-2395)。これにより、ロガー変更時に全コンポーネントが自動的に新しいロガーを使う。
ストレージのドメイン分割と合成
ストレージは StorageDomains 型で10個のドメインに分割され、各ドメインは独立した抽象クラスを持つ。
// packages/core/src/storage/base.ts:16-27
export type StorageDomains = {
workflows: WorkflowsStorage;
scores: ScoresStorage;
memory: MemoryStorage;
observability?: ObservabilityStorage;
agents?: AgentsStorage;
datasets?: DatasetsStorage;
experiments?: ExperimentsStorage;
promptBlocks?: PromptBlocksStorage;
scorerDefinitions?: ScorerDefinitionsStorage;
mcpClients?: MCPClientsStorage;
};MastraCompositeStore は default ストアと domains オーバーライドによる合成をサポートする。
// packages/core/src/storage/base.ts:152-166
const storage = new MastraCompositeStore({
id: "composite",
default: pgStore,
domains: {
memory: libsqlStore.stores?.memory,
},
});PostgresStore の実装では、共有 DB クライアント設定 (PgDomainClientConfig) を全ドメインに渡す。各ドメインクラスは StorageDomain を継承し、init() と dangerouslyClearAll() を実装する。
// stores/pg/src/storage/index.ts:136-154
const domainConfig: PgDomainClientConfig = {
client: this.#db,
schemaName: this.schema,
skipDefaultIndexes: config.skipDefaultIndexes,
indexes: config.indexes,
};
this.stores = {
scores: new ScoresPG(domainConfig),
workflows: new WorkflowsPG(domainConfig),
memory: new MemoryPG(domainConfig),
// ...
};遅延初期化の Proxy パターン
ストレージの初期化は augmentWithInit 関数による Proxy で遅延実行される。全メソッド呼び出しの前に ensureInit() を挟むことで、利用側が明示的に init() を呼ぶ必要をなくしている。
// packages/core/src/storage/storageWithInit.ts:34-63
const proxy = new Proxy(storage, {
get(target, prop) {
const value = target[prop as keyof typeof target];
if (typeof value === "function") {
if (prop === "init") {
return async (...args: unknown[]) => {
if (!hasInitialized) {
hasInitialized = Reflect.apply(value, target, args) as Promise<void>;
}
return hasInitialized;
};
}
// All other functions wait for init
return async (...args: unknown[]) => {
await ensureInit();
return Reflect.apply(value, target, args);
};
}
return Reflect.get(target, prop);
},
});サーバーアダプターの分離
HTTP サーバー機能は3層に分離されている:
- ハンドラー層 (
packages/server/src/server/handlers/): フレームワーク非依存のビジネスロジック(agents.ts, workflows.ts, memory.ts 等) - アダプター基底 (
packages/server/src/server/server-adapter/): ルート定義とミドルウェア管理 - フレームワークアダプター (
server-adapters/hono/,server-adapters/express/等): Hono/Express 固有の実装
// server-adapters/hono/src/index.ts:55
export class MastraServer extends MastraServerBase<HonoApp, HonoRequest, Context> {Hono アダプターはフレームワーク固有の型(HonoApp インターフェース)を最小限に定義し、メソッドシグネチャの厳密な型マッチングを回避している。
共有テストスイートパターン
ストレージアダプターの品質保証には、共有テストスイート (stores/_test-utils) を使用する。各アダプターは createTestSuite(storage) を呼び出すだけで、全ドメインの標準テストが実行される。
// stores/_test-utils/src/factory.ts:28
export function createTestSuite(storage: MastraStorage, capabilities: TestCapabilities = {}) {パターンカタログ
Composition Root (構造)
- 解決する問題: 多数のコンポーネント間の依存関係を明示的に管理し、暗黙的な結合を排除する
- 適用条件: プラグイン可能なコンポーネントが5種以上あるフレームワーク
- コード例:
packages/core/src/mastra/index.ts:500-699 - 注意点: コンストラクタが肥大化しやすい。登録順序の制約をコメントで明記すること
Abstract Factory / Plugin Architecture (生成)
- 解決する問題: 具体実装をコアから分離し、利用者が自由にバックエンドを差し替えられるようにする
- 適用条件: 同一インターフェースに対して複数の実装が存在する(ストレージ 23 種、ボイス 13 種等)
- コード例:
packages/core/src/storage/base.ts(MastraCompositeStore),packages/core/src/voice/voice.ts(MastraVoice) - 注意点: peerDependencies のバージョン範囲管理が重要
Null Object / NoOp (振る舞い)
- 解決する問題: オプショナルな機能の null チェック散在を防ぐ
- 適用条件: 横断的関心事(ロギング、オブザーバビリティ、キャッシュ等)がオプショナルな場合
- コード例:
packages/core/src/observability/no-op.ts(NoOpObservability) - 注意点: NoOp 実装はインターフェース変更時に追従が必要
Proxy-based Lazy Initialization (構造)
- 解決する問題: 非同期初期化が必要なリソースを、利用側に初期化タイミングを意識させずに使えるようにする
- 適用条件: データベース接続やテーブル作成など、初回利用前に非同期セットアップが必要なリソース
- コード例:
packages/core/src/storage/storageWithInit.ts:34-63 - 注意点: Proxy は vitest 環境で instanceof が正しく動作しないケースがある(コード内コメントに記載あり)
Good Patterns
- ドメイン分割ストレージと合成パターン: ストレージを機能ドメインごとに独立インターフェースに分割し、
MastraCompositeStoreで異なるバックエンドを合成可能にしている。メモリは高速な LibSQL、ワークフロー状態は PostgreSQL のように、ドメインごとに最適なバックエンドを選択できる。
// packages/core/src/storage/base.ts:194-222
if (config.default || config.domains) {
const defaultStores = config.default?.stores;
const domainOverrides = config.domains ?? {};
this.stores = {
memory: domainOverrides.memory ?? defaultStores?.memory,
workflows: domainOverrides.workflows ?? defaultStores?.workflows,
// ...
} as StorageDomains;
}- プッシュ型 DI による横断的関心事の一括伝播:
setLoggerメソッドがすべての登録済みコンポーネントにロガーを伝播する。コンテナベース DI のオーバーヘッドなしに、横断的関心事を確実に全コンポーネントへ届ける。
// packages/core/src/mastra/index.ts:2337-2395
public setLogger({ logger }: { logger: TLogger }) {
this.#logger = logger;
if (this.#agents) {
Object.keys(this.#agents).forEach(key => {
this.#agents?.[key]?.__setLogger(this.#logger);
});
}
// ... storage, vectors, mcpServers, workflows, workspace 等にも伝播
}- 共有テストスイートによるアダプター品質保証:
createTestSuite(storage)に任意のストレージ実装を渡すだけで、全ドメインの標準テストが実行される。23 種のストレージアダプターすべてが同一のテスト基準を満たすことを保証する。
// stores/_test-utils/src/factory.ts:28-30
export function createTestSuite(storage: MastraStorage, capabilities: TestCapabilities = {}) {
describe(storage.constructor.name, () => {
// 全ドメインのテストを自動登録- RequestContext による型安全なリクエストスコープ DI:
RequestContext<Values>がジェネリック型で型安全な key-value コンテナを提供し、ミドルウェアからツール実行まで一貫したコンテキスト伝播を実現する。
// packages/core/src/request-context/index.ts:33-43
export class RequestContext<Values extends Record<string, any> | unknown = unknown> {
private registry = new Map<string, unknown>();
public set<K extends Values extends Record<string, any> ? keyof Values : string>(
key: K,
value: Values extends Record<string, any> ? (K extends keyof Values ? Values[K] : never) : unknown,
): void {
this.registry.set(key as string, value);
}
}Anti-Patterns / 注意点
- コンストラクタの肥大化:
Mastraクラスのコンストラクタは約200行に及び、10種以上のコンポーネント登録、イベント設定、ロガー初期化を一手に引き受ける。コンストラクタが長大になると、初期化順序の把握が困難になり、新規コンポーネント追加時のリスクが高まる。
// Bad: 200行超のコンストラクタ
constructor(config?: Config<...>) {
// 200行にわたる初期化処理...
}
// Better: Builder パターンまたは初期化フェーズの分離
class Mastra {
private initPrimitives(config) { /* ツール・プロセッサ登録 */ }
private initInfra(config) { /* ロガー・ストレージ・オブザーバビリティ */ }
private initComponents(config) { /* エージェント・ワークフロー */ }
}- ダブルアンダースコア API の増殖:
__setLogger,__registerMastra,__registerPrimitives,__setRawConfigなど、フレームワーク内部のメソッドが__プレフィックスで命名されている。TypeScript ではprivate/#で十分であり、__は慣習的に「触るな」という意味だが、実際には public アクセス可能なままである。
// Bad: public だが __prefix で「内部用」を示す
public __setLogger(logger: IMastraLogger) { ... }
// Better: Symbol や #private を使い、型レベルで外部アクセスを防ぐ
#setLogger(logger: IMastraLogger) { ... }
// または、@internal JSDoc + API Extractor で public API から除外導出ルール
[MUST]プラガブルな周辺機能のインターフェースはコアパッケージに定義し、具体実装はpeerDependenciesで参照する独立パッケージに分離する- 根拠: Mastra は
MastraCompositeStore,MastraVoice,MastraDeployer,PubSubの抽象をコアに置き、23種のストレージ・13種のボイス等の具体実装を独立パッケージとして提供しており、コアの変更なしにバックエンドを追加できる
- 根拠: Mastra は
[MUST]フレームワークの全コンポーネントを1つのエントリポイント(Composition Root)に集約し、依存関係の解決と横断的関心事の伝播をそこで行う- 根拠:
Mastraクラスのコンストラクタがすべてのプリミティブを登録し、setLoggerで全コンポーネントにロガーを一括伝播する設計により、散在する暗黙的結合を排除している
- 根拠:
[SHOULD]オプショナルな横断的機能(ロギング・オブザーバビリティ・キャッシュ等)には NoOp 実装を提供し、利用側の null チェック散在を防ぐ- 根拠:
NoOpObservabilityがすべてのメソッドを空実装で提供し、this.#observabilityを null チェックなしで安全に呼び出せるようにしている
- 根拠:
[SHOULD]非同期初期化が必要なリソースには Proxy ベースの遅延初期化を適用し、利用側が初期化タイミングを意識しなくてよい API を提供する- 根拠:
augmentWithInitが Proxy で全メソッドの前にensureInit()を挿入し、ストレージのテーブル作成を初回利用時に自動実行する
- 根拠:
[SHOULD]プラグインの品質保証には共有テストスイートを提供し、すべての実装が同一の契約を満たすことを保証する- 根拠:
stores/_test-utilsのcreateTestSuite(storage)により、23種のストレージアダプターが全ドメインの標準テストをパスすることを保証している
- 根拠:
[SHOULD]ストレージ等のインフラ層は機能ドメインごとに独立したインターフェースに分割し、異なるバックエンドの合成を可能にする- 根拠:
StorageDomains型が10個のドメインを独立定義し、MastraCompositeStoreのdomainsオプションで異種バックエンドの混合が可能
- 根拠:
[AVOID]コンストラクタに200行を超える初期化ロジックを詰め込む。コンポーネントの登録順序制約が暗黙的になり、追加・変更時のリスクが高まる- 根拠:
Mastraクラスのコンストラクタは約200行に及び、ツール→プロセッサ→MCP→エージェントという順序制約がコメントでのみ伝達されている
- 根拠:
[AVOID]フレームワーク内部 API に__プレフィックスを使って public メソッドとして公開する。TypeScript の#privateフィールドまたは@internal+ API Extractor で型レベルの隠蔽を行うべき- 根拠:
__setLogger,__registerMastra等が public アクセス可能なまま残っており、フレームワーク利用者が誤って呼び出すリスクがある
- 根拠:
適用チェックリスト
- [ ] フレームワーク/ライブラリを設計する際、プラガブルな機能のインターフェース(抽象クラスまたは interface)をコアに定義しているか
- [ ] 具体実装(DB ドライバ、外部サービスクライアント等)は
peerDependenciesで参照する独立パッケージに分離されているか - [ ] コンポーネントの登録・取得・ライフサイクル管理を行う Composition Root が存在するか
- [ ] 横断的関心事(ロガー・テレメトリ等)の変更が全コンポーネントに確実に伝播する仕組みがあるか
- [ ] オプショナルな機能に NoOp 実装を提供し、null チェックの散在を防いでいるか
- [ ] 非同期初期化が必要なリソースに対して、利用側が初期化を意識しなくてよい API を提供しているか
- [ ] プラグイン/アダプターの品質保証に共有テストスイートを提供しているか
- [ ] ストレージ・インフラ層が機能ドメインごとに分割され、異なるバックエンドの合成が可能か
- [ ] コンストラクタが肥大化していないか(目安: 100行以内)
- [ ] 内部 API が
#privateまたは@internalで適切に隠蔽されているか