Skip to content

[debt] same-VM schema invocation の高水準 Program を L2 effects から提供する #540

Description

@proboscis

結論

same-VM の result transport は ADR-DOE-AGENTS-005 により L2 LaunchSession / AwaitResult 経路へ実装済みです。残る負債は transport ではなく、構造化結果を得る標準的なリトライ状態機械を各 caller が組み立てなければならないことです。

2026-07-16 ユーザー指示により解決形状を確定: リトライは doeff-agents が effect / handler として所有します。caller は「エージェントを起動して検証済み結果が欲しい」という意図を一つの高水準効果として発行し、方針値(締切・再試行予算・督促文)を効果の payload で渡すだけにします。リトライのループ・状態遷移・後始末は handler 側の機構であり、caller の Program には現れません。

ADR R5 は維持します。同期的な AgentHandler.handle_agent に MCP server loop を持たせるのではなく、L2 effects を合成する協調的な handler がリトライ状態機械を所有します。

設計比較(2026-07-16 監査で追加)

案A(採用): doeff-agents の高水準効果 + L2 handler がリトライ状態機械を所有

新しい効果(名前は実装時に確定。候補: InvokeAgent(spec, retry_policy) — 現在 AgentEffect の alias である v2 InvokeAgent に実体を与える形)を doeff-agents が定義し、その handler が L2 効果(LaunchSession → AwaitResult → 検証 → FollowUp → ReleaseSession)を協調的に合成する。

移植元は既存実装: packages/doeff-agents/src/doeff_agents/handlers/production.py_run_agent_task(296行目付近)が必要な意味論をすでに同期 Python で実装しています:

  • 壁時計権限は node 締切ただ一つ(L-K4-3、k8s activeDeadlineSeconds 意味論)
  • heartbeat 失効は輸送イベントであり、semantic failure にも retry 消費にもしない
  • continuable=False(terminal session)へは FollowUp しない — 即座に attempt-exhausted
  • 検証失敗時のみ FollowUp 督促を発行し、attempts を加算
  • finally で必ず ReleaseSession

ただし _run_agent_task は handler メソッドを直接呼ぶ同期関数なので、same-VM MCP server loop を飢餓させる(= R5 が L1 schema 起動を fail-fast にしている理由そのもの)。この意味論を L2 の効果ベース協調ループへ移植する。現在の L2 effectful.hyAwaitResultEffect は1回分の協調待機(cooperative timer + PRIORITY_IDLE Wait)しか持たないため、その上位に検証・FollowUp・attempts・deadline を所有する層が不足している — それがこの効果の handler。

利点: sim / test は handler 差し替えだけで no-retry・即時 retry・台本 outcome を注入できる(monkeypatch 不要)。リトライ規則の変更が全 caller へ同時に効く。

案B(却下): doeff-core-effects の汎用 Retry(program, policy) 効果

汎用リトライ効果はサブ Program の再実行を意味するが、エージェントの結果契約リトライは再実行ではなく「生きている同一 session への FollowUp 督促」である(再 Launch すると session 状態と作業内容が失われる)。意味論が一致しない。また汎用再実行リトライは非冪等操作への誤用を招く(proboscis-ema の ADR-NAK-006 Idempotency-retry law は、リトライを冪等読み取り型の完全一致 allowlist に限定し、注文・任意コマンドのリトライを禁止している)。冪等読み取り向けの汎用 Retry は将来別 issue にはなり得るが、本件の手段にはしない。

責任分界

caller が決める方針(効果 payload の typed value として渡す):

  • 全体の業務期限
  • 許容する再試行回数
  • schema 不正・入力待ち時の再依頼文
  • 最終 outcome を domain error、安全停止、gate へどう写像するか
  • domain MCP tools、prompt、result schema

doeff-agents が所有する機構(handler 内部):

  • LaunchSession
  • report_result sink と MCP server lifecycle
  • AwaitResult の協調待機
  • schema validation と typed outcome
  • heartbeat / awaiting-input / terminal / continuable の状態遷移
  • 必要時の FollowUp dispatch
  • 成功・失敗・取消時の ReleaseSession
  • retry count と deadline の一貫した適用

