News

AIエージェントの定期業務を止めない、
自己延命ジョブと多層フェイルセーフの設計

1日6回の状況ブリーフィング、毎日深夜の社内資産の同期、週1回のサイト連携。弊社ではこれらの定期業務を、コーディングエージェントのスケジュール実行に任せて無人で運転しています。構築してみてわかったのは、難しいのは業務の中身ではなく「実行が止まらないこと」だという事実です。本記事では、定期ジョブが止まる・壊れる典型パターンと、それを防いでいる設計を実装の細部まで解説します。

周回のたびに次のジョブへ引き継がれ、外側の層が異常から復旧させる

定期実行は放っておくと止まる

エージェントへの定期業務の移管は「毎朝これをやって」と登録すれば終わり、ではありません。運用を始めると、止まる理由が三つあることに気づきます。第一に、実行環境が起きていない。ジョブはエージェントのセッション内で発火するため、セッションが閉じていたりマシンがスリープしていれば、その回はスキップされます。第二に、ジョブそのものの失効。弊社が使うスケジューラでは、登録したジョブは7日で自動失効します。第三に、そして最も厄介なことに、止まったことに誰も気づかない。定期実行の失敗は「何かが起きる」のではなく「何も起きない」という形で現れるからです。

さらに、止まらなくても壊れる理由が二つあります。同じジョブが二重に登録されて重複実行される。そして、通知の宛先を誤る。以降で紹介する設計は、この五つの問題への対処をそれぞれ仕組みに落としたものです。

手順書が実行プロンプトを兼ねる

前提となる構造から説明します。スケジューラに登録するプロンプトは「この手順書を読み、記載の手順どおり実行せよ」という趣旨の一文だけです。収集する情報源、起草のフォーマット、投稿先、失敗時の扱いといった業務の中身は、すべてリポジトリ内の手順書(Markdown)に書かれています。

この分離には二つの利点があります。ひとつは、業務内容の変更が文書の編集で完結し、ジョブの再登録が不要になること。もうひとつは、手順書が末尾に「標準プロンプト」という節を持ち、自分自身を再登録するためのプロンプト文字列とスケジュール設定を記述していることです。ジョブが消えても、手順書さえ読めば誰でも(人でもエージェントでも)同じ設定で復元できる。後述する自動復旧は、この自己記述性の上に成り立っています。

自己延命: 実行のたびに自分を登録し直す

7日で失効するジョブを生かし続ける方法として、弊社は「毎回の実行の最後に、ジョブが自分自身を登録し直す」設計を採っています。同一設定の新しいジョブを登録してから古いジョブを削除し、常に1件だけが残る状態を保つ。実行のたびに失効期限が7日先へ更新されるため、ジョブが動き続ける限り失効は起きません。

# 毎回の実行の末尾に行うリフレッシュ
1. ジョブ一覧から対象ジョブを確認する
2. 同一設定(cron式・プロンプト)の新ジョブを登録する
3. 古い方のジョブを削除する(常に1件だけ残す)

# 週次ジョブは実行周期(7日)と失効期限(7日)が同じで
# 自力延命が間に合わないため、毎日実行される夜間ジョブが
# 週次ジョブのリフレッシュも一緒に引き受ける

注意が要るのは実行周期と失効期限が同じ週次ジョブです。周期7日のジョブが自分の実行で延命しようとすると、わずかな発火遅れが失効に直結します。そこで弊社では、毎日実行される夜間ジョブが週次ジョブの延命も引き受ける構成にしました。短い周期のジョブが長い周期のジョブを道連れで生かす。延命の経路を実行周期に依存させないための二重化です。

消えたら検知して、次の起動時に自動復旧する

自己延命があっても、ジョブは消えるときは消えます。セッションが長期間起動されなければ延命の機会自体がなく、誤操作で削除されることもある。そこで最後の網として、セッション開始時のフックによる検知を置いています。

