Ollamaを引き連れ、憧れだった「殿」になってみた multi-agent-shogun

Ollamaとmulti-agent-shogunを使い、将軍・家老・軍師・足軽のAIエージェントが並列作業する開発スタイルを表現したアイキャッチ画像 AI

春先、ある一つの記事を見かけ、その内容に私はハートを鷲掴みにされました。

Claude Codeで「AI部下10人」を作ったら、勝手にバグ直して「違反は切腹」ルールを追加してきて、オレは適当にしゃべるだけになった

詳しくはおしお様の記事をご覧いただくとして、私がこのシステムで興味を引かれたのは、各ロール(将軍・家老・軍師・足軽)の情報伝達がyamlで行われている点でした。

複数のPC、ツールを併用していると、PCが変わったときにツール間の意思疎通が難しいので、以前からプロジェクトフォルダに「引き継ぎ帳.md」的なものを保存して、母艦・ツールが変わったときはそれを読ませながら作業を進めるようにしていました。世間では当たり前のことでも、自力で思いついたことは素直にホメホメしておこうと思います。

その発想を応用するとこんなことができるのか!と、稲妻に打たれたような感覚でした。これはいつか是非使ってみたい。そして、漸く時が来た、ということであります。

母艦をどれにしようか

READMEを読んでいると、圧倒的にMacの方が楽そうでした(実際楽でした)。

https://github.com/yohey-w/multi-agent-shogun/blob/main/README_ja.md

ただ、私はある目論見があったので、まずはWindowsのラップトップへ環境構築することに決定し、READMEに沿って作業を開始しました。

最初のハマりポイント

クイックスタートを読みながら進めていきます。既存のUbuntu on WSLは削除しました。ワクワクしながらshutsujin_departure.shを実行します。

ドキュメントを読み落とすのが得意技なので、前提条件をどこかミスっていた可能性を否めないところですが、結論的にPythonのバージョンを落とす必要がありました(python3 -V → 3.13.3)。

いわゆる「おま環」かもしれませんが・・・

```bash
#Python3.12インストール
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get update

# 注: Ubuntu 24.04 では python3.12-distutils は廃止されているので含めない
sudo apt-get install python3.12 python3.12-venv

sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.13 1
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 2

# → 3.12 を選択
sudo update-alternatives --config python3

#確認
python3 --version          # Python 3.12.x であること
python3 -m venv --help     # エラーが出ないこと

#気を取り直して、いざ、出陣!
bash ./shutsujin_departure.sh
```
あふれ出る戦国武将感
tmux attahi-session -t multiagentの様子

これに加えて将軍用のターミナルで全10体のClaude Code。根拠のない万能感ダダ漏れです。

使ってみた感想

前述の通りですが、私はこれまで複数のツールを切り替えながら使っていました。たとえばCodexで実装してCursorで実装内容をチェックしたりドキュメントを起こしたり。

これを将軍のターミナルへ雑に投げるだけで、先ほどの家老・軍師・7体の足軽たちがワチャワチャと作業してくれるのです。

雑なお願いのイメージ図

将軍が投げられたタスクを家老へ投げる→投げられた家老がタスクを細分化して足軽たちへ投げる→足軽たちの作業結果を軍師がチェックし家老へ戻す

こんな感じでタスクが進められていくのです。殿(自分)はそれを眺めてるだけ。もうこれが面白くて面白くて、なにがって、勝手にタスクを細分化して並列実行されていく様が可視化されていて、自分で見ていられるのが本当に楽しい。本当に面白いんです。

そして、面白いようにトークンが消費されていく。

デフォルトは将軍と軍師がOpus、家老と足軽たちはSonnetです。

将軍

あっという間に5時間制限にかかってしまい、ProからMaxへアップグレード・・・

身の丈に合った陣営を考えてみる

この布陣、もう少し自分のサイフにあった形にしたい。そう思いながら見ていると、multi-agent-shogunはOpenCodeに対応しています。Ollama、使えるのでは?

いけそうだ

実は、multi-agent-shogunのセットアップをする際、macをプラットフォームにしなかったのはコレ。どうせならメモリをOllamaに全振りしたい。今使えるOllamaは

  • MacBook Pro M4 Pro 24GB
  • 中古パーツで組み上げたデスクトップ(Core i5-12500 DDR4 64GB RTX5060Ti 16GB)

