作成日: 2026-03-06
ステータス: Draft
本書は ButlerLayer_アーキテクチャ定義書_v0.3.md を前提に、Butler Orchestrator の内部実行構造・SOP管理・安全設計を定義する。
「Butlerの中でどう動くか」を定義する文書であり、Butler側の実装者が主な読者。
Butler Orchestrator: 決定論的なPythonサービス(状態機械)判断ゲート (Routing Gate): QwenがOrchestrator内で担うパターンマッチ・振り分け機能実行計画 (Execution Plan): タスクインスタンスに対する手順列実行ポリシー (Execution Policy): 実行可否と承認要否を決める規則群承認ゲート (Approval Gate): HITLによる実行前確認ステップ(Brain経由)SOP: Standard Operating Procedure。再利用可能な実行手順Intent × Target: SOP識別のための固定意図と可変対象の組candidate SOP: 実行成功後に登録されたSOP候補(Brain承認でactiveになる)active SOP: 実際に使用されるSOPButler OrchestratorはPythonサービス(状態機械)として実装する。
Butler Orchestrator(Pythonサービス)
├─ Contract Validator : 契約検証(必須フィールド・型チェック)
├─ Policy Engine : implicit_scope / require_approval_for の判定(ifコード)
├─ SOP Resolver : Intent×TargetでSOP DBを検索
├─ Approval Gate : Brain経由HITL待機ループ
├─ Execution Backend Selector : task metadata から backend を選択
├─ Executor / Execution Backend : step kindに応じてコマンド実行(subprocess等)
├─ Verifier : success criteria の達成判定
├─ Recorder : SQLiteへの証跡永続化
├─ Qwen Gate : 特定ポイントでのパターンマッチ・振り分け
├─ Agent Executor : agent タスクのライフサイクル管理
└─ Agent Backend : agent 自律ループの backend 実装
補足:
ExecutionBackend は SOP step 実行を担当し、LocalExecutionBackend / WindmillExecutionBackend を切り替えるAgentBackend は delegation_mode="agent" の自律ループを担当し、現在は QwenAgentBackend と LangGraphAgentBackend を切り替え可能agent_backend.py に置き、backend 差し替え時も再利用するLangGraphAgentBackend は ChatOllama と LangGraph の prebuilt react agent を使い、tool 呼び出しと loop 制御を graph 側へ委譲するQwenは以下のポイントでのみ呼ばれる。それ以外は純粋なコード処理。
| ポイント | Qwenの判断内容 |
|---|---|
| SOP未発見時 | need_inputかknown_onlyブロックかを判断 |
| エラー発生時 | 既知エラーパターン(transient)か未知かを判断 |
| 承認要否 | require_approval_forリストに照らして承認要否を判断 |
Qwenがやらないこと:
呼び出し仕様(Ollama):
POST http://localhost:11434/api/chatqwen2.5:7b)format: "json" 指定)呼び出しポイント詳細:
(1) SOP未発見時 — MVP ではQwenを呼ばない(コード分岐で完結)
| delegation_mode | 判定 | 理由 |
|---|---|---|
teach_if_unknown |
need_input |
コード |
known_only |
block |
コード |
ファジーマッチ(近似SOP探索)が必要になった場合はMVP後に追加する。
(2) エラー発生時
[system] あなたはButler Orchestratorの判断ゲートです。JSON形式のみで回答してください。
[user]
コマンド: {command}
エラー出力:
{error_head_tail}
このエラーは一時的(transient: timeout / 接続断 / rate limit 等)ですか、
それとも根本的(unknown)ですか?
期待レスポンス:
{"error_type": "transient"}
// または
{"error_type": "unknown"}
タイムアウト時フォールバック: {"error_type": "unknown"}(Brainへescalate)
既知エラーパターン(transient の例示)はこのプロンプトテンプレートにコードとして埋め込む。動的なパターン追加・管理はMVP外。
(3) 承認要否
[system] あなたはButler Orchestratorの判断ゲートです。JSON形式のみで回答してください。
[user]
実行コマンド: {command}
承認必要カテゴリ: {require_approval_for}
このコマンドはいずれかのカテゴリに意味的に該当しますか?
期待レスポンス:
{"requires_approval": true, "matched_category": "destructive"}
// または
{"requires_approval": false}
タイムアウト時フォールバック: {"requires_approval": true}(安全側)
Verifierはコード(決定論的)として実装する。Qwenは関与しない。
check step の評価:
value をシェルコマンドとして実行するcheck step が pass → status=okstatus=failed(証跡付きでBrainへ)success_criteria の位置づけ:
success_criteria はBrainとの意思疎通用メタデータ(人間向け説明)check stepが担うbutler__teach する際、success_criteria に対応する check stepを必ず含める| kind | 説明 | 実行方法 |
|---|---|---|
cmd |
シェルコマンド実行 | subprocess.run(shell=True) |
check |
検証コマンド実行(exit code 0 = pass) | subprocess.run(shell=True) |
wait |
待機(例: "30s") |
time.sleep() |
mcp_call |
MCPサーバーのツール呼び出し | fastmcp Client(HTTP) |
{
"kind": "mcp_call",
"server": "trilium",
"tool": "search_notes",
"arguments": {"query": "ButlerLayer"}
}
server: MCPサーバーの識別名(環境変数でURL/TOKEN を解決する)tool: 呼び出すツール名(MCPサーバーが公開するツール名)arguments: ツールへ渡す引数(dict)value フィールドは mcp_call では使用しない(空文字列)MCP_{SERVER_NAME}_URL と MCP_{SERVER_NAME}_TOKEN を環境変数で設定する。
SERVER_NAME は大文字で指定する。
# Trilium MCP
MCP_TRILIUM_URL=https://trilium-mcp.keinafarm.net
MCP_TRILIUM_TOKEN=xxx
# Gitea MCP
MCP_GITEA_URL=https://gitea.keinafarm.net/mcp
MCP_GITEA_TOKEN=xxx
# Windmill MCP
MCP_WINDMILL_URL=https://windmill.keinafarm.net/mcp
MCP_WINDMILL_TOKEN=xxx
新しいMCPサーバーを追加する際はコード変更不要。環境変数を追加するだけでSOPから参照できる。
is_always_blocked の対象外(シェルコマンドではないため)step.server が implicit_scope に含まれるかを確認
implicit_scope: ["trilium"] → serverが "trilium" のステップのみ許可BLOCKED(自動)exit_code=0, stdout にレスポンスのJSON文字列exit_code=1, stderr にエラーメッセージexit_code=1, stderr に接続エラーcheck kindの verifier はmcp_call結果には適用しない(mcp_callの成否はexit_codeで判断)。
Windmill backend を使う場合、Butler は intent ごとの専用 flow ではなく、
f/butler/execute_task_steps(既定値)へ Task Contract と resolved steps を渡す。
flow 入力:
{
"task_contract": { "...": "TaskContract" },
"steps": [{ "...": "Step" }],
"context": { "target": {}, "payload": {} }
}
flow 戻り値の想定:
{
"ok": true,
"summary": "Executed 2 step(s) successfully.",
"step_results": [{ "...": "StepResult" }],
"evidence": { "executed_commands": [], "key_outputs": [] }
}
ok: false を返した場合、Windmill job 自体が success=true でも Butler は失敗として扱うcmd / check / wait / mcp_call を実装済みmcp_call は context.mcp_servers[server] = {"url": "...", "token": "..."} を受け取り、JSON-RPC 2.0 の tools/call で MCP サーバーへ直接 POST するStep モデル互換として params も受け付けるが、runner flow 向けの推奨キーは arguments| 成長の種類 | 場所 | 頻度 |
|---|---|---|
| 「何をするか」の成長 | SOP Store(データ) | 高頻度・自動的 |
| 「どう実行するか」の成長 | Orchestratorコード | 低頻度・手動 |
新しいkindが必要になったらコード追加で対応する。MCPサーバーの追加は環境変数追加のみで対応可能(コード変更不要)。
タスク契約を提出した時点が承認。契約の implicit_scope に含まれる操作はButlerが自律実行する。
「Triliumのドキュメントを更新する」
→ implicit_scope: ["trilium"] と設定
→ Triliumへのアクセス全般は自動許可
→ 「Triliumにアクセスしていいですか?」は聞かない
「GitHubにPRを作成する」
→ implicit_scope: ["gitea"] と設定
→ Gitea MCP ツール呼び出しは自動許可
| 操作 | 承認方針 |
|---|---|
| implicit_scope内のread系 | 自動実行 |
| implicit_scope内のwrite系 | 自動実行(require_approval_forにない場合) |
| require_approval_for に列挙された操作 | Brain経由で承認確認 |
| implicit_scope外への操作 | 自動ブロック → escalate |
| destructive操作(rm -rf等) | 常時ブロック |
rm -rf / 系の広範囲削除git reset --harddocker system prune -a(条件なし)承認要求時にBrainへ提示する内容:
BrainがButlerからのescalationを受け取り、意味のある粒度で人間に提示する。
Butlerが人間に直接話しかけることはしない。
Butler → Brain: need_approval(承認要求)
人間 → Brain: 判断(承認/却下)
Brain → Butler: butler__approve(decision)
| Intent | 説明 |
|---|---|
bootstrap_context |
プロジェクト情報・コンテキストの収集 |
collect_logs |
ログ・状態情報の収集 |
triage |
障害・失敗事象の一次切り分け |
deploy |
サービスのデプロイ |
restart |
サービスの再起動 |
health_check |
ヘルスチェック・状態確認 |
commit_push |
git commit / push |
record_to_trilium |
Triliumへの作業記録 |
update_sop |
SOPの修正・更新 |
triage SOP設計制約: ステップは「診断コマンドの実行・証跡収集・エラーパターンの列挙」のみを定義する。根本原因の判定・対処方針の決定はBrainの責務。Qwenはtriageにおいてもエラーパターンの既知/未知を判定するのみで、原因特定は行わない。
| 属性 | 説明 |
|---|---|
service |
対象サービス名 |
repo |
対象リポジトリ |
environment |
prod / staging 等 |
compose_dir |
docker compose の作業ディレクトリ |
branch |
gitブランチ |
health_url |
ヘルスチェックURL |
intent + normalized_target_signature
正規化アルゴリズム:
key=value;key2=value2 形式で結合| で連結例:
target: {"service": "mcp", "environment": "prod"}
→ normalized: "environment=prod;service=mcp"
→ SOPキー: "deploy|environment=prod;service=mcp"
マッチング方式: 完全一致のみ(exact match)。部分マッチ・ファジーマッチはMVP外。
{
"sop_id": "uuid",
"intent": "deploy",
"target_signature": "environment=prod;service=mcp",
"status": "active",
"version": 3,
"steps": [
{"kind": "cmd", "value": "git pull"},
{"kind": "cmd", "value": "docker compose pull"},
{"kind": "cmd", "value": "docker compose up -d"},
{"kind": "check", "value": "health_url returns 200"}
],
"safety_level": "approval_required",
"preconditions": ["docker daemon reachable"],
"last_success_at": "ISO8601",
"success_count": 12,
"failure_count": 2
}
| status | 意味 |
|---|---|
candidate |
実行成功後に登録、Brain承認待ち |
active |
実際に使用されるSOP |
deprecated |
無効化されたSOP(修正時に旧バージョンが遷移) |
SOPが使えなくなる原因と対処法:
| ケース | SOPの状態 | 検出主体 | 対処 |
|---|---|---|---|
| 1. Brainの指示が間違い | 定義が誤り | Brain(証跡を見て判断) | intent=update_sopで書き直し |
| 2. Butlerが誤解釈 | 定義が曖昧 | Brain(結果がおかしい) | 同上(ステップを明確化して再登録) |
| 3-1. 一時的な環境問題 | SOP自体は正しい | Orchestratorが1回リトライ、上限後Brain | SOP変更なし |
| 3-2. 恒久的な環境問題 | SOPが陳腐化 | Brain(escalate後に判断) | intent=update_sopで新SOP作成 |
BrainはTask Contractのintentとして update_sop を使う(専用ツールは作らない、状態機械を通す)。
1. Brain → intent=update_sop でタスク契約提出
2. Butler: 現行SOPをdeprecatedに変更
3. Butler: 新stepsでcandidate SOP作成
4. Butler → Brain: 昇格承認要求
5. Brain: butler__approve(approve)
6. Butler: candidate → active 昇格
blocked としてBrainへneed_input(Brainへ差し戻し)blocked(Brainへ差し戻し)failed(検証フェーズでの不達)本仕様の運用評価は「実行成功数」ではなく、以下の2層で判定する。
verified_ok / Butler_DONE): >= 95%0件teach_if_unknown 系): >= 90%いずれか未達の場合は、モデル差し替えより先に運用フローとSOP品質を是正する。
delegated_tasks / all_tasks): 前週比悪化なしnew_sop_count / new_task_count): 新規領域で >= 30% 目安>= 20% 削減DONE は十分条件ではない。直接検証(外部ソース参照)を優先するagent モード検証と teach_if_unknown 検証を分離し、両方を継続監視するOrchestrator は SOP で表現しきれない複雑な stateful フロー(HITL を含む連鎖操作)を
スキルハンドラー として外部化する仕組みを持つ。
capabilities/
<スキル名>/
handler.py ← HANDLED_INTENTS と handle(conn, task) を定義する
tools.py ← agent モード向けツール定義(別途)
Orchestrator は起動時に capabilities/*/handler.py を自動スキャンし、
HANDLED_INTENTS に列挙された intent をスキルハンドラーに委譲する。
Orchestrator 自体はスキルの知識を持たない。
HITL(request_approval / wait_for_approval)は Core が提供するインフラであり、
スキルハンドラーが butler.approval から import して利用する。
reorganize_trilium_tree Intentbutler/capabilities/trilium/handler.py が処理する。
Orchestrator がスキルハンドラーを経由してディスパッチし、
butler/trilium_structure_ops.py の関数群を payload.operation で呼び出す。
delegation_mode: "known_only" で動作(SOP 不要)implicit_scope: ["trilium"] を指定すること利用可能な operation 一覧:
| operation | 説明 |
|---|---|
fetch_note_tree |
Trilium のノートツリーを再帰取得(可視化・確認用) |
resolve_target_note |
パス/タイトルヒントで配置先ノードを解決 |
apply_note_change |
ノートのコンテンツを更新(replace / append) |
verify_tree_placement |
単一ノードが期待する親ノード配下にあるか確認 |
verify_note_tree |
複数ノードが期待する親ノード配下にあるか一括確認 |
plan_note_moves |
移動計画を作成(実行はしない) |
apply_note_moves |
移動計画を実行(dry_run=true / dry_run=false) |
execute_move_plan |
plan → dry_run → HITL → apply → verify を一括実行 |
fetch_note_tree
{
"operation": "fetch_note_tree",
"root_note_id": "<ルートノートID>",
"depth": 5
}
depth のデフォルト: 5。get_note_tree で再帰ツリーを一括取得する。
verify_note_tree
{
"operation": "verify_note_tree",
"expected_parent_note_id": "<親ノートID>",
"note_ids": ["<noteId1>", "<noteId2>"]
}
list_child_notes で親ノートの子一覧を取得し、note_ids 全件が含まれているか確認する(O(1) 実装)。
verify_tree_placement
{
"operation": "verify_tree_placement",
"note_id": "<対象ノートID>",
"expected_parent_note_id": "<期待する親ノートID>"
}
plan_note_moves
{
"operation": "plan_note_moves",
"source_scope": {"parent_note_id": "<検索範囲の親ノードID>"},
"target_parent_note_id": "<移動先親ノートID>",
"selection_rule": {"title_contains": "<フィルタ文字列>"}
}
移動計画(move_plan リスト)を返すが、実行はしない。
apply_note_moves(直接呼び出し)
{
"operation": "apply_note_moves",
"move_plan": [{"note_id": "...", "target_parent_note_id": "..."}],
"dry_run": true
}
dry_run=false の場合、HITL 承認ゲートを通過してから実行する。
execute_move_plan(推奨: フル HITL フロー)
{
"operation": "execute_move_plan",
"source_scope": {"parent_note_id": "<検索範囲の親ノードID>"},
"target_parent_note_id": "<移動先親ノートID>",
"selection_rule": {"title_contains": "<フィルタ文字列>"}
}
内部で以下の順序で実行する:
plan_note_moves(計画作成)
→ apply_note_moves(dry_run=True)(シミュレーション)
→ NEED_APPROVAL(Brain 経由で人間が butler__approve を呼ぶ)
→ apply_note_moves(dry_run=False)(実行)
→ verify_note_tree(確認)
承認拒否・いずれかの段階で失敗 → blocked / failed として返却。
{
"task_id": "move-xxx-001",
"intent": "reorganize_trilium_tree",
"target": {"service": "trilium"},
"delegation_mode": "known_only",
"constraints": {
"implicit_scope": ["trilium"],
"require_approval_for": [],
"max_minutes": 5
},
"success_criteria": ["notes moved and verified"],
"requested_by": "brain",
"requested_at": "<ISO8601>",
"payload": {
"operation": "execute_move_plan",
"source_scope": {"parent_note_id": "h7sie7BVhEZQ"},
"target_parent_note_id": "uWE5IU3TgFOC",
"selection_rule": {"title_contains": "仕様書"}
}
}
max_minutes は per-MCP-call タイムアウト(タスク全体の上限ではない)。
execute_move_plan は複数フェーズを経るため、タスク全体の所要時間はフェーズ数 × max_minutes を超えうる。
ButlerLayer_アーキテクチャ定義書_v0.3.mdButlerLayer_契約通信仕様書_v0.3.mdButlerLayer_データ運用仕様書_v0.3.md../capabilities/trilium/仕様書.md以上を Butler Layer 実行・SOP仕様書 v0.3 とする。