Fragments of verbose memory

冗長な記憶の断片 - Web技術のメモをほぼ毎日更新

Mar 15, 2026 - 日記

agent-execにイベント通知が来た: ポーリング不要のジョブ完了ワークフロー

agent-exec Job Finished Events cover image

以前の記事「CLIの出力設計を間違えると、AIエージェントは何も読めない 」で紹介した agent-exec に、ずっと欲しかった機能を入れました。Rust 製ジョブランナーに追加したジョブ完了時のイベント通知です。

これまではジョブの完了を知るには agent-exec waitagent-exec status をポーリングするしかなかったんですが、実際にオーケストレーションパイプラインを組むとポーリングが辛い。間隔が短すぎるとリソースの無駄、長すぎると反応が遅れる。結局「完了したら教えてくれ」が一番素直だよなと思って、Job Finished Events を実装しました。

Job Finished Events とは

ジョブが終了状態(正常終了・異常終了・シグナルによる停止)になったとき、外部に完了イベントを通知する仕組みです。通知先は2つ選べます。

  • File Sink: NDJSONファイルにイベントを追記
  • Command Sink: 任意のコマンドを実行し、stdinにイベントJSONを渡す

両方同時に指定することもできます。

File Sink: NDJSONファイルへの追記

--notify-file を指定すると、ジョブ完了時にイベントJSONを1行としてファイルに追記します。

ファイルが存在しなければ新規作成、存在すれば末尾に追記されます。NDJSON(1行1JSON)形式なので、複数ジョブのイベントを1ファイルにまとめて tail -f で監視したり、後からバッチ処理したりできます。

1
agent-exec run --wait --notify-file /tmp/events.ndjson -- echo hello

実行後、/tmp/events.ndjson には以下のようなJSONが1行追記されます。

1
{"schema_version":"0.1","event_type":"job.finished","job_id":"abc123","state":"exited","command":["echo","hello"],"cwd":"/home/user","started_at":"2026-03-19T00:00:00Z","finished_at":"2026-03-19T00:00:01Z","duration_ms":1000,"exit_code":0,"stdout_log_path":"/tmp/agent-exec/abc123/stdout.log","stderr_log_path":"/tmp/agent-exec/abc123/stderr.log"}

Command Sink: コマンド実行による通知

--notify-command を使うと、ジョブ完了時に任意のコマンドを実行できます。イベントJSONはそのコマンドのstdinに渡されます。

v0.1.3からコマンドはシェル文字列で指定します。設定されたシェルラッパー経由で実行されるため、パイプやリダイレクトもそのまま書けます。

1
2
3
agent-exec run --wait \
  --notify-command 'cat > /tmp/event.json' \
  -- echo hello

この例ではイベントJSONを /tmp/event.json に保存していますが、実際にはSlack通知スクリプトやWebhookの呼び出しなど、好きなコマンドを指定できます。

Command Sinkで設定される環境変数

コマンド実行時、以下の環境変数が自動的に設定されます。

環境変数内容
AGENT_EXEC_EVENT_PATH永続化された completion_event.json のパス
AGENT_EXEC_JOB_ID完了したジョブのID
AGENT_EXEC_EVENT_TYPEイベントタイプ(現在は job.finished 固定)

AGENT_EXEC_EVENT_PATH が地味に便利で、通知コマンドの中でイベントJSONを再読み込みしたいときにstdinを消費せずアクセスできます。

シェルラッパーの設定

v0.1.3では、--notify-command や通常のコマンド実行に使われるシェルラッパーを設定できるようになりました。優先順位は以下の通りです。

  1. --shell-wrapper "bash -lc" CLIフラグ(最優先)
  2. --config /path/to/config.toml で指定した設定ファイル
  3. デフォルトXDGパス: ~/.config/agent-exec/config.toml
  4. ビルトインのプラットフォームデフォルト

設定ファイルはTOML形式で書けます。

1
2
3
[shell]
unix    = ["sh", "-lc"]
windows = ["cmd", "/C"]

-lc を指定するとログインシェルとして実行されるので、~/.bash_profile で設定したPATHやエイリアスが使えます。CI環境とローカル環境でシェルの挙動を揃えたいときに便利です。

イベントJSONのスキーマ

job.finished イベントのペイロードは以下のフィールドを含みます。

