はじめに
Googleは2026年5月、Agent Substrateと同じ日に、エージェントの実行・再開・分散デプロイのためのオープンソースのランタイム標準としてAgent Executorを発表しました。以前、Agent Substrateの概要で整理したAgent Substrateは、エージェントを少数のworkerへ多重化する実行基盤でした。Agent Executorは、その上でエージェントの実行を管理する側にあたります。
リポジトリ名とCLI名はaxで、READMEの見出しもAXです。ブログではAgent Executorと呼ばれており、ブログからのリンク先はこのリポジトリ(github.com/google/ax)です。
この記事では、Agent Executor v0.3.1 が提供する機能と、アーキテクチャ、Task・Workspace・Modelという3つのリソース、TaskがAgent SubstrateのActorになるまでの流れを整理します。あわせて、kindクラスタで実際に動かして確認した結果も載せています。
対象コンポーネントとバージョン
| コンポーネント | バージョン | リポジトリ | 本記事での役割 |
|---|---|---|---|
| Agent Executor(AX) | v0.3.1 | google/ax | 本記事全体のソースコード調査対象 |
| Agent Substrate | 672533541dbf | agent-substrate/substrate | AX v0.3.1のgo.modが固定しているコミット。kindでの検証にもこのコミットを使用 |
| kind | v0.33.0 | kubernetes-sigs/kind | 検証環境。ノードイメージはkindest/node:v1.37.0
|
Agent Executorはプレビューで、READMEは破壊的変更が入る見込みであることを明記しています。
We are still actively refining our core concepts, protocols, and specifications. We will likely to introduce major breaking changes prior to a stable release.
(参考: README.md#L6-L9)
本記事の実装説明は AX v0.3.1 に固定します。Agent Substrate は、このタグの依存先コミットを確認対象にします。別記事で扱った Substrate v0.5.0 とは API が異なるため、設定例をそのまま混ぜないようにします。リポジトリ名を省略したソースコードの引用は、すべて google/ax の v0.3.1 です。
TL;DR
- AX は Task を管理し、実行を Substrate の Actor に任せます。Task・Workspace・Model は独自の gRPC API で扱い、Kubernetes の CRD にはしません。
- 状態は Redis、作業キューは Redis Streams に置きます。イベント単位の分配は、Task ごとの直列化や処理成功の保証とは異なります。
-
AX の Suspend は
/workspaceのデータを保存します。中断時のプロセスメモリやルートファイルシステムの変更は保存せず、再開には golden snapshot と durable data を使います。 - ブログの機能説明と v0.3.1 の実装は分けて読みます。応答の再送や Task の分岐など、コードで確認できない機能もあります。
- 検証には WorkerPool と保存先の設定が必要です。arm64 では runner イメージを用意し、Task の準備完了とコマンドの成否を分けて確認しました。
第1章: Agent Executorとは
この章では、ブログが説明しているAgent Executorの機能を確認し、リポジトリのv0.3.1が提供している内容と照らし合わせます。
1.1 ブログの説明
Google Cloud のブログを参考に、機能と構成を整理します。以下の公式図はブログの説明する全体像で、v0.3.1 の実装状況は1.3節で確認します。
ブログは、長時間動くエージェントのワークフローが障害に弱く、本番環境で信頼性と効率を保って管理することが難しい、という課題から説明を始めています。
Today, we're introducing Agent Executor, Google's open-source runtime standard for agent execution, resumption, and distributed deployment.
(参考: Agent Executor, Google's distributed Agent Runtime | Google Cloud Blog)
Agent Executorが備える機能として、ブログは次の5つを挙げています。
| 機能 | ブログの説明の要約 |
|---|---|
| Durable execution | イベントログとスナップショットにより、中断や障害から実行を再開する |
| Secure isolation | sandbox により、コードの副作用や悪意ある処理の影響を隔離する |
| Session consistency | single-writer により、共有セッション状態への更新を調整する |
| Connection recovery | クライアントの再接続時に、最後に受信したシーケンス以降の応答を補完する |
| Trajectory branching | checkpoint から実行を分岐し、複数の処理を試す |
(参考: Agent Executor, Google's distributed Agent Runtime | Google Cloud Blog)
最初の公式図は、実行を調整する Controller、履歴を扱う Event Log、実行対象の Actors という3つの要素を示しています。Actors には、エージェントだけでなくハーネス、スキル、ツール、sandbox も含まれます。
(参考: Agent Executor, Google's distributed Agent Runtime | Google Cloud Blog, 2026-05-20)
図中の Event Log と、後述する Redis Streams の作業キューは、同じ機能として扱えません。Streams があることだけで、応答の再送や実行履歴の復元まで実装されているとは判断できません。
2つ目の公式図は、異なるエージェントや実行環境を組み合わせる構想です。Antigravity、自社で運用するエージェント、Google が管理するエージェントなどを、Agent Executor の周囲に配置しています。個々の連携が v0.3.1 に実装済みであることを示す図ではありません。
(参考: Agent Executor, Google's distributed Agent Runtime | Google Cloud Blog, 2026-05-20)
Kubernetes上でのスケールについては、同日に発表されたAgent Substrateと組み合わせると説明されています。
Agent Substrate introduces a new level of abstraction for Kubernetes that moves agents onto and off of ready compute capacity in real-time, resulting in lower latency with higher scale and efficiency.
(参考: Agent Executor, Google's distributed Agent Runtime | Google Cloud Blog)
Agent Substrateのリポジトリ側でも、Agent Executorはエコシステムの1つとして紹介されています。
Agent Executor: A distributed agent runtime that demonstrates building a secure, hyper-scalable agent harness on Agent Substrate
(参考: agent-substrate/substrate README.md#L46)
1.2 リポジトリが提供するもの
google/axのREADMEは、AXを次のように説明しています。
AX is a high-throughput, declarative orchestrator to run billions of autonomous agent workloads in a cluster. It runs on top of Agent Substrate for sandboxed execution and is built to run billions of tasks per cluster. If you have used Kubernetes,
axwill feel similar.
(参考: README.md#L13)
README の「billions」はプロジェクトが掲げる規模の目標で、本記事で検証した性能値ではありません。エージェントを「ステートレスなマイクロサービスでも、完了まで走るバッチジョブでもない新しい種類のワークロード」と位置づけ、3つのリソースで宣言的に扱う、という設計です。
Agents are a new kind of workload. They are neither stateless microservices nor run-to-completion batch jobs. They accumulate state, need strict isolation, call out to model APIs and tool servers, and can burn money in a loop if nobody is watching.
(参考: README.md#L47)
| やりたいこと | AXが提供するもの |
|---|---|
| 信頼できないエージェントのコードを、CPU/メモリ制限付きの隔離されたsandboxで実行する | Task |
| Gitリポジトリ、MCPサーバー、skillパッケージをあらかじめ用意して、エージェントがすぐ動ける状態で起動する | Workspace |
| プラットフォーム自身が使うLLMを、Kubernetesのsecretの認証情報とともに設定する | Model |
| エージェントを一時停止し、保存したデータを使って再開する |
ax suspend / ax resume
|
| 実行中のエージェントにシェルで入って中の様子を見る | ax ssh |
(参考: README.md#L49-L55の機能紹介を要約。リソース制限の実装上の注意点は6.2節)
1.3 ブログの機能とv0.3.1のコードの対応
ブログの5つの機能と、v0.3.1のソースコード・同梱ドキュメントを突き合わせると、次のようになります。「見つからなかった」と書いている箇所は、リポジトリ内をevent logやsingle-writerなどで検索した範囲での結果です。
| ブログの機能 | v0.3.1で確認できたこと |
|---|---|
| Durable execution | スナップショットによるSuspend/Resumeは、Agent SubstrateのActorTemplateのSnapshotsConfigを通じて使っている(4.4節)。イベントログに相当する実装は見つからなかった |
| Secure isolation | ActorTemplateのSandboxConfigでgVisorを指定しており、隔離はAgent Substrateのsandboxに任せている(4.4節) |
| Session consistency | シングルライターに相当する実装は見つからなかった |
| Connection recovery | 最後に受信したシーケンスから応答を補完する実装は見つからなかった。WatchTaskはTaskのステータス遷移を流すRPCで、エージェントの応答を再送するものではない(2.4節) |
| Trajectory branching | ロードマップの項目(「Stateful Task Branching」)で、実装はまだない(第7章) |
| 他のエージェントとの組み合わせ | Antigravityは、Workspaceのgoalを実行する環境準備のために使われている(5.2節)。LangChain/LangGraph、ADK、A2Aに関する記述や実装は見つからなかった |
ブログはプロジェクトの機能を説明していますが、本記事が動作を確認する対象は v0.3.1 です。表の「見つからなかった」は、このタグの公開コードで対応する実装を確認できなかったという意味です。
第2章: 全体アーキテクチャ
この章では、AXのコンポーネントと、Taskがax applyされてから実行されるまでのデータの流れを整理します。
2.1 コンポーネント
AXは4つのバイナリで構成されています。
| バイナリ | 役割 |
|---|---|
ax |
開発者向けのCLI。マニフェストの適用、リソースの参照・watch、クラスタへのトンネルを担う |
ax-server |
ステートレスな gRPC API(ポート8080)。CLI が YAML から変換した型付きリクエストを受け、検証・保存とイベント発行を行う |
ax-controller |
再調整を行うworker。Redisのストリームを読み、Agent Substrate上にatespaceとactorを用意して、Taskを望ましい状態に近づける。レプリカを増やしてスケールする |
ax-task-runner |
各Taskのコンテナの中で動くエントリポイント。ワークスペースを準備し、メタデータを提供し、エージェントのコマンドを実行する |
(参考: DESIGN.md#L37-L44)
v0.3.1 の構成では、AX が Task の管理を、Agent Substrate が Actor の実行を担当します。DESIGN.md を基に、役割の境界が分かるように整理した図です。
(参考: DESIGN.md#L7-L35)
2.2 etcdの外にRedisを置く理由
AXがCRDではなくRedisを使う理由は、DESIGN.mdの冒頭に書かれています。
Storing millions of short-lived tasks as Kubernetes CRDs pushes etcd past its comfort zone (single-digit GB storage limits, write-rate bottlenecks, control plane degradation). AX keeps its state in Redis and uses Redis Streams as the work queue between the API server and a horizontally scaled pool of controllers.
(参考: DESIGN.md#L5)
これは、AX が想定する規模に対するプロジェクトの設計判断です。etcd や CRD の一律の上限を示すものではありません。数百万の短命な Task による更新負荷を避けるため、状態をRedisに置き、Redis Streamsをapiサーバーとcontrollerの間の作業キューにする、という設計です。Kubernetes の管理対象から高頻度に変わる状態を分ける点は、Agent Substrate と共通しています。AX は Task を Redis に、依存先の Substrate は Actor の状態を PostgreSQL に保存し、それぞれ異なるリソースを管理します。
RedisのキーはTask・Workspace・Modelごとに作られます。kindで実際にax applyした直後にredis-cli --scanで確認すると、次のキーがありました。
$ redis-cli --scan | sort
ax:stream:tasks
ax:task:default:simple-task
ax:tasks:atespace:default
ax:tasks:index
ax:task:<atespace>:<name>がTask本体(protojsonでシリアライズした文字列)、ax:tasks:indexとax:tasks:atespace:<atespace>が一覧用のソート済みセットで、ax:stream:tasksがイベントのStreamです。SaveTaskは、これらのキーの更新とStreamへのイベント追加を1つのトランザクションにまとめています。
pipe := s.client.TxPipeline()
pipe.Set(ctx, s.taskKey(atespace, name), data, s.opts.TTL)
pipe.ZAdd(ctx, s.taskIndexKey(), redis.Z{Score: score, Member: member})
pipe.ZAdd(ctx, s.taskAtespaceIndexKey(atespace), redis.Z{Score: score, Member: name})
pipe.XAdd(ctx, &redis.XAddArgs{
Stream: s.opts.StreamName,
Values: map[string]interface{}{
"action": "reconcile",
"atespace": atespace,
"name": name,
},
})
pipe.Publish(ctx, s.taskPubSubChannel(atespace, name), data)
(参考: internal/store/redis/store.go#L123-L176)
最後のPublishは、ax watchのためのPub/Subです。
2.3 Redis Streamsのコンシューマーグループ
ax-controllerは、Redis Streamsのコンシューマーグループ(デフォルトはax-controllers)に参加して、イベントを受け取ります。
streams, err := sub.store.client.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: sub.group,
Consumer: sub.consumer,
Streams: []string{sub.store.opts.StreamName, ">"},
Count: sub.store.opts.ReadBatchSize,
Block: sub.store.opts.ReadBlock,
}).Result()
(参考: internal/store/redis/store.go#L631-L637)
通常の新規配信では、同じグループ内の1つのコンシューマーにイベントが割り当てられます。ただし、同じ Task に対する別々のイベントは、異なるコンシューマーで並行して処理される可能性があります。Task ごとのロックや single-writer を、この仕組みだけで保証するわけではありません。既存の検証では ax-controller を2レプリカにし、同じ Task に対して8回 ax apply を実行しました。以下の出力では2つのコンシューマーが登録され、どちらも pending が0であることを確認できます。これは均等配分や exactly-once 処理を示すものではありません。
$ kubectl -n ax-system scale deploy/ax-controller --replicas=2
$ redis-cli XINFO CONSUMERS ax:stream:tasks ax-controllers
name ax-controller-79c5679f9f-jj2sg-7357
pending 0
idle 279
name ax-controller-79c5679f9f-r74l6-2741
pending 0
idle 1195
(出力はinactiveを省略しています)
worker.goのRunは、processEventが失敗した場合もログに出力したうえで、必ずイベントをACKします。失敗したイベントがキューに残り続けて、後続のイベントの処理を止めることを避けるためです。
if err := w.processEvent(ctx, ev); err != nil {
slog.Error("error processing task event",
"id", ev.ID,
"atespace", ev.Atespace,
"name", ev.Name,
"action", ev.Action,
"error", err,
)
}
if err := sub.Ack(ctx, ev); err != nil {
slog.Warn("failed to acknowledge task event", "id", ev.ID, "error", err)
}
(参考: internal/controller/worker.go#L92-L103)
失敗したイベントを自動で再投入する処理はありません。すでに別のイベントがあれば処理されますが、それもなければ新しいイベントの発行が必要です。kindでも、失敗したTaskのイベントがACKされた状態(pending 0)のままFailedで止まり、ax resumeやax applyを実行して新しいイベントが発行されるまで、再調整は動きませんでした(8.6節)。
2.4 削除と状態変更の通知
ax deleteは、Taskのレコードをすぐには消しません。MarkTaskDeletingでフェーズをTerminatingにして、削除イベントを発行します。
pipe := s.client.TxPipeline()
pipe.Set(ctx, s.taskKey(atespace, name), data, s.opts.TTL)
pipe.Publish(ctx, s.taskPubSubChannel(atespace, name), data)
pipe.XAdd(ctx, &redis.XAddArgs{
Stream: s.opts.StreamName,
Values: map[string]interface{}{
"action": "delete",
"atespace": atespace,
"name": name,
},
})
(参考: internal/store/redis/store.go#L284-L317)
ax-controller は、削除イベントを受け取ると Actor を削除し、Task 用 ActorTemplate の削除を試みてから、Redis のレコードを消します。
if err := w.reconciler.ReconcileDelete(ctx, ev.Atespace, ev.Name); err != nil {
// Leave the record in Terminating so the failure is visible; re-running
// `ax delete` republishes the event and retries the cleanup.
return fmt.Errorf("cleaning up task %s/%s: %w", ev.Atespace, ev.Name, err)
}
if err := w.store.DeleteTask(ctx, ev.Atespace, ev.Name); err != nil {
(参考: internal/controller/worker.go#L107-L119)
Actor の削除が失敗すると Terminating のまま残り、ax delete を再実行すると再試行できます。一方、ActorTemplate の削除エラーは警告ログに記録するだけで、Task の削除は続行します。Task が一覧から消えたことだけでは、関連テンプレートがすべて削除されたとは限りません。
(参考: reconciler.go#L390-L405)
ax watchが使うWatchTaskも、この仕組みの上にあります。RedisのPub/Subを購読して、最初に現在のTaskをINITIALとして送り、以降は変更をMODIFIEDとして流します。更新通知で Task のフェーズが Running・Failed・Completed になるとストリームを閉じます。初回の INITIAL 送信では同じ終了判定を行わないため、すでに Running の Task を watch した場合は次の通知を待つことがあります(参考: internal/server/server.go#L201-L238)。ブログの「Connection recovery」のように、最後に受信した位置から応答を補完する仕組みではありません。
第3章: Task・Workspace・Model
この章では、AXが扱う3種類のリソースを、docs/concepts.mdとmanifests.mdをもとに整理します。いずれのリソースも、ax.io/v1alpha1のマニフェストとしてax applyで適用し、Atespace(Agent Substrate 上の論理的なグループと名前の識別範囲)ごとに管理されます。Kubernetes namespace や、認可・隔離の保証とは区別します。
3.1 Task
Taskは、隔離された実行の最小単位です。
The smallest unit of isolated execution. A
Taskdeclares the container image and command, compute requests and limits, environment variables, and references to one or moreWorkspaces underspec.workspaces. Each workspace is mounted at its own path, and the first serves as the command's working directory.
(参考: docs/concepts.md#L7)
最小の Task は、イメージと環境変数だけで書けます。以下は同梱例で、8章では検証環境に合わせた runner イメージへ差し替えます。command がない場合は、runner がメタデータを提供しながら待機し、LLM を自動で呼び出すわけではありません。
apiVersion: ax.io/v1alpha1
kind: Task
metadata:
name: simple-task
atespace: default
spec:
image: "gcr.io/ax-substrate/ate-images/ax-task-runner@sha256:3a0dea6ad8b55278685db58aca6e37dc4ba04056831d45bef3aaeafdca43cac6"
env:
- name: ENVIRONMENT
value: "test"
debug: true # serve guest services so `ax ssh` works; off by default
(参考: examples/simple.yaml)
status.phaseは、Pending・Running・Suspended・Failed・Terminatingなどの1語で、詳細はconditionsに入ります。ax watchで遷移を見られます。
(参考: 初期値のPendingはstore.go#L142-L144、Failed・Suspended・Runningはreconciler.go#L193-L218、Terminatingはstore.go#L284-L317。FailedからRunningへの遷移は、kindでの確認結果をもとにした図示です)
conditionsは2種類です。
| Condition | Trueになる条件 |
|---|---|
WorkspaceReady |
すべてのworkspaceのセットアップが終わった。以降はTrueのまま |
Ready |
Actor が実行中で、controller が WorkspaceReady を True と認識している |
(参考: docs/concepts.md#L15-L18)
Running や Ready=True は、タスクのコマンドが成功したことや、Git の取得が成功したことを保証しません。5.1節・5.2節で実際の判定内容を説明します。
3.2 Workspace
Workspaceは、エージェントが作業を始められる状態にするための準備(Gitリポジトリのclone、MCPサーバー、skill)を、宣言的に1回だけ書いておくためのリソースです。
A
Workspacepopulates the filesystem and tool landscape. Declare it once, bind it from as many tasks as you like, and the runner materializes it inside each sandbox before the command starts:
- Git repositories cloned into subdirectories of the workspace path.
- MCP servers and registries (Model Context Protocol) that the agent can call.
- Skill registries and the path where skills are materialized.
(参考: docs/concepts.md#L26-L30)
Taskとの紐づけにはgoalも指定できます。goalは、環境準備を自然言語で書いたもので、最初の起動時にエージェントへ渡され、ツールチェインや依存関係のインストールなどを任されます(5.2節)。
spec:
workspaces:
- name: my-service # mounted at /workspace/my-service, the command's working directory
goal: "Install dependencies and run the test suite"
- name: team-tools
path: "/workspace/tools" # explicit mount path
(参考: docs/manifests.md#L40-L47)
ただし、v0.3.1のrunnerのコードで実際に処理しているのは、Gitのclone、skillsのディレクトリの作成、goalのAntigravityへの引き渡しです(参考: internal/workspace/setup.go#L83-L124)。mcpやskills.registriesの中身は、ax describe workspaceの表示と、Taskの中の/metadata/v1alpha1/ax/workspacesが返すYAMLには含まれますが、runnerがそれをファイルに書き出す処理は見つかりませんでした。
3.3 Model
Modelは、LLMそのものではなく、名前を付けたモデルの設定です。
A
Modelis not a model. It is a named model configuration: which provider to call, which model identifier to use, provider-specific generation parameters such as temperature, and a reference to the Kubernetes secret holding the API key.
(参考: docs/concepts.md#L36)
apiVersion: ax.io/v1alpha1
kind: Model
metadata:
name: default-model
atespace: default
spec:
provider: google
model: gemini-3.8-flash
secretKey:
name: gemini-api-secret
key: GEMINI_API_KEY
(参考: docs/manifests.md#L89-L101)
同梱ドキュメントは provider に google と anthropic を挙げています(参考: docs/manifests.md#L81-L123)。concepts.mdは、この設定を使う場面として「goalからworkspaceを計画するとき」を挙げていますが、v0.3.1では、Modelを読み込むPlanner(internal/workspace/planner.go)は、定義されているだけで、controllerやrunnerからは呼ばれていません。goalの環境準備に実際に使われるAPIキーは、Model リソースではなく、Atespace と同名の Kubernetes namespace にある gemini-api-secret の GEMINI_API_KEY です。取得できない場合は controller 自身の GEMINI_API_KEY 環境変数へフォールバックします。取得した値は Task のコンテナの環境変数へ渡されます(参考: reconciler.go#L352-L368)。
第4章: TaskがActorになるまで
この章では、ax-controllerが1つのTaskを再調整する流れを、reconciler.goの実装に沿って整理します。
4.1 再調整の流れ
ax-controllerのReconcileは、Taskを受け取って、Agent SubstrateのControl APIを次の順に呼びます。
(参考: internal/controller/reconciler.go#L95-L302)
-
atespaceの作成:
EnsureAtespaceで、Taskが属するatespaceをSubstrate側に用意します(参考: reconciler.go#L119-L124)。 -
Actorの名前: Actorの名前は、常にTaskの名前と同じです。atenet-routerの
ate-target-actorヘッダーにも、そのまま使えます。// 2. The actor is always named after the task, so the two can be used // interchangeably (for example in the router's ate-target-actor header). actorName := task.Metadata.Name task.Status.Actor = actorName(参考: reconciler.go#L126-L130)
-
ActorTemplate: Taskごとに専用のActorTemplateを作ります(4.2節)。
-
Actor:
EnsureActorでActorを作ります。すでに存在し、状態がCRASHEDなら削除して作り直します(参考: internal/substrate/client.go#L306-L366)。 -
Suspend/Resume:
spec.suspendがtrueならSuspendActor、そうでなければResumeActorを呼びます。ResumeActorのレスポンスに含まれるworkerのIPが、ax get tasksのWORKER-IP列になります。 -
readyzの確認: Resumeの後、Actorの中のrunnerの
/readyz?check=workspaceを500msごとに確認します。ワークスペースのセットアップは1回しか行われないため、WorkspaceReadyが一度Trueになった後は、このポーリングは行いません(参考: reconciler.go#L220-L291)。
Substrateの認証には、ax-controllerのPodに投影されたServiceAccountのトークン(audienceはapi.ate-system.svc)を使い、SubstrateのサーバーのTLS証明書は、servicedns.podcert.ate.dev/identityというsignerのClusterTrustBundleで検証します(参考: deploy/ax-controller.yaml#L69-L119)。このsignerは、Agent Substrateの概要で整理したpodcertcontrollerが提供しているものです。
4.2 Taskごとに作られるActorTemplate
ActorTemplateは、Taskごとに作られます。名前は、Taskの名前と、イメージ・環境変数のダイジェストから決まります。
// taskTemplateName derives the per-task ActorTemplate name from the task name and a
// digest of the image and container environment, so a spec change yields a new template.
func taskTemplateName(taskName, image string, env map[string]string) string {
(参考: reconciler.go#L371-L385)
環境変数には、ユーザーがspec.envに書いたものに加えて、Taskの起動設定(AX_TASK_YAML)とバインドしたWorkspaceのYAML(AX_WORKSPACES_YAML)も含まれます。ただし、statusとsuspendのフラグは除かれています。suspendを切り替えるたびに新しいゴールデンスナップショットが作られることを避けるためです。
// Only launch configuration belongs in the template; status and suspend
// changes must not create new golden snapshots.
launchTask := proto.Clone(task).(*v1alpha1.Task)
launchTask.Status = nil
launchTask.Spec.Suspend = false
if taskYAML, err := yaml.Marshal(launchTask); err == nil {
extraEnv["AX_TASK_YAML"] = string(taskYAML)
}
(参考: reconciler.go#L157-L169)
起動設定を変更すると、新しい名前の ActorTemplate が作られます。ただし、EnsureActor は既存 Actor が正常ならその Actor を返し、参照するテンプレートを更新しません**。新しい ActorTemplate の作成と、既存 Actor への設定反映は別です**。ax apply を Deployment のローリングアップデートと同じ動作として扱うことはできません。
(参考: client.go#L306-L365)
kindで2つのTaskを作ったときは、simple-task-tmpl-5200c4f4のような名前のテンプレートが、Taskごとに1つずつ作られました。削除時は、名前のパターンに一致するテンプレートをすべて消します(参考: reconciler.go#L409-L445)。
4.3 ActorTemplateの中身
ActorTemplateは、substrate.BuildActorTemplateが組み立てます。
Containers: []*ateapipb.Container{{
Name: "guest",
Image: image,
Command: command,
Env: envList,
Readyz: &ateapipb.ContainerReadyz{
HttpGet: &ateapipb.HTTPGetAction{
Path: "/readyz",
Port: 80,
},
},
VolumeMounts: []*ateapipb.VolumeMount{{
Name: "workspace",
MountPath: "/workspace",
}},
}},
Volumes: []*ateapipb.Volume{{
Name: "workspace",
DurableDir: &ateapipb.DurableDirVolumeSource{},
}},
(参考: internal/substrate/client.go#L212-L262)
コンテナのコマンドは常に/usr/local/bin/ax-task-runnerで、/workspaceはdurableDirのボリュームとしてマウントされます。ユーザーがspec.commandに書いたコマンドは、コンテナのエントリポイントにはならず、AX_TASK_YAML経由でrunnerが受け取って実行します(5.1節)。
4.4 スナップショットとsandboxの設定
スナップショットとsandboxの設定は、次のようにコードに固定されています。
SnapshotsConfig: &ateapipb.SnapshotsConfig{
StorageLocation: snapshotsBucket,
OnPause: ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA,
OnCommit: ateapipb.SnapshotContentScope_SNAPSHOT_CONTENT_SCOPE_DATA,
OnResume: &ateapipb.OnResumeConfig{
FromData: ateapipb.ResumeSource_RESUME_SOURCE_GOLDEN,
},
},
SandboxConfig: &ateapipb.SandboxConfig{
SandboxClass: ateapipb.SandboxClass_SANDBOX_CLASS_GVISOR,
ConfigName: "gvisor-default",
},
(参考: internal/substrate/client.go#L263-L274)
Agent Substrateのprotoのコメントには、SNAPSHOT_CONTENT_SCOPE_DATAとRESUME_SOURCE_GOLDENが、次のように説明されています。
Captures durable data without process memory or root filesystem changes.
(参考: agent-substrate/substrate ateapi.proto#L194-L195)
Restores with the version's golden snapshot and the actor's own durable data.
(参考: agent-substrate/substrate ateapi.proto#L889-L890)
つまり、Suspend時に保存されるのは、プロセスのメモリやルートファイルシステムの変更を除いた、durableなデータ(/workspace)だけです。Resume時は、そのバージョン(ActorTemplate)のゴールデンスナップショットと、Actor自身のdurableなデータから復元されます。
保存するデータと、復元の土台を分けると次のようになります。
AX v0.3.1 の固定設定を簡略化した図。中断直前のプロセスメモリを保存する方式とは異なります。
(参考: client.go#L249-L274、ateapi.proto#L889-L890)
Agent Substrateの概要で動かしたCounterデモは、SNAPSHOT_FIDELITY_MEMORYでプロセスのメモリごと保存していました。AXは、メモリではなく/workspaceのデータだけを保存する設定を選んでいます。これはdocs/runner.mdにも書かれています。
The
/workspacevolume is what survives suspend and resume. Agent Substrate snapshots it when a task is suspended and restores it into a fresh container when the task is resumed, so the runner will see the same files but a new process tree.
(参考: docs/runner.md#L26)
スナップショットの保存先は、AX_SNAPSHOTS_BUCKET環境変数で指定します。未指定のときのデフォルト値は、コード内に固定されたバケットのURIです(参考: internal/substrate/client.go#L205-L228)。deploy/ax-controller.yamlも、同じバケットをAX_SNAPSHOTS_BUCKETに設定しているため(参考: deploy/ax-controller.yaml#L82-L83)、自分の環境にデプロイするときは、この値を差し替える必要があります(8.1節)。
第5章: Taskのコンテナの中
この章では、ActorとしてSubstrate上で動くコンテナの中で、ax-task-runnerが何をしているかを整理します。
5.1 runnerの契約
ax-task-runnerは、各Taskのコンテナで、PID 1として動くプログラムです。コントローラーは、spec.commandをコンテナのエントリポイントとして実行せず、常にrunnerを起動し、設定を環境変数で渡します。
| コントローラーが設定するもの | 値 |
|---|---|
| コンテナのコマンド | 常に/usr/local/bin/ax-task-runner
|
AX_TASK_YAML |
Taskの起動設定(statusとsuspendのフラグを除く)のYAML |
AX_WORKSPACES_YAML |
バインドしたWorkspaceの、複数ドキュメントのYAML |
/workspace |
durableなディレクトリとしてマウント |
| readiness probe | ポート80のGET /readyz
|
(参考: docs/runner.md#L13-L22)
runnerには、次の動作が求められます。
- ポート80でHTTPを提供し、
/healthzと/readyz(ワークスペースの準備ができるまで503)、/metadata/v1alpha1/ax/taskと/metadata/v1alpha1/ax/workspacesを返す - 各workspaceを1回だけ準備する(再起動のたびに再準備しない)
-
spec.commandを、最初のworkspaceを作業ディレクトリにして子プロセスとして実行し、監視する - コマンドが終了しても動き続ける(PID 1が終了するとメタデータサーバーも止まり、
ax sshが使えなくなるため) -
SIGTERMを受けたらコマンドのプロセスグループに転送し、猶予期間の後にSIGKILLする -
spec.debugがtrueのときだけ、ゲストサービスを提供する
(参考: docs/runner.md#L28-L47)
コマンドの終了後もrunnerが動き続ける点は、コードにも現れています。
select {
case err := <-exited:
reportExit(cfg, cmd, err)
// Keep the sandbox up and inspectable until told to stop.
<-ctx.Done()
(参考: runner/runner.go#L190-L196)
docs/runner.mdには、「コマンドの終了ステータスをコントロールプレーンは現状読み取らない」とも書かれています(参考: docs/runner.md#L43)。Taskのフェーズは、コマンドが成功したかどうかとは別に決まります。
runnerは、同梱のデフォルト実装を使わずに置き換えることもできます。イメージを継承する、runnerパッケージを自分のバイナリに埋め込む、まったく別の言語で書く、の3段階があり、いずれも/usr/local/bin/ax-task-runnerに実行ファイルを置く契約を満たす必要があります(参考: docs/runner.md#L58-L134)。
5.2 ワークスペースの準備
ワークスペースの準備は、各workspaceのパスごとに、最初の1回だけ実行されます。Gitリポジトリのclone(失敗時は5回までリトライ)、skillsのディレクトリの作成、goalが指定されていればAntigravityへの引き渡しの順です。完了すると、/axの下にマーカーファイルを書き、以降の起動ではスキップします。
markerPath := filepath.Join(AXDir, MarkerName(targetPath))
if _, err := os.Stat(markerPath); err == nil {
slog.Info("workspace already initialized; skipping maiden run setup", "path", targetPath)
return res, nil
}
(参考: internal/workspace/setup.go#L83-L124)
goalの引き渡しは、antigravity_bootstrap.pyというスクリプトに対して、GEMINI_API_KEYが環境変数にある場合に、10分(AX_BOOTSTRAP_TIMEOUTで変更可)の制限時間で実行します。スクリプトかGEMINI_API_KEYがなければ、ログを出してこのステップをスキップします。
cmd := exec.CommandContext(ctx, "python3", bootstrapScriptPath,
"--goal", goal,
"--workspace", targetPath,
"--data-dir", dataDir,
)
(参考: internal/workspace/setup.go#L277-L315)
Gitのcloneに失敗しても、セットアップ全体は失敗扱いになりません。失敗はログとして/ax/git-error.logに残り、マーカーファイルだけが書かれません。コードのコメントは、これによって次の起動で再試行されることを意図しています。ただし、kindで試した限りでは、Resumeしてもcloneは再試行されませんでした(8.5節)。runner に伝わるセットアップエラーは、作業ディレクトリの作成失敗などです。この結果、Git の取得失敗が WorkspaceReady に反映されない場合があります。8.5節で、kindでこの動作を確認しています。
5.3 ax sshの仕組み
ax sshは、SSHではなく、Agent Substrateのゲストサービス(agent-substrate/env)のgRPCを使います。spec.debug: trueのときだけ、runnerが同じポート80で、HTTPとh2cを多重化してこのサービスを提供します。
- Process service: start, inspect, stream output from, and kill processes. This is what powers
ax ssh.- File system service: streaming file reads and writes inside the workspace.
(参考: docs/sandbox.md#L32-L33)
CLIは、kubectl port-forwardでatenet-routerへ接続し、ate-target-actorヘッダーにTaskの名前を入れて、プロセスの開始と出力のストリームを呼び出します(参考: cmd/ax/main.go#L1084-L1094、internal/guest/client.go#L45-L73)。spec.debugがfalseのTaskには、CLIの側で接続を拒否します(参考: cmd/ax/main.go#L1045-L1051)。
ゲストサービスは、任意のプロセス実行とファイルアクセスを許すため、デフォルトでは無効です。
5.4 ネットワーク
Taskには、KubernetesのServiceやIngressはありません。通常のクライアントから Task へ接続する方法として、Agent Substrate の atenet-router(ate-system名前空間のatenet-routerサービス)を使い、ate-target-actor: <atespace>/<task> ヘッダーで宛先を指定します。controller の準備完了チェックには worker IP への直接接続もあります。宛先のActorが停止中なら、先に Resume してからリクエストを転送します。これは AX の spec.suspend や Task status を更新する操作ではないため、Actor と Task の表示が一致するとは限りません(参考: docs/networking.md#L3-L5)。
第6章: 構築にあたって
この章では、AXをデプロイするために必要なものと、v0.3.1の時点で注意しておきたい点を整理します。
6.1 前提となるもの
READMEは、AXを動かす前提として、Agent Substrateがクラスタで動いていることを挙げています。
AX schedules every task as a sandboxed actor on Agent Substrate, so Substrate must be running in your cluster before you deploy AX.
(参考: README.md#L63)
実際にkindで動かしたところ、次のものを別途用意する必要がありました。
| 追加で用意するもの | 役割 | 備考 |
|---|---|---|
| Agent Substrate | Actorの実行基盤 | AX v0.3.1のgo.modが固定するコミットを使った(Agent Substrate v0.5.0との組み合わせは確認していない) |
| WorkerPool | Actorを載せるworkerの集まり | AXのリポジトリにはWorkerPoolの記述がなく、デプロイ手順にも含まれない。無いとno free workers availableでResumeに失敗する |
| スナップショットの保存先 | Actorのスナップショットの保存先 |
AX_SNAPSHOTS_BUCKETで差し替える。デフォルトはコード内に固定された値 |
| Taskのruntimeイメージ |
/usr/local/bin/ax-task-runnerを含むイメージ |
デフォルトのイメージはgcr.io/ax-substrate/...から取得される。make build-task-runnerはlinux/amd64向けにビルドするため(参考: Makefile#L46-L51)、arm64の環境では自分でビルドする必要があった |
| Redis | Task・Workspace・Modelの状態とイベント |
deploy/redis.yamlで用意できる。レプリカ1、永続ボリュームなし |
(参考: Redisはdeploy/redis.yaml。WorkerPoolが記述に無いことは、リポジトリ内をWorkerPoolで検索した結果)
6.2 v0.3.1で注意しておきたい点
-
Task のリソース設定と、実際の制限を分けて確認します。
spec.resourcesは API に定義されていますが、このタグのBuildActorTemplateはその値を ActorTemplate のリソース設定へ渡していません。WorkerPool の Pod 制限が、そのまま Task ごとの指定値になるわけではありません。(参考: ax.proto#L71-L111、client.go#L212-L275) -
Streams の未 ACK イベントを回収する処理は確認できませんでした。新しいコンシューマーグループは
$から作られ、読み取りは>の新規イベントだけを対象にします。初回起動前のイベントや、コンシューマー停止で pending に残ったイベントを自動復旧できるとは限りません。(参考: store.go#L585-L659) - WorkspaceReady の待機には上限があります。ポーリングが時間切れになっても phase は Running のまま、Ready は False になります。後から runner の準備が終わっても、そのことだけで再調整を起こす処理はありません。(参考: reconciler.go#L220-L302)
-
ax-server に認証処理がありません。
grpc.NewServer()にオプションを渡しておらず、平文のHTTP/2(h2c)でポート8080を待ち受けます(参考: internal/server/server.go#L38-L46、cmd/ax-server/main.go#L66-L74)。axCLIはデフォルトでkubectl port-forwardを使ってax-system名前空間のax-serverサービスへ接続するため(参考: internal/tunnel/tunnel.go#L181-L200)、CLI経由のアクセスは、そのport-forwardを許可するKubernetesのRBACに依存します。クラスタ内の他のPodからax-serverへ直接到達できるかどうかは、NetworkPolicyなどクラスタ側の設定次第です。認証・ガバナンスは、ロードマップ(第7章)の項目に入っています。 -
Redisは単一レプリカで、永続ボリュームを持ちません。
deploy/redis.yamlはレプリカ1のDeploymentで、ボリュームがありません。確認したRedisの設定はappendonly no、save 3600 1 300 100 60 10000(デフォルトのRDBスナップショットのみ)でした。Redis Podが再作成されると、Task・Workspace・Modelの状態が失われる可能性があります。Redisをどう構成するかは、利用者が決める前提のようです。 -
失敗したイベントの自動再投入はありません。2.3節で見た通り、失敗時も ACK を試みます。別のイベントがキューにない場合は、
ax resumeやax applyなどで新しいイベントを発行する必要があります。 -
controller は1つのイベントを同期的に処理します。
Worker.Runのループは、イベントを1つ取り出し、processEventが終わってから次を読みます。ResumeActorとWorkspaceReadyのポーリング(最大15秒)がこのループの中で行われるため、スケールはレプリカを増やすことで行います。 -
Task の
status.pendingApprovalとstatus.usageは、このタグで利用する処理を確認できませんでした。spec.policies(予算・承認の設定)は、過去のバージョンから削除済み(reserved)です(参考: pkg/apis/v1alpha1/ax.proto#L85-L87)。
第7章: ロードマップ
この章では、docs/roadmap.mdに書かれている今後の予定のうち、ブログの機能に関係するものを整理します。
ロードマップは5つの項目に分かれています(参考: docs/roadmap.md)。
| 項目 | 内容 |
|---|---|
| Stabilize Core Specs |
Task・Workspace・Model・Sandbox/SandboxConfigのスキーマとライフサイクルの安定化。Taskのトークン/タイムアウトの予算や承認ポリシーも含む |
| Actor Architecture | 新しいAgent SubstrateのActor APIへの移行、ワークスペースのセットアップを別Actorに分離、最小権限のポリシー、アイドル検出と自動Suspend、Taskの分岐 |
| Agentic Environment |
goalによるワークスペースの動的な環境構築、組み込みのブートストラップとエージェントハーネスのカスタマイズ |
| Networking, Identity, Governance & Observability | TaskへのSPIFFE ID、MCPとskill registryのガバナンス、OpenTelemetryによるテレメトリと軌跡の収集 |
| Documentation | Substrateとの役割分担、カスタムイメージ、カスタムrunnerのドキュメント |
ブログが挙げる機能との関係では、次の項目が対応します。
- Idleness Detection and Automatic Suspension for Density: Continuously monitor actor activity (process execution, I/O, network traffic, and active gRPC/SSH sessions) to detect idle tasks and automatically trigger
SuspendActorcheckpointing, reclaiming worker CPU and memory to maximize cluster density.- Stateful Task Branching: Enable branching (forking) a running or suspended
Task—including its checkpointed memory and workspace filesystem state—into multiple parallel tasks to explore speculative execution paths concurrently.
(参考: docs/roadmap.md#L22-L23)
ブログの「Trajectory branching」は、ロードマップの「Stateful Task Branching」にあたります。v0.3.1 はアイドルを自動検出して Suspend する実装ではありません。CLI や上位システムからの明示的な指示が必要です。「Migration to the New Actor」は、AXがAgent Substrateの新しいActor APIへ移行する予定を示しています。今回の検証では、go.modが固定するコミットのAgent Substrateを使いました。
第8章: kindクラスタで実際に動かす
この章では、既存の kind 検証記録に、再現に必要な準備と待機手順を補って整理します。arm64 環境で Task の起動・停止、保存されるデータ、失敗時の状態を確認したものです。LLM を使う goal の実行や大規模な負荷試験は対象外です。
8.1 構築
AX v0.3.1 の go.mod は Go 1.27.1 を指定しています。Docker・kubectl・ko と、この Go バージョンを使える環境を用意します。以下は arm64 用の手順です。
Agent Substrate は依存先コミットで構築します。既存の checkout を変更せずに済むよう専用ディレクトリを作ります。クラスタ作成スクリプトは同名の既存クラスタを削除するため、ax-demo が既存環境と重複しないことを確認します。
mkdir ax-kind-review
cd ax-kind-review
git clone https://github.com/agent-substrate/substrate.git substrate-pinned
cd substrate-pinned
git checkout --detach 672533541dbf
export KIND_CLUSTER_NAME=ax-demo
hack/create-kind-cluster.sh
hack/install-ate-kind.sh --deploy-ate-system
続いて、AXのRedis、ax-controller、ax-serverをkoでビルドしてデプロイします。deploy/ax-controller.yamlが設定しているAX_SNAPSHOTS_BUCKETは、kindのRustFSのバケット(ate-snapshots)へ差し替えます。
cd ..
git clone --branch v0.3.1 --depth 1 https://github.com/google/ax.git ax-v0.3.1
cd ax-v0.3.1
go install ./cmd/ax
# Go のインストール先 bin ディレクトリを PATH に追加しておく
kubectl apply -f deploy/redis.yaml
export KO_DOCKER_REPO=localhost:5001 KO_DEFAULTPLATFORMS=linux/arm64
ko resolve -f deploy/ax-controller.yaml \
| sed 's|gs://dberkov-gke-dev3/ate-env/|gs://ate-snapshots/ax/|' \
| kubectl apply -f -
ko resolve -f deploy/ax-server.yaml | kubectl apply -f -
kubectl -n ax-system rollout status deploy/ax-redis --timeout=5m
kubectl -n ax-system rollout status deploy/ax-controller --timeout=5m
kubectl -n ax-system rollout status deploy/ax-server --timeout=5m
kubectl -n ax-system get pods
以下は既存の実行結果の抜粋です。Pod 名は実行ごとに変わります。
NAME READY STATUS RESTARTS AGE
ax-controller-79c5679f9f-jj2sg 1/1 Running 0 6s
ax-redis-86df786c87-sj8z2 1/1 Running 0 30s
ax-server-7f8fc9b657-6rdnr 1/1 Running 0 6s
Taskのruntimeイメージは、デフォルトのイメージ(gcr.io/ax-substrate/...)を使わず、追加する AX の実行ファイルを /usr/local/bin/ax-task-runner だけにしたイメージを自分でビルドしました。ax-task-runnerをlinux/arm64向けにビルドし、gitを含むベースイメージにコピーして、kindのローカルレジストリにpushします。
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o ax-task-runner ./cmd/ax-task-runner
cat > Dockerfile.kind <<'EOF'
FROM alpine/git:latest
COPY ax-task-runner /usr/local/bin/ax-task-runner
ENTRYPOINT ["/usr/local/bin/ax-task-runner"]
EOF
docker build --platform linux/arm64 -f Dockerfile.kind -t localhost:5001/ax-task-runner:kind .
docker push localhost:5001/ax-task-runner:kind
RUNNER_IMAGE=$(docker image inspect localhost:5001/ax-task-runner:kind --format '{{index .RepoDigests 0}}')
RUNNER_IMAGE には完全な digest 付きの参照が入ります。続けて、検証用の simple.yaml を作ります。
cat > simple.yaml <<EOF
apiVersion: ax.io/v1alpha1
kind: Task
metadata:
name: simple-task
atespace: default
spec:
image: "${RUNNER_IMAGE}"
env:
- name: ENVIRONMENT
value: test
debug: true
EOF
このイメージには、デフォルトのイメージに入っているPythonとAntigravity(google-antigravity)が含まれていません。そのため、goalを指定したワークスペースのブートストラップ(5.2節)は、今回の検証の対象外です。
最初の Task を適用する前に、次の出力に ax-controllers グループがあることを確認します。グループは controller が起動時に作るため、未作成なら controller のログを確認して待ちます。
kubectl -n ax-system exec deploy/ax-redis -- redis-cli XINFO GROUPS ax:stream:tasks
AX の CLI は、通常は現在の kubeconfig context から ax-server へのトンネルを自動作成します。以降は kind-ax-demo を対象とし、別の接続先を指定する AX_SERVER や --server を使わない前提です。
8.2 WorkerPoolが無いとResumeに失敗する
デプロイ直後に、READMEのとおりax applyでTaskを適用すると、TaskはFailedになりました。
$ ax apply -f simple.yaml # image は上でpushした digest 指定のイメージ
task.ax.io/simple-task created
$ ax describe task simple-task
Conditions:
TYPE STATUS REASON MESSAGE
Ready False ActorResumeFailed resuming actor default/simple-task: rpc error: code = ResourceExhausted desc = no free workers available
ActorTemplateとActorの作成には成功し、ResumeActorの段階で、Actorを載せるworkerがない、というエラーになっています。Agent Substrateでは、ActorはWorkerPoolのworkerに載せられます。kind向けのインストーラが用意するのはシステムコンポーネントまでで、WorkerPoolは含まれていません。AXのリポジトリにも、WorkerPoolを作る手順はありません。以下を ax-workers.yaml として AX 側のディレクトリに保存します。nodeSelector のバージョン値は当時の記録です。再現時は kubectl get nodes -L ate.dev/substrate-version で、インストーラが付けた値と一致するか確認します。
apiVersion: v1
kind: Namespace
metadata:
name: ax-workers
---
apiVersion: ate.dev/v1alpha1
kind: WorkerPool
metadata:
name: ax-workers
namespace: ax-workers
spec:
replicas: 2
workerImage: ko://github.com/agent-substrate/substrate/cmd/ateom-gvisor
template:
nodeSelector:
ate.dev/substrate-version: "67253354"
resources:
limits:
cpu: "1"
memory: 1Gi
requests:
cpu: 250m
memory: 1Gi
(参考: demos/counter/counter.yaml.tmplをもとに、名前とreplicasを変えたもの。workerImageのko://は、ko resolveでイメージに置き換えて適用する)
ko:// の参照は、その Go パッケージを含む Substrate 側の checkout で解決します。
cd ../substrate-pinned
ko resolve -f ../ax-v0.3.1/ax-workers.yaml | kubectl apply -f -
kubectl -n ax-workers get pods -w
2つの worker Pod が Ready になったら Ctrl-C で監視を終了し、AX のディレクトリへ戻ります。
cd ../ax-v0.3.1
ax apply -f simple.yaml
ax get tasks
ax describe task simple-task
WorkerPool を作った後、ax apply をもう一度実行した既存の記録では、TaskがRunningになり、ReadyがTrueになりました。
$ ax get tasks
NAME ATESPACE PHASE ACTOR WORKER-IP AGE
simple-task default Running simple-task 10.244.0.24 2m
$ ax describe task simple-task
Conditions:
TYPE STATUS REASON MESSAGE
WorkspaceReady True SetupComplete Workspace setup completed at 10.244.0.24
Ready True TaskRunning Task is running and its workspace is ready
ax sshで中に入ると、PID 1がax-task-runnerで、spec.envの値とAX_METADATA_URLが設定されていることを確認できました。
$ ax ssh simple-task -- sh -c 'echo $ENVIRONMENT; echo $AX_METADATA_URL; ps -o pid,args | head'
test
http://127.0.0.1:80
PID COMMAND
1 /usr/local/bin/ax-task-runner
11 sh -c echo $ENVIRONMENT; echo $AX_METADATA_URL; ps -o pid,args | head
12 ps -o pid,args
13 head
8.3 Redisの状態
Redis の内容は、たとえば kubectl -n ax-system exec deploy/ax-redis -- redis-cli XLEN ax:stream:tasks で参照できます。以下の redis-cli コマンドは、その Redis コンテナ内での実行を省略して表記しています。
ax apply を3回実行した時点の既存の記録では、次の状態でした。追加で適用すると件数は変わります。ax applyのたびに、内容が変わらなくても、Streamにイベントが1件追加されます。コンシューマーグループのpendingは0です。
$ redis-cli XLEN ax:stream:tasks
3
$ redis-cli XINFO GROUPS ax:stream:tasks
name ax-controllers
consumers 1
pending 0
last-delivered-id 1792367750651-0
entries-read 3
lag 0
$ redis-cli TTL ax:task:default:simple-task
-1
最初の2回はFailedで終わっていますが、pending 0なので、失敗したイベントもACKされています(2.3節)。TTL は設定されておらず(-1)、期限による自動削除はされません。ただし、Redis 自体の再作成によるデータ消失とは別です。
8.4 Suspend/Resumeで残るもの
ax suspend / ax resume の返却は API の受付を示します。実際の状態は ax get tasks と ax describe task で確認し、Suspended への遷移後に Resume、Ready=True の確認後に接続します。ax watch は Ready=False の Running 通知でも終了する場合があるため、終了だけを準備完了と判断しません。
Suspend/Resumeで何が残るかを確認します。/workspace と /tmp にファイルを書き、バックグラウンドで sleep を起動してから、ax suspend と ax resume を実行した記録です。このときはバックグラウンドプロセスが標準出力を保持したため、ax ssh の終了時に stream terminated by RST_STREAM が出ました。再現時はプロセスの標準入出力をリダイレクトすると、出力ストリームの保持を避けられます。
$ ax ssh simple-task -- sh -c 'echo "before suspend" > /workspace/note.txt; echo "volatile" > /tmp/volatile.txt; (sleep 3000 &)'
$ ax suspend task simple-task
task.ax.io/simple-task suspended
$ ax get tasks
NAME ATESPACE PHASE ACTOR WORKER-IP AGE
simple-task default Suspended simple-task <none> 4m
$ ax resume task simple-task
task.ax.io/simple-task resumed
$ ax ssh simple-task -- sh -c 'ls /workspace; cat /workspace/note.txt; ls /tmp; ps -o pid,args'
note.txt
before suspend
PID COMMAND
1 /usr/local/bin/ax-task-runner
9 ps -o pid,args
/workspace/note.txtだけが残り、/tmp/volatile.txtとsleepプロセスは消えました。SuspendでWORKER-IPが<none>になり、Resumeではworkerが変わることもあります(この例では10.244.0.24から10.244.0.23へ変わりました)。これは、/workspaceのデータだけを保存して復元する設定(4.4節)の動作どおりです。Agent SubstrateのCounterデモのように、プロセスのメモリが引き継がれる動作ではありません。
8.5 Git の取得失敗と準備完了判定
Workspace で Git リポジトリを指定した Task も試しました。以下の image は、8.1節で取得した完全な参照へ置き換えます。
apiVersion: ax.io/v1alpha1
kind: Workspace
metadata:
name: demo-ws
atespace: default
spec:
git:
- name: origin
repo: "https://github.com/chalk/chalk.git"
branch: "main"
depth: 1
---
apiVersion: ax.io/v1alpha1
kind: Task
metadata:
name: ws-task
atespace: default
spec:
image: "<8.1節のRUNNER_IMAGEの値に置き換える>"
workspaces:
- name: demo-ws
debug: true
Taskは数秒でRunningになり、ReadyもTrueになりましたが、中を見るとcloneは失敗していました。
$ ax ssh ws-task -- sh -c 'ls /workspace/demo-ws; ls /ax; cat /ax/git-error.log'
chalk
git-error.log
error: git fetch: exit status 128
output: Reinitialized existing Git repository in /workspace/demo-ws/chalk/.git/
error: remote origin already exists.
fatal: unable to access 'https://github.com/chalk/chalk.git/': Send failure: Broken pipe
5.2節で見た通り、Gitのエラーはセットアップ全体の失敗として扱われないため、WorkspaceReady: True / SetupCompleteと表示されながら、リポジトリは空のままでした。ログの Broken pipe だけでは、失敗原因を確定できません。egress 設定やプロキシ、接続先などの切り分けは未実施です。AXのリポジトリには、egressに関する記述がロードマップの1行(「setup egress」)しかなく(参考: docs/roadmap.md#L21)、egressポリシーを設定する手順はありません。外部のGitリポジトリをcloneするには、Agent SubstrateのEgressPolicyを別途設定する必要があると思います(今回は検証していません)。
cloneが失敗したあとにResumeしても再試行されるかを、Actorの中に作るローカルのGitリポジトリで確かめました。最初は存在しないパス(/workspace/seed.git)をWorkspaceのリポジトリに指定してTaskを起動し(cloneは失敗します)、起動後にActorの中でそのパスにbareリポジトリを作ってコミットを1つ入れてから、ax suspendとax resumeを実行しました。
$ ax ssh ws2 -- sh -c 'git init -q --bare -b main /workspace/seed.git; ...' # コミットv1を入れる
$ ax suspend task ws2
# ax get tasks で Suspended を確認してから再開する
$ ax resume task ws2
$ ax ssh ws2 -- sh -c 'ls -a /workspace/demo2/src; git -C /workspace/demo2/src log --oneline'
.
..
.git
fatal: your current branch 'master' does not have any commits yet
リポジトリが存在する状態でResumeしても、/workspace/demo2/srcは空のままで、/ax/git-error.logのタイムスタンプも最初の起動のときから変わっていませんでした。手動でgit fetch origin mainを実行すると取得できたため、リポジトリ自体は有効です。この観測では、Resume による Git 取得の再試行は確認できませんでした。Agent Substrateのprotoのコメントによると、RESUME_SOURCE_GOLDENの復元は、ActorTemplateのゴールデンスナップショットとActor自身のdurableなデータから行われます(4.4節)。runnerのプロセスは、ゴールデンスナップショットから復元されている可能性があります(今回は、それ以上の確認はしていません)。
8.6 失敗とリトライ
kindでは、次の2つの失敗を確認しました。いずれも自動では再試行されず、ax resumeなどで新しいイベントを発行するまでFailedのままでした。
-
workerの不足: workerが2つで、2つのTaskが実行中のときに3つ目のTaskを作ると、
no free workers availableでFailedになりました。WorkerPoolのreplicasを4に増やしてからax resumeすると、Runningになりました。 -
ゴールデンスナップショットの未完成: Task を作ってまもなく Suspend / Resume を実行すると、
FailedPrecondition desc = a Golden data resume requires the ActorTemplate golden snapshot, which is not availableでFailedになりました。しばらく待ってからax resumeをやり直すと、Runningになりました。ActorTemplateのゴールデンスナップショットは、Taskの作成後に非同期で作られるため、その完成前にResumeすると失敗する、と考えられます(4.4節のprotoのコメントに合わせた解釈で、作成のタイミングそのものは確認していません)。
$ ax describe task ws-task
Conditions:
TYPE STATUS REASON MESSAGE
WorkspaceReady True SetupComplete Workspace setup completed at 10.244.0.24
Ready False ActorResumeFailed resuming actor default/ws-task: rpc error: code = FailedPrecondition desc = a Golden data resume requires the ActorTemplate golden snapshot, which is not available
$ ax resume task ws-task # 少し待ってから
task.ax.io/ws-task resumed
8.7 削除
この検証では、ax delete は Terminating を経て約1.5秒で完了しました。処理時間の保証値ではありません。ax-controllerのログには、Actorの削除に続いて、ws2-tmpl-8e2ec447のようなテンプレートの削除が記録されていました。
$ ax delete task ws2
waiting for task default/ws2 to be deleted...
task.ax.io/ws2 deleted
検証が終わったら、CLI のバックグラウンドトンネルを停止し、専用クラスタを削除します。クラスタ内の Redis や RustFS の検証データも失われます。ローカルレジストリ kind-registry は別のコンテナとして残ります。
ax tunnel stop kind-ax-demo
cd ../substrate-pinned
hack/kind.sh delete cluster --name ax-demo
おわりに
Agent Substrate の上に、Task の状態を管理する API と controller を重ねる構成が理解できました。ブログに挙がる機能と、このタグのコードが担う範囲を分けて読むことが大切だと感じました。
特に、/workspace の保持と、中断時のプロセスをそのまま再開することは別でした。準備完了や更新の反映も含め、実際にどの状態が引き継がれるかを確認しながら使いたいと思います。
参考
参考リンク
本文で登場する順に並べています。