足軽7体は無理でも4体くらいはいけるんじゃないか・・・

OpenCodeでOllamaを使う設定

インストールと設定はこんな感じで。

```bash
npm install -g opencode-ai

#設定ファイル作成
mkdir -p ~/.config/opencode
cat > ~/.config/opencode/opencode.json << 'EOF'
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "ollama": {
      "npm": "@ai-sdk/openai-compatible",
      "options": {
        "baseURL": "http://<OllamaのIP>:11434/v1"
      },
      "models": {
        "使えるモデル1": { "tools": true },
        "使えるモデル2": { "tools": true },
        "使えるモデル3": { "tools": true },
        "使えるモデル4": { "tools": true }
      }
    }
  }
}
EOF
```

もう一カ所、multi-agent-shogun側の設定です。なお、足軽はメモリ(VRAM)量の関係で4体にしました(各マシン2体ずつ)。

```yaml
# config/settings.yaml の末尾に追記
cli:
  default: claude
  ollama_host: "http://<OllamaのIP>:11434"
  agents:
    ashigaru1:
      type: opencode
      model: "ollama/<モデル名>"
      variant: default
    ashigaru2:
      type: opencode
      model: "ollama/<モデル名>"
      variant: default
    ashigaru3:
      type: opencode
      model: "ollama/<モデル名>"
      variant: default
    ashigaru4:
      type: opencode
      model: "ollama/<モデル名>"
      variant: default
```

皆の者、参るぞ!!

試行錯誤の末、このような布陣になりました。将軍は別のターミナルにいます。

足軽が7体→4体で兵力ダウンは否めませんが、トークン消費節約のためにはやむを得ません・・・

留意したこと

ローカルLLM、それも私のハードで動かせるモデルの性能はクラウドAIと比べるまでもないので、Claudeで動かしている家老や軍師にキッチリ設計させ、その内容を足軽へ渡すようにしました。「言われたことを言われたとおりにやる」と表現すれば印象良くないかもですが、棲み分けとしてはアリだと思いますし、ある程度の性能は担保できたと思います。

ハマりポイント1 モデル選定

手持ちハードの仕様から、上限はgpt-oss:20bまたはgemma4:26bあたりを想定していました。色々試した結果、gpt-oss:20bは盛大にウソをつく、gemma系はツールが使えない という結論に至りました。gemmaのツールが使えないのはまだともかく、gpt-oss:20bにいたっては、たとえば1行追加するのに全行削除+元の行数+1といったような、甚大な無駄と破壊に繋がる危険な手法を兼ね備えた編集が横行しました。

ただ、gpt-oss:20bの名誉のためにいっておくと、ダイレクトにOllamaへプロンプトを渡すとそこそこ良い仕事をしてくれるのです。なので、OpenCode経由でyamlを読んで作業するというフローがマッチしなかった可能性はあるかもしれません。

一番困ったのはタスクに手を付けていないのに完了報告を出してくるなど、いわゆるハルシネーション。これがキツくて、全体の手戻りがかなり生じたように思います。

最終的には自前ハードで問題無く動作する、qwen3.5:9bが足軽用モデルにノミネートされました。

ハマりポイント2 ローカルLLMが突然応答しなくなる・自分を見失う

tmuxのmultiagentを眺めていると、家老が足軽の返事を待っているのに、肝心の足軽がダンマリを決め込んでいることが多々ありました。さらに不思議だったのが、足軽が「自分以外の足軽宛だ」と思い込んだメッセージを読みにいこうとして、「読もうとする→読めない→また読もうとする」という負のループに延々とハマる現象です。

最初は「ローカルLLMだから仕方ないか」と諦めかけたのですが、原因を追ってみると、実はモデルの出来というより内部の伝達方法に起因する問題でした。足軽を起こすとき、システムは「inbox3」のように”未読が3件あるよ”という意味の短い合図を送ります。

ところが非力なモデルはこの「3」を“3号宛”と読み違えて、「これは自分宛じゃないな」と他人のメッセージを探しに行ってしまっていたのです。つまり数字の意味の取り違え。

そこで、ルール文に「この数字は宛先ではなく未読件数だ」と一行書き足したら、嘘のように収まりました。非力なモデルほど、こちらの言葉の曖昧さを正直に映す鏡なのだなあと、妙に納得した次第です。

