Hermes Agentのcronで収集と判断を分ける|scriptで入力を固定してからAIを起こす

Hermes Agentのcronで収集と判断を分ける|scriptで入力を固定してからAIを起こす

同じ収集スクリプトを 2 回続けて動かすと、どちらも 29 行の JSONL になりました。SHA-256 も同じ63ade239b9c62308965b7f4389ef9690eeca855ad29f71c6e18762683daa49cfです。

AI エージェントへ「新着ブログを見て」とだけ頼むと、取得と選別が 1 回の推論へ混ざります。取得失敗なのか、記事がないのか、LLM が見落としたのかを後から分けにくくなります。私は、機械的な収集を Python へ寄せ、内容の判断だけを Hermes Agent へ渡す形が扱いやすいと考えています。

前回は cron の provider と model を固定し、チャット側の変更から無人ジョブを守りました。今回はモデルへ入る前のデータを固定します。

前回から今回への差分

項目前回完了時今回完了後
推論モデルper-job pinかcron.modelで固定変更なし
情報収集agentのtool呼び出しに依存Python scriptへ分離
入力形式実行ごとに揺れる可能性あり1記事1行のJSONLへ固定
変化の確認agentが毎回ページを読むbyte単位のhashで比較可能
障害の場所取得と判断が同じrunに混在scriptの終了コードとagent runを分けて確認

すでに安定した API や RSS を使い、取得結果を JSON へ整形できている場合は、「収集スクリプトの中身」を飛ばして「3 つの実行モード」から読めます。

なぜ収集をLLMから外すのか

LLM は、複数の記事から読む価値があるものを選ぶ仕事に向いています。一方、URL を開き、リンクを重複排除し、決まったキーで並べる処理は普通のプログラムの方が再現しやすいです。

今回使ったblog_subscription_monitor.pyは、次の 3 ページを巡回します。

  • Cursor Blog
  • Anthropic Research
  • OpenAI Codex Blog

各記事をsitelisting_urltitleurlの 4 項目へ揃えます。最後にsiteurlで sort し、1 記事を 1 行の JSON として出します。実行時刻は出力へ入れません。ページが変わらなければ同じ byte 列になるためです。

この境界を置くと、障害の見方も単純になります。

[公開ブログ3件]
       │ HTTP GET

[Python: 取得・重複排除・sort]
       │ 安定したJSONL
       ├─ 失敗 ───────→ scriptの非0終了として記録


[Hermes cron]
       │ stdoutをpromptへ注入

[LLM: 新着判定・要約・日本語化]


[設定済みdelivery先]

LLM が判断を誤っても、元の JSONL を再確認できます。反対に、取得先の HTML が変わって 0 件になれば、script 側で例外にできます。曖昧な空リストを agent へ渡さない設計です。

用語を揃える

用語この手順での意味
pre-run scriptagentを起動する前にHermes cronが実行するPythonまたはBashです
JSONL1行に1つのJSON objectを置く形式です。差分と行数を追いやすくします
stdoutscriptが標準出力へ書いたデータです。通常モードではagentのpromptへ入ります
stable output入力が同じならbyte列も同じになる出力です。時刻やランダム値を混ぜません
exit code0は成功、非0は失敗を表します
no-agentscriptのstdoutをそのまま配信し、LLMを一度も呼ばないモードです
monitor-scriptscript出力のhashが変わったときだけagentを起こすモードです
deliverycronの最終結果をlocal、Slack、Telegramなどへ渡す処理です

収集スクリプトを単独で検査する

Hermes へ登録する前に、Python だけで完走するかを確認します。2026 年 8 月 31 日に Hermes Agent v0.20.6 のホストで実行しました。

python3 /home/daisuke/.hermes/scripts/blog_subscription_monitor.py \
  > /tmp/hibanas-blog-index.jsonl
printf 'lines=%s\n' "$(wc -l < /tmp/hibanas-blog-index.jsonl)"

実測は 29 行です。

lines=29

次に、同じ処理を 2 回動かします。

