🎛️

AIエージェントを業務に入れると、最初は「何を任せるか」に意識が向きます。けれど実務で詰まるのは、任せたあとの制御です。長い調査を止めたい。承認待ちを一晩置きたい。古い前提のまま再開させたくない。外部 write の直前でキャンセルしたい。こうした操作をチャットの雰囲気で処理すると、agent は今の run が続行中なのか、停止済みなのか、再開待ちなのかを失います。

この記事では、AIエージェントの run control 設計 を扱います。失敗後にやり直す設計は AIエージェント再実行設計ガイド、事故後に止めて復旧する手順は AIエージェント停止手順ガイド、途中で人間判断へ戻す話は AIエージェント人間エスカレーション設計ガイド、完了前に実状態を突合する話は AIエージェント state reconciliation 設計ガイド に分けています。

今回の論点は、通常運用中の run を、開始・一時停止・再開・キャンセル・中断・完了の状態機械として扱うことです。

先に結論: run control は「止め方」ではなく「状態管理」

AIエージェントに 止めて と伝えるだけでは不十分です。

この作業はいったん止めて、あとで続きから再開して。

人間同士なら通じます。けれど agent 運用では、この一文に少なくとも次の情報が欠けています。

  • どの run を止めるのか
  • どの tool call までは完了済みなのか
  • 外部副作用は発生したのか
  • 再開時に同じ context を使ってよいのか
  • timeout 後に自動キャンセルしてよいのか
  • 中断理由を監査ログへ残すのか

私なら、run control を次の状態遷移として扱います。

状態意味次に許すこと
queuedまだ agent に渡していないintake / priority の変更
runningagent が作業中low-risk read / draft / 許可済み write
waiting_approvaltool call 直前で承認待ちapprove / reject / expire
paused人間判断や時間都合で一時停止resume 前の再検証
cancellingキャンセル要求を受けたcooperative stop / 証跡保存
cancelled以後成功扱いしない終端状態新しい run として再作成
failedagent または tool が失敗retry packet の生成
completed成果物と検証が揃ったoutput contract / review gate

この表の価値は、状態名そのものではありません。人間、agent、MCP server、監査ログ、PR review が同じ run 状態を見られることです。

既存テーマとの役割分担

このクラスタはすでに記事が多いので、run control の境界を先に切ります。

近いテーマ扱うことrun control で扱うこと
承認フローsensitive tool call の approve / reject承認待ち状態の寿命、期限切れ、再開前確認
再実行設計失敗後に安全にやり直す失敗や中断に入る前の pause / cancel
停止手順事故後の kill switch と復旧通常運用中の cooperative stop
人間エスカレーション判断不能時に人間へ戻すescalation 後に run をどの状態へ置くか
state reconciliation完了前の実状態突合resume / cancel 前に古い状態を検出する

run control は、緊急停止の runbook ではありません。普段から pausedcancelledwaiting_approval を持っておくことで、事故時にいきなり kill switch へ飛ばなくて済むようにする設計です。

最初に run ID と owner を固定する

run control は、対象の run を一意に指せないと始まりません。

私は最小構成なら、次のフィールドだけ先に固定します。

agent_run:
  run_id: "seo-blog-20260708-001"
  task_id: "blog-ai-agent-run-control"
  owner: "content-ops"
  requested_by: "automation-3"
  lane: "content_publish"
  status: "running"
  started_at: "2026-07-08T06:00:00+09:00"
  expires_at: "2026-07-08T18:00:00+09:00"
  allowed_transitions:
    - "waiting_approval"
    - "paused"
    - "cancelling"
    - "failed"
    - "completed"

ここで大事なのは、ownerexpires_at です。owner が無い run は、誰が再開判断するのか分かりません。expires が無い run は、承認待ちや一時停止がずっと残ります。

実務では、Slack thread、GitHub issue、PR、automation inbox、MCP task などに run ID を持たせます。媒体は何でもよいですが、ID が分かれた瞬間に状態が割れます。

pause と waiting_approval を混ぜない

よくある失敗は、pause を全部同じ意味で使うことです。

承認待ちは、操作が明確です。

status: "waiting_approval"
pending_action:
  tool: "github.create_pull_request"
  target: "repo:nidoneko/HP"
  risk_tier: "external_write"
  approval_expires_at: "2026-07-08T12:00:00+09:00"

一方で、一時停止は、作業自体を止める判断です。

status: "paused"
pause_reason: "owner_unavailable"
resume_requires:
  - "owner confirms current scope"
  - "git status is rechecked"
  - "external source dates are still valid"

OpenAI Agents SDK の human-in-the-loop は、sensitive tool call の前で run を止め、RunState から承認後に再開できる設計です。これは承認待ちには強いですが、業務上の pause 全部を自動で解決するわけではありません。承認待ち、owner 不在、証拠不足、期限切れは分けます。

resume 前に state を再検証する

run を再開するときに危ないのは、止めた時点の前提をそのまま信じることです。

たとえば、記事作成 automation を朝に止め、夜に再開する場合を考えます。

  • origin/main が進んでいる
  • open PR が増えている
  • keyword-plan が更新されている
  • build command の依存状態が変わっている
  • 参照していた一次情報が更新されている

この状態で resume だけ実行すると、agent は古い duplicate check を正しいものとして扱います。

私なら、resume 前に次を必ず確認します。

resume_check:
  run_id: "seo-blog-20260708-001"
  required:
    - "git_head_matches_recorded_base"
    - "open_pr_overlap_rechecked"
    - "pending_approval_not_expired"
    - "external_write_not_already_done"
    - "source_documents_still_current_enough"
  if_failed: "move_to_needs_reconciliation"

ここで needs_reconciliation へ逃がす導線が重要です。古い state のまま進めるより、一度「状態がずれた」と返す方が安全です。

cancel は「止めたい気持ち」ではなく終端状態にする

MCP Tasks の仕様では、task cancellation は専用の操作として扱われ、キャンセル済み task は cancelled に遷移します。ここから学べるのは、キャンセルを単なる通知にしないことです。

agent run でも同じです。

悪い例はこうです。

status: "running"
message: "user asked to cancel"

この状態だと、後続処理が完了してしまったときに成功扱いされる余地が残ります。

よい例は、キャンセル要求と終端状態を分けます。

status: "cancelling"
cancel_requested_at: "2026-07-08T10:15:00+09:00"
cancel_reason: "scope changed"
stop_policy:
  allow_finish_current_read: true
  block_new_write: true
  block_external_action: true
  save_partial_artifacts: true

その後、停止できたら cancelled にします。

status: "cancelled"
cancelled_at: "2026-07-08T10:16:20+09:00"
final_state:
  local_changes: "discarded"
  external_actions: "none"
  audit_record: "agent-run-log:seo-blog-20260708-001"

一度 cancelled にした run は、あとから completed にしない方がよいです。必要なら新しい run を作り、旧 run への参照を残します。

timeout を設計する

AIエージェントの run は、無限に待たせるほど安全になるわけではありません。むしろ、古い前提のまま再開される危険が増えます。

timeout は最低でも3種類に分けます。

timeout対象期限切れ時の扱い
approval_timeout承認待ち tool callreject 扱い、または再承認要求
pause_timeout人間都合の一時停止resume 前に state reconciliation
run_timeoutrun 全体failed または cancelled へ遷移

実務でよく効くのは、承認待ちを短めにすることです。数時間後に承認者が戻っても、branch や外部状態が変わっていれば、承認時に見た情報と実行時の情報がずれます。期限切れ後は、そのまま approve ではなく再提示に戻します。

Codex thread と run を同一視しない

Codex App Server の一次情報では、thread は start / resume / fork できる作業単位として扱われます。これは便利ですが、thread と業務 run を完全に同一視すると苦しくなります。

1つの thread に、複数の業務 run が入ることがあります。逆に、1つの業務 run が調査 thread、実装 thread、レビュー thread に分かれることもあります。