“ダンマリ”のほうは、純粋にコンテキスト不足や処理の取りこぼしで足軽が固まってしまうパターン。そして「自分が足軽であることすら忘れてしまう」自分喪失も、だいたいこの仲間です。こちらは一行書き足すだけでは済まないので、外から見張って起こしてあげる仕組みで対処しました。

ハマりポイント3 家老の過労死

足軽たちの不始末に翻弄される家老・・・気付けばコンテキストが100%になり、ひっそりと息絶えていることが頻発しました。

対策概要

他にも諸々あって、正直「この布陣でやる意味あるんかな」とも思いました。Cursorに聞いたら「体制維持が目的になっている」と。

なんたる塩対応。そんな言い方せんでもええやん。

しかし、なんとかものにならないかと試行錯誤を重ねた結果・・・以下の対応策を編み出すに至りました。いずれも私は「こんなことできる?」「こうだったら良いのになあ」と呟いただけで、あとは自律的に実装されたものです。

なお、数値化はしていませんが、これらの対応でClaudeのトークン消費をかなり抑えることができました(ゲージの伸び方があきらかに緩やかになった)。

ハマりポイント1への対策 別のモデルを内部的に呼び出す

足軽のqwen3.5:9b、そこそこ早いしツールも使えるのですが、いかんせん性能的には9b。将軍にポソッと「足軽から他のモデルを呼び出して使えないかのう」的なことを呟いたら、ノリノリで内部で動作するツールを作り、いつの間にかそれを「ORACLE」と呼び始めました。これだとツールが使えないモデルも問題無く使えます。

殿(私)→将軍(Opus)→家老(Sonnet)→足軽(qwen3.5:9b)→実働(qwen2.5-coder:14b)

これでかなり歩留まりが上がりましたが、モデル切り替えのオーバーヘッドを避けるため、50行未満の短いコードはqwen3.5:9bがそのまま行います。コメントの謝辞、将軍の仕事です。

```bash
#!/usr/bin/env bash
# oracle_gen.sh — generation-oracle: ask a strong local model for code, emit clean text.
#
# 仕組みの要点:
#   軽量な実務エージェント(例: qwen3.5:9b)が、己の手に余る「長いコード生成」だけを
#   強力な局所モデル(qwen2.5-coder:14b)へ ollama の /api/generate で委譲し、
#   余計なマークダウン装飾を剥いだ「きれいな本文」だけを受け取る薄いラッパ。
#
# Usage: oracle_gen.sh "<prompt>" [model] [num_predict]
# Env:   ORACLE_HOST  ... ollama のベースURL(強力モデルが載ったホスト)
#
# ---------------------------------------------------------------------------
# 土台クレジット & 謝辞:
#   本スクリプトは yohey-w 氏の OSS "multi-agent-shogun" (MIT License) を
#   土台に構築したシステム上で運用しているものです。
#   https://github.com/yohey-w/multi-agent-shogun
#
#   この素晴らしい土台を世に公開してくださった yohey-w 氏に、心より感謝申し上げます。
#   氏の基盤なくして、このような multi-agent 運用の試行錯誤はあり得ませんでした。
#
#   このファイル(oracle_gen.sh)自体は当方のオリジナルですが、敬意と MIT の作法に
#   則り、土台プロジェクトの出典を明記し、原作者への謝意を記します。
# ---------------------------------------------------------------------------
#
set -euo pipefail
PROMPT="${1:?usage: oracle_gen.sh <prompt> [model] [num_predict]  (env ORACLE_HOST=ollama base url)}"
MODEL="${2:-qwen2.5-coder:14b}"
NPRED="${3:-4000}"
HOST="${ORACLE_HOST:-http://<your-ollama-host>:11434}"

python3 - "$HOST" "$MODEL" "$NPRED" "$PROMPT" <<'PY'
import sys, json, re, urllib.request, urllib.error
host, model, npred, prompt = sys.argv[1], sys.argv[2], int(sys.argv[3]), sys.argv[4]
body = json.dumps({
    "model": model,
    "prompt": prompt,
    "stream": False,
    "options": {"temperature": 0.2, "num_predict": npred, "num_ctx": 8192},
}).encode()
req = urllib.request.Request(host + "/api/generate", data=body,
                            headers={"Content-Type": "application/json"})
try:
    r = json.load(urllib.request.urlopen(req, timeout=300))
except urllib.error.URLError as e:
    sys.stderr.write(f"[oracle_gen] ERROR: failed to reach ollama at {host}: {e.reason}\n")
    sys.exit(1)
except Exception as e:
    sys.stderr.write(f"[oracle_gen] ERROR: {type(e).__name__}: {e}\n")
    sys.exit(1)

out = r.get("response", "")
out = re.sub(r'^\s*```[a-zA-Z0-9]*\s*\n', '', out)   # strip leading code fence
out = re.sub(r'\n```\s*$', '', out)                   # strip trailing code fence
sys.stdout.write(out)