python3 /home/daisuke/.hermes/scripts/blog_subscription_monitor.py \
  > /tmp/hibanas-blog-index-1.jsonl
python3 /home/daisuke/.hermes/scripts/blog_subscription_monitor.py \
  > /tmp/hibanas-blog-index-2.jsonl
sha256sum /tmp/hibanas-blog-index-1.jsonl \
  /tmp/hibanas-blog-index-2.jsonl
cmp --silent /tmp/hibanas-blog-index-1.jsonl \
  /tmp/hibanas-blog-index-2.jsonl
printf 'same_bytes=%s\n' "$?"

2 ファイルは同じ hash になり、cmpも一致を返しました。

63ade239b9c62308965b7f4389ef9690eeca855ad29f71c6e18762683daa49cf  /tmp/hibanas-blog-index-1.jsonl
63ade239b9c62308965b7f4389ef9690eeca855ad29f71c6e18762683daa49cf  /tmp/hibanas-blog-index-2.jsonl
same_bytes=0

same_bytes=0は「違いがない」という意味です。cmpは一致したときに終了コード0を返します。

hash を固定値として監視するわけではありません。公開ブログへ新しい記事が加われば hash は変わります。必要なのは、同じページ状態を読んだ 2 回の出力が同じになることです。

収集スクリプトの中身

この script は Python 標準ライブラリだけで動きます。追加のpip installは不要です。処理の要点は次の通りです。

for source in SOURCES:
    parser = ListingParser(source)
    parser.feed(fetch(source["url"]))
    seen = set()
    articles = []
    for item in parser.items:
        if item["url"] in seen:
            continue
        seen.add(item["url"])
        articles.append(item)

for article in sorted(result, key=lambda item: (item["site"], item["url"])):
    print(json.dumps(article, ensure_ascii=False, sort_keys=True, separators=(",", ":")))

重複排除には URL を使います。出力順はページ内の見え方ではなく、siteurlで決めます。JSON の key もsort_keys=Trueで揃えます。この 3 点が、同じ入力から同じ byte 列を作るために効きます。

HTTP 取得は 1 回で諦めません。最大 3 回試し、待ち時間を 1 秒、2 秒と伸ばします。それでも取得できない場合はRuntimeErrorを投げます。また、HTML 構造が変わって記事リンクを 1 件も取れない場合も失敗にします。

for attempt in range(3):
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            return response.read().decode("utf-8", errors="replace")
    except (urllib.error.URLError, TimeoutError) as exc:
        if attempt < 2:
            time.sleep(2**attempt)

if not articles:
    raise RuntimeError(f"No article links found for {source['name']}")

「0 件」を正常として通すと、LLM は「新着なし」と判断します。しかし、selector が壊れた 0 件と、本当に記事がない 0 件は別です。定期購読するブログには通常記事があるため、0 件を失敗に倒しています。

3つの実行モードを使い分ける

Hermes Agent の cron には、script を使う経路が 3 つあります。

モードagentの起動stdoutの扱い向いている仕事
--script毎回起動promptへ注入新着の要約、分類、文章化
--monitor-scripthash変更時だけ起動変更差分をpromptへ注入更新頻度が低いページの監視
--no-agent --script起動しないそのまま配信disk警告、Heartbeat、決まった通知

今回の既存ジョブは通常のscriptモードです。毎朝、収集した 29 件を agent へ渡し、どれを読むか判断させます。登録時の形は次の通りです。

hermes cron create "0 7 * * *" \
  "収集結果からAIコーディング関連の新着を選び、日本語で調査する" \
  --script blog_subscription_monitor.py \
  --name "Daily AI blog Japanese archive + deep research" \
  --deliver local

このコマンドはジョブを新規作成します。既存ジョブがある環境では重複実行になるため、先にhermes cron listで同名ジョブを確認します。記事執筆時の既存ジョブはenabled: truelast_status: okでした。

同じ収集結果なら LLM を起こす必要がない運用では、--monitor-scriptへ変えます。

hermes cron edit 237978c85348 \
  --script "" \
  --monitor-script blog_subscription_monitor.py

