コンテンツへスキップ

ステートマシン

ステートマシンとワークフローは、それぞれ異なる問題を解決します。

ワークフローが答えるのは「次にどの作業を実行するか」です。ステートマシンが答えるのは「この対象は現在どの状態にあり、ここからどのイベントが有効か」です。構築するものが単なるステップの連なりではなく、エージェントのセッション、チャットスレッド、レビューのライフサイクル、チケット、承認フローのように、時間とともに外部入力に反応する長期的な存在である場合、この違いが重要になります。

使用例

PHP
use Queuety\Enums\StateMachineStatus;
use Queuety\Queuety;

$machine_id = Queuety::machine( 'agent_session' )
    ->version( 'agent-session.v1' )
    ->state( 'awaiting_user' )
    ->on( 'user_message', 'planning' )
    ->state( 'planning' )
    ->action( PlanSessionAction::class )
    ->on( 'planned', 'awaiting_review' )
    ->state( 'awaiting_review' )
    ->on( 'approve', 'completed', ReviewApprovedGuard::class )
    ->on( 'reject', 'awaiting_user' )
    ->state( 'completed', StateMachineStatus::Completed )
    ->dispatch( [ 'thread_id' => 42 ] );

Queuety::machine_event(
    $machine_id,
    'user_message',
    [ 'message' => 'Find competitors for this product' ]
);

Queuety::machine_event(
    $machine_id,
    'approve',
    [ 'approved' => true, 'reviewer' => 'editor@example.com' ]
);

このマシンはawaiting_userで始まり、ユーザーメッセージが届くとplanningに移行し、キューに入ったアクションがplannedを出力した後、外部承認によってセッションが完了するまでawaiting_reviewで待機します。重要なのはコード構文そのものではありません。ライフサイクルが明示的で、確認可能で、イベント駆動型であることです。

マシンの実行方法

マシンインスタンスがディスパッチされると、Queuetyは現在の状態名、パブリック状態ペイロード、定義のバージョンとハッシュ、現在の状態で有効な受信イベント、永続的なイベント履歴を保存します。

遷移先の状態にaction()がある場合、Queuetyは状態開始ジョブをキューに追加します。そのアクションはパブリック状態を更新して次のイベントを待つだけでも、_eventを出力し、必要に応じて_event_payloadを出力して次の遷移をすぐに進めてもかまいません。状態にアクションがなく、終端状態でもない場合、マシンはQueuety::machine_event()で送信された外部イベントを待ちます。

ワークフローとステートマシンの使い分け

実行が基本的にステップ指向なら、ワークフローを使います。データを取得し、作業ごとに待機し、統合して、終了します。明確な開始と終了があり、次に実行する作業が主な関心事となる場合に適したモデルです。

有効な動作が現在のライフサイクル段階に依存する場合は、ステートマシンを使います。イベントがさまざまな順序で届く可能性がある場合、対象が長期間存続する場合、またはawaiting_user、planning、running_agents、awaiting_review、completedのような状態を明示的に確認する必要がある場合に適しています。

2つを組み合わせる方法

この2つのプリミティブは、競合させるより組み合わせることで最も効果を発揮します。遷移に重い処理が必要な場合、状態アクションからワークフローをディスパッチできます。また、その処理が完了したときに、ワークフローからマシンへイベントを送ることもできます。実際には、ステートマシンがライフサイクルを管理し、ワークフローがコストの高い処理や構造化された実行を担います。

ビルダーの構成

StateMachineBuilderは意図的に小さく保たれています。initial()とstate()でライフサイクルのグラフを定義します。action()はキューに入れる作業を1つの状態に紐付けます。on()はマシンを次の状態へ移行させるイベントを宣言します。on_queue()、with_priority()、max_attempts()で状態アクションの実行方法を制御し、version()とidempotency_key()で永続的なディスパッチメタデータを追加します。dispatch()はマシンインスタンスを作成します。

状態アクションでは、ワークフローのステップやハンドラーと同じリソースポリシーキーを使うオプションのconfig()メソッドも公開できます。これにより、高コストな状態遷移を共通の同時実行グループにまとめたり、通常のステップより多くの予算を消費するアクションに高いコストウェイトを割り当てたりできます。

より広範なリソースモデルについては、Resource Managementで説明しています。

コントラクト

状態開始アクションはQueuety\Contracts\StateActionを実装します:

PHP
namespace Queuety\Contracts;

interface StateAction {
    public function handle( array $state, ?string $event = null, array $event_payload = [], array $payload = [] ): array|string;
}

$payloadはアクション定義の構造化ペイロードです。アクションが単純なクラス名で指定された場合は空になります。Queuetyはnew $class()でアクションを生成するため、アクションクラスには必須引数のないコンストラクターが必要です。

キューに入る開始アクションを、ワーカー受け入れ、重み付きキューやプロバイダーの予算、その他のリソースポリシーに参加させる場合、状態アクションにconcurrency_group、concurrency_limit、cost_unitsを持つオプションのconfig()メソッドを定義できます。

遷移ガードはQueuety\Contracts\StateGuardを実装します:

PHP
namespace Queuety\Contracts;

interface StateGuard {
    public function allows( array $state, array $event_payload, string $event, array $payload = [] ): bool;
}

確認用API

ファサードを使って実行中のマシンを確認します:

PHP
$status   = Queuety::machine_status( $machine_id );
$machines = Queuety::list_machines();
$events   = Queuety::machine_timeline( $machine_id );
$trace    = Queuety::machine_trace( $machine_id );
$state    = Queuety::machine_state_at( $machine_id, $event_id );

ステートマシン用のWP-CLIコマンドはありません。wp queuety workflow ...が対象とするのはワークフローのみです。コマンドラインからマシンを確認するには、たとえばwp eval経由で、これらのPHPAPIを使います。

これらのAPIでは、現在の状態名、ライフサイクルの状態、パブリック状態、利用可能なイベント、定義メタデータ、完全な遷移履歴、デバッガーUI向けに正規化されたトレース一式を確認できます。マシン一覧の行には定義のバージョン、ハッシュ、冪等性キーが含まれます。そのため各マシンを個別に読み込まずに、一覧画面でソースと実行時のバージョンを表示できます。トレース一式には、グループ化された状態、生のイベント、関連付けられたアクションジョブ、ログ、アーティファクト参照、チャンク参照、ハンドラーの入出力、状態の遷移前後、コンテキスト、タイミング、構造化エラーが含まれます。

エージェントセッション

ステートマシンは、パイプラインというよりセッションとして動作するエージェントシステムで特に役立ちます。awaiting_user、planning、running_agents、awaiting_review、completedを移行するセッションは、多数の待機や後戻りを含む長大なワークフロー定義よりも、明示的なライフサイクルとして扱うほうが通常は理解しやすくなります。

関連項目