caller は方針値を渡しますが、timer、CreateExternalPromise、Wait、状態 switch、cleanup loop を再実装しません。

現状の根拠

  • ADR-DOE-AGENTS-005 R1 / R2 は same-VM report_result transport を L2 defhandler に実装済みです。
  • R5 は同期的な L1 agent() 経路が server loop を飢餓させるため fail-fast し、schema session は L2 を使うと決定しています。この決定は維持します。
  • R4 の「solicitation 方針は caller 所有」は、方針の選択を caller に残す規則であり、待機・再試行機構の複製を要求するものではありません。
  • production.py_run_agent_task は必要なリトライ意味論を同期 L1 側で実装済み(上記・案Aの移植元)。
  • effectful.hyAwaitResultEffect handler は1回分の協調待機のみを所有し、検証・再試行の層が存在しません。
  • packages/doeff-agents/src/doeff_agents/programs.py は、非構造化 session について run_agent_to_completion という高水準 Program を既に提供しています。構造化 invocation に同等の深い module がありません。
  • doeff AGENTS.md は、schema validation、missing-result handling、retry loop を doeff-agents の所有と明記しています。
  • v2 InvokeAgent は現在 AgentEffect の alias であり、same-VM L2 composition ではありません。

目標 API

caller policy (typed) + AgentInvocationSpec
    -> 高水準効果を1つ発行(例: InvokeAgent(spec, retry_policy))
    -> doeff-agents の L2 handler がリトライ状態機械を所有:
         LaunchSession
         -> AwaitResult(協調待機)
         -> 検証 OK -> ReleaseSession -> validated result
         -> 検証 NG:
              heartbeat 失効     -> 再 await(attempt 消費なし・deadline のみ確認)
              continuable=False  -> attempt-exhausted(FollowUp しない)
              deadline 超過      -> deadline gate(新規 FollowUp を発行しない)
              attempts < budget  -> FollowUp 督促 -> 再 await
              attempts 尽きた    -> attempt-exhausted
         -> いずれの終端でも session / sink / MCP server を解放
    -> validated result または typed terminal outcome

段階移行のため、狭い変種 AwaitValidatedResult(handle, policy)(起動済み handle に対し検証付き待機のみを効果化する)を先に切り出してもよい。Nakagawa の現行境界関数は handle を受け取る形のため、この変種は移行の中間段階として互換性がある。最終形は全 lifecycle を所有する高水準効果とする。

完了条件

  • リトライ状態機械を effect / handler として提供する(caller の Program にループ・timer・状態 switch が現れない)。
  • _run_agent_task の意味論(deadline 単独権限・heartbeat 非消費・terminal 非督促・finally 解放)を L2 協調 handler に移植し、L1 実装と意味の同値性をテストで固定する。
  • 同期的な AgentHandler.handle_agent に same-VM server loop を追加せず、ADR R5 を維持する。
  • caller policy を typed value として定義し、mechanism と分離する。
  • heartbeat expiry 自体を semantic failure や retry 消費にしない。
  • 一つの明示 deadline が全体の壁時計上限を所有する。
  • continuable=False の outcome へ FollowUp しない。
  • schema 不正、入力待ち、結果なしの状態遷移と retry accounting を handler 一箇所で実装する。
  • 成功・失敗・期限切れ・取消の全経路で session、sink、MCP server を解放する。
  • same-VM domain MCP tool と偽 agent adapter を使った回帰試験を追加する(sim/test は handler 差し替えで no-retry / 台本 outcome を注入できることを含む)。
  • caller が独自 Timer / CreateExternalPromise / Wait loop を持たずに構造化結果を取得できる。
  • v2 InvokeAgent を本効果の実体とするか、新名称を追加して alias を維持するかを実装 PR で決定し、互換方針を記録する。
  • ADR R4 と両 repo の AGENTS.md を「caller owns policy / doeff owns mechanism」と矛盾なく記述する。
  • 既存 L2 API は高度な caller 向けに残す。

下流

proboscis/proboscis-ema#629 で Nakagawa の独自 mechanism をこの effect / handler へ移行します。業務期限、再依頼文、安全停止への写像は Nakagawa に残します。

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions