MINARA

オブザーバビリティ

実行中の Agent のロギング、監査、デバッグ

Agent が意図しない動作をしたとき、その原因を把握する必要があります。Minara Agent は、構造化ログ監査ログティアイベントという3つの情報ストリームを出力します。それぞれ役割が異なります。このページでは、何がどこに記録されるか、そして一般的な障害モードをどう調査するかを説明します。

なぜ1つの大きなログではなく3つのストリームに分けるのか? Agent の誤動作は3つの形で現れます。インフラの障害(LLM 呼び出し失敗、DB ロック)、Agent の誤った操作(不正な引数でツールを呼び出した)、パーミッションロジックの予期しないブロックです。これらすべてをカバーしようとする1つのストリームは読めなくなります。分割することで、目的のテーブルにすぐにたどり着けます。動作の問題は audit、パーミッションの問題は tier_events、インフラの問題は構造化ログです。3つすべてが trace_id を共有しているので、全体像を把握したいときにつなぎ合わせることができます。

実際の使用例6段階のセーフティスタックは、資金移動に関わるすべての呼び出しに対して tier_events へ行を書き込みます。これにより、取引が実行される前にすべてのゲートを通過したことを事後的に証明できます。

3つのストリーム

ストリーム保存先目的
構造化ログ$dataDir/logs/*.ndjson および stdout運用の可視性、起動状態、LLM 呼び出し
監査ログSQLite audit テーブル「Agent がどのツールを呼び出し、何が起きたか」
ティアイベントSQLite tier_events テーブル「なぜその呼び出しが許可またはブロックされたか」

目安として、動作をデバッグするときは監査ログから始めます。インフラをデバッグするときは構造化ログから始めます。パーミッションをデバッグするときはティアイベントから始めます。

構造化ログ

apps/agent/src/core/logger.ts は最小限の JSON ロガーを提供します。

logger.info("skills/registry", "skill_registered", { id });
logger.warn("agent/loop", "max_iterations_reached", { session_id });
logger.error("llm/anthropic-wire", "cache_miss", { reason });

各行は以下の構造を持つ JSON オブジェクトです。

{
  "ts": "2026-04-14T12:34:56.789Z",
  "level": "info",
  "category": "skills/registry",
  "event": "skill_registered",
  "correlation_id": "turn_abc123",
  "trace_id": "t_xyz",
  "data": { "id": "minara.core" }
}

コリレーション ID は、ターン実行を囲む withCorrelation(id, fn) ラッパーから生成されます。ターン内で生成されたすべてのログ行は、AsyncLocalStorage を通じて同じ ID を継承します。そのため、grep correlation_id=turn_abc123 $dataDir/logs/*.ndjson で1つのターンの全体像を取得できます。

ログローテーション

ログは $dataDir/logs/ 以下に日次ローテーションで書き込まれます。次の設定でしきい値を変更できます。

  • LOG_LEVELdebug / info / warn / error、デフォルトは warn

LOG_LEVEL=debug に設定しても安全です。構造化フォーマットなので jq でノイズを取り除けます。ログ量はおよそ4倍になります。

監査ログ:信頼できる情報源

すべてのツール呼び出しは、apps/agent/src/core/audit-log-hook.tsauditLogHook を経由します。テーブルのスキーマは以下のとおりです。

CREATE TABLE audit (
  id              TEXT PRIMARY KEY,
  session_id      TEXT,
  trace_id        TEXT,
  tool_name       TEXT,
  tool_set        TEXT,
  args_json       TEXT,       -- redacted
  result_json     TEXT,
  blocked         INTEGER,
  block_reason    TEXT,
  permission_tier INTEGER,
  source          TEXT,       -- user | cron | autopilot | delegation
  duration_ms     INTEGER,
  created_at      INTEGER
);
CREATE VIRTUAL TABLE audit_fts USING fts5(
  tool_name, block_reason, args_json, content='audit'
);

FTS5 により、履歴全体を検索できます。

SELECT created_at, tool_name, blocked, block_reason
  FROM audit
 WHERE audit MATCH 'withdraw'
 ORDER BY created_at DESC
 LIMIT 50;

調査はここから始めます。 ユーザーから「Agent がおかしな動作をした」と報告を受けたとき、最初の行動はそのセッションの監査ログ行を時系列で取得することです。LLM の推論テキスト、ツールの引数、ツールの生の出力、タイムスタンプがすべてここに記録されています。

何が秘匿されるか

tools/_shared/result.ts のリダクターは、既知の機密キー(api_keysecretpasswordtokenprivate_keymnemonicseed)を監査ログに記録される前に *** でマスクします。シークレットはそもそもツールの引数にならない(ファクトリ関数の初期化時に process.env から取得される)というルールと合わせて、監査ログはサポートへの共有やバグ報告への添付が最小限の確認だけで安全に行えます。

ティアイベント:ブロックされた理由

core/permission-tier-hook.ts は、許可・ブロックを問わずすべての判断に対して tier_events へ行を出力します。

CREATE TABLE tier_events (
  id            TEXT PRIMARY KEY,
  tool_name     TEXT,
  source        TEXT,
  tier          INTEGER,
  allow_ceiling INTEGER,
  decision      TEXT,          -- allow | block | pending_confirmation
  reason        TEXT,
  trace_id      TEXT,
  created_at    INTEGER
);

監査ログは何が起きたかを示します。ティアイベントはパーミッションシステムがなぜその判断をしたかを示します。2つは trace_id で結合でき、以下のように一緒にクエリされることがよくあります。

SELECT a.tool_name, a.blocked, te.decision, te.reason
  FROM audit a
  LEFT JOIN tier_events te ON te.trace_id = a.trace_id
                           AND te.tool_name = a.tool_name
 WHERE a.session_id = ?
 ORDER BY a.created_at;

コリレーション ID とトレース ID

2種類の ID が存在します。

  • correlation_id はターン単位です。Agent ループの各ターン開始時に生成されます。構造化ログと監査ログ行に記録されます。
  • trace_id はワークフロー実行単位またはシグナル単位です。SignalContext または WorkflowInstance から、サブ Agent への委譲を含むすべてのツール呼び出しに伝播されます。

ユーザーのチャットターンは通常、新しい correlation_id を持ち、trace_id はありません。cron の発火時は両方を持ちます。trace_id を使うと、「14:03 の BTC アラートが、生成したサブ Agent を含めてライフタイム全体で何をしたか」をクエリできます。

よくある調査

「Agent がなぜ X を呼び出すことを拒否したのか?」

SELECT tool_name, decision, reason, created_at
  FROM tier_events
 WHERE tool_name = 'swap'
   AND decision = 'block'
 ORDER BY created_at DESC
 LIMIT 10;

reason を確認してください。最も一般的な値は以下のとおりです。

  • tier_exceeds_ceiling:スキルがアクティベートされていないか、ターンの allowRiskTier がツールのティアより低い。
  • analysis_to_trade_boundary:そのターンはすでに分析系の呼び出しをしており、取引の呼び出しは別のユーザーメッセージで行う必要がある。
  • daily_cap_exceededdaily_spend にこの呼び出しの想定金額を加えると MINARA_DAILY_CAP_USD を超える。
  • kill_switch_active:誰か(または Agent 自身)が kill を呼び出した。
  • tool_set_not_allowed:ターンの allowedToolSets にこのツールが含まれていない。

「なぜこのターンが遅いのか?」

SELECT tool_name, AVG(duration_ms) AS avg_ms, COUNT(*) AS n
  FROM audit
 WHERE session_id = ?
 GROUP BY tool_name
 ORDER BY avg_ms DESC;

correlation_id でグレップした構造化ログと組み合わせると、LLM 呼び出しの所要時間やキャッシュヒット率を確認できます。

「プロンプトキャッシュは機能していたか?」

llm.cache_read_input_tokens をログで検索してください。セッション全体でこの値が 0 であれば、キャッシュ可能なプロンプトブロックが安定していない(おそらくターンごとに再生成されており、キャッシュが無効になっている)可能性があります。詳細は LLM 統合 を参照してください。

「Autopilot は夜間に何をしていたか?」

SELECT created_at, tool_name, blocked, substr(result_json, 1, 200)
  FROM audit
 WHERE source = 'autopilot'
   AND created_at > ?
 ORDER BY created_at;

ヘルスエンドポイント

HTTP ゲートウェイは以下を公開しています。

  • GET /healthz は、プロセスが起動中で DB が開けられる状態であれば 200 を返す liveness プローブです。
  • GET /status は、DB の統計、スキル数、アクティブなトリガー、最後に成功した LLM 呼び出しのタイムスタンプといった readiness の詳細を返します。

正確なスキーマについては API リファレンス を参照してください。

外部ツールへのエクスポート

Loki / Datadog / Grafana Cloud にログを送りたい場合は、stdout をパイプします。

docker run minara 2>&1 | vector --config vector.toml

すべての stdout 行は有効な NDJSON です。「構造化ログ」専用のエクスポートパスは存在しません。stdout が正規の出力先です。


オブザーバビリティは意図的にシンプルに設計されています。テーブルは3つ、ログフォーマットは1種類、コリレーションフィールドは1つ、トレースフィールドは1つ。何か問題が起きたときは行を読みます。何も問題がないときは無視します。それが設計のすべてです。

目次