フィールド説明
schema_versionstringスキーマバージョン(現在 "0.1"
event_typestring"job.finished"
job_idstringジョブID
statestring"exited""killed"、または "failed"
commandstring[]実行コマンド
cwdstring作業ディレクトリ
started_atstring開始時刻(ISO 8601)
finished_atstring終了時刻(ISO 8601)
duration_msnumber実行時間(ミリ秒)
exit_codenumber | absent終了コード(kill時はフィールド省略の場合あり)
signalstring | null停止シグナル(正常終了時はnull)
stdout_log_pathstringstdoutログのパス
stderr_log_pathstringstderrログのパス

state"killed" の場合、exit_code フィールドは省略されることがあり、代わりに signal(例: "SIGTERM")が入ります。"failed" はジョブの起動自体に失敗した場合です。

また、ジョブディレクトリには completion_event.json が自動保存されます。通知の送信結果(各Sinkの成否)も含まれるので、デバッグ時にも役立ちます。

実践例: イベント駆動のパイプライン

「テスト実行 → 結果を通知」という簡単なパイプラインを組んでみます。--notify-command にシェルコマンドを直接書けるので、スクリプトファイルを別途用意する必要はありません。

1
2
3
4
agent-exec run --wait \
  --notify-command 'EXIT_CODE=$(jq -r ".exit_code // \"N/A\"" "$AGENT_EXEC_EVENT_PATH"); JOB_ID=$(jq -r .job_id "$AGENT_EXEC_EVENT_PATH"); DURATION=$(jq -r .duration_ms "$AGENT_EXEC_EVENT_PATH"); if [ "$EXIT_CODE" = "0" ]; then echo "✅ Job $JOB_ID completed in ${DURATION}ms"; else echo "❌ Job $JOB_ID failed (exit: $EXIT_CODE) after ${DURATION}ms"; fi' \
  --notify-file /var/log/agent-exec-events.ndjson \
  -- pytest tests/ -x

--notify-file--notify-command を同時に指定しているので、NDJSONファイルに監査ログとして残しつつ、リアルタイムの通知も飛ばせます。

もう少し読みやすくしたい場合は、AGENT_EXEC_EVENT_PATH を使ったシンプルなパターンもあります。

1
2
3
4
agent-exec run --wait \
  --notify-command 'jq -r "if .exit_code == 0 then \"✅ \" + .job_id + \" completed in \" + (.duration_ms|tostring) + \"ms\" else \"❌ \" + .job_id + \" failed (exit: \" + (.exit_code // \"N/A\" | tostring) + \")\" end" "$AGENT_EXEC_EVENT_PATH"' \
  --notify-file /var/log/agent-exec-events.ndjson \
  -- pytest tests/ -x

OpenClawとの連携

agent-execのイベント通知は、OpenClaw のようなエージェントプラットフォームとの相性が特に良いです。ジョブが終わったら結果をセッションに返す、チャットに通知する、といったパターンが --notify-command ひとつで組めます。

チャットへの通知

openclaw message send を使えば、ジョブ完了をTelegramなどのチャットに直接通知できます。シェル文字列形式なので、パイプでつないでワンライナーで書けます。

1
2
3
agent-exec run \
  --notify-command 'jq -r "\"job \" + .job_id + \" state=\" + .state + \" exit=\" + (.exit_code // \"n/a\" | tostring)" | xargs -I{} openclaw message send --chat telegram:deployments --text "{}"' \
  -- long-running-command --flag value

AGENT_EXEC_EVENT_PATH を使えば、stdinを消費せずにイベントJSONを参照できるので、より複雑な通知にも対応できます。

1
2
3
agent-exec run \
  --notify-command 'openclaw message send --chat telegram:deployments --text "$(jq -c . "$AGENT_EXEC_EVENT_PATH")"' \
  -- long-running-command --flag value

起動元セッションへの結果返送

もう一つ便利なパターンが、ジョブを起動したOpenClawセッションに結果を返すことです。起動元のセッションがログを確認して、リトライするか次のタスクに進むかを判断できます。

1
2
3
4
5
export SESSION_ID="oc_session_123"

agent-exec run \
  --notify-command 'openclaw message send --session $SESSION_ID --text "$(jq -c . $AGENT_EXEC_EVENT_PATH)"' \
  -- ./run-heavy-task.sh

セッション側ではイベントペイロードを読んで stdout_log_pathstderr_log_path を確認し、次のアクションを決められます。重い処理をagent-execに任せて、結果だけをエージェントセッションで受け取る——このパターンは、エージェントオーケストレーションの基本形になると思っています。

ポーリングとの使い分け

イベント通知が入ったからといって waitstatus が不要になるわけではありません。使い分けの目安はこんな感じです。

ユースケース推奨
バッチパイプラインで次のジョブを起動--notify-command
複数ジョブのイベントを集約・分析--notify-file
インタラクティブに結果を待つagent-exec wait
ジョブ状態の随時確認agent-exec status

パイプラインやオーケストレーションでは --notify-command が自然で、人間が手元で確認するなら wait が手軽です。

まとめ

agent-exec の Job Finished Events は「ポーリングするくらいなら教えてくれ」をそのまま実装した機能です。File SinkとCommand Sinkの2つの通知先を選べて、イベントJSONにはジョブの実行時間やログパスも含まれるので、通知先で必要な情報は大体揃います。

自分のユースケースでは、エージェントのオーケストレーションで「ジョブAが終わったらジョブBを起動」というチェーンを組むのに使っています。ポーリングの間隔調整から解放されるだけで、パイプラインの設計がだいぶ楽になりました。

参考リンク