仕組みは単純で、セッションが開始されるたびに、登録済みジョブの一覧ファイルを走査するシェルスクリプトが動きます。あるべきジョブが見つからなければ、「該当の手順書を読み、記載の標準プロンプトで再登録せよ」という文脈をエージェントに注入する。人は何も操作しません。次にセッションを開いた瞬間、エージェントが自分でジョブを登録し直します。フック側は grep 一発の高速な検査だけを担い、復旧に必要な知識は手順書側に置く。検知と対処を分離しているため、業務が増えてもフックには検査対象が1行増えるだけです。

実処理は使い捨てのサブエージェントに委譲する

ジョブが発火したとき、メインのエージェントが担当するのは、実処理用サブエージェントの起動、完了待ち、前述のジョブ延命、そしてサブエージェントの終了指示だけです。情報収集や起草といった実処理は、専用に定義したサブエージェント1体に委譲します。モデルは定義ファイルで固定してあり、メインセッションがどのモデルで動いていても、定期業務は常に同じモデルで実行されます。

サブエージェントは完了報告とともに毎回終了させ、常駐も再利用もしません。前回実行の文脈を引きずらず、毎回まっさらな状態から手順書を読み直すためです。もうひとつ重要なのが分業境界の明文化で、エージェント定義には「ジョブの延命はメインループの担当であり、サブエージェントは行わない」と書いてあります。境界を曖昧にすると、双方が善意で延命を行って二重登録が生まれる。人間のチームと同じで、事故は担当の重なりから起きます。

誤送信は言い聞かせではなく構造で防ぐ

無人で通知を投稿する以上、宛先ミスは設計上いちばん許されない事故です。プロンプトに「このチャンネル以外へ送らない」と書くだけでは、確率を下げることはできても保証にはなりません。弊社では、送信ツールの実行直前に割り込むフックで、宛先を機械検査しています。

# 送信ツールの実行直前に割り込む検査(要旨)
ALLOWED = {"C0XXXXXXXXX",  # 通知専用のプライベートチャンネル
           "C0YYYYYYYYY"}  # タスク完了報告用チャンネル

if channel_id in ALLOWED:
    decision = "allow"   # 無確認で送信してよい
else:
    decision = "ask"     # 人手確認に落とす(自動送信させない)

許可リストにあるチャンネルへは無確認で送信でき、それ以外はすべて人手確認に落ちる。エージェントがどんな指示を受けていても、リスト外への自動送信は実行経路の上で不可能です。判断をモデルの注意力に置くのではなく、実行経路上の機械検査に移す。ガードレールの置き場所として、この違いは決定的だと考えています。

運用の細部

最後に、動かし続ける中で決まっていった細かな作法を挙げます。発火時刻は 0:07 のように「ちょうど」を避けてずらす。新着が何もない回も「特になし」と投稿し、定時投稿そのものを生存確認にする。一方で夜間同期のように毎日変化があるとは限らないジョブは、変更ゼロの日は投稿せずログへの1行追記だけを生存記録にする。一部の情報源の取得に失敗しても全体を止めず、注記して続行する。部分故障を全体停止にしないことは、無人運転では特に効きます。

また、実行は定額サブスクリプションの対話セッション内で発火する構成に限定し、APIキーを使った常駐プロセスは採用していません。従量課金の経路を持ち込まないことで、無人実行が費用の事故につながらないようにしています。同じ理由で、セッションを同時に複数開くと同じジョブが重複発火し得るため、常用セッションは1つに保っています。

まとめ

定期業務のAI化で本当に設計すべきだったのは、業務手順そのものよりも「実行が生き続けるための層」でした。自己延命が失効を防ぎ、起動時フックの検知が消失から復旧させ、許可リストの機械検査が誤送信を封じる。どれも、人間の運用チームが冗長化や監視でやってきた異常系設計と同じ型です。エージェント運用の信頼性はモデルの賢さからではなく、こうした運用設計の積み重ねから生まれると考えています。

ニュース一覧へ戻る