これは実行例であり、記事作成時には既存ジョブを変更していません。monitor-scriptは出力の exact-byte hash を保存し、前回と同じなら agent run を抑止します。時刻を 1 行入れるだけでも毎回 hash が変わるため、stable output が前提です。

判断が不要で、stdout 自体が通知文ならno-agentを使います。

hermes cron create "every 5m" \
  --no-agent \
  --script blog_subscription_monitor.py \
  --deliver local \
  --name "blog-index-jsonl"

ただし、この script は正常時にも 29 行を出します。そのため、5 分ごとの watchdog には向きません。no-agentで静かな監視を作るなら、正常時は何も出さず、異常時だけ 1 行を出す script に変えます。空の stdout は配信されず、非 0 終了や timeout はエラーとして通知されます。

登録前後の確認

CLI が現在の機能を持つかは、help を一次確認に使えます。

hermes --version
hermes cron create --help

実機の version は次の通りでした。

Hermes Agent v0.20.6 (2026.8.27) · upstream 71c823bb

create --helpには--script--no-agent--monitor-scriptの 3 つが表示されました。古い version で flag が見つからない場合は、公式ドキュメントと release を確認してから更新します。

登録後は、一覧だけで終わらせません。手動実行は次の scheduler tick へ予約する非同期操作です。run 直後に成功扱いせず、履歴まで確認します。

hermes cron run 237978c85348
hermes cron runs 237978c85348 --limit 5

通常モードでは、script が非 0 終了なら agent を起動しません。成功時は stdout を prompt へ加えて agent run へ進みます。delivery の失敗は処理本体の失敗と別に記録されるため、実行状態と配信状態を分けて読みます。

よくあるエラー

症状原因確認と修正
script pathが拒否される$HERMES_HOME/scripts/の外にあるscriptを/home/daisuke/.hermes/scripts/内へ置き、--scriptへbasenameを渡します
毎回monitor-scriptがagentを起こすタイムスタンプやランダム順がstdoutへ混ざる時刻を除き、URLなど不変keyでsortします
新着があるのにhashが変わらない取得件数の上限やselectorが不適切単独実行のJSONLと対象ページを比較します
0件なのに正常終了するparserの空配列を許している通常0件にならないsourceは非0終了にします
scriptは成功するが通知がないno-agentでstdoutが空、またはdelivery設定が違う単独実行でstdoutを確認し、hermes cron runsで配信状態を読みます
手動run後すぐ履歴に出ないcron runは次のtickへ予約するgatewayを動かし、最大60秒待ってからrunsを確認します
timeoutになるHTTP待ちや再試行が長い各requestへtimeoutを付け、script全体の上限も確認します

私ならこの境界で運用する

収集 script には、URL の取得、重複排除、並び順、schema 検査までを持たせます。「どれが重要か」「どう要約するか」は入れません。そこから先が agent の仕事です。

反対に、disk 使用率が 90%を超えたら警告するだけなら LLM を使いません。no-agentで十分です。ページが変わったときだけ調査したいならmonitor-scriptを使います。

毎回 AI を起こすことが自律化ではありません。固定できる処理を先に固定すると、AI へ渡す判断が小さくなります。失敗時に調べる場所も、費用も減ります。

一次情報

出典確認した内容
Hermes Agent Scheduled Tasksscript、monitor-script、no-agent、scheduler、実行履歴の仕様
Hermes Agent Script-Only Cron Jobsno-agentのstdout、終了コード、script配置規則
Hermes Agent Cron Troubleshootinggateway tick、timeout、delivery、lockの確認手順
Python urllib.requesturlopenとtimeoutを使ったHTTP取得
Python jsonsort_keysとJSON encodingの仕様
GNU Coreutils sha2 utilitiessha256sumによる出力byte列の比較

※GSC では直近 28 日間に「hermes agent cron」が 1 impression、平均掲載順位 10 でした。数値は需要の補助として使い、題材は Hermes Agent の無人運用を再現可能にする関心から選んでいます。

この記事をシェア