AI Agent WorkstationのTask GitとRunner契約を段階的に固める
4レイヤー構成の次に実行時契約を固める
前回の記事では、Todo Schedulerを単純なタスク管理アプリから、複数のAI Agent Workstationを管理する実行基盤へ移行する過程をまとめました。
主なテーマは、コードの責務を整理することでした。
- BackendをDomain、Application、Infrastructure、Presentationへ分割する
- Frontendも同じ依存方向を参考に整理する
main.goとapp.jsをComposition Rootへ近づける- Infra Runnerを複数のmoduleへ分割する
- Architecture Testで依存方向を固定する
- AI Agent Workstationの権限と通信先を制限する
その後の開発では、単純なレイヤー分割よりも、
SchedulerとRunnerが何を約束し、どの状態を誰が所有するか
という実行時の契約が重要になってきました。
今回の変更は、この契約をTask Git、required outputs、Background Execution、terminal reporting、E2Eの各境界で明示するものが中心です。
前回からの主な変化
大きく整理すると次のようになります。
| 項目 | 前回 | 今回 |
|---|---|---|
| Runner結果 | Codexの結果、validation、Git情報を報告 | Task Planが要求した任意evidenceも構造化して報告 |
| required outputs | Scheduler側に検証はあるがRunnerから提出する正式経路がない | job-local CLI / Unix socketを追加 |
| HTTP 409 | statusを中心に扱う部分が残る | reason単位でterminal outcomeを分類 |
| Background job | backgroundという大きな分類 | phaseとsource modeをversioned contract化 |
| Task Git | execution中心のworktree管理 | Planning、Execution、Aggregationまで契約を明確化 |
| terminal retry | leaseとterminal結果の境界が曖昧 | terminal POSTの冪等性とreconciliationを整理 |
| E2E | App内mockや個別contract testが中心 | 実Runnerとclean clusterを使うTask Git suiteを追加 |
| テスト運用 | suite単位の成功確認 | test lane、performance、verification manifest、review sliceを追加 |
差分自体も大きくなっています。
前回のソースから今回のソースでは、274ファイルが変更され、約26,000行が追加されています。
内訳はおおよそ、
App
210 files
+15,687
-2,929
Infra
64 files
+10,324
-948
です。
そのため、今回は単一機能の修正というより、Runner / Scheduler間の契約とTask Git周辺をまとめて固める変更になっています。
required_outputsを正式なRunner契約にする
今回の変更で特に大きいのが、Task Planのrequired_outputsです。
Task Planでは、例えば次のようなoutputを要求できます。
api_title_check
browser_title_check
changed_files
implementation_summary
以前のRunnerは、Codexの実行結果として、
- final response
- validation結果
- Git commit
- execution metadata
- runner report
などはSchedulerへ送っていました。
しかし、Plannerが任意に定義したrequired_outputsを、AIが正式なevidenceとして提出する経路がありませんでした。
そのため、
Codex成功
↓
validation成功
↓
commit成功
↓
Runnerがsuccessを送信
↓
Scheduler
required_outputs_missing
↓
HTTP 409
という状態が発生していました。
AIは作業を完了しているのに、RunnerとScheduler間の契約を満たせず、Taskが完了できない状態です。
final responseを解析しない
今回の実装では、AIの最終回答からrequired outputを推測する方式にはしていません。
代わりに、
Codex
↓
todo-scheduler-report
↓
job-local Unix socket
↓
Runnerのin-memory evidence store
↓
completion preflight
↓
Scheduler
という経路を追加しています。
AIから利用するinterfaceはCLIです。
概念的には次のようになります。
todo-scheduler-report set api_title_check "デモタイトルB"
todo-scheduler-report set-json implementation_summary \
'{"status":"passed","detail":"title.jsを更新"}'
CLIはScheduler URLやRunner credential、lease tokenを知りません。
AIが直接Scheduler APIを操作するのではなく、trusted Runner processが代理でcompletionを送ります。
これはWorkstation内のAIをsecurity boundaryとして信用しないという、これまでの設計方針とも一致しています。
required_outputsをclaim時点で渡す
Runnerがrequired outputを提出するには、そもそも何が要求されているのかをRunner自身が知る必要があります。
そこでclaim inputへversion付きcontractを追加しています。
{
"completion_contract": {
"schema_version": "runner.required-outputs.v1",
"required_outputs": [
{
"key": "api_title_check",
"kind": "evidence"
},
{
"key": "commit_sha",
"kind": "commit_sha"
}
]
}
}
重要なのは、Runnerがoutput名を見て独自に意味を推測しないことです。
Schedulerが、
evidence
commit_sha
test_report
artifact
などへ分類し、その結果をRunnerへ渡します。
これにより、Planner、Scheduler、Runnerでrequired outputの意味が分裂しにくくなります。
completion前にpreflightする
以前は、Runnerがsuccessを送ってからSchedulerに拒否されていました。
今回はSchedulerへ送る前にRunner自身が必要なevidenceを確認します。
Codex完了
↓
required outputs preflight
↓
不足あり
├─ yes → normal failure
└─ no
↓
validation / Git result
↓
complete
required outputが不足している場合は、/completeを送信しません。
required_output_evidence_missingとして通常のfailure flowへ移ります。
これによって、
required_outputs不足
↓
Scheduler 409
↓
terminal_unknown
という経路を通常動作から外すことができます。
HTTP 409を理由ごとに扱う
以前のRunnerには、HTTP 409を広くlease競合として扱う部分がありました。
しかし409には複数の意味があります。
現在はreason単位で分類しています。
例えば、
required_outputs_missing
invalid_required_outputs
task_plan_required
task_plan_not_ready
task_plan_job_mismatch
は確定的なbusiness / contract rejectionとして扱います。
一方、
stale_lease
lease_conflict
idempotency_conflict
などは、terminal requestがSchedulerに受理されたかどうかを慎重に確認する対象になります。
つまり、
HTTP statusだけではterminal outcomeを判断しない
という形へ変更しています。
terminal_unknownは、本当に結果が不明な場合へ限定する方向です。
reserved_event_type問題も修正する
Background Runnerでは別の問題も発生していました。
Runnerがprogress eventとして送ったevent typeをSchedulerが、
400 reserved_event_type
で拒否し続ける状態です。
原因は、
runner.started
runner.completed
runner.failed
のようなScheduler自身がlifecycleから生成するeventを、Runner側からもprogressとして送っていたことでした。
現在はこれらを、
- local operator logには残す
- Schedulerへのstructured progressには送らない
という形へ分離しています。
ログとして必要な情報と、wire protocolとして送信してよい情報を別のcontractとして扱うようになりました。
Background jobにも実行モードを導入する
required outputs対応の後には、Task Git側でも別の契約不整合が見つかっています。
以前のRunnerは、
job.kind == background
であれば、基本的にinput.task_gitを要求していました。
しかしBackground AIには、Gitへ変更を加えるExecutionだけでなく、
- Planning
- Replanning
- Aggregation
もあります。
これらはrepositoryを読む必要はありますが、Task worktreeを作ってcommitする処理ではありません。
そこで、Background claimへ新しいexecution contractを追加しています。
{
"execution": {
"schema_version": "runner.background-execution.v1",
"phase": "planning",
"source_mode": "repository_read_only"
}
}
source modeは大きく次の2種類です。
| source_mode | Task Git | 用途 |
|---|---|---|
repository_read_only | 不要 | Planning / Replanning / Aggregation |
task_worktree | 必須 | Execution / Integration |
Runnerは単にbackgroundかどうかではなく、このversioned contractを見て動作します。
これによって、
Background = 必ずGit変更
という暗黙の前提をなくしています。
capabilityも互換性確認に使う
SchedulerとRunnerで同じ名前の機能を別の意味として理解していると、claim後に初めて失敗します。
現在は、
background-execution-v1
task-worktree-v1
というcapabilityを組み合わせ、非互換なRunnerへjobをclaimさせない方向へ進んでいます。
これは、これまでArchitecture Testでsource codeの依存方向を固定していた考え方を、runtime contractにも広げたものと考えています。
Task Gitの検証範囲を広げる
前回の記事では、Task Gitは今後のRunner分割やGit境界の課題として扱っていました。
現在はかなり具体的なstate machineになっています。
基本形は、
Root Task
↓
detached persistent worktree
↓
Child
↓
Grandchild
↓
段階的merge
↓
Root result
↓
明示Finalize
↓
remote main
です。
Scheduler自身はGit操作を行いません。
Runnerが所有worktree上で、
- worktree作成
- commit
- merge
- validation
- cleanup
- finalize
を実行します。
特にrootは、作業完了だけではremote mainへ反映されません。
execution success
↓
finalize_ready
↓
管理操作
↓
Finalize
↓
remote main更新
という境界を維持しています。
clean-cluster E2Eを追加する
今回の差分では、Task Git用のE2E infrastructureが大幅に増えています。
新しいsuiteは、
infra.task-worktree-clean-cluster-e2e.v2
として定義されています。
単純なmock Runnerではなく、
- Scheduler
- 実Runner client
- Runner PVC
- git-bare
- Task worktree
- lease
- validation
- finalize
を実際に接続することを目的としています。
scenarioはTW-E2E-00からTW-E2E-12まで用意されています。
例えば、
Planning
Execution
Child / Grandchild merge
Pod restart
lease loss
terminal response drop
idempotent finalize
cleanup retry
cross-workspace delegation
required_outputs
stale finalize
fresh PVC
などを対象にしています。
特にTW-E2E-10では、Cross-workspace delegationとrequired outputsを同じscenarioで確認する設計になっています。
これは今回のrequired_outputs問題に対する、かなり直接的な再発防止テストです。
ただしclean-cluster acceptanceはまだ別
sourceとlocal fixtureが追加されたことと、実clusterでacceptanceが完了したことは分けています。
clean-cluster E2Eでは、最終的に20回連続実行し、
requested_repeats = 20
passed_repeats = 20
failed_cases = 0
flake_count = 0
をacceptance条件としています。
今回確認したsourceでは、runbookとharnessは用意されていますが、実clusterでの20回連続acceptanceは別のoperator作業として残されています。
unit testが通ったことだけで「Kubernetes上でも動いた」と扱わない方針です。
terminal処理にも冪等性を入れる
RunnerとSchedulerの間では、
Schedulerはcompleteを受理した
↓
responseだけRunnerへ届かなかった
というケースがあります。
この場合にRunnerがもう一度completeすると、同じ結果を二重登録してはいけません。
現在はcompleteとfailについて、
method
path
job ID
attempt
canonical body
からfingerprintを作り、terminal operationを冪等化しています。
同じoperationであれば保存済みresponseを返し、異なるpayloadを同じkeyで送った場合は、
409 idempotency_conflict
として扱います。
「通信に失敗した」と「処理そのものが失敗した」を区別するための仕組みです。
テストそのものの管理方法も変わった
前回の記事では、
App JavaScript tests
Infra tests
Codex Runner tests
Python workspace tests
というsuite単位の確認が中心でした。
今回はさらに、
fast
provider
task-git
runner
performance
E2E
といったtest laneが追加されています。
Go testについてもcacheを利用した日常開発用testと、-count=1で測定するperformance gateを分離しています。
また、実行結果を単にconsoleへ残すのではなく、boundedなverification artifactとして扱う仕組みも追加されています。
review単位も明示する
大きな変更を一つのcommitへまとめないため、独立review sliceというルールも追加されています。
機能変更を、
contract-schema
↓
provider-db
↓
ui
↓
docs
↓
evidence
の順に分離します。
commitには例えば、
Request-ID: INFRA-APP-TASK-WORKTREE-01
Change-Slice: provider-db
Verification: npm run test:provider
Verification: npm run test:task-git
というmetadataを持たせます。
検査scriptでは、
- merge commitではない
- sliceの順序が正しい
- production領域が混在していない
- 変更量が上限を超えていない
- verificationが記録されている
などを確認します。
これはArchitecture Testと同じく、
手順書に書くだけでなく、守られていない状態を機械的に検出する
という方向です。
ファイルサイズは再び増えている
前回の記事では、巨大ファイルを分割した成果として、
main.go
約4,200行 → 約1,800行
app.js
約1,900行 → 約800行
codex-runner.mjs
約1,900行 → 約1,300行
と書きました。
今回のsourceでは、おおよそ次の状態です。
main.go 2,139行
app.js 863行
codex-runner.mjs 2,016行
直前のソースと比較しても、
main.go
1,955 → 2,139
app.js
850 → 863
codex-runner.mjs
1,804 → 2,016
と増えています。
特にRunnerは、required outputs、Task Git、Background Execution、reconciliationなどの処理が増えたことで、本体も再び大きくなっています。
つまり、前回の記事で残課題としていた、
Runnerのpoll loop、active job管理、heartbeat、post-processingをさらにCoordinatorへ分割する
という課題は、まだ解消したとは言えません。
module数は増えましたが、codex-runner.mjs自体を小さなComposition Rootへする作業は引き続き必要です。
前回レビューから変わった点
今回のsourceを前回の調査と比較すると、required outputs問題についてはかなり前進しています。
前回は、
Task Plan
required_outputs
↓
Runnerは提出方法を知らない
↓
Scheduler 409
↓
terminal_unknown
という状態でした。
現在は、
Task Plan
↓
versioned completion contract
↓
Runner
↓
local reporting CLI
↓
Unix socket
↓
job-local evidence store
↓
preflight
↓
completion payload
↓
Scheduler validation
までsource上の経路が実装されています。
また、以前のログで確認したreserved_event_typeについても、reserved lifecycle eventをScheduler progressへ送らない形に修正されています。
409についてもstatus一括判定からreason単位の分類へ変更されています。
そのため、以前確認した障害の原因に対する修正方向は妥当です。
一方で新しい契約差分も見つかっている
契約を厳密にした結果、新しいdriftも検出できています。
現在のsourceでApp OpenAPIとInfra Runnerのcontract testを実行すると、
ProgressRequest properties drifted
from the canonical flat request allowlist
で失敗します。
App側のProgressRequestには、
job_id
がありますが、Infra側canonical allowlistには含まれていません。
required output関連のtestは通っていますが、App / Infra間のcontract gate全体としてはgreenではありません。
つまり、
contract testを追加したことで、別の契約差分を早い段階で発見できている
という状態です。
これは修正が必要な問題ですが、設計上は以前より健全です。
実Runnerを動かしてから契約不一致に気付くより、sourceのgateで止められるためです。
JSON evidenceにも仕様差が残る
required output CLIはJSON objectを扱えますが、現在はarrayを拒否します。
例えば、
{
"status": "passed",
"files": ["title.js"]
}
のようなobjectは扱えます。
一方、
["title.js"]
というarrayそのものは受理しません。
当初の設計例ではchanged_filesをJSON arrayとして扱う案もあったため、この点は仕様をどちらへ固定するか決める必要があります。
現在の実装に合わせるなら、
{
"files": ["title.js"]
}
のようにobjectへ包む方がcontractとして明確です。
現在の位置づけ
7月30日時点では、主なテーマは「巨大な実装を4レイヤーへ分ける」ことでした。
現在はそこから一段進み、
Source Architecture
↓
Runtime Contract
↓
Ownership / Lease
↓
Structured Evidence
↓
Idempotency
↓
Real-cluster Verification
という方向へ移っています。
単にコードの置き場所を整理するだけではなく、
- 誰がGit状態を所有するか
- AIが何を提出できるか
- Runnerが何を検証するか
- Schedulerが何を最終判定するか
- 通信結果が不明な場合にどう扱うか
- ProviderとConsumerの契約が一致しているか
- 実clusterで同じ状態遷移を再現できるか
を機械的に確認する段階です。
現在残っている課題
現時点で優先度が高いものは次のとおりです。
App / Infra contract gateをgreenにする
ProgressRequestのcanonical schemaをAppとInfraで統一します。
OpenAPI、Provider実装、Consumer contract testの三者で同じschemaを正本にする必要があります。
clean-cluster E2Eを実行する
Task Gitとrequired outputsについて、local fixtureだけでなく実Runner imageとKubernetes環境でacceptanceを確認します。
特に、
TW-E2E-10
のCross-workspace + required outputsは、今回の障害に対する重要な確認項目です。
JSON evidenceのarray方針を確定する
arrayを正式対応するのか、objectのみをversioned contractとして採用するのかを決めます。
codex-runner.mjsを再び縮小する
moduleへの分離は進んでいますが、本体は再び2,000行を超えています。
poll loop、claim coordinator、terminal coordinator、post-processingなどをさらに分離する余地があります。
Deployment provenanceを固定する
Scheduler source revision、Runner source revision、image digest、OpenAPI digestを実行証跡として結び付ける必要があります。
source checkoutと実際に動いているimageが同じものだと確認できなければ、contract mismatch発生時の調査が難しくなります。
まとめ
前回は、Todo Schedulerを4レイヤーへ分割し、AI Agent Workstationの責務を整理することが中心でした。
今回は、その境界を実際の実行時contractとして固定する作業が進んでいます。
現在の主な変化は次のとおりです。
- Task Planのrequired outputsをversioned completion contractとしてRunnerへ渡す
- AIはlocal CLIとUnix socketを通して構造化evidenceを提出する
- Scheduler credentialやlease tokenをAIへ渡さない
- success送信前にRunnerがrequired outputsをpreflightする
- HTTP 409をreason単位で分類する
- reserved lifecycle eventをScheduler progressから除外する
- Background jobへphaseとsource modeを導入する
- PlanningとExecutionでTask Gitの扱いを分離する
- terminal POSTへ冪等性を導入する
- Task Gitのclean-cluster E2E harnessを追加する
- test lane、performance gate、verification manifest、review sliceを導入する
- App / Infra contract driftをsource testで検出する
一方で、構造化が進んだことで新しい課題も見えています。
特にApp / InfraのOpenAPI contract mismatch、clean-cluster acceptance、Runner本体の再肥大化は、次の改善対象です。
4レイヤー構成への移行そのものが目的ではなく、その上でAI Agent Workstationを安全に継続実行できる契約を作ることが、現在の中心課題になっています。
今後は、Task Gitのclean-cluster E2E、deployment provenance、Runner Coordinatorの分離を進めながら、source上の設計と実際にKubernetes上で動くsystemの差をさらに減らしていく予定です。