システム設計
コアモジュール、その不変条件、および本セクション内の設計ドキュメント一覧
👥 このセクションの対象者: Minara がなぜこのように設計されているかを理解したいリサーチ志向の開発者、および不変条件や変更リスクの境界線を把握する必要があるメンテナーです。 各ページの冒頭には「なぜこれが存在するか」を平易な言葉で説明したコールアウトがあり、末尾にはユーザー向け機能へ戻る See this in use リンクを掲載しています。コントリビューター向けのガイダンスは明確に区別されています。
初めての方への推奨読書順: まず エージェントループ(1ターンの処理の核心)を読み、次に スキルシステム(機能のパッケージ化の仕組み)を読んだあと、関心のある機能が依存するサブシステムのページへ進んでください。
このセクションでは Minara Agent がどのように構築されているかを説明します。まず以下のコアモジュール概要を読み、その後ディレクトリから該当サブシステムの詳細ページへ進んでください。
ディレクトリ
ランタイム
- エージェントループ: ユーザーメッセージから最終回答までの、1ターンのフロー。
- スキルシステム: ルーティング、アクティベーション、リスクゲート、利用可能なスキルの全カタログ。
- LLM 統合: プロバイダー抽象化、プロンプトキャッシュ、モデルルーティング。
- シナリオ分類器: 手続き的なプレイブックを注入し、スキルをプリロードする L0.5 インテント分類。
状態とストレージ
- メモリ(サブセクション概要): 連携する4つのストア(セッションメモリ、パーソナライゼーション、ロールリフレクション、学習システム)がターンでどのように組み合わさるか。
- ワークスペース: Markdown ベースのアイデンティティとメモリのグラウンドトゥルース、および永続的な会話コンテンツ(チャート、レポート、アップロード)を保持するアーティファクトとファイルのストア。
セーフティと実行
- 資金安全とサンドボックス: サンドボックス、パーミッションティア、フックパイプライン、6段階の資金安全スタック。
I/O と運用
- ディープリサーチパイプライン: 構造化された市場レポートを生成する、独立した6ステップのパイプライン。
- オブザーバビリティ: 実行中の Agent のロギング、監査、デバッグ。
コアモジュール
Agent は少数のモジュールで構成されており、明示的なインターフェースを介して連携します。このページでは各モジュールについて、何を担当し、何に依存し、どの不変条件を維持するかを説明します。
app.ts: コンポジションルート
apps/agent/src/app.ts は、リポジトリ内で他のすべてのモジュールを把握している唯一のモジュールです。単一の createApp() ファクトリ関数をエクスポートし、次の処理を行います。
$dataDir/配下に SQLite データベースを開く(WAL モード、FTS5 有効)。- パーミッションティアフックと、分析→取引の境界フックをインストールした状態で
ToolRegistryを構築する。 - すべてのツールファクトリ関数(
createReadTools、createTradeTools、createMemoryTools、createFileToolsなど)をインスタンス化して登録する。必要な環境変数が存在しないファクトリ関数は[]を返し、起動時にスローしない。 BUILTIN_SKILLSと、apps/agent/src/skills/external/配下にベンダリングされたものに対するbuildExternalDomainSkills()の出力からSkillRegistryを構築する。- LLM クライアント、各レジストリ、サンドボックスリゾルバー、監査ログライターを注入して
AgentLoopを配線する。 - ゲートウェイが使用するために組み立て済みの
Appオブジェクトを返す。
createApp() は(環境変数と設定を与えられれば)純粋な関数のため、CLI REPL と HTTP ゲートウェイは同一のランタイムを共有します。「X はどこで接続されるのか」という疑問があれば、答えは常に app.ts です。
core/tool-registry.ts: 型付きディスパッチとパーミッションティア
ツールレジストリは、Agent ができることの具体的な表現です。すべてのツールは ToolEntry です。
interface ToolEntry {
name: string;
toolSet: string;
schema: ToolSchema; // JSON Schema for LLM tool-use
handler: ToolHandler; // async (args) => string
permissionTier: PermissionTier;
isAsync: boolean;
description: string;
checkFn?: () => boolean; // optional runtime availability gate
}パーミッションティア
enum PermissionTier {
READ_ONLY = 1, // price, balance, trending, fear_greed, search
CONFIRM_ONCE = 2, // analyze, research, small swap, write_file
ALWAYS_CONFIRM = 3, // perps, large swap, autopilot start/modify
MANUAL_ONLY = 4, // withdraw, external-address send, emergency stop
}ティアは BeforeToolCallHook によって強制されます。これは推奨事項ではありません。フックが ToolCallBlockedError をスローすると、エージェントループがそれをキャッチし、ツール結果として LLM にエラーを返します。LLM には他のツールエラーと同様に見えますが、監査ログは明確に区別して記録します。
ツールセット
BUILTIN_TOOL_SETS はツールを名前付きバンドル(read、trade、perps、file、browser など)にグループ化し、オプションの includes 合成をサポートします。スキルはツールを名前で参照しますが、レジストリとゲートウェイが扱う単位はツールセットです。推論しやすく、ゲートもかけやすいためです。Cron Autopilot のターンは allowedToolSets: ["read", "memory", "web"] のみで実行され、それ以外は利用できません。
コンテキスト伝播
レジストリは AsyncLocalStorage を使用して、subagent 経由のサブ Agent 呼び出しやスキルが実行するツールシーケンスを含む非同期ツール呼び出し全体に ToolCallContext を伝播させます。フックは ToolRegistry.currentContext() を読み取り、ターン単位の不変条件(分析→取引の境界、allowedToolSets ホワイトリスト、緊急停止)を強制します。
core/agent-loop.ts: 計画→呼び出し→観察→判断
エージェントループはシンプルな while (iterations < max) オーケストレーターです。
- 現在の
SkillSession状態を使用して、prompt-builder.ts経由でシステムプロンプトを構築する。 - 現在アクティブなスキルで許可されたツールセット(ターンの
allowedToolSetsと交差済み)を使って LLM を呼び出す。 - 各ツール呼び出しについて、以下を実行する。
- 宣言されたインテントが実際のツール呼び出しと一致するか検証する。
- レジストリのディスパッチを実行し、
BeforeToolCallHookを発火させる。 - 呼び出しと結果を監査ログに永続化する。
- 結果を新しいユーザーメッセージとして返す。
stop_reason: "end_turn"またはmax_iterationsに達したら終了する。
ターンの記録(リスク上限、シグナルコンテキスト)は ToolCallContext オブジェクトに保持され、ループが runInContext(ctx, fn) でラップすることで、すべてのツール呼び出しが同じ状態を参照します。
詳しい解説は エージェントループ を参照してください。
core/prompt-builder.ts: システムプロンプトの組み立て
システムプロンプトは文字列の連結ではなく、宣言されたブロックから構築されます。
system: [
{ type: "text", text: identityPrompt, cache: true },
{ type: "text", text: skillCatalog, cache: true },
{ type: "text", text: activeSkillPrompts, cache: false },
{ type: "text", text: signalContextBlock, cache: false },
]cache: true のブロックは Anthropic のプロンプトキャッシュ(TTL 5分、コスト10%削減)に渡され、アイデンティティとカタログをウォーム状態に保ちます。毎ターン変化するブロック(アクティブスキル、シグナルコンテキスト、保留中の確認)はキャッシュされません。この設計は重要です。ブロックの順序が悪いと、見かけ上「1回の Claude 呼び出し」が3回のキャッシュミスになります。
buildSystemPrompt() は、プレーンな文字列が必要な呼び出し元(テスト、CLI の --dump-prompt モード)のために buildSystemPromptBlocks() をラップします。
skills/: スキルレイヤー
DomainSkill は完全に宣言的な単位です。
{
id: "minara.core",
kind: "domain_skill",
description: "Unified Minara API — markets, portfolio, spot/perps trading, sub-account management",
prompt: "...≤ 800 tokens of instructions...",
tool_names: [
"get_price", "get_trending", "get_fear_greed", "search_tokens",
"minara_account", "lookup_token", "minara_total_balance",
"get_portfolio", "get_perps_positions", "minara_pnl",
"swap_tokens", "buy_token", "sell_token", "transfer_token",
"open_perps_position", "close_perps_position",
"minara_perps_wallets_list", "minara_perps_wallet_sweep", /* ... */
],
activation: "auto",
priority: 50,
routing: {
lifecycle_stages: ["discover", "decide", "manage"],
keywords: ["minara", "balance", "swap", "long", "short", "perp", "perps", "perps sub-account"],
asset_classes: ["crypto_major", "crypto_alt", "crypto_meme", "stablecoin", "perps"],
},
}各コンポーネントの説明です。
SkillRegistry(apps/agent/src/skills/registry.ts): カタログを保持し、登録時にrequires_envゲートを強制し、未ルーティングおよびルーティング済みのカタログブロックをレンダリングし、アクティブな ID のプロンプトフラグメントを連結し、ツールのホワイトリストを収集します。SkillSession(apps/agent/src/skills/session.ts): ターン単位のミュータブルな状態を保持します。アクティブなスキル、適用されるリスク上限、保留中の確認がここに含まれます。レジストリはステートレスであり、アクティベーションはセッションが担います。Router(apps/agent/src/skills/router.ts): L0 の決定論的プレフィルターを実行します。キーワードスコアリング、ステージ分類、アセットクラス検出、およびユーザー確認なしに高リスクのアクティベーションを短絡させる L3 リスクゲートが含まれます。FinanceTaxonomy(apps/agent/src/skills/finance-taxonomy.ts): ルーターとスキルが共有する語彙(LifecycleStage、AssetClass、RiskTier)です。
ルーティングアルゴリズムとアクティベーションフローの詳細は スキルシステム を参照してください。
tools/_shared/sandbox.ts: ファイルシステムの境界
すべてのファイルツールは resolveInSandbox(relativePath) を呼び出します。この関数は以下を行います。
- 絶対パスを拒否する。
$dataDir/sandbox/files/配下でパスを解決する。- 各パスセグメントをウォークし、シンボリックリンクをたどって、解決された実パスがサンドボックスルートをプレフィックスとして持つことを確認する。
- 正規化された絶対パスを返すか、
SandboxEscapeErrorをスローする。
ツールハンドラーは生のユーザー入力を fs.* 引数として受け取ることはありません。仮にツールがそれを試みたとしても(そうすべきではありませんが)、ディスクへのシステムコールが発生する前にリゾルバーがトラバーサルを拒否します。
別のリゾルバーでファイルを開く並列ヘルパーを書かないでください。 単一エントリーポイントという性質が、サンドボックスを防御可能にしています。
tools/_shared/result.ts: ツール結果エンベロープ
すべてのツールハンドラーは文字列を返しますが、その文字列は ok({...})、err("...")、または errFromThrow(e) によって、LLM が確実にパースできるタグ付きエンベロープに整形されます。
{"ok": true, "data": {...}}
{"ok": false, "error": "..."}これはあえてシンプルな設計です。LLM はすべてのツールから同じ形式を受け取るため、「結果がエラーなら謝罪してリトライする」というプロンプトロジックをツールごとではなく一度だけ書けばすみます。
gateway/: 2つのエントリーポイント、1つのランタイム
cli.ts: LLM の出力を stdout にストリーミングし、同じAgentLoop経由でツール呼び出しをルーティングする REPL です。実行の最初の行としてconfig/load-env.tsをインポートし、process.envを読む前に.envを反映させます。server.ts: HTTP API を公開します。詳細は HTTP ゲートウェイ を参照してください。同じcreateApp()の出力を使用するため、CLI と HTTP の動作に乖離はありません。skills-cli.ts:minara skills add/list/upgrade/removeサブコマンドです。外部の SKILL.md パッケージをapps/agent/src/skills/external/にベンダリングし、ライセンス検出結果を出力に表示します。
config/load-env.ts: 環境変数のブートストラップ
2つの責務のみを持ちます。
.envが存在する場合、Node 22 ネイティブのprocess.loadEnvFile()を呼び出す。- ファイルから読み込まれた変数とシェルから来た変数を記録し、起動ログに出力する。
副作用のみを目的としてインポートされ、すべてのエントリーポイントの最初のインポートとなります。新しいエントリーポイントを追加してこのインポートを忘れると、下流でサイレントな環境変数欠落エラーが発生します。
アーキテクチャの原則: ここに列挙した各モジュールは1つの責務を持ち、スタック上位のモジュールはスタック下位のモジュールにのみ依存します。 エージェントループはレジストリに依存し、レジストリはツールレイヤーに依存し、ツールレイヤーはサンドボックスに依存します。「上方向への参照」(例: ツールハンドラーが agent-loop.ts からインポートするなど)が生じた場合、それは後方エッジを追加するのではなく設計を見直すべきサインです。