オブザーバビリティ
実行中の 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_LEVEL(debug/info/warn/error、デフォルトはwarn)
LOG_LEVEL=debug に設定しても安全です。構造化フォーマットなので jq でノイズを取り除けます。ログ量はおよそ4倍になります。
監査ログ:信頼できる情報源
すべてのツール呼び出しは、apps/agent/src/core/audit-log-hook.ts の auditLogHook を経由します。テーブルのスキーマは以下のとおりです。
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_key、secret、password、token、private_key、mnemonic、seed)を監査ログに記録される前に *** でマスクします。シークレットはそもそもツールの引数にならない(ファクトリ関数の初期化時に 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_exceeded:daily_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つ。何か問題が起きたときは行を読みます。何も問題がないときは無視します。それが設計のすべてです。