私なら、対応関係を明示します。

agent_run:
  run_id: "seo-blog-20260708-001"
  codex_threads:
    - thread_id: "thread_topic_selection"
      role: "research"
    - thread_id: "thread_article_draft"
      role: "implementation"
    - thread_id: "thread_review"
      role: "verification"

thread を fork した場合も、fork 先を新しい run にするのか、同じ run の exploration にするのかを決めます。ここを曖昧にすると、どの結果が採用されたのかを decision log で追えません。

実務での最小 run control template

最初から orchestration platform を作る必要はありません。小さなチームなら、次の YAML を issue、PR description、automation memory、DB record のどこかに残すだけでも効果があります。

run_control:
  run_id: "agent-20260708-001"
  task: "MCP 記事のSEO改善PRを作る"
  owner: "content-ops"
  lane: "content_publish"
  status: "running"
  base_state:
    git_ref: "origin/main@08a8415"
    worktree: ".codex-worktrees/blog-ai-agent-run-control"
    source_checked_at: "2026-07-08T06:10:00+09:00"
  transition_log:
    - at: "2026-07-08T06:10:00+09:00"
      from: "queued"
      to: "running"
      reason: "duplicate check passed"
  controls:
    approval_timeout_minutes: 120
    pause_timeout_hours: 12
    run_timeout_hours: 24
    cancel_policy: "block_new_write_and_external_action"
  resume_requires:
    - "git status and base ref rechecked"
    - "open PR overlap rechecked"
    - "pending external action is not already executed"

ポイントは、状態遷移を履歴にすることです。今の状態だけでは、なぜそこにいるのか分かりません。transition_log があると、承認、キャンセル、再開、失敗の説明がかなり楽になります。

run control を入れる順番

私なら、導入順は次の通りです。

  1. run_idownerstatusexpires_at だけを全 run に付ける
  2. waiting_approvalpaused を分ける
  3. cancelled を終端状態として扱う
  4. resume 前に base_state を再検証する
  5. 外部 write を含む run だけ transition_log を監査ログへ接続する

最初から UI を作る必要はありません。むしろ、状態名と再開条件が決まっていない段階で UI を作ると、ボタンだけが増えます。まずは run の状態語彙を揃える方が先です。

よくある失敗

1. pause を「あとで見る」にする

paused にした run には、再開条件が必要です。条件が無い pause は、ただの放置です。

status: "paused"
resume_requires:
  - "owner approves narrowed scope"
  - "base branch is still current"

2. cancel 後に成果物を採用する

キャンセル後に agent が何かを返してきても、その run の成果物として採用しない方がよいです。必要なら新しい run に移し、採用理由を decision log に残します。

3. thread resume を業務 resume だと思う

thread が再開できることと、業務上そのまま続けてよいことは別です。Git、PR、MCP task、外部サービス、承認期限を見直してから再開します。

4. timeout を人間の注意力に任せる

承認待ちや一時停止は、通知だけではなく期限を持たせます。期限切れ後は expiredneeds_reconciliationcancelled のどれかへ遷移させます。

参考にした一次情報

まとめ

AIエージェントの run control は、便利な一時停止ボタンを作る話ではありません。run を識別し、状態を持たせ、pause / approval / cancel / resume / timeout を分け、再開前に実状態を確認する設計です。

ここを曖昧にしたまま自動化を増やすと、agent は止まったのか、待っているのか、終わったのかを説明できません。逆に、run control が揃っていると、承認フロー、再実行、停止手順、監査ログ、state reconciliation が同じ状態語彙でつながります。

私なら、最初の実装は run_idownerstatusexpires_atresume_requires だけで始めます。AIエージェントに write 権限や外部 action を任せるなら、少なくともその5つが無い run は本番運用に入れません。

WRITTEN BY nidoneko

Full-stack engineer with 8+ years of experience in TypeScript, React, Node.js, and cloud-native development across healthcare, finance, HR, and IoT domains.

View Profile →