トリガーリハーサル
これは何か
イベントで自分自身を起動するエージェントは、そのトリガールールと同じくらい安全です。エージェント自体は注意深くても、1日分の予算を簡単に使い果たす可能性があります。問題は通常、一段上のレベルで発生します。
- あるルールが自身の作業が生成するイベントに反応して、何度も何度も発火する。
- 2つのルールが別のシステムを介して互いに供給し合う(エージェントがコメントし、コメントが同期され、同期がウェブフックを発火する)。
- ノイズの多いソースが2分間に10回の編集を送信し、そのたびに新しい実行が開始される。
- 「たまにチェックする」つもりのcron行が1日に288回発火する。
- 2つのルールが同じイベントを処理し、作業が2回行われる。
- 返金、削除、またはメッセージ送信を行うルールが、誰にも確認せずに実行される。
これらはいずれも、1つの例でエージェントをテストしても現れません。実際のトラフィックが到着したときに現れます。Trigger Rehearsal は実際のトラフィックを事前に、机上で発生させます。ルールを1つの中立的な形式で書き留め、1日または1週間分のイベントをそれらに適用して再生し、すべてが作動する前にタイムラインを読み取ります。
使用するタイミング
- 人がプロンプトを入力しなくてもエージェントが作業を開始できるようにするルールを有効にする前。
- 既存のルールセットに新しいルールを追加するとき(新しいルールが、以前は開いていたループを閉じる可能性がある)。
- バックグラウンドエージェントが予想よりもはるかに頻繁に、またははるかに少なく実行されたとき。
- デバウンスウィンドウ、並列実行制限、1日あたりの上限を選択するとき、推測ではなく数値が必要な場合。
- レビュアーが「このリポジトリに1時間で50件のIssueが来たらどうなる?」と尋ねたとき。
対象外: エージェントが実行内で何をするかをチェックすること(それはプロンプトまたはスキルの問題です)、またはイベントソース自体の負荷テスト。
ルールシート
すべてが rules.json に変換されます。変換は正直に行ってください。実際のルールごとに1つのシートルールを作成し、実際に存在するガードを書き留めてください。追加する予定のガードではありません。
{
"tz": "Asia/Shanghai",
"run_s": 180,
"external_sources": ["github"],
"budget": {"runs_per_day": 40, "cost_per_run": 0.15, "cost_per_day": 6},
"rules": [
{
"name": "triage-new-issue",
"on": {"source": "github", "types": ["issues.opened", "issues.edited"]},
"where": {"repo": "me/app"},
"key": "subject",
"debounce_s": 300,
"parallel": 1,
"max_per_day": 20,
"unless_actor": ["my-bot"],
"unless_label": "triaged",
"emits": [{"source": "github", "types": ["issues.labeled"], "actor": "my-bot", "labels": ["triaged"]}],
"sensitive": [],
"asks": false
},
{"name": "morning-digest", "on": {"cron": "0 9 * * 1-5"}, "max_per_day": 1}
]
}
| フィールド | 意味 |
|---|---|
on.source + on.types | どのイベントがルールを開始するか。types を省略すると、そのソースのすべてのタイプに一致します。 |
on.cron | シートの tz における5フィールドのcron行。ルールは source または cron のいずれかを持ちます。 |
where | イベントフィールドが特定の値(またはリストのいずれか)と等しくなければならない条件。 |
key | 件名(Issue番号、チケットID、URL)を指定するイベントフィールド。重複したイベントのマージに使用されます。 |
debounce_s | 1つの件名に対して最後のイベントからこの時間(秒)だけ待機してから、1回実行します。 |
parallel | このルールの実行が同時にいくつまでアクティブになれるか。超過した実行は待機します。 |
max_per_day | ローカル1日あたりの上限。超過した実行はドロップされます。 |
run_s | 一般的な実行時間。キューイングに使用されます。シートの値(なければ120)にフォールバックします。 |
unless_actor, unless_label | ガード:これらのアクターによって作成された、またはこのラベルを持つイベントをスキップします。 |
emits | 実行自体が生成するイベント(コメント、ラベル、ステータス変更、後続イベント)。運ぶアクターとラベルを含めてください。 |
sensitive + asks | 人間の確認が必要なアクション(削除、支出、再起動、送信)。asks: true は、実行が停止して最初に確認することを意味します。 |
external_sources | 外部世界によって供給されるソース。これ以外はルールによって発行されなければなりません。 |
budget | オプションの1日あたりの実行数、実行あたりのコスト、1日あたりのコスト。評決に使用されます。 |
ステップバイステップ
- 実際のルールを収集する。 説明ではなく実際の設定を読んでください。エージェントハブのルールファイル、保存されたルーチン、CIワークフローの
on:ブロック、crontab行、ノーコード自動化画面。別のルールが監視しているソースに書き戻すものがあれば必ずメモしてください。 rules.jsonに変換する。python3 scripts/rehearse.py new rehearsal/から始めてください。すべてのルールについて、次の3つの質問をしてフィールドを埋めてください:何が起動するか、1回の実行で何を書き戻すか、そして自分自身の書き込みで起動するのを何が防ぐか。3番目の答えが「何もない」場合は、ガードを省略してください。リハーサルはそれを捕捉するためのものです。- Lint チェック。
python3 scripts/rehearse.py lint rehearsal/rules.jsonを実行してください。再生前にすべてのHOLDを修正してください。REVIEW項目は数値を待つことができます。 - イベントを収集する。 ベスト:実際の忙しい1日をソース(ウェブフック配信ログ、Issueタイムライン、ヘルプデスク監査ログ)からエクスポートし、
events.jsonlに、1行につき少なくともt、source、type、およびkeyとwhereで使用するフィールドを含むJSONオブジェクトとして保存します。履歴がない場合は、意図的に短い合成バーストを作成してください:3分間に10回の編集、同時に2つの件名、自分のボットが作成したイベント。 - 丸一日を再生する。
python3 scripts/rehearse.py replay rehearsal/rules.json rehearsal/events.jsonl --from 2026-11-02T00:00:00Z --to 2026-11-03T00:00:00Z --md rehearsal/report.mdを実行してください。--from/--toを指定することで、1日あたりの数値が短いウィンドウによって水増しされるのを防ぎます。 - 評決だけでなくタイムラインを読む。 同じ件名の実行が数分おきに発生していないか(デバウンスの欠落または短すぎる)、深さが増加するチェーンがないか(ループ)、長い待機時間がないか(並列実行が低すぎる)、ドロップがないか(実際の作業に対して上限が低すぎる、またはルールがノイズに発火している兆候)を確認してください。
- 調整して繰り返す。 一度に1つのノブだけを変更し、同じイベントを再生してください。そうすることで、変更による違いが明確になります。
- 決定する。 現実的な1日での最後の再生が ARM である場合にのみ ARM(作動)にしてください。最終的な
rules.json、イベントファイル、report.mdを、ルールがなぜ有効にされたかの記録として一緒に保管してください。
評決
- ARM: HOLD または REVIEW の指摘事項なし。記述されたまま有効にしても安全。
- TRIM: 実行を無駄にしている、または作業をスキップできる(キーなし、キャップなし、重複、キャップが実際のイベントをドロップ、予算に近い)。トリミング後、または書面による理由がある場合にのみ有効にしてください。
- HOLD: 有効にしないでください。自己ループ、未保護のサイクル、深さ5を超えて実行されたチェーン、確認なしのセンシティブなアクション、予算超過、または壊れたcron行。
終了コードは評決に従います(0、1、2。3は不正な入力)。これにより、リハーサルをCIでの設定変更のゲートにすることができます。
例
サポートチームがエージェントにヘルプデスクチケットを回答させたいと考えています。彼らのシート(examples/loopy/ 内)には、返信を投稿するanswerルールがあり、それがチケットを更新し、それが同じルールに一致します。通常の1日を再生すると、次のようになります。
HOLD self-loop answer-ticket: rule's own output matches its own trigger with no unless_actor/unless_label guard
HOLD runaway answer-ticket: chain kept re-firing past depth 5 (3 cuts)
HOLD sensitive-unasked refund-check: declares sensitive actions ['refund'] but 'asks' is not true
HOLD over-runs-budget -: 100.0/day against a budget of 30/day
REVIEW cron-tight inbox-poll: up to 720 fires/day
VERDICT HOLD
3件のチケット更新が18回のanswer実行になりました。修正は小さいものです。answerルールに "unless_actor": ["support-bot"] を追加し、refundルールに "asks": true を設定し、2分ごとのポーリングを、実際に受信箱が変更されたときだけ inbox.changed を発行するスクリプトに置き換えてください(examples/loopy/rules.fixed.json を参照)。同じ日の次の再生では、実行回数が100回から4回に減少し、評決は TRIM に変わります。残っている1つの指摘は、answerとrefund-checkの両方がrefundチケットを認識することですが、これはここでは意図されているため、レポートの横に1行の書面による理由を添えて受け入れられます。
ヒント
- 最良のループガードは、実行後に偽になる条件です。つまり、実行が追加するラベルとルールが除外するラベル、またはルールが一致しなくなるステータスです。
- システム間ループは見逃しやすいものです。エージェントがシステムAに書き込み、システムAがBに同期する場合、同期されたイベントを、それが表示するアクターとともに
emitsの下にリストしてください。 - デバウンスは設計上、遅延を追加します。人が一連の編集を完了するのにかかる時間からウィンドウを選択し、誰かが回答を待つ場所では短く保ってください。
- デバウンスはアクションの重複除去ではありません。不可逆的な操作については、実行自体が行動する前に「既に実行済みか?」をチェックする必要があります。
- 1日あたりの上限はヒューズであり、計画ではありません。通常の日の再生で上限に達する場合、ルールは広すぎます。
- 数分ごとのポーリングは、通常、状態を比較して変更があった場合にのみ1つのイベントを発行する小さなスクリプトとして行う方が安価です。エージェントは新しい何かがあるときにのみ起動します。
- ルールが追加されるたびにリハーサルを再実行してください。ほとんどのループは最初のルールではなく、2番目のルールによって作成されます。