Operator Approval Loop(オペレーター承認ループ)
外部の取引先と通信するエージェントは、自らの判断で送信してはならず、オペレーターに内部で常に通知する必要があります。このスキルは、すべての送信下書きがオブリゲーションとして記録され、オペレーターが正確なテキストを決定し、配送台帳が送信の証跡を残すという契約を定義します。
いつ使用するか
- エージェントが共有チャンネル、メール、チャットで顧客、サプライヤー、投資家、パートナーへの返信を作成し、送信前に人間の承認が必要な場合。
- 送信された各メッセージを正確な下書きテキスト、承認したオペレーター、決定時刻にリンクする監査証跡が必要な場合。
- 古い承認が書き換えられた下書きをリリースしたり、同じ承認済みメッセージが2人のワーカーによって重複して配信されたりするのを見たことがある場合。
- 下書きが、台帳にすでに保持されている事実を取引先に繰り返し尋ねている場合。
仕組み
オブジェクト
| オブジェクト | 意味 |
|---|---|
| オブリゲーション (Obligation) | 取引先に負っている1つの責務。ステータスは drafted、approved または rejected、sent と遷移します。direction、counterparty、channel、updated_at エポックを持ちます。 |
| 下書き (Draft) | 正確な下書きテキスト、そのSHA-256ハッシュ、起点座標(プラットフォーム、チャンネル、スレッド、ユーザー)、優先度(P0〜P3)を保持するサイドカーレコード。オブリゲーションごとに1つ存在し、再提出時に置き換えられます。 |
| 決定 (Decision) | オペレーターによる承認または却下。オペレーターID、ノンス、および決定が行われた下書きエポックとともに記録されます。 |
| 承認スナップショット (Approval snapshot) | すでに承認された決定ライターによって記録された、不変のテキスト、ハッシュ、エポック、および宛先。スナップショットがないとディスパッチを許可できません。 |
| 請求 (Claim) | ランダムなトークンと状態を持つ耐久性のある予約。オブリゲーションごとにアクティブな請求は最大1つです。 |
| 配送 (Delivery) | 1つの送信または通知を証明する台帳行。(obligation, decision) ペアごとに記録されます。 |
リファレンススキーマは references/approval-ledger.sql にあります。
下書きの提出
- 入力をクリーニングします。制御文字を除去し、単一行フィールドの空白を縮小し、長さ制限(下書き、サマリー、コンテキスト、取引先)を適用します。空またはサイズ超過のフィールドは、黙って切り詰めるのではなく拒否されます。
- ベースラインゲート(後述)を実行します。提出を拒否する場合があります。
- 下書きテキストをSHA-256でハッシュ化します。ハッシュプレフィックスはサマリーに含まれ、承認パネルがどのテキストを承認しているかを示します。
- Upsertを実行します。同じ (counterparty, channel) に対してオープンな drafted オブリゲーションがすでに存在する場合、下書きサイドカーを置き換え、オブリゲーションの
updated_atを進めます。この進みがエポックローテーションです。古いエポックにキー付けされた決定は、新しいテキストをリリースできなくなります。そうでなければ、ステータスdraftedの新しいオブリゲーションを挿入します。 - 提出領収書を、設定済みで検証済みの内部運用宛先にのみルーティングします。発信元がその内部宛先である場合、そこで確認応答します。決して沈黙しないとは、内部報告を意味し、自動的な外部返信を意味しません。下書きハッシュ、承認ステータス、オペレーターID、ワークフローメタデータを取引先から見えるチャンネルに保持してはいけません。不明または未分類の発信元は沈黙を守ります。ダイレクトメッセージは自動的に内部とはみなされません。
検証済みの内部宛先が利用できない場合、提出結果を内部ツール結果またはオペレーター画面に保持します。外部または不明な発信元にフォールバックしてはいけません。部外者に公開されるツール結果は内部画面ではありません。
下書きの提出は、外部応答を許可するものではありません。ポリシーで許可された明確化の質問や中立的な応答は、個別の送信判断であり、既存のメンション、チャンネル、下書きのみ、凍結、禁止の制約 (counterparty-channel-discipline) に従います。内部承認メタデータを開示してはいけません。
ベースラインゲート
下書きが提出される前に、取引先の現在のベースライン(一時台帳、契約ストア、CRM)を照会します。
- 署名済みまたは配信済みの契約が記録されている場合:証拠と推奨事項とともに提出を拒否します。署名後に取引先に仕様を尋ねることは、このゲートが防ぐまさにその失敗です。
- オペレーターによるオーバーライド:
force_despite_signed_contractにより提出が通過し、下書きコンテキストに[BASELINE_OVERRIDE_SIGNED_CONTRACT]がスタンプされます。 - ゲートサービスが到達不能:提出は進行し、コンテキストに
[BASELINE_CHECK_UNAVAILABLE]がスタンプされます。パネルはガードがオフであったことを確認できます。障害によってゲートが黙って無効化されることはありません。 - ファクトが利用可能な場合、最新のいくつかをコンテキストに
[BASELINE FACTS: ...]ダイジェストとして添付し、下書きが現在の真実とともに提出されるようにします。
決定
承認パネルは、ステータスが drafted で方向が we_owe_them のオブリゲーションを一覧表示します。承認または却下は、下書きエポック(draft_updated_ts)を保持する決定行を書き込み、同じトランザクション内でオブリゲーションのステータスを変更します。エポックが現在の updated_at と一致しない決定は古く、何もリリースしてはいけません。
すでに承認された承認決定の場合、同じトランザクションで不変の obligation_approval_snapshots 行が挿入されます:決定IDとオブリゲーションID、現在の下書きエポック、正確なテキストとSHA-256、プラットフォーム/チャンネル/スレッド、および種別 draft_sent。決定ライターは書き込み前に認証を確立する必要があります。リファレンスはオペレーターを認証したり、決定を作り出したりしません。自動承認ポリシーはリファレンスによって有効化または拡張されません。スナップショットのないレガシー決定は、明示的な調整または新しい承認が必要です。現在の可変的な下書きから許可をバックフィルしてはいけません。
配送
SQLiteリファレンスは references/approval_claims.py にあります。これはディスパッチ許可を付与しますが、トランスポートを呼び出しません。SQLフィクスチャから初期化された既存のローカルリファレンスデータベースを使用します。モジュールはスキーマや本番マイグレーションを適用しません。信頼された決定ライターのみが承認レコードを設定できます。すべてのライターは外部キーと再帰トリガーを有効にし、スキーマガードを尊重する必要があります。管理によるデータベース改ざんはこのモデルの範囲外です。
- バインドされた承認済み下書きを発見します。発見は許可ではありません。
claim()は独自のBEGIN IMMEDIATEトランザクションを開始し、スナップショットに対して現在の承認済みエポック、正確なテキスト、計算されたSHA-256、完全な宛先を検証し、トークンを返す前に一意の請求を挿入します。競合が発生すると、ワーカーはトランスポートの前に停止します。完了した領収書は再度請求できません。 begin_dispatch()はバインディングを再検証し、トークンを使用してclaimedをdispatchingにアトミックに変更します。勝った呼び出し元のみが、コミット後に正確なdraft_textと宛先を受け取ります。テキストを再生成したり、可変的なサイドカーをトランスポート用に再読み取りしたり、ペイロードを別の試行に再利用したりしないでください。ネストされた呼び出し元のトランザクションは拒否されます。許可が後の呼び出し元のコミットに依存することはできません。トランスポートをまたがってデータベーストランザクションを開いたままにしてはいけません。- 確認された成功結果は
complete()に送られます。これは、配送座標をアトミックに記録し、請求を配信済みとマークし、オブリゲーションをsentに変更します。同一の完了はno-opです。競合する座標は失敗します。領収書のUNIQUEキーはレコードを重複排除しますが、以前の外部効果は重複排除しません。 - 例外、タイムアウト、begin_dispatch後のワーカークラッシュ、または失敗した領収書の永続化により、ブロックされた試行が残ります。
mark_unknown()は不確実性を記録します。不明な請求は期限切れにならず、再オープンせず、自動再試行せず、そのオブリゲーションに対する別の決定がそれをバイパスすることを許可しません。信頼された呼び出し元は、確認された成功座標と証拠とともにreconcile()を使用できます。モジュールはその証拠を検証しません。領収書がないことは、未配信の証明にはなりません。
保証は、承認された決定ごとに1回の自動ディスパッチ試行であり、正確に1回の外部配信ではありません。begin_dispatch後、トランスポート前のクラッシュにより、送信ゼロで保持された請求が残る可能性があります。不明な結果を新しい試行のために解放するには、元の実行者をフェンシングし、プロバイダーのセマンティクスを検証する必要があります。このリファレンスは意図的にそのような再試行操作を提供しません。
| 請求状態 | 許可される次の状態 |
|---|---|
| claimed | dispatching または ディスパッチ前のキャンセル |
| dispatching | delivered または unknown |
| unknown | 信頼された調整を介してのみ delivered |
| delivered, cancelled | 終端。決定キーは再利用不可 |
請求がアクティブな間、データベースガードはオブリゲーション、下書き、決定の書き込み(置き換えを含む)を凍結します。スナップショットと請求は消去できません。再提出する前に、請求された操作をそのトークンでキャンセルします。古いトークンは何も許可しません。ディスパッチが開始された後は、調整のために新しい編集または取り消しを保留します。これにより、進行中の操作を呼び戻すふりをする代わりに、変更をシリアル化します。
拒否された決定と、下書きサイドカー/スナップショットのないレガシー行は、この外部下書き送信パスに入りません。これらは内部オペレーター画面に報告され、手動処理されます。内部領収書フッターは内部のままです:
approved by <operator> · receipt <decision_id> · draft sha256 <prefix>。
すでに承認された外部テキストを変更してワークフローメタデータを追加してはいけません。
焦点を絞ったローカル検証では、一時データベース、個別の接続、シミュレートされた試行カウンターを使用します。プロバイダーや実際のメッセージは使用しません:
python3 -m unittest discover -s tests/skills -p 'test_approval_delivery_claims.py'。
テストにはPython 3.11以上とSQLiteシリアライゼーションサポートが必要です。リファレンスは標準ライブラリのみを使用します。既存のdesk-pattern契約チェックは別個の互換性チェックとして残ります。
時間制限付き自動承認(オプション)
下書きは auto_send_after(エポック秒)を保持できます。スイープは、決定がないまま期限が過ぎた下書きを承認し、オペレーター auto-ttl を記録し、その後配送は通常のパスを進みます。オペレーターの操作が常に優先されます。決定はスイープがそれを見る前にステータスを変更し、再提出はエポックをローテーションし、期限を移動またはクリアします。スイープは、書き込みトランザクション内部でステータスとエポックを再チェックするため、競合はno-opとして解決されます。期限のない下書きは永久にハードゲートされたままです。
シグナルリンケージ
下書きは、それが応答するインバウンドオブリゲーションを指定できます(signal_obligation_id)。これは待ち時間測定(インバウンドシグナルから下書き応答まで)のための唯一の真実のリンクであり、SLAスキャンがそのインバウンドアイテムを回答済みとして扱うことを可能にします。参照された行が存在しない場合、提出を拒否します。
例
下書きの提出
file_request(
draft="ありがとうございます。金曜日までスロットを確保できます。どの開始日がよろしいですか?",
counterparty="acme-supplier",
context="配送ウィンドウの質問への返信",
origin_platform="slack", origin_channel="#acme-shared",
origin_thread="1712345678.000100", priority="P1",
signal_obligation_id=412)
-> {obligation_id: 431, draft_sha256: "9f2c...", refiled: false}
設定済みで検証済みの内部運用宛先には次のように表示されます:
Draft filed for approval (P1, sha 9f2c8a1b). Waiting on operator.
取引先から見える発信元チャンネルには提出通知は送信されません。検証済みの内部宛先が利用できない場合、領収書は内部ツール結果またはオペレーター画面に留まり、外部フォールバックはありません。
修正後の再提出
オペレーターがより短い下書きを要求した場合。同じ (counterparty, channel) に対して再度提出すると、refiled: true が返り、サイドカーテキストとハッシュが変更され、updated_at が進みます。古いパネル行でクリックされた承認は古いエポックを持ち、無視されます。
ゲート拒否
DeskApprovalError: baseline gate refused this draft: the ledger shows a
signed contract for 'acme-supplier'. Evidence: master agreement executed
2026-08-14. Recommendation: do not ask. Re-file with
force_despite_signed_contract=true if this is genuinely a new thread.
内部チャンネルでの配送フッター
Confirmed for Friday, start date 2026-09-08.
approved by operator-a · receipt 118 · draft sha256 9f2c8a1b2d3e4f50
テストすべき不変条件
- 提出領収書は設定済みで検証済みの内部運用のみに送信されます。発信元がその検証済み内部宛先である場合にのみ、発信元は1つを受け取ります。
- 不明な発信元は沈黙を守ります。利用できない内部宛先は内部ツール結果またはオペレーター画面を使用し、外部フォールバックはありません。
- 同じ (counterparty, channel) で2回提出すると、1つのオブリゲーション、2つのエポックが生成されます。
- 古いエポックを持つ決定は、配送行を生成しません。
- 2人の同時請求者は1つのディスパッチ許可を取得します。敗者はトランスポートを試みません。
- 不明な結果と失敗した領収書の永続化は、自動再試行を有効にしません。
- 成功した完了は、領収書と送信ステータスを1つのトランザクションで記録します。
- 変更されたエポック、テキスト、ハッシュ、または宛先は、請求を取得または開始できません。
- アクティブな請求は再提出をブロックします。ディスパッチ前のキャンセルのみがその保留を解除できます。
- ゲートが利用不可の場合はマーカーがスタンプされます。ゲートがサイン済みの場合は、強制なしで拒否されます。
- Auto-ttlは、オペレーターがその後再提出したテキストに対して発動しません。