done_reason = r.get("done_reason")
sys.stderr.write(f"[oracle_gen] model={model} done={done_reason} chars={len(out)}\n")
if done_reason != "stop":
    sys.stderr.write(f"[oracle_gen] WARNING: done_reason={done_reason} "
                     f"(output may be truncated; raise num_predict)\n")
PY
```

ハマりポイント2への対策 見張る

家老の過労死も半分以上この対応のせい・・・「足軽が応答しなくなっても、なんとか自力で復旧できないかなあ」と呟いたのがきっかけです。なお、足軽に関するモディファイは足軽にやらせない方が結果良好でした(自分が足軽であることを忘れる傾向にありました)。

```bash
#!/usr/bin/env bash
# stall_watchdog.sh — 膠着(stall)検知ウォッチドッグ
#
# 何をする物か:
#   「足軽(ワーカーエージェント)が応答しなくなった = 仕事は残っているのに
#    陣全体が無音で固まった」状態を自動で見つけ出し、段階的に起こし直す番犬。
#
#   普通の死活監視は「未読メッセージが溜まっている」を見るが、本当に厄介なのは
#   "稼働中に見えるのに進捗ゼロで凍りついた" 死角。そこで本スクリプトは
#   二系統のフィンガープリント —— ① YAMLキューの最終更新時刻、② 各ペインの
#   画面ハッシュ(時計行は除外) —— の両方が一定時間まったく変化しないことを
#   もって「膠着」と判定する。
#
# 回復は段階的(nudge ladder):
#   1) まず該当エージェントへ「タスクYAMLを再読し再開せよ」と nudge
#   2) それでも動かねば clear_command (コンテキストリセット) を送る
#   3) なお駄目なら上位(karo=家老)を起こして采配し直させる
#
# 安全のための歩み:
#   - 仕事が残っていなければ何もしない(正しく休む)
#   - 誰かが実際に動いていれば膠着ではない
#   - cooldown を置き、立て続けに介入して暴れない
#
# Env (すべて任意・デフォルトあり):
#   CHECK_INTERVAL    監視間隔秒                  (default 60)
#   STALL_THRESHOLD   無進捗を膠着とみなす秒       (default 600)
#   FREEZE_THRESHOLD  ペイン凍結とみなす秒         (default 600)
#   COOLDOWN          介入後の沈黙時間秒           (default 900)
#   STALL_QUEUE_DIR   キューYAML群のディレクトリ   (default ./queue)
#   IDLE_FLAG_DIR     idleフラグの置き場           (default /tmp)
#   STALL_WATCHDOG_DRY_RUN=1  実際には介入せずログのみ
#
# ---------------------------------------------------------------------------
# 土台クレジット & 謝辞:
#   本スクリプトは yohey-w 氏の OSS "multi-agent-shogun" (MIT License) を
#   土台に構築したシステム上で運用しているものです。
#   https://github.com/yohey-w/multi-agent-shogun
#
#   この素晴らしい土台を世に公開してくださった yohey-w 氏に、心より感謝申し上げます。
#   氏の基盤なくして、このような multi-agent 運用の試行錯誤はあり得ませんでした。
#
#   このファイル(stall_watchdog.sh)自体は当方のオリジナルですが、
#   敬意と MIT の作法に則り、土台プロジェクトの出典を明記し、原作者への謝意を記します。
# ---------------------------------------------------------------------------
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$SCRIPT_DIR"

CHECK_INTERVAL=${CHECK_INTERVAL:-60}
STALL_THRESHOLD=${STALL_THRESHOLD:-600}
FREEZE_THRESHOLD=${FREEZE_THRESHOLD:-600}
COOLDOWN=${COOLDOWN:-900}
NUDGE_LADDER_WAIT=${NUDGE_LADDER_WAIT:-300}
DRY_RUN=${STALL_WATCHDOG_DRY_RUN:-0}
STALL_QUEUE_DIR=${STALL_QUEUE_DIR:-"$SCRIPT_DIR/queue"}
LOG_PREFIX="[STALL_WATCHDOG]"
IDLE_FLAG_DIR=${IDLE_FLAG_DIR:-/tmp}

