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
各記事をsite、listing_url、title、urlの 4 項目へ揃えます。最後にsiteとurlで 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 script | agentを起動する前にHermes cronが実行するPythonまたはBashです |
| JSONL | 1行に1つのJSON objectを置く形式です。差分と行数を追いやすくします |
| stdout | scriptが標準出力へ書いたデータです。通常モードではagentのpromptへ入ります |
| stable output | 入力が同じならbyte列も同じになる出力です。時刻やランダム値を混ぜません |
| exit code | 0は成功、非0は失敗を表します |
| no-agent | scriptのstdoutをそのまま配信し、LLMを一度も呼ばないモードです |
| monitor-script | script出力のhashが変わったときだけagentを起こすモードです |
| delivery | cronの最終結果を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 を使います。出力順はページ内の見え方ではなく、siteとurlで決めます。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-script | hash変更時だけ起動 | 変更差分を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: true、last_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 Tasks | script、monitor-script、no-agent、scheduler、実行履歴の仕様 |
| Hermes Agent Script-Only Cron Jobs | no-agentのstdout、終了コード、script配置規則 |
| Hermes Agent Cron Troubleshooting | gateway tick、timeout、delivery、lockの確認手順 |
| Python urllib.request | urlopenとtimeoutを使ったHTTP取得 |
| Python json | sort_keysとJSON encodingの仕様 |
| GNU Coreutils sha2 utilities | sha256sumによる出力byte列の比較 |
※GSC では直近 28 日間に「hermes agent cron」が 1 impression、平均掲載順位 10 でした。数値は需要の補助として使い、題材は Hermes Agent の無人運用を再現可能にする関心から選んでいます。