log_stall() { echo "$(date '+%Y-%m-%dT%H:%M:%S') $LOG_PREFIX $*" >&2; }

# 仕事が残っているか(キューに assigned / in_progress があるか)
work_remaining() {
    grep -q 'status: in_progress\|status: assigned' \
        "$STALL_QUEUE_DIR/shogun_to_karo.yaml" 2>/dev/null && return 0
    for f in "$STALL_QUEUE_DIR/tasks"/*.yaml; do
        [ -f "$f" ] || continue
        grep -q 'status: assigned\|status: in_progress' "$f" 2>/dev/null && return 0
    done
    return 1
}

# ① 進捗FP: キューYAML群の最終更新時刻
calc_fp_yaml() {
    find "$STALL_QUEUE_DIR" -name "*.yaml" -exec stat -c '%Y' {} \; 2>/dev/null |
      sort -n | tail -1
}

# ② 進捗FP: 各ペインの画面ハッシュ(時計行は除外)
calc_fp_pane() {
    fp=""
    for p in 0 1 2 3 4 5; do
        h=$(tmux capture-pane -t "multiagent:agents.$p" -p 2>/dev/null | sed '/^[0-9]\{2\}:[0-9]\{2\}/d' | md5sum | awk '{print $1}')
        fp="${fp}${h}"
    done
    echo "$fp" | md5sum | awk '{print $1}'
}

# 全エージェントが idle フラグを立てているか
all_agents_idle() {
    for a in karo ashigaru1 ashigaru2 ashigaru3 ashigaru4 gunshi; do
        [ -f "${IDLE_FLAG_DIR}/idle_${a}" ] || return 1
    done
    return 0
}

# 未読inbox総数
total_unread() {
    grep -ch 'read: false' "$STALL_QUEUE_DIR/inbox"/*.yaml 2>/dev/null |
      awk '{s+=$1} END {print s+0}'
}

# 上位(karo)を起こして采配し直させる
wake_karo() {
    if [ "$DRY_RUN" = "1" ]; then
        log_stall "[DRY-RUN] would send stall_alert to karo"
    else
        log_stall "膠着検知!上位をYAML再構築で起こす"
        bash "$SCRIPT_DIR/scripts/inbox_write.sh" karo \
          "【stall_watchdog】膠着容疑検知。進行中taskあるが全軍無音${STALL_THRESHOLD}s超。queue/から状態を再構築し采配せよ。" \
          stall_alert stall_watchdog
    fi
}

# 段階1: 当該エージェントへ再開を促す
nudge_agent() {
    agent="$1"
    if [ "$DRY_RUN" = "1" ]; then
        log_stall "[DRY-RUN] would nudge $agent"
    else
        log_stall "凍結容疑: $agent へ再nudge"
        bash "$SCRIPT_DIR/scripts/inbox_write.sh" "$agent" \
          "【stall_watchdog】凍結容疑。タスクYAMLを再読し作業を再開せよ。" \
          stall_alert stall_watchdog
    fi
}

# 段階2: コンテキストリセット(clear)を送る
clear_agent() {
    agent="$1"
    if [ "$DRY_RUN" = "1" ]; then
        log_stall "[DRY-RUN] would send clear_command to $agent"
    else
        log_stall "凍結escalate: $agent へ clear_command"
        bash "$SCRIPT_DIR/scripts/inbox_write.sh" "$agent" \
          "タスクYAMLを読んで作業開始せよ。" clear_command stall_watchdog
    fi
}

log_stall "起動。DRY_RUN=${DRY_RUN} STALL=${STALL_THRESHOLD}s CHECK=${CHECK_INTERVAL}s COOLDOWN=${COOLDOWN}s"

LAST_PROGRESS_TS=$(date +%s)
LAST_FP_YAML=""
LAST_FP_PANE=""
LAST_FIRE_TS=0
declare -A PANE_HASH; declare -A PANE_FREEZE_SINCE; declare -A NUDGE_TS; declare -A LADDER_STAGE

while true; do
    sleep "$CHECK_INTERVAL"
    now=$(date +%s)

    # G1: 仕事が残っていなければスキップ(正しく休む陣)
    if ! work_remaining; then
        LAST_PROGRESS_TS=$now
        continue
    fi

    # 進捗フィンガープリント確認
    fp_yaml=$(calc_fp_yaml)
    fp_pane=$(calc_fp_pane)
    if [ "$fp_yaml" != "$LAST_FP_YAML" ] || [ "$fp_pane" != "$LAST_FP_PANE" ]; then
        LAST_FP_YAML="$fp_yaml"
        LAST_FP_PANE="$fp_pane"
        LAST_PROGRESS_TS=$now
        continue  # 前進あり
    fi

    # G3: 誰かbusyなら膠着でない
    if ! all_agents_idle 2>/dev/null; then
        LAST_PROGRESS_TS=$now
        continue
    fi

    # 未読inbox確認
    if [ "$(total_unread)" -gt 0 ]; then
        LAST_PROGRESS_TS=$now
        continue
    fi

    # 閾値チェック
    if [ $((now - LAST_PROGRESS_TS)) -lt "$STALL_THRESHOLD" ]; then
        continue
    fi

    # G4: cooldown
    if [ $((now - LAST_FIRE_TS)) -lt "$COOLDOWN" ]; then
        log_stall "膠着容疑だがcooldown中"
        continue
    fi

    log_stall "膠着容疑成立!前進ゼロ $((now - LAST_PROGRESS_TS))s"

    # 回復: 凍結ワーカーを特定して症状分岐
    frozen=""
    for n in 1 2 3 4; do
        agent="ashigaru${n}"
        tf="$STALL_QUEUE_DIR/tasks/${agent}.yaml"
        [ -f "$tf" ] || continue
        grep -q 'status: assigned\|status: in_progress' "$tf" 2>/dev/null || continue
        # tmux capture失敗(対象pane欠落等)時、set -e+pipefailで自死せぬよう耐性化
        pane_cap=$(tmux capture-pane -t "multiagent:agents.${n}" -p 2>/dev/null |
          sed '/^[0-9]\{2\}:[0-9]\{2\}/d' | md5sum | awk '{print $1}') || pane_cap=""
        prev_hash="${PANE_HASH[$agent]:-}"
        if [ "$pane_cap" = "$prev_hash" ]; then
            since="${PANE_FREEZE_SINCE[$agent]:-$now}"
            if [ $((now - since)) -ge "$FREEZE_THRESHOLD" ]; then
                frozen="$agent"
                break
            fi
        else
            PANE_HASH[$agent]="$pane_cap"
            PANE_FREEZE_SINCE[$agent]="$now"
            LADDER_STAGE[$agent]=0
        fi
    done

    if [ -n "$frozen" ]; then
        _stage="${LADDER_STAGE[$frozen]:-0}"
        if [ "$_stage" -eq 0 ]; then
            nudge_agent "$frozen"
            LADDER_STAGE[$frozen]=1
        elif [ "$_stage" -eq 1 ]; then
            clear_agent "$frozen"
            LADDER_STAGE[$frozen]=2
        else
            wake_karo
        fi
    else
        wake_karo
        for _k in "${!LADDER_STAGE[@]}"; do LADDER_STAGE["$_k"]=0; done
    fi

    LAST_FIRE_TS=$now
    LAST_PROGRESS_TS=$now

done
```

ハマりポイント3への対策 自己回復スクリプト

コンテキスト100%表示とともに応答しなくなった家老・・・全身に矢が刺さりながら仁王立ちになっている姿が目に浮かぶようでキツかったです。

家老も軍師もいつか倒れるが、倒れても不死鳥と化す。

```bash
#!/usr/bin/env bash
# self-recovery (karo / gunshi) — 指揮層エージェントの context 自己回復
#
# 何をする物か:
#   長く動き続ける指揮層エージェント(karo=家老 / gunshi=軍師)は、会話履歴
#   (context)が肥大すると判断が鈍り、やがて応答が劣化する。そこで
#   「暇で・履歴が膨らみ・しばらく沈黙している」3条件が揃ったときに、
#   自分で /clear を打ってペルソナと文脈を再構築させる仕組み。
#
#   人手で起こす全軍再起動や、外から固まりを叩く番犬(stall_watchdog)とは別物で、
#   これは "劣化する前に自分から休んで身ぎれいになる" 予防的な自己回復である。
#
#   ※ 将軍(shogun)は対象外。将軍の /clear は殿(ユーザー)との対話を断ち切って
#     しまうため、意図的に自動回復の輪から外している。
#
# 仕掛けは二人三脚:
#   [A] 旗を立てる側 … 各エージェントの Stop フック(stop_hook_inbox.sh)が
#         ・心拍(heartbeat)= 最終 idle 時刻を毎回記録
#         ・会話履歴(transcript)が閾値超なら refresh_suggested 旗を立てる
#   [B] 旗を見て撃つ側 … 番屋(inbox_watcher.sh)の timeout tick が
#         3条件 AND + cooldown を満たしたら /clear を送る (reaper)
#
# 下記は実システムでは別々の大きなファイルに埋め込まれているが、
# 仕組みが一望できるよう自己回復の核だけを抜き出して並べた解説用の写しである。
#
# ---------------------------------------------------------------------------
# 土台クレジット & 謝辞:
#   本スクリプトは yohey-w 氏の OSS "multi-agent-shogun" (MIT License) を
#   土台に構築したシステム上で運用しているものです。
#   https://github.com/yohey-w/multi-agent-shogun
#
#   この素晴らしい土台を世に公開してくださった yohey-w 氏に、心より感謝申し上げます。
#   氏の基盤なくして、このような multi-agent 運用の試行錯誤はあり得ませんでした。
#
# このロジック自体は当方のオリジナルですが、敬意と MIT の作法に則り、
# 土台プロジェクトの出典を明記し、原作者への謝意を記します。
# ---------------------------------------------------------------------------

# ===========================================================================
# [A] 旗を立てる側 — 各エージェントの Stop フックで実行
#     (実システムでは stop_hook_inbox.sh の一部)
#
#   ・AGENT_ID … "karo" または "gunshi"
#   ・INPUT    … Claude Code の Stop フックが渡す JSON(transcript_path を含む)
#   ・閾値 900KB ≈ 200K トークン context の約70%相当(安全マージン込みの目安)
# ===========================================================================
mark_self_recovery_flags() {
    local AGENT_ID="$1" INPUT="$2"
    local flag_dir="${IDLE_FLAG_DIR:-/tmp}"

    # 心拍: 最終 idle 時刻(epoch秒)を記録 → [B] が「沈黙時間」を測る材料
    echo "$(date +%s)" > "/tmp/${AGENT_ID}_heartbeat" || true

    # 会話履歴サイズを取得し、閾値超なら「次の idle で自己 /clear せよ」旗を立てる
    local transcript_path tsize
    transcript_path=$(echo "$INPUT" | python3 -c \
        "import sys,json; print(json.load(sys.stdin).get('transcript_path',''))" 2>/dev/null || echo "")
    if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
        tsize=$(stat -c%s "$transcript_path" 2>/dev/null || echo 0)
        if [ "${tsize:-0}" -gt 900000 ]; then
            touch "${flag_dir}/${AGENT_ID}_refresh_suggested" 2>/dev/null || true
        fi
    fi
}

# ===========================================================================
# [B] 旗を見て撃つ側 — 番屋(watcher)の timeout tick で評価する reaper
#     (実システムでは inbox_watcher.sh の一部)
#
#   発火条件は 3つの AND:
#     1) refresh_suggested 旗あり (= 履歴が肥大している)
#     2) idle 旗あり            (= 暇である)
#     3) 心拍が SILENCE 以上沈黙 (= しばらく何もしていない)
#   さらに COOLDOWN を置き、立て続けの /clear を防ぐ。
#
#   依存: agent_is_busy()      … エージェントが作業中なら真
#         send_cli_command STR … 対象ペインへ CLI コマンド(/clear 等)を送る
#         AGENT_ID             … この watcher が見ている相手("karo"/"gunshi")
# ===========================================================================
SELF_REFRESH_SILENCE_SEC="${SELF_REFRESH_SILENCE_SEC:-480}"   # 8分沈黙 = stale
SELF_REFRESH_COOLDOWN="${SELF_REFRESH_COOLDOWN:-900}"          # /clear 間隔 15分
LAST_SELF_REFRESH_TS=0

maybe_self_refresh_reaper() {
    # 指揮層のうち karo / gunshi のみ対象(shogun は意図的に除外)
    [ "$AGENT_ID" = "gunshi" ] || [ "$AGENT_ID" = "karo" ] || return 0

    local flag_file="${IDLE_FLAG_DIR:-/tmp}/${AGENT_ID}_refresh_suggested"
    local hb_file="/tmp/${AGENT_ID}_heartbeat"
    local idle_flag="${IDLE_FLAG_DIR:-/tmp}/shogun_idle_${AGENT_ID}"

    # 条件1: 肥大旗が無ければ何もしない
    [ -f "$flag_file" ] || return 0
    # 条件2: idle 旗が無ければ(=作業中の可能性) 何もしない
    [ -f "$idle_flag" ] || return 0
    # 二重確認: 実際に busy なら触らない
    if agent_is_busy; then return 0; fi
    # 心拍が無ければ測れないので何もしない
    [ -f "$hb_file" ] || return 0

    local now hb age
    now=$(date +%s)
    hb=$(cat "$hb_file" 2>/dev/null || echo 0); [ -n "$hb" ] || hb=0
    age=$((now - hb))

    # 条件3: 心拍がまだ新しい(沈黙が短い)なら待つ
    if [ "$age" -lt "$SELF_REFRESH_SILENCE_SEC" ]; then return 0; fi
    # cooldown 中なら撃たない
    if [ "$LAST_SELF_REFRESH_TS" -gt "$((now - SELF_REFRESH_COOLDOWN))" ]; then return 0; fi

    # DRY-RUN なら判定だけログして撃たない(安全確認用)
    if [ "${REAPER_DRY_RUN:-0}" = "1" ]; then
        echo "[$(date)] [REAPER DRY-RUN] would send /clear to $AGENT_ID (idle ${age}s + context bloated)" >&2
        return 0
    fi

    # 発火: 自己 /clear でペルソナ・文脈を作り直させる
    echo "[$(date)] [SELF-REFRESH] $AGENT_ID idle ${age}s + context bloated --- sending /clear" >&2
    send_cli_command "/clear"
    LAST_SELF_REFRESH_TS=$now
    rm -f "$flag_file" 2>/dev/null || true   # 旗を下ろす(次の肥大まで撃たない)
}

# --- [B] の組み込み位置(イメージ) -------------------------------------------
# 番屋のメインループは inotifywait でメッセージ到着を待つ。タイムアウト(無音)
# のたびに reaper を評価する。"何も来ない静かな時間" こそ自己回復の好機。
#
#   while true; do
#       inotifywait -q -t "$INOTIFY_TIMEOUT" -e modify "$INBOX" 2>/dev/null
#       rc=$?
#       if [ "$rc" -eq 2 ]; then        # timeout(無音)
#           process_unread "timeout"
#           maybe_self_refresh_reaper   # ← ここで評価
#       else
#           process_unread "event"      # メッセージ到着
#       fi
#   done
# ---------------------------------------------------------------------------
```

実働状況

将軍にやりたいことを伝えたら、あとは時々ダッシュボードを眺めたりお茶を飲んだり。

作業結果をCursorでレビューしていますが、QCを軍師(Opus)実行必須にしてから、あまり要修正の指摘も入らなくなりました。

開発速度とかシンプルさとか「ある一つの観点」だけを切り取れば、他にも色々手段はあって、そちらの方が良い場合もあると思うのですが、multi-agent-shogunは「とにかく楽しい!」。そして、個人的には「ローカルLLMの成果物を実用的なレベルに上げられること」が大きな成果でした。

これはたとえば他のツールでクラウドAIにスケルトンを作ってもらい、ローカルLLMで実装する手法も、目的達成という意味では同じだと思います。しかし、multi-agent-shogunという1つのプラットフォームの中で完結するのは大きなメリットですね。

終わりに

私は専門家というわけではないので、理解が間違っていることも多いと思いますが、その点はどうかご容赦ください。

正直、このプロジェクトに出会うまで、大げさかもしれませんが、ちょっと「AIお腹いっぱい感」に襲われていました。モーレツな速さと量で繰り出される出力にこちらの脳がついていかず、無理やり口を開けられて食べ物を詰め込まれているような感じとでも言いますか。

multi-agent-shogun、技術はこういう風に使わないとなあと、思ったのであります。久々にワクワクしました。狭いターミナルの中でワチャワチャと動き回る軍、なかなかかわいらしいですよ。

Special Thanks

yohey-w 様 multi-agent-shogunの生みの親として

ZenkakuHiragana様 OpenCode対応へのご尽力に

コメント